@vibemancer/core 0.1.4 → 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-74FFJCFF.js +0 -9140
- package/dist/chunk-74FFJCFF.js.map +0 -1
- package/src/hooks/bot-wrapper.ts +0 -84
|
@@ -1,15 +1,11 @@
|
|
|
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, 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
5
|
import {interceptAngle, moveInDirection} from '../../utils/movement.js';
|
|
9
|
-
import {
|
|
6
|
+
import {RULES, ARENA_MIN, ARENA_MAX} from '../../rules.js';
|
|
10
7
|
import {getMissileCastTime, getLeadPosition} from '../../utils/combat.js';
|
|
11
8
|
import {
|
|
12
|
-
getThreats,
|
|
13
9
|
getMostUrgentThreat,
|
|
14
10
|
getDodgeDirectionForMovement,
|
|
15
11
|
getBlinkToCenter,
|
|
@@ -18,6 +14,8 @@ import {
|
|
|
18
14
|
MAX_CAST_BUDGET,
|
|
19
15
|
MIN_USEFUL_DAMAGE,
|
|
20
16
|
getEnemyVulnerabilityTicks,
|
|
17
|
+
keepOffWalls,
|
|
18
|
+
getPreCastReposition,
|
|
21
19
|
} from '../shared.js';
|
|
22
20
|
import {useParam} from '../../engine/params-runtime.js';
|
|
23
21
|
|
|
@@ -28,28 +26,27 @@ const SHIELD_BUFFER = 3;
|
|
|
28
26
|
/**
|
|
29
27
|
* Predictive homing missile AI - leads the target.
|
|
30
28
|
*/
|
|
31
|
-
const HomingAI:
|
|
32
|
-
worldState,
|
|
33
|
-
missileState,
|
|
34
|
-
}) =>
|
|
29
|
+
const HomingAI: MissileFunction = () =>
|
|
35
30
|
{
|
|
36
|
-
const
|
|
37
|
-
|
|
31
|
+
const ctx = getMissileContext();
|
|
32
|
+
const enemy = ctx.worldState.enemies[0];
|
|
33
|
+
if (enemy.health <= 0) return flyStraight();
|
|
38
34
|
|
|
39
35
|
const angle = interceptAngle(
|
|
40
|
-
|
|
36
|
+
ctx.position,
|
|
41
37
|
enemy.position,
|
|
42
38
|
enemy.velocity,
|
|
43
|
-
|
|
39
|
+
ctx.speed,
|
|
44
40
|
);
|
|
45
41
|
if (angle !== null)
|
|
46
42
|
{
|
|
47
|
-
|
|
43
|
+
const target = moveInDirection(ctx.position, angle, 100);
|
|
44
|
+
return turnToward(target.x, target.y);
|
|
48
45
|
}
|
|
49
|
-
return
|
|
46
|
+
return turnToward(enemy.position.x, enemy.position.y);
|
|
50
47
|
};
|
|
51
48
|
|
|
52
|
-
const StraightAI:
|
|
49
|
+
const StraightAI: MissileFunction = () => flyStraight();
|
|
53
50
|
|
|
54
51
|
/**
|
|
55
52
|
* Check if position is near a wall.
|
|
@@ -57,10 +54,10 @@ const StraightAI: MissileAIFunction = () => ({});
|
|
|
57
54
|
function isNearWall(position: {x: number; y: number}): boolean
|
|
58
55
|
{
|
|
59
56
|
return (
|
|
60
|
-
position.x < WALL_BUFFER ||
|
|
61
|
-
position.x >
|
|
62
|
-
position.y < WALL_BUFFER ||
|
|
63
|
-
position.y >
|
|
57
|
+
position.x < ARENA_MIN + WALL_BUFFER ||
|
|
58
|
+
position.x > ARENA_MAX - WALL_BUFFER ||
|
|
59
|
+
position.y < ARENA_MIN + WALL_BUFFER ||
|
|
60
|
+
position.y > ARENA_MAX - WALL_BUFFER
|
|
64
61
|
);
|
|
65
62
|
}
|
|
66
63
|
|
|
@@ -78,12 +75,22 @@ function combatMove(
|
|
|
78
75
|
strafeIntensity: number,
|
|
79
76
|
approachSpeed: number,
|
|
80
77
|
distanceDeadZone: number,
|
|
78
|
+
centerBias: number,
|
|
81
79
|
): {x: number; y: number}
|
|
82
80
|
{
|
|
83
81
|
const deltaX = enemy.position.x - position.x;
|
|
84
82
|
const deltaY = enemy.position.y - position.y;
|
|
85
83
|
const normalizedDistance = currentDistance > 0 ? currentDistance : 1;
|
|
86
84
|
|
|
85
|
+
// Approach the center-side of the enemy so knockback pushes toward center, not lava.
|
|
86
|
+
const center = (ARENA_MIN + ARENA_MAX) / 2;
|
|
87
|
+
const toCenterX = center - enemy.position.x;
|
|
88
|
+
const toCenterY = center - enemy.position.y;
|
|
89
|
+
const toCenterDist = Math.sqrt(toCenterX * toCenterX + toCenterY * toCenterY) || 1;
|
|
90
|
+
const approachDirX = deltaX / normalizedDistance + (toCenterX / toCenterDist) * centerBias;
|
|
91
|
+
const approachDirY = deltaY / normalizedDistance + (toCenterY / toCenterDist) * centerBias;
|
|
92
|
+
const approachMag = Math.sqrt(approachDirX * approachDirX + approachDirY * approachDirY) || 1;
|
|
93
|
+
|
|
87
94
|
const strafeClockwise = {
|
|
88
95
|
x: -deltaY / normalizedDistance,
|
|
89
96
|
y: deltaX / normalizedDistance,
|
|
@@ -110,16 +117,16 @@ function combatMove(
|
|
|
110
117
|
y: position.y + strafeCounterClockwise.y * 100,
|
|
111
118
|
};
|
|
112
119
|
const scoreClockwise = Math.min(
|
|
113
|
-
futureClockwise.x,
|
|
114
|
-
|
|
115
|
-
futureClockwise.y,
|
|
116
|
-
|
|
120
|
+
futureClockwise.x - ARENA_MIN,
|
|
121
|
+
ARENA_MAX - futureClockwise.x,
|
|
122
|
+
futureClockwise.y - ARENA_MIN,
|
|
123
|
+
ARENA_MAX - futureClockwise.y,
|
|
117
124
|
);
|
|
118
125
|
const scoreCounterClockwise = Math.min(
|
|
119
|
-
futureCounterClockwise.x,
|
|
120
|
-
|
|
121
|
-
futureCounterClockwise.y,
|
|
122
|
-
|
|
126
|
+
futureCounterClockwise.x - ARENA_MIN,
|
|
127
|
+
ARENA_MAX - futureCounterClockwise.x,
|
|
128
|
+
futureCounterClockwise.y - ARENA_MIN,
|
|
129
|
+
ARENA_MAX - futureCounterClockwise.y,
|
|
123
130
|
);
|
|
124
131
|
strafeDirection =
|
|
125
132
|
scoreClockwise > scoreCounterClockwise
|
|
@@ -134,7 +141,7 @@ function combatMove(
|
|
|
134
141
|
|
|
135
142
|
if (dodgeDirection)
|
|
136
143
|
{
|
|
137
|
-
return {x: strafeDirection.x
|
|
144
|
+
return {x: strafeDirection.x, y: strafeDirection.y};
|
|
138
145
|
}
|
|
139
146
|
|
|
140
147
|
let moveX = strafeDirection.x * strafeIntensity;
|
|
@@ -142,26 +149,26 @@ function combatMove(
|
|
|
142
149
|
|
|
143
150
|
if (currentDistance > targetDistance + distanceDeadZone)
|
|
144
151
|
{
|
|
145
|
-
moveX += (
|
|
146
|
-
moveY += (
|
|
152
|
+
moveX += (approachDirX / approachMag) * approachSpeed;
|
|
153
|
+
moveY += (approachDirY / approachMag) * approachSpeed;
|
|
147
154
|
}
|
|
148
155
|
else if (currentDistance < targetDistance - distanceDeadZone)
|
|
149
156
|
{
|
|
150
157
|
let retreatX = -(deltaX / normalizedDistance) * approachSpeed;
|
|
151
158
|
let retreatY = -(deltaY / normalizedDistance) * approachSpeed;
|
|
152
|
-
if (position.x < WALL_BUFFER && retreatX < 0) retreatX = 0;
|
|
153
|
-
if (position.x >
|
|
154
|
-
if (position.y < WALL_BUFFER && retreatY < 0) retreatY = 0;
|
|
155
|
-
if (position.y >
|
|
159
|
+
if (position.x < ARENA_MIN + WALL_BUFFER && retreatX < 0) retreatX = 0;
|
|
160
|
+
if (position.x > ARENA_MAX - WALL_BUFFER && retreatX > 0) retreatX = 0;
|
|
161
|
+
if (position.y < ARENA_MIN + WALL_BUFFER && retreatY < 0) retreatY = 0;
|
|
162
|
+
if (position.y > ARENA_MAX - WALL_BUFFER && retreatY > 0) retreatY = 0;
|
|
156
163
|
moveX += retreatX;
|
|
157
164
|
moveY += retreatY;
|
|
158
165
|
}
|
|
159
166
|
|
|
160
167
|
const magnitude = Math.sqrt(moveX * moveX + moveY * moveY);
|
|
161
|
-
if (magnitude >
|
|
168
|
+
if (magnitude > 1)
|
|
162
169
|
{
|
|
163
|
-
moveX =
|
|
164
|
-
moveY =
|
|
170
|
+
moveX = moveX / magnitude;
|
|
171
|
+
moveY = moveY / magnitude;
|
|
165
172
|
}
|
|
166
173
|
|
|
167
174
|
return {x: moveX, y: moveY};
|
|
@@ -183,37 +190,49 @@ function combatMove(
|
|
|
183
190
|
*
|
|
184
191
|
* TIER: 3 (elite Berserker line)
|
|
185
192
|
*/
|
|
186
|
-
export
|
|
193
|
+
export function Stormforger(): FinalAction
|
|
187
194
|
{
|
|
195
|
+
const position = usePosition();
|
|
196
|
+
const enemy = useEnemy();
|
|
197
|
+
const status = useStatus();
|
|
198
|
+
const blinkCooldown = useBlinkCooldown();
|
|
199
|
+
const health = useHealth();
|
|
200
|
+
const tick = useTick();
|
|
201
|
+
const threats = useThreats();
|
|
202
|
+
const lastMissileConfig = useLastMissileConfig();
|
|
203
|
+
const castingSpell = useCastingSpell();
|
|
204
|
+
const castProgressData = useCastProgress();
|
|
205
|
+
|
|
206
|
+
|
|
188
207
|
// Tunable parameters (run optimizer script to find optimal values)
|
|
189
|
-
const targetDistance = useParam('targetDistance',
|
|
208
|
+
const targetDistance = useParam('targetDistance', 159, {
|
|
190
209
|
range: 200,
|
|
191
210
|
min: 0,
|
|
192
211
|
});
|
|
193
|
-
const minTurnRate = useParam('minTurnRate',
|
|
212
|
+
const minTurnRate = useParam('minTurnRate', -1, {range: 1.5, steps: 5});
|
|
194
213
|
const shieldHealthThreshold = useParam('shieldHealthThreshold', 20, {
|
|
195
214
|
range: 23,
|
|
196
215
|
min: 1,
|
|
197
216
|
steps: 7,
|
|
198
217
|
});
|
|
199
|
-
const strafeCyclePeriod = useParam('strafeCyclePeriod',
|
|
218
|
+
const strafeCyclePeriod = useParam('strafeCyclePeriod', 96, {
|
|
200
219
|
range: 75,
|
|
201
220
|
min: 80,
|
|
202
221
|
max: 300,
|
|
203
222
|
});
|
|
204
|
-
const strafeIntensity = useParam('strafeIntensity',
|
|
205
|
-
range:
|
|
206
|
-
min:
|
|
207
|
-
max:
|
|
223
|
+
const strafeIntensity = useParam('strafeIntensity', 0.88, {
|
|
224
|
+
range: 0.4,
|
|
225
|
+
min: 0.2,
|
|
226
|
+
max: 1,
|
|
208
227
|
steps: 7,
|
|
209
228
|
});
|
|
210
|
-
const approachSpeed = useParam('approachSpeed',
|
|
211
|
-
range:
|
|
212
|
-
min:
|
|
213
|
-
max:
|
|
229
|
+
const approachSpeed = useParam('approachSpeed', 0.6, {
|
|
230
|
+
range: 0.3,
|
|
231
|
+
min: 0.1,
|
|
232
|
+
max: 0.7,
|
|
214
233
|
steps: 7,
|
|
215
234
|
});
|
|
216
|
-
const distanceDeadZone = useParam('distanceDeadZone',
|
|
235
|
+
const distanceDeadZone = useParam('distanceDeadZone', 41, {
|
|
217
236
|
range: 25,
|
|
218
237
|
min: 10,
|
|
219
238
|
max: 60,
|
|
@@ -224,83 +243,100 @@ export const Stormforger: WizardFunction = ({state}) =>
|
|
|
224
243
|
min: 50,
|
|
225
244
|
max: 300,
|
|
226
245
|
});
|
|
227
|
-
const blinkWindowMax = useParam('blinkWindowMax',
|
|
246
|
+
const blinkWindowMax = useParam('blinkWindowMax', 97, {
|
|
228
247
|
range: 40,
|
|
229
248
|
min: 20,
|
|
230
249
|
max: 120,
|
|
231
250
|
steps: 7,
|
|
232
251
|
});
|
|
233
|
-
const attackRange = useParam('attackRange',
|
|
252
|
+
const attackRange = useParam('attackRange', 586, {
|
|
234
253
|
range: 200,
|
|
235
254
|
min: 300,
|
|
236
255
|
max: 800,
|
|
237
256
|
});
|
|
238
|
-
const flightTimeDivisor = useParam('flightTimeDivisor', 11.
|
|
257
|
+
const flightTimeDivisor = useParam('flightTimeDivisor', 11.5, {
|
|
239
258
|
range: 4,
|
|
240
259
|
min: 2,
|
|
241
260
|
max: 12,
|
|
242
261
|
steps: 5,
|
|
243
262
|
});
|
|
244
|
-
const punishMinVulnerability = useParam('punishMinVulnerability',
|
|
263
|
+
const punishMinVulnerability = useParam('punishMinVulnerability', 239, {
|
|
245
264
|
range: 80,
|
|
246
265
|
min: 30,
|
|
247
266
|
steps: 7,
|
|
248
267
|
});
|
|
268
|
+
const centerBias = useParam('centerBias', 0.16, {
|
|
269
|
+
range: 0.4,
|
|
270
|
+
min: 0,
|
|
271
|
+
max: 0.8,
|
|
272
|
+
steps: 5,
|
|
273
|
+
});
|
|
274
|
+
const maxBlinkDistance = useParam('maxBlinkDistance', 234, {
|
|
275
|
+
range: 100,
|
|
276
|
+
min: 50,
|
|
277
|
+
max: 300,
|
|
278
|
+
steps: 7,
|
|
279
|
+
});
|
|
249
280
|
|
|
250
|
-
const
|
|
251
|
-
|
|
281
|
+
const distance = distanceTo(position, enemy.position);
|
|
282
|
+
|
|
283
|
+
function capBlinkDistance(target: {x: number; y: number}): {x: number; y: number}
|
|
252
284
|
{
|
|
253
|
-
|
|
285
|
+
const dx = target.x - position.x;
|
|
286
|
+
const dy = target.y - position.y;
|
|
287
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
288
|
+
if (dist <= maxBlinkDistance) return target;
|
|
289
|
+
const scale = maxBlinkDistance / dist;
|
|
290
|
+
return {x: position.x + dx * scale, y: position.y + dy * scale};
|
|
254
291
|
}
|
|
255
292
|
|
|
256
|
-
const distance = distanceTo(state.position, enemy.position);
|
|
257
|
-
|
|
258
293
|
// Analyze threats using simulation
|
|
259
|
-
const threats = getThreats(state);
|
|
260
294
|
const urgentThreat = getMostUrgentThreat(threats);
|
|
261
295
|
|
|
262
296
|
// === DEFENSE: Handle channeling state ===
|
|
263
|
-
if (
|
|
297
|
+
if (status === 'channeling')
|
|
264
298
|
{
|
|
265
299
|
if (!urgentThreat || urgentThreat.ticksToImpact > shieldCancelThreshold)
|
|
266
300
|
{
|
|
267
|
-
return
|
|
301
|
+
return cancel();
|
|
268
302
|
}
|
|
269
|
-
return
|
|
303
|
+
return idle();
|
|
270
304
|
}
|
|
271
305
|
|
|
272
306
|
// === DEFENSE: Cancel missile cast only for LETHAL incoming damage ===
|
|
273
307
|
// DPS trade: we accepted the hit when we started casting. Only cancel to survive.
|
|
274
308
|
if (
|
|
275
|
-
|
|
276
|
-
|
|
309
|
+
status === 'casting' &&
|
|
310
|
+
castingSpell === 'missile' &&
|
|
277
311
|
urgentThreat &&
|
|
278
|
-
urgentThreat.projectile.damage >=
|
|
312
|
+
urgentThreat.projectile.damage >= health
|
|
279
313
|
)
|
|
280
314
|
{
|
|
281
|
-
const remainingCast = (
|
|
315
|
+
const remainingCast = ((castProgressData?.total) ?? 0) - ((castProgressData?.current) ?? 0);
|
|
282
316
|
if (
|
|
283
317
|
urgentThreat.ticksToImpact <=
|
|
284
|
-
remainingCast + GCD_DURATION + SHIELD_CAST_TIME + 1
|
|
318
|
+
remainingCast + RULES.GCD_DURATION + RULES.SHIELD_CAST_TIME + 1
|
|
285
319
|
)
|
|
286
320
|
{
|
|
287
321
|
const dodgeDirection = getDodgeDirectionForMovement(
|
|
288
322
|
threats,
|
|
289
|
-
|
|
323
|
+
position,
|
|
290
324
|
);
|
|
291
|
-
const
|
|
292
|
-
|
|
325
|
+
const cancelMove = combatMove(
|
|
326
|
+
position,
|
|
293
327
|
enemy,
|
|
294
328
|
distance,
|
|
295
329
|
targetDistance,
|
|
296
330
|
dodgeDirection,
|
|
297
|
-
|
|
331
|
+
tick,
|
|
298
332
|
strafeCyclePeriod,
|
|
299
333
|
strafeIntensity,
|
|
300
334
|
approachSpeed,
|
|
301
335
|
distanceDeadZone,
|
|
336
|
+
centerBias,
|
|
302
337
|
);
|
|
303
|
-
|
|
338
|
+
const safeCancelMove = keepOffWalls(position, cancelMove);
|
|
339
|
+
return cancel().move(safeCancelMove.x, safeCancelMove.y);
|
|
304
340
|
}
|
|
305
341
|
}
|
|
306
342
|
|
|
@@ -308,27 +344,25 @@ export const Stormforger: WizardFunction = ({state}) =>
|
|
|
308
344
|
// Blink dodges the incoming missile — no GCD after, so can immediately fire back.
|
|
309
345
|
// Only shield when blink is on cooldown.
|
|
310
346
|
if (
|
|
311
|
-
|
|
347
|
+
status === 'idle' &&
|
|
312
348
|
urgentThreat &&
|
|
313
|
-
|
|
349
|
+
blinkCooldown === 0 &&
|
|
314
350
|
urgentThreat.ticksToImpact >= 10 &&
|
|
315
351
|
urgentThreat.ticksToImpact <= blinkWindowMax
|
|
316
352
|
)
|
|
317
353
|
{
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
startCast: {spell: 'blink', target: getBlinkToCenter(state.position)},
|
|
321
|
-
};
|
|
354
|
+
const blinkTarget = capBlinkDistance(getBlinkToCenter(position));
|
|
355
|
+
return blink(blinkTarget.x, blinkTarget.y);
|
|
322
356
|
}
|
|
323
357
|
|
|
324
358
|
// === DEFENSE: Shield threats (when blink is on cooldown) ===
|
|
325
|
-
if (
|
|
359
|
+
if (status === 'idle' && urgentThreat)
|
|
326
360
|
{
|
|
327
361
|
const forceShield = shouldForceShield(urgentThreat);
|
|
328
362
|
const undodgeable = !urgentThreat.bestDodgeDirection;
|
|
329
|
-
const healthLow =
|
|
363
|
+
const healthLow = health < shieldHealthThreshold;
|
|
330
364
|
const healthCritical =
|
|
331
|
-
|
|
365
|
+
health <= urgentThreat.projectile.damage * 2 + 1;
|
|
332
366
|
const doShield =
|
|
333
367
|
forceShield || (undodgeable && healthLow) || healthCritical;
|
|
334
368
|
|
|
@@ -336,32 +370,54 @@ export const Stormforger: WizardFunction = ({state}) =>
|
|
|
336
370
|
{
|
|
337
371
|
if (urgentThreat.ticksToStartShield <= 3)
|
|
338
372
|
{
|
|
339
|
-
return
|
|
340
|
-
move: {x: 0, y: 0},
|
|
341
|
-
startCast: {spell: 'shield'},
|
|
342
|
-
};
|
|
373
|
+
return shield();
|
|
343
374
|
}
|
|
344
375
|
}
|
|
345
376
|
}
|
|
346
377
|
|
|
378
|
+
// === Preemptive shield — shield when enemy is casting a missile at close range ===
|
|
379
|
+
if (
|
|
380
|
+
status === 'idle' &&
|
|
381
|
+
distance < 300 &&
|
|
382
|
+
enemy.status === 'casting' &&
|
|
383
|
+
enemy.castingSpell === 'missile'
|
|
384
|
+
)
|
|
385
|
+
{
|
|
386
|
+
const remainingCast = (enemy.castDuration ?? 0) - (enemy.castProgress ?? 0);
|
|
387
|
+
const estimatedFlightTicks = distance / 8;
|
|
388
|
+
const ticksUntilHit = remainingCast + estimatedFlightTicks;
|
|
389
|
+
|
|
390
|
+
if (
|
|
391
|
+
ticksUntilHit >= RULES.SHIELD_CAST_TIME &&
|
|
392
|
+
ticksUntilHit < RULES.SHIELD_CAST_TIME + 15
|
|
393
|
+
)
|
|
394
|
+
{
|
|
395
|
+
return shield();
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
347
399
|
// === MOVEMENT: Smart combat movement ===
|
|
348
|
-
const dodgeDirection = getDodgeDirectionForMovement(threats,
|
|
400
|
+
const dodgeDirection = getDodgeDirectionForMovement(threats, position);
|
|
349
401
|
const move = combatMove(
|
|
350
|
-
|
|
402
|
+
position,
|
|
351
403
|
enemy,
|
|
352
404
|
distance,
|
|
353
405
|
targetDistance,
|
|
354
406
|
dodgeDirection,
|
|
355
|
-
|
|
407
|
+
tick,
|
|
356
408
|
strafeCyclePeriod,
|
|
357
409
|
strafeIntensity,
|
|
358
410
|
approachSpeed,
|
|
359
411
|
distanceDeadZone,
|
|
412
|
+
centerBias,
|
|
360
413
|
);
|
|
361
|
-
|
|
414
|
+
const safeMove = keepOffWalls(position, move);
|
|
362
415
|
// === OFFENSE: Vulnerability punish + adaptive missile fitting ===
|
|
363
|
-
if (
|
|
416
|
+
if (status === 'idle' && distance < attackRange)
|
|
364
417
|
{
|
|
418
|
+
const reposition = getPreCastReposition(position, enemy.position);
|
|
419
|
+
if (reposition) return moveAction(reposition.x, reposition.y);
|
|
420
|
+
|
|
365
421
|
const enemyHP = Math.ceil(enemy.health);
|
|
366
422
|
const vulnerabilityTicks = getEnemyVulnerabilityTicks(enemy);
|
|
367
423
|
|
|
@@ -381,13 +437,13 @@ export const Stormforger: WizardFunction = ({state}) =>
|
|
|
381
437
|
{
|
|
382
438
|
minTurnRate: 0,
|
|
383
439
|
maxDamage: enemyHP,
|
|
384
|
-
lastMissileConfig:
|
|
440
|
+
lastMissileConfig: lastMissileConfig,
|
|
385
441
|
},
|
|
386
442
|
);
|
|
387
443
|
|
|
388
444
|
if (config && config.damage >= MIN_USEFUL_DAMAGE)
|
|
389
445
|
{
|
|
390
|
-
const castTime = getMissileCastTime(config,
|
|
446
|
+
const castTime = getMissileCastTime(config, lastMissileConfig);
|
|
391
447
|
const flightTime = distance / config.speed;
|
|
392
448
|
|
|
393
449
|
if (castTime + flightTime < vulnerabilityTicks)
|
|
@@ -399,18 +455,9 @@ export const Stormforger: WizardFunction = ({state}) =>
|
|
|
399
455
|
enemy.position,
|
|
400
456
|
enemy.velocity,
|
|
401
457
|
config.speed,
|
|
402
|
-
|
|
458
|
+
position,
|
|
403
459
|
);
|
|
404
|
-
return
|
|
405
|
-
move,
|
|
406
|
-
startCast: {
|
|
407
|
-
spell: 'missile',
|
|
408
|
-
config,
|
|
409
|
-
missileAI:
|
|
410
|
-
config.turnRate > 0 ? HomingAI : StraightAI,
|
|
411
|
-
direction: angleTo(state.position, aimTarget),
|
|
412
|
-
},
|
|
413
|
-
};
|
|
460
|
+
return missile(config, config.turnRate > 0 ? HomingAI : StraightAI, angleTo(position, aimTarget)).move(safeMove.x, safeMove.y);
|
|
414
461
|
}
|
|
415
462
|
}
|
|
416
463
|
}
|
|
@@ -420,7 +467,7 @@ export const Stormforger: WizardFunction = ({state}) =>
|
|
|
420
467
|
let nextThreatTime = urgentThreat?.ticksToImpact ?? Infinity;
|
|
421
468
|
|
|
422
469
|
// Account for enemy's active cast — their missile will arrive after cast completes + flight time
|
|
423
|
-
if (enemy.
|
|
470
|
+
if (enemy.status === 'casting' && enemy.castingSpell === 'missile')
|
|
424
471
|
{
|
|
425
472
|
const remainingEnemyCast =
|
|
426
473
|
(enemy.castDuration ?? 0) - (enemy.castProgress ?? 0);
|
|
@@ -432,7 +479,7 @@ export const Stormforger: WizardFunction = ({state}) =>
|
|
|
432
479
|
}
|
|
433
480
|
|
|
434
481
|
const safeBudget =
|
|
435
|
-
nextThreatTime - GCD_DURATION - SHIELD_CAST_TIME - SHIELD_BUFFER;
|
|
482
|
+
nextThreatTime - RULES.GCD_DURATION - RULES.SHIELD_CAST_TIME - SHIELD_BUFFER;
|
|
436
483
|
const budgetTicks = Math.min(safeBudget, MAX_CAST_BUDGET);
|
|
437
484
|
|
|
438
485
|
// Predictive homing compensates for movement during cast
|
|
@@ -440,23 +487,12 @@ export const Stormforger: WizardFunction = ({state}) =>
|
|
|
440
487
|
const config = fitMissileToBudget(budgetTicks, distance, {
|
|
441
488
|
minTurnRate,
|
|
442
489
|
maxDamage: enemyHP,
|
|
443
|
-
lastMissileConfig:
|
|
490
|
+
lastMissileConfig: lastMissileConfig,
|
|
444
491
|
});
|
|
445
492
|
|
|
446
493
|
if (config && config.damage >= minDmg)
|
|
447
494
|
{
|
|
448
|
-
return
|
|
449
|
-
move,
|
|
450
|
-
startCast: {
|
|
451
|
-
spell: 'missile',
|
|
452
|
-
config,
|
|
453
|
-
missileAI:
|
|
454
|
-
config.turnRate > 0
|
|
455
|
-
? HomingAI
|
|
456
|
-
: (): MissileActions => ({}),
|
|
457
|
-
direction: angleTo(state.position, enemy.position),
|
|
458
|
-
},
|
|
459
|
-
};
|
|
495
|
+
return missile(config, config.turnRate > 0 ? HomingAI : StraightAI, angleTo(position, enemy.position)).move(safeMove.x, safeMove.y);
|
|
460
496
|
}
|
|
461
497
|
|
|
462
498
|
// Budget too small for useful missile — wait for threat to pass, then fire
|
|
@@ -464,18 +500,15 @@ export const Stormforger: WizardFunction = ({state}) =>
|
|
|
464
500
|
|
|
465
501
|
// === FALLBACK DEFENSE: Shield when no attack window exists ===
|
|
466
502
|
if (
|
|
467
|
-
|
|
503
|
+
status === 'idle' &&
|
|
468
504
|
urgentThreat &&
|
|
469
505
|
!urgentThreat.bestDodgeDirection &&
|
|
470
506
|
urgentThreat.canBlockInTime &&
|
|
471
507
|
urgentThreat.ticksToStartShield <= 3
|
|
472
508
|
)
|
|
473
509
|
{
|
|
474
|
-
return
|
|
475
|
-
move: {x: 0, y: 0},
|
|
476
|
-
startCast: {spell: 'shield'},
|
|
477
|
-
};
|
|
510
|
+
return shield();
|
|
478
511
|
}
|
|
479
512
|
|
|
480
|
-
return
|
|
481
|
-
}
|
|
513
|
+
return moveAction(safeMove.x, safeMove.y);
|
|
514
|
+
}
|