@vibemancer/core 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/chunk-G5T65F3W.js +10617 -0
- package/dist/chunk-G5T65F3W.js.map +1 -0
- package/dist/index-browser.d.ts +1328 -1051
- package/dist/index-browser.js +59 -17
- package/dist/index.d.ts +53 -3
- package/dist/index.js +204 -54
- package/dist/index.js.map +1 -1
- package/package.json +21 -15
- package/src/banned-bot-names.ts +53 -0
- package/src/bots/Hero.ts +867 -0
- package/src/bots/berserker/01_Stormchaser.ts +162 -120
- package/src/bots/berserker/02_Stormcaller.ts +160 -116
- package/src/bots/berserker/03_Stormforger.ts +162 -129
- package/src/bots/caster/01_Flamecaller.ts +126 -84
- package/src/bots/caster/02_Pyromancer.ts +148 -101
- package/src/bots/caster/03_Infernalist.ts +180 -160
- package/src/bots/defensive/01_Turtle.ts +48 -44
- package/src/bots/defensive/02_Sentinel.ts +57 -53
- package/src/bots/defensive/03_Golem.ts +85 -97
- package/src/bots/duelist/01_Battlemage.ts +157 -122
- package/src/bots/duelist/02_Warmage.ts +166 -121
- package/src/bots/duelist/03_Archmage.ts +198 -178
- package/src/bots/homing/01_Bonemancer.ts +20 -32
- package/src/bots/homing/02_Lich.ts +145 -93
- package/src/bots/homing/03_Archlich.ts +129 -60
- package/src/bots/kiter/01_Spellspinner.ts +145 -107
- package/src/bots/kiter/02_Spellweaver.ts +153 -105
- package/src/bots/kiter/03_Spellbinder.ts +168 -123
- package/src/bots/melee/01_Shadowblade.ts +131 -75
- package/src/bots/melee/02_Nightblade.ts +174 -130
- package/src/bots/melee/03_Voidblade.ts +266 -168
- package/src/bots/registry.ts +44 -37
- package/src/bots/shared.ts +185 -25
- package/src/bots/sniper/01_Spellshot.ts +151 -98
- package/src/bots/sniper/02_Spelltracer.ts +173 -128
- package/src/bots/sniper/03_Spellseeker.ts +177 -152
- package/src/bots/standalone/Critter.ts +46 -52
- package/src/bots/standalone/Doombringer.ts +36 -40
- package/src/bots/standalone/Hogger.ts +120 -89
- package/src/bots/standalone/Rookie.ts +21 -23
- package/src/bots/standalone/TargetDummy.ts +5 -6
- package/src/bots/test/cheater.ts +91 -85
- package/src/bots/test/crasher.ts +26 -28
- package/src/engine/bundle-fight.ts +242 -0
- package/src/engine/hooks-runtime.ts +58 -18
- package/src/engine/manual-match.ts +33 -25
- package/src/engine/missile-templates.ts +25 -43
- package/src/engine/optimizer.ts +7 -7
- package/src/engine/params-runtime.ts +3 -3
- package/src/engine/physics.ts +33 -23
- package/src/engine/sandbox-browser.ts +8 -7
- package/src/engine/sandbox-compile.ts +1 -1
- package/src/engine/sandbox-harness.ts +299 -367
- package/src/engine/sandbox.ts +3 -3
- package/src/engine/simulation.ts +291 -76
- package/src/engine/spells.ts +9 -12
- package/src/engine-version.ts +1 -1
- package/src/hooks/action-builders.ts +76 -21
- package/src/hooks/index.ts +17 -9
- package/src/hooks/state-hooks.ts +61 -25
- package/src/hooks/threat-analysis.ts +44 -20
- package/src/hooks/types.ts +62 -5
- package/src/index-browser.ts +1 -0
- package/src/index.ts +3 -0
- package/src/rules.ts +209 -63
- package/src/stats.ts +2 -2
- package/src/testing.ts +6 -30
- package/src/trace.ts +63 -3
- package/src/types.ts +127 -14
- package/src/utils/angles.ts +1 -0
- package/src/utils/combat.ts +98 -6
- package/src/utils/spatial.ts +3 -3
- package/dist/chunk-74FFJCFF.js +0 -9140
- package/dist/chunk-74FFJCFF.js.map +0 -1
- package/src/hooks/bot-wrapper.ts +0 -84
|
@@ -1,9 +1,9 @@
|
|
|
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';
|
|
5
6
|
import {
|
|
6
|
-
getThreats,
|
|
7
7
|
getMostUrgentThreat,
|
|
8
8
|
getDodgeDirectionForMovement,
|
|
9
9
|
getBlinkToCenter,
|
|
@@ -13,13 +13,16 @@ import {
|
|
|
13
13
|
shouldForceShield,
|
|
14
14
|
MAX_CAST_BUDGET,
|
|
15
15
|
getEnemyVulnerabilityTicks,
|
|
16
|
+
keepOffWalls,
|
|
17
|
+
getPreCastReposition,
|
|
16
18
|
} from '../shared.js';
|
|
17
19
|
import {useParam} from '../../engine/params-runtime.js';
|
|
18
20
|
|
|
19
|
-
const HomingAI:
|
|
21
|
+
const HomingAI: MissileFunction = () =>
|
|
20
22
|
{
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
+
const ctx = getMissileContext();
|
|
24
|
+
const enemy = ctx.worldState.enemies[0];
|
|
25
|
+
return enemy ? turnToward(enemy.position.x, enemy.position.y) : flyStraight();
|
|
23
26
|
};
|
|
24
27
|
const WALL_BUFFER = 50;
|
|
25
28
|
|
|
@@ -39,12 +42,22 @@ function combatMove(
|
|
|
39
42
|
strafeCyclePeriod: number,
|
|
40
43
|
strafeIntensity: number,
|
|
41
44
|
approachSpeed: number,
|
|
42
|
-
|
|
45
|
+
centerBias: number,
|
|
46
|
+
): {x: number; y: number}
|
|
43
47
|
{
|
|
44
48
|
const deltaX = enemy.position.x - position.x;
|
|
45
49
|
const deltaY = enemy.position.y - position.y;
|
|
46
50
|
const normalizedDistance = currentDistance > 0 ? currentDistance : 1;
|
|
47
51
|
|
|
52
|
+
// Approach the center-side of the enemy so knockback pushes toward center, not lava.
|
|
53
|
+
const center = (ARENA_MIN + ARENA_MAX) / 2;
|
|
54
|
+
const toCenterX = center - enemy.position.x;
|
|
55
|
+
const toCenterY = center - enemy.position.y;
|
|
56
|
+
const toCenterDist = Math.sqrt(toCenterX * toCenterX + toCenterY * toCenterY) || 1;
|
|
57
|
+
const approachDirX = deltaX / normalizedDistance + (toCenterX / toCenterDist) * centerBias;
|
|
58
|
+
const approachDirY = deltaY / normalizedDistance + (toCenterY / toCenterDist) * centerBias;
|
|
59
|
+
const approachMag = Math.sqrt(approachDirX * approachDirX + approachDirY * approachDirY) || 1;
|
|
60
|
+
|
|
48
61
|
const strafeClockwise = {
|
|
49
62
|
x: -deltaY / normalizedDistance,
|
|
50
63
|
y: deltaX / normalizedDistance,
|
|
@@ -61,10 +74,10 @@ function combatMove(
|
|
|
61
74
|
strafeDirection = dodgeDirection;
|
|
62
75
|
}
|
|
63
76
|
else if (
|
|
64
|
-
position.x < WALL_BUFFER ||
|
|
65
|
-
position.x >
|
|
66
|
-
position.y < WALL_BUFFER ||
|
|
67
|
-
position.y >
|
|
77
|
+
position.x < ARENA_MIN + WALL_BUFFER ||
|
|
78
|
+
position.x > ARENA_MAX - WALL_BUFFER ||
|
|
79
|
+
position.y < ARENA_MIN + WALL_BUFFER ||
|
|
80
|
+
position.y > ARENA_MAX - WALL_BUFFER
|
|
68
81
|
)
|
|
69
82
|
{
|
|
70
83
|
const futureClockwise = {
|
|
@@ -76,16 +89,16 @@ function combatMove(
|
|
|
76
89
|
y: position.y + strafeCounterClockwise.y * 100,
|
|
77
90
|
};
|
|
78
91
|
const scoreClockwise = Math.min(
|
|
79
|
-
futureClockwise.x,
|
|
80
|
-
|
|
81
|
-
futureClockwise.y,
|
|
82
|
-
|
|
92
|
+
futureClockwise.x - ARENA_MIN,
|
|
93
|
+
ARENA_MAX - futureClockwise.x,
|
|
94
|
+
futureClockwise.y - ARENA_MIN,
|
|
95
|
+
ARENA_MAX - futureClockwise.y,
|
|
83
96
|
);
|
|
84
97
|
const scoreCounterClockwise = Math.min(
|
|
85
|
-
futureCounterClockwise.x,
|
|
86
|
-
|
|
87
|
-
futureCounterClockwise.y,
|
|
88
|
-
|
|
98
|
+
futureCounterClockwise.x - ARENA_MIN,
|
|
99
|
+
ARENA_MAX - futureCounterClockwise.x,
|
|
100
|
+
futureCounterClockwise.y - ARENA_MIN,
|
|
101
|
+
ARENA_MAX - futureCounterClockwise.y,
|
|
89
102
|
);
|
|
90
103
|
strafeDirection =
|
|
91
104
|
scoreClockwise > scoreCounterClockwise
|
|
@@ -101,35 +114,35 @@ function combatMove(
|
|
|
101
114
|
// When actively dodging, commit fully
|
|
102
115
|
if (dodgeDirection)
|
|
103
116
|
{
|
|
104
|
-
return {x: strafeDirection.x
|
|
117
|
+
return {x: strafeDirection.x, y: strafeDirection.y};
|
|
105
118
|
}
|
|
106
119
|
|
|
107
120
|
let moveX = strafeDirection.x * strafeIntensity;
|
|
108
121
|
let moveY = strafeDirection.y * strafeIntensity;
|
|
109
122
|
|
|
110
123
|
// Distance management
|
|
111
|
-
if (currentDistance > maxDistance)
|
|
124
|
+
if (currentDistance > maxDistance)
|
|
112
125
|
{
|
|
113
|
-
moveX += (
|
|
114
|
-
moveY += (
|
|
126
|
+
moveX += (approachDirX / approachMag) * approachSpeed;
|
|
127
|
+
moveY += (approachDirY / approachMag) * approachSpeed;
|
|
115
128
|
}
|
|
116
129
|
else if (currentDistance < minDistance)
|
|
117
130
|
{
|
|
118
131
|
let retreatX = -(deltaX / normalizedDistance) * approachSpeed;
|
|
119
132
|
let retreatY = -(deltaY / normalizedDistance) * approachSpeed;
|
|
120
|
-
if (position.x < WALL_BUFFER && retreatX < 0) retreatX = 0;
|
|
121
|
-
if (position.x >
|
|
122
|
-
if (position.y < WALL_BUFFER && retreatY < 0) retreatY = 0;
|
|
123
|
-
if (position.y >
|
|
133
|
+
if (position.x < ARENA_MIN + WALL_BUFFER && retreatX < 0) retreatX = 0;
|
|
134
|
+
if (position.x > ARENA_MAX - WALL_BUFFER && retreatX > 0) retreatX = 0;
|
|
135
|
+
if (position.y < ARENA_MIN + WALL_BUFFER && retreatY < 0) retreatY = 0;
|
|
136
|
+
if (position.y > ARENA_MAX - WALL_BUFFER && retreatY > 0) retreatY = 0;
|
|
124
137
|
moveX += retreatX;
|
|
125
138
|
moveY += retreatY;
|
|
126
139
|
}
|
|
127
140
|
|
|
128
141
|
const magnitude = Math.sqrt(moveX * moveX + moveY * moveY);
|
|
129
|
-
if (magnitude >
|
|
142
|
+
if (magnitude > 1)
|
|
130
143
|
{
|
|
131
|
-
moveX =
|
|
132
|
-
moveY =
|
|
144
|
+
moveX = moveX / magnitude;
|
|
145
|
+
moveY = moveY / magnitude;
|
|
133
146
|
}
|
|
134
147
|
|
|
135
148
|
return {x: moveX, y: moveY};
|
|
@@ -152,36 +165,50 @@ function combatMove(
|
|
|
152
165
|
*
|
|
153
166
|
* TIER: 2 (enhanced Spellshot)
|
|
154
167
|
*/
|
|
155
|
-
export
|
|
168
|
+
export function Spelltracer(): FinalAction
|
|
156
169
|
{
|
|
170
|
+
const position = usePosition();
|
|
171
|
+
const enemy = useEnemy();
|
|
172
|
+
const status = useStatus();
|
|
173
|
+
const blinkCooldown = useBlinkCooldown();
|
|
174
|
+
const health = useHealth();
|
|
175
|
+
const tick = useTick();
|
|
176
|
+
const threats = useThreats();
|
|
177
|
+
const projectiles = useProjectiles();
|
|
178
|
+
const myProjectiles = useMyProjectiles();
|
|
179
|
+
const lastMissileConfig = useLastMissileConfig();
|
|
180
|
+
const castingSpell = useCastingSpell();
|
|
181
|
+
const castProgressData = useCastProgress();
|
|
182
|
+
|
|
183
|
+
|
|
157
184
|
// Tunable parameters (run optimizer script to find optimal values)
|
|
158
|
-
const dangerDistance = useParam('dangerDistance',
|
|
185
|
+
const dangerDistance = useParam('dangerDistance', 65, {
|
|
159
186
|
range: 175,
|
|
160
187
|
min: 0,
|
|
161
188
|
});
|
|
162
|
-
const targetDistance = useParam('targetDistance',
|
|
163
|
-
const distanceDeadZone = useParam('distanceDeadZone',
|
|
189
|
+
const targetDistance = useParam('targetDistance', 179, {range: 200, min: 0});
|
|
190
|
+
const distanceDeadZone = useParam('distanceDeadZone', 128, {
|
|
164
191
|
range: 50,
|
|
165
192
|
min: 0,
|
|
166
193
|
});
|
|
167
194
|
const rangeMin = targetDistance - distanceDeadZone;
|
|
168
195
|
const rangeMax = targetDistance + distanceDeadZone;
|
|
169
|
-
const minTurnRate = useParam('minTurnRate', 0.
|
|
170
|
-
const strafeCyclePeriod = useParam('strafeCyclePeriod',
|
|
196
|
+
const minTurnRate = useParam('minTurnRate', -0.6, {range: 1.5, steps: 5});
|
|
197
|
+
const strafeCyclePeriod = useParam('strafeCyclePeriod', 221, {
|
|
171
198
|
range: 75,
|
|
172
199
|
min: 80,
|
|
173
200
|
max: 300,
|
|
174
201
|
});
|
|
175
|
-
const strafeIntensity = useParam('strafeIntensity',
|
|
176
|
-
range:
|
|
177
|
-
min:
|
|
178
|
-
max:
|
|
202
|
+
const strafeIntensity = useParam('strafeIntensity', 0.92, {
|
|
203
|
+
range: 0.4,
|
|
204
|
+
min: 0.2,
|
|
205
|
+
max: 1,
|
|
179
206
|
steps: 7,
|
|
180
207
|
});
|
|
181
|
-
const approachSpeed = useParam('approachSpeed',
|
|
182
|
-
range:
|
|
183
|
-
min:
|
|
184
|
-
max:
|
|
208
|
+
const approachSpeed = useParam('approachSpeed', 0.46, {
|
|
209
|
+
range: 0.3,
|
|
210
|
+
min: 0.1,
|
|
211
|
+
max: 0.8,
|
|
185
212
|
steps: 7,
|
|
186
213
|
});
|
|
187
214
|
const shieldCancelThreshold = useParam('shieldCancelThreshold', 150, {
|
|
@@ -189,19 +216,19 @@ export const Spelltracer: WizardFunction = ({state}) =>
|
|
|
189
216
|
min: 50,
|
|
190
217
|
max: 300,
|
|
191
218
|
});
|
|
192
|
-
const blinkWindowMax = useParam('blinkWindowMax',
|
|
219
|
+
const blinkWindowMax = useParam('blinkWindowMax', 34, {
|
|
193
220
|
range: 40,
|
|
194
221
|
min: 20,
|
|
195
222
|
max: 120,
|
|
196
223
|
steps: 7,
|
|
197
224
|
});
|
|
198
|
-
const flightTimeDivisor = useParam('flightTimeDivisor',
|
|
225
|
+
const flightTimeDivisor = useParam('flightTimeDivisor', 5.8, {
|
|
199
226
|
range: 4,
|
|
200
227
|
min: 3,
|
|
201
228
|
max: 12,
|
|
202
229
|
steps: 5,
|
|
203
230
|
});
|
|
204
|
-
const enemyResponseEstimate = useParam('enemyResponseEstimate',
|
|
231
|
+
const enemyResponseEstimate = useParam('enemyResponseEstimate', 145, {
|
|
205
232
|
range: 100,
|
|
206
233
|
min: 50,
|
|
207
234
|
max: 300,
|
|
@@ -213,57 +240,74 @@ export const Spelltracer: WizardFunction = ({state}) =>
|
|
|
213
240
|
max: 400,
|
|
214
241
|
steps: 7,
|
|
215
242
|
});
|
|
243
|
+
const centerBias = useParam('centerBias', 0.11, {
|
|
244
|
+
range: 0.4,
|
|
245
|
+
min: 0,
|
|
246
|
+
max: 0.8,
|
|
247
|
+
steps: 5,
|
|
248
|
+
});
|
|
249
|
+
const maxBlinkDistance = useParam('maxBlinkDistance', 287, {
|
|
250
|
+
range: 100,
|
|
251
|
+
min: 50,
|
|
252
|
+
max: 300,
|
|
253
|
+
steps: 7,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const distance = distanceTo(position, enemy.position);
|
|
216
257
|
|
|
217
|
-
|
|
218
|
-
if (!enemy)
|
|
258
|
+
function capBlinkDistance(target: {x: number; y: number}): {x: number; y: number}
|
|
219
259
|
{
|
|
220
|
-
|
|
260
|
+
const dx = target.x - position.x;
|
|
261
|
+
const dy = target.y - position.y;
|
|
262
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
263
|
+
if (dist <= maxBlinkDistance) return target;
|
|
264
|
+
const scale = maxBlinkDistance / dist;
|
|
265
|
+
return {x: position.x + dx * scale, y: position.y + dy * scale};
|
|
221
266
|
}
|
|
222
267
|
|
|
223
|
-
const distance = distanceTo(state.position, enemy.position);
|
|
224
|
-
|
|
225
|
-
const threats = getThreats(state);
|
|
226
268
|
const urgentThreat = getMostUrgentThreat(threats);
|
|
227
269
|
|
|
228
270
|
// === DEFENSE: Handle channeling ===
|
|
229
|
-
if (
|
|
271
|
+
if (status === 'channeling')
|
|
230
272
|
{
|
|
231
273
|
if (!urgentThreat || urgentThreat.ticksToImpact > shieldCancelThreshold)
|
|
232
274
|
{
|
|
233
|
-
return
|
|
275
|
+
return cancel();
|
|
234
276
|
}
|
|
235
|
-
return
|
|
277
|
+
return idle();
|
|
236
278
|
}
|
|
237
279
|
|
|
238
280
|
// === DEFENSE: Cancel missile cast only for LETHAL incoming damage ===
|
|
239
281
|
// DPS trade: we accepted the hit when we started casting. Only cancel to survive.
|
|
240
282
|
if (
|
|
241
|
-
|
|
242
|
-
|
|
283
|
+
status === 'casting' &&
|
|
284
|
+
castingSpell === 'missile' &&
|
|
243
285
|
urgentThreat &&
|
|
244
|
-
urgentThreat.projectile.damage >=
|
|
286
|
+
urgentThreat.projectile.damage >= health
|
|
245
287
|
)
|
|
246
288
|
{
|
|
247
|
-
const remainingCast = (
|
|
289
|
+
const remainingCast = ((castProgressData?.total) ?? 0) - ((castProgressData?.current) ?? 0);
|
|
248
290
|
if (
|
|
249
291
|
urgentThreat.ticksToImpact <=
|
|
250
|
-
remainingCast + GCD_DURATION + SHIELD_CAST_TIME + SHIELD_BUFFER
|
|
292
|
+
remainingCast + RULES.GCD_DURATION + RULES.SHIELD_CAST_TIME + SHIELD_BUFFER
|
|
251
293
|
)
|
|
252
294
|
{
|
|
253
|
-
const dodgeDir = getDodgeDirectionForMovement(threats,
|
|
295
|
+
const dodgeDir = getDodgeDirectionForMovement(threats, position);
|
|
254
296
|
const cancelMove = combatMove(
|
|
255
|
-
|
|
297
|
+
position,
|
|
256
298
|
enemy,
|
|
257
299
|
distance,
|
|
258
300
|
rangeMin,
|
|
259
301
|
rangeMax,
|
|
260
302
|
dodgeDir,
|
|
261
|
-
|
|
303
|
+
tick,
|
|
262
304
|
strafeCyclePeriod,
|
|
263
305
|
strafeIntensity,
|
|
264
306
|
approachSpeed,
|
|
307
|
+
centerBias,
|
|
265
308
|
);
|
|
266
|
-
|
|
309
|
+
const safeCancelMove = keepOffWalls(position, cancelMove);
|
|
310
|
+
return cancel().move(safeCancelMove.x, safeCancelMove.y);
|
|
267
311
|
}
|
|
268
312
|
}
|
|
269
313
|
|
|
@@ -271,33 +315,30 @@ export const Spelltracer: WizardFunction = ({state}) =>
|
|
|
271
315
|
// When we would otherwise shield, blink instead — no GCD after blink, so can fire back immediately.
|
|
272
316
|
// Only for threats that can't be dodged by movement (homing/undodgeable).
|
|
273
317
|
if (
|
|
274
|
-
|
|
318
|
+
status === 'idle' &&
|
|
275
319
|
urgentThreat &&
|
|
276
|
-
|
|
320
|
+
blinkCooldown === 0 &&
|
|
277
321
|
(!urgentThreat.bestDodgeDirection || shouldForceShield(urgentThreat)) &&
|
|
278
322
|
urgentThreat.ticksToImpact >= 10 &&
|
|
279
323
|
urgentThreat.ticksToImpact <= blinkWindowMax
|
|
280
324
|
)
|
|
281
325
|
{
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
: getBlinkToCenter(state.position),
|
|
294
|
-
},
|
|
295
|
-
};
|
|
326
|
+
{
|
|
327
|
+
const _bt = distance < targetDistance
|
|
328
|
+
? (getBlinkToIncreaseDistance(
|
|
329
|
+
position,
|
|
330
|
+
enemy.position,
|
|
331
|
+
projectiles,
|
|
332
|
+
) ?? getBlinkToCenter(position))
|
|
333
|
+
: getBlinkToCenter(position);
|
|
334
|
+
const capped = capBlinkDistance(_bt);
|
|
335
|
+
return blink(capped.x, capped.y);
|
|
336
|
+
}
|
|
296
337
|
}
|
|
297
338
|
|
|
298
339
|
// === DEFENSE: Shield undodgeable or high-damage homing threats (when blink on cooldown) ===
|
|
299
340
|
if (
|
|
300
|
-
|
|
341
|
+
status === 'idle' &&
|
|
301
342
|
urgentThreat &&
|
|
302
343
|
(!urgentThreat.bestDodgeDirection || shouldForceShield(urgentThreat))
|
|
303
344
|
)
|
|
@@ -306,68 +347,88 @@ export const Spelltracer: WizardFunction = ({state}) =>
|
|
|
306
347
|
{
|
|
307
348
|
if (urgentThreat.ticksToStartShield <= SHIELD_BUFFER)
|
|
308
349
|
{
|
|
309
|
-
return
|
|
310
|
-
move: {x: 0, y: 0},
|
|
311
|
-
startCast: {spell: 'shield'},
|
|
312
|
-
};
|
|
350
|
+
return shield();
|
|
313
351
|
}
|
|
314
352
|
}
|
|
315
353
|
}
|
|
316
354
|
|
|
355
|
+
// === Preemptive shield — shield when enemy is casting a missile at close range ===
|
|
356
|
+
if (
|
|
357
|
+
status === 'idle' &&
|
|
358
|
+
distance < 300 &&
|
|
359
|
+
enemy.status === 'casting' &&
|
|
360
|
+
enemy.castingSpell === 'missile'
|
|
361
|
+
)
|
|
362
|
+
{
|
|
363
|
+
const remainingCast = (enemy.castDuration ?? 0) - (enemy.castProgress ?? 0);
|
|
364
|
+
const estimatedFlightTicks = distance / 8;
|
|
365
|
+
const ticksUntilHit = remainingCast + estimatedFlightTicks;
|
|
366
|
+
|
|
367
|
+
if (
|
|
368
|
+
ticksUntilHit >= RULES.SHIELD_CAST_TIME &&
|
|
369
|
+
ticksUntilHit < RULES.SHIELD_CAST_TIME + 15
|
|
370
|
+
)
|
|
371
|
+
{
|
|
372
|
+
return shield();
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
317
376
|
// === DISTANCE BLINK: enemy too close, blink to reestablish range ===
|
|
318
377
|
if (
|
|
319
|
-
|
|
378
|
+
status === 'idle' &&
|
|
320
379
|
distance < dangerDistance &&
|
|
321
|
-
|
|
380
|
+
blinkCooldown === 0 &&
|
|
322
381
|
(!urgentThreat || urgentThreat.bestDodgeDirection !== null)
|
|
323
382
|
)
|
|
324
383
|
{
|
|
325
384
|
const blinkTarget = getBlinkToIncreaseDistance(
|
|
326
|
-
|
|
385
|
+
position,
|
|
327
386
|
enemy.position,
|
|
328
|
-
|
|
387
|
+
projectiles,
|
|
329
388
|
MIN_BLINK_ESCAPE_DISTANCE,
|
|
330
389
|
);
|
|
331
|
-
if (blinkTarget)
|
|
390
|
+
if (blinkTarget)
|
|
332
391
|
{
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
startCast: {spell: 'blink', target: blinkTarget},
|
|
336
|
-
};
|
|
392
|
+
const capped = capBlinkDistance(blinkTarget);
|
|
393
|
+
return blink(capped.x, capped.y);
|
|
337
394
|
}
|
|
338
395
|
}
|
|
339
396
|
|
|
340
397
|
// Smart combat movement
|
|
341
|
-
const dodgeDirection = getDodgeDirectionForMovement(threats,
|
|
398
|
+
const dodgeDirection = getDodgeDirectionForMovement(threats, position);
|
|
342
399
|
const move = combatMove(
|
|
343
|
-
|
|
400
|
+
position,
|
|
344
401
|
enemy,
|
|
345
402
|
distance,
|
|
346
403
|
rangeMin,
|
|
347
404
|
rangeMax,
|
|
348
405
|
dodgeDirection,
|
|
349
|
-
|
|
406
|
+
tick,
|
|
350
407
|
strafeCyclePeriod,
|
|
351
408
|
strafeIntensity,
|
|
352
409
|
approachSpeed,
|
|
410
|
+
centerBias,
|
|
353
411
|
);
|
|
354
|
-
|
|
412
|
+
const safeMove = keepOffWalls(position, move);
|
|
355
413
|
// === OFFENSE: Adaptive homing missiles ===
|
|
356
414
|
// Skip offense when a lethal missile could reach us during cast+GCD — dodge at full speed instead
|
|
357
|
-
const myProjectileIds = new Set(
|
|
358
|
-
const hasNearbyLethalMissile =
|
|
415
|
+
const myProjectileIds = new Set(myProjectiles.map((p) => p.id));
|
|
416
|
+
const hasNearbyLethalMissile = projectiles.some(
|
|
359
417
|
(p) =>
|
|
360
418
|
!myProjectileIds.has(p.id) &&
|
|
361
|
-
p.damage >=
|
|
362
|
-
distanceTo(
|
|
363
|
-
MAX_CAST_BUDGET + GCD_DURATION,
|
|
419
|
+
p.damage >= health &&
|
|
420
|
+
distanceTo(position, p.position) / p.speed <
|
|
421
|
+
MAX_CAST_BUDGET + RULES.GCD_DURATION,
|
|
364
422
|
);
|
|
365
|
-
if (
|
|
423
|
+
if (status === 'idle' && distance < 600 && !hasNearbyLethalMissile)
|
|
366
424
|
{
|
|
425
|
+
const reposition = getPreCastReposition(position, enemy.position);
|
|
426
|
+
if (reposition) return moveAction(reposition.x, reposition.y);
|
|
427
|
+
|
|
367
428
|
let nextThreatTime = urgentThreat?.ticksToImpact ?? Infinity;
|
|
368
429
|
|
|
369
430
|
// Anticipate enemy's next missile based on their current state
|
|
370
|
-
if (enemy.
|
|
431
|
+
if (enemy.status === 'casting' && enemy.castingSpell === 'missile')
|
|
371
432
|
{
|
|
372
433
|
const remainingEnemyCast =
|
|
373
434
|
(enemy.castDuration ?? 0) - (enemy.castProgress ?? 0);
|
|
@@ -387,7 +448,7 @@ export const Spelltracer: WizardFunction = ({state}) =>
|
|
|
387
448
|
}
|
|
388
449
|
|
|
389
450
|
const safeBudget =
|
|
390
|
-
nextThreatTime - GCD_DURATION - SHIELD_CAST_TIME - SHIELD_BUFFER;
|
|
451
|
+
nextThreatTime - RULES.GCD_DURATION - RULES.SHIELD_CAST_TIME - SHIELD_BUFFER;
|
|
391
452
|
const budgetTicks = Math.min(safeBudget, MAX_CAST_BUDGET);
|
|
392
453
|
|
|
393
454
|
// Try adaptive fitting for the safe budget
|
|
@@ -395,7 +456,7 @@ export const Spelltracer: WizardFunction = ({state}) =>
|
|
|
395
456
|
const config = fitMissileToBudget(budgetTicks, distance, {
|
|
396
457
|
minTurnRate,
|
|
397
458
|
maxDamage: enemyHP,
|
|
398
|
-
lastMissileConfig:
|
|
459
|
+
lastMissileConfig: lastMissileConfig,
|
|
399
460
|
});
|
|
400
461
|
|
|
401
462
|
// HIGH-DAMAGE PRIORITY: Sniper missiles need to chip through shields.
|
|
@@ -407,15 +468,7 @@ export const Spelltracer: WizardFunction = ({state}) =>
|
|
|
407
468
|
|
|
408
469
|
if (config && config.damage >= 15)
|
|
409
470
|
{
|
|
410
|
-
return
|
|
411
|
-
move,
|
|
412
|
-
startCast: {
|
|
413
|
-
spell: 'missile',
|
|
414
|
-
config,
|
|
415
|
-
missileAI: HomingAI,
|
|
416
|
-
direction: angleTo(state.position, enemy.position),
|
|
417
|
-
},
|
|
418
|
-
};
|
|
471
|
+
return missile(config, HomingAI, angleTo(position, enemy.position)).move(safeMove.x, safeMove.y);
|
|
419
472
|
}
|
|
420
473
|
|
|
421
474
|
// Adaptive fitting produced weak missile or failed — fire big shot instead
|
|
@@ -423,19 +476,11 @@ export const Spelltracer: WizardFunction = ({state}) =>
|
|
|
423
476
|
// a hit during the longer cast (trade is worthwhile at high damage).
|
|
424
477
|
if (!urgentThreat || urgentThreat.ticksToImpact > 50)
|
|
425
478
|
{
|
|
426
|
-
return
|
|
427
|
-
move,
|
|
428
|
-
startCast: {
|
|
429
|
-
spell: 'missile',
|
|
430
|
-
config: BIG_SHOT,
|
|
431
|
-
missileAI: HomingAI,
|
|
432
|
-
direction: angleTo(state.position, enemy.position),
|
|
433
|
-
},
|
|
434
|
-
};
|
|
479
|
+
return missile(BIG_SHOT, HomingAI, angleTo(position, enemy.position)).move(safeMove.x, safeMove.y);
|
|
435
480
|
}
|
|
436
481
|
|
|
437
482
|
// Imminent threat — wait for it to pass, then fire
|
|
438
483
|
}
|
|
439
484
|
|
|
440
|
-
return
|
|
441
|
-
}
|
|
485
|
+
return moveAction(safeMove.x, safeMove.y);
|
|
486
|
+
}
|