deadhead-space-freight-tycoon/packages/sim/test/effective-stats.test.ts

151 lines
5.1 KiB
TypeScript

import { content, SHIP_STATS } from "@deadhead/content";
import { describe, expect, it } from "vitest";
import {
effectiveStats,
routeDistance,
shipStats,
travelTimeGameHours,
validateDispatch,
type Contract,
type DispatchProposal,
type PilotState,
type ShipState,
} from "../src/index";
/** Fuel enough for any leg in these tests (decision 0.5.94), bought at 10 cr/fu. */
const PLENTY = { fuelFu: 10_000, avgPrice: 10 };
const now = Date.parse("2100-01-01T00:30:00Z");
const courier = content.shipModel("courier")!;
const freighter = content.shipModel("freighter")!;
const contract: Contract = {
id: "c1",
originId: "earth",
destinationId: "moon",
cargoTypeId: "standard",
goodId: "consumer-goods",
shipper: "Pacific Orbital Logistics",
priority: false,
deliveryWindowGameHours: null,
tier: 0,
massTonnes: 10,
payout: 2000,
requiredSkill: null,
requiredLongHaulLevel: 0,
offeredAt: Date.parse("2100-01-01T00:00:00Z"),
expiresAt: Date.parse("2100-01-01T01:00:00Z"),
firstContact: false,
};
const ship: ShipState = {
id: "s1",
modelId: "courier",
locationId: "earth",
inFlight: false,
tank: PLENTY,
};
const pilot: PilotState = {
id: "p1",
isAvatar: false,
assignedShipId: "s1",
inFlight: false,
skills: {},
restEndsAt: null,
};
const proposal: DispatchProposal = { ship, pilot, contract, tutorial: false, now };
const reason = (p: DispatchProposal) => {
const result = validateDispatch(p, content);
return result.ok ? "valid" : result.reason;
};
describe("effective stats (Step 10I.2)", () => {
it("equals the model's stats exactly with no upgrades", () => {
expect(effectiveStats(freighter, {}, content)).toEqual({
...freighter,
handlingGameHoursPerTonne: content.config.handlingGameHoursPerTonne,
});
expect(shipStats(ship, content)).toEqual(effectiveStats(courier, {}, content));
});
it("changes only the upgrade's own stat", () => {
const base = effectiveStats(freighter, {}, content);
for (const upgrade of content.upgrades) {
const upgraded = effectiveStats(freighter, { [upgrade.id]: 1 }, content);
for (const stat of SHIP_STATS) {
if (stat === upgrade.stat) expect(upgraded[stat], upgrade.id).not.toBe(base[stat]);
else expect(upgraded[stat], `${upgrade.id} → ${stat}`).toBe(base[stat]);
}
}
});
it("gives exactly +30 % speed at engines level 3", () => {
expect(effectiveStats(freighter, { engines: 3 }, content).cruiseSpeedKmPerGameHour).toBeCloseTo(
freighter.cruiseSpeedKmPerGameHour * 1.3,
6,
);
});
it("lets a cargo-bay upgrade carry a contract that was too heavy", () => {
const heavy = { ...contract, massTonnes: 22 };
expect(reason({ ...proposal, contract: heavy })).toBe("over-capacity");
expect(
reason({ ...proposal, contract: heavy, ship: { ...ship, upgrades: { "cargo-bay": 1 } } }),
).toBe("valid");
});
it("lets an engines upgrade make a Priority window that was too short", () => {
const distance = routeDistance(content.location("earth")!, content.location("moon")!);
const hours = (stats: typeof courier) =>
travelTimeGameHours(stats, distance, contract.massTonnes, content.config);
const slow = hours(effectiveStats(courier, {}, content));
const fast = hours(effectiveStats(courier, { engines: 3 }, content));
expect(fast).toBeLessThan(slow);
const priority = { ...contract, priority: true, deliveryWindowGameHours: (slow + fast) / 2 };
expect(reason({ ...proposal, contract: priority })).toBe("too-slow");
expect(
reason({ ...proposal, contract: priority, ship: { ...ship, upgrades: { engines: 3 } } }),
).toBe("valid");
});
it("uses the ship's own handling time in travel time", () => {
const geared = effectiveStats(courier, { "handling-gear": 2 }, content);
expect(geared.handlingGameHoursPerTonne).toBeCloseTo(
content.config.handlingGameHoursPerTonne * 0.7,
9,
);
expect(travelTimeGameHours(geared, 0, 20, content.config)).toBeCloseTo(
geared.overheadGameHours + 20 * geared.handlingGameHoursPerTonne,
9,
);
});
});
describe("per-model handling (CR-012)", () => {
it("loads the Lighter faster than other ships, and upgrades scale its own base", () => {
const lighter = content.shipModel("lighter")!;
expect(effectiveStats(lighter, {}, content).handlingGameHoursPerTonne).toBe(
lighter.handlingGameHoursPerTonne,
);
expect(
effectiveStats(lighter, { "handling-gear": 1 }, content).handlingGameHoursPerTonne,
).toBeCloseTo(lighter.handlingGameHoursPerTonne! * 0.85, 9);
expect(travelTimeGameHours(lighter, 0, 50, content.config)).toBeCloseTo(
lighter.overheadGameHours + 50 * lighter.handlingGameHoursPerTonne!,
9,
);
});
});
describe("Hull Plating (Step 26.1, decision 0.5.149)", () => {
it("adds 1 hull rating a level, flat", () => {
const courier = content.shipModel("courier")!;
expect(effectiveStats(courier, { "hull-plating": 3 }, content).hullRating).toBe(
courier.hullRating + 3,
);
expect(effectiveStats(courier, { "hull-plating": 1 }, content).hullRating).toBe(
courier.hullRating + 1,
);
});
});