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

156 lines
4.4 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { z } from "zod";
import {
cargoTypeSchema,
gameConfigSchema,
locationSchema,
onboardingGoalSchema,
scriptedTutorialContractSchema,
shipModelSchema,
skillSchema,
} from "../src/index";
const validLocation = {
id: "earth",
name: "Earth",
type: "atmospheric-world",
landingRequirement: "atmospheric",
body: "earth",
launchCostMultiplier: 3,
startsDiscovered: true,
bayLeasePrice: 5000,
sprite: "body-mars",
produces: ["standard"],
demands: ["heavy"],
};
const validShipModel = {
id: "courier",
name: "Courier",
archetype: "courier",
price: 20000,
cruiseSpeedKmPerGameHour: 300000,
cargoCapacityTonnes: 10,
fuelRangeMillionKm: 5,
hullRating: 1,
landingClass: "atmospheric",
overheadGameHours: 2,
fuelCostPerMillionKm: 100,
purchasable: true,
sprite: "ship-courier",
};
const validCargoType = {
id: "standard",
name: "Standard",
basePayPerTonnePerMillionKm: 10,
tierPayMultiplier: 1,
requiredSkillId: null,
};
const validSkill = {
id: "long-haul",
name: "Long Haul",
maxLevel: 5,
description: "Access to longer routes.",
};
const validGameConfig = {
timeCompression: 60,
epoch: { realStart: "2026-01-01T00:00:00Z", ephemerisDate: "2100-01-01T00:00:00Z" },
startingCredits: 0,
tutorialPayoutShare: 0.2,
contractBoardSize: 8,
tutorialBoardSize: 50,
contractBoardPeriodRealMinutes: 60,
firstContactChance: 0.1,
firstContactPayoutMultiplier: 2,
contractMassBands: [
{ min: 1, max: 20, count: 4 },
{ min: 21, max: 100, count: 4 },
],
longHaulCutoffsMillionKm: [100, 200, 300, 400, 500],
xpPerDelivery: { base: 10, perMillionKm: 0.1, perCargoTier: 5 },
pilotLevelXpThresholds: [100, 250, 500],
skillPointsPerLevel: 1,
pilotHireCost: 1000,
pilotPayPerDelivery: 100,
tutorialLoanerShipModelId: "company-courier",
tutorialRunsBeforeDeal: 4,
tutorialMassTonnes: { light: 5, medium: 12, full: 20 },
handlingGameHoursPerTonne: 0.02,
loanRepaymentShare: 0.3,
dealLoanAmount: 5000,
};
const validScripted = {
id: "unmarked-crates",
run: 3,
name: "Unmarked crates",
cargoTypeId: "standard",
payMultiplier: 2,
flavorMessage: "Nobody asked.",
};
const validGoal = {
id: "first-hire",
title: "Hire your first pilot",
condition: { kind: "pilots-hired", count: 1 },
reward: 1000,
};
const cases: [string, z.ZodType, Record<string, unknown>][] = [
["Location", locationSchema, validLocation],
["ShipModel", shipModelSchema, validShipModel],
["CargoType", cargoTypeSchema, validCargoType],
["Skill", skillSchema, validSkill],
["GameConfig", gameConfigSchema, validGameConfig],
["ScriptedTutorialContract", scriptedTutorialContractSchema, validScripted],
["OnboardingGoal", onboardingGoalSchema, validGoal],
];
describe.each(cases)("%s schema", (_name, schema, valid) => {
it("accepts a valid example", () => {
expect(schema.safeParse(valid).success).toBe(true);
});
it.each(Object.keys(valid))("rejects an object missing %s", (field) => {
const missing = { ...valid };
delete missing[field];
expect(schema.safeParse(missing).success).toBe(false);
});
});
describe("GameConfig refinements", () => {
it("rejects tutorial mass tiers that do not increase", () => {
const config = { ...validGameConfig, tutorialMassTonnes: { light: 12, medium: 12, full: 20 } };
expect(gameConfigSchema.safeParse(config).success).toBe(false);
});
it("rejects a goal condition of an unknown kind", () => {
const goal = { ...validGoal, condition: { kind: "win-the-game" } };
expect(onboardingGoalSchema.safeParse(goal).success).toBe(false);
});
it("rejects a mass band with min above max", () => {
const config = {
...validGameConfig,
contractMassBands: [
{ min: 50, max: 10, count: 4 },
{ min: 21, max: 100, count: 4 },
],
};
expect(gameConfigSchema.safeParse(config).success).toBe(false);
});
it("rejects mass band counts that do not add up to the board size", () => {
const config = { ...validGameConfig, contractMassBands: [{ min: 1, max: 20, count: 3 }] };
expect(gameConfigSchema.safeParse(config).success).toBe(false);
});
it("rejects Long Haul cut-offs that are not increasing", () => {
const config = { ...validGameConfig, longHaulCutoffsMillionKm: [100, 100, 300] };
expect(gameConfigSchema.safeParse(config).success).toBe(false);
});
});