@vibemancer/core 0.1.4 → 0.1.5

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 (74) hide show
  1. package/README.md +2 -2
  2. package/dist/chunk-W7HWJFQ4.js +10599 -0
  3. package/dist/chunk-W7HWJFQ4.js.map +1 -0
  4. package/dist/index-browser.d.ts +1300 -1051
  5. package/dist/index-browser.js +55 -17
  6. package/dist/index.d.ts +53 -3
  7. package/dist/index.js +200 -54
  8. package/dist/index.js.map +1 -1
  9. package/package.json +21 -15
  10. package/src/bots/Hero.ts +867 -0
  11. package/src/bots/berserker/01_Stormchaser.ts +162 -120
  12. package/src/bots/berserker/02_Stormcaller.ts +160 -116
  13. package/src/bots/berserker/03_Stormforger.ts +162 -129
  14. package/src/bots/caster/01_Flamecaller.ts +126 -84
  15. package/src/bots/caster/02_Pyromancer.ts +148 -101
  16. package/src/bots/caster/03_Infernalist.ts +180 -160
  17. package/src/bots/defensive/01_Turtle.ts +48 -44
  18. package/src/bots/defensive/02_Sentinel.ts +57 -53
  19. package/src/bots/defensive/03_Golem.ts +85 -97
  20. package/src/bots/duelist/01_Battlemage.ts +157 -122
  21. package/src/bots/duelist/02_Warmage.ts +166 -121
  22. package/src/bots/duelist/03_Archmage.ts +198 -178
  23. package/src/bots/homing/01_Bonemancer.ts +20 -32
  24. package/src/bots/homing/02_Lich.ts +145 -93
  25. package/src/bots/homing/03_Archlich.ts +129 -60
  26. package/src/bots/kiter/01_Spellspinner.ts +145 -107
  27. package/src/bots/kiter/02_Spellweaver.ts +153 -105
  28. package/src/bots/kiter/03_Spellbinder.ts +168 -123
  29. package/src/bots/melee/01_Shadowblade.ts +131 -75
  30. package/src/bots/melee/02_Nightblade.ts +174 -130
  31. package/src/bots/melee/03_Voidblade.ts +266 -168
  32. package/src/bots/registry.ts +44 -37
  33. package/src/bots/shared.ts +185 -25
  34. package/src/bots/sniper/01_Spellshot.ts +151 -98
  35. package/src/bots/sniper/02_Spelltracer.ts +173 -128
  36. package/src/bots/sniper/03_Spellseeker.ts +177 -152
  37. package/src/bots/standalone/Critter.ts +46 -52
  38. package/src/bots/standalone/Doombringer.ts +36 -40
  39. package/src/bots/standalone/Hogger.ts +120 -89
  40. package/src/bots/standalone/Rookie.ts +21 -23
  41. package/src/bots/standalone/TargetDummy.ts +5 -6
  42. package/src/bots/test/cheater.ts +91 -85
  43. package/src/bots/test/crasher.ts +26 -28
  44. package/src/engine/bundle-fight.ts +242 -0
  45. package/src/engine/hooks-runtime.ts +58 -18
  46. package/src/engine/manual-match.ts +33 -25
  47. package/src/engine/missile-templates.ts +25 -43
  48. package/src/engine/optimizer.ts +7 -7
  49. package/src/engine/params-runtime.ts +3 -3
  50. package/src/engine/physics.ts +33 -23
  51. package/src/engine/sandbox-browser.ts +8 -7
  52. package/src/engine/sandbox-compile.ts +1 -1
  53. package/src/engine/sandbox-harness.ts +299 -367
  54. package/src/engine/sandbox.ts +3 -3
  55. package/src/engine/simulation.ts +291 -76
  56. package/src/engine/spells.ts +9 -12
  57. package/src/engine-version.ts +1 -1
  58. package/src/hooks/action-builders.ts +76 -21
  59. package/src/hooks/index.ts +17 -9
  60. package/src/hooks/state-hooks.ts +61 -25
  61. package/src/hooks/threat-analysis.ts +44 -20
  62. package/src/hooks/types.ts +62 -5
  63. package/src/index.ts +2 -0
  64. package/src/rules.ts +209 -63
  65. package/src/stats.ts +2 -2
  66. package/src/testing.ts +6 -30
  67. package/src/trace.ts +63 -3
  68. package/src/types.ts +127 -14
  69. package/src/utils/angles.ts +1 -0
  70. package/src/utils/combat.ts +98 -6
  71. package/src/utils/spatial.ts +3 -3
  72. package/dist/chunk-74FFJCFF.js +0 -9140
  73. package/dist/chunk-74FFJCFF.js.map +0 -1
  74. package/src/hooks/bot-wrapper.ts +0 -84
