@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,433 @@
1
+ import {WizardFunction, MissileAIFunction} from '../../types.js';
2
+ import {angleTo} from '../../utils/angles.js';
3
+ import {distanceTo} from '../../utils/distance.js';
4
+ import {getMissileCastTime} from '../../utils/combat.js';
5
+ import {ARENA_SIZE, GCD_DURATION, SHIELD_CAST_TIME} from '../../rules.js';
6
+ import {
7
+ getThreats,
8
+ getMostUrgentThreat,
9
+ getDodgeDirectionForMovement,
10
+ isSafeToAttack,
11
+ getBlinkToDecreaseDistance,
12
+ shouldForceShield,
13
+ } from '../shared.js';
14
+ import {useParam} from '../../engine/params-runtime.js';
15
+ const WALL_BUFFER = 60;
16
+
17
+ const QUICK_CONFIG = {damage: 15, speed: 5, turnRate: 1, duration: 70};
18
+ const HEAVY_CONFIG = {damage: 15, speed: 5, turnRate: 1, duration: 150};
19
+
20
+ // Missile effective range (speed * duration). Use HEAVY when QUICK can't reach.
21
+ const QUICK_RANGE = QUICK_CONFIG.speed * QUICK_CONFIG.duration; // 350
22
+
23
+ /**
24
+ * Standard homing missile AI.
25
+ */
26
+ const StandardHoming: MissileAIFunction = ({worldState}) =>
27
+ {
28
+ const enemy = worldState.enemies[0];
29
+ return {
30
+ turnToward: enemy?.position,
31
+ };
32
+ };
33
+
34
+ /**
35
+ * Check if near wall.
36
+ */
37
+ function isNearWall(position: {x: number; y: number}): boolean
38
+ {
39
+ return (
40
+ position.x < WALL_BUFFER ||
41
+ position.x > ARENA_SIZE - WALL_BUFFER ||
42
+ position.y < WALL_BUFFER ||
43
+ position.y > ARENA_SIZE - WALL_BUFFER
44
+ );
45
+ }
46
+
47
+ /**
48
+ * Smart combat movement with threat-aware dodging.
49
+ */
50
+ function combatMove(
51
+ position: {x: number; y: number},
52
+ enemy: {position: {x: number; y: number}},
53
+ currentDistance: number,
54
+ targetDistance: number,
55
+ dodgeDirection: {x: number; y: number} | null,
56
+ tick: number,
57
+ strafeCyclePeriod: number,
58
+ strafeIntensity: number,
59
+ approachSpeed: number,
60
+ distanceDeadZone: number,
61
+ ): {x: number; y: number}
62
+ {
63
+ const deltaX = enemy.position.x - position.x;
64
+ const deltaY = enemy.position.y - position.y;
65
+ const normalizedDistance = currentDistance > 0 ? currentDistance : 1;
66
+
67
+ // Perpendicular directions
68
+ const strafeClockwise = {
69
+ x: -deltaY / normalizedDistance,
70
+ y: deltaX / normalizedDistance,
71
+ };
72
+ const strafeCounterClockwise = {
73
+ x: deltaY / normalizedDistance,
74
+ y: -deltaX / normalizedDistance,
75
+ };
76
+
77
+ let strafeDirection: {x: number; y: number};
78
+
79
+ // Priority 1: Use dodge direction (from threat analysis or close missile avoidance)
80
+ if (dodgeDirection)
81
+ {
82
+ strafeDirection = dodgeDirection;
83
+ }
84
+ // Priority 2: Avoid walls
85
+ else if (isNearWall(position))
86
+ {
87
+ const futureClockwise = {
88
+ x: position.x + strafeClockwise.x * 100,
89
+ y: position.y + strafeClockwise.y * 100,
90
+ };
91
+ const futureCounterClockwise = {
92
+ x: position.x + strafeCounterClockwise.x * 100,
93
+ y: position.y + strafeCounterClockwise.y * 100,
94
+ };
95
+ const scoreClockwise = Math.min(
96
+ futureClockwise.x,
97
+ ARENA_SIZE - futureClockwise.x,
98
+ futureClockwise.y,
99
+ ARENA_SIZE - futureClockwise.y,
100
+ );
101
+ const scoreCounterClockwise = Math.min(
102
+ futureCounterClockwise.x,
103
+ ARENA_SIZE - futureCounterClockwise.x,
104
+ futureCounterClockwise.y,
105
+ ARENA_SIZE - futureCounterClockwise.y,
106
+ );
107
+ strafeDirection =
108
+ scoreClockwise > scoreCounterClockwise
109
+ ? strafeClockwise
110
+ : strafeCounterClockwise;
111
+ }
112
+ // Priority 3: Commit to direction
113
+ else
114
+ {
115
+ const cycle = Math.floor(tick / strafeCyclePeriod) % 2;
116
+ strafeDirection = cycle === 0 ? strafeClockwise : strafeCounterClockwise;
117
+ }
118
+
119
+ // When actively dodging, commit fully — no approach/retreat dilution
120
+ if (dodgeDirection)
121
+ {
122
+ return {x: strafeDirection.x * 100, y: strafeDirection.y * 100};
123
+ }
124
+
125
+ let moveX = strafeDirection.x * strafeIntensity;
126
+ let moveY = strafeDirection.y * strafeIntensity;
127
+
128
+ // Distance management (only when not dodging)
129
+ if (currentDistance > targetDistance + distanceDeadZone)
130
+ {
131
+ moveX += (deltaX / normalizedDistance) * approachSpeed;
132
+ moveY += (deltaY / normalizedDistance) * approachSpeed;
133
+ }
134
+ else if (currentDistance < targetDistance - distanceDeadZone)
135
+ {
136
+ let retreatX = -(deltaX / normalizedDistance) * approachSpeed;
137
+ let retreatY = -(deltaY / normalizedDistance) * approachSpeed;
138
+ if (position.x < WALL_BUFFER && retreatX < 0) retreatX = 0;
139
+ if (position.x > ARENA_SIZE - WALL_BUFFER && retreatX > 0) retreatX = 0;
140
+ if (position.y < WALL_BUFFER && retreatY < 0) retreatY = 0;
141
+ if (position.y > ARENA_SIZE - WALL_BUFFER && retreatY > 0) retreatY = 0;
142
+ moveX += retreatX;
143
+ moveY += retreatY;
144
+ }
145
+
146
+ // Normalize
147
+ const magnitude = Math.sqrt(moveX * moveX + moveY * moveY);
148
+ if (magnitude > 100)
149
+ {
150
+ moveX = (moveX / magnitude) * 100;
151
+ moveY = (moveY / magnitude) * 100;
152
+ }
153
+
154
+ return {x: moveX, y: moveY};
155
+ }
156
+
157
+ /**
158
+ * Bot: Battlemage
159
+ *
160
+ * BEHAVIOR: Balanced mid-range duelist. Shields undodgeable threats, interrupts
161
+ * enemy casts with quick missiles, saves blink for emergencies OR gap-closing.
162
+ * Switches between quick (10 dmg, fast) and heavy (15 dmg, slow) missile configs
163
+ * based on safety window and range. Will aggressively trade hits when health allows.
164
+ * The unique trait is cast-interruption: fires quick missiles specifically when the
165
+ * enemy is casting, punishing long cast times.
166
+ *
167
+ * PROGRESSION LINE: Battlemage → Warmage → Archmage
168
+ * - Battlemage (tier 1): Quick/heavy fixed configs, cast interruption, basic defense
169
+ * - Warmage (tier 2): Adaptive missile fitting (fitMissileToBudget), smarter attacks
170
+ * - Archmage (tier 3): Future — supreme duelist, perfect tactical mastery
171
+ *
172
+ * TIER: 1 (base)
173
+ */
174
+ export const Battlemage: WizardFunction = ({state}) =>
175
+ {
176
+ const targetDistance = useParam('targetDistance', 274, {
177
+ range: 150,
178
+ min: 150,
179
+ });
180
+ const strafeCyclePeriod = useParam('strafeCyclePeriod', 219, {
181
+ range: 75,
182
+ min: 80,
183
+ max: 300,
184
+ });
185
+ const strafeIntensity = useParam('strafeIntensity', 36, {
186
+ range: 40,
187
+ min: 20,
188
+ max: 100,
189
+ steps: 7,
190
+ });
191
+ const approachSpeed = useParam('approachSpeed', 80, {
192
+ range: 30,
193
+ min: 10,
194
+ max: 80,
195
+ steps: 7,
196
+ });
197
+ const distanceDeadZone = useParam('distanceDeadZone', 31, {
198
+ range: 30,
199
+ min: 10,
200
+ max: 80,
201
+ steps: 7,
202
+ });
203
+ const shieldCancelThreshold = useParam('shieldCancelThreshold', 150, {
204
+ range: 75,
205
+ min: 50,
206
+ max: 300,
207
+ });
208
+ const blinkWindowMax = useParam('blinkWindowMax', 35, {
209
+ range: 30,
210
+ min: 15,
211
+ max: 80,
212
+ steps: 7,
213
+ });
214
+ const blinkDistanceOffset = useParam('blinkDistanceOffset', 100, {
215
+ range: 40,
216
+ min: 0,
217
+ max: 100,
218
+ steps: 7,
219
+ });
220
+ const cancelMinRemainingCast = useParam('cancelMinRemainingCast', 19, {
221
+ range: 30,
222
+ min: 5,
223
+ max: 100,
224
+ steps: 7,
225
+ });
226
+ const interruptRange = useParam('interruptRange', 324, {
227
+ range: 150,
228
+ min: 150,
229
+ max: 600,
230
+ steps: 7,
231
+ });
232
+ const attackRange = useParam('attackRange', 556, {
233
+ range: 150,
234
+ min: 200,
235
+ max: 700,
236
+ });
237
+
238
+ const enemy = state.enemies[0];
239
+ if (!enemy)
240
+ {
241
+ return {move: {x: 0, y: 0}};
242
+ }
243
+
244
+ const distance = distanceTo(state.position, enemy.position);
245
+
246
+ // Analyze threats using simulation
247
+ const threats = getThreats(state);
248
+ const urgentThreat = getMostUrgentThreat(threats);
249
+
250
+ // === DEFENSE: Handle channeling ===
251
+ if (state.state === 'channeling')
252
+ {
253
+ // Cancel shield if no imminent threats
254
+ if (!urgentThreat || urgentThreat.ticksToImpact > shieldCancelThreshold)
255
+ {
256
+ return {move: {x: 0, y: 0}, cancel: true};
257
+ }
258
+ return {move: {x: 0, y: 0}};
259
+ }
260
+
261
+ // === DEFENSE: Cancel long missile casts if undodgeable threat approaches ===
262
+ // Only cancel when health is too low to trade (aggressive casts accepted the risk).
263
+ if (
264
+ state.state === 'casting' &&
265
+ state.castingSpell === 'missile' &&
266
+ urgentThreat &&
267
+ !urgentThreat.bestDodgeDirection
268
+ )
269
+ {
270
+ const remainingCast = (state.castDuration ?? 0) - (state.castProgress ?? 0);
271
+ if (
272
+ remainingCast > cancelMinRemainingCast &&
273
+ urgentThreat.ticksToImpact <=
274
+ remainingCast + GCD_DURATION + SHIELD_CAST_TIME + 1 &&
275
+ state.health <= urgentThreat.projectile.damage * 2 + 1
276
+ )
277
+ {
278
+ const dodgeDirection = getDodgeDirectionForMovement(
279
+ threats,
280
+ state.position,
281
+ );
282
+ const move = combatMove(
283
+ state.position,
284
+ enemy,
285
+ distance,
286
+ targetDistance,
287
+ dodgeDirection,
288
+ state.tick,
289
+ strafeCyclePeriod,
290
+ strafeIntensity,
291
+ approachSpeed,
292
+ distanceDeadZone,
293
+ );
294
+ return {move, cancel: true};
295
+ }
296
+ }
297
+
298
+ // === BLINK: Dodge missile + close gap simultaneously (first priority) ===
299
+ // Blink both dodges the incoming missile AND closes distance — better than shielding.
300
+ // Only shield when blink is on cooldown.
301
+ if (
302
+ state.state === 'idle' &&
303
+ urgentThreat &&
304
+ state.blinkCooldown === 0 &&
305
+ distance > targetDistance - blinkDistanceOffset &&
306
+ urgentThreat.ticksToImpact >= 10 &&
307
+ urgentThreat.ticksToImpact <= blinkWindowMax
308
+ )
309
+ {
310
+ const blinkTarget =
311
+ getBlinkToDecreaseDistance(
312
+ state.position,
313
+ enemy.position,
314
+ state.projectiles,
315
+ ) ?? enemy.position;
316
+ return {
317
+ move: {x: 0, y: 0},
318
+ startCast: {spell: 'blink', target: blinkTarget},
319
+ };
320
+ }
321
+
322
+ // === DEFENSE: Shield undodgeable or high-damage homing threats (only when blink can't handle it) ===
323
+ if (
324
+ state.state === 'idle' &&
325
+ urgentThreat &&
326
+ (!urgentThreat.bestDodgeDirection || shouldForceShield(urgentThreat))
327
+ )
328
+ {
329
+ if (
330
+ urgentThreat.canBlockInTime &&
331
+ urgentThreat.ticksToStartShield <= 3 &&
332
+ (state.blinkCooldown > 0 || urgentThreat.ticksToImpact < 10)
333
+ )
334
+ {
335
+ return {
336
+ move: {x: 0, y: 0},
337
+ startCast: {spell: 'shield'},
338
+ };
339
+ }
340
+ }
341
+
342
+ // Movement: smart strafe + maintain optimal distance
343
+ const dodgeDirection = getDodgeDirectionForMovement(threats, state.position);
344
+ const move = combatMove(
345
+ state.position,
346
+ enemy,
347
+ distance,
348
+ targetDistance,
349
+ dodgeDirection,
350
+ state.tick,
351
+ strafeCyclePeriod,
352
+ strafeIntensity,
353
+ approachSpeed,
354
+ distanceDeadZone,
355
+ );
356
+
357
+ // === OFFENSE ===
358
+ if (state.state === 'idle')
359
+ {
360
+ // Interrupt if enemy is casting (quick attack)
361
+ if (enemy.state === 'casting' && distance < interruptRange)
362
+ {
363
+ const castTime = getMissileCastTime(QUICK_CONFIG);
364
+ if (isSafeToAttack(threats, castTime))
365
+ {
366
+ return {
367
+ move,
368
+ startCast: {
369
+ spell: 'missile',
370
+ config: QUICK_CONFIG,
371
+ missileAI: StandardHoming,
372
+ direction: angleTo(state.position, enemy.position),
373
+ },
374
+ };
375
+ }
376
+ }
377
+
378
+ // Standard attacks at combat range
379
+ if (distance < attackRange)
380
+ {
381
+ const heavyCastTime = getMissileCastTime(HEAVY_CONFIG);
382
+ if (isSafeToAttack(threats, heavyCastTime))
383
+ {
384
+ return {
385
+ move,
386
+ startCast: {
387
+ spell: 'missile',
388
+ config: HEAVY_CONFIG,
389
+ missileAI: StandardHoming,
390
+ direction: angleTo(state.position, enemy.position),
391
+ },
392
+ };
393
+ }
394
+
395
+ // Fallback: quick homing missile when heavy isn't safe (only if in range)
396
+ const quickCastTime = getMissileCastTime(QUICK_CONFIG);
397
+ if (
398
+ distance < QUICK_RANGE * 0.9 &&
399
+ isSafeToAttack(threats, quickCastTime)
400
+ )
401
+ {
402
+ return {
403
+ move,
404
+ startCast: {
405
+ spell: 'missile',
406
+ config: QUICK_CONFIG,
407
+ missileAI: StandardHoming,
408
+ direction: angleTo(state.position, enemy.position),
409
+ },
410
+ };
411
+ }
412
+
413
+ // Not safe to attack — wait for threat to pass, then fire
414
+ }
415
+ }
416
+
417
+ // === FALLBACK DEFENSE: Shield when no attack window exists ===
418
+ if (
419
+ state.state === 'idle' &&
420
+ urgentThreat &&
421
+ !urgentThreat.bestDodgeDirection &&
422
+ urgentThreat.canBlockInTime &&
423
+ urgentThreat.ticksToStartShield <= 3
424
+ )
425
+ {
426
+ return {
427
+ move: {x: 0, y: 0},
428
+ startCast: {spell: 'shield'},
429
+ };
430
+ }
431
+
432
+ return {move};
433
+ };