@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,142 @@
1
+ /**
2
+ * VIBEMANCER - HOOKS API TYPES
3
+ *
4
+ * Types for the hooks-based bot API.
5
+ *
6
+ * UNITS REFERENCE (100 ticks = 1 second):
7
+ * Position: absolute world coordinates, 0-800 on each axis (800×800 arena)
8
+ * Velocity: units per tick on each axis (player max speed = 1 u/t)
9
+ * Health: hit points (max 60)
10
+ * Ticks: game ticks (10ms each, 100/sec). Divide by 100 for seconds.
11
+ * Angles: degrees (0°=right, 90°=down, 180°=left, 270°=up)
12
+ */
13
+
14
+ import {Position, Velocity, ProjectileState, MissileConfig, MissileAIFunction, WizardActions} from '../types.js';
15
+
16
+ /**
17
+ * Enemy wizard state as seen by your bot.
18
+ *
19
+ * Note: you cannot see the enemy's missile configs, cooldown timers, or
20
+ * damage history — only what's visible on the battlefield.
21
+ */
22
+ export interface EnemyState
23
+ {
24
+ /** Enemy position in world coordinates (0-800). */
25
+ position: Position;
26
+ /** Enemy velocity in units/tick. */
27
+ velocity: Velocity;
28
+ /** Enemy current HP (0-60). */
29
+ health: number;
30
+ /** Enemy status: 'idle', 'casting', 'channeling' (shield), or 'gcd_locked'. */
31
+ status: 'idle' | 'casting' | 'channeling' | 'gcd_locked';
32
+ /** Which spell enemy is casting, or null. */
33
+ castingSpell: 'missile' | 'shield' | 'blink' | null;
34
+ /** Enemy shield block multiplier (0 if not shielding, 0.3-0.9 if shielding). */
35
+ shieldStrength: number;
36
+ }
37
+
38
+ /**
39
+ * Pre-computed analysis of an incoming enemy projectile.
40
+ *
41
+ * All timing values are in ticks (100 ticks = 1 second).
42
+ * Dodge directions are relative to missile heading, not world axes.
43
+ */
44
+ export interface AnalyzedThreat
45
+ {
46
+ /** Unique projectile ID. */
47
+ id: string;
48
+ /** Raw projectile state (position, rotation in degrees, speed in u/t, turnRate, remainingTicks). */
49
+ projectile: ProjectileState;
50
+
51
+ /** Ticks until missile hits your current position. Infinity if predicted to miss. */
52
+ ticksToImpact: number;
53
+ /** Whether the missile will hit if you stand still. */
54
+ willHit: boolean;
55
+
56
+ /** Whether strafing left (perpendicular to missile heading) avoids it. */
57
+ canDodgeLeft: boolean;
58
+ /** Whether strafing right (perpendicular to missile heading) avoids it. */
59
+ canDodgeRight: boolean;
60
+ /** Whether moving directly away from the missile avoids it. */
61
+ canOutrun: boolean;
62
+ /** Optimal dodge direction as a unit vector {x, y}, or null if undodgeable. */
63
+ bestDodgeDirection: Position | null;
64
+
65
+ /** Whether you can channel shield before the missile arrives. */
66
+ canBlockInTime: boolean;
67
+ /** Ticks from now when you should START channeling shield to block in time. */
68
+ ticksToStartShield: number;
69
+ }
70
+
71
+ /**
72
+ * Final action that can be returned from a bot.
73
+ * Cannot be further chained.
74
+ */
75
+ export interface FinalAction
76
+ {
77
+ /** Internal: extract the WizardActions */
78
+ readonly _toAction: () => WizardActions;
79
+ }
80
+
81
+ /**
82
+ * Action builder that allows chaining .move() for simultaneous movement.
83
+ * Returned by shield(), missile(), and cancel().
84
+ */
85
+ export interface ActionBuilder extends FinalAction
86
+ {
87
+ /**
88
+ * Add movement to this action (e.g., move while casting).
89
+ * Direction vector, not absolute position. Auto-normalized.
90
+ * Positive X = right, positive Y = down.
91
+ */
92
+ move(x: number, y: number): FinalAction;
93
+ }
94
+
95
+ /**
96
+ * Bot function type for the hooks API.
97
+ * Called every tick. Read state with hooks, return an action.
98
+ */
99
+ export type BotFunction = () => FinalAction;
100
+
101
+ /**
102
+ * Internal context for game state hooks.
103
+ */
104
+ export interface BotContext
105
+ {
106
+ entityId: string;
107
+ tick: number;
108
+ position: Position;
109
+ velocity: Velocity;
110
+ health: number;
111
+ maxHealth: number;
112
+ state: 'idle' | 'casting' | 'channeling' | 'gcd_locked';
113
+ castingSpell?: 'missile' | 'shield' | 'blink';
114
+ castProgress?: number;
115
+ castDuration?: number;
116
+ channelingSpell?: 'shield';
117
+ channelDuration?: number;
118
+ gcdRemaining?: number;
119
+ blinkCooldown: number;
120
+ enemies: Array<{
121
+ id: string;
122
+ position: Position;
123
+ velocity: Velocity;
124
+ health: number;
125
+ state: 'idle' | 'casting' | 'channeling' | 'gcd_locked';
126
+ castingSpell?: 'missile' | 'shield' | 'blink';
127
+ channelingSpell?: 'shield';
128
+ channelDuration?: number;
129
+ }>;
130
+ projectiles: ProjectileState[];
131
+ myProjectiles: ProjectileState[];
132
+ arenaWidth: number;
133
+ arenaHeight: number;
134
+ damageDealt: number;
135
+ damageTaken: number;
136
+ lastHitTick: number;
137
+ }
138
+
139
+ /**
140
+ * Missile configuration for action builders.
141
+ */
142
+ export type {MissileConfig, MissileAIFunction};
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Browser-safe entry point for @vibemancer/core.
3
+ *
4
+ * Same as index.ts but excludes sandbox.ts (which imports isolated-vm,
5
+ * a native Node.js addon that can't be bundled for the browser).
6
+ *
7
+ * The browser sandbox (Web Worker-based) is still exported here.
8
+ */
9
+ export * from './types.js';
10
+ export * from './rules.js';
11
+ export * from './engine-version.js';
12
+ export * from './engine/simulation.js';
13
+ export * from './engine/hooks-runtime.js';
14
+ export * from './engine/physics.js';
15
+ export * from './engine/spells.js';
16
+ export * from './engine/manual-match.js';
17
+ export * from './engine/missile-templates.js';
18
+ export * from './utils/index.js';
19
+ export * from './bots/index.js';
20
+ export * from './hooks/index.js';
21
+ export * from './engine/params-runtime.js';
22
+ export * from './engine/optimizer.js';
23
+ export {BrowserMatchSandbox, BrowserManualMatchSandbox, browserSandboxFight, browserSandboxSimulate, createWorkerScript} from './engine/sandbox-browser.js';
24
+ export type {BrowserSandboxOptions, ManualMatchInitOptions, ManualMatchStepRequest, WorkerFactory, WorkerLike} from './engine/sandbox-browser.js';
25
+ export {testBot} from './testing.js';
26
+ export {extractStats, formatStats} from './stats.js';
27
+ export type {FightStats} from './stats.js';
28
+ export {extractTraceEvents, summarizeTrace, formatTraceEvents, formatTraceSummary, diagnoseTrace, formatDiagnosis} from './trace.js';
29
+ export type {TraceEvent, TraceEventType, TraceSummary, TraceBotSummary} from './trace.js';
30
+ export type {TestBotBuilder, TestFightResult, TestSimulateResult} from './testing.js';
package/src/index.ts ADDED
@@ -0,0 +1,24 @@
1
+ export * from './types.js';
2
+ export * from './rules.js';
3
+ export * from './engine-version.js';
4
+ export * from './engine/simulation.js';
5
+ export * from './engine/hooks-runtime.js';
6
+ export * from './engine/physics.js';
7
+ export * from './engine/spells.js';
8
+ export * from './engine/manual-match.js';
9
+ export * from './engine/missile-templates.js';
10
+ export * from './utils/index.js';
11
+ export * from './bots/index.js';
12
+ export * from './hooks/index.js';
13
+ export * from './engine/params-runtime.js';
14
+ export * from './engine/optimizer.js';
15
+ export {BotBundle, compileMatchBundle, MatchSandbox, sandboxFight, sandboxSimulate} from './engine/sandbox.js';
16
+ export type {CompileOptions, SandboxOptions} from './engine/sandbox.js';
17
+ export {BrowserMatchSandbox, BrowserManualMatchSandbox, browserSandboxFight, browserSandboxSimulate, createWorkerScript} from './engine/sandbox-browser.js';
18
+ export type {BrowserSandboxOptions, ManualMatchInitOptions, ManualMatchStepRequest, WorkerFactory, WorkerLike} from './engine/sandbox-browser.js';
19
+ export {testBot} from './testing.js';
20
+ export {extractStats, formatStats} from './stats.js';
21
+ export type {FightStats} from './stats.js';
22
+ export {extractTraceEvents, summarizeTrace, formatTraceEvents, formatTraceSummary, diagnoseTrace, formatDiagnosis} from './trace.js';
23
+ export type {TraceEvent, TraceEventType, TraceSummary, TraceBotSummary} from './trace.js';
24
+ export type {TestBotBuilder, TestFightResult, TestSimulateResult} from './testing.js';
package/src/rules.ts ADDED
@@ -0,0 +1,254 @@
1
+ /**
2
+ * VIBEMANCER - GAME RULES
3
+ *
4
+ * This is the single source of truth for all game constants.
5
+ * All game logic imports from here. Read this to understand the game.
6
+ */
7
+
8
+ import {MissileConfig} from './types.js';
9
+
10
+ // === TIMING ===
11
+ export const TICKS_PER_SECOND = 100;
12
+ export const TICK_DURATION_MS = 10;
13
+
14
+ // === WIZARD ===
15
+ export const WIZARD_HEALTH = 60;
16
+ export const WIZARD_RADIUS = 5; // units
17
+ export const MOVEMENT_SPEED = 1; // units per tick (100 units/sec)
18
+ export const CASTING_MOVEMENT_MULT = 0.5; // 50% speed while casting
19
+
20
+ // === GCD ===
21
+ export const GCD_DURATION = 100; // ticks (1 second)
22
+
23
+ // === SHIELD ===
24
+ export const SHIELD_CAST_TIME = 20; // ticks (0.2s)
25
+ export const SHIELD_MAX_BLOCK = 0.9; // 90%
26
+ export const SHIELD_DECAY_PER_SECOND = 0.2; // loses 20% per second
27
+ export const SHIELD_MIN_BLOCK = 0.3; // 30% minimum
28
+ // Note: Shield block % is applied directly to damage (no additional scaling)
29
+
30
+ // === BLINK ===
31
+ export const BLINK_CAST_TIME = 10; // ticks (0.1s)
32
+ export const BLINK_RANGE = 300; // units
33
+ export const BLINK_MAX_COOLDOWN = 2000; // ticks (20 seconds) — full-range blink
34
+ export const BLINK_MIN_COOLDOWN = 100; // ticks (1 second) — micro-blink floor
35
+ /** @deprecated Use BLINK_MAX_COOLDOWN */
36
+ export const BLINK_COOLDOWN = BLINK_MAX_COOLDOWN;
37
+
38
+ // === ARENA ===
39
+ export const ARENA_WIDTH = 800;
40
+ export const ARENA_HEIGHT = 800;
41
+ export const ARENA_SIZE = ARENA_WIDTH; // Arena is square; alias for bot convenience
42
+ export const SPAWN_DISTANCE = 600;
43
+
44
+ // === MISSILE CONSTRAINTS ===
45
+ export const MISSILE_MIN_DAMAGE = 1;
46
+ export const MISSILE_MIN_SPEED = 1.5; // must be faster than player
47
+ export const MISSILE_MIN_DURATION = 10; // ticks (0.1 seconds)
48
+ export const MISSILE_MIN_CAST_TIME = 0.1; // seconds
49
+
50
+ // === MISSILE HITBOX ===
51
+ // Missile hitbox scales with damage (design doc line 1122)
52
+ // Visual size reference (design doc lines 349-354):
53
+ // - 1-5 damage: Tiny
54
+ // - 6-15 damage: Small-Medium
55
+ // - 16-30 damage: Large
56
+ // - 31+ damage: Huge
57
+ export const MISSILE_BASE_RADIUS = 2; // base radius in units
58
+ export const MISSILE_DAMAGE_RADIUS_SCALE = 0.1; // additional radius per damage point
59
+
60
+ /**
61
+ * Calculate missile hitbox radius based on damage.
62
+ */
63
+ export function calculateMissileRadius(damage: number): number
64
+ {
65
+ return MISSILE_BASE_RADIUS + damage * MISSILE_DAMAGE_RADIUS_SCALE;
66
+ }
67
+
68
+ // === MISSILE CAST TIME FORMULA ===
69
+ // cast_time = BASE + SCALE × damage^POWER + delivery costs
70
+ //
71
+ // Power-law scaling: DPS always increases with damage (higher damage = more
72
+ // DPS but longer casts that are easier to shield/dodge). Fast casts (low
73
+ // damage) can't be shielded — that's their advantage over raw DPS.
74
+ //
75
+ // Reference points (stab, no delivery cost):
76
+ // d=10: ~1.15s cast → DPS 4.65, TTK 11.9s (fast, unshieldable)
77
+ // d=20: ~1.77s cast → DPS 7.23, TTK 7.3s (medium, borderline shieldable)
78
+ // d=30: ~2.28s cast → DPS 9.14, TTK 5.6s (slow, shieldable)
79
+ // d=60: ~3.57s cast → DPS 13.1, TTK 3.6s (very slow, easily shielded)
80
+ export const MISSILE_BASE_CAST = 0.1;
81
+ export const MISSILE_DAMAGE_SCALE = 0.226;
82
+ export const MISSILE_DAMAGE_POWER = 2 / 3; // sublinear: DPS always increases with damage
83
+ export const MISSILE_HOMING_COEFF = 0.12;
84
+ export const MISSILE_TURN_DURATION_COEFF = 0.10;
85
+ export const MISSILE_SPEED_DURATION_BASELINE = 1.5;
86
+ export const MISSILE_SPEED_DURATION_COEFF = 0.025;
87
+
88
+ /**
89
+ * Maps turnRate to effective cost for the cast time formula.
90
+ * - Positive: linear (unchanged behavior)
91
+ * - Negative: diminishing returns via -|t|/(1+|t|), saturating at -1.
92
+ * turnRate -0.5 → -0.33, -1 → -0.5, -5 → -0.83, -10 → -0.91
93
+ * Big cast speed gains from 0 to -1, worthwhile to -5, negligible after.
94
+ * No hard floor — the curve naturally caps the benefit.
95
+ */
96
+ export function effectiveTurnRateCost(turnRate: number): number
97
+ {
98
+ if (turnRate >= 0) return turnRate;
99
+ const abs = Math.abs(turnRate);
100
+ return -(abs / (1 + abs));
101
+ }
102
+
103
+ /**
104
+ * Validate and sanitize missile config.
105
+ * Ensures all values meet minimum requirements (Lesson #9).
106
+ */
107
+ export function validateMissileConfig(config: MissileConfig): MissileConfig
108
+ {
109
+ return {
110
+ damage: Math.max(MISSILE_MIN_DAMAGE, Number.isFinite(config.damage) ? config.damage : MISSILE_MIN_DAMAGE),
111
+ speed: Math.max(MISSILE_MIN_SPEED, Number.isFinite(config.speed) ? config.speed : MISSILE_MIN_SPEED),
112
+ turnRate: Number.isFinite(config.turnRate) ? config.turnRate : 0, // no floor — diminishing returns via effectiveTurnRateCost
113
+ duration: Math.max(MISSILE_MIN_DURATION, Number.isFinite(config.duration) ? Math.floor(config.duration) : MISSILE_MIN_DURATION),
114
+ };
115
+ }
116
+
117
+ /**
118
+ * Calculate missile cast time from config.
119
+ *
120
+ * Base formula:
121
+ * cast_time = 0.1
122
+ * + 0.226 × damage^(2/3)
123
+ * + 0.12 × effectiveTurnRateCost(turnRate)
124
+ * + 0.10 × (effectiveTurnRateCost(turnRate) × durationSeconds)
125
+ * + 0.025 × (speed × durationSeconds - 1.5)
126
+ *
127
+ * effectiveTurnRateCost: linear for positive, -ln(1+|t|) for negative (diminishing returns).
128
+ *
129
+ * If lastMissileConfig is a MissileConfig, applies warmup multiplier:
130
+ * - Similar to previous: up to 20% faster
131
+ * - Very different: up to 20% slower (switching penalty)
132
+ */
133
+ export function calculateMissileCastTime(config: MissileConfig, lastMissileConfig: MissileConfig | undefined | null = null): number
134
+ {
135
+ const {damage, speed, turnRate, duration} = config;
136
+ const durationSeconds = duration / TICKS_PER_SECOND;
137
+ const turnCost = effectiveTurnRateCost(turnRate);
138
+
139
+ const raw = MISSILE_BASE_CAST
140
+ + MISSILE_DAMAGE_SCALE * Math.pow(damage, MISSILE_DAMAGE_POWER)
141
+ + MISSILE_HOMING_COEFF * turnCost
142
+ + MISSILE_TURN_DURATION_COEFF * (turnCost * durationSeconds)
143
+ + MISSILE_SPEED_DURATION_COEFF * (speed * durationSeconds - MISSILE_SPEED_DURATION_BASELINE);
144
+
145
+ const baseCast = Math.max(MISSILE_MIN_CAST_TIME, raw);
146
+
147
+ // Warmup is applied when lastMissileConfig is an actual MissileConfig.
148
+ // Note: passing undefined triggers the default (null) due to JS semantics,
149
+ // so in practice only an explicit MissileConfig activates warmup.
150
+ // - null/undefined/omitted: return base cast time (no warmup)
151
+ // - MissileConfig: apply warmup multiplier based on similarity to previous
152
+ if (lastMissileConfig !== null)
153
+ {
154
+ const multiplier = calculateWarmupMultiplier(lastMissileConfig, config);
155
+ return Math.max(MISSILE_MIN_CAST_TIME, baseCast * multiplier);
156
+ }
157
+
158
+ return baseCast;
159
+ }
160
+
161
+ // === CAST WARMUP ===
162
+ // Repeated similar missile casts get faster ("muscle memory"), while
163
+ // switching to a very different config incurs a penalty (slower cast).
164
+ // This rewards specialization: melee bots that always stab have faster
165
+ // casts than ranged bots that switch to stabs opportunistically.
166
+ //
167
+ // The warmup multiplier ranges from (1 - MAX_BONUS) to (1 + MAX_PENALTY):
168
+ // Similar to previous: cast × 0.80 (20% faster)
169
+ // First cast (no previous): cast × 0.80 (full warmup, no conflict)
170
+ // Very different from previous: cast × 1.20 (20% slower, switching penalty)
171
+ export const WARMUP_MAX_BONUS = 0.20; // 20% cast time reduction at full warmup
172
+ export const WARMUP_MAX_PENALTY = 0.20; // 20% cast time increase when switching
173
+ // Tolerances for similarity calculation (differences within tolerance are "similar"):
174
+ // Note: damage is intentionally excluded — it doesn't define playstyle.
175
+ // A melee bot stabbing at d=10 and d=25 is the same style, just different power.
176
+ export const WARMUP_SPEED_TOLERANCE = 3; // speed can vary ±3
177
+ export const WARMUP_TURN_TOLERANCE = 1; // turnRate can vary ±1
178
+ export const WARMUP_DURATION_TOLERANCE = 50; // duration (ticks) can vary ±50
179
+
180
+ /**
181
+ * Calculate how similar two missile configs are (0 to 1).
182
+ * Returns 1 for identical configs, 0 for very different ones.
183
+ * Used by the warmup system to determine cast time multiplier.
184
+ *
185
+ * Only compares speed, turnRate, and duration — these define the missile
186
+ * "style" (melee stab vs ranged homing vs fast snipe). Damage is excluded
187
+ * because varying power doesn't change playstyle.
188
+ */
189
+ export function calculateMissileSimilarity(prev: MissileConfig | undefined, current: MissileConfig): number
190
+ {
191
+ if (!prev) return 1; // No previous missile = no conflicting muscle memory = full warmup
192
+
193
+ const dist = Math.abs(prev.speed - current.speed) / WARMUP_SPEED_TOLERANCE
194
+ + Math.abs(prev.turnRate - current.turnRate) / WARMUP_TURN_TOLERANCE
195
+ + Math.abs(prev.duration - current.duration) / WARMUP_DURATION_TOLERANCE;
196
+
197
+ return Math.max(0, 1 - dist / 3);
198
+ }
199
+
200
+ /**
201
+ * Calculate the cast time multiplier from the warmup system.
202
+ * Returns < 1 for bonus (faster), > 1 for penalty (slower), 1 for neutral.
203
+ *
204
+ * Similar to previous cast → multiplier approaches (1 - MAX_BONUS) = 0.80
205
+ * Very different from previous → multiplier approaches (1 + MAX_PENALTY) = 1.20
206
+ * No previous cast → full bonus (1 - MAX_BONUS) = 0.80
207
+ */
208
+ export function calculateWarmupMultiplier(prev: MissileConfig | undefined, current: MissileConfig): number
209
+ {
210
+ const similarity = calculateMissileSimilarity(prev, current);
211
+ // similarity=1 → bonus (faster): 1 - MAX_BONUS
212
+ // similarity=0 → penalty (slower): 1 + MAX_PENALTY
213
+ // Linear interpolation between penalty and bonus
214
+ return (1 + WARMUP_MAX_PENALTY) - similarity * (WARMUP_MAX_BONUS + WARMUP_MAX_PENALTY);
215
+ }
216
+
217
+ // === MATCH ===
218
+ export const MATCH_DURATION = 30000; // ticks (5 minutes)
219
+
220
+ // ============================================================
221
+ // NEW API CONSTANT ALIASES
222
+ // These are the preferred names for the new hooks-based API
223
+ // ============================================================
224
+
225
+ // Health & Combat
226
+ export const MAX_HEALTH = WIZARD_HEALTH;
227
+ export const COLLISION_RADIUS = WIZARD_RADIUS;
228
+
229
+ // Movement
230
+ export const MOVE_SPEED = MOVEMENT_SPEED;
231
+
232
+ // Missiles
233
+ export const MISSILE_RADIUS_PER_DAMAGE = MISSILE_DAMAGE_RADIUS_SCALE;
234
+
235
+ // Shield (aliases for consistency)
236
+ export const SHIELD_MAX_STRENGTH = SHIELD_MAX_BLOCK;
237
+ export const SHIELD_MIN_STRENGTH = SHIELD_MIN_BLOCK;
238
+ export const SHIELD_DECAY_RATE = SHIELD_DECAY_PER_SECOND;
239
+
240
+ // Blink (aliases for consistency)
241
+ export const BLINK_MAX_RANGE = BLINK_RANGE;
242
+
243
+ /**
244
+ * Calculate blink cooldown based on distance traveled.
245
+ * Short blinks get short cooldowns, full-range blinks get the maximum.
246
+ */
247
+ export function calculateBlinkCooldown(distance: number): number
248
+ {
249
+ const ratio = Math.min(1, distance / BLINK_MAX_RANGE);
250
+ return Math.max(BLINK_MIN_COOLDOWN, Math.round(ratio * BLINK_MAX_COOLDOWN));
251
+ }
252
+
253
+ // Arena
254
+ export const ARENA_WATER_BUFFER = 200; // units - missiles can fly this far past arena bounds