/** * Car condition and what it does to handling. Pure — no engine imports. * * Design pillar this serves: "Decline, not reset." Condition only ever falls. * Phase 0's whole job is to make that decline *felt* through the wheel, so the * handling numbers the physics layer uses are derived here rather than constants. */ export interface CarCondition { /** 1 = factory fresh, 0 = ruined. */ engine: number; tires: number; chassis: number; } export interface Handling { /** Newtons of drive force available per driven wheel at full throttle. */ engineForce: number; /** Braking impulse per wheel. */ brakeForce: number; /** Max steering angle, radians. */ maxSteer: number; /** Tyre grip. Lower = slides. */ frictionSlip: number; sideFrictionStiffness: number; /** Constant tug on the wheel from a bent chassis, radians. Signed. */ steeringPull: number; } export const freshCondition = (): CarCondition => ({ engine: 1, tires: 1, chassis: 1 }); const lerp = (a: number, b: number, t: number) => a + (b - a) * t; const clamp01 = (v: number) => Math.min(1, Math.max(0, v)); export function deriveHandling(c: CarCondition): Handling { return { // A tired engine simply cannot push as hard. engineForce: lerp(900, 2600, c.engine), // Worn pads take longer to haul the car down. brakeForce: lerp(4, 14, c.tires), maxSteer: lerp(0.35, 0.55, c.chassis), // Bald tyres are the most legible failure: the back end starts to leave. frictionSlip: lerp(1.6, 5, c.tires), sideFrictionStiffness: lerp(0.5, 1, c.tires), // A bent chassis pulls to one side. Sign is stable for a given car. steeringPull: (1 - c.chassis) * 0.06, }; } export interface WearInput { /** Seconds of simulated time. */ dt: number; /** Metres travelled this step. */ distance: number; /** Throttle actually applied, 0..1. */ throttle: number; /** Sum of impact force magnitudes registered this step, newtons. */ impactForce: number; } /** * Returns a new condition. Numbers here are deliberately aggressive so a * 15-minute session shows visible decline — tune down once the feel is right. */ export function applyWear(c: CarCondition, w: WearInput): CarCondition { const impact = w.impactForce / 1e5; return { engine: clamp01(c.engine - w.throttle * w.dt * 6e-4 - impact * 0.01), tires: clamp01(c.tires - w.distance * 8e-5 - impact * 0.02), chassis: clamp01(c.chassis - impact * 0.05), }; }