import { z } from "zod"; /** Lowercase kebab-case identifier, e.g. `earth`, `bulk-hauler`. */ export const idSchema = z.string().regex(/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/); const positiveInt = z.number().int().positive(); const nonNegativeInt = z.number().int().nonnegative(); const fraction = z.number().min(0).max(1); /** Strictly increasing list of positive numbers. */ const increasingList = z .array(z.number().positive()) .refine((list) => list.every((value, i) => i === 0 || value > list[i - 1]!), { message: "Values must be strictly increasing", }); /** Ordered from least to most capable: a ship can land wherever its class or a lower one is required. */ export const landingClassSchema = z.enum(["vacuum", "atmospheric", "hardened"]); export type LandingClass = z.infer; export const locationTypeSchema = z.enum([ "atmospheric-world", "airless-body", "cloud-platform", "belt-station", ]); export type LocationType = z.infer; /** Astronomical body a location's position is derived from. */ export const astronomyBodySchema = z.enum(["earth", "moon", "mars", "ceres", "jupiter"]); export type AstronomyBody = z.infer; export const locationSchema = z.object({ id: idSchema, name: z.string().min(1), type: locationTypeSchema, landingRequirement: landingClassSchema, body: astronomyBodySchema, /** Multiplies fuel cost for launches from this location. */ launchCostMultiplier: z.number().positive(), startsDiscovered: z.boolean(), /** Credits. */ bayLeasePrice: nonNegativeInt, /** Cargo type ids. */ produces: z.array(idSchema), /** Cargo type ids. */ demands: z.array(idSchema), }); export type Location = z.infer; export const shipArchetypeSchema = z.enum(["courier", "freighter", "bulk-hauler"]); export type ShipArchetype = z.infer; export const shipModelSchema = z.object({ id: idSchema, name: z.string().min(1), archetype: shipArchetypeSchema, /** Credits. */ price: positiveInt, cruiseSpeedKmPerGameHour: z.number().positive(), cargoCapacityTonnes: z.number().positive(), fuelRangeMillionKm: z.number().positive(), /** Stored only; no effect in the base game (decision 0.5.13). */ hullRating: nonNegativeInt, landingClass: landingClassSchema, /** Fixed takeoff + landing time added to every trip. */ overheadGameHours: z.number().nonnegative(), /** Credits per million km, before the origin's launch multiplier. */ fuelCostPerMillionKm: z.number().nonnegative(), }); export type ShipModel = z.infer; export const cargoTypeSchema = z.object({ id: idSchema, name: z.string().min(1), /** Credits per tonne per million km. */ basePayPerTonnePerMillionKm: z.number().positive(), /** Pay multiplier applied according to the contract's tier (Standard is tier 0, others 1–5). */ tierPayMultiplier: z.number().positive(), /** Skill whose level must meet the contract's tier; null for Standard. */ requiredSkillId: idSchema.nullable(), }); export type CargoType = z.infer; export const skillSchema = z.object({ id: idSchema, name: z.string().min(1), maxLevel: positiveInt, description: z.string().min(1), }); export type Skill = z.infer; export const gameConfigSchema = z.object({ /** Game hours per real hour (60 means 1 real minute = 1 game hour). */ timeCompression: z.number().positive(), /** Decision 0.5.9: the real instant the game clock starts and the in-game date it maps to. */ epoch: z.object({ realStart: z.iso.datetime(), ephemerisDate: z.iso.datetime(), }), startingCredits: nonNegativeInt, /** Fraction of a normal payout the player receives for a tutorial run. */ tutorialPayoutShare: fraction, contractBoardSize: positiveInt, /** Large enough that a player cannot exhaust it within one period (decision 0.5.21). */ tutorialBoardSize: positiveInt, contractBoardPeriodRealMinutes: z.number().positive(), firstContactChance: fraction, firstContactPayoutMultiplier: z.number().positive(), contractMassTonnes: z .object({ min: z.number().positive(), max: z.number().positive() }) .refine((range) => range.min <= range.max, { message: "min must not exceed max" }), /** One cut-off per Long Haul level (decision 0.5.7). */ longHaulCutoffsMillionKm: increasingList, xpPerDelivery: z.object({ base: z.number().nonnegative(), perMillionKm: z.number().nonnegative(), perCargoTier: z.number().nonnegative(), }), /** Total XP needed to reach level 1, 2, 3, ... */ pilotLevelXpThresholds: increasingList, skillPointsPerLevel: positiveInt, /** Credits. */ pilotHireCost: nonNegativeInt, /** Credits per delivery; the avatar is exempt (decision 0.5.12). */ pilotPayPerDelivery: nonNegativeInt, }); export type GameConfig = z.infer;