ShippedMedium

Local save/load via localStorage: checkpoint autosave, New Game/Continue screen, versioned corruption-safe saves

Proposed by Jonathan Miller 1 hour agoFunding opened 1 hour agoFunded 1 hour agoShipped 1 hour ago
Acceptance · round 1
Shipped
CINo checks
Automated reviewPass with concerns

This is a clean, well-scoped implementation: a versioned envelope with a `migrate` hook and field-by-field validation, storage wrappers that never throw and back up bad data to the corrupt key, checkpoint hooks wired into the existing `Game` mutation helpers, and a minimal start screen with a confirm dialog. The tests map closely to every acceptance criterion, and the Results-checkpoint choice (persisting `lastResult`) is documented. The main reservation is that no CI ran and nothing was tried in a real browser, so the build and typecheck claims are unverified. There is also a minor risk that input reaching the game behind the start screen could autosave over an existing run.

Acceptance criteria · 9 of 10 met
  • YESRound-trip serialize → deserialize of a non-trivial run (multiple rooms, monsters, traps, gold, raid > 1, an unlock) yields deeply equal state`tests/save.test.ts` 'round-trips a non-trivial run' builds 2 rooms on top of the starter rooms, 3 goblins, 2 traps, runs 2 raids, pushes an unlock, and asserts `toEqual` after decode and after `restore`.
  • YESStored envelope has numeric version equal to SAVE_VERSION and an ISO savedAt string'stores an envelope with numeric version and ISO savedAt' checks typeof, equality with SAVE_VERSION and that `toISOString` round-trips.
  • YESMalformed JSON, missing version, unknown version and missing required fields each return a handled error without throwing and write raw data to the corrupt keyThe parameterised cases in `tests/save.test.ts` cover all four (plus wrong-typed fields and a saved Raid phase), asserting not.toThrow, the status, and `CORRUPT_SAVE_KEY === raw`.
  • YESA mocked localStorage.setItem that throws does not make save throw'does not throw when setItem throws' stubs a QuotaExceededError and asserts no throw, a false return and a console.warn.
  • YESUnavailable localStorage (getter throws or undefined) does not make load or save throwSeparate tests cover a throwing getter, `undefined`, and a throwing getItem, each returning 'unavailable' or false.
  • YESAutosave runs on Preparation commits (build, hire, place, sell), on entering Results and on leaving Results`Game.checkpoint()` is called from `dungeonChanged()`, `finishRaid()` and `continueToPreparation()`, and `tests/checkpoints.test.ts` verifies build/hire/place/remove plus both Results transitions; 'sell' maps to `removeRoom` because no sell action exists.
  • YESNo save call in the raid update loop or any per-frame pathRaid ticks do not checkpoint, a test asserts zero saves while phase is Raid, and `snapshot()` throws during a Raid; only the one-time `finishRaid` transition saves.
  • YESStart screen shows Continue only with a valid save; Continue restores the saved phase; a mid-raid refresh restores pre-raid Preparation`StartScreen.showMenu` branches on `load.status`, and the start-screen tests cover the no-save, Continue-into-Results and corrupt cases, while the checkpoints test covers the pre-raid Preparation restore.
  • YESNew Game with an existing save requires confirmation, and Cancel leaves the stored save byte-identical`showConfirm` shows the 'will be overwritten' text, and the test compares `localStorage.getItem(SAVE_KEY)` before and after Cancel.
  • UNCLEARExisting tests, lint/typecheck and vite build pass; no backend, network or account codeNo network or backend code is added, but no CI ran, so passing tests, typecheck and build rest only on the builder's claim.
Concerns
  • No CI ran on this commit, so the claimed passing tests, typecheck and `vite build` are unverified. The builder also says the game was never opened in a real browser.
  • In `main.ts` the live `Game` already exists and `onCheckpoint(saveRun)` is registered while the start screen is still open. Any build action that reaches the game behind the overlay (e.g. a HUD keyboard shortcut, if one exists) would checkpoint a fresh run over the existing save before the player picks Continue or New Game. The overlay blocks pointer clicks, but nothing stops keyboard input or disables the HUD.
  • 'Sell' is mapped to `removeRoom` and 'progression/unlocks' is a new `unlocks: string[]` field that nothing writes to. Both are reasonable, documented gap-fills, but the round-trip test's unlock entry is pushed by hand rather than produced by gameplay.
  • The new devDependency `happy-dom` pulls in `ws`, `@types/node` and other transitive packages. These are dev-only and acceptable, but they are an addition backers may want to know about.
  • The tests' `afterEach` calls `Reflect.deleteProperty(globalThis, 'localStorage')`. On Node versions that ship a built-in `localStorage`, this global mutation could interact oddly with other test files.
