Compare commits
2 Commits
0cb8e364c6
...
7355f7dbfb
| Author | SHA1 | Date |
|---|---|---|
|
|
7355f7dbfb | |
|
|
8507fa7c1e |
|
|
@ -1741,3 +1741,12 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md`
|
|||
## Step 21.9
|
||||
|
||||
- **The game design document is left to the designer** (CR-021 §6), as with CR-016.
|
||||
|
||||
## 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.
|
||||
|
||||
## 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");
|
||||
// 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 version: 6");
|
||||
expect(text).toContain("Save version: 7");
|
||||
expect(text).toContain("Storage: unknown");
|
||||
expect(text).not.toContain("Player:");
|
||||
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)
|
||||
|
||||
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.
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,85 @@
|
|||
# Change Request CR-022 — Welcome Screens
|
||||
|
||||
> **Numbering note (added when recorded):** this request arrived titled "CR-020 — Welcome Screens" for "Phase 20". CR-020 (Bridge dashboard) and CR-021 (Independent Pilots), and Phases 20 and 21, already existed, so it is stored as **CR-022** and done as **Phase 22**: every "20.n" below means **22.n**. Its references map to this repo as: "Phase 13 and CR-001 through CR-019" = Phase 13 and **CR-001 to CR-021**; "CR-001 faster start" (the tutorial intro card, decision 0.5.36's one-time tip `tutorial-intro`) is the same number; "CR-008 settings" = **CR-011** (Settings → Help, "Show tips again", reduced motion, "Start a new company", decision 0.5.74); "CR-011 local-first" = **CR-014**. Its new decisions CR-020.D1–D2 are recorded as **0.5.131 Welcome on every new company** and **0.5.132 How to play**.
|
||||
>
|
||||
> **On recording:** the plan has no "first dispatch under 30 seconds" target from Phase 12. Step 22.4 adds that check (see DECISIONS, Step 22.4). The tutorial board is the Contracts screen during the tutorial.
|
||||
|
||||
**Project:** Deadhead: Space Freight Tycoon
|
||||
**Applies to:** a build with Phase 13 and CR-001 through CR-019 complete
|
||||
**Do this:** as **Phase 20**
|
||||
**Related docs:** `change-request-faster-start.md` (CR-001), `change-request-settings.md` (CR-008), `change-request-local-first.md` (CR-011), `DECISIONS.md`
|
||||
|
||||
This change request is written for the local-first engine (CR-011).
|
||||
|
||||
---
|
||||
|
||||
## 1. Why
|
||||
|
||||
A new player goes straight from "Start your company" to the tutorial contract board without being told what kind of game this is. A short welcome explains the idea in a few seconds, then hands off to the existing tutorial.
|
||||
|
||||
## 2. The welcome
|
||||
|
||||
Three short cards in one pop-up, swiped or tapped through with **Next**, with **Skip** on every card. The last card's button starts the game.
|
||||
|
||||
**Card 1 — What this is**
|
||||
> **Welcome to Deadhead**
|
||||
> A space trucking tycoon. Haul freight across the solar system, grow from a hired pilot into a shipping company, and never fly home empty.
|
||||
|
||||
**Card 2 — How it plays**
|
||||
> **Your ships keep flying**
|
||||
> 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.
|
||||
|
||||
**Card 3 — Where you start**
|
||||
> **First, earn your wings**
|
||||
> You're flying for Halvorsen Lunar Freight on the Earth–Moon run. Do good work, and the old man might have an offer for you.
|
||||
>
|
||||
> [Let's fly]
|
||||
|
||||
### Rules
|
||||
| Rule | Detail |
|
||||
|---|---|
|
||||
| When it shows | Right after "Start your company" creates a new company, and after "Start a new company" in Settings (CR-008). Every new company, not just the first. |
|
||||
| Order | Welcome first. When it closes, the CR-001 tutorial intro card on the Contracts screen shows as before. |
|
||||
| After closing | Skip and "Let's fly" both close it and go to the Contracts screen. |
|
||||
| Reopen | Settings → Help gets **"How to play"**, which opens the same three cards (the last button reads "Close" instead of "Let's fly"). |
|
||||
| Saved state | The save records that the welcome was seen for this company, so it doesn't reappear after a reload mid-tutorial. |
|
||||
| Motion | Card transitions slide; with reduced motion on (CR-008), they cross-fade instantly. |
|
||||
| Accessibility | Focus moves into the pop-up and stays there until it closes; Next, Skip, and the final button are reachable by keyboard and screen reader; card position is announced ("1 of 3"). |
|
||||
|
||||
All text lives in content so it can be edited without code changes. "Halvorsen Lunar Freight" comes from the existing content name.
|
||||
|
||||
## 3. Rules for this change
|
||||
|
||||
- All rules in `implementation-plan.md` §0.1 apply: steps in order, one commit per step, each step's test must pass.
|
||||
- Save migration: existing companies are marked as having seen the welcome, so current players don't get it mid-game.
|
||||
- Update `progress.md` after every step. Stop for the user's review at the end of Phase 20.
|
||||
|
||||
## 4. Decisions
|
||||
|
||||
Record these in `DECISIONS.md` and `implementation-plan.md` §0.5, using the next free numbers (labeled CR-020.D1–D2 here; note the mapping).
|
||||
|
||||
- **CR-020.D1 Welcome on every new company.** Three cards per §2, shown once per company, before the tutorial intro card.
|
||||
- **CR-020.D2 How to play.** Reopenable from Settings → Help.
|
||||
|
||||
## 5. Steps — Phase 20
|
||||
|
||||
### Step 20.1 — Content and save field
|
||||
- Add the three cards' titles, body text, and button labels to content. Add a "welcome seen" field to the save, set to false for new companies; save migration sets it to true for existing ones.
|
||||
- **Test:** Content validates. A new company has "welcome seen" false; an older save migrates with it true.
|
||||
|
||||
### Step 20.2 — Client: welcome pop-up
|
||||
- Build the three-card pop-up per §2: Next, Skip, card position, final "Let's fly", reduced-motion behavior, and focus handling. Closing it marks the welcome seen and goes to Contracts, where the CR-001 intro card appears.
|
||||
- **Test:** Component tests: after starting a company, card 1 shows; Next advances to 2 and 3; Skip on any card closes it; "Let's fly" closes it; both mark it seen and land on Contracts with the intro card. Reloading after closing doesn't show it again. With reduced motion on, no slide animation runs. Focus stays inside the pop-up until it closes.
|
||||
|
||||
### Step 20.3 — Client: new company and "How to play"
|
||||
- "Start a new company" (Settings) shows the welcome for the new company.
|
||||
- Settings → Help adds "How to play", which opens the cards with a final "Close" button and doesn't change the saved flag.
|
||||
- **Test:** Component tests: starting a new company from Settings shows card 1; "How to play" opens the cards anytime and its last button reads "Close".
|
||||
|
||||
### Step 20.4 — Regression
|
||||
- Run the full suite.
|
||||
- **Test:** CI is green. The Phase 12 "first dispatch under 30 seconds" target still holds when the simulated player skips the welcome. Manual check: start a new company on a phone and read through the three cards.
|
||||
|
||||
## 6. Out of scope
|
||||
|
||||
Illustrations or animation on the cards, a longer interactive tutorial, voice-over, and anything else in `implementation-plan.md` §0.3.
|
||||
|
|
@ -186,6 +186,8 @@ These answer the pre-build review. Log each one in `DECISIONS.md` as it is imple
|
|||
128. **Idle behavior** (CR-021). With nothing to take, a pilot waits for the next board period; after 3 empty periods in a row away from their home HQ they deadhead home when they can.
|
||||
129. **Deterministic catch-up** (CR-021). On open or resume the engine replays events since the last saved time in time order (ties by pilot id), at most 7 days; the result doesn't depend on how the time is split.
|
||||
130. **No independent-trip notifications** (CR-021). Independent trips are decided on the device as time is replayed, so none are scheduled as notifications.
|
||||
131. **Welcome on every new company** (CR-022). Three welcome cards (what the game is, how it plays, where you start), with text in content, shown once per company right after it's created ("Start your company" or "Start a new company"), before the CR-001 tutorial intro card. The save records that this company's welcome was seen; saves from before CR-022 count as seen.
|
||||
132. **How to play** (CR-022). Settings → Help reopens the three cards any time, ending in "Close"; it doesn't change the saved flag.
|
||||
|
||||
### 0.6 Translating change requests written for the server (CR-014 §7)
|
||||
|
||||
|
|
@ -873,6 +875,12 @@ Steps 21.1–21.9 are specified in `memory-bank/change-requests/CR-021-independe
|
|||
|
||||
---
|
||||
|
||||
## Phase 22 — Welcome Screens (CR-022)
|
||||
|
||||
Steps 22.1–22.4 are specified in `memory-bank/change-requests/CR-022-welcome-screens.md` §5 (written there as 20.1–20.4). Stop for review at the end.
|
||||
|
||||
---
|
||||
|
||||
## Definition of Done (Base Game)
|
||||
|
||||
- All step tests pass in CI.
|
||||
|
|
|
|||
|
|
@ -484,3 +484,12 @@ Spec: `memory-bank/change-requests/CR-021-independent-pilots.md` (received as "C
|
|||
- [x] **Step 21.9 — Plan amendments and regression.** CR-021 §6 applied: plan 0.3 (automation struck; standing orders, pilot traits, decision events and a hiring market stay deferred) and 0.5 (0.5.122–0.5.130); a note on CR-001's goals (a Dispatch Office goal later, not now); CR-014 Step 14.6 (the loop is the event-queue catch-up); CR-019 (pilots choose from completable boards); plan Step 12.1 (the away scenario). `game-design-document.md` is the designer's to update. Full suite: typecheck, lint, 1399 unit tests and 9 e2e tests pass.
|
||||
|
||||
**Phase 21 is complete; stopped for review.** Manual check for the user: buy the Dispatch Office on the Bridge, set Rosa to Independent with default orders, close the app for an hour, reopen and read her logbook; then confirm a first-contact job stayed on the board for you.
|
||||
|
||||
## Phase 22 — Welcome Screens (CR-022)
|
||||
|
||||
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.
|
||||
|
||||
- [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.3 — Client: new company and "How to play"
|
||||
- [ ] Step 22.4 — Regression
|
||||
|
|
|
|||
|
|
@ -3,7 +3,13 @@ import { gameConfig } from "./data/config";
|
|||
import { dealLoanAmount } from "./deal";
|
||||
import { goods } from "./data/goods";
|
||||
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 { upgrades } from "./data/upgrades";
|
||||
|
||||
|
|
@ -26,6 +32,9 @@ export const content = {
|
|||
scriptedTutorialContracts,
|
||||
onboardingGoals,
|
||||
veteranPilot,
|
||||
/** The tutorial employer (CR-001) and the welcome cards (CR-022). */
|
||||
tutorialCompanyName,
|
||||
welcome,
|
||||
upgrades,
|
||||
/** The deal's loan: its ships' prices plus the bay lease (decision 0.5.83). */
|
||||
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). */
|
||||
export const scriptedTutorialContracts: ScriptedTutorialContract[] = [
|
||||
|
|
|
|||
|
|
@ -417,3 +417,18 @@ export const veteranPilotSchema = z.object({
|
|||
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>;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
|||
import {
|
||||
content,
|
||||
onboardingGoalSchema,
|
||||
welcomeSchema,
|
||||
onboardingGoals,
|
||||
scriptedTutorialContractSchema,
|
||||
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,
|
||||
dealAccepted: false,
|
||||
createdAt: now,
|
||||
welcomeSeen: false,
|
||||
},
|
||||
company: {
|
||||
id: random.id(),
|
||||
|
|
|
|||
|
|
@ -95,3 +95,14 @@ describe("company creation on the device (Step 14.8)", () => {
|
|||
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. */
|
||||
export const CURRENT_SAVE_VERSION = 6;
|
||||
export const CURRENT_SAVE_VERSION = 7;
|
||||
|
||||
export const saveDocumentSchema = z.object({
|
||||
saveVersion: z.literal(CURRENT_SAVE_VERSION),
|
||||
|
|
@ -180,6 +180,8 @@ export const saveDocumentSchema = z.object({
|
|||
tutorialRunsCompleted: z.number().int().nonnegative(),
|
||||
dealAccepted: z.boolean(),
|
||||
createdAt: timeSchema,
|
||||
/** This company's welcome cards have been seen (decision 0.5.131). */
|
||||
welcomeSeen: z.boolean(),
|
||||
}),
|
||||
company: z.object({
|
||||
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[] = [
|
||||
{ from: 1, migrate: addFuelTanks },
|
||||
{ from: 2, migrate: manualRefueling },
|
||||
{ from: 3, migrate: dropBoards },
|
||||
{ from: 4, migrate: independentPilots },
|
||||
{ from: 5, migrate: catchUpState },
|
||||
{ from: 6, migrate: welcomeSeen },
|
||||
];
|
||||
|
||||
export type SaveParse = { ok: true; save: SaveDocument } | { ok: false; error: string };
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ export function fullSave(): SaveDocument {
|
|||
seed: 42,
|
||||
});
|
||||
return {
|
||||
saveVersion: 6,
|
||||
saveVersion: 7,
|
||||
saveId: "save-1",
|
||||
lastTimeSeen: T + 3_600_000,
|
||||
player: {
|
||||
|
|
@ -55,6 +55,7 @@ export function fullSave(): SaveDocument {
|
|||
tutorialRunsCompleted: 4,
|
||||
dealAccepted: true,
|
||||
createdAt: T,
|
||||
welcomeSeen: true,
|
||||
},
|
||||
company: {
|
||||
id: "co1",
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { fullSave } from "./save-fixture";
|
|||
describe("save document (Step 14.2)", () => {
|
||||
it("validates a fully populated save", () => {
|
||||
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", () => {
|
||||
|
|
@ -84,7 +84,7 @@ describe("save migration 1 → 2: fuel tanks (CR-015 §5)", () => {
|
|||
if (!result.ok) throw new Error(result.error);
|
||||
const { save } = result;
|
||||
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
|
||||
// price, 5 cr/fu since CR-018 (decision 0.5.110).
|
||||
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);
|
||||
if (!result.ok) throw new Error(result.error);
|
||||
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.fuelFu, s.fuelAvgPrice])).toEqual(
|
||||
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;
|
||||
const result = migrateSave(old);
|
||||
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({ ...before, contractBoards: [] });
|
||||
});
|
||||
|
|
@ -164,7 +164,7 @@ describe("save migration 4 → 5: independent pilots (CR-021 §3)", () => {
|
|||
}
|
||||
const result = migrateSave(old);
|
||||
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 });
|
||||
for (const pilot of result.save.pilots) {
|
||||
expect(pilot.mode).toBe("manual");
|
||||
|
|
@ -180,3 +180,19 @@ describe("save migration 4 → 5: independent pilots (CR-021 §3)", () => {
|
|||
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