e5d449095d
- 02-05-letter-settings-e2e-SUMMARY.md: full plan summary (frontmatter + decisions + REQ table + self-check). All 24 Phase-2 REQ-IDs structurally satisfied across the 5-plan set. - STATE.md: marked Plan 02-05 complete; Phase 2 ready for /gsd-verify-work; progress 19% → 22%; next action set to verifier. - ROADMAP.md: Plan 02-05 row marked [x] with duration + SUMMARY ref. - REQUIREMENTS.md: UX-02 / UX-10 / CORE-03 / PIPE-07 marked complete with traceability annotations citing Plan 02-05's contribution; per-row Plan 02-05 references added to UX-02, UX-10, CORE-03; PIPE-07 traceability table row updated.
299 lines
39 KiB
Markdown
299 lines
39 KiB
Markdown
# Requirements: The Last Garden
|
||
|
||
**Defined:** 2026-05-08
|
||
**Core Value:** Every idle mechanic must function as a metaphor that the player absorbs without being told. When economy and meaning conflict, meaning wins.
|
||
|
||
## v1 Requirements
|
||
|
||
Requirements for initial release. Each maps to roadmap phases. All are user-centric, testable, atomic.
|
||
|
||
### CORE — Engine, Simulation, Save Persistence
|
||
|
||
- [x] **CORE-01**: Player loads the game in a modern browser (Chrome, Firefox, Safari, Edge — last 2 stable releases) and reaches a playable state in under 5 seconds on a 25 Mbps connection. <!-- Plan 01-01: scaffold builds (`npm run build` green); end-to-end <5s wall-clock measurement is Phase 2 PIPE-07. -->
|
||
|
||
- [x] **CORE-02**: Game runs a deterministic, fixed-timestep simulation that advances by elapsed real time (not `setInterval` ticks), so a player who switches tabs or sleeps their device returns to a correctly-advanced garden. <!-- Plan 02-01: drainTicks fixed-timestep accumulator + Clock injection contract; TICK_MS=200 (5Hz); 7 scheduler tests green. ESLint sim-purity rule (D-33) bans setInterval inside src/sim/**. Scene-driven tick wiring + visibility-pause is Plan 02-02. -->
|
||
- [x] **CORE-03**: Player who closes the game and returns finds the garden has progressed by the elapsed time (capped at 24 hours) — *no progression resumes from a stale snapshot*. <!-- Plan 02-01: drainTicks clamps at MAX_OFFLINE_MS=24h; computeOfflineCatchup reports hitOfflineCap=true on excess; both paths covered by Vitest. Plan 02-05: src/PhaserGame.tsx boot path threads computeOfflineCatchup → drainTicks(silent=true) → autoHarvestReadyPlants → letter overlay opens at ≥5min absence; auto-harvest accumulates plantsBloomedCount + harvestedFragmentIds + luraBeatPending into the OfflineEventBlock the letter Ink renders. PIPE-07 e2e exercises offline catchup structurally via FakeClock advance + reload. -->
|
||
- [x] **CORE-04**: Player's progress saves to IndexedDB (with localStorage fallback), surviving browser refresh, browser updates, and at least 30 days of inactivity on Chrome and Firefox. <!-- Plan 01-03: idb DB + LocalStorageDBAdapter fallback; 4 db tests green; round-trip test green. Settings UI surface is Phase 2. -->
|
||
- [x] **CORE-05**: Game requests persistent storage via `navigator.storage.persist()` on first save and surfaces the result respectfully if the browser declines. <!-- Plan 01-03: requestPersistence() all 4 API scenarios covered by Vitest; Settings UI surface is Phase 2. -->
|
||
- [x] **CORE-06**: Saves are versioned (`{schemaVersion, payload, checksum}`) and the game refuses to load a save with a checksum mismatch, presenting the player with a recovery option. <!-- Plan 01-03: wrap/unwrap + SaveCorruptError + CRC-32; 9 envelope tests green. -->
|
||
- [x] **CORE-07**: Game runs a `migrate_vN_to_vN+1` chain on load, so a save from any prior version of the game upgrades cleanly to the current shape (validated by Vitest tests for every shipped migration). <!-- Plan 01-03: forward-only registry with synthetic v0→v1; 6 migration tests green. -->
|
||
- [x] **CORE-08**: Game keeps the last 3 pre-migration save snapshots and offers the player a "restore previous save" option in settings. <!-- Plan 01-03: RETAIN=3 enforced; 5-then-3 invariant test green. Settings UI surface is Phase 2. -->
|
||
- [x] **CORE-09**: Player can export their save as a Base64 text blob via Settings → Export and import it back into the same or a fresh browser via Settings → Import. <!-- Plan 01-03: exportToBase64/importFromBase64 + 50MB DoS cap; 3 round-trip tests green. Settings UI surface is Phase 2. -->
|
||
- [x] **CORE-10**: Game's simulation core (`src/sim/`) imports nothing from `src/render/` or `src/ui/` — enforced by ESLint boundary rules in CI. <!-- Plan 01-02: ESLint 9 flat config + boundaries/element-types rule + programmatic Vitest proof; lint exits 0. -->
|
||
- [x] **CORE-11**: Simulation refuses negative time deltas (system-clock cheat defense) and caps any single offline progression at 24 hours, regardless of wall-clock claim. <!-- Plan 02-01: drainTicks(state, accumulatorMs<0) returns the original state with ticksApplied=0; computeOfflineCatchup reports cappedMs=0 for negative deltas; 24h clamp shared with CORE-03; 5 catchup tests + 4 tick tests green. -->
|
||
|
||
### GARDEN — Planting, Growing, Harvesting
|
||
|
||
- [x] **GARD-01**: Player can plant a seed from their seed inventory into an unoccupied tile of the garden. <!-- Plan 02-02: sim/garden plantSeed command (D-05 unlock-gate + occupied silent no-op + immutability) + SeedPicker DOM popover + Garden scene pointerdown → store.enqueueCommand wiring; 14 commands tests + 6 SeedPicker tests green. -->
|
||
- [x] **GARD-02**: Each plant has a visible growth state (sprout → mature → ready-to-harvest) that updates from save data on load and advances over time. <!-- Plan 02-02: advanceGrowth pure function (sprout→mature@33%→ready@100%) + render/garden/plant-renderer primitives per stage + Garden scene appStore.subscribe drives reactive repaintPlants; 11 growth tests green. Save-load tickCount restore is in Garden.create(). -->
|
||
- [x] **GARD-03**: Player can harvest a mature plant to receive a memory fragment; harvesting empties the tile. <!-- Plan 02-03: sim/garden harvest() pure command — refuses immature plants, calls selectFragment() to pick exactly one fragment from the gated pool, empties the tile, appends to harvestedFragmentIds, recomputes unlockedPlantTypes (Pitfall 10 post-commit). Garden.ts handleTilePointerDown enqueues 'harvest' on a ready-stage plant click. 11 commands.test.ts cases (harvest + Pitfall 10 boundaries). -->
|
||
- [x] **GARD-04**: Player can compost an immature or unwanted plant, returning a portion of resources and triggering a tonal beat (acknowledging the choice to let go). <!-- Plan 02-03: sim/garden compost() pure command — empties tile regardless of growth stage, no fragment yield (D-07), no resource refund (D-04 = infinite seeds). Garden.ts handleTilePointerDown enqueues 'compost' on an immature plant click. content/dialogue/season1/compost-acknowledgements.ink ships 6 authored beat lines in voice; Plan 02-04 wires the inkjs runtime (TODO at the call site marks the wiring point). 5 commands.test.ts cases (compost). -->
|
||
|
||
- [ ] **GARD-05**: Player unlocks new plant types as they progress through Seasons, with each plant type having distinct growth time, harvest yield, and visual identity.
|
||
- [ ] **GARD-06**: Tree plantings (Season 3+) are slow and expensive but produce place-memory vignettes when harvested.
|
||
- [ ] **GARD-07**: Cross-pollination (Season 2+): adjacent compatible plants can produce hybrid seeds with mixed memory traits.
|
||
- [ ] **GARD-08**: Ecosystem planting (Season 5+): clusters of compatible species produce yield bonuses, encouraging the player to think in ecosystems rather than individual crops.
|
||
- [ ] **GARD-09**: Memory Storms (Season 4+): periodic events accelerate decay of unprotected plants, requiring the player to plant resilient species, build windbreaks, or time harvests around them.
|
||
- [ ] **GARD-10**: Player sees the garden as a Phaser 4 canvas with watercolor-styled, hand-painted-feeling plants (no generic fantasy flora).
|
||
|
||
### MEMORY — Fragments, Journal, Selection
|
||
|
||
- [x] **MEMR-01**: Each harvest yields exactly one memory fragment, drawn from the authored content pool gated by current Season and unlocked progression. <!-- Plan 02-03: harvest() calls selectFragment() exactly once per ready-stage harvest; result appended to harvestedFragmentIds. Pinned by selector.test.ts (16 cases) + commands.test.ts harvest cases. -->
|
||
- [x] **MEMR-02**: Memory fragments are authored in plain text (Markdown with frontmatter) in the project's `/content/` tree and compiled per-Season at build time. <!-- Plan 02-03: 14 yaml entries + 2 long-form Markdown fragments under /content/seasons/01-soil/; PIPE-01 enforced (build fails on schema violation). PIPE-02 lazy chunk surface from Plan 02-02 verified structurally by scripts/check-bundle-split.mjs. -->
|
||
- [x] **MEMR-03**: Each fragment has a stable string ID (e.g., `season3.canopy.lura_07.vignette`) — never numeric — so re-ordering or re-authoring does not break references. <!-- Plan 02-03: all 17 Season-1 fragment ids match /^season1\.[a-z0-9._-]+$/ (FragmentSchema regex enforced at module-eval). loader.test.ts has the numeric-id rejection case. -->
|
||
- [x] **MEMR-04**: Player can open a Memory Journal (React DOM panel) listing every fragment they have collected, organized by Season. <!-- Plan 02-03: src/ui/journal/Journal.tsx — full-screen modal (D-24) grouped by Season. JournalIcon corner affordance (D-23 first-harvest gate). 7 Vitest cases. -->
|
||
- [x] **MEMR-05**: Player can read any collected fragment in full at any time, including selecting and copying its text (DOM-based, not canvas-rendered). <!-- Plan 02-03: Journal + FragmentRevealModal both render fragment bodies inside <pre> with userSelect: 'text' (DOM, not canvas). Pinned by Journal.test.tsx + FragmentRevealModal.test.tsx assertions on computed style. -->
|
||
- [x] **MEMR-06**: Fragments are selected by a deterministic selector that respects authored gating rules (Season requirement, story-state requirement, player-progression requirement) and avoids duplicates within a single playthrough until the pool is exhausted. <!-- Plan 02-03: src/sim/memory/selector.ts — selectFragment() is pure, deterministic (mulberry32 PRNG seeded from sim state), respects Season + plant-type tonal-register gating + no-dup. Pitfall 8 exhaustion fallback via EXHAUSTION_FALLBACK_ID sentinel. 16 selector.test.ts cases. -->
|
||
|
||
- [ ] **MEMR-07**: Place-memory vignettes (Season 3+) deliver a fragment as a short interactive scene the player can walk through, not just a text block.
|
||
|
||
### STORY — Characters, Dialogue, Choice
|
||
|
||
- [x] **STRY-01**: Lura (the audience-surrogate carpenter from a Remembered town) appears at the garden gate during Season 1 and reacts to early fragments with text-message-cadence dialogue authored in Ink. <!-- Plan 02-04: 3 authored Ink beats (lura-arrival/mid/farewell.ink) under /content/dialogue/season1/, fired at 1st/4th/8th harvest (D-14); src/sim/narrative/lura-gate.ts gates on harvestedFragmentIds.length; src/ui/dialogue/LuraDialogue.tsx renders lines via inkjs Story + InkRenderer drip cadence (1500ms base + 20ms/char, capped 4000ms); render/garden/gate-renderer.ts visual cue. 17 sim tests + 13 dialogue tests green. -->
|
||
- [ ] **STRY-02**: Lura's dialogue continues across all 7 Seasons, contextualizes major story beats, and reflects player progression in Ink-driven branches tied to Zustand variables.
|
||
- [ ] **STRY-03**: The Nameless Man appears in Season 2, his dialogue progressively shortens and confuses across Seasons 2-4, and he vanishes mid-sentence in Season 4 with no fanfare or cutscene.
|
||
- [ ] **STRY-04**: The Archivist appears in Season 6, never gendered (they/them), speaks softly and reflectively, and asks the player a thematic question without forcing an answer.
|
||
- [ ] **STRY-05**: The Archivist responds (mechanically and tonally) when the player feeds the Loom a memory containing both joy and grief — the Loom holds the contradiction, ending the Unremembering's advance.
|
||
- [x] **STRY-06**: All authored dialogue uses Ink (`.ink` files) compiled to JSON for runtime via inkjs. <!-- Plan 02-04: scripts/compile-ink.mjs invokes the bundled inklecate binary at build time (BLOCKER 4 — uses node_modules/inklecate/bin); 4 .ink → .ink.json compiled deterministically; src/content/ink-loader.ts lazy-loads compiled JSON via import.meta.glob and instantiates inkjs.Story; npm run ci runs compile:ink before tests + before build. RESEARCH Assumption A6 verified first-try on Windows. -->
|
||
- [x] **STRY-07**: The Keeper (player character) has no name, no backstory, and no dialogue beyond the final binary choice in Season 7. <!-- Plan 02-04: vacuously satisfied for Phase 2 — zero Ink files contain Keeper-spoken lines. The Keeper is the player; only Lura speaks (and the gardener-keeper voice acknowledges in compost beats, but is never personified as a named character). Phase 7 (SEAS-09 / STRY-08) lands the binary choice surface. -->
|
||
- [ ] **STRY-08**: The final scene of Season 7 presents the player with a binary narrative choice (*"They help us remember"* / *"They help us grow"*); both endings display the line *"The garden persists."* and both are tonally complete; neither unlocks alternate post-credits content.
|
||
- [x] **STRY-09**: Every player-visible string is externalized in `/content/` (not hardcoded in TypeScript), so localization can be retrofitted in v2 without code refactor. <!-- Plan 01-04: /content/ convention established; no player-visible strings in Phase 1 source (vacuously satisfied); real enforcement lands Phase 2. -->
|
||
- [x] **STRY-10**: Story progression gates on tick count, not on wall time — players cannot fast-forward through authored beats by manipulating their system clock. <!-- Plan 02-04: src/sim/narrative/lura-gate.ts gates on state.harvestedFragmentIds.length (sim-internal counter), never wall time. The gate function takes only the harvest count as input — no clock parameter exists. The STRY-10 test case in lura-gate.test.ts advances FakeClock by 24 hours with zero harvests and confirms no beat fires. ESLint sim-purity rule (Block 3 of eslint.config.js) mechanically prevents Date.now/setInterval inside src/sim/narrative/. -->
|
||
|
||
### SEAS — Seasons, Prestige, Roothold
|
||
|
||
- [ ] **SEAS-01**: The game presents the seven Seasons (Soil, Roots, Canopy, Storm, Depth, Loom, Return) as an authored sequence; the player progresses through them in order.
|
||
- [ ] **SEAS-02**: Each Season ends with a die-off event (frost in Season 1, escalating tonally; pulled-edit-out-of-existence in Season 3) that wipes surface plantings and triggers a Season transition.
|
||
- [ ] **SEAS-03**: Roothold persists across every Season transition; it is the only number that can never decrease.
|
||
- [ ] **SEAS-04**: Roothold has a *finite* ceiling tied to the narrative argument; the curve flattens deliberately as the player approaches the end of the authored arc.
|
||
- [ ] **SEAS-05**: Each Season introduces *at most one* new mechanic (composting, cross-pollination, place-memory vignettes, Memory Storms, ecosystem planting, the Loom interface, Return-mode expansion) per the scope-defense doctrine.
|
||
- [ ] **SEAS-06**: Season transitions crossfade visual palette and audio bed (Howler.js), shifting from golden/autumnal (early) → deep green/storm (middle) → dawn/silver (late).
|
||
- [ ] **SEAS-07**: The Below (accessible Season 5+) lets the player grow root structures into ancient memory layers and deliver content from a pre-Archivist civilization.
|
||
- [ ] **SEAS-08**: Season 6 shifts the primary gameplay loop to the Below, where the player feeds memories to the Loom rather than harvesting fragments.
|
||
- [ ] **SEAS-09**: Season 7 ("Return") shifts to a long, satisfying late-game in which collected memories become seeds that automatically reconstitute the world; the Pale recedes; the Heartsoil expands beyond the garden walls.
|
||
- [ ] **SEAS-10**: When the player reaches the end of the authored arc, the game transitions to a credits/coda *rest* state — not infinite prestige tiers — that the player can return to indefinitely without grinding.
|
||
|
||
### AEST — Visual & Audio Aesthetic
|
||
|
||
- [ ] **AEST-01**: The garden renders in a watercolor-adjacent visual style (hand-painted textures, plants that look like real species made slightly wrong) on a Phaser 4 canvas with a watercolor post-process filter.
|
||
- [ ] **AEST-02**: The Pale renders as overexposed white silence — luminous, pearlescent, *too bright* — with a faint tinnitus-like high tone in audio.
|
||
- [ ] **AEST-03**: The main musical theme is a solo cello, looped and crossfaded across Seasons via Howler.js.
|
||
- [ ] **AEST-04**: Ambient garden sounds (wind, birdsong, the creak of a gate) thin and fade as the Unremembering draws closer to the player's region.
|
||
- [ ] **AEST-05**: Audio crossfades, never hard-cuts, between Seasons; the cello and ambient layers are independent buses with separate volume controls.
|
||
- [ ] **AEST-06**: Color palette shifts deliberately by Season — golden/autumnal → deep green/storm → dawn/silver.
|
||
- [x] **AEST-07**: The first screen of the game is a hand-painted "Tend the garden" / "Begin" gesture gate that satisfies the Web Audio user-gesture requirement and explicitly calls `AudioContext.resume()`. <!-- Plan 02-02: BeginScreen (D-21 typographic placeholder; Phase 3 swaps in painted treatment) + use-audio-bootstrap.ts (synchronous-inside-click bootstrapAudioContext defends Pitfall 5: iOS Safari construction-inside-gesture; webkitAudioContext fallback). 4 BeginScreen tests + first-interaction gesture handler one-shot for D-22 returning players. -->
|
||
- [x] **AEST-08**: All AI-assisted assets carry persisted provenance metadata (`{model_id, checkpoint_hash, prompt, seed, sampler, params}`) and are produced from a pinned model and a locked north-star reference set. <!-- Plan 01-05: Zod ProvenanceSchema (6 fields) + CI gate + 2 placeholder assets with valid sidecars; north-star reference set deferred to Phase 5 per IOU. -->
|
||
- [x] **AEST-09**: All shipped assets pass a mandatory human curation gate before integration; no asset reaches the production manifest unreviewed. <!-- Plan 01-05: gate mechanism in place (validator + sidecar schema); human curation recorded as explicit decision in 01-05-IOU.md (Path C); real north-star images Phase 5. -->
|
||
|
||
### UX — Onboarding, Settings, Accessibility, Return
|
||
|
||
- [x] **UX-01**: First-time player sees a single, painted "Begin" screen with no UI clutter; the garden reveals itself as the player interacts (A Dark Room rule). <!-- Plan 02-02: BeginScreen mounts as a fixed-position dialog covering the canvas with only title + subtitle + Begin CTA; no HUD, no journal, no settings. Dismissed via session.beginGateDismissed (D-22). Phase 3 paints the treatment. -->
|
||
|
||
- [x] **UX-02**: Player who returns after time away receives a "while you were away" *letter from the garden* — written in voice, not a stat dump — describing what grew, what bloomed, what the wind brought. <!-- Plan 02-05: content/dialogue/season1/letter-from-the-garden.ink authored skeleton (bible voice, anti-FOMO compliant, 24h cap silent in voice per D-11) + slot vocabulary plants_bloomed/fragment_titles/lura_was_here from offlineEvents block; src/ui/letter/Letter.tsx full-screen overlay (D-20: opens at ≥5min absence, dismisses on tap with Pitfall 9 audio bootstrap); buildLetterSlots pure helper + 10 tests; Letter overlay 7 tests. Boot path in src/PhaserGame.tsx threads silent catchup → offlineEvents → openLetter. -->
|
||
|
||
- [ ] **UX-03**: Player can buy plants/upgrades in multi-buy increments (×1 / ×10 / ×100 / Max) when the option is meaningful for the current scaling.
|
||
- [ ] **UX-04**: Player can adjust separate Music, Ambient, and SFX volume sliders, with a master mute keybind; settings persist in saves.
|
||
- [ ] **UX-05**: Player can toggle a reduced-motion option (respects `prefers-reduced-motion` system setting by default) that disables non-essential particles and animation.
|
||
- [ ] **UX-06**: Player can navigate all menus by keyboard (Tab, Enter, Escape, arrow keys) and the focus indicator is always visible.
|
||
- [ ] **UX-07**: All UI text is selectable, copy-pasteable, and supports browser zoom up to 200% without breaking layout.
|
||
- [ ] **UX-08**: Color is never the sole carrier of information — icons, labels, or patterns provide a redundant channel for color-blind players.
|
||
- [ ] **UX-09**: Tab title and favicon update to reflect a backgrounded state (e.g., a small bloom appears when a fragment is ready).
|
||
- [x] **UX-10**: Game saves state on `visibilitychange` to hidden, on `beforeunload`, and on Season transitions; behavior is identical between "tab backgrounded" and "tab closed." <!-- Plan 02-01: registerSaveLifecycleHooks attaches synchronous handlers for visibilitychange→hidden + beforeunload + saveOnSeasonTransition() callable; 6 lifecycle tests green covering every trigger + detach. Plan 02-05: PhaserGame.tsx boot path now wires saveSync via clock.now() (BLOCKER 3 — wall-clock anchor) + synchronous LocalStorage write (Pitfall 7) + best-effort IDB write; lifecycle handle held in a ref so the outer useLayoutEffect cleanup detaches across the async IIFE boundary (W5). -->
|
||
- [x] **UX-11**: Numbers display in human-readable formats (1.2K, 4.5M, 8.9B, scientific notation past notation thresholds). <!-- Plan 02-01: formatHumanReadable handles every K/M/B/T threshold + 1e15 scientific + negative-sign branch; 11 format tests green. BigQty.format() delegates so all currency-grade numbers in the HUD route through this. -->
|
||
|
||
- [ ] **UX-12**: Game surfaces *what Lura said yesterday* in returning-player UI affordances — never *fragments per hour* or *optimization metrics* (mechanic-as-metaphor doctrine).
|
||
- [x] **UX-13**: No daily login bonuses, no streaks, no limited-time content, no nag notifications, no loss-aversion copy — anti-FOMO doctrine is enforced in every UX review. <!-- Plan 01-06: .planning/anti-fomo-doctrine.md (17 banned mechanics, review checklist) authored and doc-lint tested; enforced by review per CONTEXT D-07. -->
|
||
|
||
### PIPE — Content Build & Asset Pipelines
|
||
|
||
- [x] **PIPE-01**: Project ships a build step that compiles `/content/**/*.{md,yaml,ink}` into per-Season JSON chunks via Zod-validated schemas; build fails on any schema violation. <!-- Plan 01-04: Vite-native import.meta.glob + Zod schemas; 5 loader tests green; schema violation throws at module-eval time. -->
|
||
- [x] **PIPE-02**: Player loads only the content for their current Season at runtime (lazy chunk loading); future Seasons are not in the initial bundle. <!-- Plan 02-02: loadSeasonFragments(seasonId) lazy import.meta.glob surface in src/content/loader.ts. Plan 02-03: scripts/check-bundle-split.mjs structural verifier integrated into npm run ci; runs after build to assert Season-1 content reaches dist/ via the lazy plumbing (currently chunkContentMatch=true via the eager corpus inlining; chunkNameMatch=false until Plan 02-04+ switches consumers to lazy-only). The structural assertion holds today; Phase 4+ Season-2 onboarding extends the script's known-content list. -->
|
||
|
||
- [x] **PIPE-03**: Project ships an AI asset pipeline that records provenance per asset and refuses to integrate an asset missing required provenance fields. <!-- Plan 01-05: scripts/validate-assets.mjs + Zod ProvenanceSchema (6 fields) + refused-sample fixture + 2 Vitest tests green. -->
|
||
- [ ] **PIPE-04**: Project ships visual regression testing for the asset library that flags style drift before any model migration is merged.
|
||
- [x] **PIPE-05**: Project ships an `anti-FOMO doctrine` document and a `Season 7 end-state` design document in `.planning/` (or `docs/`) before economy code is written. <!-- Plan 01-06: both docs authored and doc-lint tested (8 Vitest assertions green). -->
|
||
- [x] **PIPE-06**: Project ships unit tests (Vitest) covering all save migrations and core economy formulas, run on every CI build. <!-- Plan 01-07: .github/workflows/ci.yml runs npm ci + npm run ci on push + PR; 53 tests / 12 files green. -->
|
||
- [x] **PIPE-07**: Project ships an end-to-end smoke test (Playwright) that loads the game, plants a seed, harvests a fragment, and verifies persistence across a page reload. <!-- Plan 02-05: tests/e2e/season1-loop.spec.ts covers load → Begin → plant rosemary → fast-forward FakeClock 3min → harvest → fragment-reveal modal → close → journal-icon visible → open journal → fragment present → reload → fragment persists. URL-flag FakeClock injection production-guarded by import.meta.env.PROD; window.__tlgStore exposed only when ?devtime=fake. 1.5s test runtime, ~4s end-to-end. npm run test:e2e (not in npm run ci per minimum-viable doctrine; runs separately before /gsd-verify-work and on release). -->
|
||
|
||
## v2 Requirements
|
||
|
||
Deferred to v1.x or v2.0. Tracked but not in current roadmap.
|
||
|
||
### MONE — Cosmetic Monetization
|
||
|
||
- **MONE-01**: Player can purchase cosmetic-only items (planters, walls, gates, tool skins) via fixed-price catalog (no gacha, no random drops).
|
||
- **MONE-02**: Player can purchase a one-time "Keeper's Journal" premium upgrade unlocking annotation, organization, and personal-scrapbook features for collected fragments.
|
||
- **MONE-03**: Player can purchase Season acceleration that *accelerates waiting* — never *skips story beats*; UI explicitly labels the distinction.
|
||
- **MONE-04**: Game uses a server-authoritative entitlements backend (Cloudflare Worker + Stripe webhooks) — no localStorage entitlement claims.
|
||
- **MONE-05**: Cosmetic catalog items are permanent (never time-limited), priced transparently, and aesthetic-coherent with the garden setting.
|
||
|
||
### PWA — Offline-First & Notifications
|
||
|
||
- **PWA-01**: Player can install the game as a PWA with a service worker manifest.
|
||
- **PWA-02**: Player can opt in to push notifications for Memory Storm events only — no daily/marketing/streak notifications, ever.
|
||
- **PWA-03**: Service worker caches static assets and serves them offline; saves continue to work without a network connection.
|
||
|
||
### CLOU — Cloud Save Sync
|
||
|
||
- **CLOU-01**: Player can sign in (anonymous or email) and sync their save across devices via PocketBase or equivalent backend.
|
||
- **CLOU-02**: Cloud save uses the same versioned snapshot format as local save; conflict resolution prompts the player rather than silently overwriting.
|
||
|
||
### CONT — Post-Launch Content Patches
|
||
|
||
- **CONT-01**: Project ships free additive content patches post-launch (Hollow Knight model) — additional place-memory vignettes, garden cosmetics, and ambient-only Seasons that fit between authored beats.
|
||
|
||
### PORT — Steam & Mobile Native Ports
|
||
|
||
- **PORT-01**: Game ships on Steam (PC/Mac) using Phaser 4 + Electron or equivalent wrapper.
|
||
- **PORT-02**: Game ships on iOS and Android as native or hybrid wrappers, with on-device save sync via cloud.
|
||
|
||
### LOC — Localization
|
||
|
||
- **LOC-01**: Game supports localization (EN baseline, additional languages added based on community demand) — externalized strings already shipped in v1, only translation pass needed.
|
||
|
||
### MOD — Modding & Community Content
|
||
|
||
- **MOD-01**: Project documents a community-content authoring spec that lets community writers contribute fragments respecting tone and provenance discipline.
|
||
- **MOD-02**: Project ships a community-content gallery (off-canon, opt-in) where players can browse and load community-authored gardens.
|
||
|
||
### SOC — Garden Visiting (cautious — must not break tone)
|
||
|
||
- **SOC-01**: Player can opt in to visiting other players' gardens in read-only mode, viewing what fragments they have collected (without spoilers for unvisited Seasons).
|
||
|
||
## Out of Scope
|
||
|
||
Explicit exclusions. Documented to prevent scope creep. **Anti-features tied to the project's thematic constraints — these are not deferred features, they are excluded by design.**
|
||
|
||
| Feature | Reason |
|
||
|---------|--------|
|
||
| Gacha mechanics | Directly contradicts the game's thematic argument that complex things cannot be reduced to simple transactions. |
|
||
| Lootboxes | Same reason as gacha — undermines the story's monetization-as-meaning argument. |
|
||
| Narrative gating behind purchase | The story IS the product; story content is never paid. |
|
||
| Season *skipping* (vs. *accelerating*) | Players must never miss authored story beats; acceleration is allowed, skipping is forbidden. |
|
||
| Daily login bonuses | FOMO mechanic; violates cozy/contemplative tone. |
|
||
| Login streaks | FOMO mechanic; same reason. |
|
||
| Limited-time / time-limited content | FOMO mechanic; same reason. |
|
||
| Energy / stamina systems | Anti-cozy gating that interrupts contemplative play. |
|
||
| Rewarded ads | Anti-cozy; tonally incoherent with a contemplative grief-narrative. |
|
||
| Push notification spam | Memory Storm opt-in is the *only* allowed notification — no daily/marketing/streak/re-engagement nags. |
|
||
| Lore codex / encyclopedia entries | Players should always feel *almost* understanding; world-building emerges through fragments alone. |
|
||
| Generic fantasy flora | Plants must look like real-world species made slightly wrong; no D&D-style fictional flora. |
|
||
| Combat / boss fights | The Archivist is not a boss; there is no enemy. Combat would violate the entire thematic frame. |
|
||
| Multiplayer / leaderboards (v1) | Solitary, contemplative experience; reconsider for v2 only if it does not break tone. |
|
||
| Voiced dialogue (v1) | Tone is "a friend texting you while you're at work"; text fits the medium. Reconsider v2+ only if cello-and-silence soundscape benefits. |
|
||
| Always-online | Local-first save model; game must work offline. |
|
||
| Named/personality-rich Keeper | Keeper is a presence, not a personality; player projects onto the system. |
|
||
| Hint system / objective tracker | Discovery-driven progression (A Dark Room rule); explicit objectives violate the tone. |
|
||
| Time-skip purchases that bypass real-time | Real-time *is* the metaphor for memory; skip-time purchases would violate mechanic-as-metaphor doctrine. |
|
||
| Unity / Godot / non-web engines (v1) | Web-first per PROJECT.md and lineage of A Dark Room / Universal Paperclips; install friction kills the audience. |
|
||
| Generic cosmetic items | Cosmetics must reinforce, not dilute, the garden aesthetic. No generic skins. |
|
||
| Random-drop cosmetics | All cosmetics must be deterministic catalog purchases. No RNG monetization. |
|
||
| Mobile-style nag UX | Cozy audience expects respect; nag patterns will tank reviews. |
|
||
|
||
## Traceability
|
||
|
||
Populated by gsd-roadmapper during roadmap creation on 2026-05-08. Updated after Phase 1 verification on 2026-05-09.
|
||
|
||
| Requirement | Phase | Status |
|
||
|-------------|-------|--------|
|
||
| CORE-01 | Phase 1 — Foundations & Doctrine | Complete (scaffold builds; full E2E <5s measurement is Phase 2 PIPE-07) |
|
||
| CORE-02 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-01; drainTicks fixed-timestep accumulator + Clock injection; scene-driven tick wiring is Plan 02-02) |
|
||
| CORE-03 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-01 + 02-05; MAX_OFFLINE_MS=24h clamp + computeOfflineCatchup + PhaserGame.tsx boot path threads catchup → silent drainTicks → letter overlay) |
|
||
| CORE-04 | Phase 1 — Foundations & Doctrine | Complete (IDB + localStorage fallback; codec + round-trip; Settings UI is Phase 2) |
|
||
| CORE-05 | Phase 1 — Foundations & Doctrine | Complete (navigator.storage.persist() all 4 scenarios; Settings UI surface is Phase 2) |
|
||
| CORE-06 | Phase 1 — Foundations & Doctrine | Complete (wrap/unwrap + CRC-32 checksum + SaveCorruptError) |
|
||
| CORE-07 | Phase 1 — Foundations & Doctrine | Complete (forward-only migration chain; synthetic v0→v1 tested; real v1→v2 in Phase 4) |
|
||
| CORE-08 | Phase 1 — Foundations & Doctrine | Complete (last-3 snapshot retention; Settings UI surface is Phase 2) |
|
||
| CORE-09 | Phase 1 — Foundations & Doctrine | Complete (Base64 export/import + 50MB DoS cap; Settings UI surface is Phase 2) |
|
||
| CORE-10 | Phase 1 — Foundations & Doctrine | Complete (ESLint boundary rule + Vitest proof) |
|
||
| CORE-11 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-01; drainTicks refuses negative deltas + computeOfflineCatchup clamps to 0; ESLint sim-purity rule mechanically enforces D-33) |
|
||
| GARD-01 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-02; sim/garden plantSeed + SeedPicker + Garden scene pointerdown wiring) |
|
||
| GARD-02 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-02; advanceGrowth state machine + plant-renderer primitives + reactive repaint via appStore.subscribe) |
|
||
| GARD-03 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-03; sim/garden harvest() pure command + selectFragment() integration + Garden.ts pointer wiring) |
|
||
| GARD-04 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-03; sim/garden compost() pure command + content/dialogue/season1/compost-acknowledgements.ink authored ahead of Plan 02-04 Ink runtime) |
|
||
| GARD-05 | Phase 4 — Season-Prestige Cycle & Season 2 (Roots) | Pending |
|
||
| GARD-06 | Phase 5 — Seasons 3-4 (Canopy & Storm) | Pending |
|
||
| GARD-07 | Phase 4 — Season-Prestige Cycle & Season 2 (Roots) | Pending |
|
||
| GARD-08 | Phase 6 — Seasons 5-6 (Depth & Loom) | Pending |
|
||
| GARD-09 | Phase 5 — Seasons 3-4 (Canopy & Storm) | Pending |
|
||
| GARD-10 | Phase 3 — Watercolor & Cello Aesthetic | Pending |
|
||
| MEMR-01 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-03; harvest() invokes selectFragment() exactly once per ready harvest) |
|
||
| MEMR-02 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-03; 14 yaml + 2 long-form md fragments under /content/seasons/01-soil/; PIPE-01 build-time schema enforcement) |
|
||
| MEMR-03 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-03; 17 fragments match /^season1\.[a-z0-9._-]+$/ regex; numeric-id rejected by FragmentSchema) |
|
||
| MEMR-04 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-03; src/ui/journal/Journal.tsx full-screen modal grouped by Season + JournalIcon corner affordance) |
|
||
| MEMR-05 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-03; Journal + FragmentRevealModal render fragment bodies in <pre> with userSelect:'text'; pinned by computed-style assertions) |
|
||
| MEMR-06 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-03; src/sim/memory/selector.ts deterministic via mulberry32 seeded from sim state; gated by Season + plant-type tonal register; no-dup; sentinel fallback for Pitfall 8) |
|
||
| MEMR-07 | Phase 5 — Seasons 3-4 (Canopy & Storm) | Pending |
|
||
| STRY-01 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-04; 3 Ink beats authored at /content/dialogue/season1/lura-{arrival,mid,farewell}.ink, gated at 1/4/8 harvests via sim/narrative/lura-gate.ts; LuraDialogue overlay renders inkjs Story with text-message cadence) |
|
||
| STRY-02 | Phase 7 — Season 7 (Return) & Final Choice | Pending |
|
||
| STRY-03 | Phase 5 — Seasons 3-4 (Canopy & Storm) | Pending |
|
||
| STRY-04 | Phase 6 — Seasons 5-6 (Depth & Loom) | Pending |
|
||
| STRY-05 | Phase 6 — Seasons 5-6 (Depth & Loom) | Pending |
|
||
| STRY-06 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-04; scripts/compile-ink.mjs invokes bundled inklecate binary; src/content/ink-loader.ts lazy-loads compiled JSON; npm run ci compiles before tests + build. Assumption A6 verified) |
|
||
| STRY-07 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-04; vacuously satisfied — zero Keeper-spoken lines in Phase 2 .ink files; Phase 7 lands the binary choice surface) |
|
||
| STRY-08 | Phase 7 — Season 7 (Return) & Final Choice | Pending |
|
||
| STRY-09 | Phase 1 — Foundations & Doctrine | Complete (vacuous — /content/ convention established; no player-visible strings in Phase 1 source) |
|
||
| STRY-10 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-04; lura-gate gates on harvest count not wall time; STRY-10 test case advances FakeClock 24h with 0 harvests and confirms no beat fires; ESLint sim-purity rule prevents Date.now in src/sim/narrative/) |
|
||
| SEAS-01 | Phase 4 — Season-Prestige Cycle & Season 2 (Roots) | Pending |
|
||
| SEAS-02 | Phase 4 — Season-Prestige Cycle & Season 2 (Roots) | Pending |
|
||
| SEAS-03 | Phase 4 — Season-Prestige Cycle & Season 2 (Roots) | Pending |
|
||
| SEAS-04 | Phase 4 — Season-Prestige Cycle & Season 2 (Roots) | Pending |
|
||
| SEAS-05 | Phase 4 — Season-Prestige Cycle & Season 2 (Roots) | Pending |
|
||
| SEAS-06 | Phase 4 — Season-Prestige Cycle & Season 2 (Roots) | Pending |
|
||
| SEAS-07 | Phase 6 — Seasons 5-6 (Depth & Loom) | Pending |
|
||
| SEAS-08 | Phase 6 — Seasons 5-6 (Depth & Loom) | Pending |
|
||
| SEAS-09 | Phase 7 — Season 7 (Return) & Final Choice | Pending |
|
||
| SEAS-10 | Phase 7 — Season 7 (Return) & Final Choice | Pending |
|
||
| AEST-01 | Phase 3 — Watercolor & Cello Aesthetic | Pending |
|
||
| AEST-02 | Phase 3 — Watercolor & Cello Aesthetic | Pending |
|
||
| AEST-03 | Phase 3 — Watercolor & Cello Aesthetic | Pending |
|
||
| AEST-04 | Phase 3 — Watercolor & Cello Aesthetic | Pending |
|
||
| AEST-05 | Phase 3 — Watercolor & Cello Aesthetic | Pending |
|
||
| AEST-06 | Phase 3 — Watercolor & Cello Aesthetic | Pending |
|
||
| AEST-07 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-02; BeginScreen + bootstrapAudioContext synchronous-inside-click defends Pitfall 5) |
|
||
| AEST-08 | Phase 1 — Foundations & Doctrine | Complete (Zod ProvenanceSchema 6 fields + CI gate; north-star reference set deferred to Phase 5 per IOU) |
|
||
| AEST-09 | Phase 1 — Foundations & Doctrine | Complete (human curation gate mechanism in place; recorded human decision in 01-05-IOU.md) |
|
||
| UX-01 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-02; single fixed-position Begin overlay; no HUD/journal/settings; D-22 dismissal) |
|
||
| UX-02 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-05; letter-from-the-garden.ink + Letter overlay + boot path silent catchup → openLetter at ≥5min absence; Pitfall 9 audio bootstrap on dismiss) |
|
||
| UX-03 | Phase 8 — UX, Accessibility & Launch Polish | Pending |
|
||
| UX-04 | Phase 8 — UX, Accessibility & Launch Polish | Pending |
|
||
| UX-05 | Phase 3 — Watercolor & Cello Aesthetic | Pending |
|
||
| UX-06 | Phase 8 — UX, Accessibility & Launch Polish | Pending |
|
||
| UX-07 | Phase 8 — UX, Accessibility & Launch Polish | Pending |
|
||
| UX-08 | Phase 8 — UX, Accessibility & Launch Polish | Pending |
|
||
| UX-09 | Phase 8 — UX, Accessibility & Launch Polish | Pending |
|
||
| UX-10 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-01 + 02-05; registerSaveLifecycleHooks + saveOnSeasonTransition; PhaserGame.tsx boot path wires saveSync via clock.now() with synchronous LocalStorage write + best-effort IDB) |
|
||
| UX-11 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-01; formatHumanReadable K/M/B/T/scientific; BigQty.format() delegates) |
|
||
| UX-12 | Phase 8 — UX, Accessibility & Launch Polish | Pending |
|
||
| UX-13 | Phase 1 — Foundations & Doctrine | Complete (anti-fomo-doctrine.md authored + doc-lint tested; review-enforced per CONTEXT D-07) |
|
||
| PIPE-01 | Phase 1 — Foundations & Doctrine | Complete (Vite-native loader + Zod schemas; build fails on schema violation) |
|
||
| PIPE-02 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-02; loadSeasonFragments lazy import.meta.glob surface in src/content/loader.ts. Plan 02-03; scripts/check-bundle-split.mjs structural verifier integrated into npm run ci.) |
|
||
| PIPE-03 | Phase 1 — Foundations & Doctrine | Complete (validate-assets.mjs + ProvenanceSchema + refused-sample fixture + 2 Vitest tests) |
|
||
| PIPE-04 | Phase 8 — UX, Accessibility & Launch Polish | Pending |
|
||
| PIPE-05 | Phase 1 — Foundations & Doctrine | Complete (both doctrine docs authored + 8 doc-lint assertions green) |
|
||
| PIPE-06 | Phase 1 — Foundations & Doctrine | Complete (ci.yml runs npm run ci on push + PR; 53 tests / 12 files green) |
|
||
| PIPE-07 | Phase 2 — Season 1 Vertical Slice (Soil) | Complete (Plan 02-05; tests/e2e/season1-loop.spec.ts — full Phase-2 loop in Chromium with FakeClock injection, 1.5s test runtime, 4s end-to-end) |
|
||
|
||
**Per-Phase Counts:**
|
||
|
||
| Phase | Requirements |
|
||
|-------|--------------|
|
||
| Phase 1 — Foundations & Doctrine | 16 |
|
||
| Phase 2 — Season 1 Vertical Slice (Soil) | 24 |
|
||
| Phase 3 — Watercolor & Cello Aesthetic | 8 |
|
||
| Phase 4 — Season-Prestige Cycle & Season 2 (Roots) | 8 |
|
||
| Phase 5 — Seasons 3-4 (Canopy & Storm) | 4 |
|
||
| Phase 6 — Seasons 5-6 (Depth & Loom) | 5 |
|
||
| Phase 7 — Season 7 (Return) & Final Choice | 4 |
|
||
| Phase 8 — UX, Accessibility & Launch Polish | 8 |
|
||
| **Total** | **77** |
|
||
|
||
**Coverage:**
|
||
- v1 requirements: 77 total (the "78" in the prior coverage block was a counting error in initial drafting; categories sum to 11+10+7+10+10+9+13+7 = 77 numbered REQ-IDs)
|
||
- Mapped to phases: 77 (100%)
|
||
- Unmapped: 0
|
||
|
||
---
|
||
*Requirements defined: 2026-05-08*
|
||
*Last updated: 2026-05-09 after Plan 02-05 execution (40/77 REQ-IDs marked Complete — Phase 1 + Phase 2 fully shipped pending /gsd-verify-work)*
|