@vibemancer/core 0.1.0 → 0.1.2

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 (50) hide show
  1. package/README.md +28 -28
  2. package/dist/{chunk-L7Z7OFXD.js → chunk-OL7ETV6V.js} +3 -3
  3. package/dist/chunk-OL7ETV6V.js.map +1 -0
  4. package/dist/index-browser.d.ts +1 -1
  5. package/dist/index-browser.js +1 -1
  6. package/dist/index.js +1 -1
  7. package/package.json +79 -78
  8. package/src/bots/berserker/01_Stormchaser.ts +457 -457
  9. package/src/bots/berserker/02_Stormcaller.ts +417 -417
  10. package/src/bots/berserker/03_Stormforger.ts +481 -481
  11. package/src/bots/caster/01_Flamecaller.ts +286 -286
  12. package/src/bots/caster/02_Pyromancer.ts +350 -350
  13. package/src/bots/caster/03_Infernalist.ts +492 -492
  14. package/src/bots/defensive/01_Turtle.ts +151 -151
  15. package/src/bots/defensive/02_Sentinel.ts +134 -134
  16. package/src/bots/defensive/03_Golem.ts +357 -357
  17. package/src/bots/duelist/01_Battlemage.ts +433 -433
  18. package/src/bots/duelist/02_Warmage.ts +438 -438
  19. package/src/bots/duelist/03_Archmage.ts +588 -588
  20. package/src/bots/homing/01_Bonemancer.ts +67 -67
  21. package/src/bots/homing/02_Lich.ts +356 -356
  22. package/src/bots/homing/03_Archlich.ts +220 -220
  23. package/src/bots/kiter/01_Spellspinner.ts +398 -398
  24. package/src/bots/kiter/02_Spellweaver.ts +378 -378
  25. package/src/bots/kiter/03_Spellbinder.ts +448 -448
  26. package/src/bots/melee/01_Shadowblade.ts +270 -270
  27. package/src/bots/melee/02_Nightblade.ts +437 -437
  28. package/src/bots/melee/03_Voidblade.ts +582 -582
  29. package/src/bots/sniper/01_Spellshot.ts +385 -385
  30. package/src/bots/sniper/02_Spelltracer.ts +441 -441
  31. package/src/bots/sniper/03_Spellseeker.ts +546 -546
  32. package/src/bots/standalone/Critter.ts +89 -89
  33. package/src/bots/standalone/Doombringer.ts +91 -91
  34. package/src/bots/standalone/Hogger.ts +228 -228
  35. package/src/bots/standalone/Rookie.ts +50 -50
  36. package/src/bots/standalone/TargetDummy.ts +21 -21
  37. package/src/bots/test/cheater.ts +405 -405
  38. package/src/bots/test/crasher.ts +81 -81
  39. package/src/engine/hooks-runtime.ts +394 -394
  40. package/src/engine/manual-match.ts +289 -289
  41. package/src/engine/missile-templates.ts +155 -155
  42. package/src/engine/physics.ts +143 -143
  43. package/src/engine/simulation.ts +828 -828
  44. package/src/engine/spells.ts +128 -128
  45. package/src/engine-version.ts +1 -1
  46. package/src/index.ts +23 -23
  47. package/src/rules.ts +254 -254
  48. package/src/types.ts +193 -193
  49. package/src/utils/index.ts +6 -6
  50. package/dist/chunk-L7Z7OFXD.js.map +0 -1
