@vibemancer/core 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +2 -2
  2. package/dist/chunk-W7HWJFQ4.js +10599 -0
  3. package/dist/chunk-W7HWJFQ4.js.map +1 -0
  4. package/dist/index-browser.d.ts +1300 -1051
  5. package/dist/index-browser.js +55 -17
  6. package/dist/index.d.ts +53 -3
  7. package/dist/index.js +200 -54
  8. package/dist/index.js.map +1 -1
  9. package/package.json +21 -15
  10. package/src/bots/Hero.ts +867 -0
  11. package/src/bots/berserker/01_Stormchaser.ts +162 -120
  12. package/src/bots/berserker/02_Stormcaller.ts +160 -116
  13. package/src/bots/berserker/03_Stormforger.ts +162 -129
  14. package/src/bots/caster/01_Flamecaller.ts +126 -84
  15. package/src/bots/caster/02_Pyromancer.ts +148 -101
  16. package/src/bots/caster/03_Infernalist.ts +180 -160
  17. package/src/bots/defensive/01_Turtle.ts +48 -44
  18. package/src/bots/defensive/02_Sentinel.ts +57 -53
  19. package/src/bots/defensive/03_Golem.ts +85 -97
  20. package/src/bots/duelist/01_Battlemage.ts +157 -122
  21. package/src/bots/duelist/02_Warmage.ts +166 -121
  22. package/src/bots/duelist/03_Archmage.ts +198 -178
  23. package/src/bots/homing/01_Bonemancer.ts +20 -32
  24. package/src/bots/homing/02_Lich.ts +145 -93
  25. package/src/bots/homing/03_Archlich.ts +129 -60
  26. package/src/bots/kiter/01_Spellspinner.ts +145 -107
  27. package/src/bots/kiter/02_Spellweaver.ts +153 -105
  28. package/src/bots/kiter/03_Spellbinder.ts +168 -123
  29. package/src/bots/melee/01_Shadowblade.ts +131 -75
  30. package/src/bots/melee/02_Nightblade.ts +174 -130
  31. package/src/bots/melee/03_Voidblade.ts +266 -168
  32. package/src/bots/registry.ts +44 -37
  33. package/src/bots/shared.ts +185 -25
  34. package/src/bots/sniper/01_Spellshot.ts +151 -98
  35. package/src/bots/sniper/02_Spelltracer.ts +173 -128
  36. package/src/bots/sniper/03_Spellseeker.ts +177 -152
  37. package/src/bots/standalone/Critter.ts +46 -52
  38. package/src/bots/standalone/Doombringer.ts +36 -40
  39. package/src/bots/standalone/Hogger.ts +120 -89
  40. package/src/bots/standalone/Rookie.ts +21 -23
  41. package/src/bots/standalone/TargetDummy.ts +5 -6
  42. package/src/bots/test/cheater.ts +91 -85
  43. package/src/bots/test/crasher.ts +26 -28
  44. package/src/engine/bundle-fight.ts +242 -0
  45. package/src/engine/hooks-runtime.ts +58 -18
  46. package/src/engine/manual-match.ts +33 -25
  47. package/src/engine/missile-templates.ts +25 -43
  48. package/src/engine/optimizer.ts +7 -7
  49. package/src/engine/params-runtime.ts +3 -3
  50. package/src/engine/physics.ts +33 -23
  51. package/src/engine/sandbox-browser.ts +8 -7
  52. package/src/engine/sandbox-compile.ts +1 -1
  53. package/src/engine/sandbox-harness.ts +299 -367
  54. package/src/engine/sandbox.ts +3 -3
  55. package/src/engine/simulation.ts +291 -76
  56. package/src/engine/spells.ts +9 -12
  57. package/src/engine-version.ts +1 -1
  58. package/src/hooks/action-builders.ts +76 -21
  59. package/src/hooks/index.ts +17 -9
  60. package/src/hooks/state-hooks.ts +61 -25
  61. package/src/hooks/threat-analysis.ts +44 -20
  62. package/src/hooks/types.ts +62 -5
  63. package/src/index.ts +2 -0
  64. package/src/rules.ts +209 -63
  65. package/src/stats.ts +2 -2
  66. package/src/testing.ts +6 -30
  67. package/src/trace.ts +63 -3
  68. package/src/types.ts +127 -14
  69. package/src/utils/angles.ts +1 -0
  70. package/src/utils/combat.ts +98 -6
  71. package/src/utils/spatial.ts +3 -3
  72. package/dist/chunk-B7YYYBEC.js +0 -9140
  73. package/dist/chunk-B7YYYBEC.js.map +0 -1
  74. package/src/hooks/bot-wrapper.ts +0 -84
