135 lines
4.6 KiB
TypeScript
135 lines
4.6 KiB
TypeScript
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,
|
|
);
|
|
});
|
|
});
|