@vibemancer/core 1.0.9 → 1.0.11
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/dist/{chunk-OLGKLCCB.js → chunk-EO7JO2RZ.js} +199 -24
- package/dist/chunk-EO7JO2RZ.js.map +1 -0
- package/dist/{index-browser-BTHlrB_s.d.ts → index-browser-CM0uuzWp.d.ts} +156 -18
- package/dist/index-browser.d.ts +1 -1
- package/dist/index-browser.js +5 -1
- package/dist/index.d.ts +10 -4
- package/dist/index.js +51 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/engine/bot-compute-budget.ts +10 -0
- package/src/engine/bot-error-capture.ts +179 -0
- package/src/engine/manual-match.ts +2 -0
- package/src/engine/physics.ts +76 -0
- package/src/engine/sandbox-harness.ts +46 -0
- package/src/engine/sandbox.ts +347 -341
- package/src/engine/simulation.ts +120 -10
- package/src/engine-version.ts +1 -1
- package/src/hooks/action-builders.ts +112 -5
- package/src/hooks/state-hooks.ts +409 -407
- package/src/hooks/threat-analysis.ts +57 -18
- package/src/hooks/types.ts +16 -4
- package/src/rules.ts +8 -0
- package/src/types.ts +10 -6
- package/src/utils/combat.ts +379 -371
- package/dist/chunk-OLGKLCCB.js.map +0 -1
package/src/hooks/state-hooks.ts
CHANGED
|
@@ -1,407 +1,409 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* VIBEMANCER - STATE HOOKS
|
|
3
|
-
*
|
|
4
|
-
* React-style hooks for reading game state.
|
|
5
|
-
* These handle the "subconscious" perception that humans do instinctively.
|
|
6
|
-
*
|
|
7
|
-
* UNITS REFERENCE (100 ticks = 1 second):
|
|
8
|
-
* Position: absolute world coordinates, 0-860 on each axis. The arena is 860x860,
|
|
9
|
-
* but only [30, 830] is safe — the outer 30 units are lethal lava.
|
|
10
|
-
* Velocity: units per tick on each axis (player max speed = 1 u/t)
|
|
11
|
-
* Health: hit points (max 60)
|
|
12
|
-
* Ticks: game ticks (10ms each, 100/sec). Divide by 100 for seconds.
|
|
13
|
-
* Shield: block multiplier 0.0-0.9 (0.9 = blocks 90% damage)
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import {Position, Velocity, ProjectileState} from '../types.js';
|
|
17
|
-
import {getWizardContext, useMemo} from '../engine/hooks-runtime.js';
|
|
18
|
-
import {RULES, TICKS_PER_SECOND} from '../rules.js';
|
|
19
|
-
import type {EnemyState, AnalyzedThreat} from './types.js';
|
|
20
|
-
import {analyzeThreats} from './threat-analysis.js';
|
|
21
|
-
|
|
22
|
-
// ============================================================
|
|
23
|
-
// SELF STATE HOOKS
|
|
24
|
-
// ============================================================
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Get a seeded random number generator. Returns a function that produces
|
|
28
|
-
* deterministic values in [0, 1) — same seed + same tick = same sequence.
|
|
29
|
-
* Use this instead of Math.random() so replays are deterministic.
|
|
30
|
-
*/
|
|
31
|
-
export function useRandom(): () => number
|
|
32
|
-
{
|
|
33
|
-
return getWizardContext().random;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Get your current health (0-60). Wizard dies at 0.
|
|
38
|
-
*/
|
|
39
|
-
export function useHealth(): number
|
|
40
|
-
{
|
|
41
|
-
return getWizardContext().health;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Get your current position as {x, y} in world coordinates (0-860).
|
|
46
|
-
*
|
|
47
|
-
* Clamped to [5, 855] — the ARENA bounds minus the wizard radius, NOT the playfield. That
|
|
48
|
-
* distinction is the difference between living and dying: the playfield is [30, 830] and
|
|
49
|
-
* everything outside it is lava, so you can walk straight out of the safe area and be
|
|
50
|
-
* killed. Nothing stops you.
|
|
51
|
-
*
|
|
52
|
-
* And the number you actually need is neither of those. Lava is tested against your EDGE,
|
|
53
|
-
* not your centre (`isInLava` checks `x + WIZARD_RADIUS > ARENA_MAX`), and this returns your
|
|
54
|
-
* CENTRE — so with a radius of 5 you die once your centre passes 825. The safe range to
|
|
55
|
-
* steer by is [35, 825]. Aiming for 828 because "the playfield is [30, 830]" is fatal, which
|
|
56
|
-
* is exactly what a real fight trace showed: LAVA_DEATH at (826, 430).
|
|
57
|
-
*
|
|
58
|
-
* (This previously quoted the playfield-sized bounds as the clamp, which told the reader
|
|
59
|
-
* they were safely fenced in. They are not — see docs-match-engine.test.ts.)
|
|
60
|
-
*/
|
|
61
|
-
export function usePosition(): Position
|
|
62
|
-
{
|
|
63
|
-
const ctx = getWizardContext();
|
|
64
|
-
return {x: ctx.position.x, y: ctx.position.y};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Get your current velocity as {x, y} in units/tick.
|
|
69
|
-
* Max magnitude is 1 u/t when idle, 0.5 u/t when casting, 0 when channeling shield.
|
|
70
|
-
*/
|
|
71
|
-
export function useVelocity(): Velocity
|
|
72
|
-
{
|
|
73
|
-
const ctx = getWizardContext();
|
|
74
|
-
return {x: ctx.velocity.x, y: ctx.velocity.y};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Get your current status:
|
|
79
|
-
* - 'idle': free to act
|
|
80
|
-
* - 'casting': casting a spell (missile
|
|
81
|
-
* - 'channeling': channeling shield. Cannot move. Cancel anytime with cancel().
|
|
82
|
-
* - 'gcd_locked': global cooldown after spell. Can move at full speed, but cannot cast.
|
|
83
|
-
*/
|
|
84
|
-
export function useStatus(): 'idle' | 'casting' | 'channeling' | 'gcd_locked'
|
|
85
|
-
{
|
|
86
|
-
return getWizardContext().state;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Get ticks until you can start a new spell.
|
|
91
|
-
*
|
|
92
|
-
* Returns 0 when idle or channeling (shield can be canceled immediately).
|
|
93
|
-
* During casting: remaining cast ticks. During GCD: remaining GCD ticks.
|
|
94
|
-
*
|
|
95
|
-
* Note: 100 ticks = 1 second.
|
|
96
|
-
*/
|
|
97
|
-
export function useTicksUntilReady(): number
|
|
98
|
-
{
|
|
99
|
-
const ctx = getWizardContext();
|
|
100
|
-
|
|
101
|
-
switch (ctx.state)
|
|
102
|
-
{
|
|
103
|
-
case 'idle':
|
|
104
|
-
return 0;
|
|
105
|
-
case 'casting':
|
|
106
|
-
// Remaining cast time
|
|
107
|
-
if (ctx.castDuration !== undefined && ctx.castProgress !== undefined)
|
|
108
|
-
{
|
|
109
|
-
return ctx.castDuration - ctx.castProgress;
|
|
110
|
-
}
|
|
111
|
-
return 0;
|
|
112
|
-
case 'channeling':
|
|
113
|
-
// Can cancel anytime, so 0
|
|
114
|
-
return 0;
|
|
115
|
-
case 'gcd_locked':
|
|
116
|
-
return ctx.gcdRemaining ?? 0;
|
|
117
|
-
default:
|
|
118
|
-
return 0;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Get current shield block multiplier.
|
|
124
|
-
*
|
|
125
|
-
* Returns 0 if not channeling shield.
|
|
126
|
-
* Returns 0.3-0.9 if channeling: starts at 0.9 (blocks 90%), decays by 0.2/sec,
|
|
127
|
-
* minimum 0.3 (blocks 30%). The remaining damage gets through:
|
|
128
|
-
* actualDamage = incomingDamage × (1 - shieldStrength).
|
|
129
|
-
*/
|
|
130
|
-
export function useShieldStrength(): number
|
|
131
|
-
{
|
|
132
|
-
const ctx = getWizardContext();
|
|
133
|
-
|
|
134
|
-
if (ctx.state !== 'channeling' || ctx.channelingSpell !== 'shield')
|
|
135
|
-
{
|
|
136
|
-
return 0;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Calculate shield strength based on channel duration
|
|
140
|
-
const channelDuration = ctx.channelDuration ?? 0;
|
|
141
|
-
const decayPerTick = RULES.SHIELD_DECAY_PER_SECOND / TICKS_PER_SECOND;
|
|
142
|
-
const strength = RULES.SHIELD_MAX_BLOCK - (channelDuration * decayPerTick);
|
|
143
|
-
return Math.max(RULES.SHIELD_MIN_BLOCK, strength);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Get all projectiles currently in flight (yours and enemy's).
|
|
148
|
-
* Used for blink safety calculations and threat analysis.
|
|
149
|
-
*/
|
|
150
|
-
export function useProjectiles(): ProjectileState[]
|
|
151
|
-
{
|
|
152
|
-
return getWizardContext().projectiles;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Get the config of the last missile you fired, or undefined if none fired yet.
|
|
157
|
-
* Used for the warmup system: consecutive similar missiles cast faster.
|
|
158
|
-
*/
|
|
159
|
-
export function useLastMissileConfig(): import('../types.js').MissileConfig | undefined
|
|
160
|
-
{
|
|
161
|
-
return getWizardContext().lastMissileConfig;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Get blink cooldown remaining in ticks. Returns 0 if ready to blink.
|
|
166
|
-
*
|
|
167
|
-
* Cooldown scales with distance used:
|
|
168
|
-
* -
|
|
169
|
-
* -
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
// ============================================================
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
// ============================================================
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
// ============================================================
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
// ============================================================
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
// ============================================================
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
* -
|
|
349
|
-
* -
|
|
350
|
-
* -
|
|
351
|
-
* -
|
|
352
|
-
* -
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
ctx.
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
1
|
+
/**
|
|
2
|
+
* VIBEMANCER - STATE HOOKS
|
|
3
|
+
*
|
|
4
|
+
* React-style hooks for reading game state.
|
|
5
|
+
* These handle the "subconscious" perception that humans do instinctively.
|
|
6
|
+
*
|
|
7
|
+
* UNITS REFERENCE (100 ticks = 1 second):
|
|
8
|
+
* Position: absolute world coordinates, 0-860 on each axis. The arena is 860x860,
|
|
9
|
+
* but only [30, 830] is safe — the outer 30 units are lethal lava.
|
|
10
|
+
* Velocity: units per tick on each axis (player max speed = 1 u/t)
|
|
11
|
+
* Health: hit points (max 60)
|
|
12
|
+
* Ticks: game ticks (10ms each, 100/sec). Divide by 100 for seconds.
|
|
13
|
+
* Shield: block multiplier 0.0-0.9 (0.9 = blocks 90% damage)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {Position, Velocity, ProjectileState} from '../types.js';
|
|
17
|
+
import {getWizardContext, useMemo} from '../engine/hooks-runtime.js';
|
|
18
|
+
import {RULES, TICKS_PER_SECOND} from '../rules.js';
|
|
19
|
+
import type {EnemyState, AnalyzedThreat} from './types.js';
|
|
20
|
+
import {analyzeThreats} from './threat-analysis.js';
|
|
21
|
+
|
|
22
|
+
// ============================================================
|
|
23
|
+
// SELF STATE HOOKS
|
|
24
|
+
// ============================================================
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Get a seeded random number generator. Returns a function that produces
|
|
28
|
+
* deterministic values in [0, 1) — same seed + same tick = same sequence.
|
|
29
|
+
* Use this instead of Math.random() so replays are deterministic.
|
|
30
|
+
*/
|
|
31
|
+
export function useRandom(): () => number
|
|
32
|
+
{
|
|
33
|
+
return getWizardContext().random;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get your current health (0-60). Wizard dies at 0.
|
|
38
|
+
*/
|
|
39
|
+
export function useHealth(): number
|
|
40
|
+
{
|
|
41
|
+
return getWizardContext().health;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get your current position as {x, y} in world coordinates (0-860).
|
|
46
|
+
*
|
|
47
|
+
* Clamped to [5, 855] — the ARENA bounds minus the wizard radius, NOT the playfield. That
|
|
48
|
+
* distinction is the difference between living and dying: the playfield is [30, 830] and
|
|
49
|
+
* everything outside it is lava, so you can walk straight out of the safe area and be
|
|
50
|
+
* killed. Nothing stops you.
|
|
51
|
+
*
|
|
52
|
+
* And the number you actually need is neither of those. Lava is tested against your EDGE,
|
|
53
|
+
* not your centre (`isInLava` checks `x + WIZARD_RADIUS > ARENA_MAX`), and this returns your
|
|
54
|
+
* CENTRE — so with a radius of 5 you die once your centre passes 825. The safe range to
|
|
55
|
+
* steer by is [35, 825]. Aiming for 828 because "the playfield is [30, 830]" is fatal, which
|
|
56
|
+
* is exactly what a real fight trace showed: LAVA_DEATH at (826, 430).
|
|
57
|
+
*
|
|
58
|
+
* (This previously quoted the playfield-sized bounds as the clamp, which told the reader
|
|
59
|
+
* they were safely fenced in. They are not — see docs-match-engine.test.ts.)
|
|
60
|
+
*/
|
|
61
|
+
export function usePosition(): Position
|
|
62
|
+
{
|
|
63
|
+
const ctx = getWizardContext();
|
|
64
|
+
return {x: ctx.position.x, y: ctx.position.y};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get your current velocity as {x, y} in units/tick.
|
|
69
|
+
* Max magnitude is 1 u/t when idle, 0.5 u/t when casting, 0 when channeling shield.
|
|
70
|
+
*/
|
|
71
|
+
export function useVelocity(): Velocity
|
|
72
|
+
{
|
|
73
|
+
const ctx = getWizardContext();
|
|
74
|
+
return {x: ctx.velocity.x, y: ctx.velocity.y};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get your current status:
|
|
79
|
+
* - 'idle': free to act
|
|
80
|
+
* - 'casting': casting a spell (missile, blink OR shield). Can move at 33% speed.
|
|
81
|
+
* - 'channeling': channeling shield. Cannot move. Cancel anytime with cancel().
|
|
82
|
+
* - 'gcd_locked': global cooldown after spell. Can move at full speed, but cannot cast.
|
|
83
|
+
*/
|
|
84
|
+
export function useStatus(): 'idle' | 'casting' | 'channeling' | 'gcd_locked'
|
|
85
|
+
{
|
|
86
|
+
return getWizardContext().state;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get ticks until you can start a new spell.
|
|
91
|
+
*
|
|
92
|
+
* Returns 0 when idle or channeling (shield can be canceled immediately).
|
|
93
|
+
* During casting: remaining cast ticks. During GCD: remaining GCD ticks.
|
|
94
|
+
*
|
|
95
|
+
* Note: 100 ticks = 1 second.
|
|
96
|
+
*/
|
|
97
|
+
export function useTicksUntilReady(): number
|
|
98
|
+
{
|
|
99
|
+
const ctx = getWizardContext();
|
|
100
|
+
|
|
101
|
+
switch (ctx.state)
|
|
102
|
+
{
|
|
103
|
+
case 'idle':
|
|
104
|
+
return 0;
|
|
105
|
+
case 'casting':
|
|
106
|
+
// Remaining cast time
|
|
107
|
+
if (ctx.castDuration !== undefined && ctx.castProgress !== undefined)
|
|
108
|
+
{
|
|
109
|
+
return ctx.castDuration - ctx.castProgress;
|
|
110
|
+
}
|
|
111
|
+
return 0;
|
|
112
|
+
case 'channeling':
|
|
113
|
+
// Can cancel anytime, so 0
|
|
114
|
+
return 0;
|
|
115
|
+
case 'gcd_locked':
|
|
116
|
+
return ctx.gcdRemaining ?? 0;
|
|
117
|
+
default:
|
|
118
|
+
return 0;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get current shield block multiplier.
|
|
124
|
+
*
|
|
125
|
+
* Returns 0 if not channeling shield.
|
|
126
|
+
* Returns 0.3-0.9 if channeling: starts at 0.9 (blocks 90%), decays by 0.2/sec,
|
|
127
|
+
* minimum 0.3 (blocks 30%). The remaining damage gets through:
|
|
128
|
+
* actualDamage = incomingDamage × (1 - shieldStrength).
|
|
129
|
+
*/
|
|
130
|
+
export function useShieldStrength(): number
|
|
131
|
+
{
|
|
132
|
+
const ctx = getWizardContext();
|
|
133
|
+
|
|
134
|
+
if (ctx.state !== 'channeling' || ctx.channelingSpell !== 'shield')
|
|
135
|
+
{
|
|
136
|
+
return 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Calculate shield strength based on channel duration
|
|
140
|
+
const channelDuration = ctx.channelDuration ?? 0;
|
|
141
|
+
const decayPerTick = RULES.SHIELD_DECAY_PER_SECOND / TICKS_PER_SECOND;
|
|
142
|
+
const strength = RULES.SHIELD_MAX_BLOCK - (channelDuration * decayPerTick);
|
|
143
|
+
return Math.max(RULES.SHIELD_MIN_BLOCK, strength);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Get all projectiles currently in flight (yours and enemy's).
|
|
148
|
+
* Used for blink safety calculations and threat analysis.
|
|
149
|
+
*/
|
|
150
|
+
export function useProjectiles(): ProjectileState[]
|
|
151
|
+
{
|
|
152
|
+
return getWizardContext().projectiles;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Get the config of the last missile you fired, or undefined if none fired yet.
|
|
157
|
+
* Used for the warmup system: consecutive similar missiles cast faster.
|
|
158
|
+
*/
|
|
159
|
+
export function useLastMissileConfig(): import('../types.js').MissileConfig | undefined
|
|
160
|
+
{
|
|
161
|
+
return getWizardContext().lastMissileConfig;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Get blink cooldown remaining in ticks. Returns 0 if ready to blink.
|
|
166
|
+
*
|
|
167
|
+
* Cooldown scales with distance used:
|
|
168
|
+
* - 10 units → 100 ticks (1s); 100 units → 667 ticks; 300 → 2000 (20s)
|
|
169
|
+
* - The cooldown scales with DISTANCE, so a micro-blink is cheap and a full-range one is
|
|
170
|
+
* not. This said 100 units → ~100 ticks, which understated it by 6.7x.
|
|
171
|
+
* - 300 units (max range) → 2000 ticks (20s)
|
|
172
|
+
*
|
|
173
|
+
* Note: 100 ticks = 1 second.
|
|
174
|
+
*/
|
|
175
|
+
export function useBlinkCooldown(): number
|
|
176
|
+
{
|
|
177
|
+
return getWizardContext().blinkCooldown;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Get currently casting spell, or null if not casting.
|
|
182
|
+
* Returns 'missile', 'shield', or 'blink'.
|
|
183
|
+
*/
|
|
184
|
+
export function useCastingSpell(): 'missile' | 'shield' | 'blink' | null
|
|
185
|
+
{
|
|
186
|
+
const ctx = getWizardContext();
|
|
187
|
+
if (ctx.state !== 'casting')
|
|
188
|
+
{
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
return ctx.castingSpell ?? null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Get cast progress as {current, total} in ticks, or null if not casting.
|
|
196
|
+
*
|
|
197
|
+
* current = ticks elapsed, total = ticks needed. When current >= total, spell fires.
|
|
198
|
+
* Note: 100 ticks = 1 second.
|
|
199
|
+
*/
|
|
200
|
+
export function useCastProgress(): {current: number; total: number} | null
|
|
201
|
+
{
|
|
202
|
+
const ctx = getWizardContext();
|
|
203
|
+
if (ctx.state !== 'casting')
|
|
204
|
+
{
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
if (ctx.castProgress === undefined || ctx.castDuration === undefined)
|
|
208
|
+
{
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
return {current: ctx.castProgress, total: ctx.castDuration};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ============================================================
|
|
215
|
+
// ENEMY STATE HOOK
|
|
216
|
+
// ============================================================
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Get enemy wizard state.
|
|
220
|
+
*
|
|
221
|
+
* Returns position, velocity, health, status, casting spell, and shield strength.
|
|
222
|
+
* Note: you cannot see the enemy's missile configs or exact cooldown timers —
|
|
223
|
+
* only their status and what's visible on the field.
|
|
224
|
+
*/
|
|
225
|
+
export function useEnemy(): EnemyState
|
|
226
|
+
{
|
|
227
|
+
const ctx = getWizardContext();
|
|
228
|
+
const enemy = ctx.enemies[0];
|
|
229
|
+
|
|
230
|
+
if (!enemy)
|
|
231
|
+
{
|
|
232
|
+
// No enemy - return default state
|
|
233
|
+
return {
|
|
234
|
+
position: {x: 0, y: 0},
|
|
235
|
+
velocity: {x: 0, y: 0},
|
|
236
|
+
health: 0,
|
|
237
|
+
status: 'idle',
|
|
238
|
+
castingSpell: null,
|
|
239
|
+
castProgress: 0,
|
|
240
|
+
castDuration: 0,
|
|
241
|
+
gcdRemaining: 0,
|
|
242
|
+
channelDuration: 0,
|
|
243
|
+
shieldStrength: 0,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Calculate enemy shield strength
|
|
248
|
+
let shieldStrength = 0;
|
|
249
|
+
if (enemy.state === 'channeling' && enemy.channelingSpell === 'shield')
|
|
250
|
+
{
|
|
251
|
+
const channelDuration = enemy.channelDuration ?? 0;
|
|
252
|
+
const decayPerTick = RULES.SHIELD_DECAY_PER_SECOND / TICKS_PER_SECOND;
|
|
253
|
+
shieldStrength = Math.max(RULES.SHIELD_MIN_BLOCK, RULES.SHIELD_MAX_BLOCK - (channelDuration * decayPerTick));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
position: {x: enemy.position.x, y: enemy.position.y},
|
|
258
|
+
velocity: {x: enemy.velocity.x, y: enemy.velocity.y},
|
|
259
|
+
health: enemy.health,
|
|
260
|
+
status: enemy.state,
|
|
261
|
+
castingSpell: enemy.state === 'casting' ? (enemy.castingSpell ?? null) : null,
|
|
262
|
+
castProgress: enemy.castProgress ?? 0,
|
|
263
|
+
castDuration: enemy.castDuration ?? 0,
|
|
264
|
+
gcdRemaining: enemy.gcdRemaining ?? 0,
|
|
265
|
+
channelDuration: enemy.channelDuration ?? 0,
|
|
266
|
+
shieldStrength,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ============================================================
|
|
271
|
+
// PROJECTILE HOOKS
|
|
272
|
+
// ============================================================
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Get all your active (in-flight) projectiles.
|
|
276
|
+
* Each has position, rotation (degrees), speed (u/t), turnRate, remainingTicks.
|
|
277
|
+
*/
|
|
278
|
+
export function useMyProjectiles(): ProjectileState[]
|
|
279
|
+
{
|
|
280
|
+
return getWizardContext().myProjectiles;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ============================================================
|
|
284
|
+
// COMBAT TRACKING HOOKS
|
|
285
|
+
// ============================================================
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Get total damage you've dealt this match.
|
|
289
|
+
*/
|
|
290
|
+
export function useDamageDealt(): number
|
|
291
|
+
{
|
|
292
|
+
return getWizardContext().damageDealt;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Get total damage you've taken this match.
|
|
297
|
+
*/
|
|
298
|
+
export function useDamageTaken(): number
|
|
299
|
+
{
|
|
300
|
+
return getWizardContext().damageTaken;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Get the tick number when you last took damage. Returns 0 if never hit.
|
|
305
|
+
* Compare with useTick() to get ticks since last hit.
|
|
306
|
+
*/
|
|
307
|
+
export function useLastHitTick(): number
|
|
308
|
+
{
|
|
309
|
+
return getWizardContext().lastHitTick;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ============================================================
|
|
313
|
+
// ARENA HOOKS
|
|
314
|
+
// ============================================================
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Get arena dimensions. Default: {width: 860, height: 860} — the FULL arena, lava included.
|
|
318
|
+
* The safe playfield is [30, 830]; the outer 30 units on every side are lethal.
|
|
319
|
+
*
|
|
320
|
+
* Wizards are clamped to [5, 855] (arena bounds minus the radius of 5), which does not keep
|
|
321
|
+
* them out of the lava.
|
|
322
|
+
*/
|
|
323
|
+
export function useArenaSize(): {width: number; height: number}
|
|
324
|
+
{
|
|
325
|
+
const ctx = getWizardContext();
|
|
326
|
+
return {width: ctx.arenaWidth, height: ctx.arenaHeight};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Get current game tick (starts at 0, increments each tick).
|
|
331
|
+
* 100 ticks = 1 second. Match ends at 30,000 ticks (5 minutes).
|
|
332
|
+
*/
|
|
333
|
+
export function useTick(): number
|
|
334
|
+
{
|
|
335
|
+
return getWizardContext().tick;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ============================================================
|
|
339
|
+
// THREAT ANALYSIS HOOKS
|
|
340
|
+
// ============================================================
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Get analyzed threats from all incoming enemy projectiles.
|
|
344
|
+
* Sorted by ticksToImpact (soonest first). Only includes missiles within 500 units
|
|
345
|
+
* or that are predicted to hit.
|
|
346
|
+
*
|
|
347
|
+
* Each threat includes:
|
|
348
|
+
* - ticksToImpact: ticks until hit (Infinity if will miss)
|
|
349
|
+
* - willHit: true if missile hits your current position
|
|
350
|
+
* - canDodgeLeft/Right: whether strafing perpendicular to missile heading works
|
|
351
|
+
* - canOutrun: whether moving away from missile escapes it
|
|
352
|
+
* - bestDodgeDirection: {x, y} unit vector to dodge optimally, or null if undodgeable
|
|
353
|
+
* - canBlockInTime: whether you can raise shield before impact
|
|
354
|
+
* - ticksToStartShield: when to START channeling shield to block in time
|
|
355
|
+
*/
|
|
356
|
+
export function useThreats(): AnalyzedThreat[]
|
|
357
|
+
{
|
|
358
|
+
const ctx = getWizardContext();
|
|
359
|
+
const ticksUntilReady = useTicksUntilReady();
|
|
360
|
+
|
|
361
|
+
// Memoize to avoid recalculating every call
|
|
362
|
+
return useMemo(() =>
|
|
363
|
+
{
|
|
364
|
+
return analyzeThreats(
|
|
365
|
+
ctx.position,
|
|
366
|
+
ctx.projectiles,
|
|
367
|
+
ctx.myProjectiles,
|
|
368
|
+
ticksUntilReady,
|
|
369
|
+
);
|
|
370
|
+
}, [ctx.projectiles, ctx.myProjectiles, ctx.position.x, ctx.position.y, ticksUntilReady]);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Get the most imminent threat, or null if no threats.
|
|
375
|
+
* Shorthand for useThreats()[0].
|
|
376
|
+
*/
|
|
377
|
+
export function useClosestThreat(): AnalyzedThreat | null
|
|
378
|
+
{
|
|
379
|
+
const threats = useThreats();
|
|
380
|
+
return threats[0] ?? null;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Get your missiles analyzed from the enemy's perspective.
|
|
385
|
+
* Useful to predict when enemy will shield/dodge your attacks.
|
|
386
|
+
*/
|
|
387
|
+
export function useMyThreatsToEnemy(): AnalyzedThreat[]
|
|
388
|
+
{
|
|
389
|
+
const ctx = getWizardContext();
|
|
390
|
+
const enemy = ctx.enemies[0];
|
|
391
|
+
|
|
392
|
+
if (!enemy)
|
|
393
|
+
{
|
|
394
|
+
return [];
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Memoize to avoid recalculating every call
|
|
398
|
+
return useMemo(() =>
|
|
399
|
+
{
|
|
400
|
+
// Analyze from enemy's perspective
|
|
401
|
+
// Pass empty array for "enemy's own projectiles" since we're analyzing our missiles as threats TO them
|
|
402
|
+
return analyzeThreats(
|
|
403
|
+
enemy.position,
|
|
404
|
+
ctx.myProjectiles, // Our projectiles are their threats
|
|
405
|
+
[], // Enemy has no "own projectiles" in this context
|
|
406
|
+
0, // We don't know enemy's ticksUntilReady
|
|
407
|
+
);
|
|
408
|
+
}, [ctx.myProjectiles, enemy.position.x, enemy.position.y]);
|
|
409
|
+
}
|