129 lines
4.8 KiB
TypeScript
129 lines
4.8 KiB
TypeScript
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<typeof landingClassSchema>;
|
||
|
||
export const locationTypeSchema = z.enum([
|
||
"atmospheric-world",
|
||
"airless-body",
|
||
"cloud-platform",
|
||
"belt-station",
|
||
]);
|
||
export type LocationType = z.infer<typeof locationTypeSchema>;
|
||
|
||
/** Astronomical body a location's position is derived from. */
|
||
export const astronomyBodySchema = z.enum(["earth", "moon", "mars", "ceres", "jupiter"]);
|
||
export type AstronomyBody = z.infer<typeof astronomyBodySchema>;
|
||
|
||
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<typeof locationSchema>;
|
||
|
||
export const shipArchetypeSchema = z.enum(["courier", "freighter", "bulk-hauler"]);
|
||
export type ShipArchetype = z.infer<typeof shipArchetypeSchema>;
|
||
|
||
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<typeof shipModelSchema>;
|
||
|
||
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<typeof cargoTypeSchema>;
|
||
|
||
export const skillSchema = z.object({
|
||
id: idSchema,
|
||
name: z.string().min(1),
|
||
maxLevel: positiveInt,
|
||
description: z.string().min(1),
|
||
});
|
||
export type Skill = z.infer<typeof skillSchema>;
|
||
|
||
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<typeof gameConfigSchema>;
|