@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,134 @@
1
+ import {WizardFunction, MissileActions} from '../../types.js';
2
+ import {angleTo} from '../../utils/angles.js';
3
+ import {GCD_DURATION, SHIELD_CAST_TIME} from '../../rules.js';
4
+ import {getMissileCastTime} from '../../utils/combat.js';
5
+ import {
6
+ getThreats,
7
+ getMostUrgentThreat,
8
+ getBlinkToCenter,
9
+ } from '../shared.js';
10
+ import {useParam} from '../../engine/params-runtime.js';
11
+
12
+ /**
13
+ * Bot: Sentinel
14
+ *
15
+ * BEHAVIOR: Stationary tank with last-moment shielding AND two-tier offense.
16
+ * Like Turtle, never moves and shields at the last moment. Unlike Turtle,
17
+ * fires bigger missiles (damage 20) when the safe window is large enough,
18
+ * falling back to Turtle's fast missile (damage 12) when pressed.
19
+ *
20
+ * PROGRESSION LINE: Turtle → Sentinel → Golem
21
+ * - Turtle (tier 1): Stationary, fixed missiles, reactive shield timing
22
+ * - Sentinel (tier 2): Stationary, two-tier offense (big + fast missiles)
23
+ * - Golem (tier 3): Immovable fortress, perfect shield timing
24
+ *
25
+ * TIER: 2 (enhanced Turtle)
26
+ */
27
+ export const Sentinel: WizardFunction = ({state}) =>
28
+ {
29
+ const shieldHoldTicks = useParam('shieldHoldTicks', 2, {
30
+ range: 5,
31
+ min: 0,
32
+ max: 15,
33
+ steps: 5,
34
+ });
35
+
36
+ const enemy = state.enemies[0];
37
+
38
+ const threats = getThreats(state);
39
+ const urgentThreat = getMostUrgentThreat(threats);
40
+
41
+ const SHIELD_BUFFER = 3;
42
+ const shouldStartShield =
43
+ urgentThreat !== null &&
44
+ urgentThreat.ticksToImpact <= SHIELD_CAST_TIME + SHIELD_BUFFER;
45
+
46
+ const TICKS_TO_RESHIELD = SHIELD_CAST_TIME + SHIELD_BUFFER;
47
+ const nextThreat = threats.filter((t) => t.willHit)[1] ?? null;
48
+ const safeToCancel =
49
+ !nextThreat || nextThreat.ticksToImpact > TICKS_TO_RESHIELD;
50
+
51
+ if (state.state === 'channeling' && state.channelingSpell === 'shield')
52
+ {
53
+ const imminentThreat = threats.find(
54
+ (t) => t.willHit && t.ticksToImpact <= shieldHoldTicks,
55
+ );
56
+ if (imminentThreat) return {move: {x: 0, y: 0}};
57
+ if (safeToCancel) return {move: {x: 0, y: 0}, cancel: true};
58
+ return {move: {x: 0, y: 0}};
59
+ }
60
+
61
+ if (state.state === 'casting' && state.castingSpell === 'shield')
62
+ {
63
+ return {move: {x: 0, y: 0}};
64
+ }
65
+
66
+ if (shouldStartShield)
67
+ {
68
+ if (state.state === 'casting') return {move: {x: 0, y: 0}, cancel: true};
69
+ if (state.state === 'idle')
70
+ {
71
+ return {move: {x: 0, y: 0}, startCast: {spell: 'shield'}};
72
+ }
73
+ }
74
+
75
+ if (
76
+ state.state === 'idle' &&
77
+ urgentThreat &&
78
+ urgentThreat.ticksToImpact < SHIELD_CAST_TIME + SHIELD_BUFFER &&
79
+ urgentThreat.ticksToImpact >= 10 &&
80
+ state.blinkCooldown === 0
81
+ )
82
+ {
83
+ return {
84
+ move: {x: 0, y: 0},
85
+ startCast: {spell: 'blink', target: getBlinkToCenter(state.position)},
86
+ };
87
+ }
88
+
89
+ // Two-tier offense: big missile when safe, fast missile otherwise.
90
+ // Same speed/turnRate/duration → no warmup switching penalty.
91
+ if (state.state === 'idle' && enemy)
92
+ {
93
+ const FAST_MISSILE = {damage: 12, speed: 5, turnRate: 1, duration: 180};
94
+ const BIG_MISSILE = {damage: 20, speed: 5, turnRate: 1, duration: 180};
95
+
96
+ const nextThreatTime = urgentThreat?.ticksToImpact ?? Infinity;
97
+
98
+ const bigCast = getMissileCastTime(BIG_MISSILE, state.lastMissileConfig);
99
+ const bigWindow = bigCast + GCD_DURATION + SHIELD_CAST_TIME + SHIELD_BUFFER;
100
+ if (!urgentThreat || nextThreatTime > bigWindow)
101
+ {
102
+ return {
103
+ move: {x: 0, y: 0},
104
+ startCast: {
105
+ spell: 'missile',
106
+ config: BIG_MISSILE,
107
+ missileAI: ({worldState}): MissileActions => ({
108
+ turnToward: worldState.enemies[0]?.position,
109
+ }),
110
+ direction: angleTo(state.position, enemy.position),
111
+ },
112
+ };
113
+ }
114
+
115
+ const fastCast = getMissileCastTime(FAST_MISSILE, state.lastMissileConfig);
116
+ const fastWindow = fastCast + GCD_DURATION + SHIELD_CAST_TIME + SHIELD_BUFFER;
117
+ if (!urgentThreat || nextThreatTime > fastWindow)
118
+ {
119
+ return {
120
+ move: {x: 0, y: 0},
121
+ startCast: {
122
+ spell: 'missile',
123
+ config: FAST_MISSILE,
124
+ missileAI: ({worldState}): MissileActions => ({
125
+ turnToward: worldState.enemies[0]?.position,
126
+ }),
127
+ direction: angleTo(state.position, enemy.position),
128
+ },
129
+ };
130
+ }
131
+ }
132
+
133
+ return {move: {x: 0, y: 0}};
134
+ };
@@ -0,0 +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
+ };