@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,15 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type {FinalAction, MissileFunction} from '../../hooks/index.js';
|
|
2
|
+
import {usePosition, useEnemy, useStatus, useBlinkCooldown, useHealth, useTick, useThreats, useProjectiles, useLastMissileConfig, 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
|
-
import {
|
|
5
|
-
ARENA_SIZE,
|
|
6
|
-
GCD_DURATION,
|
|
7
|
-
SHIELD_CAST_TIME,
|
|
8
|
-
BLINK_CAST_TIME,
|
|
9
|
-
} from '../../rules.js';
|
|
5
|
+
import {RULES, ARENA_MIN, ARENA_MAX} from '../../rules.js';
|
|
10
6
|
import {getMissileCastTime, getLeadPosition} from '../../utils/combat.js';
|
|
11
7
|
import {
|
|
12
|
-
getThreats,
|
|
13
8
|
getMostUrgentThreat,
|
|
14
9
|
getDodgeDirectionForMovement,
|
|
15
10
|
getBlinkToCenter,
|
|
@@ -22,16 +17,19 @@ import {
|
|
|
22
17
|
getClosingRate,
|
|
23
18
|
getEnemyVulnerabilityTicks,
|
|
24
19
|
isSafeToAttack,
|
|
20
|
+
keepOffWalls,
|
|
21
|
+
getPreCastReposition,
|
|
25
22
|
} from '../shared.js';
|
|
26
23
|
import {useParam} from '../../engine/params-runtime.js';
|
|
27
24
|
|
|
28
|
-
const HomingAI:
|
|
25
|
+
const HomingAI: MissileFunction = () =>
|
|
29
26
|
{
|
|
30
|
-
const
|
|
31
|
-
|
|
27
|
+
const ctx = getMissileContext();
|
|
28
|
+
const enemy = ctx.worldState.enemies[0];
|
|
29
|
+
return enemy ? turnToward(enemy.position.x, enemy.position.y) : flyStraight();
|
|
32
30
|
};
|
|
33
31
|
|
|
34
|
-
const StraightAI:
|
|
32
|
+
const StraightAI: MissileFunction = () => flyStraight();
|
|
35
33
|
|
|
36
34
|
const WALL_BUFFER = 50;
|
|
37
35
|
const SHIELD_BUFFER = 3;
|
|
@@ -50,12 +48,22 @@ function combatMove(
|
|
|
50
48
|
strafeCyclePeriod: number,
|
|
51
49
|
strafeIntensity: number,
|
|
52
50
|
approachSpeed: number,
|
|
53
|
-
|
|
51
|
+
centerBias: number,
|
|
52
|
+
): {x: number; y: number}
|
|
54
53
|
{
|
|
55
54
|
const deltaX = enemy.position.x - position.x;
|
|
56
55
|
const deltaY = enemy.position.y - position.y;
|
|
57
56
|
const normalizedDistance = currentDistance > 0 ? currentDistance : 1;
|
|
58
57
|
|
|
58
|
+
// Approach the center-side of the enemy so knockback pushes toward center, not lava.
|
|
59
|
+
const center = (ARENA_MIN + ARENA_MAX) / 2;
|
|
60
|
+
const toCenterX = center - enemy.position.x;
|
|
61
|
+
const toCenterY = center - enemy.position.y;
|
|
62
|
+
const toCenterDist = Math.sqrt(toCenterX * toCenterX + toCenterY * toCenterY) || 1;
|
|
63
|
+
const approachDirX = deltaX / normalizedDistance + (toCenterX / toCenterDist) * centerBias;
|
|
64
|
+
const approachDirY = deltaY / normalizedDistance + (toCenterY / toCenterDist) * centerBias;
|
|
65
|
+
const approachMag = Math.sqrt(approachDirX * approachDirX + approachDirY * approachDirY) || 1;
|
|
66
|
+
|
|
59
67
|
const strafeClockwise = {
|
|
60
68
|
x: -deltaY / normalizedDistance,
|
|
61
69
|
y: deltaX / normalizedDistance,
|
|
@@ -72,10 +80,10 @@ function combatMove(
|
|
|
72
80
|
strafeDirection = dodgeDirection;
|
|
73
81
|
}
|
|
74
82
|
else if (
|
|
75
|
-
position.x < WALL_BUFFER ||
|
|
76
|
-
position.x >
|
|
77
|
-
position.y < WALL_BUFFER ||
|
|
78
|
-
position.y >
|
|
83
|
+
position.x < ARENA_MIN + WALL_BUFFER ||
|
|
84
|
+
position.x > ARENA_MAX - WALL_BUFFER ||
|
|
85
|
+
position.y < ARENA_MIN + WALL_BUFFER ||
|
|
86
|
+
position.y > ARENA_MAX - WALL_BUFFER
|
|
79
87
|
)
|
|
80
88
|
{
|
|
81
89
|
const futureClockwise = {
|
|
@@ -87,16 +95,16 @@ function combatMove(
|
|
|
87
95
|
y: position.y + strafeCounterClockwise.y * 100,
|
|
88
96
|
};
|
|
89
97
|
const scoreClockwise = Math.min(
|
|
90
|
-
futureClockwise.x,
|
|
91
|
-
|
|
92
|
-
futureClockwise.y,
|
|
93
|
-
|
|
98
|
+
futureClockwise.x - ARENA_MIN,
|
|
99
|
+
ARENA_MAX - futureClockwise.x,
|
|
100
|
+
futureClockwise.y - ARENA_MIN,
|
|
101
|
+
ARENA_MAX - futureClockwise.y,
|
|
94
102
|
);
|
|
95
103
|
const scoreCounterClockwise = Math.min(
|
|
96
|
-
futureCounterClockwise.x,
|
|
97
|
-
|
|
98
|
-
futureCounterClockwise.y,
|
|
99
|
-
|
|
104
|
+
futureCounterClockwise.x - ARENA_MIN,
|
|
105
|
+
ARENA_MAX - futureCounterClockwise.x,
|
|
106
|
+
futureCounterClockwise.y - ARENA_MIN,
|
|
107
|
+
ARENA_MAX - futureCounterClockwise.y,
|
|
100
108
|
);
|
|
101
109
|
strafeDirection =
|
|
102
110
|
scoreClockwise > scoreCounterClockwise
|
|
@@ -111,34 +119,34 @@ function combatMove(
|
|
|
111
119
|
|
|
112
120
|
if (dodgeDirection)
|
|
113
121
|
{
|
|
114
|
-
return {x: strafeDirection.x
|
|
122
|
+
return {x: strafeDirection.x, y: strafeDirection.y};
|
|
115
123
|
}
|
|
116
124
|
|
|
117
125
|
let moveX = strafeDirection.x * strafeIntensity;
|
|
118
126
|
let moveY = strafeDirection.y * strafeIntensity;
|
|
119
127
|
|
|
120
|
-
if (currentDistance > maxDistance)
|
|
128
|
+
if (currentDistance > maxDistance)
|
|
121
129
|
{
|
|
122
|
-
moveX += (
|
|
123
|
-
moveY += (
|
|
130
|
+
moveX += (approachDirX / approachMag) * approachSpeed;
|
|
131
|
+
moveY += (approachDirY / approachMag) * approachSpeed;
|
|
124
132
|
}
|
|
125
133
|
else if (currentDistance < minDistance)
|
|
126
134
|
{
|
|
127
135
|
let retreatX = -(deltaX / normalizedDistance) * approachSpeed;
|
|
128
136
|
let retreatY = -(deltaY / normalizedDistance) * approachSpeed;
|
|
129
|
-
if (position.x < WALL_BUFFER && retreatX < 0) retreatX = 0;
|
|
130
|
-
if (position.x >
|
|
131
|
-
if (position.y < WALL_BUFFER && retreatY < 0) retreatY = 0;
|
|
132
|
-
if (position.y >
|
|
137
|
+
if (position.x < ARENA_MIN + WALL_BUFFER && retreatX < 0) retreatX = 0;
|
|
138
|
+
if (position.x > ARENA_MAX - WALL_BUFFER && retreatX > 0) retreatX = 0;
|
|
139
|
+
if (position.y < ARENA_MIN + WALL_BUFFER && retreatY < 0) retreatY = 0;
|
|
140
|
+
if (position.y > ARENA_MAX - WALL_BUFFER && retreatY > 0) retreatY = 0;
|
|
133
141
|
moveX += retreatX;
|
|
134
142
|
moveY += retreatY;
|
|
135
143
|
}
|
|
136
144
|
|
|
137
145
|
const magnitude = Math.sqrt(moveX * moveX + moveY * moveY);
|
|
138
|
-
if (magnitude >
|
|
146
|
+
if (magnitude > 1)
|
|
139
147
|
{
|
|
140
|
-
moveX =
|
|
141
|
-
moveY =
|
|
148
|
+
moveX = moveX / magnitude;
|
|
149
|
+
moveY = moveY / magnitude;
|
|
142
150
|
}
|
|
143
151
|
|
|
144
152
|
return {x: moveX, y: moveY};
|
|
@@ -161,17 +169,30 @@ function combatMove(
|
|
|
161
169
|
* PROGRESSION LINE: Spellshot → Spelltracer → Spellseeker
|
|
162
170
|
* TIER: 3 (elite Sniper line)
|
|
163
171
|
*/
|
|
164
|
-
export
|
|
172
|
+
export function Spellseeker(): FinalAction
|
|
165
173
|
{
|
|
166
|
-
const
|
|
174
|
+
const position = usePosition();
|
|
175
|
+
const enemy = useEnemy();
|
|
176
|
+
const status = useStatus();
|
|
177
|
+
const blinkCooldown = useBlinkCooldown();
|
|
178
|
+
const health = useHealth();
|
|
179
|
+
const tick = useTick();
|
|
180
|
+
const threats = useThreats();
|
|
181
|
+
const projectiles = useProjectiles();
|
|
182
|
+
const lastMissileConfig = useLastMissileConfig();
|
|
183
|
+
const castingSpell = useCastingSpell();
|
|
184
|
+
const castProgressData = useCastProgress();
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
const dangerDistance = useParam('dangerDistance', 362, {
|
|
167
188
|
range: 175,
|
|
168
189
|
min: 0,
|
|
169
190
|
});
|
|
170
|
-
const targetDistance = useParam('targetDistance',
|
|
191
|
+
const targetDistance = useParam('targetDistance', 515, {
|
|
171
192
|
range: 200,
|
|
172
193
|
min: 0,
|
|
173
194
|
});
|
|
174
|
-
const distanceDeadZone = useParam('distanceDeadZone',
|
|
195
|
+
const distanceDeadZone = useParam('distanceDeadZone', 55, {
|
|
175
196
|
range: 50,
|
|
176
197
|
min: 0,
|
|
177
198
|
});
|
|
@@ -183,28 +204,28 @@ export const Spellseeker: WizardFunction = ({state}) =>
|
|
|
183
204
|
min: 0,
|
|
184
205
|
steps: 5,
|
|
185
206
|
});
|
|
186
|
-
const punishMinVulnerability = useParam('punishMinVulnerability',
|
|
207
|
+
const punishMinVulnerability = useParam('punishMinVulnerability', 249, {
|
|
187
208
|
range: 30,
|
|
188
209
|
min: 10,
|
|
189
210
|
steps: 5,
|
|
190
211
|
});
|
|
191
212
|
const preferBlinkDodge =
|
|
192
213
|
useParam('preferBlinkDodge', 1, {min: 0, max: 1, steps: 2}) >= 0.5;
|
|
193
|
-
const strafeCyclePeriod = useParam('strafeCyclePeriod',
|
|
214
|
+
const strafeCyclePeriod = useParam('strafeCyclePeriod', 112, {
|
|
194
215
|
range: 75,
|
|
195
216
|
min: 80,
|
|
196
217
|
max: 250,
|
|
197
218
|
});
|
|
198
|
-
const strafeIntensity = useParam('strafeIntensity',
|
|
199
|
-
range:
|
|
200
|
-
min:
|
|
201
|
-
max:
|
|
219
|
+
const strafeIntensity = useParam('strafeIntensity', 0.99, {
|
|
220
|
+
range: 0.4,
|
|
221
|
+
min: 0.2,
|
|
222
|
+
max: 1,
|
|
202
223
|
steps: 7,
|
|
203
224
|
});
|
|
204
|
-
const approachSpeed = useParam('approachSpeed',
|
|
205
|
-
range:
|
|
206
|
-
min:
|
|
207
|
-
max:
|
|
225
|
+
const approachSpeed = useParam('approachSpeed', 0.34, {
|
|
226
|
+
range: 0.3,
|
|
227
|
+
min: 0.1,
|
|
228
|
+
max: 0.8,
|
|
208
229
|
steps: 7,
|
|
209
230
|
});
|
|
210
231
|
const shieldCancelThreshold = useParam('shieldCancelThreshold', 100, {
|
|
@@ -212,13 +233,13 @@ export const Spellseeker: WizardFunction = ({state}) =>
|
|
|
212
233
|
min: 50,
|
|
213
234
|
max: 300,
|
|
214
235
|
});
|
|
215
|
-
const blinkWindowMax = useParam('blinkWindowMax',
|
|
236
|
+
const blinkWindowMax = useParam('blinkWindowMax', 93, {
|
|
216
237
|
range: 40,
|
|
217
238
|
min: 20,
|
|
218
239
|
max: 120,
|
|
219
240
|
steps: 7,
|
|
220
241
|
});
|
|
221
|
-
const proactiveBlinkRange = useParam('proactiveBlinkRange',
|
|
242
|
+
const proactiveBlinkRange = useParam('proactiveBlinkRange', 283, {
|
|
222
243
|
range: 150,
|
|
223
244
|
min: 100,
|
|
224
245
|
max: 500,
|
|
@@ -248,80 +269,90 @@ export const Spellseeker: WizardFunction = ({state}) =>
|
|
|
248
269
|
max: 400,
|
|
249
270
|
steps: 7,
|
|
250
271
|
});
|
|
251
|
-
const punishAttackRange = useParam('punishAttackRange',
|
|
272
|
+
const punishAttackRange = useParam('punishAttackRange', 288, {
|
|
252
273
|
range: 150,
|
|
253
274
|
min: 200,
|
|
254
275
|
max: 700,
|
|
255
276
|
steps: 7,
|
|
256
277
|
});
|
|
257
|
-
const
|
|
258
|
-
range:
|
|
259
|
-
min:
|
|
260
|
-
max:
|
|
278
|
+
const centerBias = useParam('centerBias', 0.69, {
|
|
279
|
+
range: 0.4,
|
|
280
|
+
min: 0,
|
|
281
|
+
max: 0.8,
|
|
261
282
|
steps: 5,
|
|
262
283
|
});
|
|
284
|
+
const maxBlinkDistance = useParam('maxBlinkDistance', 283, {
|
|
285
|
+
range: 100,
|
|
286
|
+
min: 50,
|
|
287
|
+
max: 300,
|
|
288
|
+
steps: 7,
|
|
289
|
+
});
|
|
263
290
|
|
|
264
|
-
|
|
265
|
-
if (!enemy)
|
|
291
|
+
function capBlinkDistance(target: {x: number; y: number}): {x: number; y: number}
|
|
266
292
|
{
|
|
267
|
-
|
|
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};
|
|
268
299
|
}
|
|
269
300
|
|
|
270
|
-
const distance = distanceTo(
|
|
271
|
-
const threats = getThreats(state);
|
|
301
|
+
const distance = distanceTo(position, enemy.position);
|
|
272
302
|
const urgentThreat = getMostUrgentThreat(threats);
|
|
273
|
-
const dodgeDirection = getDodgeDirectionForMovement(threats,
|
|
303
|
+
const dodgeDirection = getDodgeDirectionForMovement(threats, position);
|
|
274
304
|
const move = combatMove(
|
|
275
|
-
|
|
305
|
+
position,
|
|
276
306
|
enemy,
|
|
277
307
|
distance,
|
|
278
308
|
rangeMin,
|
|
279
309
|
rangeMax,
|
|
280
310
|
dodgeDirection,
|
|
281
|
-
|
|
311
|
+
tick,
|
|
282
312
|
strafeCyclePeriod,
|
|
283
313
|
strafeIntensity,
|
|
284
314
|
approachSpeed,
|
|
315
|
+
centerBias,
|
|
285
316
|
);
|
|
286
|
-
|
|
317
|
+
const safeMove = keepOffWalls(position, move);
|
|
287
318
|
// === STEP 1: CHANNELING — cancel shield when safe ===
|
|
288
|
-
if (
|
|
319
|
+
if (status === 'channeling')
|
|
289
320
|
{
|
|
290
321
|
if (!urgentThreat || urgentThreat.ticksToImpact > shieldCancelThreshold)
|
|
291
322
|
{
|
|
292
|
-
return
|
|
323
|
+
return cancel().move(safeMove.x, safeMove.y);
|
|
293
324
|
}
|
|
294
|
-
return
|
|
325
|
+
return idle();
|
|
295
326
|
}
|
|
296
327
|
|
|
297
328
|
// === STEP 2: PROGRESSIVE CAST-CANCEL ===
|
|
298
329
|
if (
|
|
299
|
-
|
|
300
|
-
|
|
330
|
+
status === 'casting' &&
|
|
331
|
+
castingSpell === 'missile' &&
|
|
301
332
|
urgentThreat &&
|
|
302
333
|
!urgentThreat.bestDodgeDirection
|
|
303
334
|
)
|
|
304
335
|
{
|
|
305
|
-
const remainingCast = (
|
|
336
|
+
const remainingCast = ((castProgressData?.total) ?? 0) - ((castProgressData?.current) ?? 0);
|
|
306
337
|
const defenseTime =
|
|
307
|
-
|
|
308
|
-
? BLINK_CAST_TIME
|
|
309
|
-
: SHIELD_CAST_TIME + SHIELD_BUFFER;
|
|
338
|
+
blinkCooldown === 0
|
|
339
|
+
? RULES.BLINK_CAST_TIME
|
|
340
|
+
: RULES.SHIELD_CAST_TIME + SHIELD_BUFFER;
|
|
310
341
|
const willArriveInWindow =
|
|
311
|
-
urgentThreat.ticksToImpact <= remainingCast + GCD_DURATION + defenseTime;
|
|
342
|
+
urgentThreat.ticksToImpact <= remainingCast + RULES.GCD_DURATION + defenseTime;
|
|
312
343
|
|
|
313
344
|
if (willArriveInWindow)
|
|
314
345
|
{
|
|
315
346
|
const incomingDamage = urgentThreat.projectile.damage;
|
|
316
|
-
const castRatio = (
|
|
347
|
+
const castRatio = ((castProgressData?.current) ?? 0) / ((castProgressData?.total) ?? 1);
|
|
317
348
|
|
|
318
|
-
if (incomingDamage >=
|
|
349
|
+
if (incomingDamage >= health || incomingDamage >= 20)
|
|
319
350
|
{
|
|
320
|
-
return
|
|
351
|
+
return cancel().move(safeMove.x, safeMove.y);
|
|
321
352
|
}
|
|
322
353
|
if (incomingDamage >= 10 && castRatio < 0.8)
|
|
323
354
|
{
|
|
324
|
-
return
|
|
355
|
+
return cancel().move(safeMove.x, safeMove.y);
|
|
325
356
|
}
|
|
326
357
|
}
|
|
327
358
|
}
|
|
@@ -329,11 +360,11 @@ export const Spellseeker: WizardFunction = ({state}) =>
|
|
|
329
360
|
// === STEP 3: BLINK-DODGE — 100% damage avoidance (optimizer-controlled) ===
|
|
330
361
|
if (
|
|
331
362
|
preferBlinkDodge &&
|
|
332
|
-
|
|
363
|
+
status === 'idle' &&
|
|
333
364
|
urgentThreat &&
|
|
334
|
-
|
|
365
|
+
blinkCooldown === 0 &&
|
|
335
366
|
(!urgentThreat.bestDodgeDirection || shouldForceShield(urgentThreat)) &&
|
|
336
|
-
urgentThreat.ticksToImpact >= BLINK_CAST_TIME &&
|
|
367
|
+
urgentThreat.ticksToImpact >= RULES.BLINK_CAST_TIME &&
|
|
337
368
|
urgentThreat.ticksToImpact <= blinkWindowMax
|
|
338
369
|
)
|
|
339
370
|
{
|
|
@@ -341,20 +372,18 @@ export const Spellseeker: WizardFunction = ({state}) =>
|
|
|
341
372
|
const target =
|
|
342
373
|
distance < midRange
|
|
343
374
|
? (getBlinkToIncreaseDistance(
|
|
344
|
-
|
|
375
|
+
position,
|
|
345
376
|
enemy.position,
|
|
346
|
-
|
|
347
|
-
) ?? getBlinkToCenter(
|
|
348
|
-
: getBlinkToCenter(
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
startCast: {spell: 'blink', target},
|
|
352
|
-
};
|
|
377
|
+
projectiles,
|
|
378
|
+
) ?? getBlinkToCenter(position))
|
|
379
|
+
: getBlinkToCenter(position);
|
|
380
|
+
const capped = capBlinkDistance(target);
|
|
381
|
+
return blink(capped.x, capped.y);
|
|
353
382
|
}
|
|
354
383
|
|
|
355
384
|
// === STEP 4: SHIELD — undodgeable or critical threats ===
|
|
356
385
|
if (
|
|
357
|
-
|
|
386
|
+
status === 'idle' &&
|
|
358
387
|
urgentThreat &&
|
|
359
388
|
(!urgentThreat.bestDodgeDirection || shouldForceShield(urgentThreat))
|
|
360
389
|
)
|
|
@@ -364,22 +393,40 @@ export const Spellseeker: WizardFunction = ({state}) =>
|
|
|
364
393
|
urgentThreat.ticksToStartShield <= SHIELD_BUFFER
|
|
365
394
|
)
|
|
366
395
|
{
|
|
367
|
-
return
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
396
|
+
return shield();
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// === Preemptive shield — shield when enemy is casting a missile at close range ===
|
|
401
|
+
if (
|
|
402
|
+
status === 'idle' &&
|
|
403
|
+
distance < 300 &&
|
|
404
|
+
enemy.status === 'casting' &&
|
|
405
|
+
enemy.castingSpell === 'missile'
|
|
406
|
+
)
|
|
407
|
+
{
|
|
408
|
+
const remainingCast = (enemy.castDuration ?? 0) - (enemy.castProgress ?? 0);
|
|
409
|
+
const estimatedFlightTicks = distance / 8;
|
|
410
|
+
const ticksUntilHit = remainingCast + estimatedFlightTicks;
|
|
411
|
+
|
|
412
|
+
if (
|
|
413
|
+
ticksUntilHit >= RULES.SHIELD_CAST_TIME &&
|
|
414
|
+
ticksUntilHit < RULES.SHIELD_CAST_TIME + 15
|
|
415
|
+
)
|
|
416
|
+
{
|
|
417
|
+
return shield();
|
|
371
418
|
}
|
|
372
419
|
}
|
|
373
420
|
|
|
374
421
|
// === STEP 5: PROACTIVE DISTANCE BLINK ===
|
|
375
422
|
if (
|
|
376
|
-
|
|
377
|
-
|
|
423
|
+
status === 'idle' &&
|
|
424
|
+
blinkCooldown === 0 &&
|
|
378
425
|
(!urgentThreat || urgentThreat.bestDodgeDirection !== null)
|
|
379
426
|
)
|
|
380
427
|
{
|
|
381
428
|
const closingRate = getClosingRate(
|
|
382
|
-
|
|
429
|
+
position,
|
|
383
430
|
enemy.position,
|
|
384
431
|
enemy.velocity,
|
|
385
432
|
);
|
|
@@ -389,28 +436,29 @@ export const Spellseeker: WizardFunction = ({state}) =>
|
|
|
389
436
|
)
|
|
390
437
|
{
|
|
391
438
|
const blinkTarget = getBlinkToIncreaseDistance(
|
|
392
|
-
|
|
439
|
+
position,
|
|
393
440
|
enemy.position,
|
|
394
|
-
|
|
441
|
+
projectiles,
|
|
395
442
|
MIN_BLINK_ESCAPE_DISTANCE,
|
|
396
443
|
);
|
|
397
|
-
if (blinkTarget)
|
|
444
|
+
if (blinkTarget)
|
|
398
445
|
{
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
startCast: {spell: 'blink', target: blinkTarget},
|
|
402
|
-
};
|
|
446
|
+
const capped = capBlinkDistance(blinkTarget);
|
|
447
|
+
return blink(capped.x, capped.y);
|
|
403
448
|
}
|
|
404
449
|
}
|
|
405
450
|
}
|
|
406
451
|
|
|
407
452
|
// === STEP 6: OFFENSE ===
|
|
408
|
-
if (
|
|
453
|
+
if (status === 'idle' && distance < 600)
|
|
409
454
|
{
|
|
455
|
+
const reposition = getPreCastReposition(position, enemy.position);
|
|
456
|
+
if (reposition) return moveAction(reposition.x, reposition.y);
|
|
457
|
+
|
|
410
458
|
const offenseDefenseTime =
|
|
411
|
-
|
|
412
|
-
? BLINK_CAST_TIME
|
|
413
|
-
: SHIELD_CAST_TIME + SHIELD_BUFFER;
|
|
459
|
+
blinkCooldown === 0
|
|
460
|
+
? RULES.BLINK_CAST_TIME
|
|
461
|
+
: RULES.SHIELD_CAST_TIME + SHIELD_BUFFER;
|
|
414
462
|
const enemyHP = Math.ceil(enemy.health);
|
|
415
463
|
const vulnerabilityTicks = getEnemyVulnerabilityTicks(enemy);
|
|
416
464
|
|
|
@@ -423,7 +471,7 @@ export const Spellseeker: WizardFunction = ({state}) =>
|
|
|
423
471
|
const flightTimeEstimate = distance / flightTimeDivisor;
|
|
424
472
|
const punishBudget = vulnerabilityTicks - flightTimeEstimate;
|
|
425
473
|
const nextThreatTime = urgentThreat?.ticksToImpact ?? Infinity;
|
|
426
|
-
const safeBudget = nextThreatTime - GCD_DURATION - offenseDefenseTime;
|
|
474
|
+
const safeBudget = nextThreatTime - RULES.GCD_DURATION - offenseDefenseTime;
|
|
427
475
|
const effectiveBudget = Math.min(
|
|
428
476
|
punishBudget,
|
|
429
477
|
safeBudget,
|
|
@@ -435,12 +483,12 @@ export const Spellseeker: WizardFunction = ({state}) =>
|
|
|
435
483
|
const config = fitMissileToBudget(effectiveBudget, distance, {
|
|
436
484
|
minTurnRate: 0,
|
|
437
485
|
maxDamage: enemyHP,
|
|
438
|
-
lastMissileConfig:
|
|
486
|
+
lastMissileConfig: lastMissileConfig,
|
|
439
487
|
});
|
|
440
488
|
|
|
441
489
|
if (config && config.damage >= MIN_USEFUL_DAMAGE)
|
|
442
490
|
{
|
|
443
|
-
const castTime = getMissileCastTime(config,
|
|
491
|
+
const castTime = getMissileCastTime(config, lastMissileConfig);
|
|
444
492
|
const actualFlight = distance / config.speed;
|
|
445
493
|
|
|
446
494
|
if (
|
|
@@ -455,17 +503,9 @@ export const Spellseeker: WizardFunction = ({state}) =>
|
|
|
455
503
|
enemy.position,
|
|
456
504
|
enemy.velocity,
|
|
457
505
|
config.speed,
|
|
458
|
-
|
|
506
|
+
position,
|
|
459
507
|
);
|
|
460
|
-
return
|
|
461
|
-
move,
|
|
462
|
-
startCast: {
|
|
463
|
-
spell: 'missile',
|
|
464
|
-
config,
|
|
465
|
-
missileAI: config.turnRate > 0 ? HomingAI : StraightAI,
|
|
466
|
-
direction: angleTo(state.position, aimTarget),
|
|
467
|
-
},
|
|
468
|
-
};
|
|
508
|
+
return missile(config, config.turnRate > 0 ? HomingAI : StraightAI, angleTo(position, aimTarget)).move(safeMove.x, safeMove.y);
|
|
469
509
|
}
|
|
470
510
|
}
|
|
471
511
|
}
|
|
@@ -475,7 +515,7 @@ export const Spellseeker: WizardFunction = ({state}) =>
|
|
|
475
515
|
let nextThreatTime = urgentThreat?.ticksToImpact ?? Infinity;
|
|
476
516
|
|
|
477
517
|
// Anticipate enemy's next missile based on their current state
|
|
478
|
-
if (enemy.
|
|
518
|
+
if (enemy.status === 'casting' && enemy.castingSpell === 'missile')
|
|
479
519
|
{
|
|
480
520
|
const remainingEnemyCast =
|
|
481
521
|
(enemy.castDuration ?? 0) - (enemy.castProgress ?? 0);
|
|
@@ -497,50 +537,35 @@ export const Spellseeker: WizardFunction = ({state}) =>
|
|
|
497
537
|
}
|
|
498
538
|
|
|
499
539
|
const safeBudget =
|
|
500
|
-
nextThreatTime - GCD_DURATION - SHIELD_CAST_TIME - SHIELD_BUFFER;
|
|
540
|
+
nextThreatTime - RULES.GCD_DURATION - RULES.SHIELD_CAST_TIME - SHIELD_BUFFER;
|
|
501
541
|
const budgetTicks = Math.min(safeBudget, MAX_CAST_BUDGET);
|
|
502
542
|
|
|
503
543
|
const config = fitMissileToBudget(budgetTicks, distance, {
|
|
504
544
|
minTurnRate,
|
|
505
545
|
maxDamage: enemyHP,
|
|
506
|
-
lastMissileConfig:
|
|
546
|
+
lastMissileConfig: lastMissileConfig,
|
|
507
547
|
});
|
|
508
548
|
|
|
509
549
|
// HIGH-DAMAGE PRIORITY: Sniper missiles need meaningful shield chip.
|
|
510
550
|
// Fixed big-shot fallback ensures Spellseeker always does significant damage.
|
|
511
551
|
const BIG_SHOT = {damage: 28, speed: 10, turnRate: 0.5, duration: 80};
|
|
512
552
|
|
|
513
|
-
if (
|
|
514
|
-
config &&
|
|
515
|
-
config.damage >= 15 &&
|
|
516
|
-
isSafeToAttack(threats, safeAttackCastEstimate)
|
|
517
|
-
)
|
|
553
|
+
if (config && config.damage >= 15)
|
|
518
554
|
{
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
missileAI: HomingAI,
|
|
525
|
-
direction: angleTo(state.position, enemy.position),
|
|
526
|
-
},
|
|
527
|
-
};
|
|
555
|
+
const actualCastTime = getMissileCastTime(config, lastMissileConfig);
|
|
556
|
+
if (isSafeToAttack(threats, actualCastTime))
|
|
557
|
+
{
|
|
558
|
+
return missile(config, HomingAI, angleTo(position, enemy.position)).move(safeMove.x, safeMove.y);
|
|
559
|
+
}
|
|
528
560
|
}
|
|
529
561
|
|
|
530
562
|
// Adaptive fitting weak or failed — fire big shot for meaningful damage
|
|
531
|
-
|
|
563
|
+
const bigShotCast = getMissileCastTime(BIG_SHOT, lastMissileConfig);
|
|
564
|
+
if (isSafeToAttack(threats, bigShotCast))
|
|
532
565
|
{
|
|
533
|
-
return
|
|
534
|
-
move,
|
|
535
|
-
startCast: {
|
|
536
|
-
spell: 'missile',
|
|
537
|
-
config: BIG_SHOT,
|
|
538
|
-
missileAI: HomingAI,
|
|
539
|
-
direction: angleTo(state.position, enemy.position),
|
|
540
|
-
},
|
|
541
|
-
};
|
|
566
|
+
return missile(BIG_SHOT, HomingAI, angleTo(position, enemy.position)).move(safeMove.x, safeMove.y);
|
|
542
567
|
}
|
|
543
568
|
}
|
|
544
569
|
|
|
545
|
-
return
|
|
546
|
-
}
|
|
570
|
+
return moveAction(safeMove.x, safeMove.y);
|
|
571
|
+
}
|