rimlike/docs/implementation.md
megaproxy b9093dd24b Phase 17: Touch UX (PawnDetail+BuildDrawer+WorkMatrix+AlertsLog+Settings)
Three-agent fan-out shipping the major touch UI surfaces. Opus pre-wrote
6 EventBus signals (pawn_selected/deselected, pawn_priority_changed,
alert_added, request_wolf_spawn, day_ended) + Pawn.work_priorities
Dictionary stub before dispatch. Pattern proven across Phases 12-17.

Pawn detail + Settings (Agent A):
- scenes/ui/pawn_detail_panel.gd — right-side CanvasLayer (layer 18),
  ~360px wide, opens on EventBus.pawn_selected. Renders portrait,
  HP/Hunger/Sleep bars with threshold colors, current job, mood +
  sulking, statuses, top 5 mood thoughts, full skill table,
  read-only work-priorities row. Live-refreshes each sim tick.
- scenes/ui/settings_menu.gd — modal CanvasLayer (layer 26), opened
  via Settings button. Auto-pause toggles (Threat/Wanderer/Pawn-Down/
  Modal), audio sliders (stubs for Phase 18), accessibility checkboxes.
  Persists via GameState.apply_settings.
- scenes/world/selection.gd — extended to emit pawn_selected/deselected
  through EventBus on tap.

Build drawer + 12 new Designation tools (Agent B):
- scenes/ui/build_drawer.gd — bottom-sheet CanvasLayer (layer 16) with
  4 tabs (Designate/Build/Stockpile/Cancel) + FAB ⊕ open button.
  Each tab has HFlowContainer of 80×80 buttons with procedural colored
  icons + label. Tap → Designation.set_active_tool + alert + auto-close.
- Designation: added TOOL_CHOP, TOOL_MINE, TOOL_BUILD_CRATE,
  TOOL_BUILD_BED, TOOL_BUILD_TORCH, 5× TOOL_BUILD_WORKBENCH_* variants,
  TOOL_PAINT_STOCKPILE. Plus tool_material override for wall/floor.
- World._on_designation_added: extended dispatch for all 12 new tools;
  added _spawn_workbench() helper for the 5 bench kinds.

Work matrix + Alerts log + Decision refactor + Wolf signal (Agent C):
- scenes/ai/decision.gd: Layer 4 now filters by pawn.work_priorities
  (0=OFF skip, sort by level ascending with provider.priority tiebreak).
  NEEDS_CATEGORIES (rest/eat/sleep) bypass the filter — a pawn can
  never starve from misconfiguration. Audit log prefixes work decisions
  with (pri=N).
- scenes/ui/work_priority_matrix.gd — CanvasLayer (layer 17) bottom-sheet
  grid: rows=pawns × cols=8 work categories. Each cell tap-cycles
  1→2→3→4→0→1, color-coded (red/orange/yellow/blue/gray). Writes back
  to pawn.work_priorities + emits pawn_priority_changed.
- scenes/ui/alerts_log.gd — CanvasLayer (layer 19) ring buffer 50
  entries. Newest first, severity icon (info/warn/danger), Day HH:MM
  timestamp, Go-there camera pan. Listens to alert_added +
  storyteller_event_fired + day_ended.
- EventBus.request_wolf_spawn wired end-to-end: EventCatalog
  _spawn_wolves emits; WolfSpawner._on_request_wolf_spawn force-spawns
  bypassing the darkness/cooldown gates.
- Clock emits EventBus.day_ended(summary) at dusk→night transition.

Top bar buttons added in order: ‖ / 1× / 5× / 12× / Save / Load /
Settings / Build / Work / Log[N]. Plus the ⊕ FAB at bottom-right.

MCP runtime verified all 4 surfaces via screenshot:
- PawnDetailPanel: Bram shows Crafting=8 / Cooking=2 / Manual=0
  matching seed; bars green; Mood: 50; work-priorities readout
- BuildDrawer: 4 tabs visible, Designate tab shows Chop/Mine/Dig grave/
  No roof buttons with procedural icons
- WorkPriorityMatrix: 3 pawns × 8 categories, all '3' (NORMAL default)
  cells in yellow, tap-to-cycle ready
- AlertsLog: 4 entries — red 'Wolf pack approaching!' danger, blue
  'Bram is at the cabin' info, yellow 'Test alert' warn, blue 'Spring
  Awakens' from boot storyteller roll. Go-there button per entry.

