@vibemancer/core 0.1.0 → 0.1.2

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.
Files changed (50) hide show
  1. package/README.md +28 -28
  2. package/dist/{chunk-L7Z7OFXD.js → chunk-OL7ETV6V.js} +3 -3
  3. package/dist/chunk-OL7ETV6V.js.map +1 -0
  4. package/dist/index-browser.d.ts +1 -1
  5. package/dist/index-browser.js +1 -1
  6. package/dist/index.js +1 -1
  7. package/package.json +79 -78
  8. package/src/bots/berserker/01_Stormchaser.ts +457 -457
  9. package/src/bots/berserker/02_Stormcaller.ts +417 -417
  10. package/src/bots/berserker/03_Stormforger.ts +481 -481
  11. package/src/bots/caster/01_Flamecaller.ts +286 -286
  12. package/src/bots/caster/02_Pyromancer.ts +350 -350
  13. package/src/bots/caster/03_Infernalist.ts +492 -492
  14. package/src/bots/defensive/01_Turtle.ts +151 -151
  15. package/src/bots/defensive/02_Sentinel.ts +134 -134
  16. package/src/bots/defensive/03_Golem.ts +357 -357
  17. package/src/bots/duelist/01_Battlemage.ts +433 -433
  18. package/src/bots/duelist/02_Warmage.ts +438 -438
  19. package/src/bots/duelist/03_Archmage.ts +588 -588
  20. package/src/bots/homing/01_Bonemancer.ts +67 -67
  21. package/src/bots/homing/02_Lich.ts +356 -356
  22. package/src/bots/homing/03_Archlich.ts +220 -220
  23. package/src/bots/kiter/01_Spellspinner.ts +398 -398
  24. package/src/bots/kiter/02_Spellweaver.ts +378 -378
  25. package/src/bots/kiter/03_Spellbinder.ts +448 -448
  26. package/src/bots/melee/01_Shadowblade.ts +270 -270
  27. package/src/bots/melee/02_Nightblade.ts +437 -437
  28. package/src/bots/melee/03_Voidblade.ts +582 -582
  29. package/src/bots/sniper/01_Spellshot.ts +385 -385
  30. package/src/bots/sniper/02_Spelltracer.ts +441 -441
  31. package/src/bots/sniper/03_Spellseeker.ts +546 -546
  32. package/src/bots/standalone/Critter.ts +89 -89
  33. package/src/bots/standalone/Doombringer.ts +91 -91
  34. package/src/bots/standalone/Hogger.ts +228 -228
  35. package/src/bots/standalone/Rookie.ts +50 -50
  36. package/src/bots/standalone/TargetDummy.ts +21 -21
  37. package/src/bots/test/cheater.ts +405 -405
  38. package/src/bots/test/crasher.ts +81 -81
  39. package/src/engine/hooks-runtime.ts +394 -394
  40. package/src/engine/manual-match.ts +289 -289
  41. package/src/engine/missile-templates.ts +155 -155
  42. package/src/engine/physics.ts +143 -143
  43. package/src/engine/simulation.ts +828 -828
  44. package/src/engine/spells.ts +128 -128
  45. package/src/engine-version.ts +1 -1
  46. package/src/index.ts +23 -23
  47. package/src/rules.ts +254 -254
  48. package/src/types.ts +193 -193
  49. package/src/utils/index.ts +6 -6
  50. package/dist/chunk-L7Z7OFXD.js.map +0 -1
@@ -1,394 +1,394 @@
1
- /**
2
- * VIBEMANCER - HOOKS RUNTIME
3
- *
4
- * This file implements a minimal React-like hooks runtime for AI programming.
5
- * It supports useState, useEffect, useMemo, useRef, and useParam with entity isolation.
6
- *
7
- * RULES OF HOOKS (same as React):
8
- * - Hooks must be called at the top level of the bot function
9
- * - Hooks must be called in the same order every tick
10
- * - Hooks must NOT be called conditionally
11
- *
12
- * Violating these rules throws an error (detected via hook index validation).
13
- */
14
-
15
- import type {BotContext} from '../hooks/types.js';
16
-
17
- export interface HookState
18
- {
19
- values: unknown[];
20
- effects: {
21
- callback: () => void | (() => void);
22
- deps?: unknown[];
23
- cleanup?: () => void;
24
- }[];
25
- memos: {
26
- value: unknown;
27
- deps?: unknown[];
28
- }[];
29
- /** Type of each hook call in order (for validation). */
30
- hookTypes: string[];
31
- /** Total hooks called on first successful tick. */
32
- hookCount: number;
33
- /** Whether the first tick has completed successfully (hook pattern established). */
34
- initialized: boolean;
35
- }
36
-
37
- // Use globalThis for hooks state so that multiple copies of the hooks runtime
38
- // (e.g., engine template + bot bundle in sandboxed execution) share the same state.
39
- // Without this, bot bundles that inline their own copy of the hooks runtime would
40
- // have separate currentEntityId etc., causing "useParam can only be used inside
41
- // a bot function" errors.
42
- const _g = globalThis as Record<string, unknown>;
43
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- we own __vibemancer_entityHooks
44
- const entityHooks: Map<string, HookState> = (_g.__vibemancer_entityHooks as Map<string, HookState>)
45
- ?? (_g.__vibemancer_entityHooks = new Map<string, HookState>());
46
-
47
- // Accessor functions for shared mutable state on globalThis
48
- function _getCurrentEntityId(): string | null
49
- {
50
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- we own __vibemancer_currentEntityId
51
- return (_g.__vibemancer_currentEntityId as string | null) ?? null;
52
- }
53
- function _setCurrentEntityId(id: string | null): void
54
- {
55
- _g.__vibemancer_currentEntityId = id;
56
- }
57
- function _getCurrentHookIndex(): number
58
- {
59
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- we own __vibemancer_currentHookIndex
60
- return (_g.__vibemancer_currentHookIndex as number) ?? 0;
61
- }
62
- function _setCurrentHookIndex(idx: number): void
63
- {
64
- _g.__vibemancer_currentHookIndex = idx;
65
- }
66
- function _getCurrentBotContext(): BotContext | null
67
- {
68
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- we own __vibemancer_currentBotContext
69
- return (_g.__vibemancer_currentBotContext as BotContext | null) ?? null;
70
- }
71
- function _setCurrentBotContext(ctx: BotContext | null): void
72
- {
73
- _g.__vibemancer_currentBotContext = ctx;
74
- }
75
-
76
- /**
77
- * Get or create the hook state for an entity.
78
- */
79
- function getHookState(entityId: string): HookState
80
- {
81
- let state = entityHooks.get(entityId);
82
- if (!state)
83
- {
84
- state = {
85
- values: [],
86
- effects: [],
87
- memos: [],
88
- hookTypes: [],
89
- hookCount: 0,
90
- initialized: false,
91
- };
92
- entityHooks.set(entityId, state);
93
- }
94
- return state;
95
- }
96
-
97
- /**
98
- * Validate a hook call and return its sequential index.
99
- * Ensures hooks are called in the same order every tick.
100
- *
101
- * On the first tick: records the hook type at this index.
102
- * On subsequent ticks: validates the hook type matches.
103
- *
104
- * @param type - The hook type name (e.g., 'useState', 'useEffect', 'useParam')
105
- * @returns The sequential hook index
106
- * @throws If called outside runWithHooks or if hook order changed
107
- */
108
- export function validateHookCall(type: string): number
109
- {
110
- if (!_getCurrentEntityId())
111
- {
112
- throw new Error(`${type} can only be used inside a bot function (runWithHooks)`);
113
- }
114
-
115
- const state = getHookState(_getCurrentEntityId()!);
116
- const index = _getCurrentHookIndex();
117
- _setCurrentHookIndex(index + 1);
118
-
119
- if (state.initialized)
120
- {
121
- // Validate: not more hooks than first tick
122
- if (index >= state.hookCount)
123
- {
124
- throw new Error(
125
- `Hook "${type}" called at index ${index}, but only ${state.hookCount} hooks were registered on the first tick. ` +
126
- 'This likely means a hook was called conditionally. Hooks must be called in the same order every tick.',
127
- );
128
- }
129
- // Validate: same hook type at this index
130
- if (state.hookTypes[index] !== type)
131
- {
132
- throw new Error(
133
- `Hook order changed at index ${index}: expected "${state.hookTypes[index]}", got "${type}". ` +
134
- 'Hooks must be called in the same order every tick.',
135
- );
136
- }
137
- }
138
- else
139
- {
140
- // First tick: record hook type
141
- state.hookTypes[index] = type;
142
- }
143
-
144
- return index;
145
- }
146
-
147
- /**
148
- * Finalize hook count after a successful tick.
149
- * On first tick: establishes the hook pattern.
150
- * On subsequent ticks: validates the count matches.
151
- */
152
- function finalizeHookCount(entityId: string): void
153
- {
154
- const state = entityHooks.get(entityId);
155
- if (!state) return;
156
-
157
- if (state.initialized)
158
- {
159
- if (_getCurrentHookIndex() < state.hookCount)
160
- {
161
- throw new Error(
162
- `Fewer hooks were called (${_getCurrentHookIndex()}) than on the first tick (${state.hookCount}). ` +
163
- 'This likely means a hook was called conditionally. Hooks must be called in the same order every tick.',
164
- );
165
- }
166
- }
167
- else
168
- {
169
- state.hookCount = _getCurrentHookIndex();
170
- state.initialized = true;
171
- }
172
- }
173
-
174
- /**
175
- * Run a function with a specific entity's hook context.
176
- */
177
- export function runWithHooks<T>(entityId: string, fn: () => T): T
178
- {
179
- const previousEntityId = _getCurrentEntityId();
180
- const previousHookIndex = _getCurrentHookIndex();
181
-
182
- _setCurrentEntityId(entityId);
183
- _setCurrentHookIndex(0);
184
-
185
- // If the first tick failed, reset hook type tracking so next attempt starts clean
186
- const state = entityHooks.get(entityId);
187
- if (state && !state.initialized)
188
- {
189
- state.hookTypes.length = 0;
190
- }
191
-
192
- try
193
- {
194
- const result = fn();
195
- finalizeHookCount(entityId);
196
- return result;
197
- }
198
- finally
199
- {
200
- _setCurrentEntityId(previousEntityId);
201
- _setCurrentHookIndex(previousHookIndex);
202
- }
203
- }
204
-
205
- /**
206
- * Run a bot function with full context (game state + persistence hooks).
207
- * Use this when you need to set BOTH the entity ID and the game context.
208
- */
209
- export function runBotWithContext<T>(entityId: string, context: BotContext, fn: () => T): T
210
- {
211
- const previousContext = _getCurrentBotContext();
212
- _setCurrentBotContext(context);
213
-
214
- try
215
- {
216
- return runWithHooks(entityId, fn);
217
- }
218
- finally
219
- {
220
- _setCurrentBotContext(previousContext);
221
- }
222
- }
223
-
224
- /**
225
- * Run a function with game state context only, preserving the current entity ID.
226
- * Use this inside wrapNewBot where the entity ID is already set by the outer runWithHooks.
227
- */
228
- export function withBotContext<T>(context: BotContext, fn: () => T): T
229
- {
230
- const previousContext = _getCurrentBotContext();
231
- _setCurrentBotContext(context);
232
-
233
- try
234
- {
235
- return fn();
236
- }
237
- finally
238
- {
239
- _setCurrentBotContext(previousContext);
240
- }
241
- }
242
-
243
- /**
244
- * Get the current bot context. Throws if called outside bot execution.
245
- */
246
- export function getBotContext(): BotContext
247
- {
248
- if (!_getCurrentBotContext())
249
- {
250
- throw new Error('Game state hooks can only be used inside a bot function');
251
- }
252
- return _getCurrentBotContext()!;
253
- }
254
-
255
- /**
256
- * Persist state between ticks.
257
- */
258
- export function useState<T>(initialValue: T | (() => T)): [T, (newValue: T | ((prev: T) => T)) => void]
259
- {
260
- const index = validateHookCall('useState');
261
- const state = getHookState(_getCurrentEntityId()!);
262
-
263
- if (state.values.length <= index)
264
- {
265
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
266
- state.values[index] = (typeof initialValue === 'function') ? (initialValue as () => T)() : initialValue;
267
- }
268
-
269
- const setter = (newValue: T | ((prev: T) => T)): void =>
270
- {
271
- if (typeof newValue === 'function')
272
- {
273
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
274
- state.values[index] = (newValue as (prev: T) => T)(state.values[index] as T);
275
- }
276
- else
277
- {
278
- state.values[index] = newValue;
279
- }
280
- };
281
-
282
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
283
- return [state.values[index] as T, setter];
284
- }
285
-
286
- /**
287
- * React to state changes.
288
- */
289
- export function useEffect(callback: () => void | (() => void), deps?: unknown[]): void
290
- {
291
- const index = validateHookCall('useEffect');
292
- const state = getHookState(_getCurrentEntityId()!);
293
-
294
- const oldEffect = state.effects[index];
295
- const hasChanged = !oldEffect || !deps ||
296
- deps.length !== oldEffect.deps?.length ||
297
- !deps.every((dep, i) => dep === oldEffect.deps?.[i]);
298
-
299
- if (hasChanged)
300
- {
301
- // Cleanup old effect if it exists
302
- if (oldEffect?.cleanup)
303
- {
304
- oldEffect.cleanup();
305
- }
306
-
307
- // Run new effect
308
- const cleanup = callback();
309
- state.effects[index] = {
310
- callback,
311
- deps,
312
- cleanup: typeof cleanup === 'function' ? cleanup : undefined,
313
- };
314
- }
315
- }
316
-
317
- /**
318
- * Memoize expensive calculations.
319
- */
320
- export function useMemo<T>(factory: () => T, deps?: unknown[]): T
321
- {
322
- const index = validateHookCall('useMemo');
323
- const state = getHookState(_getCurrentEntityId()!);
324
-
325
- const oldMemo = state.memos[index];
326
- const hasChanged = !oldMemo || !deps ||
327
- deps.length !== oldMemo.deps?.length ||
328
- !deps.every((dep, i) => dep === oldMemo.deps?.[i]);
329
-
330
- if (hasChanged)
331
- {
332
- const value = factory();
333
- state.memos[index] = {
334
- value,
335
- deps,
336
- };
337
- return value;
338
- }
339
-
340
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
341
- return oldMemo.value as T;
342
- }
343
-
344
- /**
345
- * Mutable reference that persists across ticks.
346
- * Unlike useState, mutations don't need a setter - just modify .current directly.
347
- */
348
- export interface RefObject<T>
349
- {
350
- current: T;
351
- }
352
-
353
- export function useRef<T>(initialValue: T): RefObject<T>
354
- {
355
- const index = validateHookCall('useRef');
356
- const state = getHookState(_getCurrentEntityId()!);
357
-
358
- if (state.values.length <= index)
359
- {
360
- state.values[index] = {current: initialValue};
361
- }
362
-
363
- // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
364
- return state.values[index] as RefObject<T>;
365
- }
366
-
367
- /**
368
- * Clear hook state for an entity (e.g., when it dies).
369
- */
370
- export function clearHooks(entityId: string): void
371
- {
372
- const state = entityHooks.get(entityId);
373
- if (state)
374
- {
375
- state.effects.forEach((effect) =>
376
- {
377
- if (effect.cleanup)
378
- {
379
- effect.cleanup();
380
- }
381
- });
382
- entityHooks.delete(entityId);
383
- }
384
- }
385
-
386
- /**
387
- * Reset all hook states (e.g., when a match restarts).
388
- */
389
- export function resetAllHooks(): void
390
- {
391
- entityHooks.forEach((_, entityId) => clearHooks(entityId));
392
- entityHooks.clear();
393
- }
394
-
1
+ /**
2
+ * VIBEMANCER - HOOKS RUNTIME
3
+ *
4
+ * This file implements a minimal React-like hooks runtime for AI programming.
5
+ * It supports useState, useEffect, useMemo, useRef, and useParam with entity isolation.
6
+ *
7
+ * RULES OF HOOKS (same as React):
8
+ * - Hooks must be called at the top level of the bot function
9
+ * - Hooks must be called in the same order every tick
10
+ * - Hooks must NOT be called conditionally
11
+ *
12
+ * Violating these rules throws an error (detected via hook index validation).
13
+ */
14
+
15
+ import type {BotContext} from '../hooks/types.js';
16
+
17
+ export interface HookState
18
+ {
19
+ values: unknown[];
20
+ effects: {
21
+ callback: () => void | (() => void);
22
+ deps?: unknown[];
23
+ cleanup?: () => void;
24
+ }[];
25
+ memos: {
26
+ value: unknown;
27
+ deps?: unknown[];
28
+ }[];
29
+ /** Type of each hook call in order (for validation). */
30
+ hookTypes: string[];
31
+ /** Total hooks called on first successful tick. */
32
+ hookCount: number;
33
+ /** Whether the first tick has completed successfully (hook pattern established). */
34
+ initialized: boolean;
35
+ }
36
+
37
+ // Use globalThis for hooks state so that multiple copies of the hooks runtime
38
+ // (e.g., engine template + bot bundle in sandboxed execution) share the same state.
39
+ // Without this, bot bundles that inline their own copy of the hooks runtime would
40
+ // have separate currentEntityId etc., causing "useParam can only be used inside
41
+ // a bot function" errors.
42
+ const _g = globalThis as Record<string, unknown>;
43
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- we own __vibemancer_entityHooks
44
+ const entityHooks: Map<string, HookState> = (_g.__vibemancer_entityHooks as Map<string, HookState>)
45
+ ?? (_g.__vibemancer_entityHooks = new Map<string, HookState>());
46
+
47
+ // Accessor functions for shared mutable state on globalThis
48
+ function _getCurrentEntityId(): string | null
49
+ {
50
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- we own __vibemancer_currentEntityId
51
+ return (_g.__vibemancer_currentEntityId as string | null) ?? null;
52
+ }
53
+ function _setCurrentEntityId(id: string | null): void
54
+ {
55
+ _g.__vibemancer_currentEntityId = id;
56
+ }
57
+ function _getCurrentHookIndex(): number
58
+ {
59
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- we own __vibemancer_currentHookIndex
60
+ return (_g.__vibemancer_currentHookIndex as number) ?? 0;
61
+ }
62
+ function _setCurrentHookIndex(idx: number): void
63
+ {
64
+ _g.__vibemancer_currentHookIndex = idx;
65
+ }
66
+ function _getCurrentBotContext(): BotContext | null
67
+ {
68
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- we own __vibemancer_currentBotContext
69
+ return (_g.__vibemancer_currentBotContext as BotContext | null) ?? null;
70
+ }
71
+ function _setCurrentBotContext(ctx: BotContext | null): void
72
+ {
73
+ _g.__vibemancer_currentBotContext = ctx;
74
+ }
75
+
76
+ /**
77
+ * Get or create the hook state for an entity.
78
+ */
79
+ function getHookState(entityId: string): HookState
80
+ {
81
+ let state = entityHooks.get(entityId);
82
+ if (!state)
83
+ {
84
+ state = {
85
+ values: [],
86
+ effects: [],
87
+ memos: [],
88
+ hookTypes: [],
89
+ hookCount: 0,
90
+ initialized: false,
91
+ };
92
+ entityHooks.set(entityId, state);
93
+ }
94
+ return state;
95
+ }
96
+
97
+ /**
98
+ * Validate a hook call and return its sequential index.
99
+ * Ensures hooks are called in the same order every tick.
100
+ *
101
+ * On the first tick: records the hook type at this index.
102
+ * On subsequent ticks: validates the hook type matches.
103
+ *
104
+ * @param type - The hook type name (e.g., 'useState', 'useEffect', 'useParam')
105
+ * @returns The sequential hook index
106
+ * @throws If called outside runWithHooks or if hook order changed
107
+ */
108
+ export function validateHookCall(type: string): number
109
+ {
110
+ if (!_getCurrentEntityId())
111
+ {
112
+ throw new Error(`${type} can only be used inside a bot function (runWithHooks)`);
113
+ }
114
+
115
+ const state = getHookState(_getCurrentEntityId()!);
116
+ const index = _getCurrentHookIndex();
117
+ _setCurrentHookIndex(index + 1);
118
+
119
+ if (state.initialized)
120
+ {
121
+ // Validate: not more hooks than first tick
122
+ if (index >= state.hookCount)
123
+ {
124
+ throw new Error(
125
+ `Hook "${type}" called at index ${index}, but only ${state.hookCount} hooks were registered on the first tick. ` +
126
+ 'This likely means a hook was called conditionally. Hooks must be called in the same order every tick.',
127
+ );
128
+ }
129
+ // Validate: same hook type at this index
130
+ if (state.hookTypes[index] !== type)
131
+ {
132
+ throw new Error(
133
+ `Hook order changed at index ${index}: expected "${state.hookTypes[index]}", got "${type}". ` +
134
+ 'Hooks must be called in the same order every tick.',
135
+ );
136
+ }
137
+ }
138
+ else
139
+ {
140
+ // First tick: record hook type
141
+ state.hookTypes[index] = type;
142
+ }
143
+
144
+ return index;
145
+ }
146
+
147
+ /**
148
+ * Finalize hook count after a successful tick.
149
+ * On first tick: establishes the hook pattern.
150
+ * On subsequent ticks: validates the count matches.
151
+ */
152
+ function finalizeHookCount(entityId: string): void
153
+ {
154
+ const state = entityHooks.get(entityId);
155
+ if (!state) return;
156
+
157
+ if (state.initialized)
158
+ {
159
+ if (_getCurrentHookIndex() < state.hookCount)
160
+ {
161
+ throw new Error(
162
+ `Fewer hooks were called (${_getCurrentHookIndex()}) than on the first tick (${state.hookCount}). ` +
163
+ 'This likely means a hook was called conditionally. Hooks must be called in the same order every tick.',
164
+ );
165
+ }
166
+ }
167
+ else
168
+ {
169
+ state.hookCount = _getCurrentHookIndex();
170
+ state.initialized = true;
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Run a function with a specific entity's hook context.
176
+ */
177
+ export function runWithHooks<T>(entityId: string, fn: () => T): T
178
+ {
179
+ const previousEntityId = _getCurrentEntityId();
180
+ const previousHookIndex = _getCurrentHookIndex();
181
+
182
+ _setCurrentEntityId(entityId);
183
+ _setCurrentHookIndex(0);
184
+
185
+ // If the first tick failed, reset hook type tracking so next attempt starts clean
186
+ const state = entityHooks.get(entityId);
187
+ if (state && !state.initialized)
188
+ {
189
+ state.hookTypes.length = 0;
190
+ }
191
+
192
+ try
193
+ {
194
+ const result = fn();
195
+ finalizeHookCount(entityId);
196
+ return result;
197
+ }
198
+ finally
199
+ {
200
+ _setCurrentEntityId(previousEntityId);
201
+ _setCurrentHookIndex(previousHookIndex);
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Run a bot function with full context (game state + persistence hooks).
207
+ * Use this when you need to set BOTH the entity ID and the game context.
208
+ */
209
+ export function runBotWithContext<T>(entityId: string, context: BotContext, fn: () => T): T
210
+ {
211
+ const previousContext = _getCurrentBotContext();
212
+ _setCurrentBotContext(context);
213
+
214
+ try
215
+ {
216
+ return runWithHooks(entityId, fn);
217
+ }
218
+ finally
219
+ {
220
+ _setCurrentBotContext(previousContext);
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Run a function with game state context only, preserving the current entity ID.
226
+ * Use this inside wrapNewBot where the entity ID is already set by the outer runWithHooks.
227
+ */
228
+ export function withBotContext<T>(context: BotContext, fn: () => T): T
229
+ {
230
+ const previousContext = _getCurrentBotContext();
231
+ _setCurrentBotContext(context);
232
+
233
+ try
234
+ {
235
+ return fn();
236
+ }
237
+ finally
238
+ {
239
+ _setCurrentBotContext(previousContext);
240
+ }
241
+ }
242
+
243
+ /**
244
+ * Get the current bot context. Throws if called outside bot execution.
245
+ */
246
+ export function getBotContext(): BotContext
247
+ {
248
+ if (!_getCurrentBotContext())
249
+ {
250
+ throw new Error('Game state hooks can only be used inside a bot function');
251
+ }
252
+ return _getCurrentBotContext()!;
253
+ }
254
+
255
+ /**
256
+ * Persist state between ticks.
257
+ */
258
+ export function useState<T>(initialValue: T | (() => T)): [T, (newValue: T | ((prev: T) => T)) => void]
259
+ {
260
+ const index = validateHookCall('useState');
261
+ const state = getHookState(_getCurrentEntityId()!);
262
+
263
+ if (state.values.length <= index)
264
+ {
265
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
266
+ state.values[index] = (typeof initialValue === 'function') ? (initialValue as () => T)() : initialValue;
267
+ }
268
+
269
+ const setter = (newValue: T | ((prev: T) => T)): void =>
270
+ {
271
+ if (typeof newValue === 'function')
272
+ {
273
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
274
+ state.values[index] = (newValue as (prev: T) => T)(state.values[index] as T);
275
+ }
276
+ else
277
+ {
278
+ state.values[index] = newValue;
279
+ }
280
+ };
281
+
282
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
283
+ return [state.values[index] as T, setter];
284
+ }
285
+
286
+ /**
287
+ * React to state changes.
288
+ */
289
+ export function useEffect(callback: () => void | (() => void), deps?: unknown[]): void
290
+ {
291
+ const index = validateHookCall('useEffect');
292
+ const state = getHookState(_getCurrentEntityId()!);
293
+
294
+ const oldEffect = state.effects[index];
295
+ const hasChanged = !oldEffect || !deps ||
296
+ deps.length !== oldEffect.deps?.length ||
297
+ !deps.every((dep, i) => dep === oldEffect.deps?.[i]);
298
+
299
+ if (hasChanged)
300
+ {
301
+ // Cleanup old effect if it exists
302
+ if (oldEffect?.cleanup)
303
+ {
304
+ oldEffect.cleanup();
305
+ }
306
+
307
+ // Run new effect
308
+ const cleanup = callback();
309
+ state.effects[index] = {
310
+ callback,
311
+ deps,
312
+ cleanup: typeof cleanup === 'function' ? cleanup : undefined,
313
+ };
314
+ }
315
+ }
316
+
317
+ /**
318
+ * Memoize expensive calculations.
319
+ */
320
+ export function useMemo<T>(factory: () => T, deps?: unknown[]): T
321
+ {
322
+ const index = validateHookCall('useMemo');
323
+ const state = getHookState(_getCurrentEntityId()!);
324
+
325
+ const oldMemo = state.memos[index];
326
+ const hasChanged = !oldMemo || !deps ||
327
+ deps.length !== oldMemo.deps?.length ||
328
+ !deps.every((dep, i) => dep === oldMemo.deps?.[i]);
329
+
330
+ if (hasChanged)
331
+ {
332
+ const value = factory();
333
+ state.memos[index] = {
334
+ value,
335
+ deps,
336
+ };
337
+ return value;
338
+ }
339
+
340
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
341
+ return oldMemo.value as T;
342
+ }
343
+
344
+ /**
345
+ * Mutable reference that persists across ticks.
346
+ * Unlike useState, mutations don't need a setter - just modify .current directly.
347
+ */
348
+ export interface RefObject<T>
349
+ {
350
+ current: T;
351
+ }
352
+
353
+ export function useRef<T>(initialValue: T): RefObject<T>
354
+ {
355
+ const index = validateHookCall('useRef');
356
+ const state = getHookState(_getCurrentEntityId()!);
357
+
358
+ if (state.values.length <= index)
359
+ {
360
+ state.values[index] = {current: initialValue};
361
+ }
362
+
363
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
364
+ return state.values[index] as RefObject<T>;
365
+ }
366
+
367
+ /**
368
+ * Clear hook state for an entity (e.g., when it dies).
369
+ */
370
+ export function clearHooks(entityId: string): void
371
+ {
372
+ const state = entityHooks.get(entityId);
373
+ if (state)
374
+ {
375
+ state.effects.forEach((effect) =>
376
+ {
377
+ if (effect.cleanup)
378
+ {
379
+ effect.cleanup();
380
+ }
381
+ });
382
+ entityHooks.delete(entityId);
383
+ }
384
+ }
385
+
386
+ /**
387
+ * Reset all hook states (e.g., when a match restarts).
388
+ */
389
+ export function resetAllHooks(): void
390
+ {
391
+ entityHooks.forEach((_, entityId) => clearHooks(entityId));
392
+ entityHooks.clear();
393
+ }
394
+