@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,15 +1,17 @@
1
- import {WizardFunction, MissileAIFunction} from '../../types.js';
1
+ import type {FinalAction, MissileFunction} from '../../hooks/index.js';
2
+ import {usePosition, useEnemy, useStatus, useBlinkCooldown, useHealth, useTick, useThreats, useProjectiles, useCastingSpell, useCastProgress, idle, move as moveAction, missile, shield, blink, cancel, getMissileContext, turnToward, flyStraight} from '../../hooks/index.js';
2
3
  import {angleTo} from '../../utils/angles.js';
3
4
  import {distanceTo} from '../../utils/distance.js';
4
5
  import {getMissileCastTime} from '../../utils/combat.js';
5
- import {ARENA_SIZE, GCD_DURATION, SHIELD_CAST_TIME} from '../../rules.js';
6
+ import {RULES, ARENA_MIN, ARENA_MAX} from '../../rules.js';
6
7
  import {
7
- getThreats,
8
8
  getMostUrgentThreat,
9
9
  getDodgeDirectionForMovement,
10
10
  isSafeToAttack,
11
11
  getBlinkToDecreaseDistance,
12
12
  shouldForceShield,
13
+ keepOffWalls,
14
+ getPreCastReposition,
13
15
  } from '../shared.js';
14
16
  import {useParam} from '../../engine/params-runtime.js';
15
17
  const WALL_BUFFER = 60;
@@ -23,12 +25,11 @@ const QUICK_RANGE = QUICK_CONFIG.speed * QUICK_CONFIG.duration; // 350
23
25
  /**
24
26
  * Standard homing missile AI.
25
27
  */
26
- const StandardHoming: MissileAIFunction = ({worldState}) =>
28
+ const StandardHoming: MissileFunction = () =>
27
29
  {
28
- const enemy = worldState.enemies[0];
29
- return {
30
- turnToward: enemy?.position,
31
- };
30
+ const ctx = getMissileContext();
31
+ const enemy = ctx.worldState.enemies[0];
32
+ return enemy ? turnToward(enemy.position.x, enemy.position.y) : flyStraight();
32
33
  };
33
34
 
