import { content, SHIP_STATS } from "@deadhead/content"; import { describe, expect, it } from "vitest"; import { effectiveStats, realToGame, routeDistance, shipStats, travelTimeGameHours, validateDispatch, type Contract, type DispatchProposal, type PilotState, type ShipState, } from "../src/index"; 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 }; const pilot: PilotState = { id: "p1", isAvatar: false, assignedShipId: "s1", inFlight: false, skills: {}, restEndsAt: null, }; const proposal: DispatchProposal = { ship, pilot, contract, tutorial: false, credits: 10_000, 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")!, realToGame(now, content.config), ); 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, ); }); });