@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,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type {FinalAction, MissileFunction} 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, 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
|
+
import {RULES, ARENA_MIN, ARENA_MAX} from '../../rules.js';
|
|
5
6
|
import {
|
|
6
|
-
getThreats,
|
|
7
7
|
getMostUrgentThreat,
|
|
8
8
|
getDodgeDirectionForMovement,
|
|
9
9
|
getBlinkToCenter,
|
|
@@ -13,6 +13,8 @@ import {
|
|
|
13
13
|
shouldForceShield,
|
|
14
14
|
MAX_CAST_BUDGET,
|
|
15
15
|
MIN_USEFUL_DAMAGE,
|
|
16
|
+
keepOffWalls,
|
|
17
|
+
getPreCastReposition,
|
|
16
18
|
} from '../shared.js';
|
|
17
19
|
import {useParam} from '../../engine/params-runtime.js';
|
|
18
20
|
const WALL_BUFFER = 60;
|
|
@@ -22,12 +24,11 @@ const SHIELD_BUFFER = 3;
|
|
|
22
24
|
/**
|
|
23
25
|
* Standard homing missile AI.
|
|
24
26
|
*/
|
|
25
|
-
const HomingAI:
|
|
27
|
+
const HomingAI: MissileFunction = () =>
|
|
26
28
|
{
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
};
|
|
29
|
+
const ctx = getMissileContext();
|
|
30
|
+
const enemy = ctx.worldState.enemies[0];
|
|
31
|
+
return enemy ? turnToward(enemy.position.x, enemy.position.y) : flyStraight();
|
|
31
32
|
};
|
|
32
33
|
|
|
33
34
|
/**
|
|
@@ -36,10 +37,10 @@ const HomingAI: MissileAIFunction = ({worldState}) =>
|
|
|
36
37
|
function isNearWall(position: {x: number; y: number}): boolean
|
|
37
38
|
{
|
|
38
39
|
return (
|
|
39
|
-
position.x < WALL_BUFFER ||
|
|
40
|
-
position.x >
|
|
41
|
-
position.y < WALL_BUFFER ||
|
|
42
|
-
position.y >
|
|
40
|
+
position.x < ARENA_MIN + WALL_BUFFER ||
|
|
41
|
+
position.x > ARENA_MAX - WALL_BUFFER ||
|
|
42
|
+
position.y < ARENA_MIN + WALL_BUFFER ||
|
|
43
|
+
position.y > ARENA_MAX - WALL_BUFFER
|
|
43
44
|
);
|
|
44
45
|
}
|
|
45
46
|
|
|
@@ -86,16 +87,16 @@ function smartStrafe(
|
|
|
86
87
|
y: position.y + strafeCounterClockwise.y * 100,
|
|
87
88
|
};
|
|
88
89
|
const scoreClockwise = Math.min(
|
|
89
|
-
futureClockwise.x,
|
|
90
|
-
|
|
91
|
-
futureClockwise.y,
|
|
92
|
-
|
|
90
|
+
futureClockwise.x - ARENA_MIN,
|
|
91
|
+
ARENA_MAX - futureClockwise.x,
|
|
92
|
+
futureClockwise.y - ARENA_MIN,
|
|
93
|
+
ARENA_MAX - futureClockwise.y,
|
|
93
94
|
);
|
|
94
95
|
const scoreCounterClockwise = Math.min(
|
|
95
|
-
futureCounterClockwise.x,
|
|
96
|
-
|
|
97
|
-
futureCounterClockwise.y,
|
|
98
|
-
|
|
96
|
+
futureCounterClockwise.x - ARENA_MIN,
|
|
97
|
+
ARENA_MAX - futureCounterClockwise.x,
|
|
98
|
+
futureCounterClockwise.y - ARENA_MIN,
|
|
99
|
+
ARENA_MAX - futureCounterClockwise.y,
|
|
99
100
|
);
|
|
100
101
|
strafeDir =
|
|
101
102
|
scoreClockwise > scoreCounterClockwise
|
|
@@ -108,7 +109,7 @@ function smartStrafe(
|
|
|
108
109
|
strafeDir = cycle === 0 ? strafeClockwise : strafeCounterClockwise;
|
|
109
110
|
}
|
|
110
111
|
|
|
111
|
-
const intensity = dodgeDirection ?
|
|
112
|
+
const intensity = dodgeDirection ? 1 : strafeIntensity;
|
|
112
113
|
return {x: strafeDir.x * intensity, y: strafeDir.y * intensity};
|
|
113
114
|
}
|
|
114
115
|
|
|
@@ -134,35 +135,49 @@ function smartStrafe(
|
|
|
134
135
|
*
|
|
135
136
|
* TIER: 2 (enhanced Spellspinner)
|
|
136
137
|
*/
|
|
137
|
-
export
|
|
138
|
+
export function Spellweaver(): FinalAction
|
|
138
139
|
{
|
|
139
|
-
const
|
|
140
|
+
const position = usePosition();
|
|
141
|
+
const enemy = useEnemy();
|
|
142
|
+
const status = useStatus();
|
|
143
|
+
const blinkCooldown = useBlinkCooldown();
|
|
144
|
+
const health = useHealth();
|
|
145
|
+
const tick = useTick();
|
|
146
|
+
const threats = useThreats();
|
|
147
|
+
const projectiles = useProjectiles();
|
|
148
|
+
const myProjectiles = useMyProjectiles();
|
|
149
|
+
const lastMissileConfig = useLastMissileConfig();
|
|
150
|
+
const castingSpell = useCastingSpell();
|
|
151
|
+
const castProgressData = useCastProgress();
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
const targetDistance = useParam('targetDistance', 257, {
|
|
140
155
|
range: 150,
|
|
141
156
|
min: 0,
|
|
142
157
|
});
|
|
143
|
-
const dangerDistance = useParam('dangerDistance',
|
|
158
|
+
const dangerDistance = useParam('dangerDistance', 295, {
|
|
144
159
|
range: 125,
|
|
145
160
|
min: 0,
|
|
146
161
|
});
|
|
147
|
-
const minTurnRate = useParam('minTurnRate', 0.
|
|
148
|
-
const strafeCyclePeriod = useParam('strafeCyclePeriod',
|
|
162
|
+
const minTurnRate = useParam('minTurnRate', 0.9, {range: 1.5, steps: 5});
|
|
163
|
+
const strafeCyclePeriod = useParam('strafeCyclePeriod', 285, {
|
|
149
164
|
range: 75,
|
|
150
165
|
min: 80,
|
|
151
166
|
max: 300,
|
|
152
167
|
});
|
|
153
|
-
const strafeIntensity = useParam('strafeIntensity',
|
|
154
|
-
range:
|
|
155
|
-
min:
|
|
156
|
-
max:
|
|
168
|
+
const strafeIntensity = useParam('strafeIntensity', 1, {
|
|
169
|
+
range: 0.4,
|
|
170
|
+
min: 0.3,
|
|
171
|
+
max: 1,
|
|
157
172
|
steps: 7,
|
|
158
173
|
});
|
|
159
|
-
const approachSpeed = useParam('approachSpeed',
|
|
160
|
-
range:
|
|
161
|
-
min:
|
|
162
|
-
max:
|
|
174
|
+
const approachSpeed = useParam('approachSpeed', 0.53, {
|
|
175
|
+
range: 0.3,
|
|
176
|
+
min: 0.1,
|
|
177
|
+
max: 0.7,
|
|
163
178
|
steps: 7,
|
|
164
179
|
});
|
|
165
|
-
const distanceDeadZone = useParam('distanceDeadZone',
|
|
180
|
+
const distanceDeadZone = useParam('distanceDeadZone', 31, {
|
|
166
181
|
range: 30,
|
|
167
182
|
min: 5,
|
|
168
183
|
max: 80,
|
|
@@ -173,32 +188,47 @@ export const Spellweaver: WizardFunction = ({state}) =>
|
|
|
173
188
|
min: 50,
|
|
174
189
|
max: 300,
|
|
175
190
|
});
|
|
176
|
-
const flightTimeDivisor = useParam('flightTimeDivisor', 2, {
|
|
191
|
+
const flightTimeDivisor = useParam('flightTimeDivisor', 2.1, {
|
|
177
192
|
range: 4,
|
|
178
193
|
min: 2,
|
|
179
194
|
max: 12,
|
|
180
195
|
steps: 5,
|
|
181
196
|
});
|
|
197
|
+
const centerBias = useParam('centerBias', 0.14, {
|
|
198
|
+
range: 0.4,
|
|
199
|
+
min: 0,
|
|
200
|
+
max: 0.8,
|
|
201
|
+
steps: 5,
|
|
202
|
+
});
|
|
203
|
+
const maxBlinkDistance = useParam('maxBlinkDistance', 239, {
|
|
204
|
+
range: 100,
|
|
205
|
+
min: 50,
|
|
206
|
+
max: 300,
|
|
207
|
+
steps: 7,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const distance = distanceTo(position, enemy.position);
|
|
182
211
|
|
|
183
|
-
|
|
184
|
-
if (!enemy)
|
|
212
|
+
function capBlinkDistance(target: {x: number; y: number}): {x: number; y: number}
|
|
185
213
|
{
|
|
186
|
-
|
|
214
|
+
const dx = target.x - position.x;
|
|
215
|
+
const dy = target.y - position.y;
|
|
216
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
217
|
+
if (dist <= maxBlinkDistance) return target;
|
|
218
|
+
const scale = maxBlinkDistance / dist;
|
|
219
|
+
return {x: position.x + dx * scale, y: position.y + dy * scale};
|
|
187
220
|
}
|
|
188
221
|
|
|
189
|
-
const distance = distanceTo(state.position, enemy.position);
|
|
190
|
-
|
|
191
222
|
// Analyze threats using simulation
|
|
192
|
-
const threats = getThreats(state);
|
|
193
223
|
const urgentThreat = getMostUrgentThreat(threats);
|
|
194
224
|
|
|
195
225
|
// Smart strafe
|
|
196
|
-
const dodgeDirection = getDodgeDirectionForMovement(threats,
|
|
226
|
+
const dodgeDirection = getDodgeDirectionForMovement(threats, position);
|
|
197
227
|
const strafe = smartStrafe(
|
|
198
|
-
|
|
228
|
+
position,
|
|
199
229
|
enemy,
|
|
200
230
|
dodgeDirection,
|
|
201
|
-
|
|
231
|
+
tick,
|
|
202
232
|
strafeCyclePeriod,
|
|
203
233
|
strafeIntensity,
|
|
204
234
|
);
|
|
@@ -209,24 +239,32 @@ export const Spellweaver: WizardFunction = ({state}) =>
|
|
|
209
239
|
|
|
210
240
|
if (!dodgeDirection)
|
|
211
241
|
{
|
|
212
|
-
const deltaX = enemy.position.x -
|
|
213
|
-
const deltaY = enemy.position.y -
|
|
242
|
+
const deltaX = enemy.position.x - position.x;
|
|
243
|
+
const deltaY = enemy.position.y - position.y;
|
|
214
244
|
const normalizedDistance = distance > 0 ? distance : 1;
|
|
215
245
|
|
|
216
|
-
if (distance > targetDistance + distanceDeadZone)
|
|
246
|
+
if (distance > targetDistance + distanceDeadZone)
|
|
217
247
|
{
|
|
218
|
-
|
|
219
|
-
|
|
248
|
+
// Approach the center-side of the enemy so knockback pushes toward center, not lava.
|
|
249
|
+
const center = (ARENA_MIN + ARENA_MAX) / 2;
|
|
250
|
+
const toCenterX = center - enemy.position.x;
|
|
251
|
+
const toCenterY = center - enemy.position.y;
|
|
252
|
+
const toCenterDist = Math.sqrt(toCenterX * toCenterX + toCenterY * toCenterY) || 1;
|
|
253
|
+
const approachDirX = deltaX / normalizedDistance + (toCenterX / toCenterDist) * centerBias;
|
|
254
|
+
const approachDirY = deltaY / normalizedDistance + (toCenterY / toCenterDist) * centerBias;
|
|
255
|
+
const approachMag = Math.sqrt(approachDirX * approachDirX + approachDirY * approachDirY) || 1;
|
|
256
|
+
moveX += (approachDirX / approachMag) * approachSpeed;
|
|
257
|
+
moveY += (approachDirY / approachMag) * approachSpeed;
|
|
220
258
|
}
|
|
221
259
|
else if (distance < targetDistance - distanceDeadZone)
|
|
222
260
|
{
|
|
223
261
|
let retreatX = -(deltaX / normalizedDistance) * approachSpeed;
|
|
224
262
|
let retreatY = -(deltaY / normalizedDistance) * approachSpeed;
|
|
225
|
-
if (
|
|
226
|
-
if (
|
|
263
|
+
if (position.x < ARENA_MIN + WALL_BUFFER && retreatX < 0) retreatX = 0;
|
|
264
|
+
if (position.x > ARENA_MAX - WALL_BUFFER && retreatX > 0)
|
|
227
265
|
retreatX = 0;
|
|
228
|
-
if (
|
|
229
|
-
if (
|
|
266
|
+
if (position.y < ARENA_MIN + WALL_BUFFER && retreatY < 0) retreatY = 0;
|
|
267
|
+
if (position.y > ARENA_MAX - WALL_BUFFER && retreatY > 0)
|
|
230
268
|
retreatY = 0;
|
|
231
269
|
moveX += retreatX;
|
|
232
270
|
moveY += retreatY;
|
|
@@ -234,46 +272,47 @@ export const Spellweaver: WizardFunction = ({state}) =>
|
|
|
234
272
|
}
|
|
235
273
|
|
|
236
274
|
const magnitude = Math.sqrt(moveX * moveX + moveY * moveY);
|
|
237
|
-
if (magnitude >
|
|
275
|
+
if (magnitude > 1)
|
|
238
276
|
{
|
|
239
|
-
moveX =
|
|
240
|
-
moveY =
|
|
277
|
+
moveX = moveX / magnitude;
|
|
278
|
+
moveY = moveY / magnitude;
|
|
241
279
|
}
|
|
242
280
|
|
|
243
281
|
const move = {x: moveX, y: moveY};
|
|
282
|
+
const safeMove = keepOffWalls(position, move);
|
|
244
283
|
|
|
245
284
|
// === DEFENSE: Handle channeling ===
|
|
246
|
-
if (
|
|
285
|
+
if (status === 'channeling')
|
|
247
286
|
{
|
|
248
287
|
if (!urgentThreat || urgentThreat.ticksToImpact > shieldCancelThreshold)
|
|
249
288
|
{
|
|
250
|
-
return
|
|
289
|
+
return cancel().move(safeMove.x, safeMove.y);
|
|
251
290
|
}
|
|
252
|
-
return
|
|
291
|
+
return idle();
|
|
253
292
|
}
|
|
254
293
|
|
|
255
294
|
// === DEFENSE: Cancel missile cast only for LETHAL incoming damage ===
|
|
256
295
|
// DPS trade: we accepted the hit when we started casting. Only cancel to survive.
|
|
257
296
|
if (
|
|
258
|
-
|
|
259
|
-
|
|
297
|
+
status === 'casting' &&
|
|
298
|
+
castingSpell === 'missile' &&
|
|
260
299
|
urgentThreat &&
|
|
261
|
-
urgentThreat.projectile.damage >=
|
|
300
|
+
urgentThreat.projectile.damage >= health
|
|
262
301
|
)
|
|
263
302
|
{
|
|
264
|
-
const remainingCast = (
|
|
303
|
+
const remainingCast = ((castProgressData?.total) ?? 0) - ((castProgressData?.current) ?? 0);
|
|
265
304
|
if (
|
|
266
305
|
urgentThreat.ticksToImpact <=
|
|
267
|
-
remainingCast + GCD_DURATION + SHIELD_CAST_TIME + SHIELD_BUFFER
|
|
306
|
+
remainingCast + RULES.GCD_DURATION + RULES.SHIELD_CAST_TIME + SHIELD_BUFFER
|
|
268
307
|
)
|
|
269
308
|
{
|
|
270
|
-
return
|
|
309
|
+
return cancel().move(safeMove.x, safeMove.y);
|
|
271
310
|
}
|
|
272
311
|
}
|
|
273
312
|
|
|
274
313
|
// === DEFENSE: Shield undodgeable or high-damage homing threats ===
|
|
275
314
|
if (
|
|
276
|
-
|
|
315
|
+
status === 'idle' &&
|
|
277
316
|
urgentThreat &&
|
|
278
317
|
(!urgentThreat.bestDodgeDirection || shouldForceShield(urgentThreat))
|
|
279
318
|
)
|
|
@@ -282,60 +321,77 @@ export const Spellweaver: WizardFunction = ({state}) =>
|
|
|
282
321
|
{
|
|
283
322
|
if (urgentThreat.ticksToStartShield <= SHIELD_BUFFER)
|
|
284
323
|
{
|
|
285
|
-
return
|
|
286
|
-
move: {x: 0, y: 0},
|
|
287
|
-
startCast: {spell: 'shield'},
|
|
288
|
-
};
|
|
324
|
+
return shield();
|
|
289
325
|
}
|
|
290
326
|
}
|
|
291
|
-
else if (
|
|
327
|
+
else if (blinkCooldown === 0)
|
|
328
|
+
{
|
|
329
|
+
const blinkTarget = capBlinkDistance(getBlinkToCenter(position));
|
|
330
|
+
return blink(blinkTarget.x, blinkTarget.y);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// === Preemptive shield — shield when enemy is casting a missile at close range ===
|
|
335
|
+
if (
|
|
336
|
+
status === 'idle' &&
|
|
337
|
+
distance < 300 &&
|
|
338
|
+
enemy.status === 'casting' &&
|
|
339
|
+
enemy.castingSpell === 'missile'
|
|
340
|
+
)
|
|
341
|
+
{
|
|
342
|
+
const remainingCast = (enemy.castDuration ?? 0) - (enemy.castProgress ?? 0);
|
|
343
|
+
const estimatedFlightTicks = distance / 8;
|
|
344
|
+
const ticksUntilHit = remainingCast + estimatedFlightTicks;
|
|
345
|
+
|
|
346
|
+
if (
|
|
347
|
+
ticksUntilHit >= RULES.SHIELD_CAST_TIME &&
|
|
348
|
+
ticksUntilHit < RULES.SHIELD_CAST_TIME + 15
|
|
349
|
+
)
|
|
292
350
|
{
|
|
293
|
-
return
|
|
294
|
-
move: {x: 0, y: 0},
|
|
295
|
-
startCast: {spell: 'blink', target: getBlinkToCenter(state.position)},
|
|
296
|
-
};
|
|
351
|
+
return shield();
|
|
297
352
|
}
|
|
298
353
|
}
|
|
299
354
|
|
|
300
355
|
// === DISTANCE BLINK: enemy too close, blink to reestablish kiting range ===
|
|
301
356
|
if (
|
|
302
|
-
|
|
357
|
+
status === 'idle' &&
|
|
303
358
|
distance < dangerDistance &&
|
|
304
|
-
|
|
359
|
+
blinkCooldown === 0 &&
|
|
305
360
|
(!urgentThreat || urgentThreat.bestDodgeDirection !== null)
|
|
306
361
|
)
|
|
307
362
|
{
|
|
308
363
|
const blinkTarget = getBlinkToIncreaseDistance(
|
|
309
|
-
|
|
364
|
+
position,
|
|
310
365
|
enemy.position,
|
|
311
|
-
|
|
366
|
+
projectiles,
|
|
312
367
|
MIN_BLINK_ESCAPE_DISTANCE,
|
|
313
368
|
);
|
|
314
|
-
if (blinkTarget)
|
|
369
|
+
if (blinkTarget)
|
|
315
370
|
{
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
startCast: {spell: 'blink', target: blinkTarget},
|
|
319
|
-
};
|
|
371
|
+
const capped = capBlinkDistance(blinkTarget);
|
|
372
|
+
return blink(capped.x, capped.y);
|
|
320
373
|
}
|
|
321
374
|
}
|
|
322
375
|
|
|
323
376
|
// === OFFENSE: Adaptive missile fitting ===
|
|
324
377
|
// Skip offense when a lethal missile could reach us during cast+GCD — dodge at full speed instead
|
|
325
|
-
const myProjectileIds = new Set(
|
|
326
|
-
const hasNearbyLethalMissile =
|
|
378
|
+
const myProjectileIds = new Set(myProjectiles.map((p) => p.id));
|
|
379
|
+
const hasNearbyLethalMissile = projectiles.some(
|
|
327
380
|
(p) =>
|
|
328
381
|
!myProjectileIds.has(p.id) &&
|
|
329
|
-
p.damage >=
|
|
330
|
-
distanceTo(
|
|
331
|
-
MAX_CAST_BUDGET + GCD_DURATION,
|
|
382
|
+
p.damage >= health &&
|
|
383
|
+
distanceTo(position, p.position) / p.speed <
|
|
384
|
+
MAX_CAST_BUDGET + RULES.GCD_DURATION,
|
|
332
385
|
);
|
|
333
|
-
if (
|
|
386
|
+
if (status === 'idle' && distance < 600 && !hasNearbyLethalMissile)
|
|
334
387
|
{
|
|
388
|
+
const reposition = getPreCastReposition(position, enemy.position);
|
|
389
|
+
if (reposition) return moveAction(reposition.x, reposition.y);
|
|
390
|
+
|
|
335
391
|
let nextThreatTime = urgentThreat?.ticksToImpact ?? Infinity;
|
|
336
392
|
|
|
337
393
|
// Account for enemy's active cast
|
|
338
|
-
if (enemy.
|
|
394
|
+
if (enemy.status === 'casting' && enemy.castingSpell === 'missile')
|
|
339
395
|
{
|
|
340
396
|
const remainingEnemyCast =
|
|
341
397
|
(enemy.castDuration ?? 0) - (enemy.castProgress ?? 0);
|
|
@@ -346,7 +402,7 @@ export const Spellweaver: WizardFunction = ({state}) =>
|
|
|
346
402
|
);
|
|
347
403
|
}
|
|
348
404
|
const safeBudget =
|
|
349
|
-
nextThreatTime - GCD_DURATION - SHIELD_CAST_TIME - SHIELD_BUFFER;
|
|
405
|
+
nextThreatTime - RULES.GCD_DURATION - RULES.SHIELD_CAST_TIME - SHIELD_BUFFER;
|
|
350
406
|
const budgetTicks = Math.min(safeBudget, MAX_CAST_BUDGET);
|
|
351
407
|
|
|
352
408
|
// Spellweaver always uses homing (kiting means enemies are always moving)
|
|
@@ -355,24 +411,16 @@ export const Spellweaver: WizardFunction = ({state}) =>
|
|
|
355
411
|
const config = fitMissileToBudget(budgetTicks, distance, {
|
|
356
412
|
minTurnRate,
|
|
357
413
|
maxDamage: enemyHP,
|
|
358
|
-
lastMissileConfig:
|
|
414
|
+
lastMissileConfig: lastMissileConfig,
|
|
359
415
|
});
|
|
360
416
|
|
|
361
417
|
if (config && config.damage >= minDmg)
|
|
362
418
|
{
|
|
363
|
-
return
|
|
364
|
-
move,
|
|
365
|
-
startCast: {
|
|
366
|
-
spell: 'missile',
|
|
367
|
-
config,
|
|
368
|
-
missileAI: HomingAI,
|
|
369
|
-
direction: angleTo(state.position, enemy.position),
|
|
370
|
-
},
|
|
371
|
-
};
|
|
419
|
+
return missile(config, HomingAI, angleTo(position, enemy.position)).move(safeMove.x, safeMove.y);
|
|
372
420
|
}
|
|
373
421
|
|
|
374
422
|
// Budget too small for useful missile — wait for threat to pass, then fire
|
|
375
423
|
}
|
|
376
424
|
|
|
377
|
-
return
|
|
378
|
-
}
|
|
425
|
+
return moveAction(safeMove.x, safeMove.y);
|
|
426
|
+
}
|