Three-agent fan-out — Opus pre-wrote Room class, World.rooms/room_at_tile/is_indoor, 4 EventBus signals before dispatch so the slices ran fully parallel. DECISION: Big-room UX = bump auto-roof cap to 16, banner above. Cabin (24 tiles) intentionally exceeds cap to exercise the warning path; a 5×5 test shed (9 interior tiles) was added to exercise the roof path. Room detection (Agent A): - scenes/world/room.gd — class_name Room, tiles/bounds/is_under_roof, contains_tile() bounds-then-list-checked, recompute_bounds() - scenes/world/room_detector.gd — class_name RoomDetector, BFS 4-dir from floor/door tiles, walls/terrain as boundary, doors counted as room interior. Detects up to 4× cap; auto-roofs only ≤16. - World.mark_wall_tile/mark_floor_tile/mark_door_tile hook BFS recompute - Door._complete() now erases wall-layer stamp + registers door tile - Designation.TOOL_NO_ROOF paint mode wired (UI button deferred Phase 17) - EventBus.room_changed / room_too_large signals Indoor/Shelter (Agent B): - Pawn._is_sheltered() rerouted: World.is_indoor() first, floor-proxy fallback - IndoorTintOverlay Node2D — _draw fills roofed-room tiles at α=0.10 warm - Crop._on_sim_tick skips stage advance when World.is_indoor(tile) Beauty + Dirtiness + Cleaning + Room thoughts (Agent C): - BeautySystem sparse map, linear falloff radius=3, Quality multiplier (SHODDY 0.5 → LEGENDARY 2.5). Base: Bed +2, Workbench +1, Torch +3, Hearth +4 - DirtinessSystem 0-100, tier crossings (clean<25/dirty<60/filthy≥60) emit tile_dirtiness_changed. bump/bump_clean/bump_pawn_traffic API - CleaningProvider priority=2, KIND_CLEAN toil, 2.5 dirt/tick for ~40 ticks - Bed/Torch/Workbench _complete() now register with BeautySystem - 7 room mood thoughts: clean_room (+2), dirty_room (-3), filthy_room (-6), beautiful_room (+4), ugly_room (-3), slept_in_room (+3 EVENT, wires Ph 17), ate_without_table (-3 EVENT, wires Ph 17) - Pawn._sync_room_thoughts called from _process_thoughts after cold block, defensive against null rooms/systems Integration recovery (Opus): - Agent C's BeautySystem/DirtinessSystem/CleaningProvider/IndoorTintOverlay instantiation in world.gd never landed (only field declarations + entity hooks survived). Added preloads + runtime add_child + autoload bindings + CleaningProvider registration + furniture pre-seed in _ready - Added _prestamp_test_shed_for_room_detector with _spawn_complete_wall/floor helpers so a 5×5 visible shed exercises the auto-roof path at boot MCP runtime verified: - Rooms: cabin Room#2 size=24 roofed=false (room_too_large fires), shed Room#3 size=9 roofed=true (auto-roof active) - beauty_map size=50 around prebuilt furniture; bed at (47,24) beauty=4.0 - Bram teleported to (36, 25) in shed → indoor=true, sheltered=true, thoughts=[clean_room +2], mood=52.0 - Screenshot: shed walls + brown floor visible; cabin warmly torch-lit; Spring 1/12 indicator; Day 1 07:52 Delegation: 3× gdscript-refactor (Sonnet) agents in parallel; integration recovery + MCP verify on Opus. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
46 KiB
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 (1–2 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 |
| ⏳ next | Phase 14 — Death, corpses, burial |
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.pngis 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.pngIS autotile-solvable. 20–30 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 + 2–4-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.godotat 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_itemsstretch,keepaspect).- Re-copy
addons/godot_mcp/from/mnt/d/godot/mcp/addons/godot_mcpand enable inproject.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, signalsSim— tick loop owner, speed/pause state, Speed enum + factor tableGameState— 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 gateSaveSystem—write_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'sInputEventScreenTouch/InputEventScreenDrag/InputEventMagnifyGesturecover them. SaveSystemskeleton: version field (SAVE_VERSION = 1),user://save_slot.json, JSON serialize, mismatch warning. Smoke-test payload only; Phase 3 expands..gitignorecovers.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 . --quitexits 0,[main] Phase 0 smoke test online.prints, no errors. Confirmed. - Acceptance (editor): open
project.godotin 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
TileMapLayernodes inscenes/world/world.tscn(Godot 4.4+ idiom — supersedes the multi-layerTileMap): 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 toart/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 inTICK_INTERVAL_S = 1/20chunks emittingEventBus.sim_tick. Default boot speed = NORMAL.set_speed()resets_accumto 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 havefocus_mode = 0so Space doesn't get eaten by focused-button activation. Active speed highlighted via modulate. - Camera rig (
scenes/world/camera_rig.tscn) perui.md"World view camera (locked)":- Pinch-zoom via
InputEventMagnifyGesture+ mouse-wheel;target_zoomlerps 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 byworld.gdonce map is built — sets Camera2Dlimit_*with 32 px bleed- No follow-cam
- Pinch-zoom via
- Indoor tint shader skeleton at
art/shaders/indoor_tint.gdshader(tint_strength = 0pass-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 viaexecute_game_script), pause freezes the tick counter. Manual interaction in the editor's Play window covers the keyboard/click pathway (MCP'ssimulate_keydoesn'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-refactoragent):Node2Droot (not CharacterBody2D — grid-snapped lerped movement, no physics needed)._draw()paints a coloured disc whose hue is hashed frompawn_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
Worldautoload: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-refactoragent):AStarGrid2Dwrapper, 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_progressadvances on eachEventBus.sim_tick. 1 tile =STEP_TICKS = 10ticks → 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_inputdiscriminates 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
Decisionpipeline (scenes/ai/decision.gd, 50 lines,gdscript-refactoragent): staticpick_next_job(pawn, providers). Layer 1 (incapacitation) probes viahas_method("is_incapacitated")— no-op until Phase 9 adds it. Layer 2 (forced job) consumespawn.forced_job. Layer 3 (status interrupt) reserved for Phase 9. Layer 4 (work) sorts providers byprioritydesc, returns first non-null Job. Layer 5 returns null (idle). WorkProviderabstract base (scenes/ai/work_provider.gd, 27 lines, Agent A):class_name WorkProvider extends Node,@export category,@export priority,find_best_for(pawn)withpush_errorguard.Job+Toil(scenes/ai/{job,toil}.gd, 59 + 76 lines, Agent A):RefCounteddata types withto_dict/from_dict. Toil kinds:WALK/WAIT/IDLE. Vector2i stored asto_x/to_yints (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 topawn.walk_along_path()on first invocation, listens forwalk_completedsignal to mark done. WAIT decrementsticks_remaining. IDLE never completes. Fullto_dict/from_dictfor save round-trip.- Forced job preempts current job (Pawn orchestration fix):
_orchestrate_aicalls Decision whenforced_job != nullOR 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): capturestile,_path(as[[x,y],...]),_step_progress,_selected,forced_job(viaJob.to_dict()),job_runner(viaJobRunner.to_dict()). On load, JobRunner's restored WALK toil hasstarted: trueand does NOT re-callwalk_along_path— the pawn's restored_pathcontinues naturally and emitswalk_completedwhen done. SaveSystem.write_save/apply_save(Opus): walksWorld.pawns, callsto_dict()/from_dict()per pawn. Single slot JSON touser://save_slot.json. Pawn dicts zipped by index (Phase 16 will add stable IDs).- Selection rewrite (Opus): drops direct
pawn.walk_along_pathcall; now builds a[walk_to(tile), idle()]Job and setspawn.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 onGo 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.mdPhase 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_projectbetween authoringclass_name-bearing files and headless validation. _orchestrate_aiinitially only called Decision whennot has_job(). The IDLE toil never completes, so a queuedforced_jobwas never seen. Fix: trigger Decision whenforced_job != nullregardless of current-job state. Caught by the runtime MCP test, not headless.execute_game_scriptwithawait Engine.get_main_loop().process_frameis 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 freshexecute_game_scriptto 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_hauldirty set onWorld(perarchitecture.md:243)StorageDestinationinterface (zones first; containers Phase 5)- No-destination fallback (locked decision): drop after 3 retry passes + passive
No stockpile accepts Xalert
- 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.5–3.5 weeks; was ~2–3, 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
BuildJobqueue onWorld, 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.pngwarm-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.
- Art: author corner / T-junction / cap / cross variants on top of
- Walls — stone (autotile-solvable as-is):
- Import
FG_Fortress.pngtan stonework directly. Build TileSet terrain (~few hours, mostly assembly).
- Import
- 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 perdesign.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:5599-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.
- 3–4 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.mdmood 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/−8fromdesign.mdTunables) - 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 (~2–3 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 instatus_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.pawnsfor 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 1–2 (design target 1–4 — 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 (~1–2 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_mapcompute (perarchitecture.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 (~1–2 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 roll —
Weatherautoload, season-weighted (placeholders, tune Phase 20). Rolls once per in-game day onClock.day_index_from_start()change. - Rain visual —
scenes/world/rain_overlay.tscnprocedural_draw()diagonal raindrops on a CanvasLayer. Ambient SFX deferred (audio is a later polish pass). - Storm = rain + screen white-flash (Tween-driven ColorRect, random 4–8s 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_accumper pawn. Accumulates whenWeather.is_raining()andnot _is_sheltered(). Decays under shelter. Mood thoughtsdamp(-3) /soaked(-6). - Cold status — accumulates in winter outdoors OR during cold_snap. Mood thought
cold(-4). Severity tiers 1–3 (mild/severe/extreme). - Season indicator UI — top-bar
SeasonLabelshows "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=26under 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 0–5 (rain → clear → rain → clear → rain). Storm flash captured mid-animation in screenshot.
Phase 13 — Rooms, roofing, beauty, dirtiness, cleaning (~2–3 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 BFS —
Room.ROOM_AUTOROOF_CAP = 16. Discovery BFS allows up to 4× that for the too-large warning. Roof flag stored on the Room instance asis_under_roof. - No-Roof designation —
Designation.TOOL_NO_ROOFpaint tool wired into the dispatch + atlas + ghost system. UI button activation deferred to Phase 17 build-drawer. room_too_largesignal —EventBus.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 tint —
scenes/world/indoor_tint_overlay.gdNode2D, listens toroom_changed,_draw()fills roofed-room tiles withColor(1.0, 0.95, 0.85, 0.10). Deliberately subtle. - Plants-don't-grow-indoors —
Crop._on_sim_tickskips stage advancement whenWorld.is_indoor(tile); one Audit line per crop on first detection. - Beauty score —
BeautySystem, sparsebeauty_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. - Dirtiness —
DirtinessSystem, 0..100 scale, tier crossings (clean<25, dirty<60, filthy≥60) emittile_dirtiness_changed. Traffic dirt viabump_pawn_traffic(tile, indoor). Combat/corpse spike API stubbed for Phase 14. - CleaningProvider —
scenes/ai/cleaning_provider.gd, priority=2 (between haul=3 and rest=0).KIND_CLEANtoil, 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 inPawn._process_thoughtsvia avg over room tiles. Pawn._is_sheltered()rerouted from "has floor below" toWorld.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 asRoom#2 is_under_roof=falsewithroom_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 (~1–2 weeks)
Goal: close the death loop properly.
- Corpse entity + decay timer: 0–50 fresh / 50–100 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 (~2–3 weeks)
Goal: the world prods the player without overwhelming them.
- Event registry: 25 prompts authored in
design.mdported todata/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 (0–100), 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 3–5 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 (~1–2 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 (~3–4 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 (~1–2 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 (~2–4 weeks)
Goal: ship-ready.
- Tune all placeholders (
memory.mdTunable 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
- Sleep mood gradient
- 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 6–12 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-DDentry tomemory.mdSession 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.