@vibemancer/core 0.1.0

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 (78) hide show
  1. package/README.md +28 -0
  2. package/dist/chunk-L7Z7OFXD.js +9140 -0
  3. package/dist/chunk-L7Z7OFXD.js.map +1 -0
  4. package/dist/index-browser.d.ts +2602 -0
  5. package/dist/index-browser.js +407 -0
  6. package/dist/index-browser.js.map +1 -0
  7. package/dist/index.d.ts +150 -0
  8. package/dist/index.js +750 -0
  9. package/dist/index.js.map +1 -0
  10. package/package.json +79 -0
  11. package/src/bots/berserker/01_Stormchaser.ts +457 -0
  12. package/src/bots/berserker/02_Stormcaller.ts +417 -0
  13. package/src/bots/berserker/03_Stormforger.ts +481 -0
  14. package/src/bots/caster/01_Flamecaller.ts +286 -0
  15. package/src/bots/caster/02_Pyromancer.ts +350 -0
  16. package/src/bots/caster/03_Infernalist.ts +492 -0
  17. package/src/bots/defensive/01_Turtle.ts +151 -0
  18. package/src/bots/defensive/02_Sentinel.ts +134 -0
  19. package/src/bots/defensive/03_Golem.ts +357 -0
  20. package/src/bots/duelist/01_Battlemage.ts +433 -0
  21. package/src/bots/duelist/02_Warmage.ts +438 -0
  22. package/src/bots/duelist/03_Archmage.ts +588 -0
  23. package/src/bots/homing/01_Bonemancer.ts +67 -0
  24. package/src/bots/homing/02_Lich.ts +356 -0
  25. package/src/bots/homing/03_Archlich.ts +220 -0
  26. package/src/bots/index.ts +30 -0
  27. package/src/bots/kiter/01_Spellspinner.ts +398 -0
  28. package/src/bots/kiter/02_Spellweaver.ts +378 -0
  29. package/src/bots/kiter/03_Spellbinder.ts +448 -0
  30. package/src/bots/melee/01_Shadowblade.ts +270 -0
  31. package/src/bots/melee/02_Nightblade.ts +437 -0
  32. package/src/bots/melee/03_Voidblade.ts +582 -0
  33. package/src/bots/registry.ts +207 -0
  34. package/src/bots/shared.ts +472 -0
  35. package/src/bots/sniper/01_Spellshot.ts +385 -0
  36. package/src/bots/sniper/02_Spelltracer.ts +441 -0
  37. package/src/bots/sniper/03_Spellseeker.ts +546 -0
  38. package/src/bots/standalone/Critter.ts +89 -0
  39. package/src/bots/standalone/Doombringer.ts +91 -0
  40. package/src/bots/standalone/Hogger.ts +228 -0
  41. package/src/bots/standalone/Rookie.ts +50 -0
  42. package/src/bots/standalone/TargetDummy.ts +21 -0
  43. package/src/bots/test/cheater.ts +405 -0
  44. package/src/bots/test/crasher.ts +81 -0
  45. package/src/engine/hooks-runtime.ts +394 -0
  46. package/src/engine/manual-match.ts +289 -0
  47. package/src/engine/missile-templates.ts +155 -0
  48. package/src/engine/optimizer.ts +220 -0
  49. package/src/engine/params-runtime.ts +189 -0
  50. package/src/engine/physics.ts +143 -0
  51. package/src/engine/sandbox-browser.ts +671 -0
  52. package/src/engine/sandbox-compile.ts +197 -0
  53. package/src/engine/sandbox-harness.ts +367 -0
  54. package/src/engine/sandbox.ts +332 -0
  55. package/src/engine/simulation.ts +828 -0
  56. package/src/engine/spells.ts +128 -0
  57. package/src/engine-version.ts +11 -0
  58. package/src/hooks/action-builders.ts +210 -0
  59. package/src/hooks/bot-wrapper.ts +84 -0
  60. package/src/hooks/index.ts +75 -0
  61. package/src/hooks/state-hooks.ts +354 -0
  62. package/src/hooks/threat-analysis.ts +365 -0
  63. package/src/hooks/types.ts +142 -0
  64. package/src/index-browser.ts +30 -0
  65. package/src/index.ts +24 -0
  66. package/src/rules.ts +254 -0
  67. package/src/stats.ts +262 -0
  68. package/src/testing.ts +207 -0
  69. package/src/trace.ts +430 -0
  70. package/src/types.ts +193 -0
  71. package/src/utils/angles.ts +47 -0
  72. package/src/utils/combat.ts +279 -0
  73. package/src/utils/distance.ts +21 -0
  74. package/src/utils/index.ts +7 -0
  75. package/src/utils/movement.ts +108 -0
  76. package/src/utils/random.ts +65 -0
  77. package/src/utils/spatial.ts +63 -0
  78. package/src/utils/targeting.ts +45 -0
