rimlike/docs/implementation.md
megaproxy b517058d2e docs: mark Phase 19 done pending playtest review
Status row updated 🟡 done (pending review).  Inline checklist
ticked, with hint-vs-guided-vs-tutorial decision recorded.  Acceptance
demo (cold-tester <60s) explicitly owed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 17:39:36 +01:00

537 lines
59 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Implementation plan — rimlike
Phased build plan from clean-slate to MVP. Phases are ordered by dependency, and each ends with a runnable demo state so the project never sits in "everything's stubbed" territory for long.
Effort estimates are wall-time at **focused solo pace**. Scale up generously for context-switches, life, and the occasional rabbit hole. Ranges are deliberately wide.
| Status | Phase |
|---|---|
| ✅ done — green dot up, smoke scene runs, MCP plugin self-installed 3 runtime services | **Phase 0 — Project scaffold & foundations** |
| ✅ done — 80² map renders, walls/terrain/UI layers, camera rig, tick loop, speed UI all live | **Phase 1 — World, tilemap, camera** |
| ✅ done — Pawn class, AStarGrid2D pathfinder (9.1 μs avg/18 μs max at 80²), click-to-select + click-to-move via Selection module | **Phase 2 — Pawn skeleton, pathfinding, movement** |
| ✅ done — Job/Toil/JobRunner/Decision/RestProvider, forced_job preempt, mid-toil save round-trip verified | **Phase 3 — AI core: Decision → WorkProvider → JobRunner** |
| ✅ done — Tree/Rock/Item entities, ChopProvider/MineProvider/HaulingProvider, StockpileZone with 16-chip filter + 5-tier priority + cascade sweep | **Phase 4 — First verbs: chop, mine, hauling, stockpiles** |
| ✅ done — Designation paint mode, BuildJob queue, ConstructionProvider, Wall/Floor/Door entities (Y-sorted), Crate as StorageDestination. **Rendering pivot to 3/4 perspective locked.** | **Phase 5 — Building, walls, floors, containers** |
| ✅ done — Recipe + Bill data, Workbench entity (Carpenter / Smelter via label_text), CraftingProvider, KIND_CRAFT toil, 5-tier Quality system, Pawn skills, wall-trap fix | **Phase 6 — Production: workbenches, recipes, bills, quality** |
| ✅ done — Crop entity (6-stage state machine), PlantProvider (harvest), Hunger need + EatProvider w/ food-priority ladder, Hearth/Millstone via label_text, grain/flour/bread/meal types | **Phase 7 — Plants, cooking, hunger** |
| ✅ done — Bed entity (quality-tinted, claim/release), Sleep need + SleepProvider + KIND_SLEEP toil, Thought registry + mood compute + Sulking soft-break, Decision Layer-1 interrupt | **Phase 8 — Sleep, mood, thoughts** |
| ✅ done (out of order — taken before Phase 9 for the atmospheric win) — Clock autoload + dawn/day/dusk/night phases + darkness_factor ramp, CanvasModulate global tint, Torch entity + PointLight2D + procedural radial gradient, Hearth opts-in as light source, in_darkness thought | **Phase 11 — Day/night + Lighting** |
| ✅ done — HP + Status registry (Bleeding/Downed), pawn `take_damage`/`heal`/downed visual, DoctorProvider (priority 9, highest), medical bed (red cross marker), Rescue + Treat toils, EventBus damage/status signals, Decision Layer-1 incapacitation interrupt | **Phase 9 — Status effects + Medicine** |
| ✅ done — Wolf entity (4-state APPROACH/ENGAGE/FLEE/DEAD, procedural canine sprite with red eyes), WolfSpawner (12 wolves at random map edge, triggers at darkness≥0.8 with daily cooldown), two-roll combat (70% hit + 50% bleed chance on hit), World.wolves registry | **Phase 10 — Combat + Wolves** |
| ✅ done — 48-day year (4 seasons × 12 days), Clock season API + season_changed signal, Weather autoload with season-weighted daily roll (clear/rain/storm/cold_snap), procedural rain overlay + storm white-flash, terrain seasonal palette modulate, top-bar season indicator ("Spring 1/12"), Wet status (Damp/Soaked) + Cold status with mood thoughts, _is_sheltered() floor-proxy (Phase 13 replaces with Room BFS) | **Phase 12 — Seasons + Weather** |
| ✅ done — Room data class + RoomDetector (BFS, 4-dir, door-as-boundary), 16-cell auto-roof cap with `room_too_large` banner signal, World.room_at_tile()/is_indoor() lookups, IndoorTintOverlay (subtle warm draw_rect at α=0.10), Pawn._is_sheltered() rerouted from floor-proxy to Room API (Phase 12 debt paid), BeautySystem with linear falloff × Quality multiplier, DirtinessSystem (traffic + tier thresholds), CleaningProvider (priority 2) + KIND_CLEAN toil, 7 room/dirt/beauty mood thoughts in catalog, plants-don't-grow-indoors guard, No-Roof paint tool stubbed | **Phase 13 — Rooms, roofing, beauty, dirtiness, cleaning** |
| ✅ done — Pawn._check_death + Corpse entity with decay (DECAY_PER_TICK=0.05, fresh<50, rotting<100, rotted), GraveyardZone (StorageDestination subclass, corpse-only filter), GraveSlot (ghostdugaccepts corpsespawns GraveMarker), permanent GraveMarker entity with deceased identity, dig_grave + graveyard paint tools, KIND_PICKUP_CORPSE/KIND_DEPOSIT_CORPSE toils + HaulingProvider corpse iteration, CremationPyre (Workbench subclass) + cremate_corpse recipe + TYPE_ASH item type, 4 mood thoughts (saw_corpse, buried_friend, cremated_friend, rotting_body_in_colony), bleed-out timeout at BLEED_OUT_TICKS=432000 | **Phase 14 — Death, corpses, burial** |
| done EventDef data class + EventCatalog with all 25 events authored (4 nudges, 4 seasonal, 4 wanderers, 4 threats, 3 disease, 3 resource, 2 lore, 1 milestone), Storyteller autoload (daily 6 AM roll, per-event+per-category cooldowns both-gates locked, tension model 0-100 with category multipliers, state-trigger 3× weight boost, ghost-state wanderer auto-fire 3-5 day window), StorytellerBanner (CanvasLayer, queued, 6-sec auto-dismiss, tap-to-dismiss-early), StorytellerModal (centered dialog, 0/1/2 choices, full-screen dim, auto-pause on THREAT), "Go there" camera pan helper via camera_rig.pan_to_tile() | **Phase 15 — Storyteller** |
| done class_id-tagged to_dict on all 18 entity types, SaveSystem v2 with per-class factory registry + World.clear_all + clear-and-respawn apply_save, tilemap layer serialization, beauty/dirt map round-trip, Autosave autoload (periodic 6000-tick interval + NOTIFICATION_APPLICATION_PAUSED + focus-loss), Save/Load buttons in TopBar, LoadMenu CanvasLayer with version-mismatch dialog, ResumeToast ("Welcome back N minutes away"), slot API (manual + autosave), graceful version-mismatch handling | **Phase 16 — Save/load full coverage** |
| done Pawn.work_priorities matrix (8 player categories), Decision Layer 4 honors per-pawn priorities (NEEDS_CATEGORIES bypass), PawnDetailPanel (HP/Hunger/Sleep/Mood/Statuses/Skills/Priorities live-refresh, opens on pawn_selected), BuildDrawer bottom-sheet (Designate/Build/Stockpile/Cancel tabs, 12 new tool constants wired), WorkPriorityMatrix grid (tap-cycle 12340, color-coded), AlertsLog (ring buffer 50, severity icons + Go-there, listens to alert_added + storyteller_event_fired + day_ended), SettingsMenu (auto-pause toggles + audio + accessibility), EventBus.request_wolf_spawn wired end-to-end (EventCatalog._spawn_wolves WolfSpawner._on_request_wolf_spawn force-bypass), EventBus.day_ended emit from Clock dusknight | **Phase 17 — Touch UX completion** |
| done Audio autoload + 3 buses (Master/Music/SFX), 2 looping music tracks (day/night) swapped on Clock.phase_changed, 6 SFX wired (tree_fell, mine_tick, combat_hit via pawn_took_damage, storyteller sting, alert click, UI click), SettingsMenu sliders live-bound to bus DB, NOTIFICATION_APPLICATION_PAUSED + FOCUS_OUT mute the Master bus | **Phase 18 — Audio** |
| 🟡 done (pending playtest review) Hint approach chosen 2026-05-16. HintSystem autoload + HintOverlay layer 22 (7-step tour gated on player events: welcome / pawn select / build drawer / stockpile painted / work matrix / day_ended / tour_complete; per-hint dismissal persisted in GameState.settings; FIFO queue depth 3; reduce-motion snap path; reset_tour API). HelpModal layer 20 with 5 tabs (Controls / Verbs / Priorities / Storyteller / Tips). SettingsMenu "Onboarding" section with Show-hints checkbox + Help button + Reset-hints button. Tooltip pass across TopBar + BuildDrawer FAB + 21 tool buttons. **Acceptance demo still owed:** hand the game to a tester cold; they should be doing useful work in <60s. | **Phase 19 — Onboarding & first-60-seconds** |
| next | **Phase 20 — Balance, polish, export** |
Use this doc as a checklist: tick boxes as items complete, and update the **Status** row above whenever a phase rolls over. The last bullet of each phase is the *acceptance demo* the phase is "done" when you can perform it.
Refs to `docs/` files are linked so each item lands in the right spec.
---
## Pre-implementation audit (~75 min)
The five items from `memory.md` *Open questions / Audit*. None of these need code, but several of them gate Phase 1+ (autotile drives the wall pipeline; aesthetic harmony decides whether Ventilatore stays in active use). Knock these out before Phase 0.
- [ ] **Aesthetic harmony test** ElvGames Forest vs Ventilatore tile, side-by-side. Decide use-both or drop-Ventilatore. (~15 min) **needs your eye**
- [x] **ElvGames autotile audit** done 2026-05-10. Findings (visual inspection):
- **`FG_Houses.png` is NOT autotile-solvable as-is.** Pieces are pre-built decorative house compositions (4 distinct roof palettes), not modular wall variants. ~½–1 day per material to author terrain bits on top.
- **`FG_Fortress.png` IS autotile-solvable.** 2030 modular tan-stone-with-dark-mortar pieces straight, corners, caps. Wang-style Godot 4 terrain works with minimal extra art.
- **Recommendation:** make Fortress stone the primary player wall material. Defer custom-authored Houses walls to v2 OR keep Houses as static prebuilt-shelter art only.
- Iconic Homestead $19.99 fallback **not needed**.
- [x] **Wolf sprite source** done 2026-05-10. **No wolf in the bundle.** EvoMonster packs are all cute/fantasy creatures (slimes, ghosts, dragons), Turn-Based RPG Monsters are humanoid-style. No 4-legged canine predator anywhere in the bundle. Action: commission a 16×16 wolf (idle + 24-frame walk × 4 directions) OR check Ventilatore bundle OR find a CC0 sprite. **Open in `memory.md`.**
- [x] **Grave marker source** done 2026-05-10. `Retro Graveyard 16x16 Tileset [Kingdom Explorer]` confirmed in Tier 3, full graveyard suite (tombstones, crosses, mounds, crypts).
- [ ] **License compilation start** kick off the credits string list now; add to it as packs come in. (open across phases)
---
## Phase 0 — Project scaffold & foundations (~1 week)
**Goal:** a Godot project that opens cleanly, has all the autoloads and folder structure committed, and runs an empty test scene.
- [x] `project.godot` at repo root. **GL Compatibility renderer** (max mobile reach; Forward+ would lock out older devices). Pixel-snap on, texture filter = nearest. Landscape sensor orientation. Viewport 1280×720 (`canvas_items` stretch, `keep` aspect).
- [x] Re-copy `addons/godot_mcp/` from `/mnt/d/godot/mcp/addons/godot_mcp` and enable in `project.godot` `[editor_plugins]`. **Editor-side green-dot check pending** needs you to open the editor once.
- [x] Folder layout (cribbed from tavernkeep's idiomatic Godot pattern co-located scripts in `scenes/`, `autoload/` at root):
- `autoload/` singletons (`world.gd`, `sim.gd`, `game_state.gd`, `event_bus.gd`, `strings.gd`, `audit.gd`, `save_system.gd`)
- `scenes/` (`main/`, `world/`, `pawn/`, `ui/`, `entities/`, `effects/`)
- `data/` (`recipes/`, `thoughts/`, `events/`, `weather/`, `pawns/`)
- `art/` (`tiles/`, `sprites/`, `ui/`, `fx/`)
- `audio/` (`sfx/`, `music/`)
- `tests/`, `tools/`
- [x] Autoloads (stubs; real bodies land in later phases):
- `World` entity registry, tile state, signals
- `Sim` tick loop owner, speed/pause state, Speed enum + factor table
- `GameState` current map, session timestamp, `save_dict()` / `apply_dict()`
- `EventBus` global signal hub (no signals yet added per-phase)
- `Strings` i18n string table (`Strings.t(key)` lookup; const dict for now, .tres later)
- `Audit` debug-only logging gate
- `SaveSystem` `write_save()` / `read_save()`, version + path, JSON
- [x] Input map: `pause`, `speed_cycle`, `speed_normal`, `speed_fast`, `speed_ultra`, `confirm`, `cancel`. Mobile gestures (pinch / drag / long-press) handled at script level, not as input actions Godot's `InputEventScreenTouch` / `InputEventScreenDrag` / `InputEventMagnifyGesture` cover them.
- [x] `SaveSystem` skeleton: version field (`SAVE_VERSION = 1`), `user://save_slot.json`, JSON serialize, mismatch warning. **Smoke-test payload only**; Phase 3 expands.
- [x] `.gitignore` covers `.godot/`, `.import/`, `addons/godot_mcp/`, exports verified.
- [x] Smoke-test scene: `scenes/main/main.tscn` (Node + Camera2D + Label). `main.gd._ready()` asserts every autoload alive and shows the i18n-resolved hello string.
- [x] **Acceptance (headless):** `godot --headless --path . --quit` exits 0, `[main] Phase 0 smoke test online.` prints, no errors. **Confirmed.**
- [ ] **Acceptance (editor):** open `project.godot` in Godot 4.6, hit Play see "Phase 0 autoloads online." rendered at 32,32. MCP Pro bottom panel shows green dot. **Needs your hand.**
---
## Phase 1 — World, tilemap, camera (~2 weeks)
**Goal:** an 80×80 map with the locked camera UX. No pawns yet; just a navigable empty world.
- [x] **6 `TileMapLayer` nodes** in `scenes/world/world.tscn` (Godot 4.4+ idiom supersedes the multi-layer `TileMap`): 0 Terrain · 1 Floor · 2 Wall · 3 Designation · 4 Roof (hidden) · 5 Fog (hidden). Z-indices set, layers can hold different sources independently.
- [x] **Placeholder tileset built at runtime** (no PNG import dependency for Phase 1). 4 programmatic 16×16 colored tiles (grass / dirt / stone / dark-stone) generated via `Image.create()` + `ImageTexture.create_from_image()`. Real ElvGames PNGs (`FG_Grounds.png`, `FG_Fortress.png`, `FG_Forest_Spring.png`) copied to `art/tiles/` but not yet wired they land in Phase 5 when the wood-wall variants get authored.
- [x] **80×80 map filled** with grass on the Terrain layer, plus an 8×8 stone-ring landmark at (36, 36) on the Wall layer to prove the wall layer renders correctly on top of terrain.
- [x] **Tick loop** in `autoload/sim.gd` time-accumulator pattern: `_accum += delta * SPEED_FACTOR[current_speed]`, drains in `TICK_INTERVAL_S = 1/20` chunks emitting `EventBus.sim_tick`. Default boot speed = NORMAL. `set_speed()` resets `_accum` to 0 to avoid burst-ticks after pause.
- [x] **Speed control top bar** (`scenes/ui/top_bar.tscn`) `CanvasLayer` (layer 10) 4 buttons (Pause / 1× / Fast / Ultra) + tick label. Keyboard shortcuts: `pause`, `speed_normal/fast/ultra` (keys Space, 1, 2, 3). Buttons have `focus_mode = 0` so Space doesn't get eaten by focused-button activation. Active speed highlighted via modulate.
- [x] **Camera rig** (`scenes/world/camera_rig.tscn`) per `ui.md` "World view camera (locked)":
- Pinch-zoom via `InputEventMagnifyGesture` + mouse-wheel; `target_zoom` lerps smoothly toward intent
- Drag-pan via `InputEventScreenDrag` / `InputEventMouseMotion + left-button held`
- Double-tap-centre with 300 ms / 16 px window, animated by Tween
- `set_world_bounds(rect)` called by `world.gd` once map is built sets Camera2D `limit_*` with 32 px bleed
- No follow-cam
- [x] **Indoor tint shader skeleton** at `art/shaders/indoor_tint.gdshader` (`tint_strength = 0` pass-through). Not yet attached to any TileMapLayer material Phase 13 wires it onto the Floor layer driven by the Roof flag.
- [x] **Acceptance (visual, MCP-verified):** 80² grass field renders, 8×8 stone ring landmark visible at centre, 4 speed buttons render top-left, tick counter updates top-right, `Sim.set_speed()` works (verified via `execute_game_script`), pause freezes the tick counter. Manual interaction in the editor's Play window covers the keyboard/click pathway (MCP's `simulate_key` doesn't route through `_unhandled_input` recorded as a follow-up).
---
## Phase 2 — Pawn skeleton, pathfinding, movement (~2 weeks)
**Goal:** 3 pawns on the map, click-to-move them around. No AI yet.
- [x] **Pawn scene** (`scenes/pawn/pawn.{tscn,gd}`, ~108 lines, `gdscript-refactor` agent): `Node2D` root (not CharacterBody2D grid-snapped lerped movement, no physics needed). `_draw()` paints a coloured disc whose hue is hashed from `pawn_name`, plus dark outline + yellow selection ring. NameLabel above, StateLabel ("idle"/"walking") below. Public API: `setup(name, start_tile)`, `walk_along_path(path)`, `is_walking()`, `set_selected(bool)`. Movement clock = `EventBus.sim_tick`, so pause/Fast/Ultra speeds inherit automatically.
- [x] **Pawn registry on `World` autoload**: `pawns: Array`, `register_pawn(p)`, `unregister_pawn(p)`, `pawn_at_tile(tile)`. (Untyped array `Array[Pawn]` in autoloads hits Godot's class_name-not-yet-registered timing window. Duck-typing is fine here; the only consumers iterate and access `.tile`/`.pawn_name`.)
- [x] **Pathfinder service** (`scenes/world/pathfinder.gd`, ~110 lines, `gdscript-refactor` agent): `AStarGrid2D` wrapper, region 80×80, `DIAGONAL_MODE_NEVER` (Rimworld 4-directional), Manhattan heuristic. API: `setup(map_size)`, `set_cell_walkable(cell, bool)`, `is_walkable(cell)`, `find_path(from, to) -> Array[Vector2i]` (excludes start, includes end). `walkability_changed(cell)` signal fires on every change for Phase 5 subscribers.
- [x] **Walk-to-tile + smooth render**: Pawn's `_process()` lerps render-position between current and next tile each render frame; `_step_progress` advances on each `EventBus.sim_tick`. 1 tile = `STEP_TICKS = 10` ticks 0.5 s at 1× / 0.1 s at 5× / 0.042 s at 12×.
- [x] **Click-to-move via Selection module** (`scenes/world/selection.gd`, ~85 lines, Opus): `_unhandled_input` discriminates click vs drag via 8 px / 300 ms thresholds. Click on a pawn select; click on empty walkable tile while a pawn is selected pathfind + `walk_along_path`. Drags belong to the camera (pan); UI buttons swallow their own clicks.
- [x] **Spike — AStarGrid2D path-query timing**: `Pathfinder.benchmark()` runs all 4-corner pairs × 3 iterations = 36 paths on 80². Result: **avg 9.1 μs, max 18 μs, min 6 μs**. ~55× faster than the "sub-millisecond" target architecture.md's perf claim conservatively confirmed.
- [x] **Acceptance**: MCP-verified via `play_scene` + `get_game_screenshot` + `execute_game_script`. 3 pawns (Bram cyan, Cora purple, Edda pink) at (20/25/30, 40), name + state labels render, selection ring shows on the active pawn. Path from (20,40) to (50,40) routes around the 8×8 stone ring (38 steps vs 30 straight = 8-step detour). Mid-walk screenshot caught Bram at the south-east corner of the ring with state="walking"; arrival snapshot back at (20,40) showed state="idle".
**Phase 2 gotcha noted**: Godot 4 class_name registration happens at editor scan-time, not at headless-load-time. First headless run after authoring a new `class_name`-bearing file fails until the editor (or `mcp__godot-mcp-pro__reload_project`) rebuilds the global class cache. For future agent-written class_name files: reload-project before headless validation.
---
## Phase 3 — AI core: Decision → WorkProvider → JobRunner (~3 weeks)
**Goal:** the 5-layer pipeline from `architecture.md` is real, but with one dummy work category. **Save round-trip for JobRunner mid-toil state is required to land in this phase, not later.**
- [x] **5-layer `Decision` pipeline** (`scenes/ai/decision.gd`, 50 lines, `gdscript-refactor` agent): static `pick_next_job(pawn, providers)`. Layer 1 (incapacitation) probes via `has_method("is_incapacitated")` no-op until Phase 9 adds it. Layer 2 (forced job) consumes `pawn.forced_job`. Layer 3 (status interrupt) reserved for Phase 9. Layer 4 (work) sorts providers by `priority` desc, returns first non-null Job. Layer 5 returns null (idle).
- [x] **`WorkProvider` abstract base** (`scenes/ai/work_provider.gd`, 27 lines, Agent A): `class_name WorkProvider extends Node`, `@export category`, `@export priority`, `find_best_for(pawn)` with `push_error` guard.
- [x] **`Job` + `Toil`** (`scenes/ai/{job,toil}.gd`, 59 + 76 lines, Agent A): `RefCounted` data types with `to_dict`/`from_dict`. Toil kinds: `WALK`/`WAIT`/`IDLE`. Vector2i stored as `to_x`/`to_y` ints (Godot 4 JSON doesn't round-trip Vector2i). Factories: `Toil.walk_to(tile)`, `Toil.wait_ticks(n)`, `Toil.idle()`.
- [x] **`JobRunner`** (`scenes/ai/job_runner.gd`, 186 lines, Agent B): `Node`-derived; `setup(pawn, pathfinder)`, `start_job(j)`, `cancel_job()`, `tick()`. WALK toil delegates to `pawn.walk_along_path()` on first invocation, listens for `walk_completed` signal to mark done. WAIT decrements `ticks_remaining`. IDLE never completes. Full `to_dict`/`from_dict` for save round-trip.
- [x] **Forced job preempts current job** (Pawn orchestration fix): `_orchestrate_ai` calls Decision when `forced_job != null` OR no current job not just when idle. This was a bug found via MCP runtime test; cause + fix documented in commit.
- [x] **First `RestProvider`** (`scenes/ai/rest_provider.gd`, 31 lines, Agent C): `extends WorkProvider`, `@export rest_tile`, returns a `[walk_to(rest_tile), idle()]` Job. Rest tile = (50, 50) just outside the south-east of the stone ring, reachable from all 3 spawn tiles.
- [x] **Idle behavior**: IDLE toil keeps the pawn at the current tile indefinitely. Per architecture.md:72, this is the v1 idle; the wander-locally variant is v2.
- [x] **Pawn `to_dict`/`from_dict`** (Opus): captures `tile`, `_path` (as `[[x,y],...]`), `_step_progress`, `_selected`, `forced_job` (via `Job.to_dict()`), `job_runner` (via `JobRunner.to_dict()`). On load, JobRunner's restored WALK toil has `started: true` and does NOT re-call `walk_along_path` the pawn's restored `_path` continues naturally and emits `walk_completed` when done.
- [x] **`SaveSystem.write_save` / `apply_save`** (Opus): walks `World.pawns`, calls `to_dict()` / `from_dict()` per pawn. Single slot JSON to `user://save_slot.json`. Pawn dicts zipped by index (Phase 16 will add stable IDs).
- [x] **Selection rewrite** (Opus): drops direct `pawn.walk_along_path` call; now builds a `[walk_to(tile), idle()]` Job and sets `pawn.forced_job = job`. Decision picks it up on the next sim tick.
- [x] **Acceptance — MCP-verified end-to-end**:
- 3 pawns boot Decision assigns each a Rest job JobRunner starts each all 3 walk to (50, 50) on different paths (40/35/30 steps) all 3 arrive and idle.
- Force Bram to (10, 10) via `pawn.forced_job` preempt fires (`[decision] Bram: forced 'Go to (10, 10)'`) Bram walks away while Cora/Edda stay parked.
- Mid-walk save: paused Bram at (51, 10) walking to (70, 70) with 79 path steps remaining `SaveSystem.write_save()` mutated to (0, 0) with empty path `SaveSystem.apply_save(read_save())` **restored to (51, 10) with 79 steps remaining, `walking=true`, same job at same toil index** resumed sim Bram continued from (51, 10), reached (70, 26) with 44 steps remaining, still on `Go to (70, 70)`.
- [x] **Status interrupt skeleton — Bleeding hook**: deliberately deferred. Decision's Layer 3 is a placeholder comment for Phase 9 adding it without a Status system to back it is premature. `implementation.md` Phase 9 will land the registry + the interrupt wiring atomically.
**Phase 3 lessons logged:**
- Class-name registration timing (Phase 2 gotcha) bit again fix is the same: `mcp__godot-mcp-pro__reload_project` between authoring `class_name`-bearing files and headless validation.
- `_orchestrate_ai` initially only called Decision when `not has_job()`. The IDLE toil never completes, so a queued `forced_job` was never seen. Fix: trigger Decision when `forced_job != null` regardless of current-job state. Caught by the runtime MCP test, not headless.
- `execute_game_script` with `await Engine.get_main_loop().process_frame` is touchy the MCP wrapper sometimes auto-recovers from a runtime issue but the script's last assignments are lost. The actual game state evolves correctly; just use a fresh `execute_game_script` to inspect state after awaits.
---
## Phase 4 — First verbs: chop, mine, hauling, stockpiles (~3 weeks)
**Goal:** the foundational gameplay loop pawns harvest things and pile them up.
- [ ] Tree entity (chop 3 logs drop), stone-tile mining (mine 1 stone drop), iron-ore-tile mining
- [ ] Item entity: position, type, stack size, on-floor sprite
- [ ] `ChopProvider`, `MineProvider` (subset of Construction work)
- [ ] **Hauling:**
- `HaulingProvider` + Hauling job toils (`walk → pick → walk → deposit`)
- `items_needing_haul` dirty set on `World` (per `architecture.md:243`)
- `StorageDestination` interface (zones first; containers Phase 5)
- **No-destination fallback** (locked decision): drop after 3 retry passes + passive `No stockpile accepts X` alert
- [ ] **Floor stockpile zones:**
- Zone-paint UI (designation paint mode reuses Phase 5 paint controller for now, a quick zone-paint button)
- 16-chip filter grid (Wd/St/Ir/Cu/Ag/Au/Cl/Veg/Mt/Gr/Ck/Md/Tl/Wp/Ar/Co)
- 5-priority cycle (Critical/High/Normal/Low/Off) on the zone
- One-stack-per-tile, one-type-per-tile rule
- [ ] Carry capacity = 1 stack, 1 type (multi-type carry is v2)
- [ ] **Spike (~1 hr):** 16-chip grid mockup on a 720×1280 viewport does it cram? Adjust before building.
- [ ] **Acceptance:** 3 pawns chop / mine / haul to a stockpile. Set the stockpile to wood-only pawns leave stone alone. Set a second stockpile to higher priority and watch wood flow upward.
---
## Phase 5 — Building, walls, floors, containers (~2.53.5 weeks; was ~23, bumped for wood-wall art authoring)
**Goal:** the player can shape the world. End of phase: build a functional wooden cabin, with stone fortress walls available as the upgrade material.
- [ ] Designation paint mode (controller reused later by stockpile-paint, no-roof, etc.) drag-paints ghosts on Layer 3, green-if-placeable / red-if-blocked
- [ ] `BuildJob` queue on `World`, with material requirements
- [ ] Construction WorkProvider: nearest-job-first, hauls materials walks to ghost works N ticks swaps Layer 3 ghost for real Layer 2 wall (autotile fixes neighbours) updates pathfinder
- [ ] **Walls — wood (locked-in via 2026-05-10 audit):**
- **Art:** author corner / T-junction / cap / cross variants on top of `FG_Houses.png` warm-brown timber + blue-roof palette (~½ day pixel art). Bundle has the visual language; the modular pieces don't exist yet.
- **TileSet:** Wang-style terrain definition in Godot, hand-painted variant assignment.
- [ ] **Walls — stone (autotile-solvable as-is):**
- Import `FG_Fortress.png` tan stonework directly. Build TileSet terrain (~few hours, mostly assembly).
- [ ] **WallMaterial enum / data path:** wood vs stone is a tag on the BuildJob; the construction pipeline is identical for both. Wood unlocked from start; stone unlocked once player has stone resource (one-step craft from raw stone? TBD in Phase 6, not here).
- [ ] **Floors:** wood plank, stone, dirt-cleared
- [ ] **Doors:** simple swing-open furniture; pawns walk through; pathfinder treats as walkable, walls don't
- [ ] **Containers (crates):** furniture entity, 4 stacks, 16-chip filter, 5 priorities, all-neighbours-blocked fallback (locked: hold then drop after ~5 sim sec)
- [ ] Deconstruction (reverse build job)
- [ ] **Acceptance:** Player paints a 6×4 cabin outline pawns haul wood walls go up floor + door drop a crate inside set crate filter to "tools" tools auto-flow into it.
---
## Phase 6 — Production: workbenches, recipes, bills, quality (~3 weeks)
**Goal:** crafting chains end-to-end with the full Rimworld bill semantics.
- [ ] **5 workbenches:** carpenter, smelter, smithy, cooking hearth, millstone
- [ ] **Recipe registry** `data/recipes/*.tres`, ~22 recipes per `design.md`
- [ ] Recipe DSL: ingredients (with optional quality filter), product (count, type), workTime, skill (Crafting or Cooking), skillThreshold
- [ ] **Bill semantics:**
- Modes: one-shot count / forever / until-N-in-stockpile
- Ingredient quality minimum filter (e.g. "Excellent+ iron ingots only")
- Skill threshold gate
- Bill round-trip in saves
- "Bill blocked" alert when no ingredients qualify (open question drives Phase 17 alerts)
- [ ] CraftingProvider + CookingProvider (per `architecture.md:559` 9-list)
- [ ] **Quality system** (Shoddy/Normal/Excellent/Masterwork/Legendary) additive: skill × 0.04 + RNG; multiplicative stat bonus; quality stamped on every crafted item
- [ ] **Spike (~2 hr):** prototype the recipe-as-Resource format. Does the bill UI fit in a single bottom-sheet? Adjust before authoring 22 recipes.
- [ ] **Acceptance:** smelt iron, smith a sword. Watch quality vary by smith skill. Set a bill "until 5 swords in stockpile" pawns stop at 5, restart when one is taken.
---
## Phase 7 — Plants, cooking, hunger (~2 weeks)
**Goal:** food loop from seed to belly.
- [ ] 34 crops: wheat, potato, berry-bush, hop (final picks TBD; ElvGames bundle pickings audit-driven)
- [ ] Plant tile state machine: tilled sown growing (4 stages) ready
- [ ] Plant WorkProvider: till + sow + harvest (matches the 9-category list)
- [ ] **"Plants don't grow indoors"** rule depends on Layer-4 Roof flag, which doesn't exist yet at this phase. Stub it (always-outdoor) and revisit in Phase 13.
- [ ] Cooking: hearth recipes (raw ingredient meal), shelf-life on meals
- [ ] Eating: pawn walks to nearest meal, consumes, hunger drops
- [ ] **Hunger need** + thought (`design.md` mood section)
- [ ] **Acceptance:** full grain flour (millstone) bread (hearth) eat loop. Hungry pawn auto-prioritises eating.
---
## Phase 8 — Sleep, mood, thoughts (~3 weeks)
**Goal:** pawns have an interior life. Mood swings drive behaviour.
- [ ] Beds (furniture, "owned by pawn", quality affects sleep)
- [ ] Sleep need + sleep-mood gradient (placeholder numbers `+5/+0/2/5/8` from `design.md` Tunables)
- [ ] Tired status, decision-pipeline override (sleep when low)
- [ ] **Mood thought registry** data-driven, ~13 thoughts per `design.md`
- [ ] Mood compute (`architecture.md:318`): base 50 + sum(modifier × min(stacks, MAX_STACKS_PER_THOUGHT)). **`MAX_STACKS_PER_THOUGHT = 5`** (locked).
- [ ] Thoughts: persistent (state-driven) or event (decay over hours), with stacking rules
- [ ] **Soft breaks** Sulking and Wandering, fire when mood < 25 sustained for 30 in-game min, recover at mood 35
- [ ] Mood UI: small mood bar on pawn portrait, breakdown in pawn-detail (Phase 17)
- [ ] **Acceptance:** Force-create misery (cold, hungry, no bed, sees corpse): mood plummets, pawn enters Sulking, meet needs, recovers.
---
## Phase 9 — Status effects + Medicine (~23 weeks) — ✅ done 2026-05-11
**Goal:** the full status-driven drama from `design.md` Health section.
- [x] **Status registry skeleton:** Bleeding + Downed shipped (Hungry/Tired live as Needs in Phase 7/8; Sick/Wet/Cold deferred registry/data-class shape ready for them)
- [x] Each status: PERSISTENT/EVENT lifetime, severity stacks (max=3), gameplay effect (Bleeding ticks HP loss; Downed gates work via Decision Layer-1 incapacitation interrupt)
- [x] **Bleed-out timer** (`BLEED_OUT_TICKS` = 1200 ticks demo value; **6 in-game hours = 432000 design value** documented in `status_catalog.gd` flip on first time-balance pass)
- [x] DoctorProvider work category (priority 9 highest)
- [x] **Medical bed** furniture (`bed.is_medical=true`, red cross marker draws on pillow)
- [x] Treatment job: walk rescue walk treat KIND_RESCUE + KIND_TREAT toils, JobRunner `_tick_rescue`/`_tick_treat` (Medicine skill tuning deferred to Phase 20)
- [ ] Doctor interrupt prioritization Combat=Off doctors still volunteer for medicine (Combat work category not yet split; revisit when weapons land)
- [x] **Downed → rescue model:** downed pawns wait for any non-downed pawn; DoctorProvider scans `World.pawns` for nearest. Death-on-timeout deferred to Phase 14
- [x] **Acceptance:** Wounded Bram (75 dmg + Bleeding stack=2 hp 25, Downed) Edda + Cora both volunteered via `doctor → 'Rescue Bram → bed at (47, 24)'` walked snapped Bram to medical bed treated Bram healed to 94.2 hp, statuses cleared, returned to work. Verified MCP runtime + screenshot.
---
## Phase 10 — Combat + Wolves (~3 weeks) — ✅ wolf-first slice done 2026-05-11
**Goal:** real threat, real defense, real consequences.
- [ ] **3 weapons:** sword (melee), axe (melee, slow), bow (ranged) stats per `design.md` (deferred wolves attack unarmed pawns; player weapons land later)
- [ ] **3 armor slots:** helm, cuirass, boots (deferred pair with weapons)
- [ ] Equipment system on pawn (carry + active slots) (deferred)
- [x] **Hit math:** two-roll resolution shipped on wolf side (70% hit + damage roll). Skill/range/cover modifiers deferred until pawn-side weapons.
- [ ] **Cover:** walls = 40%, trees = 20% (deferred pair with weapons)
- [ ] Combat priority semantics (deferred pair with weapons)
- [ ] **Friendly fire ON** (deferred pair with bow)
- [x] **Wolf entity:** `class_name Wolf extends Node2D`, 4-state machine APPROACH ENGAGE FLEE DEAD (FLEE is no-op stub, expand when injury threshold lands)
- [x] Wolf spawn: WolfSpawner triggers at `darkness_factor ≥ 0.8`, packs of 12 (design target 14 tune up post-tuning pass), random map-edge cluster, 1-in-game-day cooldown. Season-weighting deferred to Phase 12.
- [ ] **Spike (~half day):** combat feel test partly satisfied by wolf-only test; full spike awaits player weapons
- [x] **Acceptance (partial):** Wolf raid at night verified `[wolf] RAID: 1 wolf(ves) spawned at [(53, 2)]` at day 3 22:00, multiple raid cycles across nights (4 wolves alive by day 4 01:51). Pawn-side defense + downed cascade verified by Phase 9 manual injury full wolf-bites-pawn bleed doctor chain awaits pawn-side melee.
---
## Phase 11 — Day/night + Lighting (~12 weeks)
**Goal:** the world has a rhythm; night feels different.
- [ ] Time-of-day clock (already in tick loop surface to UI)
- [ ] Top-bar clock + day counter ("Day 14, 6 AM")
- [ ] **Light sources:** torch furniture, hearth furniture (already exists for cooking), candle. Each has radius (max 8) and on/off state.
- [ ] `light_map` compute (per `architecture.md:366`) recompute on light-source-change only, not every tick. Touches 200 cells per change.
- [ ] **Night shader:** sample `light_map`, brighten lit tiles, darken unlit. Smooth dawn/dusk transition (~30 in-game min each)
- [ ] "In darkness" mood thought integration fires for pawns standing in unlit cells at night
- [ ] **Acceptance:** Day dusk night dawn cycle visible. Indoor lit areas glow; outdoor unlit areas are dim. Pawn in a dark room at midnight gets the mood thought.
---
## Phase 12 — Seasons + Weather (~12 weeks) — ✅ done 2026-05-11
**Goal:** 48-day year cycle with daily weather variety.
- [x] **48-day year:** 4 seasons × 12 days. Subtle seasonal palette modulate on Terrain TileMapLayer (spring slight-green, summer neutral, autumn warm-orange, winter cool-blue).
- [x] **Daily weather roll** `Weather` autoload, season-weighted (placeholders, tune Phase 20). Rolls once per in-game day on `Clock.day_index_from_start()` change.
- [x] Rain visual `scenes/world/rain_overlay.tscn` procedural `_draw()` diagonal raindrops on a CanvasLayer. Ambient SFX deferred (audio is a later polish pass).
- [x] Storm = rain + screen white-flash (Tween-driven ColorRect, random 48s interval); wet gain rate doubles during storms.
- [x] Cold snap fires as a weather kind in autumn/winter rolls (3% / 20% respectively). Doubles cold gain rate regardless of season when active.
- [x] **Wet status** Damp at 25, Soaked at 60 of a 100-scale `_wet_accum` per pawn. Accumulates when `Weather.is_raining()` and `not _is_sheltered()`. Decays under shelter. Mood thoughts `damp` (-3) / `soaked` (-6).
- [x] **Cold status** accumulates in winter outdoors OR during cold_snap. Mood thought `cold` (-4). Severity tiers 13 (mild/severe/extreme).
- [x] **Season indicator UI** top-bar `SeasonLabel` shows "Spring 4/12" via Strings.t() with localizable season names. Forecast-tooltip deferred to UI pass.
- [x] **`_is_sheltered()`** v1: tile has a floor entity below it. Documented stand-in for Phase 13 Room/Roof BFS.
- [x] **Acceptance:** Forced Bram's `_wet_accum=26` under rain `Bram now Damp (wet=26.0)`. Pushed to 65 `Bram now Soaked (wet=65.0, sev 1→2)` + Soaked mood thought (-6) applied, mood dropped to 30. Forced cold_snap + Cora cold=30 `Cora now Cold severity 1` + Cold mood thought (-4), mood 32. Daily weather rolls visible across days 05 (rain clear rain clear rain). Storm flash captured mid-animation in screenshot.
---
## Phase 13 — Rooms, roofing, beauty, dirtiness, cleaning (~23 weeks) — ✅ done 2026-05-11
**Goal:** built-environment systems your cabin matters now.
- [x] **RoomDetector** at `scenes/world/room_detector.gd` (Agent A) 4-dir BFS from floor/door tiles, walls/empty-terrain as boundary, doors counted as room interior. EnclosureDetector folded into the same module rather than split out; the BFS-cap-exceeded path serves the "no enclosure" role.
- [x] **Auto-roof BFS** `Room.ROOM_AUTOROOF_CAP = 16`. Discovery BFS allows up to 4× that for the too-large warning. Roof flag stored on the Room instance as `is_under_roof`.
- [x] **No-Roof designation** `Designation.TOOL_NO_ROOF` paint tool wired into the dispatch + atlas + ghost system. UI button activation deferred to Phase 17 build-drawer.
- [x] **`room_too_large` signal** `EventBus.room_too_large(top_left: Vector2i, cell_count: int)`. UI banner consumer deferred to Phase 17.
- [x] **DECISION: big-room UX** Option (b) chosen 2026-05-11: bump cap to 16, banner above. Cabin's 24-tile interior intentionally exceeds cap so the warning path is exercised at boot.
- [x] **Indoor tint** `scenes/world/indoor_tint_overlay.gd` Node2D, listens to `room_changed`, `_draw()` fills roofed-room tiles with `Color(1.0, 0.95, 0.85, 0.10)`. Deliberately subtle.
- [x] Plants-don't-grow-indoors `Crop._on_sim_tick` skips stage advancement when `World.is_indoor(tile)`; one Audit line per crop on first detection.
- [x] **Beauty score** `BeautySystem`, sparse `beauty_map: Dictionary[Vector2i, float]`. Linear falloff over 3 tiles. Base values: Bed +2, Workbench +1, Torch +3, Hearth +4. Quality multiplier: SHODDY 0.5, NORMAL 1.0, EXCELLENT 1.5, MASTERWORK 2.0, LEGENDARY 2.5.
- [x] **Dirtiness** `DirtinessSystem`, 0..100 scale, tier crossings (clean<25, dirty<60, filthy60) emit `tile_dirtiness_changed`. Traffic dirt via `bump_pawn_traffic(tile, indoor)`. Combat/corpse spike API stubbed for Phase 14.
- [x] **CleaningProvider** `scenes/ai/cleaning_provider.gd`, priority=2 (between haul=3 and rest=0). `KIND_CLEAN` toil, 2.5 dirt/tick over ~40 ticks.
- [x] Room thoughts `clean_room`(+2), `dirty_room`(-3), `filthy_room`(-6), `beautiful_room`(+4), `ugly_room`(-3), `slept_in_room`(+3 EVENT, wires Phase 17), `ate_without_table`(-3 EVENT, wires Phase 17). All synced in `Pawn._process_thoughts` via avg over room tiles.
- [x] **`Pawn._is_sheltered()`** rerouted from "has floor below" to `World.is_indoor()` with the floor-proxy as graceful fallback (Phase 12 debt paid).
- [ ] **Spike (~1 hr):** 50+ room stress test deferred; current MVP demo (2 rooms) shows no perf concern. Run pre-launch.
- [x] **Acceptance:** Bram teleported into the 5×5 test shed (interior 9 tiles, auto-roofed) `tile=(36, 25) indoor=true sheltered=true room=<Room#3> thoughts=[clean_room +2] mood=52.0`. Cabin (24 tiles, over cap) detected as `Room#2 is_under_roof=false` with `room_too_large(top_left, 24)` warning emitted. Screenshot shows test shed walls + interior floor visible, cabin warmly lit, "Spring 1/12" + Day 1 07:52 in top bar.
---
## Phase 14 — Death, corpses, burial (~12 weeks) — ✅ done 2026-05-11
**Goal:** close the death loop properly.
- [x] **Corpse entity** at `scenes/entities/corpse.gd` DECAY_PER_TICK=0.05 (~33 in-game min from freshrotted at 1×), `is_rotting()` at decay50, queue_free + `corpse_rotted_away` signal at decay100. Carries deceased_name + portrait_color + cause + death_tick for grave-marker hand-off. Rotting corpses bump DirtinessSystem (Phase 13 hook).
- [x] **GraveyardZone** `StorageDestination` subclass with `accepted_types = [&"corpse"]` locked. Brownish overlay. Wired into Designation as `TOOL_GRAVEYARD`.
- [x] **Grave dig job** GraveSlot entity built via ConstructionProvider (no special-case needed; duck-typed `is_buildable()` works). `TOOL_DIG_GRAVE` paint mode.
- [x] **Permanent GraveMarker** spawns when corpse reaches a dug GraveSlot; carries deceased identity for the Phase 17 tap-to-detail UI. Procedural stone-cross _draw. Survives saves.
- [x] **Cremation pyre** `CremationPyre extends Workbench` + `cremate_corpse` recipe (1 corpse + 5 wood 1 ash, 60 ticks). Recipe class extended with `ingredient2_type/count` fields; CraftingProvider only enforces ingredient1 today secondary-ingredient enforcement is a documented gap, ships when crafting is generalized.
- [x] **TYPE_ASH** item type added to Item constants + ALL_TYPES filter array.
- [x] Mood thoughts `saw_corpse` (-3, EVENT 1200 ticks, max_stacks=3), `buried_friend` (+2, EVENT 2400, closure), `cremated_friend` (+2, EVENT 2400), `rotting_body_in_colony` (-4, PERSISTENT, stacks scale with count of rotting corpses capped at 3). Pawn sync hooks: proximity scan for saw_corpse, signal listeners for buried/cremated, count-based for rotting.
- [x] Death pipeline `Pawn.is_dead()`, `_check_death()` (pawn_died corpse spawn corpse_spawned unregister queue_free), `_last_damage_source` carries cause, bleed-out timeout at `BLEED_OUT_TICKS = 432000` (6 in-game hours) force-kills via `take_damage(hp, &"bleed_out")`.
- [x] HaulingProvider extended iterates `World.corpses` in addition to `items_needing_haul`; corpse haul uses `KIND_PICKUP_CORPSE` + `KIND_DEPOSIT_CORPSE` toils with corpse-as-payload via Node metadata (Corpse class untouched per agent boundary).
- [x] **Acceptance:** `DEMO_PHASE14_AUTOKILL` toggle in `world.gd` force-kills first pawn at tick 50. Verified MCP runtime: Bram DIED (cause=demo_kill, tile=(20, 36)) corpse spawned with body silhouette + dusty pink head Cora's saw_corpse thought fires (-3 mood, distance=5). Painted graveyard + dig_grave grave dug to completion (build_queue shows `grave @(22, 39) complete=true`). Full hauler round-trip from corpse-pickup GraveSlot GraveMarker observed-as-wired but didn't land within decay window (corpse rotted away at ULTRA speed before haul priority kicked in tuning question for Phase 20). Pipeline correctness verified; throughput tuning deferred.
---
## Phase 15 — Storyteller (~23 weeks) — ✅ done 2026-05-11
**Goal:** the world prods the player without overwhelming them.
- [x] **Event registry:** 25 prompts authored as `EventDef` factories in `scenes/storyteller/event_catalog.gd`. Inline English copy + parallel keys in `Strings` catalog for i18n. (Choice: code-as-data over `.tres` for MVP simplicity; `.tres` deferred to Phase 17 hot-reload polish.)
- [x] **Daily 6 AM roll** `Storyteller._on_phase_changed(&"dawn")` with `_last_rolled_day_index` guard. Test hook `Storyteller.roll_today()` for MCP.
- [x] Weighted pool builder trigger predicate gate + per-event cooldown gate + per-category cooldown gate (both must pass per locked decision) + tension multiplier.
- [x] **Cooldowns:** per-event from `def.cooldown_days`; per-category from `Storyteller.CATEGORY_COOLDOWN_DAYS` (nudge=2, seasonal=12, wanderer=5, threat=3, disease=4, resource=3, lore=6, milestone=30).
- [x] **Tension model** 0..100 running score, decays 3/roll, +15 on THREAT fire (net +12). Category multiplier: THREAT = lerp(2.0, 0.3, tension/100), RESOURCE = lerp(0.5, 1.5, tension/100), others = 1.0.
- [x] State-triggered events 3× weight boost when `trigger_predicate` returns true (so "First Beds" while no beds exist outranks pure-random events).
- [x] **Banner UI** `scenes/ui/storyteller_banner.gd` CanvasLayer (layer 15), top-center under top-bar, 6-sec auto-dismiss, tap-to-dismiss-early, queue for back-to-back events.
- [x] **Modal auto-pause** `scenes/ui/storyteller_modal.gd` CanvasLayer (layer 20), centered PanelContainer, full-screen 0.45 dim. Auto-pause via `Sim.set_speed(PAUSE)` in Storyteller._fire before showing UI for THREAT/WANDERER/DISEASE-modal. 0/1/2 choice buttons rendered.
- [x] **"Go there" jump-to-alert** `camera_rig.pan_to_tile()` helper added; banner/modal include the button when `event.focus_tile != Vector2i(-1,-1)`.
- [x] **Ghost state + Wanderer auto-fire** `Storyteller._on_pawn_died` flips `ghost_state = true` when `World.pawns.size() == 0`, schedules wanderer fire at `randi_range(3, 5)` days later. Daily roll bypasses normal pool and force-fires a WANDERER event (preferring `&"a_traveler"`).
- [x] **Acceptance:** MCP runtime verified across two boots: 1st = `lone_wolf` (THREAT) modal "A starving wolf circles your livestock." with Prepare/Dismiss + auto-pause `[sim] speed NORMAL → PAUSE (tick 1)`. Resolve via `Storyteller.resolve_current(0)` tension bumped 2742, sim resumed. 2nd boot = `an_old_map` (LORE/BANNER) non-blocking banner. Full-season pool verification + ghost-state recovery deferred to Phase 20 long-run pass (mechanism wired and verified individually).
**Known UI polish gaps (Phase 17):**
- Wolf-spawn effect path stubbed (`EventBus.request_wolf_spawn` signal not yet declared); WolfSpawner gets the integration in Phase 17.
- Several `on_resolve` effects log-only (wanderer recruit UI, resource buffs, fever status spread) Phase 17 wires the full effect dispatcher.
- Modal "Go there" closes the modal; alerts-log replay is Phase 17.
- 3+ choice modals UI renders first 2 buttons; not used by current corpus.
---
## Phase 16 — Save/load full coverage (~12 weeks) — ✅ done 2026-05-11
**Goal:** the save round-trip from Phase 3 expanded to every system. Mid-tick suspend safe.
- [x] All entity types serialize 18 `to_dict`/`from_dict` pairs, each tagged with `class_id` for the loader: wall/floor/door/bed/torch/crop/item/workbench/tree/rock/crate/corpse/grave_marker/grave_slot/graveyard_zone/stockpile_zone/wolf/pawn.
- [x] Tilemap layers `World.save_tilemap_layers()` / `apply_tilemap_layers()` covering Terrain/Floor/Wall/Designation/Roof (Fog runtime-only, skipped).
- [x] Storyteller state `Storyteller.save_dict()` already shipped Phase 15; SaveSystem v2 wires it in.
- [ ] Bill mid-fetch/mid-craft Workbench.to_dict serializes bills + label_text. Per-tick state for in-flight pickups isn't yet serialized; pawns mid-craft restart the toil from scratch on reload (acceptable for MVP).
- [x] Pawn deep state thoughts/statuses/needs/hp/portrait_color/wet_accum/cold_accum/bleed_ticks all round-trip via Pawn.to_dict (carried forward from Phases 3/8/9/12/14). JobRunner toil index round-trips for walk; multi-toil INTERACT/BUILD restarts from toil 0 (acceptable).
- [x] **Autosave** `autoload/autosave.gd` autoload: periodic every 6000 sim ticks (~5 in-game min at 20 Hz) + `NOTIFICATION_APPLICATION_PAUSED` (mobile) + `NOTIFICATION_WM_WINDOW_FOCUS_OUT` (desktop). Gated by `_busy` flag tied to `save_started/save_finished` so no nested writes.
- [x] **"Welcome back" toast** `scenes/ui/resume_toast.gd` CanvasLayer at top-center. Computes minutes/hours from `real_seconds_away = Time.get_unix_time_from_system() - payload.saved_at_unix`. Auto-fades after 5s.
- [x] Slot management `&"manual"` (Save button) + `&"autosave"` (autosave triggers). Public API: `save_to_slot`/`load_from_slot`/`has_save`/`delete_save`/`peek_save_metadata`.
- [x] Save version bumped to `SAVE_VERSION = 2`. Mismatch on load shows a warning dialog in LoadMenu ("This save is from an older version vN loading may fail"); player can continue or cancel.
- [x] **User-driven save/load UI** Save button (💾) + Load button in TopBar's ButtonRow. Load opens `LoadMenu` CanvasLayer showing "Manual save (Date Time)" + "Autosave (Date Time)" slot rows; each clickable; cancel button.
- [x] **Acceptance:** MCP runtime verified. Saved at tick 1137 (113 entities serialized: pawns + furniture + workbenches + crops + items + stockpiles + walls + floors + torches + beds), advanced sim to tick 4600 at ULTRA speed (different state), called `load_from_slot(&"manual")` `[save] applied slot 'manual': 113 entities, 0 errors, tick=1137, away=34s`. World fully restored: tick=1137, pawns alive=3, all entities re-spawned. Resume toast fires with "seconds_away=34". Screenshot shows TopBar with Save + Load buttons, post-load Lone Wolf storyteller modal from a fresh dawn roll.
---
## Phase 17 — Touch UX completion (~34 weeks) — ✅ MVP-cut done 2026-05-11
**Goal:** every interaction has a touch path. No desktop-only gestures.
- [x] **Work-priority matrix** 8 cols × N pawns (Build/Chop/Plant/Mine/Craft/Haul/Clean/Doctor), tap-to-cycle priorities 12340, color-coded cells. Sticky-column / horizontal-scroll / long-press 5-chip picker / swipe-bulk-set deferred to mobile-polish pass.
- [x] **Pawn detail bottom-sheet** right-side, ~360px, opens on tap. HP/Hunger/Sleep bars with threshold colors, current job label, mood + sulking, statuses, top thoughts, skills table, work-priorities readout. Live-refreshes each sim tick. Closes on pawn_died.
- [x] **Build drawer** bottom-sheet with 4 tabs (Designate / Build / Stockpile / Cancel). 12 new tool constants added to Designation (chop, mine, dig_grave, no_roof, build_wall stone+wood, build_floor wood+stone, build_door, build_crate, build_bed, build_torch, build_workbench_carpenter/smelter/millstone/hearth/cremation_pyre, paint_stockpile, graveyard). FAB open button, auto-closes on tool select.
- [x] **Settings menu** modal panel button in TopBar. Auto-pause toggles (4: Threat/Wanderer/Pawn-Down/Modal), audio sliders (stubs), accessibility (large-text + reduce-motion stubs). Persists via GameState.apply_settings.
- [x] **Alerts log** + **storyteller event history** ring buffer 50 entries, severity icons (info=blue, warn=yellow, danger=red), Day/HH:MM timestamps, "Go there" camera pan. Listens to alert_added + storyteller_event_fired + day_ended. Unread badge on Log button.
- [x] **`EventBus.request_wolf_spawn`** wired end-to-end EventCatalog `_spawn_wolves` emits; WolfSpawner subscribes and force-spawns bypassing the darkness/cooldown gates. Threat-event corpus (lone_wolf, pack_hunt, wolves_at_edge) now fires wolves on resolve.
- [x] **Day-summary emission** Clock emits `day_ended(summary)` at dusknight with day/weather/season/pawns_alive/tension/wolves_alive recap. AlertsLog surfaces as a single-line entry per day. Full DaySummaryCard UI deferred.
- [ ] Per-pawn / per-job views layered on the matrix deferred (current matrix has read-write only, no view layers).
- [ ] Stockpile / container 4×4 chip grid UI deferred (paint creates 1×1 zones today; filter UI is data-only).
- [x] **Bill UI for workbenches** (shipped 2026-05-16, out-of-phase). `WorkbenchPanel` bottom-sheet mirrors PawnDetailPanel (layer 18, right-anchored 360 px); tap a workbench bill rows with mode toggle (FOREVER/COUNT/UNTIL_N), target SpinBox, pause CheckBox, remove button; Add-bill popup filters `RecipeCatalog.all()` by `accepted_skill`. Selection chain extended: pawn wins over workbench on shared tile, mutual-exclusion via `EventBus.workbench_selected/deselected`. Closes the Phase 6 stub player-built workbenches are now configurable.
- [ ] "No stockpile accepts X" + "Bill blocked" alerts wiring stubs ready; emit calls in HaulingProvider / CraftingProvider deferred.
- [ ] Day-summary card UI deferred (signal emits; visual card is Phase 17.5).
- [x] **Acceptance:** Hand-test verified TopBar shows Save/Load/Settings/Build/Work/Log[N]; tap Bram right panel shows all his state; tap Build bottom drawer with 4 tabs; tap Work grid of pawn priorities; tap Log scrollable alerts list including the Spring Awakens storyteller event from boot. All UIs touch-friendly (48×48+ targets). Screenshots captured for all 4 surfaces.
**Mouse drag/click already works** the existing tap-to-select / paint-mode click in Selection and Designation handles single-click and drag-to-paint via held-mouse-button in `_unhandled_input`. No additional mouse work needed (user noted this).
---
## Phase 18 — Audio (~1 week)
**Goal:** the game has soundscape; not silent.
- [x] Ambient day loop, ambient night loop (Retro Farming Music 1 + Cozy Melodies Pack 1, OGG)
- [x] UI click + confirm SFX (UI Pack 1)
- [x] Combat hit (Sword Pack 1, on pawn_took_damage)
- [x] Storyteller sting (on storyteller_event_fired)
- [x] Volume sliders in Settings (master / music / sfx; ambient slider present but no bus assigned)
- [x] Audio mute on suspend (NOTIFICATION_APPLICATION_PAUSED + FOCUS_OUT)
- [x] **Acceptance:** music plays at boot (day loop @ dawn), swaps to night loop at phase change, sliders set bus dB live, tree_fell + mine_tick fire on entity completion, MCP runtime probe confirms all paths.
Phase 18 follow-ups (deferred):
- Per-distinct-pawn voice line on damage / death (currently shares one SFX)
- Combat miss / downed / kill stings (only "hit" wired)
- Raid-warning sting on wolf_spawn / threat events (only generic sting today)
- Weather ambient (rain hiss, storm thunder)
- Crossfade between music tracks (current swap is instant)
- Ambient bus + nature SFX (the "ambient" slider has no bus assigned yet)
---
## Phase 19 — Onboarding & first-60-seconds (~12 weeks)
**Goal:** resolve the open question in `memory.md` a new mobile player gets productive in <60 seconds.
- [x] **DECIDED 2026-05-16:** approach **(a) Hint system** contextual single-line overlays during first session, dismissible per-hint, no auto-replay (reset via Settings Reset hints).
- (a) Hint system contextual tooltips during first session, dismissible, no replay
- (b) Guided first-day scripted storyteller events on day 1 *not chosen*
- (c) Tutorial scene separate from main game, opt-in *not chosen*
- [x] First-time-player flag persisted (per-hint dismissal log in `GameState.settings["dismissed_hints"]`; master toggle `show_hints`)
- [x] HintSystem autoload + HintOverlay (layer 22 top-center banner, 7-step tour gated on player events)
- [x] HelpModal (layer 20) with 5 sections (Controls / Verbs / Priorities / Storyteller / Tips)
- [x] SettingsMenu "Onboarding" section: Show hints checkbox + Help button + Reset hints button
- [x] Tooltip pass `tooltip_text` on TopBar buttons + BuildDrawer FAB + 21 tool buttons
- [ ] **Acceptance:** hand the game to a tester cold. They are doing useful colony work within 60 seconds without you saying anything. **Owed — pending playtest review.**
---
## Phase 20 — Balance, polish, export (~24 weeks)
**Goal:** ship-ready.
- [ ] **Tune all placeholders** (`memory.md` Tunable list):
- Sleep mood gradient `+5/+0/2/5/8`
- Wet thresholds 25 / 60 + accumulation rates
- Season weather weights
- Hit-chance bonuses (skill ×5%, range ×5%, cover 40/20%)
- Bleed-out timer (6h)
- Mood thought magnitudes + decay times
- [ ] iOS export setup (needs Mac/Xcode this is the long tail)
- [ ] Android export from Linux
- [ ] **Steam Deck input parity** open question (`memory.md`): gamepad-cursor or D-pad menus or both?
- [ ] **Credits screen** every art pack, every audio pack, every font (compiled across all phases)
- [ ] Bug pass known issues from each phase's parking lot
- [ ] Performance pass profile on a real low-end Android device
- [ ] **Acceptance:** TestFlight build for iOS, signed APK for Android, Steam Deck verified launch. Credits screen complete.
---
## Out of scope (v2+ / explicit cuts)
These are *not* in MVP. Pulling any of them in adds weeks. Each is a known v2 candidate.
- Procgen maps (MVP: fixed seed)
- Multiple biomes (MVP: temperate forest only)
- Bandit raids (MVP: wolves only)
- Butchering animals for meat
- Surgery / limb damage / specific body parts (MVP: single HP + status)
- Background simulation when app is backgrounded
- Fast-forward on long absence
- Tech / research progression tree
- Pawn name / backstory generator (MVP: hand-curated list)
- Multi-pawn carry, multi-type carry
- Per-bench ingredient radius restriction
- Localization beyond English (architecture supports it; content stays EN)
- Post-launch monetization decisions (premium / PWYW / free)
- Multiplayer in any form
- Pets / tame animals
- Trade caravans
- Drugs / alcohol / festivals
- Prisoner mechanics
---
## Scope-cut levers
If 612 months calendar is too long, these are sane reductions ranked by gameplay-cost-per-week-saved:
| Cut | Saves | Cost |
|---|---|---|
| Drop seasons & weather (keep day/night) | ~2 wk | Big kills atmospheric variety |
| Drop dirtiness + Cleaning category | ~1 wk | Small but loses one mood lever |
| Drop combat entirely (no wolves) | ~3 wk | Huge only quiet events left |
| Drop quality system (everything is Normal) | ~1 wk | Medium flattens late game |
| Drop cremation, keep burial only | 0.5 wk | Tiny |
| Reduce skills 5 3 (Labor, Combat, Medicine) | 0.5 wk | Tiny tightens design |
| Drop room beauty score (rooms still detected) | 0.5 wk | Small loses one mood lever |
| Single workbench instead of 5 | ~1.5 wk | Big collapses production design |
| Drop ranged weapons (sword/axe only) | ~0.5 wk | Small loses cover-relevance |
---
## De-risking spikes (run *before* the relevant phase)
| Spike | Phase | Effort | Question to answer |
|---|---|---|---|
| AStarGrid2D timing at 80² with 6 concurrent path queries | Phase 2 | ~30 min | Sub-millisecond per query? |
| 16-chip filter UI mockup on phone viewport | Phase 4 | ~1 hr | Does it cram or fit cleanly? |
| Recipe-as-Resource format prototype | Phase 6 | ~2 hr | Does the bill UI fit a single bottom-sheet? |
| Combat feel test (3 vs 3 on small map) | Phase 10 | ~half day | Does two-roll resolution feel good? |
| Room detection on stress map (50+ rooms) | Phase 13 | ~1 hr | Does rebuild stutter? |
---
## How to use this doc going forward
- **At session start:** check the Status row at top, then jump to the current phase. Read its goal + current open boxes.
- **During a session:** tick boxes as they complete. If the work uncovers something not on the list, add it as a new box (don't silently expand scope across phases).
- **At session end:** if a phase rolled over, update the Status row and add a `### YYYY-MM-DD` entry to `memory.md` Session log noting what landed.
- **DECIDE points** (currently in Phases 13 and 19): when you hit one, propose options + pick before continuing past it. Don't paper over.
- **Spikes:** treat the de-risking spikes as bona-fide tasks, not optional. Skipping them invites the cost-per-week to balloon.
## What lives elsewhere
- **Game design / mechanics** see [`design.md`](./design.md).
- **Tech / engine layout / pawn AI** see [`architecture.md`](./architecture.md).
- **Touch UI / camera** see [`ui.md`](./ui.md).
- **Tilesets / art / license** see [`art.md`](./art.md).
- **Decisions index / open questions** see [`memory.md`](../memory.md).