@@ -1,357 +1,357 @@
1
- import {WizardFunction, MissileAIFunction} from '../../types.js';
2
- import {distanceTo} from '../../utils/distance.js';
3
- import {angleTo} from '../../utils/angles.js';
4
- import {
5
- GCD_DURATION,
6
- SHIELD_CAST_TIME,
7
- BLINK_CAST_TIME,
8
- } from '../../rules.js';
9
- import {getMissileCastTime, getLeadPosition} from '../../utils/combat.js';
10
- import {
11
- getThreats,
12
- getMostUrgentThreat,
13
- getBlinkToCenter,
14
- fitMissileToBudget,
15
- isSafeToAttack,
16
- MAX_CAST_BUDGET,
17
- MIN_USEFUL_DAMAGE,
18
- getEnemyVulnerabilityTicks,
19
- } from '../shared.js';
20
- import {useParam} from '../../engine/params-runtime.js';
21
-
22
- const SHIELD_BUFFER = 3;
23
- const SHIELD_HOLD_TICKS = 5;
24
-
25
- /**
26
- * Standard homing missile AI — tracks enemy position.
27
- */
28
- const HomingAI: MissileAIFunction = ({worldState}) =>
29
- {
30
- const enemy = worldState.enemies[0];
31
- return {turnToward: enemy?.position};
32
- };
33
-
34
- /**
35
- * Straight missile AI — no homing, flies in initial direction.
36
- */
37
- const StraightAI: MissileAIFunction = () => ({});
38
-
39
- /**
40
- * Bot: Golem
41
- *
42
- * BEHAVIOR: Stationary fortress with perfect shield timing and devastating
43
- * counterattacks during enemy vulnerability windows. Reads enemy cast/GCD
44
- * state to time punish missiles that land when the enemy can't shield.
45
- * Handles multi-missile volleys by holding shield through consecutive impacts.
46
- * Uses progressive cast-cancel thresholds for optimal damage trading.
47
- *
48
- * KEY IMPROVEMENTS OVER SENTINEL:
49
- * - Counterattack punish: fires during enemy cast/GCD recovery
50
- * - Multi-threat volley awareness: holds shield through consecutive hits
51
- * - Progressive cast-cancel: graduated damage thresholds
52
- * - Perfect shield timing: uses ticksToStartShield precisely
53
- *
54
- * PROGRESSION LINE: Turtle → Sentinel → Golem
55
- * TIER: 3 (elite Defensive line)
56
- */
57
- export const Golem: WizardFunction = ({state}) =>
58
- {
59
- // Positive turn rate ensures homing missiles that hit moving targets.
60
- // Hardcoded — optimizer incorrectly pushes this negative for stationary matchups.
61
- const minTurnRate = 0.5;
62
- const preemptiveShieldRange = useParam('preemptiveShieldRange', 150, {
63
- range: 85,
64
- min: 0,
65
- steps: 7,
66
- });
67
- const shieldCancelThreshold = useParam('shieldCancelThreshold', 100, {
68
- range: 75,
69
- min: 10,
70
- });
71
- const punishMinVulnerability = useParam('punishMinVulnerability', 310, {
72
- range: 30,
73
- min: 10,
74
- steps: 5,
75
- });
76
- const preferBlinkDodge =
77
- useParam('preferBlinkDodge', 1, {min: 0, max: 1, steps: 2}) >= 0.5;
78
- const cancelHeavyDmg = useParam('cancelHeavyDmg', 14, {
79
- range: 15,
80
- min: 5,
81
- max: 40,
82
- steps: 5,
83
- });
84
- const cancelMediumDmg = useParam('cancelMediumDmg', 12, {
85
- range: 10,
86
- min: 3,
87
- max: 25,
88
- steps: 5,
89
- });
90
- const cancelCastRatio = useParam('cancelCastRatio', 0.8, {
91
- range: 0.4,
92
- min: 0.3,
93
- max: 1.0,
94
- steps: 5,
95
- });
96
- const cancelLightDmg = useParam('cancelLightDmg', 8, {
97
- range: 5,
98
- min: 1,
99
- max: 15,
100
- steps: 5,
101
- });
102
- const cancelEarlyCastRatio = useParam('cancelEarlyCastRatio', 0.7, {
103
- range: 0.3,
104
- min: 0.0,
105
- max: 0.7,
106
- steps: 5,
107
- });
108
- const blinkWindowMax = useParam('blinkWindowMax', 50, {
109
- range: 40,
110
- min: 20,
111
- max: 120,
112
- steps: 7,
113
- });
114
- const preemptiveShieldBuffer = useParam('preemptiveShieldBuffer', 11.3, {
115
- range: 5,
116
- min: 0,
117
- max: 15,
118
- steps: 5,
119
- });
120
- const flightTimeDivisor = useParam('flightTimeDivisor', 9.5, {
121
- range: 4,
122
- min: 2,
123
- max: 12,
124
- steps: 5,
125
- });
126
-
127
- const enemy = state.enemies[0];
128
- const threats = getThreats(state);
129
- const urgentThreat = getMostUrgentThreat(threats);
130
- const hittingThreats = threats.filter((t) => t.willHit);
131
-
132
- // === STEP 1: CHANNELING — hold through volleys, cancel when safe ===
133
- if (state.state === 'channeling' && state.channelingSpell === 'shield')
134
- {
135
- // Hold if any missile is about to hit
136
- const imminentThreat = hittingThreats.find(
137
- (t) => t.ticksToImpact <= SHIELD_HOLD_TICKS,
138
- );
139
- if (imminentThreat)
140
- {
141
- return {move: {x: 0, y: 0}};
142
- }
143
-
144
- // Hold if next threat will arrive before we can reshield
145
- const nextHitting = hittingThreats[0];
146
- const ticksToReshield = SHIELD_CAST_TIME + SHIELD_BUFFER;
147
- if (nextHitting && nextHitting.ticksToImpact <= ticksToReshield)
148
- {
149
- return {move: {x: 0, y: 0}};
150
- }
151
-
152
- // Cancel shield if no threat or threat is far enough
153
- if (!nextHitting || nextHitting.ticksToImpact > shieldCancelThreshold)
154
- {
155
- return {move: {x: 0, y: 0}, cancel: true};
156
- }
157
- return {move: {x: 0, y: 0}};
158
- }
159
-
160
- // If casting shield, let it finish
161
- if (state.state === 'casting' && state.castingSpell === 'shield')
162
- {
163
- return {move: {x: 0, y: 0}};
164
- }
165
-
166
- // === STEP 2: PROGRESSIVE CAST-CANCEL — evaluate damage tradeoff ===
167
- if (
168
- state.state === 'casting' &&
169
- state.castingSpell === 'missile' &&
170
- urgentThreat
171
- )
172
- {
173
- // Sentinel is stationary — can't dodge. Must always consider canceling.
174
- const remainingCast = (state.castDuration ?? 0) - (state.castProgress ?? 0);
175
- const defenseTime =
176
- state.blinkCooldown === 0
177
- ? BLINK_CAST_TIME
178
- : SHIELD_CAST_TIME + SHIELD_BUFFER;
179
- const willArriveInWindow =
180
- urgentThreat.ticksToImpact <= remainingCast + GCD_DURATION + defenseTime;
181
-
182
- if (willArriveInWindow)
183
- {
184
- const incomingDamage = urgentThreat.projectile.damage;
185
- const castRatio = (state.castProgress ?? 0) / (state.castDuration ?? 1);
186
-
187
- // Always cancel for lethal or heavy damage
188
- if (incomingDamage >= state.health || incomingDamage >= cancelHeavyDmg)
189
- {
190
- return {move: {x: 0, y: 0}, cancel: true};
191
- }
192
- // Significant damage + early/mid cast: cancel
193
- if (incomingDamage >= cancelMediumDmg && castRatio < cancelCastRatio)
194
- {
195
- return {move: {x: 0, y: 0}, cancel: true};
196
- }
197
- // Medium damage + very early cast: cancel
198
- if (
199
- incomingDamage >= cancelLightDmg &&
200
- castRatio < cancelEarlyCastRatio
201
- )
202
- {
203
- return {move: {x: 0, y: 0}, cancel: true};
204
- }
205
- }
206
- }
207
-
208
- // === STEP 3: BLINK-DODGE — 100% damage avoidance (optimizer-controlled) ===
209
- if (
210
- preferBlinkDodge &&
211
- state.state === 'idle' &&
212
- urgentThreat &&
213
- state.blinkCooldown === 0 &&
214
- urgentThreat.ticksToImpact >= BLINK_CAST_TIME &&
215
- urgentThreat.ticksToImpact <= blinkWindowMax
216
- )
217
- {
218
- return {
219
- move: {x: 0, y: 0},
220
- startCast: {spell: 'blink', target: getBlinkToCenter(state.position)},
221
- };
222
- }
223
-
224
- // === STEP 3b: SHIELD — fallback when blink unavailable (90% block) ===
225
- if (
226
- state.state === 'idle' &&
227
- urgentThreat &&
228
- urgentThreat.canBlockInTime &&
229
- urgentThreat.ticksToStartShield <= SHIELD_BUFFER
230
- )
231
- {
232
- return {
233
- move: {x: 0, y: 0},
234
- startCast: {spell: 'shield'},
235
- };
236
- }
237
-
238
- // === STEP 4: PREEMPTIVE SHIELD — enemy casting at close range ===
239
- if (
240
- state.state === 'idle' &&
241
- enemy &&
242
- enemy.state === 'casting' &&
243
- enemy.castingSpell === 'missile'
244
- )
245
- {
246
- const distance = distanceTo(state.position, enemy.position);
247
- if (distance < preemptiveShieldRange)
248
- {
249
- const remainingCast =
250
- (enemy.castDuration ?? 0) - (enemy.castProgress ?? 0);
251
- const estimatedFlightTicks = distance / flightTimeDivisor;
252
- const ticksUntilHit = remainingCast + estimatedFlightTicks;
253
- if (
254
- ticksUntilHit <
255
- SHIELD_CAST_TIME + SHIELD_BUFFER + preemptiveShieldBuffer
256
- )
257
- {
258
- return {
259
- move: {x: 0, y: 0},
260
- startCast: {spell: 'shield'},
261
- };
262
- }
263
- }
264
- }
265
-
266
- // === STEP 5: OFFENSE — punish mode + standard counterattack ===
267
- if (state.state === 'idle' && enemy)
268
- {
269
- const offenseDefenseTime =
270
- state.blinkCooldown === 0
271
- ? BLINK_CAST_TIME
272
- : SHIELD_CAST_TIME + SHIELD_BUFFER;
273
- const distance = distanceTo(state.position, enemy.position);
274
- const enemyHP = Math.ceil(enemy.health);
275
- const vulnerabilityTicks = getEnemyVulnerabilityTicks(enemy);
276
-
277
- // --- PUNISH MODE: exploit enemy cast/GCD vulnerability ---
278
- if (vulnerabilityTicks >= punishMinVulnerability)
279
- {
280
- const flightTimeEstimate = distance / flightTimeDivisor;
281
- const punishBudget = vulnerabilityTicks - flightTimeEstimate;
282
- const nextThreatTime = urgentThreat?.ticksToImpact ?? Infinity;
283
- const safeBudget = nextThreatTime - GCD_DURATION - offenseDefenseTime;
284
- const effectiveBudget = Math.min(
285
- punishBudget,
286
- safeBudget,
287
- MAX_CAST_BUDGET,
288
- );
289
-
290
- if (effectiveBudget > 0)
291
- {
292
- const config = fitMissileToBudget(effectiveBudget, distance, {
293
- minTurnRate: 0,
294
- maxDamage: enemyHP,
295
- lastMissileConfig: state.lastMissileConfig,
296
- });
297
-
298
- if (config && config.damage >= MIN_USEFUL_DAMAGE)
299
- {
300
- const castTime = getMissileCastTime(config, state.lastMissileConfig);
301
- const actualFlight = distance / config.speed;
302
-
303
- if (
304
- castTime + actualFlight < vulnerabilityTicks &&
305
- isSafeToAttack(threats, castTime)
306
- )
307
- {
308
- const aimTarget =
309
- config.turnRate > 0
310
- ? enemy.position
311
- : getLeadPosition(
312
- enemy.position,
313
- enemy.velocity,
314
- config.speed,
315
- state.position,
316
- );
317
- return {
318
- move: {x: 0, y: 0},
319
- startCast: {
320
- spell: 'missile',
321
- config,
322
- missileAI: config.turnRate > 0 ? HomingAI : StraightAI,
323
- direction: angleTo(state.position, aimTarget),
324
- },
325
- };
326
- }
327
- }
328
- }
329
- }
330
-
331
- // --- STANDARD MODE: adaptive homing in safe windows ---
332
- const nextThreatTime = urgentThreat?.ticksToImpact ?? Infinity;
333
- const safeBudget = nextThreatTime - GCD_DURATION - offenseDefenseTime;
334
- const budgetTicks = Math.min(safeBudget, MAX_CAST_BUDGET);
335
-
336
- const config = fitMissileToBudget(budgetTicks, distance, {
337
- minTurnRate,
338
- maxDamage: enemyHP,
339
- lastMissileConfig: state.lastMissileConfig,
340
- });
341
-
342
- if (config && config.damage >= Math.min(MIN_USEFUL_DAMAGE, enemyHP))
343
- {
344
- return {
345
- move: {x: 0, y: 0},
346
- startCast: {
347
- spell: 'missile',
348
- config,
349
- missileAI: config.turnRate > 0 ? HomingAI : StraightAI,
350
- direction: angleTo(state.position, enemy.position),
351
- },
352
- };
353
- }
354
- }
355
-
356
- return {move: {x: 0, y: 0}};
357
- };
1
+ import {WizardFunction, MissileAIFunction} from '../../types.js';
2
+ import {distanceTo} from '../../utils/distance.js';
3
+ import {angleTo} from '../../utils/angles.js';
4
+ import {
5
+ GCD_DURATION,
6
+ SHIELD_CAST_TIME,
7
+ BLINK_CAST_TIME,
8
+ } from '../../rules.js';
9
+ import {getMissileCastTime, getLeadPosition} from '../../utils/combat.js';
10
+ import {
11
+ getThreats,
12
+ getMostUrgentThreat,
13
+ getBlinkToCenter,
14
+ fitMissileToBudget,
15
+ isSafeToAttack,
16
+ MAX_CAST_BUDGET,
17
+ MIN_USEFUL_DAMAGE,
18
+ getEnemyVulnerabilityTicks,
19
+ } from '../shared.js';
20
+ import {useParam} from '../../engine/params-runtime.js';
21
+
22
+ const SHIELD_BUFFER = 3;
23
+ const SHIELD_HOLD_TICKS = 5;
24
+
25
+ /**
26
+ * Standard homing missile AI — tracks enemy position.
27
+ */
28
+ const HomingAI: MissileAIFunction = ({worldState}) =>
29
+ {
30
+ const enemy = worldState.enemies[0];
31
+ return {turnToward: enemy?.position};
32
+ };
33
+
34
+ /**
35
+ * Straight missile AI — no homing, flies in initial direction.
36
+ */
37
+ const StraightAI: MissileAIFunction = () => ({});
38
+
39
+ /**
40
+ * Bot: Golem
41
+ *
42
+ * BEHAVIOR: Stationary fortress with perfect shield timing and devastating
43
+ * counterattacks during enemy vulnerability windows. Reads enemy cast/GCD
44
+ * state to time punish missiles that land when the enemy can't shield.
45
+ * Handles multi-missile volleys by holding shield through consecutive impacts.
46
+ * Uses progressive cast-cancel thresholds for optimal damage trading.
47
+ *
48
+ * KEY IMPROVEMENTS OVER SENTINEL:
49
+ * - Counterattack punish: fires during enemy cast/GCD recovery
50
+ * - Multi-threat volley awareness: holds shield through consecutive hits
51
+ * - Progressive cast-cancel: graduated damage thresholds
52
+ * - Perfect shield timing: uses ticksToStartShield precisely
53
+ *
54
+ * PROGRESSION LINE: Turtle → Sentinel → Golem
55
+ * TIER: 3 (elite Defensive line)
56
+ */
57
+ export const Golem: WizardFunction = ({state}) =>
58
+ {
59
+ // Positive turn rate ensures homing missiles that hit moving targets.
60
+ // Hardcoded — optimizer incorrectly pushes this negative for stationary matchups.
61
+ const minTurnRate = 0.5;
62
+ const preemptiveShieldRange = useParam('preemptiveShieldRange', 150, {
63
+ range: 85,
64
+ min: 0,
65
+ steps: 7,
66
+ });
67
+ const shieldCancelThreshold = useParam('shieldCancelThreshold', 100, {
68
+ range: 75,
69
+ min: 10,
70
+ });
71
+ const punishMinVulnerability = useParam('punishMinVulnerability', 310, {
72
+ range: 30,
73
+ min: 10,
74
+ steps: 5,
75
+ });
76
+ const preferBlinkDodge =
77
+ useParam('preferBlinkDodge', 1, {min: 0, max: 1, steps: 2}) >= 0.5;
78
+ const cancelHeavyDmg = useParam('cancelHeavyDmg', 14, {
79
+ range: 15,
80
+ min: 5,
81
+ max: 40,
82
+ steps: 5,
83
+ });
84
+ const cancelMediumDmg = useParam('cancelMediumDmg', 12, {
85
+ range: 10,
86
+ min: 3,
87
+ max: 25,
88
+ steps: 5,
89
+ });
90
+ const cancelCastRatio = useParam('cancelCastRatio', 0.8, {
91
+ range: 0.4,
92
+ min: 0.3,
93
+ max: 1.0,
94
+ steps: 5,
95
+ });
96
+ const cancelLightDmg = useParam('cancelLightDmg', 8, {
97
+ range: 5,
98
+ min: 1,
99
+ max: 15,
100
+ steps: 5,
101
+ });
102
+ const cancelEarlyCastRatio = useParam('cancelEarlyCastRatio', 0.7, {
103
+ range: 0.3,
104
+ min: 0.0,
105
+ max: 0.7,
106
+ steps: 5,
107
+ });
108
+ const blinkWindowMax = useParam('blinkWindowMax', 50, {
109
+ range: 40,
110
+ min: 20,
111
+ max: 120,
112
+ steps: 7,
113
+ });
114
+ const preemptiveShieldBuffer = useParam('preemptiveShieldBuffer', 11.3, {
115
+ range: 5,
116
+ min: 0,
117
+ max: 15,
118
+ steps: 5,
119
+ });
120
+ const flightTimeDivisor = useParam('flightTimeDivisor', 9.5, {
121
+ range: 4,
122
+ min: 2,
123
+ max: 12,
124
+ steps: 5,
125
+ });
126
+
127
+ const enemy = state.enemies[0];
128
+ const threats = getThreats(state);
129
+ const urgentThreat = getMostUrgentThreat(threats);
130
+ const hittingThreats = threats.filter((t) => t.willHit);
131
+
132
+ // === STEP 1: CHANNELING — hold through volleys, cancel when safe ===
133
+ if (state.state === 'channeling' && state.channelingSpell === 'shield')
134
+ {
135
+ // Hold if any missile is about to hit
136
+ const imminentThreat = hittingThreats.find(
137
+ (t) => t.ticksToImpact <= SHIELD_HOLD_TICKS,
138
+ );
139
+ if (imminentThreat)
140
+ {
141
+ return {move: {x: 0, y: 0}};
142
+ }
143
+
144
+ // Hold if next threat will arrive before we can reshield
145
+ const nextHitting = hittingThreats[0];
146
+ const ticksToReshield = SHIELD_CAST_TIME + SHIELD_BUFFER;
147
+ if (nextHitting && nextHitting.ticksToImpact <= ticksToReshield)
148
+ {
149
+ return {move: {x: 0, y: 0}};
150
+ }
151
+
152
+ // Cancel shield if no threat or threat is far enough
153
+ if (!nextHitting || nextHitting.ticksToImpact > shieldCancelThreshold)
154
+ {
155
+ return {move: {x: 0, y: 0}, cancel: true};
156
+ }
157
+ return {move: {x: 0, y: 0}};
158
+ }
159
+
160
+ // If casting shield, let it finish
161
+ if (state.state === 'casting' && state.castingSpell === 'shield')
162
+ {
163
+ return {move: {x: 0, y: 0}};
164
+ }
165
+
166
+ // === STEP 2: PROGRESSIVE CAST-CANCEL — evaluate damage tradeoff ===
167
+ if (
168
+ state.state === 'casting' &&
169
+ state.castingSpell === 'missile' &&
170
+ urgentThreat
171
+ )
172
+ {
173
+ // Sentinel is stationary — can't dodge. Must always consider canceling.
174
+ const remainingCast = (state.castDuration ?? 0) - (state.castProgress ?? 0);
175
+ const defenseTime =
176
+ state.blinkCooldown === 0
177
+ ? BLINK_CAST_TIME
178
+ : SHIELD_CAST_TIME + SHIELD_BUFFER;
179
+ const willArriveInWindow =
180
+ urgentThreat.ticksToImpact <= remainingCast + GCD_DURATION + defenseTime;
181
+
182
+ if (willArriveInWindow)
183
+ {
184
+ const incomingDamage = urgentThreat.projectile.damage;
185
+ const castRatio = (state.castProgress ?? 0) / (state.castDuration ?? 1);
186
+
187
+ // Always cancel for lethal or heavy damage
188
+ if (incomingDamage >= state.health || incomingDamage >= cancelHeavyDmg)
189
+ {
190
+ return {move: {x: 0, y: 0}, cancel: true};
191
+ }
192
+ // Significant damage + early/mid cast: cancel
193
+ if (incomingDamage >= cancelMediumDmg && castRatio < cancelCastRatio)
194
+ {
195
+ return {move: {x: 0, y: 0}, cancel: true};
196
+ }
197
+ // Medium damage + very early cast: cancel
198
+ if (
199
+ incomingDamage >= cancelLightDmg &&
200
+ castRatio < cancelEarlyCastRatio
201
+ )
202
+ {
203
+ return {move: {x: 0, y: 0}, cancel: true};
204
+ }
205
+ }
206
+ }
207
+
208
+ // === STEP 3: BLINK-DODGE — 100% damage avoidance (optimizer-controlled) ===
209
+ if (
210
+ preferBlinkDodge &&
211
+ state.state === 'idle' &&
212
+ urgentThreat &&
213
+ state.blinkCooldown === 0 &&
214
+ urgentThreat.ticksToImpact >= BLINK_CAST_TIME &&
215
+ urgentThreat.ticksToImpact <= blinkWindowMax
216
+ )
217
+ {
218
+ return {
219
+ move: {x: 0, y: 0},
220
+ startCast: {spell: 'blink', target: getBlinkToCenter(state.position)},
221
+ };
222
+ }
223
+
224
+ // === STEP 3b: SHIELD — fallback when blink unavailable (90% block) ===
225
+ if (
226
+ state.state === 'idle' &&
227
+ urgentThreat &&
228
+ urgentThreat.canBlockInTime &&
229
+ urgentThreat.ticksToStartShield <= SHIELD_BUFFER
230
+ )
231
+ {
232
+ return {
233
+ move: {x: 0, y: 0},
234
+ startCast: {spell: 'shield'},
235
+ };
236
+ }
237
+
238
+ // === STEP 4: PREEMPTIVE SHIELD — enemy casting at close range ===
239
+ if (
240
+ state.state === 'idle' &&
241
+ enemy &&
242
+ enemy.state === 'casting' &&
243
+ enemy.castingSpell === 'missile'
244
+ )
245
+ {
246
+ const distance = distanceTo(state.position, enemy.position);
247
+ if (distance < preemptiveShieldRange)
248
+ {
249
+ const remainingCast =
250
+ (enemy.castDuration ?? 0) - (enemy.castProgress ?? 0);
251
+ const estimatedFlightTicks = distance / flightTimeDivisor;
252
+ const ticksUntilHit = remainingCast + estimatedFlightTicks;
253
+ if (
254
+ ticksUntilHit <
255
+ SHIELD_CAST_TIME + SHIELD_BUFFER + preemptiveShieldBuffer
256
+ )
257
+ {
258
+ return {
259
+ move: {x: 0, y: 0},
260
+ startCast: {spell: 'shield'},
261
+ };
262
+ }
263
+ }
264
+ }
265
+
266
+ // === STEP 5: OFFENSE — punish mode + standard counterattack ===
267
+ if (state.state === 'idle' && enemy)
268
+ {
269
+ const offenseDefenseTime =
270
+ state.blinkCooldown === 0
271
+ ? BLINK_CAST_TIME
272
+ : SHIELD_CAST_TIME + SHIELD_BUFFER;
273
+ const distance = distanceTo(state.position, enemy.position);
274
+ const enemyHP = Math.ceil(enemy.health);
275
+ const vulnerabilityTicks = getEnemyVulnerabilityTicks(enemy);
276
+
277
+ // --- PUNISH MODE: exploit enemy cast/GCD vulnerability ---
278
+ if (vulnerabilityTicks >= punishMinVulnerability)
279
+ {
280
+ const flightTimeEstimate = distance / flightTimeDivisor;
281
+ const punishBudget = vulnerabilityTicks - flightTimeEstimate;
282
+ const nextThreatTime = urgentThreat?.ticksToImpact ?? Infinity;
283
+ const safeBudget = nextThreatTime - GCD_DURATION - offenseDefenseTime;
284
+ const effectiveBudget = Math.min(
285
+ punishBudget,
286
+ safeBudget,
287
+ MAX_CAST_BUDGET,
288
+ );
289
+
290
+ if (effectiveBudget > 0)
291
+ {
292
+ const config = fitMissileToBudget(effectiveBudget, distance, {
293
+ minTurnRate: 0,
294
+ maxDamage: enemyHP,
295
+ lastMissileConfig: state.lastMissileConfig,
296
+ });
297
+
298
+ if (config && config.damage >= MIN_USEFUL_DAMAGE)
299
+ {
300
+ const castTime = getMissileCastTime(config, state.lastMissileConfig);
301
+ const actualFlight = distance / config.speed;
302
+
303
+ if (
304
+ castTime + actualFlight < vulnerabilityTicks &&
305
+ isSafeToAttack(threats, castTime)
306
+ )
307
+ {
308
+ const aimTarget =
309
+ config.turnRate > 0
310
+ ? enemy.position
311
+ : getLeadPosition(
312
+ enemy.position,
313
+ enemy.velocity,
314
+ config.speed,
315
+ state.position,
316
+ );
317
+ return {
318
+ move: {x: 0, y: 0},
319
+ startCast: {
320
+ spell: 'missile',
321
+ config,
322
+ missileAI: config.turnRate > 0 ? HomingAI : StraightAI,
323
+ direction: angleTo(state.position, aimTarget),
324
+ },
325
+ };
326
+ }
327
+ }
328
+ }
329
+ }
330
+
331
+ // --- STANDARD MODE: adaptive homing in safe windows ---
332
+ const nextThreatTime = urgentThreat?.ticksToImpact ?? Infinity;
333
+ const safeBudget = nextThreatTime - GCD_DURATION - offenseDefenseTime;
334
+ const budgetTicks = Math.min(safeBudget, MAX_CAST_BUDGET);
335
+
336
+ const config = fitMissileToBudget(budgetTicks, distance, {
337
+ minTurnRate,
338
+ maxDamage: enemyHP,
339
+ lastMissileConfig: state.lastMissileConfig,
340
+ });
341
+
342
+ if (config && config.damage >= Math.min(MIN_USEFUL_DAMAGE, enemyHP))
343
+ {
344
+ return {
345
+ move: {x: 0, y: 0},
346
+ startCast: {
347
+ spell: 'missile',
348
+ config,
349
+ missileAI: config.turnRate > 0 ? HomingAI : StraightAI,
350
+ direction: angleTo(state.position, enemy.position),
351
+ },
352
+ };
353
+ }
354
+ }
355
+
356
+ return {move: {x: 0, y: 0}};
357
+ };