rimlike/docs/implementation.md
megaproxy 5bf0f51efb Phase 3 — Decision pipeline + JobRunner + RestProvider + save round-trip
AI core (scenes/ai/, 5 new files from 3 gdscript-refactor agents in parallel):
- job.gd (59 lines, Agent A): Job class, RefCounted, label + toils + cursor +
  to_dict/from_dict round-trip
- toil.gd (76 lines, Agent A): Toil class, RefCounted; kinds WALK/WAIT/IDLE;
  factories walk_to/wait_ticks/idle; Vector2i stored as to_x/to_y ints
  because Godot 4 JSON.stringify doesn't round-trip Vector2i
- work_provider.gd (27 lines, Agent A): abstract base, class_name, @export
  category/priority, find_best_for() with push_error subclass guard
- job_runner.gd (186 lines, Agent B): Node-derived runner; setup/start_job/
  cancel_job/tick; WALK toil delegates to pawn.walk_along_path on first
  encounter (sets data.started=true), listens for walk_completed signal;
  WAIT decrements ticks_remaining; IDLE never completes; full to_dict/from_dict
- decision.gd (50 lines, Agent C): static pick_next_job(pawn, providers); 5
  layers (incapacitation/forced/status/work/idle); layer 1 probes via
  has_method to stay future-proof for Phase 9
- rest_provider.gd (31 lines, Agent C): extends WorkProvider; @export rest_tile;
  returns [walk_to(rest_tile), idle()] Job

Integration (Opus):
- pawn.gd: added forced_job slot, job_runner ref, _orchestrate_ai called
  before _advance_walk on each sim_tick. Calls Decision when forced_job is
  queued OR when idle — was a bug initially (only-on-idle never preempted
  the never-completing IDLE toil); fixed and caught via MCP runtime test.
  Added to_dict/from_dict for save round-trip; captures tile, _path,
  _step_progress, _selected, forced_job, job_runner via their serializers.
- selection.gd: rewrote to build a forced-job [walk_to + idle] and set
  pawn.forced_job; Decision preempts current job on next tick.
- world.tscn/gd: instantiates RestProvider as child (rest_tile = (50,50)
  just outside the stone ring's south-east, reachable from all 3 spawn
  tiles); registers via World.register_work_provider; attaches a JobRunner
  child to each spawned pawn and wires setup(pawn, pathfinder).
- world.gd autoload: added work_providers list + register/clear methods.
- save_system.gd: write_save walks World.pawns calling to_dict; apply_save
  zips dicts to pawns by index (Phase 16 will add stable IDs).
- main.gd: bootstrap log line bumped Phase 2 → Phase 3.

Acceptance — MCP-verified end-to-end:
- 3 pawns boot, Decision assigns each Rest, JobRunner starts each,
  all 3 walk to (50,50) on different paths (40/35/30 steps based on
  detour around the stone ring), arrive and idle.
- Force Bram to (10,10) via pawn.forced_job; preempt fires:
  [decision] Bram: forced 'Go to (10, 10)'. Bram walks while Cora/Edda
  stay parked.
- Mid-walk save round-trip (the critical Phase 3 acceptance):
  - Paused Bram at (51,10) walking to (70,70) with 79 path steps remaining
  - SaveSystem.write_save() → SaveSystem.apply_save(read_save()) after a
    mutate-to-(0,0)-with-no-path round-trip
  - Restored Bram exactly: tile=(51,10), _path.size=79, walking=true,
    job='Go to (70, 70)' at toil_idx=0 (WALK toil with data.started=true)
  - Resumed sim → JobRunner's WALK toil saw started=true and did NOT
    re-call walk_along_path; the pawn's restored _path continued the walk
    naturally → reached (70,26) with 44 steps remaining, still on the
    same job. The architecture.md 'mid-toil suspend safe' contract is
    provably honored.

Phase 3 gotchas (logged in implementation.md):
- Class-name registration timing bit again (Phase 2 gotcha). Workflow:
  agent writes class_name file → MCP reload_project → headless validate.