@@ -1,10 +1,10 @@
1
- import {WizardFunction, MissileAIFunction} from '../../types.js';
1
+ import type {FinalAction, MissileFunction} from '../../hooks/index.js';
2
+ import {usePosition, useEnemy, useStatus, useBlinkCooldown, useHealth, useTick, useThreats, useProjectiles, useCastingSpell, useCastProgress, idle, move as moveAction, missile, shield, blink, cancel, getMissileContext, turnToward, flyStraight} from '../../hooks/index.js';
2
3
  import {angleTo} from '../../utils/angles.js';
3
4
  import {distanceTo} from '../../utils/distance.js';
4
5
  import {getMissileCastTime} from '../../utils/combat.js';
5
- import {ARENA_SIZE, GCD_DURATION} from '../../rules.js';
6
+ import {RULES, ARENA_MIN, ARENA_MAX} from '../../rules.js';
6
7
  import {
7
- getThreats,
8
8
  getMostUrgentThreat,
9
9
  getDodgeDirectionForMovement,
10
10
  isSafeToAttack,
@@ -12,6 +12,8 @@ import {
12
12
  getBlinkToIncreaseDistance,
13
13
  MIN_BLINK_ESCAPE_DISTANCE,
14
14
  shouldForceShield,
15
+ keepOffWalls,
16
+ getPreCastReposition,
15
17
  } from '../shared.js';
16
18
  import {useParam} from '../../engine/params-runtime.js';
17
19
  const WALL_BUFFER = 60;
@@ -30,12 +32,11 @@ const QUICK_CONFIG = {damage: 10, speed: 5, turnRate: 1, duration: 80};
30
32
  /**
31
33
  * Standard homing missile AI.
32
34
  */
33
- const HomingAI: MissileAIFunction = ({worldState}) =>
35
+ const HomingAI: MissileFunction = () =>
34
36
  {
35
- const enemy = worldState.enemies[0];
36
- return {
37
- turnToward: enemy?.position,
38
- };
37
+ const ctx = getMissileContext();
38
+ const enemy = ctx.worldState.enemies[0];
39
+ return enemy ? turnToward(enemy.position.x, enemy.position.y) : flyStraight();
39
40
  };
40
41
 
41
42
  /**
@@ -44,10 +45,10 @@ const HomingAI: MissileAIFunction = ({worldState}) =>
44
45
  function isNearWall(position: {x: number; y: number}): boolean
45
46
  {
46
47
  return (
47
- position.x < WALL_BUFFER ||
48
- position.x > ARENA_SIZE - WALL_BUFFER ||
49
- position.y < WALL_BUFFER ||
50
- position.y > ARENA_SIZE - WALL_BUFFER
48
+ position.x < ARENA_MIN + WALL_BUFFER ||
49
+ position.x > ARENA_MAX - WALL_BUFFER ||
50
+ position.y < ARENA_MIN + WALL_BUFFER ||
51
+ position.y > ARENA_MAX - WALL_BUFFER
51
52
  );
52
53
  }
53
54
 
@@ -97,16 +98,16 @@ function smartStrafe(
97
98
  y: position.y + strafeCounterClockwise.y * 100,
98
99
  };
99
100
  const scoreClockwise = Math.min(
100
- futureClockwise.x,
101
- ARENA_SIZE - futureClockwise.x,
102
- futureClockwise.y,
103
- ARENA_SIZE - futureClockwise.y,
101
+ futureClockwise.x - ARENA_MIN,
102
+ ARENA_MAX - futureClockwise.x,
103
+ futureClockwise.y - ARENA_MIN,
104
+ ARENA_MAX - futureClockwise.y,
104
105
  );
105
106
  const scoreCounterClockwise = Math.min(
106
- futureCounterClockwise.x,
107
- ARENA_SIZE - futureCounterClockwise.x,
108
- futureCounterClockwise.y,
109
- ARENA_SIZE - futureCounterClockwise.y,
107
+ futureCounterClockwise.x - ARENA_MIN,
108
+ ARENA_MAX - futureCounterClockwise.x,
109
+ futureCounterClockwise.y - ARENA_MIN,
110
+ ARENA_MAX - futureCounterClockwise.y,
110
111
  );
111
112
  strafeDir =
112
113
  scoreClockwise > scoreCounterClockwise
@@ -121,7 +122,7 @@ function smartStrafe(
121
122
  }
122
123
 
123
124
  // When actively dodging, commit fully — no approach/retreat dilution
124
- const intensity = dodgeDirection ? 100 : strafeIntensity;
125
+ const intensity = dodgeDirection ? 1 : strafeIntensity;
125
126
  return {x: strafeDir.x * intensity, y: strafeDir.y * intensity};
126
127
  }
127
128
 
@@ -149,35 +150,47 @@ function smartStrafe(
149
150
  *
150
151
  * TIER: 1 (base)
151
152
  */
152
- export const Spellspinner: WizardFunction = ({state}) =>
153
+ export function Spellspinner(): FinalAction
153
154
  {
154
- const targetDistance = useParam('targetDistance', 338, {
155
+ const position = usePosition();
156
+ const enemy = useEnemy();
157
+ const status = useStatus();
158
+ const blinkCooldown = useBlinkCooldown();
159
+ const health = useHealth();
160
+ const tick = useTick();
161
+ const threats = useThreats();
162
+ const projectiles = useProjectiles();
163
+ const castingSpell = useCastingSpell();
164
+ const castProgressData = useCastProgress();
165
+
166
+
167
+ const targetDistance = useParam('targetDistance', 268, {
155
168
  range: 150,
156
169
  min: 150,
157
170
  max: 450,
158
171
  });
159
- const dangerDistance = useParam('dangerDistance', 242, {
172
+ const dangerDistance = useParam('dangerDistance', 173, {
160
173
  range: 100,
161
174
  min: 100,
162
175
  });
163
- const strafeCyclePeriod = useParam('strafeCyclePeriod', 210, {
176
+ const strafeCyclePeriod = useParam('strafeCyclePeriod', 286, {
164
177
  range: 75,
165
178
  min: 80,
166
179
  max: 300,
167
180
  });
168
- const strafeIntensity = useParam('strafeIntensity', 31, {
169
- range: 40,
170
- min: 30,
171
- max: 100,
181
+ const strafeIntensity = useParam('strafeIntensity', 0.98, {
182
+ range: 0.4,
183
+ min: 0.3,
184
+ max: 1,
172
185
  steps: 7,
173
186
  });
174
- const approachSpeed = useParam('approachSpeed', 25, {
175
- range: 30,
176
- min: 10,
177
- max: 70,
187
+ const approachSpeed = useParam('approachSpeed', 0.4, {
188
+ range: 0.3,
189
+ min: 0.1,
190
+ max: 0.7,
178
191
  steps: 7,
179
192
  });
180
- const distanceDeadZone = useParam('distanceDeadZone', 40, {
193
+ const distanceDeadZone = useParam('distanceDeadZone', 13, {
181
194
  range: 30,
182
195
  min: 5,
183
196
  max: 80,
@@ -188,32 +201,47 @@ export const Spellspinner: WizardFunction = ({state}) =>
188
201
  min: 50,
189
202
  max: 300,
190
203
  });
191
- const blinkWindowMax = useParam('blinkWindowMax', 68, {
204
+ const blinkWindowMax = useParam('blinkWindowMax', 51, {
192
205
  range: 30,
193
206
  min: 15,
194
207
  max: 80,
195
208
  steps: 7,
196
209
  });
210
+ const centerBias = useParam('centerBias', 0.69, {
211
+ range: 0.4,
212
+ min: 0,
213
+ max: 0.8,
214
+ steps: 5,
215
+ });
216
+ const maxBlinkDistance = useParam('maxBlinkDistance', 300, {
217
+ range: 100,
218
+ min: 50,
219
+ max: 300,
220
+ steps: 7,
221
+ });
222
+
223
+ const distance = distanceTo(position, enemy.position);
197
224
 
198
- const enemy = state.enemies[0];
199
- if (!enemy)
225
+ function capBlinkDistance(target: {x: number; y: number}): {x: number; y: number}
200
226
  {
201
- return {move: {x: 0, y: 0}};
227
+ const dx = target.x - position.x;
228
+ const dy = target.y - position.y;
229
+ const dist = Math.sqrt(dx * dx + dy * dy);
230
+ if (dist <= maxBlinkDistance) return target;
231
+ const scale = maxBlinkDistance / dist;
232
+ return {x: position.x + dx * scale, y: position.y + dy * scale};
202
233
  }
203
234
 
204
- const distance = distanceTo(state.position, enemy.position);
205
-
206
235
  // Analyze threats using simulation
207
- const threats = getThreats(state);
208
236
  const urgentThreat = getMostUrgentThreat(threats);
209
237
 
210
238
  // Smart strafe (dodge direction includes close homing missile avoidance)
211
- const dodgeDirection = getDodgeDirectionForMovement(threats, state.position);
239
+ const dodgeDirection = getDodgeDirectionForMovement(threats, position);
212
240
  const strafe = smartStrafe(
213
- state.position,
241
+ position,
214
242
  enemy,
215
243
  dodgeDirection,
216
- state.tick,
244
+ tick,
217
245
  strafeCyclePeriod,
218
246
  strafeIntensity,
219
247
  );
@@ -224,24 +252,32 @@ export const Spellspinner: WizardFunction = ({state}) =>
224
252
 
225
253
  if (!dodgeDirection)
226
254
  {
227
- const deltaX = enemy.position.x - state.position.x;
228
- const deltaY = enemy.position.y - state.position.y;
255
+ const deltaX = enemy.position.x - position.x;
256
+ const deltaY = enemy.position.y - position.y;
229
257
  const normalizedDistance = distance > 0 ? distance : 1;
230
258
 
231
- if (distance > targetDistance + distanceDeadZone)
259
+ if (distance > targetDistance + distanceDeadZone)
232
260
  {
233
- moveX += (deltaX / normalizedDistance) * approachSpeed;
234
- moveY += (deltaY / normalizedDistance) * approachSpeed;
261
+ // Approach the center-side of the enemy so knockback pushes toward center, not lava.
262
+ const center = (ARENA_MIN + ARENA_MAX) / 2;
263
+ const toCenterX = center - enemy.position.x;
264
+ const toCenterY = center - enemy.position.y;
265
+ const toCenterDist = Math.sqrt(toCenterX * toCenterX + toCenterY * toCenterY) || 1;
266
+ const approachDirX = deltaX / normalizedDistance + (toCenterX / toCenterDist) * centerBias;
267
+ const approachDirY = deltaY / normalizedDistance + (toCenterY / toCenterDist) * centerBias;
268
+ const approachMag = Math.sqrt(approachDirX * approachDirX + approachDirY * approachDirY) || 1;
269
+ moveX += (approachDirX / approachMag) * approachSpeed;
270
+ moveY += (approachDirY / approachMag) * approachSpeed;
235
271
  }
236
272
  else if (distance < targetDistance - distanceDeadZone)
237
273
  {
238
274
  let retreatX = -(deltaX / normalizedDistance) * approachSpeed;
239
275
  let retreatY = -(deltaY / normalizedDistance) * approachSpeed;
240
- if (state.position.x < WALL_BUFFER && retreatX < 0) retreatX = 0;
241
- if (state.position.x > ARENA_SIZE - WALL_BUFFER && retreatX > 0)
276
+ if (position.x < ARENA_MIN + WALL_BUFFER && retreatX < 0) retreatX = 0;
277
+ if (position.x > ARENA_MAX - WALL_BUFFER && retreatX > 0)
242
278
  retreatX = 0;
243
- if (state.position.y < WALL_BUFFER && retreatY < 0) retreatY = 0;
244
- if (state.position.y > ARENA_SIZE - WALL_BUFFER && retreatY > 0)
279
+ if (position.y < ARENA_MIN + WALL_BUFFER && retreatY < 0) retreatY = 0;
280
+ if (position.y > ARENA_MAX - WALL_BUFFER && retreatY > 0)
245
281
  retreatY = 0;
246
282
  moveX += retreatX;
247
283
  moveY += retreatY;
@@ -250,44 +286,45 @@ export const Spellspinner: WizardFunction = ({state}) =>
250
286
 
251
287
  // Normalize
252
288
  const magnitude = Math.sqrt(moveX * moveX + moveY * moveY);
253
- if (magnitude > 100)
289
+ if (magnitude > 1)
254
290
  {
255
- moveX = (moveX / magnitude) * 100;
256
- moveY = (moveY / magnitude) * 100;
291
+ moveX = moveX / magnitude;
292
+ moveY = moveY / magnitude;
257
293
  }
258
294
 
259
295
  const move = {x: moveX, y: moveY};
296
+ const safeMove = keepOffWalls(position, move);
260
297
 
261
298
  // === DEFENSE: Handle channeling ===
262
- if (state.state === 'channeling')
299
+ if (status === 'channeling')
263
300
  {
264
301
  // Cancel shield if no imminent threats
265
302
  if (!urgentThreat || urgentThreat.ticksToImpact > shieldCancelThreshold)
266
303
  {
267
- return {move, cancel: true};
304
+ return cancel().move(safeMove.x, safeMove.y);
268
305
  }
269
- return {move: {x: 0, y: 0}}; // Hold shield if still threatened
306
+ return idle(); // Hold shield if still threatened
270
307
  }
271
308
 
272
309
  // === DEFENSE: Cancel missile cast if lethal threat arrives during GCD ===
273
310
  if (
274
- state.state === 'casting' &&
275
- state.castingSpell === 'missile' &&
311
+ status === 'casting' &&
312
+ castingSpell === 'missile' &&
276
313
  urgentThreat
277
314
  )
278
315
  {
279
- const remainingCast = (state.castDuration ?? 0) - (state.castProgress ?? 0);
316
+ const remainingCast = ((castProgressData?.total) ?? 0) - ((castProgressData?.current) ?? 0);
280
317
  const arrivesInGCD =
281
318
  urgentThreat.ticksToImpact > remainingCast &&
282
- urgentThreat.ticksToImpact <= remainingCast + GCD_DURATION;
319
+ urgentThreat.ticksToImpact <= remainingCast + RULES.GCD_DURATION;
283
320
 
284
321
  if (
285
322
  arrivesInGCD &&
286
- state.health <= urgentThreat.projectile.damage + 1 &&
323
+ health <= urgentThreat.projectile.damage + 1 &&
287
324
  remainingCast > 10
288
325
  )
289
326
  {
290
- return {move, cancel: true};
327
+ return cancel().move(safeMove.x, safeMove.y);
291
328
  }
292
329
  }
293
330
 
@@ -295,9 +332,9 @@ export const Spellspinner: WizardFunction = ({state}) =>
295
332
  // Blink to center (stays in combat range) and dodges the current missile.
296
333
  // Also blink when persistent threats block all offense (creates safe firing window).
297
334
  if (
298
- state.state === 'idle' &&
335
+ status === 'idle' &&
299
336
  urgentThreat &&
300
- state.blinkCooldown === 0 &&
337
+ blinkCooldown === 0 &&
301
338
  urgentThreat.ticksToImpact >= 10 &&
302
339
  urgentThreat.ticksToImpact <= blinkWindowMax
303
340
  )
@@ -309,18 +346,16 @@ export const Spellspinner: WizardFunction = ({state}) =>
309
346
  getMissileCastTime(QUICK_CONFIG),
310
347
  );
311
348
 
312
- if (undodgeableOrForced || cannotAttack)
349
+ if (undodgeableOrForced || cannotAttack)
313
350
  {
314
- return {
315
- move: {x: 0, y: 0},
316
- startCast: {spell: 'blink', target: getBlinkToCenter(state.position)},
317
- };
351
+ const blinkTarget = capBlinkDistance(getBlinkToCenter(position));
352
+ return blink(blinkTarget.x, blinkTarget.y);
318
353
  }
319
354
  }
320
355
 
321
356
  // === DEFENSE: Shield undodgeable/high-damage threats (when blink on cooldown) ===
322
357
  if (
323
- state.state === 'idle' &&
358
+ status === 'idle' &&
324
359
  urgentThreat &&
325
360
  (!urgentThreat.bestDodgeDirection || shouldForceShield(urgentThreat))
326
361
  )
@@ -329,70 +364,73 @@ export const Spellspinner: WizardFunction = ({state}) =>
329
364
  {
330
365
  if (urgentThreat.ticksToStartShield <= 3)
331
366
  {
332
- return {
333
- move: {x: 0, y: 0},
334
- startCast: {spell: 'shield'},
335
- };
367
+ return shield();
336
368
  }
337
369
  }
338
370
  }
339
371
 
372
+ // === Preemptive shield — shield when enemy is casting a missile at close range ===
373
+ if (
374
+ status === 'idle' &&
375
+ distance < 300 &&
376
+ enemy.status === 'casting' &&
377
+ enemy.castingSpell === 'missile'
378
+ )
379
+ {
380
+ const remainingCast = (enemy.castDuration ?? 0) - (enemy.castProgress ?? 0);
381
+ const estimatedFlightTicks = distance / 8;
382
+ const ticksUntilHit = remainingCast + estimatedFlightTicks;
383
+
384
+ if (
385
+ ticksUntilHit >= RULES.SHIELD_CAST_TIME &&
386
+ ticksUntilHit < RULES.SHIELD_CAST_TIME + 15
387
+ )
388
+ {
389
+ return shield();
390
+ }
391
+ }
392
+
340
393
  // === DISTANCE BLINK: enemy too close, blink to reestablish kiting range ===
341
394
  if (
342
- state.state === 'idle' &&
395
+ status === 'idle' &&
343
396
  distance < dangerDistance &&
344
- state.blinkCooldown === 0 &&
397
+ blinkCooldown === 0 &&
345
398
  (!urgentThreat || urgentThreat.bestDodgeDirection !== null)
346
399
  )
347
400
  {
348
401
  const blinkTarget = getBlinkToIncreaseDistance(
349
- state.position,
402
+ position,
350
403
  enemy.position,
351
- state.projectiles,
404
+ projectiles,
352
405
  MIN_BLINK_ESCAPE_DISTANCE,
353
406
  );
354
- if (blinkTarget)
407
+ if (blinkTarget)
355
408
  {
356
- return {
357
- move: {x: 0, y: 0},
358
- startCast: {spell: 'blink', target: blinkTarget},
359
- };
409
+ const capped = capBlinkDistance(blinkTarget);
410
+ return blink(capped.x, capped.y);
360
411
  }
361
412
  }
362
413
 
363
414
  // === OFFENSE: Attack when safe ===
364
- if (state.state === 'idle' && distance < 500)
415
+ if (status === 'idle' && distance < 500)
365
416
  {
417
+ const reposition = getPreCastReposition(position, enemy.position);
418
+ if (reposition) return moveAction(reposition.x, reposition.y);
419
+
366
420
  // Primary: standard homing missile
367
421
  const standardCastTime = getMissileCastTime(STANDARD_CONFIG);
368
422
  if (isSafeToAttack(threats, standardCastTime))
369
423
  {
370
- return {
371
- move,
372
- startCast: {
373
- spell: 'missile',
374
- config: STANDARD_CONFIG,
375
- missileAI: HomingAI,
376
- direction: angleTo(state.position, enemy.position),
377
- },
378
- };
424
+ return missile(STANDARD_CONFIG, HomingAI, angleTo(position, enemy.position)).move(safeMove.x, safeMove.y);
379
425
  }
380
426
 
381
427
  // Fallback: quick missile under pressure
382
428
  const quickCastTime = getMissileCastTime(QUICK_CONFIG);
383
429
  if (isSafeToAttack(threats, quickCastTime))
384
430
  {
385
- return {
386
- move,
387
- startCast: {
388
- spell: 'missile',
389
- config: QUICK_CONFIG,
390
- missileAI: HomingAI,
391
- direction: angleTo(state.position, enemy.position),
392
- },
393
- };
431
+ return missile(QUICK_CONFIG, HomingAI, angleTo(position, enemy.position)).move(safeMove.x, safeMove.y);
394
432
  }
395
433
  }
396
434
 
397
- return {move};
398
- };
435
+ return moveAction(safeMove.x, safeMove.y);
436
+ }