@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.
- package/README.md +28 -28
- package/dist/{chunk-L7Z7OFXD.js → chunk-7VDJHO22.js} +3 -3
- package/dist/chunk-7VDJHO22.js.map +1 -0
- package/dist/index-browser.d.ts +1 -1
- package/dist/index-browser.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +79 -78
- package/src/bots/berserker/01_Stormchaser.ts +457 -457
- package/src/bots/berserker/02_Stormcaller.ts +417 -417
- package/src/bots/berserker/03_Stormforger.ts +481 -481
- package/src/bots/caster/01_Flamecaller.ts +286 -286
- package/src/bots/caster/02_Pyromancer.ts +350 -350
- package/src/bots/caster/03_Infernalist.ts +492 -492
- package/src/bots/defensive/01_Turtle.ts +151 -151
- package/src/bots/defensive/02_Sentinel.ts +134 -134
- package/src/bots/defensive/03_Golem.ts +357 -357
- package/src/bots/duelist/01_Battlemage.ts +433 -433
- package/src/bots/duelist/02_Warmage.ts +438 -438
- package/src/bots/duelist/03_Archmage.ts +588 -588
- package/src/bots/homing/01_Bonemancer.ts +67 -67
- package/src/bots/homing/02_Lich.ts +356 -356
- package/src/bots/homing/03_Archlich.ts +220 -220
- package/src/bots/kiter/01_Spellspinner.ts +398 -398
- package/src/bots/kiter/02_Spellweaver.ts +378 -378
- package/src/bots/kiter/03_Spellbinder.ts +448 -448
- package/src/bots/melee/01_Shadowblade.ts +270 -270
- package/src/bots/melee/02_Nightblade.ts +437 -437
- package/src/bots/melee/03_Voidblade.ts +582 -582
- package/src/bots/sniper/01_Spellshot.ts +385 -385
- package/src/bots/sniper/02_Spelltracer.ts +441 -441
- package/src/bots/sniper/03_Spellseeker.ts +546 -546
- package/src/bots/standalone/Critter.ts +89 -89
- package/src/bots/standalone/Doombringer.ts +91 -91
- package/src/bots/standalone/Hogger.ts +228 -228
- package/src/bots/standalone/Rookie.ts +50 -50
- package/src/bots/standalone/TargetDummy.ts +21 -21
- package/src/bots/test/cheater.ts +405 -405
- package/src/bots/test/crasher.ts +81 -81
- package/src/engine/hooks-runtime.ts +394 -394
- package/src/engine/manual-match.ts +289 -289
- package/src/engine/missile-templates.ts +155 -155
- package/src/engine/physics.ts +143 -143
- package/src/engine/simulation.ts +828 -828
- package/src/engine/spells.ts +128 -128
- package/src/engine-version.ts +1 -1
- package/src/index.ts +23 -23
- package/src/rules.ts +254 -254
- package/src/types.ts +193 -193
- package/src/utils/index.ts +6 -6
- 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
|
+
};
|