Make a serious crash nearly the end of the car

The first calibration left an 85 km/h head-on at 65% chassis and
perfectly driveable, which made hitting a building at ninety an
inconvenience rather than a disaster. The reference crash now lands the
chassis at 23%, the tyres at 61% and the engine at 75% - ordered by
what a collision actually ruins.

That makes one crash survivable and two in a row the end of the car,
which is the shape the risk wants: a mistake you drive home from, and a
second mistake you do not.

The per-step cap moves up with it. It exists for the pathological case
- a car wedged between two colliders reporting forces in the billions -
and once it sits below a bad head-on it stops being a backstop and
becomes the number deciding every impact.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
dejvino 2026-08-09 07:48:37 +02:00
parent 1a7a4776c1
commit 764ea12a3f
2 changed files with 54 additions and 30 deletions

View File

@ -125,9 +125,11 @@ const PERMANENT_SHARE = 0.3;
* and 37% of its ceiling in one hit, on a car whose whole economy pays about
* 0.3 parts a mission. One crash ended the campaign.
*
* At 1.6e7 the same crash reads as 0.156 impact: about 15% of the chassis and a
* permanent 4.7% off its ceiling. Expensive, survivable, and still the most
* costly thing you can do to the car.
* At 1.6e7 the same crash reads as 0.154 impact. What that then costs is set by
* the coefficients below, and they are deliberately brutal: a proper head-on
* takes about four fifths of the chassis. Hitting a building at ninety should
* very nearly be the end of the car, and the first calibration here which
* left it at 65% and driveable made a serious crash into an inconvenience.
*/
const IMPACT_REFERENCE = 1.6e7;
@ -139,10 +141,12 @@ const IMPACT_REFERENCE = 1.6e7;
* has no natural bound. Without a cap the tail of a bad landing is worth more
* than the landing. This is what stops a single frame from writing off a part.
*
* Set well above the ordinary crash so it stays a backstop rather than the
* number that actually decides what a collision costs.
* Set above even a bad head-on so it stays a backstop rather than the number
* that actually decides what a collision costs. It exists for the pathological
* case a car wedged between two colliders reporting forces in the billions
* not for the crash the player just had.
*/
const MAX_DAMAGE_PER_STEP = 0.25;
const MAX_DAMAGE_PER_STEP = 0.85;
/**
* Returns a new condition.
@ -164,9 +168,11 @@ const MAX_DAMAGE_PER_STEP = 0.25;
export function applyWear(c: CarCondition, w: WearInput): CarCondition {
const impact = w.impactForce / IMPACT_REFERENCE;
const damage: Subsystems = {
engine: w.throttle * w.dt * 1.5e-4 + impact * 0.32,
tires: w.distance * 2e-5 + impact * 0.5,
chassis: impact * 1.0,
// Ordered by what a collision actually ruins: the shell takes the worst of
// it, the tyres and suspension a good share, the engine least of all.
engine: w.throttle * w.dt * 1.5e-4 + impact * 1.6,
tires: w.distance * 2e-5 + impact * 2.5,
chassis: impact * 5.0,
};
const level = {} as Subsystems;

View File

@ -46,43 +46,61 @@ describe('car condition', () => {
*/
const HARD_CRASH = 2.47e6;
it('leaves a car driveable after one hard crash', () => {
it('very nearly ends the car in one head-on', () => {
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);
// Hitting a building at ninety is meant to be close to the end of it. The
// shell takes the worst, the tyres a good share, the engine least.
expect(c.level.chassis).toBeLessThan(0.3);
expect(c.level.chassis).toBeGreaterThan(0.1);
expect(c.level.tires).toBeLessThan(c.level.engine);
expect(c.level.engine).toBeLessThan(0.8);
});
it('is survivable exactly once, which is what makes it worth avoiding', () => {
// One is a disaster you drive home from. Two in a row is the end of the
// car, and that is the whole shape of the risk.
const one = applyWear(freshCondition(), {
dt: 1 / 60,
distance: 0.4,
throttle: 0,
impactForce: HARD_CRASH,
});
expect(one.level.chassis).toBeGreaterThan(0);
const two = applyWear(one, {
dt: 1 / 60,
distance: 0.4,
throttle: 0,
impactForce: HARD_CRASH,
});
expect(two.level.chassis).toBe(0);
});
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(), {
// A car wedged between two colliders reports forces in the billions. The
// cap is for that, not for the crash the player just had — so it has to sit
// above even a bad head-on or it becomes the number deciding every impact.
const pathological = applyWear(freshCondition(), {
dt: 1 / 60,
distance: 0.4,
throttle: 1,
impactForce: 1e9,
});
for (const part of SUBSYSTEMS) {
expect(c.level[part]).toBeGreaterThanOrEqual(0.74);
expect(pathological.level[part]).toBeGreaterThanOrEqual(0.14);
}
});
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);
// And it must not be what decides an ordinary head-on.
const crash = applyWear(freshCondition(), {
dt: 1 / 60,
distance: 0.4,
throttle: 0,
impactForce: HARD_CRASH,
});
expect(crash.level.chassis).toBeGreaterThan(pathological.level.chassis);
});
it('lets a careful driver come out ahead of the parts they are paid', () => {