@vibemancer/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +28 -0
  2. package/dist/chunk-L7Z7OFXD.js +9140 -0
  3. package/dist/chunk-L7Z7OFXD.js.map +1 -0
  4. package/dist/index-browser.d.ts +2602 -0
  5. package/dist/index-browser.js +407 -0
  6. package/dist/index-browser.js.map +1 -0
  7. package/dist/index.d.ts +150 -0
  8. package/dist/index.js +750 -0
  9. package/dist/index.js.map +1 -0
  10. package/package.json +79 -0
  11. package/src/bots/berserker/01_Stormchaser.ts +457 -0
  12. package/src/bots/berserker/02_Stormcaller.ts +417 -0
  13. package/src/bots/berserker/03_Stormforger.ts +481 -0
  14. package/src/bots/caster/01_Flamecaller.ts +286 -0
  15. package/src/bots/caster/02_Pyromancer.ts +350 -0
  16. package/src/bots/caster/03_Infernalist.ts +492 -0
  17. package/src/bots/defensive/01_Turtle.ts +151 -0
  18. package/src/bots/defensive/02_Sentinel.ts +134 -0
  19. package/src/bots/defensive/03_Golem.ts +357 -0
  20. package/src/bots/duelist/01_Battlemage.ts +433 -0
  21. package/src/bots/duelist/02_Warmage.ts +438 -0
  22. package/src/bots/duelist/03_Archmage.ts +588 -0
  23. package/src/bots/homing/01_Bonemancer.ts +67 -0
  24. package/src/bots/homing/02_Lich.ts +356 -0
  25. package/src/bots/homing/03_Archlich.ts +220 -0
  26. package/src/bots/index.ts +30 -0
  27. package/src/bots/kiter/01_Spellspinner.ts +398 -0
  28. package/src/bots/kiter/02_Spellweaver.ts +378 -0
  29. package/src/bots/kiter/03_Spellbinder.ts +448 -0
  30. package/src/bots/melee/01_Shadowblade.ts +270 -0
  31. package/src/bots/melee/02_Nightblade.ts +437 -0
  32. package/src/bots/melee/03_Voidblade.ts +582 -0
  33. package/src/bots/registry.ts +207 -0
  34. package/src/bots/shared.ts +472 -0
  35. package/src/bots/sniper/01_Spellshot.ts +385 -0
  36. package/src/bots/sniper/02_Spelltracer.ts +441 -0
  37. package/src/bots/sniper/03_Spellseeker.ts +546 -0
  38. package/src/bots/standalone/Critter.ts +89 -0
  39. package/src/bots/standalone/Doombringer.ts +91 -0
  40. package/src/bots/standalone/Hogger.ts +228 -0
  41. package/src/bots/standalone/Rookie.ts +50 -0
  42. package/src/bots/standalone/TargetDummy.ts +21 -0
  43. package/src/bots/test/cheater.ts +405 -0
  44. package/src/bots/test/crasher.ts +81 -0
  45. package/src/engine/hooks-runtime.ts +394 -0
  46. package/src/engine/manual-match.ts +289 -0
  47. package/src/engine/missile-templates.ts +155 -0
  48. package/src/engine/optimizer.ts +220 -0
  49. package/src/engine/params-runtime.ts +189 -0
  50. package/src/engine/physics.ts +143 -0
  51. package/src/engine/sandbox-browser.ts +671 -0
  52. package/src/engine/sandbox-compile.ts +197 -0
  53. package/src/engine/sandbox-harness.ts +367 -0
  54. package/src/engine/sandbox.ts +332 -0
  55. package/src/engine/simulation.ts +828 -0
  56. package/src/engine/spells.ts +128 -0
  57. package/src/engine-version.ts +11 -0
  58. package/src/hooks/action-builders.ts +210 -0
  59. package/src/hooks/bot-wrapper.ts +84 -0
  60. package/src/hooks/index.ts +75 -0
  61. package/src/hooks/state-hooks.ts +354 -0
  62. package/src/hooks/threat-analysis.ts +365 -0
  63. package/src/hooks/types.ts +142 -0
  64. package/src/index-browser.ts +30 -0
  65. package/src/index.ts +24 -0
  66. package/src/rules.ts +254 -0
  67. package/src/stats.ts +262 -0
  68. package/src/testing.ts +207 -0
  69. package/src/trace.ts +430 -0
  70. package/src/types.ts +193 -0
  71. package/src/utils/angles.ts +47 -0
  72. package/src/utils/combat.ts +279 -0
  73. package/src/utils/distance.ts +21 -0
  74. package/src/utils/index.ts +7 -0
  75. package/src/utils/movement.ts +108 -0
  76. package/src/utils/random.ts +65 -0
  77. package/src/utils/spatial.ts +63 -0
  78. package/src/utils/targeting.ts +45 -0
