@vibemancer/core 0.1.0 → 0.1.1
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.
- package/README.md +28 -28
- package/dist/{chunk-L7Z7OFXD.js → chunk-7VDJHO22.js} +3 -3
- package/dist/chunk-7VDJHO22.js.map +1 -0
- package/dist/index-browser.d.ts +1 -1
- package/dist/index-browser.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +79 -78
- package/src/bots/berserker/01_Stormchaser.ts +457 -457
- package/src/bots/berserker/02_Stormcaller.ts +417 -417
- package/src/bots/berserker/03_Stormforger.ts +481 -481
- package/src/bots/caster/01_Flamecaller.ts +286 -286
- package/src/bots/caster/02_Pyromancer.ts +350 -350
- package/src/bots/caster/03_Infernalist.ts +492 -492
- package/src/bots/defensive/01_Turtle.ts +151 -151
- package/src/bots/defensive/02_Sentinel.ts +134 -134
- package/src/bots/defensive/03_Golem.ts +357 -357
- package/src/bots/duelist/01_Battlemage.ts +433 -433
- package/src/bots/duelist/02_Warmage.ts +438 -438
- package/src/bots/duelist/03_Archmage.ts +588 -588
- package/src/bots/homing/01_Bonemancer.ts +67 -67
- package/src/bots/homing/02_Lich.ts +356 -356
- package/src/bots/homing/03_Archlich.ts +220 -220
- package/src/bots/kiter/01_Spellspinner.ts +398 -398
- package/src/bots/kiter/02_Spellweaver.ts +378 -378
- package/src/bots/kiter/03_Spellbinder.ts +448 -448
- package/src/bots/melee/01_Shadowblade.ts +270 -270
- package/src/bots/melee/02_Nightblade.ts +437 -437
- package/src/bots/melee/03_Voidblade.ts +582 -582
- package/src/bots/sniper/01_Spellshot.ts +385 -385
- package/src/bots/sniper/02_Spelltracer.ts +441 -441
- package/src/bots/sniper/03_Spellseeker.ts +546 -546
- package/src/bots/standalone/Critter.ts +89 -89
- package/src/bots/standalone/Doombringer.ts +91 -91
- package/src/bots/standalone/Hogger.ts +228 -228
- package/src/bots/standalone/Rookie.ts +50 -50
- package/src/bots/standalone/TargetDummy.ts +21 -21
- package/src/bots/test/cheater.ts +405 -405
- package/src/bots/test/crasher.ts +81 -81
- package/src/engine/hooks-runtime.ts +394 -394
- package/src/engine/manual-match.ts +289 -289
- package/src/engine/missile-templates.ts +155 -155
- package/src/engine/physics.ts +143 -143
- package/src/engine/simulation.ts +828 -828
- package/src/engine/spells.ts +128 -128
- package/src/engine-version.ts +1 -1
- package/src/index.ts +23 -23
- package/src/rules.ts +254 -254
- package/src/types.ts +193 -193
- package/src/utils/index.ts +6 -6
- 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
|