@vibemancer/core 0.1.4 → 0.1.6

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 (76) hide show
  1. package/README.md +2 -2
  2. package/dist/chunk-G5T65F3W.js +10617 -0
  3. package/dist/chunk-G5T65F3W.js.map +1 -0
  4. package/dist/index-browser.d.ts +1328 -1051
  5. package/dist/index-browser.js +59 -17
  6. package/dist/index.d.ts +53 -3
  7. package/dist/index.js +204 -54
  8. package/dist/index.js.map +1 -1
  9. package/package.json +21 -15
  10. package/src/banned-bot-names.ts +53 -0
  11. package/src/bots/Hero.ts +867 -0
  12. package/src/bots/berserker/01_Stormchaser.ts +162 -120
  13. package/src/bots/berserker/02_Stormcaller.ts +160 -116
  14. package/src/bots/berserker/03_Stormforger.ts +162 -129
  15. package/src/bots/caster/01_Flamecaller.ts +126 -84
  16. package/src/bots/caster/02_Pyromancer.ts +148 -101
  17. package/src/bots/caster/03_Infernalist.ts +180 -160
  18. package/src/bots/defensive/01_Turtle.ts +48 -44
  19. package/src/bots/defensive/02_Sentinel.ts +57 -53
  20. package/src/bots/defensive/03_Golem.ts +85 -97
  21. package/src/bots/duelist/01_Battlemage.ts +157 -122
  22. package/src/bots/duelist/02_Warmage.ts +166 -121
  23. package/src/bots/duelist/03_Archmage.ts +198 -178
  24. package/src/bots/homing/01_Bonemancer.ts +20 -32
  25. package/src/bots/homing/02_Lich.ts +145 -93
  26. package/src/bots/homing/03_Archlich.ts +129 -60
  27. package/src/bots/kiter/01_Spellspinner.ts +145 -107
  28. package/src/bots/kiter/02_Spellweaver.ts +153 -105
  29. package/src/bots/kiter/03_Spellbinder.ts +168 -123
  30. package/src/bots/melee/01_Shadowblade.ts +131 -75
  31. package/src/bots/melee/02_Nightblade.ts +174 -130
  32. package/src/bots/melee/03_Voidblade.ts +266 -168
  33. package/src/bots/registry.ts +44 -37
  34. package/src/bots/shared.ts +185 -25
  35. package/src/bots/sniper/01_Spellshot.ts +151 -98
  36. package/src/bots/sniper/02_Spelltracer.ts +173 -128
  37. package/src/bots/sniper/03_Spellseeker.ts +177 -152
  38. package/src/bots/standalone/Critter.ts +46 -52
  39. package/src/bots/standalone/Doombringer.ts +36 -40
  40. package/src/bots/standalone/Hogger.ts +120 -89
  41. package/src/bots/standalone/Rookie.ts +21 -23
  42. package/src/bots/standalone/TargetDummy.ts +5 -6
  43. package/src/bots/test/cheater.ts +91 -85
  44. package/src/bots/test/crasher.ts +26 -28
  45. package/src/engine/bundle-fight.ts +242 -0
  46. package/src/engine/hooks-runtime.ts +58 -18
  47. package/src/engine/manual-match.ts +33 -25
  48. package/src/engine/missile-templates.ts +25 -43
  49. package/src/engine/optimizer.ts +7 -7
  50. package/src/engine/params-runtime.ts +3 -3
  51. package/src/engine/physics.ts +33 -23
  52. package/src/engine/sandbox-browser.ts +8 -7
  53. package/src/engine/sandbox-compile.ts +1 -1
  54. package/src/engine/sandbox-harness.ts +299 -367
  55. package/src/engine/sandbox.ts +3 -3
  56. package/src/engine/simulation.ts +291 -76
  57. package/src/engine/spells.ts +9 -12
  58. package/src/engine-version.ts +1 -1
  59. package/src/hooks/action-builders.ts +76 -21
  60. package/src/hooks/index.ts +17 -9
  61. package/src/hooks/state-hooks.ts +61 -25
  62. package/src/hooks/threat-analysis.ts +44 -20
  63. package/src/hooks/types.ts +62 -5
  64. package/src/index-browser.ts +1 -0
  65. package/src/index.ts +3 -0
  66. package/src/rules.ts +209 -63
  67. package/src/stats.ts +2 -2
  68. package/src/testing.ts +6 -30
  69. package/src/trace.ts +63 -3
  70. package/src/types.ts +127 -14
  71. package/src/utils/angles.ts +1 -0
  72. package/src/utils/combat.ts +98 -6
  73. package/src/utils/spatial.ts +3 -3
  74. package/dist/chunk-74FFJCFF.js +0 -9140
  75. package/dist/chunk-74FFJCFF.js.map +0 -1
  76. package/src/hooks/bot-wrapper.ts +0 -84
