@vibemancer/core 0.1.0 → 0.1.2
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-OL7ETV6V.js} +3 -3
- package/dist/chunk-OL7ETV6V.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,270 +1,270 @@
|
|
|
1
|
-
import {WizardFunction} from '../../types.js';
|
|
2
|
-
import {distanceTo} from '../../utils/distance.js';
|
|
3
|
-
import {ARENA_SIZE} from '../../rules.js';
|
|
4
|
-
import {
|
|
5
|
-
getThreats,
|
|
6
|
-
getMostUrgentThreat,
|
|
7
|
-
getDodgeDirectionForMovement,
|
|
8
|
-
getBlinkToDecreaseDistance,
|
|
9
|
-
shouldForceShield,
|
|
10
|
-
} from '../shared.js';
|
|
11
|
-
import {useParam} from '../../engine/params-runtime.js';
|
|
12
|
-
const WALL_BUFFER = 50;
|
|
13
|
-
|
|
14
|
-
// Melee stab: 20 damage, speed 8, 3-hit kill.
|
|
15
|
-
const STAB_CONFIG = {damage: 20, speed: 8, turnRate: 0, duration: 10};
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Aggressive movement: dodge missiles using threat analysis, close distance, light strafe.
|
|
19
|
-
*/
|
|
20
|
-
function aggressiveMove(
|
|
21
|
-
position: {x: number; y: number},
|
|
22
|
-
enemy: {position: {x: number; y: number}},
|
|
23
|
-
currentDistance: number,
|
|
24
|
-
dodgeDirection: {x: number; y: number} | null,
|
|
25
|
-
tick: number,
|
|
26
|
-
strafeCyclePeriod: number,
|
|
27
|
-
strafeIntensity: number,
|
|
28
|
-
approachSpeed: number,
|
|
29
|
-
): {x: number; y: number}
|
|
30
|
-
{
|
|
31
|
-
const deltaX = enemy.position.x - position.x;
|
|
32
|
-
const deltaY = enemy.position.y - position.y;
|
|
33
|
-
const normalizedDistance = currentDistance > 0 ? currentDistance : 1;
|
|
34
|
-
|
|
35
|
-
const strafeClockwise = {
|
|
36
|
-
x: -deltaY / normalizedDistance,
|
|
37
|
-
y: deltaX / normalizedDistance,
|
|
38
|
-
};
|
|
39
|
-
const strafeCounterClockwise = {
|
|
40
|
-
x: deltaY / normalizedDistance,
|
|
41
|
-
y: -deltaX / normalizedDistance,
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
let strafeDirection: {x: number; y: number};
|
|
45
|
-
|
|
46
|
-
if (dodgeDirection)
|
|
47
|
-
{
|
|
48
|
-
strafeDirection = dodgeDirection;
|
|
49
|
-
}
|
|
50
|
-
else if (
|
|
51
|
-
position.x < WALL_BUFFER ||
|
|
52
|
-
position.x > ARENA_SIZE - WALL_BUFFER ||
|
|
53
|
-
position.y < WALL_BUFFER ||
|
|
54
|
-
position.y > ARENA_SIZE - WALL_BUFFER
|
|
55
|
-
)
|
|
56
|
-
{
|
|
57
|
-
const futureClockwise = {
|
|
58
|
-
x: position.x + strafeClockwise.x * 100,
|
|
59
|
-
y: position.y + strafeClockwise.y * 100,
|
|
60
|
-
};
|
|
61
|
-
const futureCounterClockwise = {
|
|
62
|
-
x: position.x + strafeCounterClockwise.x * 100,
|
|
63
|
-
y: position.y + strafeCounterClockwise.y * 100,
|
|
64
|
-
};
|
|
65
|
-
const scoreClockwise = Math.min(
|
|
66
|
-
futureClockwise.x,
|
|
67
|
-
ARENA_SIZE - futureClockwise.x,
|
|
68
|
-
futureClockwise.y,
|
|
69
|
-
ARENA_SIZE - futureClockwise.y,
|
|
70
|
-
);
|
|
71
|
-
const scoreCounterClockwise = Math.min(
|
|
72
|
-
futureCounterClockwise.x,
|
|
73
|
-
ARENA_SIZE - futureCounterClockwise.x,
|
|
74
|
-
futureCounterClockwise.y,
|
|
75
|
-
ARENA_SIZE - futureCounterClockwise.y,
|
|
76
|
-
);
|
|
77
|
-
strafeDirection =
|
|
78
|
-
scoreClockwise > scoreCounterClockwise
|
|
79
|
-
? strafeClockwise
|
|
80
|
-
: strafeCounterClockwise;
|
|
81
|
-
}
|
|
82
|
-
else
|
|
83
|
-
{
|
|
84
|
-
const cycle = Math.floor(tick / strafeCyclePeriod) % 2;
|
|
85
|
-
strafeDirection = cycle === 0 ? strafeClockwise : strafeCounterClockwise;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// When actively dodging, commit fully
|
|
89
|
-
if (dodgeDirection)
|
|
90
|
-
{
|
|
91
|
-
return {x: strafeDirection.x * 100, y: strafeDirection.y * 100};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Light strafe + aggressive closing
|
|
95
|
-
let moveX = strafeDirection.x * strafeIntensity;
|
|
96
|
-
let moveY = strafeDirection.y * strafeIntensity;
|
|
97
|
-
|
|
98
|
-
// Always close distance aggressively (melee wants to be in the enemy's face)
|
|
99
|
-
moveX += (deltaX / normalizedDistance) * approachSpeed;
|
|
100
|
-
moveY += (deltaY / normalizedDistance) * approachSpeed;
|
|
101
|
-
|
|
102
|
-
const magnitude = Math.sqrt(moveX * moveX + moveY * moveY);
|
|
103
|
-
if (magnitude > 100)
|
|
104
|
-
{
|
|
105
|
-
moveX = (moveX / magnitude) * 100;
|
|
106
|
-
moveY = (moveY / magnitude) * 100;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return {x: moveX, y: moveY};
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Bot: Shadowblade
|
|
114
|
-
*
|
|
115
|
-
* BEHAVIOR: Melee assassin. Blinks to the enemy, then lands devastating point-blank
|
|
116
|
-
* stab attacks (15 damage, 30u range, ~60 tick cast = 2-hit kill). Runs directly
|
|
117
|
-
* at the enemy with minimal strafe, shields undodgeable threats. The entire
|
|
118
|
-
* strategy is: get close, stab, kill. Simple and brutal.
|
|
119
|
-
*
|
|
120
|
-
* PROGRESSION LINE: Shadowblade → Nightblade → Voidblade
|
|
121
|
-
* - Shadowblade (tier 1): Offensive blink, melee stabs, basic shield
|
|
122
|
-
* - Nightblade (tier 2): + missile-aware blinks, adaptive stabs, timed defense
|
|
123
|
-
* - Voidblade (tier 3): Future — perfect assassination timing, inescapable engages
|
|
124
|
-
*
|
|
125
|
-
* TIER: 1 (base)
|
|
126
|
-
*/
|
|
127
|
-
export const Shadowblade: WizardFunction = ({state}) =>
|
|
128
|
-
{
|
|
129
|
-
const stabRange = useParam('stabRange', 76, {
|
|
130
|
-
range: 30,
|
|
131
|
-
min: 20,
|
|
132
|
-
max: 80,
|
|
133
|
-
steps: 5,
|
|
134
|
-
});
|
|
135
|
-
const strafeCyclePeriod = useParam('strafeCyclePeriod', 252, {
|
|
136
|
-
range: 75,
|
|
137
|
-
min: 80,
|
|
138
|
-
max: 300,
|
|
139
|
-
});
|
|
140
|
-
const strafeIntensity = useParam('strafeIntensity', 55, {
|
|
141
|
-
range: 30,
|
|
142
|
-
min: 10,
|
|
143
|
-
max: 80,
|
|
144
|
-
steps: 7,
|
|
145
|
-
});
|
|
146
|
-
const approachSpeed = useParam('approachSpeed', 61, {
|
|
147
|
-
range: 20,
|
|
148
|
-
min: 50,
|
|
149
|
-
max: 100,
|
|
150
|
-
steps: 7,
|
|
151
|
-
});
|
|
152
|
-
const shieldCancelThreshold = useParam('shieldCancelThreshold', 150, {
|
|
153
|
-
range: 75,
|
|
154
|
-
min: 50,
|
|
155
|
-
max: 300,
|
|
156
|
-
});
|
|
157
|
-
const blinkWindowMax = useParam('blinkWindowMax', 23, {
|
|
158
|
-
range: 30,
|
|
159
|
-
min: 15,
|
|
160
|
-
max: 80,
|
|
161
|
-
steps: 7,
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
const enemy = state.enemies[0];
|
|
165
|
-
if (!enemy)
|
|
166
|
-
{
|
|
167
|
-
return {move: {x: 0, y: 0}};
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const distance = distanceTo(state.position, enemy.position);
|
|
171
|
-
|
|
172
|
-
const threats = getThreats(state);
|
|
173
|
-
const urgentThreat = getMostUrgentThreat(threats);
|
|
174
|
-
|
|
175
|
-
// Cancel shield if no longer needed
|
|
176
|
-
if (state.state === 'channeling')
|
|
177
|
-
{
|
|
178
|
-
if (!urgentThreat || urgentThreat.ticksToImpact > shieldCancelThreshold)
|
|
179
|
-
{
|
|
180
|
-
return {move: {x: 0, y: 0}, cancel: true};
|
|
181
|
-
}
|
|
182
|
-
return {move: {x: 0, y: 0}};
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (state.state === 'idle')
|
|
186
|
-
{
|
|
187
|
-
// === BLINK: Dodge missile + close gap simultaneously (first priority) ===
|
|
188
|
-
// Wait until the missile is close (<=40 ticks) so the blink actually dodges it.
|
|
189
|
-
// Blinking too early means the missile can still re-track. Too late (<10) and the
|
|
190
|
-
// blink cast won't complete in time.
|
|
191
|
-
if (
|
|
192
|
-
urgentThreat &&
|
|
193
|
-
state.blinkCooldown === 0 &&
|
|
194
|
-
distance > stabRange &&
|
|
195
|
-
urgentThreat.ticksToImpact >= 10 &&
|
|
196
|
-
urgentThreat.ticksToImpact <= blinkWindowMax
|
|
197
|
-
)
|
|
198
|
-
{
|
|
199
|
-
const blinkTarget =
|
|
200
|
-
getBlinkToDecreaseDistance(
|
|
201
|
-
state.position,
|
|
202
|
-
enemy.position,
|
|
203
|
-
state.projectiles,
|
|
204
|
-
) ?? enemy.position;
|
|
205
|
-
return {
|
|
206
|
-
move: {x: 0, y: 0},
|
|
207
|
-
startCast: {spell: 'blink', target: blinkTarget},
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// Pre-stab shield: already at melee range, survival beats landing another stab.
|
|
212
|
-
// Gated to stab range so out-of-range threats wait for the blink window.
|
|
213
|
-
if (
|
|
214
|
-
urgentThreat &&
|
|
215
|
-
distance < stabRange &&
|
|
216
|
-
(!urgentThreat.bestDodgeDirection || shouldForceShield(urgentThreat)) &&
|
|
217
|
-
urgentThreat.canBlockInTime &&
|
|
218
|
-
urgentThreat.ticksToStartShield <= 3
|
|
219
|
-
)
|
|
220
|
-
{
|
|
221
|
-
return {
|
|
222
|
-
move: {x: 0, y: 0},
|
|
223
|
-
startCast: {spell: 'shield'},
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// Melee stab when close enough (offense over defense at melee range)
|
|
228
|
-
if (distance < stabRange)
|
|
229
|
-
{
|
|
230
|
-
return {
|
|
231
|
-
move: {x: 0, y: 0},
|
|
232
|
-
startCast: {
|
|
233
|
-
spell: 'missile',
|
|
234
|
-
config: STAB_CONFIG,
|
|
235
|
-
missileAI: () => ({}), // Non-homing, point blank
|
|
236
|
-
},
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// === DEFENSE: Shield undodgeable threats (only when blink can't handle it) ===
|
|
241
|
-
// Don't shield if blink is available — wait for the blink window instead.
|
|
242
|
-
if (
|
|
243
|
-
urgentThreat &&
|
|
244
|
-
(!urgentThreat.bestDodgeDirection || shouldForceShield(urgentThreat)) &&
|
|
245
|
-
urgentThreat.canBlockInTime &&
|
|
246
|
-
urgentThreat.ticksToStartShield <= 3 &&
|
|
247
|
-
(state.blinkCooldown > 0 || urgentThreat.ticksToImpact < 10)
|
|
248
|
-
)
|
|
249
|
-
{
|
|
250
|
-
return {
|
|
251
|
-
move: {x: 0, y: 0},
|
|
252
|
-
startCast: {spell: 'shield'},
|
|
253
|
-
};
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// Aggressive movement — always close distance
|
|
258
|
-
const dodgeDirection = getDodgeDirectionForMovement(threats, state.position);
|
|
259
|
-
const move = aggressiveMove(
|
|
260
|
-
state.position,
|
|
261
|
-
enemy,
|
|
262
|
-
distance,
|
|
263
|
-
dodgeDirection,
|
|
264
|
-
state.tick,
|
|
265
|
-
strafeCyclePeriod,
|
|
266
|
-
strafeIntensity,
|
|
267
|
-
approachSpeed,
|
|
268
|
-
);
|
|
269
|
-
return {move};
|
|
270
|
-
};
|
|
1
|
+
import {WizardFunction} from '../../types.js';
|
|
2
|
+
import {distanceTo} from '../../utils/distance.js';
|
|
3
|
+
import {ARENA_SIZE} from '../../rules.js';
|
|
4
|
+
import {
|
|
5
|
+
getThreats,
|
|
6
|
+
getMostUrgentThreat,
|
|
7
|
+
getDodgeDirectionForMovement,
|
|
8
|
+
getBlinkToDecreaseDistance,
|
|
9
|
+
shouldForceShield,
|
|
10
|
+
} from '../shared.js';
|
|
11
|
+
import {useParam} from '../../engine/params-runtime.js';
|
|
12
|
+
const WALL_BUFFER = 50;
|
|
13
|
+
|
|
14
|
+
// Melee stab: 20 damage, speed 8, 3-hit kill.
|
|
15
|
+
const STAB_CONFIG = {damage: 20, speed: 8, turnRate: 0, duration: 10};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Aggressive movement: dodge missiles using threat analysis, close distance, light strafe.
|
|
19
|
+
*/
|
|
20
|
+
function aggressiveMove(
|
|
21
|
+
position: {x: number; y: number},
|
|
22
|
+
enemy: {position: {x: number; y: number}},
|
|
23
|
+
currentDistance: number,
|
|
24
|
+
dodgeDirection: {x: number; y: number} | null,
|
|
25
|
+
tick: number,
|
|
26
|
+
strafeCyclePeriod: number,
|
|
27
|
+
strafeIntensity: number,
|
|
28
|
+
approachSpeed: number,
|
|
29
|
+
): {x: number; y: number}
|
|
30
|
+
{
|
|
31
|
+
const deltaX = enemy.position.x - position.x;
|
|
32
|
+
const deltaY = enemy.position.y - position.y;
|
|
33
|
+
const normalizedDistance = currentDistance > 0 ? currentDistance : 1;
|
|
34
|
+
|
|
35
|
+
const strafeClockwise = {
|
|
36
|
+
x: -deltaY / normalizedDistance,
|
|
37
|
+
y: deltaX / normalizedDistance,
|
|
38
|
+
};
|
|
39
|
+
const strafeCounterClockwise = {
|
|
40
|
+
x: deltaY / normalizedDistance,
|
|
41
|
+
y: -deltaX / normalizedDistance,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
let strafeDirection: {x: number; y: number};
|
|
45
|
+
|
|
46
|
+
if (dodgeDirection)
|
|
47
|
+
{
|
|
48
|
+
strafeDirection = dodgeDirection;
|
|
49
|
+
}
|
|
50
|
+
else if (
|
|
51
|
+
position.x < WALL_BUFFER ||
|
|
52
|
+
position.x > ARENA_SIZE - WALL_BUFFER ||
|
|
53
|
+
position.y < WALL_BUFFER ||
|
|
54
|
+
position.y > ARENA_SIZE - WALL_BUFFER
|
|
55
|
+
)
|
|
56
|
+
{
|
|
57
|
+
const futureClockwise = {
|
|
58
|
+
x: position.x + strafeClockwise.x * 100,
|
|
59
|
+
y: position.y + strafeClockwise.y * 100,
|
|
60
|
+
};
|
|
61
|
+
const futureCounterClockwise = {
|
|
62
|
+
x: position.x + strafeCounterClockwise.x * 100,
|
|
63
|
+
y: position.y + strafeCounterClockwise.y * 100,
|
|
64
|
+
};
|
|
65
|
+
const scoreClockwise = Math.min(
|
|
66
|
+
futureClockwise.x,
|
|
67
|
+
ARENA_SIZE - futureClockwise.x,
|
|
68
|
+
futureClockwise.y,
|
|
69
|
+
ARENA_SIZE - futureClockwise.y,
|
|
70
|
+
);
|
|
71
|
+
const scoreCounterClockwise = Math.min(
|
|
72
|
+
futureCounterClockwise.x,
|
|
73
|
+
ARENA_SIZE - futureCounterClockwise.x,
|
|
74
|
+
futureCounterClockwise.y,
|
|
75
|
+
ARENA_SIZE - futureCounterClockwise.y,
|
|
76
|
+
);
|
|
77
|
+
strafeDirection =
|
|
78
|
+
scoreClockwise > scoreCounterClockwise
|
|
79
|
+
? strafeClockwise
|
|
80
|
+
: strafeCounterClockwise;
|
|
81
|
+
}
|
|
82
|
+
else
|
|
83
|
+
{
|
|
84
|
+
const cycle = Math.floor(tick / strafeCyclePeriod) % 2;
|
|
85
|
+
strafeDirection = cycle === 0 ? strafeClockwise : strafeCounterClockwise;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// When actively dodging, commit fully
|
|
89
|
+
if (dodgeDirection)
|
|
90
|
+
{
|
|
91
|
+
return {x: strafeDirection.x * 100, y: strafeDirection.y * 100};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Light strafe + aggressive closing
|
|
95
|
+
let moveX = strafeDirection.x * strafeIntensity;
|
|
96
|
+
let moveY = strafeDirection.y * strafeIntensity;
|
|
97
|
+
|
|
98
|
+
// Always close distance aggressively (melee wants to be in the enemy's face)
|
|
99
|
+
moveX += (deltaX / normalizedDistance) * approachSpeed;
|
|
100
|
+
moveY += (deltaY / normalizedDistance) * approachSpeed;
|
|
101
|
+
|
|
102
|
+
const magnitude = Math.sqrt(moveX * moveX + moveY * moveY);
|
|
103
|
+
if (magnitude > 100)
|
|
104
|
+
{
|
|
105
|
+
moveX = (moveX / magnitude) * 100;
|
|
106
|
+
moveY = (moveY / magnitude) * 100;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {x: moveX, y: moveY};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Bot: Shadowblade
|
|
114
|
+
*
|
|
115
|
+
* BEHAVIOR: Melee assassin. Blinks to the enemy, then lands devastating point-blank
|
|
116
|
+
* stab attacks (15 damage, 30u range, ~60 tick cast = 2-hit kill). Runs directly
|
|
117
|
+
* at the enemy with minimal strafe, shields undodgeable threats. The entire
|
|
118
|
+
* strategy is: get close, stab, kill. Simple and brutal.
|
|
119
|
+
*
|
|
120
|
+
* PROGRESSION LINE: Shadowblade → Nightblade → Voidblade
|
|
121
|
+
* - Shadowblade (tier 1): Offensive blink, melee stabs, basic shield
|
|
122
|
+
* - Nightblade (tier 2): + missile-aware blinks, adaptive stabs, timed defense
|
|
123
|
+
* - Voidblade (tier 3): Future — perfect assassination timing, inescapable engages
|
|
124
|
+
*
|
|
125
|
+
* TIER: 1 (base)
|
|
126
|
+
*/
|
|
127
|
+
export const Shadowblade: WizardFunction = ({state}) =>
|
|
128
|
+
{
|
|
129
|
+
const stabRange = useParam('stabRange', 76, {
|
|
130
|
+
range: 30,
|
|
131
|
+
min: 20,
|
|
132
|
+
max: 80,
|
|
133
|
+
steps: 5,
|
|
134
|
+
});
|
|
135
|
+
const strafeCyclePeriod = useParam('strafeCyclePeriod', 252, {
|
|
136
|
+
range: 75,
|
|
137
|
+
min: 80,
|
|
138
|
+
max: 300,
|
|
139
|
+
});
|
|
140
|
+
const strafeIntensity = useParam('strafeIntensity', 55, {
|
|
141
|
+
range: 30,
|
|
142
|
+
min: 10,
|
|
143
|
+
max: 80,
|
|
144
|
+
steps: 7,
|
|
145
|
+
});
|
|
146
|
+
const approachSpeed = useParam('approachSpeed', 61, {
|
|
147
|
+
range: 20,
|
|
148
|
+
min: 50,
|
|
149
|
+
max: 100,
|
|
150
|
+
steps: 7,
|
|
151
|
+
});
|
|
152
|
+
const shieldCancelThreshold = useParam('shieldCancelThreshold', 150, {
|
|
153
|
+
range: 75,
|
|
154
|
+
min: 50,
|
|
155
|
+
max: 300,
|
|
156
|
+
});
|
|
157
|
+
const blinkWindowMax = useParam('blinkWindowMax', 23, {
|
|
158
|
+
range: 30,
|
|
159
|
+
min: 15,
|
|
160
|
+
max: 80,
|
|
161
|
+
steps: 7,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const enemy = state.enemies[0];
|
|
165
|
+
if (!enemy)
|
|
166
|
+
{
|
|
167
|
+
return {move: {x: 0, y: 0}};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const distance = distanceTo(state.position, enemy.position);
|
|
171
|
+
|
|
172
|
+
const threats = getThreats(state);
|
|
173
|
+
const urgentThreat = getMostUrgentThreat(threats);
|
|
174
|
+
|
|
175
|
+
// Cancel shield if no longer needed
|
|
176
|
+
if (state.state === 'channeling')
|
|
177
|
+
{
|
|
178
|
+
if (!urgentThreat || urgentThreat.ticksToImpact > shieldCancelThreshold)
|
|
179
|
+
{
|
|
180
|
+
return {move: {x: 0, y: 0}, cancel: true};
|
|
181
|
+
}
|
|
182
|
+
return {move: {x: 0, y: 0}};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (state.state === 'idle')
|
|
186
|
+
{
|
|
187
|
+
// === BLINK: Dodge missile + close gap simultaneously (first priority) ===
|
|
188
|
+
// Wait until the missile is close (<=40 ticks) so the blink actually dodges it.
|
|
189
|
+
// Blinking too early means the missile can still re-track. Too late (<10) and the
|
|
190
|
+
// blink cast won't complete in time.
|
|
191
|
+
if (
|
|
192
|
+
urgentThreat &&
|
|
193
|
+
state.blinkCooldown === 0 &&
|
|
194
|
+
distance > stabRange &&
|
|
195
|
+
urgentThreat.ticksToImpact >= 10 &&
|
|
196
|
+
urgentThreat.ticksToImpact <= blinkWindowMax
|
|
197
|
+
)
|
|
198
|
+
{
|
|
199
|
+
const blinkTarget =
|
|
200
|
+
getBlinkToDecreaseDistance(
|
|
201
|
+
state.position,
|
|
202
|
+
enemy.position,
|
|
203
|
+
state.projectiles,
|
|
204
|
+
) ?? enemy.position;
|
|
205
|
+
return {
|
|
206
|
+
move: {x: 0, y: 0},
|
|
207
|
+
startCast: {spell: 'blink', target: blinkTarget},
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Pre-stab shield: already at melee range, survival beats landing another stab.
|
|
212
|
+
// Gated to stab range so out-of-range threats wait for the blink window.
|
|
213
|
+
if (
|
|
214
|
+
urgentThreat &&
|
|
215
|
+
distance < stabRange &&
|
|
216
|
+
(!urgentThreat.bestDodgeDirection || shouldForceShield(urgentThreat)) &&
|
|
217
|
+
urgentThreat.canBlockInTime &&
|
|
218
|
+
urgentThreat.ticksToStartShield <= 3
|
|
219
|
+
)
|
|
220
|
+
{
|
|
221
|
+
return {
|
|
222
|
+
move: {x: 0, y: 0},
|
|
223
|
+
startCast: {spell: 'shield'},
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Melee stab when close enough (offense over defense at melee range)
|
|
228
|
+
if (distance < stabRange)
|
|
229
|
+
{
|
|
230
|
+
return {
|
|
231
|
+
move: {x: 0, y: 0},
|
|
232
|
+
startCast: {
|
|
233
|
+
spell: 'missile',
|
|
234
|
+
config: STAB_CONFIG,
|
|
235
|
+
missileAI: () => ({}), // Non-homing, point blank
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// === DEFENSE: Shield undodgeable threats (only when blink can't handle it) ===
|
|
241
|
+
// Don't shield if blink is available — wait for the blink window instead.
|
|
242
|
+
if (
|
|
243
|
+
urgentThreat &&
|
|
244
|
+
(!urgentThreat.bestDodgeDirection || shouldForceShield(urgentThreat)) &&
|
|
245
|
+
urgentThreat.canBlockInTime &&
|
|
246
|
+
urgentThreat.ticksToStartShield <= 3 &&
|
|
247
|
+
(state.blinkCooldown > 0 || urgentThreat.ticksToImpact < 10)
|
|
248
|
+
)
|
|
249
|
+
{
|
|
250
|
+
return {
|
|
251
|
+
move: {x: 0, y: 0},
|
|
252
|
+
startCast: {spell: 'shield'},
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Aggressive movement — always close distance
|
|
258
|
+
const dodgeDirection = getDodgeDirectionForMovement(threats, state.position);
|
|
259
|
+
const move = aggressiveMove(
|
|
260
|
+
state.position,
|
|
261
|
+
enemy,
|
|
262
|
+
distance,
|
|
263
|
+
dodgeDirection,
|
|
264
|
+
state.tick,
|
|
265
|
+
strafeCyclePeriod,
|
|
266
|
+
strafeIntensity,
|
|
267
|
+
approachSpeed,
|
|
268
|
+
);
|
|
269
|
+
return {move};
|
|
270
|
+
};
|