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", "orbital-station", "surface-base", ]); export type LocationType = z.infer; /** 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; /** 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; 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; export const shipArchetypeSchema = z.enum([ "courier", "lighter", "clipper", "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(), /** 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; /** * 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; /** * 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; 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; 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; 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; /** 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; 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 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; /** * 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; /** 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; /** 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; /** 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; /** * 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; 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;