@vibemancer/core 0.1.0 → 0.1.1
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 +28 -28
- package/dist/{chunk-L7Z7OFXD.js → chunk-7VDJHO22.js} +3 -3
- package/dist/chunk-7VDJHO22.js.map +1 -0
- package/dist/index-browser.d.ts +1 -1
- package/dist/index-browser.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +79 -78
- package/src/bots/berserker/01_Stormchaser.ts +457 -457
- package/src/bots/berserker/02_Stormcaller.ts +417 -417
- package/src/bots/berserker/03_Stormforger.ts +481 -481
- package/src/bots/caster/01_Flamecaller.ts +286 -286
- package/src/bots/caster/02_Pyromancer.ts +350 -350
- package/src/bots/caster/03_Infernalist.ts +492 -492
- package/src/bots/defensive/01_Turtle.ts +151 -151
- package/src/bots/defensive/02_Sentinel.ts +134 -134
- package/src/bots/defensive/03_Golem.ts +357 -357
- package/src/bots/duelist/01_Battlemage.ts +433 -433
- package/src/bots/duelist/02_Warmage.ts +438 -438
- package/src/bots/duelist/03_Archmage.ts +588 -588
- package/src/bots/homing/01_Bonemancer.ts +67 -67
- package/src/bots/homing/02_Lich.ts +356 -356
- package/src/bots/homing/03_Archlich.ts +220 -220
- package/src/bots/kiter/01_Spellspinner.ts +398 -398
- package/src/bots/kiter/02_Spellweaver.ts +378 -378
- package/src/bots/kiter/03_Spellbinder.ts +448 -448
- package/src/bots/melee/01_Shadowblade.ts +270 -270
- package/src/bots/melee/02_Nightblade.ts +437 -437
- package/src/bots/melee/03_Voidblade.ts +582 -582
- package/src/bots/sniper/01_Spellshot.ts +385 -385
- package/src/bots/sniper/02_Spelltracer.ts +441 -441
- package/src/bots/sniper/03_Spellseeker.ts +546 -546
- package/src/bots/standalone/Critter.ts +89 -89
- package/src/bots/standalone/Doombringer.ts +91 -91
- package/src/bots/standalone/Hogger.ts +228 -228
- package/src/bots/standalone/Rookie.ts +50 -50
- package/src/bots/standalone/TargetDummy.ts +21 -21
- package/src/bots/test/cheater.ts +405 -405
- package/src/bots/test/crasher.ts +81 -81
- package/src/engine/hooks-runtime.ts +394 -394
- package/src/engine/manual-match.ts +289 -289
- package/src/engine/missile-templates.ts +155 -155
- package/src/engine/physics.ts +143 -143
- package/src/engine/simulation.ts +828 -828
- package/src/engine/spells.ts +128 -128
- package/src/engine-version.ts +1 -1
- package/src/index.ts +23 -23
- package/src/rules.ts +254 -254
- package/src/types.ts +193 -193
- package/src/utils/index.ts +6 -6
- package/dist/chunk-L7Z7OFXD.js.map +0 -1
|
@@ -1,492 +1,492 @@
|
|
|
1
|
-
import {WizardFunction, MissileAIFunction} from '../../types.js';
|
|
2
|
-
import {angleTo} from '../../utils/angles.js';
|
|
3
|
-
import {distanceTo} from '../../utils/distance.js';
|
|
4
|
-
import {
|
|
5
|
-
ARENA_SIZE,
|
|
6
|
-
GCD_DURATION,
|
|
7
|
-
SHIELD_CAST_TIME,
|
|
8
|
-
BLINK_CAST_TIME,
|
|
9
|
-
} from '../../rules.js';
|
|
10
|
-
import {getMissileCastTime, getLeadPosition} from '../../utils/combat.js';
|
|
11
|
-
import {
|
|
12
|
-
getThreats,
|
|
13
|
-
getMostUrgentThreat,
|
|
14
|
-
getDodgeDirectionForMovement,
|
|
15
|
-
isSafeToAttack,
|
|
16
|
-
getBlinkToIncreaseDistance,
|
|
17
|
-
fitMissileToBudget,
|
|
18
|
-
shouldForceShield,
|
|
19
|
-
MIN_BLINK_ESCAPE_DISTANCE,
|
|
20
|
-
MAX_CAST_BUDGET,
|
|
21
|
-
MIN_USEFUL_DAMAGE,
|
|
22
|
-
getClosingRate,
|
|
23
|
-
getEnemyVulnerabilityTicks,
|
|
24
|
-
} from '../shared.js';
|
|
25
|
-
import {useParam} from '../../engine/params-runtime.js';
|
|
26
|
-
|
|
27
|
-
const WALL_BUFFER = 60;
|
|
28
|
-
const SHIELD_BUFFER = 3;
|
|
29
|
-
|
|
30
|
-
const HomingAI: MissileAIFunction = ({worldState}) =>
|
|
31
|
-
{
|
|
32
|
-
const enemy = worldState.enemies[0];
|
|
33
|
-
return {turnToward: enemy?.position};
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
const StraightAI: MissileAIFunction = () => ({});
|
|
37
|
-
|
|
38
|
-
function isNearWall(position: {x: number; y: number}): boolean
|
|
39
|
-
{
|
|
40
|
-
return (
|
|
41
|
-
position.x < WALL_BUFFER ||
|
|
42
|
-
position.x > ARENA_SIZE - WALL_BUFFER ||
|
|
43
|
-
position.y < WALL_BUFFER ||
|
|
44
|
-
position.y > ARENA_SIZE - WALL_BUFFER
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function smartStrafe(
|
|
49
|
-
position: {x: number; y: number},
|
|
50
|
-
enemy: {position: {x: number; y: number}},
|
|
51
|
-
dodgeDirection: {x: number; y: number} | null,
|
|
52
|
-
tick: number,
|
|
53
|
-
strafeCyclePeriod: number,
|
|
54
|
-
strafeIntensity: number,
|
|
55
|
-
): {x: number; y: number}
|
|
56
|
-
{
|
|
57
|
-
const deltaX = enemy.position.x - position.x;
|
|
58
|
-
const deltaY = enemy.position.y - position.y;
|
|
59
|
-
const len = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
|
60
|
-
if (len === 0) return {x: 0, y: 0};
|
|
61
|
-
|
|
62
|
-
const cw = {x: -deltaY / len, y: deltaX / len};
|
|
63
|
-
const ccw = {x: deltaY / len, y: -deltaX / len};
|
|
64
|
-
|
|
65
|
-
let dir: {x: number; y: number};
|
|
66
|
-
|
|
67
|
-
if (dodgeDirection)
|
|
68
|
-
{
|
|
69
|
-
dir = dodgeDirection;
|
|
70
|
-
}
|
|
71
|
-
else if (isNearWall(position))
|
|
72
|
-
{
|
|
73
|
-
const fCw = {x: position.x + cw.x * 100, y: position.y + cw.y * 100};
|
|
74
|
-
const fCcw = {x: position.x + ccw.x * 100, y: position.y + ccw.y * 100};
|
|
75
|
-
const sCw = Math.min(fCw.x, ARENA_SIZE - fCw.x, fCw.y, ARENA_SIZE - fCw.y);
|
|
76
|
-
const sCcw = Math.min(
|
|
77
|
-
fCcw.x,
|
|
78
|
-
ARENA_SIZE - fCcw.x,
|
|
79
|
-
fCcw.y,
|
|
80
|
-
ARENA_SIZE - fCcw.y,
|
|
81
|
-
);
|
|
82
|
-
dir = sCw > sCcw ? cw : ccw;
|
|
83
|
-
}
|
|
84
|
-
else
|
|
85
|
-
{
|
|
86
|
-
const cycle = Math.floor(tick / strafeCyclePeriod) % 2;
|
|
87
|
-
dir = cycle === 0 ? cw : ccw;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const intensity = dodgeDirection ? 100 : strafeIntensity;
|
|
91
|
-
return {x: dir.x * intensity, y: dir.y * intensity};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Bot: Infernalist
|
|
96
|
-
*
|
|
97
|
-
* BEHAVIOR: Rapid-fire caster that exploits warmup bonus for accelerating DPS.
|
|
98
|
-
* Fires consistent homing missiles to build warmup, punishes vulnerability windows
|
|
99
|
-
* with warmup-boosted fast casts. Proactive blink kiting when enemy closes.
|
|
100
|
-
*
|
|
101
|
-
* KEY IMPROVEMENTS OVER PYROMANCER:
|
|
102
|
-
* - Warmup exploitation: always passes lastMissileConfig for bonus
|
|
103
|
-
* - Punish mode: straight missiles during enemy vulnerability
|
|
104
|
-
* - Proactive blink kiting: monitors closing rate
|
|
105
|
-
* - Progressive cast-cancel: graduated thresholds
|
|
106
|
-
*
|
|
107
|
-
* PROGRESSION LINE: Flamecaller → Pyromancer → Infernalist
|
|
108
|
-
* TIER: 3 (elite Caster line)
|
|
109
|
-
*/
|
|
110
|
-
export const Infernalist: WizardFunction = ({state}) =>
|
|
111
|
-
{
|
|
112
|
-
const targetDistance = useParam('targetDistance', 285, {range: 225, min: 0});
|
|
113
|
-
const minTurnRate = useParam('minTurnRate', 0.2, {range: 1.5, steps: 5});
|
|
114
|
-
const dangerDistance = useParam('dangerDistance', 222, {
|
|
115
|
-
range: 150,
|
|
116
|
-
min: 0,
|
|
117
|
-
});
|
|
118
|
-
const closingRateThreshold = useParam('closingRateThreshold', 0.94, {
|
|
119
|
-
range: 0.65,
|
|
120
|
-
min: 0,
|
|
121
|
-
steps: 5,
|
|
122
|
-
});
|
|
123
|
-
const punishMinVulnerability = useParam('punishMinVulnerability', 340, {
|
|
124
|
-
range: 30,
|
|
125
|
-
min: 10,
|
|
126
|
-
steps: 5,
|
|
127
|
-
});
|
|
128
|
-
const preferBlinkDodge =
|
|
129
|
-
useParam('preferBlinkDodge', 0, {min: 0, max: 1, steps: 2}) >= 0.5;
|
|
130
|
-
const strafeCyclePeriod = useParam('strafeCyclePeriod', 109, {
|
|
131
|
-
range: 75,
|
|
132
|
-
min: 80,
|
|
133
|
-
max: 250,
|
|
134
|
-
});
|
|
135
|
-
const strafeIntensity = useParam('strafeIntensity', 23, {
|
|
136
|
-
range: 40,
|
|
137
|
-
min: 20,
|
|
138
|
-
max: 100,
|
|
139
|
-
steps: 7,
|
|
140
|
-
});
|
|
141
|
-
const approachSpeed = useParam('approachSpeed', 53, {
|
|
142
|
-
range: 30,
|
|
143
|
-
min: 10,
|
|
144
|
-
max: 70,
|
|
145
|
-
steps: 7,
|
|
146
|
-
});
|
|
147
|
-
const distanceDeadZone = useParam('distanceDeadZone', 34, {
|
|
148
|
-
range: 30,
|
|
149
|
-
min: 5,
|
|
150
|
-
max: 80,
|
|
151
|
-
steps: 7,
|
|
152
|
-
});
|
|
153
|
-
const cancelHeavyDmg = useParam('cancelHeavyDmg', 40, {
|
|
154
|
-
range: 15,
|
|
155
|
-
min: 5,
|
|
156
|
-
max: 40,
|
|
157
|
-
steps: 5,
|
|
158
|
-
});
|
|
159
|
-
const cancelMediumDmg = useParam('cancelMediumDmg', 9, {
|
|
160
|
-
range: 10,
|
|
161
|
-
min: 3,
|
|
162
|
-
max: 25,
|
|
163
|
-
steps: 5,
|
|
164
|
-
});
|
|
165
|
-
const cancelCastRatio = useParam('cancelCastRatio', 0.78, {
|
|
166
|
-
range: 0.4,
|
|
167
|
-
min: 0.3,
|
|
168
|
-
max: 1.0,
|
|
169
|
-
steps: 5,
|
|
170
|
-
});
|
|
171
|
-
const shieldCancelThreshold = useParam('shieldCancelThreshold', 100, {
|
|
172
|
-
range: 75,
|
|
173
|
-
min: 50,
|
|
174
|
-
max: 300,
|
|
175
|
-
});
|
|
176
|
-
const blinkWindowMax = useParam('blinkWindowMax', 75, {
|
|
177
|
-
range: 40,
|
|
178
|
-
min: 20,
|
|
179
|
-
max: 120,
|
|
180
|
-
steps: 7,
|
|
181
|
-
});
|
|
182
|
-
const proactiveBlinkRange = useParam('proactiveBlinkRange', 100, {
|
|
183
|
-
range: 150,
|
|
184
|
-
min: 100,
|
|
185
|
-
max: 500,
|
|
186
|
-
steps: 7,
|
|
187
|
-
});
|
|
188
|
-
const flightTimeDivisor = useParam('flightTimeDivisor', 5.4, {
|
|
189
|
-
range: 4,
|
|
190
|
-
min: 2,
|
|
191
|
-
max: 12,
|
|
192
|
-
steps: 5,
|
|
193
|
-
});
|
|
194
|
-
const safeAttackCastEstimate = useParam('safeAttackCastEstimate', 35, {
|
|
195
|
-
range: 25,
|
|
196
|
-
min: 10,
|
|
197
|
-
max: 80,
|
|
198
|
-
steps: 5,
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
const enemy = state.enemies[0];
|
|
202
|
-
if (!enemy)
|
|
203
|
-
{
|
|
204
|
-
return {move: {x: 0, y: 0}};
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const distance = distanceTo(state.position, enemy.position);
|
|
208
|
-
const threats = getThreats(state);
|
|
209
|
-
const urgentThreat = getMostUrgentThreat(threats);
|
|
210
|
-
const dodgeDirection = getDodgeDirectionForMovement(threats, state.position);
|
|
211
|
-
const strafe = smartStrafe(
|
|
212
|
-
state.position,
|
|
213
|
-
enemy,
|
|
214
|
-
dodgeDirection,
|
|
215
|
-
state.tick,
|
|
216
|
-
strafeCyclePeriod,
|
|
217
|
-
strafeIntensity,
|
|
218
|
-
);
|
|
219
|
-
|
|
220
|
-
// Distance management with wall-aware retreat
|
|
221
|
-
let moveX = strafe.x;
|
|
222
|
-
let moveY = strafe.y;
|
|
223
|
-
|
|
224
|
-
if (!dodgeDirection)
|
|
225
|
-
{
|
|
226
|
-
const deltaX = enemy.position.x - state.position.x;
|
|
227
|
-
const deltaY = enemy.position.y - state.position.y;
|
|
228
|
-
const norm = distance > 0 ? distance : 1;
|
|
229
|
-
|
|
230
|
-
if (distance > targetDistance + distanceDeadZone)
|
|
231
|
-
{
|
|
232
|
-
moveX += (deltaX / norm) * approachSpeed;
|
|
233
|
-
moveY += (deltaY / norm) * approachSpeed;
|
|
234
|
-
}
|
|
235
|
-
else if (distance < targetDistance - distanceDeadZone)
|
|
236
|
-
{
|
|
237
|
-
let rx = -(deltaX / norm) * approachSpeed;
|
|
238
|
-
let ry = -(deltaY / norm) * approachSpeed;
|
|
239
|
-
if (state.position.x < WALL_BUFFER && rx < 0) rx = 0;
|
|
240
|
-
if (state.position.x > ARENA_SIZE - WALL_BUFFER && rx > 0) rx = 0;
|
|
241
|
-
if (state.position.y < WALL_BUFFER && ry < 0) ry = 0;
|
|
242
|
-
if (state.position.y > ARENA_SIZE - WALL_BUFFER && ry > 0) ry = 0;
|
|
243
|
-
moveX += rx;
|
|
244
|
-
moveY += ry;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
const mag = Math.sqrt(moveX * moveX + moveY * moveY);
|
|
249
|
-
if (mag > 100)
|
|
250
|
-
{
|
|
251
|
-
moveX = (moveX / mag) * 100;
|
|
252
|
-
moveY = (moveY / mag) * 100;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
const move = {x: moveX, y: moveY};
|
|
256
|
-
|
|
257
|
-
// === STEP 1: CHANNELING ===
|
|
258
|
-
if (state.state === 'channeling')
|
|
259
|
-
{
|
|
260
|
-
if (!urgentThreat || urgentThreat.ticksToImpact > shieldCancelThreshold)
|
|
261
|
-
{
|
|
262
|
-
return {move, cancel: true};
|
|
263
|
-
}
|
|
264
|
-
return {move: {x: 0, y: 0}};
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// === STEP 2: PROGRESSIVE CAST-CANCEL ===
|
|
268
|
-
if (
|
|
269
|
-
state.state === 'casting' &&
|
|
270
|
-
state.castingSpell === 'missile' &&
|
|
271
|
-
urgentThreat &&
|
|
272
|
-
!urgentThreat.bestDodgeDirection
|
|
273
|
-
)
|
|
274
|
-
{
|
|
275
|
-
const remainingCast = (state.castDuration ?? 0) - (state.castProgress ?? 0);
|
|
276
|
-
const defenseTime =
|
|
277
|
-
state.blinkCooldown === 0
|
|
278
|
-
? BLINK_CAST_TIME
|
|
279
|
-
: SHIELD_CAST_TIME + SHIELD_BUFFER;
|
|
280
|
-
const willArriveInWindow =
|
|
281
|
-
urgentThreat.ticksToImpact <= remainingCast + GCD_DURATION + defenseTime;
|
|
282
|
-
|
|
283
|
-
if (willArriveInWindow)
|
|
284
|
-
{
|
|
285
|
-
const incomingDamage = urgentThreat.projectile.damage;
|
|
286
|
-
const castRatio = (state.castProgress ?? 0) / (state.castDuration ?? 1);
|
|
287
|
-
|
|
288
|
-
if (incomingDamage >= state.health || incomingDamage >= cancelHeavyDmg)
|
|
289
|
-
{
|
|
290
|
-
return {move, cancel: true};
|
|
291
|
-
}
|
|
292
|
-
if (incomingDamage >= cancelMediumDmg && castRatio < cancelCastRatio)
|
|
293
|
-
{
|
|
294
|
-
return {move, cancel: true};
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// === BLINK-DODGE — optimizer-controlled alternative to shield ===
|
|
300
|
-
if (
|
|
301
|
-
preferBlinkDodge &&
|
|
302
|
-
state.state === 'idle' &&
|
|
303
|
-
urgentThreat &&
|
|
304
|
-
state.blinkCooldown === 0 &&
|
|
305
|
-
(!urgentThreat.bestDodgeDirection || shouldForceShield(urgentThreat)) &&
|
|
306
|
-
urgentThreat.ticksToImpact >= BLINK_CAST_TIME &&
|
|
307
|
-
urgentThreat.ticksToImpact <= blinkWindowMax
|
|
308
|
-
)
|
|
309
|
-
{
|
|
310
|
-
const blinkTarget = getBlinkToIncreaseDistance(
|
|
311
|
-
state.position,
|
|
312
|
-
enemy.position,
|
|
313
|
-
state.projectiles,
|
|
314
|
-
MIN_BLINK_ESCAPE_DISTANCE,
|
|
315
|
-
);
|
|
316
|
-
if (blinkTarget)
|
|
317
|
-
{
|
|
318
|
-
return {
|
|
319
|
-
move: {x: 0, y: 0},
|
|
320
|
-
startCast: {spell: 'blink', target: blinkTarget},
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// === STEP 3: SHIELD — undodgeable or high-damage homing threats ===
|
|
326
|
-
if (
|
|
327
|
-
state.state === 'idle' &&
|
|
328
|
-
urgentThreat &&
|
|
329
|
-
(!urgentThreat.bestDodgeDirection || shouldForceShield(urgentThreat))
|
|
330
|
-
)
|
|
331
|
-
{
|
|
332
|
-
if (
|
|
333
|
-
urgentThreat.canBlockInTime &&
|
|
334
|
-
urgentThreat.ticksToStartShield <= SHIELD_BUFFER
|
|
335
|
-
)
|
|
336
|
-
{
|
|
337
|
-
return {
|
|
338
|
-
move: {x: 0, y: 0},
|
|
339
|
-
startCast: {spell: 'shield'},
|
|
340
|
-
};
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// === STEP 5: PROACTIVE BLINK KITING ===
|
|
345
|
-
if (
|
|
346
|
-
state.state === 'idle' &&
|
|
347
|
-
state.blinkCooldown === 0 &&
|
|
348
|
-
(!urgentThreat || urgentThreat.bestDodgeDirection !== null)
|
|
349
|
-
)
|
|
350
|
-
{
|
|
351
|
-
const closingRate = getClosingRate(
|
|
352
|
-
state.position,
|
|
353
|
-
enemy.position,
|
|
354
|
-
enemy.velocity,
|
|
355
|
-
);
|
|
356
|
-
if (
|
|
357
|
-
(closingRate > closingRateThreshold && distance < proactiveBlinkRange) ||
|
|
358
|
-
distance < dangerDistance
|
|
359
|
-
)
|
|
360
|
-
{
|
|
361
|
-
const blinkTarget = getBlinkToIncreaseDistance(
|
|
362
|
-
state.position,
|
|
363
|
-
enemy.position,
|
|
364
|
-
state.projectiles,
|
|
365
|
-
MIN_BLINK_ESCAPE_DISTANCE,
|
|
366
|
-
);
|
|
367
|
-
if (blinkTarget)
|
|
368
|
-
{
|
|
369
|
-
return {
|
|
370
|
-
move: {x: 0, y: 0},
|
|
371
|
-
startCast: {spell: 'blink', target: blinkTarget},
|
|
372
|
-
};
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
// === STEP 6: OFFENSE ===
|
|
378
|
-
if (state.state === 'idle')
|
|
379
|
-
{
|
|
380
|
-
const enemyHP = Math.ceil(enemy.health);
|
|
381
|
-
const vulnerabilityTicks = getEnemyVulnerabilityTicks(enemy);
|
|
382
|
-
|
|
383
|
-
// --- PUNISH MODE: warmup-accelerated missiles during vulnerability ---
|
|
384
|
-
if (vulnerabilityTicks >= punishMinVulnerability && distance < 500)
|
|
385
|
-
{
|
|
386
|
-
const flightTimeEstimate = distance / flightTimeDivisor;
|
|
387
|
-
const punishBudget = vulnerabilityTicks - flightTimeEstimate;
|
|
388
|
-
const nextThreatTime = urgentThreat?.ticksToImpact ?? Infinity;
|
|
389
|
-
const safeBudget =
|
|
390
|
-
nextThreatTime - GCD_DURATION - SHIELD_CAST_TIME - SHIELD_BUFFER;
|
|
391
|
-
const effectiveBudget = Math.min(
|
|
392
|
-
punishBudget,
|
|
393
|
-
safeBudget,
|
|
394
|
-
MAX_CAST_BUDGET,
|
|
395
|
-
);
|
|
396
|
-
|
|
397
|
-
if (effectiveBudget > 0)
|
|
398
|
-
{
|
|
399
|
-
const config = fitMissileToBudget(effectiveBudget, distance, {
|
|
400
|
-
minTurnRate: 0,
|
|
401
|
-
maxDamage: enemyHP,
|
|
402
|
-
lastMissileConfig: state.lastMissileConfig,
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
if (config && config.damage >= MIN_USEFUL_DAMAGE)
|
|
406
|
-
{
|
|
407
|
-
const castTime = getMissileCastTime(config, state.lastMissileConfig);
|
|
408
|
-
const actualFlight = distance / config.speed;
|
|
409
|
-
|
|
410
|
-
if (
|
|
411
|
-
castTime + actualFlight < vulnerabilityTicks &&
|
|
412
|
-
isSafeToAttack(threats, castTime)
|
|
413
|
-
)
|
|
414
|
-
{
|
|
415
|
-
const aimTarget =
|
|
416
|
-
config.turnRate > 0
|
|
417
|
-
? enemy.position
|
|
418
|
-
: getLeadPosition(
|
|
419
|
-
enemy.position,
|
|
420
|
-
enemy.velocity,
|
|
421
|
-
config.speed,
|
|
422
|
-
state.position,
|
|
423
|
-
);
|
|
424
|
-
return {
|
|
425
|
-
move,
|
|
426
|
-
startCast: {
|
|
427
|
-
spell: 'missile',
|
|
428
|
-
config,
|
|
429
|
-
missileAI: config.turnRate > 0 ? HomingAI : StraightAI,
|
|
430
|
-
direction: angleTo(state.position, aimTarget),
|
|
431
|
-
},
|
|
432
|
-
};
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
// --- STANDARD MODE: warmup-consistent homing missiles ---
|
|
439
|
-
const nextThreatTime = urgentThreat?.ticksToImpact ?? Infinity;
|
|
440
|
-
const safeBudget =
|
|
441
|
-
nextThreatTime - GCD_DURATION - SHIELD_CAST_TIME - SHIELD_BUFFER;
|
|
442
|
-
const budgetTicks = Math.min(safeBudget, MAX_CAST_BUDGET);
|
|
443
|
-
|
|
444
|
-
const minDmg = Math.min(MIN_USEFUL_DAMAGE, enemyHP);
|
|
445
|
-
let config = fitMissileToBudget(budgetTicks, distance, {
|
|
446
|
-
minTurnRate,
|
|
447
|
-
maxDamage: enemyHP,
|
|
448
|
-
lastMissileConfig: state.lastMissileConfig,
|
|
449
|
-
});
|
|
450
|
-
|
|
451
|
-
if (
|
|
452
|
-
config &&
|
|
453
|
-
config.damage >= minDmg &&
|
|
454
|
-
isSafeToAttack(threats, safeAttackCastEstimate)
|
|
455
|
-
)
|
|
456
|
-
{
|
|
457
|
-
return {
|
|
458
|
-
move,
|
|
459
|
-
startCast: {
|
|
460
|
-
spell: 'missile',
|
|
461
|
-
config,
|
|
462
|
-
missileAI: HomingAI,
|
|
463
|
-
direction: angleTo(state.position, enemy.position),
|
|
464
|
-
},
|
|
465
|
-
};
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// Fallback: fire bigger missile, accept incoming hit
|
|
469
|
-
if (!config || config.damage < minDmg)
|
|
470
|
-
{
|
|
471
|
-
config = fitMissileToBudget(MAX_CAST_BUDGET, distance, {
|
|
472
|
-
minTurnRate,
|
|
473
|
-
maxDamage: enemyHP,
|
|
474
|
-
lastMissileConfig: state.lastMissileConfig,
|
|
475
|
-
});
|
|
476
|
-
if (config)
|
|
477
|
-
{
|
|
478
|
-
return {
|
|
479
|
-
move,
|
|
480
|
-
startCast: {
|
|
481
|
-
spell: 'missile',
|
|
482
|
-
config,
|
|
483
|
-
missileAI: HomingAI,
|
|
484
|
-
direction: angleTo(state.position, enemy.position),
|
|
485
|
-
},
|
|
486
|
-
};
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
return {move};
|
|
492
|
-
};
|
|
1
|
+
import {WizardFunction, MissileAIFunction} from '../../types.js';
|
|
2
|
+
import {angleTo} from '../../utils/angles.js';
|
|
3
|
+
import {distanceTo} from '../../utils/distance.js';
|
|
4
|
+
import {
|
|
5
|
+
ARENA_SIZE,
|
|
6
|
+
GCD_DURATION,
|
|
7
|
+
SHIELD_CAST_TIME,
|
|
8
|
+
BLINK_CAST_TIME,
|
|
9
|
+
} from '../../rules.js';
|
|
10
|
+
import {getMissileCastTime, getLeadPosition} from '../../utils/combat.js';
|
|
11
|
+
import {
|
|
12
|
+
getThreats,
|
|
13
|
+
getMostUrgentThreat,
|
|
14
|
+
getDodgeDirectionForMovement,
|
|
15
|
+
isSafeToAttack,
|
|
16
|
+
getBlinkToIncreaseDistance,
|
|
17
|
+
fitMissileToBudget,
|
|
18
|
+
shouldForceShield,
|
|
19
|
+
MIN_BLINK_ESCAPE_DISTANCE,
|
|
20
|
+
MAX_CAST_BUDGET,
|
|
21
|
+
MIN_USEFUL_DAMAGE,
|
|
22
|
+
getClosingRate,
|
|
23
|
+
getEnemyVulnerabilityTicks,
|
|
24
|
+
} from '../shared.js';
|
|
25
|
+
import {useParam} from '../../engine/params-runtime.js';
|
|
26
|
+
|
|
27
|
+
const WALL_BUFFER = 60;
|
|
28
|
+
const SHIELD_BUFFER = 3;
|
|
29
|
+
|
|
30
|
+
const HomingAI: MissileAIFunction = ({worldState}) =>
|
|
31
|
+
{
|
|
32
|
+
const enemy = worldState.enemies[0];
|
|
33
|
+
return {turnToward: enemy?.position};
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const StraightAI: MissileAIFunction = () => ({});
|
|
37
|
+
|
|
38
|
+
function isNearWall(position: {x: number; y: number}): boolean
|
|
39
|
+
{
|
|
40
|
+
return (
|
|
41
|
+
position.x < WALL_BUFFER ||
|
|
42
|
+
position.x > ARENA_SIZE - WALL_BUFFER ||
|
|
43
|
+
position.y < WALL_BUFFER ||
|
|
44
|
+
position.y > ARENA_SIZE - WALL_BUFFER
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function smartStrafe(
|
|
49
|
+
position: {x: number; y: number},
|
|
50
|
+
enemy: {position: {x: number; y: number}},
|
|
51
|
+
dodgeDirection: {x: number; y: number} | null,
|
|
52
|
+
tick: number,
|
|
53
|
+
strafeCyclePeriod: number,
|
|
54
|
+
strafeIntensity: number,
|
|
55
|
+
): {x: number; y: number}
|
|
56
|
+
{
|
|
57
|
+
const deltaX = enemy.position.x - position.x;
|
|
58
|
+
const deltaY = enemy.position.y - position.y;
|
|
59
|
+
const len = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
|
60
|
+
if (len === 0) return {x: 0, y: 0};
|
|
61
|
+
|
|
62
|
+
const cw = {x: -deltaY / len, y: deltaX / len};
|
|
63
|
+
const ccw = {x: deltaY / len, y: -deltaX / len};
|
|
64
|
+
|
|
65
|
+
let dir: {x: number; y: number};
|
|
66
|
+
|
|
67
|
+
if (dodgeDirection)
|
|
68
|
+
{
|
|
69
|
+
dir = dodgeDirection;
|
|
70
|
+
}
|
|
71
|
+
else if (isNearWall(position))
|
|
72
|
+
{
|
|
73
|
+
const fCw = {x: position.x + cw.x * 100, y: position.y + cw.y * 100};
|
|
74
|
+
const fCcw = {x: position.x + ccw.x * 100, y: position.y + ccw.y * 100};
|
|
75
|
+
const sCw = Math.min(fCw.x, ARENA_SIZE - fCw.x, fCw.y, ARENA_SIZE - fCw.y);
|
|
76
|
+
const sCcw = Math.min(
|
|
77
|
+
fCcw.x,
|
|
78
|
+
ARENA_SIZE - fCcw.x,
|
|
79
|
+
fCcw.y,
|
|
80
|
+
ARENA_SIZE - fCcw.y,
|
|
81
|
+
);
|
|
82
|
+
dir = sCw > sCcw ? cw : ccw;
|
|
83
|
+
}
|
|
84
|
+
else
|
|
85
|
+
{
|
|
86
|
+
const cycle = Math.floor(tick / strafeCyclePeriod) % 2;
|
|
87
|
+
dir = cycle === 0 ? cw : ccw;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const intensity = dodgeDirection ? 100 : strafeIntensity;
|
|
91
|
+
return {x: dir.x * intensity, y: dir.y * intensity};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Bot: Infernalist
|
|
96
|
+
*
|
|
97
|
+
* BEHAVIOR: Rapid-fire caster that exploits warmup bonus for accelerating DPS.
|
|
98
|
+
* Fires consistent homing missiles to build warmup, punishes vulnerability windows
|
|
99
|
+
* with warmup-boosted fast casts. Proactive blink kiting when enemy closes.
|
|
100
|
+
*
|
|
101
|
+
* KEY IMPROVEMENTS OVER PYROMANCER:
|
|
102
|
+
* - Warmup exploitation: always passes lastMissileConfig for bonus
|
|
103
|
+
* - Punish mode: straight missiles during enemy vulnerability
|
|
104
|
+
* - Proactive blink kiting: monitors closing rate
|
|
105
|
+
* - Progressive cast-cancel: graduated thresholds
|
|
106
|
+
*
|
|
107
|
+
* PROGRESSION LINE: Flamecaller → Pyromancer → Infernalist
|
|
108
|
+
* TIER: 3 (elite Caster line)
|
|
109
|
+
*/
|
|
110
|
+
export const Infernalist: WizardFunction = ({state}) =>
|
|
111
|
+
{
|
|
112
|
+
const targetDistance = useParam('targetDistance', 285, {range: 225, min: 0});
|
|
113
|
+
const minTurnRate = useParam('minTurnRate', 0.2, {range: 1.5, steps: 5});
|
|
114
|
+
const dangerDistance = useParam('dangerDistance', 222, {
|
|
115
|
+
range: 150,
|
|
116
|
+
min: 0,
|
|
117
|
+
});
|
|
118
|
+
const closingRateThreshold = useParam('closingRateThreshold', 0.94, {
|
|
119
|
+
range: 0.65,
|
|
120
|
+
min: 0,
|
|
121
|
+
steps: 5,
|
|
122
|
+
});
|
|
123
|
+
const punishMinVulnerability = useParam('punishMinVulnerability', 340, {
|
|
124
|
+
range: 30,
|
|
125
|
+
min: 10,
|
|
126
|
+
steps: 5,
|
|
127
|
+
});
|
|
128
|
+
const preferBlinkDodge =
|
|
129
|
+
useParam('preferBlinkDodge', 0, {min: 0, max: 1, steps: 2}) >= 0.5;
|
|
130
|
+
const strafeCyclePeriod = useParam('strafeCyclePeriod', 109, {
|
|
131
|
+
range: 75,
|
|
132
|
+
min: 80,
|
|
133
|
+
max: 250,
|
|
134
|
+
});
|
|
135
|
+
const strafeIntensity = useParam('strafeIntensity', 23, {
|
|
136
|
+
range: 40,
|
|
137
|
+
min: 20,
|
|
138
|
+
max: 100,
|
|
139
|
+
steps: 7,
|
|
140
|
+
});
|
|
141
|
+
const approachSpeed = useParam('approachSpeed', 53, {
|
|
142
|
+
range: 30,
|
|
143
|
+
min: 10,
|
|
144
|
+
max: 70,
|
|
145
|
+
steps: 7,
|
|
146
|
+
});
|
|
147
|
+
const distanceDeadZone = useParam('distanceDeadZone', 34, {
|
|
148
|
+
range: 30,
|
|
149
|
+
min: 5,
|
|
150
|
+
max: 80,
|
|
151
|
+
steps: 7,
|
|
152
|
+
});
|
|
153
|
+
const cancelHeavyDmg = useParam('cancelHeavyDmg', 40, {
|
|
154
|
+
range: 15,
|
|
155
|
+
min: 5,
|
|
156
|
+
max: 40,
|
|
157
|
+
steps: 5,
|
|
158
|
+
});
|
|
159
|
+
const cancelMediumDmg = useParam('cancelMediumDmg', 9, {
|
|
160
|
+
range: 10,
|
|
161
|
+
min: 3,
|
|
162
|
+
max: 25,
|
|
163
|
+
steps: 5,
|
|
164
|
+
});
|
|
165
|
+
const cancelCastRatio = useParam('cancelCastRatio', 0.78, {
|
|
166
|
+
range: 0.4,
|
|
167
|
+
min: 0.3,
|
|
168
|
+
max: 1.0,
|
|
169
|
+
steps: 5,
|
|
170
|
+
});
|
|
171
|
+
const shieldCancelThreshold = useParam('shieldCancelThreshold', 100, {
|
|
172
|
+
range: 75,
|
|
173
|
+
min: 50,
|
|
174
|
+
max: 300,
|
|
175
|
+
});
|
|
176
|
+
const blinkWindowMax = useParam('blinkWindowMax', 75, {
|
|
177
|
+
range: 40,
|
|
178
|
+
min: 20,
|
|
179
|
+
max: 120,
|
|
180
|
+
steps: 7,
|
|
181
|
+
});
|
|
182
|
+
const proactiveBlinkRange = useParam('proactiveBlinkRange', 100, {
|
|
183
|
+
range: 150,
|
|
184
|
+
min: 100,
|
|
185
|
+
max: 500,
|
|
186
|
+
steps: 7,
|
|
187
|
+
});
|
|
188
|
+
const flightTimeDivisor = useParam('flightTimeDivisor', 5.4, {
|
|
189
|
+
range: 4,
|
|
190
|
+
min: 2,
|
|
191
|
+
max: 12,
|
|
192
|
+
steps: 5,
|
|
193
|
+
});
|
|
194
|
+
const safeAttackCastEstimate = useParam('safeAttackCastEstimate', 35, {
|
|
195
|
+
range: 25,
|
|
196
|
+
min: 10,
|
|
197
|
+
max: 80,
|
|
198
|
+
steps: 5,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const enemy = state.enemies[0];
|
|
202
|
+
if (!enemy)
|
|
203
|
+
{
|
|
204
|
+
return {move: {x: 0, y: 0}};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const distance = distanceTo(state.position, enemy.position);
|
|
208
|
+
const threats = getThreats(state);
|
|
209
|
+
const urgentThreat = getMostUrgentThreat(threats);
|
|
210
|
+
const dodgeDirection = getDodgeDirectionForMovement(threats, state.position);
|
|
211
|
+
const strafe = smartStrafe(
|
|
212
|
+
state.position,
|
|
213
|
+
enemy,
|
|
214
|
+
dodgeDirection,
|
|
215
|
+
state.tick,
|
|
216
|
+
strafeCyclePeriod,
|
|
217
|
+
strafeIntensity,
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
// Distance management with wall-aware retreat
|
|
221
|
+
let moveX = strafe.x;
|
|
222
|
+
let moveY = strafe.y;
|
|
223
|
+
|
|
224
|
+
if (!dodgeDirection)
|
|
225
|
+
{
|
|
226
|
+
const deltaX = enemy.position.x - state.position.x;
|
|
227
|
+
const deltaY = enemy.position.y - state.position.y;
|
|
228
|
+
const norm = distance > 0 ? distance : 1;
|
|
229
|
+
|
|
230
|
+
if (distance > targetDistance + distanceDeadZone)
|
|
231
|
+
{
|
|
232
|
+
moveX += (deltaX / norm) * approachSpeed;
|
|
233
|
+
moveY += (deltaY / norm) * approachSpeed;
|
|
234
|
+
}
|
|
235
|
+
else if (distance < targetDistance - distanceDeadZone)
|
|
236
|
+
{
|
|
237
|
+
let rx = -(deltaX / norm) * approachSpeed;
|
|
238
|
+
let ry = -(deltaY / norm) * approachSpeed;
|
|
239
|
+
if (state.position.x < WALL_BUFFER && rx < 0) rx = 0;
|
|
240
|
+
if (state.position.x > ARENA_SIZE - WALL_BUFFER && rx > 0) rx = 0;
|
|
241
|
+
if (state.position.y < WALL_BUFFER && ry < 0) ry = 0;
|
|
242
|
+
if (state.position.y > ARENA_SIZE - WALL_BUFFER && ry > 0) ry = 0;
|
|
243
|
+
moveX += rx;
|
|
244
|
+
moveY += ry;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const mag = Math.sqrt(moveX * moveX + moveY * moveY);
|
|
249
|
+
if (mag > 100)
|
|
250
|
+
{
|
|
251
|
+
moveX = (moveX / mag) * 100;
|
|
252
|
+
moveY = (moveY / mag) * 100;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const move = {x: moveX, y: moveY};
|
|
256
|
+
|
|
257
|
+
// === STEP 1: CHANNELING ===
|
|
258
|
+
if (state.state === 'channeling')
|
|
259
|
+
{
|
|
260
|
+
if (!urgentThreat || urgentThreat.ticksToImpact > shieldCancelThreshold)
|
|
261
|
+
{
|
|
262
|
+
return {move, cancel: true};
|
|
263
|
+
}
|
|
264
|
+
return {move: {x: 0, y: 0}};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// === STEP 2: PROGRESSIVE CAST-CANCEL ===
|
|
268
|
+
if (
|
|
269
|
+
state.state === 'casting' &&
|
|
270
|
+
state.castingSpell === 'missile' &&
|
|
271
|
+
urgentThreat &&
|
|
272
|
+
!urgentThreat.bestDodgeDirection
|
|
273
|
+
)
|
|
274
|
+
{
|
|
275
|
+
const remainingCast = (state.castDuration ?? 0) - (state.castProgress ?? 0);
|
|
276
|
+
const defenseTime =
|
|
277
|
+
state.blinkCooldown === 0
|
|
278
|
+
? BLINK_CAST_TIME
|
|
279
|
+
: SHIELD_CAST_TIME + SHIELD_BUFFER;
|
|
280
|
+
const willArriveInWindow =
|
|
281
|
+
urgentThreat.ticksToImpact <= remainingCast + GCD_DURATION + defenseTime;
|
|
282
|
+
|
|
283
|
+
if (willArriveInWindow)
|
|
284
|
+
{
|
|
285
|
+
const incomingDamage = urgentThreat.projectile.damage;
|
|
286
|
+
const castRatio = (state.castProgress ?? 0) / (state.castDuration ?? 1);
|
|
287
|
+
|
|
288
|
+
if (incomingDamage >= state.health || incomingDamage >= cancelHeavyDmg)
|
|
289
|
+
{
|
|
290
|
+
return {move, cancel: true};
|
|
291
|
+
}
|
|
292
|
+
if (incomingDamage >= cancelMediumDmg && castRatio < cancelCastRatio)
|
|
293
|
+
{
|
|
294
|
+
return {move, cancel: true};
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// === BLINK-DODGE — optimizer-controlled alternative to shield ===
|
|
300
|
+
if (
|
|
301
|
+
preferBlinkDodge &&
|
|
302
|
+
state.state === 'idle' &&
|
|
303
|
+
urgentThreat &&
|
|
304
|
+
state.blinkCooldown === 0 &&
|
|
305
|
+
(!urgentThreat.bestDodgeDirection || shouldForceShield(urgentThreat)) &&
|
|
306
|
+
urgentThreat.ticksToImpact >= BLINK_CAST_TIME &&
|
|
307
|
+
urgentThreat.ticksToImpact <= blinkWindowMax
|
|
308
|
+
)
|
|
309
|
+
{
|
|
310
|
+
const blinkTarget = getBlinkToIncreaseDistance(
|
|
311
|
+
state.position,
|
|
312
|
+
enemy.position,
|
|
313
|
+
state.projectiles,
|
|
314
|
+
MIN_BLINK_ESCAPE_DISTANCE,
|
|
315
|
+
);
|
|
316
|
+
if (blinkTarget)
|
|
317
|
+
{
|
|
318
|
+
return {
|
|
319
|
+
move: {x: 0, y: 0},
|
|
320
|
+
startCast: {spell: 'blink', target: blinkTarget},
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// === STEP 3: SHIELD — undodgeable or high-damage homing threats ===
|
|
326
|
+
if (
|
|
327
|
+
state.state === 'idle' &&
|
|
328
|
+
urgentThreat &&
|
|
329
|
+
(!urgentThreat.bestDodgeDirection || shouldForceShield(urgentThreat))
|
|
330
|
+
)
|
|
331
|
+
{
|
|
332
|
+
if (
|
|
333
|
+
urgentThreat.canBlockInTime &&
|
|
334
|
+
urgentThreat.ticksToStartShield <= SHIELD_BUFFER
|
|
335
|
+
)
|
|
336
|
+
{
|
|
337
|
+
return {
|
|
338
|
+
move: {x: 0, y: 0},
|
|
339
|
+
startCast: {spell: 'shield'},
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// === STEP 5: PROACTIVE BLINK KITING ===
|
|
345
|
+
if (
|
|
346
|
+
state.state === 'idle' &&
|
|
347
|
+
state.blinkCooldown === 0 &&
|
|
348
|
+
(!urgentThreat || urgentThreat.bestDodgeDirection !== null)
|
|
349
|
+
)
|
|
350
|
+
{
|
|
351
|
+
const closingRate = getClosingRate(
|
|
352
|
+
state.position,
|
|
353
|
+
enemy.position,
|
|
354
|
+
enemy.velocity,
|
|
355
|
+
);
|
|
356
|
+
if (
|
|
357
|
+
(closingRate > closingRateThreshold && distance < proactiveBlinkRange) ||
|
|
358
|
+
distance < dangerDistance
|
|
359
|
+
)
|
|
360
|
+
{
|
|
361
|
+
const blinkTarget = getBlinkToIncreaseDistance(
|
|
362
|
+
state.position,
|
|
363
|
+
enemy.position,
|
|
364
|
+
state.projectiles,
|
|
365
|
+
MIN_BLINK_ESCAPE_DISTANCE,
|
|
366
|
+
);
|
|
367
|
+
if (blinkTarget)
|
|
368
|
+
{
|
|
369
|
+
return {
|
|
370
|
+
move: {x: 0, y: 0},
|
|
371
|
+
startCast: {spell: 'blink', target: blinkTarget},
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// === STEP 6: OFFENSE ===
|
|
378
|
+
if (state.state === 'idle')
|
|
379
|
+
{
|
|
380
|
+
const enemyHP = Math.ceil(enemy.health);
|
|
381
|
+
const vulnerabilityTicks = getEnemyVulnerabilityTicks(enemy);
|
|
382
|
+
|
|
383
|
+
// --- PUNISH MODE: warmup-accelerated missiles during vulnerability ---
|
|
384
|
+
if (vulnerabilityTicks >= punishMinVulnerability && distance < 500)
|
|
385
|
+
{
|
|
386
|
+
const flightTimeEstimate = distance / flightTimeDivisor;
|
|
387
|
+
const punishBudget = vulnerabilityTicks - flightTimeEstimate;
|
|
388
|
+
const nextThreatTime = urgentThreat?.ticksToImpact ?? Infinity;
|
|
389
|
+
const safeBudget =
|
|
390
|
+
nextThreatTime - GCD_DURATION - SHIELD_CAST_TIME - SHIELD_BUFFER;
|
|
391
|
+
const effectiveBudget = Math.min(
|
|
392
|
+
punishBudget,
|
|
393
|
+
safeBudget,
|
|
394
|
+
MAX_CAST_BUDGET,
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
if (effectiveBudget > 0)
|
|
398
|
+
{
|
|
399
|
+
const config = fitMissileToBudget(effectiveBudget, distance, {
|
|
400
|
+
minTurnRate: 0,
|
|
401
|
+
maxDamage: enemyHP,
|
|
402
|
+
lastMissileConfig: state.lastMissileConfig,
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
if (config && config.damage >= MIN_USEFUL_DAMAGE)
|
|
406
|
+
{
|
|
407
|
+
const castTime = getMissileCastTime(config, state.lastMissileConfig);
|
|
408
|
+
const actualFlight = distance / config.speed;
|
|
409
|
+
|
|
410
|
+
if (
|
|
411
|
+
castTime + actualFlight < vulnerabilityTicks &&
|
|
412
|
+
isSafeToAttack(threats, castTime)
|
|
413
|
+
)
|
|
414
|
+
{
|
|
415
|
+
const aimTarget =
|
|
416
|
+
config.turnRate > 0
|
|
417
|
+
? enemy.position
|
|
418
|
+
: getLeadPosition(
|
|
419
|
+
enemy.position,
|
|
420
|
+
enemy.velocity,
|
|
421
|
+
config.speed,
|
|
422
|
+
state.position,
|
|
423
|
+
);
|
|
424
|
+
return {
|
|
425
|
+
move,
|
|
426
|
+
startCast: {
|
|
427
|
+
spell: 'missile',
|
|
428
|
+
config,
|
|
429
|
+
missileAI: config.turnRate > 0 ? HomingAI : StraightAI,
|
|
430
|
+
direction: angleTo(state.position, aimTarget),
|
|
431
|
+
},
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// --- STANDARD MODE: warmup-consistent homing missiles ---
|
|
439
|
+
const nextThreatTime = urgentThreat?.ticksToImpact ?? Infinity;
|
|
440
|
+
const safeBudget =
|
|
441
|
+
nextThreatTime - GCD_DURATION - SHIELD_CAST_TIME - SHIELD_BUFFER;
|
|
442
|
+
const budgetTicks = Math.min(safeBudget, MAX_CAST_BUDGET);
|
|
443
|
+
|
|
444
|
+
const minDmg = Math.min(MIN_USEFUL_DAMAGE, enemyHP);
|
|
445
|
+
let config = fitMissileToBudget(budgetTicks, distance, {
|
|
446
|
+
minTurnRate,
|
|
447
|
+
maxDamage: enemyHP,
|
|
448
|
+
lastMissileConfig: state.lastMissileConfig,
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
if (
|
|
452
|
+
config &&
|
|
453
|
+
config.damage >= minDmg &&
|
|
454
|
+
isSafeToAttack(threats, safeAttackCastEstimate)
|
|
455
|
+
)
|
|
456
|
+
{
|
|
457
|
+
return {
|
|
458
|
+
move,
|
|
459
|
+
startCast: {
|
|
460
|
+
spell: 'missile',
|
|
461
|
+
config,
|
|
462
|
+
missileAI: HomingAI,
|
|
463
|
+
direction: angleTo(state.position, enemy.position),
|
|
464
|
+
},
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Fallback: fire bigger missile, accept incoming hit
|
|
469
|
+
if (!config || config.damage < minDmg)
|
|
470
|
+
{
|
|
471
|
+
config = fitMissileToBudget(MAX_CAST_BUDGET, distance, {
|
|
472
|
+
minTurnRate,
|
|
473
|
+
maxDamage: enemyHP,
|
|
474
|
+
lastMissileConfig: state.lastMissileConfig,
|
|
475
|
+
});
|
|
476
|
+
if (config)
|
|
477
|
+
{
|
|
478
|
+
return {
|
|
479
|
+
move,
|
|
480
|
+
startCast: {
|
|
481
|
+
spell: 'missile',
|
|
482
|
+
config,
|
|
483
|
+
missileAI: HomingAI,
|
|
484
|
+
direction: angleTo(state.position, enemy.position),
|
|
485
|
+
},
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return {move};
|
|
492
|
+
};
|