@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,398 @@
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} from '../../rules.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
+ const WALL_BUFFER = 60;
18
+
19
+ /**
20
+ * Standard homing missile: higher damage, good range. s=5 × dur=150 = 750u range.
21
+ */
22
+ const STANDARD_CONFIG = {damage: 15, speed: 5, turnRate: 1, duration: 150};
23
+
24
+ /**
25
+ * Quick homing missile for firing under pressure.
26
+ * Similar speed/turn for warmup compatibility.
27
+ */
28
+ const QUICK_CONFIG = {damage: 10, speed: 5, turnRate: 1, duration: 80};
29
+
30
+ /**
31
+ * Standard homing missile AI.
32
+ */
33
+ const HomingAI: MissileAIFunction = ({worldState}) =>
34
+ {
35
+ const enemy = worldState.enemies[0];
36
+ return {
37
+ turnToward: enemy?.position,
38
+ };
39
+ };
40
+
41
+ /**
42
+ * Check if near wall.
43
+ */
44
+ function isNearWall(position: {x: number; y: number}): boolean
45
+ {
46
+ 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
51
+ );
52
+ }
53
+
54
+ /**
55
+ * Smart strafe with threat-aware dodging and wall avoidance.
56
+ */
57
+ function smartStrafe(
58
+ position: {x: number; y: number},
59
+ enemy: {position: {x: number; y: number}},
60
+ dodgeDirection: {x: number; y: number} | null,
61
+ tick: number,
62
+ strafeCyclePeriod: number,
63
+ strafeIntensity: number,
64
+ ): {x: number; y: number}
65
+ {
66
+ const deltaX = enemy.position.x - position.x;
67
+ const deltaY = enemy.position.y - position.y;
68
+ const normalizedDistance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
69
+ if (normalizedDistance === 0) return {x: 0, y: 0};
70
+
71
+ // Perpendicular directions
72
+ const strafeClockwise = {
73
+ x: -deltaY / normalizedDistance,
74
+ y: deltaX / normalizedDistance,
75
+ };
76
+ const strafeCounterClockwise = {
77
+ x: deltaY / normalizedDistance,
78
+ y: -deltaX / normalizedDistance,
79
+ };
80
+
81
+ let strafeDir: {x: number; y: number};
82
+
83
+ // Priority 1: Use dodge direction (from threat analysis or close missile avoidance)
84
+ if (dodgeDirection)
85
+ {
86
+ strafeDir = dodgeDirection;
87
+ }
88
+ // Priority 2: Avoid walls
89
+ else if (isNearWall(position))
90
+ {
91
+ const futureClockwise = {
92
+ x: position.x + strafeClockwise.x * 100,
93
+ y: position.y + strafeClockwise.y * 100,
94
+ };
95
+ const futureCounterClockwise = {
96
+ x: position.x + strafeCounterClockwise.x * 100,
97
+ y: position.y + strafeCounterClockwise.y * 100,
98
+ };
99
+ const scoreClockwise = Math.min(
100
+ futureClockwise.x,
101
+ ARENA_SIZE - futureClockwise.x,
102
+ futureClockwise.y,
103
+ ARENA_SIZE - futureClockwise.y,
104
+ );
105
+ const scoreCounterClockwise = Math.min(
106
+ futureCounterClockwise.x,
107
+ ARENA_SIZE - futureCounterClockwise.x,
108
+ futureCounterClockwise.y,
109
+ ARENA_SIZE - futureCounterClockwise.y,
110
+ );
111
+ strafeDir =
112
+ scoreClockwise > scoreCounterClockwise
113
+ ? strafeClockwise
114
+ : strafeCounterClockwise;
115
+ }
116
+ // Priority 3: Commit to direction (switch every ~200 ticks)
117
+ else
118
+ {
119
+ const cycle = Math.floor(tick / strafeCyclePeriod) % 2;
120
+ strafeDir = cycle === 0 ? strafeClockwise : strafeCounterClockwise;
121
+ }
122
+
123
+ // When actively dodging, commit fully — no approach/retreat dilution
124
+ const intensity = dodgeDirection ? 100 : strafeIntensity;
125
+ return {x: strafeDir.x * intensity, y: strafeDir.y * intensity};
126
+ }
127
+
128
+ /**
129
+ * Bot: Spellspinner
130
+ *
131
+ * BEHAVIOR: Maintains medium range (350 units), strafes constantly to dodge
132
+ * missiles, and fires homing missiles (damage 10, speed 4, turnRate 2). Heavy
133
+ * emphasis on movement — 80% strafe intensity when not dodging, 100% when dodging.
134
+ * Shields only undodgeable threats, emergency blinks when shield isn't available.
135
+ * The constant circular strafing motion traces patterns like thread being spun.
136
+ *
137
+ * NAMING RATIONALE: Like a spider spinning a web of projectiles while circling its
138
+ * prey. The constant strafing movement pattern traces circles — spinning thread
139
+ * around the arena. "Spell" + "spinner" = a wizard who spins spells around the
140
+ * battlefield. The kiting behavior (maintaining distance while attacking) creates
141
+ * a web-like pattern of missiles and movement that traps opponents.
142
+ *
143
+ * PROGRESSION LINE: Spellspinner → Spellweaver → Spellbinder
144
+ * - Spellspinner (tier 1): Fixed homing missiles, constant strafe, basic defense
145
+ * - Spellweaver (tier 2): + adaptive missile fitting, more sophisticated patterns
146
+ * - Spellbinder (tier 3): Future — inescapable web of magic, perfect distance control
147
+ * The progression: spinner (raw thread) → weaver (creates patterns) → binder
148
+ * (constrains and traps). Each tier's projectile web becomes harder to escape.
149
+ *
150
+ * TIER: 1 (base)
151
+ */
152
+ export const Spellspinner: WizardFunction = ({state}) =>
153
+ {
154
+ const targetDistance = useParam('targetDistance', 338, {
155
+ range: 150,
156
+ min: 150,
157
+ max: 450,
158
+ });
159
+ const dangerDistance = useParam('dangerDistance', 242, {
160
+ range: 100,
161
+ min: 100,
162
+ });
163
+ const strafeCyclePeriod = useParam('strafeCyclePeriod', 210, {
164
+ range: 75,
165
+ min: 80,
166
+ max: 300,
167
+ });
168
+ const strafeIntensity = useParam('strafeIntensity', 31, {
169
+ range: 40,
170
+ min: 30,
171
+ max: 100,
172
+ steps: 7,
173
+ });
174
+ const approachSpeed = useParam('approachSpeed', 25, {
175
+ range: 30,
176
+ min: 10,
177
+ max: 70,
178
+ steps: 7,
179
+ });
180
+ const distanceDeadZone = useParam('distanceDeadZone', 40, {
181
+ range: 30,
182
+ min: 5,
183
+ max: 80,
184
+ steps: 7,
185
+ });
186
+ const shieldCancelThreshold = useParam('shieldCancelThreshold', 150, {
187
+ range: 75,
188
+ min: 50,
189
+ max: 300,
190
+ });
191
+ const blinkWindowMax = useParam('blinkWindowMax', 68, {
192
+ range: 30,
193
+ min: 15,
194
+ max: 80,
195
+ steps: 7,
196
+ });
197
+
198
+ const enemy = state.enemies[0];
199
+ if (!enemy)
200
+ {
201
+ return {move: {x: 0, y: 0}};
202
+ }
203
+
204
+ const distance = distanceTo(state.position, enemy.position);
205
+
206
+ // Analyze threats using simulation
207
+ const threats = getThreats(state);
208
+ const urgentThreat = getMostUrgentThreat(threats);
209
+
210
+ // Smart strafe (dodge direction includes close homing missile avoidance)
211
+ const dodgeDirection = getDodgeDirectionForMovement(threats, state.position);
212
+ const strafe = smartStrafe(
213
+ state.position,
214
+ enemy,
215
+ dodgeDirection,
216
+ state.tick,
217
+ strafeCyclePeriod,
218
+ strafeIntensity,
219
+ );
220
+
221
+ // Distance management (only when not dodging)
222
+ let moveX = strafe.x;
223
+ let moveY = strafe.y;
224
+
225
+ if (!dodgeDirection)
226
+ {
227
+ const deltaX = enemy.position.x - state.position.x;
228
+ const deltaY = enemy.position.y - state.position.y;
229
+ const normalizedDistance = distance > 0 ? distance : 1;
230
+
231
+ if (distance > targetDistance + distanceDeadZone)
232
+ {
233
+ moveX += (deltaX / normalizedDistance) * approachSpeed;
234
+ moveY += (deltaY / normalizedDistance) * approachSpeed;
235
+ }
236
+ else if (distance < targetDistance - distanceDeadZone)
237
+ {
238
+ let retreatX = -(deltaX / normalizedDistance) * approachSpeed;
239
+ 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)
242
+ retreatX = 0;
243
+ if (state.position.y < WALL_BUFFER && retreatY < 0) retreatY = 0;
244
+ if (state.position.y > ARENA_SIZE - WALL_BUFFER && retreatY > 0)
245
+ retreatY = 0;
246
+ moveX += retreatX;
247
+ moveY += retreatY;
248
+ }
249
+ }
250
+
251
+ // Normalize
252
+ const magnitude = Math.sqrt(moveX * moveX + moveY * moveY);
253
+ if (magnitude > 100)
254
+ {
255
+ moveX = (moveX / magnitude) * 100;
256
+ moveY = (moveY / magnitude) * 100;
257
+ }
258
+
259
+ const move = {x: moveX, y: moveY};
260
+
261
+ // === DEFENSE: Handle channeling ===
262
+ if (state.state === 'channeling')
263
+ {
264
+ // Cancel shield if no imminent threats
265
+ if (!urgentThreat || urgentThreat.ticksToImpact > shieldCancelThreshold)
266
+ {
267
+ return {move, cancel: true};
268
+ }
269
+ return {move: {x: 0, y: 0}}; // Hold shield if still threatened
270
+ }
271
+
272
+ // === DEFENSE: Cancel missile cast if lethal threat arrives during GCD ===
273
+ if (
274
+ state.state === 'casting' &&
275
+ state.castingSpell === 'missile' &&
276
+ urgentThreat
277
+ )
278
+ {
279
+ const remainingCast = (state.castDuration ?? 0) - (state.castProgress ?? 0);
280
+ const arrivesInGCD =
281
+ urgentThreat.ticksToImpact > remainingCast &&
282
+ urgentThreat.ticksToImpact <= remainingCast + GCD_DURATION;
283
+
284
+ if (
285
+ arrivesInGCD &&
286
+ state.health <= urgentThreat.projectile.damage + 1 &&
287
+ remainingCast > 10
288
+ )
289
+ {
290
+ return {move, cancel: true};
291
+ }
292
+ }
293
+
294
+ // === BLINK-DODGE: For undodgeable threats OR to create attack window ===
295
+ // Blink to center (stays in combat range) and dodges the current missile.
296
+ // Also blink when persistent threats block all offense (creates safe firing window).
297
+ if (
298
+ state.state === 'idle' &&
299
+ urgentThreat &&
300
+ state.blinkCooldown === 0 &&
301
+ urgentThreat.ticksToImpact >= 10 &&
302
+ urgentThreat.ticksToImpact <= blinkWindowMax
303
+ )
304
+ {
305
+ const undodgeableOrForced =
306
+ !urgentThreat.bestDodgeDirection || shouldForceShield(urgentThreat);
307
+ const cannotAttack = !isSafeToAttack(
308
+ threats,
309
+ getMissileCastTime(QUICK_CONFIG),
310
+ );
311
+
312
+ if (undodgeableOrForced || cannotAttack)
313
+ {
314
+ return {
315
+ move: {x: 0, y: 0},
316
+ startCast: {spell: 'blink', target: getBlinkToCenter(state.position)},
317
+ };
318
+ }
319
+ }
320
+
321
+ // === DEFENSE: Shield undodgeable/high-damage threats (when blink on cooldown) ===
322
+ if (
323
+ state.state === 'idle' &&
324
+ urgentThreat &&
325
+ (!urgentThreat.bestDodgeDirection || shouldForceShield(urgentThreat))
326
+ )
327
+ {
328
+ if (urgentThreat.canBlockInTime)
329
+ {
330
+ if (urgentThreat.ticksToStartShield <= 3)
331
+ {
332
+ return {
333
+ move: {x: 0, y: 0},
334
+ startCast: {spell: 'shield'},
335
+ };
336
+ }
337
+ }
338
+ }
339
+
340
+ // === DISTANCE BLINK: enemy too close, blink to reestablish kiting range ===
341
+ if (
342
+ state.state === 'idle' &&
343
+ distance < dangerDistance &&
344
+ state.blinkCooldown === 0 &&
345
+ (!urgentThreat || urgentThreat.bestDodgeDirection !== null)
346
+ )
347
+ {
348
+ const blinkTarget = getBlinkToIncreaseDistance(
349
+ state.position,
350
+ enemy.position,
351
+ state.projectiles,
352
+ MIN_BLINK_ESCAPE_DISTANCE,
353
+ );
354
+ if (blinkTarget)
355
+ {
356
+ return {
357
+ move: {x: 0, y: 0},
358
+ startCast: {spell: 'blink', target: blinkTarget},
359
+ };
360
+ }
361
+ }
362
+
363
+ // === OFFENSE: Attack when safe ===
364
+ if (state.state === 'idle' && distance < 500)
365
+ {
366
+ // Primary: standard homing missile
367
+ const standardCastTime = getMissileCastTime(STANDARD_CONFIG);
368
+ if (isSafeToAttack(threats, standardCastTime))
369
+ {
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
+ };
379
+ }
380
+
381
+ // Fallback: quick missile under pressure
382
+ const quickCastTime = getMissileCastTime(QUICK_CONFIG);
383
+ if (isSafeToAttack(threats, quickCastTime))
384
+ {
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
+ };
394
+ }
395
+ }
396
+
397
+ return {move};
398
+ };