@@ -1,12 +1,7 @@
1
1
  import {WizardState, MissileConfig, ProjectileState} from '../types.js';
2
2
  import {
3
- SHIELD_MAX_BLOCK,
4
- SHIELD_DECAY_PER_SECOND,
5
- SHIELD_MIN_BLOCK,
3
+ RULES,
6
4
  TICKS_PER_SECOND,
7
- GCD_DURATION,
8
- SHIELD_CAST_TIME,
9
- BLINK_CAST_TIME,
10
5
  calculateMissileCastTime,
11
6
  calculateWarmupMultiplier,
12
7
  } from '../rules.js';
@@ -21,6 +16,7 @@ export function startCast(wizard: WizardState, spell: 'missile' | 'shield' | 'bl
21
16
  wizard.castingSpell = spell;
22
17
  wizard.castProgress = 0;
23
18
 
19
+ // Stryker disable next-line ConditionalExpression: `true && config` equivalent for valid calls (spell is always correct type)
24
20
  if (spell === 'missile' && config)
25
21
  {
26
22
  // calculateMissileCastTime includes warmup when lastMissileConfig is passed
@@ -31,11 +27,12 @@ export function startCast(wizard: WizardState, spell: 'missile' | 'shield' | 'bl
31
27
  }
32
28
  else if (spell === 'shield')
33
29
  {
34
- wizard.castDuration = SHIELD_CAST_TIME;
30
+ wizard.castDuration = RULES.SHIELD_CAST_TIME;
35
31
  }
36
- else if (spell === 'blink')
32
+ else
37
33
  {
38
- wizard.castDuration = BLINK_CAST_TIME;
34
+ // Blink is the only remaining case after missile and shield
35
+ wizard.castDuration = RULES.BLINK_CAST_TIME;
39
36
  }
40
37
  }
41
38
 
@@ -66,7 +63,7 @@ export function completeCast(wizard: WizardState): void
66
63
  else if (spell === 'missile')
67
64
  {
68
65
  wizard.state = 'gcd_locked';
69
- wizard.gcdRemaining = GCD_DURATION;
66
+ wizard.gcdRemaining = RULES.GCD_DURATION;
70
67
  }
71
68
  else
72
69
  {
@@ -96,8 +93,8 @@ export function updateShield(wizard: WizardState, deltaTicks: number): void
96
93
  export function calculateShieldBlock(channelDurationTicks: number): number
97
94
  {
98
95
  const channelSeconds = channelDurationTicks / TICKS_PER_SECOND;
99
- const decay = channelSeconds * SHIELD_DECAY_PER_SECOND;
100
- return Math.max(SHIELD_MIN_BLOCK, SHIELD_MAX_BLOCK - decay);
96
+ const decay = channelSeconds * RULES.SHIELD_DECAY_PER_SECOND;
97
+ return Math.max(RULES.SHIELD_MIN_BLOCK, RULES.SHIELD_MAX_BLOCK - decay);
101
98
  }
102
99
 
103
100
  /**
@@ -8,4 +8,4 @@
8
8
  * Used to gate spectator replays: a recorded match can only be re-simulated when
9
9
  * the runtime engine version matches the version that produced the match.
10
10
  */
11
- export const ENGINE_VERSION = 1777687052301;
11
+ export const ENGINE_VERSION = 1782047842068;
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * UNITS REFERENCE (100 ticks = 1 second):
7
7
  * Position: absolute world coordinates, 0-800 on each axis (800×800 arena)
8
- * Movement: direction vector, magnitude auto-normalized to max 100
8
+ * Movement: direction vector, magnitude clamped to max 1 (speed in [0, 1])
9
9
  * Speed: units per tick (player moves at 1 unit/tick = 100 units/sec)
10
10
  * Duration: ticks (divide by 100 for seconds)
11
11
  * Angles: degrees (0°=right, 90°=down, 180°=left, 270°=up)
@@ -13,8 +13,8 @@
13
13
  * Turn rate: degrees per tick the missile can rotate
14
14
  */
15
15
 
16
- import {WizardActions, MissileConfig, MissileAIFunction} from '../types.js';
17
- import type {FinalAction, ActionBuilder} from './types.js';
16
+ import {WizardActions, MissileConfig, MissileActions} from '../types.js';
17
+ import type {FinalAction, ActionBuilder, MissileAction, MissileFunction} from './types.js';
18
18
 
19
19
  /**
20
20
  * Create a FinalAction (no further chaining possible).
@@ -63,7 +63,7 @@ function createActionBuilder(baseAction: Omit<WizardActions, 'move'>): ActionBui
63
63
  *
64
64
  * @example
65
65
  * return shield(); // shield and stay still
66
- * return shield().move(100, 0); // move right while cast starts
66
+ * return shield().move(1, 0); // move right while cast starts
67
67
  */
68
68
  export function shield(): ActionBuilder
69
69
  {
@@ -90,27 +90,30 @@ export function shield(): ActionBuilder
90
90
  * - duration: ticks the missile lives (min 10). Range ≈ speed × duration.
91
91
  * - turnRate: degrees/tick of homing (0 = straight line, 3 = moderate homing, 5+ = strong).
92
92
  * Negative = no homing + minor speed cost reduction.
93
- * @param ai - Called every tick to control missile steering. Receives:
94
- * - missileState: the missile's position, rotation (degrees), speed, remainingTicks
95
- * - worldState: full game state (all wizards, projectiles)
96
- * - random(): seeded PRNG [0, 1)
97
- * Return { turnToward: {x, y} } to home toward a position, or {} to fly straight.
93
+ * @param ai - Called every tick to control missile steering. Use getMissileContext()
94
+ * to read the missile's state (position, rotation, speed, etc.), the full game
95
+ * state (worldState), and a seeded PRNG (random). Return turnToward(x, y) to home
96
+ * toward a position, or flyStraight() to fly straight.
98
97
  * @param direction - Launch angle in degrees (0°=right, 90°=down, 180°=left, 270°=up).
99
98
  * Tip: use Math.atan2(dy, dx) * (180 / Math.PI) to aim at a target.
100
99
  *
101
100
  * @example
102
101
  * // Straight missile aimed at enemy
103
102
  * const angle = Math.atan2(dy, dx) * (180 / Math.PI);
104
- * return missile({damage: 15, speed: 6, duration: 200, turnRate: 0}, () => ({}), angle);
103
+ * return missile({damage: 15, speed: 6, duration: 200, turnRate: 0}, () => flyStraight(), angle);
105
104
  *
106
105
  * // Homing missile that tracks enemy
107
106
  * return missile(
108
107
  * {damage: 10, speed: 5, duration: 300, turnRate: 3},
109
- * ({worldState}) => ({turnToward: worldState.enemies[0]?.position}),
108
+ * () => {
109
+ * const ctx = getMissileContext();
110
+ * const enemy = ctx.worldState.enemies[0];
111
+ * return enemy ? turnToward(enemy.position.x, enemy.position.y) : flyStraight();
112
+ * },
110
113
  * angle,
111
114
  * );
112
115
  */
113
- export function missile(config: MissileConfig, ai: MissileAIFunction, direction: number): ActionBuilder
116
+ export function missile(config: MissileConfig, ai: MissileFunction, direction: number): ActionBuilder
114
117
  {
115
118
  return createActionBuilder({
116
119
  startCast: {
@@ -157,7 +160,7 @@ export function blink(x: number, y: number): FinalAction
157
160
  * Can chain .move() for simultaneous movement.
158
161
  *
159
162
  * @example
160
- * return cancel().move(-100, 0); // cancel and dodge left
163
+ * return cancel().move(-1, 0); // cancel and dodge left
161
164
  */
162
165
  export function cancel(): ActionBuilder
163
166
  {
@@ -169,19 +172,17 @@ export function cancel(): ActionBuilder
169
172
  /**
170
173
  * Move in a direction without casting any spell.
171
174
  *
172
- * This is a **direction vector**, not a target position. The engine normalizes
173
- * the magnitude to max 100, then moves at 1 unit/tick (100 units/sec).
174
- * Positive X = right, positive Y = down.
175
- *
176
- * To move toward a target position, subtract your position:
177
- * move(target.x - myPos.x, target.y - myPos.y)
175
+ * This is a **direction vector**, not a target position. Values are in the
176
+ * range [-1, 1] where 1 = full speed. Larger values (like raw position
177
+ * deltas) are clamped to full speed automatically.
178
178
  *
179
179
  * @param x - Horizontal direction (positive = right, negative = left)
180
180
  * @param y - Vertical direction (positive = down, negative = up)
181
181
  *
182
182
  * @example
183
- * return move(100, 0); // move right
184
- * return move(enemy.position.x - myPos.x, enemy.position.y - myPos.y); // move toward enemy
183
+ * return move(1, 0); // move right at full speed
184
+ * return move(dx / dist, dy / dist); // normalized unit vector = full speed
185
+ * return move(enemy.position.x - myPos.x, enemy.position.y - myPos.y); // raw delta = clamped to full speed
185
186
  */
186
187
  export function move(x: number, y: number): FinalAction
187
188
  {
@@ -200,6 +201,60 @@ export function idle(): FinalAction
200
201
  });
201
202
  }
202
203
 
204
+ // ============================================================
205
+ // MISSILE ACTION BUILDERS
206
+ // ============================================================
207
+
208
+ /**
209
+ * Steer the missile toward a world position.
210
+ * The engine clamps the actual turn to the missile's turnRate.
211
+ *
212
+ * @param x - Target X position (world coordinates)
213
+ * @param y - Target Y position (world coordinates)
214
+ */
215
+ export function turnToward(x: number, y: number): MissileAction
216
+ {
217
+ return {
218
+ _toMissileAction: () => ({turnToward: {x, y}}),
219
+ };
220
+ }
221
+
222
+ /**
223
+ * Steer the missile toward a specific angle (degrees).
224
+ * The engine clamps the actual turn to the missile's turnRate.
225
+ *
226
+ * @param degrees - Target rotation in degrees (0=right, 90=down, 180=left, 270=up)
227
+ */
228
+ export function turnToAngle(degrees: number): MissileAction
229
+ {
230
+ return {
231
+ _toMissileAction: () => ({turnToAngle: degrees}),
232
+ };
233
+ }
234
+
235
+ /**
236
+ * Fly straight — no steering this tick.
237
+ */
238
+ export function flyStraight(): MissileAction
239
+ {
240
+ return {
241
+ _toMissileAction: () => ({}),
242
+ };
243
+ }
244
+
245
+ /**
246
+ * Extract MissileActions from a MissileAction.
247
+ * Used by the engine to get the actual missile action.
248
+ */
249
+ export function extractMissileAction(action: MissileAction): MissileActions
250
+ {
251
+ return action._toMissileAction();
252
+ }
253
+
254
+ // ============================================================
255
+ // WIZARD ACTION EXTRACTION
256
+ // ============================================================
257
+
203
258
  /**
204
259
  * Extract WizardActions from a FinalAction.
205
260
  * Used by the engine to get the actual action.
@@ -10,14 +10,17 @@ export type {
10
10
  AnalyzedThreat,
11
11
  FinalAction,
12
12
  ActionBuilder,
13
- BotFunction,
14
- BotContext,
13
+ WizardFunction,
14
+ WizardContext,
15
15
  MissileConfig,
16
- MissileAIFunction,
16
+ MissileContext,
17
+ MissileAction,
18
+ MissileFunction,
17
19
  } from './types.js';
18
20
 
19
21
  // State hooks
20
22
  export {
23
+ useRandom,
21
24
  useHealth,
22
25
  usePosition,
23
26
  useVelocity,
@@ -28,7 +31,9 @@ export {
28
31
  useCastingSpell,
29
32
  useCastProgress,
30
33
  useEnemy,
34
+ useProjectiles,
31
35
  useMyProjectiles,
36
+ useLastMissileConfig,
32
37
  useDamageDealt,
33
38
  useDamageTaken,
34
39
  useLastHitTick,
@@ -51,6 +56,10 @@ export {
51
56
  move,
52
57
  idle,
53
58
  extractAction,
59
+ turnToward,
60
+ turnToAngle,
61
+ flyStraight,
62
+ extractMissileAction,
54
63
  } from './action-builders.js';
55
64
 
56
65
  // Re-export persistence hooks from runtime
@@ -59,17 +68,16 @@ export {
59
68
  useEffect,
60
69
  useMemo,
61
70
  useRef,
62
- runBotWithContext,
63
- withBotContext,
64
- getBotContext,
71
+ runWizardWithContext,
72
+ withWizardContext,
73
+ withMissileContext,
74
+ getWizardContext,
75
+ getMissileContext,
65
76
  } from '../engine/hooks-runtime.js';
66
77
 
67
78
  // Re-export RefObject type
68
79
  export type {RefObject} from '../engine/hooks-runtime.js';
69
80
 
70
- // Bot wrapper for integration
71
- export {wrapNewBot, isNewStyleBot} from './bot-wrapper.js';
72
-
73
81
  // Parameter tuning hook
74
82
  export {useParam} from '../engine/params-runtime.js';
75
83
  export type {ParamDeclaration} from '../engine/params-runtime.js';
@@ -13,8 +13,8 @@
13
13
  */
14
14
 
15
15
  import {Position, Velocity, ProjectileState} from '../types.js';
16
- import {getBotContext, useMemo} from '../engine/hooks-runtime.js';
17
- import {SHIELD_MAX_BLOCK, SHIELD_DECAY_PER_SECOND, SHIELD_MIN_BLOCK, TICKS_PER_SECOND} from '../rules.js';
16
+ import {getWizardContext, useMemo} from '../engine/hooks-runtime.js';
17
+ import {RULES, TICKS_PER_SECOND} from '../rules.js';
18
18
  import type {EnemyState, AnalyzedThreat} from './types.js';
19
19
  import {analyzeThreats} from './threat-analysis.js';
20
20
 
@@ -22,12 +22,22 @@ import {analyzeThreats} from './threat-analysis.js';
22
22
  // SELF STATE HOOKS
23
23
  // ============================================================
24
24
 
25
+ /**
26
+ * Get a seeded random number generator. Returns a function that produces
27
+ * deterministic values in [0, 1) — same seed + same tick = same sequence.
28
+ * Use this instead of Math.random() so replays are deterministic.
29
+ */
30
+ export function useRandom(): () => number
31
+ {
32
+ return getWizardContext().random;
33
+ }
34
+
25
35
  /**
26
36
  * Get your current health (0-60). Wizard dies at 0.
27
37
  */
28
38
  export function useHealth(): number
29
39
  {
30
- return getBotContext().health;
40
+ return getWizardContext().health;
31
41
  }
32
42
 
33
43
  /**
@@ -36,7 +46,7 @@ export function useHealth(): number
36
46
  */
37
47
  export function usePosition(): Position
38
48
  {
39
- const ctx = getBotContext();
49
+ const ctx = getWizardContext();
40
50
  return {x: ctx.position.x, y: ctx.position.y};
41
51
  }
42
52
 
@@ -46,7 +56,7 @@ export function usePosition(): Position
46
56
  */
47
57
  export function useVelocity(): Velocity
48
58
  {
49
- const ctx = getBotContext();
59
+ const ctx = getWizardContext();
50
60
  return {x: ctx.velocity.x, y: ctx.velocity.y};
51
61
  }
52
62
 
@@ -59,7 +69,7 @@ export function useVelocity(): Velocity
59
69
  */
60
70
  export function useStatus(): 'idle' | 'casting' | 'channeling' | 'gcd_locked'
61
71
  {
62
- return getBotContext().state;
72
+ return getWizardContext().state;
63
73
  }
64
74
 
65
75
  /**
@@ -72,7 +82,7 @@ export function useStatus(): 'idle' | 'casting' | 'channeling' | 'gcd_locked'
72
82
  */
73
83
  export function useTicksUntilReady(): number
74
84
  {
75
- const ctx = getBotContext();
85
+ const ctx = getWizardContext();
76
86
 
77
87
  switch (ctx.state)
78
88
  {
@@ -105,7 +115,7 @@ export function useTicksUntilReady(): number
105
115
  */
106
116
  export function useShieldStrength(): number
107
117
  {
108
- const ctx = getBotContext();
118
+ const ctx = getWizardContext();
109
119
 
110
120
  if (ctx.state !== 'channeling' || ctx.channelingSpell !== 'shield')
111
121
  {
@@ -114,9 +124,27 @@ export function useShieldStrength(): number
114
124
 
115
125
  // Calculate shield strength based on channel duration
116
126
  const channelDuration = ctx.channelDuration ?? 0;
117
- const decayPerTick = SHIELD_DECAY_PER_SECOND / TICKS_PER_SECOND;
118
- const strength = SHIELD_MAX_BLOCK - (channelDuration * decayPerTick);
119
- return Math.max(SHIELD_MIN_BLOCK, strength);
127
+ const decayPerTick = RULES.SHIELD_DECAY_PER_SECOND / TICKS_PER_SECOND;
128
+ const strength = RULES.SHIELD_MAX_BLOCK - (channelDuration * decayPerTick);
129
+ return Math.max(RULES.SHIELD_MIN_BLOCK, strength);
130
+ }
131
+
132
+ /**
133
+ * Get all projectiles currently in flight (yours and enemy's).
134
+ * Used for blink safety calculations and threat analysis.
135
+ */
136
+ export function useProjectiles(): ProjectileState[]
137
+ {
138
+ return getWizardContext().projectiles;
139
+ }
140
+
141
+ /**
142
+ * Get the config of the last missile you fired, or undefined if none fired yet.
143
+ * Used for the warmup system: consecutive similar missiles cast faster.
144
+ */
145
+ export function useLastMissileConfig(): import('../types.js').MissileConfig | undefined
146
+ {
147
+ return getWizardContext().lastMissileConfig;
120
148
  }
121
149
 
122
150
  /**
@@ -130,7 +158,7 @@ export function useShieldStrength(): number
130
158
  */
131
159
  export function useBlinkCooldown(): number
132
160
  {
133
- return getBotContext().blinkCooldown;
161
+ return getWizardContext().blinkCooldown;
134
162
  }
135
163
 
136
164
  /**
@@ -139,7 +167,7 @@ export function useBlinkCooldown(): number
139
167
  */
140
168
  export function useCastingSpell(): 'missile' | 'shield' | 'blink' | null
141
169
  {
142
- const ctx = getBotContext();
170
+ const ctx = getWizardContext();
143
171
  if (ctx.state !== 'casting')
144
172
  {
145
173
  return null;
@@ -155,7 +183,7 @@ export function useCastingSpell(): 'missile' | 'shield' | 'blink' | null
155
183
  */
156
184
  export function useCastProgress(): {current: number; total: number} | null
157
185
  {
158
- const ctx = getBotContext();
186
+ const ctx = getWizardContext();
159
187
  if (ctx.state !== 'casting')
160
188
  {
161
189
  return null;
@@ -180,7 +208,7 @@ export function useCastProgress(): {current: number; total: number} | null
180
208
  */
181
209
  export function useEnemy(): EnemyState
182
210
  {
183
- const ctx = getBotContext();
211
+ const ctx = getWizardContext();
184
212
  const enemy = ctx.enemies[0];
185
213
 
186
214
  if (!enemy)
@@ -192,6 +220,10 @@ export function useEnemy(): EnemyState
192
220
  health: 0,
193
221
  status: 'idle',
194
222
  castingSpell: null,
223
+ castProgress: 0,
224
+ castDuration: 0,
225
+ gcdRemaining: 0,
226
+ channelDuration: 0,
195
227
  shieldStrength: 0,
196
228
  };
197
229
  }
@@ -201,8 +233,8 @@ export function useEnemy(): EnemyState
201
233
  if (enemy.state === 'channeling' && enemy.channelingSpell === 'shield')
202
234
  {
203
235
  const channelDuration = enemy.channelDuration ?? 0;
204
- const decayPerTick = SHIELD_DECAY_PER_SECOND / TICKS_PER_SECOND;
205
- shieldStrength = Math.max(SHIELD_MIN_BLOCK, SHIELD_MAX_BLOCK - (channelDuration * decayPerTick));
236
+ const decayPerTick = RULES.SHIELD_DECAY_PER_SECOND / TICKS_PER_SECOND;
237
+ shieldStrength = Math.max(RULES.SHIELD_MIN_BLOCK, RULES.SHIELD_MAX_BLOCK - (channelDuration * decayPerTick));
206
238
  }
207
239
 
208
240
  return {
@@ -211,6 +243,10 @@ export function useEnemy(): EnemyState
211
243
  health: enemy.health,
212
244
  status: enemy.state,
213
245
  castingSpell: enemy.state === 'casting' ? (enemy.castingSpell ?? null) : null,
246
+ castProgress: enemy.castProgress ?? 0,
247
+ castDuration: enemy.castDuration ?? 0,
248
+ gcdRemaining: enemy.gcdRemaining ?? 0,
249
+ channelDuration: enemy.channelDuration ?? 0,
214
250
  shieldStrength,
215
251
  };
216
252
  }
@@ -225,7 +261,7 @@ export function useEnemy(): EnemyState
225
261
  */
226
262
  export function useMyProjectiles(): ProjectileState[]
227
263
  {
228
- return getBotContext().myProjectiles;
264
+ return getWizardContext().myProjectiles;
229
265
  }
230
266
 
231
267
  // ============================================================
@@ -237,7 +273,7 @@ export function useMyProjectiles(): ProjectileState[]
237
273
  */
238
274
  export function useDamageDealt(): number
239
275
  {
240
- return getBotContext().damageDealt;
276
+ return getWizardContext().damageDealt;
241
277
  }
242
278
 
243
279
  /**
@@ -245,7 +281,7 @@ export function useDamageDealt(): number
245
281
  */
246
282
  export function useDamageTaken(): number
247
283
  {
248
- return getBotContext().damageTaken;
284
+ return getWizardContext().damageTaken;
249
285
  }
250
286
 
251
287
  /**
@@ -254,7 +290,7 @@ export function useDamageTaken(): number
254
290
  */
255
291
  export function useLastHitTick(): number
256
292
  {
257
- return getBotContext().lastHitTick;
293
+ return getWizardContext().lastHitTick;
258
294
  }
259
295
 
260
296
  // ============================================================
@@ -267,7 +303,7 @@ export function useLastHitTick(): number
267
303
  */
268
304
  export function useArenaSize(): {width: number; height: number}
269
305
  {
270
- const ctx = getBotContext();
306
+ const ctx = getWizardContext();
271
307
  return {width: ctx.arenaWidth, height: ctx.arenaHeight};
272
308
  }
273
309
 
@@ -277,7 +313,7 @@ export function useArenaSize(): {width: number; height: number}
277
313
  */
278
314
  export function useTick(): number
279
315
  {
280
- return getBotContext().tick;
316
+ return getWizardContext().tick;
281
317
  }
282
318
 
283
319
  // ============================================================
@@ -300,7 +336,7 @@ export function useTick(): number
300
336
  */
301
337
  export function useThreats(): AnalyzedThreat[]
302
338
  {
303
- const ctx = getBotContext();
339
+ const ctx = getWizardContext();
304
340
  const ticksUntilReady = useTicksUntilReady();
305
341
 
306
342
  // Memoize to avoid recalculating every call
@@ -331,7 +367,7 @@ export function useClosestThreat(): AnalyzedThreat | null
331
367
  */
332
368
  export function useMyThreatsToEnemy(): AnalyzedThreat[]
333
369
  {
334
- const ctx = getBotContext();
370
+ const ctx = getWizardContext();
335
371
  const enemy = ctx.enemies[0];
336
372
 
337
373
  if (!enemy)