@vibemancer/core 0.1.0 → 0.1.1

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-7VDJHO22.js} +3 -3
  3. package/dist/chunk-7VDJHO22.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,492 +1,492 @@
1
- import {WizardFunction, MissileAIFunction} from '../../types.js';
2
- import {angleTo} from '../../utils/angles.js';
3
- 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';
10
- import {getMissileCastTime, getLeadPosition} from '../../utils/combat.js';
11
- import {
12
- getThreats,
13
- getMostUrgentThreat,
14
- getDodgeDirectionForMovement,
15
- isSafeToAttack,
16
- getBlinkToIncreaseDistance,
17
- fitMissileToBudget,
18
- shouldForceShield,
19
- MIN_BLINK_ESCAPE_DISTANCE,
20
- MAX_CAST_BUDGET,
21
- MIN_USEFUL_DAMAGE,
22
- getClosingRate,
23
- getEnemyVulnerabilityTicks,
24
- } from '../shared.js';
25
- import {useParam} from '../../engine/params-runtime.js';
26
-
27
- const WALL_BUFFER = 60;
28
- const SHIELD_BUFFER = 3;
29
-
30
- const HomingAI: MissileAIFunction = ({worldState}) =>
31
- {
32
- const enemy = worldState.enemies[0];
33
- return {turnToward: enemy?.position};
34
- };
35
-
36
- const StraightAI: MissileAIFunction = () => ({});
37
-
38
- function isNearWall(position: {x: number; y: number}): boolean
39
- {
40
- 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
45
- );
46
- }
47
-
48
- function smartStrafe(
49
- position: {x: number; y: number},
50
- enemy: {position: {x: number; y: number}},
51
- dodgeDirection: {x: number; y: number} | null,
52
- tick: number,
53
- strafeCyclePeriod: number,
54
- strafeIntensity: number,
55
- ): {x: number; y: number}
56
- {
57
- const deltaX = enemy.position.x - position.x;
58
- const deltaY = enemy.position.y - position.y;
59
- const len = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
60
- if (len === 0) return {x: 0, y: 0};
61
-
62
- const cw = {x: -deltaY / len, y: deltaX / len};
63
- const ccw = {x: deltaY / len, y: -deltaX / len};
64
-
65
- let dir: {x: number; y: number};
66
-
67
- if (dodgeDirection)
68
- {
69
- dir = dodgeDirection;
70
- }
71
- else if (isNearWall(position))
72
- {
73
- const fCw = {x: position.x + cw.x * 100, y: position.y + cw.y * 100};
74
- 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);
76
- const sCcw = Math.min(
77
- fCcw.x,
78
- ARENA_SIZE - fCcw.x,
79
- fCcw.y,
80
- ARENA_SIZE - fCcw.y,
81
- );
82
- dir = sCw > sCcw ? cw : ccw;
83
- }
84
- else
85
- {
86
- const cycle = Math.floor(tick / strafeCyclePeriod) % 2;
87
- dir = cycle === 0 ? cw : ccw;
88
- }
89
-
90
- const intensity = dodgeDirection ? 100 : strafeIntensity;
91
- return {x: dir.x * intensity, y: dir.y * intensity};
92
- }
93
-
94
- /**
95
- * Bot: Infernalist
96
- *
97
- * BEHAVIOR: Rapid-fire caster that exploits warmup bonus for accelerating DPS.
98
- * Fires consistent homing missiles to build warmup, punishes vulnerability windows
99
- * with warmup-boosted fast casts. Proactive blink kiting when enemy closes.
100
- *
101
- * KEY IMPROVEMENTS OVER PYROMANCER:
102
- * - Warmup exploitation: always passes lastMissileConfig for bonus
103
- * - Punish mode: straight missiles during enemy vulnerability
104
- * - Proactive blink kiting: monitors closing rate
105
- * - Progressive cast-cancel: graduated thresholds
106
- *
107
- * PROGRESSION LINE: Flamecaller → Pyromancer → Infernalist
108
- * TIER: 3 (elite Caster line)
109
- */
110
- export const Infernalist: WizardFunction = ({state}) =>
111
- {
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, {
115
- range: 150,
116
- min: 0,
117
- });
118
- const closingRateThreshold = useParam('closingRateThreshold', 0.94, {
119
- range: 0.65,
120
- min: 0,
121
- steps: 5,
122
- });
123
- const punishMinVulnerability = useParam('punishMinVulnerability', 340, {
124
- range: 30,
125
- min: 10,
126
- steps: 5,
127
- });
128
- const preferBlinkDodge =
129
- useParam('preferBlinkDodge', 0, {min: 0, max: 1, steps: 2}) >= 0.5;
130
- const strafeCyclePeriod = useParam('strafeCyclePeriod', 109, {
131
- range: 75,
132
- min: 80,
133
- max: 250,
134
- });
135
- const strafeIntensity = useParam('strafeIntensity', 23, {
136
- range: 40,
137
- min: 20,
138
- max: 100,
139
- steps: 7,
140
- });
141
- const approachSpeed = useParam('approachSpeed', 53, {
142
- range: 30,
143
- min: 10,
144
- max: 70,
145
- steps: 7,
146
- });
147
- const distanceDeadZone = useParam('distanceDeadZone', 34, {
148
- range: 30,
149
- min: 5,
150
- max: 80,
151
- steps: 7,
152
- });
153
- const cancelHeavyDmg = useParam('cancelHeavyDmg', 40, {
154
- range: 15,
155
- min: 5,
156
- max: 40,
157
- steps: 5,
158
- });
159
- const cancelMediumDmg = useParam('cancelMediumDmg', 9, {
160
- range: 10,
161
- min: 3,
162
- max: 25,
163
- steps: 5,
164
- });
165
- const cancelCastRatio = useParam('cancelCastRatio', 0.78, {
166
- range: 0.4,
167
- min: 0.3,
168
- max: 1.0,
169
- steps: 5,
170
- });
171
- const shieldCancelThreshold = useParam('shieldCancelThreshold', 100, {
172
- range: 75,
173
- min: 50,
174
- max: 300,
175
- });
176
- const blinkWindowMax = useParam('blinkWindowMax', 75, {
177
- range: 40,
178
- min: 20,
179
- max: 120,
180
- steps: 7,
181
- });
182
- const proactiveBlinkRange = useParam('proactiveBlinkRange', 100, {
183
- range: 150,
184
- min: 100,
185
- max: 500,
186
- steps: 7,
187
- });
188
- const flightTimeDivisor = useParam('flightTimeDivisor', 5.4, {
189
- range: 4,
190
- min: 2,
191
- max: 12,
192
- steps: 5,
193
- });
194
- const safeAttackCastEstimate = useParam('safeAttackCastEstimate', 35, {
195
- range: 25,
196
- min: 10,
197
- max: 80,
198
- steps: 5,
199
- });
200
-
201
- const enemy = state.enemies[0];
202
- if (!enemy)
203
- {
204
- return {move: {x: 0, y: 0}};
205
- }
206
-
207
- const distance = distanceTo(state.position, enemy.position);
208
- const threats = getThreats(state);
209
- const urgentThreat = getMostUrgentThreat(threats);
210
- const dodgeDirection = getDodgeDirectionForMovement(threats, state.position);
211
- const strafe = smartStrafe(
212
- state.position,
213
- enemy,
214
- dodgeDirection,
215
- state.tick,
216
- strafeCyclePeriod,
217
- strafeIntensity,
218
- );
219
-
220
- // Distance management with wall-aware retreat
221
- let moveX = strafe.x;
222
- let moveY = strafe.y;
223
-
224
- if (!dodgeDirection)
225
- {
226
- const deltaX = enemy.position.x - state.position.x;
227
- const deltaY = enemy.position.y - state.position.y;
228
- const norm = distance > 0 ? distance : 1;
229
-
230
- if (distance > targetDistance + distanceDeadZone)
231
- {
232
- moveX += (deltaX / norm) * approachSpeed;
233
- moveY += (deltaY / norm) * approachSpeed;
234
- }
235
- else if (distance < targetDistance - distanceDeadZone)
236
- {
237
- let rx = -(deltaX / norm) * approachSpeed;
238
- 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;
243
- moveX += rx;
244
- moveY += ry;
245
- }
246
- }
247
-
248
- const mag = Math.sqrt(moveX * moveX + moveY * moveY);
249
- if (mag > 100)
250
- {
251
- moveX = (moveX / mag) * 100;
252
- moveY = (moveY / mag) * 100;
253
- }
254
-
255
- const move = {x: moveX, y: moveY};
256
-
257
- // === STEP 1: CHANNELING ===
258
- if (state.state === 'channeling')
259
- {
260
- if (!urgentThreat || urgentThreat.ticksToImpact > shieldCancelThreshold)
261
- {
262
- return {move, cancel: true};
263
- }
264
- return {move: {x: 0, y: 0}};
265
- }
266
-
267
- // === STEP 2: PROGRESSIVE CAST-CANCEL ===
268
- if (
269
- state.state === 'casting' &&
270
- state.castingSpell === 'missile' &&
271
- urgentThreat &&
272
- !urgentThreat.bestDodgeDirection
273
- )
274
- {
275
- const remainingCast = (state.castDuration ?? 0) - (state.castProgress ?? 0);
276
- const defenseTime =
277
- state.blinkCooldown === 0
278
- ? BLINK_CAST_TIME
279
- : SHIELD_CAST_TIME + SHIELD_BUFFER;
280
- const willArriveInWindow =
281
- urgentThreat.ticksToImpact <= remainingCast + GCD_DURATION + defenseTime;
282
-
283
- if (willArriveInWindow)
284
- {
285
- const incomingDamage = urgentThreat.projectile.damage;
286
- const castRatio = (state.castProgress ?? 0) / (state.castDuration ?? 1);
287
-
288
- if (incomingDamage >= state.health || incomingDamage >= cancelHeavyDmg)
289
- {
290
- return {move, cancel: true};
291
- }
292
- if (incomingDamage >= cancelMediumDmg && castRatio < cancelCastRatio)
293
- {
294
- return {move, cancel: true};
295
- }
296
- }
297
- }
298
-
299
- // === BLINK-DODGE — optimizer-controlled alternative to shield ===
300
- 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
- )
309
- {
310
- const blinkTarget = getBlinkToIncreaseDistance(
311
- state.position,
312
- enemy.position,
313
- state.projectiles,
314
- MIN_BLINK_ESCAPE_DISTANCE,
315
- );
316
- if (blinkTarget)
317
- {
318
- return {
319
- move: {x: 0, y: 0},
320
- startCast: {spell: 'blink', target: blinkTarget},
321
- };
322
- }
323
- }
324
-
325
- // === STEP 3: SHIELD — undodgeable or high-damage homing threats ===
326
- if (
327
- state.state === 'idle' &&
328
- urgentThreat &&
329
- (!urgentThreat.bestDodgeDirection || shouldForceShield(urgentThreat))
330
- )
331
- {
332
- if (
333
- urgentThreat.canBlockInTime &&
334
- urgentThreat.ticksToStartShield <= SHIELD_BUFFER
335
- )
336
- {
337
- return {
338
- move: {x: 0, y: 0},
339
- startCast: {spell: 'shield'},
340
- };
341
- }
342
- }
343
-
344
- // === STEP 5: PROACTIVE BLINK KITING ===
345
- if (
346
- state.state === 'idle' &&
347
- state.blinkCooldown === 0 &&
348
- (!urgentThreat || urgentThreat.bestDodgeDirection !== null)
349
- )
350
- {
351
- const closingRate = getClosingRate(
352
- state.position,
353
- enemy.position,
354
- enemy.velocity,
355
- );
356
- if (
357
- (closingRate > closingRateThreshold && distance < proactiveBlinkRange) ||
358
- distance < dangerDistance
359
- )
360
- {
361
- const blinkTarget = getBlinkToIncreaseDistance(
362
- state.position,
363
- enemy.position,
364
- state.projectiles,
365
- MIN_BLINK_ESCAPE_DISTANCE,
366
- );
367
- if (blinkTarget)
368
- {
369
- return {
370
- move: {x: 0, y: 0},
371
- startCast: {spell: 'blink', target: blinkTarget},
372
- };
373
- }
374
- }
375
- }
376
-
377
- // === STEP 6: OFFENSE ===
378
- if (state.state === 'idle')
379
- {
380
- const enemyHP = Math.ceil(enemy.health);
381
- const vulnerabilityTicks = getEnemyVulnerabilityTicks(enemy);
382
-
383
- // --- PUNISH MODE: warmup-accelerated missiles during vulnerability ---
384
- if (vulnerabilityTicks >= punishMinVulnerability && distance < 500)
385
- {
386
- const flightTimeEstimate = distance / flightTimeDivisor;
387
- const punishBudget = vulnerabilityTicks - flightTimeEstimate;
388
- const nextThreatTime = urgentThreat?.ticksToImpact ?? Infinity;
389
- const safeBudget =
390
- nextThreatTime - GCD_DURATION - SHIELD_CAST_TIME - SHIELD_BUFFER;
391
- const effectiveBudget = Math.min(
392
- punishBudget,
393
- safeBudget,
394
- MAX_CAST_BUDGET,
395
- );
396
-
397
- if (effectiveBudget > 0)
398
- {
399
- const config = fitMissileToBudget(effectiveBudget, distance, {
400
- minTurnRate: 0,
401
- maxDamage: enemyHP,
402
- lastMissileConfig: state.lastMissileConfig,
403
- });
404
-
405
- if (config && config.damage >= MIN_USEFUL_DAMAGE)
406
- {
407
- const castTime = getMissileCastTime(config, state.lastMissileConfig);
408
- const actualFlight = distance / config.speed;
409
-
410
- if (
411
- castTime + actualFlight < vulnerabilityTicks &&
412
- isSafeToAttack(threats, castTime)
413
- )
414
- {
415
- const aimTarget =
416
- config.turnRate > 0
417
- ? enemy.position
418
- : getLeadPosition(
419
- enemy.position,
420
- enemy.velocity,
421
- config.speed,
422
- state.position,
423
- );
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
- };
433
- }
434
- }
435
- }
436
- }
437
-
438
- // --- STANDARD MODE: warmup-consistent homing missiles ---
439
- const nextThreatTime = urgentThreat?.ticksToImpact ?? Infinity;
440
- const safeBudget =
441
- nextThreatTime - GCD_DURATION - SHIELD_CAST_TIME - SHIELD_BUFFER;
442
- const budgetTicks = Math.min(safeBudget, MAX_CAST_BUDGET);
443
-
444
- const minDmg = Math.min(MIN_USEFUL_DAMAGE, enemyHP);
445
- let config = fitMissileToBudget(budgetTicks, distance, {
446
- minTurnRate,
447
- maxDamage: enemyHP,
448
- lastMissileConfig: state.lastMissileConfig,
449
- });
450
-
451
- if (
452
- config &&
453
- config.damage >= minDmg &&
454
- isSafeToAttack(threats, safeAttackCastEstimate)
455
- )
456
- {
457
- return {
458
- move,
459
- startCast: {
460
- spell: 'missile',
461
- config,
462
- missileAI: HomingAI,
463
- direction: angleTo(state.position, enemy.position),
464
- },
465
- };
466
- }
467
-
468
- // Fallback: fire bigger missile, accept incoming hit
469
- if (!config || config.damage < minDmg)
470
- {
471
- config = fitMissileToBudget(MAX_CAST_BUDGET, distance, {
472
- minTurnRate,
473
- maxDamage: enemyHP,
474
- lastMissileConfig: state.lastMissileConfig,
475
- });
476
- if (config)
477
- {
478
- return {
479
- move,
480
- startCast: {
481
- spell: 'missile',
482
- config,
483
- missileAI: HomingAI,
484
- direction: angleTo(state.position, enemy.position),
485
- },
486
- };
487
- }
488
- }
489
- }
490
-
491
- return {move};
492
- };
1
+ import {WizardFunction, MissileAIFunction} from '../../types.js';
2
+ import {angleTo} from '../../utils/angles.js';
3
+ 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';
10
+ import {getMissileCastTime, getLeadPosition} from '../../utils/combat.js';
11
+ import {
12
+ getThreats,
13
+ getMostUrgentThreat,
14
+ getDodgeDirectionForMovement,
15
+ isSafeToAttack,
16
+ getBlinkToIncreaseDistance,
17
+ fitMissileToBudget,
18
+ shouldForceShield,
19
+ MIN_BLINK_ESCAPE_DISTANCE,
20
+ MAX_CAST_BUDGET,
21
+ MIN_USEFUL_DAMAGE,
22
+ getClosingRate,
23
+ getEnemyVulnerabilityTicks,
24
+ } from '../shared.js';
25
+ import {useParam} from '../../engine/params-runtime.js';
26
+
27
+ const WALL_BUFFER = 60;
28
+ const SHIELD_BUFFER = 3;
29
+
30
+ const HomingAI: MissileAIFunction = ({worldState}) =>
31
+ {
32
+ const enemy = worldState.enemies[0];
33
+ return {turnToward: enemy?.position};
34
+ };
35
+
36
+ const StraightAI: MissileAIFunction = () => ({});
37
+
38
+ function isNearWall(position: {x: number; y: number}): boolean
39
+ {
40
+ 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
45
+ );
46
+ }
47
+
48
+ function smartStrafe(
49
+ position: {x: number; y: number},
50
+ enemy: {position: {x: number; y: number}},
51
+ dodgeDirection: {x: number; y: number} | null,
52
+ tick: number,
53
+ strafeCyclePeriod: number,
54
+ strafeIntensity: number,
55
+ ): {x: number; y: number}
56
+ {
57
+ const deltaX = enemy.position.x - position.x;
58
+ const deltaY = enemy.position.y - position.y;
59
+ const len = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
60
+ if (len === 0) return {x: 0, y: 0};
61
+
62
+ const cw = {x: -deltaY / len, y: deltaX / len};
63
+ const ccw = {x: deltaY / len, y: -deltaX / len};
64
+
65
+ let dir: {x: number; y: number};
66
+
67
+ if (dodgeDirection)
68
+ {
69
+ dir = dodgeDirection;
70
+ }
71
+ else if (isNearWall(position))
72
+ {
73
+ const fCw = {x: position.x + cw.x * 100, y: position.y + cw.y * 100};
74
+ 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);
76
+ const sCcw = Math.min(
77
+ fCcw.x,
78
+ ARENA_SIZE - fCcw.x,
79
+ fCcw.y,
80
+ ARENA_SIZE - fCcw.y,
81
+ );
82
+ dir = sCw > sCcw ? cw : ccw;
83
+ }
84
+ else
85
+ {
86
+ const cycle = Math.floor(tick / strafeCyclePeriod) % 2;
87
+ dir = cycle === 0 ? cw : ccw;
88
+ }
89
+
90
+ const intensity = dodgeDirection ? 100 : strafeIntensity;
91
+ return {x: dir.x * intensity, y: dir.y * intensity};
92
+ }
93
+
94
+ /**
95
+ * Bot: Infernalist
96
+ *
97
+ * BEHAVIOR: Rapid-fire caster that exploits warmup bonus for accelerating DPS.
98
+ * Fires consistent homing missiles to build warmup, punishes vulnerability windows
99
+ * with warmup-boosted fast casts. Proactive blink kiting when enemy closes.
100
+ *
101
+ * KEY IMPROVEMENTS OVER PYROMANCER:
102
+ * - Warmup exploitation: always passes lastMissileConfig for bonus
103
+ * - Punish mode: straight missiles during enemy vulnerability
104
+ * - Proactive blink kiting: monitors closing rate
105
+ * - Progressive cast-cancel: graduated thresholds
106
+ *
107
+ * PROGRESSION LINE: Flamecaller → Pyromancer → Infernalist
108
+ * TIER: 3 (elite Caster line)
109
+ */
110
+ export const Infernalist: WizardFunction = ({state}) =>
111
+ {
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, {
115
+ range: 150,
116
+ min: 0,
117
+ });
118
+ const closingRateThreshold = useParam('closingRateThreshold', 0.94, {
119
+ range: 0.65,
120
+ min: 0,
121
+ steps: 5,
122
+ });
123
+ const punishMinVulnerability = useParam('punishMinVulnerability', 340, {
124
+ range: 30,
125
+ min: 10,
126
+ steps: 5,
127
+ });
128
+ const preferBlinkDodge =
129
+ useParam('preferBlinkDodge', 0, {min: 0, max: 1, steps: 2}) >= 0.5;
130
+ const strafeCyclePeriod = useParam('strafeCyclePeriod', 109, {
131
+ range: 75,
132
+ min: 80,
133
+ max: 250,
134
+ });
135
+ const strafeIntensity = useParam('strafeIntensity', 23, {
136
+ range: 40,
137
+ min: 20,
138
+ max: 100,
139
+ steps: 7,
140
+ });
141
+ const approachSpeed = useParam('approachSpeed', 53, {
142
+ range: 30,
143
+ min: 10,
144
+ max: 70,
145
+ steps: 7,
146
+ });
147
+ const distanceDeadZone = useParam('distanceDeadZone', 34, {
148
+ range: 30,
149
+ min: 5,
150
+ max: 80,
151
+ steps: 7,
152
+ });
153
+ const cancelHeavyDmg = useParam('cancelHeavyDmg', 40, {
154
+ range: 15,
155
+ min: 5,
156
+ max: 40,
157
+ steps: 5,
158
+ });
159
+ const cancelMediumDmg = useParam('cancelMediumDmg', 9, {
160
+ range: 10,
161
+ min: 3,
162
+ max: 25,
163
+ steps: 5,
164
+ });
165
+ const cancelCastRatio = useParam('cancelCastRatio', 0.78, {
166
+ range: 0.4,
167
+ min: 0.3,
168
+ max: 1.0,
169
+ steps: 5,
170
+ });
171
+ const shieldCancelThreshold = useParam('shieldCancelThreshold', 100, {
172
+ range: 75,
173
+ min: 50,
174
+ max: 300,
175
+ });
176
+ const blinkWindowMax = useParam('blinkWindowMax', 75, {
177
+ range: 40,
178
+ min: 20,
179
+ max: 120,
180
+ steps: 7,
181
+ });
182
+ const proactiveBlinkRange = useParam('proactiveBlinkRange', 100, {
183
+ range: 150,
184
+ min: 100,
185
+ max: 500,
186
+ steps: 7,
187
+ });
188
+ const flightTimeDivisor = useParam('flightTimeDivisor', 5.4, {
189
+ range: 4,
190
+ min: 2,
191
+ max: 12,
192
+ steps: 5,
193
+ });
194
+ const safeAttackCastEstimate = useParam('safeAttackCastEstimate', 35, {
195
+ range: 25,
196
+ min: 10,
197
+ max: 80,
198
+ steps: 5,
199
+ });
200
+
201
+ const enemy = state.enemies[0];
202
+ if (!enemy)
203
+ {
204
+ return {move: {x: 0, y: 0}};
205
+ }
206
+
207
+ const distance = distanceTo(state.position, enemy.position);
208
+ const threats = getThreats(state);
209
+ const urgentThreat = getMostUrgentThreat(threats);
210
+ const dodgeDirection = getDodgeDirectionForMovement(threats, state.position);
211
+ const strafe = smartStrafe(
212
+ state.position,
213
+ enemy,
214
+ dodgeDirection,
215
+ state.tick,
216
+ strafeCyclePeriod,
217
+ strafeIntensity,
218
+ );
219
+
220
+ // Distance management with wall-aware retreat
221
+ let moveX = strafe.x;
222
+ let moveY = strafe.y;
223
+
224
+ if (!dodgeDirection)
225
+ {
226
+ const deltaX = enemy.position.x - state.position.x;
227
+ const deltaY = enemy.position.y - state.position.y;
228
+ const norm = distance > 0 ? distance : 1;
229
+
230
+ if (distance > targetDistance + distanceDeadZone)
231
+ {
232
+ moveX += (deltaX / norm) * approachSpeed;
233
+ moveY += (deltaY / norm) * approachSpeed;
234
+ }
235
+ else if (distance < targetDistance - distanceDeadZone)
236
+ {
237
+ let rx = -(deltaX / norm) * approachSpeed;
238
+ 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;
243
+ moveX += rx;
244
+ moveY += ry;
245
+ }
246
+ }
247
+
248
+ const mag = Math.sqrt(moveX * moveX + moveY * moveY);
249
+ if (mag > 100)
250
+ {
251
+ moveX = (moveX / mag) * 100;
252
+ moveY = (moveY / mag) * 100;
253
+ }
254
+
255
+ const move = {x: moveX, y: moveY};
256
+
257
+ // === STEP 1: CHANNELING ===
258
+ if (state.state === 'channeling')
259
+ {
260
+ if (!urgentThreat || urgentThreat.ticksToImpact > shieldCancelThreshold)
261
+ {
262
+ return {move, cancel: true};
263
+ }
264
+ return {move: {x: 0, y: 0}};
265
+ }
266
+
267
+ // === STEP 2: PROGRESSIVE CAST-CANCEL ===
268
+ if (
269
+ state.state === 'casting' &&
270
+ state.castingSpell === 'missile' &&
271
+ urgentThreat &&
272
+ !urgentThreat.bestDodgeDirection
273
+ )
274
+ {
275
+ const remainingCast = (state.castDuration ?? 0) - (state.castProgress ?? 0);
276
+ const defenseTime =
277
+ state.blinkCooldown === 0
278
+ ? BLINK_CAST_TIME
279
+ : SHIELD_CAST_TIME + SHIELD_BUFFER;
280
+ const willArriveInWindow =
281
+ urgentThreat.ticksToImpact <= remainingCast + GCD_DURATION + defenseTime;
282
+
283
+ if (willArriveInWindow)
284
+ {
285
+ const incomingDamage = urgentThreat.projectile.damage;
286
+ const castRatio = (state.castProgress ?? 0) / (state.castDuration ?? 1);
287
+
288
+ if (incomingDamage >= state.health || incomingDamage >= cancelHeavyDmg)
289
+ {
290
+ return {move, cancel: true};
291
+ }
292
+ if (incomingDamage >= cancelMediumDmg && castRatio < cancelCastRatio)
293
+ {
294
+ return {move, cancel: true};
295
+ }
296
+ }
297
+ }
298
+
299
+ // === BLINK-DODGE — optimizer-controlled alternative to shield ===
300
+ 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
+ )
309
+ {
310
+ const blinkTarget = getBlinkToIncreaseDistance(
311
+ state.position,
312
+ enemy.position,
313
+ state.projectiles,
314
+ MIN_BLINK_ESCAPE_DISTANCE,
315
+ );
316
+ if (blinkTarget)
317
+ {
318
+ return {
319
+ move: {x: 0, y: 0},
320
+ startCast: {spell: 'blink', target: blinkTarget},
321
+ };
322
+ }
323
+ }
324
+
325
+ // === STEP 3: SHIELD — undodgeable or high-damage homing threats ===
326
+ if (
327
+ state.state === 'idle' &&
328
+ urgentThreat &&
329
+ (!urgentThreat.bestDodgeDirection || shouldForceShield(urgentThreat))
330
+ )
331
+ {
332
+ if (
333
+ urgentThreat.canBlockInTime &&
334
+ urgentThreat.ticksToStartShield <= SHIELD_BUFFER
335
+ )
336
+ {
337
+ return {
338
+ move: {x: 0, y: 0},
339
+ startCast: {spell: 'shield'},
340
+ };
341
+ }
342
+ }
343
+
344
+ // === STEP 5: PROACTIVE BLINK KITING ===
345
+ if (
346
+ state.state === 'idle' &&
347
+ state.blinkCooldown === 0 &&
348
+ (!urgentThreat || urgentThreat.bestDodgeDirection !== null)
349
+ )
350
+ {
351
+ const closingRate = getClosingRate(
352
+ state.position,
353
+ enemy.position,
354
+ enemy.velocity,
355
+ );
356
+ if (
357
+ (closingRate > closingRateThreshold && distance < proactiveBlinkRange) ||
358
+ distance < dangerDistance
359
+ )
360
+ {
361
+ const blinkTarget = getBlinkToIncreaseDistance(
362
+ state.position,
363
+ enemy.position,
364
+ state.projectiles,
365
+ MIN_BLINK_ESCAPE_DISTANCE,
366
+ );
367
+ if (blinkTarget)
368
+ {
369
+ return {
370
+ move: {x: 0, y: 0},
371
+ startCast: {spell: 'blink', target: blinkTarget},
372
+ };
373
+ }
374
+ }
375
+ }
376
+
377
+ // === STEP 6: OFFENSE ===
378
+ if (state.state === 'idle')
379
+ {
380
+ const enemyHP = Math.ceil(enemy.health);
381
+ const vulnerabilityTicks = getEnemyVulnerabilityTicks(enemy);
382
+
383
+ // --- PUNISH MODE: warmup-accelerated missiles during vulnerability ---
384
+ if (vulnerabilityTicks >= punishMinVulnerability && distance < 500)
385
+ {
386
+ const flightTimeEstimate = distance / flightTimeDivisor;
387
+ const punishBudget = vulnerabilityTicks - flightTimeEstimate;
388
+ const nextThreatTime = urgentThreat?.ticksToImpact ?? Infinity;
389
+ const safeBudget =
390
+ nextThreatTime - GCD_DURATION - SHIELD_CAST_TIME - SHIELD_BUFFER;
391
+ const effectiveBudget = Math.min(
392
+ punishBudget,
393
+ safeBudget,
394
+ MAX_CAST_BUDGET,
395
+ );
396
+
397
+ if (effectiveBudget > 0)
398
+ {
399
+ const config = fitMissileToBudget(effectiveBudget, distance, {
400
+ minTurnRate: 0,
401
+ maxDamage: enemyHP,
402
+ lastMissileConfig: state.lastMissileConfig,
403
+ });
404
+
405
+ if (config && config.damage >= MIN_USEFUL_DAMAGE)
406
+ {
407
+ const castTime = getMissileCastTime(config, state.lastMissileConfig);
408
+ const actualFlight = distance / config.speed;
409
+
410
+ if (
411
+ castTime + actualFlight < vulnerabilityTicks &&
412
+ isSafeToAttack(threats, castTime)
413
+ )
414
+ {
415
+ const aimTarget =
416
+ config.turnRate > 0
417
+ ? enemy.position
418
+ : getLeadPosition(
419
+ enemy.position,
420
+ enemy.velocity,
421
+ config.speed,
422
+ state.position,
423
+ );
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
+ };
433
+ }
434
+ }
435
+ }
436
+ }
437
+
438
+ // --- STANDARD MODE: warmup-consistent homing missiles ---
439
+ const nextThreatTime = urgentThreat?.ticksToImpact ?? Infinity;
440
+ const safeBudget =
441
+ nextThreatTime - GCD_DURATION - SHIELD_CAST_TIME - SHIELD_BUFFER;
442
+ const budgetTicks = Math.min(safeBudget, MAX_CAST_BUDGET);
443
+
444
+ const minDmg = Math.min(MIN_USEFUL_DAMAGE, enemyHP);
445
+ let config = fitMissileToBudget(budgetTicks, distance, {
446
+ minTurnRate,
447
+ maxDamage: enemyHP,
448
+ lastMissileConfig: state.lastMissileConfig,
449
+ });
450
+
451
+ if (
452
+ config &&
453
+ config.damage >= minDmg &&
454
+ isSafeToAttack(threats, safeAttackCastEstimate)
455
+ )
456
+ {
457
+ return {
458
+ move,
459
+ startCast: {
460
+ spell: 'missile',
461
+ config,
462
+ missileAI: HomingAI,
463
+ direction: angleTo(state.position, enemy.position),
464
+ },
465
+ };
466
+ }
467
+
468
+ // Fallback: fire bigger missile, accept incoming hit
469
+ if (!config || config.damage < minDmg)
470
+ {
471
+ config = fitMissileToBudget(MAX_CAST_BUDGET, distance, {
472
+ minTurnRate,
473
+ maxDamage: enemyHP,
474
+ lastMissileConfig: state.lastMissileConfig,
475
+ });
476
+ if (config)
477
+ {
478
+ return {
479
+ move,
480
+ startCast: {
481
+ spell: 'missile',
482
+ config,
483
+ missileAI: HomingAI,
484
+ direction: angleTo(state.position, enemy.position),
485
+ },
486
+ };
487
+ }
488
+ }
489
+ }
490
+
491
+ return {move};
492
+ };