34
35
  /**
@@ -37,10 +38,10 @@ const StandardHoming: MissileAIFunction = ({worldState}) =>
37
38
  function isNearWall(position: {x: number; y: number}): boolean
38
39
  {
39
40
  return (
40
- position.x < WALL_BUFFER ||
41
- position.x > ARENA_SIZE - WALL_BUFFER ||
42
- position.y < WALL_BUFFER ||
43
- position.y > ARENA_SIZE - WALL_BUFFER
41
+ position.x < ARENA_MIN + WALL_BUFFER ||
42
+ position.x > ARENA_MAX - WALL_BUFFER ||
43
+ position.y < ARENA_MIN + WALL_BUFFER ||
44
+ position.y > ARENA_MAX - WALL_BUFFER
44
45
  );
45
46
  }
46
47
 
@@ -58,12 +59,22 @@ function combatMove(
58
59
  strafeIntensity: number,
59
60
  approachSpeed: number,
60
61
  distanceDeadZone: number,
61
- ): {x: number; y: number}
62
+ centerBias: number,
63
+ ): {x: number; y: number}
62
64
  {
63
65
  const deltaX = enemy.position.x - position.x;
64
66
  const deltaY = enemy.position.y - position.y;
65
67
  const normalizedDistance = currentDistance > 0 ? currentDistance : 1;
66
68
 
69
+ // Approach the center-side of the enemy so knockback pushes toward center, not lava.
70
+ const center = (ARENA_MIN + ARENA_MAX) / 2;
71
+ const toCenterX = center - enemy.position.x;
72
+ const toCenterY = center - enemy.position.y;
73
+ const toCenterDist = Math.sqrt(toCenterX * toCenterX + toCenterY * toCenterY) || 1;
74
+ const approachDirX = deltaX / normalizedDistance + (toCenterX / toCenterDist) * centerBias;
75
+ const approachDirY = deltaY / normalizedDistance + (toCenterY / toCenterDist) * centerBias;
76
+ const approachMag = Math.sqrt(approachDirX * approachDirX + approachDirY * approachDirY) || 1;
77
+
67
78
  // Perpendicular directions
68
79
  const strafeClockwise = {
69
80
  x: -deltaY / normalizedDistance,
@@ -93,16 +104,16 @@ function combatMove(
93
104
  y: position.y + strafeCounterClockwise.y * 100,
94
105
  };
95
106
  const scoreClockwise = Math.min(
96
- futureClockwise.x,
97
- ARENA_SIZE - futureClockwise.x,
98
- futureClockwise.y,
99
- ARENA_SIZE - futureClockwise.y,
107
+ futureClockwise.x - ARENA_MIN,
108
+ ARENA_MAX - futureClockwise.x,
109
+ futureClockwise.y - ARENA_MIN,
110
+ ARENA_MAX - futureClockwise.y,
100
111
  );
101
112
  const scoreCounterClockwise = Math.min(
102
- futureCounterClockwise.x,
103
- ARENA_SIZE - futureCounterClockwise.x,
104
- futureCounterClockwise.y,
105
- ARENA_SIZE - futureCounterClockwise.y,
113
+ futureCounterClockwise.x - ARENA_MIN,
114
+ ARENA_MAX - futureCounterClockwise.x,
115
+ futureCounterClockwise.y - ARENA_MIN,
116
+ ARENA_MAX - futureCounterClockwise.y,
106
117
  );
107
118
  strafeDirection =
108
119
  scoreClockwise > scoreCounterClockwise
@@ -119,36 +130,36 @@ function combatMove(
119
130
  // When actively dodging, commit fully — no approach/retreat dilution
120
131
  if (dodgeDirection)
121
132
  {
122
- return {x: strafeDirection.x * 100, y: strafeDirection.y * 100};
133
+ return {x: strafeDirection.x, y: strafeDirection.y};
123
134
  }
124
135
 
125
136
  let moveX = strafeDirection.x * strafeIntensity;
126
137
  let moveY = strafeDirection.y * strafeIntensity;
127
138
 
128
139
  // Distance management (only when not dodging)
129
- if (currentDistance > targetDistance + distanceDeadZone)
140
+ if (currentDistance > targetDistance + distanceDeadZone)
130
141
  {
131
- moveX += (deltaX / normalizedDistance) * approachSpeed;
132
- moveY += (deltaY / normalizedDistance) * approachSpeed;
142
+ moveX += (approachDirX / approachMag) * approachSpeed;
143
+ moveY += (approachDirY / approachMag) * approachSpeed;
133
144
  }
134
145
  else if (currentDistance < targetDistance - distanceDeadZone)
135
146
  {
136
147
  let retreatX = -(deltaX / normalizedDistance) * approachSpeed;
137
148
  let retreatY = -(deltaY / normalizedDistance) * approachSpeed;
138
- if (position.x < WALL_BUFFER && retreatX < 0) retreatX = 0;
139
- if (position.x > ARENA_SIZE - WALL_BUFFER && retreatX > 0) retreatX = 0;
140
- if (position.y < WALL_BUFFER && retreatY < 0) retreatY = 0;
141
- if (position.y > ARENA_SIZE - WALL_BUFFER && retreatY > 0) retreatY = 0;
149
+ if (position.x < ARENA_MIN + WALL_BUFFER && retreatX < 0) retreatX = 0;
150
+ if (position.x > ARENA_MAX - WALL_BUFFER && retreatX > 0) retreatX = 0;
151
+ if (position.y < ARENA_MIN + WALL_BUFFER && retreatY < 0) retreatY = 0;
152
+ if (position.y > ARENA_MAX - WALL_BUFFER && retreatY > 0) retreatY = 0;
142
153
  moveX += retreatX;
143
154
  moveY += retreatY;
144
155
  }
145
156
 
146
157
  // Normalize
147
158
  const magnitude = Math.sqrt(moveX * moveX + moveY * moveY);
148
- if (magnitude > 100)
159
+ if (magnitude > 1)
149
160
  {
150
- moveX = (moveX / magnitude) * 100;
151
- moveY = (moveY / magnitude) * 100;
161
+ moveX = moveX / magnitude;
162
+ moveY = moveY / magnitude;
152
163
  }
153
164
 
154
165
  return {x: moveX, y: moveY};
@@ -171,30 +182,42 @@ function combatMove(
171
182
  *
172
183
  * TIER: 1 (base)
173
184
  */
