@vibemancer/core 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +2 -2
  2. package/dist/chunk-G5T65F3W.js +10617 -0
  3. package/dist/chunk-G5T65F3W.js.map +1 -0
  4. package/dist/index-browser.d.ts +1328 -1051
  5. package/dist/index-browser.js +59 -17
  6. package/dist/index.d.ts +53 -3
  7. package/dist/index.js +204 -54
  8. package/dist/index.js.map +1 -1
  9. package/package.json +21 -15
  10. package/src/banned-bot-names.ts +53 -0
  11. package/src/bots/Hero.ts +867 -0
  12. package/src/bots/berserker/01_Stormchaser.ts +162 -120
  13. package/src/bots/berserker/02_Stormcaller.ts +160 -116
  14. package/src/bots/berserker/03_Stormforger.ts +162 -129
  15. package/src/bots/caster/01_Flamecaller.ts +126 -84
  16. package/src/bots/caster/02_Pyromancer.ts +148 -101
  17. package/src/bots/caster/03_Infernalist.ts +180 -160
  18. package/src/bots/defensive/01_Turtle.ts +48 -44
  19. package/src/bots/defensive/02_Sentinel.ts +57 -53
  20. package/src/bots/defensive/03_Golem.ts +85 -97
  21. package/src/bots/duelist/01_Battlemage.ts +157 -122
  22. package/src/bots/duelist/02_Warmage.ts +166 -121
  23. package/src/bots/duelist/03_Archmage.ts +198 -178
  24. package/src/bots/homing/01_Bonemancer.ts +20 -32
  25. package/src/bots/homing/02_Lich.ts +145 -93
  26. package/src/bots/homing/03_Archlich.ts +129 -60
  27. package/src/bots/kiter/01_Spellspinner.ts +145 -107
  28. package/src/bots/kiter/02_Spellweaver.ts +153 -105
  29. package/src/bots/kiter/03_Spellbinder.ts +168 -123
  30. package/src/bots/melee/01_Shadowblade.ts +131 -75
  31. package/src/bots/melee/02_Nightblade.ts +174 -130
  32. package/src/bots/melee/03_Voidblade.ts +266 -168
  33. package/src/bots/registry.ts +44 -37
  34. package/src/bots/shared.ts +185 -25
  35. package/src/bots/sniper/01_Spellshot.ts +151 -98
  36. package/src/bots/sniper/02_Spelltracer.ts +173 -128
  37. package/src/bots/sniper/03_Spellseeker.ts +177 -152
  38. package/src/bots/standalone/Critter.ts +46 -52
  39. package/src/bots/standalone/Doombringer.ts +36 -40
  40. package/src/bots/standalone/Hogger.ts +120 -89
  41. package/src/bots/standalone/Rookie.ts +21 -23
  42. package/src/bots/standalone/TargetDummy.ts +5 -6
  43. package/src/bots/test/cheater.ts +91 -85
  44. package/src/bots/test/crasher.ts +26 -28
  45. package/src/engine/bundle-fight.ts +242 -0
  46. package/src/engine/hooks-runtime.ts +58 -18
  47. package/src/engine/manual-match.ts +33 -25
  48. package/src/engine/missile-templates.ts +25 -43
  49. package/src/engine/optimizer.ts +7 -7
  50. package/src/engine/params-runtime.ts +3 -3
  51. package/src/engine/physics.ts +33 -23
  52. package/src/engine/sandbox-browser.ts +8 -7
  53. package/src/engine/sandbox-compile.ts +1 -1
  54. package/src/engine/sandbox-harness.ts +299 -367
  55. package/src/engine/sandbox.ts +3 -3
  56. package/src/engine/simulation.ts +291 -76
  57. package/src/engine/spells.ts +9 -12
  58. package/src/engine-version.ts +1 -1
  59. package/src/hooks/action-builders.ts +76 -21
  60. package/src/hooks/index.ts +17 -9
  61. package/src/hooks/state-hooks.ts +61 -25
  62. package/src/hooks/threat-analysis.ts +44 -20
  63. package/src/hooks/types.ts +62 -5
  64. package/src/index-browser.ts +1 -0
  65. package/src/index.ts +3 -0
  66. package/src/rules.ts +209 -63
  67. package/src/stats.ts +2 -2
  68. package/src/testing.ts +6 -30
  69. package/src/trace.ts +63 -3
  70. package/src/types.ts +127 -14
  71. package/src/utils/angles.ts +1 -0
  72. package/src/utils/combat.ts +98 -6
  73. package/src/utils/spatial.ts +3 -3
  74. package/dist/chunk-74FFJCFF.js +0 -9140
  75. package/dist/chunk-74FFJCFF.js.map +0 -1
  76. package/src/hooks/bot-wrapper.ts +0 -84
