@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,151 +1,151 @@
1
- import {WizardFunction} 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: Turtle
14
- *
15
- * BEHAVIOR: Stationary tank with a simple defense-first loop: shield → missile
16
- * → wait → shield → repeat. Never moves. Always prioritizes having shield
17
- * ready over dealing damage.
18
- *
19
- * COMBAT LOOP:
20
- * 1. Incoming missile? → Shield immediately (cancel anything else)
21
- * 2. Hold shield until missile has actually impacted (don't cancel early!)
22
- * 3. Cancel shield → GCD → Fire one missile → GCD → idle
23
- * 4. Wait idle for next threat
24
- * 5. Goto 1
25
- *
26
- * CRITICAL: Does NOT cancel shield until incoming missiles have impacted.
27
- * Shield must be held through impact to actually block damage.
28
- *
29
- * Uses damage 10, speed 5, turnRate 1, duration 180 (~81 tick cast).
30
- * Emergency blinks toward center when caught in GCD with unavoidable hit.
31
- *
32
- * NAMING RATIONALE: "Turtling" is the universal gaming term for extreme
33
- * defensive play. Sits still and blocks everything.
34
- *
35
- * PROGRESSION LINE: Turtle → Sentinel → Golem
36
- * - Turtle (tier 1): Stationary, fixed missiles, always-shield-first
37
- * - Sentinel (tier 2): Stationary, adaptive missile fitting (fitMissileToBudget)
38
- * - Golem (tier 3): Future — immovable fortress, perfect timing, punishing hits
39
- *
40
- * TIER: 1 (base)
41
- */
42
-
43
- // Homing missile: damage 12, speed 5, turnRate 1, duration 180.
44
- // Range = 5 × 180 = 900 units.
45
- const MISSILE_CONFIG = {damage: 12, speed: 5, turnRate: 1, duration: 180};
46
-
47
- export const Turtle: WizardFunction = ({state}) =>
48
- {
49
- const shieldBuffer = useParam('shieldBuffer', 3.1, {
50
- range: 5,
51
- min: 0,
52
- steps: 5,
53
- });
54
- const shieldHoldTicks = useParam('shieldHoldTicks', 5, {
55
- range: 5,
56
- min: 0,
57
- steps: 5,
58
- });
59
- const ticksToReshield = SHIELD_CAST_TIME + shieldBuffer;
60
-
61
- const enemy = state.enemies[0];
62
- if (!enemy) return {move: {x: 0, y: 0}};
63
-
64
- const threats = getThreats(state);
65
- const urgentThreat = getMostUrgentThreat(threats);
66
-
67
- const shouldStartShield =
68
- urgentThreat !== null &&
69
- urgentThreat.ticksToImpact <= SHIELD_CAST_TIME + shieldBuffer;
70
-
71
- // === CHANNELING SHIELD ===
72
- // Hold shield until the imminent missile(s) have actually impacted.
73
- if (state.state === 'channeling' && state.channelingSpell === 'shield')
74
- {
75
- // Don't cancel while a missile is about to hit
76
- const imminentThreat = threats.find(
77
- (t) => t.willHit && t.ticksToImpact <= shieldHoldTicks,
78
- );
79
- if (imminentThreat)
80
- {
81
- return {move: {x: 0, y: 0}};
82
- }
83
-
84
- // Safe to cancel if no next threat, or next threat is far enough to reshield
85
- const nextThreat = threats.find((t) => t.willHit);
86
- if (!nextThreat || nextThreat.ticksToImpact > ticksToReshield)
87
- {
88
- return {move: {x: 0, y: 0}, cancel: true};
89
- }
90
- // Next threat too soon — keep channeling
91
- return {move: {x: 0, y: 0}};
92
- }
93
-
94
- // === CASTING SHIELD: let it finish ===
95
- if (state.state === 'casting' && state.castingSpell === 'shield')
96
- {
97
- return {move: {x: 0, y: 0}};
98
- }
99
-
100
- // === NEED TO SHIELD NOW: cancel anything else ===
101
- if (shouldStartShield)
102
- {
103
- if (state.state === 'casting')
104
- {
105
- return {move: {x: 0, y: 0}, cancel: true};
106
- }
107
- if (state.state === 'idle')
108
- {
109
- return {move: {x: 0, y: 0}, startCast: {spell: 'shield'}};
110
- }
111
- }
112
-
113
- // === EMERGENCY BLINK: missile too close for shield, blink available ===
114
- if (
115
- state.state === 'idle' &&
116
- urgentThreat &&
117
- urgentThreat.ticksToImpact < SHIELD_CAST_TIME + shieldBuffer &&
118
- urgentThreat.ticksToImpact >= 10 &&
119
- state.blinkCooldown === 0
120
- )
121
- {
122
- return {
123
- move: {x: 0, y: 0},
124
- startCast: {spell: 'blink', target: getBlinkToCenter(state.position)},
125
- };
126
- }
127
-
128
- // === ATTACK: one missile if safe window exists ===
129
- // Dynamic safe window: actual cast time (with warmup) + GCD + shield + buffer
130
- const castTime = getMissileCastTime(MISSILE_CONFIG, state.lastMissileConfig);
131
- const safeWindow = castTime + GCD_DURATION + SHIELD_CAST_TIME + shieldBuffer;
132
- if (
133
- state.state === 'idle' &&
134
- (!urgentThreat || urgentThreat.ticksToImpact > safeWindow)
135
- )
136
- {
137
- return {
138
- move: {x: 0, y: 0},
139
- startCast: {
140
- spell: 'missile',
141
- config: MISSILE_CONFIG,
142
- missileAI: ({worldState}) => ({
143
- turnToward: worldState.enemies[0]?.position,
144
- }),
145
- direction: angleTo(state.position, enemy.position),
146
- },
147
- };
148
- }
149
-
150
- return {move: {x: 0, y: 0}};
151
- };
1
+ import {WizardFunction} 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: Turtle
14
+ *
15
+ * BEHAVIOR: Stationary tank with a simple defense-first loop: shield → missile
16
+ * → wait → shield → repeat. Never moves. Always prioritizes having shield
17
+ * ready over dealing damage.
18
+ *
19
+ * COMBAT LOOP:
20
+ * 1. Incoming missile? → Shield immediately (cancel anything else)
21
+ * 2. Hold shield until missile has actually impacted (don't cancel early!)
22
+ * 3. Cancel shield → GCD → Fire one missile → GCD → idle
23
+ * 4. Wait idle for next threat
24
+ * 5. Goto 1
25
+ *
26
+ * CRITICAL: Does NOT cancel shield until incoming missiles have impacted.
27
+ * Shield must be held through impact to actually block damage.
28
+ *
29
+ * Uses damage 10, speed 5, turnRate 1, duration 180 (~81 tick cast).
30
+ * Emergency blinks toward center when caught in GCD with unavoidable hit.
31
+ *
32
+ * NAMING RATIONALE: "Turtling" is the universal gaming term for extreme
33
+ * defensive play. Sits still and blocks everything.
34
+ *
35
+ * PROGRESSION LINE: Turtle → Sentinel → Golem
36
+ * - Turtle (tier 1): Stationary, fixed missiles, always-shield-first
37
+ * - Sentinel (tier 2): Stationary, adaptive missile fitting (fitMissileToBudget)
38
+ * - Golem (tier 3): Future — immovable fortress, perfect timing, punishing hits
39
+ *
40
+ * TIER: 1 (base)
41
+ */
42
+
43
+ // Homing missile: damage 12, speed 5, turnRate 1, duration 180.
44
+ // Range = 5 × 180 = 900 units.
45
+ const MISSILE_CONFIG = {damage: 12, speed: 5, turnRate: 1, duration: 180};
46
+
47
+ export const Turtle: WizardFunction = ({state}) =>
48
+ {
49
+ const shieldBuffer = useParam('shieldBuffer', 3.1, {
50
+ range: 5,
51
+ min: 0,
52
+ steps: 5,
53
+ });
54
+ const shieldHoldTicks = useParam('shieldHoldTicks', 5, {
55
+ range: 5,
56
+ min: 0,
57
+ steps: 5,
58
+ });
59
+ const ticksToReshield = SHIELD_CAST_TIME + shieldBuffer;
60
+
61
+ const enemy = state.enemies[0];
62
+ if (!enemy) return {move: {x: 0, y: 0}};
63
+
64
+ const threats = getThreats(state);
65
+ const urgentThreat = getMostUrgentThreat(threats);
66
+
67
+ const shouldStartShield =
68
+ urgentThreat !== null &&
69
+ urgentThreat.ticksToImpact <= SHIELD_CAST_TIME + shieldBuffer;
70
+
71
+ // === CHANNELING SHIELD ===
72
+ // Hold shield until the imminent missile(s) have actually impacted.
73
+ if (state.state === 'channeling' && state.channelingSpell === 'shield')
74
+ {
75
+ // Don't cancel while a missile is about to hit
76
+ const imminentThreat = threats.find(
77
+ (t) => t.willHit && t.ticksToImpact <= shieldHoldTicks,
78
+ );
79
+ if (imminentThreat)
80
+ {
81
+ return {move: {x: 0, y: 0}};
82
+ }
83
+
84
+ // Safe to cancel if no next threat, or next threat is far enough to reshield
85
+ const nextThreat = threats.find((t) => t.willHit);
86
+ if (!nextThreat || nextThreat.ticksToImpact > ticksToReshield)
87
+ {
88
+ return {move: {x: 0, y: 0}, cancel: true};
89
+ }
90
+ // Next threat too soon — keep channeling
91
+ return {move: {x: 0, y: 0}};
92
+ }
93
+
94
+ // === CASTING SHIELD: let it finish ===
95
+ if (state.state === 'casting' && state.castingSpell === 'shield')
96
+ {
97
+ return {move: {x: 0, y: 0}};
98
+ }
99
+
100
+ // === NEED TO SHIELD NOW: cancel anything else ===
101
+ if (shouldStartShield)
102
+ {
103
+ if (state.state === 'casting')
104
+ {
105
+ return {move: {x: 0, y: 0}, cancel: true};
106
+ }
107
+ if (state.state === 'idle')
108
+ {
109
+ return {move: {x: 0, y: 0}, startCast: {spell: 'shield'}};
110
+ }
111
+ }
112
+
113
+ // === EMERGENCY BLINK: missile too close for shield, blink available ===
114
+ if (
115
+ state.state === 'idle' &&
116
+ urgentThreat &&
117
+ urgentThreat.ticksToImpact < SHIELD_CAST_TIME + shieldBuffer &&
118
+ urgentThreat.ticksToImpact >= 10 &&
119
+ state.blinkCooldown === 0
120
+ )
121
+ {
122
+ return {
123
+ move: {x: 0, y: 0},
124
+ startCast: {spell: 'blink', target: getBlinkToCenter(state.position)},
125
+ };
126
+ }
127
+
128
+ // === ATTACK: one missile if safe window exists ===
129
+ // Dynamic safe window: actual cast time (with warmup) + GCD + shield + buffer
130
+ const castTime = getMissileCastTime(MISSILE_CONFIG, state.lastMissileConfig);
131
+ const safeWindow = castTime + GCD_DURATION + SHIELD_CAST_TIME + shieldBuffer;
132
+ if (
133
+ state.state === 'idle' &&
134
+ (!urgentThreat || urgentThreat.ticksToImpact > safeWindow)
135
+ )
136
+ {
137
+ return {
138
+ move: {x: 0, y: 0},
139
+ startCast: {
140
+ spell: 'missile',
141
+ config: MISSILE_CONFIG,
142
+ missileAI: ({worldState}) => ({
143
+ turnToward: worldState.enemies[0]?.position,
144
+ }),
145
+ direction: angleTo(state.position, enemy.position),
146
+ },
147
+ };
148
+ }
149
+
150
+ return {move: {x: 0, y: 0}};
151
+ };
@@ -1,134 +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
- };
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
+ };