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