174
- export const Battlemage: WizardFunction = ({state}) =>
185
+ export function Battlemage(): FinalAction
175
186
  {
176
- const targetDistance = useParam('targetDistance', 274, {
187
+ const position = usePosition();
188
+ const enemy = useEnemy();
189
+ const status = useStatus();
190
+ const blinkCooldown = useBlinkCooldown();
191
+ const health = useHealth();
192
+ const tick = useTick();
193
+ const threats = useThreats();
194
+ const projectiles = useProjectiles();
195
+ const castingSpell = useCastingSpell();
196
+ const castProgressData = useCastProgress();
197
+
198
+
199
+ const targetDistance = useParam('targetDistance', 392, {
177
200
  range: 150,
178
201
  min: 150,
179
202
  });
180
- const strafeCyclePeriod = useParam('strafeCyclePeriod', 219, {
203
+ const strafeCyclePeriod = useParam('strafeCyclePeriod', 240, {
181
204
  range: 75,
182
205
  min: 80,
183
206
  max: 300,
184
207
  });
185
- const strafeIntensity = useParam('strafeIntensity', 36, {
186
- range: 40,
187
- min: 20,
188
- max: 100,
208
+ const strafeIntensity = useParam('strafeIntensity', 0.87, {
209
+ range: 0.4,
210
+ min: 0.2,
211
+ max: 1,
189
212
  steps: 7,
190
213
  });
191
- const approachSpeed = useParam('approachSpeed', 80, {
192
- range: 30,
193
- min: 10,
194
- max: 80,
214
+ const approachSpeed = useParam('approachSpeed', 0.8, {
215
+ range: 0.3,
216
+ min: 0.1,
217
+ max: 0.8,
195
218
  steps: 7,
196
219
  });
197
- const distanceDeadZone = useParam('distanceDeadZone', 31, {
220
+ const distanceDeadZone = useParam('distanceDeadZone', 64, {
198
221
  range: 30,
199
222
  min: 10,
200
223
  max: 80,
@@ -205,7 +228,7 @@ export const Battlemage: WizardFunction = ({state}) =>
205
228
  min: 50,
206
229
  max: 300,
207
230
  });
208
- const blinkWindowMax = useParam('blinkWindowMax', 35, {
231
+ const blinkWindowMax = useParam('blinkWindowMax', 15, {
209
232
  range: 30,
210
233
  min: 15,
211
234
  max: 80,
@@ -217,81 +240,98 @@ export const Battlemage: WizardFunction = ({state}) =>
217
240
  max: 100,
218
241
  steps: 7,
219
242
  });
220
- const cancelMinRemainingCast = useParam('cancelMinRemainingCast', 19, {
243
+ const cancelMinRemainingCast = useParam('cancelMinRemainingCast', 36, {
221
244
  range: 30,
222
245
  min: 5,
223
246
  max: 100,
224
247
  steps: 7,
225
248
  });
226
- const interruptRange = useParam('interruptRange', 324, {
249
+ const interruptRange = useParam('interruptRange', 150, {
227
250
  range: 150,
228
251
  min: 150,
229
252
  max: 600,
230
253
  steps: 7,
231
254
  });
232
- const attackRange = useParam('attackRange', 556, {
255
+ const attackRange = useParam('attackRange', 662, {
233
256
  range: 150,
234
257
  min: 200,
235
258
  max: 700,
236
259
  });
260
+ const centerBias = useParam('centerBias', 0.04, {
261
+ range: 0.4,
262
+ min: 0,
263
+ max: 0.8,
264
+ steps: 5,
265
+ });
266
+ const maxBlinkDistance = useParam('maxBlinkDistance', 216, {
267
+ range: 100,
268
+ min: 50,
269
+ max: 300,
270
+ steps: 7,
271
+ });
272
+
273
+ const distance = distanceTo(position, enemy.position);
237
274
 
238
- const enemy = state.enemies[0];
239
- if (!enemy)
275
+ function capBlinkDistance(target: {x: number; y: number}): {x: number; y: number}
240
276
  {
241
- return {move: {x: 0, y: 0}};
277
+ const dx = target.x - position.x;
278
+ const dy = target.y - position.y;
279
+ const dist = Math.sqrt(dx * dx + dy * dy);
280
+ if (dist <= maxBlinkDistance) return target;
281
+ const scale = maxBlinkDistance / dist;
282
+ return {x: position.x + dx * scale, y: position.y + dy * scale};
242
283
  }
243
284
 
244
- const distance = distanceTo(state.position, enemy.position);
245
-
246
285
  // Analyze threats using simulation
247
- const threats = getThreats(state);
248
286
  const urgentThreat = getMostUrgentThreat(threats);
249
287
 
250
288
  // === DEFENSE: Handle channeling ===
251
- if (state.state === 'channeling')
289
+ if (status === 'channeling')
252
290
  {
253
291
  // Cancel shield if no imminent threats
254
292
  if (!urgentThreat || urgentThreat.ticksToImpact > shieldCancelThreshold)
255
293
  {
256
- return {move: {x: 0, y: 0}, cancel: true};
294
+ return cancel();
257
295
  }
258
- return {move: {x: 0, y: 0}};
296
+ return idle();
259
297
  }
260
298
 
261
299
  // === DEFENSE: Cancel long missile casts if undodgeable threat approaches ===
262
300
  // Only cancel when health is too low to trade (aggressive casts accepted the risk).
263
301
  if (
264
- state.state === 'casting' &&
265
- state.castingSpell === 'missile' &&
302
+ status === 'casting' &&
303
+ castingSpell === 'missile' &&
266
304
  urgentThreat &&
267
305
  !urgentThreat.bestDodgeDirection
268
306
  )
269
307
  {
270
- const remainingCast = (state.castDuration ?? 0) - (state.castProgress ?? 0);
308
+ const remainingCast = ((castProgressData?.total) ?? 0) - ((castProgressData?.current) ?? 0);
271
309
  if (
272
310
  remainingCast > cancelMinRemainingCast &&
273
311
  urgentThreat.ticksToImpact <=
274
- remainingCast + GCD_DURATION + SHIELD_CAST_TIME + 1 &&
275
- state.health <= urgentThreat.projectile.damage * 2 + 1
312
+ remainingCast + RULES.GCD_DURATION + RULES.SHIELD_CAST_TIME + 1 &&
313
+ health <= urgentThreat.projectile.damage * 2 + 1
276
314
  )
277
315
  {
278
316
  const dodgeDirection = getDodgeDirectionForMovement(
279
317
  threats,
280
- state.position,
318
+ position,
281
319
  );
282
- const move = combatMove(
283
- state.position,
320
+ const cancelMove = combatMove(
321
+ position,
284
322
  enemy,
285
323
  distance,
286
324
  targetDistance,
287
325
  dodgeDirection,
288
- state.tick,
326
+ tick,
289
327
  strafeCyclePeriod,
290
328
  strafeIntensity,
291
329
  approachSpeed,
292
330
  distanceDeadZone,
331
+ centerBias,
293
332
  );
294
- return {move, cancel: true};
333
+ const safeCancelMove = keepOffWalls(position, cancelMove);
334
+ return cancel().move(safeCancelMove.x, safeCancelMove.y);
295
335
  }
296
336
  }
297
337
 
@@ -299,29 +339,27 @@ export const Battlemage: WizardFunction = ({state}) =>
299
339
  // Blink both dodges the incoming missile AND closes distance — better than shielding.
300
340
  // Only shield when blink is on cooldown.
301
341
  if (
302
- state.state === 'idle' &&
342
+ status === 'idle' &&
303
343
  urgentThreat &&
304
- state.blinkCooldown === 0 &&
344
+ blinkCooldown === 0 &&
305
345
  distance > targetDistance - blinkDistanceOffset &&
306
346
  urgentThreat.ticksToImpact >= 10 &&
307
347
  urgentThreat.ticksToImpact <= blinkWindowMax
308
348
  )
309
349
  {
310
- const blinkTarget =
350
+ const rawBlinkTarget =
311
351
  getBlinkToDecreaseDistance(
312
- state.position,
352
+ position,
313
353
  enemy.position,
314
- state.projectiles,
354
+ projectiles,
315
355
  ) ?? enemy.position;
316
- return {
317
- move: {x: 0, y: 0},
318
- startCast: {spell: 'blink', target: blinkTarget},
319
- };
356
+ const blinkTarget = capBlinkDistance(rawBlinkTarget);
357
+ return blink(blinkTarget.x, blinkTarget.y);
320
358
  }
321
359
 
322
360
  // === DEFENSE: Shield undodgeable or high-damage homing threats (only when blink can't handle it) ===
323
361
  if (
324
- state.state === 'idle' &&
362
+ status === 'idle' &&
325
363
  urgentThreat &&
326
364
  (!urgentThreat.bestDodgeDirection || shouldForceShield(urgentThreat))
327
365
  )
@@ -329,49 +367,65 @@ export const Battlemage: WizardFunction = ({state}) =>
329
367
  if (
330
368
  urgentThreat.canBlockInTime &&
331
369
  urgentThreat.ticksToStartShield <= 3 &&
332
- (state.blinkCooldown > 0 || urgentThreat.ticksToImpact < 10)
370
+ (blinkCooldown > 0 || urgentThreat.ticksToImpact < 10)
333
371
  )
334
372
  {
335
- return {
336
- move: {x: 0, y: 0},
337
- startCast: {spell: 'shield'},
338
- };
373
+ return shield();
374
+ }
375
+ }
376
+
377
+ // === Preemptive shield — shield when enemy is casting a missile at close range ===
378
+ if (
379
+ status === 'idle' &&
380
+ distance < 300 &&
381
+ enemy.status === 'casting' &&
382
+ enemy.castingSpell === 'missile'
383
+ )
384
+ {
385
+ const remainingCast = (enemy.castDuration ?? 0) - (enemy.castProgress ?? 0);
386
+ const estimatedFlightTicks = distance / 8;
387
+ const ticksUntilHit = remainingCast + estimatedFlightTicks;
388
+
389
+ if (
390
+ ticksUntilHit >= RULES.SHIELD_CAST_TIME &&
391
+ ticksUntilHit < RULES.SHIELD_CAST_TIME + 15
392
+ )
393
+ {
394
+ return shield();
339
395
  }
340
396
  }
341
397
 
342
398
  // Movement: smart strafe + maintain optimal distance
343
- const dodgeDirection = getDodgeDirectionForMovement(threats, state.position);
399
+ const dodgeDirection = getDodgeDirectionForMovement(threats, position);
344
400
  const move = combatMove(
345
- state.position,
401
+ position,
346
402
  enemy,
347
403
  distance,
348
404
  targetDistance,
349
405
  dodgeDirection,
350
- state.tick,
406
+ tick,
351
407
  strafeCyclePeriod,
352
408
  strafeIntensity,
353
409
  approachSpeed,
354
410
  distanceDeadZone,
411
+ centerBias,
355
412
  );
356
-
413
+ const safeMove = keepOffWalls(position, move);
357
414
  // === OFFENSE ===
358
- if (state.state === 'idle')
415
+ if (status === 'idle')
359
416
  {
417
+ // Pre-cast wall discipline: don't commit to a cast we can't react out of while
418
+ // pinned near a wall — step toward center first so knockback can't reach lava.
419
+ const reposition = getPreCastReposition(position, enemy.position);
420
+ if (reposition) return moveAction(reposition.x, reposition.y);
421
+
360
422
  // Interrupt if enemy is casting (quick attack)
361
- if (enemy.state === 'casting' && distance < interruptRange)
423
+ if (enemy.status === 'casting' && distance < interruptRange)
362
424
  {
363
425
  const castTime = getMissileCastTime(QUICK_CONFIG);
364
426
  if (isSafeToAttack(threats, castTime))
365
427
  {
366
- return {
367
- move,
368
- startCast: {
369
- spell: 'missile',
370
- config: QUICK_CONFIG,
371
- missileAI: StandardHoming,
372
- direction: angleTo(state.position, enemy.position),
373
- },
374
- };
428
+ return missile(QUICK_CONFIG, StandardHoming, angleTo(position, enemy.position)).move(safeMove.x, safeMove.y);
375
429
  }
376
430
  }
377
431
 
@@ -381,15 +435,7 @@ export const Battlemage: WizardFunction = ({state}) =>
381
435
  const heavyCastTime = getMissileCastTime(HEAVY_CONFIG);
382
436
  if (isSafeToAttack(threats, heavyCastTime))
383
437
  {
384
- return {
385
- move,
386
- startCast: {
387
- spell: 'missile',
388
- config: HEAVY_CONFIG,
389
- missileAI: StandardHoming,
390
- direction: angleTo(state.position, enemy.position),
391
- },
392
- };
438
+ return missile(HEAVY_CONFIG, StandardHoming, angleTo(position, enemy.position)).move(safeMove.x, safeMove.y);
393
439
  }
394
440
 
395
441
  // Fallback: quick homing missile when heavy isn't safe (only if in range)
@@ -399,15 +445,7 @@ export const Battlemage: WizardFunction = ({state}) =>
399
445
  isSafeToAttack(threats, quickCastTime)
400
446
  )
401
447
  {
402
- return {
403
- move,
404
- startCast: {
405
- spell: 'missile',
406
- config: QUICK_CONFIG,
407
- missileAI: StandardHoming,
408
- direction: angleTo(state.position, enemy.position),
409
- },
410
- };
448
+ return missile(QUICK_CONFIG, StandardHoming, angleTo(position, enemy.position)).move(safeMove.x, safeMove.y);
411
449
  }
412
450
 
413
451
  // Not safe to attack — wait for threat to pass, then fire
@@ -416,18 +454,15 @@ export const Battlemage: WizardFunction = ({state}) =>
416
454
 
417
455
  // === FALLBACK DEFENSE: Shield when no attack window exists ===
418
456
  if (
419
- state.state === 'idle' &&
457
+ status === 'idle' &&
420
458
  urgentThreat &&
421
459
  !urgentThreat.bestDodgeDirection &&
422
460
  urgentThreat.canBlockInTime &&
423
461
  urgentThreat.ticksToStartShield <= 3
424
462
  )
425
463
  {
426
- return {
427
- move: {x: 0, y: 0},
428
- startCast: {spell: 'shield'},
429
- };
464
+ return shield();
430
465
  }
431
466
 
432
- return {move};
433
- };
467
+ return moveAction(safeMove.x, safeMove.y);
468
+ }