@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,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
|
fitMissileToBudget,
|
|
@@ -21,27 +16,30 @@ import {
|
|
|
21
16
|
MIN_USEFUL_DAMAGE,
|
|
22
17
|
getEnemyVulnerabilityTicks,
|
|
23
18
|
isSafeToAttack,
|
|
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
|
|
|
@@ -59,12 +57,22 @@ function combatMove(
|
|
|
59
57
|
strafeIntensity: number,
|
|
60
58
|
approachSpeed: number,
|
|
61
59
|
distanceDeadZone: number,
|
|
62
|
-
|
|
60
|
+
centerBias: number,
|
|
61
|
+
): {x: number; y: number}
|
|
63
62
|
{
|
|
64
63
|
const deltaX = enemy.position.x - position.x;
|
|
65
64
|
const deltaY = enemy.position.y - position.y;
|
|
66
65
|
const normalizedDistance = currentDistance > 0 ? currentDistance : 1;
|
|
67
66
|
|
|
67
|
+
// Approach the center-side of the enemy so knockback pushes toward center, not lava.
|
|
68
|
+
const center = (ARENA_MIN + ARENA_MAX) / 2;
|
|
69
|
+
const toCenterX = center - enemy.position.x;
|
|
70
|
+
const toCenterY = center - enemy.position.y;
|
|
71
|
+
const toCenterDist = Math.sqrt(toCenterX * toCenterX + toCenterY * toCenterY) || 1;
|
|
72
|
+
const approachDirX = deltaX / normalizedDistance + (toCenterX / toCenterDist) * centerBias;
|
|
73
|
+
const approachDirY = deltaY / normalizedDistance + (toCenterY / toCenterDist) * centerBias;
|
|
74
|
+
const approachMag = Math.sqrt(approachDirX * approachDirX + approachDirY * approachDirY) || 1;
|
|
75
|
+
|
|
68
76
|
const strafeClockwise = {
|
|
69
77
|
x: -deltaY / normalizedDistance,
|
|
70
78
|
y: deltaX / normalizedDistance,
|
|
@@ -91,16 +99,16 @@ function combatMove(
|
|
|
91
99
|
y: position.y + strafeCounterClockwise.y * 100,
|
|
92
100
|
};
|
|
93
101
|
const scoreClockwise = Math.min(
|
|
94
|
-
futureClockwise.x,
|
|
95
|
-
|
|
96
|
-
futureClockwise.y,
|
|
97
|
-
|
|
102
|
+
futureClockwise.x - ARENA_MIN,
|
|
103
|
+
ARENA_MAX - futureClockwise.x,
|
|
104
|
+
futureClockwise.y - ARENA_MIN,
|
|
105
|
+
ARENA_MAX - futureClockwise.y,
|
|
98
106
|
);
|
|
99
107
|
const scoreCounterClockwise = Math.min(
|
|
100
|
-
futureCounterClockwise.x,
|
|
101
|
-
|
|
102
|
-
futureCounterClockwise.y,
|
|
103
|
-
|
|
108
|
+
futureCounterClockwise.x - ARENA_MIN,
|
|
109
|
+
ARENA_MAX - futureCounterClockwise.x,
|
|
110
|
+
futureCounterClockwise.y - ARENA_MIN,
|
|
111
|
+
ARENA_MAX - futureCounterClockwise.y,
|
|
104
112
|
);
|
|
105
113
|
strafeDirection =
|
|
106
114
|
scoreClockwise > scoreCounterClockwise
|
|
@@ -115,34 +123,34 @@ function combatMove(
|
|
|
115
123
|
|
|
116
124
|
if (dodgeDirection)
|
|
117
125
|
{
|
|
118
|
-
return {x: strafeDirection.x
|
|
126
|
+
return {x: strafeDirection.x, y: strafeDirection.y};
|
|
119
127
|
}
|
|
120
128
|
|
|
121
129
|
let moveX = strafeDirection.x * strafeIntensity;
|
|
122
130
|
let moveY = strafeDirection.y * strafeIntensity;
|
|
123
131
|
|
|
124
|
-
if (currentDistance > targetDistance + distanceDeadZone)
|
|
132
|
+
if (currentDistance > targetDistance + distanceDeadZone)
|
|
125
133
|
{
|
|
126
|
-
moveX += (
|
|
127
|
-
moveY += (
|
|
134
|
+
moveX += (approachDirX / approachMag) * approachSpeed;
|
|
135
|
+
moveY += (approachDirY / approachMag) * approachSpeed;
|
|
128
136
|
}
|
|
129
137
|
else if (currentDistance < targetDistance - distanceDeadZone)
|
|
130
138
|
{
|
|
131
139
|
let retreatX = -(deltaX / normalizedDistance) * approachSpeed;
|
|
132
140
|
let retreatY = -(deltaY / normalizedDistance) * approachSpeed;
|
|
133
|
-
if (position.x < WALL_BUFFER && retreatX < 0) retreatX = 0;
|
|
134
|
-
if (position.x >
|
|
135
|
-
if (position.y < WALL_BUFFER && retreatY < 0) retreatY = 0;
|
|
136
|
-
if (position.y >
|
|
141
|
+
if (position.x < ARENA_MIN + WALL_BUFFER && retreatX < 0) retreatX = 0;
|
|
142
|
+
if (position.x > ARENA_MAX - WALL_BUFFER && retreatX > 0) retreatX = 0;
|
|
143
|
+
if (position.y < ARENA_MIN + WALL_BUFFER && retreatY < 0) retreatY = 0;
|
|
144
|
+
if (position.y > ARENA_MAX - WALL_BUFFER && retreatY > 0) retreatY = 0;
|
|
137
145
|
moveX += retreatX;
|
|
138
146
|
moveY += retreatY;
|
|
139
147
|
}
|
|
140
148
|
|
|
141
149
|
const magnitude = Math.sqrt(moveX * moveX + moveY * moveY);
|
|
142
|
-
if (magnitude >
|
|
150
|
+
if (magnitude > 1)
|
|
143
151
|
{
|
|
144
|
-
moveX =
|
|
145
|
-
moveY =
|
|
152
|
+
moveX = moveX / magnitude;
|
|
153
|
+
moveY = moveY / magnitude;
|
|
146
154
|
}
|
|
147
155
|
|
|
148
156
|
return {x: moveX, y: moveY};
|
|
@@ -165,78 +173,91 @@ function combatMove(
|
|
|
165
173
|
* PROGRESSION LINE: Battlemage → Warmage → Archmage
|
|
166
174
|
* TIER: 3 (elite Duelist line)
|
|
167
175
|
*/
|
|
168
|
-
export
|
|
176
|
+
export function Archmage(): FinalAction
|
|
169
177
|
{
|
|
170
|
-
const
|
|
178
|
+
const position = usePosition();
|
|
179
|
+
const enemy = useEnemy();
|
|
180
|
+
const status = useStatus();
|
|
181
|
+
const blinkCooldown = useBlinkCooldown();
|
|
182
|
+
const health = useHealth();
|
|
183
|
+
const tick = useTick();
|
|
184
|
+
const threats = useThreats();
|
|
185
|
+
const projectiles = useProjectiles();
|
|
186
|
+
const lastMissileConfig = useLastMissileConfig();
|
|
187
|
+
const castingSpell = useCastingSpell();
|
|
188
|
+
const castProgressData = useCastProgress();
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
const targetDistance = useParam('targetDistance', 341, {
|
|
171
192
|
range: 150,
|
|
172
193
|
min: 0,
|
|
173
194
|
});
|
|
174
|
-
const minTurnRate = useParam('minTurnRate',
|
|
175
|
-
const blinkEscapeDistance = useParam('blinkEscapeDistance',
|
|
195
|
+
const minTurnRate = useParam('minTurnRate', 0.4, {range: 1.5, min: 0, steps: 5});
|
|
196
|
+
const blinkEscapeDistance = useParam('blinkEscapeDistance', 233, {
|
|
176
197
|
range: 190,
|
|
177
198
|
min: 0,
|
|
178
199
|
steps: 7,
|
|
179
200
|
});
|
|
180
|
-
const punishMinVulnerability = useParam('punishMinVulnerability',
|
|
201
|
+
const punishMinVulnerability = useParam('punishMinVulnerability', 70, {
|
|
181
202
|
range: 30,
|
|
182
203
|
min: 10,
|
|
183
204
|
steps: 5,
|
|
184
205
|
});
|
|
185
|
-
const aggressiveHPThreshold = useParam('aggressiveHPThreshold',
|
|
206
|
+
const aggressiveHPThreshold = useParam('aggressiveHPThreshold', 8, {
|
|
186
207
|
range: 20,
|
|
187
208
|
min: 0,
|
|
188
209
|
steps: 5,
|
|
189
210
|
});
|
|
190
211
|
const preferBlinkDodge =
|
|
191
212
|
useParam('preferBlinkDodge', 1, {min: 0, max: 1, steps: 2}) >= 0.5;
|
|
192
|
-
const strafeCyclePeriod = useParam('strafeCyclePeriod',
|
|
213
|
+
const strafeCyclePeriod = useParam('strafeCyclePeriod', 249, {
|
|
193
214
|
range: 75,
|
|
194
215
|
min: 80,
|
|
195
216
|
max: 250,
|
|
196
217
|
});
|
|
197
|
-
const strafeIntensity = useParam('strafeIntensity',
|
|
198
|
-
range:
|
|
199
|
-
min:
|
|
200
|
-
max:
|
|
218
|
+
const strafeIntensity = useParam('strafeIntensity', 0.97, {
|
|
219
|
+
range: 0.4,
|
|
220
|
+
min: 0.2,
|
|
221
|
+
max: 1,
|
|
201
222
|
steps: 7,
|
|
202
223
|
});
|
|
203
|
-
const approachSpeed = useParam('approachSpeed',
|
|
204
|
-
range:
|
|
205
|
-
min:
|
|
206
|
-
max:
|
|
224
|
+
const approachSpeed = useParam('approachSpeed', 0.7, {
|
|
225
|
+
range: 0.3,
|
|
226
|
+
min: 0.1,
|
|
227
|
+
max: 0.8,
|
|
207
228
|
steps: 7,
|
|
208
229
|
});
|
|
209
|
-
const distanceDeadZone = useParam('distanceDeadZone',
|
|
230
|
+
const distanceDeadZone = useParam('distanceDeadZone', 30, {
|
|
210
231
|
range: 30,
|
|
211
232
|
min: 10,
|
|
212
233
|
max: 80,
|
|
213
234
|
steps: 7,
|
|
214
235
|
});
|
|
215
|
-
const aggressiveDistanceOffset = useParam('aggressiveDistanceOffset',
|
|
236
|
+
const aggressiveDistanceOffset = useParam('aggressiveDistanceOffset', 11, {
|
|
216
237
|
range: 50,
|
|
217
238
|
min: 0,
|
|
218
239
|
max: 100,
|
|
219
240
|
steps: 7,
|
|
220
241
|
});
|
|
221
|
-
const cautiousDistanceOffset = useParam('cautiousDistanceOffset',
|
|
242
|
+
const cautiousDistanceOffset = useParam('cautiousDistanceOffset', 31, {
|
|
222
243
|
range: 50,
|
|
223
244
|
min: 0,
|
|
224
245
|
max: 120,
|
|
225
246
|
steps: 7,
|
|
226
247
|
});
|
|
227
|
-
const cancelHeavyDmg = useParam('cancelHeavyDmg',
|
|
248
|
+
const cancelHeavyDmg = useParam('cancelHeavyDmg', 30, {
|
|
228
249
|
range: 15,
|
|
229
250
|
min: 5,
|
|
230
251
|
max: 40,
|
|
231
252
|
steps: 5,
|
|
232
253
|
});
|
|
233
|
-
const cancelMediumDmg = useParam('cancelMediumDmg',
|
|
254
|
+
const cancelMediumDmg = useParam('cancelMediumDmg', 21, {
|
|
234
255
|
range: 10,
|
|
235
256
|
min: 3,
|
|
236
257
|
max: 25,
|
|
237
258
|
steps: 5,
|
|
238
259
|
});
|
|
239
|
-
const cancelCastRatio = useParam('cancelCastRatio', 0.
|
|
260
|
+
const cancelCastRatio = useParam('cancelCastRatio', 0.6, {
|
|
240
261
|
range: 0.4,
|
|
241
262
|
min: 0.3,
|
|
242
263
|
max: 1.0,
|
|
@@ -247,19 +268,19 @@ export const Archmage: WizardFunction = ({state}) =>
|
|
|
247
268
|
min: 50,
|
|
248
269
|
max: 300,
|
|
249
270
|
});
|
|
250
|
-
const blinkWindowMax = useParam('blinkWindowMax',
|
|
271
|
+
const blinkWindowMax = useParam('blinkWindowMax', 69, {
|
|
251
272
|
range: 40,
|
|
252
273
|
min: 20,
|
|
253
274
|
max: 120,
|
|
254
275
|
steps: 7,
|
|
255
276
|
});
|
|
256
|
-
const blinkGapThreshold = useParam('blinkGapThreshold',
|
|
277
|
+
const blinkGapThreshold = useParam('blinkGapThreshold', 111, {
|
|
257
278
|
range: 75,
|
|
258
279
|
min: 20,
|
|
259
280
|
max: 200,
|
|
260
281
|
steps: 7,
|
|
261
282
|
});
|
|
262
|
-
const flightTimeDivisor = useParam('flightTimeDivisor',
|
|
283
|
+
const flightTimeDivisor = useParam('flightTimeDivisor', 8.9, {
|
|
263
284
|
range: 4,
|
|
264
285
|
min: 2,
|
|
265
286
|
max: 12,
|
|
@@ -270,82 +291,92 @@ export const Archmage: WizardFunction = ({state}) =>
|
|
|
270
291
|
50,
|
|
271
292
|
{range: 100, min: 50, max: 300, steps: 7},
|
|
272
293
|
);
|
|
273
|
-
const
|
|
274
|
-
range:
|
|
275
|
-
min:
|
|
276
|
-
max:
|
|
294
|
+
const centerBias = useParam('centerBias', 0.48, {
|
|
295
|
+
range: 0.4,
|
|
296
|
+
min: 0,
|
|
297
|
+
max: 0.8,
|
|
277
298
|
steps: 5,
|
|
278
299
|
});
|
|
300
|
+
const maxBlinkDistance = useParam('maxBlinkDistance', 200, {
|
|
301
|
+
range: 100,
|
|
302
|
+
min: 50,
|
|
303
|
+
max: 300,
|
|
304
|
+
steps: 7,
|
|
305
|
+
});
|
|
279
306
|
|
|
280
|
-
|
|
281
|
-
if (!enemy)
|
|
307
|
+
function capBlinkDistance(target: {x: number; y: number}): {x: number; y: number}
|
|
282
308
|
{
|
|
283
|
-
|
|
309
|
+
const dx = target.x - position.x;
|
|
310
|
+
const dy = target.y - position.y;
|
|
311
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
312
|
+
if (dist <= maxBlinkDistance) return target;
|
|
313
|
+
const scale = maxBlinkDistance / dist;
|
|
314
|
+
return {x: position.x + dx * scale, y: position.y + dy * scale};
|
|
284
315
|
}
|
|
285
316
|
|
|
286
|
-
const distance = distanceTo(
|
|
287
|
-
const threats = getThreats(state);
|
|
317
|
+
const distance = distanceTo(position, enemy.position);
|
|
288
318
|
const urgentThreat = getMostUrgentThreat(threats);
|
|
289
|
-
const dodgeDirection = getDodgeDirectionForMovement(threats,
|
|
319
|
+
const dodgeDirection = getDodgeDirectionForMovement(threats, position);
|
|
290
320
|
|
|
291
321
|
// HP-aware distance: ahead → fight closer, behind → kite at range
|
|
292
|
-
const hpDiff =
|
|
322
|
+
const hpDiff = health - enemy.health;
|
|
293
323
|
const isAggressive = hpDiff >= -aggressiveHPThreshold;
|
|
294
324
|
const effectiveTargetDistance = isAggressive
|
|
295
325
|
? targetDistance - aggressiveDistanceOffset
|
|
296
326
|
: targetDistance + cautiousDistanceOffset;
|
|
297
327
|
|
|
298
328
|
const move = combatMove(
|
|
299
|
-
|
|
329
|
+
position,
|
|
300
330
|
enemy,
|
|
301
331
|
distance,
|
|
302
332
|
effectiveTargetDistance,
|
|
303
333
|
dodgeDirection,
|
|
304
|
-
|
|
334
|
+
tick,
|
|
305
335
|
strafeCyclePeriod,
|
|
306
336
|
strafeIntensity,
|
|
307
337
|
approachSpeed,
|
|
308
338
|
distanceDeadZone,
|
|
339
|
+
centerBias,
|
|
309
340
|
);
|
|
310
|
-
|
|
341
|
+
const safeMove = keepOffWalls(position, move);
|
|
311
342
|
// === STEP 1: CHANNELING ===
|
|
312
|
-
if (
|
|
343
|
+
if (status === 'channeling')
|
|
313
344
|
{
|
|
314
345
|
if (!urgentThreat || urgentThreat.ticksToImpact > shieldCancelThreshold)
|
|
315
346
|
{
|
|
316
|
-
return
|
|
347
|
+
return cancel().move(safeMove.x, safeMove.y);
|
|
317
348
|
}
|
|
318
|
-
return
|
|
349
|
+
return idle();
|
|
319
350
|
}
|
|
320
351
|
|
|
321
352
|
// === STEP 2: PROGRESSIVE CAST-CANCEL ===
|
|
322
353
|
if (
|
|
323
|
-
|
|
324
|
-
|
|
354
|
+
status === 'casting' &&
|
|
355
|
+
castingSpell === 'missile' &&
|
|
325
356
|
urgentThreat &&
|
|
326
357
|
!urgentThreat.bestDodgeDirection
|
|
327
358
|
)
|
|
328
359
|
{
|
|
329
|
-
const remainingCast = (
|
|
360
|
+
const remainingCast = ((castProgressData?.total) ?? 0) - ((castProgressData?.current) ?? 0);
|
|
330
361
|
const defenseTime =
|
|
331
|
-
|
|
332
|
-
? BLINK_CAST_TIME
|
|
333
|
-
: SHIELD_CAST_TIME + SHIELD_BUFFER;
|
|
362
|
+
blinkCooldown === 0
|
|
363
|
+
? RULES.BLINK_CAST_TIME
|
|
364
|
+
: RULES.SHIELD_CAST_TIME + SHIELD_BUFFER;
|
|
334
365
|
const willArriveInWindow =
|
|
335
|
-
urgentThreat.ticksToImpact <= remainingCast + GCD_DURATION + defenseTime;
|
|
366
|
+
urgentThreat.ticksToImpact <= remainingCast + RULES.GCD_DURATION + defenseTime;
|
|
336
367
|
|
|
337
368
|
if (willArriveInWindow)
|
|
338
369
|
{
|
|
339
370
|
const incomingDamage = urgentThreat.projectile.damage;
|
|
340
|
-
const castRatio = (
|
|
371
|
+
const castRatio = ((castProgressData?.current) ?? 0) / ((castProgressData?.total) ?? 1);
|
|
341
372
|
|
|
342
|
-
if (incomingDamage >=
|
|
373
|
+
if (incomingDamage >= health || incomingDamage >= cancelHeavyDmg)
|
|
343
374
|
{
|
|
344
|
-
return
|
|
375
|
+
return cancel().move(safeMove.x, safeMove.y);
|
|
345
376
|
}
|
|
346
377
|
if (incomingDamage >= cancelMediumDmg && castRatio < cancelCastRatio)
|
|
347
378
|
{
|
|
348
|
-
return
|
|
379
|
+
return cancel().move(safeMove.x, safeMove.y);
|
|
349
380
|
}
|
|
350
381
|
}
|
|
351
382
|
}
|
|
@@ -353,72 +384,66 @@ export const Archmage: WizardFunction = ({state}) =>
|
|
|
353
384
|
// === STEP 3: BLINK-DODGE — 100% damage avoidance (optimizer-controlled) ===
|
|
354
385
|
if (
|
|
355
386
|
preferBlinkDodge &&
|
|
356
|
-
|
|
387
|
+
status === 'idle' &&
|
|
357
388
|
urgentThreat &&
|
|
358
|
-
|
|
389
|
+
blinkCooldown === 0 &&
|
|
359
390
|
(!urgentThreat.bestDodgeDirection || shouldForceShield(urgentThreat)) &&
|
|
360
|
-
urgentThreat.ticksToImpact >= BLINK_CAST_TIME &&
|
|
391
|
+
urgentThreat.ticksToImpact >= RULES.BLINK_CAST_TIME &&
|
|
361
392
|
urgentThreat.ticksToImpact <= blinkWindowMax
|
|
362
393
|
)
|
|
363
394
|
{
|
|
364
395
|
// Aggressive: blink toward enemy to dodge + close gap
|
|
365
396
|
// Defensive: blink away to dodge + increase distance
|
|
366
|
-
if (isAggressive && distance > effectiveTargetDistance)
|
|
397
|
+
if (isAggressive && distance > effectiveTargetDistance)
|
|
367
398
|
{
|
|
368
399
|
const blinkTarget = getBlinkToDecreaseDistance(
|
|
369
|
-
|
|
400
|
+
position,
|
|
370
401
|
enemy.position,
|
|
371
|
-
|
|
402
|
+
projectiles,
|
|
372
403
|
);
|
|
373
|
-
if (blinkTarget)
|
|
404
|
+
if (blinkTarget)
|
|
374
405
|
{
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
startCast: {spell: 'blink', target: blinkTarget},
|
|
378
|
-
};
|
|
406
|
+
const capped = capBlinkDistance(blinkTarget);
|
|
407
|
+
return blink(capped.x, capped.y);
|
|
379
408
|
}
|
|
380
409
|
}
|
|
381
410
|
const blinkTarget = getBlinkToIncreaseDistance(
|
|
382
|
-
|
|
411
|
+
position,
|
|
383
412
|
enemy.position,
|
|
384
|
-
|
|
413
|
+
projectiles,
|
|
385
414
|
MIN_BLINK_ESCAPE_DISTANCE,
|
|
386
415
|
);
|
|
387
|
-
if (blinkTarget)
|
|
416
|
+
if (blinkTarget)
|
|
388
417
|
{
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
startCast: {spell: 'blink', target: blinkTarget},
|
|
392
|
-
};
|
|
418
|
+
const capped = capBlinkDistance(blinkTarget);
|
|
419
|
+
return blink(capped.x, capped.y);
|
|
393
420
|
}
|
|
394
421
|
}
|
|
395
422
|
|
|
396
423
|
// === STEP 4: BLINK ESCAPE — enemy too close ===
|
|
397
424
|
if (
|
|
398
|
-
|
|
425
|
+
status === 'idle' &&
|
|
399
426
|
distance < blinkEscapeDistance &&
|
|
400
|
-
|
|
427
|
+
blinkCooldown === 0 &&
|
|
401
428
|
(!urgentThreat || urgentThreat.bestDodgeDirection !== null)
|
|
402
429
|
)
|
|
403
430
|
{
|
|
404
431
|
const escapeTarget = getBlinkToIncreaseDistance(
|
|
405
|
-
|
|
432
|
+
position,
|
|
406
433
|
enemy.position,
|
|
407
|
-
|
|
434
|
+
projectiles,
|
|
408
435
|
MIN_BLINK_ESCAPE_DISTANCE,
|
|
409
436
|
);
|
|
410
|
-
if (escapeTarget)
|
|
437
|
+
if (escapeTarget)
|
|
411
438
|
{
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
startCast: {spell: 'blink', target: escapeTarget},
|
|
415
|
-
};
|
|
439
|
+
const capped = capBlinkDistance(escapeTarget);
|
|
440
|
+
return blink(capped.x, capped.y);
|
|
416
441
|
}
|
|
417
442
|
}
|
|
418
443
|
|
|
419
444
|
// === STEP 5: SHIELD ===
|
|
420
445
|
if (
|
|
421
|
-
|
|
446
|
+
status === 'idle' &&
|
|
422
447
|
urgentThreat &&
|
|
423
448
|
(!urgentThreat.bestDodgeDirection || shouldForceShield(urgentThreat))
|
|
424
449
|
)
|
|
@@ -426,20 +451,38 @@ export const Archmage: WizardFunction = ({state}) =>
|
|
|
426
451
|
if (
|
|
427
452
|
urgentThreat.canBlockInTime &&
|
|
428
453
|
urgentThreat.ticksToStartShield <= SHIELD_BUFFER &&
|
|
429
|
-
(
|
|
454
|
+
(blinkCooldown > 0 || urgentThreat.ticksToImpact < 10)
|
|
430
455
|
)
|
|
431
456
|
{
|
|
432
|
-
return
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
457
|
+
return shield();
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// === Preemptive shield — shield when enemy is casting a missile at close range ===
|
|
462
|
+
if (
|
|
463
|
+
status === 'idle' &&
|
|
464
|
+
distance < 300 &&
|
|
465
|
+
enemy.status === 'casting' &&
|
|
466
|
+
enemy.castingSpell === 'missile'
|
|
467
|
+
)
|
|
468
|
+
{
|
|
469
|
+
const remainingCast = (enemy.castDuration ?? 0) - (enemy.castProgress ?? 0);
|
|
470
|
+
const estimatedFlightTicks = distance / 8;
|
|
471
|
+
const ticksUntilHit = remainingCast + estimatedFlightTicks;
|
|
472
|
+
|
|
473
|
+
if (
|
|
474
|
+
ticksUntilHit >= RULES.SHIELD_CAST_TIME &&
|
|
475
|
+
ticksUntilHit < RULES.SHIELD_CAST_TIME + 15
|
|
476
|
+
)
|
|
477
|
+
{
|
|
478
|
+
return shield();
|
|
436
479
|
}
|
|
437
480
|
}
|
|
438
481
|
|
|
439
482
|
// === STEP 6: VULNERABILITY BLINK — gap-close during enemy cast/GCD ===
|
|
440
483
|
if (
|
|
441
|
-
|
|
442
|
-
|
|
484
|
+
status === 'idle' &&
|
|
485
|
+
blinkCooldown === 0 &&
|
|
443
486
|
isAggressive &&
|
|
444
487
|
distance > effectiveTargetDistance + blinkGapThreshold &&
|
|
445
488
|
(!urgentThreat || urgentThreat.bestDodgeDirection !== null)
|
|
@@ -449,27 +492,28 @@ export const Archmage: WizardFunction = ({state}) =>
|
|
|
449
492
|
if (vulnerabilityTicks >= punishMinVulnerability)
|
|
450
493
|
{
|
|
451
494
|
const blinkTarget = getBlinkToDecreaseDistance(
|
|
452
|
-
|
|
495
|
+
position,
|
|
453
496
|
enemy.position,
|
|
454
|
-
|
|
497
|
+
projectiles,
|
|
455
498
|
);
|
|
456
|
-
if (blinkTarget)
|
|
499
|
+
if (blinkTarget)
|
|
457
500
|
{
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
startCast: {spell: 'blink', target: blinkTarget},
|
|
461
|
-
};
|
|
501
|
+
const capped = capBlinkDistance(blinkTarget);
|
|
502
|
+
return blink(capped.x, capped.y);
|
|
462
503
|
}
|
|
463
504
|
}
|
|
464
505
|
}
|
|
465
506
|
|
|
466
507
|
// === STEP 7: OFFENSE ===
|
|
467
|
-
if (
|
|
508
|
+
if (status === 'idle' && distance < 500)
|
|
468
509
|
{
|
|
510
|
+
const reposition = getPreCastReposition(position, enemy.position);
|
|
511
|
+
if (reposition) return moveAction(reposition.x, reposition.y);
|
|
512
|
+
|
|
469
513
|
const offenseDefenseTime =
|
|
470
|
-
|
|
471
|
-
? BLINK_CAST_TIME
|
|
472
|
-
: SHIELD_CAST_TIME + SHIELD_BUFFER;
|
|
514
|
+
blinkCooldown === 0
|
|
515
|
+
? RULES.BLINK_CAST_TIME
|
|
516
|
+
: RULES.SHIELD_CAST_TIME + SHIELD_BUFFER;
|
|
473
517
|
const enemyHP = Math.ceil(enemy.health);
|
|
474
518
|
const vulnerabilityTicks = getEnemyVulnerabilityTicks(enemy);
|
|
475
519
|
|
|
@@ -479,7 +523,7 @@ export const Archmage: WizardFunction = ({state}) =>
|
|
|
479
523
|
const flightTimeEstimate = distance / flightTimeDivisor;
|
|
480
524
|
const punishBudget = vulnerabilityTicks - flightTimeEstimate;
|
|
481
525
|
const nextThreatTime = urgentThreat?.ticksToImpact ?? Infinity;
|
|
482
|
-
const safeBudget = nextThreatTime - GCD_DURATION - offenseDefenseTime;
|
|
526
|
+
const safeBudget = nextThreatTime - RULES.GCD_DURATION - offenseDefenseTime;
|
|
483
527
|
const effectiveBudget = Math.min(
|
|
484
528
|
punishBudget,
|
|
485
529
|
safeBudget,
|
|
@@ -493,12 +537,12 @@ export const Archmage: WizardFunction = ({state}) =>
|
|
|
493
537
|
const config = fitMissileToBudget(effectiveBudget, distance, {
|
|
494
538
|
minTurnRate: punishTurnRate,
|
|
495
539
|
maxDamage: enemyHP,
|
|
496
|
-
lastMissileConfig:
|
|
540
|
+
lastMissileConfig: lastMissileConfig,
|
|
497
541
|
});
|
|
498
542
|
|
|
499
543
|
if (config && config.damage >= MIN_USEFUL_DAMAGE)
|
|
500
544
|
{
|
|
501
|
-
const castTime = getMissileCastTime(config,
|
|
545
|
+
const castTime = getMissileCastTime(config, lastMissileConfig);
|
|
502
546
|
const actualFlight = distance / config.speed;
|
|
503
547
|
|
|
504
548
|
if (
|
|
@@ -513,17 +557,9 @@ export const Archmage: WizardFunction = ({state}) =>
|
|
|
513
557
|
enemy.position,
|
|
514
558
|
enemy.velocity,
|
|
515
559
|
config.speed,
|
|
516
|
-
|
|
560
|
+
position,
|
|
517
561
|
);
|
|
518
|
-
return
|
|
519
|
-
move,
|
|
520
|
-
startCast: {
|
|
521
|
-
spell: 'missile',
|
|
522
|
-
config,
|
|
523
|
-
missileAI: config.turnRate > 0 ? HomingAI : StraightAI,
|
|
524
|
-
direction: angleTo(state.position, aimTarget),
|
|
525
|
-
},
|
|
526
|
-
};
|
|
562
|
+
return missile(config, config.turnRate > 0 ? HomingAI : StraightAI, angleTo(position, aimTarget)).move(safeMove.x, safeMove.y);
|
|
527
563
|
}
|
|
528
564
|
}
|
|
529
565
|
}
|
|
@@ -531,7 +567,7 @@ export const Archmage: WizardFunction = ({state}) =>
|
|
|
531
567
|
|
|
532
568
|
// --- STANDARD MODE: range-adaptive missiles ---
|
|
533
569
|
const nextThreatTime = urgentThreat?.ticksToImpact ?? Infinity;
|
|
534
|
-
const safeBudget = nextThreatTime - GCD_DURATION - offenseDefenseTime;
|
|
570
|
+
const safeBudget = nextThreatTime - RULES.GCD_DURATION - offenseDefenseTime;
|
|
535
571
|
const budgetTicks = Math.min(safeBudget, MAX_CAST_BUDGET);
|
|
536
572
|
|
|
537
573
|
// Adapt turn rate to distance: close → straight, far → homing
|
|
@@ -541,48 +577,32 @@ export const Archmage: WizardFunction = ({state}) =>
|
|
|
541
577
|
let config = fitMissileToBudget(budgetTicks, distance, {
|
|
542
578
|
minTurnRate: effectiveMinTurnRate,
|
|
543
579
|
maxDamage: enemyHP,
|
|
544
|
-
lastMissileConfig:
|
|
580
|
+
lastMissileConfig: lastMissileConfig,
|
|
545
581
|
});
|
|
546
582
|
|
|
547
|
-
if (
|
|
548
|
-
config &&
|
|
549
|
-
config.damage >= minDmg &&
|
|
550
|
-
isSafeToAttack(threats, safeAttackCastEstimate)
|
|
551
|
-
)
|
|
583
|
+
if (config && config.damage >= minDmg)
|
|
552
584
|
{
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
missileAI: config.turnRate > 0 ? HomingAI : StraightAI,
|
|
559
|
-
direction: angleTo(state.position, enemy.position),
|
|
560
|
-
},
|
|
561
|
-
};
|
|
585
|
+
const actualCastTime = getMissileCastTime(config, lastMissileConfig);
|
|
586
|
+
if (isSafeToAttack(threats, actualCastTime))
|
|
587
|
+
{
|
|
588
|
+
return missile(config, config.turnRate > 0 ? HomingAI : StraightAI, angleTo(position, enemy.position)).move(safeMove.x, safeMove.y);
|
|
589
|
+
}
|
|
562
590
|
}
|
|
563
591
|
|
|
564
|
-
// Fallback: fire bigger missile, accept incoming hit
|
|
565
|
-
if (!config || config.damage < minDmg)
|
|
592
|
+
// Fallback: fire bigger missile, accept incoming hit — but only if no undodgeable threat
|
|
593
|
+
if (!config || config.damage < minDmg)
|
|
566
594
|
{
|
|
567
595
|
config = fitMissileToBudget(MAX_CAST_BUDGET, distance, {
|
|
568
596
|
minTurnRate: effectiveMinTurnRate,
|
|
569
597
|
maxDamage: enemyHP,
|
|
570
|
-
lastMissileConfig:
|
|
598
|
+
lastMissileConfig: lastMissileConfig,
|
|
571
599
|
});
|
|
572
|
-
if (config)
|
|
600
|
+
if (config && (!urgentThreat || urgentThreat.bestDodgeDirection !== null))
|
|
573
601
|
{
|
|
574
|
-
return
|
|
575
|
-
move,
|
|
576
|
-
startCast: {
|
|
577
|
-
spell: 'missile',
|
|
578
|
-
config,
|
|
579
|
-
missileAI: config.turnRate > 0 ? HomingAI : StraightAI,
|
|
580
|
-
direction: angleTo(state.position, enemy.position),
|
|
581
|
-
},
|
|
582
|
-
};
|
|
602
|
+
return missile(config, config.turnRate > 0 ? HomingAI : StraightAI, angleTo(position, enemy.position)).move(safeMove.x, safeMove.y);
|
|
583
603
|
}
|
|
584
604
|
}
|
|
585
605
|
}
|
|
586
606
|
|
|
587
|
-
return
|
|
588
|
-
}
|
|
607
|
+
return moveAction(safeMove.x, safeMove.y);
|
|
608
|
+
}
|