deadhead-space-freight-tycoon/packages/schema/src/entities.ts

191 lines
6.8 KiB
TypeScript

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<typeof serverTimeSchema>;
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<typeof companySchema>;
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<typeof shipSchema>;
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<typeof pilotSchema>;
export const baySchema = z.object({
id: idSchema,
locationId: idSchema,
});
export type Bay = z.infer<typeof baySchema>;
export const locationStateSchema = z.object({
id: idSchema,
discovered: z.boolean(),
});
export type LocationState = z.infer<typeof locationStateSchema>;
/** 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<typeof contractSchema>;
/** 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<typeof dispatchSchema>;
/** 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<typeof resolutionSummarySchema>;
/** 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<typeof tutorialStateSchema>;
/** 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<typeof goalStateSchema>;
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<typeof playerStateSchema>;