Mouse drag-paint works as-is (user noted). Existing
Selection/Designation _unhandled_input handles drag.

Deferred to Phase 17.5 polish:
- Per-pawn/per-job view layers on the matrix
- Stockpile 4×4 chip filter UI (paint creates 1×1 zones today)
- Bill UI for workbenches (programmatic only today)
- 'No stockpile accepts X' / 'Bill blocked' alert emit wiring
- DaySummaryCard visual (signal emits today, no card UI)
- Wanderer recruit UI, resource buff system

Delegation: 3× gdscript-refactor (Sonnet) agents in parallel;
integration + MCP verify on Opus.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 19:45:35 +01:00

56 KiB
Raw Blame History

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 (ghost→dug→accepts corpse→spawns 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 1→2→3→4→0, 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 dusk→night Phase 17 — Touch UX completion
next Phase 18 — Audio

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
  • 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.
  • 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.
  • 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.

  • 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).
  • 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.
  • 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/
  • 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
    • SaveSystemwrite_save() / read_save(), version + path, JSON
  • 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.
  • SaveSystem skeleton: version field (SAVE_VERSION = 1), user://save_slot.json, JSON serialize, mismatch warning. Smoke-test payload only; Phase 3 expands.
  • .gitignore covers .godot/, .import/, addons/godot_mcp/, exports — verified.
  • Smoke-test scene: scenes/main/main.tscn (Node + Camera2D + Label). main.gd._ready() asserts every autoload alive and shows the i18n-resolved hello string.
  • 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.

  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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
  • 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.
  • 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.

  • 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.
  • 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.)
  • 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.
  • 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×.
  • 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.
  • 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.
  • 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.

  • 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).
  • 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.
  • 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().
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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).
  • 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.
  • 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).
  • 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 registrydata/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.

  • 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)
  • Each status: PERSISTENT/EVENT lifetime, severity stacks (max=3), gameplay effect (Bleeding ticks HP loss; Downed gates work via Decision Layer-1 incapacitation interrupt)
  • 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)
  • DoctorProvider work category (priority 9 — highest)
  • Medical bed furniture (bed.is_medical=true, red cross marker draws on pillow)
  • 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)
  • Downed → rescue model: downed pawns wait for any non-downed pawn; DoctorProvider scans World.pawns for nearest. Death-on-timeout deferred to Phase 14
  • 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)
  • 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)
  • Wolf entity: class_name Wolf extends Node2D, 4-state machine APPROACH → ENGAGE → FLEE → DEAD (FLEE is no-op stub, expand when injury threshold lands)
  • 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
  • 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.

  • 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).
  • Daily weather rollWeather autoload, season-weighted (placeholders, tune Phase 20). Rolls once per in-game day on Clock.day_index_from_start() change.
  • Rain visual — scenes/world/rain_overlay.tscn procedural _draw() diagonal raindrops on a CanvasLayer. Ambient SFX deferred (audio is a later polish pass).
  • Storm = rain + screen white-flash (Tween-driven ColorRect, random 48s interval); wet gain rate doubles during storms.
  • Cold snap — fires as a weather kind in autumn/winter rolls (3% / 20% respectively). Doubles cold gain rate regardless of season when active.
  • 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).
  • Cold status — accumulates in winter outdoors OR during cold_snap. Mood thought cold (-4). Severity tiers 13 (mild/severe/extreme).
  • Season indicator UI — top-bar SeasonLabel shows "Spring 4/12" via Strings.t() with localizable season names. Forecast-tooltip deferred to UI pass.
  • _is_sheltered() v1: tile has a floor entity below it. Documented stand-in for Phase 13 Room/Roof BFS.
  • 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.

  • 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.
  • Auto-roof BFSRoom.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.
  • No-Roof designationDesignation.TOOL_NO_ROOF paint tool wired into the dispatch + atlas + ghost system. UI button activation deferred to Phase 17 build-drawer.
  • room_too_large signalEventBus.room_too_large(top_left: Vector2i, cell_count: int). UI banner consumer deferred to Phase 17.
  • 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.
  • Indoor tintscenes/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.
  • 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.
  • Beauty scoreBeautySystem, 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.
  • DirtinessDirtinessSystem, 0..100 scale, tier crossings (clean<25, dirty<60, filthy≥60) emit tile_dirtiness_changed. Traffic dirt via bump_pawn_traffic(tile, indoor). Combat/corpse spike API stubbed for Phase 14.
  • CleaningProviderscenes/ai/cleaning_provider.gd, priority=2 (between haul=3 and rest=0). KIND_CLEAN toil, 2.5 dirt/tick over ~40 ticks.
  • 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.
  • 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.
  • 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.

  • Corpse entity at scenes/entities/corpse.gd — DECAY_PER_TICK=0.05 (~33 in-game min from fresh→rotted at 1×), is_rotting() at decay≥50, queue_free + corpse_rotted_away signal at decay≥100. Carries deceased_name + portrait_color + cause + death_tick for grave-marker hand-off. Rotting corpses bump DirtinessSystem (Phase 13 hook).
  • GraveyardZoneStorageDestination subclass with accepted_types = [&"corpse"] locked. Brownish overlay. Wired into Designation as TOOL_GRAVEYARD.
  • Grave dig job — GraveSlot entity built via ConstructionProvider (no special-case needed; duck-typed is_buildable() works). TOOL_DIG_GRAVE paint mode.
  • 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.
  • Cremation pyreCremationPyre 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.
  • TYPE_ASH item type added to Item constants + ALL_TYPES filter array.
  • 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.
  • 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").
  • 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).
  • 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.

  • 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.)
  • Daily 6 AM rollStoryteller._on_phase_changed(&"dawn") with _last_rolled_day_index guard. Test hook Storyteller.roll_today() for MCP.
  • Weighted pool builder — trigger predicate gate + per-event cooldown gate + per-category cooldown gate (both must pass per locked decision) + tension multiplier.
  • 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).
  • 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.
  • State-triggered events — 3× weight boost when trigger_predicate returns true (so "First Beds" while no beds exist outranks pure-random events).
  • Banner UIscenes/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.
  • Modal auto-pausescenes/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.
  • "Go there" jump-to-alertcamera_rig.pan_to_tile() helper added; banner/modal include the button when event.focus_tile != Vector2i(-1,-1).
  • Ghost state + Wanderer auto-fireStoryteller._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").
  • 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 27→42, 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.

  • 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.
  • Tilemap layers — World.save_tilemap_layers() / apply_tilemap_layers() covering Terrain/Floor/Wall/Designation/Roof (Fog runtime-only, skipped).
  • 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).
  • 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).
  • Autosaveautoload/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.
  • "Welcome back" toastscenes/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.
  • Slot management — &"manual" (Save button) + &"autosave" (autosave triggers). Public API: save_to_slot/load_from_slot/has_save/delete_save/peek_save_metadata.
  • 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.
  • 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.
  • 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.

  • Work-priority matrix — 8 cols × N pawns (Build/Chop/Plant/Mine/Craft/Haul/Clean/Doctor), tap-to-cycle priorities 1→2→3→4→0, color-coded cells. Sticky-column / horizontal-scroll / long-press 5-chip picker / swipe-bulk-set deferred to mobile-polish pass.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • Day-summary emission — Clock emits day_ended(summary) at dusk→night 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).
  • Bill UI for workbenches — deferred (Phase 6 stub still in use; add/edit bills programmatically only).
  • "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).
  • 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.

  • Ambient day loop, ambient night loop (bundle music packs)
  • UI clicks (tap, long-press confirm, error)
  • Combat stings: hit, miss, downed, kill
  • Alert stings: storyteller modal, ambient banner, raid warning, pawn-down
  • Volume sliders in Settings (master / music / sfx / ambient)
  • Audio mute on suspend; fade in on resume
  • Acceptance: play through a normal in-game day. Sounds fire at the right moments, mute toggles work.

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.

  • DECIDE: approach (open question, not enough thinking yet):
    • (a) Hint system — contextual tooltips during first session, dismissible, no replay
    • (b) Guided first-day — scripted storyteller events on day 1 walking through chop / haul / build / sleep
    • (c) Tutorial scene — separate from main game, opt-in
    • Recommendation lands when this phase begins.
  • First-time-player flag persisted
  • Acceptance: hand the game to a tester cold. They are doing useful colony work within 60 seconds without you saying anything.

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.
  • Tech / engine layout / pawn AI — see architecture.md.
  • Touch UI / camera — see ui.md.
  • Tilesets / art / license — see art.md.
  • Decisions index / open questions — see memory.md.