The impact coefficients in applyWear were written against an imagined scale. Measured on the running game, a head-on into a building at 85 km/h comes back as a single contact event of ~2.5e6 N — 25 units of "impact" against the old 1e5 reference. Through the old coefficients that was 123% of the chassis and 37% of its ceiling, gone, in one hit, in a game whose missions pay about 0.3 parts each. One crash ended the campaign, and the campaign had no way to end. Anchor the reference to the measured figure instead, and cap what any single step can take: a crash is a pile of contacts across several steps plus whatever the car scrapes on the way to a standstill, and that sum has no natural bound. The same 14-second flat-out run into a building now costs chassis 100 -> 65% and its ceiling 100 -> 89%. Expensive, survivable, and still the worst thing that can happen to the car. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
157 lines
5.4 KiB
TypeScript
157 lines
5.4 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { generateWorld } from './world';
|
|
import { applyWear, deriveHandling, freshCondition, repair, SUBSYSTEMS, type CarCondition } from './car';
|
|
|
|
describe('world generation', () => {
|
|
it('is reproducible from a seed', () => {
|
|
expect(generateWorld(42)).toEqual(generateWorld(42));
|
|
});
|
|
|
|
it('differs between seeds', () => {
|
|
expect(generateWorld(42)).not.toEqual(generateWorld(43));
|
|
});
|
|
|
|
it('keeps the spawn point clear', () => {
|
|
const world = generateWorld(7);
|
|
for (const o of world.obstacles) {
|
|
expect(Math.hypot(o.x - world.spawn.x, o.z - world.spawn.z)).toBeGreaterThanOrEqual(12);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('car condition', () => {
|
|
it('never recovers on its own', () => {
|
|
let c = freshCondition();
|
|
for (let i = 0; i < 600; i++) {
|
|
const next = applyWear(c, { dt: 1 / 60, distance: 0.4, throttle: 1, impactForce: 0 });
|
|
expect(next.level.engine).toBeLessThanOrEqual(c.level.engine);
|
|
expect(next.level.tires).toBeLessThanOrEqual(c.level.tires);
|
|
c = next;
|
|
}
|
|
});
|
|
|
|
it('bottoms out rather than going negative', () => {
|
|
let c = freshCondition();
|
|
for (let i = 0; i < 200; i++) {
|
|
c = applyWear(c, { dt: 1 / 60, distance: 5, throttle: 1, impactForce: 5e5 });
|
|
}
|
|
expect(c.level.chassis).toBe(0);
|
|
expect(c.level.tires).toBe(0);
|
|
});
|
|
|
|
/**
|
|
* The force magnitude Rapier actually reports for a head-on into a building
|
|
* at 85 km/h, read off the running game. Every number below is anchored to it
|
|
* rather than to a guess about what a collision "should" be worth.
|
|
*/
|
|
const HARD_CRASH = 2.47e6;
|
|
|
|
it('leaves a car driveable after one hard crash', () => {
|
|
const c = applyWear(freshCondition(), {
|
|
dt: 1 / 60,
|
|
distance: 0.4,
|
|
throttle: 0,
|
|
impactForce: HARD_CRASH,
|
|
});
|
|
// Expensive — the worst single thing that can happen to the car — but the
|
|
// campaign continues. The first pass wrote off the chassis in this one step.
|
|
expect(c.level.chassis).toBeGreaterThan(0.7);
|
|
expect(c.level.chassis).toBeLessThan(0.95);
|
|
expect(c.ceiling.chassis).toBeGreaterThan(0.9);
|
|
});
|
|
|
|
it('caps what a single step can take, however violent the contact', () => {
|
|
// A pileup reports contact forces without any natural bound; without a cap
|
|
// one frame of a bad landing is worth more than the crash that caused it.
|
|
const c = applyWear(freshCondition(), {
|
|
dt: 1 / 60,
|
|
distance: 0.4,
|
|
throttle: 1,
|
|
impactForce: 1e9,
|
|
});
|
|
for (const part of SUBSYSTEMS) {
|
|
expect(c.level[part]).toBeGreaterThanOrEqual(0.74);
|
|
}
|
|
});
|
|
|
|
it('takes a sustained battering to write the car off', () => {
|
|
let c = freshCondition();
|
|
let crashes = 0;
|
|
while (c.level.chassis > 0 && crashes < 100) {
|
|
c = applyWear(c, { dt: 1 / 60, distance: 0.4, throttle: 0, impactForce: HARD_CRASH });
|
|
crashes++;
|
|
}
|
|
// Enough hard crashes to be a story about the driver, not one bad corner.
|
|
expect(crashes).toBeGreaterThan(5);
|
|
});
|
|
|
|
it('makes a worn car measurably worse to drive', () => {
|
|
const fresh = deriveHandling(freshCondition());
|
|
const worn = deriveHandling({
|
|
level: { engine: 0.3, tires: 0.3, chassis: 0.3 },
|
|
ceiling: { engine: 1, tires: 1, chassis: 1 },
|
|
});
|
|
expect(worn.engineForce).toBeLessThan(fresh.engineForce);
|
|
expect(worn.frictionSlip).toBeLessThan(fresh.frictionSlip);
|
|
expect(worn.steeringPull).toBeGreaterThan(fresh.steeringPull);
|
|
});
|
|
});
|
|
|
|
describe('repair', () => {
|
|
const damaged = () => {
|
|
let c = freshCondition();
|
|
for (let i = 0; i < 40; i++) {
|
|
c = applyWear(c, { dt: 1 / 60, distance: 3, throttle: 1, impactForce: 2e5 });
|
|
}
|
|
return c;
|
|
};
|
|
|
|
it('lowers the ceiling permanently, so a car never comes back whole', () => {
|
|
const c = damaged();
|
|
expect(c.ceiling.chassis).toBeLessThan(1);
|
|
// No amount of parts can undo it.
|
|
const restored = repair(c, 99).condition;
|
|
expect(restored.level.chassis).toBeLessThan(1);
|
|
expect(restored.level.chassis).toBeCloseTo(c.ceiling.chassis, 6);
|
|
});
|
|
|
|
it('never lifts a subsystem above its ceiling', () => {
|
|
const c = damaged();
|
|
const restored = repair(c, 99).condition;
|
|
for (const part of SUBSYSTEMS) {
|
|
expect(restored.level[part]).toBeLessThanOrEqual(restored.ceiling[part] + 1e-9);
|
|
}
|
|
});
|
|
|
|
it('hands back parts it could not use', () => {
|
|
const { unused } = repair(freshCondition(), 5);
|
|
expect(unused).toBeCloseTo(5, 6);
|
|
});
|
|
|
|
it('spends a scarce part on the worst subsystem', () => {
|
|
const c: CarCondition = {
|
|
level: { engine: 0.9, tires: 0.2, chassis: 0.8 },
|
|
ceiling: { engine: 1, tires: 1, chassis: 1 },
|
|
};
|
|
const restored = repair(c, 0.1).condition;
|
|
expect(restored.level.tires).toBeCloseTo(0.3, 6);
|
|
expect(restored.level.engine).toBe(0.9);
|
|
});
|
|
|
|
it('ratchets down over repeated damage-and-repair cycles', () => {
|
|
let c = freshCondition();
|
|
let previousCeiling = 1;
|
|
for (let cycle = 0; cycle < 5; cycle++) {
|
|
for (let i = 0; i < 30; i++) {
|
|
c = applyWear(c, { dt: 1 / 60, distance: 3, throttle: 1, impactForce: 2e4 });
|
|
}
|
|
c = repair(c, 99).condition;
|
|
// Fully repaired every cycle, and still worse off every cycle.
|
|
expect(c.level.chassis).toBeCloseTo(c.ceiling.chassis, 6);
|
|
expect(c.ceiling.chassis).toBeLessThan(previousCeiling);
|
|
previousCeiling = c.ceiling.chassis;
|
|
}
|
|
expect(previousCeiling).toBeGreaterThan(0);
|
|
});
|
|
});
|