package/src/types.ts CHANGED
@@ -104,6 +104,9 @@ export interface GameState
104
104
  damageDealt: number; // total damage dealt this match
105
105
  damageTaken: number; // total damage taken this match
106
106
  lastHitTick: number; // tick when last took damage (0 if never)
107
+
108
+ /** Events emitted during the most recent tick (empty for the initial state). */
109
+ events: SimEvent[];
107
110
  }
108
111
 
109
112
  /**
@@ -133,7 +136,7 @@ export interface WizardActions
133
136
  startCast?: {
134
137
  spell: 'missile';
135
138
  config: MissileConfig;
136
- missileAI: MissileAIFunction;
139
+ missileAI: MissileFunction;
137
140
  direction?: number; // initial rotation override (auto-aims at enemy by default)
138
141
  } | {
139
142
  spell: 'shield';
@@ -153,6 +156,20 @@ export interface WizardActions
153
156
  * Only applies while state === 'casting' and castingSpell === 'missile'.
154
157
  */
155
158
  aimDirection?: number;
159
+
160
+ /**
161
+ * Missile guide — manual play mode only. Takes direct control of a
162
+ * specific missile using WASD/joystick input. While active, the wizard
163
+ * is invulnerable and frozen (move ignored), and the missile steers
164
+ * using its turnRate toward the given direction.
165
+ *
166
+ * direction: {x, y} → converted to target angle via atan2(y, x)
167
+ * direction: null → missile flies straight (no steering input)
168
+ */
169
+ missileGuide?: {
170
+ missileId: string;
171
+ direction: {x: number; y: number} | null;
172
+ } | null;
156
173
  }
157
174
 
158
175
  /**
@@ -167,27 +184,123 @@ export interface MissileConfig
167
184
  }
168
185
 
169
186
  /**
170
- * Missile AI function type.
187
+ * Missile AI function type — re-exported from hooks/types.ts.
171
188
  */
172
- export type MissileAIFunction = (props: {
173
- missileState: ProjectileState;
174
- worldState: GameState;
175
- random: () => number; // Seeded PRNG, isolated per entity, 0-1 range
176
- }) => MissileActions;
189
+ type MissileFunction = import('./hooks/types.js').MissileFunction;
177
190
 
178
191
  /**
179
192
  * Actions returned by missile each tick.
180
193
  */
181
194
  export interface MissileActions