CI details
No CI checks ran on this commit.

BackersAccepted
1 accept · 0 rebuild · 0 not voted · quorum 1 of 1
MaintainerMerge

Accepted by the backers and merged by the maintainer.

Ballots · 1
AcceptJonathan Miller

Automated review cost $0.16, counted as builder cost.

Motivation

A page refresh currently loses the run. Dungeon Company is meant to grow over many raid cycles, so it needs reliable local persistence and a New Game / Continue flow.

Scope

  1. Save module in src/save/ that serializes and deserializes all state needed to rebuild a run:
    • dungeon layout (rooms)
    • gold and economy
    • hired monsters
    • placed traps
    • progression and unlocks
    • raid number
    • current phase (only Preparation or Results is ever saved)
    • any other state required to reconstruct the run
    • Serialization must use plain JSON-safe data, not Phaser objects. If run state is currently spread across scenes, the agent may introduce a small serializable run-state accessor. A broader refactor is not in scope.
  2. Storage: localStorage under the single key dungeon-company:save. Assumption: saves are small, so IndexedDB is not needed.
  3. Envelope:
    • Format: { version: number, savedAt: ISO-8601 string, state: {...} }.
    • Export a SAVE_VERSION constant.
    • Provide a migrate(data) hook that currently only accepts the current version and rejects anything else as incompatible.
  4. Autosave checkpoints (synchronous, never per-frame):
    • after each committed Preparation change: build, hire, place, sell
    • on entering Results
    • on leaving Results into the next Preparation
    • No save during an active raid. A refresh mid-raid restores the most recent Preparation checkpoint, which is the state just before that raid started.
    • Hero positions, timers, transient combat state and RNG state are not persisted.
    • Assumption: if the Results screen cannot be rebuilt from the saved state (e.g. it needs a transient raid summary), persist the minimal summary it needs. If that is impractical, a Results checkpoint may restore into the following Preparation phase instead. Document whichever choice is made in the PR.
  5. Start flow:
    • On load, if a valid save exists, show Continue and New Game. Otherwise show only New Game.
    • If no start screen exists, create a minimal one styled consistently with the current UI.
    • Do not redesign menus or the title screen.
    • Continue restores the run into the saved phase.
  6. New Game confirmation:
    • When a save exists, New Game shows a confirm dialog stating the current run will be overwritten.
    • Confirm starts a fresh run, and the old save is overwritten at the first checkpoint or immediately.
    • Cancel leaves the save untouched and returns to the start screen.
  7. Corrupt or incompatible data:
    • Covers JSON parse failure, missing or unknown version, and failed schema validation (missing or wrong-typed required fields).
    • Load returns a handled error result and never throws.
    • The raw string is copied to dungeon-company:save:corrupt before anything overwrites it.
    • The start screen shows a short message with a Start New Game option. Continue is not shown.
  8. Storage errors:
    • If localStorage is unavailable or a write throws (e.g. quota exceeded), log a console.warn.
    • Save and load return gracefully, and the game remains playable without persistence.
  9. RNG: no seeded RNG or deterministic replay is required. Future RNG persistence would arrive via a schema version bump.

Acceptance criteria

  • Unit test: round-trip serialize → deserialize of a non-trivial run yields deeply equal state. The run includes multiple rooms, multiple monsters, multiple traps, gold, raid > 1 and at least one progression or unlock entry.
  • Unit test: the stored envelope has a numeric version equal to SAVE_VERSION and an ISO savedAt string.
  • Unit tests, each returning a handled error result without throwing and writing the raw data to dungeon-company:save:corrupt:
    • malformed JSON
    • missing version
    • unknown version
    • missing required fields
  • Unit test: a mocked localStorage.setItem that throws does not cause the save call to throw.
  • Unit test: unavailable localStorage (getter throws or is undefined) does not cause load or save to throw.
  • Autosave runs on Preparation commits (build, hire, place, sell), on entering Results and on leaving Results. This is verified by a test or by explicit calls in the commit and phase-transition code.
  • There is no save call in the raid update loop or any per-frame path.
  • The start screen shows Continue only when a valid save exists. Continue restores the run into the saved phase, and a mid-raid refresh restores the pre-raid Preparation state.
  • New Game with an existing save requires confirmation, and Cancel leaves the stored save byte-identical.
  • Existing tests, lint/typecheck (if configured) and vite build pass. No backend, network or account code is added.

Out of scope

  • cloud, account or cross-device sync
  • multiple save slots
  • import/export of save files
  • multiplayer state
  • resuming mid-raid simulation state
  • seeded or deterministic RNG
  • real migrations beyond the version hook
  • a title or menu redesign

No comments yet.