@vibemancer/core 0.1.0
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 +28 -0
- package/dist/chunk-L7Z7OFXD.js +9140 -0
- package/dist/chunk-L7Z7OFXD.js.map +1 -0
- package/dist/index-browser.d.ts +2602 -0
- package/dist/index-browser.js +407 -0
- package/dist/index-browser.js.map +1 -0
- package/dist/index.d.ts +150 -0
- package/dist/index.js +750 -0
- package/dist/index.js.map +1 -0
- package/package.json +79 -0
- package/src/bots/berserker/01_Stormchaser.ts +457 -0
- package/src/bots/berserker/02_Stormcaller.ts +417 -0
- package/src/bots/berserker/03_Stormforger.ts +481 -0
- package/src/bots/caster/01_Flamecaller.ts +286 -0
- package/src/bots/caster/02_Pyromancer.ts +350 -0
- package/src/bots/caster/03_Infernalist.ts +492 -0
- package/src/bots/defensive/01_Turtle.ts +151 -0
- package/src/bots/defensive/02_Sentinel.ts +134 -0
- package/src/bots/defensive/03_Golem.ts +357 -0
- package/src/bots/duelist/01_Battlemage.ts +433 -0
- package/src/bots/duelist/02_Warmage.ts +438 -0
- package/src/bots/duelist/03_Archmage.ts +588 -0
- package/src/bots/homing/01_Bonemancer.ts +67 -0
- package/src/bots/homing/02_Lich.ts +356 -0
- package/src/bots/homing/03_Archlich.ts +220 -0
- package/src/bots/index.ts +30 -0
- package/src/bots/kiter/01_Spellspinner.ts +398 -0
- package/src/bots/kiter/02_Spellweaver.ts +378 -0
- package/src/bots/kiter/03_Spellbinder.ts +448 -0
- package/src/bots/melee/01_Shadowblade.ts +270 -0
- package/src/bots/melee/02_Nightblade.ts +437 -0
- package/src/bots/melee/03_Voidblade.ts +582 -0
- package/src/bots/registry.ts +207 -0
- package/src/bots/shared.ts +472 -0
- package/src/bots/sniper/01_Spellshot.ts +385 -0
- package/src/bots/sniper/02_Spelltracer.ts +441 -0
- package/src/bots/sniper/03_Spellseeker.ts +546 -0
- package/src/bots/standalone/Critter.ts +89 -0
- package/src/bots/standalone/Doombringer.ts +91 -0
- package/src/bots/standalone/Hogger.ts +228 -0
- package/src/bots/standalone/Rookie.ts +50 -0
- package/src/bots/standalone/TargetDummy.ts +21 -0
- package/src/bots/test/cheater.ts +405 -0
- package/src/bots/test/crasher.ts +81 -0
- package/src/engine/hooks-runtime.ts +394 -0
- package/src/engine/manual-match.ts +289 -0
- package/src/engine/missile-templates.ts +155 -0
- package/src/engine/optimizer.ts +220 -0
- package/src/engine/params-runtime.ts +189 -0
- package/src/engine/physics.ts +143 -0
- package/src/engine/sandbox-browser.ts +671 -0
- package/src/engine/sandbox-compile.ts +197 -0
- package/src/engine/sandbox-harness.ts +367 -0
- package/src/engine/sandbox.ts +332 -0
- package/src/engine/simulation.ts +828 -0
- package/src/engine/spells.ts +128 -0
- package/src/engine-version.ts +11 -0
- package/src/hooks/action-builders.ts +210 -0
- package/src/hooks/bot-wrapper.ts +84 -0
- package/src/hooks/index.ts +75 -0
- package/src/hooks/state-hooks.ts +354 -0
- package/src/hooks/threat-analysis.ts +365 -0
- package/src/hooks/types.ts +142 -0
- package/src/index-browser.ts +30 -0
- package/src/index.ts +24 -0
- package/src/rules.ts +254 -0
- package/src/stats.ts +262 -0
- package/src/testing.ts +207 -0
- package/src/trace.ts +430 -0
- package/src/types.ts +193 -0
- package/src/utils/angles.ts +47 -0
- package/src/utils/combat.ts +279 -0
- package/src/utils/distance.ts +21 -0
- package/src/utils/index.ts +7 -0
- package/src/utils/movement.ts +108 -0
- package/src/utils/random.ts +65 -0
- package/src/utils/spatial.ts +63 -0
- package/src/utils/targeting.ts +45 -0
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
import {WizardFunction, MissileAIFunction} from '../../types.js';
|
|
2
|
+
import {angleTo} from '../../utils/angles.js';
|
|
3
|
+
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';
|
|
10
|
+
import {getMissileCastTime, getLeadPosition} from '../../utils/combat.js';
|
|
11
|
+
import {
|
|
12
|
+
getThreats,
|
|
13
|
+
getMostUrgentThreat,
|
|
14
|
+
getDodgeDirectionForMovement,
|
|
15
|
+
getBlinkToCenter,
|
|
16
|
+
getBlinkToIncreaseDistance,
|
|
17
|
+
fitMissileToBudget,
|
|
18
|
+
shouldForceShield,
|
|
19
|
+
MIN_BLINK_ESCAPE_DISTANCE,
|
|
20
|
+
MAX_CAST_BUDGET,
|
|
21
|
+
MIN_USEFUL_DAMAGE,
|
|
22
|
+
getClosingRate,
|
|
23
|
+
getEnemyVulnerabilityTicks,
|
|
24
|
+
isSafeToAttack,
|
|
25
|
+
} from '../shared.js';
|
|
26
|
+
import {useParam} from '../../engine/params-runtime.js';
|
|
27
|
+
|
|
28
|
+
const HomingAI: MissileAIFunction = ({worldState}) =>
|
|
29
|
+
{
|
|
30
|
+
const enemy = worldState.enemies[0];
|
|
31
|
+
return {turnToward: enemy?.position};
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const StraightAI: MissileAIFunction = () => ({});
|
|
35
|
+
|
|
36
|
+
const WALL_BUFFER = 50;
|
|
37
|
+
const SHIELD_BUFFER = 3;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Smart combat movement: dodge, avoid walls, maintain sniper range.
|
|
41
|
+
*/
|
|
42
|
+
function combatMove(
|
|
43
|
+
position: {x: number; y: number},
|
|
44
|
+
enemy: {position: {x: number; y: number}},
|
|
45
|
+
currentDistance: number,
|
|
46
|
+
minDistance: number,
|
|
47
|
+
maxDistance: number,
|
|
48
|
+
dodgeDirection: {x: number; y: number} | null,
|
|
49
|
+
tick: number,
|
|
50
|
+
strafeCyclePeriod: number,
|
|
51
|
+
strafeIntensity: number,
|
|
52
|
+
approachSpeed: number,
|
|
53
|
+
): {x: number; y: number}
|
|
54
|
+
{
|
|
55
|
+
const deltaX = enemy.position.x - position.x;
|
|
56
|
+
const deltaY = enemy.position.y - position.y;
|
|
57
|
+
const normalizedDistance = currentDistance > 0 ? currentDistance : 1;
|
|
58
|
+
|
|
59
|
+
const strafeClockwise = {
|
|
60
|
+
x: -deltaY / normalizedDistance,
|
|
61
|
+
y: deltaX / normalizedDistance,
|
|
62
|
+
};
|
|
63
|
+
const strafeCounterClockwise = {
|
|
64
|
+
x: deltaY / normalizedDistance,
|
|
65
|
+
y: -deltaX / normalizedDistance,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
let strafeDirection: {x: number; y: number};
|
|
69
|
+
|
|
70
|
+
if (dodgeDirection)
|
|
71
|
+
{
|
|
72
|
+
strafeDirection = dodgeDirection;
|
|
73
|
+
}
|
|
74
|
+
else if (
|
|
75
|
+
position.x < WALL_BUFFER ||
|
|
76
|
+
position.x > ARENA_SIZE - WALL_BUFFER ||
|
|
77
|
+
position.y < WALL_BUFFER ||
|
|
78
|
+
position.y > ARENA_SIZE - WALL_BUFFER
|
|
79
|
+
)
|
|
80
|
+
{
|
|
81
|
+
const futureClockwise = {
|
|
82
|
+
x: position.x + strafeClockwise.x * 100,
|
|
83
|
+
y: position.y + strafeClockwise.y * 100,
|
|
84
|
+
};
|
|
85
|
+
const futureCounterClockwise = {
|
|
86
|
+
x: position.x + strafeCounterClockwise.x * 100,
|
|
87
|
+
y: position.y + strafeCounterClockwise.y * 100,
|
|
88
|
+
};
|
|
89
|
+
const scoreClockwise = Math.min(
|
|
90
|
+
futureClockwise.x,
|
|
91
|
+
ARENA_SIZE - futureClockwise.x,
|
|
92
|
+
futureClockwise.y,
|
|
93
|
+
ARENA_SIZE - futureClockwise.y,
|
|
94
|
+
);
|
|
95
|
+
const scoreCounterClockwise = Math.min(
|
|
96
|
+
futureCounterClockwise.x,
|
|
97
|
+
ARENA_SIZE - futureCounterClockwise.x,
|
|
98
|
+
futureCounterClockwise.y,
|
|
99
|
+
ARENA_SIZE - futureCounterClockwise.y,
|
|
100
|
+
);
|
|
101
|
+
strafeDirection =
|
|
102
|
+
scoreClockwise > scoreCounterClockwise
|
|
103
|
+
? strafeClockwise
|
|
104
|
+
: strafeCounterClockwise;
|
|
105
|
+
}
|
|
106
|
+
else
|
|
107
|
+
{
|
|
108
|
+
const cycle = Math.floor(tick / strafeCyclePeriod) % 2;
|
|
109
|
+
strafeDirection = cycle === 0 ? strafeClockwise : strafeCounterClockwise;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (dodgeDirection)
|
|
113
|
+
{
|
|
114
|
+
return {x: strafeDirection.x * 100, y: strafeDirection.y * 100};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let moveX = strafeDirection.x * strafeIntensity;
|
|
118
|
+
let moveY = strafeDirection.y * strafeIntensity;
|
|
119
|
+
|
|
120
|
+
if (currentDistance > maxDistance)
|
|
121
|
+
{
|
|
122
|
+
moveX += (deltaX / normalizedDistance) * approachSpeed;
|
|
123
|
+
moveY += (deltaY / normalizedDistance) * approachSpeed;
|
|
124
|
+
}
|
|
125
|
+
else if (currentDistance < minDistance)
|
|
126
|
+
{
|
|
127
|
+
let retreatX = -(deltaX / normalizedDistance) * approachSpeed;
|
|
128
|
+
let retreatY = -(deltaY / normalizedDistance) * approachSpeed;
|
|
129
|
+
if (position.x < WALL_BUFFER && retreatX < 0) retreatX = 0;
|
|
130
|
+
if (position.x > ARENA_SIZE - WALL_BUFFER && retreatX > 0) retreatX = 0;
|
|
131
|
+
if (position.y < WALL_BUFFER && retreatY < 0) retreatY = 0;
|
|
132
|
+
if (position.y > ARENA_SIZE - WALL_BUFFER && retreatY > 0) retreatY = 0;
|
|
133
|
+
moveX += retreatX;
|
|
134
|
+
moveY += retreatY;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const magnitude = Math.sqrt(moveX * moveX + moveY * moveY);
|
|
138
|
+
if (magnitude > 100)
|
|
139
|
+
{
|
|
140
|
+
moveX = (moveX / magnitude) * 100;
|
|
141
|
+
moveY = (moveY / magnitude) * 100;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return {x: moveX, y: moveY};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Bot: Spellseeker
|
|
149
|
+
*
|
|
150
|
+
* BEHAVIOR: Elite sniper that uses intercept-aimed straight missiles during vulnerability
|
|
151
|
+
* windows. Combines Spelltracer's adaptive fitting with precise lead-position aiming
|
|
152
|
+
* and vulnerability exploitation. Straight punish missiles at sniper range are nearly
|
|
153
|
+
* unavoidable. Proactive distance control via closing rate detection.
|
|
154
|
+
*
|
|
155
|
+
* KEY IMPROVEMENTS OVER SPELLTRACER:
|
|
156
|
+
* - Intercept-aimed punish: getLeadPosition + straight missiles during vulnerability
|
|
157
|
+
* - Proactive distance blink: monitors closing rate, blinks before danger zone
|
|
158
|
+
* - Progressive cast-cancel: graduated damage thresholds
|
|
159
|
+
* - Warmup exploitation: always passes lastMissileConfig
|
|
160
|
+
*
|
|
161
|
+
* PROGRESSION LINE: Spellshot → Spelltracer → Spellseeker
|
|
162
|
+
* TIER: 3 (elite Sniper line)
|
|
163
|
+
*/
|
|
164
|
+
export const Spellseeker: WizardFunction = ({state}) =>
|
|
165
|
+
{
|
|
166
|
+
const dangerDistance = useParam('dangerDistance', 65, {
|
|
167
|
+
range: 175,
|
|
168
|
+
min: 0,
|
|
169
|
+
});
|
|
170
|
+
const targetDistance = useParam('targetDistance', 289, {
|
|
171
|
+
range: 200,
|
|
172
|
+
min: 0,
|
|
173
|
+
});
|
|
174
|
+
const distanceDeadZone = useParam('distanceDeadZone', 22, {
|
|
175
|
+
range: 50,
|
|
176
|
+
min: 0,
|
|
177
|
+
});
|
|
178
|
+
const rangeMin = targetDistance - distanceDeadZone;
|
|
179
|
+
const rangeMax = targetDistance + distanceDeadZone;
|
|
180
|
+
const minTurnRate = useParam('minTurnRate', 3.5, {range: 1.5, steps: 5});
|
|
181
|
+
const closingRateThreshold = useParam('closingRateThreshold', 1.79, {
|
|
182
|
+
range: 0.65,
|
|
183
|
+
min: 0,
|
|
184
|
+
steps: 5,
|
|
185
|
+
});
|
|
186
|
+
const punishMinVulnerability = useParam('punishMinVulnerability', 279, {
|
|
187
|
+
range: 30,
|
|
188
|
+
min: 10,
|
|
189
|
+
steps: 5,
|
|
190
|
+
});
|
|
191
|
+
const preferBlinkDodge =
|
|
192
|
+
useParam('preferBlinkDodge', 1, {min: 0, max: 1, steps: 2}) >= 0.5;
|
|
193
|
+
const strafeCyclePeriod = useParam('strafeCyclePeriod', 231, {
|
|
194
|
+
range: 75,
|
|
195
|
+
min: 80,
|
|
196
|
+
max: 250,
|
|
197
|
+
});
|
|
198
|
+
const strafeIntensity = useParam('strafeIntensity', 87, {
|
|
199
|
+
range: 40,
|
|
200
|
+
min: 20,
|
|
201
|
+
max: 100,
|
|
202
|
+
steps: 7,
|
|
203
|
+
});
|
|
204
|
+
const approachSpeed = useParam('approachSpeed', 80, {
|
|
205
|
+
range: 30,
|
|
206
|
+
min: 10,
|
|
207
|
+
max: 80,
|
|
208
|
+
steps: 7,
|
|
209
|
+
});
|
|
210
|
+
const shieldCancelThreshold = useParam('shieldCancelThreshold', 100, {
|
|
211
|
+
range: 75,
|
|
212
|
+
min: 50,
|
|
213
|
+
max: 300,
|
|
214
|
+
});
|
|
215
|
+
const blinkWindowMax = useParam('blinkWindowMax', 88, {
|
|
216
|
+
range: 40,
|
|
217
|
+
min: 20,
|
|
218
|
+
max: 120,
|
|
219
|
+
steps: 7,
|
|
220
|
+
});
|
|
221
|
+
const proactiveBlinkRange = useParam('proactiveBlinkRange', 433, {
|
|
222
|
+
range: 150,
|
|
223
|
+
min: 100,
|
|
224
|
+
max: 500,
|
|
225
|
+
steps: 7,
|
|
226
|
+
});
|
|
227
|
+
const flightTimeDivisor = useParam('flightTimeDivisor', 12, {
|
|
228
|
+
range: 4,
|
|
229
|
+
min: 2,
|
|
230
|
+
max: 12,
|
|
231
|
+
steps: 5,
|
|
232
|
+
});
|
|
233
|
+
const castingFlightTimeDivisor = useParam('castingFlightTimeDivisor', 12, {
|
|
234
|
+
range: 4,
|
|
235
|
+
min: 3,
|
|
236
|
+
max: 12,
|
|
237
|
+
steps: 5,
|
|
238
|
+
});
|
|
239
|
+
const enemyResponseEstimate = useParam('enemyResponseEstimate', 168, {
|
|
240
|
+
range: 100,
|
|
241
|
+
min: 50,
|
|
242
|
+
max: 300,
|
|
243
|
+
steps: 7,
|
|
244
|
+
});
|
|
245
|
+
const threatFallbackThreshold = useParam('threatFallbackThreshold', 250, {
|
|
246
|
+
range: 100,
|
|
247
|
+
min: 100,
|
|
248
|
+
max: 400,
|
|
249
|
+
steps: 7,
|
|
250
|
+
});
|
|
251
|
+
const punishAttackRange = useParam('punishAttackRange', 213, {
|
|
252
|
+
range: 150,
|
|
253
|
+
min: 200,
|
|
254
|
+
max: 700,
|
|
255
|
+
steps: 7,
|
|
256
|
+
});
|
|
257
|
+
const safeAttackCastEstimate = useParam('safeAttackCastEstimate', 35, {
|
|
258
|
+
range: 25,
|
|
259
|
+
min: 10,
|
|
260
|
+
max: 80,
|
|
261
|
+
steps: 5,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const enemy = state.enemies[0];
|
|
265
|
+
if (!enemy)
|
|
266
|
+
{
|
|
267
|
+
return {move: {x: 0, y: 0}};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const distance = distanceTo(state.position, enemy.position);
|
|
271
|
+
const threats = getThreats(state);
|
|
272
|
+
const urgentThreat = getMostUrgentThreat(threats);
|
|
273
|
+
const dodgeDirection = getDodgeDirectionForMovement(threats, state.position);
|
|
274
|
+
const move = combatMove(
|
|
275
|
+
state.position,
|
|
276
|
+
enemy,
|
|
277
|
+
distance,
|
|
278
|
+
rangeMin,
|
|
279
|
+
rangeMax,
|
|
280
|
+
dodgeDirection,
|
|
281
|
+
state.tick,
|
|
282
|
+
strafeCyclePeriod,
|
|
283
|
+
strafeIntensity,
|
|
284
|
+
approachSpeed,
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
// === STEP 1: CHANNELING — cancel shield when safe ===
|
|
288
|
+
if (state.state === 'channeling')
|
|
289
|
+
{
|
|
290
|
+
if (!urgentThreat || urgentThreat.ticksToImpact > shieldCancelThreshold)
|
|
291
|
+
{
|
|
292
|
+
return {move, cancel: true};
|
|
293
|
+
}
|
|
294
|
+
return {move: {x: 0, y: 0}};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// === STEP 2: PROGRESSIVE CAST-CANCEL ===
|
|
298
|
+
if (
|
|
299
|
+
state.state === 'casting' &&
|
|
300
|
+
state.castingSpell === 'missile' &&
|
|
301
|
+
urgentThreat &&
|
|
302
|
+
!urgentThreat.bestDodgeDirection
|
|
303
|
+
)
|
|
304
|
+
{
|
|
305
|
+
const remainingCast = (state.castDuration ?? 0) - (state.castProgress ?? 0);
|
|
306
|
+
const defenseTime =
|
|
307
|
+
state.blinkCooldown === 0
|
|
308
|
+
? BLINK_CAST_TIME
|
|
309
|
+
: SHIELD_CAST_TIME + SHIELD_BUFFER;
|
|
310
|
+
const willArriveInWindow =
|
|
311
|
+
urgentThreat.ticksToImpact <= remainingCast + GCD_DURATION + defenseTime;
|
|
312
|
+
|
|
313
|
+
if (willArriveInWindow)
|
|
314
|
+
{
|
|
315
|
+
const incomingDamage = urgentThreat.projectile.damage;
|
|
316
|
+
const castRatio = (state.castProgress ?? 0) / (state.castDuration ?? 1);
|
|
317
|
+
|
|
318
|
+
if (incomingDamage >= state.health || incomingDamage >= 20)
|
|
319
|
+
{
|
|
320
|
+
return {move, cancel: true};
|
|
321
|
+
}
|
|
322
|
+
if (incomingDamage >= 10 && castRatio < 0.8)
|
|
323
|
+
{
|
|
324
|
+
return {move, cancel: true};
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// === STEP 3: BLINK-DODGE — 100% damage avoidance (optimizer-controlled) ===
|
|
330
|
+
if (
|
|
331
|
+
preferBlinkDodge &&
|
|
332
|
+
state.state === 'idle' &&
|
|
333
|
+
urgentThreat &&
|
|
334
|
+
state.blinkCooldown === 0 &&
|
|
335
|
+
(!urgentThreat.bestDodgeDirection || shouldForceShield(urgentThreat)) &&
|
|
336
|
+
urgentThreat.ticksToImpact >= BLINK_CAST_TIME &&
|
|
337
|
+
urgentThreat.ticksToImpact <= blinkWindowMax
|
|
338
|
+
)
|
|
339
|
+
{
|
|
340
|
+
const midRange = targetDistance;
|
|
341
|
+
const target =
|
|
342
|
+
distance < midRange
|
|
343
|
+
? (getBlinkToIncreaseDistance(
|
|
344
|
+
state.position,
|
|
345
|
+
enemy.position,
|
|
346
|
+
state.projectiles,
|
|
347
|
+
) ?? getBlinkToCenter(state.position))
|
|
348
|
+
: getBlinkToCenter(state.position);
|
|
349
|
+
return {
|
|
350
|
+
move: {x: 0, y: 0},
|
|
351
|
+
startCast: {spell: 'blink', target},
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// === STEP 4: SHIELD — undodgeable or critical threats ===
|
|
356
|
+
if (
|
|
357
|
+
state.state === 'idle' &&
|
|
358
|
+
urgentThreat &&
|
|
359
|
+
(!urgentThreat.bestDodgeDirection || shouldForceShield(urgentThreat))
|
|
360
|
+
)
|
|
361
|
+
{
|
|
362
|
+
if (
|
|
363
|
+
urgentThreat.canBlockInTime &&
|
|
364
|
+
urgentThreat.ticksToStartShield <= SHIELD_BUFFER
|
|
365
|
+
)
|
|
366
|
+
{
|
|
367
|
+
return {
|
|
368
|
+
move: {x: 0, y: 0},
|
|
369
|
+
startCast: {spell: 'shield'},
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// === STEP 5: PROACTIVE DISTANCE BLINK ===
|
|
375
|
+
if (
|
|
376
|
+
state.state === 'idle' &&
|
|
377
|
+
state.blinkCooldown === 0 &&
|
|
378
|
+
(!urgentThreat || urgentThreat.bestDodgeDirection !== null)
|
|
379
|
+
)
|
|
380
|
+
{
|
|
381
|
+
const closingRate = getClosingRate(
|
|
382
|
+
state.position,
|
|
383
|
+
enemy.position,
|
|
384
|
+
enemy.velocity,
|
|
385
|
+
);
|
|
386
|
+
if (
|
|
387
|
+
(closingRate > closingRateThreshold && distance < proactiveBlinkRange) ||
|
|
388
|
+
distance < dangerDistance
|
|
389
|
+
)
|
|
390
|
+
{
|
|
391
|
+
const blinkTarget = getBlinkToIncreaseDistance(
|
|
392
|
+
state.position,
|
|
393
|
+
enemy.position,
|
|
394
|
+
state.projectiles,
|
|
395
|
+
MIN_BLINK_ESCAPE_DISTANCE,
|
|
396
|
+
);
|
|
397
|
+
if (blinkTarget)
|
|
398
|
+
{
|
|
399
|
+
return {
|
|
400
|
+
move: {x: 0, y: 0},
|
|
401
|
+
startCast: {spell: 'blink', target: blinkTarget},
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// === STEP 6: OFFENSE ===
|
|
408
|
+
if (state.state === 'idle' && distance < 600)
|
|
409
|
+
{
|
|
410
|
+
const offenseDefenseTime =
|
|
411
|
+
state.blinkCooldown === 0
|
|
412
|
+
? BLINK_CAST_TIME
|
|
413
|
+
: SHIELD_CAST_TIME + SHIELD_BUFFER;
|
|
414
|
+
const enemyHP = Math.ceil(enemy.health);
|
|
415
|
+
const vulnerabilityTicks = getEnemyVulnerabilityTicks(enemy);
|
|
416
|
+
|
|
417
|
+
// --- PUNISH MODE: intercept-aimed straight missiles during vulnerability ---
|
|
418
|
+
if (
|
|
419
|
+
vulnerabilityTicks >= punishMinVulnerability &&
|
|
420
|
+
distance < punishAttackRange
|
|
421
|
+
)
|
|
422
|
+
{
|
|
423
|
+
const flightTimeEstimate = distance / flightTimeDivisor;
|
|
424
|
+
const punishBudget = vulnerabilityTicks - flightTimeEstimate;
|
|
425
|
+
const nextThreatTime = urgentThreat?.ticksToImpact ?? Infinity;
|
|
426
|
+
const safeBudget = nextThreatTime - GCD_DURATION - offenseDefenseTime;
|
|
427
|
+
const effectiveBudget = Math.min(
|
|
428
|
+
punishBudget,
|
|
429
|
+
safeBudget,
|
|
430
|
+
MAX_CAST_BUDGET,
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
if (effectiveBudget > 0)
|
|
434
|
+
{
|
|
435
|
+
const config = fitMissileToBudget(effectiveBudget, distance, {
|
|
436
|
+
minTurnRate: 0,
|
|
437
|
+
maxDamage: enemyHP,
|
|
438
|
+
lastMissileConfig: state.lastMissileConfig,
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
if (config && config.damage >= MIN_USEFUL_DAMAGE)
|
|
442
|
+
{
|
|
443
|
+
const castTime = getMissileCastTime(config, state.lastMissileConfig);
|
|
444
|
+
const actualFlight = distance / config.speed;
|
|
445
|
+
|
|
446
|
+
if (
|
|
447
|
+
castTime + actualFlight < vulnerabilityTicks &&
|
|
448
|
+
isSafeToAttack(threats, castTime)
|
|
449
|
+
)
|
|
450
|
+
{
|
|
451
|
+
const aimTarget =
|
|
452
|
+
config.turnRate > 0
|
|
453
|
+
? enemy.position
|
|
454
|
+
: getLeadPosition(
|
|
455
|
+
enemy.position,
|
|
456
|
+
enemy.velocity,
|
|
457
|
+
config.speed,
|
|
458
|
+
state.position,
|
|
459
|
+
);
|
|
460
|
+
return {
|
|
461
|
+
move,
|
|
462
|
+
startCast: {
|
|
463
|
+
spell: 'missile',
|
|
464
|
+
config,
|
|
465
|
+
missileAI: config.turnRate > 0 ? HomingAI : StraightAI,
|
|
466
|
+
direction: angleTo(state.position, aimTarget),
|
|
467
|
+
},
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// --- STANDARD MODE: adaptive homing missiles ---
|
|
475
|
+
let nextThreatTime = urgentThreat?.ticksToImpact ?? Infinity;
|
|
476
|
+
|
|
477
|
+
// Anticipate enemy's next missile based on their current state
|
|
478
|
+
if (enemy.state === 'casting' && enemy.castingSpell === 'missile')
|
|
479
|
+
{
|
|
480
|
+
const remainingEnemyCast =
|
|
481
|
+
(enemy.castDuration ?? 0) - (enemy.castProgress ?? 0);
|
|
482
|
+
nextThreatTime = Math.min(
|
|
483
|
+
nextThreatTime,
|
|
484
|
+
remainingEnemyCast + distance / castingFlightTimeDivisor,
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
// No imminent threat — estimate enemy's likely response time
|
|
488
|
+
if (nextThreatTime > threatFallbackThreshold)
|
|
489
|
+
{
|
|
490
|
+
const enemyLockTime = getEnemyVulnerabilityTicks(enemy);
|
|
491
|
+
nextThreatTime = Math.min(
|
|
492
|
+
nextThreatTime,
|
|
493
|
+
enemyLockTime +
|
|
494
|
+
enemyResponseEstimate +
|
|
495
|
+
distance / castingFlightTimeDivisor,
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const safeBudget =
|
|
500
|
+
nextThreatTime - GCD_DURATION - SHIELD_CAST_TIME - SHIELD_BUFFER;
|
|
501
|
+
const budgetTicks = Math.min(safeBudget, MAX_CAST_BUDGET);
|
|
502
|
+
|
|
503
|
+
const config = fitMissileToBudget(budgetTicks, distance, {
|
|
504
|
+
minTurnRate,
|
|
505
|
+
maxDamage: enemyHP,
|
|
506
|
+
lastMissileConfig: state.lastMissileConfig,
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
// HIGH-DAMAGE PRIORITY: Sniper missiles need meaningful shield chip.
|
|
510
|
+
// Fixed big-shot fallback ensures Spellseeker always does significant damage.
|
|
511
|
+
const BIG_SHOT = {damage: 28, speed: 10, turnRate: 0.5, duration: 80};
|
|
512
|
+
|
|
513
|
+
if (
|
|
514
|
+
config &&
|
|
515
|
+
config.damage >= 15 &&
|
|
516
|
+
isSafeToAttack(threats, safeAttackCastEstimate)
|
|
517
|
+
)
|
|
518
|
+
{
|
|
519
|
+
return {
|
|
520
|
+
move,
|
|
521
|
+
startCast: {
|
|
522
|
+
spell: 'missile',
|
|
523
|
+
config,
|
|
524
|
+
missileAI: HomingAI,
|
|
525
|
+
direction: angleTo(state.position, enemy.position),
|
|
526
|
+
},
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Adaptive fitting weak or failed — fire big shot for meaningful damage
|
|
531
|
+
if (!urgentThreat || urgentThreat.ticksToImpact > 50)
|
|
532
|
+
{
|
|
533
|
+
return {
|
|
534
|
+
move,
|
|
535
|
+
startCast: {
|
|
536
|
+
spell: 'missile',
|
|
537
|
+
config: BIG_SHOT,
|
|
538
|
+
missileAI: HomingAI,
|
|
539
|
+
direction: angleTo(state.position, enemy.position),
|
|
540
|
+
},
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
return {move};
|
|
546
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import {WizardFunction} from '../../types.js';
|
|
2
|
+
import {ARENA_WIDTH, ARENA_HEIGHT} from '../../rules.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Bot: Critter
|
|
6
|
+
*
|
|
7
|
+
* BEHAVIOR: Picks random valid actions each tick — random movement, random spells,
|
|
8
|
+
* random missile configs, random directions. Occasionally cancels its own casts.
|
|
9
|
+
* Uses engine-provided seeded random for deterministic behavior. Useful for
|
|
10
|
+
* finding edge cases in the engine, but completely useless in combat.
|
|
11
|
+
*
|
|
12
|
+
* NAMING RATIONALE: In WoW, critters are the 1-HP ambient mobs (rabbits, squirrels,
|
|
13
|
+
* prairie dogs) that wander around doing nothing useful and die to literally anything.
|
|
14
|
+
* This bot is the wizard equivalent — it flails around randomly and gets destroyed by
|
|
15
|
+
* anyone with a plan. The word "Critter" immediately tells any gamer "this thing is
|
|
16
|
+
* helpless and exists only to fill space."
|
|
17
|
+
*
|
|
18
|
+
* STANDALONE — no tier progression. Critters don't level up. However, a future
|
|
19
|
+
* "Hogger" bot could be an elite critter: same chaotic spirit but actually dangerous
|
|
20
|
+
* (like the famous WoW elite that wipes unprepared lowbies).
|
|
21
|
+
*/
|
|
22
|
+
export const Critter: WizardFunction = ({state, random}) =>
|
|
23
|
+
{
|
|
24
|
+
// Use the engine-provided random function (seeded, isolated, deterministic)
|
|
25
|
+
// World-space movement: random x/y in [-100, 100] range
|
|
26
|
+
const move = {
|
|
27
|
+
x: random() * 200 - 100, // -100 to 100
|
|
28
|
+
y: random() * 200 - 100, // -100 to 100
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
if (state.state === 'idle' && random() > 0.95)
|
|
32
|
+
{
|
|
33
|
+
const spellRoll = random();
|
|
34
|
+
if (spellRoll > 0.6)
|
|
35
|
+
{
|
|
36
|
+
return {
|
|
37
|
+
move,
|
|
38
|
+
startCast: {
|
|
39
|
+
spell: 'missile',
|
|
40
|
+
config: {
|
|
41
|
+
damage: Math.floor(random() * 20) + 5,
|
|
42
|
+
speed: random() * 5 + 2,
|
|
43
|
+
turnRate: random() * 3,
|
|
44
|
+
duration: Math.floor(random() * 200) + 100,
|
|
45
|
+
},
|
|
46
|
+
// Missile AI gets its own random from engine - use it for random targeting
|
|
47
|
+
missileAI: ({random: missileRandom}) => ({
|
|
48
|
+
turnToward:
|
|
49
|
+
missileRandom() > 0.5
|
|
50
|
+
? {
|
|
51
|
+
x: missileRandom() * ARENA_WIDTH,
|
|
52
|
+
y: missileRandom() * ARENA_HEIGHT,
|
|
53
|
+
}
|
|
54
|
+
: undefined,
|
|
55
|
+
}),
|
|
56
|
+
direction: random() * 360,
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
else if (spellRoll > 0.3)
|
|
61
|
+
{
|
|
62
|
+
return {
|
|
63
|
+
move,
|
|
64
|
+
startCast: {spell: 'shield'},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
else if (state.blinkCooldown === 0)
|
|
68
|
+
{
|
|
69
|
+
return {
|
|
70
|
+
move,
|
|
71
|
+
startCast: {
|
|
72
|
+
spell: 'blink',
|
|
73
|
+
target: {x: random() * ARENA_WIDTH, y: random() * ARENA_HEIGHT},
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (
|
|
80
|
+
(state.state === 'casting' || state.state === 'channeling') &&
|
|
81
|
+
random() > 0.98
|
|
82
|
+
)
|
|
83
|
+
{
|
|
84
|
+
return {move, cancel: true};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Override auto-aim with random direction — Critter doesn't know where anything is
|
|
88
|
+
return {move, aimDirection: random() * 360};
|
|
89
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import {WizardFunction, MissileAIFunction} from '../../types.js';
|
|
2
|
+
import {angleTo} from '../../utils/angles.js';
|
|
3
|
+
import {distanceTo} from '../../utils/distance.js';
|
|
4
|
+
import {fitMissileToBudget} from '../../utils/combat.js';
|
|
5
|
+
import {
|
|
6
|
+
getThreats,
|
|
7
|
+
getMostUrgentThreat,
|
|
8
|
+
getBlinkToCenter,
|
|
9
|
+
} from '../shared.js';
|
|
10
|
+
|
|
11
|
+
const HomingAI: MissileAIFunction = ({worldState}) =>
|
|
12
|
+
{
|
|
13
|
+
const enemy = worldState.enemies[0];
|
|
14
|
+
return {turnToward: enemy?.position};
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Bot: Doombringer
|
|
19
|
+
*
|
|
20
|
+
* BEHAVIOR: Fires a single maximum-damage homing missile with infinite budget.
|
|
21
|
+
* No damage cap — goes for the biggest possible hit. Exists as a benchmark to
|
|
22
|
+
* demonstrate why lower-damage + shield play is superior. Has basic shield
|
|
23
|
+
* defense but no sophisticated tactics. One fat cast, one fat hit.
|
|
24
|
+
*
|
|
25
|
+
* STANDALONE — no tier progression. Benchmark/test bot.
|
|
26
|
+
*/
|
|
27
|
+
export const Doombringer: WizardFunction = ({state}) =>
|
|
28
|
+
{
|
|
29
|
+
const enemy = state.enemies[0];
|
|
30
|
+
if (!enemy)
|
|
31
|
+
{
|
|
32
|
+
return {move: {x: 0, y: 0}};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const distance = distanceTo(state.position, enemy.position);
|
|
36
|
+
const threats = getThreats(state);
|
|
37
|
+
const urgentThreat = getMostUrgentThreat(threats);
|
|
38
|
+
|
|
39
|
+
// === DEFENSE: Handle channeling ===
|
|
40
|
+
if (state.state === 'channeling')
|
|
41
|
+
{
|
|
42
|
+
if (!urgentThreat || urgentThreat.ticksToImpact > 150)
|
|
43
|
+
{
|
|
44
|
+
return {move: {x: 0, y: 0}, cancel: true};
|
|
45
|
+
}
|
|
46
|
+
return {move: {x: 0, y: 0}};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// === DEFENSE: Shield undodgeable threats ===
|
|
50
|
+
if (
|
|
51
|
+
state.state === 'idle' &&
|
|
52
|
+
urgentThreat &&
|
|
53
|
+
!urgentThreat.bestDodgeDirection
|
|
54
|
+
)
|
|
55
|
+
{
|
|
56
|
+
if (urgentThreat.canBlockInTime && urgentThreat.ticksToStartShield <= 3)
|
|
57
|
+
{
|
|
58
|
+
return {
|
|
59
|
+
move: {x: 0, y: 0},
|
|
60
|
+
startCast: {spell: 'shield'},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
else if (!urgentThreat.canBlockInTime && state.blinkCooldown === 0)
|
|
64
|
+
{
|
|
65
|
+
return {
|
|
66
|
+
move: {x: 0, y: 0},
|
|
67
|
+
startCast: {spell: 'blink', target: getBlinkToCenter(state.position)},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// === OFFENSE: Fire max-damage homing missile (no damage cap) ===
|
|
73
|
+
if (state.state === 'idle')
|
|
74
|
+
{
|
|
75
|
+
const config = fitMissileToBudget(Infinity, distance, {minTurnRate: 0.5});
|
|
76
|
+
if (config)
|
|
77
|
+
{
|
|
78
|
+
return {
|
|
79
|
+
move: {x: 0, y: 0},
|
|
80
|
+
startCast: {
|
|
81
|
+
spell: 'missile',
|
|
82
|
+
config,
|
|
83
|
+
missileAI: HomingAI,
|
|
84
|
+
direction: angleTo(state.position, enemy.position),
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {move: {x: 0, y: 0}};
|
|
91
|
+
};
|