@vibemancer/core 0.1.4 → 0.1.6
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-G5T65F3W.js +10617 -0
- package/dist/chunk-G5T65F3W.js.map +1 -0
- package/dist/index-browser.d.ts +1328 -1051
- package/dist/index-browser.js +59 -17
- package/dist/index.d.ts +53 -3
- package/dist/index.js +204 -54
- package/dist/index.js.map +1 -1
- package/package.json +21 -15
- package/src/banned-bot-names.ts +53 -0
- 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-browser.ts +1 -0
- package/src/index.ts +3 -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,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type {FinalAction, MissileFunction} from '../../hooks/index.js';
|
|
2
|
+
import {usePosition, useEnemy, useStatus, useBlinkCooldown, useHealth, useTick, useThreats, useProjectiles, 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
5
|
import {getMissileCastTime} from '../../utils/combat.js';
|
|
5
|
-
import {
|
|
6
|
+
import {RULES, ARENA_MIN, ARENA_MAX} from '../../rules.js';
|
|
6
7
|
import {
|
|
7
|
-
getThreats,
|
|
8
8
|
getMostUrgentThreat,
|
|
9
9
|
getDodgeDirectionForMovement,
|
|
10
10
|
isSafeToAttack,
|
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
getBlinkToIncreaseDistance,
|
|
13
13
|
MIN_BLINK_ESCAPE_DISTANCE,
|
|
14
14
|
shouldForceShield,
|
|
15
|
+
keepOffWalls,
|
|
16
|
+
getPreCastReposition,
|
|
15
17
|
} from '../shared.js';
|
|
16
18
|
import {useParam} from '../../engine/params-runtime.js';
|
|
17
19
|
const WALL_BUFFER = 60;
|
|
@@ -30,12 +32,11 @@ const QUICK_CONFIG = {damage: 10, speed: 5, turnRate: 1, duration: 80};
|
|
|
30
32
|
/**
|
|
31
33
|
* Standard homing missile AI.
|
|
32
34
|
*/
|
|
33
|
-
const HomingAI:
|
|
35
|
+
const HomingAI: MissileFunction = () =>
|
|
34
36
|
{
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
};
|
|
37
|
+
const ctx = getMissileContext();
|
|
38
|
+
const enemy = ctx.worldState.enemies[0];
|
|
39
|
+
return enemy ? turnToward(enemy.position.x, enemy.position.y) : flyStraight();
|
|
39
40
|
};
|
|
40
41
|
|
|
41
42
|
/**
|
|
@@ -44,10 +45,10 @@ const HomingAI: MissileAIFunction = ({worldState}) =>
|
|
|
44
45
|
function isNearWall(position: {x: number; y: number}): boolean
|
|
45
46
|
{
|
|
46
47
|
return (
|
|
47
|
-
position.x < WALL_BUFFER ||
|
|
48
|
-
position.x >
|
|
49
|
-
position.y < WALL_BUFFER ||
|
|
50
|
-
position.y >
|
|
48
|
+
position.x < ARENA_MIN + WALL_BUFFER ||
|
|
49
|
+
position.x > ARENA_MAX - WALL_BUFFER ||
|
|
50
|
+
position.y < ARENA_MIN + WALL_BUFFER ||
|
|
51
|
+
position.y > ARENA_MAX - WALL_BUFFER
|
|
51
52
|
);
|
|
52
53
|
}
|
|
53
54
|
|
|
@@ -97,16 +98,16 @@ function smartStrafe(
|
|
|
97
98
|
y: position.y + strafeCounterClockwise.y * 100,
|
|
98
99
|
};
|
|
99
100
|
const scoreClockwise = Math.min(
|
|
100
|
-
futureClockwise.x,
|
|
101
|
-
|
|
102
|
-
futureClockwise.y,
|
|
103
|
-
|
|
101
|
+
futureClockwise.x - ARENA_MIN,
|
|
102
|
+
ARENA_MAX - futureClockwise.x,
|
|
103
|
+
futureClockwise.y - ARENA_MIN,
|
|
104
|
+
ARENA_MAX - futureClockwise.y,
|
|
104
105
|
);
|
|
105
106
|
const scoreCounterClockwise = Math.min(
|
|
106
|
-
futureCounterClockwise.x,
|
|
107
|
-
|
|
108
|
-
futureCounterClockwise.y,
|
|
109
|
-
|
|
107
|
+
futureCounterClockwise.x - ARENA_MIN,
|
|
108
|
+
ARENA_MAX - futureCounterClockwise.x,
|
|
109
|
+
futureCounterClockwise.y - ARENA_MIN,
|
|
110
|
+
ARENA_MAX - futureCounterClockwise.y,
|
|
110
111
|
);
|
|
111
112
|
strafeDir =
|
|
112
113
|
scoreClockwise > scoreCounterClockwise
|
|
@@ -121,7 +122,7 @@ function smartStrafe(
|
|
|
121
122
|
}
|
|
122
123
|
|
|
123
124
|
// When actively dodging, commit fully — no approach/retreat dilution
|
|
124
|
-
const intensity = dodgeDirection ?
|
|
125
|
+
const intensity = dodgeDirection ? 1 : strafeIntensity;
|
|
125
126
|
return {x: strafeDir.x * intensity, y: strafeDir.y * intensity};
|
|
126
127
|
}
|
|
127
128
|
|
|
@@ -149,35 +150,47 @@ function smartStrafe(
|
|
|
149
150
|
*
|
|
150
151
|
* TIER: 1 (base)
|
|
151
152
|
*/
|
|
152
|
-
export
|
|
153
|
+
export function Spellspinner(): FinalAction
|
|
153
154
|
{
|
|
154
|
-
const
|
|
155
|
+
const position = usePosition();
|
|
156
|
+
const enemy = useEnemy();
|
|
157
|
+
const status = useStatus();
|
|
158
|
+
const blinkCooldown = useBlinkCooldown();
|
|
159
|
+
const health = useHealth();
|
|
160
|
+
const tick = useTick();
|
|
161
|
+
const threats = useThreats();
|
|
162
|
+
const projectiles = useProjectiles();
|
|
163
|
+
const castingSpell = useCastingSpell();
|
|
164
|
+
const castProgressData = useCastProgress();
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
const targetDistance = useParam('targetDistance', 268, {
|
|
155
168
|
range: 150,
|
|
156
169
|
min: 150,
|
|
157
170
|
max: 450,
|
|
158
171
|
});
|
|
159
|
-
const dangerDistance = useParam('dangerDistance',
|
|
172
|
+
const dangerDistance = useParam('dangerDistance', 173, {
|
|
160
173
|
range: 100,
|
|
161
174
|
min: 100,
|
|
162
175
|
});
|
|
163
|
-
const strafeCyclePeriod = useParam('strafeCyclePeriod',
|
|
176
|
+
const strafeCyclePeriod = useParam('strafeCyclePeriod', 286, {
|
|
164
177
|
range: 75,
|
|
165
178
|
min: 80,
|
|
166
179
|
max: 300,
|
|
167
180
|
});
|
|
168
|
-
const strafeIntensity = useParam('strafeIntensity',
|
|
169
|
-
range:
|
|
170
|
-
min:
|
|
171
|
-
max:
|
|
181
|
+
const strafeIntensity = useParam('strafeIntensity', 0.98, {
|
|
182
|
+
range: 0.4,
|
|
183
|
+
min: 0.3,
|
|
184
|
+
max: 1,
|
|
172
185
|
steps: 7,
|
|
173
186
|
});
|
|
174
|
-
const approachSpeed = useParam('approachSpeed',
|
|
175
|
-
range:
|
|
176
|
-
min:
|
|
177
|
-
max:
|
|
187
|
+
const approachSpeed = useParam('approachSpeed', 0.4, {
|
|
188
|
+
range: 0.3,
|
|
189
|
+
min: 0.1,
|
|
190
|
+
max: 0.7,
|
|
178
191
|
steps: 7,
|
|
179
192
|
});
|
|
180
|
-
const distanceDeadZone = useParam('distanceDeadZone',
|
|
193
|
+
const distanceDeadZone = useParam('distanceDeadZone', 13, {
|
|
181
194
|
range: 30,
|
|
182
195
|
min: 5,
|
|
183
196
|
max: 80,
|
|
@@ -188,32 +201,47 @@ export const Spellspinner: WizardFunction = ({state}) =>
|
|
|
188
201
|
min: 50,
|
|
189
202
|
max: 300,
|
|
190
203
|
});
|
|
191
|
-
const blinkWindowMax = useParam('blinkWindowMax',
|
|
204
|
+
const blinkWindowMax = useParam('blinkWindowMax', 51, {
|
|
192
205
|
range: 30,
|
|
193
206
|
min: 15,
|
|
194
207
|
max: 80,
|
|
195
208
|
steps: 7,
|
|
196
209
|
});
|
|
210
|
+
const centerBias = useParam('centerBias', 0.69, {
|
|
211
|
+
range: 0.4,
|
|
212
|
+
min: 0,
|
|
213
|
+
max: 0.8,
|
|
214
|
+
steps: 5,
|
|
215
|
+
});
|
|
216
|
+
const maxBlinkDistance = useParam('maxBlinkDistance', 300, {
|
|
217
|
+
range: 100,
|
|
218
|
+
min: 50,
|
|
219
|
+
max: 300,
|
|
220
|
+
steps: 7,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const distance = distanceTo(position, enemy.position);
|
|
197
224
|
|
|
198
|
-
|
|
199
|
-
if (!enemy)
|
|
225
|
+
function capBlinkDistance(target: {x: number; y: number}): {x: number; y: number}
|
|
200
226
|
{
|
|
201
|
-
|
|
227
|
+
const dx = target.x - position.x;
|
|
228
|
+
const dy = target.y - position.y;
|
|
229
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
230
|
+
if (dist <= maxBlinkDistance) return target;
|
|
231
|
+
const scale = maxBlinkDistance / dist;
|
|
232
|
+
return {x: position.x + dx * scale, y: position.y + dy * scale};
|
|
202
233
|
}
|
|
203
234
|
|
|
204
|
-
const distance = distanceTo(state.position, enemy.position);
|
|
205
|
-
|
|
206
235
|
// Analyze threats using simulation
|
|
207
|
-
const threats = getThreats(state);
|
|
208
236
|
const urgentThreat = getMostUrgentThreat(threats);
|
|
209
237
|
|
|
210
238
|
// Smart strafe (dodge direction includes close homing missile avoidance)
|
|
211
|
-
const dodgeDirection = getDodgeDirectionForMovement(threats,
|
|
239
|
+
const dodgeDirection = getDodgeDirectionForMovement(threats, position);
|
|
212
240
|
const strafe = smartStrafe(
|
|
213
|
-
|
|
241
|
+
position,
|
|
214
242
|
enemy,
|
|
215
243
|
dodgeDirection,
|
|
216
|
-
|
|
244
|
+
tick,
|
|
217
245
|
strafeCyclePeriod,
|
|
218
246
|
strafeIntensity,
|
|
219
247
|
);
|
|
@@ -224,24 +252,32 @@ export const Spellspinner: WizardFunction = ({state}) =>
|
|
|
224
252
|
|
|
225
253
|
if (!dodgeDirection)
|
|
226
254
|
{
|
|
227
|
-
const deltaX = enemy.position.x -
|
|
228
|
-
const deltaY = enemy.position.y -
|
|
255
|
+
const deltaX = enemy.position.x - position.x;
|
|
256
|
+
const deltaY = enemy.position.y - position.y;
|
|
229
257
|
const normalizedDistance = distance > 0 ? distance : 1;
|
|
230
258
|
|
|
231
|
-
if (distance > targetDistance + distanceDeadZone)
|
|
259
|
+
if (distance > targetDistance + distanceDeadZone)
|
|
232
260
|
{
|
|
233
|
-
|
|
234
|
-
|
|
261
|
+
// Approach the center-side of the enemy so knockback pushes toward center, not lava.
|
|
262
|
+
const center = (ARENA_MIN + ARENA_MAX) / 2;
|
|
263
|
+
const toCenterX = center - enemy.position.x;
|
|
264
|
+
const toCenterY = center - enemy.position.y;
|
|
265
|
+
const toCenterDist = Math.sqrt(toCenterX * toCenterX + toCenterY * toCenterY) || 1;
|
|
266
|
+
const approachDirX = deltaX / normalizedDistance + (toCenterX / toCenterDist) * centerBias;
|
|
267
|
+
const approachDirY = deltaY / normalizedDistance + (toCenterY / toCenterDist) * centerBias;
|
|
268
|
+
const approachMag = Math.sqrt(approachDirX * approachDirX + approachDirY * approachDirY) || 1;
|
|
269
|
+
moveX += (approachDirX / approachMag) * approachSpeed;
|
|
270
|
+
moveY += (approachDirY / approachMag) * approachSpeed;
|
|
235
271
|
}
|
|
236
272
|
else if (distance < targetDistance - distanceDeadZone)
|
|
237
273
|
{
|
|
238
274
|
let retreatX = -(deltaX / normalizedDistance) * approachSpeed;
|
|
239
275
|
let retreatY = -(deltaY / normalizedDistance) * approachSpeed;
|
|
240
|
-
if (
|
|
241
|
-
if (
|
|
276
|
+
if (position.x < ARENA_MIN + WALL_BUFFER && retreatX < 0) retreatX = 0;
|
|
277
|
+
if (position.x > ARENA_MAX - WALL_BUFFER && retreatX > 0)
|
|
242
278
|
retreatX = 0;
|
|
243
|
-
if (
|
|
244
|
-
if (
|
|
279
|
+
if (position.y < ARENA_MIN + WALL_BUFFER && retreatY < 0) retreatY = 0;
|
|
280
|
+
if (position.y > ARENA_MAX - WALL_BUFFER && retreatY > 0)
|
|
245
281
|
retreatY = 0;
|
|
246
282
|
moveX += retreatX;
|
|
247
283
|
moveY += retreatY;
|
|
@@ -250,44 +286,45 @@ export const Spellspinner: WizardFunction = ({state}) =>
|
|
|
250
286
|
|
|
251
287
|
// Normalize
|
|
252
288
|
const magnitude = Math.sqrt(moveX * moveX + moveY * moveY);
|
|
253
|
-
if (magnitude >
|
|
289
|
+
if (magnitude > 1)
|
|
254
290
|
{
|
|
255
|
-
moveX =
|
|
256
|
-
moveY =
|
|
291
|
+
moveX = moveX / magnitude;
|
|
292
|
+
moveY = moveY / magnitude;
|
|
257
293
|
}
|
|
258
294
|
|
|
259
295
|
const move = {x: moveX, y: moveY};
|
|
296
|
+
const safeMove = keepOffWalls(position, move);
|
|
260
297
|
|
|
261
298
|
// === DEFENSE: Handle channeling ===
|
|
262
|
-
if (
|
|
299
|
+
if (status === 'channeling')
|
|
263
300
|
{
|
|
264
301
|
// Cancel shield if no imminent threats
|
|
265
302
|
if (!urgentThreat || urgentThreat.ticksToImpact > shieldCancelThreshold)
|
|
266
303
|
{
|
|
267
|
-
return
|
|
304
|
+
return cancel().move(safeMove.x, safeMove.y);
|
|
268
305
|
}
|
|
269
|
-
return
|
|
306
|
+
return idle(); // Hold shield if still threatened
|
|
270
307
|
}
|
|
271
308
|
|
|
272
309
|
// === DEFENSE: Cancel missile cast if lethal threat arrives during GCD ===
|
|
273
310
|
if (
|
|
274
|
-
|
|
275
|
-
|
|
311
|
+
status === 'casting' &&
|
|
312
|
+
castingSpell === 'missile' &&
|
|
276
313
|
urgentThreat
|
|
277
314
|
)
|
|
278
315
|
{
|
|
279
|
-
const remainingCast = (
|
|
316
|
+
const remainingCast = ((castProgressData?.total) ?? 0) - ((castProgressData?.current) ?? 0);
|
|
280
317
|
const arrivesInGCD =
|
|
281
318
|
urgentThreat.ticksToImpact > remainingCast &&
|
|
282
|
-
urgentThreat.ticksToImpact <= remainingCast + GCD_DURATION;
|
|
319
|
+
urgentThreat.ticksToImpact <= remainingCast + RULES.GCD_DURATION;
|
|
283
320
|
|
|
284
321
|
if (
|
|
285
322
|
arrivesInGCD &&
|
|
286
|
-
|
|
323
|
+
health <= urgentThreat.projectile.damage + 1 &&
|
|
287
324
|
remainingCast > 10
|
|
288
325
|
)
|
|
289
326
|
{
|
|
290
|
-
return
|
|
327
|
+
return cancel().move(safeMove.x, safeMove.y);
|
|
291
328
|
}
|
|
292
329
|
}
|
|
293
330
|
|
|
@@ -295,9 +332,9 @@ export const Spellspinner: WizardFunction = ({state}) =>
|
|
|
295
332
|
// Blink to center (stays in combat range) and dodges the current missile.
|
|
296
333
|
// Also blink when persistent threats block all offense (creates safe firing window).
|
|
297
334
|
if (
|
|
298
|
-
|
|
335
|
+
status === 'idle' &&
|
|
299
336
|
urgentThreat &&
|
|
300
|
-
|
|
337
|
+
blinkCooldown === 0 &&
|
|
301
338
|
urgentThreat.ticksToImpact >= 10 &&
|
|
302
339
|
urgentThreat.ticksToImpact <= blinkWindowMax
|
|
303
340
|
)
|
|
@@ -309,18 +346,16 @@ export const Spellspinner: WizardFunction = ({state}) =>
|
|
|
309
346
|
getMissileCastTime(QUICK_CONFIG),
|
|
310
347
|
);
|
|
311
348
|
|
|
312
|
-
if (undodgeableOrForced || cannotAttack)
|
|
349
|
+
if (undodgeableOrForced || cannotAttack)
|
|
313
350
|
{
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
startCast: {spell: 'blink', target: getBlinkToCenter(state.position)},
|
|
317
|
-
};
|
|
351
|
+
const blinkTarget = capBlinkDistance(getBlinkToCenter(position));
|
|
352
|
+
return blink(blinkTarget.x, blinkTarget.y);
|
|
318
353
|
}
|
|
319
354
|
}
|
|
320
355
|
|
|
321
356
|
// === DEFENSE: Shield undodgeable/high-damage threats (when blink on cooldown) ===
|
|
322
357
|
if (
|
|
323
|
-
|
|
358
|
+
status === 'idle' &&
|
|
324
359
|
urgentThreat &&
|
|
325
360
|
(!urgentThreat.bestDodgeDirection || shouldForceShield(urgentThreat))
|
|
326
361
|
)
|
|
@@ -329,70 +364,73 @@ export const Spellspinner: WizardFunction = ({state}) =>
|
|
|
329
364
|
{
|
|
330
365
|
if (urgentThreat.ticksToStartShield <= 3)
|
|
331
366
|
{
|
|
332
|
-
return
|
|
333
|
-
move: {x: 0, y: 0},
|
|
334
|
-
startCast: {spell: 'shield'},
|
|
335
|
-
};
|
|
367
|
+
return shield();
|
|
336
368
|
}
|
|
337
369
|
}
|
|
338
370
|
}
|
|
339
371
|
|
|
372
|
+
// === Preemptive shield — shield when enemy is casting a missile at close range ===
|
|
373
|
+
if (
|
|
374
|
+
status === 'idle' &&
|
|
375
|
+
distance < 300 &&
|
|
376
|
+
enemy.status === 'casting' &&
|
|
377
|
+
enemy.castingSpell === 'missile'
|
|
378
|
+
)
|
|
379
|
+
{
|
|
380
|
+
const remainingCast = (enemy.castDuration ?? 0) - (enemy.castProgress ?? 0);
|
|
381
|
+
const estimatedFlightTicks = distance / 8;
|
|
382
|
+
const ticksUntilHit = remainingCast + estimatedFlightTicks;
|
|
383
|
+
|
|
384
|
+
if (
|
|
385
|
+
ticksUntilHit >= RULES.SHIELD_CAST_TIME &&
|
|
386
|
+
ticksUntilHit < RULES.SHIELD_CAST_TIME + 15
|
|
387
|
+
)
|
|
388
|
+
{
|
|
389
|
+
return shield();
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
340
393
|
// === DISTANCE BLINK: enemy too close, blink to reestablish kiting range ===
|
|
341
394
|
if (
|
|
342
|
-
|
|
395
|
+
status === 'idle' &&
|
|
343
396
|
distance < dangerDistance &&
|
|
344
|
-
|
|
397
|
+
blinkCooldown === 0 &&
|
|
345
398
|
(!urgentThreat || urgentThreat.bestDodgeDirection !== null)
|
|
346
399
|
)
|
|
347
400
|
{
|
|
348
401
|
const blinkTarget = getBlinkToIncreaseDistance(
|
|
349
|
-
|
|
402
|
+
position,
|
|
350
403
|
enemy.position,
|
|
351
|
-
|
|
404
|
+
projectiles,
|
|
352
405
|
MIN_BLINK_ESCAPE_DISTANCE,
|
|
353
406
|
);
|
|
354
|
-
if (blinkTarget)
|
|
407
|
+
if (blinkTarget)
|
|
355
408
|
{
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
startCast: {spell: 'blink', target: blinkTarget},
|
|
359
|
-
};
|
|
409
|
+
const capped = capBlinkDistance(blinkTarget);
|
|
410
|
+
return blink(capped.x, capped.y);
|
|
360
411
|
}
|
|
361
412
|
}
|
|
362
413
|
|
|
363
414
|
// === OFFENSE: Attack when safe ===
|
|
364
|
-
if (
|
|
415
|
+
if (status === 'idle' && distance < 500)
|
|
365
416
|
{
|
|
417
|
+
const reposition = getPreCastReposition(position, enemy.position);
|
|
418
|
+
if (reposition) return moveAction(reposition.x, reposition.y);
|
|
419
|
+
|
|
366
420
|
// Primary: standard homing missile
|
|
367
421
|
const standardCastTime = getMissileCastTime(STANDARD_CONFIG);
|
|
368
422
|
if (isSafeToAttack(threats, standardCastTime))
|
|
369
423
|
{
|
|
370
|
-
return
|
|
371
|
-
move,
|
|
372
|
-
startCast: {
|
|
373
|
-
spell: 'missile',
|
|
374
|
-
config: STANDARD_CONFIG,
|
|
375
|
-
missileAI: HomingAI,
|
|
376
|
-
direction: angleTo(state.position, enemy.position),
|
|
377
|
-
},
|
|
378
|
-
};
|
|
424
|
+
return missile(STANDARD_CONFIG, HomingAI, angleTo(position, enemy.position)).move(safeMove.x, safeMove.y);
|
|
379
425
|
}
|
|
380
426
|
|
|
381
427
|
// Fallback: quick missile under pressure
|
|
382
428
|
const quickCastTime = getMissileCastTime(QUICK_CONFIG);
|
|
383
429
|
if (isSafeToAttack(threats, quickCastTime))
|
|
384
430
|
{
|
|
385
|
-
return
|
|
386
|
-
move,
|
|
387
|
-
startCast: {
|
|
388
|
-
spell: 'missile',
|
|
389
|
-
config: QUICK_CONFIG,
|
|
390
|
-
missileAI: HomingAI,
|
|
391
|
-
direction: angleTo(state.position, enemy.position),
|
|
392
|
-
},
|
|
393
|
-
};
|
|
431
|
+
return missile(QUICK_CONFIG, HomingAI, angleTo(position, enemy.position)).move(safeMove.x, safeMove.y);
|
|
394
432
|
}
|
|
395
433
|
}
|
|
396
434
|
|
|
397
|
-
return
|
|
398
|
-
}
|
|
435
|
+
return moveAction(safeMove.x, safeMove.y);
|
|
436
|
+
}
|