@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,13 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
MissileAIFunction,
|
|
4
|
-
MissileActions,
|
|
5
|
-
} from '../../types.js';
|
|
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';
|
|
6
3
|
import {angleTo} from '../../utils/angles.js';
|
|
7
4
|
import {distanceTo} from '../../utils/distance.js';
|
|
8
|
-
import {
|
|
5
|
+
import {RULES, ARENA_MIN, ARENA_MAX} from '../../rules.js';
|
|
9
6
|
import {
|
|
10
|
-
getThreats,
|
|
11
7
|
getMostUrgentThreat,
|
|
12
8
|
getDodgeDirectionForMovement,
|
|
13
9
|
fitMissileToBudget,
|
|
@@ -17,6 +13,8 @@ import {
|
|
|
17
13
|
MIN_BLINK_ESCAPE_DISTANCE,
|
|
18
14
|
MAX_CAST_BUDGET,
|
|
19
15
|
MIN_USEFUL_DAMAGE,
|
|
16
|
+
keepOffWalls,
|
|
17
|
+
getPreCastReposition,
|
|
20
18
|
} from '../shared.js';
|
|
21
19
|
import {useParam} from '../../engine/params-runtime.js';
|
|
22
20
|
const WALL_BUFFER = 60;
|
|
@@ -26,24 +24,25 @@ const SHIELD_BUFFER = 3;
|
|
|
26
24
|
/**
|
|
27
25
|
* Standard homing missile AI.
|
|
28
26
|
*/
|
|
29
|
-
const StandardHoming:
|
|
27
|
+
const StandardHoming: MissileFunction = () =>
|
|
30
28
|
{
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
};
|
|
29
|
+
const ctx = getMissileContext();
|
|
30
|
+
const enemy = ctx.worldState.enemies[0];
|
|
31
|
+
return enemy ? turnToward(enemy.position.x, enemy.position.y) : flyStraight();
|
|
35
32
|
};
|
|
36
33
|
|
|
34
|
+
const StraightAI: MissileFunction = () => flyStraight();
|
|
35
|
+
|
|
37
36
|
/**
|
|
38
37
|
* Check if near wall.
|
|
39
38
|
*/
|
|
40
39
|
function isNearWall(position: {x: number; y: number}): boolean
|
|
41
40
|
{
|
|
42
41
|
return (
|
|
43
|
-
position.x < WALL_BUFFER ||
|
|
44
|
-
position.x >
|
|
45
|
-
position.y < WALL_BUFFER ||
|
|
46
|
-
position.y >
|
|
42
|
+
position.x < ARENA_MIN + WALL_BUFFER ||
|
|
43
|
+
position.x > ARENA_MAX - WALL_BUFFER ||
|
|
44
|
+
position.y < ARENA_MIN + WALL_BUFFER ||
|
|
45
|
+
position.y > ARENA_MAX - WALL_BUFFER
|
|
47
46
|
);
|
|
48
47
|
}
|
|
49
48
|
|
|
@@ -61,12 +60,22 @@ function combatMove(
|
|
|
61
60
|
strafeIntensity: number,
|
|
62
61
|
approachSpeed: number,
|
|
63
62
|
distanceDeadZone: number,
|
|
64
|
-
|
|
63
|
+
centerBias: number,
|
|
64
|
+
): {x: number; y: number}
|
|
65
65
|
{
|
|
66
66
|
const deltaX = enemy.position.x - position.x;
|
|
67
67
|
const deltaY = enemy.position.y - position.y;
|
|
68
68
|
const normalizedDistance = currentDistance > 0 ? currentDistance : 1;
|
|
69
69
|
|
|
70
|
+
// Approach the center-side of the enemy so knockback pushes toward center, not lava.
|
|
71
|
+
const center = (ARENA_MIN + ARENA_MAX) / 2;
|
|
72
|
+
const toCenterX = center - enemy.position.x;
|
|
73
|
+
const toCenterY = center - enemy.position.y;
|
|
74
|
+
const toCenterDist = Math.sqrt(toCenterX * toCenterX + toCenterY * toCenterY) || 1;
|
|
75
|
+
const approachDirX = deltaX / normalizedDistance + (toCenterX / toCenterDist) * centerBias;
|
|
76
|
+
const approachDirY = deltaY / normalizedDistance + (toCenterY / toCenterDist) * centerBias;
|
|
77
|
+
const approachMag = Math.sqrt(approachDirX * approachDirX + approachDirY * approachDirY) || 1;
|
|
78
|
+
|
|
70
79
|
// Perpendicular directions
|
|
71
80
|
const strafeClockwise = {
|
|
72
81
|
x: -deltaY / normalizedDistance,
|
|
@@ -94,16 +103,16 @@ function combatMove(
|
|
|
94
103
|
y: position.y + strafeCounterClockwise.y * 100,
|
|
95
104
|
};
|
|
96
105
|
const scoreClockwise = Math.min(
|
|
97
|
-
futureClockwise.x,
|
|
98
|
-
|
|
99
|
-
futureClockwise.y,
|
|
100
|
-
|
|
106
|
+
futureClockwise.x - ARENA_MIN,
|
|
107
|
+
ARENA_MAX - futureClockwise.x,
|
|
108
|
+
futureClockwise.y - ARENA_MIN,
|
|
109
|
+
ARENA_MAX - futureClockwise.y,
|
|
101
110
|
);
|
|
102
111
|
const scoreCounterClockwise = Math.min(
|
|
103
|
-
futureCounterClockwise.x,
|
|
104
|
-
|
|
105
|
-
futureCounterClockwise.y,
|
|
106
|
-
|
|
112
|
+
futureCounterClockwise.x - ARENA_MIN,
|
|
113
|
+
ARENA_MAX - futureCounterClockwise.x,
|
|
114
|
+
futureCounterClockwise.y - ARENA_MIN,
|
|
115
|
+
ARENA_MAX - futureCounterClockwise.y,
|
|
107
116
|
);
|
|
108
117
|
strafeDirection =
|
|
109
118
|
scoreClockwise > scoreCounterClockwise
|
|
@@ -118,34 +127,34 @@ function combatMove(
|
|
|
118
127
|
|
|
119
128
|
if (dodgeDirection)
|
|
120
129
|
{
|
|
121
|
-
return {x: strafeDirection.x
|
|
130
|
+
return {x: strafeDirection.x, y: strafeDirection.y};
|
|
122
131
|
}
|
|
123
132
|
|
|
124
133
|
let moveX = strafeDirection.x * strafeIntensity;
|
|
125
134
|
let moveY = strafeDirection.y * strafeIntensity;
|
|
126
135
|
|
|
127
|
-
if (currentDistance > targetDistance + distanceDeadZone)
|
|
136
|
+
if (currentDistance > targetDistance + distanceDeadZone)
|
|
128
137
|
{
|
|
129
|
-
moveX += (
|
|
130
|
-
moveY += (
|
|
138
|
+
moveX += (approachDirX / approachMag) * approachSpeed;
|
|
139
|
+
moveY += (approachDirY / approachMag) * approachSpeed;
|
|
131
140
|
}
|
|
132
141
|
else if (currentDistance < targetDistance - distanceDeadZone)
|
|
133
142
|
{
|
|
134
143
|
let retreatX = -(deltaX / normalizedDistance) * approachSpeed;
|
|
135
144
|
let retreatY = -(deltaY / normalizedDistance) * approachSpeed;
|
|
136
|
-
if (position.x < WALL_BUFFER && retreatX < 0) retreatX = 0;
|
|
137
|
-
if (position.x >
|
|
138
|
-
if (position.y < WALL_BUFFER && retreatY < 0) retreatY = 0;
|
|
139
|
-
if (position.y >
|
|
145
|
+
if (position.x < ARENA_MIN + WALL_BUFFER && retreatX < 0) retreatX = 0;
|
|
146
|
+
if (position.x > ARENA_MAX - WALL_BUFFER && retreatX > 0) retreatX = 0;
|
|
147
|
+
if (position.y < ARENA_MIN + WALL_BUFFER && retreatY < 0) retreatY = 0;
|
|
148
|
+
if (position.y > ARENA_MAX - WALL_BUFFER && retreatY > 0) retreatY = 0;
|
|
140
149
|
moveX += retreatX;
|
|
141
150
|
moveY += retreatY;
|
|
142
151
|
}
|
|
143
152
|
|
|
144
153
|
const magnitude = Math.sqrt(moveX * moveX + moveY * moveY);
|
|
145
|
-
if (magnitude >
|
|
154
|
+
if (magnitude > 1)
|
|
146
155
|
{
|
|
147
|
-
moveX =
|
|
148
|
-
moveY =
|
|
156
|
+
moveX = moveX / magnitude;
|
|
157
|
+
moveY = moveY / magnitude;
|
|
149
158
|
}
|
|
150
159
|
|
|
151
160
|
return {x: moveX, y: moveY};
|
|
@@ -167,36 +176,49 @@ function combatMove(
|
|
|
167
176
|
*
|
|
168
177
|
* TIER: 2 (enhanced Battlemage)
|
|
169
178
|
*/
|
|
170
|
-
export
|
|
179
|
+
export function Warmage(): FinalAction
|
|
171
180
|
{
|
|
172
|
-
const
|
|
181
|
+
const position = usePosition();
|
|
182
|
+
const enemy = useEnemy();
|
|
183
|
+
const status = useStatus();
|
|
184
|
+
const blinkCooldown = useBlinkCooldown();
|
|
185
|
+
const health = useHealth();
|
|
186
|
+
const tick = useTick();
|
|
187
|
+
const threats = useThreats();
|
|
188
|
+
const projectiles = useProjectiles();
|
|
189
|
+
const lastMissileConfig = useLastMissileConfig();
|
|
190
|
+
const castingSpell = useCastingSpell();
|
|
191
|
+
const castProgressData = useCastProgress();
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
const targetDistance = useParam('targetDistance', 154, {
|
|
173
195
|
range: 150,
|
|
174
196
|
min: 0,
|
|
175
197
|
});
|
|
176
|
-
const minTurnRate = useParam('minTurnRate', 0.
|
|
177
|
-
const blinkEscapeDistance = useParam('blinkEscapeDistance',
|
|
198
|
+
const minTurnRate = useParam('minTurnRate', 0.8, {range: 1.5, min: 0, steps: 5});
|
|
199
|
+
const blinkEscapeDistance = useParam('blinkEscapeDistance', 220, {
|
|
178
200
|
range: 190,
|
|
179
201
|
min: 0,
|
|
180
202
|
steps: 7,
|
|
181
203
|
});
|
|
182
|
-
const strafeCyclePeriod = useParam('strafeCyclePeriod',
|
|
204
|
+
const strafeCyclePeriod = useParam('strafeCyclePeriod', 136, {
|
|
183
205
|
range: 75,
|
|
184
206
|
min: 80,
|
|
185
207
|
max: 300,
|
|
186
208
|
});
|
|
187
|
-
const strafeIntensity = useParam('strafeIntensity',
|
|
188
|
-
range:
|
|
189
|
-
min:
|
|
190
|
-
max:
|
|
209
|
+
const strafeIntensity = useParam('strafeIntensity', 0.64, {
|
|
210
|
+
range: 0.4,
|
|
211
|
+
min: 0.2,
|
|
212
|
+
max: 1,
|
|
191
213
|
steps: 7,
|
|
192
214
|
});
|
|
193
|
-
const approachSpeed = useParam('approachSpeed',
|
|
194
|
-
range:
|
|
195
|
-
min:
|
|
196
|
-
max:
|
|
215
|
+
const approachSpeed = useParam('approachSpeed', 0.74, {
|
|
216
|
+
range: 0.3,
|
|
217
|
+
min: 0.1,
|
|
218
|
+
max: 0.8,
|
|
197
219
|
steps: 7,
|
|
198
220
|
});
|
|
199
|
-
const distanceDeadZone = useParam('distanceDeadZone',
|
|
221
|
+
const distanceDeadZone = useParam('distanceDeadZone', 29, {
|
|
200
222
|
range: 30,
|
|
201
223
|
min: 10,
|
|
202
224
|
max: 80,
|
|
@@ -207,7 +229,7 @@ export const Warmage: WizardFunction = ({state}) =>
|
|
|
207
229
|
min: 50,
|
|
208
230
|
max: 300,
|
|
209
231
|
});
|
|
210
|
-
const blinkWindowMax = useParam('blinkWindowMax',
|
|
232
|
+
const blinkWindowMax = useParam('blinkWindowMax', 32, {
|
|
211
233
|
range: 30,
|
|
212
234
|
min: 15,
|
|
213
235
|
max: 80,
|
|
@@ -219,72 +241,89 @@ export const Warmage: WizardFunction = ({state}) =>
|
|
|
219
241
|
max: 100,
|
|
220
242
|
steps: 7,
|
|
221
243
|
});
|
|
222
|
-
const attackRange = useParam('attackRange',
|
|
244
|
+
const attackRange = useParam('attackRange', 425, {
|
|
223
245
|
range: 150,
|
|
224
246
|
min: 200,
|
|
225
247
|
max: 700,
|
|
226
248
|
});
|
|
227
|
-
const flightTimeDivisor = useParam('flightTimeDivisor',
|
|
249
|
+
const flightTimeDivisor = useParam('flightTimeDivisor', 7, {
|
|
228
250
|
range: 4,
|
|
229
251
|
min: 2,
|
|
230
252
|
max: 12,
|
|
231
253
|
steps: 5,
|
|
232
254
|
});
|
|
255
|
+
const centerBias = useParam('centerBias', 0.57, {
|
|
256
|
+
range: 0.4,
|
|
257
|
+
min: 0,
|
|
258
|
+
max: 0.8,
|
|
259
|
+
steps: 5,
|
|
260
|
+
});
|
|
261
|
+
const maxBlinkDistance = useParam('maxBlinkDistance', 224, {
|
|
262
|
+
range: 100,
|
|
263
|
+
min: 50,
|
|
264
|
+
max: 300,
|
|
265
|
+
steps: 7,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const distance = distanceTo(position, enemy.position);
|
|
233
269
|
|
|
234
|
-
|
|
235
|
-
if (!enemy)
|
|
270
|
+
function capBlinkDistance(target: {x: number; y: number}): {x: number; y: number}
|
|
236
271
|
{
|
|
237
|
-
|
|
272
|
+
const dx = target.x - position.x;
|
|
273
|
+
const dy = target.y - position.y;
|
|
274
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
275
|
+
if (dist <= maxBlinkDistance) return target;
|
|
276
|
+
const scale = maxBlinkDistance / dist;
|
|
277
|
+
return {x: position.x + dx * scale, y: position.y + dy * scale};
|
|
238
278
|
}
|
|
239
279
|
|
|
240
|
-
const distance = distanceTo(state.position, enemy.position);
|
|
241
|
-
|
|
242
280
|
// Analyze threats using simulation
|
|
243
|
-
const threats = getThreats(state);
|
|
244
281
|
const urgentThreat = getMostUrgentThreat(threats);
|
|
245
282
|
|
|
246
283
|
// === DEFENSE: Handle channeling ===
|
|
247
|
-
if (
|
|
284
|
+
if (status === 'channeling')
|
|
248
285
|
{
|
|
249
286
|
if (!urgentThreat || urgentThreat.ticksToImpact > shieldCancelThreshold)
|
|
250
287
|
{
|
|
251
|
-
return
|
|
288
|
+
return cancel();
|
|
252
289
|
}
|
|
253
|
-
return
|
|
290
|
+
return idle();
|
|
254
291
|
}
|
|
255
292
|
|
|
256
293
|
// === DEFENSE: Cancel missile cast only for LETHAL incoming damage ===
|
|
257
294
|
// DPS trade: we accepted the hit when we started casting. Only cancel to survive.
|
|
258
295
|
if (
|
|
259
|
-
|
|
260
|
-
|
|
296
|
+
status === 'casting' &&
|
|
297
|
+
castingSpell === 'missile' &&
|
|
261
298
|
urgentThreat &&
|
|
262
|
-
urgentThreat.projectile.damage >=
|
|
299
|
+
urgentThreat.projectile.damage >= health
|
|
263
300
|
)
|
|
264
301
|
{
|
|
265
|
-
const remainingCast = (
|
|
302
|
+
const remainingCast = ((castProgressData?.total) ?? 0) - ((castProgressData?.current) ?? 0);
|
|
266
303
|
if (
|
|
267
304
|
urgentThreat.ticksToImpact <=
|
|
268
|
-
remainingCast + GCD_DURATION + SHIELD_CAST_TIME + 1
|
|
305
|
+
remainingCast + RULES.GCD_DURATION + RULES.SHIELD_CAST_TIME + 1
|
|
269
306
|
)
|
|
270
307
|
{
|
|
271
308
|
const dodgeDirection = getDodgeDirectionForMovement(
|
|
272
309
|
threats,
|
|
273
|
-
|
|
310
|
+
position,
|
|
274
311
|
);
|
|
275
|
-
const
|
|
276
|
-
|
|
312
|
+
const cancelMove = combatMove(
|
|
313
|
+
position,
|
|
277
314
|
enemy,
|
|
278
315
|
distance,
|
|
279
316
|
targetDistance,
|
|
280
317
|
dodgeDirection,
|
|
281
|
-
|
|
318
|
+
tick,
|
|
282
319
|
strafeCyclePeriod,
|
|
283
320
|
strafeIntensity,
|
|
284
321
|
approachSpeed,
|
|
285
322
|
distanceDeadZone,
|
|
323
|
+
centerBias,
|
|
286
324
|
);
|
|
287
|
-
|
|
325
|
+
const safeCancelMove = keepOffWalls(position, cancelMove);
|
|
326
|
+
return cancel().move(safeCancelMove.x, safeCancelMove.y);
|
|
288
327
|
}
|
|
289
328
|
}
|
|
290
329
|
|
|
@@ -292,53 +331,49 @@ export const Warmage: WizardFunction = ({state}) =>
|
|
|
292
331
|
// Blink both dodges the incoming missile AND closes distance — better than shielding.
|
|
293
332
|
// Only shield when blink is on cooldown.
|
|
294
333
|
if (
|
|
295
|
-
|
|
334
|
+
status === 'idle' &&
|
|
296
335
|
urgentThreat &&
|
|
297
|
-
|
|
336
|
+
blinkCooldown === 0 &&
|
|
298
337
|
distance > targetDistance - blinkDistanceOffset &&
|
|
299
338
|
urgentThreat.ticksToImpact >= 10 &&
|
|
300
339
|
urgentThreat.ticksToImpact <= blinkWindowMax
|
|
301
340
|
)
|
|
302
341
|
{
|
|
303
|
-
const
|
|
342
|
+
const rawBlinkTarget =
|
|
304
343
|
getBlinkToDecreaseDistance(
|
|
305
|
-
|
|
344
|
+
position,
|
|
306
345
|
enemy.position,
|
|
307
|
-
|
|
346
|
+
projectiles,
|
|
308
347
|
) ?? enemy.position;
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
startCast: {spell: 'blink', target: blinkTarget},
|
|
312
|
-
};
|
|
348
|
+
const blinkTarget = capBlinkDistance(rawBlinkTarget);
|
|
349
|
+
return blink(blinkTarget.x, blinkTarget.y);
|
|
313
350
|
}
|
|
314
351
|
|
|
315
352
|
// === BLINK ESCAPE: Enemy too close, blink away to reestablish dueling range ===
|
|
316
353
|
// Warmage is a duelist at 280 range — if enemy closes to melee, blink out.
|
|
317
354
|
if (
|
|
318
|
-
|
|
355
|
+
status === 'idle' &&
|
|
319
356
|
distance < blinkEscapeDistance &&
|
|
320
|
-
|
|
357
|
+
blinkCooldown === 0 &&
|
|
321
358
|
(!urgentThreat || urgentThreat.bestDodgeDirection !== null)
|
|
322
359
|
)
|
|
323
360
|
{
|
|
324
361
|
const escapeTarget = getBlinkToIncreaseDistance(
|
|
325
|
-
|
|
362
|
+
position,
|
|
326
363
|
enemy.position,
|
|
327
|
-
|
|
364
|
+
projectiles,
|
|
328
365
|
MIN_BLINK_ESCAPE_DISTANCE,
|
|
329
366
|
);
|
|
330
|
-
if (escapeTarget)
|
|
367
|
+
if (escapeTarget)
|
|
331
368
|
{
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
startCast: {spell: 'blink', target: escapeTarget},
|
|
335
|
-
};
|
|
369
|
+
const capped = capBlinkDistance(escapeTarget);
|
|
370
|
+
return blink(capped.x, capped.y);
|
|
336
371
|
}
|
|
337
372
|
}
|
|
338
373
|
|
|
339
374
|
// === DEFENSE: Shield undodgeable or high-damage homing threats (only when blink can't handle it) ===
|
|
340
375
|
if (
|
|
341
|
-
|
|
376
|
+
status === 'idle' &&
|
|
342
377
|
urgentThreat &&
|
|
343
378
|
(!urgentThreat.bestDodgeDirection || shouldForceShield(urgentThreat))
|
|
344
379
|
)
|
|
@@ -346,38 +381,60 @@ export const Warmage: WizardFunction = ({state}) =>
|
|
|
346
381
|
if (
|
|
347
382
|
urgentThreat.canBlockInTime &&
|
|
348
383
|
urgentThreat.ticksToStartShield <= 3 &&
|
|
349
|
-
(
|
|
384
|
+
(blinkCooldown > 0 || urgentThreat.ticksToImpact < 10)
|
|
350
385
|
)
|
|
351
386
|
{
|
|
352
|
-
return
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
387
|
+
return shield();
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// === Preemptive shield — shield when enemy is casting a missile at close range ===
|
|
392
|
+
if (
|
|
393
|
+
status === 'idle' &&
|
|
394
|
+
distance < 300 &&
|
|
395
|
+
enemy.status === 'casting' &&
|
|
396
|
+
enemy.castingSpell === 'missile'
|
|
397
|
+
)
|
|
398
|
+
{
|
|
399
|
+
const remainingCast = (enemy.castDuration ?? 0) - (enemy.castProgress ?? 0);
|
|
400
|
+
const estimatedFlightTicks = distance / 8;
|
|
401
|
+
const ticksUntilHit = remainingCast + estimatedFlightTicks;
|
|
402
|
+
|
|
403
|
+
if (
|
|
404
|
+
ticksUntilHit >= RULES.SHIELD_CAST_TIME &&
|
|
405
|
+
ticksUntilHit < RULES.SHIELD_CAST_TIME + 15
|
|
406
|
+
)
|
|
407
|
+
{
|
|
408
|
+
return shield();
|
|
356
409
|
}
|
|
357
410
|
}
|
|
358
411
|
|
|
359
412
|
// Movement
|
|
360
|
-
const dodgeDirection = getDodgeDirectionForMovement(threats,
|
|
413
|
+
const dodgeDirection = getDodgeDirectionForMovement(threats, position);
|
|
361
414
|
const move = combatMove(
|
|
362
|
-
|
|
415
|
+
position,
|
|
363
416
|
enemy,
|
|
364
417
|
distance,
|
|
365
418
|
targetDistance,
|
|
366
419
|
dodgeDirection,
|
|
367
|
-
|
|
420
|
+
tick,
|
|
368
421
|
strafeCyclePeriod,
|
|
369
422
|
strafeIntensity,
|
|
370
423
|
approachSpeed,
|
|
371
424
|
distanceDeadZone,
|
|
425
|
+
centerBias,
|
|
372
426
|
);
|
|
373
|
-
|
|
427
|
+
const safeMove = keepOffWalls(position, move);
|
|
374
428
|
// === OFFENSE: Adaptive missile fitting ===
|
|
375
|
-
if (
|
|
429
|
+
if (status === 'idle' && distance < attackRange)
|
|
376
430
|
{
|
|
431
|
+
const reposition = getPreCastReposition(position, enemy.position);
|
|
432
|
+
if (reposition) return moveAction(reposition.x, reposition.y);
|
|
433
|
+
|
|
377
434
|
let nextThreatTime = urgentThreat?.ticksToImpact ?? Infinity;
|
|
378
435
|
|
|
379
436
|
// Account for enemy's active cast — their missile will arrive after cast completes + flight time
|
|
380
|
-
if (enemy.
|
|
437
|
+
if (enemy.status === 'casting' && enemy.castingSpell === 'missile')
|
|
381
438
|
{
|
|
382
439
|
const remainingEnemyCast =
|
|
383
440
|
(enemy.castDuration ?? 0) - (enemy.castProgress ?? 0);
|
|
@@ -389,7 +446,7 @@ export const Warmage: WizardFunction = ({state}) =>
|
|
|
389
446
|
}
|
|
390
447
|
|
|
391
448
|
const safeBudget =
|
|
392
|
-
nextThreatTime - GCD_DURATION - SHIELD_CAST_TIME - SHIELD_BUFFER;
|
|
449
|
+
nextThreatTime - RULES.GCD_DURATION - RULES.SHIELD_CAST_TIME - SHIELD_BUFFER;
|
|
393
450
|
const budgetTicks = Math.min(safeBudget, MAX_CAST_BUDGET);
|
|
394
451
|
|
|
395
452
|
// Warmage moves while casting, so always need homing to compensate
|
|
@@ -398,21 +455,12 @@ export const Warmage: WizardFunction = ({state}) =>
|
|
|
398
455
|
const config = fitMissileToBudget(budgetTicks, distance, {
|
|
399
456
|
minTurnRate,
|
|
400
457
|
maxDamage: enemyHP,
|
|
401
|
-
lastMissileConfig:
|
|
458
|
+
lastMissileConfig: lastMissileConfig,
|
|
402
459
|
});
|
|
403
460
|
|
|
404
461
|
if (config && config.damage >= minDmg)
|
|
405
462
|
{
|
|
406
|
-
return
|
|
407
|
-
move,
|
|
408
|
-
startCast: {
|
|
409
|
-
spell: 'missile',
|
|
410
|
-
config,
|
|
411
|
-
missileAI:
|
|
412
|
-
config.turnRate > 0 ? StandardHoming : (): MissileActions => ({}),
|
|
413
|
-
direction: angleTo(state.position, enemy.position),
|
|
414
|
-
},
|
|
415
|
-
};
|
|
463
|
+
return missile(config, config.turnRate > 0 ? StandardHoming : StraightAI, angleTo(position, enemy.position)).move(safeMove.x, safeMove.y);
|
|
416
464
|
}
|
|
417
465
|
|
|
418
466
|
// Budget too small for useful missile — wait for threat to pass, then fire
|
|
@@ -421,18 +469,15 @@ export const Warmage: WizardFunction = ({state}) =>
|
|
|
421
469
|
// === FALLBACK DEFENSE: Shield when no attack window exists ===
|
|
422
470
|
// If all offense checks failed and there's an undodgeable threat, shield it.
|
|
423
471
|
if (
|
|
424
|
-
|
|
472
|
+
status === 'idle' &&
|
|
425
473
|
urgentThreat &&
|
|
426
474
|
!urgentThreat.bestDodgeDirection &&
|
|
427
475
|
urgentThreat.canBlockInTime &&
|
|
428
476
|
urgentThreat.ticksToStartShield <= 3
|
|
429
477
|
)
|
|
430
478
|
{
|
|
431
|
-
return
|
|
432
|
-
move: {x: 0, y: 0},
|
|
433
|
-
startCast: {spell: 'shield'},
|
|
434
|
-
};
|
|
479
|
+
return shield();
|
|
435
480
|
}
|
|
436
481
|
|
|
437
|
-
return
|
|
438
|
-
}
|
|
482
|
+
return moveAction(safeMove.x, safeMove.y);
|
|
483
|
+
}
|