182
195
  {
183
- turnToward?: Position; // engine handles turn rate limit
196
+ turnToward?: Position;
197
+ turnToAngle?: number;
198
+ }
199
+
200
+ // ============================================================
201
+ // GAME EVENTS
202
+ // ============================================================
203
+
204
+ export interface MissileHitEvent
205
+ {
206
+ type: 'missile-hit';
207
+ position: Position;
208
+ damage: number;
209
+ actualDamage: number;
210
+ speed: number;
211
+ ownerId: string;
212
+ targetId: string;
213
+ }
214
+
215
+ export interface MissileExpiredEvent
216
+ {
217
+ type: 'missile-expired';
218
+ position: Position;
219
+ }
220
+
221
+ export interface MissileOobEvent
222
+ {
223
+ type: 'missile-oob';
224
+ position: Position;
225
+ }
226
+
227
+ export interface BlinkEvent
228
+ {
229
+ type: 'blink';
230
+ wizardId: string;
231
+ from: Position;
232
+ to: Position;
233
+ }
234
+
235
+ export interface ShieldStartEvent
236
+ {
237
+ type: 'shield-start';
238
+ wizardId: string;
239
+ position: Position;
240
+ }
241
+
242
+ export interface CastStartEvent
243
+ {
244
+ type: 'cast-start';
245
+ wizardId: string;
246
+ position: Position;
247
+ spell: 'missile' | 'shield' | 'blink';
248
+ }
249
+
250
+ export interface WizardDeathEvent
251
+ {
252
+ type: 'wizard-death';
253
+ wizardId: string;
254
+ position: Position;
255
+ }
256
+
257
+ export interface WizardLavaDeathEvent
258
+ {
259
+ type: 'wizard-lava-death';
260
+ wizardId: string;
261
+ position: Position;
184
262
  }
185
263
 
264
+ export interface CastCancelEvent
265
+ {
266
+ type: 'cast-cancel';
267
+ wizardId: string;
268
+ position: Position;
269
+ spell: 'missile' | 'shield' | 'blink';
270
+ }
271
+
272
+ export interface MissileLaunchEvent
273
+ {
274
+ type: 'missile-launch';
275
+ position: Position;
276
+ damage: number;
277
+ speed: number;
278
+ ownerId: string;
279
+ rotation: number;
280
+ }
281
+
282
+ export interface ShieldBlockEvent
283
+ {
284
+ type: 'shield-block';
285
+ wizardId: string;
286
+ position: Position;
287
+ damageBlocked: number;
288
+ damageThrough: number;
289
+ }
290
+
291
+ export type SimEvent =
292
+ | MissileHitEvent
293
+ | MissileExpiredEvent
294
+ | MissileOobEvent
295
+ | MissileLaunchEvent
296
+ | BlinkEvent
297
+ | ShieldStartEvent
298
+ | ShieldBlockEvent
299
+ | CastStartEvent
300
+ | CastCancelEvent
301
+ | WizardDeathEvent
302
+ | WizardLavaDeathEvent;
303
+
186
304
  /**
187
- * Main wizard function type.
305
+ * @deprecated Use WizardFunction from hooks/types.ts instead.
188
306
  */
189
- export type WizardFunction = (props: {
190
- state: GameState;
191
- config: GameConfig;
192
- random: () => number; // Seeded PRNG, isolated per entity, 0-1 range
193
- }) => WizardActions;
@@ -30,6 +30,7 @@ export function normalizeAngle(angle: number): number
30
30
  */
31
31
  export function angleDiff(angleA: number, angleB: number): number
