@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.
- package/README.md +28 -0
- package/dist/chunk-L7Z7OFXD.js +9140 -0
- package/dist/chunk-L7Z7OFXD.js.map +1 -0
- package/dist/index-browser.d.ts +2602 -0
- package/dist/index-browser.js +407 -0
- package/dist/index-browser.js.map +1 -0
- package/dist/index.d.ts +150 -0
- package/dist/index.js +750 -0
- package/dist/index.js.map +1 -0
- package/package.json +79 -0
- package/src/bots/berserker/01_Stormchaser.ts +457 -0
- package/src/bots/berserker/02_Stormcaller.ts +417 -0
- package/src/bots/berserker/03_Stormforger.ts +481 -0
- package/src/bots/caster/01_Flamecaller.ts +286 -0
- package/src/bots/caster/02_Pyromancer.ts +350 -0
- package/src/bots/caster/03_Infernalist.ts +492 -0
- package/src/bots/defensive/01_Turtle.ts +151 -0
- package/src/bots/defensive/02_Sentinel.ts +134 -0
- package/src/bots/defensive/03_Golem.ts +357 -0
- package/src/bots/duelist/01_Battlemage.ts +433 -0
- package/src/bots/duelist/02_Warmage.ts +438 -0
- package/src/bots/duelist/03_Archmage.ts +588 -0
- package/src/bots/homing/01_Bonemancer.ts +67 -0
- package/src/bots/homing/02_Lich.ts +356 -0
- package/src/bots/homing/03_Archlich.ts +220 -0
- package/src/bots/index.ts +30 -0
- package/src/bots/kiter/01_Spellspinner.ts +398 -0
- package/src/bots/kiter/02_Spellweaver.ts +378 -0
- package/src/bots/kiter/03_Spellbinder.ts +448 -0
- package/src/bots/melee/01_Shadowblade.ts +270 -0
- package/src/bots/melee/02_Nightblade.ts +437 -0
- package/src/bots/melee/03_Voidblade.ts +582 -0
- package/src/bots/registry.ts +207 -0
- package/src/bots/shared.ts +472 -0
- package/src/bots/sniper/01_Spellshot.ts +385 -0
- package/src/bots/sniper/02_Spelltracer.ts +441 -0
- package/src/bots/sniper/03_Spellseeker.ts +546 -0
- package/src/bots/standalone/Critter.ts +89 -0
- package/src/bots/standalone/Doombringer.ts +91 -0
- package/src/bots/standalone/Hogger.ts +228 -0
- package/src/bots/standalone/Rookie.ts +50 -0
- package/src/bots/standalone/TargetDummy.ts +21 -0
- package/src/bots/test/cheater.ts +405 -0
- package/src/bots/test/crasher.ts +81 -0
- package/src/engine/hooks-runtime.ts +394 -0
- package/src/engine/manual-match.ts +289 -0
- package/src/engine/missile-templates.ts +155 -0
- package/src/engine/optimizer.ts +220 -0
- package/src/engine/params-runtime.ts +189 -0
- package/src/engine/physics.ts +143 -0
- package/src/engine/sandbox-browser.ts +671 -0
- package/src/engine/sandbox-compile.ts +197 -0
- package/src/engine/sandbox-harness.ts +367 -0
- package/src/engine/sandbox.ts +332 -0
- package/src/engine/simulation.ts +828 -0
- package/src/engine/spells.ts +128 -0
- package/src/engine-version.ts +11 -0
- package/src/hooks/action-builders.ts +210 -0
- package/src/hooks/bot-wrapper.ts +84 -0
- package/src/hooks/index.ts +75 -0
- package/src/hooks/state-hooks.ts +354 -0
- package/src/hooks/threat-analysis.ts +365 -0
- package/src/hooks/types.ts +142 -0
- package/src/index-browser.ts +30 -0
- package/src/index.ts +24 -0
- package/src/rules.ts +254 -0
- package/src/stats.ts +262 -0
- package/src/testing.ts +207 -0
- package/src/trace.ts +430 -0
- package/src/types.ts +193 -0
- package/src/utils/angles.ts +47 -0
- package/src/utils/combat.ts +279 -0
- package/src/utils/distance.ts +21 -0
- package/src/utils/index.ts +7 -0
- package/src/utils/movement.ts +108 -0
- package/src/utils/random.ts +65 -0
- package/src/utils/spatial.ts +63 -0
- package/src/utils/targeting.ts +45 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VIBEMANCER - COMBAT UTILITIES
|
|
3
|
+
*
|
|
4
|
+
* Utilities for combat calculations.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {Position, Velocity, MissileConfig} from '../types.js';
|
|
8
|
+
import {
|
|
9
|
+
calculateMissileCastTime,
|
|
10
|
+
calculateWarmupMultiplier,
|
|
11
|
+
validateMissileConfig,
|
|
12
|
+
TICKS_PER_SECOND,
|
|
13
|
+
WIZARD_HEALTH,
|
|
14
|
+
MISSILE_BASE_CAST,
|
|
15
|
+
MISSILE_DAMAGE_SCALE,
|
|
16
|
+
MISSILE_DAMAGE_POWER,
|
|
17
|
+
MISSILE_HOMING_COEFF,
|
|
18
|
+
MISSILE_TURN_DURATION_COEFF,
|
|
19
|
+
MISSILE_SPEED_DURATION_COEFF,
|
|
20
|
+
MISSILE_SPEED_DURATION_BASELINE,
|
|
21
|
+
MISSILE_MIN_CAST_TIME,
|
|
22
|
+
MISSILE_MIN_DURATION,
|
|
23
|
+
MISSILE_MIN_DAMAGE,
|
|
24
|
+
effectiveTurnRateCost,
|
|
25
|
+
} from '../rules.js';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get cast time in ticks for a missile configuration.
|
|
29
|
+
* Applies the same clamps/validation as the engine before calculating,
|
|
30
|
+
* so the result matches the actual cast time that will be used in-game.
|
|
31
|
+
*
|
|
32
|
+
* If lastMissileConfig is provided, includes warmup multiplier.
|
|
33
|
+
* Pass undefined for first cast (full warmup) or null for base time only.
|
|
34
|
+
*/
|
|
35
|
+
export function getMissileCastTime(config: MissileConfig, lastMissileConfig?: MissileConfig | null): number
|
|
36
|
+
{
|
|
37
|
+
const validated = validateMissileConfig(config);
|
|
38
|
+
return Math.ceil(calculateMissileCastTime(validated, lastMissileConfig) * TICKS_PER_SECOND);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Calculate the position to aim at to hit a moving target.
|
|
43
|
+
* Returns the intercept point where a missile would hit the target.
|
|
44
|
+
*
|
|
45
|
+
* @param targetPos - Current target position
|
|
46
|
+
* @param targetVel - Target velocity (units per tick)
|
|
47
|
+
* @param missileSpeed - Missile speed (units per tick)
|
|
48
|
+
* @param myPos - Shooter position
|
|
49
|
+
* @returns The position to aim at
|
|
50
|
+
*/
|
|
51
|
+
export function getLeadPosition(
|
|
52
|
+
targetPos: Position,
|
|
53
|
+
targetVel: Velocity,
|
|
54
|
+
missileSpeed: number,
|
|
55
|
+
myPos: Position,
|
|
56
|
+
): Position
|
|
57
|
+
{
|
|
58
|
+
// Vector from shooter to target
|
|
59
|
+
const dx = targetPos.x - myPos.x;
|
|
60
|
+
const dy = targetPos.y - myPos.y;
|
|
61
|
+
|
|
62
|
+
// Target velocity
|
|
63
|
+
const vx = targetVel.x;
|
|
64
|
+
const vy = targetVel.y;
|
|
65
|
+
|
|
66
|
+
// Quadratic coefficients for intercept time
|
|
67
|
+
// |target + velocity * t - shooter|^2 = (missileSpeed * t)^2
|
|
68
|
+
// Expands to: (vx^2 + vy^2 - speed^2) * t^2 + 2*(vx*dx + vy*dy) * t + (dx^2 + dy^2) = 0
|
|
69
|
+
const a = vx * vx + vy * vy - missileSpeed * missileSpeed;
|
|
70
|
+
const b = 2 * (vx * dx + vy * dy);
|
|
71
|
+
const c = dx * dx + dy * dy;
|
|
72
|
+
|
|
73
|
+
let t: number | null = null;
|
|
74
|
+
|
|
75
|
+
// Handle the linear case (|velocity| === missileSpeed)
|
|
76
|
+
if (Math.abs(a) < 1e-10)
|
|
77
|
+
{
|
|
78
|
+
if (Math.abs(b) > 1e-10)
|
|
79
|
+
{
|
|
80
|
+
const linearT = -c / b;
|
|
81
|
+
if (linearT > 0)
|
|
82
|
+
{
|
|
83
|
+
t = linearT;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
else
|
|
88
|
+
{
|
|
89
|
+
// Quadratic case
|
|
90
|
+
const discriminant = b * b - 4 * a * c;
|
|
91
|
+
|
|
92
|
+
if (discriminant >= 0)
|
|
93
|
+
{
|
|
94
|
+
const sqrtD = Math.sqrt(discriminant);
|
|
95
|
+
const t1 = (-b + sqrtD) / (2 * a);
|
|
96
|
+
const t2 = (-b - sqrtD) / (2 * a);
|
|
97
|
+
|
|
98
|
+
if (t1 > 0 && t2 > 0)
|
|
99
|
+
{
|
|
100
|
+
t = Math.min(t1, t2);
|
|
101
|
+
}
|
|
102
|
+
else if (t1 > 0)
|
|
103
|
+
{
|
|
104
|
+
t = t1;
|
|
105
|
+
}
|
|
106
|
+
else if (t2 > 0)
|
|
107
|
+
{
|
|
108
|
+
t = t2;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// If no intercept solution, just aim at current position
|
|
114
|
+
if (t === null)
|
|
115
|
+
{
|
|
116
|
+
return {x: targetPos.x, y: targetPos.y};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Calculate intercept position
|
|
120
|
+
return {
|
|
121
|
+
x: targetPos.x + vx * t,
|
|
122
|
+
y: targetPos.y + vy * t,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Calculate optimal missile configuration based on target behavior.
|
|
128
|
+
*/
|
|
129
|
+
export function getAdaptiveMissileConfig(
|
|
130
|
+
targetVelocity: Position,
|
|
131
|
+
distance: number,
|
|
132
|
+
): {speed: number; turnRate: number; damage: number; duration: number}
|
|
133
|
+
{
|
|
134
|
+
const targetSpeed = Math.sqrt(targetVelocity.x ** 2 + targetVelocity.y ** 2);
|
|
135
|
+
|
|
136
|
+
// Fast moving target = more homing
|
|
137
|
+
// Slow/stationary target = faster missile, light homing for correction
|
|
138
|
+
if (targetSpeed > 0.8)
|
|
139
|
+
{
|
|
140
|
+
// Moving target - use significant homing
|
|
141
|
+
const turnRate = Math.min(3, 1 + targetSpeed * 1.5);
|
|
142
|
+
return {
|
|
143
|
+
damage: 10,
|
|
144
|
+
speed: 5,
|
|
145
|
+
turnRate,
|
|
146
|
+
duration: Math.min(250, 100 + distance * 0.5),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
else
|
|
150
|
+
{
|
|
151
|
+
// Stationary/slow target - faster missile with light homing for accuracy
|
|
152
|
+
return {
|
|
153
|
+
damage: 10,
|
|
154
|
+
speed: 6,
|
|
155
|
+
turnRate: 1, // Light homing to correct any aiming errors
|
|
156
|
+
duration: 180,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Given a cast-time budget (in ticks) and a target distance, find the best
|
|
163
|
+
* missile config that fits. Maximizes damage while ensuring the missile
|
|
164
|
+
* can reach the target and finishes casting in time.
|
|
165
|
+
*
|
|
166
|
+
* Returns null if no useful missile fits in the budget.
|
|
167
|
+
*
|
|
168
|
+
* How it works: tries several speed/turnRate templates. For each, calculates
|
|
169
|
+
* the minimum duration to reach `distance`, then solves the cast-time formula
|
|
170
|
+
* for the maximum damage that fits within `budgetTicks`.
|
|
171
|
+
*
|
|
172
|
+
* If `lastMissileConfig` is provided, accounts for warmup bonus: similar
|
|
173
|
+
* missiles cast faster, so more damage can fit in the same budget.
|
|
174
|
+
*/
|
|
175
|
+
export function fitMissileToBudget(
|
|
176
|
+
budgetTicks: number,
|
|
177
|
+
distance: number,
|
|
178
|
+
options?: {minTurnRate?: number; maxDamage?: number; lastMissileConfig?: MissileConfig},
|
|
179
|
+
): MissileConfig | null
|
|
180
|
+
{
|
|
181
|
+
const budgetSec = budgetTicks / TICKS_PER_SECOND;
|
|
182
|
+
const minTurnRate = options?.minTurnRate ?? 0;
|
|
183
|
+
const maxDamage = options?.maxDamage ?? WIZARD_HEALTH;
|
|
184
|
+
// Warmup is active when caller explicitly passes lastMissileConfig in options.
|
|
185
|
+
// Value can be undefined (first cast = full warmup) or MissileConfig (similarity-based).
|
|
186
|
+
// When key is absent (old callers), no warmup is applied.
|
|
187
|
+
const warmupActive = options !== undefined && 'lastMissileConfig' in options;
|
|
188
|
+
const lastMissile = options?.lastMissileConfig;
|
|
189
|
+
|
|
190
|
+
// Minimum useful cast time is 10 ticks (0.1s)
|
|
191
|
+
if (budgetSec < MISSILE_MIN_CAST_TIME)
|
|
192
|
+
{
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Templates: [speed, turnRate] combos to try, ordered by preference.
|
|
197
|
+
// Lower turnRate = cheaper cast. Higher speed = shorter duration needed.
|
|
198
|
+
// Negative turnRate = anti-homing (turns away from target) but cheaper cast time.
|
|
199
|
+
const templates: [number, number][] = [
|
|
200
|
+
[7, -1], // Fast anti-homing: cheapest cast, curves away from target
|
|
201
|
+
[7, -0.5],// Fast light anti-homing
|
|
202
|
+
[7, 0], // Fast straight: cheapest cast, only hits stationary
|
|
203
|
+
[5, 0], // Medium straight
|
|
204
|
+
[6, 0.5], // Fast with light tracking
|
|
205
|
+
[5, 1], // Medium with tracking
|
|
206
|
+
[4, 1], // Slow with tracking (long range)
|
|
207
|
+
[4, 1.5], // Slow with strong tracking
|
|
208
|
+
[3, 2], // Very slow with heavy tracking (for aggressive homing)
|
|
209
|
+
];
|
|
210
|
+
|
|
211
|
+
let best: MissileConfig | null = null;
|
|
212
|
+
let bestDamage = 0;
|
|
213
|
+
|
|
214
|
+
for (const [speed, turnRate] of templates)
|
|
215
|
+
{
|
|
216
|
+
if (turnRate < minTurnRate) continue;
|
|
217
|
+
// Minimum duration to reach target (with 20% margin for homing curve)
|
|
218
|
+
const margin = turnRate > 0 ? 1.2 : 1.05;
|
|
219
|
+
const minDurationTicks = Math.max(
|
|
220
|
+
MISSILE_MIN_DURATION,
|
|
221
|
+
Math.ceil(distance * margin / speed),
|
|
222
|
+
);
|
|
223
|
+
const durationSec = minDurationTicks / TICKS_PER_SECOND;
|
|
224
|
+
|
|
225
|
+
// Solve cast formula for damage:
|
|
226
|
+
// budgetSec = BASE + SCALE * d^POWER + delivery costs
|
|
227
|
+
// → d = ((budgetSec - fixedCost) / SCALE)^(1/POWER)
|
|
228
|
+
const turnCost = effectiveTurnRateCost(turnRate);
|
|
229
|
+
const fixedCost = MISSILE_BASE_CAST
|
|
230
|
+
+ MISSILE_HOMING_COEFF * turnCost
|
|
231
|
+
+ MISSILE_TURN_DURATION_COEFF * (turnCost * durationSec)
|
|
232
|
+
+ MISSILE_SPEED_DURATION_COEFF * (speed * durationSec - MISSILE_SPEED_DURATION_BASELINE);
|
|
233
|
+
|
|
234
|
+
// Estimate warmup multiplier for this template (depends on speed/turnRate/duration,
|
|
235
|
+
// not damage, so we can estimate before solving for damage)
|
|
236
|
+
let effectiveBudgetSec = budgetSec;
|
|
237
|
+
if (warmupActive && lastMissile)
|
|
238
|
+
{
|
|
239
|
+
const probe: MissileConfig = {damage: lastMissile.damage, speed, turnRate, duration: minDurationTicks};
|
|
240
|
+
const multiplier = calculateWarmupMultiplier(lastMissile, probe);
|
|
241
|
+
// multiplier < 1 = faster (more budget), > 1 = slower (less budget)
|
|
242
|
+
effectiveBudgetSec = budgetSec / multiplier;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const remaining = effectiveBudgetSec - fixedCost;
|
|
246
|
+
if (remaining <= 0)
|
|
247
|
+
{
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const maxDamageFloat = Math.pow(remaining / MISSILE_DAMAGE_SCALE, 1 / MISSILE_DAMAGE_POWER);
|
|
252
|
+
const damage = Math.min(maxDamage, Math.floor(maxDamageFloat));
|
|
253
|
+
|
|
254
|
+
if (damage < MISSILE_MIN_DAMAGE)
|
|
255
|
+
{
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Verify actual cast time with real warmup (using final damage)
|
|
260
|
+
const candidate: MissileConfig = {damage, speed, turnRate, duration: minDurationTicks};
|
|
261
|
+
const actualCastSec = (warmupActive && lastMissile)
|
|
262
|
+
? calculateMissileCastTime(candidate, lastMissile)
|
|
263
|
+
: calculateMissileCastTime(candidate);
|
|
264
|
+
const actualCastTicks = Math.max(1, Math.ceil(actualCastSec * TICKS_PER_SECOND));
|
|
265
|
+
|
|
266
|
+
if (actualCastTicks > budgetTicks)
|
|
267
|
+
{
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (damage > bestDamage)
|
|
272
|
+
{
|
|
273
|
+
bestDamage = damage;
|
|
274
|
+
best = candidate;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return best;
|
|
279
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {Position} from '../types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Calculate the distance between two points.
|
|
5
|
+
*/
|
|
6
|
+
export function distanceTo(a: Position, b: Position): number
|
|
7
|
+
{
|
|
8
|
+
const dx = b.x - a.x;
|
|
9
|
+
const dy = b.y - a.y;
|
|
10
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Check if two points are within a certain range of each other.
|
|
15
|
+
*/
|
|
16
|
+
export function inRange(a: Position, b: Position, range: number): boolean
|
|
17
|
+
{
|
|
18
|
+
const dx = b.x - a.x;
|
|
19
|
+
const dy = b.y - a.y;
|
|
20
|
+
return (dx * dx + dy * dy) <= range * range;
|
|
21
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import {Position, Velocity} from '../types.js';
|
|
2
|
+
import {normalizeAngle} from './angles.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Get the position after moving a certain distance in a direction.
|
|
6
|
+
*/
|
|
7
|
+
export function moveInDirection(position: Position, angle: number, distance: number): Position
|
|
8
|
+
{
|
|
9
|
+
const radians = angle * (Math.PI / 180);
|
|
10
|
+
return {
|
|
11
|
+
x: position.x + Math.cos(radians) * distance,
|
|
12
|
+
y: position.y + Math.sin(radians) * distance,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Predict the position after N ticks given current velocity.
|
|
18
|
+
*/
|
|
19
|
+
export function predictPosition(position: Position, velocity: Velocity, ticks: number): Position
|
|
20
|
+
{
|
|
21
|
+
return {
|
|
22
|
+
x: position.x + velocity.x * ticks,
|
|
23
|
+
y: position.y + velocity.y * ticks,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Calculate the intercept angle for a target moving at a certain velocity.
|
|
29
|
+
* Returns null if no intercept solution exists.
|
|
30
|
+
*/
|
|
31
|
+
export function interceptAngle(
|
|
32
|
+
shooterPosition: Position,
|
|
33
|
+
targetPosition: Position,
|
|
34
|
+
targetVelocity: Velocity,
|
|
35
|
+
projectileSpeed: number,
|
|
36
|
+
): number | null
|
|
37
|
+
{
|
|
38
|
+
const dx = targetPosition.x - shooterPosition.x;
|
|
39
|
+
const dy = targetPosition.y - shooterPosition.y;
|
|
40
|
+
|
|
41
|
+
const vx = targetVelocity.x;
|
|
42
|
+
const vy = targetVelocity.y;
|
|
43
|
+
|
|
44
|
+
const a = vx * vx + vy * vy - projectileSpeed * projectileSpeed;
|
|
45
|
+
const b = 2 * (vx * dx + vy * dy);
|
|
46
|
+
const c = dx * dx + dy * dy;
|
|
47
|
+
|
|
48
|
+
let t: number | null = null;
|
|
49
|
+
|
|
50
|
+
// Handle the linear case when a is approximately zero
|
|
51
|
+
// This happens when |targetVelocity| === projectileSpeed
|
|
52
|
+
if (Math.abs(a) < 1e-10)
|
|
53
|
+
{
|
|
54
|
+
// Linear equation: b*t + c = 0 => t = -c/b
|
|
55
|
+
if (Math.abs(b) < 1e-10)
|
|
56
|
+
{
|
|
57
|
+
// No solution possible
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const linearT = -c / b;
|
|
61
|
+
if (linearT > 0)
|
|
62
|
+
{
|
|
63
|
+
t = linearT;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else
|
|
67
|
+
{
|
|
68
|
+
// Quadratic case
|
|
69
|
+
const discriminant = b * b - 4 * a * c;
|
|
70
|
+
|
|
71
|
+
if (discriminant < 0)
|
|
72
|
+
{
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const sqrtD = Math.sqrt(discriminant);
|
|
77
|
+
const t1 = (-b + sqrtD) / (2 * a);
|
|
78
|
+
const t2 = (-b - sqrtD) / (2 * a);
|
|
79
|
+
|
|
80
|
+
if (t1 > 0 && t2 > 0)
|
|
81
|
+
{
|
|
82
|
+
t = Math.min(t1, t2);
|
|
83
|
+
}
|
|
84
|
+
else if (t1 > 0)
|
|
85
|
+
{
|
|
86
|
+
t = t1;
|
|
87
|
+
}
|
|
88
|
+
else if (t2 > 0)
|
|
89
|
+
{
|
|
90
|
+
t = t2;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (t === null)
|
|
95
|
+
{
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const interceptPos = {
|
|
100
|
+
x: targetPosition.x + vx * t,
|
|
101
|
+
y: targetPosition.y + vy * t,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const dx2 = interceptPos.x - shooterPosition.x;
|
|
105
|
+
const dy2 = interceptPos.y - shooterPosition.y;
|
|
106
|
+
|
|
107
|
+
return normalizeAngle(Math.atan2(dy2, dx2) * (180 / Math.PI));
|
|
108
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seeded PRNG utilities for deterministic randomness.
|
|
3
|
+
*
|
|
4
|
+
* Each entity (wizard, missile) gets its own random sequence that:
|
|
5
|
+
* - Is deterministic: same seed = same sequence
|
|
6
|
+
* - Is isolated: one entity's calls don't affect another's
|
|
7
|
+
* - Advances state: each call produces a different value
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const MAX_INT = 0x7fffffff;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Combine two seeds into one using a simple hash.
|
|
14
|
+
*/
|
|
15
|
+
export function hashCombine(a: number, b: number): number
|
|
16
|
+
{
|
|
17
|
+
// Simple hash combining two integers
|
|
18
|
+
let hash = a;
|
|
19
|
+
hash = ((hash << 5) + hash) ^ b;
|
|
20
|
+
return hash & MAX_INT;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Advance the random state using a Linear Congruential Generator.
|
|
25
|
+
* Parameters from glibc (widely tested).
|
|
26
|
+
*/
|
|
27
|
+
export function nextRandom(state: number): number
|
|
28
|
+
{
|
|
29
|
+
return (state * 1103515245 + 12345) & MAX_INT;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Create a seeded random number generator.
|
|
34
|
+
* Returns a function that produces values in [0, 1) and advances internal state.
|
|
35
|
+
*/
|
|
36
|
+
export function createRandom(seed: number): () => number
|
|
37
|
+
{
|
|
38
|
+
let state = seed & MAX_INT;
|
|
39
|
+
|
|
40
|
+
return (): number =>
|
|
41
|
+
{
|
|
42
|
+
state = nextRandom(state);
|
|
43
|
+
return state / MAX_INT;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Create a deterministic seed for an entity based on match seed, entity ID, and tick.
|
|
49
|
+
* This ensures reproducibility: same match + same entity + same tick = same random sequence.
|
|
50
|
+
*/
|
|
51
|
+
export function createEntitySeed(matchSeed: number, entityId: string, tick: number): number
|
|
52
|
+
{
|
|
53
|
+
// Hash the entity ID string into a number
|
|
54
|
+
let entityHash = 0;
|
|
55
|
+
for (let i = 0; i < entityId.length; i++)
|
|
56
|
+
{
|
|
57
|
+
entityHash = ((entityHash << 5) + entityHash) ^ entityId.charCodeAt(i);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Combine match seed, entity hash, and tick
|
|
61
|
+
let combined = hashCombine(matchSeed, entityHash & MAX_INT);
|
|
62
|
+
combined = hashCombine(combined, tick);
|
|
63
|
+
|
|
64
|
+
return combined;
|
|
65
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VIBEMANCER - SPATIAL UTILITIES
|
|
3
|
+
*
|
|
4
|
+
* Direction vectors and arena bounds utilities.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {Position} from '../types.js';
|
|
8
|
+
import {ARENA_WIDTH, ARENA_HEIGHT} from '../rules.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Normalize a vector to unit length.
|
|
12
|
+
* Returns {x: 0, y: 0} for zero-length vectors.
|
|
13
|
+
*/
|
|
14
|
+
export function normalize(vector: Position): Position
|
|
15
|
+
{
|
|
16
|
+
const len = Math.sqrt(vector.x * vector.x + vector.y * vector.y);
|
|
17
|
+
if (len === 0)
|
|
18
|
+
{
|
|
19
|
+
return {x: 0, y: 0};
|
|
20
|
+
}
|
|
21
|
+
return {x: vector.x / len, y: vector.y / len};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get normalized direction vector from one position toward another.
|
|
26
|
+
* Returns {x: 0, y: 0} if positions are identical.
|
|
27
|
+
*/
|
|
28
|
+
export function directionTo(from: Position, to: Position): Position
|
|
29
|
+
{
|
|
30
|
+
const dx = to.x - from.x;
|
|
31
|
+
const dy = to.y - from.y;
|
|
32
|
+
return normalize({x: dx, y: dy});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get normalized direction vector from one position away from another.
|
|
37
|
+
* Returns {x: 0, y: 0} if positions are identical.
|
|
38
|
+
*/
|
|
39
|
+
export function directionAway(from: Position, to: Position): Position
|
|
40
|
+
{
|
|
41
|
+
const dir = directionTo(from, to);
|
|
42
|
+
return {x: -dir.x, y: -dir.y};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Clamp a position to valid arena bounds.
|
|
47
|
+
* Note: For wizard-specific clamping with radius, use clampToArena from physics.ts
|
|
48
|
+
*/
|
|
49
|
+
export function clampPositionToArena(position: Position): Position
|
|
50
|
+
{
|
|
51
|
+
return {
|
|
52
|
+
x: Math.max(0, Math.min(ARENA_WIDTH, position.x)),
|
|
53
|
+
y: Math.max(0, Math.min(ARENA_HEIGHT, position.y)),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get the length/magnitude of a vector.
|
|
59
|
+
*/
|
|
60
|
+
export function magnitude(vector: Position): number
|
|
61
|
+
{
|
|
62
|
+
return Math.sqrt(vector.x * vector.x + vector.y * vector.y);
|
|
63
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {Position} from '../types.js';
|
|
2
|
+
import {distanceTo} from './distance.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Find the nearest entity from a list.
|
|
6
|
+
*/
|
|
7
|
+
export function findNearest<T extends {position: Position}>(from: Position, entities: T[]): T | null
|
|
8
|
+
{
|
|
9
|
+
if (entities.length === 0)
|
|
10
|
+
{
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let nearest = entities[0]!;
|
|
15
|
+
let minDistance = distanceTo(from, nearest.position);
|
|
16
|
+
|
|
17
|
+
for (let i = 1; i < entities.length; i++)
|
|
18
|
+
{
|
|
19
|
+
const entity = entities[i]!;
|
|
20
|
+
const distance = distanceTo(from, entity.position);
|
|
21
|
+
if (distance < minDistance)
|
|
22
|
+
{
|
|
23
|
+
minDistance = distance;
|
|
24
|
+
nearest = entity;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return nearest;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Find all entities within a certain range.
|
|
33
|
+
*/
|
|
34
|
+
export function findInRange<T extends {position: Position}>(from: Position, entities: T[], range: number): T[]
|
|
35
|
+
{
|
|
36
|
+
return entities.filter((entity) => distanceTo(from, entity.position) <= range);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Sort entities by distance (closest first).
|
|
41
|
+
*/
|
|
42
|
+
export function sortByDistance<T extends {position: Position}>(from: Position, entities: T[]): T[]
|
|
43
|
+
{
|
|
44
|
+
return [...entities].sort((a, b) => distanceTo(from, a.position) - distanceTo(from, b.position));
|
|
45
|
+
}
|