@vibemancer/core 0.1.3 → 0.1.5
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-W7HWJFQ4.js +10599 -0
- package/dist/chunk-W7HWJFQ4.js.map +1 -0
- package/dist/index-browser.d.ts +1300 -1051
- package/dist/index-browser.js +55 -17
- package/dist/index.d.ts +53 -3
- package/dist/index.js +200 -54
- package/dist/index.js.map +1 -1
- package/package.json +21 -15
- 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.ts +2 -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-B7YYYBEC.js +0 -9140
- package/dist/chunk-B7YYYBEC.js.map +0 -1
- package/src/hooks/bot-wrapper.ts +0 -84
|
@@ -1,20 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type {FinalAction} from '../../hooks/index.js';
|
|
2
|
+
import {usePosition, useEnemy, useStatus, useBlinkCooldown, useHealth, useTick, useThreats, useProjectiles, useMyProjectiles, useLastMissileConfig, useCastingSpell, useCastProgress, idle, move as moveAction, missile, shield, blink, cancel, flyStraight} from '../../hooks/index.js';
|
|
2
3
|
import {distanceTo} from '../../utils/distance.js';
|
|
3
4
|
import {angleTo} from '../../utils/angles.js';
|
|
4
|
-
import {
|
|
5
|
+
import {RULES, ARENA_MIN, ARENA_MAX} from '../../rules.js';
|
|
5
6
|
import {
|
|
6
|
-
getThreats,
|
|
7
7
|
getMostUrgentThreat,
|
|
8
8
|
getDodgeDirectionForMovement,
|
|
9
9
|
getBlinkToDecreaseDistance,
|
|
10
10
|
shouldForceShield,
|
|
11
|
-
|
|
11
|
+
fitMissileForEscapingTarget,
|
|
12
12
|
MAX_CAST_BUDGET,
|
|
13
13
|
MIN_USEFUL_DAMAGE,
|
|
14
|
+
keepOffWalls,
|
|
15
|
+
getPreCastReposition,
|
|
14
16
|
} from '../shared.js';
|
|
15
17
|
import {useParam} from '../../engine/params-runtime.js';
|
|
16
18
|
const WALL_BUFFER = 50;
|
|
17
19
|
const SHIELD_BUFFER = 3;
|
|
20
|
+
const FAST_STAB = {speed: 8, turnRate: 0, duration: 25};
|
|
18
21
|
|
|
19
22
|
/**
|
|
20
23
|
* Aggressive movement: dodge missiles, close distance, light strafe.
|
|
@@ -28,12 +31,22 @@ function aggressiveMove(
|
|
|
28
31
|
strafeCyclePeriod: number,
|
|
29
32
|
strafeIntensity: number,
|
|
30
33
|
approachSpeed: number,
|
|
31
|
-
|
|
34
|
+
centerBias: number,
|
|
35
|
+
): {x: number; y: number}
|
|
32
36
|
{
|
|
33
37
|
const deltaX = enemy.position.x - position.x;
|
|
34
38
|
const deltaY = enemy.position.y - position.y;
|
|
35
39
|
const normalizedDistance = currentDistance > 0 ? currentDistance : 1;
|
|
36
40
|
|
|
41
|
+
// Approach the center-side of the enemy so knockback pushes toward center, not lava.
|
|
42
|
+
const center = (ARENA_MIN + ARENA_MAX) / 2;
|
|
43
|
+
const toCenterX = center - enemy.position.x;
|
|
44
|
+
const toCenterY = center - enemy.position.y;
|
|
45
|
+
const toCenterDist = Math.sqrt(toCenterX * toCenterX + toCenterY * toCenterY) || 1;
|
|
46
|
+
const approachDirX = deltaX / normalizedDistance + (toCenterX / toCenterDist) * centerBias;
|
|
47
|
+
const approachDirY = deltaY / normalizedDistance + (toCenterY / toCenterDist) * centerBias;
|
|
48
|
+
const approachMag = Math.sqrt(approachDirX * approachDirX + approachDirY * approachDirY) || 1;
|
|
49
|
+
|
|
37
50
|
const strafeClockwise = {
|
|
38
51
|
x: -deltaY / normalizedDistance,
|
|
39
52
|
y: deltaX / normalizedDistance,
|
|
@@ -50,10 +63,10 @@ function aggressiveMove(
|
|
|
50
63
|
strafeDirection = dodgeDirection;
|
|
51
64
|
}
|
|
52
65
|
else if (
|
|
53
|
-
position.x < WALL_BUFFER ||
|
|
54
|
-
position.x >
|
|
55
|
-
position.y < WALL_BUFFER ||
|
|
56
|
-
position.y >
|
|
66
|
+
position.x < ARENA_MIN + WALL_BUFFER ||
|
|
67
|
+
position.x > ARENA_MAX - WALL_BUFFER ||
|
|
68
|
+
position.y < ARENA_MIN + WALL_BUFFER ||
|
|
69
|
+
position.y > ARENA_MAX - WALL_BUFFER
|
|
57
70
|
)
|
|
58
71
|
{
|
|
59
72
|
const futureClockwise = {
|
|
@@ -65,16 +78,16 @@ function aggressiveMove(
|
|
|
65
78
|
y: position.y + strafeCounterClockwise.y * 100,
|
|
66
79
|
};
|
|
67
80
|
const scoreClockwise = Math.min(
|
|
68
|
-
futureClockwise.x,
|
|
69
|
-
|
|
70
|
-
futureClockwise.y,
|
|
71
|
-
|
|
81
|
+
futureClockwise.x - ARENA_MIN,
|
|
82
|
+
ARENA_MAX - futureClockwise.x,
|
|
83
|
+
futureClockwise.y - ARENA_MIN,
|
|
84
|
+
ARENA_MAX - futureClockwise.y,
|
|
72
85
|
);
|
|
73
86
|
const scoreCounterClockwise = Math.min(
|
|
74
|
-
futureCounterClockwise.x,
|
|
75
|
-
|
|
76
|
-
futureCounterClockwise.y,
|
|
77
|
-
|
|
87
|
+
futureCounterClockwise.x - ARENA_MIN,
|
|
88
|
+
ARENA_MAX - futureCounterClockwise.x,
|
|
89
|
+
futureCounterClockwise.y - ARENA_MIN,
|
|
90
|
+
ARENA_MAX - futureCounterClockwise.y,
|
|
78
91
|
);
|
|
79
92
|
strafeDirection =
|
|
80
93
|
scoreClockwise > scoreCounterClockwise
|
|
@@ -90,7 +103,7 @@ function aggressiveMove(
|
|
|
90
103
|
// When actively dodging, commit fully
|
|
91
104
|
if (dodgeDirection)
|
|
92
105
|
{
|
|
93
|
-
return {x: strafeDirection.x
|
|
106
|
+
return {x: strafeDirection.x, y: strafeDirection.y};
|
|
94
107
|
}
|
|
95
108
|
|
|
96
109
|
// Light strafe + aggressive closing
|
|
@@ -98,14 +111,14 @@ function aggressiveMove(
|
|
|
98
111
|
let moveY = strafeDirection.y * strafeIntensity;
|
|
99
112
|
|
|
100
113
|
// Always close distance aggressively
|
|
101
|
-
moveX += (
|
|
102
|
-
moveY += (
|
|
114
|
+
moveX += (approachDirX / approachMag) * approachSpeed;
|
|
115
|
+
moveY += (approachDirY / approachMag) * approachSpeed;
|
|
103
116
|
|
|
104
117
|
const magnitude = Math.sqrt(moveX * moveX + moveY * moveY);
|
|
105
|
-
if (magnitude >
|
|
118
|
+
if (magnitude > 1)
|
|
106
119
|
{
|
|
107
|
-
moveX =
|
|
108
|
-
moveY =
|
|
120
|
+
moveX = moveX / magnitude;
|
|
121
|
+
moveY = moveY / magnitude;
|
|
109
122
|
}
|
|
110
123
|
|
|
111
124
|
return {x: moveX, y: moveY};
|
|
@@ -128,11 +141,25 @@ function aggressiveMove(
|
|
|
128
141
|
*
|
|
129
142
|
* TIER: 2 (enhanced Shadowblade)
|
|
130
143
|
*/
|
|
131
|
-
export
|
|
144
|
+
export function Nightblade(): FinalAction
|
|
132
145
|
{
|
|
133
|
-
const
|
|
146
|
+
const position = usePosition();
|
|
147
|
+
const enemy = useEnemy();
|
|
148
|
+
const status = useStatus();
|
|
149
|
+
const blinkCooldown = useBlinkCooldown();
|
|
150
|
+
const health = useHealth();
|
|
151
|
+
const tick = useTick();
|
|
152
|
+
const threats = useThreats();
|
|
153
|
+
const projectiles = useProjectiles();
|
|
154
|
+
const myProjectiles = useMyProjectiles();
|
|
155
|
+
const lastMissileConfig = useLastMissileConfig();
|
|
156
|
+
const castingSpell = useCastingSpell();
|
|
157
|
+
const castProgressData = useCastProgress();
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
const stabRange = useParam('stabRange', 142, {range: 38, min: 0, steps: 5});
|
|
134
161
|
const stabRangeVsCasting = stabRange + 20;
|
|
135
|
-
const preemptiveShieldRange = useParam('preemptiveShieldRange',
|
|
162
|
+
const preemptiveShieldRange = useParam('preemptiveShieldRange', 94, {
|
|
136
163
|
range: 85,
|
|
137
164
|
min: 0,
|
|
138
165
|
steps: 7,
|
|
@@ -143,22 +170,22 @@ export const Nightblade: WizardFunction = ({state}) =>
|
|
|
143
170
|
max: 1,
|
|
144
171
|
steps: 5,
|
|
145
172
|
});
|
|
146
|
-
const minTurnRate = useParam('minTurnRate',
|
|
147
|
-
const strafeCyclePeriod = useParam('strafeCyclePeriod',
|
|
173
|
+
const minTurnRate = useParam('minTurnRate', 0, {range: 2, min: 0, steps: 5});
|
|
174
|
+
const strafeCyclePeriod = useParam('strafeCyclePeriod', 174, {
|
|
148
175
|
range: 75,
|
|
149
176
|
min: 80,
|
|
150
177
|
max: 300,
|
|
151
178
|
});
|
|
152
|
-
const strafeIntensity = useParam('strafeIntensity',
|
|
153
|
-
range:
|
|
154
|
-
min:
|
|
155
|
-
max:
|
|
179
|
+
const strafeIntensity = useParam('strafeIntensity', 0.2, {
|
|
180
|
+
range: 0.3,
|
|
181
|
+
min: 0.1,
|
|
182
|
+
max: 0.8,
|
|
156
183
|
steps: 7,
|
|
157
184
|
});
|
|
158
|
-
const approachSpeed = useParam('approachSpeed',
|
|
159
|
-
range:
|
|
160
|
-
min:
|
|
161
|
-
max:
|
|
185
|
+
const approachSpeed = useParam('approachSpeed', 0.98, {
|
|
186
|
+
range: 0.2,
|
|
187
|
+
min: 0.5,
|
|
188
|
+
max: 1,
|
|
162
189
|
steps: 7,
|
|
163
190
|
});
|
|
164
191
|
const shieldCancelThreshold = useParam('shieldCancelThreshold', 50, {
|
|
@@ -166,13 +193,13 @@ export const Nightblade: WizardFunction = ({state}) =>
|
|
|
166
193
|
min: 50,
|
|
167
194
|
max: 300,
|
|
168
195
|
});
|
|
169
|
-
const blinkWindowMax = useParam('blinkWindowMax',
|
|
196
|
+
const blinkWindowMax = useParam('blinkWindowMax', 15, {
|
|
170
197
|
range: 30,
|
|
171
198
|
min: 15,
|
|
172
199
|
max: 80,
|
|
173
200
|
steps: 7,
|
|
174
201
|
});
|
|
175
|
-
const flightTimeDivisor = useParam('flightTimeDivisor',
|
|
202
|
+
const flightTimeDivisor = useParam('flightTimeDivisor', 11, {
|
|
176
203
|
range: 4,
|
|
177
204
|
min: 2,
|
|
178
205
|
max: 12,
|
|
@@ -184,43 +211,67 @@ export const Nightblade: WizardFunction = ({state}) =>
|
|
|
184
211
|
max: 100,
|
|
185
212
|
steps: 7,
|
|
186
213
|
});
|
|
187
|
-
const meleeAbortRange = useParam('meleeAbortRange',
|
|
214
|
+
const meleeAbortRange = useParam('meleeAbortRange', 178, {
|
|
188
215
|
range: 50,
|
|
189
216
|
min: 50,
|
|
190
217
|
max: 200,
|
|
191
218
|
steps: 7,
|
|
192
219
|
});
|
|
220
|
+
const useDynamicStab =
|
|
221
|
+
useParam('useDynamicStab', 0, {min: 0, max: 1, steps: 2}) >= 0.5;
|
|
222
|
+
const stabDamage = Math.round(useParam('stabDamage', 23, {
|
|
223
|
+
range: 10,
|
|
224
|
+
min: 5,
|
|
225
|
+
max: 25,
|
|
226
|
+
steps: 7,
|
|
227
|
+
}));
|
|
228
|
+
const centerBias = useParam('centerBias', 0.7, {
|
|
229
|
+
range: 0.4,
|
|
230
|
+
min: 0,
|
|
231
|
+
max: 0.8,
|
|
232
|
+
steps: 5,
|
|
233
|
+
});
|
|
234
|
+
const maxBlinkDistance = useParam('maxBlinkDistance', 200, {
|
|
235
|
+
range: 100,
|
|
236
|
+
min: 50,
|
|
237
|
+
max: 300,
|
|
238
|
+
steps: 7,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const distance = distanceTo(position, enemy.position);
|
|
193
242
|
|
|
194
|
-
|
|
195
|
-
if (!enemy)
|
|
243
|
+
function capBlinkDistance(target: {x: number; y: number}): {x: number; y: number}
|
|
196
244
|
{
|
|
197
|
-
|
|
245
|
+
const dx = target.x - position.x;
|
|
246
|
+
const dy = target.y - position.y;
|
|
247
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
248
|
+
if (dist <= maxBlinkDistance) return target;
|
|
249
|
+
const scale = maxBlinkDistance / dist;
|
|
250
|
+
return {x: position.x + dx * scale, y: position.y + dy * scale};
|
|
198
251
|
}
|
|
199
252
|
|
|
200
|
-
const distance = distanceTo(state.position, enemy.position);
|
|
201
|
-
|
|
202
|
-
const threats = getThreats(state);
|
|
203
253
|
const urgentThreat = getMostUrgentThreat(threats);
|
|
204
254
|
|
|
205
|
-
const dodgeDirection = getDodgeDirectionForMovement(threats,
|
|
255
|
+
const dodgeDirection = getDodgeDirectionForMovement(threats, position);
|
|
206
256
|
const move = aggressiveMove(
|
|
207
|
-
|
|
257
|
+
position,
|
|
208
258
|
enemy,
|
|
209
259
|
distance,
|
|
210
260
|
dodgeDirection,
|
|
211
|
-
|
|
261
|
+
tick,
|
|
212
262
|
strafeCyclePeriod,
|
|
213
263
|
strafeIntensity,
|
|
214
264
|
approachSpeed,
|
|
265
|
+
centerBias,
|
|
215
266
|
);
|
|
216
|
-
|
|
267
|
+
const safeMove = keepOffWalls(position, move);
|
|
217
268
|
// === DEFENSE: Handle channeling ===
|
|
218
|
-
if (
|
|
269
|
+
if (status === 'channeling')
|
|
219
270
|
{
|
|
220
271
|
// Keep shield up if enemy is about to fire at close range (preemptive shield scenario)
|
|
221
272
|
const enemyFiringSoon =
|
|
222
273
|
distance < preemptiveShieldRange &&
|
|
223
|
-
enemy.
|
|
274
|
+
enemy.status === 'casting' &&
|
|
224
275
|
enemy.castingSpell === 'missile' &&
|
|
225
276
|
(enemy.castDuration ?? 0) -
|
|
226
277
|
(enemy.castProgress ?? 0) +
|
|
@@ -231,27 +282,27 @@ export const Nightblade: WizardFunction = ({state}) =>
|
|
|
231
282
|
(urgentThreat && urgentThreat.ticksToImpact > shieldCancelThreshold)
|
|
232
283
|
)
|
|
233
284
|
{
|
|
234
|
-
return
|
|
285
|
+
return cancel().move(safeMove.x, safeMove.y);
|
|
235
286
|
}
|
|
236
|
-
return
|
|
287
|
+
return idle();
|
|
237
288
|
}
|
|
238
289
|
|
|
239
290
|
// === DEFENSE: Cancel missile cast for LETHAL incoming damage ===
|
|
240
291
|
// DPS trade: we accepted the hit when we started casting. Only cancel to survive.
|
|
241
292
|
if (
|
|
242
|
-
|
|
243
|
-
|
|
293
|
+
status === 'casting' &&
|
|
294
|
+
castingSpell === 'missile' &&
|
|
244
295
|
urgentThreat &&
|
|
245
|
-
urgentThreat.projectile.damage >=
|
|
296
|
+
urgentThreat.projectile.damage >= health
|
|
246
297
|
)
|
|
247
298
|
{
|
|
248
|
-
const remainingCast = (
|
|
299
|
+
const remainingCast = ((castProgressData?.total) ?? 0) - ((castProgressData?.current) ?? 0);
|
|
249
300
|
if (
|
|
250
301
|
urgentThreat.ticksToImpact <=
|
|
251
|
-
remainingCast + GCD_DURATION + SHIELD_CAST_TIME + SHIELD_BUFFER
|
|
302
|
+
remainingCast + RULES.GCD_DURATION + RULES.SHIELD_CAST_TIME + SHIELD_BUFFER
|
|
252
303
|
)
|
|
253
304
|
{
|
|
254
|
-
return
|
|
305
|
+
return cancel().move(safeMove.x, safeMove.y);
|
|
255
306
|
}
|
|
256
307
|
}
|
|
257
308
|
|
|
@@ -259,51 +310,46 @@ export const Nightblade: WizardFunction = ({state}) =>
|
|
|
259
310
|
// Melee missiles are non-homing with short range (~63u). If the enemy blinked away,
|
|
260
311
|
// continuing the cast is wasted time while we're vulnerable.
|
|
261
312
|
if (
|
|
262
|
-
|
|
263
|
-
|
|
313
|
+
status === 'casting' &&
|
|
314
|
+
castingSpell === 'missile' &&
|
|
264
315
|
distance > meleeAbortRange
|
|
265
316
|
)
|
|
266
317
|
{
|
|
267
|
-
return
|
|
318
|
+
return cancel().move(safeMove.x, safeMove.y);
|
|
268
319
|
}
|
|
269
320
|
|
|
270
321
|
// === BLINK: Dodge missile + close gap simultaneously (first priority) ===
|
|
271
322
|
// Blink both dodges the incoming missile AND closes distance — better than shielding.
|
|
272
323
|
// Only shield when blink is on cooldown.
|
|
273
324
|
if (
|
|
274
|
-
|
|
325
|
+
status === 'idle' &&
|
|
275
326
|
urgentThreat &&
|
|
276
|
-
|
|
327
|
+
blinkCooldown === 0 &&
|
|
277
328
|
distance > stabRange &&
|
|
278
329
|
urgentThreat.ticksToImpact >= 10 &&
|
|
279
330
|
urgentThreat.ticksToImpact <= blinkWindowMax
|
|
280
331
|
)
|
|
281
332
|
{
|
|
282
|
-
const
|
|
333
|
+
const rawBlinkTarget =
|
|
283
334
|
getBlinkToDecreaseDistance(
|
|
284
|
-
|
|
335
|
+
position,
|
|
285
336
|
enemy.position,
|
|
286
|
-
|
|
337
|
+
projectiles,
|
|
287
338
|
) ?? enemy.position;
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
startCast: {spell: 'blink', target: blinkTarget},
|
|
291
|
-
};
|
|
339
|
+
const blinkTarget = capBlinkDistance(rawBlinkTarget);
|
|
340
|
+
return blink(blinkTarget.x, blinkTarget.y);
|
|
292
341
|
}
|
|
293
342
|
|
|
294
343
|
// === DEFENSE: Shield lethal/high-damage undodgeable threats — survival over offense ===
|
|
295
344
|
if (
|
|
296
|
-
|
|
345
|
+
status === 'idle' &&
|
|
297
346
|
urgentThreat &&
|
|
298
347
|
(!urgentThreat.bestDodgeDirection || shouldForceShield(urgentThreat)) &&
|
|
299
|
-
urgentThreat.projectile.damage >=
|
|
348
|
+
urgentThreat.projectile.damage >= health &&
|
|
300
349
|
urgentThreat.canBlockInTime
|
|
301
350
|
)
|
|
302
351
|
{
|
|
303
|
-
return
|
|
304
|
-
move: {x: 0, y: 0},
|
|
305
|
-
startCast: {spell: 'shield'},
|
|
306
|
-
};
|
|
352
|
+
return shield();
|
|
307
353
|
}
|
|
308
354
|
|
|
309
355
|
// === OFFENSE: Budget-based melee stab when close enough ===
|
|
@@ -311,28 +357,31 @@ export const Nightblade: WizardFunction = ({state}) =>
|
|
|
311
357
|
// before needing to shield. Also stabs from farther when enemy is casting/channeling
|
|
312
358
|
// (they move at 0.5 or 0 speed, so the cast delay doesn't let them dodge).
|
|
313
359
|
const effectiveStabRange =
|
|
314
|
-
enemy.
|
|
360
|
+
enemy.status === 'casting' || enemy.status === 'channeling'
|
|
315
361
|
? stabRangeVsCasting
|
|
316
362
|
: stabRange;
|
|
317
363
|
// Skip offense when a lethal missile could reach us during cast+GCD — dodge at full speed instead
|
|
318
|
-
const myProjectileIds = new Set(
|
|
319
|
-
const hasNearbyLethalMissile =
|
|
364
|
+
const myProjectileIds = new Set(myProjectiles.map((p) => p.id));
|
|
365
|
+
const hasNearbyLethalMissile = projectiles.some(
|
|
320
366
|
(p) =>
|
|
321
367
|
!myProjectileIds.has(p.id) &&
|
|
322
|
-
p.damage >=
|
|
323
|
-
distanceTo(
|
|
324
|
-
MAX_CAST_BUDGET + GCD_DURATION,
|
|
368
|
+
p.damage >= health &&
|
|
369
|
+
distanceTo(position, p.position) / p.speed <
|
|
370
|
+
MAX_CAST_BUDGET + RULES.GCD_DURATION,
|
|
325
371
|
);
|
|
326
372
|
if (
|
|
327
|
-
|
|
373
|
+
status === 'idle' &&
|
|
328
374
|
distance < effectiveStabRange &&
|
|
329
375
|
!hasNearbyLethalMissile
|
|
330
|
-
)
|
|
376
|
+
)
|
|
331
377
|
{
|
|
378
|
+
const reposition = getPreCastReposition(position, enemy.position);
|
|
379
|
+
if (reposition) return moveAction(reposition.x, reposition.y);
|
|
380
|
+
|
|
332
381
|
let nextThreatTime = urgentThreat?.ticksToImpact ?? Infinity;
|
|
333
382
|
|
|
334
383
|
// Account for enemy's active cast — their missile will arrive after cast completes + flight time
|
|
335
|
-
if (enemy.
|
|
384
|
+
if (enemy.status === 'casting' && enemy.castingSpell === 'missile')
|
|
336
385
|
{
|
|
337
386
|
const remainingEnemyCast =
|
|
338
387
|
(enemy.castDuration ?? 0) - (enemy.castProgress ?? 0);
|
|
@@ -344,65 +393,63 @@ export const Nightblade: WizardFunction = ({state}) =>
|
|
|
344
393
|
}
|
|
345
394
|
|
|
346
395
|
const safeBudget =
|
|
347
|
-
nextThreatTime - GCD_DURATION - SHIELD_CAST_TIME - SHIELD_BUFFER;
|
|
396
|
+
nextThreatTime - RULES.GCD_DURATION - RULES.SHIELD_CAST_TIME - SHIELD_BUFFER;
|
|
348
397
|
const budgetTicks = Math.min(safeBudget, MAX_CAST_BUDGET);
|
|
349
398
|
const enemyHP = Math.ceil(enemy.health);
|
|
350
399
|
const minDmg = Math.min(MIN_USEFUL_DAMAGE, enemyHP);
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
400
|
+
const stabConfig = {damage: stabDamage, speed: FAST_STAB.speed, turnRate: FAST_STAB.turnRate, duration: FAST_STAB.duration};
|
|
401
|
+
|
|
402
|
+
let config = useDynamicStab
|
|
403
|
+
? fitMissileForEscapingTarget(budgetTicks, distance, {
|
|
404
|
+
minTurnRate,
|
|
405
|
+
maxDamage: enemyHP,
|
|
406
|
+
lastMissileConfig: lastMissileConfig,
|
|
407
|
+
})
|
|
408
|
+
: null;
|
|
409
|
+
|
|
410
|
+
// If dynamic fitting failed, try bigger budget accepting a hit
|
|
360
411
|
const enemyCasting =
|
|
361
|
-
enemy.
|
|
412
|
+
enemy.status === 'casting' && enemy.castingSpell === 'missile';
|
|
362
413
|
if (
|
|
414
|
+
useDynamicStab &&
|
|
363
415
|
(!config || config.damage < minDmg) &&
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
)
|
|
416
|
+
!enemyCasting &&
|
|
417
|
+
(!urgentThreat ||
|
|
418
|
+
urgentThreat.projectile.damage < health * acceptHitFraction)
|
|
419
|
+
)
|
|
368
420
|
{
|
|
369
|
-
config =
|
|
421
|
+
config = fitMissileForEscapingTarget(MAX_CAST_BUDGET, distance, {
|
|
370
422
|
minTurnRate,
|
|
371
423
|
maxDamage: enemyHP,
|
|
372
|
-
lastMissileConfig:
|
|
424
|
+
lastMissileConfig: lastMissileConfig,
|
|
373
425
|
});
|
|
374
426
|
}
|
|
375
427
|
|
|
376
|
-
|
|
428
|
+
// Fallback to fixed fast stab
|
|
429
|
+
if (!config || config.damage < minDmg)
|
|
377
430
|
{
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
direction: angleTo(state.position, enemy.position),
|
|
385
|
-
},
|
|
386
|
-
};
|
|
431
|
+
config = stabConfig;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (config)
|
|
435
|
+
{
|
|
436
|
+
return missile(config, () => flyStraight(), angleTo(position, enemy.position)).move(safeMove.x, safeMove.y);
|
|
387
437
|
}
|
|
388
438
|
}
|
|
389
439
|
|
|
390
440
|
// === DEFENSE: Shield undodgeable or high-damage homing threats (only when blink can't handle it) ===
|
|
391
441
|
if (
|
|
392
|
-
|
|
442
|
+
status === 'idle' &&
|
|
393
443
|
urgentThreat &&
|
|
394
444
|
(!urgentThreat.bestDodgeDirection || shouldForceShield(urgentThreat))
|
|
395
445
|
)
|
|
396
446
|
{
|
|
397
447
|
if (
|
|
398
448
|
urgentThreat.canBlockInTime &&
|
|
399
|
-
(
|
|
449
|
+
(blinkCooldown > 0 || urgentThreat.ticksToImpact < 10)
|
|
400
450
|
)
|
|
401
451
|
{
|
|
402
|
-
return
|
|
403
|
-
move: {x: 0, y: 0},
|
|
404
|
-
startCast: {spell: 'shield'},
|
|
405
|
-
};
|
|
452
|
+
return shield();
|
|
406
453
|
}
|
|
407
454
|
}
|
|
408
455
|
|
|
@@ -410,9 +457,9 @@ export const Nightblade: WizardFunction = ({state}) =>
|
|
|
410
457
|
// At melee range, missiles arrive almost instantly after launch — too fast to react.
|
|
411
458
|
// Shield BEFORE the missile launches. Trigger when shield will finish BEFORE missile hits.
|
|
412
459
|
if (
|
|
413
|
-
|
|
460
|
+
status === 'idle' &&
|
|
414
461
|
distance < preemptiveShieldRange &&
|
|
415
|
-
enemy.
|
|
462
|
+
enemy.status === 'casting' &&
|
|
416
463
|
enemy.castingSpell === 'missile'
|
|
417
464
|
)
|
|
418
465
|
{
|
|
@@ -420,18 +467,15 @@ export const Nightblade: WizardFunction = ({state}) =>
|
|
|
420
467
|
const estimatedFlightTicks = distance / flightTimeDivisor;
|
|
421
468
|
const ticksUntilHit = remainingCast + estimatedFlightTicks;
|
|
422
469
|
// Shield when hit is close enough that we MUST start now, but far enough that
|
|
423
|
-
// the shield finishes in time. The window: SHIELD_CAST_TIME <= ticksUntilHit < SHIELD_CAST_TIME+15
|
|
470
|
+
// the shield finishes in time. The window: RULES.SHIELD_CAST_TIME <= ticksUntilHit < RULES.SHIELD_CAST_TIME+15
|
|
424
471
|
if (
|
|
425
|
-
ticksUntilHit >= SHIELD_CAST_TIME &&
|
|
426
|
-
ticksUntilHit < SHIELD_CAST_TIME + 15
|
|
472
|
+
ticksUntilHit >= RULES.SHIELD_CAST_TIME &&
|
|
473
|
+
ticksUntilHit < RULES.SHIELD_CAST_TIME + 15
|
|
427
474
|
)
|
|
428
475
|
{
|
|
429
|
-
return
|
|
430
|
-
move: {x: 0, y: 0},
|
|
431
|
-
startCast: {spell: 'shield'},
|
|
432
|
-
};
|
|
476
|
+
return shield();
|
|
433
477
|
}
|
|
434
478
|
}
|
|
435
479
|
|
|
436
|
-
return
|
|
437
|
-
}
|
|
480
|
+
return moveAction(safeMove.x, safeMove.y);
|
|
481
|
+
}
|