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

667 lines
27 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",
"orbital-station",
"surface-base",
]);
export type LocationType = z.infer<typeof locationTypeSchema>;
/** A fixed point around the Sun (decision 0.5.93): radius in million km, angle in degrees. */
export const fixedPositionSchema = z.object({
radiusMillionKm: z.number().nonnegative(),
angleDegrees: z.number(),
});
export type FixedPosition = z.infer<typeof fixedPositionSchema>;
/** Where a place is, for event routes (CR-026): the inner system, the belt, or the Jupiter system. */
export const regionSchema = z.enum(["inner", "belt", "jupiter"]);
export type Region = z.infer<typeof regionSchema>;
export const locationSchema = z.object({
id: idSchema,
name: z.string().min(1),
/** The inner system, the belt or the Jupiter system, for event routes (CR-026). */
region: regionSchema,
type: locationTypeSchema,
landingRequirement: landingClassSchema,
/** Where it sits, for good (decision 0.5.93). */
position: fixedPositionSchema,
/**
* Set for a body that rides with another (the Moon with Earth): its position
* is the parent's moved straight out from the Sun by the offset.
*/
orbitParent: z
.object({ locationId: idSchema, offsetMillionKm: z.number().positive() })
.optional(),
/** Fuel price here as a multiple of the base price (decision 0.5.96). */
fuelPrice: z.number().positive(),
/** Whether ships can buy fuel here (all base locations do; decision 0.5.96). */
sellsFuel: z.boolean(),
/**
* Multiplies a ship's reach for legs launched from here (decision 0.5.78):
* low gravity stretches it, so a leg from here burns less fuel (0.5.95).
*/
launchRangeFactor: z.number().positive(),
startsDiscovered: z.boolean(),
/** Credits. */
bayLeasePrice: nonNegativeInt,
/** Good ids made here (CR-007), derived from the goods catalog. */
produces: z.array(idSchema),
/** Good ids wanted here (CR-007), derived from the goods catalog. */
demands: z.array(idSchema),
/**
* Cargo class ids that steer a board's class mix (decision 0.5.57): a
* contract's class is one produced here and demanded at its destination.
*/
cargoClassMix: z.object({ produces: z.array(idSchema), demands: z.array(idSchema) }),
/** Flavor company names for contracts from here (decision 0.5.58). */
shippers: z.array(z.string().min(1)).min(1),
/** Map art asset id (CR-003), e.g. `body-earth`; see the web app's art manifest. */
sprite: idSchema,
/** Optional map badge asset id drawn on the body's edge (CR-003). */
badge: idSchema.optional(),
});
export type Location = z.infer<typeof locationSchema>;
export const shipArchetypeSchema = z.enum([
"courier",
"lighter",
"clipper",
"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(),
/** Fuel the tank holds, in fu (decision 0.5.94). */
tankCapacityFu: z.number().positive(),
/**
* fu burned per million km of economic distance (decisions 0.5.84, 0.5.95):
* a leg burns `burn × economicDistance(distance ÷ origin range factor)`.
*/
burnFuPerMillionKm: 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(),
/** False for company-only ships such as the tutorial loaner (decision 0.5.29). */
purchasable: z.boolean(),
/** One line on what the ship is for, shown in the Market (CR-012). */
role: z.string().min(1),
/** Loading time per tonne; defaults to the game-wide `handlingGameHoursPerTonne` (CR-012). */
handlingGameHoursPerTonne: z.number().positive().optional(),
/** Map art asset id (CR-003), e.g. `ship-freighter`. */
sprite: idSchema,
});
export type ShipModel = z.infer<typeof shipModelSchema>;
/**
* Ship stats an upgrade can change (CR-009). The first four are ShipModel
* fields; handling time per tonne is a game-wide base (`handlingGameHoursPerTonne`)
* that becomes per ship once upgraded.
*/
export const SHIP_STATS = [
"cruiseSpeedKmPerGameHour",
"cargoCapacityTonnes",
"tankCapacityFu",
"overheadGameHours",
"handlingGameHoursPerTonne",
"hullRating",
] as const;
export const shipStatSchema = z.enum(SHIP_STATS);
export type ShipStat = z.infer<typeof shipStatSchema>;
/**
* One upgrade (decision 0.5.63): a stat change per level, as a percent of the
* base stat, or a flat amount per level (Hull Plating, decision 0.5.149).
*/
export const upgradeSchema = z.object({
id: idSchema,
name: z.string().min(1),
stat: shipStatSchema,
/** Percent of the model's base stat per level; negative lowers it (e.g. −15 overhead). 0 with `flatPerLevel`. */
percentPerLevel: z.number(),
/** Added to the stat per level instead of a percent (Hull Plating: +1 hull rating). */
flatPerLevel: z.number().optional(),
maxLevel: positiveInt,
/** Per ship model overrides (decisions 0.5.67, 0.5.68). */
modelOverrides: z
.record(
idSchema,
z.object({ percentPerLevel: z.number().optional(), maxLevel: positiveInt.optional() }),
)
.optional(),
});
export type Upgrade = z.infer<typeof upgradeSchema>;
const fraction01 = z.number().min(0).max(1);
const hoursRange = z
.tuple([z.number(), z.number()])
.refine(([low, high]) => low <= high, "A range runs low to high");
/** What an option does (CR-026 §3): every field optional, all numbers in content. */
export const eventEffectsSchema = z
.object({
/** Game hours added to the trip; negative brings arrival forward (never below the trip's floor). */
delayGameHours: hoursRange.optional(),
/** Hull condition lost, in percentage points before the hull rating (decision 0.5.148). */
hullLoss: hoursRange.optional(),
/** Cargo pay lost, as a percent of the payout. */
payLossPct: z.number().positive().optional(),
/** A fee, as a percent of the payout, at least `feeMin` credits. */
feePct: z.number().positive().optional(),
feeMin: z.number().nonnegative().optional(),
/** Extra pay on delivery, as a percent of the payout. */
bonusPct: hoursRange.optional(),
/** Extra XP for the trip, as a percent. */
xpBonusPct: z.number().positive().optional(),
/** Extra fuel burned for the leg, as a percent of its fuel. */
extraFuelPct: z.number().positive().optional(),
})
.strict();
export type EventEffects = z.infer<typeof eventEffectsSchema>;
export const eventOptionSchema = z.object({
id: idSchema,
label: z.string().min(1),
/** The cautious choice, taken when a radio call goes unanswered (decision 0.5.144). */
default: z.boolean(),
/** Needs this much of the hold free (percent of capacity). */
requiresFreeHoldPct: z.number().positive().optional(),
/** Needs the fuel for its extra burn aboard. */
requiresFuel: z.boolean().optional(),
/** Always applies when chosen. */
effects: eventEffectsSchema,
/** Exclusive outcomes on top; the rest of 100% adds nothing. */
chances: z.array(z.object({ chance: fraction01, effects: eventEffectsSchema })),
});
/** Which trips an event can happen on (CR-026 §3); every condition given must hold. */
export const eventWhenSchema = z
.object({
minTripGameHours: z.number().positive().optional(),
/** Legs between regions, which cross or enter the belt. */
crossesBelt: z.boolean().optional(),
/** Legs with an end in the inner system or the belt. */
innerOrBelt: z.boolean().optional(),
/** Arrivals in one of these regions or at one of these places. */
arrivalRegions: z.array(regionSchema).optional(),
arrivalIds: z.array(idSchema).optional(),
/** Contracts only (pay effects need a payout). */
contract: z.boolean().optional(),
/** Contracts of this cargo class. */
cargoClassId: idSchema.optional(),
/** Each level of this skill above the job's tier halves the chance. */
skillHalving: idSchema.optional(),
/** Priority contracts carrying one of these goods. */
priorityGoods: z.array(idSchema).optional(),
/** More likely when the hull is below a level. */
hullBelowWeightFactor: z
.object({ belowPct: z.number().positive(), factor: z.number().positive() })
.optional(),
})
.strict();
export const tripEventSchema = z
.object({
id: idSchema,
/** Automatic events just happen; radio calls ask the player (decision 0.5.144). */
kind: z.enum(["automatic", "radio"]),
good: z.boolean(),
weight: z.number().positive(),
title: z.string().min(1),
/** What happened, with `{pilot}` for the pilot's name. */
text: z.string().min(1),
when: eventWhenSchema,
options: z.array(eventOptionSchema).min(1).max(3),
})
.refine((e) => e.options.filter((o) => o.default).length === 1, {
message: "An event has exactly one default option",
})
.refine((e) => e.kind === "radio" || e.options.length === 1, {
message: "An automatic event has one option",
})
.refine((e) => e.kind === "automatic" || e.options.length >= 2, {
message: "A radio call has two or three options",
})
.refine(
(e) => e.options.every((o) => o.chances.reduce((sum, c) => sum + c.chance, 0) <= 1 + 1e-9),
{ message: "An option's chances add up to at most 100%" },
);
export type TripEventDef = z.infer<typeof tripEventSchema>;
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>;
/** A named good (CR-007, decision 0.5.54). */
export const goodSchema = z
.object({
id: idSchema,
name: z.string().min(1),
/** Cargo class (cargo type) id. */
cargoClassId: idSchema,
massTonnes: z.object({ min: z.number().positive(), max: z.number().positive() }),
/** Multiplies the class's base pay. */
payMultiplier: z.number().positive(),
/** High-Value and Dangerous goods only: the required skill level (decision 0.5.55). */
tier: z.number().int().min(1).max(5).optional(),
priorityEligible: z.boolean(),
/** Shown instead of the name on a Priority contract, e.g. "Emergency Parts". */
priorityName: z.string().min(1).optional(),
producedAt: z.array(idSchema).min(1),
demandedAt: z.array(idSchema).min(1),
})
.refine((g) => g.massTonnes.min <= g.massTonnes.max, {
message: "Mass min must not exceed max",
path: ["massTonnes"],
});
export type Good = z.infer<typeof goodSchema>;
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 shown at that instant (only a displayed date since the map is fixed, 0.5.93).
*/
epoch: z.object({
realStart: z.iso.datetime(),
gameStart: z.iso.datetime(),
}),
startingCredits: nonNegativeInt,
/** Fraction of a normal payout the player receives for a tutorial run. */
tutorialPayoutShare: fraction,
contractBoardSize: positiveInt,
/** Contracts per tutorial board: one per mass tier (CR-001). */
tutorialBoardSize: positiveInt,
/** Ship model the company lends during the tutorial (decision 0.5.29). */
tutorialLoanerShipModelId: idSchema,
/** Tutorial runs completed before the deal is offered (decisions 0.5.30, 0.5.106). */
tutorialRunsBeforeDeal: positiveInt,
/** Tutorial runs earn normal XP × this (decision 0.5.107). */
tutorialXpMultiplier: z.number().min(1),
/** Cargo mass of the three tutorial contracts (decision 0.5.31). */
tutorialMassTonnes: z
.object({
light: z.number().positive(),
medium: z.number().positive(),
full: z.number().positive(),
})
.refine((t) => t.light < t.medium && t.medium < t.full, {
message: "Tiers must increase: light < medium < full",
}),
/** Loading and unloading time per tonne of cargo, in game hours (decision 0.5.31). */
handlingGameHoursPerTonne: z.number().nonnegative(),
/** Share of each positive payout the lender takes until the loan is repaid (0.5.32). */
loanRepaymentShare: fraction,
/**
* What the deal hands over (decisions 0.5.30, 0.5.82): the avatar's ship, the
* veteran's ship and the bay. The loan is their prices plus the lease (0.5.83).
*/
deal: z.object({
avatarShipModelId: z.string().min(1),
veteranShipModelId: z.string().min(1),
bayLocationId: z.string().min(1),
}),
contractBoardPeriodRealMinutes: z.number().positive(),
/** Past board periods whose boards and accepted contracts are kept (Step 7.3). */
boardRetentionPeriods: positiveInt,
/** Resolved, seen dispatches older than this are pruned from the save (CR-014). */
savedDispatchRetentionRealHours: z.number().positive(),
/** Real days logbook entries are kept (decision 0.5.141). */
logbookRetentionRealDays: z.number().positive(),
/**
* Pay and fuel use `reference × (distance / reference)^exponent` (decision
* 0.5.84), so long legs aren't worth hundreds of short ones.
*/
/** Credits per fu before a location's price multiple (decision 0.5.96). */
baseFuelPricePerFu: z.number().positive(),
/** Emergency fuel costs this many times the local price (decision 0.5.99). */
emergencyFuelMultiplier: z.number().min(1),
economicDistance: z.object({
referenceMillionKm: z.number().positive(),
exponent: z.number().positive().max(1),
}),
/**
* Upgrade levels 1, 2, 3, …: cost as a percent of the ship model's price and
* real minutes in the shop (CR-009).
*/
upgradeLevels: z
.array(
z.object({
costPercentOfPrice: z.number().positive(),
shopRealMinutes: z.number().positive(),
}),
)
.min(1),
/** Game hours a pilot may fly before mandatory rest, at Long Haul 0 (decision 0.5.43). */
dutyLimitGameHours: z.number().positive(),
/** Extra duty game hours per Long Haul level (decision 0.5.43). */
longHaulDutyBonusGameHours: z.number().positive(),
/** How long a rest lasts, in game hours; at least one board period (decision 0.5.46). */
restGameHours: z.number().positive(),
firstContactChance: fraction,
firstContactPayoutMultiplier: z.number().positive(),
/** Each board contract's pay rate varies within ± this share of 1 (decision 0.5.60). */
contractPayVariance: fraction,
/** Share of Priority-eligible contracts that become Priority (decision 0.5.56). */
priorityChance: fraction,
/** Payout multiplier for a Priority contract. */
priorityPayoutMultiplier: z.number().positive(),
/** Ship model whose trip time sets a Priority contract's delivery window. */
priorityReferenceShipModelId: idSchema,
/** Multiplies the reference trip time to give the window. */
priorityWindowFactor: z.number().positive(),
/**
* Contract mass bands, one per ship size class: each board has `count`
* contracts with a whole-tonne mass in [min, max]. Counts add up to the
* board size (see the refinement below).
*/
contractMassBands: z
.array(
z
.object({
min: z.number().int().positive(),
max: z.number().int().positive(),
count: positiveInt,
})
.refine((band) => band.min <= band.max, { message: "min must not exceed max" }),
)
.min(1),
/**
* Contracts kept in a mass band the player's fleet can't carry: a preview
* of bigger ships' work (decision 0.5.52). The rest move to flyable bands.
*/
boardPreviewSlots: nonNegativeInt,
/** Contracts per flyable band left open (skill-gated, first contact) rather than fit (0.5.52). */
boardOpenSlotsPerBand: nonNegativeInt,
/** Candidate contracts a board tries before stopping short (decision 0.5.114). */
boardMaxAttempts: positiveInt,
/** 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,
/** Hired pilots' share of profit (payout − fuel) per contract delivery (decision 0.5.12, CR-010). */
pilotProfitShare: fraction,
/** Independent pilots (CR-021, decisions 0.5.123–0.5.129). */
independentPilots: z.object({
/** Credits: the one-time Dispatch Office (0.5.123). */
dispatchOfficePrice: positiveInt,
/** Real minutes into each board period that its jobs are the player's alone (0.5.126). */
firstLookRealMinutes: z.number().positive(),
/** Game minutes from a pilot becoming able to act to their next job (CR-021 §2). */
turnaroundGameMinutes: z.number().positive(),
/** Empty board periods in a row away from home before heading home (0.5.128). */
emptyPeriodsBeforeHome: positiveInt,
/** Real days of absence catch-up replays at most (0.5.129). */
maxCatchUpRealDays: z.number().positive(),
/** Credits pilots never spend the company below, by default (0.5.127). */
defaultSpendingFloor: positiveInt,
}),
/**
* Fleet sleep (decision 0.5.137): real hours after the last manual decision
* until Independent pilots stop starting new work, and the choices offered
* in Settings → Company, shortest first.
*/
fleetSleep: z.object({
defaultHours: z.number().positive(),
optionsHours: z.array(z.number().positive()).min(1),
}),
/** Event pacing (decision 0.5.145). */
events: z.object({
/** Chance per trip: min(max, base + perGameHour × trip game hours) × route multipliers. */
baseChance: z.number().min(0).max(1),
chancePerGameHour: z.number().min(0),
maxChance: z.number().min(0).max(1),
/** Earth↔Moon and legs inside the Jupiter system. */
localLoopMultiplier: z.number().positive(),
beltCrossingMultiplier: z.number().positive(),
/** Arrivals in the Jupiter system or at Venus. */
stormArrivalMultiplier: z.number().positive(),
shipCooldownTrips: z.number().int().nonnegative(),
fleetRadioGapRealMinutes: z.number().nonnegative(),
graceTripsAfterDeal: z.number().int().nonnegative(),
firstEventId: z.string().min(1),
firstEventMinTripGameHours: z.number().positive(),
/** Where in the trip an event fires, as fractions of it. */
fireFrom: z.number().min(0).max(1),
fireTo: z.number().min(0).max(1),
responseWindowRealMinutes: z.number().positive(),
/** A speed-up never makes a trip shorter than this share of it. */
minTripFraction: z.number().min(0).max(1),
}),
/** Auto-repair (decision 0.5.157): the default threshold and the choices, in percent. */
autoRepair: z.object({
defaultThresholdPct: z.number().min(0).max(100),
minPct: z.number().min(0).max(100),
maxPct: z.number().min(0).max(100),
stepPct: z.number().positive(),
}),
/** Hull condition and repairs (decision 0.5.148). */
hull: z.object({
floorPct: z.number().min(0).max(100),
/** Each hull rating point cuts damage chance and amount by this, up to the max. */
ratingReductionPerPoint: z.number().min(0).max(1),
maxRatingReduction: z.number().min(0).max(1),
noDangerousBelowPct: z.number().min(0).max(100),
needsRepairBelowPct: z.number().min(0).max(100),
/** Below `needsRepairBelowPct`, damage chances are multiplied by this. */
damagedChanceMultiplier: z.number().positive(),
/** Repair cost per missing point, as a fraction of the model's price. */
repairCostPerPoint: z.number().positive(),
repairRealMinutesPerPoint: z.number().positive(),
repairMinRealMinutes: z.number().positive(),
/** Condition points every hull loses per game hour flown (decision 0.5.152). */
wearPerGameHour: z.number().nonnegative(),
}),
})
.refine(
(config) =>
config.contractMassBands.reduce((sum, band) => sum + band.count, 0) ===
config.contractBoardSize,
{ message: "Mass band counts must add up to contractBoardSize", path: ["contractMassBands"] },
)
.refine(
(config) =>
config.independentPilots.firstLookRealMinutes < config.contractBoardPeriodRealMinutes,
{
message: "The first-look window must be shorter than the board period",
path: ["independentPilots", "firstLookRealMinutes"],
},
)
.refine(
({ fleetSleep: { optionsHours } }) =>
optionsHours.every((hours, i) => i === 0 || hours > optionsHours[i - 1]!),
{
message: "Sleep options must be in rising order, each once",
path: ["fleetSleep", "optionsHours"],
},
)
.refine(
({ autoRepair: a }) =>
a.minPct <= a.defaultThresholdPct &&
a.defaultThresholdPct <= a.maxPct &&
(a.defaultThresholdPct - a.minPct) % a.stepPct === 0 &&
(a.maxPct - a.minPct) % a.stepPct === 0,
{
message: "The auto-repair default must be one of the steps from min to max",
path: ["autoRepair", "defaultThresholdPct"],
},
)
.refine(({ events }) => events.fireFrom < events.fireTo, {
message: "Events fire inside the trip, early before late",
path: ["events", "fireFrom"],
})
.refine(({ fleetSleep }) => fleetSleep.optionsHours.includes(fleetSleep.defaultHours), {
message: "The default sleep threshold must be one of the options",
path: ["fleetSleep", "defaultHours"],
});
export type GameConfig = z.infer<typeof gameConfigSchema>;
/**
* A special tutorial contract on one run (decision 0.5.33). It replaces the
* medium slot of that run's board.
*/
export const scriptedTutorialContractSchema = z.object({
id: idSchema,
/** The tutorial run (1-based) whose board carries this contract. */
run: positiveInt,
name: z.string().min(1),
cargoTypeId: idSchema,
/** Multiplies the normal tutorial payout. */
payMultiplier: z.number().positive(),
/** Shown in the delivery summary once the contract is delivered. */
flavorMessage: z.string().min(1),
});
export type ScriptedTutorialContract = z.infer<typeof scriptedTutorialContractSchema>;
/** What completes an onboarding goal, checked against a player state summary. */
export const goalConditionSchema = z.discriminatedUnion("kind", [
/** Contracts delivered by the player's own ships (not tutorial runs). */
z.object({ kind: z.literal("own-ship-deliveries"), count: positiveInt }),
/** Skill points spent across all pilots. */
z.object({ kind: z.literal("skill-points-spent"), count: positiveInt }),
z.object({ kind: z.literal("ships-owned"), count: positiveInt }),
/** Ships bought from the Market; the deal's ships don't count (CR-013). */
z.object({ kind: z.literal("market-ships-bought"), count: positiveInt }),
z.object({ kind: z.literal("pilots-hired"), count: positiveInt }),
/** Priority contracts delivered, by any pilot (CR-013). */
z.object({ kind: z.literal("priority-deliveries"), count: positiveInt }),
/** Locations discovered beyond the ones that start discovered. */
z.object({ kind: z.literal("locations-discovered"), count: positiveInt }),
/** The deal's loan is fully repaid. */
z.object({ kind: z.literal("loan-paid-off") }),
]);
export type GoalCondition = z.infer<typeof goalConditionSchema>;
/** One onboarding goal with a one-time credit reward (decision 0.5.34). */
export const onboardingGoalSchema = z.object({
id: idSchema,
title: z.string().min(1),
condition: goalConditionSchema,
/** Credits, claimed once. */
reward: positiveInt,
});
export type OnboardingGoal = z.infer<typeof onboardingGoalSchema>;
/** The veteran pilot who comes with the deal (decision 0.5.70, CR-010). */
export const veteranPilotSchema = z.object({
name: z.string().min(1),
/** One line shown on the deal and the pilot card. */
bio: z.string().min(1),
level: positiveInt,
/** Skill levels already bought, by skill id. */
skills: z.record(idSchema, positiveInt),
});
export type VeteranPilot = z.infer<typeof veteranPilotSchema>;
/**
* The welcome cards (decisions 0.5.131, 0.5.132): what the game is, how it
* plays and where you start. `{company}` in a body is the tutorial employer's name.
*/
export const welcomeSchema = z.object({
cards: z.array(z.object({ title: z.string().min(1), body: z.string().min(1) })).min(1),
next: z.string().min(1),
skip: z.string().min(1),
/** The last card's button for a new company. */
start: z.string().min(1),
/** The last card's button when reopened from Settings → Help. */
close: z.string().min(1),
});
export type Welcome = z.infer<typeof welcomeSchema>;
const line = z.string().min(1);
/**
* "While you were away" wording (decision 0.5.140). `{name}` marks a value
* the client fills in: places, goods, pilots, ships, credits and counts.
*/
export const awayWordingSchema = z.object({
title: line,
totals: z.object({ net: line, deliveries: line, discoveries: line, loanRepaid: line }),
highlights: z.object({
discovery: line,
levelUp: line,
goalReady: line,
bestJob: line,
noJobs: line,
lowFuel: line,
emergencyFuel: line,
belowFloor: line,
serviceWaitingFunds: line,
serviceCantReach: line,
}),
moreHighlights: line,
pilotLine: line,
leveledUp: line,
morePilots: line,
idlePilots: line,
idlePilot: line,
direct: z.object({
delivery: line,
deadhead: line,
upgrade: line,
discovery: line,
levelUp: line,
}),
viewLogbook: line,
done: line,
});
export type AwayWording = z.infer<typeof awayWordingSchema>;