@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,9 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type {FinalAction, MissileFunction} from '../../hooks/index.js';
|
|
2
|
+
import {usePosition, useEnemy, useStatus, useBlinkCooldown, useHealth, useTick, useThreats, useProjectiles, useMyProjectiles, useLastMissileConfig, useCastingSpell, useCastProgress, idle, move as moveAction, missile, shield, blink, cancel, getMissileContext, turnToward, flyStraight} from '../../hooks/index.js';
|
|
2
3
|
import {angleTo} from '../../utils/angles.js';
|
|
3
4
|
import {distanceTo} from '../../utils/distance.js';
|
|
4
|
-
import {
|
|
5
|
+
import {RULES, ARENA_MIN, ARENA_MAX} from '../../rules.js';
|
|
6
|
+
import {getMissileCastTime} from '../../utils/combat.js';
|
|
5
7
|
import {
|
|
6
|
-
getThreats,
|
|
7
8
|
getMostUrgentThreat,
|
|
8
9
|
getDodgeDirectionForMovement,
|
|
9
10
|
isSafeToAttack,
|
|
@@ -12,25 +13,28 @@ import {
|
|
|
12
13
|
shouldForceShield,
|
|
13
14
|
MAX_CAST_BUDGET,
|
|
14
15
|
MIN_USEFUL_DAMAGE,
|
|
16
|
+
keepOffWalls,
|
|
17
|
+
getPreCastReposition,
|
|
15
18
|
} from '../shared.js';
|
|
16
19
|
import {useParam} from '../../engine/params-runtime.js';
|
|
17
20
|
const WALL_BUFFER = 60;
|
|
18
21
|
|
|
19
22
|
const SHIELD_BUFFER = 3;
|
|
20
23
|
|
|
21
|
-
const HomingAI:
|
|
24
|
+
const HomingAI: MissileFunction = () =>
|
|
22
25
|
{
|
|
23
|
-
const
|
|
24
|
-
|
|
26
|
+
const ctx = getMissileContext();
|
|
27
|
+
const enemy = ctx.worldState.enemies[0];
|
|
28
|
+
return enemy ? turnToward(enemy.position.x, enemy.position.y) : flyStraight();
|
|
25
29
|
};
|
|
26
30
|
|
|
27
31
|
function isNearWall(position: {x: number; y: number}): boolean
|
|
28
32
|
{
|
|
29
33
|
return (
|
|
30
|
-
position.x < WALL_BUFFER ||
|
|
31
|
-
position.x >
|
|
32
|
-
position.y < WALL_BUFFER ||
|
|
33
|
-
position.y >
|
|
34
|
+
position.x < ARENA_MIN + WALL_BUFFER ||
|
|
35
|
+
position.x > ARENA_MAX - WALL_BUFFER ||
|
|
36
|
+
position.y < ARENA_MIN + WALL_BUFFER ||
|
|
37
|
+
position.y > ARENA_MAX - WALL_BUFFER
|
|
34
38
|
);
|
|
35
39
|
}
|
|
36
40
|
|
|
@@ -74,16 +78,16 @@ function smartStrafe(
|
|
|
74
78
|
y: position.y + strafeCounterClockwise.y * 100,
|
|
75
79
|
};
|
|
76
80
|
const scoreClockwise = Math.min(
|
|
77
|
-
futureClockwise.x,
|
|
78
|
-
|
|
79
|
-
futureClockwise.y,
|
|
80
|
-
|
|
81
|
+
futureClockwise.x - ARENA_MIN,
|
|
82
|
+
ARENA_MAX - futureClockwise.x,
|
|
83
|
+
futureClockwise.y - ARENA_MIN,
|
|
84
|
+
ARENA_MAX - futureClockwise.y,
|
|
81
85
|
);
|
|
82
86
|
const scoreCounterClockwise = Math.min(
|
|
83
|
-
futureCounterClockwise.x,
|
|
84
|
-
|
|
85
|
-
futureCounterClockwise.y,
|
|
86
|
-
|
|
87
|
+
futureCounterClockwise.x - ARENA_MIN,
|
|
88
|
+
ARENA_MAX - futureCounterClockwise.x,
|
|
89
|
+
futureCounterClockwise.y - ARENA_MIN,
|
|
90
|
+
ARENA_MAX - futureCounterClockwise.y,
|
|
87
91
|
);
|
|
88
92
|
strafeDir =
|
|
89
93
|
scoreClockwise > scoreCounterClockwise
|
|
@@ -96,7 +100,7 @@ function smartStrafe(
|
|
|
96
100
|
strafeDir = cycle === 0 ? strafeClockwise : strafeCounterClockwise;
|
|
97
101
|
}
|
|
98
102
|
|
|
99
|
-
const intensity = dodgeDirection ?
|
|
103
|
+
const intensity = dodgeDirection ? 1 : strafeIntensity;
|
|
100
104
|
return {x: strafeDir.x * intensity, y: strafeDir.y * intensity};
|
|
101
105
|
}
|
|
102
106
|
|
|
@@ -116,31 +120,45 @@ function smartStrafe(
|
|
|
116
120
|
*
|
|
117
121
|
* TIER: 2 (enhanced Flamecaller)
|
|
118
122
|
*/
|
|
119
|
-
export
|
|
123
|
+
export function Pyromancer(): FinalAction
|
|
120
124
|
{
|
|
121
|
-
const
|
|
125
|
+
const position = usePosition();
|
|
126
|
+
const enemy = useEnemy();
|
|
127
|
+
const status = useStatus();
|
|
128
|
+
const blinkCooldown = useBlinkCooldown();
|
|
129
|
+
const health = useHealth();
|
|
130
|
+
const tick = useTick();
|
|
131
|
+
const threats = useThreats();
|
|
132
|
+
const projectiles = useProjectiles();
|
|
133
|
+
const myProjectiles = useMyProjectiles();
|
|
134
|
+
const lastMissileConfig = useLastMissileConfig();
|
|
135
|
+
const castingSpell = useCastingSpell();
|
|
136
|
+
const castProgressData = useCastProgress();
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
const targetDistance = useParam('targetDistance', 299, {
|
|
122
140
|
range: 225,
|
|
123
141
|
min: 0,
|
|
124
142
|
});
|
|
125
143
|
const minTurnRate = useParam('minTurnRate', 0.2, {range: 1.5, steps: 5});
|
|
126
|
-
const strafeCyclePeriod = useParam('strafeCyclePeriod',
|
|
144
|
+
const strafeCyclePeriod = useParam('strafeCyclePeriod', 99, {
|
|
127
145
|
range: 75,
|
|
128
146
|
min: 80,
|
|
129
147
|
max: 300,
|
|
130
148
|
});
|
|
131
|
-
const strafeIntensity = useParam('strafeIntensity',
|
|
132
|
-
range:
|
|
133
|
-
min:
|
|
134
|
-
max:
|
|
149
|
+
const strafeIntensity = useParam('strafeIntensity', 1, {
|
|
150
|
+
range: 0.4,
|
|
151
|
+
min: 0.2,
|
|
152
|
+
max: 1,
|
|
135
153
|
steps: 7,
|
|
136
154
|
});
|
|
137
|
-
const approachSpeed = useParam('approachSpeed',
|
|
138
|
-
range:
|
|
139
|
-
min:
|
|
140
|
-
max:
|
|
155
|
+
const approachSpeed = useParam('approachSpeed', 0.1, {
|
|
156
|
+
range: 0.3,
|
|
157
|
+
min: 0.1,
|
|
158
|
+
max: 0.7,
|
|
141
159
|
steps: 7,
|
|
142
160
|
});
|
|
143
|
-
const distanceDeadZone = useParam('distanceDeadZone',
|
|
161
|
+
const distanceDeadZone = useParam('distanceDeadZone', 36, {
|
|
144
162
|
range: 30,
|
|
145
163
|
min: 5,
|
|
146
164
|
max: 80,
|
|
@@ -151,42 +169,51 @@ export const Pyromancer: WizardFunction = ({state}) =>
|
|
|
151
169
|
min: 50,
|
|
152
170
|
max: 300,
|
|
153
171
|
});
|
|
154
|
-
const blinkWindowMax = useParam('blinkWindowMax',
|
|
172
|
+
const blinkWindowMax = useParam('blinkWindowMax', 61, {
|
|
155
173
|
range: 40,
|
|
156
174
|
min: 20,
|
|
157
175
|
max: 120,
|
|
158
176
|
steps: 7,
|
|
159
177
|
});
|
|
160
|
-
const flightTimeDivisor = useParam('flightTimeDivisor',
|
|
178
|
+
const flightTimeDivisor = useParam('flightTimeDivisor', 3.4, {
|
|
161
179
|
range: 4,
|
|
162
180
|
min: 2,
|
|
163
181
|
max: 12,
|
|
164
182
|
steps: 5,
|
|
165
183
|
});
|
|
166
|
-
const
|
|
167
|
-
range:
|
|
168
|
-
min:
|
|
169
|
-
max:
|
|
184
|
+
const centerBias = useParam('centerBias', 0.1, {
|
|
185
|
+
range: 0.4,
|
|
186
|
+
min: 0,
|
|
187
|
+
max: 0.8,
|
|
170
188
|
steps: 5,
|
|
171
189
|
});
|
|
190
|
+
const maxBlinkDistance = useParam('maxBlinkDistance', 245, {
|
|
191
|
+
range: 100,
|
|
192
|
+
min: 50,
|
|
193
|
+
max: 300,
|
|
194
|
+
steps: 7,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const distance = distanceTo(position, enemy.position);
|
|
172
198
|
|
|
173
|
-
|
|
174
|
-
if (!enemy)
|
|
199
|
+
function capBlinkDistance(target: {x: number; y: number}): {x: number; y: number}
|
|
175
200
|
{
|
|
176
|
-
|
|
201
|
+
const dx = target.x - position.x;
|
|
202
|
+
const dy = target.y - position.y;
|
|
203
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
204
|
+
if (dist <= maxBlinkDistance) return target;
|
|
205
|
+
const scale = maxBlinkDistance / dist;
|
|
206
|
+
return {x: position.x + dx * scale, y: position.y + dy * scale};
|
|
177
207
|
}
|
|
178
208
|
|
|
179
|
-
const distance = distanceTo(state.position, enemy.position);
|
|
180
|
-
|
|
181
|
-
const threats = getThreats(state);
|
|
182
209
|
const urgentThreat = getMostUrgentThreat(threats);
|
|
183
210
|
|
|
184
|
-
const dodgeDirection = getDodgeDirectionForMovement(threats,
|
|
211
|
+
const dodgeDirection = getDodgeDirectionForMovement(threats, position);
|
|
185
212
|
const strafe = smartStrafe(
|
|
186
|
-
|
|
213
|
+
position,
|
|
187
214
|
enemy,
|
|
188
215
|
dodgeDirection,
|
|
189
|
-
|
|
216
|
+
tick,
|
|
190
217
|
strafeCyclePeriod,
|
|
191
218
|
strafeIntensity,
|
|
192
219
|
);
|
|
@@ -195,18 +222,26 @@ export const Pyromancer: WizardFunction = ({state}) =>
|
|
|
195
222
|
let moveX = strafe.x;
|
|
196
223
|
let moveY = strafe.y;
|
|
197
224
|
|
|
198
|
-
if (!dodgeDirection)
|
|
225
|
+
if (!dodgeDirection)
|
|
199
226
|
{
|
|
200
|
-
const deltaX = enemy.position.x -
|
|
201
|
-
const deltaY = enemy.position.y -
|
|
227
|
+
const deltaX = enemy.position.x - position.x;
|
|
228
|
+
const deltaY = enemy.position.y - position.y;
|
|
202
229
|
const normalizedDistance = distance > 0 ? distance : 1;
|
|
203
230
|
|
|
204
|
-
if (distance > targetDistance + distanceDeadZone)
|
|
231
|
+
if (distance > targetDistance + distanceDeadZone)
|
|
205
232
|
{
|
|
206
|
-
|
|
207
|
-
|
|
233
|
+
// Approach the center-side of the enemy so knockback pushes toward center, not lava.
|
|
234
|
+
const center = (ARENA_MIN + ARENA_MAX) / 2;
|
|
235
|
+
const toCenterX = center - enemy.position.x;
|
|
236
|
+
const toCenterY = center - enemy.position.y;
|
|
237
|
+
const toCenterDist = Math.sqrt(toCenterX * toCenterX + toCenterY * toCenterY) || 1;
|
|
238
|
+
const approachDirX = deltaX / normalizedDistance + (toCenterX / toCenterDist) * centerBias;
|
|
239
|
+
const approachDirY = deltaY / normalizedDistance + (toCenterY / toCenterDist) * centerBias;
|
|
240
|
+
const approachMag = Math.sqrt(approachDirX * approachDirX + approachDirY * approachDirY) || 1;
|
|
241
|
+
moveX += (approachDirX / approachMag) * approachSpeed;
|
|
242
|
+
moveY += (approachDirY / approachMag) * approachSpeed;
|
|
208
243
|
}
|
|
209
|
-
else if (distance < targetDistance - distanceDeadZone)
|
|
244
|
+
else if (distance < targetDistance - distanceDeadZone)
|
|
210
245
|
{
|
|
211
246
|
moveX -= (deltaX / normalizedDistance) * approachSpeed;
|
|
212
247
|
moveY -= (deltaY / normalizedDistance) * approachSpeed;
|
|
@@ -214,40 +249,41 @@ export const Pyromancer: WizardFunction = ({state}) =>
|
|
|
214
249
|
}
|
|
215
250
|
|
|
216
251
|
const magnitude = Math.sqrt(moveX * moveX + moveY * moveY);
|
|
217
|
-
if (magnitude >
|
|
252
|
+
if (magnitude > 1)
|
|
218
253
|
{
|
|
219
|
-
moveX =
|
|
220
|
-
moveY =
|
|
254
|
+
moveX = moveX / magnitude;
|
|
255
|
+
moveY = moveY / magnitude;
|
|
221
256
|
}
|
|
222
257
|
|
|
223
258
|
const move = {x: moveX, y: moveY};
|
|
259
|
+
const safeMove = keepOffWalls(position, move);
|
|
224
260
|
|
|
225
261
|
// === DEFENSE: Handle channeling ===
|
|
226
|
-
if (
|
|
262
|
+
if (status === 'channeling')
|
|
227
263
|
{
|
|
228
264
|
if (!urgentThreat || urgentThreat.ticksToImpact > shieldCancelThreshold)
|
|
229
265
|
{
|
|
230
|
-
return
|
|
266
|
+
return cancel().move(safeMove.x, safeMove.y);
|
|
231
267
|
}
|
|
232
|
-
return
|
|
268
|
+
return idle();
|
|
233
269
|
}
|
|
234
270
|
|
|
235
271
|
// === DEFENSE: Cancel missile cast only for LETHAL incoming damage ===
|
|
236
272
|
// DPS trade: we accepted the hit when we started casting. Only cancel to survive.
|
|
237
273
|
if (
|
|
238
|
-
|
|
239
|
-
|
|
274
|
+
status === 'casting' &&
|
|
275
|
+
castingSpell === 'missile' &&
|
|
240
276
|
urgentThreat &&
|
|
241
|
-
urgentThreat.projectile.damage >=
|
|
277
|
+
urgentThreat.projectile.damage >= health
|
|
242
278
|
)
|
|
243
279
|
{
|
|
244
|
-
const remainingCast = (
|
|
280
|
+
const remainingCast = ((castProgressData?.total) ?? 0) - ((castProgressData?.current) ?? 0);
|
|
245
281
|
if (
|
|
246
282
|
urgentThreat.ticksToImpact <=
|
|
247
|
-
remainingCast + GCD_DURATION + SHIELD_CAST_TIME + SHIELD_BUFFER
|
|
283
|
+
remainingCast + RULES.GCD_DURATION + RULES.SHIELD_CAST_TIME + SHIELD_BUFFER
|
|
248
284
|
)
|
|
249
285
|
{
|
|
250
|
-
return
|
|
286
|
+
return cancel().move(safeMove.x, safeMove.y);
|
|
251
287
|
}
|
|
252
288
|
}
|
|
253
289
|
|
|
@@ -256,22 +292,20 @@ export const Pyromancer: WizardFunction = ({state}) =>
|
|
|
256
292
|
// Wide window (10-80) prevents idle gaps where budget is too small for a missile
|
|
257
293
|
// but the threat is too far for the old narrow blink window.
|
|
258
294
|
if (
|
|
259
|
-
|
|
295
|
+
status === 'idle' &&
|
|
260
296
|
urgentThreat &&
|
|
261
|
-
|
|
297
|
+
blinkCooldown === 0 &&
|
|
262
298
|
urgentThreat.ticksToImpact >= 10 &&
|
|
263
299
|
urgentThreat.ticksToImpact <= blinkWindowMax
|
|
264
300
|
)
|
|
265
301
|
{
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
startCast: {spell: 'blink', target: getBlinkToCenter(state.position)},
|
|
269
|
-
};
|
|
302
|
+
const blinkTarget = capBlinkDistance(getBlinkToCenter(position));
|
|
303
|
+
return blink(blinkTarget.x, blinkTarget.y);
|
|
270
304
|
}
|
|
271
305
|
|
|
272
306
|
// === DEFENSE: Shield undodgeable or high-damage homing threats (when blink on cooldown) ===
|
|
273
307
|
if (
|
|
274
|
-
|
|
308
|
+
status === 'idle' &&
|
|
275
309
|
urgentThreat &&
|
|
276
310
|
(!urgentThreat.bestDodgeDirection || shouldForceShield(urgentThreat))
|
|
277
311
|
)
|
|
@@ -280,30 +314,51 @@ export const Pyromancer: WizardFunction = ({state}) =>
|
|
|
280
314
|
{
|
|
281
315
|
if (urgentThreat.ticksToStartShield <= SHIELD_BUFFER)
|
|
282
316
|
{
|
|
283
|
-
return
|
|
284
|
-
move: {x: 0, y: 0},
|
|
285
|
-
startCast: {spell: 'shield'},
|
|
286
|
-
};
|
|
317
|
+
return shield();
|
|
287
318
|
}
|
|
288
319
|
}
|
|
289
320
|
}
|
|
290
321
|
|
|
322
|
+
// === Preemptive shield — shield when enemy is casting a missile at close range ===
|
|
323
|
+
if (
|
|
324
|
+
status === 'idle' &&
|
|
325
|
+
distance < 300 &&
|
|
326
|
+
enemy.status === 'casting' &&
|
|
327
|
+
enemy.castingSpell === 'missile'
|
|
328
|
+
)
|
|
329
|
+
{
|
|
330
|
+
const remainingCast = (enemy.castDuration ?? 0) - (enemy.castProgress ?? 0);
|
|
331
|
+
const estimatedFlightTicks = distance / 8;
|
|
332
|
+
const ticksUntilHit = remainingCast + estimatedFlightTicks;
|
|
333
|
+
|
|
334
|
+
if (
|
|
335
|
+
ticksUntilHit >= RULES.SHIELD_CAST_TIME &&
|
|
336
|
+
ticksUntilHit < RULES.SHIELD_CAST_TIME + 15
|
|
337
|
+
)
|
|
338
|
+
{
|
|
339
|
+
return shield();
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
291
343
|
// === OFFENSE: Adaptive homing missiles ===
|
|
292
344
|
// Skip offense when a lethal missile could reach us during cast+GCD — dodge at full speed instead
|
|
293
|
-
const myProjectileIds = new Set(
|
|
294
|
-
const hasNearbyLethalMissile =
|
|
345
|
+
const myProjectileIds = new Set(myProjectiles.map((p) => p.id));
|
|
346
|
+
const hasNearbyLethalMissile = projectiles.some(
|
|
295
347
|
(p) =>
|
|
296
348
|
!myProjectileIds.has(p.id) &&
|
|
297
|
-
p.damage >=
|
|
298
|
-
distanceTo(
|
|
299
|
-
MAX_CAST_BUDGET + GCD_DURATION,
|
|
349
|
+
p.damage >= health &&
|
|
350
|
+
distanceTo(position, p.position) / p.speed <
|
|
351
|
+
MAX_CAST_BUDGET + RULES.GCD_DURATION,
|
|
300
352
|
);
|
|
301
|
-
if (
|
|
353
|
+
if (status === 'idle' && !hasNearbyLethalMissile)
|
|
302
354
|
{
|
|
355
|
+
const reposition = getPreCastReposition(position, enemy.position);
|
|
356
|
+
if (reposition) return moveAction(reposition.x, reposition.y);
|
|
357
|
+
|
|
303
358
|
let nextThreatTime = urgentThreat?.ticksToImpact ?? Infinity;
|
|
304
359
|
|
|
305
360
|
// Account for enemy's active cast — their missile will arrive after cast completes + flight time
|
|
306
|
-
if (enemy.
|
|
361
|
+
if (enemy.status === 'casting' && enemy.castingSpell === 'missile')
|
|
307
362
|
{
|
|
308
363
|
const remainingEnemyCast =
|
|
309
364
|
(enemy.castDuration ?? 0) - (enemy.castProgress ?? 0);
|
|
@@ -315,7 +370,7 @@ export const Pyromancer: WizardFunction = ({state}) =>
|
|
|
315
370
|
}
|
|
316
371
|
|
|
317
372
|
const safeBudget =
|
|
318
|
-
nextThreatTime - GCD_DURATION - SHIELD_CAST_TIME - SHIELD_BUFFER;
|
|
373
|
+
nextThreatTime - RULES.GCD_DURATION - RULES.SHIELD_CAST_TIME - SHIELD_BUFFER;
|
|
319
374
|
const budgetTicks = Math.min(safeBudget, MAX_CAST_BUDGET);
|
|
320
375
|
|
|
321
376
|
// Pyromancer wants homing missiles — no maxDamage cap so missiles punch through shields
|
|
@@ -323,28 +378,20 @@ export const Pyromancer: WizardFunction = ({state}) =>
|
|
|
323
378
|
const minDmg = Math.min(MIN_USEFUL_DAMAGE, enemyHP);
|
|
324
379
|
const config = fitMissileToBudget(budgetTicks, distance, {
|
|
325
380
|
minTurnRate,
|
|
326
|
-
lastMissileConfig:
|
|
381
|
+
lastMissileConfig: lastMissileConfig,
|
|
327
382
|
});
|
|
328
383
|
|
|
329
|
-
if (
|
|
330
|
-
config &&
|
|
331
|
-
config.damage >= minDmg &&
|
|
332
|
-
isSafeToAttack(threats, safeAttackCastEstimate)
|
|
333
|
-
)
|
|
384
|
+
if (config && config.damage >= minDmg)
|
|
334
385
|
{
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
missileAI: HomingAI,
|
|
341
|
-
direction: angleTo(state.position, enemy.position),
|
|
342
|
-
},
|
|
343
|
-
};
|
|
386
|
+
const actualCastTime = getMissileCastTime(config, lastMissileConfig);
|
|
387
|
+
if (isSafeToAttack(threats, actualCastTime))
|
|
388
|
+
{
|
|
389
|
+
return missile(config, HomingAI, angleTo(position, enemy.position)).move(safeMove.x, safeMove.y);
|
|
390
|
+
}
|
|
344
391
|
}
|
|
345
392
|
|
|
346
393
|
// Budget too small for useful missile — wait for threat to pass, then fire
|
|
347
394
|
}
|
|
348
395
|
|
|
349
|
-
return
|
|
350
|
-
}
|
|
396
|
+
return moveAction(safeMove.x, safeMove.y);
|
|
397
|
+
}
|