wuira 0.2.4 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.md +62 -831
- package/data/icons/forever.json +90051 -0
- package/docs/icons.md +28 -0
- package/docs/source-converters.md +34 -0
- package/lib/datasources/DataSource.d.ts +8 -0
- package/lib/datasources/DataSource.lua +7 -0
- package/lib/datasources/MathConverter.d.ts +6 -0
- package/lib/datasources/MathConverter.lua +61 -0
- package/lib/datasources/MathExpression.d.ts +16 -0
- package/lib/datasources/MathExpression.lua +281 -0
- package/lib/datasources/SourceConverter.d.ts +39 -0
- package/lib/datasources/SourceConverter.lua +568 -0
- package/lib/datasources/UnitInfo.d.ts +6 -0
- package/lib/datasources/UnitInfo.lua +347 -15
- package/lib/datasources/index.d.ts +4 -0
- package/lib/datasources/index.lua +9 -0
- package/lib/icons/index.d.ts +45634 -0
- package/lib/icons/index.lua +3 -0
- package/lib/index.d.ts +1 -0
- package/lib/store/index.lua +20 -18
- package/package.json +20 -4
- package/tools/build-library.mjs +1 -1
- package/tools/generate-icons.mjs +10 -0
- package/tools/icon-data.mjs +59 -0
- package/tools/update-icons.mjs +36 -0
package/docs/icons.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Forever icon catalog
|
|
2
|
+
|
|
3
|
+
`Icons` contains native texture **FileDataIDs** from the same verified Forever build as the spell enums. Icon identity is independent of spell or item actions.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { UI } from "wuira";
|
|
7
|
+
import { Icons } from "wuira/icons";
|
|
8
|
+
import { Icon } from "wuira/ui";
|
|
9
|
+
|
|
10
|
+
const image = <Icon width={24} height={24} texture={Icons.BlessingOfFreedom_135968} />;
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Every icon has an explicit ID-suffixed member. A readable short alias is generated only when that name maps to one icon. Multiple spells or items may share the same texture; they produce one catalog entry with searchable aliases.
|
|
14
|
+
|
|
15
|
+
The snapshot combines SpellMisc spell/active icon fields with Item icon fields and ItemSparse names from the pinned client build. It covers every positive icon ID referenced by these tables, including unused records and entries without a readable name; it does not claim to list every texture or atlas in the game. No other client branch is merged into it. The names describe associated spells or items, rather than guessing asset filenames. Unnamed records receive a `Spell <id>` or `Item <id>` label.
|
|
16
|
+
|
|
17
|
+
The generated enum is erased to numeric literals by TypeScriptToLua. Editor tools can read the packaged `data/icons/forever.json` to create a searchable catalog without constructing every icon in game. A selected texture is saved as a FileDataID, not a spell ID.
|
|
18
|
+
|
|
19
|
+
Game data belongs to Blizzard Entertainment. Source URLs, row counts, build and SHA-256 hashes are recorded in the snapshot.
|
|
20
|
+
|
|
21
|
+
## Update
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
npm run icons:update -- --build 1.60.1.70374
|
|
25
|
+
npm run icons:generate
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Use `--cached` to regenerate the snapshot from downloaded CSVs. Updating requires the build to match `data/spells/forever.json`. Normal consumers use the committed snapshot and do not download game data during a build.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Source converters
|
|
2
|
+
|
|
3
|
+
`SourceConverter` transforms a readable source value. It consumes the input's change notifications, shares one upstream subscription across its observers, and does not create a timer or dispose the shared input. Sources without notifications remain available as checked one-time reads.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { ClassSource, SourceConverter, CaseConverter, HP, MathConverter, Unit } from "wuira/datasources";
|
|
7
|
+
|
|
8
|
+
const characterClass = new ClassSource(Unit.Player);
|
|
9
|
+
const token = new SourceConverter(characterClass, info => info?.token);
|
|
10
|
+
const color = new CaseConverter(token, [
|
|
11
|
+
{ equals: "PALADIN", value: { r: 1, g: 0.5, b: 0.7, a: 1 } },
|
|
12
|
+
{ equals: "MAGE", value: { r: 0.4, g: 0.8, b: 1, a: 1 } },
|
|
13
|
+
], { r: 0.35, g: 0.4, b: 0.45, a: 1 });
|
|
14
|
+
|
|
15
|
+
const hp = new HP(Unit.Player);
|
|
16
|
+
const doubled = new MathConverter(hp, "2 * input");
|
|
17
|
+
const stop = doubled.BindTo(value => console.log(value));
|
|
18
|
+
// On removal:
|
|
19
|
+
stop();
|
|
20
|
+
doubled.Dispose();
|
|
21
|
+
color.Dispose();
|
|
22
|
+
token.Dispose();
|
|
23
|
+
characterClass.Dispose();
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`Value` and `Compute()` are checked reads. `TryCompute()` performs one checked read and returns `{ readable: false }` for restricted data, without invoking the callback. Converter chains use it to avoid repeated leaf reads. `onChange()` observes future readable output changes; `BindTo()` also applies the current readable value immediately. Scalar outputs with the same value do not emit again. `When()` filters emissions without changing the checked `Value` getter. Call `Dispose()` when the converter is no longer owned by a component.
|
|
27
|
+
|
|
28
|
+
Cases compare strings, finite numbers and booleans with strict equality; `1` and `"1"` are different keys. Duplicate keys are rejected. Unmatched values return the supplied default, or `undefined` when none was provided. Case outputs can be colors, texture IDs, text or other data.
|
|
29
|
+
|
|
30
|
+
Math expressions support `input`, finite numeric literals, parentheses, unary signs, `+`, `-`, `*`, `/`, `%`, and exponentiation (`^` or `**`). Exponents associate to the right; `-input^2` means `-(input^2)`. The bounded parser runs once at construction. It does not evaluate JavaScript or Lua, access globals, or call functions. Invalid syntax, division by zero and non-finite results throw errors. `MathConverter.Validate(expression)` checks syntax without reading the input.
|
|
31
|
+
|
|
32
|
+
Restricted values, including nested secret fields, never reach a conversion callback. Restrictions are checked when values are consumed; combat alone does not mark a value secret. ClassSource subscriptions follow unit identity changes and relevant native events, and release those listeners when the last consumer unsubscribes.
|
|
33
|
+
|
|
34
|
+
Converters produce data. They do not grant permission to change protected buttons or their actions during combat.
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
export type ReadResult<T> = {
|
|
2
|
+
readonly readable: true;
|
|
3
|
+
readonly value: T;
|
|
4
|
+
} | {
|
|
5
|
+
readonly readable: false;
|
|
6
|
+
};
|
|
1
7
|
/** A value-producing source. Secrecy describes data, not native frame protection. */
|
|
2
8
|
export declare abstract class DataSource<T> {
|
|
3
9
|
private value;
|
|
@@ -8,6 +14,8 @@ export declare abstract class DataSource<T> {
|
|
|
8
14
|
IsSecret(): boolean;
|
|
9
15
|
get Value(): T;
|
|
10
16
|
Compute<TResult>(compute: (value: T) => TResult): TResult;
|
|
17
|
+
/** One checked read, without exposing a restricted value or invoking its callback. */
|
|
18
|
+
TryCompute<TResult>(compute: (value: T) => TResult): ReadResult<TResult>;
|
|
11
19
|
protected GetValue(): T;
|
|
12
20
|
/** Filters native binding emissions; Value remains a checked, fresh snapshot. */
|
|
13
21
|
When(condition: (value: T) => boolean): this;
|
|
@@ -165,6 +165,13 @@ function DataSource.prototype.Compute(self, compute)
|
|
|
165
165
|
self:AssertReadable(value)
|
|
166
166
|
return compute(nil, value)
|
|
167
167
|
end
|
|
168
|
+
function DataSource.prototype.TryCompute(self, compute)
|
|
169
|
+
local value = self:GetValue()
|
|
170
|
+
return self:IsValueSecret(value) and ({readable = false}) or ({
|
|
171
|
+
readable = true,
|
|
172
|
+
value = compute(nil, value)
|
|
173
|
+
})
|
|
174
|
+
end
|
|
168
175
|
function DataSource.prototype.GetValue(self)
|
|
169
176
|
return self.value
|
|
170
177
|
end
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { SourceConverter } from "./SourceConverter";
|
|
2
|
+
import type { ValueSource } from "./SourceConverter";
|
|
3
|
+
export declare class MathConverter extends SourceConverter<number, number> {
|
|
4
|
+
constructor(input: ValueSource<number>, expression: string);
|
|
5
|
+
static Validate(expression: string): void;
|
|
6
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
--[[ Generated with https://github.com/TypeScriptToLua/TypeScriptToLua ]]
|
|
2
|
+
-- Lua Library inline imports
|
|
3
|
+
local function __TS__Class(self)
|
|
4
|
+
local c = {prototype = {}}
|
|
5
|
+
c.prototype.__index = c.prototype
|
|
6
|
+
c.prototype.constructor = c
|
|
7
|
+
return c
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
local function __TS__ClassExtends(target, base)
|
|
11
|
+
target.____super = base
|
|
12
|
+
local staticMetatable = setmetatable({__index = base}, base)
|
|
13
|
+
setmetatable(target, staticMetatable)
|
|
14
|
+
local baseMetatable = getmetatable(base)
|
|
15
|
+
if baseMetatable then
|
|
16
|
+
if type(baseMetatable.__index) == "function" then
|
|
17
|
+
staticMetatable.__index = baseMetatable.__index
|
|
18
|
+
end
|
|
19
|
+
if type(baseMetatable.__newindex) == "function" then
|
|
20
|
+
staticMetatable.__newindex = baseMetatable.__newindex
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
setmetatable(target.prototype, base.prototype)
|
|
24
|
+
if type(base.prototype.__index) == "function" then
|
|
25
|
+
target.prototype.__index = base.prototype.__index
|
|
26
|
+
end
|
|
27
|
+
if type(base.prototype.__newindex) == "function" then
|
|
28
|
+
target.prototype.__newindex = base.prototype.__newindex
|
|
29
|
+
end
|
|
30
|
+
if type(base.prototype.__tostring) == "function" then
|
|
31
|
+
target.prototype.__tostring = base.prototype.__tostring
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
local function __TS__New(target, ...)
|
|
36
|
+
local instance = setmetatable({}, target.prototype)
|
|
37
|
+
instance:____constructor(...)
|
|
38
|
+
return instance
|
|
39
|
+
end
|
|
40
|
+
-- End of Lua Library inline imports
|
|
41
|
+
local ____exports = {}
|
|
42
|
+
local ____SourceConverter = require("datasources.SourceConverter")
|
|
43
|
+
local SourceConverter = ____SourceConverter.SourceConverter
|
|
44
|
+
local ____MathExpression = require("datasources.MathExpression")
|
|
45
|
+
local MathExpression = ____MathExpression.MathExpression
|
|
46
|
+
____exports.MathConverter = __TS__Class()
|
|
47
|
+
local MathConverter = ____exports.MathConverter
|
|
48
|
+
MathConverter.name = "MathConverter"
|
|
49
|
+
__TS__ClassExtends(MathConverter, SourceConverter)
|
|
50
|
+
function MathConverter.prototype.____constructor(self, input, expression)
|
|
51
|
+
local formula = __TS__New(MathExpression, expression)
|
|
52
|
+
SourceConverter.prototype.____constructor(
|
|
53
|
+
self,
|
|
54
|
+
input,
|
|
55
|
+
function(____, value) return formula:Evaluate(value) end
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
function MathConverter.Validate(self, expression)
|
|
59
|
+
__TS__New(MathExpression, expression)
|
|
60
|
+
end
|
|
61
|
+
return ____exports
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Small bounded arithmetic grammar. Expressions cannot call functions or execute Lua. */
|
|
2
|
+
export declare class MathExpression {
|
|
3
|
+
private readonly text;
|
|
4
|
+
private offset;
|
|
5
|
+
private token;
|
|
6
|
+
private tokens;
|
|
7
|
+
private readonly tree;
|
|
8
|
+
constructor(text: string);
|
|
9
|
+
private Next;
|
|
10
|
+
private Depth;
|
|
11
|
+
private Add;
|
|
12
|
+
private Multiply;
|
|
13
|
+
private Unary;
|
|
14
|
+
private Primary;
|
|
15
|
+
Evaluate(input: number): number;
|
|
16
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
--[[ Generated with https://github.com/TypeScriptToLua/TypeScriptToLua ]]
|
|
2
|
+
-- Lua Library inline imports
|
|
3
|
+
local function __TS__Class(self)
|
|
4
|
+
local c = {prototype = {}}
|
|
5
|
+
c.prototype.__index = c.prototype
|
|
6
|
+
c.prototype.constructor = c
|
|
7
|
+
return c
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
local function __TS__StringAccess(self, index)
|
|
11
|
+
if index >= 0 and index < #self then
|
|
12
|
+
return string.sub(self, index + 1, index + 1)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
local function __TS__StringIncludes(self, searchString, position)
|
|
17
|
+
if not position then
|
|
18
|
+
position = 1
|
|
19
|
+
else
|
|
20
|
+
position = position + 1
|
|
21
|
+
end
|
|
22
|
+
local index = string.find(self, searchString, position, true)
|
|
23
|
+
return index ~= nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
local function __TS__StringSlice(self, start, ____end)
|
|
27
|
+
if start == nil or start ~= start then
|
|
28
|
+
start = 0
|
|
29
|
+
end
|
|
30
|
+
if ____end ~= ____end then
|
|
31
|
+
____end = 0
|
|
32
|
+
end
|
|
33
|
+
if start >= 0 then
|
|
34
|
+
start = start + 1
|
|
35
|
+
end
|
|
36
|
+
if ____end ~= nil and ____end < 0 then
|
|
37
|
+
____end = ____end - 1
|
|
38
|
+
end
|
|
39
|
+
return string.sub(self, start, ____end)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
local function __TS__Number(value)
|
|
43
|
+
local valueType = type(value)
|
|
44
|
+
if valueType == "number" then
|
|
45
|
+
return value
|
|
46
|
+
elseif valueType == "string" then
|
|
47
|
+
local numberValue = tonumber(value)
|
|
48
|
+
if numberValue then
|
|
49
|
+
return numberValue
|
|
50
|
+
end
|
|
51
|
+
if value == "Infinity" then
|
|
52
|
+
return math.huge
|
|
53
|
+
end
|
|
54
|
+
if value == "-Infinity" then
|
|
55
|
+
return -math.huge
|
|
56
|
+
end
|
|
57
|
+
local stringWithoutSpaces = string.gsub(value, "%s", "")
|
|
58
|
+
if stringWithoutSpaces == "" then
|
|
59
|
+
return 0
|
|
60
|
+
end
|
|
61
|
+
return 0 / 0
|
|
62
|
+
elseif valueType == "boolean" then
|
|
63
|
+
return value and 1 or 0
|
|
64
|
+
else
|
|
65
|
+
return 0 / 0
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
local function __TS__NumberIsFinite(value)
|
|
70
|
+
return type(value) == "number" and value == value and value ~= math.huge and value ~= -math.huge
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
local function __TS__ArrayIncludes(self, searchElement, fromIndex)
|
|
74
|
+
if fromIndex == nil then
|
|
75
|
+
fromIndex = 0
|
|
76
|
+
end
|
|
77
|
+
local len = #self
|
|
78
|
+
local k = fromIndex
|
|
79
|
+
if fromIndex < 0 then
|
|
80
|
+
k = len + fromIndex
|
|
81
|
+
end
|
|
82
|
+
if k < 0 then
|
|
83
|
+
k = 0
|
|
84
|
+
end
|
|
85
|
+
for i = k + 1, len do
|
|
86
|
+
if self[i] == searchElement then
|
|
87
|
+
return true
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
return false
|
|
91
|
+
end
|
|
92
|
+
-- End of Lua Library inline imports
|
|
93
|
+
local ____exports = {}
|
|
94
|
+
--- Small bounded arithmetic grammar. Expressions cannot call functions or execute Lua.
|
|
95
|
+
____exports.MathExpression = __TS__Class()
|
|
96
|
+
local MathExpression = ____exports.MathExpression
|
|
97
|
+
MathExpression.name = "MathExpression"
|
|
98
|
+
function MathExpression.prototype.____constructor(self, text)
|
|
99
|
+
self.text = text
|
|
100
|
+
self.offset = 0
|
|
101
|
+
self.token = ""
|
|
102
|
+
self.tokens = 0
|
|
103
|
+
if issecretvalue(text) or type(text) ~= "string" or #text == 0 or #text > 256 then
|
|
104
|
+
error("MathConverter: expression must contain 1–256 characters")
|
|
105
|
+
end
|
|
106
|
+
self:Next()
|
|
107
|
+
self.tree = self:Add(0)
|
|
108
|
+
if self.token ~= "" then
|
|
109
|
+
error("MathConverter: unexpected token " .. self.token)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
function MathExpression.prototype.Next(self)
|
|
113
|
+
local ____self_0, ____tokens_1 = self, "tokens"
|
|
114
|
+
local ____self_tokens_2 = ____self_0[____tokens_1] + 1
|
|
115
|
+
____self_0[____tokens_1] = ____self_tokens_2
|
|
116
|
+
if ____self_tokens_2 > 128 then
|
|
117
|
+
error("MathConverter: expression is too complex")
|
|
118
|
+
end
|
|
119
|
+
while __TS__StringIncludes(
|
|
120
|
+
" \t\r\n",
|
|
121
|
+
__TS__StringAccess(self.text, self.offset) or "\0"
|
|
122
|
+
) do
|
|
123
|
+
self.offset = self.offset + 1
|
|
124
|
+
end
|
|
125
|
+
local char = __TS__StringAccess(self.text, self.offset)
|
|
126
|
+
if char == nil then
|
|
127
|
+
self.token = ""
|
|
128
|
+
return
|
|
129
|
+
end
|
|
130
|
+
if __TS__StringSlice(self.text, self.offset, self.offset + 5) == "input" then
|
|
131
|
+
self.offset = self.offset + 5
|
|
132
|
+
self.token = "input"
|
|
133
|
+
return
|
|
134
|
+
end
|
|
135
|
+
if __TS__StringIncludes("0123456789.", char) then
|
|
136
|
+
local ____self_3, ____offset_4 = self, "offset"
|
|
137
|
+
local ____self_offset_5 = ____self_3[____offset_4]
|
|
138
|
+
____self_3[____offset_4] = ____self_offset_5 + 1
|
|
139
|
+
local start = ____self_offset_5
|
|
140
|
+
while __TS__StringIncludes(
|
|
141
|
+
"0123456789.",
|
|
142
|
+
__TS__StringAccess(self.text, self.offset) or "\0"
|
|
143
|
+
) do
|
|
144
|
+
self.offset = self.offset + 1
|
|
145
|
+
end
|
|
146
|
+
if __TS__StringAccess(self.text, self.offset) == "e" or __TS__StringAccess(self.text, self.offset) == "E" then
|
|
147
|
+
self.offset = self.offset + 1
|
|
148
|
+
if __TS__StringAccess(self.text, self.offset) == "+" or __TS__StringAccess(self.text, self.offset) == "-" then
|
|
149
|
+
self.offset = self.offset + 1
|
|
150
|
+
end
|
|
151
|
+
while __TS__StringIncludes(
|
|
152
|
+
"0123456789",
|
|
153
|
+
__TS__StringAccess(self.text, self.offset) or "\0"
|
|
154
|
+
) do
|
|
155
|
+
self.offset = self.offset + 1
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
self.token = __TS__StringSlice(self.text, start, self.offset)
|
|
159
|
+
if not __TS__NumberIsFinite(__TS__Number(self.token)) then
|
|
160
|
+
error("MathConverter: invalid number")
|
|
161
|
+
end
|
|
162
|
+
return
|
|
163
|
+
end
|
|
164
|
+
if __TS__StringIncludes("+-*/%^()", char) then
|
|
165
|
+
self.offset = self.offset + 1
|
|
166
|
+
self.token = char
|
|
167
|
+
if char == "*" and __TS__StringAccess(self.text, self.offset) == "*" then
|
|
168
|
+
self.offset = self.offset + 1
|
|
169
|
+
self.token = "^"
|
|
170
|
+
end
|
|
171
|
+
return
|
|
172
|
+
end
|
|
173
|
+
error("MathConverter: only input, numbers and arithmetic operators are allowed")
|
|
174
|
+
end
|
|
175
|
+
function MathExpression.prototype.Depth(self, depth)
|
|
176
|
+
if depth > 32 then
|
|
177
|
+
error("MathConverter: expression nesting is too deep")
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
function MathExpression.prototype.Add(self, depth)
|
|
181
|
+
local left = self:Multiply(depth)
|
|
182
|
+
while self.token == "+" or self.token == "-" do
|
|
183
|
+
local operator = self.token
|
|
184
|
+
self:Next()
|
|
185
|
+
left = {
|
|
186
|
+
kind = "binary",
|
|
187
|
+
operator = operator,
|
|
188
|
+
left = left,
|
|
189
|
+
right = self:Multiply(depth)
|
|
190
|
+
}
|
|
191
|
+
end
|
|
192
|
+
return left
|
|
193
|
+
end
|
|
194
|
+
function MathExpression.prototype.Multiply(self, depth)
|
|
195
|
+
local left = self:Unary(depth)
|
|
196
|
+
while __TS__ArrayIncludes({"*", "/", "%"}, self.token) do
|
|
197
|
+
local operator = self.token
|
|
198
|
+
self:Next()
|
|
199
|
+
left = {
|
|
200
|
+
kind = "binary",
|
|
201
|
+
operator = operator,
|
|
202
|
+
left = left,
|
|
203
|
+
right = self:Unary(depth)
|
|
204
|
+
}
|
|
205
|
+
end
|
|
206
|
+
return left
|
|
207
|
+
end
|
|
208
|
+
function MathExpression.prototype.Unary(self, depth)
|
|
209
|
+
self:Depth(depth)
|
|
210
|
+
if self.token == "+" or self.token == "-" then
|
|
211
|
+
local sign = self.token == "-" and -1 or 1
|
|
212
|
+
self:Next()
|
|
213
|
+
return {
|
|
214
|
+
kind = "unary",
|
|
215
|
+
sign = sign,
|
|
216
|
+
child = self:Unary(depth + 1)
|
|
217
|
+
}
|
|
218
|
+
end
|
|
219
|
+
local left = self:Primary(depth)
|
|
220
|
+
if self.token == "^" then
|
|
221
|
+
self:Next()
|
|
222
|
+
left = {
|
|
223
|
+
kind = "binary",
|
|
224
|
+
operator = "^",
|
|
225
|
+
left = left,
|
|
226
|
+
right = self:Unary(depth + 1)
|
|
227
|
+
}
|
|
228
|
+
end
|
|
229
|
+
return left
|
|
230
|
+
end
|
|
231
|
+
function MathExpression.prototype.Primary(self, depth)
|
|
232
|
+
self:Depth(depth)
|
|
233
|
+
if self.token == "input" then
|
|
234
|
+
self:Next()
|
|
235
|
+
return {kind = "input"}
|
|
236
|
+
end
|
|
237
|
+
if self.token == "(" then
|
|
238
|
+
self:Next()
|
|
239
|
+
local value = self:Add(depth + 1)
|
|
240
|
+
if self.token ~= ")" then
|
|
241
|
+
error("MathConverter: missing closing parenthesis")
|
|
242
|
+
end
|
|
243
|
+
self:Next()
|
|
244
|
+
return value
|
|
245
|
+
end
|
|
246
|
+
if self.token ~= "" and __TS__NumberIsFinite(__TS__Number(self.token)) then
|
|
247
|
+
local value = __TS__Number(self.token)
|
|
248
|
+
self:Next()
|
|
249
|
+
return {kind = "number", value = value}
|
|
250
|
+
end
|
|
251
|
+
error("MathConverter: expected input, a number or parentheses")
|
|
252
|
+
end
|
|
253
|
+
function MathExpression.prototype.Evaluate(self, input)
|
|
254
|
+
if issecretvalue(input) or type(input) ~= "number" or not __TS__NumberIsFinite(input) then
|
|
255
|
+
error("MathConverter: input must be a readable finite number")
|
|
256
|
+
end
|
|
257
|
+
local read
|
|
258
|
+
read = function(____, node)
|
|
259
|
+
if node.kind == "input" then
|
|
260
|
+
return input
|
|
261
|
+
end
|
|
262
|
+
if node.kind == "number" then
|
|
263
|
+
return node.value
|
|
264
|
+
end
|
|
265
|
+
if node.kind == "unary" then
|
|
266
|
+
return node.sign * read(nil, node.child)
|
|
267
|
+
end
|
|
268
|
+
local left = read(nil, node.left)
|
|
269
|
+
local right = read(nil, node.right)
|
|
270
|
+
if (node.operator == "/" or node.operator == "%") and right == 0 then
|
|
271
|
+
error("MathConverter: division by zero")
|
|
272
|
+
end
|
|
273
|
+
local result = node.operator == "+" and left + right or (node.operator == "-" and left - right or (node.operator == "*" and left * right or (node.operator == "/" and left / right or (node.operator == "%" and left % right or left ^ right))))
|
|
274
|
+
if not __TS__NumberIsFinite(result) then
|
|
275
|
+
error("MathConverter: result must be finite")
|
|
276
|
+
end
|
|
277
|
+
return result
|
|
278
|
+
end
|
|
279
|
+
return read(nil, self.tree)
|
|
280
|
+
end
|
|
281
|
+
return ____exports
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { DataSource } from "./DataSource";
|
|
2
|
+
import type { ReadResult } from "./DataSource";
|
|
3
|
+
/** Structural contract: converters can consume a shared source without owning it. */
|
|
4
|
+
export interface ValueSource<T> {
|
|
5
|
+
readonly Value: T;
|
|
6
|
+
IsSecret(): boolean;
|
|
7
|
+
TryCompute?<TResult>(compute: (value: T) => TResult): ReadResult<TResult>;
|
|
8
|
+
onChange?(listener: (value: T) => void): () => void;
|
|
9
|
+
}
|
|
10
|
+
/** Checked value transformation. No timers, native widgets or ownership of the input. */
|
|
11
|
+
export declare class SourceConverter<TInput, TOutput> extends DataSource<TOutput> {
|
|
12
|
+
private readonly input;
|
|
13
|
+
private readonly convert;
|
|
14
|
+
private readonly listeners;
|
|
15
|
+
private stop?;
|
|
16
|
+
private disposed;
|
|
17
|
+
private blocked;
|
|
18
|
+
private previous?;
|
|
19
|
+
private readable;
|
|
20
|
+
constructor(input: ValueSource<TInput>, convert: (value: TInput) => TOutput);
|
|
21
|
+
protected GetValue(): TOutput;
|
|
22
|
+
private Convert;
|
|
23
|
+
protected HasSecretFields(value: TOutput): boolean;
|
|
24
|
+
/** Future readable changes. The first observer attaches one upstream subscription. */
|
|
25
|
+
onChange(listener: (value: TOutput) => void): () => void;
|
|
26
|
+
private Refresh;
|
|
27
|
+
/** One immediate readable value, followed by event-driven changes when supported. */
|
|
28
|
+
BindTo(apply: (value: TOutput) => void): () => void;
|
|
29
|
+
Dispose(): void;
|
|
30
|
+
}
|
|
31
|
+
export type CaseKey = string | number | boolean;
|
|
32
|
+
export interface SourceCase<T> {
|
|
33
|
+
readonly equals: CaseKey;
|
|
34
|
+
readonly value: T;
|
|
35
|
+
}
|
|
36
|
+
/** Strict equality, first matching case, then the optional default value. */
|
|
37
|
+
export declare class CaseConverter<TInput, TOutput> extends SourceConverter<TInput, TOutput | undefined> {
|
|
38
|
+
constructor(input: ValueSource<TInput>, cases: readonly SourceCase<TOutput>[], fallback?: TOutput);
|
|
39
|
+
}
|