@@ -1,9 +1,9 @@
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, useMyProjectiles, useLastMissileConfig, 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
- import {ARENA_SIZE, GCD_DURATION, SHIELD_CAST_TIME} from '../../rules.js';
5
+ import {RULES, ARENA_MIN, ARENA_MAX} from '../../rules.js';
5
6
  import {
6
- getThreats,
7
7
  getMostUrgentThreat,
8
8
  getDodgeDirectionForMovement,
9
9
  getBlinkToCenter,
@@ -13,6 +13,8 @@ import {
13
13
  shouldForceShield,
14
14
  MAX_CAST_BUDGET,
15
15
  MIN_USEFUL_DAMAGE,
16
+ keepOffWalls,
17
+ getPreCastReposition,
16
18
  } from '../shared.js';
17
19
  import {useParam} from '../../engine/params-runtime.js';
18
20
  const WALL_BUFFER = 60;
@@ -22,12 +24,11 @@ const SHIELD_BUFFER = 3;
22
24
  /**
23
25
  * Standard homing missile AI.
24
26
  */
25
- const HomingAI: MissileAIFunction = ({worldState}) =>
27
+ const HomingAI: MissileFunction = () =>
26
28
  {
27
- const enemy = worldState.enemies[0];
28
- return {
29
- turnToward: enemy?.position,
30
- };
29
+ const ctx = getMissileContext();
30
+ const enemy = ctx.worldState.enemies[0];
31
+ return enemy ? turnToward(enemy.position.x, enemy.position.y) : flyStraight();
31
32
  };
32
33
 
33
34
  /**
@@ -36,10 +37,10 @@ const HomingAI: MissileAIFunction = ({worldState}) =>
36
37
  function isNearWall(position: {x: number; y: number}): boolean
37
38
  {
38
39
  return (
39
- position.x < WALL_BUFFER ||
40
- position.x > ARENA_SIZE - WALL_BUFFER ||
41
- position.y < WALL_BUFFER ||
42
- position.y > ARENA_SIZE - WALL_BUFFER
40
+ position.x < ARENA_MIN + WALL_BUFFER ||
41
+ position.x > ARENA_MAX - WALL_BUFFER ||
42
+ position.y < ARENA_MIN + WALL_BUFFER ||
43
+ position.y > ARENA_MAX - WALL_BUFFER
43
44
  );
44
45
  }
45
46
 
@@ -86,16 +87,16 @@ function smartStrafe(
86
87
  y: position.y + strafeCounterClockwise.y * 100,
87
88
  };
88
89
  const scoreClockwise = Math.min(
89
- futureClockwise.x,
90
- ARENA_SIZE - futureClockwise.x,
91
- futureClockwise.y,
92
- ARENA_SIZE - futureClockwise.y,
90
+ futureClockwise.x - ARENA_MIN,
91
+ ARENA_MAX - futureClockwise.x,
92
+ futureClockwise.y - ARENA_MIN,
93
+ ARENA_MAX - futureClockwise.y,
93
94
  );
94
95
  const scoreCounterClockwise = Math.min(
95
- futureCounterClockwise.x,
96
- ARENA_SIZE - futureCounterClockwise.x,
97
- futureCounterClockwise.y,
98
- ARENA_SIZE - futureCounterClockwise.y,
96
+ futureCounterClockwise.x - ARENA_MIN,
97
+ ARENA_MAX - futureCounterClockwise.x,
98
+ futureCounterClockwise.y - ARENA_MIN,
99
+ ARENA_MAX - futureCounterClockwise.y,
99
100
  );
100
101
  strafeDir =
101
102
  scoreClockwise > scoreCounterClockwise
@@ -108,7 +109,7 @@ function smartStrafe(
108
109
  strafeDir = cycle === 0 ? strafeClockwise : strafeCounterClockwise;
109
110
  }
110
111
 
111
- const intensity = dodgeDirection ? 100 : strafeIntensity;
112
+ const intensity = dodgeDirection ? 1 : strafeIntensity;
112
113
  return {x: strafeDir.x * intensity, y: strafeDir.y * intensity};
113
114
  }
114
115
 
@@ -134,35 +135,49 @@ function smartStrafe(
134
135
  *
135
136
  * TIER: 2 (enhanced Spellspinner)
136
137
  */
137
- export const Spellweaver: WizardFunction = ({state}) =>
138
+ export function Spellweaver(): FinalAction
138
139
  {
139
- const targetDistance = useParam('targetDistance', 322, {
140
+ const position = usePosition();
141
+ const enemy = useEnemy();
142
+ const status = useStatus();
143
+ const blinkCooldown = useBlinkCooldown();
144
+ const health = useHealth();
145
+ const tick = useTick();
146
+ const threats = useThreats();
147
+ const projectiles = useProjectiles();
148
+ const myProjectiles = useMyProjectiles();
149
+ const lastMissileConfig = useLastMissileConfig();
150
+ const castingSpell = useCastingSpell();
151
+ const castProgressData = useCastProgress();
152
+
153
+
154
+ const targetDistance = useParam('targetDistance', 257, {
140
155
  range: 150,
141
156
  min: 0,
142
157
  });
143
- const dangerDistance = useParam('dangerDistance', 272, {
158
+ const dangerDistance = useParam('dangerDistance', 295, {
144
159
  range: 125,
145
160
  min: 0,
146
161
  });
147
- const minTurnRate = useParam('minTurnRate', 0.2, {range: 1.5, steps: 5});
148
- const strafeCyclePeriod = useParam('strafeCyclePeriod', 296, {
162
+ const minTurnRate = useParam('minTurnRate', 0.9, {range: 1.5, steps: 5});
163
+ const strafeCyclePeriod = useParam('strafeCyclePeriod', 285, {
149
164
  range: 75,
150
165
  min: 80,
151
166
  max: 300,
152
167
  });
153
- const strafeIntensity = useParam('strafeIntensity', 42, {
154
- range: 40,
155
- min: 30,
156
- max: 100,
168
+ const strafeIntensity = useParam('strafeIntensity', 1, {
169
+ range: 0.4,
170
+ min: 0.3,
171
+ max: 1,
157
172
  steps: 7,
158
173
  });
159
- const approachSpeed = useParam('approachSpeed', 68, {
160
- range: 30,
161
- min: 10,
162
- max: 70,
174
+ const approachSpeed = useParam('approachSpeed', 0.53, {
175
+ range: 0.3,
176
+ min: 0.1,
177
+ max: 0.7,
163
178
  steps: 7,
164
179
  });
165
- const distanceDeadZone = useParam('distanceDeadZone', 12, {
180
+ const distanceDeadZone = useParam('distanceDeadZone', 31, {
166
181
  range: 30,
167
182
  min: 5,
168
183
  max: 80,
@@ -173,32 +188,47 @@ export const Spellweaver: WizardFunction = ({state}) =>
173
188
  min: 50,
174
189
  max: 300,
175
190
  });
176
- const flightTimeDivisor = useParam('flightTimeDivisor', 2, {
191
+ const flightTimeDivisor = useParam('flightTimeDivisor', 2.1, {
177
192
  range: 4,
178
193
  min: 2,
179
194
  max: 12,
180
195
  steps: 5,
181
196
  });
197
+ const centerBias = useParam('centerBias', 0.14, {
198
+ range: 0.4,
199
+ min: 0,
200
+ max: 0.8,
201
+ steps: 5,
202
+ });
203
+ const maxBlinkDistance = useParam('maxBlinkDistance', 239, {
204
+ range: 100,
205
+ min: 50,
206
+ max: 300,
207
+ steps: 7,
208
+ });
209
+
210
+ const distance = distanceTo(position, enemy.position);
182
211
 
183
- const enemy = state.enemies[0];
184
- if (!enemy)
212
+ function capBlinkDistance(target: {x: number; y: number}): {x: number; y: number}
185
213
  {
186
- return {move: {x: 0, y: 0}};
214
+ const dx = target.x - position.x;
215
+ const dy = target.y - position.y;
216
+ const dist = Math.sqrt(dx * dx + dy * dy);
217
+ if (dist <= maxBlinkDistance) return target;
218
+ const scale = maxBlinkDistance / dist;
219
+ return {x: position.x + dx * scale, y: position.y + dy * scale};
187
220
  }
188
221
 
189
- const distance = distanceTo(state.position, enemy.position);
190
-
191
222
  // Analyze threats using simulation
192
- const threats = getThreats(state);
193
223
  const urgentThreat = getMostUrgentThreat(threats);
194
224
 
195
225
  // Smart strafe
196
- const dodgeDirection = getDodgeDirectionForMovement(threats, state.position);
226
+ const dodgeDirection = getDodgeDirectionForMovement(threats, position);
197
227
  const strafe = smartStrafe(
198
- state.position,
228
+ position,
199
229
  enemy,
200
230
  dodgeDirection,
201
- state.tick,
231
+ tick,
202
232
  strafeCyclePeriod,
203
233
  strafeIntensity,
204
234
  );
@@ -209,24 +239,32 @@ export const Spellweaver: WizardFunction = ({state}) =>
209
239
 
210
240
  if (!dodgeDirection)
211
241
  {
212
- const deltaX = enemy.position.x - state.position.x;
213
- const deltaY = enemy.position.y - state.position.y;
242
+ const deltaX = enemy.position.x - position.x;
243
+ const deltaY = enemy.position.y - position.y;
214
244
  const normalizedDistance = distance > 0 ? distance : 1;
215
245
 
216
- if (distance > targetDistance + distanceDeadZone)
246
+ if (distance > targetDistance + distanceDeadZone)
217
247
  {
218
- moveX += (deltaX / normalizedDistance) * approachSpeed;
219
- moveY += (deltaY / normalizedDistance) * approachSpeed;
248
+ // Approach the center-side of the enemy so knockback pushes toward center, not lava.
249
+ const center = (ARENA_MIN + ARENA_MAX) / 2;
250
+ const toCenterX = center - enemy.position.x;
251
+ const toCenterY = center - enemy.position.y;
252
+ const toCenterDist = Math.sqrt(toCenterX * toCenterX + toCenterY * toCenterY) || 1;
253
+ const approachDirX = deltaX / normalizedDistance + (toCenterX / toCenterDist) * centerBias;
254
+ const approachDirY = deltaY / normalizedDistance + (toCenterY / toCenterDist) * centerBias;
255
+ const approachMag = Math.sqrt(approachDirX * approachDirX + approachDirY * approachDirY) || 1;
256
+ moveX += (approachDirX / approachMag) * approachSpeed;
257
+ moveY += (approachDirY / approachMag) * approachSpeed;
220
258
  }
221
259
  else if (distance < targetDistance - distanceDeadZone)
222
260
  {
223
261
  let retreatX = -(deltaX / normalizedDistance) * approachSpeed;
224
262
  let retreatY = -(deltaY / normalizedDistance) * approachSpeed;
225
- if (state.position.x < WALL_BUFFER && retreatX < 0) retreatX = 0;
226
- if (state.position.x > ARENA_SIZE - WALL_BUFFER && retreatX > 0)
263
+ if (position.x < ARENA_MIN + WALL_BUFFER && retreatX < 0) retreatX = 0;
264
+ if (position.x > ARENA_MAX - WALL_BUFFER && retreatX > 0)
227
265
  retreatX = 0;
228
- if (state.position.y < WALL_BUFFER && retreatY < 0) retreatY = 0;
229
- if (state.position.y > ARENA_SIZE - WALL_BUFFER && retreatY > 0)
266
+ if (position.y < ARENA_MIN + WALL_BUFFER && retreatY < 0) retreatY = 0;
267
+ if (position.y > ARENA_MAX - WALL_BUFFER && retreatY > 0)
230
268
  retreatY = 0;
231
269
  moveX += retreatX;
232
270
  moveY += retreatY;
@@ -234,46 +272,47 @@ export const Spellweaver: WizardFunction = ({state}) =>
234
272
  }
235
273
 
236
274
  const magnitude = Math.sqrt(moveX * moveX + moveY * moveY);
237
- if (magnitude > 100)
275
+ if (magnitude > 1)
238
276
  {
239
- moveX = (moveX / magnitude) * 100;
240
- moveY = (moveY / magnitude) * 100;
277
+ moveX = moveX / magnitude;
278
+ moveY = moveY / magnitude;
241
279
  }
242
280
 
243
281
  const move = {x: moveX, y: moveY};
282
+ const safeMove = keepOffWalls(position, move);
244
283
 
245
284
  // === DEFENSE: Handle channeling ===
246
- if (state.state === 'channeling')
285
+ if (status === 'channeling')
247
286
  {
248
287
  if (!urgentThreat || urgentThreat.ticksToImpact > shieldCancelThreshold)
249
288
  {
250
- return {move, cancel: true};
289
+ return cancel().move(safeMove.x, safeMove.y);
251
290
  }
252
- return {move: {x: 0, y: 0}};
291
+ return idle();
253
292
  }
254
293
 
255
294
  // === DEFENSE: Cancel missile cast only for LETHAL incoming damage ===
256
295
  // DPS trade: we accepted the hit when we started casting. Only cancel to survive.
257
296
  if (
258
- state.state === 'casting' &&
259
- state.castingSpell === 'missile' &&
297
+ status === 'casting' &&
298
+ castingSpell === 'missile' &&
260
299
  urgentThreat &&
261
- urgentThreat.projectile.damage >= state.health
300
+ urgentThreat.projectile.damage >= health
262
301
  )
263
302
  {
264
- const remainingCast = (state.castDuration ?? 0) - (state.castProgress ?? 0);
303
+ const remainingCast = ((castProgressData?.total) ?? 0) - ((castProgressData?.current) ?? 0);
265
304
  if (
266
305
  urgentThreat.ticksToImpact <=
267
- remainingCast + GCD_DURATION + SHIELD_CAST_TIME + SHIELD_BUFFER
306
+ remainingCast + RULES.GCD_DURATION + RULES.SHIELD_CAST_TIME + SHIELD_BUFFER
268
307
  )
269
308
  {
270
- return {move, cancel: true};
309
+ return cancel().move(safeMove.x, safeMove.y);
271
310
  }
272
311
  }
273
312
 
274
313
  // === DEFENSE: Shield undodgeable or high-damage homing threats ===
275
314
  if (
276
- state.state === 'idle' &&
315
+ status === 'idle' &&
277
316
  urgentThreat &&
278
317
  (!urgentThreat.bestDodgeDirection || shouldForceShield(urgentThreat))
279
318
  )
@@ -282,60 +321,77 @@ export const Spellweaver: WizardFunction = ({state}) =>
282
321
  {
283
322
  if (urgentThreat.ticksToStartShield <= SHIELD_BUFFER)
284
323
  {
285
- return {
286
- move: {x: 0, y: 0},
287
- startCast: {spell: 'shield'},
288
- };
324
+ return shield();
289
325
  }
290
326
  }
291
- else if (state.blinkCooldown === 0)
327
+ else if (blinkCooldown === 0)
328
+ {
329
+ const blinkTarget = capBlinkDistance(getBlinkToCenter(position));
330
+ return blink(blinkTarget.x, blinkTarget.y);
331
+ }
332
+ }
333
+
334
+ // === Preemptive shield — shield when enemy is casting a missile at close range ===
335
+ if (
336
+ status === 'idle' &&
337
+ distance < 300 &&
338
+ enemy.status === 'casting' &&
339
+ enemy.castingSpell === 'missile'
340
+ )
341
+ {
342
+ const remainingCast = (enemy.castDuration ?? 0) - (enemy.castProgress ?? 0);
343
+ const estimatedFlightTicks = distance / 8;
344
+ const ticksUntilHit = remainingCast + estimatedFlightTicks;
345
+
346
+ if (
347
+ ticksUntilHit >= RULES.SHIELD_CAST_TIME &&
348
+ ticksUntilHit < RULES.SHIELD_CAST_TIME + 15
349
+ )
292
350
  {
293
- return {
294
- move: {x: 0, y: 0},
295
- startCast: {spell: 'blink', target: getBlinkToCenter(state.position)},
296
- };
351
+ return shield();
297
352
  }
298
353
  }
299
354
 
300
355
  // === DISTANCE BLINK: enemy too close, blink to reestablish kiting range ===
301
356
  if (
302
- state.state === 'idle' &&
357
+ status === 'idle' &&
303
358
  distance < dangerDistance &&
304
- state.blinkCooldown === 0 &&
359
+ blinkCooldown === 0 &&
305
360
  (!urgentThreat || urgentThreat.bestDodgeDirection !== null)
306
361
  )
307
362
  {
308
363
  const blinkTarget = getBlinkToIncreaseDistance(
309
- state.position,
364
+ position,
310
365
  enemy.position,
311
- state.projectiles,
366
+ projectiles,
312
367
  MIN_BLINK_ESCAPE_DISTANCE,
313
368
  );
314
- if (blinkTarget)
369
+ if (blinkTarget)
315
370
  {
316
- return {
317
- move: {x: 0, y: 0},
318
- startCast: {spell: 'blink', target: blinkTarget},
319
- };
371
+ const capped = capBlinkDistance(blinkTarget);
372
+ return blink(capped.x, capped.y);
320
373
  }
321
374
  }
322
375
 
323
376
  // === OFFENSE: Adaptive missile fitting ===
324
377
  // Skip offense when a lethal missile could reach us during cast+GCD — dodge at full speed instead
325
- const myProjectileIds = new Set(state.myProjectiles.map((p) => p.id));
326
- const hasNearbyLethalMissile = state.projectiles.some(
378
+ const myProjectileIds = new Set(myProjectiles.map((p) => p.id));
379
+ const hasNearbyLethalMissile = projectiles.some(
327
380
  (p) =>
328
381
  !myProjectileIds.has(p.id) &&
329
- p.damage >= state.health &&
330
- distanceTo(state.position, p.position) / p.speed <
331
- MAX_CAST_BUDGET + GCD_DURATION,
382
+ p.damage >= health &&
383
+ distanceTo(position, p.position) / p.speed <
384
+ MAX_CAST_BUDGET + RULES.GCD_DURATION,
332
385
  );
333
- if (state.state === 'idle' && distance < 600 && !hasNearbyLethalMissile)
386
+ if (status === 'idle' && distance < 600 && !hasNearbyLethalMissile)
334
387
  {
388
+ const reposition = getPreCastReposition(position, enemy.position);
389
+ if (reposition) return moveAction(reposition.x, reposition.y);
390
+
335
391
  let nextThreatTime = urgentThreat?.ticksToImpact ?? Infinity;
336
392
 
337
393
  // Account for enemy's active cast
338
- if (enemy.state === 'casting' && enemy.castingSpell === 'missile')
394
+ if (enemy.status === 'casting' && enemy.castingSpell === 'missile')
339
395
  {
340
396
  const remainingEnemyCast =
341
397
  (enemy.castDuration ?? 0) - (enemy.castProgress ?? 0);
@@ -346,7 +402,7 @@ export const Spellweaver: WizardFunction = ({state}) =>
346
402
  );
347
403
  }
348
404
  const safeBudget =
349
- nextThreatTime - GCD_DURATION - SHIELD_CAST_TIME - SHIELD_BUFFER;
405
+ nextThreatTime - RULES.GCD_DURATION - RULES.SHIELD_CAST_TIME - SHIELD_BUFFER;
350
406
  const budgetTicks = Math.min(safeBudget, MAX_CAST_BUDGET);
351
407
 
352
408
  // Spellweaver always uses homing (kiting means enemies are always moving)
@@ -355,24 +411,16 @@ export const Spellweaver: WizardFunction = ({state}) =>
355
411
  const config = fitMissileToBudget(budgetTicks, distance, {
356
412
  minTurnRate,
357
413
  maxDamage: enemyHP,
358
- lastMissileConfig: state.lastMissileConfig,
414
+ lastMissileConfig: lastMissileConfig,
359
415
  });
360
416
 
361
417
  if (config && config.damage >= minDmg)
362
418
  {
363
- return {
364
- move,
365
- startCast: {
366
- spell: 'missile',
367
- config,
368
- missileAI: HomingAI,
369
- direction: angleTo(state.position, enemy.position),
370
- },
371
- };
419
+ return missile(config, HomingAI, angleTo(position, enemy.position)).move(safeMove.x, safeMove.y);
372
420
  }
373
421
 
374
422
  // Budget too small for useful missile — wait for threat to pass, then fire
375
423
  }
376
424
 
377
- return {move};
378
- };
425
+ return moveAction(safeMove.x, safeMove.y);
426
+ }