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:
parent
1a7a4776c1
commit
764ea12a3f
@ -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
|
* 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.
|
* 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
|
* At 1.6e7 the same crash reads as 0.154 impact. What that then costs is set by
|
||||||
* permanent 4.7% off its ceiling. Expensive, survivable, and still the most
|
* the coefficients below, and they are deliberately brutal: a proper head-on
|
||||||
* costly thing you can do to the car.
|
* 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;
|
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
|
* 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.
|
* 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
|
* Set above even a bad head-on so it stays a backstop rather than the number
|
||||||
* number that actually decides what a collision costs.
|
* 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.
|
* Returns a new condition.
|
||||||
@ -164,9 +168,11 @@ const MAX_DAMAGE_PER_STEP = 0.25;
|
|||||||
export function applyWear(c: CarCondition, w: WearInput): CarCondition {
|
export function applyWear(c: CarCondition, w: WearInput): CarCondition {
|
||||||
const impact = w.impactForce / IMPACT_REFERENCE;
|
const impact = w.impactForce / IMPACT_REFERENCE;
|
||||||
const damage: Subsystems = {
|
const damage: Subsystems = {
|
||||||
engine: w.throttle * w.dt * 1.5e-4 + impact * 0.32,
|
// Ordered by what a collision actually ruins: the shell takes the worst of
|
||||||
tires: w.distance * 2e-5 + impact * 0.5,
|
// it, the tyres and suspension a good share, the engine least of all.
|
||||||
chassis: impact * 1.0,
|
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;
|
const level = {} as Subsystems;
|
||||||
|
|||||||
@ -46,43 +46,61 @@ describe('car condition', () => {
|
|||||||
*/
|
*/
|
||||||
const HARD_CRASH = 2.47e6;
|
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(), {
|
const c = applyWear(freshCondition(), {
|
||||||
dt: 1 / 60,
|
dt: 1 / 60,
|
||||||
distance: 0.4,
|
distance: 0.4,
|
||||||
throttle: 0,
|
throttle: 0,
|
||||||
impactForce: HARD_CRASH,
|
impactForce: HARD_CRASH,
|
||||||
});
|
});
|
||||||
// Expensive — the worst single thing that can happen to the car — but the
|
// Hitting a building at ninety is meant to be close to the end of it. The
|
||||||
// campaign continues. The first pass wrote off the chassis in this one step.
|
// shell takes the worst, the tyres a good share, the engine least.
|
||||||
expect(c.level.chassis).toBeGreaterThan(0.7);
|
expect(c.level.chassis).toBeLessThan(0.3);
|
||||||
expect(c.level.chassis).toBeLessThan(0.95);
|
expect(c.level.chassis).toBeGreaterThan(0.1);
|
||||||
expect(c.ceiling.chassis).toBeGreaterThan(0.9);
|
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', () => {
|
it('caps what a single step can take, however violent the contact', () => {
|
||||||
// A pileup reports contact forces without any natural bound; without a cap
|
// A car wedged between two colliders reports forces in the billions. The
|
||||||
// one frame of a bad landing is worth more than the crash that caused it.
|
// cap is for that, not for the crash the player just had — so it has to sit
|
||||||
const c = applyWear(freshCondition(), {
|
// above even a bad head-on or it becomes the number deciding every impact.
|
||||||
|
const pathological = applyWear(freshCondition(), {
|
||||||
dt: 1 / 60,
|
dt: 1 / 60,
|
||||||
distance: 0.4,
|
distance: 0.4,
|
||||||
throttle: 1,
|
throttle: 1,
|
||||||
impactForce: 1e9,
|
impactForce: 1e9,
|
||||||
});
|
});
|
||||||
for (const part of SUBSYSTEMS) {
|
for (const part of SUBSYSTEMS) {
|
||||||
expect(c.level[part]).toBeGreaterThanOrEqual(0.74);
|
expect(pathological.level[part]).toBeGreaterThanOrEqual(0.14);
|
||||||
}
|
}
|
||||||
});
|
// And it must not be what decides an ordinary head-on.
|
||||||
|
const crash = applyWear(freshCondition(), {
|
||||||
it('takes a sustained battering to write the car off', () => {
|
dt: 1 / 60,
|
||||||
let c = freshCondition();
|
distance: 0.4,
|
||||||
let crashes = 0;
|
throttle: 0,
|
||||||
while (c.level.chassis > 0 && crashes < 100) {
|
impactForce: HARD_CRASH,
|
||||||
c = applyWear(c, { dt: 1 / 60, distance: 0.4, throttle: 0, impactForce: HARD_CRASH });
|
});
|
||||||
crashes++;
|
expect(crash.level.chassis).toBeGreaterThan(pathological.level.chassis);
|
||||||
}
|
|
||||||
// Enough hard crashes to be a story about the driver, not one bad corner.
|
|
||||||
expect(crashes).toBeGreaterThan(5);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('lets a careful driver come out ahead of the parts they are paid', () => {
|
it('lets a careful driver come out ahead of the parts they are paid', () => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user