@@ -0,0 +1,385 @@
1
+ import {WizardFunction, MissileAIFunction} from '../../types.js';
2
+ import {angleTo} from '../../utils/angles.js';
3
+ import {distanceTo} from '../../utils/distance.js';
4
+ import {ARENA_SIZE, GCD_DURATION, SHIELD_CAST_TIME} from '../../rules.js';
5
+ import {getMissileCastTime} from '../../utils/combat.js';
6
+ import {
7
+ getThreats,
8
+ getMostUrgentThreat,
9
+ getDodgeDirectionForMovement,
10
+ isSafeToAttack,
11
+ getBlinkToCenter,
12
+ getBlinkToIncreaseDistance,
13
+ MIN_BLINK_ESCAPE_DISTANCE,
14
+ shouldForceShield,
15
+ } from '../shared.js';
16
+ import {useParam} from '../../engine/params-runtime.js';
17
+
18
+ const HomingAI: MissileAIFunction = ({worldState}) =>
19
+ {
20
+ const enemy = worldState.enemies[0];
21
+ return {turnToward: enemy?.position};
22
+ };
23
+ const WALL_BUFFER = 50;
24
+
25
+ /**
26
+ * Smart combat movement: dodge using threat analysis, avoid walls, commit to direction.
27
+ */
28
+ function combatMove(
29
+ position: {x: number; y: number},
30
+ enemy: {position: {x: number; y: number}},
31
+ currentDistance: number,
32
+ minDistance: number,
33
+ maxDistance: number,
34
+ dodgeDirection: {x: number; y: number} | null,
35
+ tick: number,
36
+ strafeCyclePeriod: number,
37
+ strafeIntensity: number,
38
+ approachSpeed: number,
39
+ ): {x: number; y: number}
40
+ {
41
+ const deltaX = enemy.position.x - position.x;
42
+ const deltaY = enemy.position.y - position.y;
43
+ const normalizedDistance = currentDistance > 0 ? currentDistance : 1;
44
+
45
+ const strafeClockwise = {
46
+ x: -deltaY / normalizedDistance,
47
+ y: deltaX / normalizedDistance,
48
+ };
49
+ const strafeCounterClockwise = {
50
+ x: deltaY / normalizedDistance,
51
+ y: -deltaX / normalizedDistance,
52
+ };
53
+
54
+ let strafeDirection: {x: number; y: number};
55
+
56
+ // Priority 1: Use dodge direction (from threat analysis or close missile avoidance)
57
+ if (dodgeDirection)
58
+ {
59
+ strafeDirection = dodgeDirection;
60
+ }
61
+ // Priority 2: Avoid walls
62
+ else if (
63
+ position.x < WALL_BUFFER ||
64
+ position.x > ARENA_SIZE - WALL_BUFFER ||
65
+ position.y < WALL_BUFFER ||
66
+ position.y > ARENA_SIZE - WALL_BUFFER
67
+ )
68
+ {
69
+ const futureClockwise = {
70
+ x: position.x + strafeClockwise.x * 100,
71
+ y: position.y + strafeClockwise.y * 100,
72
+ };
73
+ const futureCounterClockwise = {
74
+ x: position.x + strafeCounterClockwise.x * 100,
75
+ y: position.y + strafeCounterClockwise.y * 100,
76
+ };
77
+ const scoreClockwise = Math.min(
78
+ futureClockwise.x,
79
+ ARENA_SIZE - futureClockwise.x,
80
+ futureClockwise.y,
81
+ ARENA_SIZE - futureClockwise.y,
82
+ );
83
+ const scoreCounterClockwise = Math.min(
84
+ futureCounterClockwise.x,
85
+ ARENA_SIZE - futureCounterClockwise.x,
86
+ futureCounterClockwise.y,
87
+ ARENA_SIZE - futureCounterClockwise.y,
88
+ );
89
+ strafeDirection =
90
+ scoreClockwise > scoreCounterClockwise
91
+ ? strafeClockwise
92
+ : strafeCounterClockwise;
93
+ }
94
+ // Priority 3: Alternate strafe direction
95
+ else
96
+ {
97
+ const cycle = Math.floor(tick / strafeCyclePeriod) % 2;
98
+ strafeDirection = cycle === 0 ? strafeClockwise : strafeCounterClockwise;
99
+ }
100
+
101
+ // When actively dodging, commit fully — no approach/retreat dilution
102
+ if (dodgeDirection)
103
+ {
104
+ return {x: strafeDirection.x * 100, y: strafeDirection.y * 100};
105
+ }
106
+
107
+ let moveX = strafeDirection.x * strafeIntensity;
108
+ let moveY = strafeDirection.y * strafeIntensity;
109
+
110
+ // Distance management (only when not dodging)
111
+ if (currentDistance > maxDistance)
112
+ {
113
+ moveX += (deltaX / normalizedDistance) * approachSpeed;
114
+ moveY += (deltaY / normalizedDistance) * approachSpeed;
115
+ }
116
+ else if (currentDistance < minDistance)
117
+ {
118
+ let retreatX = -(deltaX / normalizedDistance) * approachSpeed;
119
+ let retreatY = -(deltaY / normalizedDistance) * approachSpeed;
120
+ if (position.x < WALL_BUFFER && retreatX < 0) retreatX = 0;
121
+ if (position.x > ARENA_SIZE - WALL_BUFFER && retreatX > 0) retreatX = 0;
122
+ if (position.y < WALL_BUFFER && retreatY < 0) retreatY = 0;
123
+ if (position.y > ARENA_SIZE - WALL_BUFFER && retreatY > 0) retreatY = 0;
124
+ moveX += retreatX;
125
+ moveY += retreatY;
126
+ }
127
+
128
+ const magnitude = Math.sqrt(moveX * moveX + moveY * moveY);
129
+ if (magnitude > 100)
130
+ {
131
+ moveX = (moveX / magnitude) * 100;
132
+ moveY = (moveY / magnitude) * 100;
133
+ }
134
+
135
+ return {x: moveX, y: moveY};
136
+ }
137
+
138
+ /**
139
+ * Bot: Spellshot
140
+ *
141
+ * BEHAVIOR: Uses interceptAngle to calculate where the enemy will be and fires
142
+ * fast, non-homing missiles (speed 8, turnRate 0) along the predicted path.
143
+ * Strafes at medium range (300-400), shields undodgeable threats, emergency
144
+ * blinks when shield isn't available. The key mechanic is PREDICTION — these
145
+ * missiles don't track, they go exactly where you calculated the enemy would be.
146
+ *
147
+ * NAMING RATIONALE: "Spellshot" — a spell that is a single, precisely aimed shot.
148
+ * Like a sniper's "called shot" but magical. The defining feature is the intercept
149
+ * calculation: this bot doesn't fire tracking missiles, it calculates the exact
150
+ * angle needed to hit a moving target. "Shot" implies precision, singular impact,
151
+ * and skill-based aiming — everything this bot is about.
152
+ *
153
+ * PROGRESSION LINE: Spellshot → Spelltracer → Spellseeker
154
+ * - Spellshot (tier 1): Basic intercept prediction, non-homing missiles
155
+ * - Spelltracer (tier 2): Future — predictive homing (missiles that lead AND track)
156
+ * - Spellseeker (tier 3): Future — perfect prediction, multi-angle attacks
157
+ * The naming progression: shot (single bullet) → tracer (bullet that tracks a path)
158
+ * → seeker (actively hunts). Each tier adds more intelligence to the projectile,
159
+ * evolving from "I calculate where you'll be" to "my missile calculates where you'll be."
160
+ *
161
+ * NOTE: A separate future archetype "Spellslinger" (volume-of-fire) is reserved
162
+ * for a rapid-fire bot that prioritizes quantity over prediction.
163
+ *
164
+ * TIER: 1 (base)
165
+ */
166
+ export const Spellshot: WizardFunction = ({state}) =>
167
+ {
168
+ const dangerDistance = useParam('dangerDistance', 45, {range: 100, min: 0});
169
+ const targetDistance = useParam('targetDistance', 339, {
170
+ range: 150,
171
+ min: 0,
172
+ });
173
+ const distanceDeadZone = useParam('distanceDeadZone', 22, {
174
+ range: 50,
175
+ min: 0,
176
+ });
177
+ const minCombatDistance = targetDistance - distanceDeadZone;
178
+ const maxCombatDistance = targetDistance + distanceDeadZone;
179
+ const strafeCyclePeriod = useParam('strafeCyclePeriod', 230, {
180
+ range: 75,
181
+ min: 80,
182
+ max: 300,
183
+ });
184
+ const strafeIntensity = useParam('strafeIntensity', 36, {
185
+ range: 40,
186
+ min: 20,
187
+ max: 100,
188
+ steps: 7,
189
+ });
190
+ const approachSpeed = useParam('approachSpeed', 78, {
191
+ range: 30,
192
+ min: 10,
193
+ max: 80,
194
+ steps: 7,
195
+ });
196
+ const shieldCancelThreshold = useParam('shieldCancelThreshold', 150, {
197
+ range: 75,
198
+ min: 50,
199
+ max: 300,
200
+ });
201
+ const missileDamage = useParam('missileDamage', 30, {
202
+ range: 10,
203
+ min: 5,
204
+ max: 30,
205
+ steps: 5,
206
+ });
207
+ const missileSpeed = useParam('missileSpeed', 12, {
208
+ range: 3,
209
+ min: 3,
210
+ max: 12,
211
+ steps: 5,
212
+ });
213
+ const missileTurnRate = useParam('missileTurnRate', 0.3, {
214
+ range: 1.5,
215
+ steps: 5,
216
+ });
217
+ const missileMinDuration = useParam('missileMinDuration', 55, {
218
+ range: 30,
219
+ min: 20,
220
+ max: 100,
221
+ steps: 7,
222
+ });
223
+ const durationRangeFactor = useParam('durationRangeFactor', 0.67, {
224
+ range: 0.5,
225
+ min: 0.5,
226
+ max: 2.0,
227
+ steps: 5,
228
+ });
229
+
230
+ const enemy = state.enemies[0];
231
+ if (!enemy)
232
+ {
233
+ return {move: {x: 0, y: 0}};
234
+ }
235
+
236
+ const distance = distanceTo(state.position, enemy.position);
237
+
238
+ // Analyze threats using simulation
239
+ const threats = getThreats(state);
240
+ const urgentThreat = getMostUrgentThreat(threats);
241
+
242
+ // === DEFENSE: Handle channeling ===
243
+ if (state.state === 'channeling')
244
+ {
245
+ if (!urgentThreat || urgentThreat.ticksToImpact > shieldCancelThreshold)
246
+ {
247
+ return {move: {x: 0, y: 0}, cancel: true};
248
+ }
249
+ return {move: {x: 0, y: 0}};
250
+ }
251
+
252
+ // === DEFENSE: Cancel missile cast if dangerous threat will arrive before we can shield ===
253
+ // Only cancel for: high-damage homing (always) or undodgeable when health is at risk
254
+ if (
255
+ state.state === 'casting' &&
256
+ state.castingSpell === 'missile' &&
257
+ urgentThreat &&
258
+ (!urgentThreat.bestDodgeDirection || shouldForceShield(urgentThreat))
259
+ )
260
+ {
261
+ const mustCancel =
262
+ shouldForceShield(urgentThreat) ||
263
+ state.health <= urgentThreat.projectile.damage * 2 + 1;
264
+ if (mustCancel)
265
+ {
266
+ const remainingCast =
267
+ (state.castDuration ?? 0) - (state.castProgress ?? 0);
268
+ if (
269
+ urgentThreat.ticksToImpact <=
270
+ remainingCast + GCD_DURATION + SHIELD_CAST_TIME + 3
271
+ )
272
+ {
273
+ const dodgeDir = getDodgeDirectionForMovement(threats, state.position);
274
+ const cancelMove = combatMove(
275
+ state.position,
276
+ enemy,
277
+ distance,
278
+ minCombatDistance,
279
+ maxCombatDistance,
280
+ dodgeDir,
281
+ state.tick,
282
+ strafeCyclePeriod,
283
+ strafeIntensity,
284
+ approachSpeed,
285
+ );
286
+ return {move: cancelMove, cancel: true};
287
+ }
288
+ }
289
+ }
290
+
291
+ // === DEFENSE: Shield undodgeable or high-damage homing threats ===
292
+ if (
293
+ state.state === 'idle' &&
294
+ urgentThreat &&
295
+ (!urgentThreat.bestDodgeDirection || shouldForceShield(urgentThreat))
296
+ )
297
+ {
298
+ if (urgentThreat.canBlockInTime)
299
+ {
300
+ // Start shield at the right time (small buffer for safety)
301
+ if (urgentThreat.ticksToStartShield <= 3)
302
+ {
303
+ return {
304
+ move: {x: 0, y: 0},
305
+ startCast: {spell: 'shield'},
306
+ };
307
+ }
308
+ }
309
+ // Can't dodge, can't shield → blink away (emergency)
310
+ else if (state.blinkCooldown === 0)
311
+ {
312
+ return {
313
+ move: {x: 0, y: 0},
314
+ startCast: {spell: 'blink', target: getBlinkToCenter(state.position)},
315
+ };
316
+ }
317
+ }
318
+
319
+ // === DISTANCE BLINK: enemy too close, blink to reestablish kiting range ===
320
+ if (
321
+ state.state === 'idle' &&
322
+ distance < dangerDistance &&
323
+ state.blinkCooldown === 0 &&
324
+ (!urgentThreat || urgentThreat.bestDodgeDirection !== null)
325
+ )
326
+ {
327
+ const blinkTarget = getBlinkToIncreaseDistance(
328
+ state.position,
329
+ enemy.position,
330
+ state.projectiles,
331
+ MIN_BLINK_ESCAPE_DISTANCE,
332
+ );
333
+ if (blinkTarget)
334
+ {
335
+ return {
336
+ move: {x: 0, y: 0},
337
+ startCast: {spell: 'blink', target: blinkTarget},
338
+ };
339
+ }
340
+ }
341
+
342
+ // Smart combat movement
343
+ const dodgeDirection = getDodgeDirectionForMovement(threats, state.position);
344
+ const move = combatMove(
345
+ state.position,
346
+ enemy,
347
+ distance,
348
+ minCombatDistance,
349
+ maxCombatDistance,
350
+ dodgeDirection,
351
+ state.tick,
352
+ strafeCyclePeriod,
353
+ strafeIntensity,
354
+ approachSpeed,
355
+ );
356
+
357
+ // === OFFENSE: Fast homing missiles ===
358
+ if (state.state === 'idle' && distance < 600)
359
+ {
360
+ const missileConfig = {
361
+ damage: missileDamage,
362
+ speed: missileSpeed,
363
+ turnRate: missileTurnRate,
364
+ duration: Math.max(
365
+ missileMinDuration,
366
+ Math.ceil((distance * durationRangeFactor) / missileSpeed),
367
+ ),
368
+ };
369
+ const castTime = getMissileCastTime(missileConfig, state.lastMissileConfig);
370
+ if (isSafeToAttack(threats, castTime))
371
+ {
372
+ return {
373
+ move,
374
+ startCast: {
375
+ spell: 'missile',
376
+ config: missileConfig,
377
+ missileAI: HomingAI,
378
+ direction: angleTo(state.position, enemy.position),
379
+ },
380
+ };
381
+ }
382
+ }
383
+
384
+ return {move};
385
+ };