G: large_text scales global theme font (14→20 at 1.4×) via new
GameState.get_font_scale + EventBus.settings_changed. reduce_motion
gates ResumeToast fade (HintOverlay already gated).
I: InspectTooltip long-press wired (500ms hold, 12px drift cancel,
tap-to-clear pin). Stale Phase 19 TODO replaced with accurate doc.
H: Pawn.arrived_at_destination now also emitted on
EventBus.pawn_arrived_at_destination; DirtinessSystem subscribes and
bumps indoor traffic dirt (BUMP_INDOOR_TRAFFIC = 0.2). Outdoor-tracked
bump needs Pawn.prev_tile — flagged for Phase 20.
P: CraftingProvider caches ingredient item ref on Job.ingredient_item;
JobRunner._tick_pickup validates is_instance_valid + not being_carried
before the tile scan, cancels cleanly if another pawn grabbed it.
J: rest_provider.gd deleted. Removed @onready + register call from
world.gd, ext_resource + node from world.tscn. Provider count comment
updated to 9.
M: DIRTY_THRESHOLD extracted — cleaning_provider and job_runner now
reference DirtinessSystem.DIRT_DIRTY_THRESHOLD.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three-agent fan-out (gdscript-refactor x3) ships the chosen Phase 19
approach: contextual hints during first session + a Help reference,
plus a sweep of hover tooltips for desktop discoverability.
- HintSystem (autoload) + HintOverlay (layer 22 top-center banner):
7-step tour gated on player events — welcome (boot+2s), pawn select,
build drawer open, stockpile painted, work matrix open, day_ended,
tour_complete. Per-hint dismissals persist as Array[String] in
GameState.settings['dismissed_hints']. Max-3 FIFO queue if hints
chain. Reduce-motion path snaps in/out instead of tweening.
Reset_tour() public API for the Help modal.
- HelpModal (layer 20): 5-tab static reference (Controls / Verbs /
Priorities / Storyteller / Tips). Opens via EventBus.help_requested,
dimmed backdrop, X/Esc/backdrop-tap dismiss. SettingsMenu gains an
'Onboarding' section: Show-hints checkbox, Help button (emits
help_requested), Reset hints button (calls HintSystem.reset_tour with
has_method guard). Pre-existing 'W' keybind reference fixed to 'P'.
- Tooltip pass: tooltip_text via Strings.t on every TopBar button
(10 buttons incl. speed shortcuts), BuildDrawer FAB, and every tool
button in BuildDrawer (21 tools). _add_tool_btn extended with optional
tooltip param. ~34 new tooltip.* string keys.
Contracts pre-written (Opus): EventBus.help_requested, hint_dismissed,
ui_panel_opened signals; GameState show_hints + dismissed_hints
defaults; BuildDrawer.open + WorkPriorityMatrix.open emit
ui_panel_opened so HintSystem can subscribe via one signal.
Also recorded [MED] known bug in memory.md: drag-paint with active
paint tool is eaten by camera drag-pan.
MCP runtime verified: welcome banner fires 2s after boot, dismiss
queues build_drawer hint on next ui_panel_opened, dismissed_hints
persisted as ['welcome'], HelpModal opens via help_requested with
tab switching working (Controls → Tips verified visually).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Theme (scenes/ui/medieval_theme.gd):
- Procedural builder for an app-wide Theme: parchment buttons on
dark-wood border, tan panels, ink text, gold focus ring.
- Applied on the root Window in Main._ready, with a tree-walk that
re-seeds every CanvasLayer's topmost Control (CanvasLayer
interrupts the root-Window theme cascade in Godot 4).
- Late-mounting popups + modals get themed via a child_entered_tree
hook on each CanvasLayer.
Build drawer thumbnails (scenes/ui/build_drawer_thumb.gd):
- New BuildDrawerThumb Control that dispatches on tool_id and draws
a recognisable silhouette of the entity each tool builds. 17 tools
covered: chop/mine/dig_grave/no_roof (Designate), stone+wood walls,
wood+stone floors, door, crate, bed, torch, 5 workbenches
(Carpenter/Smelter/Millstone/Hearth/Cremation Pyre), stockpile,
graveyard.
- Replaces the flat ColorRect placeholder. _add_tool_btn signature
changed from (label, color, callback) to (label, tool_id, callback).
Adds an InspectTooltip CanvasLayer that follows the mouse, samples the
tile under the cursor each frame, and renders a small dark panel with a
short description of whatever's there.
Per-entity describers cover the playable surface:
* Pawn: name + HP + mood + current job
* Tree / rock / big rock: progress %, "marked" tag if designated
* Wall: material + ghost/% if unbuilt
* Floor / door / torch: ghost vs complete state
* Bed: occupant or "available", medical tag
* Crate: full contents broken down by item type and count
* Workbench: label + active bills count
* Item on ground: type + stack size
* Corpse: deceased name + fresh/rotting/rotted state
* Wolf: HP + state
* Grave marker: deceased name
* Stockpile / graveyard zone: name + priority + accepted types
Layer 50 so the tooltip sits above the world but below modals (which
sit at 100+). process_mode = ALWAYS so hovering still works during
storyteller modals. Position auto-flips to the other side of the cursor
when it would overflow the viewport.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Pawn (scenes/pawn/{tscn,gd}, ~108 lines, gdscript-refactor agent):
- Node2D root (no physics — grid-snapped lerped motion); name + state labels
- _draw() paints body disc with hue derived from name.hash(), dark outline,
yellow selection ring when selected
- Clock = EventBus.sim_tick: each tick advances _step_progress by 1/10;
at 1.0 snaps tile to next waypoint, pops path. STEP_TICKS = 10 →
1 tile / 0.5 s at 1×, scales with Sim speed for free (pause/Fast/Ultra)
- _process() lerps render position between current and next tile every
render frame for smooth visual between sim ticks
- Public API: setup, walk_along_path, is_walking, set_selected,
signals walk_started/walk_completed/arrived_at_destination
Pathfinder (scenes/world/pathfinder.gd, ~110 lines, gdscript-refactor agent):
- AStarGrid2D wrapper, 80² region, DIAGONAL_MODE_NEVER (Rimworld
4-directional), Manhattan heuristic
- API: setup, set_cell_walkable (emits walkability_changed signal),
is_walkable, find_path (excludes start tile, includes end), benchmark
- find_path returns empty Array[Vector2i] for OOB endpoints, solid
destination, or disconnected areas
Selection (scenes/world/selection.gd, ~85 lines, Opus):
- Lives as a Node child of World; _unhandled_input handles mouse clicks
- Click-vs-drag discrimination: 8 px max drift + 300 ms max duration →
drags belong to the camera, only true clicks select/command
- Click on pawn → select (yellow ring); click on walkable empty tile
with a pawn selected → pathfinder.find_path + pawn.walk_along_path
World autoload (autoload/world.gd):
- Added pawn registry: register_pawn, unregister_pawn, pawn_at_tile, clear_pawns
- Untyped Array (Array[Pawn] hits Godot's class_name-not-yet-registered
timing in autoload init; duck typing fine for current consumers)
World scene (scenes/world/{tscn,gd}):
- Pathfinder + Selection nodes added as children
- _ready() wires: pathfinder.setup(MAP_SIZE_TILES), walls → pathfinder
(28 cells from 8×8 stone ring marked impassable), selection.bind(pathfinder),
spawns 3 pawns (Bram/Cora/Edda) at (20/25/30, 40), runs spike benchmark
- main.gd bootstrap line bumped Phase 1 → Phase 2
i18n: 2 new keys (pawn.state.idle, pawn.state.walking)
Spike result — AStarGrid2D path-query timing at 80²:
- 36 paths (all 4-corner pairs × 3 iterations)
- min 6 μs, avg 9.1 μs, max 18 μs
- ~55× faster than the 'sub-millisecond' target in architecture.md
MCP runtime verification:
- play_scene → 3 pawns visible with distinct hashed-hue body colours
- execute_game_script: pathfinder.find_path((20,40)→(50,40)) returns
38-step path (30 straight + 8 detour around the ring)
- bram.walk_along_path(path) → screenshot caught him mid-walk on south
side of ring with state='walking' + selection ring visible
- arrival snapshot: state='idle'
Phase 2 gotcha (documented in implementation.md): class_name registration
happens at editor scan-time, not headless-load-time. First headless run
after authoring class_name files fails until reload_project rebuilds the
global class cache. Workflow: agent writes → MCP reload_project → headless
validate. Documented for future phases.
Delegation report this phase:
- gdscript-refactor (Sonnet) #1: Pawn class — scene, script, draw logic,
movement loop, i18n keys. ~108 lines pawn.gd + 22 lines pawn.tscn.
Headless-validated by the subagent (note: validated before world.gd's
Pawn reference was added).
- gdscript-refactor (Sonnet) #2: Pathfinder class — AStarGrid2D wrapper,
4-dir Manhattan, benchmark utility. ~110 lines pathfinder.gd. Headless-
validated by the subagent.
- Opus: Selection module + World autoload registry + scene integration
(world.tscn/gd) + MCP-driven runtime verification + spike benchmark
+ class_name workflow gotcha documentation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
World scene (scenes/world/world.{tscn,gd}):
- 6 TileMapLayer nodes per architecture.md split: Terrain (0), Floor (1),
Wall (2), Designation (3), Roof (4, hidden), Fog (5, hidden).
- Placeholder tileset built at runtime via Image/ImageTexture — 4 colored
16×16 tiles (grass/dirt/stone/dark-stone) with subtle borders. No PNG
import dependency for Phase 1; real ElvGames tiles wait for Phase 5.
- Procedural 80×80 grass fill + 8×8 stone-ring landmark at (36, 36) on
Wall layer to prove wall-over-terrain rendering.
- Calls camera_rig.set_world_bounds() once map dimensions known.
- ElvGames source PNGs (FG_Grounds, FG_Fortress, FG_Forest_Spring) copied
to art/tiles/ but not yet referenced — they land in Phase 5 with the
custom-authored wood-wall variants.
Camera rig (scenes/world/camera_rig.{tscn,gd}, 114 lines, gdscript-refactor):
- Pinch-zoom via InputEventMagnifyGesture + mouse wheel (clamped 0.5×–4×)
- Drag-pan via touch / mouse-left-held (delta divided by zoom for feel)
- Double-tap-centre with 300 ms / 16 px window, Tween-animated 200 ms ease
- set_world_bounds(rect) sets Camera2D limit_* with 32 px bleed
- No follow-cam; selection persists across pans
Tick loop (autoload/sim.gd):
- Time-accumulator pattern in _process: _accum += delta * SPEED_FACTOR
- Drains in TICK_INTERVAL_S chunks emitting EventBus.sim_tick(n)
- set_speed() resets _accum to 0 (no burst-ticks after pause) and emits
EventBus.speed_changed(int). Boot default = NORMAL.
- Audit.log on every speed transition for runtime diagnostics.
- Early-return guard against redundant set_speed calls.
EventBus (autoload/event_bus.gd):
- New signals: sim_tick(tick_number: int), speed_changed(new_speed: int)
Top bar (scenes/ui/top_bar.{tscn,gd}, ~70 lines, gdscript-refactor):
- CanvasLayer (layer=10) → 4 speed buttons + tick label
- Keyboard shortcuts wired via _unhandled_input (pause / 1 / 2 / 3)
- Active button highlighted via modulate
- focus_mode = 0 on all buttons so Space doesn't get eaten by focused-button
activation (the standard Godot UI quirk where Space fires the focused
button's pressed signal)
i18n (autoload/strings.gd):
- 5 new keys: speed.pause/normal/fast/ultra, hud.tick (template with {n})
Main bootstrap (scenes/main/main.{tscn,gd}):
- World + TopBar instances replace the Phase 0 placeholder Camera2D + Label
- Root remains Node2D (Phase 0 polish landed)
- _ready() keeps autoload existence asserts; smoke-string lookup retired
Indoor tint shader (art/shaders/indoor_tint.gdshader):
- Stub: tint_strength = 0 pass-through. Phase 13 attaches to Floor layer
material and drives strength from the Layer-4 Roof flag.
Acceptance: MCP-verified via play_scene + get_game_screenshot. 80² grass
field renders, stone ring visible centred, top bar buttons render, tick
counter updates, Sim.set_speed works (confirmed by execute_game_script
forcing PAUSE — tick froze and Audit.log emitted the transition line).
Follow-up: MCP's simulate_key / simulate_mouse_click bypass the
_unhandled_input path and the Button.pressed signal — events don't reach
the handler. Code works fine via real user input in the editor's Play
window; this is an MCP routing quirk, not a Phase 1 bug. Documented as
a known limitation when scripting input tests.
Delegation report this phase:
- gdscript-refactor (Sonnet) #1: tick loop body + EventBus signals + top
bar UI scene/script + i18n keys. ~3 file mods + 2 new files. Headless-
validated by the subagent.
- gdscript-refactor (Sonnet) #2: camera rig scene + script. 2 new files,
114 lines GDScript. Headless-validated by the subagent.
- Opus: world scene + procedural tileset + map fill + integration into
main.tscn + MCP-driven runtime verification.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plain 'Node' root causes Godot to open the scene in 3D perspective
view because the engine can't infer the intended dimension. Node2D
root is the right default for our 2D project; the editor opens in
2D view automatically.
Confirmed via MCP get_editor_screenshot — Phase 0 verification was
landing in 3D editor view because of this.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Project scaffold:
- project.godot at repo root, GL Compatibility renderer (max mobile reach),
pixel-snap on, texture filter nearest, sensor_landscape orientation
- 7 autoloads: World, Sim, GameState, EventBus, Strings, Audit, SaveSystem
- scenes/main/main.{tscn,gd} smoke-test scene with autoload assertions
- Folder layout matches tavernkeep idiom: autoload/ at root, scripts
co-located with scenes/ (not the scripts/autoloads/ mirror originally
sketched in implementation.md)
- Input map: pause, speed_cycle, speed_normal/fast/ultra, confirm, cancel.
Mobile gestures (pinch/drag/long-press) handled at script level via
Godot's InputEventScreenTouch/Drag/MagnifyGesture.
- SaveSystem skeleton: SAVE_VERSION=1, JSON to user://save_slot.json,
version-mismatch warning. Phase 3 expands to real entity state.
- icon.svg placeholder (cabin silhouette on dark green field)
- README.md points at memory.md / implementation.md / docs/
Headless verification: 'godot --headless --path . --quit' exits 0,
'[main] Phase 0 smoke test online.' prints, no errors. Editor-side
green-dot check still pending — needs human launch of editor.
Asset audit (researcher Haiku, 2026-05-10):
- FG_Houses.png NOT autotile-solvable — pre-built decorative house
compositions, 4 distinct roof palettes, no modular wall family.
~½–1 day per material to author terrain bits on top.
- FG_Fortress.png IS autotile-solvable — ~20–30 modular tan-stone
pieces. Wang-style Godot 4 terrain works with minimal extra art.
Iconic Homestead $19.99 fallback not needed.
- No wolf sprite anywhere in the bundle. EvoMonster packs all
cute/fantasy. Need commission, CC0 source, or Ventilatore check.
- Retro Graveyard 16x16 [Kingdom Explorer] confirmed in Tier 3 with
full graveyard suite — direct use in Phase 14.
New open questions surfaced in memory.md:
- Player-built wall material strategy (3 options laid out)
- Wolf sprite acquisition path (Phase 10 blocker)
Project move:
- Repo physical location moved from ~/claude/projects/rimlike to
/mnt/d/godot/rimlike (D: drive, fast for Windows-side editor).
- Symlink at the original WSL path preserves the home-CLAUDE.md
layout convention. Mirrors tavernkeep's pattern.
- Set core.filemode=false to silence DrvFs's everything-is-0777
false-positive on git diff.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>