@vibemancer/core 1.0.3 → 1.0.4
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/dist/{chunk-E7R4JFAN.js → chunk-US4MEDKT.js} +85 -20
- package/dist/chunk-US4MEDKT.js.map +1 -0
- package/dist/index-browser-CYoJrb2d.d.ts +3065 -0
- package/dist/index-browser.d.ts +1 -2878
- package/dist/index-browser.js +3 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +35 -8
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/bots/index.ts +5 -0
- package/src/engine/bot-compute-budget.ts +173 -0
- package/src/engine/bundle-fight.ts +9 -3
- package/src/engine/sandbox-browser.ts +7 -1
- package/src/engine/sandbox-harness.ts +15 -4
- package/src/engine/sandbox.ts +11 -2
- package/src/engine/simulation.ts +113 -17
- package/src/engine-version.ts +1 -1
- package/src/index.ts +2 -0
- package/dist/chunk-E7R4JFAN.js.map +0 -1
|
@@ -0,0 +1,3065 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VIBEMANCER - HOOKS API TYPES
|
|
3
|
+
*
|
|
4
|
+
* Types for the hooks-based bot API.
|
|
5
|
+
*
|
|
6
|
+
* UNITS REFERENCE (100 ticks = 1 second):
|
|
7
|
+
* Position: absolute world coordinates, 0-800 on each axis (800×800 arena)
|
|
8
|
+
* Velocity: units per tick on each axis (player max speed = 1 u/t)
|
|
9
|
+
* Health: hit points (max 60)
|
|
10
|
+
* Ticks: game ticks (10ms each, 100/sec). Divide by 100 for seconds.
|
|
11
|
+
* Angles: degrees (0°=right, 90°=down, 180°=left, 270°=up)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Enemy wizard state as seen by your bot.
|
|
16
|
+
*
|
|
17
|
+
* Note: you cannot see the enemy's missile configs, cooldown timers, or
|
|
18
|
+
* damage history — only what's visible on the battlefield.
|
|
19
|
+
*/
|
|
20
|
+
interface EnemyState {
|
|
21
|
+
/** Enemy position in world coordinates (0-800). */
|
|
22
|
+
position: Position;
|
|
23
|
+
/** Enemy velocity in units/tick. */
|
|
24
|
+
velocity: Velocity;
|
|
25
|
+
/** Enemy current HP (0-60). */
|
|
26
|
+
health: number;
|
|
27
|
+
/** Enemy status: 'idle', 'casting', 'channeling' (shield), or 'gcd_locked'. */
|
|
28
|
+
status: 'idle' | 'casting' | 'channeling' | 'gcd_locked';
|
|
29
|
+
/** Which spell enemy is casting, or null. */
|
|
30
|
+
castingSpell: 'missile' | 'shield' | 'blink' | null;
|
|
31
|
+
/** Cast progress in ticks (0 if not casting). */
|
|
32
|
+
castProgress: number;
|
|
33
|
+
/** Total cast duration in ticks (0 if not casting). */
|
|
34
|
+
castDuration: number;
|
|
35
|
+
/** Remaining GCD ticks (0 if not in GCD). */
|
|
36
|
+
gcdRemaining: number;
|
|
37
|
+
/** Duration the enemy has been channeling in ticks (0 if not channeling). */
|
|
38
|
+
channelDuration: number;
|
|
39
|
+
/** Enemy shield block multiplier (0 if not shielding, 0.3-0.9 if shielding). */
|
|
40
|
+
shieldStrength: number;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Pre-computed analysis of an incoming enemy projectile.
|
|
44
|
+
*
|
|
45
|
+
* All timing values are in ticks (100 ticks = 1 second).
|
|
46
|
+
* Dodge directions are relative to missile heading, not world axes.
|
|
47
|
+
*/
|
|
48
|
+
interface AnalyzedThreat {
|
|
49
|
+
/** Unique projectile ID. */
|
|
50
|
+
id: string;
|
|
51
|
+
/** Raw projectile state (position, rotation in degrees, speed in u/t, turnRate, remainingTicks). */
|
|
52
|
+
projectile: ProjectileState;
|
|
53
|
+
/** Ticks until missile hits your current position. Infinity if predicted to miss. */
|
|
54
|
+
ticksToImpact: number;
|
|
55
|
+
/** Whether the missile will hit if you stand still. */
|
|
56
|
+
willHit: boolean;
|
|
57
|
+
/** Whether strafing left (perpendicular to missile heading) avoids it. */
|
|
58
|
+
canDodgeLeft: boolean;
|
|
59
|
+
/** Whether strafing right (perpendicular to missile heading) avoids it. */
|
|
60
|
+
canDodgeRight: boolean;
|
|
61
|
+
/** Whether moving directly away from the missile avoids it. */
|
|
62
|
+
canOutrun: boolean;
|
|
63
|
+
/** Optimal dodge direction as a unit vector {x, y}, or null if undodgeable. */
|
|
64
|
+
bestDodgeDirection: Position | null;
|
|
65
|
+
/** Whether you can channel shield before the missile arrives. */
|
|
66
|
+
canBlockInTime: boolean;
|
|
67
|
+
/** Ticks from now when you should START channeling shield to block in time. */
|
|
68
|
+
ticksToStartShield: number;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Final action that can be returned from a bot.
|
|
72
|
+
* Cannot be further chained.
|
|
73
|
+
*/
|
|
74
|
+
interface FinalAction {
|
|
75
|
+
/** Internal: extract the WizardActions */
|
|
76
|
+
readonly _toAction: () => WizardActions;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Action builder that allows chaining .move() for simultaneous movement.
|
|
80
|
+
* Returned by shield(), missile(), and cancel().
|
|
81
|
+
*/
|
|
82
|
+
interface ActionBuilder extends FinalAction {
|
|
83
|
+
/**
|
|
84
|
+
* Add movement to this action (e.g., move while casting).
|
|
85
|
+
* Direction vector, not absolute position. Auto-normalized.
|
|
86
|
+
* Positive X = right, positive Y = down.
|
|
87
|
+
*/
|
|
88
|
+
move(x: number, y: number): FinalAction;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Wizard function type for the hooks API.
|
|
92
|
+
* Called every tick. Read state with hooks, return an action.
|
|
93
|
+
*/
|
|
94
|
+
type WizardFunction = () => FinalAction;
|
|
95
|
+
/**
|
|
96
|
+
* Internal context for game state hooks.
|
|
97
|
+
*/
|
|
98
|
+
interface WizardContext {
|
|
99
|
+
entityId: string;
|
|
100
|
+
tick: number;
|
|
101
|
+
position: Position;
|
|
102
|
+
velocity: Velocity;
|
|
103
|
+
health: number;
|
|
104
|
+
maxHealth: number;
|
|
105
|
+
state: 'idle' | 'casting' | 'channeling' | 'gcd_locked';
|
|
106
|
+
castingSpell?: 'missile' | 'shield' | 'blink';
|
|
107
|
+
castProgress?: number;
|
|
108
|
+
castDuration?: number;
|
|
109
|
+
channelingSpell?: 'shield';
|
|
110
|
+
channelDuration?: number;
|
|
111
|
+
gcdRemaining?: number;
|
|
112
|
+
blinkCooldown: number;
|
|
113
|
+
lastMissileConfig?: MissileConfig;
|
|
114
|
+
enemies: Array<{
|
|
115
|
+
id: string;
|
|
116
|
+
position: Position;
|
|
117
|
+
velocity: Velocity;
|
|
118
|
+
health: number;
|
|
119
|
+
state: 'idle' | 'casting' | 'channeling' | 'gcd_locked';
|
|
120
|
+
castingSpell?: 'missile' | 'shield' | 'blink';
|
|
121
|
+
castProgress?: number;
|
|
122
|
+
castDuration?: number;
|
|
123
|
+
gcdRemaining?: number;
|
|
124
|
+
channelingSpell?: 'shield';
|
|
125
|
+
channelDuration?: number;
|
|
126
|
+
}>;
|
|
127
|
+
projectiles: ProjectileState[];
|
|
128
|
+
myProjectiles: ProjectileState[];
|
|
129
|
+
arenaWidth: number;
|
|
130
|
+
arenaHeight: number;
|
|
131
|
+
damageDealt: number;
|
|
132
|
+
damageTaken: number;
|
|
133
|
+
lastHitTick: number;
|
|
134
|
+
random: () => number;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Context available to missile AI functions via getMissileContext().
|
|
138
|
+
* Provides missile state, the full game state from the owner's perspective,
|
|
139
|
+
* and a seeded PRNG.
|
|
140
|
+
*/
|
|
141
|
+
interface MissileContext {
|
|
142
|
+
/** Missile position in world coordinates. */
|
|
143
|
+
position: Position;
|
|
144
|
+
/** Missile heading in degrees (0=right, 90=down). */
|
|
145
|
+
rotation: number;
|
|
146
|
+
/** Missile speed in units/tick. */
|
|
147
|
+
speed: number;
|
|
148
|
+
/** Missile turn rate in degrees/tick. */
|
|
149
|
+
turnRate: number;
|
|
150
|
+
/** Missile damage on hit. */
|
|
151
|
+
damage: number;
|
|
152
|
+
/** Ticks remaining before the missile expires. */
|
|
153
|
+
remainingTicks: number;
|
|
154
|
+
/** ID of the wizard who owns this missile. */
|
|
155
|
+
ownerId: string;
|
|
156
|
+
/** Full game state from the missile owner's perspective. */
|
|
157
|
+
worldState: GameState;
|
|
158
|
+
/** Seeded PRNG [0, 1). Deterministic per missile per tick. */
|
|
159
|
+
random: () => number;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Action returned by a missile AI function.
|
|
163
|
+
* Call turnToward(x, y) to steer, or flyStraight() to coast.
|
|
164
|
+
*/
|
|
165
|
+
interface MissileAction {
|
|
166
|
+
/** Internal: extract the MissileActions */
|
|
167
|
+
readonly _toMissileAction: () => MissileActions;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Missile AI function type for the hooks API.
|
|
171
|
+
* Called every tick for each in-flight missile.
|
|
172
|
+
* Read state with getMissileContext(), return a MissileAction.
|
|
173
|
+
*/
|
|
174
|
+
type MissileFunction$1 = () => MissileAction;
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* VIBEMANCER - TYPES
|
|
178
|
+
*
|
|
179
|
+
* This file contains all the TypeScript interfaces used by the game engine.
|
|
180
|
+
*/
|
|
181
|
+
/**
|
|
182
|
+
* Position in 2D space.
|
|
183
|
+
*/
|
|
184
|
+
interface Position {
|
|
185
|
+
x: number;
|
|
186
|
+
y: number;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Velocity in 2D space (units per tick).
|
|
190
|
+
*/
|
|
191
|
+
interface Velocity {
|
|
192
|
+
x: number;
|
|
193
|
+
y: number;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Wizard state.
|
|
197
|
+
*/
|
|
198
|
+
interface WizardState {
|
|
199
|
+
id: string;
|
|
200
|
+
position: Position;
|
|
201
|
+
rotation: number;
|
|
202
|
+
health: number;
|
|
203
|
+
maxHealth: number;
|
|
204
|
+
state: 'idle' | 'casting' | 'channeling' | 'gcd_locked';
|
|
205
|
+
castingSpell?: 'missile' | 'shield' | 'blink';
|
|
206
|
+
castProgress?: number;
|
|
207
|
+
castDuration?: number;
|
|
208
|
+
channelingSpell?: 'shield';
|
|
209
|
+
channelDuration?: number;
|
|
210
|
+
gcdRemaining?: number;
|
|
211
|
+
blinkCooldown: number;
|
|
212
|
+
velocity: Velocity;
|
|
213
|
+
lastMissileConfig?: MissileConfig;
|
|
214
|
+
warmupMultiplier?: number;
|
|
215
|
+
invincible?: boolean;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Projectile (missile) state.
|
|
219
|
+
*/
|
|
220
|
+
interface ProjectileState {
|
|
221
|
+
id: string;
|
|
222
|
+
type: 'missile';
|
|
223
|
+
ownerId: string;
|
|
224
|
+
position: Position;
|
|
225
|
+
rotation: number;
|
|
226
|
+
speed: number;
|
|
227
|
+
turnRate: number;
|
|
228
|
+
damage: number;
|
|
229
|
+
remainingTicks: number;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Game configuration (read-only).
|
|
233
|
+
*/
|
|
234
|
+
interface GameConfig {
|
|
235
|
+
arenaSize: {
|
|
236
|
+
width: number;
|
|
237
|
+
height: number;
|
|
238
|
+
};
|
|
239
|
+
tickRate: number;
|
|
240
|
+
maxTicks: number;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Full game state passed to wizard.
|
|
244
|
+
*/
|
|
245
|
+
interface GameState {
|
|
246
|
+
tick: number;
|
|
247
|
+
position: Position;
|
|
248
|
+
rotation: number;
|
|
249
|
+
health: number;
|
|
250
|
+
maxHealth: number;
|
|
251
|
+
state: WizardState['state'];
|
|
252
|
+
castingSpell?: 'missile' | 'shield' | 'blink';
|
|
253
|
+
castProgress?: number;
|
|
254
|
+
castDuration?: number;
|
|
255
|
+
channelingSpell?: 'shield';
|
|
256
|
+
channelDuration?: number;
|
|
257
|
+
gcdRemaining?: number;
|
|
258
|
+
blinkCooldown: number;
|
|
259
|
+
velocity: Velocity;
|
|
260
|
+
lastMissileConfig?: MissileConfig;
|
|
261
|
+
warmupMultiplier?: number;
|
|
262
|
+
enemies: WizardState[];
|
|
263
|
+
projectiles: ProjectileState[];
|
|
264
|
+
myProjectiles: ProjectileState[];
|
|
265
|
+
damageDealt: number;
|
|
266
|
+
damageTaken: number;
|
|
267
|
+
lastHitTick: number;
|
|
268
|
+
/** Events emitted during the most recent tick (empty for the initial state). */
|
|
269
|
+
events: SimEvent[];
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Actions returned by wizard each tick.
|
|
273
|
+
*
|
|
274
|
+
* Movement uses world-space coordinates:
|
|
275
|
+
* - x: +100 = right, -100 = left
|
|
276
|
+
* - y: +100 = down, -100 = up
|
|
277
|
+
* - Diagonal movement is normalized (magnitude capped at 100)
|
|
278
|
+
* - No rotation tracking - just output (x, y) direction
|
|
279
|
+
*/
|
|
280
|
+
interface WizardActions {
|
|
281
|
+
/**
|
|
282
|
+
* Movement direction in world-space.
|
|
283
|
+
* Values are clamped to [-100, 100] range.
|
|
284
|
+
* Magnitude is normalized to max 100 for diagonal movement.
|
|
285
|
+
*/
|
|
286
|
+
move: {
|
|
287
|
+
x: number;
|
|
288
|
+
y: number;
|
|
289
|
+
};
|
|
290
|
+
/**
|
|
291
|
+
* Start a new cast (only works if state === 'idle').
|
|
292
|
+
*/
|
|
293
|
+
startCast?: {
|
|
294
|
+
spell: 'missile';
|
|
295
|
+
config: MissileConfig;
|
|
296
|
+
missileAI: MissileFunction;
|
|
297
|
+
direction?: number;
|
|
298
|
+
} | {
|
|
299
|
+
spell: 'shield';
|
|
300
|
+
} | {
|
|
301
|
+
spell: 'blink';
|
|
302
|
+
target: Position;
|
|
303
|
+
};
|
|
304
|
+
/**
|
|
305
|
+
* Cancel current cast or channel (works if casting or channeling).
|
|
306
|
+
*/
|
|
307
|
+
cancel?: boolean;
|
|
308
|
+
/**
|
|
309
|
+
* Update aim direction while casting a missile (degrees).
|
|
310
|
+
* The missile fires in this direction at launch, allowing tracking during cast.
|
|
311
|
+
* Only applies while state === 'casting' and castingSpell === 'missile'.
|
|
312
|
+
*/
|
|
313
|
+
aimDirection?: number;
|
|
314
|
+
/**
|
|
315
|
+
* Missile guide — manual play mode only. Takes direct control of a
|
|
316
|
+
* specific missile using WASD/joystick input. While active, the wizard
|
|
317
|
+
* is invulnerable and frozen (move ignored), and the missile steers
|
|
318
|
+
* using its turnRate toward the given direction.
|
|
319
|
+
*
|
|
320
|
+
* direction: {x, y} → converted to target angle via atan2(y, x)
|
|
321
|
+
* direction: null → missile flies straight (no steering input)
|
|
322
|
+
*/
|
|
323
|
+
missileGuide?: {
|
|
324
|
+
missileId: string;
|
|
325
|
+
direction: {
|
|
326
|
+
x: number;
|
|
327
|
+
y: number;
|
|
328
|
+
} | null;
|
|
329
|
+
} | null;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Missile configuration.
|
|
333
|
+
*/
|
|
334
|
+
interface MissileConfig {
|
|
335
|
+
damage: number;
|
|
336
|
+
speed: number;
|
|
337
|
+
turnRate: number;
|
|
338
|
+
duration: number;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Missile AI function type — re-exported from hooks/types.ts.
|
|
342
|
+
*/
|
|
343
|
+
type MissileFunction = MissileFunction$1;
|
|
344
|
+
/**
|
|
345
|
+
* Actions returned by missile each tick.
|
|
346
|
+
*/
|
|
347
|
+
interface MissileActions {
|
|
348
|
+
turnToward?: Position;
|
|
349
|
+
turnToAngle?: number;
|
|
350
|
+
}
|
|
351
|
+
interface MissileHitEvent {
|
|
352
|
+
type: 'missile-hit';
|
|
353
|
+
position: Position;
|
|
354
|
+
damage: number;
|
|
355
|
+
actualDamage: number;
|
|
356
|
+
speed: number;
|
|
357
|
+
ownerId: string;
|
|
358
|
+
targetId: string;
|
|
359
|
+
}
|
|
360
|
+
interface MissileExpiredEvent {
|
|
361
|
+
type: 'missile-expired';
|
|
362
|
+
position: Position;
|
|
363
|
+
}
|
|
364
|
+
interface MissileOobEvent {
|
|
365
|
+
type: 'missile-oob';
|
|
366
|
+
position: Position;
|
|
367
|
+
}
|
|
368
|
+
interface BlinkEvent {
|
|
369
|
+
type: 'blink';
|
|
370
|
+
wizardId: string;
|
|
371
|
+
from: Position;
|
|
372
|
+
to: Position;
|
|
373
|
+
}
|
|
374
|
+
interface ShieldStartEvent {
|
|
375
|
+
type: 'shield-start';
|
|
376
|
+
wizardId: string;
|
|
377
|
+
position: Position;
|
|
378
|
+
}
|
|
379
|
+
interface CastStartEvent {
|
|
380
|
+
type: 'cast-start';
|
|
381
|
+
wizardId: string;
|
|
382
|
+
position: Position;
|
|
383
|
+
spell: 'missile' | 'shield' | 'blink';
|
|
384
|
+
}
|
|
385
|
+
interface WizardDeathEvent {
|
|
386
|
+
type: 'wizard-death';
|
|
387
|
+
wizardId: string;
|
|
388
|
+
position: Position;
|
|
389
|
+
}
|
|
390
|
+
interface WizardLavaDeathEvent {
|
|
391
|
+
type: 'wizard-lava-death';
|
|
392
|
+
wizardId: string;
|
|
393
|
+
position: Position;
|
|
394
|
+
}
|
|
395
|
+
interface CastCancelEvent {
|
|
396
|
+
type: 'cast-cancel';
|
|
397
|
+
wizardId: string;
|
|
398
|
+
position: Position;
|
|
399
|
+
spell: 'missile' | 'shield' | 'blink';
|
|
400
|
+
}
|
|
401
|
+
interface MissileLaunchEvent {
|
|
402
|
+
type: 'missile-launch';
|
|
403
|
+
position: Position;
|
|
404
|
+
damage: number;
|
|
405
|
+
speed: number;
|
|
406
|
+
ownerId: string;
|
|
407
|
+
rotation: number;
|
|
408
|
+
}
|
|
409
|
+
interface ShieldBlockEvent {
|
|
410
|
+
type: 'shield-block';
|
|
411
|
+
wizardId: string;
|
|
412
|
+
position: Position;
|
|
413
|
+
damageBlocked: number;
|
|
414
|
+
damageThrough: number;
|
|
415
|
+
}
|
|
416
|
+
type SimEvent = MissileHitEvent | MissileExpiredEvent | MissileOobEvent | MissileLaunchEvent | BlinkEvent | ShieldStartEvent | ShieldBlockEvent | CastStartEvent | CastCancelEvent | WizardDeathEvent | WizardLavaDeathEvent;
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* VIBEMANCER - GAME RULES
|
|
420
|
+
*
|
|
421
|
+
* This is the single source of truth for all game constants.
|
|
422
|
+
* All game logic imports from here. Read this to understand the game.
|
|
423
|
+
*
|
|
424
|
+
* SWEEPABLE CONSTANTS — the balance-search ruleset:
|
|
425
|
+
* Every combat-balance constant lives on the mutable `RULES` object. The engine
|
|
426
|
+
* and bots read `RULES.X` so the balance-search optimizer can override any of
|
|
427
|
+
* them at runtime via applyRulesetOverrides() — object property reads are live
|
|
428
|
+
* across every module and survive bundling (unlike a reassigned `let`, which
|
|
429
|
+
* esbuild/Vite snapshot at the import site). Structural constants (arena size,
|
|
430
|
+
* tick rate, hitbox radius, missile floors) stay plain `const`.
|
|
431
|
+
*
|
|
432
|
+
* The UPPER_CASE named exports below (WIZARD_HEALTH, SHIELD_CAST_TIME, …) are
|
|
433
|
+
* default SNAPSHOTS for external/UI/MCP consumers that only need the factory
|
|
434
|
+
* value. They do NOT track overrides — anything that must respond to a sweep
|
|
435
|
+
* reads `RULES.X`.
|
|
436
|
+
*/
|
|
437
|
+
|
|
438
|
+
declare const TICKS_PER_SECOND = 100;
|
|
439
|
+
declare const TICK_DURATION_MS = 10;
|
|
440
|
+
/**
|
|
441
|
+
* The mutable ruleset: the single source of truth for every sweepable combat
|
|
442
|
+
* constant. Engine + bots read these via `RULES.X`. Override with
|
|
443
|
+
* applyRulesetOverrides(); restore with resetRuleset().
|
|
444
|
+
*/
|
|
445
|
+
declare const RULES: {
|
|
446
|
+
WIZARD_HEALTH: number;
|
|
447
|
+
MOVEMENT_SPEED: number;
|
|
448
|
+
CASTING_MOVEMENT_MULT: number;
|
|
449
|
+
GCD_DURATION: number;
|
|
450
|
+
SHIELD_CAST_TIME: number;
|
|
451
|
+
SHIELD_MAX_BLOCK: number;
|
|
452
|
+
SHIELD_DECAY_PER_SECOND: number;
|
|
453
|
+
SHIELD_MIN_BLOCK: number;
|
|
454
|
+
BLINK_CAST_TIME: number;
|
|
455
|
+
BLINK_RANGE: number;
|
|
456
|
+
BLINK_MAX_COOLDOWN: number;
|
|
457
|
+
BLINK_MIN_COOLDOWN: number;
|
|
458
|
+
KNOCKBACK_DAMAGE_THRESHOLD: number;
|
|
459
|
+
KNOCKBACK_SPEED_PER_DAMAGE: number;
|
|
460
|
+
KNOCKBACK_DELAY: number;
|
|
461
|
+
KNOCKBACK_DECAY: number;
|
|
462
|
+
MISSILE_MIN_CAST_TIME: number;
|
|
463
|
+
MISSILE_BASE_RADIUS: number;
|
|
464
|
+
MISSILE_DAMAGE_RADIUS_SCALE: number;
|
|
465
|
+
MISSILE_BASE_CAST: number;
|
|
466
|
+
MISSILE_DAMAGE_SCALE: number;
|
|
467
|
+
MISSILE_DAMAGE_POWER: number;
|
|
468
|
+
MISSILE_HOMING_COEFF: number;
|
|
469
|
+
MISSILE_TURN_DURATION_COEFF: number;
|
|
470
|
+
MISSILE_SPEED_DURATION_BASELINE: number;
|
|
471
|
+
MISSILE_SPEED_DURATION_COEFF: number;
|
|
472
|
+
WARMUP_MAX_BONUS: number;
|
|
473
|
+
WARMUP_MAX_PENALTY: number;
|
|
474
|
+
};
|
|
475
|
+
declare const WIZARD_HEALTH: number;
|
|
476
|
+
declare const WIZARD_RADIUS = 5;
|
|
477
|
+
declare const MOVEMENT_SPEED: number;
|
|
478
|
+
declare const CASTING_MOVEMENT_MULT: number;
|
|
479
|
+
declare const GCD_DURATION: number;
|
|
480
|
+
declare const SHIELD_CAST_TIME: number;
|
|
481
|
+
declare const SHIELD_MAX_BLOCK: number;
|
|
482
|
+
declare const SHIELD_DECAY_PER_SECOND: number;
|
|
483
|
+
declare const SHIELD_MIN_BLOCK: number;
|
|
484
|
+
declare const BLINK_CAST_TIME: number;
|
|
485
|
+
declare const BLINK_RANGE: number;
|
|
486
|
+
declare const BLINK_MAX_COOLDOWN: number;
|
|
487
|
+
declare const BLINK_MIN_COOLDOWN: number;
|
|
488
|
+
/** @deprecated Use BLINK_MAX_COOLDOWN */
|
|
489
|
+
declare const BLINK_COOLDOWN: number;
|
|
490
|
+
declare const KNOCKBACK_DAMAGE_THRESHOLD: number;
|
|
491
|
+
declare const KNOCKBACK_SPEED_PER_DAMAGE: number;
|
|
492
|
+
declare const KNOCKBACK_DELAY: number;
|
|
493
|
+
declare const KNOCKBACK_DECAY: number;
|
|
494
|
+
declare const ARENA_SIZE = 860;
|
|
495
|
+
declare const LAVA_BORDER_WIDTH = 30;
|
|
496
|
+
declare const ARENA_MIN = 30;
|
|
497
|
+
declare const ARENA_MAX: number;
|
|
498
|
+
declare const SPAWN_DISTANCE = 600;
|
|
499
|
+
declare const MISSILE_MIN_DAMAGE = 1;
|
|
500
|
+
declare const MISSILE_MIN_SPEED = 1.5;
|
|
501
|
+
declare const MISSILE_MIN_DURATION = 10;
|
|
502
|
+
declare const MISSILE_MIN_CAST_TIME: number;
|
|
503
|
+
declare const MISSILE_BASE_RADIUS: number;
|
|
504
|
+
declare const MISSILE_DAMAGE_RADIUS_SCALE: number;
|
|
505
|
+
/**
|
|
506
|
+
* Calculate missile hitbox radius based on damage.
|
|
507
|
+
*/
|
|
508
|
+
declare function calculateMissileRadius(damage: number): number;
|
|
509
|
+
declare const MISSILE_BASE_CAST: number;
|
|
510
|
+
declare const MISSILE_DAMAGE_SCALE: number;
|
|
511
|
+
declare const MISSILE_DAMAGE_POWER: number;
|
|
512
|
+
declare const MISSILE_HOMING_COEFF: number;
|
|
513
|
+
declare const MISSILE_TURN_DURATION_COEFF: number;
|
|
514
|
+
declare const MISSILE_SPEED_DURATION_BASELINE: number;
|
|
515
|
+
declare const MISSILE_SPEED_DURATION_COEFF: number;
|
|
516
|
+
/**
|
|
517
|
+
* Maps turnRate to effective cost for the cast time formula.
|
|
518
|
+
* Higher turn rate = more expensive cast. Negative values clamped to 0.
|
|
519
|
+
*/
|
|
520
|
+
declare function effectiveTurnRateCost(turnRate: number): number;
|
|
521
|
+
/**
|
|
522
|
+
* Validate and sanitize missile config.
|
|
523
|
+
* Ensures all values meet minimum requirements (Lesson #9).
|
|
524
|
+
*/
|
|
525
|
+
declare function validateMissileConfig(config: MissileConfig): MissileConfig;
|
|
526
|
+
/**
|
|
527
|
+
* Calculate missile cast time from config.
|
|
528
|
+
*
|
|
529
|
+
* Base formula:
|
|
530
|
+
* cast_time = 0.1
|
|
531
|
+
* + 0.226 × damage^(2/3)
|
|
532
|
+
* + 0.12 × effectiveTurnRateCost(turnRate)
|
|
533
|
+
* + 0.10 × (effectiveTurnRateCost(turnRate) × durationSeconds)
|
|
534
|
+
* + 0.025 × (speed × durationSeconds - 1.5)
|
|
535
|
+
*
|
|
536
|
+
* turnRate is clamped to >= 0. Cost is linear.
|
|
537
|
+
*
|
|
538
|
+
* If lastMissileConfig is a MissileConfig, applies warmup multiplier:
|
|
539
|
+
* - Similar to previous: up to 20% faster
|
|
540
|
+
* - Very different: up to 20% slower (switching penalty)
|
|
541
|
+
*/
|
|
542
|
+
declare function calculateMissileCastTime(config: MissileConfig, lastMissileConfig?: MissileConfig | undefined | null): number;
|
|
543
|
+
declare const WARMUP_MAX_BONUS: number;
|
|
544
|
+
declare const WARMUP_MAX_PENALTY: number;
|
|
545
|
+
declare const WARMUP_SPEED_TOLERANCE = 3;
|
|
546
|
+
declare const WARMUP_TURN_TOLERANCE = 1;
|
|
547
|
+
declare const WARMUP_DURATION_TOLERANCE = 50;
|
|
548
|
+
/**
|
|
549
|
+
* Calculate how similar two missile configs are (0 to 1).
|
|
550
|
+
* Returns 1 for identical configs, 0 for very different ones.
|
|
551
|
+
* Used by the warmup system to determine cast time multiplier.
|
|
552
|
+
*
|
|
553
|
+
* Only compares speed, turnRate, and duration — these define the missile
|
|
554
|
+
* "style" (melee stab vs ranged homing vs fast snipe). Damage is excluded
|
|
555
|
+
* because varying power doesn't change playstyle.
|
|
556
|
+
*/
|
|
557
|
+
declare function calculateMissileSimilarity(prev: MissileConfig | undefined, current: MissileConfig): number;
|
|
558
|
+
/**
|
|
559
|
+
* Calculate the cast time multiplier from the warmup system.
|
|
560
|
+
* Returns < 1 for bonus (faster), > 1 for penalty (slower), 1 for neutral.
|
|
561
|
+
*
|
|
562
|
+
* Similar to previous cast → multiplier approaches (1 - MAX_BONUS) = 0.80
|
|
563
|
+
* Very different from previous → multiplier approaches (1 + MAX_PENALTY) = 1.20
|
|
564
|
+
* No previous cast → full bonus (1 - MAX_BONUS) = 0.80
|
|
565
|
+
*/
|
|
566
|
+
declare function calculateWarmupMultiplier(prev: MissileConfig | undefined, current: MissileConfig): number;
|
|
567
|
+
declare const MATCH_DURATION = 30000;
|
|
568
|
+
declare const DEFAULT_SEED = 42;
|
|
569
|
+
declare const MAX_HEALTH: number;
|
|
570
|
+
declare const COLLISION_RADIUS = 5;
|
|
571
|
+
declare const MOVE_SPEED: number;
|
|
572
|
+
declare const MISSILE_RADIUS_PER_DAMAGE: number;
|
|
573
|
+
declare const SHIELD_MAX_STRENGTH: number;
|
|
574
|
+
declare const SHIELD_MIN_STRENGTH: number;
|
|
575
|
+
declare const SHIELD_DECAY_RATE: number;
|
|
576
|
+
declare const BLINK_MAX_RANGE: number;
|
|
577
|
+
/**
|
|
578
|
+
* Calculate blink cooldown based on distance traveled.
|
|
579
|
+
* Short blinks get short cooldowns, full-range blinks get the maximum.
|
|
580
|
+
*/
|
|
581
|
+
declare function calculateBlinkCooldown(distance: number): number;
|
|
582
|
+
declare const ARENA_WATER_BUFFER = 200;
|
|
583
|
+
/**
|
|
584
|
+
* Search bounds for each sweepable constant. The current value is the start.
|
|
585
|
+
* Every key here is read by the engine/bots through `RULES.X`, so overrides
|
|
586
|
+
* take effect across the whole simulation.
|
|
587
|
+
*/
|
|
588
|
+
declare const RULESET_RANGES: Record<string, {
|
|
589
|
+
min: number;
|
|
590
|
+
max: number;
|
|
591
|
+
}>;
|
|
592
|
+
/** Merge overrides onto the RULES object and apply them globally. */
|
|
593
|
+
declare function applyRulesetOverrides(overrides: Record<string, number>): void;
|
|
594
|
+
/** Restore all swept constants to their factory defaults. */
|
|
595
|
+
declare function resetRuleset(): void;
|
|
596
|
+
/** Current value of every sweepable constant (the optimizer's starting point). */
|
|
597
|
+
declare function currentRuleset(): Record<string, number>;
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Engine version — a stable content hash of packages/core/src.
|
|
601
|
+
*
|
|
602
|
+
* Derived by `scripts/update-engine-version.mjs` from the engine sources, so it changes
|
|
603
|
+
* only when the engine changes — NOT on every build.
|
|
604
|
+
* The same value is propagated to `packages/functions/src/engine-version.generated.ts`
|
|
605
|
+
* so cloud functions and the core engine always agree on one version.
|
|
606
|
+
*
|
|
607
|
+
* Used to gate spectator replays: a recorded match can only be re-simulated when
|
|
608
|
+
* the runtime engine version matches the version that produced the match.
|
|
609
|
+
*/
|
|
610
|
+
declare const ENGINE_VERSION = 1709865010946010;
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* VIBEMANCER — BOT COMPUTE BUDGET
|
|
614
|
+
*
|
|
615
|
+
* The real-time guard, moved off the whole fight and onto each bot.
|
|
616
|
+
*
|
|
617
|
+
* Decision 0002: fight LENGTH is simulation time (`maxTicks`) and always was; "stop a bot
|
|
618
|
+
* looping forever" genuinely needs real time. Conflating both into one wall-clock timeout
|
|
619
|
+
* around the entire match meant a slow SERVER was indistinguishable from a broken bot — and
|
|
620
|
+
* matchmaking blamed the player for it, up to auto-deactivating their bot.
|
|
621
|
+
*
|
|
622
|
+
* The constraint that shapes this module: the tick loop runs INSIDE the isolate, where the
|
|
623
|
+
* only clock is `Date.now()`. There is no way to measure a bot in machine-independent units
|
|
624
|
+
* from in there. So this guard is wall-clock, and wall clock on a shared server is exactly
|
|
625
|
+
* the thing that caused the original bug.
|
|
626
|
+
*
|
|
627
|
+
* The resolution is therefore in what going over budget COSTS:
|
|
628
|
+
*
|
|
629
|
+
* - A bot has ONE budget: total thinking time across a whole fight.
|
|
630
|
+
* - Spend it and the bot is not called again for the rest of the fight. It stands still.
|
|
631
|
+
* - That is never recorded as an error, so it cannot increment `consecutiveCrashes` and
|
|
632
|
+
* cannot auto-deactivate a bot.
|
|
633
|
+
*
|
|
634
|
+
* A bot too slow to answer loses the fight it was too slow for, which is the same thing that
|
|
635
|
+
* happens to a lagging player in any real-time game. A busy server can cost someone a match;
|
|
636
|
+
* it must never cost them their bot.
|
|
637
|
+
*
|
|
638
|
+
* ## Why there is no per-tick limit
|
|
639
|
+
*
|
|
640
|
+
* There was one, briefly: overrun 50ms in a single tick and that tick's action was
|
|
641
|
+
* discarded. It had to go, and the reason is the important part of this file.
|
|
642
|
+
*
|
|
643
|
+
* This engine is DETERMINISTIC by design. Spectate re-simulates a recorded match and gates
|
|
644
|
+
* on ENGINE_VERSION precisely so that the same version and seed reproduce the same fight. A
|
|
645
|
+
* per-tick threshold breaks that: under load a single call can cross 50ms through a GC pause
|
|
646
|
+
* or a scheduler hiccup, its action is discarded, and the fight diverges. Outcomes then
|
|
647
|
+
* depend on how busy the machine was — which is the original bug, reintroduced at finer
|
|
648
|
+
* granularity by the thing meant to fix it. It was caught by a test that passed alone and
|
|
649
|
+
* failed inside a loaded run.
|
|
650
|
+
*
|
|
651
|
+
* A single cumulative budget does not have that problem in practice. Measurement still
|
|
652
|
+
* varies with load, but it only changes BEHAVIOUR at one point — exhaustion — and an honest
|
|
653
|
+
* bot never approaches it: the worst real bot measured spends 5.6s of a 45s allowance. So
|
|
654
|
+
* every legitimate fight is bit-identical to an unbudgeted one, and determinism holds.
|
|
655
|
+
*
|
|
656
|
+
* A bot that stalls inside one tick is left to the outer wall-clock backstop, which is the
|
|
657
|
+
* only thing that could ever catch it anyway: a `while(true)` cannot be interrupted from
|
|
658
|
+
* inside a single-threaded isolate, no matter what the tick loop measures.
|
|
659
|
+
*
|
|
660
|
+
* Everything here is pure arithmetic so it can be tested exhaustively. The only impure part
|
|
661
|
+
* — reading the clock — stays in the tick loop.
|
|
662
|
+
*
|
|
663
|
+
* A note on precision, because it looks broken and is not. `Date.now()` resolves to 1ms here
|
|
664
|
+
* (measured), while a typical bot call is microseconds — so nearly every individual call
|
|
665
|
+
* measures 0ms and contributes nothing. That is fine, and deliberately not "fixed": a call
|
|
666
|
+
* lasting d milliseconds (d < 1) straddles a millisecond boundary with probability d, so it
|
|
667
|
+
* reads 1 exactly that often and 0 otherwise. The expected measurement equals the true
|
|
668
|
+
* duration, which over the thousands of calls in a fight is what a cumulative budget needs.
|
|
669
|
+
* Swapping in a higher-resolution clock is not an option anyway: inside the isolate there is
|
|
670
|
+
* no other clock, and a per-call `performance.now()` would cost more than it measures.
|
|
671
|
+
*/
|
|
672
|
+
/** Limits applied to a single bot for one fight. */
|
|
673
|
+
interface BudgetLimits {
|
|
674
|
+
/** Milliseconds a bot may spend thinking, in total, before it stops being called. */
|
|
675
|
+
totalMs: number;
|
|
676
|
+
}
|
|
677
|
+
/** A single bot's running spend for one fight. */
|
|
678
|
+
interface BotBudgetState {
|
|
679
|
+
spentMs: number;
|
|
680
|
+
exhausted: boolean;
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Default, sized from measurement (2026-08-30) — and resized once, after the first
|
|
684
|
+
* measurement turned out to be worthless.
|
|
685
|
+
*
|
|
686
|
+
* The first pass measured TargetDummy, a bot whose entire body is `return idle()`. Against
|
|
687
|
+
* that, a full-length 30,000-tick match costs ~196ms and any budget looks generous. Real
|
|
688
|
+
* built-in bots are nothing like it. One `fight()` — ten matches — between two of them:
|
|
689
|
+
*
|
|
690
|
+
* Bonemancer vs Turtle 18392ms
|
|
691
|
+
* Bonemancer vs Spellseeker 11251ms
|
|
692
|
+
* Turtle vs Hogger 10185ms
|
|
693
|
+
*
|
|
694
|
+
* and instrumenting the calls shows bot code is 70-78% of that, one honest bot spending
|
|
695
|
+
* 5611ms in a single fight. A 20s budget, which had looked like 200x headroom, was really
|
|
696
|
+
* about 2-4x — it would have fired on innocent play the first time the server was busy,
|
|
697
|
+
* which is the exact bug it exists to prevent.
|
|
698
|
+
*
|
|
699
|
+
* Hence 45s per bot per fight: roughly 8x the honest worst case measured here. That margin
|
|
700
|
+
* is also what keeps fights deterministic, since behaviour only changes if it is reached.
|
|
701
|
+
*
|
|
702
|
+
* This exists to catch runaway code, not to make anyone optimise.
|
|
703
|
+
*/
|
|
704
|
+
declare const DEFAULT_BUDGET: BudgetLimits;
|
|
705
|
+
/**
|
|
706
|
+
* Last-resort wall-clock backstop for a whole fight (ms).
|
|
707
|
+
*
|
|
708
|
+
* Lives here, next to the budget, because the two numbers only make sense together: the
|
|
709
|
+
* budget is what bounds bot compute, and this is only for the case no in-isolate guard can
|
|
710
|
+
* reach — a bot in `while(true)`, which cannot be interrupted from inside a single-threaded
|
|
711
|
+
* isolate no matter what the tick loop measures.
|
|
712
|
+
*
|
|
713
|
+
* One runaway bot (45s of budget) plus an honest opponent has to fit inside it, or the
|
|
714
|
+
* backstop fires first and the budget never gets to attribute anything. It is capped at 110s
|
|
715
|
+
* rather than raised further because the mcp Cloud Function's own `timeoutSeconds` is 120 —
|
|
716
|
+
* above that the platform kills the request first and nothing useful is reported.
|
|
717
|
+
*
|
|
718
|
+
* It is a single exported constant precisely because it was previously four separate
|
|
719
|
+
* literals (bundle-fight, sandbox twice, functions/fight-runner) that drifted apart.
|
|
720
|
+
*/
|
|
721
|
+
declare const DEFAULT_FIGHT_BACKSTOP_MS = 110000;
|
|
722
|
+
/**
|
|
723
|
+
* Both bots' budgets for ONE FIGHT.
|
|
724
|
+
*
|
|
725
|
+
* The scope matters more than it looks. `fight()` is not one simulation, it is ten (five
|
|
726
|
+
* spawn distances, each played from both sides). A budget scoped to a single `simulate`
|
|
727
|
+
* would hand a runaway bot its whole total ten times over — 450 seconds of bot compute
|
|
728
|
+
* inside a 110-second backstop — and would look like it was bounding something while
|
|
729
|
+
* bounding nothing. So the budget belongs to the fight, and is carried across its matches.
|
|
730
|
+
*
|
|
731
|
+
* This is the one mutable thing in this module: `simulate` updates `states` in place as it
|
|
732
|
+
* runs so the spend survives from one match to the next.
|
|
733
|
+
*/
|
|
734
|
+
interface FightBudget {
|
|
735
|
+
states: [BotBudgetState, BotBudgetState];
|
|
736
|
+
limits: BudgetLimits;
|
|
737
|
+
}
|
|
738
|
+
/** A fresh budget for one bot at the start of a fight. */
|
|
739
|
+
declare function createBudgetState(): BotBudgetState;
|
|
740
|
+
/** A fresh budget covering both bots for one whole fight (all of its matches). */
|
|
741
|
+
declare function createFightBudget(limits?: BudgetLimits): FightBudget;
|
|
742
|
+
/** May this bot still be called at all? */
|
|
743
|
+
declare function mayAct(state: BotBudgetState): boolean;
|
|
744
|
+
/**
|
|
745
|
+
* Record what one call cost.
|
|
746
|
+
*
|
|
747
|
+
* A non-finite elapsed time is ignored rather than charged, and a negative one cannot refund
|
|
748
|
+
* budget — `Date.now()` can step backwards on an NTP correction or a VM migration, and that
|
|
749
|
+
* must not become a way to earn compute.
|
|
750
|
+
*/
|
|
751
|
+
declare function recordSpend(state: BotBudgetState, limits: BudgetLimits, elapsedMs: number): BotBudgetState;
|
|
752
|
+
|
|
753
|
+
interface InternalWizardState extends WizardState {
|
|
754
|
+
missileConfig?: MissileConfig;
|
|
755
|
+
missileAI?: MissileFunction$1;
|
|
756
|
+
blinkTarget?: {
|
|
757
|
+
x: number;
|
|
758
|
+
y: number;
|
|
759
|
+
};
|
|
760
|
+
damageDealt: number;
|
|
761
|
+
damageTaken: number;
|
|
762
|
+
lastHitTick: number;
|
|
763
|
+
knockbackVx?: number;
|
|
764
|
+
knockbackVy?: number;
|
|
765
|
+
knockbackDelay?: number;
|
|
766
|
+
knockbackPendingVx?: number;
|
|
767
|
+
knockbackPendingVy?: number;
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* Initialize a new match state.
|
|
771
|
+
*/
|
|
772
|
+
declare function createInitialState(_seed: number, spawnDist?: number): GameState;
|
|
773
|
+
/**
|
|
774
|
+
* Process one game tick.
|
|
775
|
+
*/
|
|
776
|
+
declare function tick(currentTick: number, wizard1AI: WizardFunction, wizard2AI: WizardFunction, config: GameConfig, wizards: InternalWizardState[], projectiles: ProjectileState[], missileAIs: Map<string, MissileFunction$1>, matchSeed: number, budgets?: {
|
|
777
|
+
states: [BotBudgetState, BotBudgetState];
|
|
778
|
+
limits: BudgetLimits;
|
|
779
|
+
}): {
|
|
780
|
+
nextTick: number;
|
|
781
|
+
wizards: InternalWizardState[];
|
|
782
|
+
projectiles: ProjectileState[];
|
|
783
|
+
events: SimEvent[];
|
|
784
|
+
errors: BotError[];
|
|
785
|
+
budgets?: [BotBudgetState, BotBudgetState];
|
|
786
|
+
};
|
|
787
|
+
/**
|
|
788
|
+
* Get the game state from a specific player's perspective.
|
|
789
|
+
* Returns a deep clone to prevent mutation of history entries.
|
|
790
|
+
* Used for history recording where independent snapshots are needed.
|
|
791
|
+
*/
|
|
792
|
+
declare function getPlayerState(playerIndex: number, wizards: InternalWizardState[], projectiles: ProjectileState[], tick: number, events?: SimEvent[]): GameState;
|
|
793
|
+
/** Winner of a single match: a wizard ID, 'draw' (simultaneous kill), or null (timeout). */
|
|
794
|
+
type MatchWinner = 'wizard-1' | 'wizard-2' | 'draw' | null;
|
|
795
|
+
/** Winner of a fight (aggregate): a wizard ID or 'draw'. Never null. */
|
|
796
|
+
type FightWinner = 'wizard-1' | 'wizard-2' | 'draw';
|
|
797
|
+
/**
|
|
798
|
+
* Result of a simulation.
|
|
799
|
+
*/
|
|
800
|
+
/** A runtime error captured from a bot or missile AI function. */
|
|
801
|
+
interface BotError {
|
|
802
|
+
tick: number;
|
|
803
|
+
entityId: string;
|
|
804
|
+
message: string;
|
|
805
|
+
}
|
|
806
|
+
interface SimulateResult {
|
|
807
|
+
/** 'wizard-1'/'wizard-2' = killed opponent, 'draw' = simultaneous kill, null = timeout */
|
|
808
|
+
winner: MatchWinner;
|
|
809
|
+
ticks: number;
|
|
810
|
+
finalState: GameState;
|
|
811
|
+
history: GameState[];
|
|
812
|
+
/** Runtime errors thrown by bot or missile AI functions (empty if no errors). */
|
|
813
|
+
errors: BotError[];
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* Result of a fight (best-of-5 at different spawn distances).
|
|
817
|
+
*/
|
|
818
|
+
interface FightResult {
|
|
819
|
+
wizard1Wins: number;
|
|
820
|
+
wizard2Wins: number;
|
|
821
|
+
draws: number;
|
|
822
|
+
/** Winner of the fight: 'wizard-1', 'wizard-2', or 'draw' (never null) */
|
|
823
|
+
winner: FightWinner;
|
|
824
|
+
/**
|
|
825
|
+
* Individual match results (one per spawn distance, non-swapped only).
|
|
826
|
+
* Used for visual playback in the web viewer. Scoring includes both sides.
|
|
827
|
+
*/
|
|
828
|
+
matches: SimulateResult[];
|
|
829
|
+
}
|
|
830
|
+
/** Spawn distances for the 5 matches in a fight (creates butterfly effect variation) */
|
|
831
|
+
declare const FIGHT_SPAWN_DISTANCES: number[];
|
|
832
|
+
/**
|
|
833
|
+
* Run a fight: 10 matches (5 spawn distances × 2 sides) between two bots.
|
|
834
|
+
* Each spawn distance is played twice — once with each bot on each side —
|
|
835
|
+
* to ensure results are independent of starting position.
|
|
836
|
+
*
|
|
837
|
+
* The `matches` array contains only the 5 non-swapped matches (for visual playback).
|
|
838
|
+
* The scoring aggregates (wizard1Wins, wizard2Wins, draws) include all 10 matches.
|
|
839
|
+
*
|
|
840
|
+
* This is the standard way to determine who wins a matchup.
|
|
841
|
+
* Used by both the tournament system and the visual UI.
|
|
842
|
+
*/
|
|
843
|
+
declare function fight(wizard1AI: WizardFunction, wizard2AI: WizardFunction, options?: {
|
|
844
|
+
seed?: number;
|
|
845
|
+
maxTicks?: number;
|
|
846
|
+
/**
|
|
847
|
+
* Per-bot compute budget for the WHOLE fight — all ten matches share it. Omitted
|
|
848
|
+
* means unbudgeted. See bot-compute-budget.ts.
|
|
849
|
+
*/
|
|
850
|
+
budgetLimits?: BudgetLimits;
|
|
851
|
+
}): FightResult;
|
|
852
|
+
/**
|
|
853
|
+
* Run a full match simulation.
|
|
854
|
+
*
|
|
855
|
+
* @param options.skipHistory - When true, skips recording per-tick history snapshots.
|
|
856
|
+
* This dramatically improves performance (no deep cloning per tick) and is used
|
|
857
|
+
* by the optimizer and fight() scoring. The returned history array will be empty
|
|
858
|
+
* and finalState will still be populated.
|
|
859
|
+
*/
|
|
860
|
+
declare function simulate(wizard1AI: WizardFunction, wizard2AI: WizardFunction, options?: {
|
|
861
|
+
maxTicks?: number;
|
|
862
|
+
seed?: number;
|
|
863
|
+
spawnDistance?: number;
|
|
864
|
+
skipHistory?: boolean;
|
|
865
|
+
/**
|
|
866
|
+
* Per-bot compute budget for this match alone. Omitted means unbudgeted, which is
|
|
867
|
+
* what manual play and most tests want — the clock is then never read at all. See
|
|
868
|
+
* bot-compute-budget.ts for why exceeding it costs a bot its action rather than
|
|
869
|
+
* producing an error.
|
|
870
|
+
*/
|
|
871
|
+
budgetLimits?: BudgetLimits;
|
|
872
|
+
/**
|
|
873
|
+
* A budget SHARED across every match of a fight, updated in place as this match
|
|
874
|
+
* runs. This is what `fight()` passes, because a fight is ten matches and a
|
|
875
|
+
* per-match budget would bound nothing. Takes precedence over `budgetLimits`.
|
|
876
|
+
*/
|
|
877
|
+
budget?: FightBudget;
|
|
878
|
+
}): SimulateResult;
|
|
879
|
+
|
|
880
|
+
/**
|
|
881
|
+
* VIBEMANCER - HOOKS RUNTIME
|
|
882
|
+
*
|
|
883
|
+
* This file implements a minimal React-like hooks runtime for AI programming.
|
|
884
|
+
* It supports useState, useEffect, useMemo, useRef, and useParam with entity isolation.
|
|
885
|
+
*
|
|
886
|
+
* RULES OF HOOKS (same as React):
|
|
887
|
+
* - Hooks must be called at the top level of the bot function
|
|
888
|
+
* - Hooks must be called in the same order every tick
|
|
889
|
+
* - Hooks must NOT be called conditionally
|
|
890
|
+
*
|
|
891
|
+
* Violating these rules throws an error (detected via hook index validation).
|
|
892
|
+
*/
|
|
893
|
+
|
|
894
|
+
interface HookState {
|
|
895
|
+
values: unknown[];
|
|
896
|
+
effects: {
|
|
897
|
+
callback: () => void | (() => void);
|
|
898
|
+
deps?: unknown[];
|
|
899
|
+
cleanup?: () => void;
|
|
900
|
+
}[];
|
|
901
|
+
memos: {
|
|
902
|
+
value: unknown;
|
|
903
|
+
deps?: unknown[];
|
|
904
|
+
}[];
|
|
905
|
+
/** Type of each hook call in order (for validation). */
|
|
906
|
+
hookTypes: string[];
|
|
907
|
+
/** Total hooks called on first successful tick. */
|
|
908
|
+
hookCount: number;
|
|
909
|
+
/** Whether the first tick has completed successfully (hook pattern established). */
|
|
910
|
+
initialized: boolean;
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Validate a hook call and return its sequential index.
|
|
914
|
+
* Ensures hooks are called in the same order every tick.
|
|
915
|
+
*
|
|
916
|
+
* On the first tick: records the hook type at this index.
|
|
917
|
+
* On subsequent ticks: validates the hook type matches.
|
|
918
|
+
*
|
|
919
|
+
* @param type - The hook type name (e.g., 'useState', 'useEffect', 'useParam')
|
|
920
|
+
* @returns The sequential hook index
|
|
921
|
+
* @throws If called outside runWithHooks or if hook order changed
|
|
922
|
+
*/
|
|
923
|
+
declare function validateHookCall(type: string): number;
|
|
924
|
+
/**
|
|
925
|
+
* Run a function with a specific entity's hook context.
|
|
926
|
+
*/
|
|
927
|
+
declare function runWithHooks<T>(entityId: string, fn: () => T): T;
|
|
928
|
+
/**
|
|
929
|
+
* Run a bot function with full context (game state + persistence hooks).
|
|
930
|
+
* Use this when you need to set BOTH the entity ID and the game context.
|
|
931
|
+
*/
|
|
932
|
+
declare function runWizardWithContext<T>(entityId: string, context: WizardContext, fn: () => T): T;
|
|
933
|
+
/**
|
|
934
|
+
* Run a function with game state context only, preserving the current entity ID.
|
|
935
|
+
* Used by the simulation to set up WizardContext before calling hooks-style bots.
|
|
936
|
+
*/
|
|
937
|
+
declare function withWizardContext<T>(context: WizardContext, fn: () => T): T;
|
|
938
|
+
/**
|
|
939
|
+
* Get the current bot context. Throws if called outside bot execution.
|
|
940
|
+
*/
|
|
941
|
+
declare function getWizardContext(): WizardContext;
|
|
942
|
+
/**
|
|
943
|
+
* Run a function with missile context set, preserving the current entity ID.
|
|
944
|
+
* Used by the simulation to set up MissileContext before calling hooks-style missile AIs.
|
|
945
|
+
*/
|
|
946
|
+
declare function withMissileContext<T>(context: MissileContext, fn: () => T): T;
|
|
947
|
+
/**
|
|
948
|
+
* Get the current missile context. Throws if called outside missile AI execution.
|
|
949
|
+
*/
|
|
950
|
+
declare function getMissileContext(): MissileContext;
|
|
951
|
+
/**
|
|
952
|
+
* Persist state between ticks.
|
|
953
|
+
*/
|
|
954
|
+
declare function useState<T>(initialValue: T | (() => T)): [T, (newValue: T | ((prev: T) => T)) => void];
|
|
955
|
+
/**
|
|
956
|
+
* React to state changes.
|
|
957
|
+
*/
|
|
958
|
+
declare function useEffect(callback: () => void | (() => void), deps?: unknown[]): void;
|
|
959
|
+
/**
|
|
960
|
+
* Memoize expensive calculations.
|
|
961
|
+
*/
|
|
962
|
+
declare function useMemo<T>(factory: () => T, deps?: unknown[]): T;
|
|
963
|
+
/**
|
|
964
|
+
* Mutable reference that persists across ticks.
|
|
965
|
+
* Unlike useState, mutations don't need a setter - just modify .current directly.
|
|
966
|
+
*/
|
|
967
|
+
interface RefObject<T> {
|
|
968
|
+
current: T;
|
|
969
|
+
}
|
|
970
|
+
declare function useRef<T>(initialValue: T): RefObject<T>;
|
|
971
|
+
/**
|
|
972
|
+
* Clear hook state for an entity (e.g., when it dies).
|
|
973
|
+
*/
|
|
974
|
+
declare function clearHooks(entityId: string): void;
|
|
975
|
+
/**
|
|
976
|
+
* Reset all hook states (e.g., when a match restarts).
|
|
977
|
+
*/
|
|
978
|
+
declare function resetAllHooks(): void;
|
|
979
|
+
|
|
980
|
+
/**
|
|
981
|
+
* Move a wizard based on world-space input direction.
|
|
982
|
+
*
|
|
983
|
+
* Any vector works: direction is preserved, speed is clamped to [0, 1].
|
|
984
|
+
* (0.5, 0) = half speed right. (300, 200) = full speed at 33.7°.
|
|
985
|
+
*/
|
|
986
|
+
declare function moveWizard(wizard: WizardState, move: {
|
|
987
|
+
x: number;
|
|
988
|
+
y: number;
|
|
989
|
+
}, deltaTicks: number): Position;
|
|
990
|
+
/**
|
|
991
|
+
* Move a projectile in its current rotation direction.
|
|
992
|
+
*/
|
|
993
|
+
declare function moveProjectile(projectile: ProjectileState, deltaTicks: number): Position;
|
|
994
|
+
/**
|
|
995
|
+
* Clamp a position to the full arena bounds (0-860).
|
|
996
|
+
* No playfield clamping — wizards CAN walk/blink into lava.
|
|
997
|
+
*/
|
|
998
|
+
declare function clampToArena(position: Position, radius: number): Position;
|
|
999
|
+
/**
|
|
1000
|
+
* Check if a position is in the lava zone (outside the playfield).
|
|
1001
|
+
* Lava zones: [0, ARENA_MIN] and [ARENA_MAX, ARENA_SIZE] on each axis.
|
|
1002
|
+
*/
|
|
1003
|
+
declare function isInLava(position: Position, radius: number): boolean;
|
|
1004
|
+
/**
|
|
1005
|
+
* Resolve body collision between two wizards.
|
|
1006
|
+
* Pushes both apart equally so they don't overlap. Neither is blocked — they just can't stack.
|
|
1007
|
+
* Iterates until stable: wizard push → wall clamp → re-check overlap → repeat.
|
|
1008
|
+
*/
|
|
1009
|
+
declare function resolveWizardCollision(wizard1: WizardState, wizard2: WizardState): void;
|
|
1010
|
+
/**
|
|
1011
|
+
* Perform swept circle collision detection between a moving point (projectile) and a stationary circle (wizard).
|
|
1012
|
+
* Returns true if a collision occurred during the movement from oldPos to newPos.
|
|
1013
|
+
*/
|
|
1014
|
+
declare function sweptCircleCollision(oldPos: Position, newPos: Position, radius: number, targetPos: Position, targetRadius: number): boolean;
|
|
1015
|
+
|
|
1016
|
+
/**
|
|
1017
|
+
* Start casting a spell.
|
|
1018
|
+
* For missiles, applies warmup system (bonus for similar, penalty for switching).
|
|
1019
|
+
*/
|
|
1020
|
+
declare function startCast(wizard: WizardState, spell: 'missile' | 'shield' | 'blink', config?: MissileConfig): void;
|
|
1021
|
+
/**
|
|
1022
|
+
* Cancel the current cast.
|
|
1023
|
+
*/
|
|
1024
|
+
declare function cancelCast(wizard: WizardState): void;
|
|
1025
|
+
/**
|
|
1026
|
+
* Complete the current cast and trigger the spell effect.
|
|
1027
|
+
*/
|
|
1028
|
+
declare function completeCast(wizard: WizardState): void;
|
|
1029
|
+
/**
|
|
1030
|
+
* Update the shield channel state.
|
|
1031
|
+
*/
|
|
1032
|
+
declare function updateShield(wizard: WizardState, deltaTicks: number): void;
|
|
1033
|
+
/**
|
|
1034
|
+
* Calculate the current block percentage of a shield based on channel duration.
|
|
1035
|
+
*/
|
|
1036
|
+
declare function calculateShieldBlock(channelDurationTicks: number): number;
|
|
1037
|
+
/**
|
|
1038
|
+
* Apply damage to a wizard, considering shield mitigation.
|
|
1039
|
+
*
|
|
1040
|
+
* Shield blocks a percentage of damage based on channel duration:
|
|
1041
|
+
* - Fresh shield (0s): 90% block → 10% damage through
|
|
1042
|
+
* - Decayed shield: block % decreases over time (20%/sec)
|
|
1043
|
+
* - Minimum: 30% block → 70% damage through
|
|
1044
|
+
*
|
|
1045
|
+
* (Design doc lines 196-212)
|
|
1046
|
+
*/
|
|
1047
|
+
declare function applyDamage(wizard: WizardState, damage: number, _projectile?: ProjectileState): number;
|
|
1048
|
+
|
|
1049
|
+
/**
|
|
1050
|
+
* VIBEMANCER - MANUAL MATCH
|
|
1051
|
+
*
|
|
1052
|
+
* Tick-by-tick match runner used by manual play mode. Wraps the same
|
|
1053
|
+
* `tick()` primitive that `simulate()` uses, so stepping forward
|
|
1054
|
+
* produces the same trajectory as a batched `simulate()` with the
|
|
1055
|
+
* same seed.
|
|
1056
|
+
*
|
|
1057
|
+
* Extra capabilities over `simulate()`:
|
|
1058
|
+
* - step(N) advances N ticks at a time (default 1) — caller controls pacing
|
|
1059
|
+
* - replaceMissileAI(id, ai) hot-swaps a missile's AI mid-flight (used by
|
|
1060
|
+
* the missile-guide feature in manual play)
|
|
1061
|
+
* - setInvincible(wizardIndex, on) toggles damage immunity per wizard
|
|
1062
|
+
*/
|
|
1063
|
+
|
|
1064
|
+
interface ManualMatchOptions {
|
|
1065
|
+
seed?: number;
|
|
1066
|
+
spawnDistance?: number;
|
|
1067
|
+
maxTicks?: number;
|
|
1068
|
+
}
|
|
1069
|
+
interface StepResult {
|
|
1070
|
+
gameState: GameState;
|
|
1071
|
+
errors: BotError[];
|
|
1072
|
+
done: boolean;
|
|
1073
|
+
}
|
|
1074
|
+
declare class ManualMatch {
|
|
1075
|
+
private readonly wizard1AI;
|
|
1076
|
+
private readonly wizard2AI;
|
|
1077
|
+
private readonly seed;
|
|
1078
|
+
private readonly maxTicks;
|
|
1079
|
+
private readonly config;
|
|
1080
|
+
private wizards;
|
|
1081
|
+
private projectiles;
|
|
1082
|
+
private missileAIs;
|
|
1083
|
+
/** First-replacement originals for guided missiles. Used by restoreMissileAI. */
|
|
1084
|
+
private originalMissileAIs;
|
|
1085
|
+
private currentTick;
|
|
1086
|
+
private done;
|
|
1087
|
+
private deathTick;
|
|
1088
|
+
private allErrors;
|
|
1089
|
+
private history;
|
|
1090
|
+
private lastTickEvents;
|
|
1091
|
+
constructor(wizard1AI: WizardFunction, wizard2AI: WizardFunction, options?: ManualMatchOptions);
|
|
1092
|
+
/**
|
|
1093
|
+
* Advance the match by `count` ticks (default 1). Stops early if the
|
|
1094
|
+
* match completes mid-batch.
|
|
1095
|
+
*/
|
|
1096
|
+
step(count?: number): StepResult;
|
|
1097
|
+
/**
|
|
1098
|
+
* Hot-swap a missile's AI function. Used by the missile-guide feature
|
|
1099
|
+
* in manual play. The first replacement remembers the original AI so
|
|
1100
|
+
* `restoreMissileAI` can put it back. Subsequent replacements update
|
|
1101
|
+
* the active AI but leave the remembered original alone.
|
|
1102
|
+
*
|
|
1103
|
+
* No-op if the projectile id doesn't exist.
|
|
1104
|
+
*/
|
|
1105
|
+
replaceMissileAI(projectileId: string, ai: MissileFunction$1): void;
|
|
1106
|
+
/**
|
|
1107
|
+
* Restore a previously guided missile's original AI function.
|
|
1108
|
+
* No-op if the projectile id doesn't exist or was never guided.
|
|
1109
|
+
*/
|
|
1110
|
+
restoreMissileAI(projectileId: string): void;
|
|
1111
|
+
/**
|
|
1112
|
+
* Set the invincibility flag for a wizard. Invincible wizards take 0
|
|
1113
|
+
* damage from all sources.
|
|
1114
|
+
*/
|
|
1115
|
+
setInvincible(wizardIndex: 0 | 1, on: boolean): void;
|
|
1116
|
+
/**
|
|
1117
|
+
* Get the current game state from wizard-1's perspective.
|
|
1118
|
+
* Returned object is a deep clone — safe to mutate.
|
|
1119
|
+
*/
|
|
1120
|
+
getGameState(): GameState;
|
|
1121
|
+
getCurrentTick(): number;
|
|
1122
|
+
isComplete(): {
|
|
1123
|
+
done: boolean;
|
|
1124
|
+
winner: MatchWinner;
|
|
1125
|
+
};
|
|
1126
|
+
getResult(): SimulateResult;
|
|
1127
|
+
dispose(): void;
|
|
1128
|
+
private computeWinner;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
/**
|
|
1132
|
+
* VIBEMANCER - MISSILE TEMPLATES
|
|
1133
|
+
*
|
|
1134
|
+
* Reusable missile config + AI factories for manual play and bot examples.
|
|
1135
|
+
* Each factory clamps its inputs to rules.ts minimums and returns a pure
|
|
1136
|
+
* AI function that's safe to register with the engine.
|
|
1137
|
+
*/
|
|
1138
|
+
|
|
1139
|
+
interface BaseParams {
|
|
1140
|
+
damage: number;
|
|
1141
|
+
speed: number;
|
|
1142
|
+
duration: number;
|
|
1143
|
+
}
|
|
1144
|
+
interface TurningParams extends BaseParams {
|
|
1145
|
+
turnRate: number;
|
|
1146
|
+
}
|
|
1147
|
+
type StraightParams = BaseParams;
|
|
1148
|
+
type HomingParams = TurningParams;
|
|
1149
|
+
interface SpiralParams extends BaseParams {
|
|
1150
|
+
spiralRadius: number;
|
|
1151
|
+
spiralFreq: number;
|
|
1152
|
+
}
|
|
1153
|
+
interface SeekerParams extends TurningParams {
|
|
1154
|
+
minLockDistance: number;
|
|
1155
|
+
}
|
|
1156
|
+
interface MissileTemplate {
|
|
1157
|
+
config: MissileConfig;
|
|
1158
|
+
ai: MissileFunction$1;
|
|
1159
|
+
}
|
|
1160
|
+
/**
|
|
1161
|
+
* Fire-and-forget missile. No steering — flies in a straight line.
|
|
1162
|
+
*/
|
|
1163
|
+
declare function straightMissile(p: StraightParams): MissileTemplate;
|
|
1164
|
+
/**
|
|
1165
|
+
* Homing missile. Steers toward the enemy each tick.
|
|
1166
|
+
*/
|
|
1167
|
+
declare function homingMissile(p: HomingParams): MissileTemplate;
|
|
1168
|
+
/**
|
|
1169
|
+
* Spiral missile. Continuously orbits its current heading while advancing.
|
|
1170
|
+
* Uses missileState.remainingTicks as a deterministic phase counter so the
|
|
1171
|
+
* AI is fully pure (no closure state).
|
|
1172
|
+
*/
|
|
1173
|
+
declare function spiralMissile(p: SpiralParams): MissileTemplate;
|
|
1174
|
+
/**
|
|
1175
|
+
* Seeker missile. Homes toward the enemy, but only when farther away than
|
|
1176
|
+
* minLockDistance — closer than that, the missile coasts straight (so it
|
|
1177
|
+
* doesn't whirl around a target it's about to hit).
|
|
1178
|
+
*/
|
|
1179
|
+
declare function seekerMissile(p: SeekerParams): MissileTemplate;
|
|
1180
|
+
|
|
1181
|
+
/**
|
|
1182
|
+
* Calculate the distance between two points.
|
|
1183
|
+
*/
|
|
1184
|
+
declare function distanceTo(a: Position, b: Position): number;
|
|
1185
|
+
/**
|
|
1186
|
+
* Check if two points are within a certain range of each other.
|
|
1187
|
+
*/
|
|
1188
|
+
declare function inRange(a: Position, b: Position, range: number): boolean;
|
|
1189
|
+
|
|
1190
|
+
/**
|
|
1191
|
+
* Calculate the angle from one point to another in degrees.
|
|
1192
|
+
* 0° = right, 90° = down, 180° = left, 270° = up.
|
|
1193
|
+
*/
|
|
1194
|
+
declare function angleTo(from: Position, to: Position): number;
|
|
1195
|
+
/**
|
|
1196
|
+
* Normalize an angle to the 0-360 range.
|
|
1197
|
+
*/
|
|
1198
|
+
declare function normalizeAngle(angle: number): number;
|
|
1199
|
+
/**
|
|
1200
|
+
* Calculate the shortest difference between two angles (-180 to +180).
|
|
1201
|
+
*/
|
|
1202
|
+
declare function angleDiff(angleA: number, angleB: number): number;
|
|
1203
|
+
/**
|
|
1204
|
+
* Check if an angle is within a certain range of a target angle.
|
|
1205
|
+
*/
|
|
1206
|
+
declare function angleInRange(angle: number, target: number, range: number): boolean;
|
|
1207
|
+
|
|
1208
|
+
/**
|
|
1209
|
+
* Get the position after moving a certain distance in a direction.
|
|
1210
|
+
*/
|
|
1211
|
+
declare function moveInDirection(position: Position, angle: number, distance: number): Position;
|
|
1212
|
+
/**
|
|
1213
|
+
* Predict the position after N ticks given current velocity.
|
|
1214
|
+
*/
|
|
1215
|
+
declare function predictPosition(position: Position, velocity: Velocity, ticks: number): Position;
|
|
1216
|
+
/**
|
|
1217
|
+
* Calculate the intercept angle for a target moving at a certain velocity.
|
|
1218
|
+
* Returns null if no intercept solution exists.
|
|
1219
|
+
*/
|
|
1220
|
+
declare function interceptAngle(shooterPosition: Position, targetPosition: Position, targetVelocity: Velocity, projectileSpeed: number): number | null;
|
|
1221
|
+
|
|
1222
|
+
/**
|
|
1223
|
+
* Find the nearest entity from a list.
|
|
1224
|
+
*/
|
|
1225
|
+
declare function findNearest<T extends {
|
|
1226
|
+
position: Position;
|
|
1227
|
+
}>(from: Position, entities: T[]): T | null;
|
|
1228
|
+
/**
|
|
1229
|
+
* Find all entities within a certain range.
|
|
1230
|
+
*/
|
|
1231
|
+
declare function findInRange<T extends {
|
|
1232
|
+
position: Position;
|
|
1233
|
+
}>(from: Position, entities: T[], range: number): T[];
|
|
1234
|
+
/**
|
|
1235
|
+
* Sort entities by distance (closest first).
|
|
1236
|
+
*/
|
|
1237
|
+
declare function sortByDistance<T extends {
|
|
1238
|
+
position: Position;
|
|
1239
|
+
}>(from: Position, entities: T[]): T[];
|
|
1240
|
+
|
|
1241
|
+
/**
|
|
1242
|
+
* Seeded PRNG utilities for deterministic randomness.
|
|
1243
|
+
*
|
|
1244
|
+
* Each entity (wizard, missile) gets its own random sequence that:
|
|
1245
|
+
* - Is deterministic: same seed = same sequence
|
|
1246
|
+
* - Is isolated: one entity's calls don't affect another's
|
|
1247
|
+
* - Advances state: each call produces a different value
|
|
1248
|
+
*/
|
|
1249
|
+
/**
|
|
1250
|
+
* Combine two seeds into one using a simple hash.
|
|
1251
|
+
*/
|
|
1252
|
+
declare function hashCombine(a: number, b: number): number;
|
|
1253
|
+
/**
|
|
1254
|
+
* Advance the random state using a Linear Congruential Generator.
|
|
1255
|
+
* Parameters from glibc (widely tested).
|
|
1256
|
+
*/
|
|
1257
|
+
declare function nextRandom(state: number): number;
|
|
1258
|
+
/**
|
|
1259
|
+
* Create a seeded random number generator.
|
|
1260
|
+
* Returns a function that produces values in [0, 1) and advances internal state.
|
|
1261
|
+
*/
|
|
1262
|
+
declare function createRandom(seed: number): () => number;
|
|
1263
|
+
/**
|
|
1264
|
+
* Create a deterministic seed for an entity based on match seed, entity ID, and tick.
|
|
1265
|
+
* This ensures reproducibility: same match + same entity + same tick = same random sequence.
|
|
1266
|
+
*/
|
|
1267
|
+
declare function createEntitySeed(matchSeed: number, entityId: string, tick: number): number;
|
|
1268
|
+
|
|
1269
|
+
/**
|
|
1270
|
+
* VIBEMANCER - SPATIAL UTILITIES
|
|
1271
|
+
*
|
|
1272
|
+
* Direction vectors and arena bounds utilities.
|
|
1273
|
+
*/
|
|
1274
|
+
|
|
1275
|
+
/**
|
|
1276
|
+
* Normalize a vector to unit length.
|
|
1277
|
+
* Returns {x: 0, y: 0} for zero-length vectors.
|
|
1278
|
+
*/
|
|
1279
|
+
declare function normalize(vector: Position): Position;
|
|
1280
|
+
/**
|
|
1281
|
+
* Get normalized direction vector from one position toward another.
|
|
1282
|
+
* Returns {x: 0, y: 0} if positions are identical.
|
|
1283
|
+
*/
|
|
1284
|
+
declare function directionTo(from: Position, to: Position): Position;
|
|
1285
|
+
/**
|
|
1286
|
+
* Get normalized direction vector from one position away from another.
|
|
1287
|
+
* Returns {x: 0, y: 0} if positions are identical.
|
|
1288
|
+
*/
|
|
1289
|
+
declare function directionAway(from: Position, to: Position): Position;
|
|
1290
|
+
/**
|
|
1291
|
+
* Clamp a position to valid arena bounds.
|
|
1292
|
+
* Note: For wizard-specific clamping with radius, use clampToArena from physics.ts
|
|
1293
|
+
*/
|
|
1294
|
+
declare function clampPositionToArena(position: Position): Position;
|
|
1295
|
+
/**
|
|
1296
|
+
* Get the length/magnitude of a vector.
|
|
1297
|
+
*/
|
|
1298
|
+
declare function magnitude(vector: Position): number;
|
|
1299
|
+
|
|
1300
|
+
/**
|
|
1301
|
+
* VIBEMANCER - COMBAT UTILITIES
|
|
1302
|
+
*
|
|
1303
|
+
* Utilities for combat calculations.
|
|
1304
|
+
*/
|
|
1305
|
+
|
|
1306
|
+
/**
|
|
1307
|
+
* Get cast time in ticks for a missile configuration.
|
|
1308
|
+
* Applies the same clamps/validation as the engine before calculating,
|
|
1309
|
+
* so the result matches the actual cast time that will be used in-game.
|
|
1310
|
+
*
|
|
1311
|
+
* If lastMissileConfig is provided, includes warmup multiplier.
|
|
1312
|
+
* Pass undefined for first cast (full warmup) or null for base time only.
|
|
1313
|
+
*/
|
|
1314
|
+
declare function getMissileCastTime(config: MissileConfig, lastMissileConfig?: MissileConfig | null): number;
|
|
1315
|
+
/**
|
|
1316
|
+
* Calculate the position to aim at to hit a moving target.
|
|
1317
|
+
* Returns the intercept point where a missile would hit the target.
|
|
1318
|
+
*
|
|
1319
|
+
* @param targetPos - Current target position
|
|
1320
|
+
* @param targetVel - Target velocity (units per tick)
|
|
1321
|
+
* @param missileSpeed - Missile speed (units per tick)
|
|
1322
|
+
* @param myPos - Shooter position
|
|
1323
|
+
* @returns The position to aim at
|
|
1324
|
+
*/
|
|
1325
|
+
declare function getLeadPosition(targetPos: Position, targetVel: Velocity, missileSpeed: number, myPos: Position): Position;
|
|
1326
|
+
/**
|
|
1327
|
+
* Calculate optimal missile configuration based on target behavior.
|
|
1328
|
+
*/
|
|
1329
|
+
declare function getAdaptiveMissileConfig(targetVelocity: Position, distance: number): {
|
|
1330
|
+
speed: number;
|
|
1331
|
+
turnRate: number;
|
|
1332
|
+
damage: number;
|
|
1333
|
+
duration: number;
|
|
1334
|
+
};
|
|
1335
|
+
/**
|
|
1336
|
+
* Given a cast-time budget (in ticks) and a target distance, find the best
|
|
1337
|
+
* missile config that fits. Maximizes damage while ensuring the missile
|
|
1338
|
+
* can reach the target and finishes casting in time.
|
|
1339
|
+
*
|
|
1340
|
+
* Returns null if no useful missile fits in the budget.
|
|
1341
|
+
*
|
|
1342
|
+
* How it works: tries several speed/turnRate templates. For each, calculates
|
|
1343
|
+
* the minimum duration to reach `distance`, then solves the cast-time formula
|
|
1344
|
+
* for the maximum damage that fits within `budgetTicks`.
|
|
1345
|
+
*
|
|
1346
|
+
* If `lastMissileConfig` is provided, accounts for warmup bonus: similar
|
|
1347
|
+
* missiles cast faster, so more damage can fit in the same budget.
|
|
1348
|
+
*/
|
|
1349
|
+
/**
|
|
1350
|
+
* Simulate a missile trajectory to find the minimum duration (ticks) needed
|
|
1351
|
+
* to reach a target at the given distance. Works for all turnRate values:
|
|
1352
|
+
* positive (homing) and zero (straight).
|
|
1353
|
+
*
|
|
1354
|
+
* The simulation starts the missile aimed directly at the target and steps
|
|
1355
|
+
* through the trajectory tick by tick. For homing, the missile tracks the
|
|
1356
|
+
* target each tick matching the engine's steering physics.
|
|
1357
|
+
*/
|
|
1358
|
+
/**
|
|
1359
|
+
* Simulate a missile trajectory to find the minimum duration (ticks) needed
|
|
1360
|
+
* to reach a target at the given distance. Works for straight (turnRate=0)
|
|
1361
|
+
* and homing (turnRate>0) missiles.
|
|
1362
|
+
*
|
|
1363
|
+
* The missile starts aimed directly at the target at (dist, 0) and steps
|
|
1364
|
+
* through the trajectory tick by tick. For homing, the missile tracks the
|
|
1365
|
+
* target each tick matching the engine's steering physics.
|
|
1366
|
+
*
|
|
1367
|
+
* Returns 500 if the missile cannot reach the target within 500 ticks.
|
|
1368
|
+
*/
|
|
1369
|
+
declare function simulateMinDuration(speed: number, turnRateDeg: number, dist: number, collisionRadius?: number): number;
|
|
1370
|
+
declare function fitMissileToBudget(budgetTicks: number, distance: number, options?: {
|
|
1371
|
+
minTurnRate?: number;
|
|
1372
|
+
maxDamage?: number;
|
|
1373
|
+
lastMissileConfig?: MissileConfig;
|
|
1374
|
+
}): MissileConfig | null;
|
|
1375
|
+
/**
|
|
1376
|
+
* Fit a missile config that accounts for the enemy escaping during cast time.
|
|
1377
|
+
*
|
|
1378
|
+
* During casting, the caster moves at CASTING_MOVEMENT_MULT speed while
|
|
1379
|
+
* the enemy moves at full MOVEMENT_SPEED. This means the effective distance
|
|
1380
|
+
* at launch is larger than the current distance. This function iteratively
|
|
1381
|
+
* converges on a missile config whose range covers the escape distance.
|
|
1382
|
+
*
|
|
1383
|
+
* @param currentDistance - Current distance to enemy
|
|
1384
|
+
* @param budgetTicks - Maximum cast time budget in ticks
|
|
1385
|
+
* @param options - Same options as fitMissileToBudget, plus:
|
|
1386
|
+
* - enemyApproaching: if true, enemy is moving toward caster (reduces escape)
|
|
1387
|
+
* - distanceBuffer: flat units added to target distance for safety margin (default 20)
|
|
1388
|
+
* - maxIterations: convergence iterations (default 5)
|
|
1389
|
+
*/
|
|
1390
|
+
declare function fitMissileForEscapingTarget(currentDistance: number, budgetTicks: number, options?: {
|
|
1391
|
+
minTurnRate?: number;
|
|
1392
|
+
maxDamage?: number;
|
|
1393
|
+
lastMissileConfig?: MissileConfig;
|
|
1394
|
+
enemyApproaching?: boolean;
|
|
1395
|
+
distanceBuffer?: number;
|
|
1396
|
+
maxIterations?: number;
|
|
1397
|
+
}): MissileConfig | null;
|
|
1398
|
+
|
|
1399
|
+
/**
|
|
1400
|
+
* Get a seeded random number generator. Returns a function that produces
|
|
1401
|
+
* deterministic values in [0, 1) — same seed + same tick = same sequence.
|
|
1402
|
+
* Use this instead of Math.random() so replays are deterministic.
|
|
1403
|
+
*/
|
|
1404
|
+
declare function useRandom(): () => number;
|
|
1405
|
+
/**
|
|
1406
|
+
* Get your current health (0-60). Wizard dies at 0.
|
|
1407
|
+
*/
|
|
1408
|
+
declare function useHealth(): number;
|
|
1409
|
+
/**
|
|
1410
|
+
* Get your current position as {x, y} in world coordinates (0-800).
|
|
1411
|
+
* Position is clamped to [5, 795] (arena bounds minus wizard radius).
|
|
1412
|
+
*/
|
|
1413
|
+
declare function usePosition(): Position;
|
|
1414
|
+
/**
|
|
1415
|
+
* Get your current velocity as {x, y} in units/tick.
|
|
1416
|
+
* Max magnitude is 1 u/t when idle, 0.5 u/t when casting, 0 when channeling shield.
|
|
1417
|
+
*/
|
|
1418
|
+
declare function useVelocity(): Velocity;
|
|
1419
|
+
/**
|
|
1420
|
+
* Get your current status:
|
|
1421
|
+
* - 'idle': free to act
|
|
1422
|
+
* - 'casting': casting a spell (missile or blink). Can move at 50% speed.
|
|
1423
|
+
* - 'channeling': channeling shield. Cannot move. Cancel anytime with cancel().
|
|
1424
|
+
* - 'gcd_locked': global cooldown after spell. Can move at full speed, but cannot cast.
|
|
1425
|
+
*/
|
|
1426
|
+
declare function useStatus(): 'idle' | 'casting' | 'channeling' | 'gcd_locked';
|
|
1427
|
+
/**
|
|
1428
|
+
* Get ticks until you can start a new spell.
|
|
1429
|
+
*
|
|
1430
|
+
* Returns 0 when idle or channeling (shield can be canceled immediately).
|
|
1431
|
+
* During casting: remaining cast ticks. During GCD: remaining GCD ticks.
|
|
1432
|
+
*
|
|
1433
|
+
* Note: 100 ticks = 1 second.
|
|
1434
|
+
*/
|
|
1435
|
+
declare function useTicksUntilReady(): number;
|
|
1436
|
+
/**
|
|
1437
|
+
* Get current shield block multiplier.
|
|
1438
|
+
*
|
|
1439
|
+
* Returns 0 if not channeling shield.
|
|
1440
|
+
* Returns 0.3-0.9 if channeling: starts at 0.9 (blocks 90%), decays by 0.2/sec,
|
|
1441
|
+
* minimum 0.3 (blocks 30%). The remaining damage gets through:
|
|
1442
|
+
* actualDamage = incomingDamage × (1 - shieldStrength).
|
|
1443
|
+
*/
|
|
1444
|
+
declare function useShieldStrength(): number;
|
|
1445
|
+
/**
|
|
1446
|
+
* Get all projectiles currently in flight (yours and enemy's).
|
|
1447
|
+
* Used for blink safety calculations and threat analysis.
|
|
1448
|
+
*/
|
|
1449
|
+
declare function useProjectiles(): ProjectileState[];
|
|
1450
|
+
/**
|
|
1451
|
+
* Get the config of the last missile you fired, or undefined if none fired yet.
|
|
1452
|
+
* Used for the warmup system: consecutive similar missiles cast faster.
|
|
1453
|
+
*/
|
|
1454
|
+
declare function useLastMissileConfig(): MissileConfig | undefined;
|
|
1455
|
+
/**
|
|
1456
|
+
* Get blink cooldown remaining in ticks. Returns 0 if ready to blink.
|
|
1457
|
+
*
|
|
1458
|
+
* Cooldown scales with distance used:
|
|
1459
|
+
* - 100 units → ~100 ticks (1s)
|
|
1460
|
+
* - 300 units (max range) → 2000 ticks (20s)
|
|
1461
|
+
*
|
|
1462
|
+
* Note: 100 ticks = 1 second.
|
|
1463
|
+
*/
|
|
1464
|
+
declare function useBlinkCooldown(): number;
|
|
1465
|
+
/**
|
|
1466
|
+
* Get currently casting spell, or null if not casting.
|
|
1467
|
+
* Returns 'missile', 'shield', or 'blink'.
|
|
1468
|
+
*/
|
|
1469
|
+
declare function useCastingSpell(): 'missile' | 'shield' | 'blink' | null;
|
|
1470
|
+
/**
|
|
1471
|
+
* Get cast progress as {current, total} in ticks, or null if not casting.
|
|
1472
|
+
*
|
|
1473
|
+
* current = ticks elapsed, total = ticks needed. When current >= total, spell fires.
|
|
1474
|
+
* Note: 100 ticks = 1 second.
|
|
1475
|
+
*/
|
|
1476
|
+
declare function useCastProgress(): {
|
|
1477
|
+
current: number;
|
|
1478
|
+
total: number;
|
|
1479
|
+
} | null;
|
|
1480
|
+
/**
|
|
1481
|
+
* Get enemy wizard state.
|
|
1482
|
+
*
|
|
1483
|
+
* Returns position, velocity, health, status, casting spell, and shield strength.
|
|
1484
|
+
* Note: you cannot see the enemy's missile configs or exact cooldown timers —
|
|
1485
|
+
* only their status and what's visible on the field.
|
|
1486
|
+
*/
|
|
1487
|
+
declare function useEnemy(): EnemyState;
|
|
1488
|
+
/**
|
|
1489
|
+
* Get all your active (in-flight) projectiles.
|
|
1490
|
+
* Each has position, rotation (degrees), speed (u/t), turnRate, remainingTicks.
|
|
1491
|
+
*/
|
|
1492
|
+
declare function useMyProjectiles(): ProjectileState[];
|
|
1493
|
+
/**
|
|
1494
|
+
* Get total damage you've dealt this match.
|
|
1495
|
+
*/
|
|
1496
|
+
declare function useDamageDealt(): number;
|
|
1497
|
+
/**
|
|
1498
|
+
* Get total damage you've taken this match.
|
|
1499
|
+
*/
|
|
1500
|
+
declare function useDamageTaken(): number;
|
|
1501
|
+
/**
|
|
1502
|
+
* Get the tick number when you last took damage. Returns 0 if never hit.
|
|
1503
|
+
* Compare with useTick() to get ticks since last hit.
|
|
1504
|
+
*/
|
|
1505
|
+
declare function useLastHitTick(): number;
|
|
1506
|
+
/**
|
|
1507
|
+
* Get arena dimensions. Default: {width: 800, height: 800}.
|
|
1508
|
+
* Wizards are clamped to [5, 795] on each axis (radius = 5).
|
|
1509
|
+
*/
|
|
1510
|
+
declare function useArenaSize(): {
|
|
1511
|
+
width: number;
|
|
1512
|
+
height: number;
|
|
1513
|
+
};
|
|
1514
|
+
/**
|
|
1515
|
+
* Get current game tick (starts at 0, increments each tick).
|
|
1516
|
+
* 100 ticks = 1 second. Match ends at 30,000 ticks (5 minutes).
|
|
1517
|
+
*/
|
|
1518
|
+
declare function useTick(): number;
|
|
1519
|
+
/**
|
|
1520
|
+
* Get analyzed threats from all incoming enemy projectiles.
|
|
1521
|
+
* Sorted by ticksToImpact (soonest first). Only includes missiles within 500 units
|
|
1522
|
+
* or that are predicted to hit.
|
|
1523
|
+
*
|
|
1524
|
+
* Each threat includes:
|
|
1525
|
+
* - ticksToImpact: ticks until hit (Infinity if will miss)
|
|
1526
|
+
* - willHit: true if missile hits your current position
|
|
1527
|
+
* - canDodgeLeft/Right: whether strafing perpendicular to missile heading works
|
|
1528
|
+
* - canOutrun: whether moving away from missile escapes it
|
|
1529
|
+
* - bestDodgeDirection: {x, y} unit vector to dodge optimally, or null if undodgeable
|
|
1530
|
+
* - canBlockInTime: whether you can raise shield before impact
|
|
1531
|
+
* - ticksToStartShield: when to START channeling shield to block in time
|
|
1532
|
+
*/
|
|
1533
|
+
declare function useThreats(): AnalyzedThreat[];
|
|
1534
|
+
/**
|
|
1535
|
+
* Get the most imminent threat, or null if no threats.
|
|
1536
|
+
* Shorthand for useThreats()[0].
|
|
1537
|
+
*/
|
|
1538
|
+
declare function useClosestThreat(): AnalyzedThreat | null;
|
|
1539
|
+
/**
|
|
1540
|
+
* Get your missiles analyzed from the enemy's perspective.
|
|
1541
|
+
* Useful to predict when enemy will shield/dodge your attacks.
|
|
1542
|
+
*/
|
|
1543
|
+
declare function useMyThreatsToEnemy(): AnalyzedThreat[];
|
|
1544
|
+
|
|
1545
|
+
/**
|
|
1546
|
+
* VIBEMANCER - THREAT ANALYSIS
|
|
1547
|
+
*
|
|
1548
|
+
* Pre-computes threat information for incoming projectiles.
|
|
1549
|
+
* This handles the "subconscious" perception of missile trajectories.
|
|
1550
|
+
*/
|
|
1551
|
+
|
|
1552
|
+
/**
|
|
1553
|
+
* Analyze all threats from enemy projectiles.
|
|
1554
|
+
*
|
|
1555
|
+
* @param myPos - Current position of the wizard
|
|
1556
|
+
* @param projectiles - All projectiles in the game
|
|
1557
|
+
* @param myProjectiles - Only the bot's own projectiles (used for filtering)
|
|
1558
|
+
* @param ticksUntilReady - Ticks until wizard can start a new action
|
|
1559
|
+
* @returns Array of analyzed threats sorted by ticksToImpact (soonest first)
|
|
1560
|
+
*/
|
|
1561
|
+
declare function analyzeThreats(myPos: Position, projectiles: ProjectileState[], myProjectiles: ProjectileState[], ticksUntilReady: number): AnalyzedThreat[];
|
|
1562
|
+
|
|
1563
|
+
/**
|
|
1564
|
+
* VIBEMANCER - ACTION BUILDERS
|
|
1565
|
+
*
|
|
1566
|
+
* Fluent API for constructing bot actions with type-safe chaining.
|
|
1567
|
+
*
|
|
1568
|
+
* UNITS REFERENCE (100 ticks = 1 second):
|
|
1569
|
+
* Position: absolute world coordinates, 0-800 on each axis (800×800 arena)
|
|
1570
|
+
* Movement: direction vector, magnitude clamped to max 1 (speed in [0, 1])
|
|
1571
|
+
* Speed: units per tick (player moves at 1 unit/tick = 100 units/sec)
|
|
1572
|
+
* Duration: ticks (divide by 100 for seconds)
|
|
1573
|
+
* Angles: degrees (0°=right, 90°=down, 180°=left, 270°=up)
|
|
1574
|
+
* Damage: raw HP removed on hit (wizard has 60 HP)
|
|
1575
|
+
* Turn rate: degrees per tick the missile can rotate
|
|
1576
|
+
*/
|
|
1577
|
+
|
|
1578
|
+
/**
|
|
1579
|
+
* Channel a shield that blocks incoming damage.
|
|
1580
|
+
*
|
|
1581
|
+
* Starts at 90% block, decays by 20% per second, minimum 30%.
|
|
1582
|
+
* Takes 20 ticks (0.2s) to activate. Movement is disabled while channeling.
|
|
1583
|
+
* Cancel anytime with cancel(). Triggers 100-tick (1s) GCD after cancel.
|
|
1584
|
+
*
|
|
1585
|
+
* Can chain .move() — movement applies during the 20-tick cast, NOT during channel.
|
|
1586
|
+
*
|
|
1587
|
+
* @example
|
|
1588
|
+
* return shield(); // shield and stay still
|
|
1589
|
+
* return shield().move(1, 0); // move right while cast starts
|
|
1590
|
+
*/
|
|
1591
|
+
declare function shield(): ActionBuilder;
|
|
1592
|
+
/**
|
|
1593
|
+
* Cast a missile spell.
|
|
1594
|
+
*
|
|
1595
|
+
* Cast time scales with damage, speed, duration, and turn rate — bigger missiles
|
|
1596
|
+
* take longer to cast. While casting you move at 50% speed. After firing, 100-tick
|
|
1597
|
+
* (1s) GCD before next spell.
|
|
1598
|
+
*
|
|
1599
|
+
* Repeated similar missiles cast 20% faster (warmup bonus). Switching styles
|
|
1600
|
+
* incurs a 20% penalty.
|
|
1601
|
+
*
|
|
1602
|
+
* Can chain .move() for simultaneous movement while casting.
|
|
1603
|
+
*
|
|
1604
|
+
* @param config - Missile stats:
|
|
1605
|
+
* - damage: HP removed on hit (1-60 typical). Also sets hitbox: radius = 2 + 0.1×damage.
|
|
1606
|
+
* - speed: units/tick (min 1.5). Player moves at 1 u/t, so 5 = 5× player speed.
|
|
1607
|
+
* - duration: ticks the missile lives (min 10). Range ≈ speed × duration.
|
|
1608
|
+
* - turnRate: degrees/tick of homing (0 = straight line, 3 = moderate homing, 5+ = strong).
|
|
1609
|
+
* Negative = no homing + minor speed cost reduction.
|
|
1610
|
+
* @param ai - Called every tick to control missile steering. Use getMissileContext()
|
|
1611
|
+
* to read the missile's state (position, rotation, speed, etc.), the full game
|
|
1612
|
+
* state (worldState), and a seeded PRNG (random). Return turnToward(x, y) to home
|
|
1613
|
+
* toward a position, or flyStraight() to fly straight.
|
|
1614
|
+
* @param direction - Launch angle in degrees (0°=right, 90°=down, 180°=left, 270°=up).
|
|
1615
|
+
* Tip: use Math.atan2(dy, dx) * (180 / Math.PI) to aim at a target.
|
|
1616
|
+
*
|
|
1617
|
+
* @example
|
|
1618
|
+
* // Straight missile aimed at enemy
|
|
1619
|
+
* const angle = Math.atan2(dy, dx) * (180 / Math.PI);
|
|
1620
|
+
* return missile({damage: 15, speed: 6, duration: 200, turnRate: 0}, () => flyStraight(), angle);
|
|
1621
|
+
*
|
|
1622
|
+
* // Homing missile that tracks enemy
|
|
1623
|
+
* return missile(
|
|
1624
|
+
* {damage: 10, speed: 5, duration: 300, turnRate: 3},
|
|
1625
|
+
* () => {
|
|
1626
|
+
* const ctx = getMissileContext();
|
|
1627
|
+
* const enemy = ctx.worldState.enemies[0];
|
|
1628
|
+
* return enemy ? turnToward(enemy.position.x, enemy.position.y) : flyStraight();
|
|
1629
|
+
* },
|
|
1630
|
+
* angle,
|
|
1631
|
+
* );
|
|
1632
|
+
*/
|
|
1633
|
+
declare function missile(config: MissileConfig, ai: MissileFunction$1, direction: number): ActionBuilder;
|
|
1634
|
+
/**
|
|
1635
|
+
* Teleport to an absolute position on the arena.
|
|
1636
|
+
*
|
|
1637
|
+
* Max range: 300 units from current position (clamped by engine if further).
|
|
1638
|
+
* Cast time: 10 ticks (0.1s). Cooldown scales with distance:
|
|
1639
|
+
* - 100 units → 100 ticks (1s)
|
|
1640
|
+
* - 300 units → 2000 ticks (20s)
|
|
1641
|
+
*
|
|
1642
|
+
* Cannot chain .move() — blink IS the movement.
|
|
1643
|
+
*
|
|
1644
|
+
* @param x - Target X position (0-800, absolute world coordinate)
|
|
1645
|
+
* @param y - Target Y position (0-800, absolute world coordinate)
|
|
1646
|
+
*
|
|
1647
|
+
* @example
|
|
1648
|
+
* return blink(400, 400); // blink to center
|
|
1649
|
+
* return blink(enemy.position.x, enemy.position.y); // blink to enemy
|
|
1650
|
+
*/
|
|
1651
|
+
declare function blink(x: number, y: number): FinalAction;
|
|
1652
|
+
/**
|
|
1653
|
+
* Cancel current cast or channel (e.g. stop shielding to attack).
|
|
1654
|
+
*
|
|
1655
|
+
* Canceling a cast/channel triggers 100-tick (1s) GCD.
|
|
1656
|
+
* Can chain .move() for simultaneous movement.
|
|
1657
|
+
*
|
|
1658
|
+
* @example
|
|
1659
|
+
* return cancel().move(-1, 0); // cancel and dodge left
|
|
1660
|
+
*/
|
|
1661
|
+
declare function cancel(): ActionBuilder;
|
|
1662
|
+
/**
|
|
1663
|
+
* Move in a direction without casting any spell.
|
|
1664
|
+
*
|
|
1665
|
+
* This is a **direction vector**, not a target position. Values are in the
|
|
1666
|
+
* range [-1, 1] where 1 = full speed. Larger values (like raw position
|
|
1667
|
+
* deltas) are clamped to full speed automatically.
|
|
1668
|
+
*
|
|
1669
|
+
* @param x - Horizontal direction (positive = right, negative = left)
|
|
1670
|
+
* @param y - Vertical direction (positive = down, negative = up)
|
|
1671
|
+
*
|
|
1672
|
+
* @example
|
|
1673
|
+
* return move(1, 0); // move right at full speed
|
|
1674
|
+
* return move(dx / dist, dy / dist); // normalized unit vector = full speed
|
|
1675
|
+
* return move(enemy.position.x - myPos.x, enemy.position.y - myPos.y); // raw delta = clamped to full speed
|
|
1676
|
+
*/
|
|
1677
|
+
declare function move(x: number, y: number): FinalAction;
|
|
1678
|
+
/**
|
|
1679
|
+
* Do nothing — no action, no movement.
|
|
1680
|
+
*/
|
|
1681
|
+
declare function idle(): FinalAction;
|
|
1682
|
+
/**
|
|
1683
|
+
* Steer the missile toward a world position.
|
|
1684
|
+
* The engine clamps the actual turn to the missile's turnRate.
|
|
1685
|
+
*
|
|
1686
|
+
* @param x - Target X position (world coordinates)
|
|
1687
|
+
* @param y - Target Y position (world coordinates)
|
|
1688
|
+
*/
|
|
1689
|
+
declare function turnToward(x: number, y: number): MissileAction;
|
|
1690
|
+
/**
|
|
1691
|
+
* Steer the missile toward a specific angle (degrees).
|
|
1692
|
+
* The engine clamps the actual turn to the missile's turnRate.
|
|
1693
|
+
*
|
|
1694
|
+
* @param degrees - Target rotation in degrees (0=right, 90=down, 180=left, 270=up)
|
|
1695
|
+
*/
|
|
1696
|
+
declare function turnToAngle(degrees: number): MissileAction;
|
|
1697
|
+
/**
|
|
1698
|
+
* Fly straight — no steering this tick.
|
|
1699
|
+
*/
|
|
1700
|
+
declare function flyStraight(): MissileAction;
|
|
1701
|
+
/**
|
|
1702
|
+
* Extract MissileActions from a MissileAction.
|
|
1703
|
+
* Used by the engine to get the actual missile action.
|
|
1704
|
+
*/
|
|
1705
|
+
declare function extractMissileAction(action: MissileAction): MissileActions;
|
|
1706
|
+
/**
|
|
1707
|
+
* Extract WizardActions from a FinalAction.
|
|
1708
|
+
* Used by the engine to get the actual action.
|
|
1709
|
+
*/
|
|
1710
|
+
declare function extractAction(finalAction: FinalAction): WizardActions;
|
|
1711
|
+
|
|
1712
|
+
/**
|
|
1713
|
+
* VIBEMANCER - PARAMETER RUNTIME
|
|
1714
|
+
*
|
|
1715
|
+
* Provides the useParam() hook for bots to declare tunable parameters,
|
|
1716
|
+
* and the infrastructure for the optimizer to inject/discover parameter values.
|
|
1717
|
+
*
|
|
1718
|
+
* Design: Module-level state (JS is single-threaded, no race conditions).
|
|
1719
|
+
* The optimizer sets param values before running a bot, and clears them after.
|
|
1720
|
+
* During discovery, all useParam calls are recorded.
|
|
1721
|
+
*
|
|
1722
|
+
* ## useParam API
|
|
1723
|
+
*
|
|
1724
|
+
* ```typescript
|
|
1725
|
+
* // Basic: just a value, no optimizer config
|
|
1726
|
+
* const damage = useParam('damage', 15);
|
|
1727
|
+
*
|
|
1728
|
+
* // With range: optimizer searches value ± range (sliding window)
|
|
1729
|
+
* const distance = useParam('distance', 350, {range: 150, min: 0});
|
|
1730
|
+
*
|
|
1731
|
+
* // With fixed min/max: optimizer searches [min, max] (fixed bounds)
|
|
1732
|
+
* const damage = useParam('damage', 15, {min: 5, max: 25});
|
|
1733
|
+
*
|
|
1734
|
+
* // With all: range defines search radius, min/max clamp it
|
|
1735
|
+
* const fraction = useParam('fraction', 0.25, {range: 0.2, min: 0, max: 1});
|
|
1736
|
+
*
|
|
1737
|
+
* // With custom step count: optimizer tests 20 values instead of default 10
|
|
1738
|
+
* const distance = useParam('distance', 500, {range: 200, min: 0, steps: 20});
|
|
1739
|
+
* ```
|
|
1740
|
+
*
|
|
1741
|
+
* - **Arg 1** `name` — unique parameter name (must be consistent across ticks)
|
|
1742
|
+
* - **Arg 2** `value` — the actual value used in gameplay. This is what your bot
|
|
1743
|
+
* uses during matches. The optimizer script automatically updates this value.
|
|
1744
|
+
* - **Arg 3** `config` (optional) — optimizer search configuration:
|
|
1745
|
+
* - `range` — search radius: optimizer checks `value ± range`. The auto-optimizer
|
|
1746
|
+
* rewrites `value` after each run, so the search window slides automatically.
|
|
1747
|
+
* - `min` / `max` — hard constraints (e.g., distance ≥ 0, fraction ≤ 1).
|
|
1748
|
+
* When `range` is omitted, these define fixed search bounds (old-style).
|
|
1749
|
+
* - `steps` — how many evenly-spaced values the optimizer tests per pass (default: 10).
|
|
1750
|
+
* - `substeps` — steps to use in refinement passes (passes 2+). Set to 0 to freeze
|
|
1751
|
+
* after pass 1 (ideal for boolean params). Defaults to `steps` if not specified.
|
|
1752
|
+
* - At least `range` or both `min` + `max` must be provided.
|
|
1753
|
+
*
|
|
1754
|
+
* Without optimizer config, useParam simply returns `value` every tick.
|
|
1755
|
+
* With optimizer config, the offline optimizer script can override the value during search.
|
|
1756
|
+
*
|
|
1757
|
+
* ## Rules of Hooks
|
|
1758
|
+
* useParam follows the same rules as useState/useEffect/etc:
|
|
1759
|
+
* - Must be called at the top level of your bot function (not inside conditionals)
|
|
1760
|
+
* - Must be called in the same order every tick
|
|
1761
|
+
* - Violations are detected and throw errors
|
|
1762
|
+
*/
|
|
1763
|
+
|
|
1764
|
+
/**
|
|
1765
|
+
* Declaration of a tunable parameter, as discovered by the optimizer.
|
|
1766
|
+
*/
|
|
1767
|
+
interface ParamDeclaration {
|
|
1768
|
+
name: string;
|
|
1769
|
+
value: number;
|
|
1770
|
+
range?: number;
|
|
1771
|
+
min?: number;
|
|
1772
|
+
max?: number;
|
|
1773
|
+
steps: number;
|
|
1774
|
+
/** Steps to use in refinement passes (passes 2+). 0 = freeze after pass 1. Defaults to `steps`. */
|
|
1775
|
+
substeps?: number;
|
|
1776
|
+
}
|
|
1777
|
+
/**
|
|
1778
|
+
* Declare a tunable parameter. Returns the current value (optimizer-injected or the provided value).
|
|
1779
|
+
*
|
|
1780
|
+
* @param name - Unique parameter name (consistent across ticks)
|
|
1781
|
+
* @param value - The gameplay value. The auto-optimizer rewrites this in source code.
|
|
1782
|
+
* @param config - Optional optimizer search configuration
|
|
1783
|
+
* @returns The optimizer-injected value during optimization, or `value` during normal play
|
|
1784
|
+
*
|
|
1785
|
+
* @example
|
|
1786
|
+
* // Simple: no optimizer config
|
|
1787
|
+
* const damage = useParam('damage', 15);
|
|
1788
|
+
*
|
|
1789
|
+
* // With range: optimizer searches value ± range (sliding window)
|
|
1790
|
+
* const distance = useParam('distance', 350, {range: 150, min: 0});
|
|
1791
|
+
*
|
|
1792
|
+
* // With fixed min/max: optimizer searches [min, max]
|
|
1793
|
+
* const damage = useParam('damage', 15, {min: 5, max: 25});
|
|
1794
|
+
*/
|
|
1795
|
+
declare function useParam(name: string, value: number, config?: {
|
|
1796
|
+
range?: number;
|
|
1797
|
+
min?: number;
|
|
1798
|
+
max?: number;
|
|
1799
|
+
steps?: number;
|
|
1800
|
+
substeps?: number;
|
|
1801
|
+
}): number;
|
|
1802
|
+
/**
|
|
1803
|
+
* Inject parameter values for the next bot execution.
|
|
1804
|
+
* The wrapped bot will read these values via useParam().
|
|
1805
|
+
*/
|
|
1806
|
+
declare function setParamValues(values: Record<string, number>): void;
|
|
1807
|
+
/**
|
|
1808
|
+
* Clear injected parameter values. useParam() will return its provided value.
|
|
1809
|
+
*/
|
|
1810
|
+
declare function clearParamValues(): void;
|
|
1811
|
+
/**
|
|
1812
|
+
* Start discovery mode. All subsequent useParam() calls with optimizer config
|
|
1813
|
+
* will register their declarations.
|
|
1814
|
+
*/
|
|
1815
|
+
declare function startDiscovery(): void;
|
|
1816
|
+
/**
|
|
1817
|
+
* Stop discovery mode and return all discovered parameter declarations.
|
|
1818
|
+
*/
|
|
1819
|
+
declare function stopDiscovery(): ParamDeclaration[];
|
|
1820
|
+
/**
|
|
1821
|
+
* Wrap a bot function to inject specific parameter values.
|
|
1822
|
+
* The returned function sets params before calling the bot and clears them after.
|
|
1823
|
+
*/
|
|
1824
|
+
declare function wrapWithParams(bot: WizardFunction, params: Record<string, number>): WizardFunction;
|
|
1825
|
+
|
|
1826
|
+
/**
|
|
1827
|
+
* Bot: TargetDummy
|
|
1828
|
+
*
|
|
1829
|
+
* BEHAVIOR: Does absolutely nothing. No movement, no spells, no AI.
|
|
1830
|
+
*
|
|
1831
|
+
* NAMING RATIONALE: "Target Dummy" is universal MMO player vocabulary for the
|
|
1832
|
+
* practice objects found in capital cities. Every WoW/FFXIV player has beaten
|
|
1833
|
+
* on a target dummy to test DPS rotations. That's exactly what this bot is —
|
|
1834
|
+
* a punching bag for testing missile mechanics and baseline damage output.
|
|
1835
|
+
* Nobody calls them "training dummies"; the player term is always "target dummy."
|
|
1836
|
+
*
|
|
1837
|
+
* STANDALONE — no tier progression. It's a test fixture, not a combatant.
|
|
1838
|
+
*/
|
|
1839
|
+
declare function TargetDummy(): FinalAction;
|
|
1840
|
+
|
|
1841
|
+
/**
|
|
1842
|
+
* Bot: Rookie
|
|
1843
|
+
*
|
|
1844
|
+
* BEHAVIOR: Stands perfectly still and fires straight (non-homing) missiles at
|
|
1845
|
+
* the enemy. No movement, no dodging, no shielding. Knows one spell and uses
|
|
1846
|
+
* it on cooldown. The wizard equivalent of an FPS player who stands in the open
|
|
1847
|
+
* and holds left-click.
|
|
1848
|
+
*
|
|
1849
|
+
* NAMING RATIONALE: "Rookie" is the universal term for a first-timer who barely
|
|
1850
|
+
* knows what they're doing. This bot is a day-one player who learned how to cast
|
|
1851
|
+
* missile and nothing else. No movement, no defense, just raw "I press the button."
|
|
1852
|
+
* We considered "Noob" (more accurate) but Rookie is less abrasive while conveying
|
|
1853
|
+
* the same thing — a beginner who doesn't know any better.
|
|
1854
|
+
*
|
|
1855
|
+
* STANDALONE — no tier progression. Rookies either learn to play a real class
|
|
1856
|
+
* or quit. This bot represents the rock-bottom of "at least it shoots."
|
|
1857
|
+
*/
|
|
1858
|
+
declare function Rookie(): FinalAction;
|
|
1859
|
+
|
|
1860
|
+
/**
|
|
1861
|
+
* Bot: Critter
|
|
1862
|
+
*
|
|
1863
|
+
* BEHAVIOR: Picks random valid actions each tick — random movement, random spells,
|
|
1864
|
+
* random missile configs, random directions. Occasionally cancels its own casts.
|
|
1865
|
+
* Uses engine-provided seeded random for deterministic behavior. Useful for
|
|
1866
|
+
* finding edge cases in the engine, but completely useless in combat.
|
|
1867
|
+
*
|
|
1868
|
+
* NAMING RATIONALE: In WoW, critters are the 1-HP ambient mobs (rabbits, squirrels,
|
|
1869
|
+
* prairie dogs) that wander around doing nothing useful and die to literally anything.
|
|
1870
|
+
* This bot is the wizard equivalent — it flails around randomly and gets destroyed by
|
|
1871
|
+
* anyone with a plan. The word "Critter" immediately tells any gamer "this thing is
|
|
1872
|
+
* helpless and exists only to fill space."
|
|
1873
|
+
*
|
|
1874
|
+
* STANDALONE — no tier progression. Critters don't level up. However, a future
|
|
1875
|
+
* "Hogger" bot could be an elite critter: same chaotic spirit but actually dangerous
|
|
1876
|
+
* (like the famous WoW elite that wipes unprepared lowbies).
|
|
1877
|
+
*/
|
|
1878
|
+
declare function Critter(): FinalAction;
|
|
1879
|
+
|
|
1880
|
+
/**
|
|
1881
|
+
* Bot: Hogger
|
|
1882
|
+
*
|
|
1883
|
+
* BEHAVIOR: The elite critter. Chaotic and unpredictable but genuinely dangerous.
|
|
1884
|
+
* Randomly varies missile configs each cast (damage 7-15, speed 3-8, random homing),
|
|
1885
|
+
* moves erratically but still somewhat toward/away from the enemy, shields when
|
|
1886
|
+
* in real danger, and blinks unpredictably. The randomness makes Hogger hard to
|
|
1887
|
+
* predict — you never know if the next missile will be a slow tracker or a fast
|
|
1888
|
+
* snipe. Unlike Critter's pure randomness, Hogger has enough combat awareness
|
|
1889
|
+
* to actually win fights.
|
|
1890
|
+
*
|
|
1891
|
+
* NAMING RATIONALE: In WoW, Hogger is the iconic level 11 elite gnoll in Elwynn
|
|
1892
|
+
* Forest who infamously kills unprepared lowbies. He's technically a basic mob
|
|
1893
|
+
* but hits way harder than expected. This bot is the Critter that learned to
|
|
1894
|
+
* fight — still chaotic, still a bit dumb, but capable of ending you if you
|
|
1895
|
+
* underestimate it. "Hogger" is one of WoW's most recognizable references and
|
|
1896
|
+
* perfectly captures "deceptively dangerous chaos."
|
|
1897
|
+
*
|
|
1898
|
+
* STANDALONE — no tier progression. There's only one Hogger.
|
|
1899
|
+
*/
|
|
1900
|
+
declare function Hogger(): FinalAction;
|
|
1901
|
+
|
|
1902
|
+
/**
|
|
1903
|
+
* Bot: Doombringer
|
|
1904
|
+
*
|
|
1905
|
+
* BEHAVIOR: Fires a single maximum-damage homing missile with infinite budget.
|
|
1906
|
+
* No damage cap — goes for the biggest possible hit. Exists as a benchmark to
|
|
1907
|
+
* demonstrate why lower-damage + shield play is superior. Has basic shield
|
|
1908
|
+
* defense but no sophisticated tactics. One fat cast, one fat hit.
|
|
1909
|
+
*
|
|
1910
|
+
* STANDALONE — no tier progression. Benchmark/test bot.
|
|
1911
|
+
*/
|
|
1912
|
+
declare function Doombringer(): FinalAction;
|
|
1913
|
+
|
|
1914
|
+
declare function Turtle(): FinalAction;
|
|
1915
|
+
|
|
1916
|
+
/**
|
|
1917
|
+
* Bot: Sentinel
|
|
1918
|
+
*
|
|
1919
|
+
* BEHAVIOR: Stationary tank with last-moment shielding AND two-tier offense.
|
|
1920
|
+
* Like Turtle, never moves and shields at the last moment. Unlike Turtle,
|
|
1921
|
+
* fires bigger missiles (damage 20) when the safe window is large enough,
|
|
1922
|
+
* falling back to Turtle's fast missile (damage 12) when pressed.
|
|
1923
|
+
*
|
|
1924
|
+
* PROGRESSION LINE: Turtle → Sentinel → Golem
|
|
1925
|
+
* - Turtle (tier 1): Stationary, fixed missiles, reactive shield timing
|
|
1926
|
+
* - Sentinel (tier 2): Stationary, two-tier offense (big + fast missiles)
|
|
1927
|
+
* - Golem (tier 3): Immovable fortress, perfect shield timing
|
|
1928
|
+
*
|
|
1929
|
+
* TIER: 2 (enhanced Turtle)
|
|
1930
|
+
*/
|
|
1931
|
+
declare function Sentinel(): FinalAction;
|
|
1932
|
+
|
|
1933
|
+
/**
|
|
1934
|
+
* Bot: Golem
|
|
1935
|
+
*
|
|
1936
|
+
* BEHAVIOR: Stationary fortress with perfect shield timing and devastating
|
|
1937
|
+
* counterattacks during enemy vulnerability windows. Reads enemy cast/GCD
|
|
1938
|
+
* state to time punish missiles that land when the enemy can't shield.
|
|
1939
|
+
* Handles multi-missile volleys by holding shield through consecutive impacts.
|
|
1940
|
+
* Uses progressive cast-cancel thresholds for optimal damage trading.
|
|
1941
|
+
*
|
|
1942
|
+
* KEY IMPROVEMENTS OVER SENTINEL:
|
|
1943
|
+
* - Counterattack punish: fires during enemy cast/GCD recovery
|
|
1944
|
+
* - Multi-threat volley awareness: holds shield through consecutive hits
|
|
1945
|
+
* - Progressive cast-cancel: graduated damage thresholds
|
|
1946
|
+
* - Perfect shield timing: uses ticksToStartShield precisely
|
|
1947
|
+
*
|
|
1948
|
+
* PROGRESSION LINE: Turtle → Sentinel → Golem
|
|
1949
|
+
* TIER: 3 (elite Defensive line)
|
|
1950
|
+
*/
|
|
1951
|
+
declare function Golem(): FinalAction;
|
|
1952
|
+
|
|
1953
|
+
/**
|
|
1954
|
+
* Bot: Shadowblade
|
|
1955
|
+
*
|
|
1956
|
+
* BEHAVIOR: Melee assassin. Blinks to the enemy, then lands devastating point-blank
|
|
1957
|
+
* stab attacks (15 damage, 30u range, ~60 tick cast = 2-hit kill). Runs directly
|
|
1958
|
+
* at the enemy with minimal strafe, shields undodgeable threats. The entire
|
|
1959
|
+
* strategy is: get close, stab, kill. Simple and brutal.
|
|
1960
|
+
*
|
|
1961
|
+
* PROGRESSION LINE: Shadowblade → Nightblade → Voidblade
|
|
1962
|
+
* - Shadowblade (tier 1): Offensive blink, melee stabs, basic shield
|
|
1963
|
+
* - Nightblade (tier 2): + missile-aware blinks, adaptive stabs, timed defense
|
|
1964
|
+
* - Voidblade (tier 3): Future — perfect assassination timing, inescapable engages
|
|
1965
|
+
*
|
|
1966
|
+
* TIER: 1 (base)
|
|
1967
|
+
*/
|
|
1968
|
+
declare function Shadowblade(): FinalAction;
|
|
1969
|
+
|
|
1970
|
+
/**
|
|
1971
|
+
* Bot: Nightblade
|
|
1972
|
+
*
|
|
1973
|
+
* BEHAVIOR: Enhanced melee assassin. Same aggressive engagement as Shadowblade —
|
|
1974
|
+
* blinks directly to the enemy and stabs for 15 damage (2-hit kill). The tier 2
|
|
1975
|
+
* upgrade is PREEMPTIVE DEFENSE: Nightblade watches the enemy's cast bar and
|
|
1976
|
+
* shields before a point-blank missile is even launched. At melee range, missiles
|
|
1977
|
+
* arrive almost instantly after launch — too fast to react. Nightblade anticipates
|
|
1978
|
+
* the threat. Also has emergency blink and proper channeling management.
|
|
1979
|
+
*
|
|
1980
|
+
* PROGRESSION LINE: Shadowblade → Nightblade → Voidblade
|
|
1981
|
+
* - Shadowblade (tier 1): Offensive blink, melee stabs, basic shield (reactive only)
|
|
1982
|
+
* - Nightblade (tier 2): + preemptive shield vs enemy casts, emergency blink
|
|
1983
|
+
* - Voidblade (tier 3): Future — perfect assassination timing, inescapable engages
|
|
1984
|
+
*
|
|
1985
|
+
* TIER: 2 (enhanced Shadowblade)
|
|
1986
|
+
*/
|
|
1987
|
+
declare function Nightblade(): FinalAction;
|
|
1988
|
+
|
|
1989
|
+
/**
|
|
1990
|
+
* Bot: Voidblade
|
|
1991
|
+
*
|
|
1992
|
+
* BEHAVIOR: Reactive counter-puncher. Shields everything, then fires quick stabs
|
|
1993
|
+
* during enemy vulnerability windows (GCD/casting) when they can't shield back.
|
|
1994
|
+
* At melee range, the shield blocks ~90% of incoming damage while Voidblade's
|
|
1995
|
+
* counter-stabs land at full damage — winning through attrition.
|
|
1996
|
+
*
|
|
1997
|
+
* CORE LOOP (melee range):
|
|
1998
|
+
* 1. Enemy casts missile → Voidblade blink-dodges (100% avoid) or shields (90% block)
|
|
1999
|
+
* 2. Enemy enters GCD → Voidblade fires quick stab (lands unblocked)
|
|
2000
|
+
* 3. Voidblade enters GCD → enemy recovers → repeat
|
|
2001
|
+
*
|
|
2002
|
+
* KEY IMPROVEMENTS OVER NIGHTBLADE:
|
|
2003
|
+
* - Blink-dodge priority: avoids 100% of damage when blink available, shields as fallback
|
|
2004
|
+
* - Reads enemy vulnerability to time counter-stabs perfectly
|
|
2005
|
+
* - Cancel-into-defense: aborts own cast if enemy missile incoming
|
|
2006
|
+
* - Punish budget: sizes stabs to fit exactly in the vulnerability window
|
|
2007
|
+
*
|
|
2008
|
+
* PROGRESSION LINE: Shadowblade → Nightblade → Voidblade
|
|
2009
|
+
* TIER: 3 (elite Melee line)
|
|
2010
|
+
*/
|
|
2011
|
+
declare function Voidblade(): FinalAction;
|
|
2012
|
+
|
|
2013
|
+
/**
|
|
2014
|
+
* Bot: Bonemancer
|
|
2015
|
+
*
|
|
2016
|
+
* BEHAVIOR: Stands still and fires slow, homing missiles constantly. Every missile
|
|
2017
|
+
* tracks the enemy with turnRate 2 — they curve relentlessly toward the target.
|
|
2018
|
+
* No movement, no shields, just an unending stream of seeking projectiles. The
|
|
2019
|
+
* missiles are slow (speed 3) but long-lived (300 ticks) and will chase you across
|
|
2020
|
+
* the entire arena.
|
|
2021
|
+
*
|
|
2022
|
+
* NAMING RATIONALE: Named after Diablo 2's Bone Necromancer ("Bonemancer"), whose
|
|
2023
|
+
* signature spell Bone Spirit is a slow-moving, auto-tracking projectile that hunts
|
|
2024
|
+
* enemies relentlessly. That's exactly what this bot does — it stands in place and
|
|
2025
|
+
* spams seeking missiles. The homing behavior is the key identity: these aren't
|
|
2026
|
+
* aimed shots, they're heat-seeking spirits that chase you down. Every D2 player
|
|
2027
|
+
* knows the Bonemancer — it's one of the most iconic builds.
|
|
2028
|
+
*
|
|
2029
|
+
* PROGRESSION LINE: Bonemancer → Lich → Archlich
|
|
2030
|
+
* - Bonemancer (tier 1): Stationary, spams slow homing missiles
|
|
2031
|
+
* - Lich (tier 2): Future — enhanced homing with adaptive missiles and defense
|
|
2032
|
+
* - Archlich (tier 3): Future — master of tracking magic, undodgeable death swarm
|
|
2033
|
+
* The progression follows the D2 necromancer power fantasy: from bone apprentice
|
|
2034
|
+
* to undead overlord, each tier's missiles become harder to escape.
|
|
2035
|
+
*
|
|
2036
|
+
* TIER: 1 (base)
|
|
2037
|
+
*/
|
|
2038
|
+
declare function Bonemancer(): FinalAction;
|
|
2039
|
+
|
|
2040
|
+
/**
|
|
2041
|
+
* Bot: Lich
|
|
2042
|
+
*
|
|
2043
|
+
* BEHAVIOR: Homing missile specialist with strong-tracking adaptive missiles.
|
|
2044
|
+
* Uses fitMissileToBudget with minTurnRate 1.0 — higher than other bots (0.5) —
|
|
2045
|
+
* producing missiles with superior tracking at the cost of some damage/speed.
|
|
2046
|
+
* Strafing launches missiles from different angles, creating multi-angle pressure.
|
|
2047
|
+
*
|
|
2048
|
+
* Shields undodgeable/critical threats, emergency blinks. Cancels missile cast
|
|
2049
|
+
* only for lethal incoming damage.
|
|
2050
|
+
*
|
|
2051
|
+
* KEY DIFFERENCES FROM BONEMANCER:
|
|
2052
|
+
* - Bonemancer: stationary, no defense, fixed d=7/s=3/t=2/dur=300
|
|
2053
|
+
* - Lich: mobile, full defense, adaptive strong-tracking homing missiles
|
|
2054
|
+
*
|
|
2055
|
+
* PROGRESSION LINE: Bonemancer → Lich → Archlich
|
|
2056
|
+
* - Bonemancer (tier 1): Stationary, spams fixed slow homing missiles, no defense
|
|
2057
|
+
* - Lich (tier 2): Mobile + defense, adaptive strong-tracking missiles (minTurnRate 1.0)
|
|
2058
|
+
* - Archlich (tier 3): Future — converging web patterns, impossible to escape
|
|
2059
|
+
*
|
|
2060
|
+
* TIER: 2 (enhanced Bonemancer)
|
|
2061
|
+
*/
|
|
2062
|
+
declare function Lich(): FinalAction;
|
|
2063
|
+
|
|
2064
|
+
/**
|
|
2065
|
+
* Bot: Archlich
|
|
2066
|
+
*
|
|
2067
|
+
* BEHAVIOR: Lich's proven core (mobile homing specialist) plus vulnerability
|
|
2068
|
+
* exploitation. Defense, movement, and standard offense are identical to Lich.
|
|
2069
|
+
* The T3 advantage: when the enemy is locked in GCD/cast, fires fast straight
|
|
2070
|
+
* punish missiles that land during the vulnerability window.
|
|
2071
|
+
*
|
|
2072
|
+
* PROGRESSION LINE: Bonemancer → Lich → Archlich
|
|
2073
|
+
* TIER: 3 (elite Homing line)
|
|
2074
|
+
*/
|
|
2075
|
+
declare function Archlich(): FinalAction;
|
|
2076
|
+
|
|
2077
|
+
/**
|
|
2078
|
+
* Bot: Flamecaller
|
|
2079
|
+
*
|
|
2080
|
+
* BEHAVIOR: Long-range homing missile caster with fixed missile config.
|
|
2081
|
+
* Maintains 350u distance, strafes to dodge, and fires standard homing
|
|
2082
|
+
* missiles (d=10, s=5, t=1, dur=180). Shields undodgeable threats,
|
|
2083
|
+
* emergency blinks. A straightforward ranged caster that trades
|
|
2084
|
+
* consistency for adaptability.
|
|
2085
|
+
*
|
|
2086
|
+
* PROGRESSION LINE: Flamecaller → Pyromancer → Infernalist
|
|
2087
|
+
* - Flamecaller (tier 1): Fixed homing missiles, basic strafe and defense
|
|
2088
|
+
* - Pyromancer (tier 2): + adaptive fitting, cast canceling, smart fallbacks
|
|
2089
|
+
* - Infernalist (tier 3): Future — overwhelming adaptive fire
|
|
2090
|
+
*
|
|
2091
|
+
* TIER: 1 (base)
|
|
2092
|
+
*/
|
|
2093
|
+
declare function Flamecaller(): FinalAction;
|
|
2094
|
+
|
|
2095
|
+
/**
|
|
2096
|
+
* Bot: Pyromancer
|
|
2097
|
+
*
|
|
2098
|
+
* BEHAVIOR: Adaptive homing missile specialist at long range. Maintains 400u
|
|
2099
|
+
* distance, strafes to dodge, and uses fitMissileToBudget with minTurnRate 0.5
|
|
2100
|
+
* to fire the highest-damage homing missile that fits in the safe window.
|
|
2101
|
+
* Shields undodgeable threats, emergency blinks. A versatile ranged caster
|
|
2102
|
+
* that adapts its missiles to the situation.
|
|
2103
|
+
*
|
|
2104
|
+
* PROGRESSION LINE: Flamecaller → Pyromancer → Infernalist
|
|
2105
|
+
* - Flamecaller (tier 1): Fixed homing missiles, basic strafe and defense
|
|
2106
|
+
* - Pyromancer (tier 2): + adaptive fitting, cast canceling, smart fallbacks
|
|
2107
|
+
* - Infernalist (tier 3): Future — overwhelming adaptive fire
|
|
2108
|
+
*
|
|
2109
|
+
* TIER: 2 (enhanced Flamecaller)
|
|
2110
|
+
*/
|
|
2111
|
+
declare function Pyromancer(): FinalAction;
|
|
2112
|
+
|
|
2113
|
+
/**
|
|
2114
|
+
* Bot: Infernalist
|
|
2115
|
+
*
|
|
2116
|
+
* BEHAVIOR: Rapid-fire caster that exploits warmup bonus for accelerating DPS.
|
|
2117
|
+
* Fires consistent homing missiles to build warmup, punishes vulnerability windows
|
|
2118
|
+
* with warmup-boosted fast casts. Proactive blink kiting when enemy closes.
|
|
2119
|
+
*
|
|
2120
|
+
* KEY IMPROVEMENTS OVER PYROMANCER:
|
|
2121
|
+
* - Warmup exploitation: always passes lastMissileConfig for bonus
|
|
2122
|
+
* - Punish mode: straight missiles during enemy vulnerability
|
|
2123
|
+
* - Proactive blink kiting: monitors closing rate
|
|
2124
|
+
* - Progressive cast-cancel: graduated thresholds
|
|
2125
|
+
*
|
|
2126
|
+
* PROGRESSION LINE: Flamecaller → Pyromancer → Infernalist
|
|
2127
|
+
* TIER: 3 (elite Caster line)
|
|
2128
|
+
*/
|
|
2129
|
+
declare function Infernalist(): FinalAction;
|
|
2130
|
+
|
|
2131
|
+
/**
|
|
2132
|
+
* Bot: Spellshot
|
|
2133
|
+
*
|
|
2134
|
+
* BEHAVIOR: Uses interceptAngle to calculate where the enemy will be and fires
|
|
2135
|
+
* fast, non-homing missiles (speed 8, turnRate 0) along the predicted path.
|
|
2136
|
+
* Strafes at medium range (300-400), shields undodgeable threats, emergency
|
|
2137
|
+
* blinks when shield isn't available. The key mechanic is PREDICTION — these
|
|
2138
|
+
* missiles don't track, they go exactly where you calculated the enemy would be.
|
|
2139
|
+
*
|
|
2140
|
+
* NAMING RATIONALE: "Spellshot" — a spell that is a single, precisely aimed shot.
|
|
2141
|
+
* Like a sniper's "called shot" but magical. The defining feature is the intercept
|
|
2142
|
+
* calculation: this bot doesn't fire tracking missiles, it calculates the exact
|
|
2143
|
+
* angle needed to hit a moving target. "Shot" implies precision, singular impact,
|
|
2144
|
+
* and skill-based aiming — everything this bot is about.
|
|
2145
|
+
*
|
|
2146
|
+
* PROGRESSION LINE: Spellshot → Spelltracer → Spellseeker
|
|
2147
|
+
* - Spellshot (tier 1): Basic intercept prediction, non-homing missiles
|
|
2148
|
+
* - Spelltracer (tier 2): Future — predictive homing (missiles that lead AND track)
|
|
2149
|
+
* - Spellseeker (tier 3): Future — perfect prediction, multi-angle attacks
|
|
2150
|
+
* The naming progression: shot (single bullet) → tracer (bullet that tracks a path)
|
|
2151
|
+
* → seeker (actively hunts). Each tier adds more intelligence to the projectile,
|
|
2152
|
+
* evolving from "I calculate where you'll be" to "my missile calculates where you'll be."
|
|
2153
|
+
*
|
|
2154
|
+
* NOTE: A separate future archetype "Spellslinger" (volume-of-fire) is reserved
|
|
2155
|
+
* for a rapid-fire bot that prioritizes quantity over prediction.
|
|
2156
|
+
*
|
|
2157
|
+
* TIER: 1 (base)
|
|
2158
|
+
*/
|
|
2159
|
+
declare function Spellshot(): FinalAction;
|
|
2160
|
+
|
|
2161
|
+
/**
|
|
2162
|
+
* Bot: Spelltracer
|
|
2163
|
+
*
|
|
2164
|
+
* BEHAVIOR: Enhanced ranged sniper with adaptive missile fitting and intercept
|
|
2165
|
+
* prediction. Uses fitMissileToBudget to find the highest-damage fast missile
|
|
2166
|
+
* that fits the safe window, then fires it along the predicted intercept angle.
|
|
2167
|
+
* Maintains medium-long range (300-450), shields undodgeable threats with proper
|
|
2168
|
+
* timing, emergency blinks, and distance blinks when cornered. The key mechanic
|
|
2169
|
+
* is still PREDICTION — but now with adaptive damage optimization.
|
|
2170
|
+
*
|
|
2171
|
+
* PROGRESSION LINE: Spellshot → Spelltracer → Spellseeker
|
|
2172
|
+
* - Spellshot (tier 1): Fixed config intercept prediction, non-homing missiles
|
|
2173
|
+
* - Spelltracer (tier 2): + adaptive fitting, timed defense, distance management
|
|
2174
|
+
* - Spellseeker (tier 3): Future — perfect prediction, multi-angle attacks
|
|
2175
|
+
*
|
|
2176
|
+
* TIER: 2 (enhanced Spellshot)
|
|
2177
|
+
*/
|
|
2178
|
+
declare function Spelltracer(): FinalAction;
|
|
2179
|
+
|
|
2180
|
+
/**
|
|
2181
|
+
* Bot: Spellseeker
|
|
2182
|
+
*
|
|
2183
|
+
* BEHAVIOR: Elite sniper that uses intercept-aimed straight missiles during vulnerability
|
|
2184
|
+
* windows. Combines Spelltracer's adaptive fitting with precise lead-position aiming
|
|
2185
|
+
* and vulnerability exploitation. Straight punish missiles at sniper range are nearly
|
|
2186
|
+
* unavoidable. Proactive distance control via closing rate detection.
|
|
2187
|
+
*
|
|
2188
|
+
* KEY IMPROVEMENTS OVER SPELLTRACER:
|
|
2189
|
+
* - Intercept-aimed punish: getLeadPosition + straight missiles during vulnerability
|
|
2190
|
+
* - Proactive distance blink: monitors closing rate, blinks before danger zone
|
|
2191
|
+
* - Progressive cast-cancel: graduated damage thresholds
|
|
2192
|
+
* - Warmup exploitation: always passes lastMissileConfig
|
|
2193
|
+
*
|
|
2194
|
+
* PROGRESSION LINE: Spellshot → Spelltracer → Spellseeker
|
|
2195
|
+
* TIER: 3 (elite Sniper line)
|
|
2196
|
+
*/
|
|
2197
|
+
declare function Spellseeker(): FinalAction;
|
|
2198
|
+
|
|
2199
|
+
/**
|
|
2200
|
+
* Bot: Battlemage
|
|
2201
|
+
*
|
|
2202
|
+
* BEHAVIOR: Balanced mid-range duelist. Shields undodgeable threats, interrupts
|
|
2203
|
+
* enemy casts with quick missiles, saves blink for emergencies OR gap-closing.
|
|
2204
|
+
* Switches between quick (10 dmg, fast) and heavy (15 dmg, slow) missile configs
|
|
2205
|
+
* based on safety window and range. Will aggressively trade hits when health allows.
|
|
2206
|
+
* The unique trait is cast-interruption: fires quick missiles specifically when the
|
|
2207
|
+
* enemy is casting, punishing long cast times.
|
|
2208
|
+
*
|
|
2209
|
+
* PROGRESSION LINE: Battlemage → Warmage → Archmage
|
|
2210
|
+
* - Battlemage (tier 1): Quick/heavy fixed configs, cast interruption, basic defense
|
|
2211
|
+
* - Warmage (tier 2): Adaptive missile fitting (fitMissileToBudget), smarter attacks
|
|
2212
|
+
* - Archmage (tier 3): Future — supreme duelist, perfect tactical mastery
|
|
2213
|
+
*
|
|
2214
|
+
* TIER: 1 (base)
|
|
2215
|
+
*/
|
|
2216
|
+
declare function Battlemage(): FinalAction;
|
|
2217
|
+
|
|
2218
|
+
/**
|
|
2219
|
+
* Bot: Warmage
|
|
2220
|
+
*
|
|
2221
|
+
* BEHAVIOR: Enhanced Battlemage with adaptive missile fitting. Uses fitMissileToBudget
|
|
2222
|
+
* to maximize damage within safe attack windows instead of fixed quick/heavy configs.
|
|
2223
|
+
* Same close-range playstyle: shields undodgeable threats, blinks to close distance
|
|
2224
|
+
* or escape, aggressive hit-trading when health allows. The adaptive fitting means
|
|
2225
|
+
* every attack is optimized for the current situation — no wasted cast time.
|
|
2226
|
+
*
|
|
2227
|
+
* PROGRESSION LINE: Battlemage → Warmage → Archmage
|
|
2228
|
+
* - Battlemage (tier 1): Quick/heavy fixed configs, cast interruption, basic defense
|
|
2229
|
+
* - Warmage (tier 2): Adaptive missile fitting, optimized damage windows
|
|
2230
|
+
* - Archmage (tier 3): Future — supreme duelist, perfect tactical mastery
|
|
2231
|
+
*
|
|
2232
|
+
* TIER: 2 (enhanced Battlemage)
|
|
2233
|
+
*/
|
|
2234
|
+
declare function Warmage(): FinalAction;
|
|
2235
|
+
|
|
2236
|
+
/**
|
|
2237
|
+
* Bot: Archmage
|
|
2238
|
+
*
|
|
2239
|
+
* BEHAVIOR: Versatile duelist that adapts missile choice based on distance and HP.
|
|
2240
|
+
* Close range → straight missiles (no turn cost = more damage). Mid/far range → homing.
|
|
2241
|
+
* Uses dual-blink aggressively (gap-close during vulnerability, escape when trade is bad).
|
|
2242
|
+
* HP-aware: ahead → aggressive close range; behind → defensive ranged kiting.
|
|
2243
|
+
*
|
|
2244
|
+
* KEY IMPROVEMENTS OVER WARMAGE:
|
|
2245
|
+
* - Range-adaptive missiles: straight close, homing far
|
|
2246
|
+
* - Vulnerability-timed blinks: gap-close during enemy cast/GCD
|
|
2247
|
+
* - HP-aware aggression: adjusts distance + risk tolerance based on HP differential
|
|
2248
|
+
* - Progressive cast-cancel: graduated thresholds
|
|
2249
|
+
*
|
|
2250
|
+
* PROGRESSION LINE: Battlemage → Warmage → Archmage
|
|
2251
|
+
* TIER: 3 (elite Duelist line)
|
|
2252
|
+
*/
|
|
2253
|
+
declare function Archmage(): FinalAction;
|
|
2254
|
+
|
|
2255
|
+
/**
|
|
2256
|
+
* Bot: Stormchaser
|
|
2257
|
+
*
|
|
2258
|
+
* BEHAVIOR: Fights aggressively while managing defense intelligently. Uses
|
|
2259
|
+
* two fixed missile configs (standard homing + quick attack) with predictive
|
|
2260
|
+
* homing missile AI (interceptAngle on the missile itself). Blink-dodges
|
|
2261
|
+
* incoming threats, shields when blink is on cooldown. Tight distance
|
|
2262
|
+
* management (350 units, ±30 band).
|
|
2263
|
+
*
|
|
2264
|
+
* PROGRESSION LINE: Stormchaser → Stormcaller → Stormforger
|
|
2265
|
+
* - Stormchaser (tier 1): Fixed missiles, predictive homing AI, blink-dodge
|
|
2266
|
+
* - Stormcaller (tier 2): + fitMissileToBudget, predictive homing, optimized damage
|
|
2267
|
+
* - Stormforger (tier 3): Future — supreme berserker, perfect aggression
|
|
2268
|
+
*
|
|
2269
|
+
* TIER: 1 (base)
|
|
2270
|
+
*/
|
|
2271
|
+
declare function Stormchaser(): FinalAction;
|
|
2272
|
+
|
|
2273
|
+
/**
|
|
2274
|
+
* Bot: Stormcaller
|
|
2275
|
+
*
|
|
2276
|
+
* BEHAVIOR: Enhanced Stormchaser with adaptive missile fitting (fitMissileToBudget)
|
|
2277
|
+
* AND predictive homing missiles. Combines aggressive fighting philosophy with
|
|
2278
|
+
* optimized damage output. Uses budget-based missile fitting to maximize damage
|
|
2279
|
+
* within safe windows. Falls back to quick missiles under pressure. Same smart
|
|
2280
|
+
* trade/shield decisions as Stormchaser but with better resource usage.
|
|
2281
|
+
*
|
|
2282
|
+
* PROGRESSION LINE: Stormchaser → Stormcaller → Stormforger
|
|
2283
|
+
* - Stormchaser (tier 1): Adaptive missiles, smart trading, aggressive defense
|
|
2284
|
+
* - Stormcaller (tier 2): + fitMissileToBudget, predictive homing, optimized damage
|
|
2285
|
+
* - Stormforger (tier 3): Future — supreme berserker, perfect aggression
|
|
2286
|
+
*
|
|
2287
|
+
* TIER: 2 (enhanced Stormchaser)
|
|
2288
|
+
*/
|
|
2289
|
+
declare function Stormcaller(): FinalAction;
|
|
2290
|
+
|
|
2291
|
+
/**
|
|
2292
|
+
* Bot: Stormforger
|
|
2293
|
+
*
|
|
2294
|
+
* BEHAVIOR: Enhanced Stormcaller with vulnerability exploitation. Takes the exact
|
|
2295
|
+
* Stormcaller foundation (adaptive missile fitting + predictive homing) and adds
|
|
2296
|
+
* a punish mode that fires fast straight missiles when the enemy is locked in
|
|
2297
|
+
* GCD or cast animation. During vulnerability windows, uses getLeadPosition for
|
|
2298
|
+
* accurate straight shots that arrive before the enemy can react.
|
|
2299
|
+
*
|
|
2300
|
+
* PROGRESSION LINE: Stormchaser → Stormcaller → Stormforger
|
|
2301
|
+
* - Stormchaser (tier 1): Adaptive missiles, smart trading, aggressive defense
|
|
2302
|
+
* - Stormcaller (tier 2): + fitMissileToBudget, predictive homing, optimized damage
|
|
2303
|
+
* - Stormforger (tier 3): + vulnerability exploitation, punish missiles during enemy GCD
|
|
2304
|
+
*
|
|
2305
|
+
* TIER: 3 (elite Berserker line)
|
|
2306
|
+
*/
|
|
2307
|
+
declare function Stormforger(): FinalAction;
|
|
2308
|
+
|
|
2309
|
+
/**
|
|
2310
|
+
* Bot: Spellspinner
|
|
2311
|
+
*
|
|
2312
|
+
* BEHAVIOR: Maintains medium range (350 units), strafes constantly to dodge
|
|
2313
|
+
* missiles, and fires homing missiles (damage 10, speed 4, turnRate 2). Heavy
|
|
2314
|
+
* emphasis on movement — 80% strafe intensity when not dodging, 100% when dodging.
|
|
2315
|
+
* Shields only undodgeable threats, emergency blinks when shield isn't available.
|
|
2316
|
+
* The constant circular strafing motion traces patterns like thread being spun.
|
|
2317
|
+
*
|
|
2318
|
+
* NAMING RATIONALE: Like a spider spinning a web of projectiles while circling its
|
|
2319
|
+
* prey. The constant strafing movement pattern traces circles — spinning thread
|
|
2320
|
+
* around the arena. "Spell" + "spinner" = a wizard who spins spells around the
|
|
2321
|
+
* battlefield. The kiting behavior (maintaining distance while attacking) creates
|
|
2322
|
+
* a web-like pattern of missiles and movement that traps opponents.
|
|
2323
|
+
*
|
|
2324
|
+
* PROGRESSION LINE: Spellspinner → Spellweaver → Spellbinder
|
|
2325
|
+
* - Spellspinner (tier 1): Fixed homing missiles, constant strafe, basic defense
|
|
2326
|
+
* - Spellweaver (tier 2): + adaptive missile fitting, more sophisticated patterns
|
|
2327
|
+
* - Spellbinder (tier 3): Future — inescapable web of magic, perfect distance control
|
|
2328
|
+
* The progression: spinner (raw thread) → weaver (creates patterns) → binder
|
|
2329
|
+
* (constrains and traps). Each tier's projectile web becomes harder to escape.
|
|
2330
|
+
*
|
|
2331
|
+
* TIER: 1 (base)
|
|
2332
|
+
*/
|
|
2333
|
+
declare function Spellspinner(): FinalAction;
|
|
2334
|
+
|
|
2335
|
+
/**
|
|
2336
|
+
* Bot: Spellweaver
|
|
2337
|
+
*
|
|
2338
|
+
* BEHAVIOR: Enhanced Spellspinner with adaptive missile fitting. Same medium-range
|
|
2339
|
+
* kiting playstyle — maintains distance, strafes heavily — but uses fitMissileToBudget
|
|
2340
|
+
* to maximize damage within safe attack windows. Always uses homing missiles since
|
|
2341
|
+
* kiting means enemies are always moving. More sophisticated than Spellspinner's
|
|
2342
|
+
* fixed damage/speed/turnRate configuration.
|
|
2343
|
+
*
|
|
2344
|
+
* NAMING RATIONALE: A weaver creates intricate patterns from raw thread. Where the
|
|
2345
|
+
* Spellspinner produces raw threads of magic (fixed missiles), the Spellweaver
|
|
2346
|
+
* combines them into optimized patterns (adaptive fitting). The name suggests
|
|
2347
|
+
* craftsmanship and sophistication — the same kiting web, but deliberately woven
|
|
2348
|
+
* rather than chaotically spun.
|
|
2349
|
+
*
|
|
2350
|
+
* PROGRESSION LINE: Spellspinner → Spellweaver → Spellbinder
|
|
2351
|
+
* - Spellspinner (tier 1): Fixed homing missiles, constant strafe, basic defense
|
|
2352
|
+
* - Spellweaver (tier 2): + adaptive missile fitting, more sophisticated patterns
|
|
2353
|
+
* - Spellbinder (tier 3): Future — inescapable web, perfect distance control
|
|
2354
|
+
*
|
|
2355
|
+
* TIER: 2 (enhanced Spellspinner)
|
|
2356
|
+
*/
|
|
2357
|
+
declare function Spellweaver(): FinalAction;
|
|
2358
|
+
|
|
2359
|
+
/**
|
|
2360
|
+
* Bot: Spellbinder
|
|
2361
|
+
*
|
|
2362
|
+
* BEHAVIOR: Enhanced Spellweaver with vulnerability exploitation. Same medium-range
|
|
2363
|
+
* kiting playstyle — maintains distance, strafes heavily, uses fitMissileToBudget
|
|
2364
|
+
* for adaptive homing missiles. The T3 upgrade adds a punish mode that fires fast
|
|
2365
|
+
* straight missiles timed to land while the enemy is locked in a cast or GCD,
|
|
2366
|
+
* when they cannot shield. Defense, movement, and standard offense are identical
|
|
2367
|
+
* to Spellweaver.
|
|
2368
|
+
*
|
|
2369
|
+
* NAMING RATIONALE: A binder constrains and locks down opponents. Where the
|
|
2370
|
+
* Spellweaver optimizes missile patterns (adaptive fitting), the Spellbinder
|
|
2371
|
+
* reads the enemy's state and punishes vulnerability windows — binding them
|
|
2372
|
+
* to their commitments with unavoidable damage.
|
|
2373
|
+
*
|
|
2374
|
+
* PROGRESSION LINE: Spellspinner → Spellweaver → Spellbinder
|
|
2375
|
+
* - Spellspinner (tier 1): Fixed homing missiles, constant strafe, basic defense
|
|
2376
|
+
* - Spellweaver (tier 2): + adaptive missile fitting, more sophisticated patterns
|
|
2377
|
+
* - Spellbinder (tier 3): + vulnerability punish mode with fast straight missiles
|
|
2378
|
+
*
|
|
2379
|
+
* TIER: 3 (elite Spellspinner line)
|
|
2380
|
+
*/
|
|
2381
|
+
declare function Spellbinder(): FinalAction;
|
|
2382
|
+
|
|
2383
|
+
/**
|
|
2384
|
+
* Bot: Hero
|
|
2385
|
+
*
|
|
2386
|
+
* BEHAVIOR: Adaptive duelist that classifies the opponent's range/missile style from
|
|
2387
|
+
* live visible signals (how close they hold / whether they're closing, and their active
|
|
2388
|
+
* missiles) and counters by adjusting engagement distance — KITE aggressors (close /
|
|
2389
|
+
* closing / fast-straight stabbers) and CLOSE on kiters/casters (far / homing missiles).
|
|
2390
|
+
* This opponent-classification layer sits on top of Archmage's HP-aware, range-adaptive
|
|
2391
|
+
* engine: close range → straight missiles (no turn cost = more damage), mid/far → homing,
|
|
2392
|
+
* aggressive dual-blink (gap-close during vulnerability, escape when a trade is bad), and
|
|
2393
|
+
* HP-aware aggression (ahead → fight closer, behind → kite at range).
|
|
2394
|
+
*
|
|
2395
|
+
* KEY IMPROVEMENTS OVER ARCHMAGE:
|
|
2396
|
+
* - Opponent classification: reads range/missile style and counters via distance offset
|
|
2397
|
+
* - Counters aggressors by kiting, counters kiters/casters by closing in
|
|
2398
|
+
*
|
|
2399
|
+
* INHERITED FROM ARCHMAGE:
|
|
2400
|
+
* - Range-adaptive missiles: straight close, homing far
|
|
2401
|
+
* - Vulnerability-timed blinks: gap-close during enemy cast/GCD
|
|
2402
|
+
* - HP-aware aggression: adjusts distance + risk tolerance based on HP differential
|
|
2403
|
+
* - Progressive cast-cancel: graduated thresholds
|
|
2404
|
+
*/
|
|
2405
|
+
declare function Hero(): FinalAction;
|
|
2406
|
+
|
|
2407
|
+
/**
|
|
2408
|
+
* VIBEMANCER - BOT REGISTRY
|
|
2409
|
+
*
|
|
2410
|
+
* Single source of truth for all bots, ordered from weakest to strongest.
|
|
2411
|
+
* Run the tournament test to determine the correct ordering.
|
|
2412
|
+
*
|
|
2413
|
+
* To reorder: run `npx vitest run tests/bots/tournament.test.ts`
|
|
2414
|
+
* and update the list below based on the results.
|
|
2415
|
+
*
|
|
2416
|
+
* BOT NAMING SCHEME (3-tier progression):
|
|
2417
|
+
*
|
|
2418
|
+
* | Group | Tier 1 (base) | Tier 2 (enhanced) | Tier 3 (elite) |
|
|
2419
|
+
* |------------|----------------|-------------------|-----------------|
|
|
2420
|
+
* | Defensive | Turtle | Sentinel | Golem |
|
|
2421
|
+
* | Duelist | Battlemage | Warmage | Archmage |
|
|
2422
|
+
* | Homing | Bonemancer | Lich | Archlich |
|
|
2423
|
+
* | Caster | Flamecaller | Pyromancer | Infernalist |
|
|
2424
|
+
* | Melee | Shadowblade | Nightblade | Voidblade |
|
|
2425
|
+
* | Sniper | Spellshot | Spelltracer | Spellseeker |
|
|
2426
|
+
* | Berserker | Stormchaser | Stormcaller | Stormforger |
|
|
2427
|
+
* | Kiter | Spellspinner | Spellweaver | Spellbinder |
|
|
2428
|
+
*
|
|
2429
|
+
* Standalone: TargetDummy, Critter, Hogger, Rookie, Doombringer, Hero
|
|
2430
|
+
* (Hero is the adaptive SHOWCASE — the #1 reference bot users build their own to beat.
|
|
2431
|
+
* Deliberately kept as a single isolated bot, NOT a tier line: writing adaptive bots is
|
|
2432
|
+
* the game, so the roster ships one exemplar and players make the rest.)
|
|
2433
|
+
* Reserved: Spellslinger (future volume-of-fire archetype)
|
|
2434
|
+
*
|
|
2435
|
+
*/
|
|
2436
|
+
|
|
2437
|
+
interface WizardEntry {
|
|
2438
|
+
name: string;
|
|
2439
|
+
ai: WizardFunction;
|
|
2440
|
+
description: string;
|
|
2441
|
+
tier?: number;
|
|
2442
|
+
group: string;
|
|
2443
|
+
}
|
|
2444
|
+
interface WizardGroup {
|
|
2445
|
+
label: string;
|
|
2446
|
+
bots: WizardEntry[];
|
|
2447
|
+
}
|
|
2448
|
+
/**
|
|
2449
|
+
* All bots organized by progression line.
|
|
2450
|
+
* Each group contains bots from the same archetype, ordered by tier.
|
|
2451
|
+
*/
|
|
2452
|
+
declare const BOT_GROUPS: WizardGroup[];
|
|
2453
|
+
declare const ALL_BOTS: WizardEntry[];
|
|
2454
|
+
|
|
2455
|
+
/**
|
|
2456
|
+
* VIBEMANCER - OPTIMIZER UTILITIES
|
|
2457
|
+
*
|
|
2458
|
+
* Core functions used by the offline parameter optimizer script.
|
|
2459
|
+
* These handle combo generation, range narrowing, and fight scoring.
|
|
2460
|
+
*
|
|
2461
|
+
* The optimizer uses multi-pass coordinate descent:
|
|
2462
|
+
* - Pass 1: Coarse search across the effective range for each param
|
|
2463
|
+
* - Pass 2+: Fine search zoomed into the neighborhood of the best result
|
|
2464
|
+
*
|
|
2465
|
+
* Parameters can define their search window two ways:
|
|
2466
|
+
* - `range`: sliding window centered on current value (value ± range)
|
|
2467
|
+
* - `min`/`max`: fixed bounds (old-style)
|
|
2468
|
+
*
|
|
2469
|
+
* Both can be combined: range defines the search radius, min/max clamp it.
|
|
2470
|
+
*/
|
|
2471
|
+
|
|
2472
|
+
/**
|
|
2473
|
+
* Compute the effective min/max search window for a parameter.
|
|
2474
|
+
*
|
|
2475
|
+
* - If `range` is set: window is `[value - range, value + range]`, clamped by optional min/max
|
|
2476
|
+
* - If only `min`/`max` are set: window is `[min, max]` directly
|
|
2477
|
+
* - If neither: returns `[value, value]` (no search)
|
|
2478
|
+
*/
|
|
2479
|
+
declare function getEffectiveRange(p: ParamDeclaration): {
|
|
2480
|
+
min: number;
|
|
2481
|
+
max: number;
|
|
2482
|
+
};
|
|
2483
|
+
/**
|
|
2484
|
+
* Generate evenly-spaced candidate values for a single parameter.
|
|
2485
|
+
*
|
|
2486
|
+
* Divides [min, max] into `steps` evenly-spaced values. For example,
|
|
2487
|
+
* min=0, max=100, steps=5 produces [0, 25, 50, 75, 100].
|
|
2488
|
+
*
|
|
2489
|
+
* @param min - Lower bound of search range
|
|
2490
|
+
* @param max - Upper bound of search range
|
|
2491
|
+
* @param steps - Number of evenly-spaced values to generate (minimum 2)
|
|
2492
|
+
* @returns Array of candidate values, sorted ascending
|
|
2493
|
+
*/
|
|
2494
|
+
declare function generateCandidates(min: number, max: number, steps: number): number[];
|
|
2495
|
+
/**
|
|
2496
|
+
* Generate all combinations (cartesian product) of candidate values for multiple params.
|
|
2497
|
+
*
|
|
2498
|
+
* For N params with S1, S2, ... SN steps each, produces S1 × S2 × ... × SN combinations.
|
|
2499
|
+
* Each combination is a Record<string, number> mapping param name to value.
|
|
2500
|
+
*
|
|
2501
|
+
* @param params - Parameter declarations with search ranges and step counts
|
|
2502
|
+
* @returns Array of all parameter combinations to evaluate
|
|
2503
|
+
*/
|
|
2504
|
+
declare function generateCombos(params: ParamDeclaration[]): Record<string, number>[];
|
|
2505
|
+
/**
|
|
2506
|
+
* Narrow parameter ranges around the best combo found in the previous pass.
|
|
2507
|
+
*
|
|
2508
|
+
* Centers each param on its best value and shrinks the search range to one gap width.
|
|
2509
|
+
* This provides finer resolution in subsequent passes. Hard min/max constraints are preserved.
|
|
2510
|
+
*
|
|
2511
|
+
* @param params - Original parameter declarations
|
|
2512
|
+
* @param best - Best parameter combination from the previous pass
|
|
2513
|
+
* @returns New parameter declarations with narrowed ranges for the next pass
|
|
2514
|
+
*/
|
|
2515
|
+
declare function narrowRange(params: ParamDeclaration[], best: Record<string, number>): ParamDeclaration[];
|
|
2516
|
+
/**
|
|
2517
|
+
* Score a FightResult from one bot's perspective.
|
|
2518
|
+
*
|
|
2519
|
+
* Returns a continuous score that provides gradient information beyond binary win/loss:
|
|
2520
|
+
* - Win: 3.0 base + up to 0.5 HP bonus (higher remaining HP = better)
|
|
2521
|
+
* - Draw: 1.0
|
|
2522
|
+
* - Loss: 0.0 base + up to 0.5 bonus for low enemy HP (closer fights = better)
|
|
2523
|
+
*
|
|
2524
|
+
* For a mirrored fight set (bot as wizard-1 AND wizard-2), call this twice
|
|
2525
|
+
* and sum the scores for a balanced evaluation.
|
|
2526
|
+
*
|
|
2527
|
+
* @param result - The fight result to score
|
|
2528
|
+
* @returns Continuous score in range [0, 3.5] per match
|
|
2529
|
+
*/
|
|
2530
|
+
declare function scoreFight(result: FightResult): number;
|
|
2531
|
+
/**
|
|
2532
|
+
* Score a FightResult from wizard-2's perspective.
|
|
2533
|
+
* Same scoring logic as scoreFight but with roles reversed.
|
|
2534
|
+
*
|
|
2535
|
+
* Use this for tournaments where both sides of a pairing need scoring.
|
|
2536
|
+
*/
|
|
2537
|
+
declare function scoreFightAsWizard2(result: FightResult): number;
|
|
2538
|
+
|
|
2539
|
+
/**
|
|
2540
|
+
* Bot-name filter — the single source of truth for which bot names are
|
|
2541
|
+
* disallowed. Called everywhere a bot name is served or displayed (matchmaker,
|
|
2542
|
+
* upload, handle/botname selectors, leaderboard, CLI) so the rule is consistent.
|
|
2543
|
+
*
|
|
2544
|
+
* Deliberately a pure function over stateless rules: nothing is written to the
|
|
2545
|
+
* bot, so adding a rule hides matching bots everywhere at once, and REMOVING a
|
|
2546
|
+
* rule brings them straight back — no data to migrate or un-flag. Editing the
|
|
2547
|
+
* lists below is a (rare) release; the bots themselves are never mutated.
|
|
2548
|
+
*
|
|
2549
|
+
* On a takedown (trademark / defamation / illegal content), add the exact name
|
|
2550
|
+
* to BANNED_EXACT, or a pattern to BANNED_PATTERNS for a family of names.
|
|
2551
|
+
*
|
|
2552
|
+
* FULL RUNBOOK (read this before banning anything): docs/BANNING_BOT_NAMES.md
|
|
2553
|
+
* — it explains the why, the exact-match semantics, and the release steps
|
|
2554
|
+
* required for the change to reach the (published-core) Cloud Functions.
|
|
2555
|
+
*/
|
|
2556
|
+
/**
|
|
2557
|
+
* Pure matcher — exported for testing. Returns true if `name` matches any of the
|
|
2558
|
+
* given exact names or patterns (case-insensitive, trimmed).
|
|
2559
|
+
*/
|
|
2560
|
+
declare function matchesAnyBanRule(name: string, exact: ReadonlySet<string>, patterns: readonly RegExp[]): boolean;
|
|
2561
|
+
/**
|
|
2562
|
+
* True if a bot name is disallowed. Call this at every surface that serves or
|
|
2563
|
+
* displays bot names; never persist the result onto the bot.
|
|
2564
|
+
*/
|
|
2565
|
+
declare function isBannedBotName(name: string): boolean;
|
|
2566
|
+
|
|
2567
|
+
/**
|
|
2568
|
+
* VIBEMANCER — BROWSER SANDBOX
|
|
2569
|
+
*
|
|
2570
|
+
* Provides Web Worker-based sandboxing for bot code execution in the browser.
|
|
2571
|
+
* Same compiled bundles as the isolated-vm sandbox (MatchSandbox), but runs
|
|
2572
|
+
* in a Web Worker instead of a V8 isolate.
|
|
2573
|
+
*
|
|
2574
|
+
* Architecture:
|
|
2575
|
+
* - Host: creates Worker from Blob URL, communicates via postMessage
|
|
2576
|
+
* - Worker: loads compiled bundle (sets globalThis.__fight/__simulate),
|
|
2577
|
+
* dispatches fight/simulate calls, posts results back
|
|
2578
|
+
*
|
|
2579
|
+
* Safety:
|
|
2580
|
+
* - Timeout via setTimeout + worker.terminate() catches infinite loops
|
|
2581
|
+
* - No memory limit (browser manages worker memory; worst case = tab crash)
|
|
2582
|
+
* - Prototype freeze prevents cross-bot sabotage (same banner as isolated-vm)
|
|
2583
|
+
* - No Node.js APIs available in Web Workers
|
|
2584
|
+
*
|
|
2585
|
+
* NOTE: This file has ZERO Node.js dependencies. It works in any JS environment.
|
|
2586
|
+
*/
|
|
2587
|
+
|
|
2588
|
+
/**
|
|
2589
|
+
* Minimal Worker interface for dependency injection.
|
|
2590
|
+
* Matches the browser Worker API subset we need.
|
|
2591
|
+
* For tests, a Node.js worker_threads adapter can implement this.
|
|
2592
|
+
*/
|
|
2593
|
+
interface WorkerLike {
|
|
2594
|
+
postMessage(data: unknown): void;
|
|
2595
|
+
terminate(): void;
|
|
2596
|
+
addEventListener(type: string, listener: (ev: unknown) => void): void;
|
|
2597
|
+
removeEventListener(type: string, listener: (ev: unknown) => void): void;
|
|
2598
|
+
}
|
|
2599
|
+
/**
|
|
2600
|
+
* Factory function that creates a WorkerLike from a JavaScript code string.
|
|
2601
|
+
* Default: creates a browser Web Worker via Blob URL.
|
|
2602
|
+
* Override in options.createWorker for testing with Node.js worker_threads.
|
|
2603
|
+
*/
|
|
2604
|
+
type WorkerFactory = (code: string) => {
|
|
2605
|
+
worker: WorkerLike;
|
|
2606
|
+
cleanup?: () => void;
|
|
2607
|
+
};
|
|
2608
|
+
interface BrowserSandboxOptions {
|
|
2609
|
+
/** Timeout in ms for fight/simulate calls (default: 30000). */
|
|
2610
|
+
timeoutMs?: number;
|
|
2611
|
+
/** Custom worker factory for dependency injection (testing). */
|
|
2612
|
+
createWorker?: WorkerFactory;
|
|
2613
|
+
}
|
|
2614
|
+
/**
|
|
2615
|
+
* Create the full worker script from a compiled match bundle.
|
|
2616
|
+
* Appends the message-handling bootstrap to the bundle IIFE.
|
|
2617
|
+
*/
|
|
2618
|
+
declare function createWorkerScript(bundle: string): string;
|
|
2619
|
+
/**
|
|
2620
|
+
* Browser-compatible sandboxed match runner using Web Workers.
|
|
2621
|
+
*
|
|
2622
|
+
* Same compiled bundles as MatchSandbox (isolated-vm), but runs in a
|
|
2623
|
+
* Web Worker instead. All fight/simulate calls are async (postMessage-based).
|
|
2624
|
+
*
|
|
2625
|
+
* Usage:
|
|
2626
|
+
* ```ts
|
|
2627
|
+
* // Bundle is compiled server-side or at build time (Node.js only)
|
|
2628
|
+
* const bundle = await MatchSandbox.compile(bot1, bot2);
|
|
2629
|
+
*
|
|
2630
|
+
* // Run in browser via Web Worker
|
|
2631
|
+
* const sandbox = await BrowserMatchSandbox.fromBundle(bundle);
|
|
2632
|
+
* const result = await sandbox.fight({ seed: 42 });
|
|
2633
|
+
* sandbox.dispose();
|
|
2634
|
+
* ```
|
|
2635
|
+
*/
|
|
2636
|
+
declare class BrowserMatchSandbox {
|
|
2637
|
+
private worker;
|
|
2638
|
+
private workerCleanup;
|
|
2639
|
+
private timeout;
|
|
2640
|
+
private disposed;
|
|
2641
|
+
private nextId;
|
|
2642
|
+
private pending;
|
|
2643
|
+
private messageHandler;
|
|
2644
|
+
private errorHandler;
|
|
2645
|
+
private constructor();
|
|
2646
|
+
/**
|
|
2647
|
+
* Create a browser sandbox from a pre-compiled bundle string.
|
|
2648
|
+
* The bundle should be the output of MatchSandbox.compile() (or equivalent IIFE
|
|
2649
|
+
* that sets globalThis.__fight and globalThis.__simulate).
|
|
2650
|
+
*/
|
|
2651
|
+
static fromBundle(bundle: string, options?: BrowserSandboxOptions): Promise<BrowserMatchSandbox>;
|
|
2652
|
+
/**
|
|
2653
|
+
* Wait for the worker to post {type: 'ready'}, then attach permanent handlers.
|
|
2654
|
+
*/
|
|
2655
|
+
private waitForReady;
|
|
2656
|
+
/**
|
|
2657
|
+
* Extract message data from a browser MessageEvent or raw Node.js data.
|
|
2658
|
+
*/
|
|
2659
|
+
private unwrapEvent;
|
|
2660
|
+
/**
|
|
2661
|
+
* Attach permanent message and error handlers for fight/simulate responses.
|
|
2662
|
+
*/
|
|
2663
|
+
private attachHandlers;
|
|
2664
|
+
/**
|
|
2665
|
+
* Run a full fight (10 matches: 5 spawn distances x 2 sides).
|
|
2666
|
+
* Returns a Promise because Worker communication is async.
|
|
2667
|
+
*/
|
|
2668
|
+
fight(options?: {
|
|
2669
|
+
seed?: number;
|
|
2670
|
+
maxTicks?: number;
|
|
2671
|
+
}): Promise<FightResult>;
|
|
2672
|
+
/**
|
|
2673
|
+
* Run a single simulation. Returns a Promise.
|
|
2674
|
+
*
|
|
2675
|
+
* @param options.params1 - useParam overrides for bot 1 (wizard-1)
|
|
2676
|
+
* @param options.params2 - useParam overrides for bot 2 (wizard-2)
|
|
2677
|
+
*/
|
|
2678
|
+
simulate(options?: {
|
|
2679
|
+
seed?: number;
|
|
2680
|
+
maxTicks?: number;
|
|
2681
|
+
spawnDistance?: number;
|
|
2682
|
+
skipHistory?: boolean;
|
|
2683
|
+
params1?: Record<string, number>;
|
|
2684
|
+
params2?: Record<string, number>;
|
|
2685
|
+
}): Promise<SimulateResult>;
|
|
2686
|
+
/**
|
|
2687
|
+
* Dispose the worker and free all resources.
|
|
2688
|
+
* The sandbox cannot be used after disposal.
|
|
2689
|
+
*/
|
|
2690
|
+
dispose(): void;
|
|
2691
|
+
/**
|
|
2692
|
+
* Whether this sandbox has been disposed.
|
|
2693
|
+
*/
|
|
2694
|
+
get isDisposed(): boolean;
|
|
2695
|
+
/**
|
|
2696
|
+
* Send a generic method call to the worker. Used by sibling sandboxes
|
|
2697
|
+
* (e.g. BrowserManualMatchSandbox) that need to dispatch to method
|
|
2698
|
+
* names other than fight/simulate. The worker bootstrap looks up
|
|
2699
|
+
* `globalThis['__' + method]` and calls it with `options`.
|
|
2700
|
+
*/
|
|
2701
|
+
callRaw(method: string, options: unknown): Promise<unknown>;
|
|
2702
|
+
/**
|
|
2703
|
+
* Send a method call to the worker and wait for the response.
|
|
2704
|
+
* Times out and terminates the worker if no response within timeout.
|
|
2705
|
+
*/
|
|
2706
|
+
private call;
|
|
2707
|
+
private ensureNotDisposed;
|
|
2708
|
+
}
|
|
2709
|
+
/**
|
|
2710
|
+
* One-shot browser-sandboxed fight. Creates worker, runs fight, disposes.
|
|
2711
|
+
*/
|
|
2712
|
+
declare function browserSandboxFight(bundle: string, options?: {
|
|
2713
|
+
seed?: number;
|
|
2714
|
+
maxTicks?: number;
|
|
2715
|
+
} & BrowserSandboxOptions): Promise<FightResult>;
|
|
2716
|
+
/**
|
|
2717
|
+
* One-shot browser-sandboxed simulate. Creates worker, runs simulate, disposes.
|
|
2718
|
+
*/
|
|
2719
|
+
declare function browserSandboxSimulate(bundle: string, options?: {
|
|
2720
|
+
seed?: number;
|
|
2721
|
+
maxTicks?: number;
|
|
2722
|
+
spawnDistance?: number;
|
|
2723
|
+
skipHistory?: boolean;
|
|
2724
|
+
params1?: Record<string, number>;
|
|
2725
|
+
params2?: Record<string, number>;
|
|
2726
|
+
} & BrowserSandboxOptions): Promise<SimulateResult>;
|
|
2727
|
+
/**
|
|
2728
|
+
* Options accepted by `__manualMatchInit` (worker-side). Mirrors
|
|
2729
|
+
* `ManualMatchOptions` from manual-match.ts, but without the constructor's
|
|
2730
|
+
* AI parameters since the player AI is a worker-local stub.
|
|
2731
|
+
*/
|
|
2732
|
+
interface ManualMatchInitOptions {
|
|
2733
|
+
seed?: number;
|
|
2734
|
+
spawnDistance?: number;
|
|
2735
|
+
maxTicks?: number;
|
|
2736
|
+
initialHumanActions?: WizardActions;
|
|
2737
|
+
}
|
|
2738
|
+
interface ManualMatchStepRequest {
|
|
2739
|
+
humanActions?: WizardActions;
|
|
2740
|
+
humanMissileTargets?: Record<string, {
|
|
2741
|
+
x: number;
|
|
2742
|
+
y: number;
|
|
2743
|
+
}>;
|
|
2744
|
+
count?: number;
|
|
2745
|
+
}
|
|
2746
|
+
/**
|
|
2747
|
+
* Long-lived Web Worker sandbox holding a single ManualMatch instance.
|
|
2748
|
+
*
|
|
2749
|
+
* Unlike BrowserMatchSandbox (which runs one batched fight/simulate per
|
|
2750
|
+
* worker), BrowserManualMatchSandbox keeps the worker alive across many
|
|
2751
|
+
* step calls so the engine state and hook state persist between ticks.
|
|
2752
|
+
* This is what manual play mode uses: one worker per session, disposed
|
|
2753
|
+
* when the user leaves the page or starts a new match.
|
|
2754
|
+
*
|
|
2755
|
+
* Usage:
|
|
2756
|
+
* ```ts
|
|
2757
|
+
* const sandbox = await BrowserManualMatchSandbox.fromBundle(opponentBundle);
|
|
2758
|
+
* await sandbox.init({seed: 42});
|
|
2759
|
+
* for (let i = 0; i < 100; i++) {
|
|
2760
|
+
* await sandbox.step({humanActions: {move: {x: 100, y: 0}}, count: 1});
|
|
2761
|
+
* }
|
|
2762
|
+
* await sandbox.dispose();
|
|
2763
|
+
* ```
|
|
2764
|
+
*/
|
|
2765
|
+
declare class BrowserManualMatchSandbox {
|
|
2766
|
+
private readonly inner;
|
|
2767
|
+
private constructor();
|
|
2768
|
+
/**
|
|
2769
|
+
* Create a manual-match sandbox from a pre-compiled bundle. The bundle
|
|
2770
|
+
* must be the output of `compileManualMatchBundle()` — the regular
|
|
2771
|
+
* `compileMatchBundle()` output won't work since it doesn't expose the
|
|
2772
|
+
* `__manualMatch*` globals.
|
|
2773
|
+
*/
|
|
2774
|
+
static fromBundle(bundle: string, options?: BrowserSandboxOptions): Promise<BrowserManualMatchSandbox>;
|
|
2775
|
+
/**
|
|
2776
|
+
* Initialize the worker-side ManualMatch instance.
|
|
2777
|
+
* Returns the initial GameState (tick 0).
|
|
2778
|
+
*/
|
|
2779
|
+
init(options?: ManualMatchInitOptions): Promise<GameState>;
|
|
2780
|
+
/**
|
|
2781
|
+
* Advance the match by `request.count` ticks (default 1), updating
|
|
2782
|
+
* the player's human actions and any guided missile targets first.
|
|
2783
|
+
*/
|
|
2784
|
+
step(request?: ManualMatchStepRequest): Promise<StepResult>;
|
|
2785
|
+
/**
|
|
2786
|
+
* Replace a missile's AI with a worker-local guide stub that reads
|
|
2787
|
+
* from `latestHumanMissileTargets[projectileId]`. Subsequent step()
|
|
2788
|
+
* calls with `humanMissileTargets` populated for this id steer the
|
|
2789
|
+
* missile.
|
|
2790
|
+
*/
|
|
2791
|
+
guideMissile(projectileId: string): Promise<void>;
|
|
2792
|
+
/**
|
|
2793
|
+
* Restore a guided missile's original AI.
|
|
2794
|
+
*/
|
|
2795
|
+
releaseMissile(projectileId: string): Promise<void>;
|
|
2796
|
+
/**
|
|
2797
|
+
* Toggle invincibility for a wizard.
|
|
2798
|
+
*/
|
|
2799
|
+
setInvincible(wizardIndex: 0 | 1, on: boolean): Promise<void>;
|
|
2800
|
+
/**
|
|
2801
|
+
* Get the current game state without advancing.
|
|
2802
|
+
*/
|
|
2803
|
+
getState(): Promise<GameState>;
|
|
2804
|
+
/**
|
|
2805
|
+
* Get the full SimulateResult-compatible result object (history + winner).
|
|
2806
|
+
*/
|
|
2807
|
+
getResult(): Promise<SimulateResult>;
|
|
2808
|
+
/**
|
|
2809
|
+
* Dispose the worker-side ManualMatch instance. Does NOT terminate the
|
|
2810
|
+
* worker — call dispose() for that.
|
|
2811
|
+
*/
|
|
2812
|
+
resetMatch(): Promise<void>;
|
|
2813
|
+
/**
|
|
2814
|
+
* Terminate the worker and free all resources.
|
|
2815
|
+
*/
|
|
2816
|
+
dispose(): void;
|
|
2817
|
+
get isDisposed(): boolean;
|
|
2818
|
+
}
|
|
2819
|
+
|
|
2820
|
+
/**
|
|
2821
|
+
* VIBEMANCER - FIGHT STATISTICS
|
|
2822
|
+
*
|
|
2823
|
+
* Extracts detailed per-bot statistics from a SimulateResult history.
|
|
2824
|
+
* Used by testBot().simulate() and vibemancer trace for debugging.
|
|
2825
|
+
*/
|
|
2826
|
+
|
|
2827
|
+
/** Detailed statistics for one bot in a single match. */
|
|
2828
|
+
interface FightStats {
|
|
2829
|
+
/** Total damage dealt to the opponent. */
|
|
2830
|
+
damageDealt: number;
|
|
2831
|
+
/** Total damage taken from the opponent. */
|
|
2832
|
+
damageTaken: number;
|
|
2833
|
+
/** Number of missiles launched. */
|
|
2834
|
+
missilesLaunched: number;
|
|
2835
|
+
/** Number of missiles that dealt damage (hit the opponent). */
|
|
2836
|
+
missileHits: number;
|
|
2837
|
+
/** Hit rate (0-1). NaN if no missiles fired. */
|
|
2838
|
+
hitRate: number;
|
|
2839
|
+
/** Number of our missiles that hit while enemy was shielding. */
|
|
2840
|
+
hitsEnemyShielded: number;
|
|
2841
|
+
/** Number of our missiles that hit while enemy had no shield. */
|
|
2842
|
+
hitsEnemyUnshielded: number;
|
|
2843
|
+
/** Total raw damage our missiles would have dealt without shields. */
|
|
2844
|
+
rawDamageDealt: number;
|
|
2845
|
+
/** Damage blocked by enemy shields. */
|
|
2846
|
+
damageBlockedByEnemy: number;
|
|
2847
|
+
/** Percentage of our raw damage blocked by enemy shields (0-1). */
|
|
2848
|
+
enemyBlockRate: number;
|
|
2849
|
+
/** Number of times we were hit by enemy missiles. */
|
|
2850
|
+
hitsReceived: number;
|
|
2851
|
+
/** Number of hits received while shield was channeling. */
|
|
2852
|
+
hitsShielded: number;
|
|
2853
|
+
/** Number of hits received without shield. */
|
|
2854
|
+
hitsUnshielded: number;
|
|
2855
|
+
/** Total raw damage that hit us (before shield reduction). */
|
|
2856
|
+
rawDamageReceived: number;
|
|
2857
|
+
/** Total damage blocked by shields. */
|
|
2858
|
+
damageBlocked: number;
|
|
2859
|
+
/** Percentage of incoming raw damage that was blocked (0-1). */
|
|
2860
|
+
blockRate: number;
|
|
2861
|
+
/** Ticks spent in 'casting' state. */
|
|
2862
|
+
castingTicks: number;
|
|
2863
|
+
/** Ticks spent channeling shield. */
|
|
2864
|
+
shieldTicks: number;
|
|
2865
|
+
/** Ticks spent in GCD lockout. */
|
|
2866
|
+
gcdTicks: number;
|
|
2867
|
+
/** Ticks spent idle (not casting, channeling, or in GCD). */
|
|
2868
|
+
idleTicks: number;
|
|
2869
|
+
/** Number of times shield was activated. */
|
|
2870
|
+
shieldCount: number;
|
|
2871
|
+
/** Number of times blink was used. */
|
|
2872
|
+
blinkCount: number;
|
|
2873
|
+
/** Total match duration in ticks. */
|
|
2874
|
+
totalTicks: number;
|
|
2875
|
+
}
|
|
2876
|
+
/**
|
|
2877
|
+
* Extract fight statistics from a SimulateResult.
|
|
2878
|
+
* Returns stats for wizard-1 (the bot under test).
|
|
2879
|
+
*/
|
|
2880
|
+
declare function extractStats(result: SimulateResult): FightStats;
|
|
2881
|
+
/** Format stats as a human-readable summary string. */
|
|
2882
|
+
declare function formatStats(stats: FightStats, botName: string): string;
|
|
2883
|
+
|
|
2884
|
+
/**
|
|
2885
|
+
* VIBEMANCER - TESTING UTILITIES
|
|
2886
|
+
*
|
|
2887
|
+
* Helpers for writing automated tests for your bot.
|
|
2888
|
+
* Import from '@vibemancer/core' in your test files.
|
|
2889
|
+
*
|
|
2890
|
+
* @example
|
|
2891
|
+
* import {testBot} from '@vibemancer/core';
|
|
2892
|
+
* import {MyWizard} from '../src/bot';
|
|
2893
|
+
*
|
|
2894
|
+
* test('beats TargetDummy', async () => {
|
|
2895
|
+
* const result = await testBot(MyWizard).fight('TargetDummy');
|
|
2896
|
+
* expect(result.won).toBe(true);
|
|
2897
|
+
* });
|
|
2898
|
+
*/
|
|
2899
|
+
|
|
2900
|
+
/** Result of a testBot().fight() call with convenience accessors. */
|
|
2901
|
+
interface TestFightResult {
|
|
2902
|
+
/** The raw FightResult from the simulation engine. */
|
|
2903
|
+
raw: FightResult;
|
|
2904
|
+
/** Overall winner: 'wizard-1' | 'wizard-2' | 'draw'. */
|
|
2905
|
+
winner: FightWinner;
|
|
2906
|
+
/** True if your bot won the fight. */
|
|
2907
|
+
won: boolean;
|
|
2908
|
+
/** True if your bot lost the fight. */
|
|
2909
|
+
lost: boolean;
|
|
2910
|
+
/** True if the fight was a draw. */
|
|
2911
|
+
drawn: boolean;
|
|
2912
|
+
/** Number of individual matches your bot won (out of 10). */
|
|
2913
|
+
wins: number;
|
|
2914
|
+
/** Number of individual matches your bot lost (out of 10). */
|
|
2915
|
+
losses: number;
|
|
2916
|
+
/** Number of individual matches that were draws. */
|
|
2917
|
+
draws: number;
|
|
2918
|
+
}
|
|
2919
|
+
/** Result of a testBot().simulate() call with convenience accessors. */
|
|
2920
|
+
interface TestSimulateResult {
|
|
2921
|
+
/** The raw SimulateResult from the simulation engine. */
|
|
2922
|
+
raw: SimulateResult;
|
|
2923
|
+
/** Match winner. */
|
|
2924
|
+
winner: MatchWinner;
|
|
2925
|
+
/** True if your bot won. */
|
|
2926
|
+
won: boolean;
|
|
2927
|
+
/** True if your bot lost. */
|
|
2928
|
+
lost: boolean;
|
|
2929
|
+
/** True if the match was a draw or timeout. */
|
|
2930
|
+
drawn: boolean;
|
|
2931
|
+
/** Number of ticks the match lasted. */
|
|
2932
|
+
ticks: number;
|
|
2933
|
+
/** Your bot's remaining HP. */
|
|
2934
|
+
myHealth: number;
|
|
2935
|
+
/** Enemy's remaining HP. */
|
|
2936
|
+
enemyHealth: number;
|
|
2937
|
+
/** Detailed fight statistics (missiles, damage, shield usage, time breakdown). */
|
|
2938
|
+
stats: FightStats;
|
|
2939
|
+
/** Runtime errors thrown by bot or missile AI (empty if none). */
|
|
2940
|
+
errors: Array<{
|
|
2941
|
+
tick: number;
|
|
2942
|
+
entityId: string;
|
|
2943
|
+
message: string;
|
|
2944
|
+
}>;
|
|
2945
|
+
}
|
|
2946
|
+
/** Builder returned by testBot(). */
|
|
2947
|
+
interface TestBotBuilder {
|
|
2948
|
+
/**
|
|
2949
|
+
* Run a full fight (10 matches) against a named built-in bot.
|
|
2950
|
+
* @param opponent - Built-in bot name (e.g., 'Battlemage', 'TargetDummy').
|
|
2951
|
+
*/
|
|
2952
|
+
fight(opponent: string, options?: {
|
|
2953
|
+
seed?: number;
|
|
2954
|
+
}): TestFightResult;
|
|
2955
|
+
/**
|
|
2956
|
+
* Run a single match against a named built-in bot.
|
|
2957
|
+
* @param opponent - Built-in bot name.
|
|
2958
|
+
*/
|
|
2959
|
+
simulate(opponent: string, options?: {
|
|
2960
|
+
seed?: number;
|
|
2961
|
+
spawnDistance?: number;
|
|
2962
|
+
maxTicks?: number;
|
|
2963
|
+
}): TestSimulateResult;
|
|
2964
|
+
}
|
|
2965
|
+
/**
|
|
2966
|
+
* Create a test builder for your bot.
|
|
2967
|
+
*
|
|
2968
|
+
* @param bot - Your bot function (the same function you export from src/bot.ts).
|
|
2969
|
+
* @returns A builder with .fight() and .simulate() methods.
|
|
2970
|
+
*
|
|
2971
|
+
* @example
|
|
2972
|
+
* ```ts
|
|
2973
|
+
* import {testBot} from '@vibemancer/core';
|
|
2974
|
+
* import {MyWizard} from '../src/bot';
|
|
2975
|
+
*
|
|
2976
|
+
* test('beats TargetDummy', () => {
|
|
2977
|
+
* const result = testBot(MyWizard).fight('TargetDummy');
|
|
2978
|
+
* expect(result.won).toBe(true);
|
|
2979
|
+
* });
|
|
2980
|
+
*
|
|
2981
|
+
* test('survives 10 seconds against Battlemage', () => {
|
|
2982
|
+
* const result = testBot(MyWizard).simulate('Battlemage', {maxTicks: 1000});
|
|
2983
|
+
* expect(result.myHealth).toBeGreaterThan(0);
|
|
2984
|
+
* });
|
|
2985
|
+
*
|
|
2986
|
+
* test('kills at close range', () => {
|
|
2987
|
+
* const result = testBot(MyWizard).simulate('TargetDummy', {spawnDistance: 200});
|
|
2988
|
+
* expect(result.won).toBe(true);
|
|
2989
|
+
* expect(result.ticks).toBeLessThan(1000);
|
|
2990
|
+
* });
|
|
2991
|
+
* ```
|
|
2992
|
+
*/
|
|
2993
|
+
declare function testBot(bot: WizardFunction): TestBotBuilder;
|
|
2994
|
+
|
|
2995
|
+
/**
|
|
2996
|
+
* VIBEMANCER - FIGHT TRACE
|
|
2997
|
+
*
|
|
2998
|
+
* Extracts a structured event log from a SimulateResult history.
|
|
2999
|
+
* Both bots' actions are tracked: state changes, missile launches with
|
|
3000
|
+
* full config, hits, damage, dodge proximity, movement patterns.
|
|
3001
|
+
*
|
|
3002
|
+
* Used by:
|
|
3003
|
+
* - vibemancer trace (CLI debug command)
|
|
3004
|
+
* - scripts/fight-trace.ts (internal diagnostic)
|
|
3005
|
+
* - User tests that want event-level analysis
|
|
3006
|
+
*/
|
|
3007
|
+
|
|
3008
|
+
interface TraceEvent {
|
|
3009
|
+
tick: number;
|
|
3010
|
+
/** 'W1' = wizard-1, 'W2' = wizard-2 */
|
|
3011
|
+
actor: 'W1' | 'W2';
|
|
3012
|
+
type: TraceEventType;
|
|
3013
|
+
detail: string;
|
|
3014
|
+
}
|
|
3015
|
+
type TraceEventType = 'STATE' | 'FIRE' | 'HIT' | 'HURT' | 'DEATH' | 'LAVA_DEATH' | 'SHIELD_BLOCK' | 'KNOCKBACK' | 'BLINK' | 'DODGE_START' | 'DODGE_CLOSE' | 'MOVE' | 'ERROR' | 'WARNING';
|
|
3016
|
+
interface TraceSummary {
|
|
3017
|
+
winner: string;
|
|
3018
|
+
ticks: number;
|
|
3019
|
+
w1Name: string;
|
|
3020
|
+
w2Name: string;
|
|
3021
|
+
w1FinalHp: number;
|
|
3022
|
+
w2FinalHp: number;
|
|
3023
|
+
w1: TraceBotSummary;
|
|
3024
|
+
w2: TraceBotSummary;
|
|
3025
|
+
}
|
|
3026
|
+
interface TraceBotSummary {
|
|
3027
|
+
missilesLaunched: number;
|
|
3028
|
+
hits: number;
|
|
3029
|
+
damageDealt: number;
|
|
3030
|
+
damageReceived: number;
|
|
3031
|
+
shields: number;
|
|
3032
|
+
blinks: number;
|
|
3033
|
+
dodgeEncounters: number;
|
|
3034
|
+
causeOfDeath: 'missile' | 'lava' | 'alive' | 'timeout';
|
|
3035
|
+
movement: {
|
|
3036
|
+
strafe: number;
|
|
3037
|
+
approach: number;
|
|
3038
|
+
retreat: number;
|
|
3039
|
+
still: number;
|
|
3040
|
+
};
|
|
3041
|
+
}
|
|
3042
|
+
/**
|
|
3043
|
+
* Extract a structured event log from simulation history.
|
|
3044
|
+
* Tracks both bots' state changes, missile launches (with full config + range),
|
|
3045
|
+
* hits, damage, dodge proximity, movement patterns, and runtime errors.
|
|
3046
|
+
*/
|
|
3047
|
+
declare function extractTraceEvents(history: GameState[], errors?: BotError[]): TraceEvent[];
|
|
3048
|
+
/**
|
|
3049
|
+
* Generate a summary from trace events and the simulation result.
|
|
3050
|
+
*/
|
|
3051
|
+
declare function summarizeTrace(events: TraceEvent[], result: SimulateResult, w1Name: string, w2Name: string): TraceSummary;
|
|
3052
|
+
/** Format trace events as a human-readable string. */
|
|
3053
|
+
declare function formatTraceEvents(events: TraceEvent[]): string;
|
|
3054
|
+
/** Format a full trace summary as a human-readable string. */
|
|
3055
|
+
declare function formatTraceSummary(summary: TraceSummary): string;
|
|
3056
|
+
/**
|
|
3057
|
+
* Generate diagnostic tips based on trace analysis.
|
|
3058
|
+
* Identifies common problems and suggests fixes.
|
|
3059
|
+
* Returns an array of human-readable tips (empty if no issues found).
|
|
3060
|
+
*/
|
|
3061
|
+
declare function diagnoseTrace(events: TraceEvent[], summary: TraceSummary): string[];
|
|
3062
|
+
/** Format diagnostic tips as a human-readable string. */
|
|
3063
|
+
declare function formatDiagnosis(tips: string[]): string;
|
|
3064
|
+
|
|
3065
|
+
export { type InternalWizardState as $, ALL_BOTS as A, BLINK_CAST_TIME as B, CASTING_MOVEMENT_MULT as C, Critter as D, DEFAULT_BUDGET as E, type FightResult as F, DEFAULT_FIGHT_BACKSTOP_MS as G, DEFAULT_SEED as H, Doombringer as I, ENGINE_VERSION as J, type EnemyState as K, FIGHT_SPAWN_DISTANCES as L, type FightBudget as M, type FightStats as N, type FightWinner as O, type FinalAction as P, Flamecaller as Q, GCD_DURATION as R, type SimulateResult as S, type GameConfig as T, type GameState as U, Golem as V, Hero as W, Hogger as X, type HomingParams as Y, type HookState as Z, Infernalist as _, ARENA_MAX as a, Spellseeker as a$, KNOCKBACK_DAMAGE_THRESHOLD as a0, KNOCKBACK_DECAY as a1, KNOCKBACK_DELAY as a2, KNOCKBACK_SPEED_PER_DAMAGE as a3, LAVA_BORDER_WIDTH as a4, Lich as a5, MATCH_DURATION as a6, MAX_HEALTH as a7, MISSILE_BASE_CAST as a8, MISSILE_BASE_RADIUS as a9, type MissileLaunchEvent as aA, type MissileOobEvent as aB, type MissileTemplate as aC, Nightblade as aD, type ParamDeclaration as aE, type Position as aF, type ProjectileState as aG, Pyromancer as aH, RULES as aI, RULESET_RANGES as aJ, type RefObject as aK, Rookie as aL, SHIELD_CAST_TIME as aM, SHIELD_DECAY_PER_SECOND as aN, SHIELD_DECAY_RATE as aO, SHIELD_MAX_BLOCK as aP, SHIELD_MAX_STRENGTH as aQ, SHIELD_MIN_BLOCK as aR, SHIELD_MIN_STRENGTH as aS, SPAWN_DISTANCE as aT, type SeekerParams as aU, Sentinel as aV, Shadowblade as aW, type ShieldBlockEvent as aX, type ShieldStartEvent as aY, type SimEvent as aZ, Spellbinder as a_, MISSILE_DAMAGE_POWER as aa, MISSILE_DAMAGE_RADIUS_SCALE as ab, MISSILE_DAMAGE_SCALE as ac, MISSILE_HOMING_COEFF as ad, MISSILE_MIN_CAST_TIME as ae, MISSILE_MIN_DAMAGE as af, MISSILE_MIN_DURATION as ag, MISSILE_MIN_SPEED as ah, MISSILE_RADIUS_PER_DAMAGE as ai, MISSILE_SPEED_DURATION_BASELINE as aj, MISSILE_SPEED_DURATION_COEFF as ak, MISSILE_TURN_DURATION_COEFF as al, MOVEMENT_SPEED as am, MOVE_SPEED as an, ManualMatch as ao, type ManualMatchInitOptions as ap, type ManualMatchOptions as aq, type ManualMatchStepRequest as ar, type MatchWinner as as, type MissileAction as at, type MissileActions as au, type MissileConfig as av, type MissileContext as aw, type MissileExpiredEvent as ax, type MissileFunction$1 as ay, type MissileHitEvent as az, ARENA_MIN as b, createBudgetState as b$, Spellshot as b0, Spellspinner as b1, Spelltracer as b2, Spellweaver as b3, type SpiralParams as b4, type StepResult as b5, Stormcaller as b6, Stormchaser as b7, Stormforger as b8, type StraightParams as b9, type WizardGroup as bA, type WizardLavaDeathEvent as bB, type WizardState as bC, type WorkerFactory as bD, type WorkerLike as bE, analyzeThreats as bF, angleDiff as bG, angleInRange as bH, angleTo as bI, applyDamage as bJ, applyRulesetOverrides as bK, blink as bL, browserSandboxFight as bM, browserSandboxSimulate as bN, calculateBlinkCooldown as bO, calculateMissileCastTime as bP, calculateMissileRadius as bQ, calculateMissileSimilarity as bR, calculateShieldBlock as bS, calculateWarmupMultiplier as bT, cancel as bU, cancelCast as bV, clampPositionToArena as bW, clampToArena as bX, clearHooks as bY, clearParamValues as bZ, completeCast as b_, TICKS_PER_SECOND as ba, TICK_DURATION_MS as bb, TargetDummy as bc, type TestBotBuilder as bd, type TestFightResult as be, type TestSimulateResult as bf, type TraceBotSummary as bg, type TraceEvent as bh, type TraceEventType as bi, type TraceSummary as bj, Turtle as bk, type Velocity as bl, Voidblade as bm, WARMUP_DURATION_TOLERANCE as bn, WARMUP_MAX_BONUS as bo, WARMUP_MAX_PENALTY as bp, WARMUP_SPEED_TOLERANCE as bq, WARMUP_TURN_TOLERANCE as br, WIZARD_HEALTH as bs, WIZARD_RADIUS as bt, Warmage as bu, type WizardActions as bv, type WizardContext as bw, type WizardDeathEvent as bx, type WizardEntry as by, type WizardFunction as bz, ARENA_SIZE as c, setParamValues as c$, createEntitySeed as c0, createFightBudget as c1, createInitialState as c2, createRandom as c3, createWorkerScript as c4, currentRuleset as c5, diagnoseTrace as c6, directionAway as c7, directionTo as c8, distanceTo as c9, idle as cA, inRange as cB, interceptAngle as cC, isBannedBotName as cD, isInLava as cE, magnitude as cF, matchesAnyBanRule as cG, mayAct as cH, missile as cI, move as cJ, moveInDirection as cK, moveProjectile as cL, moveWizard as cM, narrowRange as cN, nextRandom as cO, normalize as cP, normalizeAngle as cQ, predictPosition as cR, recordSpend as cS, resetAllHooks as cT, resetRuleset as cU, resolveWizardCollision as cV, runWithHooks as cW, runWizardWithContext as cX, scoreFight as cY, scoreFightAsWizard2 as cZ, seekerMissile as c_, effectiveTurnRateCost as ca, extractAction as cb, extractMissileAction as cc, extractStats as cd, extractTraceEvents as ce, fight as cf, findInRange as cg, findNearest as ch, fitMissileForEscapingTarget as ci, fitMissileToBudget as cj, flyStraight as ck, formatDiagnosis as cl, formatStats as cm, formatTraceEvents as cn, formatTraceSummary as co, generateCandidates as cp, generateCombos as cq, getAdaptiveMissileConfig as cr, getEffectiveRange as cs, getLeadPosition as ct, getMissileCastTime as cu, getMissileContext as cv, getPlayerState as cw, getWizardContext as cx, hashCombine as cy, homingMissile as cz, ARENA_WATER_BUFFER as d, shield as d0, simulate as d1, simulateMinDuration as d2, sortByDistance as d3, spiralMissile as d4, startCast as d5, startDiscovery as d6, stopDiscovery as d7, straightMissile as d8, summarizeTrace as d9, useRef as dA, useShieldStrength as dB, useState as dC, useStatus as dD, useThreats as dE, useTick as dF, useTicksUntilReady as dG, useVelocity as dH, validateHookCall as dI, validateMissileConfig as dJ, withMissileContext as dK, withWizardContext as dL, wrapWithParams as dM, sweptCircleCollision as da, testBot as db, tick as dc, turnToAngle as dd, turnToward as de, updateShield as df, useArenaSize as dg, useBlinkCooldown as dh, useCastProgress as di, useCastingSpell as dj, useClosestThreat as dk, useDamageDealt as dl, useDamageTaken as dm, useEffect as dn, useEnemy as dp, useHealth as dq, useLastHitTick as dr, useLastMissileConfig as ds, useMemo as dt, useMyProjectiles as du, useMyThreatsToEnemy as dv, useParam as dw, usePosition as dx, useProjectiles as dy, useRandom as dz, type ActionBuilder as e, type AnalyzedThreat as f, Archlich as g, Archmage as h, BLINK_COOLDOWN as i, BLINK_MAX_COOLDOWN as j, BLINK_MAX_RANGE as k, BLINK_MIN_COOLDOWN as l, BLINK_RANGE as m, BOT_GROUPS as n, Battlemage as o, type BlinkEvent as p, Bonemancer as q, type BotBudgetState as r, type BotError as s, BrowserManualMatchSandbox as t, BrowserMatchSandbox as u, type BrowserSandboxOptions as v, type BudgetLimits as w, COLLISION_RADIUS as x, type CastCancelEvent as y, type CastStartEvent as z };
|