- Forced-job preempt requires triggering Decision when forced_job != null,
  not just when idle (IDLE toil never completes).
- execute_game_script + await Engine.get_main_loop().process_frame is
  flaky — MCP auto-recovers but the script's last lines may be lost.
  Workaround: split state-inspection into a fresh execute_game_script.

Delegation report this phase:
- gdscript-refactor (Sonnet) Agent A: Job + Toil + WorkProvider abstract
  base. 3 files, 162 lines.
- gdscript-refactor (Sonnet) Agent B: JobRunner with toil-execution match
  + walk_completed signal handling + full save round-trip. 1 file, 186
  lines.
- gdscript-refactor (Sonnet) Agent C: Decision pipeline + RestProvider.
  2 files, 81 lines.
- Opus: Pawn integration (forced_job slot, orchestration, to_dict/from_dict),
  Selection rewrite, world.tscn/gd wiring, World autoload work_providers
  list, SaveSystem extension, MCP-driven runtime verification including
  the mid-walk save round-trip demo, gotcha logging.

~70% of Phase 3's GDScript was written by subagents.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 21:05:50 +01:00

501 lines
40 KiB
Markdown
Raw 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** |
| ⏳ next | **Phase 4 — First verbs: chop, mine, hauling, stockpiles** |
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)
**Goal:** the full status-driven drama from `design.md` Health section.
- [ ] **Status registry:** Hungry, Tired, Bleeding, Sick, Downed, Wet (Damp/Soaked), Cold
- [ ] Each status: trigger condition, decay rate, gameplay effect (move-speed, work-speed, mood thought, threshold-flips)
- [ ] **Bleed-out timer** (`BLEED_OUT_TICKS` = 6 in-game hours from `design.md:418`, locked)
- [ ] Doctor work category (already in 9-list; provider lands here)
- [ ] **Medical bed** furniture
- [ ] Treatment job: walk fetch supplies walk treat (timed by Medicine skill)
- [ ] Doctor interrupt prioritization Combat=Off doctors still volunteer for medicine
- [ ] **Downed → rescue model:** Downed pawns get a timer; doctor walks them to medical bed; if timer expires before bed, pawn dies
- [ ] **Acceptance:** Wound a pawn bleeds second pawn breaks off work carries to medical bed treats recovery thoughts fire. Try with no doctor available pawn dies, watch the death pipeline run (Phase 14 closes the loop).
---
## Phase 10 — Combat + Wolves (~3 weeks)
**Goal:** real threat, real defense, real consequences.
- [ ] **3 weapons:** sword (melee), axe (melee, slow), bow (ranged) stats per `design.md`
- [ ] **3 armor slots:** helm, cuirass, boots
- [ ] Equipment system on pawn (carry + active slots)
- [ ] **Hit math:** two-roll resolution (hit roll damage roll, armor reduces damage). Bonuses: skill ×5%, range ×5%, cover 40/20%. Numbers placeholder, tune in Phase 20.
- [ ] **Cover:** walls = 40%, trees = 20%. Cover lookup is per-shooter, per-target line.
- [ ] Combat priority semantics: Off "won't fight" Off = "defends if cornered, won't volunteer" (per `design.md:64`)
- [ ] **Friendly fire ON** bow pawns can hit teammates in line
- [ ] **Wolf entity:** Animal class, 4-state machine (APPROACH ENGAGE FLEE DEAD per `architecture.md:700`)
- [ ] Wolf spawn: storyteller-driven, at map edge, in packs of 14, **night only**, season-weighted (more in winter)
- [ ] **Spike (~half day):** combat feel 3 pawns vs 3 wolves on a small map. Does the two-roll resolution feel good? If not, dial numbers before Phase 20.
- [ ] **Acceptance:** Wolf raid at night pawns auto-fight (or flee per priorities) some get downed doctor saves who they can bodies on the field.
---
## 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)
**Goal:** 48-day year cycle with daily weather variety.
- [ ] **48-day year:** 4 seasons × 12 days. Seasonal palette modulate on tilemap (subtle).
- [ ] **Daily weather roll** (`design.md:582`): clear / rain / storm / cold-snap, season-weighted (placeholder weights, tune Phase 20)
- [ ] Rain visual + ambient sfx (sourced from bundle SFX packs)
- [ ] Storm = rain + lightning flashes + dampness rate ×2
- [ ] Cold snap = winter-only, applies Cold status faster
- [ ] **Wet status** accumulation outdoors in rain (Damp at 25, Soaked at 60), decays indoors. Mood thought tiers.
- [ ] **Cold status** in winter outdoors, slower decay than wet
- [ ] **Season indicator UI** (top bar): "Spring 4/12", tap forecast tooltip
- [ ] **Acceptance:** Run 1 in-game year, see all 4 seasons cycle. Trigger rain watch outdoor pawns get Damp, then Soaked. They head indoors and dry off.
---
## Phase 13 — Rooms, roofing, beauty, dirtiness, cleaning (~23 weeks)
**Goal:** built-environment systems your cabin matters now.
- [ ] **EnclosureDetector** + **RoomDetector** (per `architecture.md:967` and 982)
- [ ] **Auto-roof BFS** (≤8 cells, per `architecture.md:983`) sets Layer-4 Roof flag
- [ ] **No-Roof designation** (paint mode) courtyards stay open
- [ ] **`room_too_large` signal** when BFS hits the cap on an enclosed area (locked decision from this session)
- [ ] **DECIDE: big-room UX** (open question in `memory.md`):
- (a) Keep 8 cap, surface "split with an interior wall" banner minimal scope
- (b) Bump cap to ~16, banner at the new threshold
- (c) Detect any enclosed area regardless of size bigger architectural shift
- Recommendation lands here; deferring past Phase 13 means bugs.
- [ ] **Indoor tint** driven by Roof flag wires to the shader skeleton from Phase 1
- [ ] Plants-don't-grow-indoors rule wires up properly (was stubbed in Phase 7)
- [ ] **Beauty score** per cell, derived from nearby furniture × Quality multiplier
- [ ] **Dirtiness** accumulation, traffic-weighted, spike events (blood from combat = +20, corpse decay = +5/h)
- [ ] **Cleaning WorkProvider** (the 9-list category earlier doc text said "8th"; that's stale)
- [ ] Room thoughts: clean/dirty, beautiful/ugly, ate-without-table, slept-in-room
- [ ] **Spike (~1 hr):** room detection on a stress map (50+ rooms). Does it stutter on rebuild?
- [ ] **Acceptance:** Build a kitchen mood reflects it (table, beauty). Bloody combat in bedroom room turns ugly until cleaned. Build a 12-cell enclosed room big-room banner fires (per (a)/(b)/(c) decision).
---
## Phase 14 — Death, corpses, burial (~12 weeks)
**Goal:** close the death loop properly.
- [ ] **Corpse entity** + decay timer: 050 fresh / 50100 rotting / 100 rotted
- [ ] **Graveyard stockpile** special filter: Corpses-only chip
- [ ] **Grave dig job** (Manual Labor) produces a grave slot
- [ ] **Permanent grave marker** entity: tap opens deceased-pawn detail (Phase 17)
- [ ] **Cremation pyre** furniture + recipe (1 corpse + 5 wood ash + brief mood thought for pawns nearby)
- [ ] Mood thoughts: "saw corpse", "buried friend", "cremated friend", "rotting body in colony" (severity scales)
- [ ] Death triggers in pawn pipeline (already wired in Phase 9) end here corpse drops, hauler fires.
- [ ] **Acceptance:** Pawn dies (combat or untreated illness) corpse on the floor graveyard zone painted hauler takes corpse to grave slot digger digs marker placed. Tap marker, see deceased pawn's portrait + 1-line backstory + mood-thought legacy.
---
## Phase 15 — Storyteller (~23 weeks)
**Goal:** the world prods the player without overwhelming them.
- [ ] **Event registry:** 25 prompts authored in `design.md` ported to `data/events/*.tres`
- [ ] **Daily 6 AM roll** picks one event from a weighted pool
- [ ] Weighted pool builder: trigger predicate, **per-event AND per-category cooldowns** (locked: both gates must pass), tension modifier
- [ ] **Cooldowns:** per-event from event def; per-category from `CATEGORY_COOLDOWN` (3 days threats, 5 days wanderers, etc.)
- [ ] **Tension model:** running tension score (0100), high tension reduces threat weight (×0.3), low tension boosts (×2.0)
- [ ] State-triggered events ("First Beds" while no beds exist) at higher weight than random
- [ ] **Banner UI** (ambient, dismissible, no pause) for nudges/seasonal/lore
- [ ] **Modal auto-pause** for wanderer/threat/disease/milestone (player choice)
- [ ] **"Go there" jump-to-alert** integration every alert/banner includes the camera-pan tap (locked)
- [ ] **Ghost state + Wanderer event recovery** when all colonists dead/gone, sim half-speed, wanderer fires in 35 days
- [ ] **Acceptance:** Play a full season, all event categories fire at least once. Trigger ghost state by killing all 3 pawns wanderer arrives within the window.
---
## Phase 16 — Save/load full coverage (~12 weeks)
**Goal:** the save round-trip from Phase 3 expanded to every system. Mid-tick suspend safe.
- [ ] All entity types serialize (pawn, item, furniture, container, corpse, wolf, plant tile)
- [ ] Tilemap layers serialize via `get_used_cells_by_id`
- [ ] Storyteller state (current tension, recent-fired log per event + per category, scheduled events)
- [ ] Bill states (mid-fetch, mid-craft)
- [ ] Pawn deep state: thoughts, statuses, equipment, current job + JobRunner toil index
- [ ] **Autosave on suspend** (mobile platforms `NOTIFICATION_APPLICATION_PAUSED`)
- [ ] **"You've been away X minutes" toast** on resume (no fast-forward in MVP)
- [ ] Slot management: single slot for MVP, manual save + autosave file
- [ ] Save version number; load barfs gracefully on mismatch
- [ ] **Acceptance:** Kill the app mid-anything (mid-haul, mid-craft, mid-bleed-out, mid-storyteller-modal). Reopen. Everything resumes seamlessly. No exceptions, no visual desync.
---
## Phase 17 — Touch UX completion (~34 weeks)
**Goal:** every interaction has a touch path. No desktop-only gestures.
- [ ] **Work-priority matrix** (9 cols × N pawns, sticky pawn-name column, horizontal scroll on phone, tap-to-cycle priority, long-press 5-chip picker, swipe-column bulk-set)
- [ ] **Per-pawn / per-job views** layered on the matrix
- [ ] **Stockpile / container UI** 4×4 chip grid, priority cycle, allow/forbid all
- [ ] **Build drawer** bottom-sheet tabs (Walls / Floors / Furniture / Production / Designate). Material-pick UI when multiple materials match.
- [ ] **Storyteller event modal** vs ambient banner UX
- [ ] **Pawn detail** screen (bottom-sheet, full-height): needs bars, status effects, current job, equipment, mood thoughts breakdown, skill table, deceased-state
- [ ] **Settings** speeds, auto-pause toggles, audio volumes, accessibility
- [ ] **Day-summary card** recap at end-of-day, gives short sessions a stopping point (`ui.md:620`)
- [ ] **Alerts log** + **storyteller event history**
- [ ] **Bill UI** for workbenches (created in Phase 6 stub; full UX here)
- [ ] **"No stockpile accepts X"** alert from Phase 4 hauling fallback wires up here
- [ ] **"Bill blocked"** alert from Phase 6 quality-filter wires up here
- [ ] **Acceptance:** every screen in `ui.md` "Screens still to design" exists and is touch-driven. Hand the device to someone who's never played they can navigate without instruction.
---
## 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`](./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).