@vibemancer/core 0.1.4 → 0.1.6
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 +2 -2
- package/dist/chunk-G5T65F3W.js +10617 -0
- package/dist/chunk-G5T65F3W.js.map +1 -0
- package/dist/index-browser.d.ts +1328 -1051
- package/dist/index-browser.js +59 -17
- package/dist/index.d.ts +53 -3
- package/dist/index.js +204 -54
- package/dist/index.js.map +1 -1
- package/package.json +21 -15
- package/src/banned-bot-names.ts +53 -0
- package/src/bots/Hero.ts +867 -0
- package/src/bots/berserker/01_Stormchaser.ts +162 -120
- package/src/bots/berserker/02_Stormcaller.ts +160 -116
- package/src/bots/berserker/03_Stormforger.ts +162 -129
- package/src/bots/caster/01_Flamecaller.ts +126 -84
- package/src/bots/caster/02_Pyromancer.ts +148 -101
- package/src/bots/caster/03_Infernalist.ts +180 -160
- package/src/bots/defensive/01_Turtle.ts +48 -44
- package/src/bots/defensive/02_Sentinel.ts +57 -53
- package/src/bots/defensive/03_Golem.ts +85 -97
- package/src/bots/duelist/01_Battlemage.ts +157 -122
- package/src/bots/duelist/02_Warmage.ts +166 -121
- package/src/bots/duelist/03_Archmage.ts +198 -178
- package/src/bots/homing/01_Bonemancer.ts +20 -32
- package/src/bots/homing/02_Lich.ts +145 -93
- package/src/bots/homing/03_Archlich.ts +129 -60
- package/src/bots/kiter/01_Spellspinner.ts +145 -107
- package/src/bots/kiter/02_Spellweaver.ts +153 -105
- package/src/bots/kiter/03_Spellbinder.ts +168 -123
- package/src/bots/melee/01_Shadowblade.ts +131 -75
- package/src/bots/melee/02_Nightblade.ts +174 -130
- package/src/bots/melee/03_Voidblade.ts +266 -168
- package/src/bots/registry.ts +44 -37
- package/src/bots/shared.ts +185 -25
- package/src/bots/sniper/01_Spellshot.ts +151 -98
- package/src/bots/sniper/02_Spelltracer.ts +173 -128
- package/src/bots/sniper/03_Spellseeker.ts +177 -152
- package/src/bots/standalone/Critter.ts +46 -52
- package/src/bots/standalone/Doombringer.ts +36 -40
- package/src/bots/standalone/Hogger.ts +120 -89
- package/src/bots/standalone/Rookie.ts +21 -23
- package/src/bots/standalone/TargetDummy.ts +5 -6
- package/src/bots/test/cheater.ts +91 -85
- package/src/bots/test/crasher.ts +26 -28
- package/src/engine/bundle-fight.ts +242 -0
- package/src/engine/hooks-runtime.ts +58 -18
- package/src/engine/manual-match.ts +33 -25
- package/src/engine/missile-templates.ts +25 -43
- package/src/engine/optimizer.ts +7 -7
- package/src/engine/params-runtime.ts +3 -3
- package/src/engine/physics.ts +33 -23
- package/src/engine/sandbox-browser.ts +8 -7
- package/src/engine/sandbox-compile.ts +1 -1
- package/src/engine/sandbox-harness.ts +299 -367
- package/src/engine/sandbox.ts +3 -3
- package/src/engine/simulation.ts +291 -76
- package/src/engine/spells.ts +9 -12
- package/src/engine-version.ts +1 -1
- package/src/hooks/action-builders.ts +76 -21
- package/src/hooks/index.ts +17 -9
- package/src/hooks/state-hooks.ts +61 -25
- package/src/hooks/threat-analysis.ts +44 -20
- package/src/hooks/types.ts +62 -5
- package/src/index-browser.ts +1 -0
- package/src/index.ts +3 -0
- package/src/rules.ts +209 -63
- package/src/stats.ts +2 -2
- package/src/testing.ts +6 -30
- package/src/trace.ts +63 -3
- package/src/types.ts +127 -14
- package/src/utils/angles.ts +1 -0
- package/src/utils/combat.ts +98 -6
- package/src/utils/spatial.ts +3 -3
- package/dist/chunk-74FFJCFF.js +0 -9140
- package/dist/chunk-74FFJCFF.js.map +0 -1
- package/src/hooks/bot-wrapper.ts +0 -84
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type {FinalAction, MissileFunction} from '../../hooks/index.js';
|
|
2
|
+
import {usePosition, useEnemy, useStatus, useBlinkCooldown, useHealth, useTick, useThreats, useCastingSpell, useCastProgress, idle, move as moveAction, missile, shield, blink, cancel, getMissileContext, turnToward, flyStraight} from '../../hooks/index.js';
|
|
2
3
|
import {angleTo} from '../../utils/angles.js';
|
|
3
4
|
import {distanceTo} from '../../utils/distance.js';
|
|
4
5
|
import {interceptAngle, moveInDirection} from '../../utils/movement.js';
|
|
5
6
|
import {getMissileCastTime} from '../../utils/combat.js';
|
|
6
|
-
import {
|
|
7
|
+
import {RULES, ARENA_MIN, ARENA_MAX} from '../../rules.js';
|
|
7
8
|
import {
|
|
8
|
-
getThreats,
|
|
9
9
|
getMostUrgentThreat,
|
|
10
10
|
getDodgeDirectionForMovement,
|
|
11
11
|
isSafeToAttack,
|
|
12
12
|
getBlinkToCenter,
|
|
13
13
|
shouldForceShield,
|
|
14
|
+
keepOffWalls,
|
|
15
|
+
getPreCastReposition,
|
|
14
16
|
} from '../shared.js';
|
|
15
17
|
import {useParam} from '../../engine/params-runtime.js';
|
|
16
18
|
const WALL_BUFFER = 80;
|
|
@@ -29,25 +31,24 @@ const QUICK_CONFIG = {damage: 10, speed: 5, turnRate: 1, duration: 80};
|
|
|
29
31
|
/**
|
|
30
32
|
* Predictive homing missile AI - leads the target.
|
|
31
33
|
*/
|
|
32
|
-
const StormchaserMissileAI:
|
|
33
|
-
worldState,
|
|
34
|
-
missileState,
|
|
35
|
-
}) =>
|
|
34
|
+
const StormchaserMissileAI: MissileFunction = () =>
|
|
36
35
|
{
|
|
37
|
-
const
|
|
38
|
-
|
|
36
|
+
const ctx = getMissileContext();
|
|
37
|
+
const enemy = ctx.worldState.enemies[0];
|
|
38
|
+
if (enemy.health <= 0) return flyStraight();
|
|
39
39
|
|
|
40
40
|
const angle = interceptAngle(
|
|
41
|
-
|
|
41
|
+
ctx.position,
|
|
42
42
|
enemy.position,
|
|
43
43
|
enemy.velocity,
|
|
44
|
-
|
|
44
|
+
ctx.speed,
|
|
45
45
|
);
|
|
46
|
-
if (angle !== null)
|
|
46
|
+
if (angle !== null)
|
|
47
47
|
{
|
|
48
|
-
|
|
48
|
+
const target = moveInDirection(ctx.position, angle, 100);
|
|
49
|
+
return turnToward(target.x, target.y);
|
|
49
50
|
}
|
|
50
|
-
return
|
|
51
|
+
return turnToward(enemy.position.x, enemy.position.y);
|
|
51
52
|
};
|
|
52
53
|
|
|
53
54
|
/**
|
|
@@ -56,10 +57,10 @@ const StormchaserMissileAI: MissileAIFunction = ({
|
|
|
56
57
|
function isNearWall(position: {x: number; y: number}): boolean
|
|
57
58
|
{
|
|
58
59
|
return (
|
|
59
|
-
position.x < WALL_BUFFER ||
|
|
60
|
-
position.x >
|
|
61
|
-
position.y < WALL_BUFFER ||
|
|
62
|
-
position.y >
|
|
60
|
+
position.x < ARENA_MIN + WALL_BUFFER ||
|
|
61
|
+
position.x > ARENA_MAX - WALL_BUFFER ||
|
|
62
|
+
position.y < ARENA_MIN + WALL_BUFFER ||
|
|
63
|
+
position.y > ARENA_MAX - WALL_BUFFER
|
|
63
64
|
);
|
|
64
65
|
}
|
|
65
66
|
|
|
@@ -77,12 +78,22 @@ function combatMove(
|
|
|
77
78
|
strafeIntensity: number,
|
|
78
79
|
approachSpeed: number,
|
|
79
80
|
distanceDeadZone: number,
|
|
80
|
-
|
|
81
|
+
centerBias: number,
|
|
82
|
+
): {x: number; y: number}
|
|
81
83
|
{
|
|
82
84
|
const deltaX = enemy.position.x - position.x;
|
|
83
85
|
const deltaY = enemy.position.y - position.y;
|
|
84
86
|
const normalizedDistance = currentDistance > 0 ? currentDistance : 1;
|
|
85
87
|
|
|
88
|
+
// Approach the center-side of the enemy so knockback pushes toward center, not lava.
|
|
89
|
+
const center = (ARENA_MIN + ARENA_MAX) / 2;
|
|
90
|
+
const toCenterX = center - enemy.position.x;
|
|
91
|
+
const toCenterY = center - enemy.position.y;
|
|
92
|
+
const toCenterDist = Math.sqrt(toCenterX * toCenterX + toCenterY * toCenterY) || 1;
|
|
93
|
+
const approachDirX = deltaX / normalizedDistance + (toCenterX / toCenterDist) * centerBias;
|
|
94
|
+
const approachDirY = deltaY / normalizedDistance + (toCenterY / toCenterDist) * centerBias;
|
|
95
|
+
const approachMag = Math.sqrt(approachDirX * approachDirX + approachDirY * approachDirY) || 1;
|
|
96
|
+
|
|
86
97
|
// Perpendicular directions for strafing
|
|
87
98
|
const strafeClockwise = {
|
|
88
99
|
x: -deltaY / normalizedDistance,
|
|
@@ -112,16 +123,16 @@ function combatMove(
|
|
|
112
123
|
y: position.y + strafeCounterClockwise.y * 100,
|
|
113
124
|
};
|
|
114
125
|
const scoreClockwise = Math.min(
|
|
115
|
-
futureClockwise.x,
|
|
116
|
-
|
|
117
|
-
futureClockwise.y,
|
|
118
|
-
|
|
126
|
+
futureClockwise.x - ARENA_MIN,
|
|
127
|
+
ARENA_MAX - futureClockwise.x,
|
|
128
|
+
futureClockwise.y - ARENA_MIN,
|
|
129
|
+
ARENA_MAX - futureClockwise.y,
|
|
119
130
|
);
|
|
120
131
|
const scoreCounterClockwise = Math.min(
|
|
121
|
-
futureCounterClockwise.x,
|
|
122
|
-
|
|
123
|
-
futureCounterClockwise.y,
|
|
124
|
-
|
|
132
|
+
futureCounterClockwise.x - ARENA_MIN,
|
|
133
|
+
ARENA_MAX - futureCounterClockwise.x,
|
|
134
|
+
futureCounterClockwise.y - ARENA_MIN,
|
|
135
|
+
ARENA_MAX - futureCounterClockwise.y,
|
|
125
136
|
);
|
|
126
137
|
strafeDirection =
|
|
127
138
|
scoreClockwise > scoreCounterClockwise
|
|
@@ -138,36 +149,36 @@ function combatMove(
|
|
|
138
149
|
// When actively dodging, commit fully — no approach/retreat dilution
|
|
139
150
|
if (dodgeDirection)
|
|
140
151
|
{
|
|
141
|
-
return {x: strafeDirection.x
|
|
152
|
+
return {x: strafeDirection.x, y: strafeDirection.y};
|
|
142
153
|
}
|
|
143
154
|
|
|
144
155
|
let moveX = strafeDirection.x * strafeIntensity;
|
|
145
156
|
let moveY = strafeDirection.y * strafeIntensity;
|
|
146
157
|
|
|
147
158
|
// Distance management (tighter band than other bots, only when not dodging)
|
|
148
|
-
if (currentDistance > targetDistance + distanceDeadZone)
|
|
159
|
+
if (currentDistance > targetDistance + distanceDeadZone)
|
|
149
160
|
{
|
|
150
|
-
moveX += (
|
|
151
|
-
moveY += (
|
|
161
|
+
moveX += (approachDirX / approachMag) * approachSpeed;
|
|
162
|
+
moveY += (approachDirY / approachMag) * approachSpeed;
|
|
152
163
|
}
|
|
153
164
|
else if (currentDistance < targetDistance - distanceDeadZone)
|
|
154
165
|
{
|
|
155
166
|
let retreatX = -(deltaX / normalizedDistance) * approachSpeed;
|
|
156
167
|
let retreatY = -(deltaY / normalizedDistance) * approachSpeed;
|
|
157
|
-
if (position.x < WALL_BUFFER && retreatX < 0) retreatX = 0;
|
|
158
|
-
if (position.x >
|
|
159
|
-
if (position.y < WALL_BUFFER && retreatY < 0) retreatY = 0;
|
|
160
|
-
if (position.y >
|
|
168
|
+
if (position.x < ARENA_MIN + WALL_BUFFER && retreatX < 0) retreatX = 0;
|
|
169
|
+
if (position.x > ARENA_MAX - WALL_BUFFER && retreatX > 0) retreatX = 0;
|
|
170
|
+
if (position.y < ARENA_MIN + WALL_BUFFER && retreatY < 0) retreatY = 0;
|
|
171
|
+
if (position.y > ARENA_MAX - WALL_BUFFER && retreatY > 0) retreatY = 0;
|
|
161
172
|
moveX += retreatX;
|
|
162
173
|
moveY += retreatY;
|
|
163
174
|
}
|
|
164
175
|
|
|
165
176
|
// Normalize to max speed
|
|
166
177
|
const magnitude = Math.sqrt(moveX * moveX + moveY * moveY);
|
|
167
|
-
if (magnitude >
|
|
178
|
+
if (magnitude > 1)
|
|
168
179
|
{
|
|
169
|
-
moveX =
|
|
170
|
-
moveY =
|
|
180
|
+
moveX = moveX / magnitude;
|
|
181
|
+
moveY = moveY / magnitude;
|
|
171
182
|
}
|
|
172
183
|
|
|
173
184
|
return {x: moveX, y: moveY};
|
|
@@ -189,9 +200,20 @@ function combatMove(
|
|
|
189
200
|
*
|
|
190
201
|
* TIER: 1 (base)
|
|
191
202
|
*/
|
|
192
|
-
export
|
|
203
|
+
export function Stormchaser(): FinalAction
|
|
193
204
|
{
|
|
194
|
-
const
|
|
205
|
+
const position = usePosition();
|
|
206
|
+
const enemy = useEnemy();
|
|
207
|
+
const status = useStatus();
|
|
208
|
+
const blinkCooldown = useBlinkCooldown();
|
|
209
|
+
const health = useHealth();
|
|
210
|
+
const tick = useTick();
|
|
211
|
+
const threats = useThreats();
|
|
212
|
+
const castingSpell = useCastingSpell();
|
|
213
|
+
const castProgressData = useCastProgress();
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
const targetDistance = useParam('targetDistance', 263, {
|
|
195
217
|
range: 150,
|
|
196
218
|
min: 0,
|
|
197
219
|
});
|
|
@@ -200,24 +222,24 @@ export const Stormchaser: WizardFunction = ({state}) =>
|
|
|
200
222
|
min: 1,
|
|
201
223
|
steps: 5,
|
|
202
224
|
});
|
|
203
|
-
const strafeCyclePeriod = useParam('strafeCyclePeriod',
|
|
225
|
+
const strafeCyclePeriod = useParam('strafeCyclePeriod', 205, {
|
|
204
226
|
range: 75,
|
|
205
227
|
min: 80,
|
|
206
228
|
max: 300,
|
|
207
229
|
});
|
|
208
|
-
const strafeIntensity = useParam('strafeIntensity',
|
|
209
|
-
range:
|
|
210
|
-
min:
|
|
211
|
-
max:
|
|
230
|
+
const strafeIntensity = useParam('strafeIntensity', 0.93, {
|
|
231
|
+
range: 0.4,
|
|
232
|
+
min: 0.2,
|
|
233
|
+
max: 1,
|
|
212
234
|
steps: 7,
|
|
213
235
|
});
|
|
214
|
-
const approachSpeed = useParam('approachSpeed',
|
|
215
|
-
range:
|
|
216
|
-
min:
|
|
217
|
-
max:
|
|
236
|
+
const approachSpeed = useParam('approachSpeed', 0.38, {
|
|
237
|
+
range: 0.3,
|
|
238
|
+
min: 0.1,
|
|
239
|
+
max: 0.7,
|
|
218
240
|
steps: 7,
|
|
219
241
|
});
|
|
220
|
-
const distanceDeadZone = useParam('distanceDeadZone',
|
|
242
|
+
const distanceDeadZone = useParam('distanceDeadZone', 15, {
|
|
221
243
|
range: 25,
|
|
222
244
|
min: 10,
|
|
223
245
|
max: 60,
|
|
@@ -228,7 +250,7 @@ export const Stormchaser: WizardFunction = ({state}) =>
|
|
|
228
250
|
min: 50,
|
|
229
251
|
max: 300,
|
|
230
252
|
});
|
|
231
|
-
const blinkWindowMax = useParam('blinkWindowMax',
|
|
253
|
+
const blinkWindowMax = useParam('blinkWindowMax', 80, {
|
|
232
254
|
range: 30,
|
|
233
255
|
min: 15,
|
|
234
256
|
max: 80,
|
|
@@ -240,82 +262,99 @@ export const Stormchaser: WizardFunction = ({state}) =>
|
|
|
240
262
|
max: 150,
|
|
241
263
|
steps: 7,
|
|
242
264
|
});
|
|
243
|
-
const attackRange = useParam('attackRange',
|
|
265
|
+
const attackRange = useParam('attackRange', 697, {
|
|
244
266
|
range: 200,
|
|
245
267
|
min: 300,
|
|
246
268
|
max: 800,
|
|
247
269
|
});
|
|
248
|
-
const healthDamageRatio = useParam('healthDamageRatio', 5, {
|
|
270
|
+
const healthDamageRatio = useParam('healthDamageRatio', 4.5, {
|
|
249
271
|
range: 2,
|
|
250
272
|
min: 0.5,
|
|
251
273
|
max: 5,
|
|
252
274
|
steps: 7,
|
|
253
275
|
});
|
|
276
|
+
const centerBias = useParam('centerBias', 0.13, {
|
|
277
|
+
range: 0.4,
|
|
278
|
+
min: 0,
|
|
279
|
+
max: 0.8,
|
|
280
|
+
steps: 5,
|
|
281
|
+
});
|
|
282
|
+
const maxBlinkDistance = useParam('maxBlinkDistance', 300, {
|
|
283
|
+
range: 100,
|
|
284
|
+
min: 50,
|
|
285
|
+
max: 300,
|
|
286
|
+
steps: 7,
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
const distance = distanceTo(position, enemy.position);
|
|
254
290
|
|
|
255
|
-
|
|
256
|
-
if (!enemy)
|
|
291
|
+
function capBlinkDistance(target: {x: number; y: number}): {x: number; y: number}
|
|
257
292
|
{
|
|
258
|
-
|
|
293
|
+
const dx = target.x - position.x;
|
|
294
|
+
const dy = target.y - position.y;
|
|
295
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
296
|
+
if (dist <= maxBlinkDistance) return target;
|
|
297
|
+
const scale = maxBlinkDistance / dist;
|
|
298
|
+
return {x: position.x + dx * scale, y: position.y + dy * scale};
|
|
259
299
|
}
|
|
260
300
|
|
|
261
|
-
const distance = distanceTo(state.position, enemy.position);
|
|
262
|
-
|
|
263
301
|
// Analyze threats using simulation
|
|
264
|
-
const threats = getThreats(state);
|
|
265
302
|
const urgentThreat = getMostUrgentThreat(threats);
|
|
266
303
|
|
|
267
304
|
// === DEFENSE: Handle channeling state ===
|
|
268
|
-
if (
|
|
305
|
+
if (status === 'channeling')
|
|
269
306
|
{
|
|
270
307
|
// Cancel shield if no imminent danger
|
|
271
308
|
if (!urgentThreat || urgentThreat.ticksToImpact > shieldCancelThreshold)
|
|
272
309
|
{
|
|
273
|
-
return
|
|
310
|
+
return cancel();
|
|
274
311
|
}
|
|
275
|
-
return
|
|
312
|
+
return idle();
|
|
276
313
|
}
|
|
277
314
|
|
|
278
315
|
// === DEFENSE: Cancel missile cast for threats arriving during vulnerability window ===
|
|
279
316
|
if (
|
|
280
|
-
|
|
281
|
-
|
|
317
|
+
status === 'casting' &&
|
|
318
|
+
castingSpell === 'missile' &&
|
|
282
319
|
urgentThreat
|
|
283
320
|
)
|
|
284
321
|
{
|
|
285
|
-
const remainingCast = (
|
|
322
|
+
const remainingCast = ((castProgressData?.total) ?? 0) - ((castProgressData?.current) ?? 0);
|
|
286
323
|
const incomingDamage = urgentThreat.projectile.damage;
|
|
287
324
|
const arrivesInGCD =
|
|
288
325
|
urgentThreat.ticksToImpact > remainingCast &&
|
|
289
|
-
urgentThreat.ticksToImpact <= remainingCast + GCD_DURATION;
|
|
326
|
+
urgentThreat.ticksToImpact <= remainingCast + RULES.GCD_DURATION;
|
|
290
327
|
|
|
291
328
|
// Cancel for undodgeable threats when trade isn't worth it
|
|
292
329
|
if (!urgentThreat.bestDodgeDirection)
|
|
293
330
|
{
|
|
294
331
|
const canSurviveHit =
|
|
295
|
-
|
|
332
|
+
health > incomingDamage * healthDamageRatio + 1;
|
|
296
333
|
const tradeWorthIt =
|
|
297
334
|
canSurviveHit && incomingDamage <= QUICK_CONFIG.damage;
|
|
298
335
|
if (
|
|
299
336
|
!tradeWorthIt &&
|
|
300
337
|
remainingCast > cancelMinRemainingCast &&
|
|
301
338
|
urgentThreat.ticksToImpact <=
|
|
302
|
-
remainingCast + GCD_DURATION + SHIELD_CAST_TIME + 1
|
|
339
|
+
remainingCast + RULES.GCD_DURATION + RULES.SHIELD_CAST_TIME + 1
|
|
303
340
|
)
|
|
304
341
|
{
|
|
305
|
-
const dodgeDir = getDodgeDirectionForMovement(threats,
|
|
342
|
+
const dodgeDir = getDodgeDirectionForMovement(threats, position);
|
|
306
343
|
const cancelMove = combatMove(
|
|
307
|
-
|
|
344
|
+
position,
|
|
308
345
|
enemy,
|
|
309
346
|
distance,
|
|
310
347
|
targetDistance,
|
|
311
348
|
dodgeDir,
|
|
312
|
-
|
|
349
|
+
tick,
|
|
313
350
|
strafeCyclePeriod,
|
|
314
351
|
strafeIntensity,
|
|
315
352
|
approachSpeed,
|
|
316
353
|
distanceDeadZone,
|
|
354
|
+
centerBias,
|
|
317
355
|
);
|
|
318
|
-
|
|
356
|
+
const safeCancelMove = keepOffWalls(position, cancelMove);
|
|
357
|
+
return cancel().move(safeCancelMove.x, safeCancelMove.y);
|
|
319
358
|
}
|
|
320
359
|
}
|
|
321
360
|
|
|
@@ -323,24 +362,26 @@ export const Stormchaser: WizardFunction = ({state}) =>
|
|
|
323
362
|
// At low health, surviving is more important than finishing the attack.
|
|
324
363
|
if (
|
|
325
364
|
arrivesInGCD &&
|
|
326
|
-
|
|
365
|
+
health <= incomingDamage + 1 &&
|
|
327
366
|
remainingCast > 10
|
|
328
367
|
)
|
|
329
368
|
{
|
|
330
|
-
const dodgeDir = getDodgeDirectionForMovement(threats,
|
|
369
|
+
const dodgeDir = getDodgeDirectionForMovement(threats, position);
|
|
331
370
|
const cancelMove = combatMove(
|
|
332
|
-
|
|
371
|
+
position,
|
|
333
372
|
enemy,
|
|
334
373
|
distance,
|
|
335
374
|
targetDistance,
|
|
336
375
|
dodgeDir,
|
|
337
|
-
|
|
376
|
+
tick,
|
|
338
377
|
strafeCyclePeriod,
|
|
339
378
|
strafeIntensity,
|
|
340
379
|
approachSpeed,
|
|
341
380
|
distanceDeadZone,
|
|
381
|
+
centerBias,
|
|
342
382
|
);
|
|
343
|
-
|
|
383
|
+
const safeCancelMove = keepOffWalls(position, cancelMove);
|
|
384
|
+
return cancel().move(safeCancelMove.x, safeCancelMove.y);
|
|
344
385
|
}
|
|
345
386
|
}
|
|
346
387
|
|
|
@@ -348,27 +389,25 @@ export const Stormchaser: WizardFunction = ({state}) =>
|
|
|
348
389
|
// Blink dodges the incoming missile — no GCD after, so can immediately fire back.
|
|
349
390
|
// Only shield when blink is on cooldown.
|
|
350
391
|
if (
|
|
351
|
-
|
|
392
|
+
status === 'idle' &&
|
|
352
393
|
urgentThreat &&
|
|
353
|
-
|
|
394
|
+
blinkCooldown === 0 &&
|
|
354
395
|
urgentThreat.ticksToImpact >= 10 &&
|
|
355
396
|
urgentThreat.ticksToImpact <= blinkWindowMax
|
|
356
397
|
)
|
|
357
398
|
{
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
startCast: {spell: 'blink', target: getBlinkToCenter(state.position)},
|
|
361
|
-
};
|
|
399
|
+
const blinkTarget = capBlinkDistance(getBlinkToCenter(position));
|
|
400
|
+
return blink(blinkTarget.x, blinkTarget.y);
|
|
362
401
|
}
|
|
363
402
|
|
|
364
403
|
// === DEFENSE: Shield threats (when blink is on cooldown) ===
|
|
365
|
-
if (
|
|
404
|
+
if (status === 'idle' && urgentThreat)
|
|
366
405
|
{
|
|
367
406
|
const forceShield = shouldForceShield(urgentThreat);
|
|
368
407
|
const undodgeable = !urgentThreat.bestDodgeDirection;
|
|
369
|
-
const healthLow =
|
|
408
|
+
const healthLow = health < healthLowThreshold;
|
|
370
409
|
const healthCritical =
|
|
371
|
-
|
|
410
|
+
health <= urgentThreat.projectile.damage * healthDamageRatio + 1;
|
|
372
411
|
const shouldShield =
|
|
373
412
|
forceShield || (undodgeable && healthLow) || healthCritical;
|
|
374
413
|
|
|
@@ -376,60 +415,66 @@ export const Stormchaser: WizardFunction = ({state}) =>
|
|
|
376
415
|
{
|
|
377
416
|
if (urgentThreat.ticksToStartShield <= 3)
|
|
378
417
|
{
|
|
379
|
-
return
|
|
380
|
-
move: {x: 0, y: 0},
|
|
381
|
-
startCast: {spell: 'shield'},
|
|
382
|
-
};
|
|
418
|
+
return shield();
|
|
383
419
|
}
|
|
384
420
|
}
|
|
385
421
|
}
|
|
386
422
|
|
|
423
|
+
// === Preemptive shield — shield when enemy is casting a missile at close range ===
|
|
424
|
+
if (
|
|
425
|
+
status === 'idle' &&
|
|
426
|
+
distance < 300 &&
|
|
427
|
+
enemy.status === 'casting' &&
|
|
428
|
+
enemy.castingSpell === 'missile'
|
|
429
|
+
)
|
|
430
|
+
{
|
|
431
|
+
const remainingCast = (enemy.castDuration ?? 0) - (enemy.castProgress ?? 0);
|
|
432
|
+
const estimatedFlightTicks = distance / 8;
|
|
433
|
+
const ticksUntilHit = remainingCast + estimatedFlightTicks;
|
|
434
|
+
|
|
435
|
+
if (
|
|
436
|
+
ticksUntilHit >= RULES.SHIELD_CAST_TIME &&
|
|
437
|
+
ticksUntilHit < RULES.SHIELD_CAST_TIME + 15
|
|
438
|
+
)
|
|
439
|
+
{
|
|
440
|
+
return shield();
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
387
444
|
// === MOVEMENT: Smart combat movement ===
|
|
388
|
-
const dodgeDirection = getDodgeDirectionForMovement(threats,
|
|
445
|
+
const dodgeDirection = getDodgeDirectionForMovement(threats, position);
|
|
389
446
|
const move = combatMove(
|
|
390
|
-
|
|
447
|
+
position,
|
|
391
448
|
enemy,
|
|
392
449
|
distance,
|
|
393
450
|
targetDistance,
|
|
394
451
|
dodgeDirection,
|
|
395
|
-
|
|
452
|
+
tick,
|
|
396
453
|
strafeCyclePeriod,
|
|
397
454
|
strafeIntensity,
|
|
398
455
|
approachSpeed,
|
|
399
456
|
distanceDeadZone,
|
|
457
|
+
centerBias,
|
|
400
458
|
);
|
|
401
|
-
|
|
459
|
+
const safeMove = keepOffWalls(position, move);
|
|
402
460
|
// === OFFENSE: Attack aggressively while moving ===
|
|
403
|
-
if (
|
|
461
|
+
if (status === 'idle' && distance < attackRange)
|
|
404
462
|
{
|
|
463
|
+
const reposition = getPreCastReposition(position, enemy.position);
|
|
464
|
+
if (reposition) return moveAction(reposition.x, reposition.y);
|
|
465
|
+
|
|
405
466
|
// Primary: standard homing missile
|
|
406
467
|
const standardCastTime = getMissileCastTime(STANDARD_CONFIG);
|
|
407
468
|
if (isSafeToAttack(threats, standardCastTime))
|
|
408
469
|
{
|
|
409
|
-
return
|
|
410
|
-
move,
|
|
411
|
-
startCast: {
|
|
412
|
-
spell: 'missile',
|
|
413
|
-
config: STANDARD_CONFIG,
|
|
414
|
-
missileAI: StormchaserMissileAI,
|
|
415
|
-
direction: angleTo(state.position, enemy.position),
|
|
416
|
-
},
|
|
417
|
-
};
|
|
470
|
+
return missile(STANDARD_CONFIG, StormchaserMissileAI, angleTo(position, enemy.position)).move(safeMove.x, safeMove.y);
|
|
418
471
|
}
|
|
419
472
|
|
|
420
473
|
// Fallback: quick missile under pressure (shorter cast = smaller vulnerability window)
|
|
421
474
|
const quickCastTime = getMissileCastTime(QUICK_CONFIG);
|
|
422
475
|
if (isSafeToAttack(threats, quickCastTime))
|
|
423
476
|
{
|
|
424
|
-
return
|
|
425
|
-
move,
|
|
426
|
-
startCast: {
|
|
427
|
-
spell: 'missile',
|
|
428
|
-
config: QUICK_CONFIG,
|
|
429
|
-
missileAI: StormchaserMissileAI,
|
|
430
|
-
direction: angleTo(state.position, enemy.position),
|
|
431
|
-
},
|
|
432
|
-
};
|
|
477
|
+
return missile(QUICK_CONFIG, StormchaserMissileAI, angleTo(position, enemy.position)).move(safeMove.x, safeMove.y);
|
|
433
478
|
}
|
|
434
479
|
|
|
435
480
|
// Not safe to attack — wait for threat to pass, then fire
|
|
@@ -439,19 +484,16 @@ export const Stormchaser: WizardFunction = ({state}) =>
|
|
|
439
484
|
// If all offense checks failed and there's an undodgeable threat, shield it
|
|
440
485
|
// rather than sitting idle and taking the hit.
|
|
441
486
|
if (
|
|
442
|
-
|
|
487
|
+
status === 'idle' &&
|
|
443
488
|
urgentThreat &&
|
|
444
489
|
!urgentThreat.bestDodgeDirection &&
|
|
445
490
|
urgentThreat.canBlockInTime &&
|
|
446
491
|
urgentThreat.ticksToStartShield <= 3
|
|
447
492
|
)
|
|
448
493
|
{
|
|
449
|
-
return
|
|
450
|
-
move: {x: 0, y: 0},
|
|
451
|
-
startCast: {spell: 'shield'},
|
|
452
|
-
};
|
|
494
|
+
return shield();
|
|
453
495
|
}
|
|
454
496
|
|
|
455
497
|
// Default: just move (dodging/positioning)
|
|
456
|
-
return
|
|
457
|
-
}
|
|
498
|
+
return moveAction(safeMove.x, safeMove.y);
|
|
499
|
+
}
|