"0.34 parts" read as a fraction of something unnamed. It is a payment, so it now looks like one: ¤34. The symbol is Unicode's generic currency sign - it looks like money and is nobody's actual money, which is right for a place whose banks are somebody else's problem. The scale moves with it. FUNDS_PER_CONDITION fixes the exchange rate in one place, so every balance number stays in the unit it is actually about and how large the currency reads stays a presentational decision. A supply run pays ¤34, an overhaul runs ¤200 to undo a crash's permanent cost, and a full rebuild after a bad one is about a dozen missions. The debug panel gets ±¤ next to the existing condition levers, so the workshop and the board can be exercised without first driving eight missions to afford looking at them. Save format bumped: quests.parts is now quests.funds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
109 lines
3.9 KiB
TypeScript
109 lines
3.9 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { applyWear, freshCondition, repair, SUBSYSTEMS, type CarCondition } from './car';
|
|
import { isWrittenOff, KIA_LAND_LOSS, replacementCar } from './kia';
|
|
import { createFront, applyMissionImpact, depthAt } from './regions';
|
|
import { accept, createQuests, failQuest, type Offer } from './quests';
|
|
|
|
const at = (chassis: number): CarCondition => ({
|
|
level: { engine: 0.5, tires: 0.5, chassis },
|
|
ceiling: { engine: 0.8, tires: 0.8, chassis: 0.8 },
|
|
});
|
|
|
|
describe('being written off', () => {
|
|
it('is the chassis that kills you, not the engine or the tyres', () => {
|
|
expect(isWrittenOff(at(0))).toBe(true);
|
|
expect(isWrittenOff(at(0.01))).toBe(false);
|
|
expect(
|
|
isWrittenOff({
|
|
level: { engine: 0, tires: 0, chassis: 0.4 },
|
|
ceiling: { engine: 1, tires: 1, chassis: 1 },
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it('is reachable — a car can actually be driven into the ground', () => {
|
|
let c = freshCondition();
|
|
for (let i = 0; i < 400 && !isWrittenOff(c); i++) {
|
|
c = applyWear(c, { dt: 1 / 60, distance: 3, throttle: 1, impactForce: 2.5e6 });
|
|
}
|
|
expect(isWrittenOff(c)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('the replacement car', () => {
|
|
it('is handed over running, or it would kill you again immediately', () => {
|
|
const next = replacementCar(at(0));
|
|
expect(isWrittenOff(next)).toBe(false);
|
|
for (const part of SUBSYSTEMS) {
|
|
expect(next.level[part]).toBeCloseTo(next.ceiling[part], 9);
|
|
}
|
|
});
|
|
|
|
it('is always worse than the one you wrote off', () => {
|
|
const before = at(0);
|
|
const next = replacementCar(before);
|
|
for (const part of SUBSYSTEMS) {
|
|
expect(next.ceiling[part]).toBeLessThan(before.ceiling[part]);
|
|
}
|
|
});
|
|
|
|
it('ratchets down over a long campaign without ever becoming undriveable', () => {
|
|
let c = freshCondition();
|
|
let previous = 1;
|
|
for (let death = 0; death < 40; death++) {
|
|
c = replacementCar({ ...c, level: { ...c.level, chassis: 0 } });
|
|
expect(c.ceiling.chassis).toBeLessThanOrEqual(previous);
|
|
previous = c.ceiling.chassis;
|
|
// Whatever else is true, you can always get back on the road.
|
|
expect(isWrittenOff(c)).toBe(false);
|
|
expect(repair(c, 99).condition.level.chassis).toBeGreaterThan(0);
|
|
}
|
|
// Twenty-odd deaths in and the car is a genuinely miserable thing to drive.
|
|
expect(previous).toBeLessThan(0.35);
|
|
});
|
|
});
|
|
|
|
describe('what a death costs', () => {
|
|
it('gives ground back to the enemy', () => {
|
|
const front = createFront(11, 400, { x: 0, z: 0 });
|
|
const before = front.boundaries.liberated;
|
|
applyMissionImpact(front, -KIA_LAND_LOSS);
|
|
expect(front.boundaries.liberated).toBeLessThan(before);
|
|
});
|
|
|
|
it('loses the job in hand without counting it as a run', () => {
|
|
const quests = createQuests();
|
|
const offer = {
|
|
id: 1,
|
|
type: 'supply',
|
|
targetNode: 4,
|
|
novel: true,
|
|
route: { nodes: [0, 4], segments: [0], length: 900 },
|
|
intel: { worst: 'clear', unknownCount: 1, stalest: null },
|
|
reward: 0.3,
|
|
impact: 15,
|
|
} as unknown as Offer;
|
|
accept(quests, offer, { nodeId: 0, x: 0, z: 0, hutX: 0, hutZ: 0, name: 'Anvil' });
|
|
|
|
const lost = failQuest(quests);
|
|
expect(lost?.type).toBe('supply');
|
|
expect(quests.active).toBeNull();
|
|
expect(quests.completed).toBe(0);
|
|
expect(quests.failed).toBe(1);
|
|
expect(quests.funds).toBe(0);
|
|
});
|
|
|
|
it('is a setback, not a campaign reversal', () => {
|
|
// Sanity on the scale: a death should cost less ground than a couple of
|
|
// decent missions win, or dying once undoes an evening's work.
|
|
const front = createFront(3, 400, { x: 0, z: 0 });
|
|
const start = depthAt(front, 0, 0);
|
|
void start;
|
|
applyMissionImpact(front, 40);
|
|
const won = front.boundaries.liberated;
|
|
applyMissionImpact(front, -KIA_LAND_LOSS);
|
|
expect(front.boundaries.liberated).toBeLessThan(won);
|
|
expect(front.boundaries.liberated).toBeGreaterThan(won - 40);
|
|
});
|
|
});
|