@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,128 @@
1
+ import {WizardState, MissileConfig, ProjectileState} from '../types.js';
2
+ import {
3
+ SHIELD_MAX_BLOCK,
4
+ SHIELD_DECAY_PER_SECOND,
5
+ SHIELD_MIN_BLOCK,
6
+ TICKS_PER_SECOND,
7
+ GCD_DURATION,
8
+ SHIELD_CAST_TIME,
9
+ BLINK_CAST_TIME,
10
+ calculateMissileCastTime,
11
+ calculateWarmupMultiplier,
12
+ } from '../rules.js';
13
+
14
+ /**
15
+ * Start casting a spell.
16
+ * For missiles, applies warmup system (bonus for similar, penalty for switching).
17
+ */
18
+ export function startCast(wizard: WizardState, spell: 'missile' | 'shield' | 'blink', config?: MissileConfig): void
19
+ {
20
+ wizard.state = 'casting';
21
+ wizard.castingSpell = spell;
22
+ wizard.castProgress = 0;
23
+
24
+ if (spell === 'missile' && config)
25
+ {
26
+ // calculateMissileCastTime includes warmup when lastMissileConfig is passed
27
+ const castTimeSec = calculateMissileCastTime(config, wizard.lastMissileConfig);
28
+ wizard.castDuration = Math.max(1, Math.round(castTimeSec * TICKS_PER_SECOND));
29
+ // Store warmup multiplier for visualization
30
+ wizard.warmupMultiplier = calculateWarmupMultiplier(wizard.lastMissileConfig, config);
31
+ }
32
+ else if (spell === 'shield')
33
+ {
34
+ wizard.castDuration = SHIELD_CAST_TIME;
35
+ }
36
+ else if (spell === 'blink')
37
+ {
38
+ wizard.castDuration = BLINK_CAST_TIME;
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Cancel the current cast.
44
+ */
45
+ export function cancelCast(wizard: WizardState): void
46
+ {
47
+ wizard.state = 'idle';
48
+ delete wizard.castingSpell;
49
+ delete wizard.castProgress;
50
+ delete wizard.castDuration;
51
+ }
52
+
53
+ /**
54
+ * Complete the current cast and trigger the spell effect.
55
+ */
56
+ export function completeCast(wizard: WizardState): void
57
+ {
58
+ const spell = wizard.castingSpell;
59
+
60
+ if (spell === 'shield')
61
+ {
62
+ wizard.state = 'channeling';
63
+ wizard.channelingSpell = 'shield';
64
+ wizard.channelDuration = 0;
65
+ }
66
+ else if (spell === 'missile')
67
+ {
68
+ wizard.state = 'gcd_locked';
69
+ wizard.gcdRemaining = GCD_DURATION;
70
+ }
71
+ else
72
+ {
73
+ // Blink: no GCD — can act immediately after teleporting
74
+ wizard.state = 'idle';
75
+ }
76
+
77
+ delete wizard.castingSpell;
78
+ delete wizard.castProgress;
79
+ delete wizard.castDuration;
80
+ }
81
+
82
+ /**
83
+ * Update the shield channel state.
84
+ */
85
+ export function updateShield(wizard: WizardState, deltaTicks: number): void
86
+ {
87
+ if (wizard.state === 'channeling' && wizard.channelingSpell === 'shield')
88
+ {
89
+ wizard.channelDuration = (wizard.channelDuration ?? 0) + deltaTicks;
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Calculate the current block percentage of a shield based on channel duration.
95
+ */
96
+ export function calculateShieldBlock(channelDurationTicks: number): number
97
+ {
98
+ const channelSeconds = channelDurationTicks / TICKS_PER_SECOND;
99
+ const decay = channelSeconds * SHIELD_DECAY_PER_SECOND;
100
+ return Math.max(SHIELD_MIN_BLOCK, SHIELD_MAX_BLOCK - decay);
101
+ }
102
+
103
+ /**
104
+ * Apply damage to a wizard, considering shield mitigation.
105
+ *
106
+ * Shield blocks a percentage of damage based on channel duration:
107
+ * - Fresh shield (0s): 90% block → 10% damage through
108
+ * - Decayed shield: block % decreases over time (20%/sec)
109
+ * - Minimum: 30% block → 70% damage through
110
+ *
111
+ * (Design doc lines 196-212)
112
+ */
113
+ export function applyDamage(wizard: WizardState, damage: number, _projectile?: ProjectileState): number
114
+ {
115
+ if (wizard.invincible) return 0;
116
+
117
+ let finalDamage = damage;
118
+
119
+ if (wizard.state === 'channeling' && wizard.channelingSpell === 'shield')
120
+ {
121
+ const blockPercent = calculateShieldBlock(wizard.channelDuration ?? 0);
122
+ // Simple block: damage through = damage * (1 - blockPercent)
123
+ finalDamage = damage * (1 - blockPercent);
124
+ }
125
+
126
+ wizard.health = Math.max(0, wizard.health - finalDamage);
127
+ return finalDamage;
128
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Engine version — milliseconds since the Unix epoch.
3
+ *
4
+ * Bumped automatically by `scripts/update-engine-version.mjs` on every build.
5
+ * The same value is propagated to `packages/functions/src/engine-version.generated.ts`
6
+ * so cloud functions and the core engine always agree on a single version per deploy.
7
+ *
8
+ * Used to gate spectator replays: a recorded match can only be re-simulated when
9
+ * the runtime engine version matches the version that produced the match.
10
+ */
11
+ export const ENGINE_VERSION = 1777556890699;
@@ -0,0 +1,210 @@
1
+ /**
2
+ * VIBEMANCER - ACTION BUILDERS
3
+ *
4
+ * Fluent API for constructing bot actions with type-safe chaining.
5
+ *
6
+ * UNITS REFERENCE (100 ticks = 1 second):
7
+ * Position: absolute world coordinates, 0-800 on each axis (800×800 arena)
8
+ * Movement: direction vector, magnitude auto-normalized to max 100
9
+ * Speed: units per tick (player moves at 1 unit/tick = 100 units/sec)
10
+ * Duration: ticks (divide by 100 for seconds)
11
+ * Angles: degrees (0°=right, 90°=down, 180°=left, 270°=up)
12
+ * Damage: raw HP removed on hit (wizard has 60 HP)
13
+ * Turn rate: degrees per tick the missile can rotate
14
+ */
15
+
16
+ import {WizardActions, MissileConfig, MissileAIFunction} from '../types.js';
17
+ import type {FinalAction, ActionBuilder} from './types.js';
18
+
19
+ /**
20
+ * Create a FinalAction (no further chaining possible).
21
+ */
22
+ function createFinalAction(action: WizardActions): FinalAction
23
+ {
24
+ return {
25
+ _toAction: () => action,
26
+ };
27
+ }
28
+
29
+ /**
30
+ * Create an ActionBuilder (can chain .move()).
31
+ */
32
+ function createActionBuilder(baseAction: Omit<WizardActions, 'move'>): ActionBuilder
33
+ {
34
+ const action: WizardActions = {
35
+ ...baseAction,
36
+ move: {x: 0, y: 0},
37
+ };
38
+
39
+ return {
40
+ _toAction: () => action,
41
+ move(x: number, y: number): FinalAction
42
+ {
43
+ return createFinalAction({
44
+ ...action,
45
+ move: {x, y},
46
+ });
47
+ },
48
+ };
49
+ }
50
+
51
+ // ============================================================
52
+ // ACTION BUILDERS
53
+ // ============================================================
54
+
55
+ /**
56
+ * Channel a shield that blocks incoming damage.
57
+ *
58
+ * Starts at 90% block, decays by 20% per second, minimum 30%.
59
+ * Takes 20 ticks (0.2s) to activate. Movement is disabled while channeling.
60
+ * Cancel anytime with cancel(). Triggers 100-tick (1s) GCD after cancel.
61
+ *
62
+ * Can chain .move() — movement applies during the 20-tick cast, NOT during channel.
63
+ *
64
+ * @example
65
+ * return shield(); // shield and stay still
66
+ * return shield().move(100, 0); // move right while cast starts
67
+ */
68
+ export function shield(): ActionBuilder
69
+ {
70
+ return createActionBuilder({
71
+ startCast: {spell: 'shield'},
72
+ });
73
+ }
74
+
75
+ /**
76
+ * Cast a missile spell.
77
+ *
78
+ * Cast time scales with damage, speed, duration, and turn rate — bigger missiles
79
+ * take longer to cast. While casting you move at 50% speed. After firing, 100-tick
80
+ * (1s) GCD before next spell.
81
+ *
82
+ * Repeated similar missiles cast 20% faster (warmup bonus). Switching styles
83
+ * incurs a 20% penalty.
84
+ *
85
+ * Can chain .move() for simultaneous movement while casting.
86
+ *
87
+ * @param config - Missile stats:
88
+ * - damage: HP removed on hit (1-60 typical). Also sets hitbox: radius = 2 + 0.1×damage.
89
+ * - speed: units/tick (min 1.5). Player moves at 1 u/t, so 5 = 5× player speed.
90
+ * - duration: ticks the missile lives (min 10). Range ≈ speed × duration.
91
+ * - turnRate: degrees/tick of homing (0 = straight line, 3 = moderate homing, 5+ = strong).
92
+ * Negative = no homing + minor speed cost reduction.
93
+ * @param ai - Called every tick to control missile steering. Receives:
94
+ * - missileState: the missile's position, rotation (degrees), speed, remainingTicks
95
+ * - worldState: full game state (all wizards, projectiles)
96
+ * - random(): seeded PRNG [0, 1)
97
+ * Return { turnToward: {x, y} } to home toward a position, or {} to fly straight.
98
+ * @param direction - Launch angle in degrees (0°=right, 90°=down, 180°=left, 270°=up).
99
+ * Tip: use Math.atan2(dy, dx) * (180 / Math.PI) to aim at a target.
100
+ *
101
+ * @example
102
+ * // Straight missile aimed at enemy
103
+ * const angle = Math.atan2(dy, dx) * (180 / Math.PI);
104
+ * return missile({damage: 15, speed: 6, duration: 200, turnRate: 0}, () => ({}), angle);
105
+ *
106
+ * // Homing missile that tracks enemy
107
+ * return missile(
108
+ * {damage: 10, speed: 5, duration: 300, turnRate: 3},
109
+ * ({worldState}) => ({turnToward: worldState.enemies[0]?.position}),
110
+ * angle,
111
+ * );
112
+ */
113
+ export function missile(config: MissileConfig, ai: MissileAIFunction, direction: number): ActionBuilder
114
+ {
115
+ return createActionBuilder({
116
+ startCast: {
117
+ spell: 'missile',
118
+ config,
119
+ missileAI: ai,
120
+ direction,
121
+ },
122
+ });
123
+ }
124
+
125
+ /**
126
+ * Teleport to an absolute position on the arena.
127
+ *
128
+ * Max range: 300 units from current position (clamped by engine if further).
129
+ * Cast time: 10 ticks (0.1s). Cooldown scales with distance:
130
+ * - 100 units → 100 ticks (1s)
131
+ * - 300 units → 2000 ticks (20s)
132
+ *
133
+ * Cannot chain .move() — blink IS the movement.
134
+ *
135
+ * @param x - Target X position (0-800, absolute world coordinate)
136
+ * @param y - Target Y position (0-800, absolute world coordinate)
137
+ *
138
+ * @example
139
+ * return blink(400, 400); // blink to center
140
+ * return blink(enemy.position.x, enemy.position.y); // blink to enemy
141
+ */
142
+ export function blink(x: number, y: number): FinalAction
143
+ {
144
+ return createFinalAction({
145
+ move: {x: 0, y: 0}, // No manual movement with blink
146
+ startCast: {
147
+ spell: 'blink',
148
+ target: {x, y},
149
+ },
150
+ });
151
+ }
152
+
153
+ /**
154
+ * Cancel current cast or channel (e.g. stop shielding to attack).
155
+ *
156
+ * Canceling a cast/channel triggers 100-tick (1s) GCD.
157
+ * Can chain .move() for simultaneous movement.
158
+ *
159
+ * @example
160
+ * return cancel().move(-100, 0); // cancel and dodge left
161
+ */
162
+ export function cancel(): ActionBuilder
163
+ {
164
+ return createActionBuilder({
165
+ cancel: true,
166
+ });
167
+ }
168
+
169
+ /**
170
+ * Move in a direction without casting any spell.
171
+ *
172
+ * This is a **direction vector**, not a target position. The engine normalizes
173
+ * the magnitude to max 100, then moves at 1 unit/tick (100 units/sec).
174
+ * Positive X = right, positive Y = down.
175
+ *
176
+ * To move toward a target position, subtract your position:
177
+ * move(target.x - myPos.x, target.y - myPos.y)
178
+ *
179
+ * @param x - Horizontal direction (positive = right, negative = left)
180
+ * @param y - Vertical direction (positive = down, negative = up)
181
+ *
182
+ * @example
183
+ * return move(100, 0); // move right
184
+ * return move(enemy.position.x - myPos.x, enemy.position.y - myPos.y); // move toward enemy
185
+ */
186
+ export function move(x: number, y: number): FinalAction
187
+ {
188
+ return createFinalAction({
189
+ move: {x, y},
190
+ });
191
+ }
192
+
193
+ /**
194
+ * Do nothing — no action, no movement.
195
+ */
196
+ export function idle(): FinalAction
197
+ {
198
+ return createFinalAction({
199
+ move: {x: 0, y: 0},
200
+ });
201
+ }
202
+
203
+ /**
204
+ * Extract WizardActions from a FinalAction.
205
+ * Used by the engine to get the actual action.
206
+ */
207
+ export function extractAction(finalAction: FinalAction): WizardActions
208
+ {
209
+ return finalAction._toAction();
210
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * VIBEMANCER - BOT WRAPPER
3
+ *
4
+ * Utilities to convert between old-style (WizardFunction) and new-style (BotFunction) bots.
5
+ */
6
+
7
+ import {WizardFunction, WizardActions, GameState, GameConfig} from '../types.js';
8
+ import {withBotContext} from '../engine/hooks-runtime.js';
9
+ import {extractAction} from './action-builders.js';
10
+ import type {BotFunction, BotContext} from './types.js';
11
+
12
+ /**
13
+ * Convert a new-style bot (using hooks) to an old-style bot (WizardFunction).
14
+ *
15
+ * This allows new bots to work with the existing simulation.
16
+ *
17
+ * @example
18
+ * const NewBot: BotFunction = () => {
19
+ * const health = useHealth();
20
+ * return health < 10 ? shield() : idle();
21
+ * };
22
+ *
23
+ * // Convert to work with simulate()
24
+ * const oldStyleBot = wrapNewBot(NewBot);
25
+ * simulate(oldStyleBot, opponent);
26
+ */
27
+ export function wrapNewBot(newBot: BotFunction): WizardFunction
28
+ {
29
+ return ({state, config}: {state: GameState; config: GameConfig; random: () => number}): WizardActions =>
30
+ {
31
+ // Create the BotContext from GameState
32
+ // Note: entityId is inherited from the simulation's outer runWithHooks call
33
+ const botContext: BotContext = {
34
+ entityId: 'unused', // Not used - entity ID comes from simulation
35
+ tick: state.tick,
36
+ position: state.position,
37
+ velocity: state.velocity,
38
+ health: state.health,
39
+ maxHealth: state.maxHealth,
40
+ state: state.state,
41
+ castingSpell: state.castingSpell,
42
+ castProgress: state.castProgress,
43
+ castDuration: state.castDuration,
44
+ channelingSpell: state.channelingSpell,
45
+ channelDuration: state.channelDuration,
46
+ gcdRemaining: state.gcdRemaining,
47
+ blinkCooldown: state.blinkCooldown,
48
+ enemies: state.enemies.map((e) => ({
49
+ id: e.id,
50
+ position: e.position,
51
+ velocity: e.velocity,
52
+ health: e.health,
53
+ state: e.state,
54
+ castingSpell: e.castingSpell,
55
+ channelingSpell: e.channelingSpell,
56
+ channelDuration: e.channelDuration,
57
+ })),
58
+ projectiles: state.projectiles,
59
+ myProjectiles: state.myProjectiles,
60
+ arenaWidth: config.arenaSize.width,
61
+ arenaHeight: config.arenaSize.height,
62
+ damageDealt: state.damageDealt,
63
+ damageTaken: state.damageTaken,
64
+ lastHitTick: state.lastHitTick,
65
+ };
66
+
67
+ // Run the new-style bot with context
68
+ // Use withBotContext (not runBotWithContext) to preserve the entity ID
69
+ // set by the simulation's outer runWithHooks call
70
+ const finalAction = withBotContext(botContext, newBot);
71
+
72
+ // Extract WizardActions from FinalAction
73
+ return extractAction(finalAction);
74
+ };
75
+ }
76
+
77
+ /**
78
+ * Type guard to check if a bot is a new-style BotFunction.
79
+ * New-style bots have 0 parameters, old-style bots have at least 1.
80
+ */
81
+ export function isNewStyleBot(bot: WizardFunction | BotFunction): bot is BotFunction
82
+ {
83
+ return bot.length === 0;
84
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * VIBEMANCER - HOOKS API
3
+ *
4
+ * Main export file for the hooks-based bot API.
5
+ */
6
+
7
+ // Types
8
+ export type {
9
+ EnemyState,
10
+ AnalyzedThreat,
11
+ FinalAction,
12
+ ActionBuilder,
13
+ BotFunction,
14
+ BotContext,
15
+ MissileConfig,
16
+ MissileAIFunction,
17
+ } from './types.js';
18
+
19
+ // State hooks
20
+ export {
21
+ useHealth,
22
+ usePosition,
23
+ useVelocity,
24
+ useStatus,
25
+ useTicksUntilReady,
26
+ useShieldStrength,
27
+ useBlinkCooldown,
28
+ useCastingSpell,
29
+ useCastProgress,
30
+ useEnemy,
31
+ useMyProjectiles,
32
+ useDamageDealt,
33
+ useDamageTaken,
34
+ useLastHitTick,
35
+ useArenaSize,
36
+ useTick,
37
+ useThreats,
38
+ useClosestThreat,
39
+ useMyThreatsToEnemy,
40
+ } from './state-hooks.js';
41
+
42
+ // Threat analysis (for advanced users who want direct access)
43
+ export {analyzeThreats} from './threat-analysis.js';
44
+
45
+ // Action builders
46
+ export {
47
+ shield,
48
+ missile,
49
+ blink,
50
+ cancel,
51
+ move,
52
+ idle,
53
+ extractAction,
54
+ } from './action-builders.js';
55
+
56
+ // Re-export persistence hooks from runtime
57
+ export {
58
+ useState,
59
+ useEffect,
60
+ useMemo,
61
+ useRef,
62
+ runBotWithContext,
63
+ withBotContext,
64
+ getBotContext,
65
+ } from '../engine/hooks-runtime.js';
66
+
67
+ // Re-export RefObject type
68
+ export type {RefObject} from '../engine/hooks-runtime.js';
69
+
70
+ // Bot wrapper for integration
71
+ export {wrapNewBot, isNewStyleBot} from './bot-wrapper.js';
72
+
73
+ // Parameter tuning hook
74
+ export {useParam} from '../engine/params-runtime.js';
75
+ export type {ParamDeclaration} from '../engine/params-runtime.js';