@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,10 +1,10 @@
|
|
|
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 {getMissileCastTime, getLeadPosition} from '../../utils/combat.js';
|
|
6
7
|
import {
|
|
7
|
-
getThreats,
|
|
8
8
|
getMostUrgentThreat,
|
|
9
9
|
getDodgeDirectionForMovement,
|
|
10
10
|
getBlinkToCenter,
|
|
@@ -15,6 +15,8 @@ import {
|
|
|
15
15
|
MAX_CAST_BUDGET,
|
|
16
16
|
MIN_USEFUL_DAMAGE,
|
|
17
17
|
getEnemyVulnerabilityTicks,
|
|
18
|
+
keepOffWalls,
|
|
19
|
+
getPreCastReposition,
|
|
18
20
|
} from '../shared.js';
|
|
19
21
|
import {useParam} from '../../engine/params-runtime.js';
|
|
20
22
|
const WALL_BUFFER = 60;
|
|
@@ -24,18 +26,17 @@ const SHIELD_BUFFER = 3;
|
|
|
24
26
|
/**
|
|
25
27
|
* Standard homing missile AI.
|
|
26
28
|
*/
|
|
27
|
-
const HomingAI:
|
|
29
|
+
const HomingAI: MissileFunction = () =>
|
|
28
30
|
{
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
};
|
|
31
|
+
const ctx = getMissileContext();
|
|
32
|
+
const enemy = ctx.worldState.enemies[0];
|
|
33
|
+
return enemy ? turnToward(enemy.position.x, enemy.position.y) : flyStraight();
|
|
33
34
|
};
|
|
34
35
|
|
|
35
36
|
/**
|
|
36
37
|
* Straight missile AI — no homing, flies in initial direction.
|
|
37
38
|
*/
|
|
38
|
-
const StraightAI:
|
|
39
|
+
const StraightAI: MissileFunction = () => flyStraight();
|
|
39
40
|
|
|
40
41
|
/**
|
|
41
42
|
* Check if near wall.
|
|
@@ -43,10 +44,10 @@ const StraightAI: MissileAIFunction = () => ({});
|
|
|
43
44
|
function isNearWall(position: {x: number; y: number}): boolean
|
|
44
45
|
{
|
|
45
46
|
return (
|
|
46
|
-
position.x < WALL_BUFFER ||
|
|
47
|
-
position.x >
|
|
48
|
-
position.y < WALL_BUFFER ||
|
|
49
|
-
position.y >
|
|
47
|
+
position.x < ARENA_MIN + WALL_BUFFER ||
|
|
48
|
+
position.x > ARENA_MAX - WALL_BUFFER ||
|
|
49
|
+
position.y < ARENA_MIN + WALL_BUFFER ||
|
|
50
|
+
position.y > ARENA_MAX - WALL_BUFFER
|
|
50
51
|
);
|
|
51
52
|
}
|
|
52
53
|
|
|
@@ -93,16 +94,16 @@ function smartStrafe(
|
|
|
93
94
|
y: position.y + strafeCounterClockwise.y * 100,
|
|
94
95
|
};
|
|
95
96
|
const scoreClockwise = Math.min(
|
|
96
|
-
futureClockwise.x,
|
|
97
|
-
|
|
98
|
-
futureClockwise.y,
|
|
99
|
-
|
|
97
|
+
futureClockwise.x - ARENA_MIN,
|
|
98
|
+
ARENA_MAX - futureClockwise.x,
|
|
99
|
+
futureClockwise.y - ARENA_MIN,
|
|
100
|
+
ARENA_MAX - futureClockwise.y,
|
|
100
101
|
);
|
|
101
102
|
const scoreCounterClockwise = Math.min(
|
|
102
|
-
futureCounterClockwise.x,
|
|
103
|
-
|
|
104
|
-
futureCounterClockwise.y,
|
|
105
|
-
|
|
103
|
+
futureCounterClockwise.x - ARENA_MIN,
|
|
104
|
+
ARENA_MAX - futureCounterClockwise.x,
|
|
105
|
+
futureCounterClockwise.y - ARENA_MIN,
|
|
106
|
+
ARENA_MAX - futureCounterClockwise.y,
|
|
106
107
|
);
|
|
107
108
|
strafeDir =
|
|
108
109
|
scoreClockwise > scoreCounterClockwise
|
|
@@ -115,7 +116,7 @@ function smartStrafe(
|
|
|
115
116
|
strafeDir = cycle === 0 ? strafeClockwise : strafeCounterClockwise;
|
|
116
117
|
}
|
|
117
118
|
|
|
118
|
-
const intensity = dodgeDirection ?
|
|
119
|
+
const intensity = dodgeDirection ? 1 : strafeIntensity;
|
|
119
120
|
return {x: strafeDir.x * intensity, y: strafeDir.y * intensity};
|
|
120
121
|
}
|
|
121
122
|
|
|
@@ -141,35 +142,48 @@ function smartStrafe(
|
|
|
141
142
|
*
|
|
142
143
|
* TIER: 3 (elite Spellspinner line)
|
|
143
144
|
*/
|
|
144
|
-
export
|
|
145
|
+
export function Spellbinder(): FinalAction
|
|
145
146
|
{
|
|
146
|
-
const
|
|
147
|
+
const position = usePosition();
|
|
148
|
+
const enemy = useEnemy();
|
|
149
|
+
const status = useStatus();
|
|
150
|
+
const blinkCooldown = useBlinkCooldown();
|
|
151
|
+
const health = useHealth();
|
|
152
|
+
const tick = useTick();
|
|
153
|
+
const threats = useThreats();
|
|
154
|
+
const projectiles = useProjectiles();
|
|
155
|
+
const myProjectiles = useMyProjectiles();
|
|
156
|
+
const lastMissileConfig = useLastMissileConfig();
|
|
157
|
+
const castingSpell = useCastingSpell();
|
|
158
|
+
const castProgressData = useCastProgress();
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
const targetDistance = useParam('targetDistance', 197, {
|
|
147
162
|
range: 150,
|
|
148
163
|
min: 0,
|
|
149
164
|
});
|
|
150
|
-
const dangerDistance = useParam('dangerDistance',
|
|
165
|
+
const dangerDistance = useParam('dangerDistance', 171, {
|
|
151
166
|
range: 125,
|
|
152
167
|
min: 0,
|
|
153
168
|
});
|
|
154
|
-
const
|
|
155
|
-
const strafeCyclePeriod = useParam('strafeCyclePeriod', 123, {
|
|
169
|
+
const strafeCyclePeriod = useParam('strafeCyclePeriod', 179, {
|
|
156
170
|
range: 75,
|
|
157
171
|
min: 80,
|
|
158
172
|
max: 300,
|
|
159
173
|
});
|
|
160
|
-
const strafeIntensity = useParam('strafeIntensity',
|
|
161
|
-
range:
|
|
162
|
-
min:
|
|
163
|
-
max:
|
|
174
|
+
const strafeIntensity = useParam('strafeIntensity', 1, {
|
|
175
|
+
range: 0.4,
|
|
176
|
+
min: 0.3,
|
|
177
|
+
max: 1,
|
|
164
178
|
steps: 7,
|
|
165
179
|
});
|
|
166
|
-
const approachSpeed = useParam('approachSpeed',
|
|
167
|
-
range:
|
|
168
|
-
min:
|
|
169
|
-
max:
|
|
180
|
+
const approachSpeed = useParam('approachSpeed', 0.4, {
|
|
181
|
+
range: 0.3,
|
|
182
|
+
min: 0.1,
|
|
183
|
+
max: 0.7,
|
|
170
184
|
steps: 7,
|
|
171
185
|
});
|
|
172
|
-
const distanceDeadZone = useParam('distanceDeadZone',
|
|
186
|
+
const distanceDeadZone = useParam('distanceDeadZone', 71, {
|
|
173
187
|
range: 30,
|
|
174
188
|
min: 5,
|
|
175
189
|
max: 80,
|
|
@@ -180,37 +194,52 @@ export const Spellbinder: WizardFunction = ({state}) =>
|
|
|
180
194
|
min: 50,
|
|
181
195
|
max: 300,
|
|
182
196
|
});
|
|
183
|
-
const flightTimeDivisor = useParam('flightTimeDivisor', 10
|
|
197
|
+
const flightTimeDivisor = useParam('flightTimeDivisor', 10, {
|
|
184
198
|
range: 4,
|
|
185
199
|
min: 2,
|
|
186
200
|
max: 12,
|
|
187
201
|
steps: 5,
|
|
188
202
|
});
|
|
189
|
-
const punishMinVulnerability = useParam('punishMinVulnerability',
|
|
203
|
+
const punishMinVulnerability = useParam('punishMinVulnerability', 91, {
|
|
190
204
|
range: 80,
|
|
191
205
|
min: 30,
|
|
192
206
|
steps: 7,
|
|
193
207
|
});
|
|
208
|
+
const centerBias = useParam('centerBias', 0.39, {
|
|
209
|
+
range: 0.4,
|
|
210
|
+
min: 0,
|
|
211
|
+
max: 0.8,
|
|
212
|
+
steps: 5,
|
|
213
|
+
});
|
|
214
|
+
const maxBlinkDistance = useParam('maxBlinkDistance', 258, {
|
|
215
|
+
range: 100,
|
|
216
|
+
min: 50,
|
|
217
|
+
max: 300,
|
|
218
|
+
steps: 7,
|
|
219
|
+
});
|
|
194
220
|
|
|
195
|
-
const
|
|
196
|
-
|
|
221
|
+
const distance = distanceTo(position, enemy.position);
|
|
222
|
+
|
|
223
|
+
function capBlinkDistance(target: {x: number; y: number}): {x: number; y: number}
|
|
197
224
|
{
|
|
198
|
-
|
|
225
|
+
const dx = target.x - position.x;
|
|
226
|
+
const dy = target.y - position.y;
|
|
227
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
228
|
+
if (dist <= maxBlinkDistance) return target;
|
|
229
|
+
const scale = maxBlinkDistance / dist;
|
|
230
|
+
return {x: position.x + dx * scale, y: position.y + dy * scale};
|
|
199
231
|
}
|
|
200
232
|
|
|
201
|
-
const distance = distanceTo(state.position, enemy.position);
|
|
202
|
-
|
|
203
233
|
// Analyze threats using simulation
|
|
204
|
-
const threats = getThreats(state);
|
|
205
234
|
const urgentThreat = getMostUrgentThreat(threats);
|
|
206
235
|
|
|
207
236
|
// Smart strafe
|
|
208
|
-
const dodgeDirection = getDodgeDirectionForMovement(threats,
|
|
237
|
+
const dodgeDirection = getDodgeDirectionForMovement(threats, position);
|
|
209
238
|
const strafe = smartStrafe(
|
|
210
|
-
|
|
239
|
+
position,
|
|
211
240
|
enemy,
|
|
212
241
|
dodgeDirection,
|
|
213
|
-
|
|
242
|
+
tick,
|
|
214
243
|
strafeCyclePeriod,
|
|
215
244
|
strafeIntensity,
|
|
216
245
|
);
|
|
@@ -221,24 +250,32 @@ export const Spellbinder: WizardFunction = ({state}) =>
|
|
|
221
250
|
|
|
222
251
|
if (!dodgeDirection)
|
|
223
252
|
{
|
|
224
|
-
const deltaX = enemy.position.x -
|
|
225
|
-
const deltaY = enemy.position.y -
|
|
253
|
+
const deltaX = enemy.position.x - position.x;
|
|
254
|
+
const deltaY = enemy.position.y - position.y;
|
|
226
255
|
const normalizedDistance = distance > 0 ? distance : 1;
|
|
227
256
|
|
|
228
257
|
if (distance > targetDistance + distanceDeadZone)
|
|
229
258
|
{
|
|
230
|
-
|
|
231
|
-
|
|
259
|
+
// Approach the center-side of the enemy so knockback pushes toward center, not lava.
|
|
260
|
+
const center = (ARENA_MIN + ARENA_MAX) / 2;
|
|
261
|
+
const toCenterX = center - enemy.position.x;
|
|
262
|
+
const toCenterY = center - enemy.position.y;
|
|
263
|
+
const toCenterDist = Math.sqrt(toCenterX * toCenterX + toCenterY * toCenterY) || 1;
|
|
264
|
+
const approachDirX = deltaX / normalizedDistance + (toCenterX / toCenterDist) * centerBias;
|
|
265
|
+
const approachDirY = deltaY / normalizedDistance + (toCenterY / toCenterDist) * centerBias;
|
|
266
|
+
const approachMag = Math.sqrt(approachDirX * approachDirX + approachDirY * approachDirY) || 1;
|
|
267
|
+
moveX += (approachDirX / approachMag) * approachSpeed;
|
|
268
|
+
moveY += (approachDirY / approachMag) * approachSpeed;
|
|
232
269
|
}
|
|
233
270
|
else if (distance < targetDistance - distanceDeadZone)
|
|
234
271
|
{
|
|
235
272
|
let retreatX = -(deltaX / normalizedDistance) * approachSpeed;
|
|
236
273
|
let retreatY = -(deltaY / normalizedDistance) * approachSpeed;
|
|
237
|
-
if (
|
|
238
|
-
if (
|
|
274
|
+
if (position.x < ARENA_MIN + WALL_BUFFER && retreatX < 0) retreatX = 0;
|
|
275
|
+
if (position.x > ARENA_MAX - WALL_BUFFER && retreatX > 0)
|
|
239
276
|
retreatX = 0;
|
|
240
|
-
if (
|
|
241
|
-
if (
|
|
277
|
+
if (position.y < ARENA_MIN + WALL_BUFFER && retreatY < 0) retreatY = 0;
|
|
278
|
+
if (position.y > ARENA_MAX - WALL_BUFFER && retreatY > 0)
|
|
242
279
|
retreatY = 0;
|
|
243
280
|
moveX += retreatX;
|
|
244
281
|
moveY += retreatY;
|
|
@@ -246,95 +283,119 @@ export const Spellbinder: WizardFunction = ({state}) =>
|
|
|
246
283
|
}
|
|
247
284
|
|
|
248
285
|
const magnitude = Math.sqrt(moveX * moveX + moveY * moveY);
|
|
249
|
-
if (magnitude >
|
|
286
|
+
if (magnitude > 1)
|
|
250
287
|
{
|
|
251
|
-
moveX =
|
|
252
|
-
moveY =
|
|
288
|
+
moveX = moveX / magnitude;
|
|
289
|
+
moveY = moveY / magnitude;
|
|
253
290
|
}
|
|
254
291
|
|
|
255
292
|
const move = {x: moveX, y: moveY};
|
|
293
|
+
const safeMove = keepOffWalls(position, move);
|
|
256
294
|
|
|
257
295
|
// === DEFENSE: Handle channeling ===
|
|
258
|
-
if (
|
|
296
|
+
if (status === 'channeling')
|
|
259
297
|
{
|
|
260
298
|
if (!urgentThreat || urgentThreat.ticksToImpact > shieldCancelThreshold)
|
|
261
299
|
{
|
|
262
|
-
return
|
|
300
|
+
return cancel().move(safeMove.x, safeMove.y);
|
|
263
301
|
}
|
|
264
|
-
return
|
|
302
|
+
return idle();
|
|
265
303
|
}
|
|
266
304
|
|
|
267
305
|
// === DEFENSE: Cancel missile cast only for LETHAL incoming damage ===
|
|
268
306
|
// DPS trade: we accepted the hit when we started casting. Only cancel to survive.
|
|
269
307
|
if (
|
|
270
|
-
|
|
271
|
-
|
|
308
|
+
status === 'casting' &&
|
|
309
|
+
castingSpell === 'missile' &&
|
|
272
310
|
urgentThreat &&
|
|
273
|
-
urgentThreat.projectile.damage >=
|
|
311
|
+
urgentThreat.projectile.damage >= health
|
|
274
312
|
)
|
|
275
313
|
{
|
|
276
|
-
const remainingCast = (
|
|
314
|
+
const remainingCast = ((castProgressData?.total) ?? 0) - ((castProgressData?.current) ?? 0);
|
|
277
315
|
if (
|
|
278
316
|
urgentThreat.ticksToImpact <=
|
|
279
|
-
remainingCast + GCD_DURATION + SHIELD_CAST_TIME + SHIELD_BUFFER
|
|
317
|
+
remainingCast + RULES.GCD_DURATION + RULES.SHIELD_CAST_TIME + SHIELD_BUFFER
|
|
280
318
|
)
|
|
281
319
|
{
|
|
282
|
-
return
|
|
320
|
+
return cancel().move(safeMove.x, safeMove.y);
|
|
283
321
|
}
|
|
284
322
|
}
|
|
285
323
|
|
|
286
324
|
// === DEFENSE: Shield undodgeable or high-damage homing threats ===
|
|
287
325
|
if (
|
|
288
|
-
|
|
326
|
+
status === 'idle' &&
|
|
289
327
|
urgentThreat &&
|
|
290
328
|
(!urgentThreat.bestDodgeDirection || shouldForceShield(urgentThreat))
|
|
291
329
|
)
|
|
292
330
|
{
|
|
331
|
+
if (urgentThreat.canBlockInTime && urgentThreat.ticksToStartShield <= SHIELD_BUFFER)
|
|
332
|
+
{
|
|
333
|
+
return shield();
|
|
334
|
+
}
|
|
335
|
+
if (blinkCooldown === 0)
|
|
336
|
+
{
|
|
337
|
+
const blinkTarget = capBlinkDistance(getBlinkToCenter(position));
|
|
338
|
+
return blink(blinkTarget.x, blinkTarget.y);
|
|
339
|
+
}
|
|
293
340
|
if (urgentThreat.canBlockInTime)
|
|
294
341
|
{
|
|
295
|
-
|
|
296
|
-
{
|
|
297
|
-
return {
|
|
298
|
-
move: {x: 0, y: 0},
|
|
299
|
-
startCast: {spell: 'shield'},
|
|
300
|
-
};
|
|
301
|
-
}
|
|
342
|
+
return shield();
|
|
302
343
|
}
|
|
303
|
-
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// === Preemptive shield — shield when enemy is casting a missile at close range ===
|
|
347
|
+
if (
|
|
348
|
+
status === 'idle' &&
|
|
349
|
+
distance < 300 &&
|
|
350
|
+
enemy.status === 'casting' &&
|
|
351
|
+
enemy.castingSpell === 'missile'
|
|
352
|
+
)
|
|
353
|
+
{
|
|
354
|
+
const remainingCast = (enemy.castDuration ?? 0) - (enemy.castProgress ?? 0);
|
|
355
|
+
const estimatedFlightTicks = distance / 8;
|
|
356
|
+
const ticksUntilHit = remainingCast + estimatedFlightTicks;
|
|
357
|
+
|
|
358
|
+
if (
|
|
359
|
+
ticksUntilHit >= RULES.SHIELD_CAST_TIME &&
|
|
360
|
+
ticksUntilHit < RULES.SHIELD_CAST_TIME + 15
|
|
361
|
+
)
|
|
304
362
|
{
|
|
305
|
-
return
|
|
306
|
-
move: {x: 0, y: 0},
|
|
307
|
-
startCast: {spell: 'blink', target: getBlinkToCenter(state.position)},
|
|
308
|
-
};
|
|
363
|
+
return shield();
|
|
309
364
|
}
|
|
310
365
|
}
|
|
311
366
|
|
|
312
367
|
// === DISTANCE BLINK: enemy too close, blink to reestablish kiting range ===
|
|
313
368
|
if (
|
|
314
|
-
|
|
369
|
+
status === 'idle' &&
|
|
315
370
|
distance < dangerDistance &&
|
|
316
|
-
|
|
371
|
+
blinkCooldown === 0 &&
|
|
317
372
|
(!urgentThreat || urgentThreat.bestDodgeDirection !== null)
|
|
318
373
|
)
|
|
319
374
|
{
|
|
320
375
|
const blinkTarget = getBlinkToIncreaseDistance(
|
|
321
|
-
|
|
376
|
+
position,
|
|
322
377
|
enemy.position,
|
|
323
|
-
|
|
378
|
+
projectiles,
|
|
324
379
|
MIN_BLINK_ESCAPE_DISTANCE,
|
|
325
380
|
);
|
|
326
381
|
if (blinkTarget)
|
|
327
382
|
{
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
startCast: {spell: 'blink', target: blinkTarget},
|
|
331
|
-
};
|
|
383
|
+
const capped = capBlinkDistance(blinkTarget);
|
|
384
|
+
return blink(capped.x, capped.y);
|
|
332
385
|
}
|
|
333
386
|
}
|
|
334
387
|
|
|
388
|
+
// Pre-cast wall discipline: don't commit to a cast we can't react out of while
|
|
389
|
+
// pinned near a wall — step toward center first so knockback can't reach lava.
|
|
390
|
+
if (status === 'idle')
|
|
391
|
+
{
|
|
392
|
+
const reposition = getPreCastReposition(position, enemy.position);
|
|
393
|
+
if (reposition) return moveAction(reposition.x, reposition.y);
|
|
394
|
+
}
|
|
395
|
+
|
|
335
396
|
// === OFFENSE: Punish mode — fast straight missiles during enemy vulnerability ===
|
|
336
397
|
if (
|
|
337
|
-
|
|
398
|
+
status === 'idle' &&
|
|
338
399
|
getEnemyVulnerabilityTicks(enemy) >= punishMinVulnerability &&
|
|
339
400
|
distance < 500
|
|
340
401
|
)
|
|
@@ -344,7 +405,7 @@ export const Spellbinder: WizardFunction = ({state}) =>
|
|
|
344
405
|
const punishBudget = vulnerabilityTicks - flightTimeEstimate;
|
|
345
406
|
const nextThreatTime = urgentThreat?.ticksToImpact ?? Infinity;
|
|
346
407
|
const safeBudget =
|
|
347
|
-
nextThreatTime - GCD_DURATION - SHIELD_CAST_TIME - SHIELD_BUFFER;
|
|
408
|
+
nextThreatTime - RULES.GCD_DURATION - RULES.SHIELD_CAST_TIME - SHIELD_BUFFER;
|
|
348
409
|
const effectiveBudget = Math.min(
|
|
349
410
|
punishBudget,
|
|
350
411
|
safeBudget,
|
|
@@ -357,12 +418,12 @@ export const Spellbinder: WizardFunction = ({state}) =>
|
|
|
357
418
|
const config = fitMissileToBudget(effectiveBudget, distance, {
|
|
358
419
|
minTurnRate: 0,
|
|
359
420
|
maxDamage: enemyHP,
|
|
360
|
-
lastMissileConfig:
|
|
421
|
+
lastMissileConfig: lastMissileConfig,
|
|
361
422
|
});
|
|
362
423
|
|
|
363
424
|
if (config && config.damage >= MIN_USEFUL_DAMAGE)
|
|
364
425
|
{
|
|
365
|
-
const castTime = getMissileCastTime(config,
|
|
426
|
+
const castTime = getMissileCastTime(config, lastMissileConfig);
|
|
366
427
|
const actualFlight = distance / config.speed;
|
|
367
428
|
|
|
368
429
|
if (castTime + actualFlight < vulnerabilityTicks)
|
|
@@ -374,17 +435,9 @@ export const Spellbinder: WizardFunction = ({state}) =>
|
|
|
374
435
|
enemy.position,
|
|
375
436
|
enemy.velocity,
|
|
376
437
|
config.speed,
|
|
377
|
-
|
|
438
|
+
position,
|
|
378
439
|
);
|
|
379
|
-
return
|
|
380
|
-
move,
|
|
381
|
-
startCast: {
|
|
382
|
-
spell: 'missile',
|
|
383
|
-
config,
|
|
384
|
-
missileAI: config.turnRate > 0 ? HomingAI : StraightAI,
|
|
385
|
-
direction: angleTo(state.position, aimTarget),
|
|
386
|
-
},
|
|
387
|
-
};
|
|
440
|
+
return missile(config, config.turnRate > 0 ? HomingAI : StraightAI, angleTo(position, aimTarget)).move(safeMove.x, safeMove.y);
|
|
388
441
|
}
|
|
389
442
|
}
|
|
390
443
|
}
|
|
@@ -392,20 +445,20 @@ export const Spellbinder: WizardFunction = ({state}) =>
|
|
|
392
445
|
|
|
393
446
|
// === OFFENSE: Adaptive missile fitting (identical to Spellweaver) ===
|
|
394
447
|
// Skip offense when a lethal missile could reach us during cast+GCD — dodge at full speed instead
|
|
395
|
-
const myProjectileIds = new Set(
|
|
396
|
-
const hasNearbyLethalMissile =
|
|
448
|
+
const myProjectileIds = new Set(myProjectiles.map((p) => p.id));
|
|
449
|
+
const hasNearbyLethalMissile = projectiles.some(
|
|
397
450
|
(p) =>
|
|
398
451
|
!myProjectileIds.has(p.id) &&
|
|
399
|
-
p.damage >=
|
|
400
|
-
distanceTo(
|
|
401
|
-
MAX_CAST_BUDGET + GCD_DURATION,
|
|
452
|
+
p.damage >= health &&
|
|
453
|
+
distanceTo(position, p.position) / p.speed <
|
|
454
|
+
MAX_CAST_BUDGET + RULES.GCD_DURATION,
|
|
402
455
|
);
|
|
403
|
-
if (
|
|
456
|
+
if (status === 'idle' && distance < 600 && !hasNearbyLethalMissile)
|
|
404
457
|
{
|
|
405
458
|
let nextThreatTime = urgentThreat?.ticksToImpact ?? Infinity;
|
|
406
459
|
|
|
407
460
|
// Account for enemy's active cast
|
|
408
|
-
if (enemy.
|
|
461
|
+
if (enemy.status === 'casting' && enemy.castingSpell === 'missile')
|
|
409
462
|
{
|
|
410
463
|
const remainingEnemyCast =
|
|
411
464
|
(enemy.castDuration ?? 0) - (enemy.castProgress ?? 0);
|
|
@@ -416,33 +469,25 @@ export const Spellbinder: WizardFunction = ({state}) =>
|
|
|
416
469
|
);
|
|
417
470
|
}
|
|
418
471
|
const safeBudget =
|
|
419
|
-
nextThreatTime - GCD_DURATION - SHIELD_CAST_TIME - SHIELD_BUFFER;
|
|
472
|
+
nextThreatTime - RULES.GCD_DURATION - RULES.SHIELD_CAST_TIME - SHIELD_BUFFER;
|
|
420
473
|
const budgetTicks = Math.min(safeBudget, MAX_CAST_BUDGET);
|
|
421
474
|
|
|
422
475
|
// Spellbinder always uses homing in standard mode (kiting means enemies are always moving)
|
|
423
476
|
const enemyHP = Math.ceil(enemy.health);
|
|
424
477
|
const minDmg = Math.min(MIN_USEFUL_DAMAGE, enemyHP);
|
|
425
478
|
const config = fitMissileToBudget(budgetTicks, distance, {
|
|
426
|
-
minTurnRate,
|
|
479
|
+
minTurnRate: 0,
|
|
427
480
|
maxDamage: enemyHP,
|
|
428
|
-
lastMissileConfig:
|
|
481
|
+
lastMissileConfig: lastMissileConfig,
|
|
429
482
|
});
|
|
430
483
|
|
|
431
484
|
if (config && config.damage >= minDmg)
|
|
432
485
|
{
|
|
433
|
-
return
|
|
434
|
-
move,
|
|
435
|
-
startCast: {
|
|
436
|
-
spell: 'missile',
|
|
437
|
-
config,
|
|
438
|
-
missileAI: HomingAI,
|
|
439
|
-
direction: angleTo(state.position, enemy.position),
|
|
440
|
-
},
|
|
441
|
-
};
|
|
486
|
+
return missile(config, HomingAI, angleTo(position, enemy.position)).move(safeMove.x, safeMove.y);
|
|
442
487
|
}
|
|
443
488
|
|
|
444
489
|
// Budget too small for useful missile — wait for threat to pass, then fire
|
|
445
490
|
}
|
|
446
491
|
|
|
447
|
-
return
|
|
448
|
-
}
|
|
492
|
+
return moveAction(safeMove.x, safeMove.y);
|
|
493
|
+
}
|