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/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
+ }