deadhead-space-freight-tycoon/packages/content/src/schemas.ts

129 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>;