@@ -0,0 +1,546 @@
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
+ getBlinkToCenter,
16
+ getBlinkToIncreaseDistance,
17
+ fitMissileToBudget,
18
+ shouldForceShield,
19
+ MIN_BLINK_ESCAPE_DISTANCE,
20
+ MAX_CAST_BUDGET,
21
+ MIN_USEFUL_DAMAGE,
22
+ getClosingRate,
23
+ getEnemyVulnerabilityTicks,
24
+ isSafeToAttack,
25
+ } from '../shared.js';
26
+ import {useParam} from '../../engine/params-runtime.js';
27
+
28
+ const HomingAI: MissileAIFunction = ({worldState}) =>
29
+ {
30
+ const enemy = worldState.enemies[0];
31
+ return {turnToward: enemy?.position};
32
+ };
33
+
34
+ const StraightAI: MissileAIFunction = () => ({});
35
+
36
+ const WALL_BUFFER = 50;
37
+ const SHIELD_BUFFER = 3;
38
+
39
+ /**
40
+ * Smart combat movement: dodge, avoid walls, maintain sniper range.
41
+ */
42
+ function combatMove(
43
+ position: {x: number; y: number},
44
+ enemy: {position: {x: number; y: number}},
45
+ currentDistance: number,
46
+ minDistance: number,
47
+ maxDistance: number,
48
+ dodgeDirection: {x: number; y: number} | null,
49
+ tick: number,
50
+ strafeCyclePeriod: number,
51
+ strafeIntensity: number,
52
+ approachSpeed: number,
53
+ ): {x: number; y: number}
54
+ {
55
+ const deltaX = enemy.position.x - position.x;
56
+ const deltaY = enemy.position.y - position.y;
57
+ const normalizedDistance = currentDistance > 0 ? currentDistance : 1;
58
+
59
+ const strafeClockwise = {
60
+ x: -deltaY / normalizedDistance,
61
+ y: deltaX / normalizedDistance,
62
+ };
63
+ const strafeCounterClockwise = {
64
+ x: deltaY / normalizedDistance,
65
+ y: -deltaX / normalizedDistance,
66
+ };
67
+
68
+ let strafeDirection: {x: number; y: number};
69
+
70
+ if (dodgeDirection)
71
+ {
72
+ strafeDirection = dodgeDirection;
73
+ }
74
+ else if (
75
+ position.x < WALL_BUFFER ||
76
+ position.x > ARENA_SIZE - WALL_BUFFER ||
77
+ position.y < WALL_BUFFER ||
78
+ position.y > ARENA_SIZE - WALL_BUFFER
79
+ )
80
+ {
81
+ const futureClockwise = {
82
+ x: position.x + strafeClockwise.x * 100,
83
+ y: position.y + strafeClockwise.y * 100,
84
+ };
85
+ const futureCounterClockwise = {
86
+ x: position.x + strafeCounterClockwise.x * 100,
87
+ y: position.y + strafeCounterClockwise.y * 100,
88
+ };
89
+ const scoreClockwise = Math.min(
90
+ futureClockwise.x,
91
+ ARENA_SIZE - futureClockwise.x,
92
+ futureClockwise.y,
93
+ ARENA_SIZE - futureClockwise.y,
94
+ );
95
+ const scoreCounterClockwise = Math.min(
96
+ futureCounterClockwise.x,
97
+ ARENA_SIZE - futureCounterClockwise.x,
98
+ futureCounterClockwise.y,
99
+ ARENA_SIZE - futureCounterClockwise.y,
100
+ );
101
+ strafeDirection =
102
+ scoreClockwise > scoreCounterClockwise
103
+ ? strafeClockwise
104
+ : strafeCounterClockwise;
105
+ }
106
+ else
107
+ {
108
+ const cycle = Math.floor(tick / strafeCyclePeriod) % 2;
109
+ strafeDirection = cycle === 0 ? strafeClockwise : strafeCounterClockwise;
110
+ }
111
+
112
+ if (dodgeDirection)
113
+ {
114
+ return {x: strafeDirection.x * 100, y: strafeDirection.y * 100};
115
+ }
116
+
117
+ let moveX = strafeDirection.x * strafeIntensity;
118
+ let moveY = strafeDirection.y * strafeIntensity;
119
+
120
+ if (currentDistance > maxDistance)
121
+ {
122
+ moveX += (deltaX / normalizedDistance) * approachSpeed;
123
+ moveY += (deltaY / normalizedDistance) * approachSpeed;
124
+ }
125
+ else if (currentDistance < minDistance)
126
+ {
127
+ let retreatX = -(deltaX / normalizedDistance) * approachSpeed;
128
+ let retreatY = -(deltaY / normalizedDistance) * approachSpeed;
129
+ if (position.x < WALL_BUFFER && retreatX < 0) retreatX = 0;
130
+ if (position.x > ARENA_SIZE - WALL_BUFFER && retreatX > 0) retreatX = 0;
131
+ if (position.y < WALL_BUFFER && retreatY < 0) retreatY = 0;
132
+ if (position.y > ARENA_SIZE - WALL_BUFFER && retreatY > 0) retreatY = 0;
133
+ moveX += retreatX;
134
+ moveY += retreatY;
135
+ }
136
+
137
+ const magnitude = Math.sqrt(moveX * moveX + moveY * moveY);
138
+ if (magnitude > 100)
139
+ {
140
+ moveX = (moveX / magnitude) * 100;
141
+ moveY = (moveY / magnitude) * 100;
142
+ }
143
+
144
+ return {x: moveX, y: moveY};
145
+ }
146
+
147
+ /**
148
+ * Bot: Spellseeker
149
+ *
150
+ * BEHAVIOR: Elite sniper that uses intercept-aimed straight missiles during vulnerability
151
+ * windows. Combines Spelltracer's adaptive fitting with precise lead-position aiming
152
+ * and vulnerability exploitation. Straight punish missiles at sniper range are nearly
153
+ * unavoidable. Proactive distance control via closing rate detection.
154
+ *
155
+ * KEY IMPROVEMENTS OVER SPELLTRACER:
156
+ * - Intercept-aimed punish: getLeadPosition + straight missiles during vulnerability
157
+ * - Proactive distance blink: monitors closing rate, blinks before danger zone
158
+ * - Progressive cast-cancel: graduated damage thresholds
159
+ * - Warmup exploitation: always passes lastMissileConfig
160
+ *
161
+ * PROGRESSION LINE: Spellshot → Spelltracer → Spellseeker
162
+ * TIER: 3 (elite Sniper line)
163
+ */
164
+ export const Spellseeker: WizardFunction = ({state}) =>
165
+ {
166
+ const dangerDistance = useParam('dangerDistance', 65, {
167
+ range: 175,
168
+ min: 0,
169
+ });
170
+ const targetDistance = useParam('targetDistance', 289, {
171
+ range: 200,
172
+ min: 0,
173
+ });
174
+ const distanceDeadZone = useParam('distanceDeadZone', 22, {
175
+ range: 50,
176
+ min: 0,
177
+ });
178
+ const rangeMin = targetDistance - distanceDeadZone;
179
+ const rangeMax = targetDistance + distanceDeadZone;
180
+ const minTurnRate = useParam('minTurnRate', 3.5, {range: 1.5, steps: 5});
181
+ const closingRateThreshold = useParam('closingRateThreshold', 1.79, {
182
+ range: 0.65,
183
+ min: 0,
184
+ steps: 5,
185
+ });
186
+ const punishMinVulnerability = useParam('punishMinVulnerability', 279, {
187
+ range: 30,
188
+ min: 10,
189
+ steps: 5,
190
+ });
191
+ const preferBlinkDodge =
192
+ useParam('preferBlinkDodge', 1, {min: 0, max: 1, steps: 2}) >= 0.5;
193
+ const strafeCyclePeriod = useParam('strafeCyclePeriod', 231, {
194
+ range: 75,
195
+ min: 80,
196
+ max: 250,
197
+ });
198
+ const strafeIntensity = useParam('strafeIntensity', 87, {
199
+ range: 40,
200
+ min: 20,
201
+ max: 100,
202
+ steps: 7,
203
+ });
204
+ const approachSpeed = useParam('approachSpeed', 80, {
205
+ range: 30,
206
+ min: 10,
207
+ max: 80,
208
+ steps: 7,
209
+ });
210
+ const shieldCancelThreshold = useParam('shieldCancelThreshold', 100, {
211
+ range: 75,
212
+ min: 50,
213
+ max: 300,
214
+ });
215
+ const blinkWindowMax = useParam('blinkWindowMax', 88, {
216
+ range: 40,
217
+ min: 20,
218
+ max: 120,
219
+ steps: 7,
220
+ });
221
+ const proactiveBlinkRange = useParam('proactiveBlinkRange', 433, {
222
+ range: 150,
223
+ min: 100,
224
+ max: 500,
225
+ steps: 7,
226
+ });
227
+ const flightTimeDivisor = useParam('flightTimeDivisor', 12, {
228
+ range: 4,
229
+ min: 2,
230
+ max: 12,
231
+ steps: 5,
232
+ });
233
+ const castingFlightTimeDivisor = useParam('castingFlightTimeDivisor', 12, {
234
+ range: 4,
235
+ min: 3,
236
+ max: 12,
237
+ steps: 5,
238
+ });
239
+ const enemyResponseEstimate = useParam('enemyResponseEstimate', 168, {
240
+ range: 100,
241
+ min: 50,
242
+ max: 300,
243
+ steps: 7,
244
+ });
245
+ const threatFallbackThreshold = useParam('threatFallbackThreshold', 250, {
246
+ range: 100,
247
+ min: 100,
248
+ max: 400,
249
+ steps: 7,
250
+ });
251
+ const punishAttackRange = useParam('punishAttackRange', 213, {
252
+ range: 150,
253
+ min: 200,
254
+ max: 700,
255
+ steps: 7,
256
+ });
257
+ const safeAttackCastEstimate = useParam('safeAttackCastEstimate', 35, {
258
+ range: 25,
259
+ min: 10,
260
+ max: 80,
261
+ steps: 5,
262
+ });
263
+
264
+ const enemy = state.enemies[0];
265
+ if (!enemy)
266
+ {
267
+ return {move: {x: 0, y: 0}};
268
+ }
269
+
270
+ const distance = distanceTo(state.position, enemy.position);
271
+ const threats = getThreats(state);
272
+ const urgentThreat = getMostUrgentThreat(threats);
273
+ const dodgeDirection = getDodgeDirectionForMovement(threats, state.position);
274
+ const move = combatMove(
275
+ state.position,
276
+ enemy,
277
+ distance,
278
+ rangeMin,
279
+ rangeMax,
280
+ dodgeDirection,
281
+ state.tick,
282
+ strafeCyclePeriod,
283
+ strafeIntensity,
284
+ approachSpeed,
285
+ );
286
+
287
+ // === STEP 1: CHANNELING — cancel shield when safe ===
288
+ if (state.state === 'channeling')
289
+ {
290
+ if (!urgentThreat || urgentThreat.ticksToImpact > shieldCancelThreshold)
291
+ {
292
+ return {move, cancel: true};
293
+ }
294
+ return {move: {x: 0, y: 0}};
295
+ }
296
+
297
+ // === STEP 2: PROGRESSIVE CAST-CANCEL ===
298
+ if (
299
+ state.state === 'casting' &&
300
+ state.castingSpell === 'missile' &&
301
+ urgentThreat &&
302
+ !urgentThreat.bestDodgeDirection
303
+ )
304
+ {
305
+ const remainingCast = (state.castDuration ?? 0) - (state.castProgress ?? 0);
306
+ const defenseTime =
307
+ state.blinkCooldown === 0
308
+ ? BLINK_CAST_TIME
309
+ : SHIELD_CAST_TIME + SHIELD_BUFFER;
310
+ const willArriveInWindow =
311
+ urgentThreat.ticksToImpact <= remainingCast + GCD_DURATION + defenseTime;
312
+
313
+ if (willArriveInWindow)
314
+ {
315
+ const incomingDamage = urgentThreat.projectile.damage;
316
+ const castRatio = (state.castProgress ?? 0) / (state.castDuration ?? 1);
317
+
318
+ if (incomingDamage >= state.health || incomingDamage >= 20)
319
+ {
320
+ return {move, cancel: true};
321
+ }
322
+ if (incomingDamage >= 10 && castRatio < 0.8)
323
+ {
324
+ return {move, cancel: true};
325
+ }
326
+ }
327
+ }
328
+
329
+ // === STEP 3: BLINK-DODGE — 100% damage avoidance (optimizer-controlled) ===
330
+ if (
331
+ preferBlinkDodge &&
332
+ state.state === 'idle' &&
333
+ urgentThreat &&
334
+ state.blinkCooldown === 0 &&
335
+ (!urgentThreat.bestDodgeDirection || shouldForceShield(urgentThreat)) &&
336
+ urgentThreat.ticksToImpact >= BLINK_CAST_TIME &&
337
+ urgentThreat.ticksToImpact <= blinkWindowMax
338
+ )
339
+ {
340
+ const midRange = targetDistance;
341
+ const target =
342
+ distance < midRange
343
+ ? (getBlinkToIncreaseDistance(
344
+ state.position,
345
+ enemy.position,
346
+ state.projectiles,
347
+ ) ?? getBlinkToCenter(state.position))
348
+ : getBlinkToCenter(state.position);
349
+ return {
350
+ move: {x: 0, y: 0},
351
+ startCast: {spell: 'blink', target},
352
+ };
353
+ }
354
+
355
+ // === STEP 4: SHIELD — undodgeable or critical threats ===
356
+ if (
357
+ state.state === 'idle' &&
358
+ urgentThreat &&
359
+ (!urgentThreat.bestDodgeDirection || shouldForceShield(urgentThreat))
360
+ )
361
+ {
362
+ if (
363
+ urgentThreat.canBlockInTime &&
364
+ urgentThreat.ticksToStartShield <= SHIELD_BUFFER
365
+ )
366
+ {
367
+ return {
368
+ move: {x: 0, y: 0},
369
+ startCast: {spell: 'shield'},
370
+ };
371
+ }
372
+ }
373
+
374
+ // === STEP 5: PROACTIVE DISTANCE BLINK ===
375
+ if (
376
+ state.state === 'idle' &&
377
+ state.blinkCooldown === 0 &&
378
+ (!urgentThreat || urgentThreat.bestDodgeDirection !== null)
379
+ )
380
+ {
381
+ const closingRate = getClosingRate(
382
+ state.position,
383
+ enemy.position,
384
+ enemy.velocity,
385
+ );
386
+ if (
387
+ (closingRate > closingRateThreshold && distance < proactiveBlinkRange) ||
388
+ distance < dangerDistance
389
+ )
390
+ {
391
+ const blinkTarget = getBlinkToIncreaseDistance(
392
+ state.position,
393
+ enemy.position,
394
+ state.projectiles,
395
+ MIN_BLINK_ESCAPE_DISTANCE,
396
+ );
397
+ if (blinkTarget)
398
+ {
399
+ return {
400
+ move: {x: 0, y: 0},
401
+ startCast: {spell: 'blink', target: blinkTarget},
402
+ };
403
+ }
404
+ }
405
+ }
406
+
407
+ // === STEP 6: OFFENSE ===
408
+ if (state.state === 'idle' && distance < 600)
409
+ {
410
+ const offenseDefenseTime =
411
+ state.blinkCooldown === 0
412
+ ? BLINK_CAST_TIME
413
+ : SHIELD_CAST_TIME + SHIELD_BUFFER;
414
+ const enemyHP = Math.ceil(enemy.health);
415
+ const vulnerabilityTicks = getEnemyVulnerabilityTicks(enemy);
416
+
417
+ // --- PUNISH MODE: intercept-aimed straight missiles during vulnerability ---
418
+ if (
419
+ vulnerabilityTicks >= punishMinVulnerability &&
420
+ distance < punishAttackRange
421
+ )
422
+ {
423
+ const flightTimeEstimate = distance / flightTimeDivisor;
424
+ const punishBudget = vulnerabilityTicks - flightTimeEstimate;
425
+ const nextThreatTime = urgentThreat?.ticksToImpact ?? Infinity;
426
+ const safeBudget = nextThreatTime - GCD_DURATION - offenseDefenseTime;
427
+ const effectiveBudget = Math.min(
428
+ punishBudget,
429
+ safeBudget,
430
+ MAX_CAST_BUDGET,
431
+ );
432
+
433
+ if (effectiveBudget > 0)
434
+ {
435
+ const config = fitMissileToBudget(effectiveBudget, distance, {
436
+ minTurnRate: 0,
437
+ maxDamage: enemyHP,
438
+ lastMissileConfig: state.lastMissileConfig,
439
+ });
440
+
441
+ if (config && config.damage >= MIN_USEFUL_DAMAGE)
442
+ {
443
+ const castTime = getMissileCastTime(config, state.lastMissileConfig);
444
+ const actualFlight = distance / config.speed;
445
+
446
+ if (
447
+ castTime + actualFlight < vulnerabilityTicks &&
448
+ isSafeToAttack(threats, castTime)
449
+ )
450
+ {
451
+ const aimTarget =
452
+ config.turnRate > 0
453
+ ? enemy.position
454
+ : getLeadPosition(
455
+ enemy.position,
456
+ enemy.velocity,
457
+ config.speed,
458
+ state.position,
459
+ );
460
+ return {
461
+ move,
462
+ startCast: {
463
+ spell: 'missile',
464
+ config,
465
+ missileAI: config.turnRate > 0 ? HomingAI : StraightAI,
466
+ direction: angleTo(state.position, aimTarget),
467
+ },
468
+ };
469
+ }
470
+ }
471
+ }
472
+ }
473
+
474
+ // --- STANDARD MODE: adaptive homing missiles ---
475
+ let nextThreatTime = urgentThreat?.ticksToImpact ?? Infinity;
476
+
477
+ // Anticipate enemy's next missile based on their current state
478
+ if (enemy.state === 'casting' && enemy.castingSpell === 'missile')
479
+ {
480
+ const remainingEnemyCast =
481
+ (enemy.castDuration ?? 0) - (enemy.castProgress ?? 0);
482
+ nextThreatTime = Math.min(
483
+ nextThreatTime,
484
+ remainingEnemyCast + distance / castingFlightTimeDivisor,
485
+ );
486
+ }
487
+ // No imminent threat — estimate enemy's likely response time
488
+ if (nextThreatTime > threatFallbackThreshold)
489
+ {
490
+ const enemyLockTime = getEnemyVulnerabilityTicks(enemy);
491
+ nextThreatTime = Math.min(
492
+ nextThreatTime,
493
+ enemyLockTime +
494
+ enemyResponseEstimate +
495
+ distance / castingFlightTimeDivisor,
496
+ );
497
+ }
498
+
499
+ const safeBudget =
500
+ nextThreatTime - GCD_DURATION - SHIELD_CAST_TIME - SHIELD_BUFFER;
501
+ const budgetTicks = Math.min(safeBudget, MAX_CAST_BUDGET);
502
+
503
+ const config = fitMissileToBudget(budgetTicks, distance, {
504
+ minTurnRate,
505
+ maxDamage: enemyHP,
506
+ lastMissileConfig: state.lastMissileConfig,
507
+ });
508
+
509
+ // HIGH-DAMAGE PRIORITY: Sniper missiles need meaningful shield chip.
510
+ // Fixed big-shot fallback ensures Spellseeker always does significant damage.
511
+ const BIG_SHOT = {damage: 28, speed: 10, turnRate: 0.5, duration: 80};
512
+
513
+ if (
514
+ config &&
515
+ config.damage >= 15 &&
516
+ isSafeToAttack(threats, safeAttackCastEstimate)
517
+ )
518
+ {
519
+ return {
520
+ move,
521
+ startCast: {
522
+ spell: 'missile',
523
+ config,
524
+ missileAI: HomingAI,
525
+ direction: angleTo(state.position, enemy.position),
526
+ },
527
+ };
528
+ }
529
+
530
+ // Adaptive fitting weak or failed — fire big shot for meaningful damage
531
+ if (!urgentThreat || urgentThreat.ticksToImpact > 50)
532
+ {
533
+ return {
534
+ move,
535
+ startCast: {
536
+ spell: 'missile',
537
+ config: BIG_SHOT,
538
+ missileAI: HomingAI,
539
+ direction: angleTo(state.position, enemy.position),
540
+ },
541
+ };
542
+ }
543
+ }
544
+
545
+ return {move};
546
+ };
@@ -0,0 +1,89 @@
1
+ import {WizardFunction} from '../../types.js';
2
+ import {ARENA_WIDTH, ARENA_HEIGHT} from '../../rules.js';
3
+
4
+ /**
5
+ * Bot: Critter
6
+ *
7
+ * BEHAVIOR: Picks random valid actions each tick — random movement, random spells,
8
+ * random missile configs, random directions. Occasionally cancels its own casts.
9
+ * Uses engine-provided seeded random for deterministic behavior. Useful for
10
+ * finding edge cases in the engine, but completely useless in combat.
11
+ *
12
+ * NAMING RATIONALE: In WoW, critters are the 1-HP ambient mobs (rabbits, squirrels,
13
+ * prairie dogs) that wander around doing nothing useful and die to literally anything.
14
+ * This bot is the wizard equivalent — it flails around randomly and gets destroyed by
15
+ * anyone with a plan. The word "Critter" immediately tells any gamer "this thing is
16
+ * helpless and exists only to fill space."
17
+ *
18
+ * STANDALONE — no tier progression. Critters don't level up. However, a future
19
+ * "Hogger" bot could be an elite critter: same chaotic spirit but actually dangerous
20
+ * (like the famous WoW elite that wipes unprepared lowbies).
21
+ */
22
+ export const Critter: WizardFunction = ({state, random}) =>
23
+ {
24
+ // Use the engine-provided random function (seeded, isolated, deterministic)
25
+ // World-space movement: random x/y in [-100, 100] range
26
+ const move = {
27
+ x: random() * 200 - 100, // -100 to 100
28
+ y: random() * 200 - 100, // -100 to 100
29
+ };
30
+
31
+ if (state.state === 'idle' && random() > 0.95)
32
+ {
33
+ const spellRoll = random();
34
+ if (spellRoll > 0.6)
35
+ {
36
+ return {
37
+ move,
38
+ startCast: {
39
+ spell: 'missile',
40
+ config: {
41
+ damage: Math.floor(random() * 20) + 5,
42
+ speed: random() * 5 + 2,
43
+ turnRate: random() * 3,
44
+ duration: Math.floor(random() * 200) + 100,
45
+ },
46
+ // Missile AI gets its own random from engine - use it for random targeting
47
+ missileAI: ({random: missileRandom}) => ({
48
+ turnToward:
49
+ missileRandom() > 0.5
50
+ ? {
51
+ x: missileRandom() * ARENA_WIDTH,
52
+ y: missileRandom() * ARENA_HEIGHT,
53
+ }
54
+ : undefined,
55
+ }),
56
+ direction: random() * 360,
57
+ },
58
+ };
59
+ }
60
+ else if (spellRoll > 0.3)
61
+ {
62
+ return {
63
+ move,
64
+ startCast: {spell: 'shield'},
65
+ };
66
+ }
67
+ else if (state.blinkCooldown === 0)
68
+ {
69
+ return {
70
+ move,
71
+ startCast: {
72
+ spell: 'blink',
73
+ target: {x: random() * ARENA_WIDTH, y: random() * ARENA_HEIGHT},
74
+ },
75
+ };
76
+ }
77
+ }
78
+
79
+ if (
80
+ (state.state === 'casting' || state.state === 'channeling') &&
81
+ random() > 0.98
82
+ )
83
+ {
84
+ return {move, cancel: true};
85
+ }
86
+
87
+ // Override auto-aim with random direction — Critter doesn't know where anything is
88
+ return {move, aimDirection: random() * 360};
89
+ };
@@ -0,0 +1,91 @@
1
+ import {WizardFunction, MissileAIFunction} from '../../types.js';
2
+ import {angleTo} from '../../utils/angles.js';
3
+ import {distanceTo} from '../../utils/distance.js';
4
+ import {fitMissileToBudget} from '../../utils/combat.js';
5
+ import {
6
+ getThreats,
7
+ getMostUrgentThreat,
8
+ getBlinkToCenter,
9
+ } from '../shared.js';
10
+
11
+ const HomingAI: MissileAIFunction = ({worldState}) =>
12
+ {
13
+ const enemy = worldState.enemies[0];
14
+ return {turnToward: enemy?.position};
15
+ };
16
+
17
+ /**
18
+ * Bot: Doombringer
19
+ *
20
+ * BEHAVIOR: Fires a single maximum-damage homing missile with infinite budget.
21
+ * No damage cap — goes for the biggest possible hit. Exists as a benchmark to
22
+ * demonstrate why lower-damage + shield play is superior. Has basic shield
23
+ * defense but no sophisticated tactics. One fat cast, one fat hit.
24
+ *
25
+ * STANDALONE — no tier progression. Benchmark/test bot.
26
+ */
27
+ export const Doombringer: WizardFunction = ({state}) =>
28
+ {
29
+ const enemy = state.enemies[0];
30
+ if (!enemy)
31
+ {
32
+ return {move: {x: 0, y: 0}};
33
+ }
34
+
35
+ const distance = distanceTo(state.position, enemy.position);
36
+ const threats = getThreats(state);
37
+ const urgentThreat = getMostUrgentThreat(threats);
38
+
39
+ // === DEFENSE: Handle channeling ===
40
+ if (state.state === 'channeling')
41
+ {
42
+ if (!urgentThreat || urgentThreat.ticksToImpact > 150)
43
+ {
44
+ return {move: {x: 0, y: 0}, cancel: true};
45
+ }
46
+ return {move: {x: 0, y: 0}};
47
+ }
48
+
49
+ // === DEFENSE: Shield undodgeable threats ===
50
+ if (
51
+ state.state === 'idle' &&
52
+ urgentThreat &&
53
+ !urgentThreat.bestDodgeDirection
54
+ )
55
+ {
56
+ if (urgentThreat.canBlockInTime && urgentThreat.ticksToStartShield <= 3)
57
+ {
58
+ return {
59
+ move: {x: 0, y: 0},
60
+ startCast: {spell: 'shield'},
61
+ };
62
+ }
63
+ else if (!urgentThreat.canBlockInTime && state.blinkCooldown === 0)
64
+ {
65
+ return {
66
+ move: {x: 0, y: 0},
67
+ startCast: {spell: 'blink', target: getBlinkToCenter(state.position)},
68
+ };
69
+ }
70
+ }
71
+
72
+ // === OFFENSE: Fire max-damage homing missile (no damage cap) ===
73
+ if (state.state === 'idle')
74
+ {
75
+ const config = fitMissileToBudget(Infinity, distance, {minTurnRate: 0.5});
76
+ if (config)
77
+ {
78
+ return {
79
+ move: {x: 0, y: 0},
80
+ startCast: {
81
+ spell: 'missile',
82
+ config,
83
+ missileAI: HomingAI,
84
+ direction: angleTo(state.position, enemy.position),
85
+ },
86
+ };
87
+ }
88
+ }
89
+
90
+ return {move: {x: 0, y: 0}};
91
+ };