@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,10 @@
|
|
|
1
|
-
import {
|
|
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';
|
|
2
3
|
import {angleTo} from '../../utils/angles.js';
|
|
3
4
|
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';
|
|
5
|
+
import {RULES, ARENA_MIN, ARENA_MAX} from '../../rules.js';
|
|
10
6
|
import {getMissileCastTime, getLeadPosition} from '../../utils/combat.js';
|
|
11
7
|
import {
|
|
12
|
-
getThreats,
|
|
13
8
|
getMostUrgentThreat,
|
|
14
9
|
getDodgeDirectionForMovement,
|
|
15
10
|
isSafeToAttack,
|
|
@@ -21,27 +16,30 @@ import {
|
|
|
21
16
|
MIN_USEFUL_DAMAGE,
|
|
22
17
|
getClosingRate,
|
|
23
18
|
getEnemyVulnerabilityTicks,
|
|
19
|
+
keepOffWalls,
|
|
20
|
+
getPreCastReposition,
|
|
24
21
|
} from '../shared.js';
|
|
25
22
|
import {useParam} from '../../engine/params-runtime.js';
|
|
26
23
|
|
|
27
24
|
const WALL_BUFFER = 60;
|
|
28
25
|
const SHIELD_BUFFER = 3;
|
|
29
26
|
|
|
30
|
-
const HomingAI:
|
|
27
|
+
const HomingAI: MissileFunction = () =>
|
|
31
28
|
{
|
|
32
|
-
const
|
|
33
|
-
|
|
29
|
+
const ctx = getMissileContext();
|
|
30
|
+
const enemy = ctx.worldState.enemies[0];
|
|
31
|
+
return enemy ? turnToward(enemy.position.x, enemy.position.y) : flyStraight();
|
|
34
32
|
};
|
|
35
33
|
|
|
36
|
-
const StraightAI:
|
|
34
|
+
const StraightAI: MissileFunction = () => flyStraight();
|
|
37
35
|
|
|
38
36
|
function isNearWall(position: {x: number; y: number}): boolean
|
|
39
37
|
{
|
|
40
38
|
return (
|
|
41
|
-
position.x < WALL_BUFFER ||
|
|
42
|
-
position.x >
|
|
43
|
-
position.y < WALL_BUFFER ||
|
|
44
|
-
position.y >
|
|
39
|
+
position.x < ARENA_MIN + WALL_BUFFER ||
|
|
40
|
+
position.x > ARENA_MAX - WALL_BUFFER ||
|
|
41
|
+
position.y < ARENA_MIN + WALL_BUFFER ||
|
|
42
|
+
position.y > ARENA_MAX - WALL_BUFFER
|
|
45
43
|
);
|
|
46
44
|
}
|
|
47
45
|
|
|
@@ -72,12 +70,12 @@ function smartStrafe(
|
|
|
72
70
|
{
|
|
73
71
|
const fCw = {x: position.x + cw.x * 100, y: position.y + cw.y * 100};
|
|
74
72
|
const fCcw = {x: position.x + ccw.x * 100, y: position.y + ccw.y * 100};
|
|
75
|
-
const sCw = Math.min(fCw.x,
|
|
73
|
+
const sCw = Math.min(fCw.x - ARENA_MIN, ARENA_MAX - fCw.x, fCw.y - ARENA_MIN, ARENA_MAX - fCw.y);
|
|
76
74
|
const sCcw = Math.min(
|
|
77
75
|
fCcw.x,
|
|
78
|
-
|
|
76
|
+
ARENA_MAX - fCcw.x,
|
|
79
77
|
fCcw.y,
|
|
80
|
-
|
|
78
|
+
ARENA_MAX - fCcw.y,
|
|
81
79
|
);
|
|
82
80
|
dir = sCw > sCcw ? cw : ccw;
|
|
83
81
|
}
|
|
@@ -87,7 +85,7 @@ function smartStrafe(
|
|
|
87
85
|
dir = cycle === 0 ? cw : ccw;
|
|
88
86
|
}
|
|
89
87
|
|
|
90
|
-
const intensity = dodgeDirection ?
|
|
88
|
+
const intensity = dodgeDirection ? 1 : strafeIntensity;
|
|
91
89
|
return {x: dir.x * intensity, y: dir.y * intensity};
|
|
92
90
|
}
|
|
93
91
|
|
|
@@ -107,11 +105,24 @@ function smartStrafe(
|
|
|
107
105
|
* PROGRESSION LINE: Flamecaller → Pyromancer → Infernalist
|
|
108
106
|
* TIER: 3 (elite Caster line)
|
|
109
107
|
*/
|
|
110
|
-
export
|
|
108
|
+
export function Infernalist(): FinalAction
|
|
111
109
|
{
|
|
112
|
-
const
|
|
113
|
-
const
|
|
114
|
-
const
|
|
110
|
+
const position = usePosition();
|
|
111
|
+
const enemy = useEnemy();
|
|
112
|
+
const status = useStatus();
|
|
113
|
+
const blinkCooldown = useBlinkCooldown();
|
|
114
|
+
const health = useHealth();
|
|
115
|
+
const tick = useTick();
|
|
116
|
+
const threats = useThreats();
|
|
117
|
+
const projectiles = useProjectiles();
|
|
118
|
+
const lastMissileConfig = useLastMissileConfig();
|
|
119
|
+
const castingSpell = useCastingSpell();
|
|
120
|
+
const castProgressData = useCastProgress();
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
const targetDistance = useParam('targetDistance', 141, {range: 225, min: 0});
|
|
124
|
+
const minTurnRate = useParam('minTurnRate', 0.9, {range: 1.5, steps: 5});
|
|
125
|
+
const dangerDistance = useParam('dangerDistance', 332, {
|
|
115
126
|
range: 150,
|
|
116
127
|
min: 0,
|
|
117
128
|
});
|
|
@@ -125,61 +136,59 @@ export const Infernalist: WizardFunction = ({state}) =>
|
|
|
125
136
|
min: 10,
|
|
126
137
|
steps: 5,
|
|
127
138
|
});
|
|
128
|
-
const
|
|
129
|
-
useParam('preferBlinkDodge', 0, {min: 0, max: 1, steps: 2}) >= 0.5;
|
|
130
|
-
const strafeCyclePeriod = useParam('strafeCyclePeriod', 109, {
|
|
139
|
+
const strafeCyclePeriod = useParam('strafeCyclePeriod', 174, {
|
|
131
140
|
range: 75,
|
|
132
141
|
min: 80,
|
|
133
142
|
max: 250,
|
|
134
143
|
});
|
|
135
|
-
const strafeIntensity = useParam('strafeIntensity',
|
|
136
|
-
range:
|
|
137
|
-
min:
|
|
138
|
-
max:
|
|
144
|
+
const strafeIntensity = useParam('strafeIntensity', 0.87, {
|
|
145
|
+
range: 0.4,
|
|
146
|
+
min: 0.2,
|
|
147
|
+
max: 1,
|
|
139
148
|
steps: 7,
|
|
140
149
|
});
|
|
141
|
-
const approachSpeed = useParam('approachSpeed',
|
|
142
|
-
range:
|
|
143
|
-
min:
|
|
144
|
-
max:
|
|
150
|
+
const approachSpeed = useParam('approachSpeed', 0.66, {
|
|
151
|
+
range: 0.3,
|
|
152
|
+
min: 0.1,
|
|
153
|
+
max: 0.7,
|
|
145
154
|
steps: 7,
|
|
146
155
|
});
|
|
147
|
-
const distanceDeadZone = useParam('distanceDeadZone',
|
|
156
|
+
const distanceDeadZone = useParam('distanceDeadZone', 29, {
|
|
148
157
|
range: 30,
|
|
149
158
|
min: 5,
|
|
150
159
|
max: 80,
|
|
151
160
|
steps: 7,
|
|
152
161
|
});
|
|
153
|
-
const cancelHeavyDmg = useParam('cancelHeavyDmg',
|
|
162
|
+
const cancelHeavyDmg = useParam('cancelHeavyDmg', 31, {
|
|
154
163
|
range: 15,
|
|
155
164
|
min: 5,
|
|
156
165
|
max: 40,
|
|
157
166
|
steps: 5,
|
|
158
167
|
});
|
|
159
|
-
const cancelMediumDmg = useParam('cancelMediumDmg',
|
|
168
|
+
const cancelMediumDmg = useParam('cancelMediumDmg', 12, {
|
|
160
169
|
range: 10,
|
|
161
170
|
min: 3,
|
|
162
171
|
max: 25,
|
|
163
172
|
steps: 5,
|
|
164
173
|
});
|
|
165
|
-
const cancelCastRatio = useParam('cancelCastRatio', 0.
|
|
174
|
+
const cancelCastRatio = useParam('cancelCastRatio', 0.77, {
|
|
166
175
|
range: 0.4,
|
|
167
176
|
min: 0.3,
|
|
168
177
|
max: 1.0,
|
|
169
178
|
steps: 5,
|
|
170
179
|
});
|
|
171
|
-
const shieldCancelThreshold = useParam('shieldCancelThreshold',
|
|
180
|
+
const shieldCancelThreshold = useParam('shieldCancelThreshold', 60, {
|
|
172
181
|
range: 75,
|
|
173
182
|
min: 50,
|
|
174
183
|
max: 300,
|
|
175
184
|
});
|
|
176
|
-
const blinkWindowMax = useParam('blinkWindowMax',
|
|
185
|
+
const blinkWindowMax = useParam('blinkWindowMax', 23, {
|
|
177
186
|
range: 40,
|
|
178
187
|
min: 20,
|
|
179
188
|
max: 120,
|
|
180
189
|
steps: 7,
|
|
181
190
|
});
|
|
182
|
-
const proactiveBlinkRange = useParam('proactiveBlinkRange',
|
|
191
|
+
const proactiveBlinkRange = useParam('proactiveBlinkRange', 175, {
|
|
183
192
|
range: 150,
|
|
184
193
|
min: 100,
|
|
185
194
|
max: 500,
|
|
@@ -191,28 +200,37 @@ export const Infernalist: WizardFunction = ({state}) =>
|
|
|
191
200
|
max: 12,
|
|
192
201
|
steps: 5,
|
|
193
202
|
});
|
|
194
|
-
const
|
|
195
|
-
range:
|
|
196
|
-
min:
|
|
197
|
-
max:
|
|
203
|
+
const centerBias = useParam('centerBias', 0.13, {
|
|
204
|
+
range: 0.4,
|
|
205
|
+
min: 0,
|
|
206
|
+
max: 0.8,
|
|
198
207
|
steps: 5,
|
|
199
208
|
});
|
|
209
|
+
const maxBlinkDistance = useParam('maxBlinkDistance', 123, {
|
|
210
|
+
range: 100,
|
|
211
|
+
min: 50,
|
|
212
|
+
max: 300,
|
|
213
|
+
steps: 7,
|
|
214
|
+
});
|
|
200
215
|
|
|
201
|
-
|
|
202
|
-
if (!enemy)
|
|
216
|
+
function capBlinkDistance(target: {x: number; y: number}): {x: number; y: number}
|
|
203
217
|
{
|
|
204
|
-
|
|
218
|
+
const dx = target.x - position.x;
|
|
219
|
+
const dy = target.y - position.y;
|
|
220
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
221
|
+
if (dist <= maxBlinkDistance) return target;
|
|
222
|
+
const scale = maxBlinkDistance / dist;
|
|
223
|
+
return {x: position.x + dx * scale, y: position.y + dy * scale};
|
|
205
224
|
}
|
|
206
225
|
|
|
207
|
-
const distance = distanceTo(
|
|
208
|
-
const threats = getThreats(state);
|
|
226
|
+
const distance = distanceTo(position, enemy.position);
|
|
209
227
|
const urgentThreat = getMostUrgentThreat(threats);
|
|
210
|
-
const dodgeDirection = getDodgeDirectionForMovement(threats,
|
|
228
|
+
const dodgeDirection = getDodgeDirectionForMovement(threats, position);
|
|
211
229
|
const strafe = smartStrafe(
|
|
212
|
-
|
|
230
|
+
position,
|
|
213
231
|
enemy,
|
|
214
232
|
dodgeDirection,
|
|
215
|
-
|
|
233
|
+
tick,
|
|
216
234
|
strafeCyclePeriod,
|
|
217
235
|
strafeIntensity,
|
|
218
236
|
);
|
|
@@ -223,133 +241,158 @@ export const Infernalist: WizardFunction = ({state}) =>
|
|
|
223
241
|
|
|
224
242
|
if (!dodgeDirection)
|
|
225
243
|
{
|
|
226
|
-
const deltaX = enemy.position.x -
|
|
227
|
-
const deltaY = enemy.position.y -
|
|
244
|
+
const deltaX = enemy.position.x - position.x;
|
|
245
|
+
const deltaY = enemy.position.y - position.y;
|
|
228
246
|
const norm = distance > 0 ? distance : 1;
|
|
229
247
|
|
|
230
|
-
if (distance > targetDistance + distanceDeadZone)
|
|
248
|
+
if (distance > targetDistance + distanceDeadZone)
|
|
231
249
|
{
|
|
232
|
-
|
|
233
|
-
|
|
250
|
+
// Approach the center-side of the enemy so knockback pushes toward center, not lava.
|
|
251
|
+
const center = (ARENA_MIN + ARENA_MAX) / 2;
|
|
252
|
+
const toCenterX = center - enemy.position.x;
|
|
253
|
+
const toCenterY = center - enemy.position.y;
|
|
254
|
+
const toCenterDist = Math.sqrt(toCenterX * toCenterX + toCenterY * toCenterY) || 1;
|
|
255
|
+
const approachDirX = deltaX / norm + (toCenterX / toCenterDist) * centerBias;
|
|
256
|
+
const approachDirY = deltaY / norm + (toCenterY / toCenterDist) * centerBias;
|
|
257
|
+
const approachMag = Math.sqrt(approachDirX * approachDirX + approachDirY * approachDirY) || 1;
|
|
258
|
+
moveX += (approachDirX / approachMag) * approachSpeed;
|
|
259
|
+
moveY += (approachDirY / approachMag) * approachSpeed;
|
|
234
260
|
}
|
|
235
261
|
else if (distance < targetDistance - distanceDeadZone)
|
|
236
262
|
{
|
|
237
263
|
let rx = -(deltaX / norm) * approachSpeed;
|
|
238
264
|
let ry = -(deltaY / norm) * approachSpeed;
|
|
239
|
-
if (
|
|
240
|
-
if (
|
|
241
|
-
if (
|
|
242
|
-
if (
|
|
265
|
+
if (position.x < ARENA_MIN + WALL_BUFFER && rx < 0) rx = 0;
|
|
266
|
+
if (position.x > ARENA_MAX - WALL_BUFFER && rx > 0) rx = 0;
|
|
267
|
+
if (position.y < ARENA_MIN + WALL_BUFFER && ry < 0) ry = 0;
|
|
268
|
+
if (position.y > ARENA_MAX - WALL_BUFFER && ry > 0) ry = 0;
|
|
243
269
|
moveX += rx;
|
|
244
270
|
moveY += ry;
|
|
245
271
|
}
|
|
246
272
|
}
|
|
247
273
|
|
|
248
274
|
const mag = Math.sqrt(moveX * moveX + moveY * moveY);
|
|
249
|
-
if (mag >
|
|
275
|
+
if (mag > 1)
|
|
250
276
|
{
|
|
251
|
-
moveX =
|
|
252
|
-
moveY =
|
|
277
|
+
moveX = moveX / mag;
|
|
278
|
+
moveY = moveY / mag;
|
|
253
279
|
}
|
|
254
280
|
|
|
255
281
|
const move = {x: moveX, y: moveY};
|
|
282
|
+
const safeMove = keepOffWalls(position, move);
|
|
256
283
|
|
|
257
284
|
// === STEP 1: CHANNELING ===
|
|
258
|
-
if (
|
|
285
|
+
if (status === 'channeling')
|
|
259
286
|
{
|
|
260
287
|
if (!urgentThreat || urgentThreat.ticksToImpact > shieldCancelThreshold)
|
|
261
288
|
{
|
|
262
|
-
return
|
|
289
|
+
return cancel().move(safeMove.x, safeMove.y);
|
|
263
290
|
}
|
|
264
|
-
return
|
|
291
|
+
return idle();
|
|
265
292
|
}
|
|
266
293
|
|
|
267
294
|
// === STEP 2: PROGRESSIVE CAST-CANCEL ===
|
|
268
295
|
if (
|
|
269
|
-
|
|
270
|
-
|
|
296
|
+
status === 'casting' &&
|
|
297
|
+
castingSpell === 'missile' &&
|
|
271
298
|
urgentThreat &&
|
|
272
299
|
!urgentThreat.bestDodgeDirection
|
|
273
300
|
)
|
|
274
301
|
{
|
|
275
|
-
const remainingCast = (
|
|
302
|
+
const remainingCast = ((castProgressData?.total) ?? 0) - ((castProgressData?.current) ?? 0);
|
|
276
303
|
const defenseTime =
|
|
277
|
-
|
|
278
|
-
? BLINK_CAST_TIME
|
|
279
|
-
: SHIELD_CAST_TIME + SHIELD_BUFFER;
|
|
304
|
+
blinkCooldown === 0
|
|
305
|
+
? RULES.BLINK_CAST_TIME
|
|
306
|
+
: RULES.SHIELD_CAST_TIME + SHIELD_BUFFER;
|
|
280
307
|
const willArriveInWindow =
|
|
281
|
-
urgentThreat.ticksToImpact <= remainingCast + GCD_DURATION + defenseTime;
|
|
308
|
+
urgentThreat.ticksToImpact <= remainingCast + RULES.GCD_DURATION + defenseTime;
|
|
282
309
|
|
|
283
310
|
if (willArriveInWindow)
|
|
284
311
|
{
|
|
285
312
|
const incomingDamage = urgentThreat.projectile.damage;
|
|
286
|
-
const castRatio = (
|
|
313
|
+
const castRatio = ((castProgressData?.current) ?? 0) / ((castProgressData?.total) ?? 1);
|
|
287
314
|
|
|
288
|
-
if (incomingDamage >=
|
|
315
|
+
if (incomingDamage >= health || incomingDamage >= cancelHeavyDmg)
|
|
289
316
|
{
|
|
290
|
-
return
|
|
317
|
+
return cancel().move(safeMove.x, safeMove.y);
|
|
291
318
|
}
|
|
292
319
|
if (incomingDamage >= cancelMediumDmg && castRatio < cancelCastRatio)
|
|
293
320
|
{
|
|
294
|
-
return
|
|
321
|
+
return cancel().move(safeMove.x, safeMove.y);
|
|
295
322
|
}
|
|
296
323
|
}
|
|
297
324
|
}
|
|
298
325
|
|
|
299
|
-
// === BLINK-DODGE —
|
|
326
|
+
// === BLINK-DODGE — dodge undodgeable threats by blinking away ===
|
|
300
327
|
if (
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
)
|
|
328
|
+
status === 'idle' &&
|
|
329
|
+
urgentThreat &&
|
|
330
|
+
blinkCooldown === 0 &&
|
|
331
|
+
(!urgentThreat.bestDodgeDirection || shouldForceShield(urgentThreat)) &&
|
|
332
|
+
urgentThreat.ticksToImpact >= RULES.BLINK_CAST_TIME &&
|
|
333
|
+
urgentThreat.ticksToImpact <= blinkWindowMax
|
|
334
|
+
)
|
|
309
335
|
{
|
|
310
336
|
const blinkTarget = getBlinkToIncreaseDistance(
|
|
311
|
-
|
|
337
|
+
position,
|
|
312
338
|
enemy.position,
|
|
313
|
-
|
|
339
|
+
projectiles,
|
|
314
340
|
MIN_BLINK_ESCAPE_DISTANCE,
|
|
315
341
|
);
|
|
316
|
-
if (blinkTarget)
|
|
342
|
+
if (blinkTarget)
|
|
317
343
|
{
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
startCast: {spell: 'blink', target: blinkTarget},
|
|
321
|
-
};
|
|
344
|
+
const capped = capBlinkDistance(blinkTarget);
|
|
345
|
+
return blink(capped.x, capped.y);
|
|
322
346
|
}
|
|
323
347
|
}
|
|
324
348
|
|
|
325
349
|
// === STEP 3: SHIELD — undodgeable or high-damage homing threats ===
|
|
326
350
|
if (
|
|
327
|
-
|
|
351
|
+
status === 'idle' &&
|
|
328
352
|
urgentThreat &&
|
|
329
353
|
(!urgentThreat.bestDodgeDirection || shouldForceShield(urgentThreat))
|
|
330
|
-
)
|
|
354
|
+
)
|
|
331
355
|
{
|
|
356
|
+
if (urgentThreat.canBlockInTime && urgentThreat.ticksToStartShield <= SHIELD_BUFFER)
|
|
357
|
+
{
|
|
358
|
+
return shield();
|
|
359
|
+
}
|
|
360
|
+
if (urgentThreat.canBlockInTime)
|
|
361
|
+
{
|
|
362
|
+
return shield();
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// === Preemptive shield — shield when enemy is casting a missile at close range ===
|
|
367
|
+
if (
|
|
368
|
+
status === 'idle' &&
|
|
369
|
+
distance < 300 &&
|
|
370
|
+
enemy.status === 'casting' &&
|
|
371
|
+
enemy.castingSpell === 'missile'
|
|
372
|
+
)
|
|
373
|
+
{
|
|
374
|
+
const remainingCast = (enemy.castDuration ?? 0) - (enemy.castProgress ?? 0);
|
|
375
|
+
const estimatedFlightTicks = distance / 8;
|
|
376
|
+
const ticksUntilHit = remainingCast + estimatedFlightTicks;
|
|
377
|
+
|
|
332
378
|
if (
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
)
|
|
379
|
+
ticksUntilHit >= RULES.SHIELD_CAST_TIME &&
|
|
380
|
+
ticksUntilHit < RULES.SHIELD_CAST_TIME + 15
|
|
381
|
+
)
|
|
336
382
|
{
|
|
337
|
-
return
|
|
338
|
-
move: {x: 0, y: 0},
|
|
339
|
-
startCast: {spell: 'shield'},
|
|
340
|
-
};
|
|
383
|
+
return shield();
|
|
341
384
|
}
|
|
342
385
|
}
|
|
343
386
|
|
|
344
387
|
// === STEP 5: PROACTIVE BLINK KITING ===
|
|
345
388
|
if (
|
|
346
|
-
|
|
347
|
-
|
|
389
|
+
status === 'idle' &&
|
|
390
|
+
blinkCooldown === 0 &&
|
|
348
391
|
(!urgentThreat || urgentThreat.bestDodgeDirection !== null)
|
|
349
392
|
)
|
|
350
393
|
{
|
|
351
394
|
const closingRate = getClosingRate(
|
|
352
|
-
|
|
395
|
+
position,
|
|
353
396
|
enemy.position,
|
|
354
397
|
enemy.velocity,
|
|
355
398
|
);
|
|
@@ -359,24 +402,25 @@ export const Infernalist: WizardFunction = ({state}) =>
|
|
|
359
402
|
)
|
|
360
403
|
{
|
|
361
404
|
const blinkTarget = getBlinkToIncreaseDistance(
|
|
362
|
-
|
|
405
|
+
position,
|
|
363
406
|
enemy.position,
|
|
364
|
-
|
|
407
|
+
projectiles,
|
|
365
408
|
MIN_BLINK_ESCAPE_DISTANCE,
|
|
366
409
|
);
|
|
367
|
-
if (blinkTarget)
|
|
410
|
+
if (blinkTarget)
|
|
368
411
|
{
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
startCast: {spell: 'blink', target: blinkTarget},
|
|
372
|
-
};
|
|
412
|
+
const capped = capBlinkDistance(blinkTarget);
|
|
413
|
+
return blink(capped.x, capped.y);
|
|
373
414
|
}
|
|
374
415
|
}
|
|
375
416
|
}
|
|
376
417
|
|
|
377
418
|
// === STEP 6: OFFENSE ===
|
|
378
|
-
if (
|
|
419
|
+
if (status === 'idle')
|
|
379
420
|
{
|
|
421
|
+
const reposition = getPreCastReposition(position, enemy.position);
|
|
422
|
+
if (reposition) return moveAction(reposition.x, reposition.y);
|
|
423
|
+
|
|
380
424
|
const enemyHP = Math.ceil(enemy.health);
|
|
381
425
|
const vulnerabilityTicks = getEnemyVulnerabilityTicks(enemy);
|
|
382
426
|
|
|
@@ -387,7 +431,7 @@ export const Infernalist: WizardFunction = ({state}) =>
|
|
|
387
431
|
const punishBudget = vulnerabilityTicks - flightTimeEstimate;
|
|
388
432
|
const nextThreatTime = urgentThreat?.ticksToImpact ?? Infinity;
|
|
389
433
|
const safeBudget =
|
|
390
|
-
nextThreatTime - GCD_DURATION - SHIELD_CAST_TIME - SHIELD_BUFFER;
|
|
434
|
+
nextThreatTime - RULES.GCD_DURATION - RULES.SHIELD_CAST_TIME - SHIELD_BUFFER;
|
|
391
435
|
const effectiveBudget = Math.min(
|
|
392
436
|
punishBudget,
|
|
393
437
|
safeBudget,
|
|
@@ -399,12 +443,12 @@ export const Infernalist: WizardFunction = ({state}) =>
|
|
|
399
443
|
const config = fitMissileToBudget(effectiveBudget, distance, {
|
|
400
444
|
minTurnRate: 0,
|
|
401
445
|
maxDamage: enemyHP,
|
|
402
|
-
lastMissileConfig:
|
|
446
|
+
lastMissileConfig: lastMissileConfig,
|
|
403
447
|
});
|
|
404
448
|
|
|
405
449
|
if (config && config.damage >= MIN_USEFUL_DAMAGE)
|
|
406
450
|
{
|
|
407
|
-
const castTime = getMissileCastTime(config,
|
|
451
|
+
const castTime = getMissileCastTime(config, lastMissileConfig);
|
|
408
452
|
const actualFlight = distance / config.speed;
|
|
409
453
|
|
|
410
454
|
if (
|
|
@@ -419,17 +463,9 @@ export const Infernalist: WizardFunction = ({state}) =>
|
|
|
419
463
|
enemy.position,
|
|
420
464
|
enemy.velocity,
|
|
421
465
|
config.speed,
|
|
422
|
-
|
|
466
|
+
position,
|
|
423
467
|
);
|
|
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
|
-
};
|
|
468
|
+
return missile(config, config.turnRate > 0 ? HomingAI : StraightAI, angleTo(position, aimTarget)).move(safeMove.x, safeMove.y);
|
|
433
469
|
}
|
|
434
470
|
}
|
|
435
471
|
}
|
|
@@ -438,55 +474,39 @@ export const Infernalist: WizardFunction = ({state}) =>
|
|
|
438
474
|
// --- STANDARD MODE: warmup-consistent homing missiles ---
|
|
439
475
|
const nextThreatTime = urgentThreat?.ticksToImpact ?? Infinity;
|
|
440
476
|
const safeBudget =
|
|
441
|
-
nextThreatTime - GCD_DURATION - SHIELD_CAST_TIME - SHIELD_BUFFER;
|
|
477
|
+
nextThreatTime - RULES.GCD_DURATION - RULES.SHIELD_CAST_TIME - SHIELD_BUFFER;
|
|
442
478
|
const budgetTicks = Math.min(safeBudget, MAX_CAST_BUDGET);
|
|
443
479
|
|
|
444
480
|
const minDmg = Math.min(MIN_USEFUL_DAMAGE, enemyHP);
|
|
445
481
|
let config = fitMissileToBudget(budgetTicks, distance, {
|
|
446
482
|
minTurnRate,
|
|
447
483
|
maxDamage: enemyHP,
|
|
448
|
-
lastMissileConfig:
|
|
484
|
+
lastMissileConfig: lastMissileConfig,
|
|
449
485
|
});
|
|
450
486
|
|
|
451
|
-
if (
|
|
452
|
-
config &&
|
|
453
|
-
config.damage >= minDmg &&
|
|
454
|
-
isSafeToAttack(threats, safeAttackCastEstimate)
|
|
455
|
-
)
|
|
487
|
+
if (config && config.damage >= minDmg)
|
|
456
488
|
{
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
missileAI: HomingAI,
|
|
463
|
-
direction: angleTo(state.position, enemy.position),
|
|
464
|
-
},
|
|
465
|
-
};
|
|
489
|
+
const actualCastTime = getMissileCastTime(config, lastMissileConfig);
|
|
490
|
+
if (isSafeToAttack(threats, actualCastTime))
|
|
491
|
+
{
|
|
492
|
+
return missile(config, HomingAI, angleTo(position, enemy.position)).move(safeMove.x, safeMove.y);
|
|
493
|
+
}
|
|
466
494
|
}
|
|
467
495
|
|
|
468
|
-
// Fallback: fire bigger missile, accept incoming hit
|
|
469
|
-
if (!config || config.damage < minDmg)
|
|
496
|
+
// Fallback: fire bigger missile, accept incoming hit — but only if no undodgeable threat
|
|
497
|
+
if (!config || config.damage < minDmg)
|
|
470
498
|
{
|
|
471
499
|
config = fitMissileToBudget(MAX_CAST_BUDGET, distance, {
|
|
472
500
|
minTurnRate,
|
|
473
501
|
maxDamage: enemyHP,
|
|
474
|
-
lastMissileConfig:
|
|
502
|
+
lastMissileConfig: lastMissileConfig,
|
|
475
503
|
});
|
|
476
|
-
if (config)
|
|
504
|
+
if (config && (!urgentThreat || urgentThreat.bestDodgeDirection !== null))
|
|
477
505
|
{
|
|
478
|
-
return
|
|
479
|
-
move,
|
|
480
|
-
startCast: {
|
|
481
|
-
spell: 'missile',
|
|
482
|
-
config,
|
|
483
|
-
missileAI: HomingAI,
|
|
484
|
-
direction: angleTo(state.position, enemy.position),
|
|
485
|
-
},
|
|
486
|
-
};
|
|
506
|
+
return missile(config, HomingAI, angleTo(position, enemy.position)).move(safeMove.x, safeMove.y);
|
|
487
507
|
}
|
|
488
508
|
}
|
|
489
509
|
}
|
|
490
510
|
|
|
491
|
-
return
|
|
492
|
-
}
|
|
511
|
+
return moveAction(safeMove.x, safeMove.y);
|
|
512
|
+
}
|