@vibemancer/core 0.1.0 → 0.1.2

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 (50) hide show
  1. package/README.md +28 -28
  2. package/dist/{chunk-L7Z7OFXD.js → chunk-OL7ETV6V.js} +3 -3
  3. package/dist/chunk-OL7ETV6V.js.map +1 -0
  4. package/dist/index-browser.d.ts +1 -1
  5. package/dist/index-browser.js +1 -1
  6. package/dist/index.js +1 -1
  7. package/package.json +79 -78
  8. package/src/bots/berserker/01_Stormchaser.ts +457 -457
  9. package/src/bots/berserker/02_Stormcaller.ts +417 -417
  10. package/src/bots/berserker/03_Stormforger.ts +481 -481
  11. package/src/bots/caster/01_Flamecaller.ts +286 -286
  12. package/src/bots/caster/02_Pyromancer.ts +350 -350
  13. package/src/bots/caster/03_Infernalist.ts +492 -492
  14. package/src/bots/defensive/01_Turtle.ts +151 -151
  15. package/src/bots/defensive/02_Sentinel.ts +134 -134
  16. package/src/bots/defensive/03_Golem.ts +357 -357
  17. package/src/bots/duelist/01_Battlemage.ts +433 -433
  18. package/src/bots/duelist/02_Warmage.ts +438 -438
  19. package/src/bots/duelist/03_Archmage.ts +588 -588
  20. package/src/bots/homing/01_Bonemancer.ts +67 -67
  21. package/src/bots/homing/02_Lich.ts +356 -356
  22. package/src/bots/homing/03_Archlich.ts +220 -220
  23. package/src/bots/kiter/01_Spellspinner.ts +398 -398
  24. package/src/bots/kiter/02_Spellweaver.ts +378 -378
  25. package/src/bots/kiter/03_Spellbinder.ts +448 -448
  26. package/src/bots/melee/01_Shadowblade.ts +270 -270
  27. package/src/bots/melee/02_Nightblade.ts +437 -437
  28. package/src/bots/melee/03_Voidblade.ts +582 -582
  29. package/src/bots/sniper/01_Spellshot.ts +385 -385
  30. package/src/bots/sniper/02_Spelltracer.ts +441 -441
  31. package/src/bots/sniper/03_Spellseeker.ts +546 -546
  32. package/src/bots/standalone/Critter.ts +89 -89
  33. package/src/bots/standalone/Doombringer.ts +91 -91
  34. package/src/bots/standalone/Hogger.ts +228 -228
  35. package/src/bots/standalone/Rookie.ts +50 -50
  36. package/src/bots/standalone/TargetDummy.ts +21 -21
  37. package/src/bots/test/cheater.ts +405 -405
  38. package/src/bots/test/crasher.ts +81 -81
  39. package/src/engine/hooks-runtime.ts +394 -394
  40. package/src/engine/manual-match.ts +289 -289
  41. package/src/engine/missile-templates.ts +155 -155
  42. package/src/engine/physics.ts +143 -143
  43. package/src/engine/simulation.ts +828 -828
  44. package/src/engine/spells.ts +128 -128
  45. package/src/engine-version.ts +1 -1
  46. package/src/index.ts +23 -23
  47. package/src/rules.ts +254 -254
  48. package/src/types.ts +193 -193
  49. package/src/utils/index.ts +6 -6
  50. package/dist/chunk-L7Z7OFXD.js.map +0 -1
package/src/rules.ts CHANGED
@@ -1,254 +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
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