32
32
  {
33
+ // Stryker disable next-line ArithmeticOperator: +180 vs -180 in modular arithmetic produces identical results for all inputs
33
34
  let diff = (angleB - angleA + 180) % 360 - 180;
34
35
  if (diff < -180)
35
36
  {
@@ -10,6 +10,8 @@ import {
10
10
  calculateWarmupMultiplier,
11
11
  validateMissileConfig,
12
12
  TICKS_PER_SECOND,
13
+ MOVEMENT_SPEED,
14
+ CASTING_MOVEMENT_MULT,
13
15
  WIZARD_HEALTH,
14
16
  MISSILE_BASE_CAST,
15
17
  MISSILE_DAMAGE_SCALE,
@@ -21,6 +23,7 @@ import {
21
23
  MISSILE_MIN_CAST_TIME,
22
24
  MISSILE_MIN_DURATION,
23
25
  MISSILE_MIN_DAMAGE,
26
+ WIZARD_RADIUS,
24
27
  effectiveTurnRateCost,
25
28
  } from '../rules.js';
26
29
 
@@ -172,12 +175,61 @@ export function getAdaptiveMissileConfig(
172
175
  * If `lastMissileConfig` is provided, accounts for warmup bonus: similar
173
176
  * missiles cast faster, so more damage can fit in the same budget.
174
177
  */
178
+ /**
179
+ * Simulate a missile trajectory to find the minimum duration (ticks) needed
180
+ * to reach a target at the given distance. Works for all turnRate values:
181
+ * positive (homing) and zero (straight).
182
+ *
183
+ * The simulation starts the missile aimed directly at the target and steps
184
+ * through the trajectory tick by tick. For homing, the missile tracks the
185
+ * target each tick matching the engine's steering physics.
186
+ */
187
+ /**
188
+ * Simulate a missile trajectory to find the minimum duration (ticks) needed
189
+ * to reach a target at the given distance. Works for straight (turnRate=0)
190
+ * and homing (turnRate>0) missiles.
191
+ *
192
+ * The missile starts aimed directly at the target at (dist, 0) and steps
193
+ * through the trajectory tick by tick. For homing, the missile tracks the
194
+ * target each tick matching the engine's steering physics.
195
+ *
196
+ * Returns 500 if the missile cannot reach the target within 500 ticks.
197
+ */
198
+ export function simulateMinDuration(speed: number, turnRateDeg: number, dist: number, collisionRadius?: number): number
199
+ {
200
+ if (dist <= 0) return 1;
201
+ const hitR = collisionRadius ?? (WIZARD_RADIUS + 8);
202
+ if (turnRateDeg === 0)
203
+ {
204
+ return Math.max(1, Math.ceil((dist - hitR) / speed));
205
+ }
206
+ const maxTurn = Math.abs(turnRateDeg) * Math.PI / 180;
207
+ let angle = 0;
208
+ let x = 0;
209
+ let y = 0;
210
+ for (let t = 1; t <= 500; t++)
211
+ {
212
+ const targetAngle = Math.atan2(-y, dist - x);
213
+ let diff = targetAngle - angle;
214
+ while (diff > Math.PI) diff -= 2 * Math.PI;
215
+ while (diff < -Math.PI) diff += 2 * Math.PI;
216
+ const turn = Math.max(-maxTurn, Math.min(maxTurn, diff));
217
+ angle += turn;
218
+ x += Math.cos(angle) * speed;
219
+ y += Math.sin(angle) * speed;
220
+ const d = Math.sqrt((x - dist) * (x - dist) + y * y);
221
+ if (d <= hitR) return t;
222
+ }
223
+ return 500;
224
+ }
225
+
175
226
  export function fitMissileToBudget(
176
227
  budgetTicks: number,
177
228
  distance: number,
178
229
  options?: {minTurnRate?: number; maxDamage?: number; lastMissileConfig?: MissileConfig},
179
230
  ): MissileConfig | null
180
231
  {
232
+
181
233
  const budgetSec = budgetTicks / TICKS_PER_SECOND;
182
234
  const minTurnRate = options?.minTurnRate ?? 0;
183
235
  const maxDamage = options?.maxDamage ?? WIZARD_HEALTH;
@@ -195,10 +247,7 @@ export function fitMissileToBudget(
195
247
 
196
248
  // Templates: [speed, turnRate] combos to try, ordered by preference.
197
249
  // Lower turnRate = cheaper cast. Higher speed = shorter duration needed.
198
- // Negative turnRate = anti-homing (turns away from target) but cheaper cast time.
199
250
  const templates: [number, number][] = [
200
- [7, -1], // Fast anti-homing: cheapest cast, curves away from target
201
- [7, -0.5],// Fast light anti-homing
202
251
  [7, 0], // Fast straight: cheapest cast, only hits stationary
203
252
  [5, 0], // Medium straight
204
253
  [6, 0.5], // Fast with light tracking
@@ -214,11 +263,9 @@ export function fitMissileToBudget(
214
263
  for (const [speed, turnRate] of templates)
215
264
  {
216
265
  if (turnRate < minTurnRate) continue;
217
- // Minimum duration to reach target (with 20% margin for homing curve)
218
- const margin = turnRate > 0 ? 1.2 : 1.05;
219
266
  const minDurationTicks = Math.max(
220
267
  MISSILE_MIN_DURATION,
221
- Math.ceil(distance * margin / speed),
268
+ simulateMinDuration(speed, turnRate, distance),
222
269
  );
223
270
  const durationSec = minDurationTicks / TICKS_PER_SECOND;
224
271
 
@@ -277,3 +324,48 @@ export function fitMissileToBudget(
277
324
 
278
325
  return best;
279
326
  }
327
+
328
+ /**
329
+ * Fit a missile config that accounts for the enemy escaping during cast time.
330
+ *
331
+ * During casting, the caster moves at CASTING_MOVEMENT_MULT speed while
332
+ * the enemy moves at full MOVEMENT_SPEED. This means the effective distance
333
+ * at launch is larger than the current distance. This function iteratively
334
+ * converges on a missile config whose range covers the escape distance.
335
+ *
336
+ * @param currentDistance - Current distance to enemy
337
+ * @param budgetTicks - Maximum cast time budget in ticks
338
+ * @param options - Same options as fitMissileToBudget, plus:
339
+ * - enemyApproaching: if true, enemy is moving toward caster (reduces escape)
340
+ * - distanceBuffer: flat units added to target distance for safety margin (default 20)
341
+ * - maxIterations: convergence iterations (default 5)
342
+ */
343
+ export function fitMissileForEscapingTarget(
344
+ currentDistance: number,
345
+ budgetTicks: number,
346
+ options?: {minTurnRate?: number; maxDamage?: number; lastMissileConfig?: MissileConfig; enemyApproaching?: boolean; distanceBuffer?: number; maxIterations?: number},
347
+ ): MissileConfig | null
348
+ {
349
+ const maxIter = options?.maxIterations ?? 5;
350
+ const buffer = options?.distanceBuffer ?? 20;
351
+ const escapeRate = options?.enemyApproaching
352
+ ? 0
353
+ : MOVEMENT_SPEED - MOVEMENT_SPEED * CASTING_MOVEMENT_MULT;
354
+
355
+ let targetDistance = currentDistance + buffer;
356
+ let lastCastTicks = 0;
357
+
358
+ for (let i = 0; i < maxIter; i++)
359
+ {
360
+ const config = fitMissileToBudget(budgetTicks, targetDistance, options);
361
+ if (!config) return null;
362
+
363
+ const castTicks = getMissileCastTime(config, options?.lastMissileConfig);
364
+ if (castTicks === lastCastTicks) return config;
365
+ lastCastTicks = castTicks;
366
+
367
+ targetDistance = currentDistance + castTicks * escapeRate + buffer;
368
+ }
369
+
370
+ return fitMissileToBudget(budgetTicks, targetDistance, options);
371
+ }
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import {Position} from '../types.js';
8
- import {ARENA_WIDTH, ARENA_HEIGHT} from '../rules.js';
8
+ import {ARENA_SIZE} from '../rules.js';
9
9
 
10
10
  /**
11
11
  * Normalize a vector to unit length.
@@ -49,8 +49,8 @@ export function directionAway(from: Position, to: Position): Position
49
49
  export function clampPositionToArena(position: Position): Position
50
50
  {
51
51
  return {
52
- x: Math.max(0, Math.min(ARENA_WIDTH, position.x)),
53
- y: Math.max(0, Math.min(ARENA_HEIGHT, position.y)),
52
+ x: Math.max(0, Math.min(ARENA_SIZE, position.x)),
53
+ y: Math.max(0, Math.min(ARENA_SIZE, position.y)),
54
54
  };
55
55
  }
56
56