375 lines
10 KiB
TypeScript
375 lines
10 KiB
TypeScript
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<SimContract>().toExtend<Contract>();
|
|
expectTypeOf<Contract>().toExtend<SimContract>();
|
|
expectTypeOf<DispatchRecord>().toExtend<Omit<Dispatch, "status" | "resolvedAt">>();
|
|
});
|
|
});
|