Step 22.1: content and save field
CI / check (push) Successful in 6m51s
Details
CI / check (push) Successful in 6m51s
Details
Welcome card text and the tutorial employer's name in content; save version 7 records whether this company's welcome was seen (decision 0.5.131). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
8507fa7c1e
commit
7355f7dbfb
|
|
@ -1745,3 +1745,8 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md`
|
||||||
## CR-022 recorded (Welcome Screens)
|
## CR-022 recorded (Welcome Screens)
|
||||||
|
|
||||||
- **Numbering:** received as "CR-020 — Welcome Screens" for "Phase 20"; stored as **CR-022**, built as **Phase 22** (its 20.n = 22.n), since CR-020 (Bridge) and CR-021 (Independent Pilots) exist. Its decisions CR-020.D1–D2 are **0.5.131–0.5.132**. Its references: CR-008 → CR-011, CR-011 → CR-014; CR-001 is unchanged.
|
- **Numbering:** received as "CR-020 — Welcome Screens" for "Phase 20"; stored as **CR-022**, built as **Phase 22** (its 20.n = 22.n), since CR-020 (Bridge) and CR-021 (Independent Pilots) exist. Its decisions CR-020.D1–D2 are **0.5.131–0.5.132**. Its references: CR-008 → CR-011, CR-011 → CR-014; CR-001 is unchanged.
|
||||||
|
|
||||||
|
## Step 22.1
|
||||||
|
|
||||||
|
- **"Halvorsen Lunar Freight" is now content** (`tutorialCompanyName`): the CR says the welcome takes it "from the existing content name", but it was hard-coded in three screens. Card 3 says `{company}`, and Step 22.2 points the screens at the content name too.
|
||||||
|
- **`welcomeSeen` lives on the save's `player`**, alongside the tutorial state, since it belongs to the company, not the device. That's why it isn't a one-time tip in device preferences like the tutorial intro card.
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ describe("about and debug info (Step 10K.10)", () => {
|
||||||
expect(text).toContain("Build: test");
|
expect(text).toContain("Build: test");
|
||||||
// CR-014: the save, its format version and where it is stored; no player id.
|
// CR-014: the save, its format version and where it is stored; no player id.
|
||||||
expect(text).toContain("Save: player-1");
|
expect(text).toContain("Save: player-1");
|
||||||
expect(text).toContain("Save version: 6");
|
expect(text).toContain("Save version: 7");
|
||||||
expect(text).toContain("Storage: unknown");
|
expect(text).toContain("Storage: unknown");
|
||||||
expect(text).not.toContain("Player:");
|
expect(text).not.toContain("Player:");
|
||||||
expect(text).toMatch(/Game time: \d{4}-\d{2}-\d{2}T/);
|
expect(text).toMatch(/Game time: \d{4}-\d{2}-\d{2}T/);
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ The `routes` catalog is the list of engine commands (CR-014). Each entry has `re
|
||||||
|
|
||||||
## Save document (`packages/schema/src/save.ts`, CR-014)
|
## Save document (`packages/schema/src/save.ts`, CR-014)
|
||||||
|
|
||||||
One document per company on the device, validated on every load and save: `saveVersion` (currently 5; 1 → 2 added fuel tanks, CR-015; 2 → 3 put every ship on Manual refueling, CR-018; 3 → 4 dropped stored boards, CR-019; 4 → 5 added pilot `mode` and `orders` and the company's `dispatchOffice` and `npcSpendingFloor`, CR-021; 5 → 6 added pilot `nextDecisionAt` and `emptyPeriods` and dispatch `independent`, CR-021), `saveId`, `lastTimeSeen` (the clock never goes below it), `player` (world seed, tutorial state, deal accepted), `company` (name, credits, loan), `ships` (with `upgrades` as a level map, shop fields, and `fuelFu`, `fuelAvgPrice`, `refuelPolicy` since CR-015), `pilots`, `bays`, `discoveredLocations`, `contractBoards` (stored boards), `dispatches` (sim record, status, resolution, acknowledged), `acceptedContracts`, `claimedGoals`. It mirrors the old server's database tables without player ids; the stored contract, dispatch record and resolution are type-checked against the sim types. `migrateSave(raw)` steps an older save through `SAVE_MIGRATIONS` (`applyMigrations`) and validates it, returning a readable error instead of throwing.
|
One document per company on the device, validated on every load and save: `saveVersion` (currently 5; 1 → 2 added fuel tanks, CR-015; 2 → 3 put every ship on Manual refueling, CR-018; 3 → 4 dropped stored boards, CR-019; 4 → 5 added pilot `mode` and `orders` and the company's `dispatchOffice` and `npcSpendingFloor`, CR-021; 5 → 6 added pilot `nextDecisionAt` and `emptyPeriods` and dispatch `independent`, CR-021; 6 → 7 added `player.welcomeSeen`, true for existing companies, CR-022), `saveId`, `lastTimeSeen` (the clock never goes below it), `player` (world seed, tutorial state, deal accepted), `company` (name, credits, loan), `ships` (with `upgrades` as a level map, shop fields, and `fuelFu`, `fuelAvgPrice`, `refuelPolicy` since CR-015), `pilots`, `bays`, `discoveredLocations`, `contractBoards` (stored boards), `dispatches` (sim record, status, resolution, acknowledged), `acceptedContracts`, `claimedGoals`. It mirrors the old server's database tables without player ids; the stored contract, dispatch record and resolution are type-checked against the sim types. `migrateSave(raw)` steps an older save through `SAVE_MIGRATIONS` (`applyMigrations`) and validates it, returning a readable error instead of throwing.
|
||||||
|
|
||||||
Commands (`packages/engine/src/game/`): `GameEngine.call(name, input)` runs the route catalog's endpoints on the save, with the same schemas and error codes (`CommandError`); `getPlayerState` with no save answers `unauthorized`. Every command resolves due arrivals first and prunes on save; saves are skipped when nothing changed. `stats` keeps lifetime delivery counts for goals, so seen trips older than `savedDispatchRetentionRealHours` can be pruned.
|
Commands (`packages/engine/src/game/`): `GameEngine.call(name, input)` runs the route catalog's endpoints on the save, with the same schemas and error codes (`CommandError`); `getPlayerState` with no save answers `unauthorized`. Every command resolves due arrivals first and prunes on save; saves are skipped when nothing changed. `stats` keeps lifetime delivery counts for goals, so seen trips older than `savedDispatchRetentionRealHours` can be pruned.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -489,7 +489,7 @@ Spec: `memory-bank/change-requests/CR-021-independent-pilots.md` (received as "C
|
||||||
|
|
||||||
Spec: `memory-bank/change-requests/CR-022-welcome-screens.md` (received as "CR-020 … Phase 20"). New decisions recorded as plan §0.5.131–0.5.132.
|
Spec: `memory-bank/change-requests/CR-022-welcome-screens.md` (received as "CR-020 … Phase 20"). New decisions recorded as plan §0.5.131–0.5.132.
|
||||||
|
|
||||||
- [ ] Step 22.1 — Content and save field
|
- [x] **Step 22.1 — Content and save field.** Content: `welcome` (three cards' titles and bodies, and the Next, Skip, "Let's fly" and Close labels; `welcomeSchema`), with `{company}` in the last card filled from the new `tutorialCompanyName` ("Halvorsen Lunar Freight", until now written into three screens). Save version 7: `player.welcomeSeen`, false for a new company; the 6 → 7 migration sets it true for existing ones. Tests: `packages/content/test/onboarding.test.ts` (the cards validate, in order, with `{company}` and the labels; empty text is rejected); `packages/engine/test/start.test.ts` (a new company's welcome is unseen); `packages/schema/test/save.test.ts` (6 → 7 marks it seen and changes nothing else).
|
||||||
- [ ] Step 22.2 — Client: welcome pop-up
|
- [ ] Step 22.2 — Client: welcome pop-up
|
||||||
- [ ] Step 22.3 — Client: new company and "How to play"
|
- [ ] Step 22.3 — Client: new company and "How to play"
|
||||||
- [ ] Step 22.4 — Regression
|
- [ ] Step 22.4 — Regression
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,13 @@ import { gameConfig } from "./data/config";
|
||||||
import { dealLoanAmount } from "./deal";
|
import { dealLoanAmount } from "./deal";
|
||||||
import { goods } from "./data/goods";
|
import { goods } from "./data/goods";
|
||||||
import { locations } from "./data/locations";
|
import { locations } from "./data/locations";
|
||||||
import { onboardingGoals, scriptedTutorialContracts, veteranPilot } from "./data/onboarding";
|
import {
|
||||||
|
onboardingGoals,
|
||||||
|
scriptedTutorialContracts,
|
||||||
|
tutorialCompanyName,
|
||||||
|
veteranPilot,
|
||||||
|
welcome,
|
||||||
|
} from "./data/onboarding";
|
||||||
import { shipModels } from "./data/ships";
|
import { shipModels } from "./data/ships";
|
||||||
import { upgrades } from "./data/upgrades";
|
import { upgrades } from "./data/upgrades";
|
||||||
|
|
||||||
|
|
@ -26,6 +32,9 @@ export const content = {
|
||||||
scriptedTutorialContracts,
|
scriptedTutorialContracts,
|
||||||
onboardingGoals,
|
onboardingGoals,
|
||||||
veteranPilot,
|
veteranPilot,
|
||||||
|
/** The tutorial employer (CR-001) and the welcome cards (CR-022). */
|
||||||
|
tutorialCompanyName,
|
||||||
|
welcome,
|
||||||
upgrades,
|
upgrades,
|
||||||
/** The deal's loan: its ships' prices plus the bay lease (decision 0.5.83). */
|
/** The deal's loan: its ships' prices plus the bay lease (decision 0.5.83). */
|
||||||
dealLoanAmount: dealLoanAmount({ config: gameConfig, shipModels, locations }),
|
dealLoanAmount: dealLoanAmount({ config: gameConfig, shipModels, locations }),
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,29 @@
|
||||||
import type { OnboardingGoal, ScriptedTutorialContract, VeteranPilot } from "../schemas";
|
import type { OnboardingGoal, ScriptedTutorialContract, VeteranPilot, Welcome } from "../schemas";
|
||||||
|
|
||||||
|
/** The company the player flies for in the tutorial (CR-001). */
|
||||||
|
export const tutorialCompanyName = "Halvorsen Lunar Freight";
|
||||||
|
|
||||||
|
/** The welcome cards for every new company (decisions 0.5.131, 0.5.132). */
|
||||||
|
export const welcome: Welcome = {
|
||||||
|
cards: [
|
||||||
|
{
|
||||||
|
title: "Welcome to Deadhead",
|
||||||
|
body: "A space trucking tycoon. Haul freight across the solar system, grow from a hired pilot into a shipping company, and never fly home empty.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Your ships keep flying",
|
||||||
|
body: "Pick contracts, choose a ship and pilot, and send them off. Trips run in real time, even when the app is closed. Come back to finished deliveries, then send them out again.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "First, earn your wings",
|
||||||
|
body: "You're flying for {company} on the Earth–Moon run. Do good work, and the old man might have an offer for you.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
next: "Next",
|
||||||
|
skip: "Skip",
|
||||||
|
start: "Let's fly",
|
||||||
|
close: "Close",
|
||||||
|
};
|
||||||
|
|
||||||
/** Special tutorial contracts by run (decision 0.5.33). */
|
/** Special tutorial contracts by run (decision 0.5.33). */
|
||||||
export const scriptedTutorialContracts: ScriptedTutorialContract[] = [
|
export const scriptedTutorialContracts: ScriptedTutorialContract[] = [
|
||||||
|
|
|
||||||
|
|
@ -417,3 +417,18 @@ export const veteranPilotSchema = z.object({
|
||||||
skills: z.record(idSchema, positiveInt),
|
skills: z.record(idSchema, positiveInt),
|
||||||
});
|
});
|
||||||
export type VeteranPilot = z.infer<typeof veteranPilotSchema>;
|
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>;
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||||
import {
|
import {
|
||||||
content,
|
content,
|
||||||
onboardingGoalSchema,
|
onboardingGoalSchema,
|
||||||
|
welcomeSchema,
|
||||||
onboardingGoals,
|
onboardingGoals,
|
||||||
scriptedTutorialContractSchema,
|
scriptedTutorialContractSchema,
|
||||||
scriptedTutorialContracts,
|
scriptedTutorialContracts,
|
||||||
|
|
@ -60,3 +61,29 @@ describe("onboarding goals", () => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("welcome cards (Step 22.1, decision 0.5.131)", () => {
|
||||||
|
it("validates, with three cards and the tutorial employer's name in the last", () => {
|
||||||
|
expect(() => welcomeSchema.parse(content.welcome)).not.toThrow();
|
||||||
|
expect(content.welcome.cards.map((c) => c.title)).toEqual([
|
||||||
|
"Welcome to Deadhead",
|
||||||
|
"Your ships keep flying",
|
||||||
|
"First, earn your wings",
|
||||||
|
]);
|
||||||
|
expect(content.welcome.cards[2]!.body).toContain("{company}");
|
||||||
|
expect(content.tutorialCompanyName).toBe("Halvorsen Lunar Freight");
|
||||||
|
expect(content.welcome).toMatchObject({
|
||||||
|
next: "Next",
|
||||||
|
skip: "Skip",
|
||||||
|
start: "Let's fly",
|
||||||
|
close: "Close",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects empty text", () => {
|
||||||
|
expect(welcomeSchema.safeParse({ ...content.welcome, cards: [] }).success).toBe(false);
|
||||||
|
expect(
|
||||||
|
welcomeSchema.safeParse({ ...content.welcome, cards: [{ title: "", body: "x" }] }).success,
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ export function startingSave(
|
||||||
tutorialRunsCompleted: 0,
|
tutorialRunsCompleted: 0,
|
||||||
dealAccepted: false,
|
dealAccepted: false,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
|
welcomeSeen: false,
|
||||||
},
|
},
|
||||||
company: {
|
company: {
|
||||||
id: random.id(),
|
id: random.id(),
|
||||||
|
|
|
||||||
|
|
@ -95,3 +95,14 @@ describe("company creation on the device (Step 14.8)", () => {
|
||||||
expect(b.state.company.name).toBe(companyName(2));
|
expect(b.state.company.name).toBe(companyName(2));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("welcome cards in the save (Step 22.1, decision 0.5.131)", () => {
|
||||||
|
it("starts a new company with its welcome not yet seen", async () => {
|
||||||
|
const backend = new MemoryBackend();
|
||||||
|
const engine = engineOn(backend);
|
||||||
|
await engine.call("createGuest", {});
|
||||||
|
const save = await new SaveStorage(backend).load();
|
||||||
|
if (save.status !== "loaded") throw new Error(save.status);
|
||||||
|
expect(save.save.player.welcomeSeen).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -164,7 +164,7 @@ export const savedDispatchSchema = z.object({
|
||||||
});
|
});
|
||||||
|
|
||||||
/** The save version this build writes. Bump it with a new migration. */
|
/** The save version this build writes. Bump it with a new migration. */
|
||||||
export const CURRENT_SAVE_VERSION = 6;
|
export const CURRENT_SAVE_VERSION = 7;
|
||||||
|
|
||||||
export const saveDocumentSchema = z.object({
|
export const saveDocumentSchema = z.object({
|
||||||
saveVersion: z.literal(CURRENT_SAVE_VERSION),
|
saveVersion: z.literal(CURRENT_SAVE_VERSION),
|
||||||
|
|
@ -180,6 +180,8 @@ export const saveDocumentSchema = z.object({
|
||||||
tutorialRunsCompleted: z.number().int().nonnegative(),
|
tutorialRunsCompleted: z.number().int().nonnegative(),
|
||||||
dealAccepted: z.boolean(),
|
dealAccepted: z.boolean(),
|
||||||
createdAt: timeSchema,
|
createdAt: timeSchema,
|
||||||
|
/** This company's welcome cards have been seen (decision 0.5.131). */
|
||||||
|
welcomeSeen: z.boolean(),
|
||||||
}),
|
}),
|
||||||
company: z.object({
|
company: z.object({
|
||||||
id: idSchema,
|
id: idSchema,
|
||||||
|
|
@ -327,12 +329,21 @@ function catchUpState(save: Doc): Doc {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Version 6 → 7 (CR-022 §3): companies from before the welcome cards count as
|
||||||
|
* having seen them, so nobody gets them mid-game.
|
||||||
|
*/
|
||||||
|
function welcomeSeen(save: Doc): Doc {
|
||||||
|
return { ...save, player: { ...(save.player as Doc), welcomeSeen: true } };
|
||||||
|
}
|
||||||
|
|
||||||
export const SAVE_MIGRATIONS: readonly SaveMigration[] = [
|
export const SAVE_MIGRATIONS: readonly SaveMigration[] = [
|
||||||
{ from: 1, migrate: addFuelTanks },
|
{ from: 1, migrate: addFuelTanks },
|
||||||
{ from: 2, migrate: manualRefueling },
|
{ from: 2, migrate: manualRefueling },
|
||||||
{ from: 3, migrate: dropBoards },
|
{ from: 3, migrate: dropBoards },
|
||||||
{ from: 4, migrate: independentPilots },
|
{ from: 4, migrate: independentPilots },
|
||||||
{ from: 5, migrate: catchUpState },
|
{ from: 5, migrate: catchUpState },
|
||||||
|
{ from: 6, migrate: welcomeSeen },
|
||||||
];
|
];
|
||||||
|
|
||||||
export type SaveParse = { ok: true; save: SaveDocument } | { ok: false; error: string };
|
export type SaveParse = { ok: true; save: SaveDocument } | { ok: false; error: string };
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ export function fullSave(): SaveDocument {
|
||||||
seed: 42,
|
seed: 42,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
saveVersion: 6,
|
saveVersion: 7,
|
||||||
saveId: "save-1",
|
saveId: "save-1",
|
||||||
lastTimeSeen: T + 3_600_000,
|
lastTimeSeen: T + 3_600_000,
|
||||||
player: {
|
player: {
|
||||||
|
|
@ -55,6 +55,7 @@ export function fullSave(): SaveDocument {
|
||||||
tutorialRunsCompleted: 4,
|
tutorialRunsCompleted: 4,
|
||||||
dealAccepted: true,
|
dealAccepted: true,
|
||||||
createdAt: T,
|
createdAt: T,
|
||||||
|
welcomeSeen: true,
|
||||||
},
|
},
|
||||||
company: {
|
company: {
|
||||||
id: "co1",
|
id: "co1",
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import { fullSave } from "./save-fixture";
|
||||||
describe("save document (Step 14.2)", () => {
|
describe("save document (Step 14.2)", () => {
|
||||||
it("validates a fully populated save", () => {
|
it("validates a fully populated save", () => {
|
||||||
expect(saveDocumentSchema.safeParse(fullSave()).success).toBe(true);
|
expect(saveDocumentSchema.safeParse(fullSave()).success).toBe(true);
|
||||||
expect(CURRENT_SAVE_VERSION).toBe(6);
|
expect(CURRENT_SAVE_VERSION).toBe(7);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects an invalid save with a readable error naming the field", () => {
|
it("rejects an invalid save with a readable error naming the field", () => {
|
||||||
|
|
@ -84,7 +84,7 @@ describe("save migration 1 → 2: fuel tanks (CR-015 §5)", () => {
|
||||||
if (!result.ok) throw new Error(result.error);
|
if (!result.ok) throw new Error(result.error);
|
||||||
const { save } = result;
|
const { save } = result;
|
||||||
const before = fullSave();
|
const before = fullSave();
|
||||||
expect(save.saveVersion).toBe(6);
|
expect(save.saveVersion).toBe(7);
|
||||||
// The Courier has Extended Fuel Tanks 1 (40 fu × 4.8) and is in flight: the base
|
// The Courier has Extended Fuel Tanks 1 (40 fu × 4.8) and is in flight: the base
|
||||||
// price, 5 cr/fu since CR-018 (decision 0.5.110).
|
// price, 5 cr/fu since CR-018 (decision 0.5.110).
|
||||||
expect(save.ships[0]).toMatchObject({ fuelFu: 192, fuelAvgPrice: 5, refuelPolicy: "manual" });
|
expect(save.ships[0]).toMatchObject({ fuelFu: 192, fuelAvgPrice: 5, refuelPolicy: "manual" });
|
||||||
|
|
@ -116,7 +116,7 @@ describe("save migration 2 → 3: Manual refueling (CR-018 §3)", () => {
|
||||||
const result = migrateSave(old);
|
const result = migrateSave(old);
|
||||||
if (!result.ok) throw new Error(result.error);
|
if (!result.ok) throw new Error(result.error);
|
||||||
const { save } = result;
|
const { save } = result;
|
||||||
expect(save.saveVersion).toBe(6);
|
expect(save.saveVersion).toBe(7);
|
||||||
expect(save.ships.map((s) => s.refuelPolicy)).toEqual(save.ships.map(() => "manual"));
|
expect(save.ships.map((s) => s.refuelPolicy)).toEqual(save.ships.map(() => "manual"));
|
||||||
expect(save.ships.map((s) => [s.fuelFu, s.fuelAvgPrice])).toEqual(
|
expect(save.ships.map((s) => [s.fuelFu, s.fuelAvgPrice])).toEqual(
|
||||||
before.ships.map((s) => [s.fuelFu, s.fuelAvgPrice]),
|
before.ships.map((s) => [s.fuelFu, s.fuelAvgPrice]),
|
||||||
|
|
@ -142,7 +142,7 @@ describe("save migration 3 → 4: completable boards (CR-019 §3)", () => {
|
||||||
for (const board of old.contractBoards) delete board.fleet;
|
for (const board of old.contractBoards) delete board.fleet;
|
||||||
const result = migrateSave(old);
|
const result = migrateSave(old);
|
||||||
if (!result.ok) throw new Error(result.error);
|
if (!result.ok) throw new Error(result.error);
|
||||||
expect(result.save.saveVersion).toBe(6);
|
expect(result.save.saveVersion).toBe(7);
|
||||||
expect(result.save.contractBoards).toEqual([]);
|
expect(result.save.contractBoards).toEqual([]);
|
||||||
expect({ ...result.save, contractBoards: [] }).toEqual({ ...before, contractBoards: [] });
|
expect({ ...result.save, contractBoards: [] }).toEqual({ ...before, contractBoards: [] });
|
||||||
});
|
});
|
||||||
|
|
@ -164,7 +164,7 @@ describe("save migration 4 → 5: independent pilots (CR-021 §3)", () => {
|
||||||
}
|
}
|
||||||
const result = migrateSave(old);
|
const result = migrateSave(old);
|
||||||
if (!result.ok) throw new Error(result.error);
|
if (!result.ok) throw new Error(result.error);
|
||||||
expect(result.save.saveVersion).toBe(6);
|
expect(result.save.saveVersion).toBe(7);
|
||||||
expect(result.save.company).toMatchObject({ dispatchOffice: false, npcSpendingFloor: 500 });
|
expect(result.save.company).toMatchObject({ dispatchOffice: false, npcSpendingFloor: 500 });
|
||||||
for (const pilot of result.save.pilots) {
|
for (const pilot of result.save.pilots) {
|
||||||
expect(pilot.mode).toBe("manual");
|
expect(pilot.mode).toBe("manual");
|
||||||
|
|
@ -180,3 +180,19 @@ describe("save migration 4 → 5: independent pilots (CR-021 §3)", () => {
|
||||||
expect(result.save).toEqual(before);
|
expect(result.save).toEqual(before);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("save migration 6 → 7: welcome cards (CR-022 §3)", () => {
|
||||||
|
it("marks an existing company's welcome as seen and changes nothing else", () => {
|
||||||
|
const before = fullSave();
|
||||||
|
const old = JSON.parse(JSON.stringify(before)) as Record<string, unknown> & {
|
||||||
|
player: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
old.saveVersion = 6;
|
||||||
|
delete old.player.welcomeSeen;
|
||||||
|
const result = migrateSave(old);
|
||||||
|
if (!result.ok) throw new Error(result.error);
|
||||||
|
expect(result.save.saveVersion).toBe(7);
|
||||||
|
expect(result.save.player.welcomeSeen).toBe(true);
|
||||||
|
expect(result.save).toEqual(before);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue