diff --git a/src/sim/car.ts b/src/sim/car.ts index 3d5ef2b..aae6862 100644 --- a/src/sim/car.ts +++ b/src/sim/car.ts @@ -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; diff --git a/src/sim/sim.test.ts b/src/sim/sim.test.ts index 0f05371..555fe31 100644 --- a/src/sim/sim.test.ts +++ b/src/sim/sim.test.ts @@ -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', () => {