@vibemancer/core 0.1.4 → 0.1.5
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 +2 -2
- package/dist/chunk-W7HWJFQ4.js +10599 -0
- package/dist/chunk-W7HWJFQ4.js.map +1 -0
- package/dist/index-browser.d.ts +1300 -1051
- package/dist/index-browser.js +55 -17
- package/dist/index.d.ts +53 -3
- package/dist/index.js +200 -54
- package/dist/index.js.map +1 -1
- package/package.json +21 -15
- package/src/bots/Hero.ts +867 -0
- package/src/bots/berserker/01_Stormchaser.ts +162 -120
- package/src/bots/berserker/02_Stormcaller.ts +160 -116
- package/src/bots/berserker/03_Stormforger.ts +162 -129
- package/src/bots/caster/01_Flamecaller.ts +126 -84
- package/src/bots/caster/02_Pyromancer.ts +148 -101
- package/src/bots/caster/03_Infernalist.ts +180 -160
- package/src/bots/defensive/01_Turtle.ts +48 -44
- package/src/bots/defensive/02_Sentinel.ts +57 -53
- package/src/bots/defensive/03_Golem.ts +85 -97
- package/src/bots/duelist/01_Battlemage.ts +157 -122
- package/src/bots/duelist/02_Warmage.ts +166 -121
- package/src/bots/duelist/03_Archmage.ts +198 -178
- package/src/bots/homing/01_Bonemancer.ts +20 -32
- package/src/bots/homing/02_Lich.ts +145 -93
- package/src/bots/homing/03_Archlich.ts +129 -60
- package/src/bots/kiter/01_Spellspinner.ts +145 -107
- package/src/bots/kiter/02_Spellweaver.ts +153 -105
- package/src/bots/kiter/03_Spellbinder.ts +168 -123
- package/src/bots/melee/01_Shadowblade.ts +131 -75
- package/src/bots/melee/02_Nightblade.ts +174 -130
- package/src/bots/melee/03_Voidblade.ts +266 -168
- package/src/bots/registry.ts +44 -37
- package/src/bots/shared.ts +185 -25
- package/src/bots/sniper/01_Spellshot.ts +151 -98
- package/src/bots/sniper/02_Spelltracer.ts +173 -128
- package/src/bots/sniper/03_Spellseeker.ts +177 -152
- package/src/bots/standalone/Critter.ts +46 -52
- package/src/bots/standalone/Doombringer.ts +36 -40
- package/src/bots/standalone/Hogger.ts +120 -89
- package/src/bots/standalone/Rookie.ts +21 -23
- package/src/bots/standalone/TargetDummy.ts +5 -6
- package/src/bots/test/cheater.ts +91 -85
- package/src/bots/test/crasher.ts +26 -28
- package/src/engine/bundle-fight.ts +242 -0
- package/src/engine/hooks-runtime.ts +58 -18
- package/src/engine/manual-match.ts +33 -25
- package/src/engine/missile-templates.ts +25 -43
- package/src/engine/optimizer.ts +7 -7
- package/src/engine/params-runtime.ts +3 -3
- package/src/engine/physics.ts +33 -23
- package/src/engine/sandbox-browser.ts +8 -7
- package/src/engine/sandbox-compile.ts +1 -1
- package/src/engine/sandbox-harness.ts +299 -367
- package/src/engine/sandbox.ts +3 -3
- package/src/engine/simulation.ts +291 -76
- package/src/engine/spells.ts +9 -12
- package/src/engine-version.ts +1 -1
- package/src/hooks/action-builders.ts +76 -21
- package/src/hooks/index.ts +17 -9
- package/src/hooks/state-hooks.ts +61 -25
- package/src/hooks/threat-analysis.ts +44 -20
- package/src/hooks/types.ts +62 -5
- package/src/index.ts +2 -0
- package/src/rules.ts +209 -63
- package/src/stats.ts +2 -2
- package/src/testing.ts +6 -30
- package/src/trace.ts +63 -3
- package/src/types.ts +127 -14
- package/src/utils/angles.ts +1 -0
- package/src/utils/combat.ts +98 -6
- package/src/utils/spatial.ts +3 -3
- package/dist/chunk-74FFJCFF.js +0 -9140
- package/dist/chunk-74FFJCFF.js.map +0 -1
- package/src/hooks/bot-wrapper.ts +0 -84
package/src/rules.ts
CHANGED
|
@@ -3,49 +3,120 @@
|
|
|
3
3
|
*
|
|
4
4
|
* This is the single source of truth for all game constants.
|
|
5
5
|
* All game logic imports from here. Read this to understand the game.
|
|
6
|
+
*
|
|
7
|
+
* SWEEPABLE CONSTANTS — the balance-search ruleset:
|
|
8
|
+
* Every combat-balance constant lives on the mutable `RULES` object. The engine
|
|
9
|
+
* and bots read `RULES.X` so the balance-search optimizer can override any of
|
|
10
|
+
* them at runtime via applyRulesetOverrides() — object property reads are live
|
|
11
|
+
* across every module and survive bundling (unlike a reassigned `let`, which
|
|
12
|
+
* esbuild/Vite snapshot at the import site). Structural constants (arena size,
|
|
13
|
+
* tick rate, hitbox radius, missile floors) stay plain `const`.
|
|
14
|
+
*
|
|
15
|
+
* The UPPER_CASE named exports below (WIZARD_HEALTH, SHIELD_CAST_TIME, …) are
|
|
16
|
+
* default SNAPSHOTS for external/UI/MCP consumers that only need the factory
|
|
17
|
+
* value. They do NOT track overrides — anything that must respond to a sweep
|
|
18
|
+
* reads `RULES.X`.
|
|
6
19
|
*/
|
|
7
20
|
|
|
8
21
|
import {MissileConfig} from './types.js';
|
|
9
22
|
|
|
10
|
-
// === TIMING ===
|
|
23
|
+
// === TIMING (structural) ===
|
|
11
24
|
export const TICKS_PER_SECOND = 100;
|
|
12
25
|
export const TICK_DURATION_MS = 10;
|
|
13
26
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
27
|
+
/* eslint-disable @typescript-eslint/naming-convention -- keys mirror constant names */
|
|
28
|
+
/**
|
|
29
|
+
* The mutable ruleset: the single source of truth for every sweepable combat
|
|
30
|
+
* constant. Engine + bots read these via `RULES.X`. Override with
|
|
31
|
+
* applyRulesetOverrides(); restore with resetRuleset().
|
|
32
|
+
*/
|
|
33
|
+
export const RULES = {
|
|
34
|
+
// Wizard
|
|
35
|
+
WIZARD_HEALTH: 60,
|
|
36
|
+
MOVEMENT_SPEED: 1.5, // units per tick (150 units/sec)
|
|
37
|
+
CASTING_MOVEMENT_MULT: 0.33, // 33% speed while casting
|
|
38
|
+
|
|
39
|
+
// GCD
|
|
40
|
+
GCD_DURATION: 100, // ticks (1 second)
|
|
41
|
+
|
|
42
|
+
// Shield
|
|
43
|
+
SHIELD_CAST_TIME: 20, // ticks (0.2s)
|
|
44
|
+
SHIELD_MAX_BLOCK: 0.9, // 90%
|
|
45
|
+
SHIELD_DECAY_PER_SECOND: 0.2, // loses 20% per second
|
|
46
|
+
SHIELD_MIN_BLOCK: 0.3, // 30% minimum
|
|
47
|
+
|
|
48
|
+
// Blink
|
|
49
|
+
BLINK_CAST_TIME: 10, // ticks (0.1s)
|
|
50
|
+
BLINK_RANGE: 300, // units
|
|
51
|
+
BLINK_MAX_COOLDOWN: 2000, // ticks (20 seconds) — full-range blink
|
|
52
|
+
BLINK_MIN_COOLDOWN: 100, // ticks (1 second) — micro-blink floor
|
|
53
|
+
|
|
54
|
+
// Knockback
|
|
55
|
+
KNOCKBACK_DAMAGE_THRESHOLD: 10, // damage above this causes knockback
|
|
56
|
+
KNOCKBACK_SPEED_PER_DAMAGE: 2, // knockback speed per damage over threshold
|
|
57
|
+
KNOCKBACK_DELAY: 5, // ticks before the impulse applies (explosion expand)
|
|
58
|
+
KNOCKBACK_DECAY: 0.7, // per-tick knockback velocity decay
|
|
59
|
+
|
|
60
|
+
// Missile cast-time formula
|
|
61
|
+
MISSILE_MIN_CAST_TIME: 0.1, // seconds
|
|
62
|
+
MISSILE_BASE_RADIUS: 2, // base hitbox radius in units
|
|
63
|
+
MISSILE_DAMAGE_RADIUS_SCALE: 0.1, // additional radius per damage point
|
|
64
|
+
MISSILE_BASE_CAST: 0.1,
|
|
65
|
+
MISSILE_DAMAGE_SCALE: 0.226,
|
|
66
|
+
MISSILE_DAMAGE_POWER: 2 / 3, // sublinear: DPS always increases with damage
|
|
67
|
+
MISSILE_HOMING_COEFF: 0.12,
|
|
68
|
+
MISSILE_TURN_DURATION_COEFF: 0.10,
|
|
69
|
+
MISSILE_SPEED_DURATION_BASELINE: 1.5,
|
|
70
|
+
MISSILE_SPEED_DURATION_COEFF: 0.025,
|
|
71
|
+
|
|
72
|
+
// Cast warmup
|
|
73
|
+
WARMUP_MAX_BONUS: 0.20, // 20% cast time reduction at full warmup
|
|
74
|
+
WARMUP_MAX_PENALTY: 0.20, // 20% cast time increase when switching
|
|
75
|
+
};
|
|
76
|
+
/* eslint-enable @typescript-eslint/naming-convention */
|
|
77
|
+
|
|
78
|
+
// === WIZARD (default snapshots — engine/bots read RULES.X) ===
|
|
79
|
+
export const WIZARD_HEALTH = RULES.WIZARD_HEALTH;
|
|
80
|
+
export const WIZARD_RADIUS = 5; // units (structural — hitbox)
|
|
81
|
+
export const MOVEMENT_SPEED = RULES.MOVEMENT_SPEED;
|
|
82
|
+
export const CASTING_MOVEMENT_MULT = RULES.CASTING_MOVEMENT_MULT;
|
|
19
83
|
|
|
20
84
|
// === GCD ===
|
|
21
|
-
export const GCD_DURATION =
|
|
85
|
+
export const GCD_DURATION = RULES.GCD_DURATION;
|
|
22
86
|
|
|
23
87
|
// === SHIELD ===
|
|
24
|
-
export const SHIELD_CAST_TIME =
|
|
25
|
-
export const SHIELD_MAX_BLOCK =
|
|
26
|
-
export const SHIELD_DECAY_PER_SECOND =
|
|
27
|
-
export const SHIELD_MIN_BLOCK =
|
|
88
|
+
export const SHIELD_CAST_TIME = RULES.SHIELD_CAST_TIME;
|
|
89
|
+
export const SHIELD_MAX_BLOCK = RULES.SHIELD_MAX_BLOCK;
|
|
90
|
+
export const SHIELD_DECAY_PER_SECOND = RULES.SHIELD_DECAY_PER_SECOND;
|
|
91
|
+
export const SHIELD_MIN_BLOCK = RULES.SHIELD_MIN_BLOCK;
|
|
28
92
|
// Note: Shield block % is applied directly to damage (no additional scaling)
|
|
29
93
|
|
|
30
94
|
// === BLINK ===
|
|
31
|
-
export const BLINK_CAST_TIME =
|
|
32
|
-
export const BLINK_RANGE =
|
|
33
|
-
export const BLINK_MAX_COOLDOWN =
|
|
34
|
-
export const BLINK_MIN_COOLDOWN =
|
|
95
|
+
export const BLINK_CAST_TIME = RULES.BLINK_CAST_TIME;
|
|
96
|
+
export const BLINK_RANGE = RULES.BLINK_RANGE;
|
|
97
|
+
export const BLINK_MAX_COOLDOWN = RULES.BLINK_MAX_COOLDOWN; // full-range blink
|
|
98
|
+
export const BLINK_MIN_COOLDOWN = RULES.BLINK_MIN_COOLDOWN; // micro-blink floor
|
|
35
99
|
/** @deprecated Use BLINK_MAX_COOLDOWN */
|
|
36
|
-
export const BLINK_COOLDOWN = BLINK_MAX_COOLDOWN;
|
|
37
|
-
|
|
38
|
-
// ===
|
|
39
|
-
export const
|
|
40
|
-
export const
|
|
41
|
-
export const
|
|
100
|
+
export const BLINK_COOLDOWN = RULES.BLINK_MAX_COOLDOWN;
|
|
101
|
+
|
|
102
|
+
// === KNOCKBACK ===
|
|
103
|
+
export const KNOCKBACK_DAMAGE_THRESHOLD = RULES.KNOCKBACK_DAMAGE_THRESHOLD;
|
|
104
|
+
export const KNOCKBACK_SPEED_PER_DAMAGE = RULES.KNOCKBACK_SPEED_PER_DAMAGE;
|
|
105
|
+
export const KNOCKBACK_DELAY = RULES.KNOCKBACK_DELAY;
|
|
106
|
+
export const KNOCKBACK_DECAY = RULES.KNOCKBACK_DECAY;
|
|
107
|
+
|
|
108
|
+
// === ARENA (structural) ===
|
|
109
|
+
export const ARENA_SIZE = 860;
|
|
110
|
+
export const LAVA_BORDER_WIDTH = 30;
|
|
111
|
+
export const ARENA_MIN = LAVA_BORDER_WIDTH; // playfield starts here (30)
|
|
112
|
+
export const ARENA_MAX = ARENA_SIZE - LAVA_BORDER_WIDTH; // playfield ends here (830)
|
|
42
113
|
export const SPAWN_DISTANCE = 600;
|
|
43
114
|
|
|
44
|
-
// === MISSILE CONSTRAINTS ===
|
|
115
|
+
// === MISSILE CONSTRAINTS (structural) ===
|
|
45
116
|
export const MISSILE_MIN_DAMAGE = 1;
|
|
46
117
|
export const MISSILE_MIN_SPEED = 1.5; // must be faster than player
|
|
47
118
|
export const MISSILE_MIN_DURATION = 10; // ticks (0.1 seconds)
|
|
48
|
-
export const MISSILE_MIN_CAST_TIME =
|
|
119
|
+
export const MISSILE_MIN_CAST_TIME = RULES.MISSILE_MIN_CAST_TIME;
|
|
49
120
|
|
|
50
121
|
// === MISSILE HITBOX ===
|
|
51
122
|
// Missile hitbox scales with damage (design doc line 1122)
|
|
@@ -54,15 +125,15 @@ export const MISSILE_MIN_CAST_TIME = 0.1; // seconds
|
|
|
54
125
|
// - 6-15 damage: Small-Medium
|
|
55
126
|
// - 16-30 damage: Large
|
|
56
127
|
// - 31+ damage: Huge
|
|
57
|
-
export const MISSILE_BASE_RADIUS =
|
|
58
|
-
export const MISSILE_DAMAGE_RADIUS_SCALE =
|
|
128
|
+
export const MISSILE_BASE_RADIUS = RULES.MISSILE_BASE_RADIUS;
|
|
129
|
+
export const MISSILE_DAMAGE_RADIUS_SCALE = RULES.MISSILE_DAMAGE_RADIUS_SCALE;
|
|
59
130
|
|
|
60
131
|
/**
|
|
61
132
|
* Calculate missile hitbox radius based on damage.
|
|
62
133
|
*/
|
|
63
134
|
export function calculateMissileRadius(damage: number): number
|
|
64
135
|
{
|
|
65
|
-
return MISSILE_BASE_RADIUS + damage * MISSILE_DAMAGE_RADIUS_SCALE;
|
|
136
|
+
return (RULES.MISSILE_BASE_RADIUS + damage * RULES.MISSILE_DAMAGE_RADIUS_SCALE) * 3;
|
|
66
137
|
}
|
|
67
138
|
|
|
68
139
|
// === MISSILE CAST TIME FORMULA ===
|
|
@@ -77,27 +148,21 @@ export function calculateMissileRadius(damage: number): number
|
|
|
77
148
|
// d=20: ~1.77s cast → DPS 7.23, TTK 7.3s (medium, borderline shieldable)
|
|
78
149
|
// d=30: ~2.28s cast → DPS 9.14, TTK 5.6s (slow, shieldable)
|
|
79
150
|
// d=60: ~3.57s cast → DPS 13.1, TTK 3.6s (very slow, easily shielded)
|
|
80
|
-
export const MISSILE_BASE_CAST =
|
|
81
|
-
export const MISSILE_DAMAGE_SCALE =
|
|
82
|
-
export const MISSILE_DAMAGE_POWER =
|
|
83
|
-
export const MISSILE_HOMING_COEFF =
|
|
84
|
-
export const MISSILE_TURN_DURATION_COEFF =
|
|
85
|
-
export const MISSILE_SPEED_DURATION_BASELINE =
|
|
86
|
-
export const MISSILE_SPEED_DURATION_COEFF =
|
|
151
|
+
export const MISSILE_BASE_CAST = RULES.MISSILE_BASE_CAST;
|
|
152
|
+
export const MISSILE_DAMAGE_SCALE = RULES.MISSILE_DAMAGE_SCALE;
|
|
153
|
+
export const MISSILE_DAMAGE_POWER = RULES.MISSILE_DAMAGE_POWER; // sublinear: DPS always increases with damage
|
|
154
|
+
export const MISSILE_HOMING_COEFF = RULES.MISSILE_HOMING_COEFF;
|
|
155
|
+
export const MISSILE_TURN_DURATION_COEFF = RULES.MISSILE_TURN_DURATION_COEFF;
|
|
156
|
+
export const MISSILE_SPEED_DURATION_BASELINE = RULES.MISSILE_SPEED_DURATION_BASELINE;
|
|
157
|
+
export const MISSILE_SPEED_DURATION_COEFF = RULES.MISSILE_SPEED_DURATION_COEFF;
|
|
87
158
|
|
|
88
159
|
/**
|
|
89
160
|
* Maps turnRate to effective cost for the cast time formula.
|
|
90
|
-
*
|
|
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.
|
|
161
|
+
* Higher turn rate = more expensive cast. Negative values clamped to 0.
|
|
95
162
|
*/
|
|
96
163
|
export function effectiveTurnRateCost(turnRate: number): number
|
|
97
164
|
{
|
|
98
|
-
|
|
99
|
-
const abs = Math.abs(turnRate);
|
|
100
|
-
return -(abs / (1 + abs));
|
|
165
|
+
return Math.max(0, turnRate);
|
|
101
166
|
}
|
|
102
167
|
|
|
103
168
|
/**
|
|
@@ -109,7 +174,7 @@ export function validateMissileConfig(config: MissileConfig): MissileConfig
|
|
|
109
174
|
return {
|
|
110
175
|
damage: Math.max(MISSILE_MIN_DAMAGE, Number.isFinite(config.damage) ? config.damage : MISSILE_MIN_DAMAGE),
|
|
111
176
|
speed: Math.max(MISSILE_MIN_SPEED, Number.isFinite(config.speed) ? config.speed : MISSILE_MIN_SPEED),
|
|
112
|
-
turnRate: Number.isFinite(config.turnRate) ? config.turnRate : 0,
|
|
177
|
+
turnRate: Math.max(0, Number.isFinite(config.turnRate) ? config.turnRate : 0),
|
|
113
178
|
duration: Math.max(MISSILE_MIN_DURATION, Number.isFinite(config.duration) ? Math.floor(config.duration) : MISSILE_MIN_DURATION),
|
|
114
179
|
};
|
|
115
180
|
}
|
|
@@ -124,7 +189,7 @@ export function validateMissileConfig(config: MissileConfig): MissileConfig
|
|
|
124
189
|
* + 0.10 × (effectiveTurnRateCost(turnRate) × durationSeconds)
|
|
125
190
|
* + 0.025 × (speed × durationSeconds - 1.5)
|
|
126
191
|
*
|
|
127
|
-
*
|
|
192
|
+
* turnRate is clamped to >= 0. Cost is linear.
|
|
128
193
|
*
|
|
129
194
|
* If lastMissileConfig is a MissileConfig, applies warmup multiplier:
|
|
130
195
|
* - Similar to previous: up to 20% faster
|
|
@@ -136,13 +201,13 @@ export function calculateMissileCastTime(config: MissileConfig, lastMissileConfi
|
|
|
136
201
|
const durationSeconds = duration / TICKS_PER_SECOND;
|
|
137
202
|
const turnCost = effectiveTurnRateCost(turnRate);
|
|
138
203
|
|
|
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);
|
|
204
|
+
const raw = RULES.MISSILE_BASE_CAST
|
|
205
|
+
+ RULES.MISSILE_DAMAGE_SCALE * Math.pow(damage, RULES.MISSILE_DAMAGE_POWER)
|
|
206
|
+
+ RULES.MISSILE_HOMING_COEFF * turnCost
|
|
207
|
+
+ RULES.MISSILE_TURN_DURATION_COEFF * (turnCost * durationSeconds)
|
|
208
|
+
+ RULES.MISSILE_SPEED_DURATION_COEFF * (speed * durationSeconds - RULES.MISSILE_SPEED_DURATION_BASELINE);
|
|
144
209
|
|
|
145
|
-
const baseCast = Math.max(MISSILE_MIN_CAST_TIME, raw);
|
|
210
|
+
const baseCast = Math.max(RULES.MISSILE_MIN_CAST_TIME, raw);
|
|
146
211
|
|
|
147
212
|
// Warmup is applied when lastMissileConfig is an actual MissileConfig.
|
|
148
213
|
// Note: passing undefined triggers the default (null) due to JS semantics,
|
|
@@ -152,7 +217,7 @@ export function calculateMissileCastTime(config: MissileConfig, lastMissileConfi
|
|
|
152
217
|
if (lastMissileConfig !== null)
|
|
153
218
|
{
|
|
154
219
|
const multiplier = calculateWarmupMultiplier(lastMissileConfig, config);
|
|
155
|
-
return Math.max(MISSILE_MIN_CAST_TIME, baseCast * multiplier);
|
|
220
|
+
return Math.max(RULES.MISSILE_MIN_CAST_TIME, baseCast * multiplier);
|
|
156
221
|
}
|
|
157
222
|
|
|
158
223
|
return baseCast;
|
|
@@ -168,8 +233,8 @@ export function calculateMissileCastTime(config: MissileConfig, lastMissileConfi
|
|
|
168
233
|
// Similar to previous: cast × 0.80 (20% faster)
|
|
169
234
|
// First cast (no previous): cast × 0.80 (full warmup, no conflict)
|
|
170
235
|
// Very different from previous: cast × 1.20 (20% slower, switching penalty)
|
|
171
|
-
export const WARMUP_MAX_BONUS =
|
|
172
|
-
export const WARMUP_MAX_PENALTY =
|
|
236
|
+
export const WARMUP_MAX_BONUS = RULES.WARMUP_MAX_BONUS;
|
|
237
|
+
export const WARMUP_MAX_PENALTY = RULES.WARMUP_MAX_PENALTY;
|
|
173
238
|
// Tolerances for similarity calculation (differences within tolerance are "similar"):
|
|
174
239
|
// Note: damage is intentionally excluded — it doesn't define playstyle.
|
|
175
240
|
// A melee bot stabbing at d=10 and d=25 is the same style, just different power.
|
|
@@ -190,9 +255,11 @@ export function calculateMissileSimilarity(prev: MissileConfig | undefined, curr
|
|
|
190
255
|
{
|
|
191
256
|
if (!prev) return 1; // No previous missile = no conflicting muscle memory = full warmup
|
|
192
257
|
|
|
258
|
+
// Stryker disable all: /WARMUP_TURN_TOLERANCE equivalent when tolerance=1 (dividing by 1 = multiplying by 1)
|
|
193
259
|
const dist = Math.abs(prev.speed - current.speed) / WARMUP_SPEED_TOLERANCE
|
|
194
260
|
+ Math.abs(prev.turnRate - current.turnRate) / WARMUP_TURN_TOLERANCE
|
|
195
261
|
+ Math.abs(prev.duration - current.duration) / WARMUP_DURATION_TOLERANCE;
|
|
262
|
+
// Stryker restore all
|
|
196
263
|
|
|
197
264
|
return Math.max(0, 1 - dist / 3);
|
|
198
265
|
}
|
|
@@ -211,10 +278,10 @@ export function calculateWarmupMultiplier(prev: MissileConfig | undefined, curre
|
|
|
211
278
|
// similarity=1 → bonus (faster): 1 - MAX_BONUS
|
|
212
279
|
// similarity=0 → penalty (slower): 1 + MAX_PENALTY
|
|
213
280
|
// Linear interpolation between penalty and bonus
|
|
214
|
-
return (1 + WARMUP_MAX_PENALTY) - similarity * (WARMUP_MAX_BONUS + WARMUP_MAX_PENALTY);
|
|
281
|
+
return (1 + RULES.WARMUP_MAX_PENALTY) - similarity * (RULES.WARMUP_MAX_BONUS + RULES.WARMUP_MAX_PENALTY);
|
|
215
282
|
}
|
|
216
283
|
|
|
217
|
-
// === MATCH ===
|
|
284
|
+
// === MATCH (structural) ===
|
|
218
285
|
export const MATCH_DURATION = 30000; // ticks (5 minutes)
|
|
219
286
|
|
|
220
287
|
// ============================================================
|
|
@@ -223,22 +290,22 @@ export const MATCH_DURATION = 30000; // ticks (5 minutes)
|
|
|
223
290
|
// ============================================================
|
|
224
291
|
|
|
225
292
|
// Health & Combat
|
|
226
|
-
export const MAX_HEALTH = WIZARD_HEALTH;
|
|
293
|
+
export const MAX_HEALTH = RULES.WIZARD_HEALTH;
|
|
227
294
|
export const COLLISION_RADIUS = WIZARD_RADIUS;
|
|
228
295
|
|
|
229
296
|
// Movement
|
|
230
|
-
export const MOVE_SPEED = MOVEMENT_SPEED;
|
|
297
|
+
export const MOVE_SPEED = RULES.MOVEMENT_SPEED;
|
|
231
298
|
|
|
232
299
|
// Missiles
|
|
233
|
-
export const MISSILE_RADIUS_PER_DAMAGE = MISSILE_DAMAGE_RADIUS_SCALE;
|
|
300
|
+
export const MISSILE_RADIUS_PER_DAMAGE = RULES.MISSILE_DAMAGE_RADIUS_SCALE;
|
|
234
301
|
|
|
235
302
|
// 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;
|
|
303
|
+
export const SHIELD_MAX_STRENGTH = RULES.SHIELD_MAX_BLOCK;
|
|
304
|
+
export const SHIELD_MIN_STRENGTH = RULES.SHIELD_MIN_BLOCK;
|
|
305
|
+
export const SHIELD_DECAY_RATE = RULES.SHIELD_DECAY_PER_SECOND;
|
|
239
306
|
|
|
240
307
|
// Blink (aliases for consistency)
|
|
241
|
-
export const BLINK_MAX_RANGE = BLINK_RANGE;
|
|
308
|
+
export const BLINK_MAX_RANGE = RULES.BLINK_RANGE;
|
|
242
309
|
|
|
243
310
|
/**
|
|
244
311
|
* Calculate blink cooldown based on distance traveled.
|
|
@@ -246,9 +313,88 @@ export const BLINK_MAX_RANGE = BLINK_RANGE;
|
|
|
246
313
|
*/
|
|
247
314
|
export function calculateBlinkCooldown(distance: number): number
|
|
248
315
|
{
|
|
249
|
-
const ratio = Math.min(1, distance /
|
|
250
|
-
return Math.max(BLINK_MIN_COOLDOWN, Math.round(ratio * BLINK_MAX_COOLDOWN));
|
|
316
|
+
const ratio = Math.min(1, distance / RULES.BLINK_RANGE);
|
|
317
|
+
return Math.max(RULES.BLINK_MIN_COOLDOWN, Math.round(ratio * RULES.BLINK_MAX_COOLDOWN));
|
|
251
318
|
}
|
|
252
319
|
|
|
253
320
|
// Arena
|
|
254
321
|
export const ARENA_WATER_BUFFER = 200; // units - missiles can fly this far past arena bounds
|
|
322
|
+
|
|
323
|
+
// ============================================================
|
|
324
|
+
// RULESET OVERRIDES (for the balance-search optimizer)
|
|
325
|
+
// ============================================================
|
|
326
|
+
//
|
|
327
|
+
// applyRulesetOverrides() mutates the RULES object in place; every engine/bot
|
|
328
|
+
// read of RULES.X sees the new value immediately. Each optimizer worker calls
|
|
329
|
+
// applyRulesetOverrides() before a batch of sims and resetRuleset() after, so a
|
|
330
|
+
// sweep never leaks across evaluations.
|
|
331
|
+
|
|
332
|
+
/** Factory defaults, snapshotted from the live values at module load (no drift). */
|
|
333
|
+
const RULES_DEFAULTS: Record<string, number> = {...RULES};
|
|
334
|
+
|
|
335
|
+
/* eslint-disable @typescript-eslint/naming-convention -- keys mirror constant names */
|
|
336
|
+
/**
|
|
337
|
+
* Search bounds for each sweepable constant. The current value is the start.
|
|
338
|
+
* Every key here is read by the engine/bots through `RULES.X`, so overrides
|
|
339
|
+
* take effect across the whole simulation.
|
|
340
|
+
*/
|
|
341
|
+
export const RULESET_RANGES: Record<string, {min: number; max: number}> = {
|
|
342
|
+
// Wizard
|
|
343
|
+
WIZARD_HEALTH: {min: 30, max: 120},
|
|
344
|
+
MOVEMENT_SPEED: {min: 0.75, max: 3},
|
|
345
|
+
CASTING_MOVEMENT_MULT: {min: 0, max: 1},
|
|
346
|
+
// GCD
|
|
347
|
+
GCD_DURATION: {min: 50, max: 200},
|
|
348
|
+
// Shield
|
|
349
|
+
SHIELD_CAST_TIME: {min: 5, max: 60},
|
|
350
|
+
SHIELD_MAX_BLOCK: {min: 0.5, max: 0.95},
|
|
351
|
+
SHIELD_DECAY_PER_SECOND: {min: 0, max: 0.6},
|
|
352
|
+
SHIELD_MIN_BLOCK: {min: 0, max: 0.6},
|
|
353
|
+
// Blink
|
|
354
|
+
BLINK_CAST_TIME: {min: 2, max: 40},
|
|
355
|
+
BLINK_RANGE: {min: 150, max: 500},
|
|
356
|
+
BLINK_MAX_COOLDOWN: {min: 500, max: 4000},
|
|
357
|
+
BLINK_MIN_COOLDOWN: {min: 20, max: 400},
|
|
358
|
+
// Knockback (anti-melee lever)
|
|
359
|
+
KNOCKBACK_DAMAGE_THRESHOLD: {min: 0, max: 30},
|
|
360
|
+
KNOCKBACK_SPEED_PER_DAMAGE: {min: 0, max: 8},
|
|
361
|
+
KNOCKBACK_DELAY: {min: 0, max: 15},
|
|
362
|
+
KNOCKBACK_DECAY: {min: 0.3, max: 0.95},
|
|
363
|
+
// Missile cast-time formula (the core "cast speed" balance lever)
|
|
364
|
+
MISSILE_BASE_CAST: {min: 0, max: 0.5},
|
|
365
|
+
MISSILE_DAMAGE_SCALE: {min: 0.1, max: 0.5},
|
|
366
|
+
MISSILE_DAMAGE_POWER: {min: 0.4, max: 1},
|
|
367
|
+
MISSILE_HOMING_COEFF: {min: 0, max: 0.4},
|
|
368
|
+
MISSILE_TURN_DURATION_COEFF: {min: 0, max: 0.3},
|
|
369
|
+
MISSILE_SPEED_DURATION_BASELINE: {min: 0.5, max: 3},
|
|
370
|
+
MISSILE_SPEED_DURATION_COEFF: {min: 0, max: 0.1},
|
|
371
|
+
MISSILE_MIN_CAST_TIME: {min: 0.05, max: 0.5},
|
|
372
|
+
// Missile hitbox
|
|
373
|
+
MISSILE_BASE_RADIUS: {min: 0.5, max: 5},
|
|
374
|
+
MISSILE_DAMAGE_RADIUS_SCALE: {min: 0, max: 0.3},
|
|
375
|
+
// Warmup
|
|
376
|
+
WARMUP_MAX_BONUS: {min: 0, max: 0.5},
|
|
377
|
+
WARMUP_MAX_PENALTY: {min: 0, max: 0.5},
|
|
378
|
+
};
|
|
379
|
+
/* eslint-enable @typescript-eslint/naming-convention */
|
|
380
|
+
|
|
381
|
+
/** Merge overrides onto the RULES object and apply them globally. */
|
|
382
|
+
export function applyRulesetOverrides(overrides: Record<string, number>): void
|
|
383
|
+
{
|
|
384
|
+
for (const [key, value] of Object.entries(overrides))
|
|
385
|
+
{
|
|
386
|
+
if (key in RULES) (RULES as Record<string, number>)[key] = value;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** Restore all swept constants to their factory defaults. */
|
|
391
|
+
export function resetRuleset(): void
|
|
392
|
+
{
|
|
393
|
+
Object.assign(RULES, RULES_DEFAULTS);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** Current value of every sweepable constant (the optimizer's starting point). */
|
|
397
|
+
export function currentRuleset(): Record<string, number>
|
|
398
|
+
{
|
|
399
|
+
return {...RULES};
|
|
400
|
+
}
|
package/src/stats.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import type {GameState} from './types.js';
|
|
9
9
|
import type {SimulateResult} from './engine/simulation.js';
|
|
10
|
-
import {
|
|
10
|
+
import {RULES, TICKS_PER_SECOND} from './rules.js';
|
|
11
11
|
|
|
12
12
|
/** Detailed statistics for one bot in a single match. */
|
|
13
13
|
export interface FightStats
|
|
@@ -67,7 +67,7 @@ export interface FightStats
|
|
|
67
67
|
function shieldBlockPct(channelDurationTicks: number): number
|
|
68
68
|
{
|
|
69
69
|
const channelSec = channelDurationTicks / TICKS_PER_SECOND;
|
|
70
|
-
return Math.max(SHIELD_MIN_BLOCK, SHIELD_MAX_BLOCK - SHIELD_DECAY_PER_SECOND * channelSec);
|
|
70
|
+
return Math.max(RULES.SHIELD_MIN_BLOCK, RULES.SHIELD_MAX_BLOCK - RULES.SHIELD_DECAY_PER_SECOND * channelSec);
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
/**
|
package/src/testing.ts
CHANGED
|
@@ -14,11 +14,9 @@
|
|
|
14
14
|
* });
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import type {
|
|
18
|
-
import type {WizardFunction} from './types.js';
|
|
17
|
+
import type {WizardFunction} from './hooks/types.js';
|
|
19
18
|
import type {FightResult, SimulateResult, MatchWinner, FightWinner} from './engine/simulation.js';
|
|
20
19
|
import {fight, simulate, FIGHT_SPAWN_DISTANCES} from './engine/simulation.js';
|
|
21
|
-
import {isNewStyleBot, wrapNewBot} from './hooks/bot-wrapper.js';
|
|
22
20
|
import {extractStats} from './stats.js';
|
|
23
21
|
import type {FightStats} from './stats.js';
|
|
24
22
|
|
|
@@ -97,34 +95,14 @@ export interface TestBotBuilder
|
|
|
97
95
|
function resolveBuiltinBot(name: string): WizardFunction
|
|
98
96
|
{
|
|
99
97
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- dynamic bot lookup
|
|
100
|
-
const botMap = allBots as unknown as Record<string, WizardFunction
|
|
98
|
+
const botMap = allBots as unknown as Record<string, WizardFunction>;
|
|
101
99
|
const bot = botMap[name];
|
|
102
100
|
if (!bot)
|
|
103
101
|
{
|
|
104
102
|
const available = Object.keys(botMap).filter((k) => typeof botMap[k] === 'function').join(', ');
|
|
105
103
|
throw new Error(`Unknown opponent "${name}". Available bots: ${available}`);
|
|
106
104
|
}
|
|
107
|
-
|
|
108
|
-
return typeof bot === 'function' && bot.length === 0 && isNewStyleBot(bot as BotFunction)
|
|
109
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- guarded by isNewStyleBot above
|
|
110
|
-
? wrapNewBot(bot as BotFunction)
|
|
111
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- legacy style bot after guard
|
|
112
|
-
: bot as WizardFunction;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Wrap a bot function for use with the simulation engine.
|
|
117
|
-
*/
|
|
118
|
-
function wrapBot(bot: WizardFunction | BotFunction): WizardFunction
|
|
119
|
-
{
|
|
120
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- runtime shape detection
|
|
121
|
-
if (isNewStyleBot(bot as BotFunction))
|
|
122
|
-
{
|
|
123
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- guarded by isNewStyleBot above
|
|
124
|
-
return wrapNewBot(bot as BotFunction);
|
|
125
|
-
}
|
|
126
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- legacy style bot after guard
|
|
127
|
-
return bot as WizardFunction;
|
|
105
|
+
return bot;
|
|
128
106
|
}
|
|
129
107
|
|
|
130
108
|
/**
|
|
@@ -155,15 +133,13 @@ function wrapBot(bot: WizardFunction | BotFunction): WizardFunction
|
|
|
155
133
|
* });
|
|
156
134
|
* ```
|
|
157
135
|
*/
|
|
158
|
-
export function testBot(bot: WizardFunction
|
|
136
|
+
export function testBot(bot: WizardFunction): TestBotBuilder
|
|
159
137
|
{
|
|
160
|
-
const wrappedBot = wrapBot(bot);
|
|
161
|
-
|
|
162
138
|
return {
|
|
163
139
|
fight(opponent: string, options?: {seed?: number}): TestFightResult
|
|
164
140
|
{
|
|
165
141
|
const opponentBot = resolveBuiltinBot(opponent);
|
|
166
|
-
const raw = fight(
|
|
142
|
+
const raw = fight(bot, opponentBot, {seed: options?.seed});
|
|
167
143
|
|
|
168
144
|
return {
|
|
169
145
|
raw,
|
|
@@ -184,7 +160,7 @@ export function testBot(bot: WizardFunction | BotFunction): TestBotBuilder
|
|
|
184
160
|
}): TestSimulateResult
|
|
185
161
|
{
|
|
186
162
|
const opponentBot = resolveBuiltinBot(opponent);
|
|
187
|
-
const raw = simulate(
|
|
163
|
+
const raw = simulate(bot, opponentBot, {
|
|
188
164
|
seed: options?.seed,
|
|
189
165
|
spawnDistance: options?.spawnDistance ?? FIGHT_SPAWN_DISTANCES[2], // 600 (middle distance)
|
|
190
166
|
maxTicks: options?.maxTicks,
|
package/src/trace.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* - User tests that want event-level analysis
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import type {GameState, ProjectileState} from './types.js';
|
|
14
|
+
import type {GameState, ProjectileState, SimEvent} from './types.js';
|
|
15
15
|
import type {SimulateResult, BotError} from './engine/simulation.js';
|
|
16
16
|
import {distanceTo} from './utils/distance.js';
|
|
17
17
|
|
|
@@ -31,6 +31,11 @@ export type TraceEventType =
|
|
|
31
31
|
| 'FIRE' // Missile launched (includes config + range)
|
|
32
32
|
| 'HIT' // Damage dealt to opponent
|
|
33
33
|
| 'HURT' // Damage taken from opponent
|
|
34
|
+
| 'DEATH' // Wizard killed (missile hit)
|
|
35
|
+
| 'LAVA_DEATH' // Wizard killed by lava (knockback/walk into border)
|
|
36
|
+
| 'SHIELD_BLOCK' // Shield absorbed damage (shows blocked + through amounts)
|
|
37
|
+
| 'KNOCKBACK' // Knocked back by missile impact
|
|
38
|
+
| 'BLINK' // Blink teleport (from → to positions)
|
|
34
39
|
| 'DODGE_START' // Enemy missile enters 150u proximity
|
|
35
40
|
| 'DODGE_CLOSE' // Enemy missile enters 50u proximity
|
|
36
41
|
| 'MOVE' // Movement summary (every 100 ticks)
|
|
@@ -58,6 +63,7 @@ export interface TraceBotSummary
|
|
|
58
63
|
shields: number;
|
|
59
64
|
blinks: number;
|
|
60
65
|
dodgeEncounters: number;
|
|
66
|
+
causeOfDeath: 'missile' | 'lava' | 'alive' | 'timeout';
|
|
61
67
|
movement: {strafe: number; approach: number; retreat: number; still: number};
|
|
62
68
|
}
|
|
63
69
|
|
|
@@ -189,6 +195,41 @@ export function extractTraceEvents(history: GameState[], errors?: BotError[]): T
|
|
|
189
195
|
}
|
|
190
196
|
}
|
|
191
197
|
|
|
198
|
+
// --- Simulation events (death, lava, shield block, knockback, blink) ---
|
|
199
|
+
if (state.events)
|
|
200
|
+
{
|
|
201
|
+
for (const ev of state.events)
|
|
202
|
+
{
|
|
203
|
+
const actor = actorFromId(ev);
|
|
204
|
+
if (ev.type === 'wizard-death')
|
|
205
|
+
{
|
|
206
|
+
events.push({tick, actor, type: 'DEATH', detail: `killed at (${ev.position.x.toFixed(0)},${ev.position.y.toFixed(0)})`});
|
|
207
|
+
}
|
|
208
|
+
else if (ev.type === 'wizard-lava-death')
|
|
209
|
+
{
|
|
210
|
+
events.push({tick, actor, type: 'LAVA_DEATH', detail: `walked/knocked into lava at (${ev.position.x.toFixed(0)},${ev.position.y.toFixed(0)})`});
|
|
211
|
+
}
|
|
212
|
+
else if (ev.type === 'shield-block')
|
|
213
|
+
{
|
|
214
|
+
events.push({tick, actor, type: 'SHIELD_BLOCK', detail: `blocked ${ev.damageBlocked.toFixed(1)} (${ev.damageThrough.toFixed(1)} through)`});
|
|
215
|
+
}
|
|
216
|
+
else if (ev.type === 'missile-hit')
|
|
217
|
+
{
|
|
218
|
+
const target = ev.targetId === 'wizard-1' ? 'W1' : 'W2';
|
|
219
|
+
const owner = ev.ownerId === 'wizard-1' ? 'W1' : 'W2';
|
|
220
|
+
if (ev.actualDamage < ev.damage)
|
|
221
|
+
{
|
|
222
|
+
events.push({tick, actor: target, type: 'HURT', detail: `hit by ${owner} missile: ${ev.damage} raw → ${ev.actualDamage.toFixed(1)} actual (shielded) dist=${distance.toFixed(0)}`});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
else if (ev.type === 'blink')
|
|
226
|
+
{
|
|
227
|
+
const blinkDist = distanceTo(ev.from, ev.to);
|
|
228
|
+
events.push({tick, actor, type: 'BLINK', detail: `(${ev.from.x.toFixed(0)},${ev.from.y.toFixed(0)}) → (${ev.to.x.toFixed(0)},${ev.to.y.toFixed(0)}) ${blinkDist.toFixed(0)}u`});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
192
233
|
// --- Movement summary every 100 ticks ---
|
|
193
234
|
if (tick > 0 && tick % 100 === 0)
|
|
194
235
|
{
|
|
@@ -265,6 +306,10 @@ export function summarizeTrace(
|
|
|
265
306
|
shields: myEvents.filter((e) => e.type === 'STATE' && e.detail.includes('shield')).length,
|
|
266
307
|
blinks: myEvents.filter((e) => e.type === 'STATE' && e.detail.includes('blink')).length,
|
|
267
308
|
dodgeEncounters: myEvents.filter((e) => e.type.startsWith('DODGE')).length,
|
|
309
|
+
causeOfDeath: myEvents.some((e) => e.type === 'LAVA_DEATH') ? 'lava'
|
|
310
|
+
: myEvents.some((e) => e.type === 'DEATH') ? 'missile'
|
|
311
|
+
: (final && ((actor === 'W1' ? final.health : final.enemies[0]?.health ?? 0) <= 0)) ? 'missile'
|
|
312
|
+
: result.winner === null ? 'timeout' : 'alive',
|
|
268
313
|
movement: {
|
|
269
314
|
strafe: moveEvents.filter((e) => e.detail.startsWith('STRAFE')).length,
|
|
270
315
|
approach: moveEvents.filter((e) => e.detail.startsWith('APPROACH')).length,
|
|
@@ -309,12 +354,20 @@ export function formatTraceEvents(events: TraceEvent[]): string
|
|
|
309
354
|
export function formatTraceSummary(summary: TraceSummary): string
|
|
310
355
|
{
|
|
311
356
|
const {w1Name, w2Name, w1, w2} = summary;
|
|
357
|
+
function formatBot(name: string, b: TraceBotSummary): string
|
|
358
|
+
{
|
|
359
|
+
const hitPct = b.missilesLaunched > 0 ? ((b.hits / b.missilesLaunched) * 100).toFixed(0) : 'N/A';
|
|
360
|
+
const death = b.causeOfDeath === 'lava' ? ' [LAVA DEATH]'
|
|
361
|
+
: b.causeOfDeath === 'missile' ? ' [KILLED]'
|
|
362
|
+
: b.causeOfDeath === 'timeout' ? '' : '';
|
|
363
|
+
return ` ${name}: ${b.missilesLaunched}m ${b.hits}h (${hitPct}%), ${b.damageDealt.toFixed(1)} dealt, ${b.damageReceived.toFixed(1)} taken, ${b.shields}s ${b.blinks}b${death}`;
|
|
364
|
+
}
|
|
312
365
|
const lines = [
|
|
313
366
|
` Winner: ${summary.winner} @ T${summary.ticks}`,
|
|
314
367
|
` Final HP: ${w1Name}=${summary.w1FinalHp.toFixed(1)}, ${w2Name}=${summary.w2FinalHp.toFixed(1)}`,
|
|
315
368
|
'',
|
|
316
|
-
|
|
317
|
-
|
|
369
|
+
formatBot(w1Name, w1),
|
|
370
|
+
formatBot(w2Name, w2),
|
|
318
371
|
'',
|
|
319
372
|
` ${w1Name} movement: ${w1.movement.approach} approach, ${w1.movement.strafe} strafe, ${w1.movement.retreat} retreat, ${w1.movement.still} still | ${w1.dodgeEncounters} dodge encounters`,
|
|
320
373
|
];
|
|
@@ -413,6 +466,13 @@ function dodgeAngle(wizardVelocity: {x: number; y: number}, missileRotation: num
|
|
|
413
466
|
return Math.acos(Math.max(-1, Math.min(1, Math.abs(dot)))) * (180 / Math.PI);
|
|
414
467
|
}
|
|
415
468
|
|
|
469
|
+
function actorFromId(ev: SimEvent): 'W1' | 'W2'
|
|
470
|
+
{
|
|
471
|
+
if ('wizardId' in ev) return ev.wizardId === 'wizard-1' ? 'W1' : 'W2';
|
|
472
|
+
if ('ownerId' in ev) return ev.ownerId === 'wizard-1' ? 'W1' : 'W2';
|
|
473
|
+
return 'W1';
|
|
474
|
+
}
|
|
475
|
+
|
|
416
476
|
function classifyMovement(state: GameState, enemy: GameState['enemies'][0], speed: number): string
|
|
417
477
|
{
|
|
418
478
|
if (speed < 0.1) return 'STILL';
|