@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,17 @@
|
|
|
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,
|
|
11
11
|
getBlinkToDecreaseDistance,
|
|
12
12
|
shouldForceShield,
|
|
13
|
+
keepOffWalls,
|
|
14
|
+
getPreCastReposition,
|
|
13
15
|
} from '../shared.js';
|
|
14
16
|
import {useParam} from '../../engine/params-runtime.js';
|
|
15
17
|
const WALL_BUFFER = 60;
|
|
@@ -23,12 +25,11 @@ const QUICK_RANGE = QUICK_CONFIG.speed * QUICK_CONFIG.duration; // 350
|
|
|
23
25
|
/**
|
|
24
26
|
* Standard homing missile AI.
|
|
25
27
|
*/
|
|
26
|
-
const StandardHoming:
|
|
28
|
+
const StandardHoming: MissileFunction = () =>
|
|
27
29
|
{
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
};
|
|
30
|
+
const ctx = getMissileContext();
|
|
31
|
+
const enemy = ctx.worldState.enemies[0];
|
|
32
|
+
return enemy ? turnToward(enemy.position.x, enemy.position.y) : flyStraight();
|
|
32
33
|
};
|
|
33
34
|
|
|
34
35
|
/**
|
|
@@ -37,10 +38,10 @@ const StandardHoming: MissileAIFunction = ({worldState}) =>
|
|
|
37
38
|
function isNearWall(position: {x: number; y: number}): boolean
|
|
38
39
|
{
|
|
39
40
|
return (
|
|
40
|
-
position.x < WALL_BUFFER ||
|
|
41
|
-
position.x >
|
|
42
|
-
position.y < WALL_BUFFER ||
|
|
43
|
-
position.y >
|
|
41
|
+
position.x < ARENA_MIN + WALL_BUFFER ||
|
|
42
|
+
position.x > ARENA_MAX - WALL_BUFFER ||
|
|
43
|
+
position.y < ARENA_MIN + WALL_BUFFER ||
|
|
44
|
+
position.y > ARENA_MAX - WALL_BUFFER
|
|
44
45
|
);
|
|
45
46
|
}
|
|
46
47
|
|
|
@@ -58,12 +59,22 @@ function combatMove(
|
|
|
58
59
|
strafeIntensity: number,
|
|
59
60
|
approachSpeed: number,
|
|
60
61
|
distanceDeadZone: number,
|
|
61
|
-
|
|
62
|
+
centerBias: number,
|
|
63
|
+
): {x: number; y: number}
|
|
62
64
|
{
|
|
63
65
|
const deltaX = enemy.position.x - position.x;
|
|
64
66
|
const deltaY = enemy.position.y - position.y;
|
|
65
67
|
const normalizedDistance = currentDistance > 0 ? currentDistance : 1;
|
|
66
68
|
|
|
69
|
+
// Approach the center-side of the enemy so knockback pushes toward center, not lava.
|
|
70
|
+
const center = (ARENA_MIN + ARENA_MAX) / 2;
|
|
71
|
+
const toCenterX = center - enemy.position.x;
|
|
72
|
+
const toCenterY = center - enemy.position.y;
|
|
73
|
+
const toCenterDist = Math.sqrt(toCenterX * toCenterX + toCenterY * toCenterY) || 1;
|
|
74
|
+
const approachDirX = deltaX / normalizedDistance + (toCenterX / toCenterDist) * centerBias;
|
|
75
|
+
const approachDirY = deltaY / normalizedDistance + (toCenterY / toCenterDist) * centerBias;
|
|
76
|
+
const approachMag = Math.sqrt(approachDirX * approachDirX + approachDirY * approachDirY) || 1;
|
|
77
|
+
|
|
67
78
|
// Perpendicular directions
|
|
68
79
|
const strafeClockwise = {
|
|
69
80
|
x: -deltaY / normalizedDistance,
|
|
@@ -93,16 +104,16 @@ function combatMove(
|
|
|
93
104
|
y: position.y + strafeCounterClockwise.y * 100,
|
|
94
105
|
};
|
|
95
106
|
const scoreClockwise = Math.min(
|
|
96
|
-
futureClockwise.x,
|
|
97
|
-
|
|
98
|
-
futureClockwise.y,
|
|
99
|
-
|
|
107
|
+
futureClockwise.x - ARENA_MIN,
|
|
108
|
+
ARENA_MAX - futureClockwise.x,
|
|
109
|
+
futureClockwise.y - ARENA_MIN,
|
|
110
|
+
ARENA_MAX - futureClockwise.y,
|
|
100
111
|
);
|
|
101
112
|
const scoreCounterClockwise = Math.min(
|
|
102
|
-
futureCounterClockwise.x,
|
|
103
|
-
|
|
104
|
-
futureCounterClockwise.y,
|
|
105
|
-
|
|
113
|
+
futureCounterClockwise.x - ARENA_MIN,
|
|
114
|
+
ARENA_MAX - futureCounterClockwise.x,
|
|
115
|
+
futureCounterClockwise.y - ARENA_MIN,
|
|
116
|
+
ARENA_MAX - futureCounterClockwise.y,
|
|
106
117
|
);
|
|
107
118
|
strafeDirection =
|
|
108
119
|
scoreClockwise > scoreCounterClockwise
|
|
@@ -119,36 +130,36 @@ function combatMove(
|
|
|
119
130
|
// When actively dodging, commit fully — no approach/retreat dilution
|
|
120
131
|
if (dodgeDirection)
|
|
121
132
|
{
|
|
122
|
-
return {x: strafeDirection.x
|
|
133
|
+
return {x: strafeDirection.x, y: strafeDirection.y};
|
|
123
134
|
}
|
|
124
135
|
|
|
125
136
|
let moveX = strafeDirection.x * strafeIntensity;
|
|
126
137
|
let moveY = strafeDirection.y * strafeIntensity;
|
|
127
138
|
|
|
128
139
|
// Distance management (only when not dodging)
|
|
129
|
-
if (currentDistance > targetDistance + distanceDeadZone)
|
|
140
|
+
if (currentDistance > targetDistance + distanceDeadZone)
|
|
130
141
|
{
|
|
131
|
-
moveX += (
|
|
132
|
-
moveY += (
|
|
142
|
+
moveX += (approachDirX / approachMag) * approachSpeed;
|
|
143
|
+
moveY += (approachDirY / approachMag) * approachSpeed;
|
|
133
144
|
}
|
|
134
145
|
else if (currentDistance < targetDistance - distanceDeadZone)
|
|
135
146
|
{
|
|
136
147
|
let retreatX = -(deltaX / normalizedDistance) * approachSpeed;
|
|
137
148
|
let retreatY = -(deltaY / normalizedDistance) * approachSpeed;
|
|
138
|
-
if (position.x < WALL_BUFFER && retreatX < 0) retreatX = 0;
|
|
139
|
-
if (position.x >
|
|
140
|
-
if (position.y < WALL_BUFFER && retreatY < 0) retreatY = 0;
|
|
141
|
-
if (position.y >
|
|
149
|
+
if (position.x < ARENA_MIN + WALL_BUFFER && retreatX < 0) retreatX = 0;
|
|
150
|
+
if (position.x > ARENA_MAX - WALL_BUFFER && retreatX > 0) retreatX = 0;
|
|
151
|
+
if (position.y < ARENA_MIN + WALL_BUFFER && retreatY < 0) retreatY = 0;
|
|
152
|
+
if (position.y > ARENA_MAX - WALL_BUFFER && retreatY > 0) retreatY = 0;
|
|
142
153
|
moveX += retreatX;
|
|
143
154
|
moveY += retreatY;
|
|
144
155
|
}
|
|
145
156
|
|
|
146
157
|
// Normalize
|
|
147
158
|
const magnitude = Math.sqrt(moveX * moveX + moveY * moveY);
|
|
148
|
-
if (magnitude >
|
|
159
|
+
if (magnitude > 1)
|
|
149
160
|
{
|
|
150
|
-
moveX =
|
|
151
|
-
moveY =
|
|
161
|
+
moveX = moveX / magnitude;
|
|
162
|
+
moveY = moveY / magnitude;
|
|
152
163
|
}
|
|
153
164
|
|
|
154
165
|
return {x: moveX, y: moveY};
|
|
@@ -171,30 +182,42 @@ function combatMove(
|
|
|
171
182
|
*
|
|
172
183
|
* TIER: 1 (base)
|
|
173
184
|
*/
|
|
174
|
-
export
|
|
185
|
+
export function Battlemage(): FinalAction
|
|
175
186
|
{
|
|
176
|
-
const
|
|
187
|
+
const position = usePosition();
|
|
188
|
+
const enemy = useEnemy();
|
|
189
|
+
const status = useStatus();
|
|
190
|
+
const blinkCooldown = useBlinkCooldown();
|
|
191
|
+
const health = useHealth();
|
|
192
|
+
const tick = useTick();
|
|
193
|
+
const threats = useThreats();
|
|
194
|
+
const projectiles = useProjectiles();
|
|
195
|
+
const castingSpell = useCastingSpell();
|
|
196
|
+
const castProgressData = useCastProgress();
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
const targetDistance = useParam('targetDistance', 392, {
|
|
177
200
|
range: 150,
|
|
178
201
|
min: 150,
|
|
179
202
|
});
|
|
180
|
-
const strafeCyclePeriod = useParam('strafeCyclePeriod',
|
|
203
|
+
const strafeCyclePeriod = useParam('strafeCyclePeriod', 240, {
|
|
181
204
|
range: 75,
|
|
182
205
|
min: 80,
|
|
183
206
|
max: 300,
|
|
184
207
|
});
|
|
185
|
-
const strafeIntensity = useParam('strafeIntensity',
|
|
186
|
-
range:
|
|
187
|
-
min:
|
|
188
|
-
max:
|
|
208
|
+
const strafeIntensity = useParam('strafeIntensity', 0.87, {
|
|
209
|
+
range: 0.4,
|
|
210
|
+
min: 0.2,
|
|
211
|
+
max: 1,
|
|
189
212
|
steps: 7,
|
|
190
213
|
});
|
|
191
|
-
const approachSpeed = useParam('approachSpeed',
|
|
192
|
-
range:
|
|
193
|
-
min:
|
|
194
|
-
max:
|
|
214
|
+
const approachSpeed = useParam('approachSpeed', 0.8, {
|
|
215
|
+
range: 0.3,
|
|
216
|
+
min: 0.1,
|
|
217
|
+
max: 0.8,
|
|
195
218
|
steps: 7,
|
|
196
219
|
});
|
|
197
|
-
const distanceDeadZone = useParam('distanceDeadZone',
|
|
220
|
+
const distanceDeadZone = useParam('distanceDeadZone', 64, {
|
|
198
221
|
range: 30,
|
|
199
222
|
min: 10,
|
|
200
223
|
max: 80,
|
|
@@ -205,7 +228,7 @@ export const Battlemage: WizardFunction = ({state}) =>
|
|
|
205
228
|
min: 50,
|
|
206
229
|
max: 300,
|
|
207
230
|
});
|
|
208
|
-
const blinkWindowMax = useParam('blinkWindowMax',
|
|
231
|
+
const blinkWindowMax = useParam('blinkWindowMax', 15, {
|
|
209
232
|
range: 30,
|
|
210
233
|
min: 15,
|
|
211
234
|
max: 80,
|
|
@@ -217,81 +240,98 @@ export const Battlemage: WizardFunction = ({state}) =>
|
|
|
217
240
|
max: 100,
|
|
218
241
|
steps: 7,
|
|
219
242
|
});
|
|
220
|
-
const cancelMinRemainingCast = useParam('cancelMinRemainingCast',
|
|
243
|
+
const cancelMinRemainingCast = useParam('cancelMinRemainingCast', 36, {
|
|
221
244
|
range: 30,
|
|
222
245
|
min: 5,
|
|
223
246
|
max: 100,
|
|
224
247
|
steps: 7,
|
|
225
248
|
});
|
|
226
|
-
const interruptRange = useParam('interruptRange',
|
|
249
|
+
const interruptRange = useParam('interruptRange', 150, {
|
|
227
250
|
range: 150,
|
|
228
251
|
min: 150,
|
|
229
252
|
max: 600,
|
|
230
253
|
steps: 7,
|
|
231
254
|
});
|
|
232
|
-
const attackRange = useParam('attackRange',
|
|
255
|
+
const attackRange = useParam('attackRange', 662, {
|
|
233
256
|
range: 150,
|
|
234
257
|
min: 200,
|
|
235
258
|
max: 700,
|
|
236
259
|
});
|
|
260
|
+
const centerBias = useParam('centerBias', 0.04, {
|
|
261
|
+
range: 0.4,
|
|
262
|
+
min: 0,
|
|
263
|
+
max: 0.8,
|
|
264
|
+
steps: 5,
|
|
265
|
+
});
|
|
266
|
+
const maxBlinkDistance = useParam('maxBlinkDistance', 216, {
|
|
267
|
+
range: 100,
|
|
268
|
+
min: 50,
|
|
269
|
+
max: 300,
|
|
270
|
+
steps: 7,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
const distance = distanceTo(position, enemy.position);
|
|
237
274
|
|
|
238
|
-
|
|
239
|
-
if (!enemy)
|
|
275
|
+
function capBlinkDistance(target: {x: number; y: number}): {x: number; y: number}
|
|
240
276
|
{
|
|
241
|
-
|
|
277
|
+
const dx = target.x - position.x;
|
|
278
|
+
const dy = target.y - position.y;
|
|
279
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
280
|
+
if (dist <= maxBlinkDistance) return target;
|
|
281
|
+
const scale = maxBlinkDistance / dist;
|
|
282
|
+
return {x: position.x + dx * scale, y: position.y + dy * scale};
|
|
242
283
|
}
|
|
243
284
|
|
|
244
|
-
const distance = distanceTo(state.position, enemy.position);
|
|
245
|
-
|
|
246
285
|
// Analyze threats using simulation
|
|
247
|
-
const threats = getThreats(state);
|
|
248
286
|
const urgentThreat = getMostUrgentThreat(threats);
|
|
249
287
|
|
|
250
288
|
// === DEFENSE: Handle channeling ===
|
|
251
|
-
if (
|
|
289
|
+
if (status === 'channeling')
|
|
252
290
|
{
|
|
253
291
|
// Cancel shield if no imminent threats
|
|
254
292
|
if (!urgentThreat || urgentThreat.ticksToImpact > shieldCancelThreshold)
|
|
255
293
|
{
|
|
256
|
-
return
|
|
294
|
+
return cancel();
|
|
257
295
|
}
|
|
258
|
-
return
|
|
296
|
+
return idle();
|
|
259
297
|
}
|
|
260
298
|
|
|
261
299
|
// === DEFENSE: Cancel long missile casts if undodgeable threat approaches ===
|
|
262
300
|
// Only cancel when health is too low to trade (aggressive casts accepted the risk).
|
|
263
301
|
if (
|
|
264
|
-
|
|
265
|
-
|
|
302
|
+
status === 'casting' &&
|
|
303
|
+
castingSpell === 'missile' &&
|
|
266
304
|
urgentThreat &&
|
|
267
305
|
!urgentThreat.bestDodgeDirection
|
|
268
306
|
)
|
|
269
307
|
{
|
|
270
|
-
const remainingCast = (
|
|
308
|
+
const remainingCast = ((castProgressData?.total) ?? 0) - ((castProgressData?.current) ?? 0);
|
|
271
309
|
if (
|
|
272
310
|
remainingCast > cancelMinRemainingCast &&
|
|
273
311
|
urgentThreat.ticksToImpact <=
|
|
274
|
-
remainingCast + GCD_DURATION + SHIELD_CAST_TIME + 1 &&
|
|
275
|
-
|
|
312
|
+
remainingCast + RULES.GCD_DURATION + RULES.SHIELD_CAST_TIME + 1 &&
|
|
313
|
+
health <= urgentThreat.projectile.damage * 2 + 1
|
|
276
314
|
)
|
|
277
315
|
{
|
|
278
316
|
const dodgeDirection = getDodgeDirectionForMovement(
|
|
279
317
|
threats,
|
|
280
|
-
|
|
318
|
+
position,
|
|
281
319
|
);
|
|
282
|
-
const
|
|
283
|
-
|
|
320
|
+
const cancelMove = combatMove(
|
|
321
|
+
position,
|
|
284
322
|
enemy,
|
|
285
323
|
distance,
|
|
286
324
|
targetDistance,
|
|
287
325
|
dodgeDirection,
|
|
288
|
-
|
|
326
|
+
tick,
|
|
289
327
|
strafeCyclePeriod,
|
|
290
328
|
strafeIntensity,
|
|
291
329
|
approachSpeed,
|
|
292
330
|
distanceDeadZone,
|
|
331
|
+
centerBias,
|
|
293
332
|
);
|
|
294
|
-
|
|
333
|
+
const safeCancelMove = keepOffWalls(position, cancelMove);
|
|
334
|
+
return cancel().move(safeCancelMove.x, safeCancelMove.y);
|
|
295
335
|
}
|
|
296
336
|
}
|
|
297
337
|
|
|
@@ -299,29 +339,27 @@ export const Battlemage: WizardFunction = ({state}) =>
|
|
|
299
339
|
// Blink both dodges the incoming missile AND closes distance — better than shielding.
|
|
300
340
|
// Only shield when blink is on cooldown.
|
|
301
341
|
if (
|
|
302
|
-
|
|
342
|
+
status === 'idle' &&
|
|
303
343
|
urgentThreat &&
|
|
304
|
-
|
|
344
|
+
blinkCooldown === 0 &&
|
|
305
345
|
distance > targetDistance - blinkDistanceOffset &&
|
|
306
346
|
urgentThreat.ticksToImpact >= 10 &&
|
|
307
347
|
urgentThreat.ticksToImpact <= blinkWindowMax
|
|
308
348
|
)
|
|
309
349
|
{
|
|
310
|
-
const
|
|
350
|
+
const rawBlinkTarget =
|
|
311
351
|
getBlinkToDecreaseDistance(
|
|
312
|
-
|
|
352
|
+
position,
|
|
313
353
|
enemy.position,
|
|
314
|
-
|
|
354
|
+
projectiles,
|
|
315
355
|
) ?? enemy.position;
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
startCast: {spell: 'blink', target: blinkTarget},
|
|
319
|
-
};
|
|
356
|
+
const blinkTarget = capBlinkDistance(rawBlinkTarget);
|
|
357
|
+
return blink(blinkTarget.x, blinkTarget.y);
|
|
320
358
|
}
|
|
321
359
|
|
|
322
360
|
// === DEFENSE: Shield undodgeable or high-damage homing threats (only when blink can't handle it) ===
|
|
323
361
|
if (
|
|
324
|
-
|
|
362
|
+
status === 'idle' &&
|
|
325
363
|
urgentThreat &&
|
|
326
364
|
(!urgentThreat.bestDodgeDirection || shouldForceShield(urgentThreat))
|
|
327
365
|
)
|
|
@@ -329,49 +367,65 @@ export const Battlemage: WizardFunction = ({state}) =>
|
|
|
329
367
|
if (
|
|
330
368
|
urgentThreat.canBlockInTime &&
|
|
331
369
|
urgentThreat.ticksToStartShield <= 3 &&
|
|
332
|
-
(
|
|
370
|
+
(blinkCooldown > 0 || urgentThreat.ticksToImpact < 10)
|
|
333
371
|
)
|
|
334
372
|
{
|
|
335
|
-
return
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
373
|
+
return shield();
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// === Preemptive shield — shield when enemy is casting a missile at close range ===
|
|
378
|
+
if (
|
|
379
|
+
status === 'idle' &&
|
|
380
|
+
distance < 300 &&
|
|
381
|
+
enemy.status === 'casting' &&
|
|
382
|
+
enemy.castingSpell === 'missile'
|
|
383
|
+
)
|
|
384
|
+
{
|
|
385
|
+
const remainingCast = (enemy.castDuration ?? 0) - (enemy.castProgress ?? 0);
|
|
386
|
+
const estimatedFlightTicks = distance / 8;
|
|
387
|
+
const ticksUntilHit = remainingCast + estimatedFlightTicks;
|
|
388
|
+
|
|
389
|
+
if (
|
|
390
|
+
ticksUntilHit >= RULES.SHIELD_CAST_TIME &&
|
|
391
|
+
ticksUntilHit < RULES.SHIELD_CAST_TIME + 15
|
|
392
|
+
)
|
|
393
|
+
{
|
|
394
|
+
return shield();
|
|
339
395
|
}
|
|
340
396
|
}
|
|
341
397
|
|
|
342
398
|
// Movement: smart strafe + maintain optimal distance
|
|
343
|
-
const dodgeDirection = getDodgeDirectionForMovement(threats,
|
|
399
|
+
const dodgeDirection = getDodgeDirectionForMovement(threats, position);
|
|
344
400
|
const move = combatMove(
|
|
345
|
-
|
|
401
|
+
position,
|
|
346
402
|
enemy,
|
|
347
403
|
distance,
|
|
348
404
|
targetDistance,
|
|
349
405
|
dodgeDirection,
|
|
350
|
-
|
|
406
|
+
tick,
|
|
351
407
|
strafeCyclePeriod,
|
|
352
408
|
strafeIntensity,
|
|
353
409
|
approachSpeed,
|
|
354
410
|
distanceDeadZone,
|
|
411
|
+
centerBias,
|
|
355
412
|
);
|
|
356
|
-
|
|
413
|
+
const safeMove = keepOffWalls(position, move);
|
|
357
414
|
// === OFFENSE ===
|
|
358
|
-
if (
|
|
415
|
+
if (status === 'idle')
|
|
359
416
|
{
|
|
417
|
+
// Pre-cast wall discipline: don't commit to a cast we can't react out of while
|
|
418
|
+
// pinned near a wall — step toward center first so knockback can't reach lava.
|
|
419
|
+
const reposition = getPreCastReposition(position, enemy.position);
|
|
420
|
+
if (reposition) return moveAction(reposition.x, reposition.y);
|
|
421
|
+
|
|
360
422
|
// Interrupt if enemy is casting (quick attack)
|
|
361
|
-
if (enemy.
|
|
423
|
+
if (enemy.status === 'casting' && distance < interruptRange)
|
|
362
424
|
{
|
|
363
425
|
const castTime = getMissileCastTime(QUICK_CONFIG);
|
|
364
426
|
if (isSafeToAttack(threats, castTime))
|
|
365
427
|
{
|
|
366
|
-
return
|
|
367
|
-
move,
|
|
368
|
-
startCast: {
|
|
369
|
-
spell: 'missile',
|
|
370
|
-
config: QUICK_CONFIG,
|
|
371
|
-
missileAI: StandardHoming,
|
|
372
|
-
direction: angleTo(state.position, enemy.position),
|
|
373
|
-
},
|
|
374
|
-
};
|
|
428
|
+
return missile(QUICK_CONFIG, StandardHoming, angleTo(position, enemy.position)).move(safeMove.x, safeMove.y);
|
|
375
429
|
}
|
|
376
430
|
}
|
|
377
431
|
|
|
@@ -381,15 +435,7 @@ export const Battlemage: WizardFunction = ({state}) =>
|
|
|
381
435
|
const heavyCastTime = getMissileCastTime(HEAVY_CONFIG);
|
|
382
436
|
if (isSafeToAttack(threats, heavyCastTime))
|
|
383
437
|
{
|
|
384
|
-
return
|
|
385
|
-
move,
|
|
386
|
-
startCast: {
|
|
387
|
-
spell: 'missile',
|
|
388
|
-
config: HEAVY_CONFIG,
|
|
389
|
-
missileAI: StandardHoming,
|
|
390
|
-
direction: angleTo(state.position, enemy.position),
|
|
391
|
-
},
|
|
392
|
-
};
|
|
438
|
+
return missile(HEAVY_CONFIG, StandardHoming, angleTo(position, enemy.position)).move(safeMove.x, safeMove.y);
|
|
393
439
|
}
|
|
394
440
|
|
|
395
441
|
// Fallback: quick homing missile when heavy isn't safe (only if in range)
|
|
@@ -399,15 +445,7 @@ export const Battlemage: WizardFunction = ({state}) =>
|
|
|
399
445
|
isSafeToAttack(threats, quickCastTime)
|
|
400
446
|
)
|
|
401
447
|
{
|
|
402
|
-
return
|
|
403
|
-
move,
|
|
404
|
-
startCast: {
|
|
405
|
-
spell: 'missile',
|
|
406
|
-
config: QUICK_CONFIG,
|
|
407
|
-
missileAI: StandardHoming,
|
|
408
|
-
direction: angleTo(state.position, enemy.position),
|
|
409
|
-
},
|
|
410
|
-
};
|
|
448
|
+
return missile(QUICK_CONFIG, StandardHoming, angleTo(position, enemy.position)).move(safeMove.x, safeMove.y);
|
|
411
449
|
}
|
|
412
450
|
|
|
413
451
|
// Not safe to attack — wait for threat to pass, then fire
|
|
@@ -416,18 +454,15 @@ export const Battlemage: WizardFunction = ({state}) =>
|
|
|
416
454
|
|
|
417
455
|
// === FALLBACK DEFENSE: Shield when no attack window exists ===
|
|
418
456
|
if (
|
|
419
|
-
|
|
457
|
+
status === 'idle' &&
|
|
420
458
|
urgentThreat &&
|
|
421
459
|
!urgentThreat.bestDodgeDirection &&
|
|
422
460
|
urgentThreat.canBlockInTime &&
|
|
423
461
|
urgentThreat.ticksToStartShield <= 3
|
|
424
462
|
)
|
|
425
463
|
{
|
|
426
|
-
return
|
|
427
|
-
move: {x: 0, y: 0},
|
|
428
|
-
startCast: {spell: 'shield'},
|
|
429
|
-
};
|
|
464
|
+
return shield();
|
|
430
465
|
}
|
|
431
466
|
|
|
432
|
-
return
|
|
433
|
-
}
|
|
467
|
+
return moveAction(safeMove.x, safeMove.y);
|
|
468
|
+
}
|