import { DISPATCH_FAILURES, REPOSITION_FAILURES, type Contract as SimContract, type DispatchRecord, } from "@deadhead/sim"; import { describe, expect, expectTypeOf, it } from "vitest"; import type { z } from "zod"; import { acknowledgeSummariesRequestSchema, acknowledgeSummariesResponseSchema, renameCompanyRequestSchema, startUpgradeRequestSchema, assignPilotRequestSchema, baySchema, claimGoalRequestSchema, companyResponseSchema, goalStateSchema, repayLoanRequestSchema, bayResponseSchema, buyShipRequestSchema, companySchema, contractBoardRequestSchema, contractBoardResponseSchema, contractSchema, createDispatchRequestSchema, createGuestResponseSchema, dispatchResponseSchema, dispatchSchema, emptyRequestSchema, ERROR_CODES, errorResponseSchema, hirePilotRequestSchema, leaseBayRequestSchema, locationStateSchema, pilotResponseSchema, pilotSchema, restPilotRequestSchema, playerStateSchema, resolutionSummarySchema, serverTimeSchema, shipResponseSchema, shipSchema, spendSkillPointRequestSchema, tutorialStateSchema, unassignPilotRequestSchema, type Contract, type Dispatch, } from "../src/index"; const company = { id: "co1", name: "Deadhead Freight", credits: 5000, loanBalance: 3500 }; const ship = { id: "s1", modelId: "courier", name: "Dusty Mule", status: "idle", locationId: "earth", dispatchId: null, pilotId: "p1", lastArrivalAt: 4_102_444_800_000, upgrades: { "cargo-bay": 1 }, stats: { cruiseSpeedKmPerGameHour: 3_000_000, cargoCapacityTonnes: 23, tankCapacityFu: 40, burnFuPerMillionKm: 20, overheadGameHours: 2, handlingGameHoursPerTonne: 0.02, }, inShopUntil: null, shop: { upgradeId: "cargo-bay", level: 1, until: 4_102_444_000_000 }, fuelFu: 32.3, fuelAvgPrice: 15.2, refuelPolicy: "auto", }; const pilot = { id: "p1", name: "Avery Kade", isAvatar: true, isVeteran: false, xp: 120, level: 1, skills: { "long-haul": 1 }, skillPointsAvailable: 1, assignedShipId: "s1", status: "idle", locationId: "earth", dutyHours: 7.5, dutyLimit: 14, resting: false, restEndsAt: null, restingInShopUntil: null, mode: "manual", orders: { destinationIds: null, cargoClassIds: ["standard"], takePriority: true, minProfit: 0, homeLocationId: "earth", }, }; const bay = { id: "b1", locationId: "earth" }; const location = { id: "mars", discovered: false }; const contract = { id: "earth:1:0", originId: "earth", destinationId: "moon", cargoTypeId: "standard", goodId: "consumer-goods", shipper: "Pacific Orbital Logistics", priority: false, deliveryWindowGameHours: null, tier: 0, massTonnes: 12, payout: 480, requiredSkill: null, requiredLongHaulLevel: 0, offeredAt: 3_600_000, expiresAt: 7_200_000, firstContact: false, }; const dispatch = { id: "d1", shipId: "s1", pilotId: "p1", contract, originId: "earth", destinationId: "moon", tutorial: false, departureAt: 4_000_000, arrivalAt: 4_130_000, distanceMillionKm: 0.39, fuelBurnedFu: 7.8, fuelCost: 234, expectedPayout: 480, seed: 123456, status: "in-flight", resolvedAt: null, }; const summary = { dispatchId: "d0", shipId: "s1", pilotId: "p1", originId: "moon", destinationId: "earth", tutorial: true, credits: 240, deadhead: false, independent: false, fuelCost: 0, topUp: null, goodId: "consumer-goods", massTonnes: 12, shipper: "Pacific Orbital Logistics", loanRepayment: 0, pilotPay: 0, pilotXp: 10, discoveredLocationId: null, scriptedId: "unmarked-crates", restEndsAt: null, resolvedAt: 3_900_000, }; const tutorial = { loanerLocationId: "earth", runInFlight: false, runsCompleted: 2, dealAvailable: false, }; const goal = { id: "first-hire", status: "ready" }; const playerState = { playerId: "player-1", company, ships: [ship], pilots: [pilot], bays: [bay], locations: [location], tutorial, activeDispatches: [dispatch], summaries: [summary], goals: [goal], dispatchOffice: { owned: true, spendingFloor: 500 }, fleetSleep: { afterHours: 24, sleepsAt: 90_000_000, asleep: false }, welcomeSeen: true, }; const cases: [string, z.ZodType, unknown, unknown][] = [ [ "server time", serverTimeSchema, { realTime: 1, gameTime: 60 }, { realTime: "now", gameTime: 60 }, ], ["company", companySchema, company, { ...company, credits: 1.5 }], ["ship", shipSchema, ship, { ...ship, status: "lost" }], [ "ship in the shop", shipSchema, { ...ship, inShopUntil: 4_102_445_000_000 }, { ...ship, upgrades: { engines: 0 } }, ], [ "ship stats", shipSchema, { ...ship, shop: null }, { ...ship, stats: { ...ship.stats, cargoCapacityTonnes: 0 } }, ], ["rename company request", renameCompanyRequestSchema, { name: "Kell & Co." }, { name: "Hi" }], [ "start upgrade request", startUpgradeRequestSchema, { shipId: "s1", upgradeId: "engines" }, { shipId: "s1" }, ], ["pilot", pilotSchema, pilot, { ...pilot, xp: -1 }], [ "resting pilot", pilotSchema, { ...pilot, dutyHours: 0, resting: true, restEndsAt: 4_000_000 }, { ...pilot, dutyHours: -1 }, ], ["pilot duty limit", pilotSchema, { ...pilot, dutyLimit: 12 }, { ...pilot, dutyLimit: 0 }], ["rest pilot request", restPilotRequestSchema, { pilotId: "p1" }, { pilotId: "" }], ["bay", baySchema, bay, { id: "b1" }], ["location", locationStateSchema, location, { id: "mars", discovered: "no" }], ["contract", contractSchema, contract, { ...contract, tier: -1 }], [ "priority contract (CR-007)", contractSchema, { ...contract, priority: true, deliveryWindowGameHours: 4.5 }, { ...contract, priority: true, deliveryWindowGameHours: -1 }, ], ["contract without a good", contractSchema, contract, { ...contract, goodId: "" }], [ "delivery summary goods (CR-007)", resolutionSummarySchema, { ...summary, goodId: null, massTonnes: null, shipper: null }, { ...summary, massTonnes: 0 }, ], ["dispatch", dispatchSchema, dispatch, { ...dispatch, status: "lost" }], ["resolution summary", resolutionSummarySchema, summary, { ...summary, pilotXp: -5 }], [ "summary with a pilot share", resolutionSummarySchema, { ...summary, pilotPay: 63 }, { ...summary, pilotPay: -1 }, ], [ "summary with rest started", resolutionSummarySchema, { ...summary, restEndsAt: 7_500_000 }, { ...summary, restEndsAt: -1 }, ], ["tutorial", tutorialStateSchema, tutorial, { ...tutorial, loanerLocationId: "mars" }], ["goal state", goalStateSchema, goal, { ...goal, status: "done" }], ["repay loan request", repayLoanRequestSchema, { amount: 500 }, { amount: 0 }], ["claim goal request", claimGoalRequestSchema, { goalId: "first-hire" }, {}], [ "company response", companyResponseSchema, { company }, { company: { ...company, loanBalance: -1 } }, ], ["player state", playerStateSchema, playerState, { ...playerState, ships: null }], [ "error", errorResponseSchema, { error: { code: "out-of-range", message: "Destination is out of range." } }, { error: { code: "made-up", message: "?" } }, ], ["empty request", emptyRequestSchema, {}, { extra: 1 }], [ "create guest response", createGuestResponseSchema, { playerId: "pl1", state: playerState }, { playerId: "pl1" }, ], ["contract board request", contractBoardRequestSchema, { locationId: "earth" }, {}], [ "contract board response", contractBoardResponseSchema, { contracts: [contract], periodEndsAt: 7_200_000 }, { contracts: [contract] }, ], [ "create dispatch request", createDispatchRequestSchema, { kind: "contract", contractId: "c1", shipId: "s1", pilotId: "p1" }, { kind: "contract", contractId: "c1" }, ], ["dispatch response", dispatchResponseSchema, { dispatch }, { dispatch: {} }], [ "buy ship request", buyShipRequestSchema, { modelId: "courier", bayId: "b1" }, { modelId: "courier" }, ], ["ship response", shipResponseSchema, { ship }, {}], ["lease bay request", leaseBayRequestSchema, { locationId: "moon" }, { locationId: 3 }], ["bay response", bayResponseSchema, { bay }, { bay: null }], ["hire pilot request", hirePilotRequestSchema, { locationId: "earth" }, {}], [ "assign pilot request", assignPilotRequestSchema, { pilotId: "p1", shipId: "s1" }, { pilotId: "p1" }, ], ["unassign pilot request", unassignPilotRequestSchema, { pilotId: "p1" }, {}], [ "spend skill point request", spendSkillPointRequestSchema, { pilotId: "p1", skillId: "heavy-cargo" }, { pilotId: "p1" }, ], ["pilot response", pilotResponseSchema, { pilot }, { pilot: {} }], [ "acknowledge summaries request", acknowledgeSummariesRequestSchema, { dispatchIds: ["d0"] }, { dispatchIds: [] }, ], [ "acknowledge a finished upgrade", acknowledgeSummariesRequestSchema, { dispatchIds: [], shipIds: ["s1"] }, { dispatchIds: [], shipIds: [] }, ], ["acknowledge summaries response", acknowledgeSummariesResponseSchema, { acknowledged: 1 }, {}], ]; describe.each(cases)("%s schema", (_name, schema, valid, invalid) => { it("accepts a valid example", () => { expect(schema.safeParse(valid).success).toBe(true); }); it("rejects an invalid example", () => { expect(schema.safeParse(invalid).success).toBe(false); }); }); describe("create dispatch request", () => { it("accepts tutorial and reposition variants", () => { expect( createDispatchRequestSchema.safeParse({ kind: "tutorial", contractId: "c1" }).success, ).toBe(true); expect( createDispatchRequestSchema.safeParse({ kind: "reposition", shipId: "s1", pilotId: "p1", destinationId: "moon", }).success, ).toBe(true); }); }); describe("error codes", () => { it("include every sim dispatch and reposition validation reason", () => { for (const reason of [...DISPATCH_FAILURES, ...REPOSITION_FAILURES]) { expect(ERROR_CODES).toContain(reason); } }); it("are unique", () => { expect(new Set(ERROR_CODES).size).toBe(ERROR_CODES.length); }); }); describe("sim compatibility", () => { it("keeps contracts and dispatches assignable to and from the sim types", () => { expectTypeOf().toExtend(); expectTypeOf().toExtend(); expectTypeOf().toExtend>(); }); });