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