@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,10 @@
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, 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 {
5
- ARENA_SIZE,
6
- GCD_DURATION,
7
- SHIELD_CAST_TIME,
8
- BLINK_CAST_TIME,
9
- } from '../../rules.js';
5
+ import {RULES, ARENA_MIN, ARENA_MAX} from '../../rules.js';
10
6
  import {getMissileCastTime, getLeadPosition} from '../../utils/combat.js';
11
7
  import {
12
- getThreats,
13
8
  getMostUrgentThreat,
14
9
  getDodgeDirectionForMovement,
15
10
  isSafeToAttack,
@@ -21,27 +16,30 @@ import {
21
16
  MIN_USEFUL_DAMAGE,
22
17
  getClosingRate,
23
18
  getEnemyVulnerabilityTicks,
19
+ keepOffWalls,
20
+ getPreCastReposition,
24
21
  } from '../shared.js';
25
22
  import {useParam} from '../../engine/params-runtime.js';
26
23
 
27
24
  const WALL_BUFFER = 60;
28
25
  const SHIELD_BUFFER = 3;
29
26
 
30
- const HomingAI: MissileAIFunction = ({worldState}) =>
27
+ const HomingAI: MissileFunction = () =>
31
28
  {
32
- const enemy = worldState.enemies[0];
33
- return {turnToward: enemy?.position};
29
+ const ctx = getMissileContext();
30
+ const enemy = ctx.worldState.enemies[0];
31
+ return enemy ? turnToward(enemy.position.x, enemy.position.y) : flyStraight();
34
32
  };
35
33
 
36
- const StraightAI: MissileAIFunction = () => ({});
34
+ const StraightAI: MissileFunction = () => flyStraight();
37
35
 
38
36
  function isNearWall(position: {x: number; y: number}): boolean
39
37
  {
40
38
  return (
41
- position.x < WALL_BUFFER ||
42
- position.x > ARENA_SIZE - WALL_BUFFER ||
43
- position.y < WALL_BUFFER ||
44
- position.y > ARENA_SIZE - WALL_BUFFER
39
+ position.x < ARENA_MIN + WALL_BUFFER ||
40
+ position.x > ARENA_MAX - WALL_BUFFER ||
41
+ position.y < ARENA_MIN + WALL_BUFFER ||
42
+ position.y > ARENA_MAX - WALL_BUFFER
45
43
  );
46
44
  }
47
45
 
@@ -72,12 +70,12 @@ function smartStrafe(
72
70
  {
73
71
  const fCw = {x: position.x + cw.x * 100, y: position.y + cw.y * 100};
74
72
  const fCcw = {x: position.x + ccw.x * 100, y: position.y + ccw.y * 100};
75
- const sCw = Math.min(fCw.x, ARENA_SIZE - fCw.x, fCw.y, ARENA_SIZE - fCw.y);
73
+ const sCw = Math.min(fCw.x - ARENA_MIN, ARENA_MAX - fCw.x, fCw.y - ARENA_MIN, ARENA_MAX - fCw.y);
76
74
  const sCcw = Math.min(
77
75
  fCcw.x,
78
- ARENA_SIZE - fCcw.x,
76
+ ARENA_MAX - fCcw.x,
79
77
  fCcw.y,
80
- ARENA_SIZE - fCcw.y,
78
+ ARENA_MAX - fCcw.y,
81
79
  );
82
80
  dir = sCw > sCcw ? cw : ccw;
83
81
  }
@@ -87,7 +85,7 @@ function smartStrafe(
87
85
  dir = cycle === 0 ? cw : ccw;
88
86
  }
89
87
 
90
- const intensity = dodgeDirection ? 100 : strafeIntensity;
88
+ const intensity = dodgeDirection ? 1 : strafeIntensity;
91
89
  return {x: dir.x * intensity, y: dir.y * intensity};
92
90
  }
93
91
 
@@ -107,11 +105,24 @@ function smartStrafe(
107
105
  * PROGRESSION LINE: Flamecaller → Pyromancer → Infernalist
108
106
  * TIER: 3 (elite Caster line)
109
107
  */
110
- export const Infernalist: WizardFunction = ({state}) =>
108
+ export function Infernalist(): FinalAction
111
109
  {
112
- const targetDistance = useParam('targetDistance', 285, {range: 225, min: 0});
113
- const minTurnRate = useParam('minTurnRate', 0.2, {range: 1.5, steps: 5});
114
- const dangerDistance = useParam('dangerDistance', 222, {
110
+ const position = usePosition();
111
+ const enemy = useEnemy();
112
+ const status = useStatus();
113
+ const blinkCooldown = useBlinkCooldown();
114
+ const health = useHealth();
115
+ const tick = useTick();
116
+ const threats = useThreats();
117
+ const projectiles = useProjectiles();
118
+ const lastMissileConfig = useLastMissileConfig();
119
+ const castingSpell = useCastingSpell();
120
+ const castProgressData = useCastProgress();
121
+
122
+
123
+ const targetDistance = useParam('targetDistance', 141, {range: 225, min: 0});
124
+ const minTurnRate = useParam('minTurnRate', 0.9, {range: 1.5, steps: 5});
125
+ const dangerDistance = useParam('dangerDistance', 332, {
115
126
  range: 150,
116
127
  min: 0,
117
128
  });
@@ -125,61 +136,59 @@ export const Infernalist: WizardFunction = ({state}) =>
125
136
  min: 10,
126
137
  steps: 5,
127
138
  });
128
- const preferBlinkDodge =
129
- useParam('preferBlinkDodge', 0, {min: 0, max: 1, steps: 2}) >= 0.5;
130
- const strafeCyclePeriod = useParam('strafeCyclePeriod', 109, {
139
+ const strafeCyclePeriod = useParam('strafeCyclePeriod', 174, {
131
140
  range: 75,
132
141
  min: 80,
133
142
  max: 250,
134
143
  });
135
- const strafeIntensity = useParam('strafeIntensity', 23, {
136
- range: 40,
137
- min: 20,
138
- max: 100,
144
+ const strafeIntensity = useParam('strafeIntensity', 0.87, {
145
+ range: 0.4,
146
+ min: 0.2,
147
+ max: 1,
139
148
  steps: 7,
140
149
  });
141
- const approachSpeed = useParam('approachSpeed', 53, {
142
- range: 30,
143
- min: 10,
144
- max: 70,
150
+ const approachSpeed = useParam('approachSpeed', 0.66, {
151
+ range: 0.3,
152
+ min: 0.1,
153
+ max: 0.7,
145
154
  steps: 7,
146
155
  });
147
- const distanceDeadZone = useParam('distanceDeadZone', 34, {
156
+ const distanceDeadZone = useParam('distanceDeadZone', 29, {
148
157
  range: 30,
149
158
  min: 5,
150
159
  max: 80,
151
160
  steps: 7,
152
161
  });
153
- const cancelHeavyDmg = useParam('cancelHeavyDmg', 40, {
162
+ const cancelHeavyDmg = useParam('cancelHeavyDmg', 31, {
154
163
  range: 15,
155
164
  min: 5,
156
165
  max: 40,
157
166
  steps: 5,
158
167
  });
159
- const cancelMediumDmg = useParam('cancelMediumDmg', 9, {
168
+ const cancelMediumDmg = useParam('cancelMediumDmg', 12, {
160
169
  range: 10,
161
170
  min: 3,
162
171
  max: 25,
163
172
  steps: 5,
164
173
  });
165
- const cancelCastRatio = useParam('cancelCastRatio', 0.78, {
174
+ const cancelCastRatio = useParam('cancelCastRatio', 0.77, {
166
175
  range: 0.4,
167
176
  min: 0.3,
168
177
  max: 1.0,
169
178
  steps: 5,
170
179
  });
171
- const shieldCancelThreshold = useParam('shieldCancelThreshold', 100, {
180
+ const shieldCancelThreshold = useParam('shieldCancelThreshold', 60, {
172
181
  range: 75,
173
182
  min: 50,
174
183
  max: 300,
175
184
  });
176
- const blinkWindowMax = useParam('blinkWindowMax', 75, {
185
+ const blinkWindowMax = useParam('blinkWindowMax', 23, {
177
186
  range: 40,
178
187
  min: 20,
179
188
  max: 120,
180
189
  steps: 7,
181
190
  });
182
- const proactiveBlinkRange = useParam('proactiveBlinkRange', 100, {
191
+ const proactiveBlinkRange = useParam('proactiveBlinkRange', 175, {
183
192
  range: 150,
184
193
  min: 100,
185
194
  max: 500,
@@ -191,28 +200,37 @@ export const Infernalist: WizardFunction = ({state}) =>
191
200
  max: 12,
192
201
  steps: 5,
193
202
  });
194
- const safeAttackCastEstimate = useParam('safeAttackCastEstimate', 35, {
195
- range: 25,
196
- min: 10,
197
- max: 80,
203
+ const centerBias = useParam('centerBias', 0.13, {
204
+ range: 0.4,
205
+ min: 0,
206
+ max: 0.8,
198
207
  steps: 5,
199
208
  });
209
+ const maxBlinkDistance = useParam('maxBlinkDistance', 123, {
210
+ range: 100,
211
+ min: 50,
212
+ max: 300,
213
+ steps: 7,
214
+ });
200
215
 
201
- const enemy = state.enemies[0];
202
- if (!enemy)
216
+ function capBlinkDistance(target: {x: number; y: number}): {x: number; y: number}
203
217
  {
204
- return {move: {x: 0, y: 0}};
218
+ const dx = target.x - position.x;
219
+ const dy = target.y - position.y;
220
+ const dist = Math.sqrt(dx * dx + dy * dy);
221
+ if (dist <= maxBlinkDistance) return target;
222
+ const scale = maxBlinkDistance / dist;
223
+ return {x: position.x + dx * scale, y: position.y + dy * scale};
205
224
  }
206
225
 
207
- const distance = distanceTo(state.position, enemy.position);
208
- const threats = getThreats(state);
226
+ const distance = distanceTo(position, enemy.position);
209
227
  const urgentThreat = getMostUrgentThreat(threats);
210
- const dodgeDirection = getDodgeDirectionForMovement(threats, state.position);
228
+ const dodgeDirection = getDodgeDirectionForMovement(threats, position);
211
229
  const strafe = smartStrafe(
212
- state.position,
230
+ position,
213
231
  enemy,
214
232
  dodgeDirection,
215
- state.tick,
233
+ tick,
216
234
  strafeCyclePeriod,
217
235
  strafeIntensity,
218
236
  );
@@ -223,133 +241,158 @@ export const Infernalist: WizardFunction = ({state}) =>
223
241
 
224
242
  if (!dodgeDirection)
225
243
  {
226
- const deltaX = enemy.position.x - state.position.x;
227
- const deltaY = enemy.position.y - state.position.y;
244
+ const deltaX = enemy.position.x - position.x;
245
+ const deltaY = enemy.position.y - position.y;
228
246
  const norm = distance > 0 ? distance : 1;
229
247
 
230
- if (distance > targetDistance + distanceDeadZone)
248
+ if (distance > targetDistance + distanceDeadZone)
231
249
  {
232
- moveX += (deltaX / norm) * approachSpeed;
233
- moveY += (deltaY / norm) * approachSpeed;
250
+ // Approach the center-side of the enemy so knockback pushes toward center, not lava.
251
+ const center = (ARENA_MIN + ARENA_MAX) / 2;
252
+ const toCenterX = center - enemy.position.x;
253
+ const toCenterY = center - enemy.position.y;
254
+ const toCenterDist = Math.sqrt(toCenterX * toCenterX + toCenterY * toCenterY) || 1;
255
+ const approachDirX = deltaX / norm + (toCenterX / toCenterDist) * centerBias;
256
+ const approachDirY = deltaY / norm + (toCenterY / toCenterDist) * centerBias;
257
+ const approachMag = Math.sqrt(approachDirX * approachDirX + approachDirY * approachDirY) || 1;
258
+ moveX += (approachDirX / approachMag) * approachSpeed;
259
+ moveY += (approachDirY / approachMag) * approachSpeed;
234
260
  }
235
261
  else if (distance < targetDistance - distanceDeadZone)
236
262
  {
237
263
  let rx = -(deltaX / norm) * approachSpeed;
238
264
  let ry = -(deltaY / norm) * approachSpeed;
239
- if (state.position.x < WALL_BUFFER && rx < 0) rx = 0;
240
- if (state.position.x > ARENA_SIZE - WALL_BUFFER && rx > 0) rx = 0;
241
- if (state.position.y < WALL_BUFFER && ry < 0) ry = 0;
242
- if (state.position.y > ARENA_SIZE - WALL_BUFFER && ry > 0) ry = 0;
265
+ if (position.x < ARENA_MIN + WALL_BUFFER && rx < 0) rx = 0;
266
+ if (position.x > ARENA_MAX - WALL_BUFFER && rx > 0) rx = 0;
267
+ if (position.y < ARENA_MIN + WALL_BUFFER && ry < 0) ry = 0;
268
+ if (position.y > ARENA_MAX - WALL_BUFFER && ry > 0) ry = 0;
243
269
  moveX += rx;
244
270
  moveY += ry;
245
271
  }
246
272
  }
247
273
 
248
274
  const mag = Math.sqrt(moveX * moveX + moveY * moveY);
249
- if (mag > 100)
275
+ if (mag > 1)
250
276
  {
251
- moveX = (moveX / mag) * 100;
252
- moveY = (moveY / mag) * 100;
277
+ moveX = moveX / mag;
278
+ moveY = moveY / mag;
253
279
  }
254
280
 
255
281
  const move = {x: moveX, y: moveY};
282
+ const safeMove = keepOffWalls(position, move);
256
283
 
257
284
  // === STEP 1: CHANNELING ===
258
- if (state.state === 'channeling')
285
+ if (status === 'channeling')
259
286
  {
260
287
  if (!urgentThreat || urgentThreat.ticksToImpact > shieldCancelThreshold)
261
288
  {
262
- return {move, cancel: true};
289
+ return cancel().move(safeMove.x, safeMove.y);
263
290
  }
264
- return {move: {x: 0, y: 0}};
291
+ return idle();
265
292
  }
266
293
 
267
294
  // === STEP 2: PROGRESSIVE CAST-CANCEL ===
268
295
  if (
269
- state.state === 'casting' &&
270
- state.castingSpell === 'missile' &&
296
+ status === 'casting' &&
297
+ castingSpell === 'missile' &&
271
298
  urgentThreat &&
272
299
  !urgentThreat.bestDodgeDirection
273
300
  )
274
301
  {
275
- const remainingCast = (state.castDuration ?? 0) - (state.castProgress ?? 0);
302
+ const remainingCast = ((castProgressData?.total) ?? 0) - ((castProgressData?.current) ?? 0);
276
303
  const defenseTime =
277
- state.blinkCooldown === 0
278
- ? BLINK_CAST_TIME
279
- : SHIELD_CAST_TIME + SHIELD_BUFFER;
304
+ blinkCooldown === 0
305
+ ? RULES.BLINK_CAST_TIME
306
+ : RULES.SHIELD_CAST_TIME + SHIELD_BUFFER;
280
307
  const willArriveInWindow =
281
- urgentThreat.ticksToImpact <= remainingCast + GCD_DURATION + defenseTime;
308
+ urgentThreat.ticksToImpact <= remainingCast + RULES.GCD_DURATION + defenseTime;
282
309
 
283
310
  if (willArriveInWindow)
284
311
  {
285
312
  const incomingDamage = urgentThreat.projectile.damage;
286
- const castRatio = (state.castProgress ?? 0) / (state.castDuration ?? 1);
313
+ const castRatio = ((castProgressData?.current) ?? 0) / ((castProgressData?.total) ?? 1);
287
314
 
288
- if (incomingDamage >= state.health || incomingDamage >= cancelHeavyDmg)
315
+ if (incomingDamage >= health || incomingDamage >= cancelHeavyDmg)
289
316
  {
290
- return {move, cancel: true};
317
+ return cancel().move(safeMove.x, safeMove.y);
291
318
  }
292
319
  if (incomingDamage >= cancelMediumDmg && castRatio < cancelCastRatio)
293
320
  {
294
- return {move, cancel: true};
321
+ return cancel().move(safeMove.x, safeMove.y);
295
322
  }
296
323
  }
297
324
  }
298
325
 
299
- // === BLINK-DODGE — optimizer-controlled alternative to shield ===
326
+ // === BLINK-DODGE — dodge undodgeable threats by blinking away ===
300
327
  if (
301
- preferBlinkDodge &&
302
- state.state === 'idle' &&
303
- urgentThreat &&
304
- state.blinkCooldown === 0 &&
305
- (!urgentThreat.bestDodgeDirection || shouldForceShield(urgentThreat)) &&
306
- urgentThreat.ticksToImpact >= BLINK_CAST_TIME &&
307
- urgentThreat.ticksToImpact <= blinkWindowMax
308
- )
328
+ status === 'idle' &&
329
+ urgentThreat &&
330
+ blinkCooldown === 0 &&
331
+ (!urgentThreat.bestDodgeDirection || shouldForceShield(urgentThreat)) &&
332
+ urgentThreat.ticksToImpact >= RULES.BLINK_CAST_TIME &&
333
+ urgentThreat.ticksToImpact <= blinkWindowMax
334
+ )
309
335
  {
310
336
  const blinkTarget = getBlinkToIncreaseDistance(
311
- state.position,
337
+ position,
312
338
  enemy.position,
313
- state.projectiles,
339
+ projectiles,
314
340
  MIN_BLINK_ESCAPE_DISTANCE,
315
341
  );
316
- if (blinkTarget)
342
+ if (blinkTarget)
317
343
  {
318
- return {
319
- move: {x: 0, y: 0},
320
- startCast: {spell: 'blink', target: blinkTarget},
321
- };
344
+ const capped = capBlinkDistance(blinkTarget);
345
+ return blink(capped.x, capped.y);
322
346
  }
323
347
  }
324
348
 
325
349
  // === STEP 3: SHIELD — undodgeable or high-damage homing threats ===
326
350
  if (
327
- state.state === 'idle' &&
351
+ status === 'idle' &&
328
352
  urgentThreat &&
329
353
  (!urgentThreat.bestDodgeDirection || shouldForceShield(urgentThreat))
330
- )
354
+ )
331
355
  {
356
+ if (urgentThreat.canBlockInTime && urgentThreat.ticksToStartShield <= SHIELD_BUFFER)
357
+ {
358
+ return shield();
359
+ }
360
+ if (urgentThreat.canBlockInTime)
361
+ {
362
+ return shield();
363
+ }
364
+ }
365
+
366
+ // === Preemptive shield — shield when enemy is casting a missile at close range ===
367
+ if (
368
+ status === 'idle' &&
369
+ distance < 300 &&
370
+ enemy.status === 'casting' &&
371
+ enemy.castingSpell === 'missile'
372
+ )
373
+ {
374
+ const remainingCast = (enemy.castDuration ?? 0) - (enemy.castProgress ?? 0);
375
+ const estimatedFlightTicks = distance / 8;
376
+ const ticksUntilHit = remainingCast + estimatedFlightTicks;
377
+
332
378
  if (
333
- urgentThreat.canBlockInTime &&
334
- urgentThreat.ticksToStartShield <= SHIELD_BUFFER
335
- )
379
+ ticksUntilHit >= RULES.SHIELD_CAST_TIME &&
380
+ ticksUntilHit < RULES.SHIELD_CAST_TIME + 15
381
+ )
336
382
  {
337
- return {
338
- move: {x: 0, y: 0},
339
- startCast: {spell: 'shield'},
340
- };
383
+ return shield();
341
384
  }
342
385
  }
343
386
 
344
387
  // === STEP 5: PROACTIVE BLINK KITING ===
345
388
  if (
346
- state.state === 'idle' &&
347
- state.blinkCooldown === 0 &&
389
+ status === 'idle' &&
390
+ blinkCooldown === 0 &&
348
391
  (!urgentThreat || urgentThreat.bestDodgeDirection !== null)
349
392
  )
350
393
  {
351
394
  const closingRate = getClosingRate(
352
- state.position,
395
+ position,
353
396
  enemy.position,
354
397
  enemy.velocity,
355
398
  );
@@ -359,24 +402,25 @@ export const Infernalist: WizardFunction = ({state}) =>
359
402
  )
360
403
  {
361
404
  const blinkTarget = getBlinkToIncreaseDistance(
362
- state.position,
405
+ position,
363
406
  enemy.position,
364
- state.projectiles,
407
+ projectiles,
365
408
  MIN_BLINK_ESCAPE_DISTANCE,
366
409
  );
367
- if (blinkTarget)
410
+ if (blinkTarget)
368
411
  {
369
- return {
370
- move: {x: 0, y: 0},
371
- startCast: {spell: 'blink', target: blinkTarget},
372
- };
412
+ const capped = capBlinkDistance(blinkTarget);
413
+ return blink(capped.x, capped.y);
373
414
  }
374
415
  }
375
416
  }
376
417
 
377
418
  // === STEP 6: OFFENSE ===
378
- if (state.state === 'idle')
419
+ if (status === 'idle')
379
420
  {
421
+ const reposition = getPreCastReposition(position, enemy.position);
422
+ if (reposition) return moveAction(reposition.x, reposition.y);
423
+
380
424
  const enemyHP = Math.ceil(enemy.health);
381
425
  const vulnerabilityTicks = getEnemyVulnerabilityTicks(enemy);
382
426
 
@@ -387,7 +431,7 @@ export const Infernalist: WizardFunction = ({state}) =>
387
431
  const punishBudget = vulnerabilityTicks - flightTimeEstimate;
388
432
  const nextThreatTime = urgentThreat?.ticksToImpact ?? Infinity;
389
433
  const safeBudget =
390
- nextThreatTime - GCD_DURATION - SHIELD_CAST_TIME - SHIELD_BUFFER;
434
+ nextThreatTime - RULES.GCD_DURATION - RULES.SHIELD_CAST_TIME - SHIELD_BUFFER;
391
435
  const effectiveBudget = Math.min(
392
436
  punishBudget,
393
437
  safeBudget,
@@ -399,12 +443,12 @@ export const Infernalist: WizardFunction = ({state}) =>
399
443
  const config = fitMissileToBudget(effectiveBudget, distance, {
400
444
  minTurnRate: 0,
401
445
  maxDamage: enemyHP,
402
- lastMissileConfig: state.lastMissileConfig,
446
+ lastMissileConfig: lastMissileConfig,
403
447
  });
404
448
 
405
449
  if (config && config.damage >= MIN_USEFUL_DAMAGE)
406
450
  {
407
- const castTime = getMissileCastTime(config, state.lastMissileConfig);
451
+ const castTime = getMissileCastTime(config, lastMissileConfig);
408
452
  const actualFlight = distance / config.speed;
409
453
 
410
454
  if (
@@ -419,17 +463,9 @@ export const Infernalist: WizardFunction = ({state}) =>
419
463
  enemy.position,
420
464
  enemy.velocity,
421
465
  config.speed,
422
- state.position,
466
+ position,
423
467
  );
424
- return {
425
- move,
426
- startCast: {
427
- spell: 'missile',
428
- config,
429
- missileAI: config.turnRate > 0 ? HomingAI : StraightAI,
430
- direction: angleTo(state.position, aimTarget),
431
- },
432
- };
468
+ return missile(config, config.turnRate > 0 ? HomingAI : StraightAI, angleTo(position, aimTarget)).move(safeMove.x, safeMove.y);
433
469
  }
434
470
  }
435
471
  }
@@ -438,55 +474,39 @@ export const Infernalist: WizardFunction = ({state}) =>
438
474
  // --- STANDARD MODE: warmup-consistent homing missiles ---
439
475
  const nextThreatTime = urgentThreat?.ticksToImpact ?? Infinity;
440
476
  const safeBudget =
441
- nextThreatTime - GCD_DURATION - SHIELD_CAST_TIME - SHIELD_BUFFER;
477
+ nextThreatTime - RULES.GCD_DURATION - RULES.SHIELD_CAST_TIME - SHIELD_BUFFER;
442
478
  const budgetTicks = Math.min(safeBudget, MAX_CAST_BUDGET);
443
479
 
444
480
  const minDmg = Math.min(MIN_USEFUL_DAMAGE, enemyHP);
445
481
  let config = fitMissileToBudget(budgetTicks, distance, {
446
482
  minTurnRate,
447
483
  maxDamage: enemyHP,
448
- lastMissileConfig: state.lastMissileConfig,
484
+ lastMissileConfig: lastMissileConfig,
449
485
  });
450
486
 
451
- if (
452
- config &&
453
- config.damage >= minDmg &&
454
- isSafeToAttack(threats, safeAttackCastEstimate)
455
- )
487
+ if (config && config.damage >= minDmg)
456
488
  {
457
- return {
458
- move,
459
- startCast: {
460
- spell: 'missile',
461
- config,
462
- missileAI: HomingAI,
463
- direction: angleTo(state.position, enemy.position),
464
- },
465
- };
489
+ const actualCastTime = getMissileCastTime(config, lastMissileConfig);
490
+ if (isSafeToAttack(threats, actualCastTime))
491
+ {
492
+ return missile(config, HomingAI, angleTo(position, enemy.position)).move(safeMove.x, safeMove.y);
493
+ }
466
494
  }
467
495
 
468
- // Fallback: fire bigger missile, accept incoming hit
469
- if (!config || config.damage < minDmg)
496
+ // Fallback: fire bigger missile, accept incoming hit — but only if no undodgeable threat
497
+ if (!config || config.damage < minDmg)
470
498
  {
471
499
  config = fitMissileToBudget(MAX_CAST_BUDGET, distance, {
472
500
  minTurnRate,
473
501
  maxDamage: enemyHP,
474
- lastMissileConfig: state.lastMissileConfig,
502
+ lastMissileConfig: lastMissileConfig,
475
503
  });
476
- if (config)
504
+ if (config && (!urgentThreat || urgentThreat.bestDodgeDirection !== null))
477
505
  {
478
- return {
479
- move,
480
- startCast: {
481
- spell: 'missile',
482
- config,
483
- missileAI: HomingAI,
484
- direction: angleTo(state.position, enemy.position),
485
- },
486
- };
506
+ return missile(config, HomingAI, angleTo(position, enemy.position)).move(safeMove.x, safeMove.y);
487
507
  }
488
508
  }
489
509
  }
490
510
 
491
- return {move};
492
- };
511
+ return moveAction(safeMove.x, safeMove.y);
512
+ }