import { z } from "zod"; /** * Per-player data sent over the API. Static data (location details, ship * models, cargo, skills, config) comes from `@deadhead/content`; these shapes * refer to it by id. Times are Unix milliseconds, as in `@deadhead/sim`. */ export const idSchema = z.string().min(1); export const timeSchema = z.number().int().nonnegative(); const credits = z.number().int(); export const serverTimeSchema = z.object({ realTime: timeSchema, /** In-game instant (sim `realToGame` of `realTime`). */ gameTime: z.number().int(), }); export type ServerTime = z.infer; export const companySchema = z.object({ id: idSchema, name: z.string().min(1), /** May go negative only through pilot pay on a low payout (see DECISIONS Step 3.17). */ credits, /** Owed to the lender from the deal (decision 0.5.32); 0 when paid off or never taken. */ loanBalance: credits.nonnegative(), }); export type Company = z.infer; export const shipSchema = z.object({ id: idSchema, modelId: idSchema, name: z.string().min(1), status: z.enum(["idle", "in-flight"]), /** Where an idle ship is; null while in flight. */ locationId: idSchema.nullable(), /** The active dispatch while in flight; null when idle. */ dispatchId: idSchema.nullable(), pilotId: idSchema.nullable(), /** * When the ship last arrived somewhere: its latest resolved flight, or its * delivery if it has not flown. Picks the default Contracts location (CR-002). */ lastArrivalAt: timeSchema, }); export type Ship = z.infer; export const pilotSchema = z.object({ id: idSchema, name: z.string().min(1), isAvatar: z.boolean(), xp: z.number().int().nonnegative(), level: z.number().int().nonnegative(), /** Skill levels by skill id; a missing skill is level 0. */ skills: z.record(z.string(), z.number().int().nonnegative()), skillPointsAvailable: z.number().int().nonnegative(), assignedShipId: idSchema.nullable(), status: z.enum(["idle", "in-flight"]), /** Where the pilot is (their ship's location if assigned); null while in flight. */ locationId: idSchema.nullable(), /** Game hours flown since the last rest, and the limit before mandatory rest (CR-004). */ dutyHours: z.number().nonnegative(), dutyLimit: z.number().positive(), /** Resting pilots cannot fly until `restEndsAt` (null when not resting). */ resting: z.boolean(), restEndsAt: timeSchema.nullable(), }); export type Pilot = z.infer; export const baySchema = z.object({ id: idSchema, locationId: idSchema, }); export type Bay = z.infer; export const locationStateSchema = z.object({ id: idSchema, discovered: z.boolean(), }); export type LocationState = z.infer; /** Mirrors the sim `Contract`. */ export const contractSchema = z.object({ id: idSchema, originId: idSchema, destinationId: idSchema, cargoTypeId: idSchema, /** The named good (CR-007). */ goodId: idSchema, /** Flavor: the shipping company (decision 0.5.58). */ shipper: z.string().min(1), priority: z.boolean(), /** Priority only: the longest allowed trip in game hours (decision 0.5.56). */ deliveryWindowGameHours: z.number().positive().nullable(), tier: z.number().int().min(0), massTonnes: z.number().positive(), payout: credits, requiredSkill: z.object({ skillId: idSchema, level: z.number().int().positive() }).nullable(), requiredLongHaulLevel: z.number().int().nonnegative(), offeredAt: timeSchema, expiresAt: timeSchema, firstContact: z.boolean(), /** Set on a scripted tutorial contract (decision 0.5.33). */ scriptedId: idSchema.optional(), }); export type Contract = z.infer; /** Mirrors the sim `DispatchRecord`, plus server-side status. */ export const dispatchSchema = z.object({ id: idSchema, shipId: idSchema, pilotId: idSchema, contract: contractSchema.nullable(), originId: idSchema, destinationId: idSchema, tutorial: z.boolean(), departureAt: timeSchema, arrivalAt: timeSchema, distanceMillionKm: z.number().nonnegative(), fuelCost: credits.nonnegative(), expectedPayout: credits, seed: z.number().int().nonnegative(), status: z.enum(["in-flight", "resolved"]), resolvedAt: timeSchema.nullable(), }); export type Dispatch = z.infer; /** One "while you were away" entry: a resolved dispatch not yet acknowledged. */ export const resolutionSummarySchema = z.object({ dispatchId: idSchema, shipId: idSchema, pilotId: idSchema, originId: idSchema, destinationId: idSchema, tutorial: z.boolean(), /** An empty reposition flight: no cargo, no pay (CR-002). */ deadhead: z.boolean(), /** Fuel paid at departure. */ fuelCost: credits.nonnegative(), /** What was delivered and for whom (CR-007); null for a deadhead. */ goodId: idSchema.nullable(), massTonnes: z.number().positive().nullable(), shipper: z.string().min(1).nullable(), /** Credits added after pilot pay and loan repayment. */ credits, /** Credits the lender took from this payout (decision 0.5.32). */ loanRepayment: credits.nonnegative(), pilotXp: z.number().int().nonnegative(), /** Set when the delivery discovered its destination. */ discoveredLocationId: idSchema.nullable(), /** The scripted tutorial contract delivered, if any (for its flavor message). */ scriptedId: idSchema.nullable(), /** Set when the trip ended in mandatory rest: the pilot rests until then (CR-004). */ restEndsAt: timeSchema.nullable(), resolvedAt: timeSchema, }); export type ResolutionSummary = z.infer; /** Decision 0.5.26: the loaner is not in the ships list. Null once the tutorial ends. */ export const tutorialStateSchema = z.object({ loanerLocationId: z.enum(["earth", "moon"]), runInFlight: z.boolean(), runsCompleted: z.number().int().nonnegative(), /** The one-time deal can be accepted now (decision 0.5.30). */ dealAvailable: z.boolean(), }); export type TutorialState = z.infer; /** An onboarding goal's progress (decision 0.5.34). Static details come from content. */ export const goalStateSchema = z.object({ id: idSchema, status: z.enum(["incomplete", "ready", "claimed"]), }); export type GoalState = z.infer; export const playerStateSchema = z.object({ company: companySchema, ships: z.array(shipSchema), pilots: z.array(pilotSchema), bays: z.array(baySchema), locations: z.array(locationStateSchema), tutorial: tutorialStateSchema.nullable(), /** In-flight dispatches, including the tutorial run. */ activeDispatches: z.array(dispatchSchema), /** Unacknowledged "while you were away" entries. */ summaries: z.array(resolutionSummarySchema), /** Onboarding goals in content order; empty during the tutorial. */ goals: z.array(goalStateSchema), }); export type PlayerState = z.infer;