@vibemancer/core 0.1.4 → 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-74FFJCFF.js +0 -9140
- package/dist/chunk-74FFJCFF.js.map +0 -1
- package/src/hooks/bot-wrapper.ts +0 -84
|
@@ -1,28 +1,32 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type {FinalAction, MissileAction} from '../../hooks/index.js';
|
|
2
|
+
import {usePosition, useEnemy, useStatus, useBlinkCooldown, useTick, useThreats, useProjectiles, useLastMissileConfig, useCastingSpell, useCastProgress, idle, move as moveAction, missile, shield, blink, cancel, flyStraight, getMissileContext, turnToward} from '../../hooks/index.js';
|
|
2
3
|
import {distanceTo} from '../../utils/distance.js';
|
|
3
4
|
import {angleTo} from '../../utils/angles.js';
|
|
5
|
+
import {RULES, ARENA_MIN, ARENA_MAX} from '../../rules.js';
|
|
4
6
|
import {
|
|
5
|
-
ARENA_SIZE,
|
|
6
|
-
GCD_DURATION,
|
|
7
|
-
SHIELD_CAST_TIME,
|
|
8
|
-
BLINK_CAST_TIME,
|
|
9
|
-
} from '../../rules.js';
|
|
10
|
-
import {
|
|
11
|
-
getThreats,
|
|
12
7
|
getMostUrgentThreat,
|
|
13
8
|
getDodgeDirectionForMovement,
|
|
14
9
|
getBlinkToDecreaseDistance,
|
|
15
10
|
shouldForceShield,
|
|
16
|
-
|
|
11
|
+
fitMissileForEscapingTarget,
|
|
17
12
|
MIN_USEFUL_DAMAGE,
|
|
18
13
|
getEnemyVulnerabilityTicks,
|
|
14
|
+
keepOffWalls,
|
|
15
|
+
getPreCastReposition,
|
|
19
16
|
} from '../shared.js';
|
|
20
|
-
import {getMissileCastTime} from '../../utils/combat.js';
|
|
17
|
+
import {getMissileCastTime, getLeadPosition} from '../../utils/combat.js';
|
|
21
18
|
import {useParam} from '../../engine/params-runtime.js';
|
|
22
19
|
|
|
23
20
|
const WALL_BUFFER = 50;
|
|
24
21
|
const SHIELD_BUFFER = 3;
|
|
25
22
|
|
|
23
|
+
const HomingStabAI = (): MissileAction =>
|
|
24
|
+
{
|
|
25
|
+
const ctx = getMissileContext();
|
|
26
|
+
const enemy = ctx.worldState.enemies[0];
|
|
27
|
+
return enemy ? turnToward(enemy.position.x, enemy.position.y) : flyStraight();
|
|
28
|
+
};
|
|
29
|
+
|
|
26
30
|
/**
|
|
27
31
|
* Aggressive movement: dodge missiles, close distance, light strafe.
|
|
28
32
|
*/
|
|
@@ -35,12 +39,22 @@ function aggressiveMove(
|
|
|
35
39
|
strafeCyclePeriod: number,
|
|
36
40
|
strafeIntensity: number,
|
|
37
41
|
approachSpeed: number,
|
|
38
|
-
|
|
42
|
+
centerBias: number,
|
|
43
|
+
): {x: number; y: number}
|
|
39
44
|
{
|
|
40
45
|
const deltaX = enemy.position.x - position.x;
|
|
41
46
|
const deltaY = enemy.position.y - position.y;
|
|
42
47
|
const normalizedDistance = currentDistance > 0 ? currentDistance : 1;
|
|
43
48
|
|
|
49
|
+
// Approach the center-side of the enemy so knockback pushes toward center, not lava.
|
|
50
|
+
const center = (ARENA_MIN + ARENA_MAX) / 2;
|
|
51
|
+
const toCenterX = center - enemy.position.x;
|
|
52
|
+
const toCenterY = center - enemy.position.y;
|
|
53
|
+
const toCenterDist = Math.sqrt(toCenterX * toCenterX + toCenterY * toCenterY) || 1;
|
|
54
|
+
const approachDirX = deltaX / normalizedDistance + (toCenterX / toCenterDist) * centerBias;
|
|
55
|
+
const approachDirY = deltaY / normalizedDistance + (toCenterY / toCenterDist) * centerBias;
|
|
56
|
+
const approachMag = Math.sqrt(approachDirX * approachDirX + approachDirY * approachDirY) || 1;
|
|
57
|
+
|
|
44
58
|
const strafeClockwise = {
|
|
45
59
|
x: -deltaY / normalizedDistance,
|
|
46
60
|
y: deltaX / normalizedDistance,
|
|
@@ -57,10 +71,10 @@ function aggressiveMove(
|
|
|
57
71
|
strafeDirection = dodgeDirection;
|
|
58
72
|
}
|
|
59
73
|
else if (
|
|
60
|
-
position.x < WALL_BUFFER ||
|
|
61
|
-
position.x >
|
|
62
|
-
position.y < WALL_BUFFER ||
|
|
63
|
-
position.y >
|
|
74
|
+
position.x < ARENA_MIN + WALL_BUFFER ||
|
|
75
|
+
position.x > ARENA_MAX - WALL_BUFFER ||
|
|
76
|
+
position.y < ARENA_MIN + WALL_BUFFER ||
|
|
77
|
+
position.y > ARENA_MAX - WALL_BUFFER
|
|
64
78
|
)
|
|
65
79
|
{
|
|
66
80
|
const futureClockwise = {
|
|
@@ -72,16 +86,16 @@ function aggressiveMove(
|
|
|
72
86
|
y: position.y + strafeCounterClockwise.y * 100,
|
|
73
87
|
};
|
|
74
88
|
const scoreClockwise = Math.min(
|
|
75
|
-
futureClockwise.x,
|
|
76
|
-
|
|
77
|
-
futureClockwise.y,
|
|
78
|
-
|
|
89
|
+
futureClockwise.x - ARENA_MIN,
|
|
90
|
+
ARENA_MAX - futureClockwise.x,
|
|
91
|
+
futureClockwise.y - ARENA_MIN,
|
|
92
|
+
ARENA_MAX - futureClockwise.y,
|
|
79
93
|
);
|
|
80
94
|
const scoreCounterClockwise = Math.min(
|
|
81
|
-
futureCounterClockwise.x,
|
|
82
|
-
|
|
83
|
-
futureCounterClockwise.y,
|
|
84
|
-
|
|
95
|
+
futureCounterClockwise.x - ARENA_MIN,
|
|
96
|
+
ARENA_MAX - futureCounterClockwise.x,
|
|
97
|
+
futureCounterClockwise.y - ARENA_MIN,
|
|
98
|
+
ARENA_MAX - futureCounterClockwise.y,
|
|
85
99
|
);
|
|
86
100
|
strafeDirection =
|
|
87
101
|
scoreClockwise > scoreCounterClockwise
|
|
@@ -96,20 +110,20 @@ function aggressiveMove(
|
|
|
96
110
|
|
|
97
111
|
if (dodgeDirection)
|
|
98
112
|
{
|
|
99
|
-
return {x: strafeDirection.x
|
|
113
|
+
return {x: strafeDirection.x, y: strafeDirection.y};
|
|
100
114
|
}
|
|
101
115
|
|
|
102
116
|
let moveX = strafeDirection.x * strafeIntensity;
|
|
103
117
|
let moveY = strafeDirection.y * strafeIntensity;
|
|
104
118
|
|
|
105
|
-
moveX += (
|
|
106
|
-
moveY += (
|
|
119
|
+
moveX += (approachDirX / approachMag) * approachSpeed;
|
|
120
|
+
moveY += (approachDirY / approachMag) * approachSpeed;
|
|
107
121
|
|
|
108
122
|
const magnitude = Math.sqrt(moveX * moveX + moveY * moveY);
|
|
109
|
-
if (magnitude >
|
|
123
|
+
if (magnitude > 1)
|
|
110
124
|
{
|
|
111
|
-
moveX =
|
|
112
|
-
moveY =
|
|
125
|
+
moveX = moveX / magnitude;
|
|
126
|
+
moveY = moveY / magnitude;
|
|
113
127
|
}
|
|
114
128
|
|
|
115
129
|
return {x: moveX, y: moveY};
|
|
@@ -137,41 +151,53 @@ function aggressiveMove(
|
|
|
137
151
|
* PROGRESSION LINE: Shadowblade → Nightblade → Voidblade
|
|
138
152
|
* TIER: 3 (elite Melee line)
|
|
139
153
|
*/
|
|
140
|
-
export
|
|
154
|
+
export function Voidblade(): FinalAction
|
|
141
155
|
{
|
|
156
|
+
const position = usePosition();
|
|
157
|
+
const enemy = useEnemy();
|
|
158
|
+
const status = useStatus();
|
|
159
|
+
const blinkCooldown = useBlinkCooldown();
|
|
160
|
+
const tick = useTick();
|
|
161
|
+
const threats = useThreats();
|
|
162
|
+
const projectiles = useProjectiles();
|
|
163
|
+
const lastMissileConfig = useLastMissileConfig();
|
|
164
|
+
const castingSpell = useCastingSpell();
|
|
165
|
+
const castProgressData = useCastProgress();
|
|
166
|
+
|
|
167
|
+
|
|
142
168
|
const preemptiveShieldRange = useParam('preemptiveShieldRange', 348, {
|
|
143
169
|
range: 85,
|
|
144
170
|
min: 50,
|
|
145
171
|
steps: 7,
|
|
146
172
|
});
|
|
147
|
-
const shieldWindow = useParam('shieldWindow',
|
|
173
|
+
const shieldWindow = useParam('shieldWindow', 5, {
|
|
148
174
|
range: 10,
|
|
149
175
|
min: 5,
|
|
150
176
|
max: 30,
|
|
151
177
|
steps: 5,
|
|
152
178
|
});
|
|
153
|
-
const punishMinVulnerability = useParam('punishMinVulnerability',
|
|
179
|
+
const punishMinVulnerability = useParam('punishMinVulnerability', 15, {
|
|
154
180
|
range: 15,
|
|
155
181
|
min: 10,
|
|
156
182
|
steps: 5,
|
|
157
183
|
});
|
|
158
184
|
const preferBlinkDodge =
|
|
159
185
|
useParam('preferBlinkDodge', 0, {min: 0, max: 1, steps: 2}) >= 0.5;
|
|
160
|
-
const strafeCyclePeriod = useParam('strafeCyclePeriod',
|
|
186
|
+
const strafeCyclePeriod = useParam('strafeCyclePeriod', 230, {
|
|
161
187
|
range: 75,
|
|
162
188
|
min: 80,
|
|
163
189
|
max: 250,
|
|
164
190
|
});
|
|
165
|
-
const strafeIntensity = useParam('strafeIntensity',
|
|
166
|
-
range:
|
|
167
|
-
min:
|
|
168
|
-
max:
|
|
191
|
+
const strafeIntensity = useParam('strafeIntensity', 0.28, {
|
|
192
|
+
range: 0.3,
|
|
193
|
+
min: 0.1,
|
|
194
|
+
max: 0.8,
|
|
169
195
|
steps: 7,
|
|
170
196
|
});
|
|
171
|
-
const approachSpeed = useParam('approachSpeed',
|
|
172
|
-
range:
|
|
173
|
-
min:
|
|
174
|
-
max:
|
|
197
|
+
const approachSpeed = useParam('approachSpeed', 1, {
|
|
198
|
+
range: 0.2,
|
|
199
|
+
min: 0.5,
|
|
200
|
+
max: 1,
|
|
175
201
|
steps: 7,
|
|
176
202
|
});
|
|
177
203
|
const shieldCancelThreshold = useParam('shieldCancelThreshold', 150, {
|
|
@@ -185,7 +211,7 @@ export const Voidblade: WizardFunction = ({state}) =>
|
|
|
185
211
|
max: 80,
|
|
186
212
|
steps: 7,
|
|
187
213
|
});
|
|
188
|
-
const blinkPastMargin = useParam('blinkPastMargin',
|
|
214
|
+
const blinkPastMargin = useParam('blinkPastMargin', 61, {
|
|
189
215
|
range: 30,
|
|
190
216
|
min: 20,
|
|
191
217
|
max: 100,
|
|
@@ -203,13 +229,13 @@ export const Voidblade: WizardFunction = ({state}) =>
|
|
|
203
229
|
max: 200,
|
|
204
230
|
steps: 7,
|
|
205
231
|
});
|
|
206
|
-
const counterStabRange = useParam('counterStabRange',
|
|
207
|
-
range:
|
|
232
|
+
const counterStabRange = useParam('counterStabRange', 14, {
|
|
233
|
+
range: 30,
|
|
208
234
|
min: 10,
|
|
209
|
-
max:
|
|
235
|
+
max: 80,
|
|
210
236
|
steps: 7,
|
|
211
237
|
});
|
|
212
|
-
const flightTimeDivisor = useParam('flightTimeDivisor',
|
|
238
|
+
const flightTimeDivisor = useParam('flightTimeDivisor', 3, {
|
|
213
239
|
range: 4,
|
|
214
240
|
min: 3,
|
|
215
241
|
max: 12,
|
|
@@ -221,16 +247,16 @@ export const Voidblade: WizardFunction = ({state}) =>
|
|
|
221
247
|
max: 10,
|
|
222
248
|
steps: 5,
|
|
223
249
|
});
|
|
224
|
-
const idleProvokeBudget = useParam('idleProvokeBudget',
|
|
250
|
+
const idleProvokeBudget = useParam('idleProvokeBudget', 110, {
|
|
225
251
|
range: 40,
|
|
226
252
|
min: 20,
|
|
227
253
|
max: 120,
|
|
228
254
|
steps: 7,
|
|
229
255
|
});
|
|
230
|
-
const meleeAbortRange = useParam('meleeAbortRange',
|
|
231
|
-
range:
|
|
256
|
+
const meleeAbortRange = useParam('meleeAbortRange', 100, {
|
|
257
|
+
range: 100,
|
|
232
258
|
min: 50,
|
|
233
|
-
max:
|
|
259
|
+
max: 400,
|
|
234
260
|
steps: 7,
|
|
235
261
|
});
|
|
236
262
|
const enemyFiringSoonThreshold = useParam('enemyFiringSoonThreshold', 50, {
|
|
@@ -239,60 +265,136 @@ export const Voidblade: WizardFunction = ({state}) =>
|
|
|
239
265
|
max: 100,
|
|
240
266
|
steps: 7,
|
|
241
267
|
});
|
|
268
|
+
const useDynamicStab =
|
|
269
|
+
useParam('useDynamicStab', 1, {min: 0, max: 1, steps: 2}) >= 0.5;
|
|
270
|
+
const stabDamage = Math.round(useParam('stabDamage', 5, {
|
|
271
|
+
range: 10,
|
|
272
|
+
min: 5,
|
|
273
|
+
max: 25,
|
|
274
|
+
steps: 7,
|
|
275
|
+
}));
|
|
276
|
+
const centerBias = useParam('centerBias', 0.8, {
|
|
277
|
+
range: 0.3,
|
|
278
|
+
min: 0,
|
|
279
|
+
max: 0.8,
|
|
280
|
+
steps: 5,
|
|
281
|
+
});
|
|
282
|
+
const saveBlinkForChase =
|
|
283
|
+
useParam('saveBlinkForChase', 0, {min: 0, max: 1, steps: 2}) >= 0.5;
|
|
284
|
+
const chaseBlinkRange = useParam('chaseBlinkRange', 200, {
|
|
285
|
+
range: 100,
|
|
286
|
+
min: 100,
|
|
287
|
+
max: 400,
|
|
288
|
+
steps: 5,
|
|
289
|
+
});
|
|
290
|
+
const maxBlinkDistance = useParam('maxBlinkDistance', 278, {
|
|
291
|
+
range: 100,
|
|
292
|
+
min: 50,
|
|
293
|
+
max: 300,
|
|
294
|
+
steps: 7,
|
|
295
|
+
});
|
|
296
|
+
const stabTurnRate = useParam('stabTurnRate', 0, {
|
|
297
|
+
min: 0,
|
|
298
|
+
max: 1,
|
|
299
|
+
steps: 5,
|
|
300
|
+
});
|
|
301
|
+
const stabSpeed = useParam('stabSpeed', 11.3, {
|
|
302
|
+
range: 3,
|
|
303
|
+
min: 5,
|
|
304
|
+
max: 12,
|
|
305
|
+
steps: 5,
|
|
306
|
+
});
|
|
307
|
+
const stabDuration = Math.round(useParam('stabDuration', 33, {
|
|
308
|
+
range: 15,
|
|
309
|
+
min: 15,
|
|
310
|
+
max: 50,
|
|
311
|
+
steps: 5,
|
|
312
|
+
}));
|
|
313
|
+
const useLeadAiming =
|
|
314
|
+
useParam('useLeadAiming', 0, {min: 0, max: 1, steps: 2}) >= 0.5;
|
|
315
|
+
const useHomingAI =
|
|
316
|
+
useParam('useHomingAI', 0, {min: 0, max: 1, steps: 2}) >= 0.5;
|
|
317
|
+
const FAST_STAB = {speed: stabSpeed, turnRate: stabTurnRate, duration: stabDuration};
|
|
242
318
|
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
319
|
+
const distance = distanceTo(position, enemy.position);
|
|
320
|
+
|
|
321
|
+
const stabAimAngle = useLeadAiming
|
|
322
|
+
? angleTo(position, getLeadPosition(enemy.position, enemy.velocity, FAST_STAB.speed, position))
|
|
323
|
+
: angleTo(position, enemy.position);
|
|
324
|
+
const stabAI = useHomingAI ? HomingStabAI : (): MissileAction => flyStraight();
|
|
248
325
|
|
|
249
|
-
const distance = distanceTo(state.position, enemy.position);
|
|
250
|
-
const threats = getThreats(state);
|
|
251
326
|
const urgentThreat = getMostUrgentThreat(threats);
|
|
252
|
-
const dodgeDirection = getDodgeDirectionForMovement(threats,
|
|
327
|
+
const dodgeDirection = getDodgeDirectionForMovement(threats, position);
|
|
253
328
|
const vulnerabilityTicks = getEnemyVulnerabilityTicks(enemy);
|
|
254
329
|
|
|
255
330
|
const move = aggressiveMove(
|
|
256
|
-
|
|
331
|
+
position,
|
|
257
332
|
enemy,
|
|
258
333
|
distance,
|
|
259
334
|
dodgeDirection,
|
|
260
|
-
|
|
335
|
+
tick,
|
|
261
336
|
strafeCyclePeriod,
|
|
262
337
|
strafeIntensity,
|
|
263
338
|
approachSpeed,
|
|
339
|
+
centerBias,
|
|
264
340
|
);
|
|
341
|
+
const safeMove = keepOffWalls(position, move);
|
|
342
|
+
|
|
343
|
+
function capBlinkDistance(target: {x: number; y: number}): {x: number; y: number}
|
|
344
|
+
{
|
|
345
|
+
const dx = target.x - position.x;
|
|
346
|
+
const dy = target.y - position.y;
|
|
347
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
348
|
+
if (dist <= maxBlinkDistance) return target;
|
|
349
|
+
const scale = maxBlinkDistance / dist;
|
|
350
|
+
return {x: position.x + dx * scale, y: position.y + dy * scale};
|
|
351
|
+
}
|
|
265
352
|
|
|
266
353
|
// Precompute blink targets — used by blink-dodge and engage-blink.
|
|
267
354
|
// blinkCloseTarget: from getBlinkToDecreaseDistance (dodge + close gap). Null at melee range.
|
|
268
355
|
// blinkPastTarget: blink ~200u past enemy (dodge only, loses position). For in-flight missiles only.
|
|
269
356
|
let blinkCloseTarget: {x: number; y: number} | null = null;
|
|
270
357
|
let blinkPastTarget: {x: number; y: number} | null = null;
|
|
271
|
-
if (
|
|
358
|
+
if (blinkCooldown === 0)
|
|
272
359
|
{
|
|
273
|
-
|
|
274
|
-
|
|
360
|
+
const rawBlinkTarget = getBlinkToDecreaseDistance(
|
|
361
|
+
position,
|
|
275
362
|
enemy.position,
|
|
276
|
-
|
|
363
|
+
projectiles,
|
|
277
364
|
);
|
|
278
|
-
if (
|
|
365
|
+
if (rawBlinkTarget)
|
|
279
366
|
{
|
|
280
|
-
const
|
|
281
|
-
const
|
|
367
|
+
const center = (ARENA_MIN + ARENA_MAX) / 2;
|
|
368
|
+
const enemyDistToCenter = Math.max(
|
|
369
|
+
Math.abs(enemy.position.x - center),
|
|
370
|
+
Math.abs(enemy.position.y - center),
|
|
371
|
+
);
|
|
372
|
+
const capped = capBlinkDistance(rawBlinkTarget);
|
|
373
|
+
const blinkDistToCenter = Math.max(
|
|
374
|
+
Math.abs(capped.x - center),
|
|
375
|
+
Math.abs(capped.y - center),
|
|
376
|
+
);
|
|
377
|
+
if (blinkDistToCenter <= enemyDistToCenter + 30) blinkCloseTarget = capped;
|
|
378
|
+
}
|
|
379
|
+
if (!blinkCloseTarget)
|
|
380
|
+
{
|
|
381
|
+
const dx = enemy.position.x - position.x;
|
|
382
|
+
const dy = enemy.position.y - position.y;
|
|
282
383
|
const norm = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
384
|
+
const cappedDist = Math.min(blinkPastDistance, maxBlinkDistance);
|
|
283
385
|
blinkPastTarget = {
|
|
284
386
|
x: Math.max(
|
|
285
|
-
blinkPastMargin,
|
|
387
|
+
ARENA_MIN + blinkPastMargin,
|
|
286
388
|
Math.min(
|
|
287
|
-
|
|
288
|
-
|
|
389
|
+
ARENA_MAX - blinkPastMargin,
|
|
390
|
+
position.x + (dx / norm) * cappedDist,
|
|
289
391
|
),
|
|
290
392
|
),
|
|
291
393
|
y: Math.max(
|
|
292
|
-
blinkPastMargin,
|
|
394
|
+
ARENA_MIN + blinkPastMargin,
|
|
293
395
|
Math.min(
|
|
294
|
-
|
|
295
|
-
|
|
396
|
+
ARENA_MAX - blinkPastMargin,
|
|
397
|
+
position.y + (dy / norm) * cappedDist,
|
|
296
398
|
),
|
|
297
399
|
),
|
|
298
400
|
};
|
|
@@ -300,12 +402,12 @@ export const Voidblade: WizardFunction = ({state}) =>
|
|
|
300
402
|
}
|
|
301
403
|
|
|
302
404
|
// === STEP 1: CHANNELING — hold shield until missile passes ===
|
|
303
|
-
if (
|
|
405
|
+
if (status === 'channeling')
|
|
304
406
|
{
|
|
305
407
|
// Hold shield while there's an active threat OR enemy is about to fire
|
|
306
408
|
const enemyFiringSoon =
|
|
307
409
|
distance < preemptiveShieldRange &&
|
|
308
|
-
enemy.
|
|
410
|
+
enemy.status === 'casting' &&
|
|
309
411
|
enemy.castingSpell === 'missile' &&
|
|
310
412
|
(enemy.castDuration ?? 0) -
|
|
311
413
|
(enemy.castProgress ?? 0) +
|
|
@@ -317,39 +419,39 @@ export const Voidblade: WizardFunction = ({state}) =>
|
|
|
317
419
|
(urgentThreat && urgentThreat.ticksToImpact > shieldCancelThreshold)
|
|
318
420
|
)
|
|
319
421
|
{
|
|
320
|
-
return
|
|
422
|
+
return cancel().move(safeMove.x, safeMove.y);
|
|
321
423
|
}
|
|
322
|
-
return
|
|
424
|
+
return idle();
|
|
323
425
|
}
|
|
324
426
|
|
|
325
427
|
// === STEP 2: CAST-CANCEL INTO DEFENSE ===
|
|
326
428
|
// If we're casting a stab and a missile is incoming, cancel so we can
|
|
327
429
|
// blink-dodge (if available) or shield. Defense time depends on blink availability.
|
|
328
|
-
if (
|
|
430
|
+
if (status === 'casting' && castingSpell === 'missile')
|
|
329
431
|
{
|
|
330
432
|
// Cancel if target escaped melee range
|
|
331
|
-
if (distance > meleeAbortRange)
|
|
433
|
+
if (distance > meleeAbortRange)
|
|
332
434
|
{
|
|
333
|
-
return
|
|
435
|
+
return cancel().move(safeMove.x, safeMove.y);
|
|
334
436
|
}
|
|
335
437
|
|
|
336
438
|
// Defense time: blink (10t) is faster than shield (20t + 3t buffer)
|
|
337
439
|
const defenseTime =
|
|
338
|
-
|
|
339
|
-
? BLINK_CAST_TIME
|
|
340
|
-
: SHIELD_CAST_TIME + SHIELD_BUFFER;
|
|
440
|
+
blinkCooldown === 0
|
|
441
|
+
? RULES.BLINK_CAST_TIME
|
|
442
|
+
: RULES.SHIELD_CAST_TIME + SHIELD_BUFFER;
|
|
341
443
|
|
|
342
444
|
// Cancel for any undodgeable incoming threat we can't finish casting before
|
|
343
445
|
if (urgentThreat && !urgentThreat.bestDodgeDirection)
|
|
344
446
|
{
|
|
345
447
|
const remainingCast =
|
|
346
|
-
(
|
|
448
|
+
((castProgressData?.total) ?? 0) - ((castProgressData?.current) ?? 0);
|
|
347
449
|
const willArriveInWindow =
|
|
348
450
|
urgentThreat.ticksToImpact <=
|
|
349
|
-
remainingCast + GCD_DURATION + defenseTime;
|
|
350
|
-
if (willArriveInWindow)
|
|
451
|
+
remainingCast + RULES.GCD_DURATION + defenseTime;
|
|
452
|
+
if (willArriveInWindow)
|
|
351
453
|
{
|
|
352
|
-
return
|
|
454
|
+
return cancel().move(safeMove.x, safeMove.y);
|
|
353
455
|
}
|
|
354
456
|
}
|
|
355
457
|
|
|
@@ -357,28 +459,28 @@ export const Voidblade: WizardFunction = ({state}) =>
|
|
|
357
459
|
// at melee range their missile will arrive too fast to react after our cast finishes
|
|
358
460
|
if (
|
|
359
461
|
distance < preemptiveShieldRange &&
|
|
360
|
-
enemy.
|
|
462
|
+
enemy.status === 'casting' &&
|
|
361
463
|
enemy.castingSpell === 'missile'
|
|
362
464
|
)
|
|
363
465
|
{
|
|
364
466
|
const remainingEnemyCast =
|
|
365
467
|
(enemy.castDuration ?? 0) - (enemy.castProgress ?? 0);
|
|
366
468
|
const remainingOurCast =
|
|
367
|
-
(
|
|
469
|
+
((castProgressData?.total) ?? 0) - ((castProgressData?.current) ?? 0);
|
|
368
470
|
const estimatedFlightTicks = distance / enemyFlightTimeDivisor;
|
|
369
471
|
const ticksUntilEnemyHit = remainingEnemyCast + estimatedFlightTicks;
|
|
370
|
-
const ticksAfterOurCast = remainingOurCast + GCD_DURATION + defenseTime;
|
|
472
|
+
const ticksAfterOurCast = remainingOurCast + RULES.GCD_DURATION + defenseTime;
|
|
371
473
|
|
|
372
474
|
// If enemy missile will arrive before we can defend after our cast, cancel now
|
|
373
|
-
if (ticksUntilEnemyHit < ticksAfterOurCast)
|
|
475
|
+
if (ticksUntilEnemyHit < ticksAfterOurCast)
|
|
374
476
|
{
|
|
375
|
-
return
|
|
477
|
+
return cancel().move(safeMove.x, safeMove.y);
|
|
376
478
|
}
|
|
377
479
|
}
|
|
378
480
|
}
|
|
379
481
|
|
|
380
482
|
// === STEP 3: BLINK-DODGE — 100% damage avoidance (preferred over shield) ===
|
|
381
|
-
if (
|
|
483
|
+
if (status === 'idle' && blinkCooldown === 0)
|
|
382
484
|
{
|
|
383
485
|
// Blink target for dodging: close-gap target if available, else blink-past for in-flight only
|
|
384
486
|
const dodgeBlinkTarget = blinkCloseTarget ?? blinkPastTarget;
|
|
@@ -392,10 +494,7 @@ export const Voidblade: WizardFunction = ({state}) =>
|
|
|
392
494
|
urgentThreat.ticksToImpact <= blinkWindowMax
|
|
393
495
|
)
|
|
394
496
|
{
|
|
395
|
-
return
|
|
396
|
-
move: {x: 0, y: 0},
|
|
397
|
-
startCast: {spell: 'blink', target: dodgeBlinkTarget},
|
|
398
|
-
};
|
|
497
|
+
return blink(dodgeBlinkTarget.x, dodgeBlinkTarget.y);
|
|
399
498
|
}
|
|
400
499
|
|
|
401
500
|
// 3b: Preemptive blink-dodge — only when we have a close-gap target.
|
|
@@ -405,7 +504,7 @@ export const Voidblade: WizardFunction = ({state}) =>
|
|
|
405
504
|
preferBlinkDodge &&
|
|
406
505
|
blinkCloseTarget &&
|
|
407
506
|
distance < preemptiveShieldRange &&
|
|
408
|
-
enemy.
|
|
507
|
+
enemy.status === 'casting' &&
|
|
409
508
|
enemy.castingSpell === 'missile'
|
|
410
509
|
)
|
|
411
510
|
{
|
|
@@ -415,29 +514,40 @@ export const Voidblade: WizardFunction = ({state}) =>
|
|
|
415
514
|
const ticksUntilHit = remainingCast + estimatedFlightTicks;
|
|
416
515
|
|
|
417
516
|
if (
|
|
418
|
-
ticksUntilHit >= BLINK_CAST_TIME &&
|
|
419
|
-
ticksUntilHit < BLINK_CAST_TIME + shieldWindow
|
|
517
|
+
ticksUntilHit >= RULES.BLINK_CAST_TIME &&
|
|
518
|
+
ticksUntilHit < RULES.BLINK_CAST_TIME + shieldWindow
|
|
420
519
|
)
|
|
421
520
|
{
|
|
422
|
-
return
|
|
423
|
-
move: {x: 0, y: 0},
|
|
424
|
-
startCast: {spell: 'blink', target: blinkCloseTarget},
|
|
425
|
-
};
|
|
521
|
+
return blink(blinkCloseTarget.x, blinkCloseTarget.y);
|
|
426
522
|
}
|
|
427
523
|
}
|
|
428
524
|
|
|
429
525
|
// 3c: Engage blink — close distance during enemy vulnerability (they can't react)
|
|
526
|
+
// Skip if saving blink for chase (use blink reactively, not proactively)
|
|
430
527
|
if (
|
|
528
|
+
!saveBlinkForChase &&
|
|
431
529
|
blinkCloseTarget &&
|
|
432
530
|
distance > engageBlinkRange &&
|
|
433
531
|
vulnerabilityTicks >= punishMinVulnerability &&
|
|
434
532
|
(!urgentThreat || urgentThreat.bestDodgeDirection !== null)
|
|
435
|
-
)
|
|
533
|
+
)
|
|
436
534
|
{
|
|
437
|
-
return
|
|
438
|
-
|
|
439
|
-
|
|
535
|
+
return blink(blinkCloseTarget.x, blinkCloseTarget.y);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// 3d: Chase blink — enemy blinked away after we've fought at melee range
|
|
539
|
+
if (distance > chaseBlinkRange && lastMissileConfig)
|
|
540
|
+
{
|
|
541
|
+
const center = (ARENA_MIN + ARENA_MAX) / 2;
|
|
542
|
+
const toCX = center - enemy.position.x;
|
|
543
|
+
const toCY = center - enemy.position.y;
|
|
544
|
+
const toCDist = Math.sqrt(toCX * toCX + toCY * toCY) || 1;
|
|
545
|
+
const rawChase = {
|
|
546
|
+
x: enemy.position.x + (toCX / toCDist) * 15,
|
|
547
|
+
y: enemy.position.y + (toCY / toCDist) * 15,
|
|
440
548
|
};
|
|
549
|
+
const chaseTarget = capBlinkDistance(rawChase);
|
|
550
|
+
return blink(chaseTarget.x, chaseTarget.y);
|
|
441
551
|
}
|
|
442
552
|
}
|
|
443
553
|
|
|
@@ -446,25 +556,22 @@ export const Voidblade: WizardFunction = ({state}) =>
|
|
|
446
556
|
// 4a: Shield in-flight missiles
|
|
447
557
|
// Shield when: no blink target at all, or missile too close for blink (<10 ticks)
|
|
448
558
|
if (
|
|
449
|
-
|
|
559
|
+
status === 'idle' &&
|
|
450
560
|
urgentThreat &&
|
|
451
561
|
(!urgentThreat.bestDodgeDirection || shouldForceShield(urgentThreat)) &&
|
|
452
562
|
urgentThreat.canBlockInTime &&
|
|
453
563
|
((!blinkCloseTarget && !blinkPastTarget) || urgentThreat.ticksToImpact < 10)
|
|
454
|
-
)
|
|
564
|
+
)
|
|
455
565
|
{
|
|
456
|
-
return
|
|
457
|
-
move: {x: 0, y: 0},
|
|
458
|
-
startCast: {spell: 'shield'},
|
|
459
|
-
};
|
|
566
|
+
return shield();
|
|
460
567
|
}
|
|
461
568
|
|
|
462
569
|
// 4b: Preemptive shield — when blink can't close gap (melee range or cooldown)
|
|
463
570
|
if (
|
|
464
|
-
|
|
571
|
+
status === 'idle' &&
|
|
465
572
|
!blinkCloseTarget &&
|
|
466
573
|
distance < preemptiveShieldRange &&
|
|
467
|
-
enemy.
|
|
574
|
+
enemy.status === 'casting' &&
|
|
468
575
|
enemy.castingSpell === 'missile'
|
|
469
576
|
)
|
|
470
577
|
{
|
|
@@ -473,26 +580,26 @@ export const Voidblade: WizardFunction = ({state}) =>
|
|
|
473
580
|
const ticksUntilHit = remainingCast + estimatedFlightTicks;
|
|
474
581
|
|
|
475
582
|
if (
|
|
476
|
-
ticksUntilHit >= SHIELD_CAST_TIME &&
|
|
477
|
-
ticksUntilHit < SHIELD_CAST_TIME + shieldWindow
|
|
583
|
+
ticksUntilHit >= RULES.SHIELD_CAST_TIME &&
|
|
584
|
+
ticksUntilHit < RULES.SHIELD_CAST_TIME + shieldWindow
|
|
478
585
|
)
|
|
479
586
|
{
|
|
480
|
-
return
|
|
481
|
-
move: {x: 0, y: 0},
|
|
482
|
-
startCast: {spell: 'shield'},
|
|
483
|
-
};
|
|
587
|
+
return shield();
|
|
484
588
|
}
|
|
485
589
|
}
|
|
486
590
|
|
|
487
591
|
// === STEP 5: COUNTER-STAB — fire during enemy vulnerability ===
|
|
488
592
|
// Only fire when enemy CAN'T shield (in GCD or mid-cast).
|
|
489
593
|
// Use small, fast stabs that fit within the vulnerability window.
|
|
490
|
-
if (
|
|
594
|
+
if (status === 'idle' && distance < counterStabRange)
|
|
491
595
|
{
|
|
596
|
+
const reposition = getPreCastReposition(position, enemy.position);
|
|
597
|
+
if (reposition) return moveAction(reposition.x, reposition.y);
|
|
598
|
+
|
|
492
599
|
const enemyHP = Math.ceil(enemy.health);
|
|
493
600
|
const flightTime = Math.ceil(distance / flightTimeDivisor) + 1;
|
|
494
601
|
|
|
495
|
-
if (vulnerabilityTicks >= punishMinVulnerability)
|
|
602
|
+
if (vulnerabilityTicks >= punishMinVulnerability)
|
|
496
603
|
{
|
|
497
604
|
// Budget: stab must land while enemy is still vulnerable
|
|
498
605
|
// castTime + flightTime < vulnerabilityTicks
|
|
@@ -502,7 +609,7 @@ export const Voidblade: WizardFunction = ({state}) =>
|
|
|
502
609
|
let nextThreatTime = urgentThreat?.ticksToImpact ?? Infinity;
|
|
503
610
|
|
|
504
611
|
// Account for enemy's active cast — their missile will arrive after cast completes + flight time
|
|
505
|
-
if (enemy.
|
|
612
|
+
if (enemy.status === 'casting' && enemy.castingSpell === 'missile')
|
|
506
613
|
{
|
|
507
614
|
const remainingEnemyCast =
|
|
508
615
|
(enemy.castDuration ?? 0) - (enemy.castProgress ?? 0);
|
|
@@ -515,35 +622,31 @@ export const Voidblade: WizardFunction = ({state}) =>
|
|
|
515
622
|
|
|
516
623
|
// Defense time: blink (10t) is faster than shield (20t + 3t buffer)
|
|
517
624
|
const defenseTime =
|
|
518
|
-
|
|
519
|
-
? BLINK_CAST_TIME
|
|
520
|
-
: SHIELD_CAST_TIME + SHIELD_BUFFER;
|
|
521
|
-
const safeBudget = nextThreatTime - GCD_DURATION - defenseTime;
|
|
625
|
+
blinkCooldown === 0
|
|
626
|
+
? RULES.BLINK_CAST_TIME
|
|
627
|
+
: RULES.SHIELD_CAST_TIME + SHIELD_BUFFER;
|
|
628
|
+
const safeBudget = nextThreatTime - RULES.GCD_DURATION - defenseTime;
|
|
522
629
|
const effectiveBudget = Math.min(punishBudget, safeBudget);
|
|
523
630
|
|
|
524
|
-
if (effectiveBudget > 0)
|
|
631
|
+
if (effectiveBudget > 0)
|
|
525
632
|
{
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
633
|
+
let config = useDynamicStab
|
|
634
|
+
? fitMissileForEscapingTarget(effectiveBudget, distance, {
|
|
635
|
+
minTurnRate: 0,
|
|
636
|
+
maxDamage: enemyHP,
|
|
637
|
+
lastMissileConfig: lastMissileConfig,
|
|
638
|
+
})
|
|
639
|
+
: null;
|
|
640
|
+
|
|
641
|
+
if (!config || config.damage < MIN_USEFUL_DAMAGE)
|
|
642
|
+
{
|
|
643
|
+
config = {damage: stabDamage, speed: FAST_STAB.speed, turnRate: FAST_STAB.turnRate, duration: FAST_STAB.duration};
|
|
644
|
+
}
|
|
531
645
|
|
|
532
|
-
|
|
646
|
+
const castTime = getMissileCastTime(config, lastMissileConfig);
|
|
647
|
+
if (castTime <= effectiveBudget && castTime + flightTime <= vulnerabilityTicks)
|
|
533
648
|
{
|
|
534
|
-
|
|
535
|
-
if (castTime + flightTime <= vulnerabilityTicks)
|
|
536
|
-
{
|
|
537
|
-
return {
|
|
538
|
-
move,
|
|
539
|
-
startCast: {
|
|
540
|
-
spell: 'missile',
|
|
541
|
-
config,
|
|
542
|
-
missileAI: () => ({}),
|
|
543
|
-
direction: angleTo(state.position, enemy.position),
|
|
544
|
-
},
|
|
545
|
-
};
|
|
546
|
-
}
|
|
649
|
+
return missile(config, stabAI, stabAimAngle).move(safeMove.x, safeMove.y);
|
|
547
650
|
}
|
|
548
651
|
}
|
|
549
652
|
}
|
|
@@ -553,30 +656,25 @@ export const Voidblade: WizardFunction = ({state}) =>
|
|
|
553
656
|
// Channeling: shield blocks ~90% but 10% still lands. Stabs whittle HP so they
|
|
554
657
|
// can't just hold shield forever. Why: otherwise the bot waits for a
|
|
555
658
|
// vulnerability window that a channeling player never opens.
|
|
556
|
-
if ((enemy.
|
|
659
|
+
if ((enemy.status === 'idle' || enemy.status === 'channeling') && !urgentThreat)
|
|
557
660
|
{
|
|
558
|
-
// Use a small budget to keep stabs fast and stay reactive
|
|
559
661
|
const quickBudget = idleProvokeBudget;
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
662
|
+
let config = useDynamicStab
|
|
663
|
+
? fitMissileForEscapingTarget(quickBudget, distance, {
|
|
664
|
+
minTurnRate: 0,
|
|
665
|
+
maxDamage: enemyHP,
|
|
666
|
+
lastMissileConfig: lastMissileConfig,
|
|
667
|
+
})
|
|
668
|
+
: null;
|
|
565
669
|
|
|
566
|
-
if (config)
|
|
670
|
+
if (!config)
|
|
567
671
|
{
|
|
568
|
-
|
|
569
|
-
move,
|
|
570
|
-
startCast: {
|
|
571
|
-
spell: 'missile',
|
|
572
|
-
config,
|
|
573
|
-
missileAI: () => ({}),
|
|
574
|
-
direction: angleTo(state.position, enemy.position),
|
|
575
|
-
},
|
|
576
|
-
};
|
|
672
|
+
config = {damage: stabDamage, speed: FAST_STAB.speed, turnRate: FAST_STAB.turnRate, duration: FAST_STAB.duration};
|
|
577
673
|
}
|
|
674
|
+
|
|
675
|
+
return missile(config, stabAI, stabAimAngle).move(safeMove.x, safeMove.y);
|
|
578
676
|
}
|
|
579
677
|
}
|
|
580
678
|
|
|
581
|
-
return
|
|
582
|
-
}
|
|
679
|
+
return moveAction(safeMove.x, safeMove.y);
|
|
680
|
+
}
|