Each entity completion handler (wall/floor/door/bed/torch/workbench/crate
/tree/rock/big_rock/grave_slot) now calls World.clear_designation_at(tile)
so the orange/blue/etc. highlight overlay disappears with the job.
BigRock iterates its footprint to clear all four tiles.
World.designation_ctl is set during the scene boot wire-up; the helper
no-ops when the controller is absent (e.g. headless tests).
Beds are 1×2 (foot tile + head tile extending up), but the seeded cabin
was 8×6 with the north interior row at y=24 directly below the perimeter
wall at y=23. Bed headboards clipped into the wall.
Shift cabin origin up one row (44, 23 → 44, 22) and bump height (6 → 7)
so the interior is now 6×5 (rows 23..27). Bed at row 24 has its head
land in the new row 23 — interior floor, not wall.
Affected: _seed_phase5_demo_buildings and _prestamp_cabin_for_room_detector
(must stay in sync). Bottom wall + door + workbench + bed + torch + crate
positions all unchanged in absolute coords — only the top wall moved up.
Interior crate now sits at (50, 23) instead of (50, 24); comment updated.
The user mentioned eventually wanting no premade buildings at all. That's
a future change (probably Phase 19 onboarding or Phase 20 polish); kept
out of scope.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The old FG_Interior atlas coords (32,22)/(35,22)/(38,22) were misidentified
in the 2026-05-12 visual pass — they're actually side-on chairs with
cushions, not beds. Player reported the sprite is confusing and they
can't tell where the bed actually is.
New 1×2 procedural draw at the bed entity's anchor (foot tile, anchor at
bottom; sprite spans local Y -32..0):
* Wood frame outline (dark + light pass for depth)
* White pillow at the head with subtle underside shadow
* Saturated coloured blanket — three variants (warm tan / cool blue /
rose) picked by deterministic hash from tile, so the same bed stays
the same colour across boots and saves. Saturation tuned to survive
the cabin's torch-lit CanvasModulate warm tint.
* Sheet fold + foot board accent at the foot tile for top-down depth cue
* Medical cross overlay sits on the pillow region (preserved from the
prior atlas-era position, retargeted to the procedural coords).
Drops _BED_TEX / _BED_VARIANT_COORDS / _BED_TILE_W/H constants and
_build_sprite() helper. setup() now just snaps position and queue_redraws
— no Sprite2D child to manage. Existing saves load cleanly: any legacy
"Sprite" child from a pre-procedural save is queue_free'd in setup().
Verified visually: three cabin beds render with distinct colours (tan /
pink-medical / blue). Each silhouette clearly reads as "bed" at the
default zoom.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Player report: hovering trees works after the canopy fix, but crops show
nothing and beds still report "Wood floor" sometimes.
* Crops are added to the lookup, showing kind + growth stage + percent
("Wheat | sown · 98%", "Wheat | ready to harvest", "tilled — not sown").
* Bed sprite is 16×32 (two tiles tall, anchor at the foot). Hovering on
the headboard (one tile above the anchor) used to miss. Added a sprite-
canopy pass for beds mirroring the existing 4-tile tree canopy logic.
* Full layer audit and reordering. Final priority top-down:
1. Pawn / Wolf / Corpse / Grave marker — entities the player cares
about first.
2. Crop — small sprite, exact tile only.
3. Tree — trunk tile + 4-tile canopy.
4. Big rock (2×2 footprint) / Rock — exact tile / footprint.
5. Furniture at exact tile (wall / door / bed / crate / workbench /
torch).
6. Furniture sprite-canopy (currently only bed; future tall furniture
slots in here).
7. Item on the ground (loose stack).
8. Stockpile / zone region overlay.
9. Floor — only when nothing physical is on the tile.
Confirmed at runtime: bed foot → Bed; bed headboard → Bed (via canopy);
two-tiles-above-bed → empty; crop at growing stage shows percent; bare
floor still says Wood floor; furniture on wood floor wins over floor.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two playtest gaps reported:
* Hovering on a tree showed nothing — trees anchor to the trunk tile but
the canopy sprite rises ~4 tiles upward. Now any hover within the
vertical band [trunk.y - 4, trunk.y] resolves to the tree.
* Hovering inside the cabin always said "Wood floor" — both floor and
furniture register in World.build_queue, and the floor was found
first. Now we two-pass the queue: remember any floor we hit, but keep
scanning for furniture (bed / crate / workbench / torch / etc.) and
return that if found. Items on the ground also win over the bare
floor. Floor only shows when nothing else occupies the tile.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Adds an AudioManager autoload with three buses (Master, Music routed to
Master, SFX routed to Master), a small catalog of looping music + one-shot
SFX, and a single persistent AudioStreamPlayer for the music director.
Music
* Day and night loops swap on Clock.phase_changed (night during the night
phase, day everywhere else). Tracks pulled from Retro Farming Music 1
(day) and Cozy Melodies Pack 1 (night), both loopable OGG.
SFX
* Tree.fell, Rock.mined, BigRock.mined → tree_fell / mine_tick.
* EventBus.pawn_took_damage → combat_hit (Sword Pack 1).
* EventBus.storyteller_event_fired → ui_confirm sting.
* EventBus.alert_added → ui_click.
* play_sfx is rate-limited per key (80ms cooldown) so fast-sim doesn't
saturate the mixer.
Settings + suspend
* SettingsMenu master/music/sfx sliders now live-bind to the bus dB via
Audio.set_*_linear (linear → dB internally, 0 → -80dB silence). The
ambient slider is intentionally unwired; no ambient bus this pass.
* NOTIFICATION_APPLICATION_PAUSED + FOCUS_OUT mute the Master bus to
match the existing "no background sim" rule. Resume + focus restore it.
Bundle housekeeping
* Two zipped packs in the ElvGames bundle (Cozy Melodies Pack 1, Retro
Farming Music 1) extracted in place to keep pack identity intact for
the license/credits string. 8 OGG files curated into audio/ at ~5.3MB.
Verified end-to-end via MCP runtime: buses online, day_loop plays at
boot, manual phase swap day→night→day round-trips, slider linear→dB
mapping correct (0.5 → -6.02dB, 0.0 → -80dB), tree_fell SFX triggers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the Going-Medieval / Rimworld "door replaces wall" convention.
Painting a door on a tile occupied by a Wall (ghost OR completed):
* Reverses Wall._complete: erases the wall_layer stamp, marks the
pathfinder cell walkable, triggers room recompute.
* queue_free's the wall entity.
* Spawns the door ghost in its place via the normal designation flow.
Source of truth for "is there a wall here?" is World.build_queue, so the
rule covers both designation-painted walls and pre-built seeds (cabin,
test shed) which self-register via Wall._ready but aren't in
_build_sites_by_tile.
Verified via MCP: completed wall + door paint → wall gone, door ghost,
tile walkable, layer unstamped. Ghost wall + door paint → wall replaced
cleanly with no leftover ghosts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Player reported pawns ignoring chop designations. Root cause was a
lingering door designation at (36, 27) — painted on the test shed wall,
which is pre-built and impassable. ConstructionProvider (priority 6)
kept offering the doomed job; Decision picked it over chop (priority 5);
JobRunner cancelled the empty walk each tick. Busy-spin starved all
elective work.
Mirrors the reachability pattern from HaulingProvider / DoctorProvider /
EatProvider. For pathing-blocking sites (walls) we probe from an adjacent
walkable cell; for other sites (doors / beds / crates / torches) we probe
the site tile directly. Unreachable sites are skipped silently so the
queue can sit dormant without starving lower-priority work.
Verified via MCP: with a deliberately-unreachable door designation in
the queue, all three pawns successfully picked up chop jobs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Player reported pawns ignoring chop designations. Root cause:
ChopProvider/MineProvider iterated World.trees/World.rocks
unconditionally — paint set a null sentinel and never touched the entity,
so designation was cosmetic only. Pawns auto-chopped nearest unfelled tree.
* Added chop_designated: bool to Tree, mine_designated: bool to Rock and
BigRock (footprint-aware: paint on any of the 4 footprint cells flags
the boulder). Save/load round-trips the flag.
* world.gd._on_designation_added 'chop'/'mine' cases now find the entity
at the painted tile and flip the flag. _on_designation_cleared inverts.
* Boot seed auto-designates SAMPLE_TREES / SAMPLE_ROCKS / SAMPLE_BIG_ROCKS
so the cabin demo still produces wood + stone end-to-end without
requiring the player to paint first.
Also from the same audit (researcher mapped all 11 WorkProviders):
* DoctorProvider + EatProvider now pre-check reachability with
pathfinder.find_path before issuing a job, mirroring HaulingProvider's
pattern. Previously they handed out doomed walks that JobRunner had to
cancel, busy-spinning at 20 Hz.
Verified end-to-end via MCP runtime: undesignated tree/rock returns null
from provider; paint flips the flag and provider returns a chop/mine job;
un-paint clears the flag; BigRock footprint paint works on any of the 4
cells.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three playtest-reported bugs fixed out-of-phase before Phase 18:
* Furniture build-queue gap: Torch / Bed / Crate / Workbench / CremationPyre
were missing World.register_build_site(self) in _ready, so newly-painted
designations never entered ConstructionProvider's iteration. The seeded
cabin pre-built everything via _spawn_complete_* helpers, masking the gap
until a player painted a fresh furniture designation.
* Wall-trap regression for bystanders + walk-through pawns: Wall._complete
now dislodges any pawn on the tile via new Pathfinder.find_nearest_walkable
BFS helper; Pawn._advance_walk re-checks next tile walkability before
stepping, aborts walk + cancels job + lets Decision reroute. Phase 6's
adjacent-stand fix only protected the BUILDING pawn.
* Floor / Pawn Y-sort ambiguity: Floor was anchored at tile-center
(same Y as Pawn), so Y-sort tiebreak fell to scene-tree order and
Floor (spawned later) drew over Pawn. Moved Floor origin to top-of-tile
so Floor.y < Pawn.y under Y-sort; _draw rect offsets compensate.
All three verified via MCP runtime: torch built end-to-end, all 3 pawns
working on different jobs with no idle traps, pawn renders over floor.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Walls, items, and doors swapped from procedural draws to ElvGames atlas
sprites this session. Notes on tileset survey results (which atlases have
1-tile-wide doors vs castle gates), MCP execute_game_script statement
limit, and the .import-generation step required for new textures.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous 32×32 FG_Fortress arched gate at (4,19) was rejected as 'a
door for a entrance to a castle' — too imposing for a cabin's front door.
Pivot: FG_Village atlas (3, 24) — a single 16×16 olive-wood plank door
with a white U-handle, extracted from the red-roofed cottage template.
Sprite occupies one tile (no overhang, no rising lintel), bottom-anchored
the same as Wall so Y-sort layers pawns correctly.
Verified in MCP play_scene: the door slots cleanly into the demo cabin's
south-wall gap at (47, 28), matching the surrounding stone-brick wall
height. Ghost-alpha pipeline (0.4 in-build → 1.0 on _complete) intact.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closed wooden door with stone arch from FG_Fortress (4..5, 19..20) — a 32×32
sprite bottom-anchored on the door tile. The 2-tile-wide sprite extends 8 px
into each flanking tile so the stone arch merges into adjacent wall sprites,
and the lintel rises one tile above the door tile (Y-sorted, occludes pawns).
Ghost state stays at 40% alpha until build completes, matching the Wall/Bed/
Workbench convention. _draw() is now a no-op; the sprite handles everything.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stone, iron ore, gold, wood, plank now render as ElvGames FG_Abandoned_Mines
sprites instead of hue-hashed coloured squares. Other types fall back to the
procedural square; quality border + stack count badge are layered on top of
whichever base renders.
Atlas coords picked from /tmp/item_finals.png + /tmp/wood_plank_alts.png:
stone (5, 33) rock cluster with stone chunks
iron_ore (9, 33) rock with silver chunks
gold (13, 33) rock with gold chunks
wood (20, 13) chunky brown log pile
plank (28, 18) horizontal-grain plank stack
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Carpenter → FG_Interior (24, 20) 16×32 wood cabinet w/ drawers
- Smelter → FG_Marketplace (8, 30) anvil + hot metal
- Hearth → FG_Interior (16, 32) stove w/ burners
- Millstone → FG_Interior (17, 40) wood barrel + procedural wheel overlay
Adds label_text setter so the sprite rebuilds idempotently whether the
caller assigns label before or after setup() (world.gd assigns after,
SaveSystem after). Setter also calls _maybe_build_light() to fix a
pre-existing Phase 11 bug where Hearth never built its PointLight2D
(label_text was still default when _ready fired).
Unrecognised label_texts fall through to _draw_generic so ad-hoc
workbench variants keep rendering.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User flagged the current walls as looking off. Investigation: coord (1, 1)
in FG_Fortress.png is actually a tan stone FLOOR tile — no brick texture,
no capstone, no depth — which is why a row of them read as pavement rather
than a wall.
Swap to coord (13, 4): the middle column of a 3-tile-wide capped wall in
the autotile region. Used as a single non-autotile sprite, it shows a dark
medieval brick wall with a visible top cap, brick coursing, and mortar
shadow. The side-by-side simulation at /tmp/wall_rows.png makes the
difference obvious — capped brick reads as a real wall, the floor tile
reads as graph paper.
Phase 5 wall rendering stays non-autotile (every wall uses the same sprite
regardless of neighbours), so even at corners the cabin will read more
clearly as a stone structure. Autotile pass is a separate future task.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The procedural 3/4-perspective bed (16×16 box of stacked draw_rect calls)
gets replaced with a 16×32 single-column Sprite2D cropped from the ElvGames
House Interior tileset. The sprite spans the bed's tile plus the tile
immediately south — head with rounded pillow + frame in the bed tile, body
and wood foot rail in the foot tile. The foot tile stays walkable in the
pathfinder (Phase-4 simplification, matches small rocks).
Three variants chosen deterministically by tile hash so the same bed renders
the same colour across boots and saves:
• (32, 22) brown wood frame
• (35, 22) blue quilt
• (38, 22) pink quilt
Crops are LEFT-edge columns of the 2-/3-wide bundle beds — visually verified
in /tmp/bed_candidates.png against MIDDLE/RIGHT alternatives. LEFT shows the
clearest pillow shape + visible wood frame in a single column.
Setup work (sprite, position, y-sort) now lives inside setup() not _ready()
because _ready fires inside add_child() before the caller passes in the real
tile — same lesson as BigRock. _build_sprite() is idempotent (drops the
previous Sprite child) so the save-load flow (factory.setup → factory
on_build_tick → from_dict.setup) re-creates the sprite without leaks.
_draw() now renders only the medical-cross overlay when is_medical is true
— sprite handles ghost alpha via modulate. _complete() solidifies the sprite
from 0.4 to 1.0 alpha. The quality-tinted procedural sheets are dropped;
quality still drives sleep mood, just not visual colour.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A new entity for multi-tile rock formations. Same duck-typed contract as
single-tile Rock so MineProvider scans both transparently via World.rocks.
Differences from Rock:
• Occupies a 2×2 footprint anchored at origin_tile (top-left).
• Renders a single 32×32 Sprite2D drawn from the FG_Grasslands_Spring 2×2
cluster sprites at (22, 3) brown and (30, 3) gray.
• Blocks pathfinding on all four footprint tiles — pawns route around it.
• MineProvider asks `rock.approach_tile_for(pawn.tile)` for the walk
destination, so the pawn stands beside the boulder instead of trying to
path into the blocked footprint. Rock returns its own tile (walkable);
BigRock picks the nearest walkable perimeter neighbour.
• Mining takes 480 ticks (4× Rock) and drops 4 stone, one per footprint tile.
All init work happens in setup() rather than _ready(): the calling pattern is
`add_child(big); big.setup(origin)`, and _ready fires inside add_child with
origin_tile still at its zero default — anything reading origin_tile from
_ready would stamp the pathfinder at the wrong tile.
Wired through SaveSystem: factory preload, spawn-priority tier 0 (same as
Rock — static structures spawn before pawns), and a `&"big_rock"` factory.
World seed adds two demo boulders near the small-rock cluster
(65, 58) + (56, 64) so the visual contrast is on-screen from boot.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The earlier coords (24,7), (28,7), (12,13) in FG_Grasslands_Spring.png are
autotile interior pieces — clipping their 16×16 windows produces sprites that
visibly continue beyond the tile edge, betraying the autotile cluster they
were cut from.
Replace with six clean single-tile boulders from the rock band (x=16..29 at
y=3 and y=5). Each has a green margin on all four edges, so they read as
proper standalone rocks. Two color families (brown + gray) × three sizes
(round / peaked / squat) gives more visual variety than before too.
Multi-tile big-boulder formations at (22..23, 3..4) brown and (30..31, 3..4)
gray are noted in the comment for a future BigRock entity.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces three procedural _draw() entities with bundle sprites:
- Tree: was draw_rect trunk + draw_circle canopy. Now Sprite2D using
FG_Tree_Spring.png (64x80, 4 variants picked deterministically from
tile coord). Bottom-anchored so trunk base sits at tile bottom, canopy
rises into the cell above; y_sort_enabled so canopies tuck behind
pawns south of the trunk. Chop-progress notch overlay retained.
- Rock: was draw_colored_polygon hex. Now Sprite2D reading from the
existing FG_Grasslands_Spring.png decoration atlas at three eyeballed
coords (2 gray boulders, 1 brown rock pile). Variant deterministic
per tile. Mine-progress crack overlay retained.
- Stone wall: was procedural top-band + front-band + mortar lines. Now
Sprite2D from FG_Fortress.png at (1,1) — clean tan-stone brick fill.
Bottom-anchored (offset.y=-8) so the 16x16 sprite spans y=-16..0,
matching the procedural draw box exactly. Ghost state via modulate.a.
Wood walls still use procedural _draw_wood_wall — no clean 16x16 wood
tile found in the bundle yet (Pixel Crawler Walls.png is 32x32, would
need crop+rescale).
Asset additions:
- art/sprites/FG_Tree_Spring.png (Tier 1, Grasslands pack)
- FG_Fortress.png and FG_Grasslands_Spring.png were already in art/tiles
from earlier passes; this commit just consumes them from new sites.
Headless boots clean, runtime verified: trees look like chunky pixel-art
trees with root flare, rocks read as real boulders, cabin walls show
proper brick texture.
License: all ElvGames Humble bundle — commercial OK with credit.
Credit-string compilation still open.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First visual pass: replaces nothing, adds a decoration TileMapLayer above
Terrain that paints ~8% of cells with random overlay sprites pulled from
FG_Grasslands_Spring.png. Mix of 3 grass-sprout variants, 3 white-with-
colored-center flowers, and 2 scattered-grass-dot patches. ~540 cells
populated on the 80x80 map; deterministic seed so layout is stable.
The bundle's design assumes flat-color terrain + overlay sprites for
visual richness — so this is the cheapest possible win that uses the
art we own. Terrain base, walls, trees, pawns, UI all unchanged.
Implementation lives in world.gd as _build_decoration_tileset() +
_paint_decorations(); the Decoration TileMapLayer is added to
world.tscn at z_index=0 (siblings render in tree order so it draws
between Terrain and Floor). Tileset is built at runtime pointing at
res://art/tiles/FG_Grasslands_Spring.png, mirroring the existing
_build_placeholder_tileset pattern.
Verified MCP runtime: world feels like a meadow now, no perf hit.
Headless boot logs '[world] decoration: painted 541 overlay cells'.
License: ElvGames Humble bundle — commercial use OK with credit (see
docs/art.md). Credit string compilation is still an open audit item.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two compounding bugs made hauling appear broken when targets were behind
walls. User report: 'i set a stockpile and there is stuff to move' — items
sat indefinitely.
JobRunner._tick_walk treated 'path is empty' (unreachable) by marking the
walk toil done and silently advancing to the next toil. Pickup/deposit
then ran at the pawn's CURRENT tile instead of the intended target —
'Bram pickup: no item at (44, 25)' for an item that lived at (45, 21).
The job 'completed' wrongly. Now an unreachable walk cancel_job()'s,
letting Decision pick something else next tick.
HaulingProvider didn't pre-check reachability before handing out a job.
With the JobRunner fix alone, Decision would have re-picked the same
unreachable haul every tick (busy-spin at 20 Hz). Now the item loop and
corpse loop both skip targets where find_path is empty from pawn.tile.
Cost: ~10 us pathfind per candidate; trivial at MVP scale.
Verified MCP runtime: bread at (45, 21) (reachable) hauled end-to-end to
the stockpile at (15, 62). Bread at (50, 21) (unreachable behind the
cabin wall arrangement) correctly skipped — no job assigned, no busy
spin in the log. Bram completed the haul and picked up his next job
(Harvest wheat) naturally.
Note: the JobRunner unreachable-cancel fix also helps any other provider
whose walk_to leg fails — chop/mine/construction were silently 'finishing'
the same way when targets walled off. They now cancel cleanly too. Their
providers don't yet pre-check reachability, so they could cancel-loop on
unreachable targets if nothing else is queued — left for a followup once
a real case surfaces.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Visible bug: with 1 wall ghost queued, all 3 pawns picked the same site;
2 stood idle while 1 built. Same shape would affect chop/mine/haul/etc.
Design: Job carries an untyped target_node ref (the tree/rock/build-site/
crop/item/patient/workbench/etc the job is acting on). Job.is_target_taken_
by_other(target, excluding_pawn) does an O(pawns) scan of live job state to
ask 'is anyone else already working this?'. Each WorkProvider's find_best_
for() now skips claimed targets in its scan and sets j.target_node before
returning. No per-entity claim state, no .claim()/.release() bookkeeping,
no save-format change — target_node is intentionally not serialized
because pawns re-decide and re-bind naturally after load.
Providers updated: construction / chop / mine / plant (harvest path) /
hauling (item AND corpse) / cleaning (target is Vector2i tile not Node;
field is untyped, doc'd) / doctor / crafting (workbench).
Not touched: rest (everyone shares the rest tile, that's fine), eat /
sleep (food and beds have their own availability gates; flagged as a
followup if multi-pawn food contention surfaces).
Verified MCP runtime: fresh boot, 3 pawns picked 3 distinct wall sites
(44,28)/(45,28)/(44,27) with distinct target_node refs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
NEEDS_CATEGORIES used to be [&"rest", &"eat", &"sleep"]. Decision iterates
needs providers before eligible work providers and picks the first one
that returns a non-null job. EatProvider/SleepProvider correctly gate on
hunger/sleep thresholds (return null when not hungry/tired), so they
yield to work when no need is pressing. But RestProvider always returns
a job — by design, it's the catch-all fallback ("stand here"). With
"rest" in NEEDS_CATEGORIES the loop preempted on RestProvider every
tick: pawns assigned 'Rest at (50, 50)' and never reached the
chop/mine/construction providers despite valid designations.
Fix: remove &"rest" from NEEDS_CATEGORIES. RestProvider falls into the
eligible bucket; its provider.priority=0 sorts it last in the eligible
sort (chop=5, mine/construction=5, etc, win the priority tiebreaker).
work_priorities doesn't include a "rest" key so priorities.get falls
through to NORMAL=3, keeping rest eligible for everyone (the UI matrix
deliberately omits rest per Pawn.work_priorities comment).
Verified MCP runtime: fresh boot, all 3 pawns picked 'Build stone wall
at (44, 28)' instead of 'Rest at (50, 50)'; advanced to next wall site
within 5 seconds.
Bug surfaced by first real playtest with painted designations — the
in-context audit caught it within ~5 min of inspection.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three alert signals had no UI subscribers — gameplay failures vanished
silently. Now all three feed AlertsLog via translator handlers that
forward to the generic alert_added sink.
- EventBus: new no_stockpile_accepts(item_type, tile) and
bill_blocked(recipe_label, reason, focus_tile) signals.
- HaulingProvider: per-item-type 30s cooldown; emits when find_best_for
scan finishes with viable items but no destinations.
- CraftingProvider: per-(workbench, reason) 60s cooldown; emits at the
skill_too_low and missing_ingredient continue sites. no_workbench
reason declared for future use but not emitted (the iteration shape
has no natural site for it).
- AlertsLog: connect + disconnect for all three signals using the same
has_signal-guarded pattern; translator handlers convert to localized
alert_added(severity, text, focus_tile).
- AlertsLog catch-up: room_too_large emits during World init, before
this CanvasLayer mounts. _catch_up_room_too_large() in _ready scans
World.rooms for rooms > ROOM_AUTOROOF_CAP and replays them, so the
pre-built cabin's 24-tile-too-large warning lands in the log on every
boot. Hauling/bill signals fire at runtime so they need no catch-up.
Verified runtime: cabin warning shows up in AlertsLog with severity
'warn' and focus_tile (45, 24) — the cabin top-left.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The auto_pause flag on THREAT events paused Sim when the modal fired, but
resolve_current() never resumed it. Player dismisses the modal expecting play
to continue; sim stays paused; pawns appear stuck. (Surfaced by first real PC
playtest after the controls patch.)
Now: capture Sim.current_speed before paying the pause, restore it on
resolve if the player hasn't manually changed speed during the modal
(current_speed != PAUSE skips restore so the player's choice wins). Field
round-trips via save_dict for the save-during-modal edge case.
Verified MCP runtime: lone_wolf modal fires at boot, speed=0, dismissal
restores speed=1 and pawns immediately walk toward their next job.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds full PC keyboard+mouse support on top of existing touch controls. Touch
paths untouched. All input goes through named actions in project.godot.
Bindings:
- WASD / arrows: camera pan (speed scales with zoom)
- = / -: keyboard zoom in/out
- C / Home: center on selected pawn
- Tab / Shift+Tab: cycle through pawns (pans camera to selection)
- B / L / P / ,: toggle BuildDrawer / AlertsLog / WorkPriorityMatrix / Settings
- Escape: cancel active designation tool > close topmost panel > deselect pawn
- Right-click: cancel active tool or deselect pawn (RTS convention)
- F: speed_cycle (action restored; handler still TODO)
- pawn_prev action removed; Shift+Tab read via event.shift_pressed inline
Escape priority enforced by Designation._input running before _unhandled_input
plus each panel consuming its own cancel action when visible.
Also fixes a pre-existing pre-Phase-17 bug: WorkPriorityMatrix, AlertsLog,
StorytellerModal, LoadMenu, and SettingsMenu had MOUSE_FILTER_STOP Controls
(Backdrop / Dim) that remained input-active when the panel was "closed" —
their open/close paths only toggled _root.visible / _panel.visible, never
CanvasLayer.visible. World mouse events (right-click deselect, left-click
pawn-select) were silently eaten. Now each _set_visible / open / close
toggles self.visible (the CanvasLayer) so input dispatch shuts off properly.
Verified end-to-end via MCP runtime: WASD pan, zoom keys, Tab+Shift+Tab
cycle, B-open + Escape-close, right-click deselect, left-click pawn-select
all working in sequence with no input bleed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 'drama pair' shipped together via 3-agent fan-out.
Phase 9 — Status effects + Medicine:
- Status data class (PERSISTENT/EVENT, severity stacks max=3) + StatusCatalog
(Bleeding ticks HP loss; Downed = incapacitated)
- Pawn HP (100 max, 30 downed threshold, 50 revive threshold), take_damage,
heal, add_status/remove_status_by_id, is_downed/is_incapacitated, downed
visual (body rotated 90° + desaturated)
- DoctorProvider (priority 9, highest) — scans World.pawns for nearest downed
pawn, finds medical bed (or any bed fallback), emits 4-toil job:
walk_to_patient → rescue → walk_to_bed → treat
- Bed.is_medical with red-cross marker draw on pillow; round-trips save
- KIND_RESCUE + KIND_TREAT toils + JobRunner _tick_rescue/_tick_treat
(snap-to-bed on first treat tick, +0.5 hp/tick, bleed cure at 100-tick
intervals; done at HP≥50 + no bleeding, 600-tick timeout)
- EventBus: pawn_took_damage, pawn_status_added, pawn_status_removed
Phase 10 — Combat + Wolves (wolf-first slice):
- Wolf entity (Node2D, 4-state APPROACH/ENGAGE/FLEE/DEAD, procedural
canine sprite with red glowing eyes, 40 HP)
- Two-roll combat: 70% hit + 50% chance to apply Bleeding(1) on hit
- WolfSpawner — triggers at Clock.darkness_factor()≥0.8 with 1-in-game-day
cooldown, packs of 1–2 at random map-edge cluster
- World.wolves registry + register_wolf/unregister_wolf
Integration: world.tscn load_steps 15→17 with DoctorProvider + WolfSpawner
nodes. world.gd registers doctor at top of provider list (priority 9 >
sleep 8 > eat 7 > construction 6 > chop≈plant 5 > mine≈craft 4 > haul 3
> rest 0). Middle bed at (47,24) marked is_medical=true.
MCP runtime verified: Bram took 75 dmg + Bleeding(2) → Downed (hp 25) →
Edda + Cora both volunteered doctor job → walked to patient → carried to
medical bed → treated → Bram healed to 94.2 hp, statuses cleared, back to
work. Wolf raid at day 3 22:00 fired; 4 wolves alive across raid cycles
by day 4 01:51. Screenshots confirm red-cross medical bed and wolf
silhouettes at night.
Phase 10 deliberately partial: wolf-side combat ships, pawn-side
weapons/armor/cover/friendly-fire deferred — full chain
(wolf→bites→pawn→bleeds→doctor) awaits player weapons.
Bleed-out timer at demo value (1200) vs design value (432000 = 6 in-game
hours) — documented in status_catalog.gd for first time-balance pass.
Delegation: Agent A (status + pawn HP), Agent B (doctor + treatment),
Agent C (wolf + spawner) — all Sonnet gdscript-refactor; integration on
Opus.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three gdscript-refactor agents in parallel; Opus integrated and verified
the day-night transition + torch lighting via MCP runtime + screenshot.
Clock autoload (Agent A, autoload/clock.gd, ~138 lines):
- TICKS_PER_DAY = 4800 → 4 min/day at 1× / 48 s at Fast / 20 s at Ultra
- TICKS_PER_HOUR = 200 (so 60 min × ~3 ticks per minute)
- 4-phase day: night → dawn (5–7) → day (7–19) → dusk (19–22) → night
- darkness_factor() returns 0..1 with linear ramps across dawn/dusk
- phase_changed signal fires on phase transitions
- save_dict / apply_dict for save round-trip
- Boots at Day 1, 06:00 (mid-dawn for atmospheric start)
- Registered in project.godot autoload list (Opus)
Top-bar clock UI (Agent A):
- ClockLabel added to top_bar.tscn (center-anchored at ±80 px)
- _on_clock_refresh in top_bar.gd; early-out string compare to skip text
assignments when unchanged (cheap per-tick)
Torch entity + lights registry (Agent B, scenes/entities/torch.{gd,tscn} +
workbench.gd + world.gd, ~210 lines):
- class Torch: buildable furniture, BUILD_TICKS=30, LIGHT_RADIUS=6
- Procedural radial gradient texture (64×64) generated at runtime with
smoothstep falloff → no PNG dependency
- PointLight2D child with the gradient texture, warm fire tint, energy 1.2
- is_on / get_light_tile / get_light_radius duck-typed interface; same
shape exposed by Workbench when label_text='Hearth' (HEARTH_LIGHT_RADIUS=5)
- World.light_sources registry + register/unregister + is_tile_lit(tile)
(Manhattan distance, no occlusion — Phase 13 may add wall-occlusion)
CanvasModulate darkness + in_darkness thought (Agent C, ~30 lines mod +
new factory):
- DarkOverlay CanvasModulate node added to world.tscn (first child of
World root so it tints all sibling layers + entities)
- world.gd._update_dark_overlay lerps DAY_TINT (white) ↔ NIGHT_TINT
(0.20, 0.22, 0.40 deep cool blue) by Clock.darkness_factor() each tick
- ThoughtCatalog.in_darkness(): persistent, -3 mood, fires when
darkness > 0.3 AND World.is_tile_lit(pawn.tile) is false
- Pawn._process_thoughts syncs in_darkness alongside hungry/tired
Opus integration:
- project.godot: Clock autoload registered
- world.tscn: DarkOverlay CanvasModulate node, plus the agent additions
- Demo seed: 2 torches inside cabin at (46, 26) + (49, 26), pre-built
- MCP-driven runtime test verified day→night transition + lighting
effects:
- Noon: world bright green, torches barely visible (over-bright at noon
is minor polish — Phase 17 may scale torch energy by darkness)
- Midnight: world deep blue/green tinted, torches cast yellow halos,
Hearth ember glows orange, cabin interior warmly lit, exterior dark
- top_bar clock label updates each sim tick (early-out on no-change)
Phase 11 followups for later phases:
- Torch energy should scale with darkness — visible halos at noon are
silly. Phase 17 will likely tie PointLight2D.energy to clamp(darkness,
0.2, 1.0) so they're invisible at midday
- Wall-occlusion for light_map — Phase 13's room-detection BFS could
treat completed wall tiles as occluders so light doesn't bleed through
- 'In darkness' thought currently treats ALL unlit cells as darkness;
Phase 13's roof flag could differentiate 'indoors-dark' (different
thought) from 'outdoors-dark'
- Light source visibility through CanvasModulate works correctly thanks
to PointLight2D's additive blend mode
Acceptance — MCP-verified via play_scene + get_game_screenshot:
- ✅ Day → Dusk → Night cycle visible (Clock.current_phase emits events)
- ✅ CanvasModulate tints world deep blue at night
- ✅ Torches cast visible yellow halos via PointLight2D additive blend
- ✅ Hearth opts-in as a light source via label_text='Hearth' check
- ✅ Top-bar clock shows 'Day N, HH:MM' format and updates each tick
- ✅ in_darkness thought wires through _process_thoughts (would fire if
a pawn were standing in an unlit night tile — demo didn't capture this
specifically but the code path is verified)
Delegation report this phase:
- Agent A: Clock autoload + 4-phase day cycle + top-bar UI extension
- Agent B: Torch entity + PointLight2D + procedural radial texture +
Workbench Hearth opt-in + World.light_sources registry
- Agent C: CanvasModulate world.tscn node + day/night colour lerp +
in_darkness ThoughtCatalog entry + Pawn persistent thought sync
- Opus: Clock autoload registration in project.godot + 2 torches in
demo seed + MCP runtime verification at midnight vs noon
~75% of Phase 11 GDScript was subagent-authored.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three gdscript-refactor agents in parallel; Opus integrated and tuned
the priority + hunger-decay numbers via MCP runtime observation.
Crop entity + PlantProvider (Agent A, scenes/entities/crop.{gd,tscn} +
scenes/ai/plant_provider.gd, ~225 lines):
- Crop: 6-stage state machine (TILLED → SOWN → GROWING_1/2/3 → READY).
STAGE_TICKS=200 sim ticks per stage × 4 stages = 800 total to maturity.
Listens to EventBus.sim_tick for growth. Procedural _draw with growing
plant + ready-state golden grain accent.
- on_harvest_tick: drops a grain (wheat) or vegetable (potato) Item, resets
to TILLED (re-sowable).
- on_sow_tick: TILLED → SOWN — used when Phase 17 paint UI lands.
- PlantProvider priority=5 (above crafting=4) — harvest-only for Phase 7.
Sow returns null to avoid the infinite harvest+sow loop that would
starve crafting forever.
- JobRunner._tick_interact extended with is_harvestable/is_sowable probes
alongside existing is_choppable/is_mineable. Unified probe array.
Hunger + Eating (Agent B, scenes/pawn/pawn.gd + scenes/ai/eat_provider.gd +
toil.gd/job_runner.gd extensions, ~150 lines):
- Pawn.hunger: float 0..100. HUNGER_DECAY_PER_TICK=0.02 (tuned down 5×
from agent's 0.10 after MCP runtime test showed pawns starving before
cooking pipeline could finish — 0.02 means 100→0 in 5000 ticks =
~4 min at 1× / ~20s at Ultra).
- is_hungry() at <30 — triggers EatProvider job
- is_starving() at <5 — Phase 9 status-interrupt hook reserved
- Toil.KIND_EAT + JobRunner._tick_eat — consumes carried_item, applies
nutrition bonus by type (MEAL +60, BREAD +45, VEGETABLE +25, GRAIN +10)
- EatProvider priority=7 (highest) — food-priority ladder:
MEAL > BREAD > VEGETABLE > GRAIN
- Pawn.skills extended with cooking init; hunger round-trip in to_dict
Cooking recipes (Agent C, recipe_catalog.gd + item.gd + workbench.gd
draw extensions, ~120 lines):
- New Item types: TYPE_FLOUR, TYPE_BREAD (TYPE_MEAL was already in base
16-chip set)
- RecipeCatalog adds:
* flour() — grain → flour, Crafting skill, 50 ticks
* bread() — flour → bread, Cooking skill, 90 ticks
* meal_from_vegetables() — vegetable → meal, Cooking, 80 ticks
- Workbench._draw extends label_text dispatch:
* Hearth: dark stone + large orange flame + smoke wisp
* Millstone: light grey + dark circular stone wheel
- i18n: item.flour, item.bread, item.meal, workbench.hearth, workbench.millstone
Opus integration:
- world.tscn: PlantProvider + EatProvider nodes (8 providers total)
- world.gd registers all 8 in priority order:
eat=7 > construction=6 > chop=5 > plant=5 > mine=4 > crafting=4 >
haul=3 > rest=0
- Pawn spawn data extended with cooking skill (Bram=2 / Cora=6 / Edda=1)
for hearth-recipe quality spread
- _seed_phase5_demo_buildings extended (now spans Phase 5/6/7):
- Millstone at (46, 27) inside cabin south-row: flour bill FOREVER
- Hearth at (49, 27) inside cabin south-row: bread + meal bills FOREVER
- 6 wheat crops east of cabin at (54-55, 24-26), all SOWN at boot
- 2 pre-baked breads at (45-50, 21) so eat-loop unblocks before cooking
chain completes
Wall-trap fix from Phase 6 confirmed working — pawn paths now go to
(44, 29) adjacent to the south-west corner wall, not on top of it.
Acceptance — MCP-verified end-to-end:
- 6 wheat crops grow over ~800 sim ticks; PlantProvider picks them up
- Pawns harvest all 6 → 6 grain items dropped (PlantProvider priority 5
> Crafting priority 4 means harvest interrupts plank crafting)
- Hunger decays steadily; at <30 EatProvider takes over (priority 7
beats all work providers)
- 2 pre-baked breads consumed first (priority 2 > grain priority 0)
- Pawns then ate the raw grain (priority 0 last resort) before flour
could be milled — this is by-design 'starving pawn settles for raw'
behaviour, not a bug. Phase 17 balance pass may add a wait-for-cooked
preference if it feels wrong in playtest.
- Planks crafted with EXCELLENT quality at (46, 25) — quality system from
Phase 6 still works on top of the new pipeline
Phase 7 tuning lessons (logged):
- Agent's initial 0.10/tick hunger decay made pawns starve in <60 sim
seconds — too fast for any multi-step chain (grain→flour→bread is
~140 sim ticks per cycle). Tuned to 0.02/tick post-runtime.
- PlantProvider's sow+harvest both returning jobs caused infinite plant
loops at priority 5. Sow returns null until Phase 17 splits the
providers or adds designation-paint sow.
- The 'raw grain eaten before flour milled' isn't a bug — it's the food
priority ladder doing its job. To showcase the full chain in a demo,
either reduce hunger decay further or pre-seed cooked food.
Delegation report this phase:
- Agent A: Crop entity + PlantProvider + JobRunner probe extension
- Agent B: Pawn.hunger + EatProvider + KIND_EAT toil
- Agent C: Recipe catalog extension (flour/bread/meal) + Workbench draw
branches for Hearth/Millstone
- Opus: scene wiring + pawn cooking-skill init + demo seed (Millstone +
Hearth + 6 crops + pre-baked breads) + MCP-driven runtime tuning of
hunger decay and plant priority
~75% of Phase 7 GDScript was subagent-authored.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three gdscript-refactor agents in parallel; Opus did integration + caught
the wall-trap bug via MCP runtime test.
Data layer (Agent A):
- scenes/ai/recipe.gd: class Recipe (RefCounted) — id, ingredient_type,
output_type, work_ticks, required_skill (Crafting/Cooking), skill_threshold
- scenes/ai/bill.gd: class Bill — Mode enum (FOREVER/COUNT/UNTIL_N),
recipe ref, target_count, completed_count, paused, is_active() per-mode
logic. UNTIL_N walks World.items each call (acceptable at MVP scale; cache
if items grow large in Phase 16+)
- scenes/ai/recipe_catalog.gd: RecipeCatalog.plank() / .stone_block() — 2
starter recipes; Phase 7+ expands toward the design.md ~22 catalog
- Item: added Quality enum (SHODDY/NORMAL/EXCELLENT/MASTERWORK/LEGENDARY),
@export quality field, quality-coloured border in _draw (dull-grey / no /
blue / gold / magenta), TYPE_PLANK + TYPE_STONE_BLOCK constants
- Pawn: added skills dict (5 skills × levels 0–10), get_skill/set_skill,
skills round-trip in to_dict/from_dict
- strings.gd: item.plank, item.stone_block, quality.* (5 keys)
Workbench entity (Agent B, scenes/entities/workbench.{gd,tscn}, ~310 lines):
- class Workbench extends Node2D, bottom-anchored 3/4 perspective like Wall
- BuildJob interface (is_buildable / on_build_tick / _complete) — same
pattern as Wall / Crate
- Bills queue (add_bill, find_active_bill matches by accepted_skill)
- Craft cycle hooks: begin_craft / tick_craft / on_craft_complete /
on_craft_interrupted — JobRunner._tick_craft delegates to these
- Procedural _draw differentiates Carpenter (brown bench + vise) vs
Smelter (dark stone + orange ember glow) via the @export label_text
field — no subclass needed for Phase 6
- World autoload: workbenches registry + register_workbench/unregister_workbench
Crafting AI (Agent C):
- Toil.KIND_CRAFT + Toil.craft_at(workbench_path, bill_index) factory
- JobRunner._tick_craft: validates pawn-at-workbench, ingredient match;
delegates progress to wb.tick_craft; on complete spawns output Item
with QualityCalc.roll() applied; records bill completion
- crafting_provider.gd: priority=4 WorkProvider, 4-toil job
(walk_to(ingredient) → pickup → walk_to(wb) → craft_at)
- quality.gd: QualityCalc.roll(skill) — additive formula
skill × 0.04 + RNG(0, 0.6) with bucket thresholds matching
architecture.md spec. Skill 0 caps at Excellent; Skill 10 reaches
Legendary ~8% of the time
Opus integration:
- world.tscn: CraftingProvider node added
- world.gd: registered crafting_provider with World (priority order:
construction=6 > chop=5 > mine=4 > crafting=4 > haul=3 > rest=0)
- Pawn spawn data extended with crafting skill (Bram=8, Cora=4, Edda=0)
for visible quality variation in the demo
- _seed_phase5_demo_buildings extended: pre-built Carpenter at (46, 25)
with plank bill (FOREVER) + Smelter at (48, 25) with stone_block bill
(UNTIL_N=5)
The wall-trap bug (caught via MCP runtime — initial Phase 6 run hung):
- Pawns building walls stood ON the wall tile. When wall._complete fired
set_cell_walkable(false), the pawn was stuck on a solid cell.
AStarGrid2D returns no path when start cell is solid → all subsequent
jobs failed pathfinding from the trapped position.
- Fix: ConstructionProvider checks site.blocks_pathing_when_complete()
(new method on Wall, returns true; not implemented on Floor/Door/Crate/
Workbench since they remain walkable). Walls route the pawn to an
adjacent walkable cell via _find_adjacent_walkable. Floors/doors/etc.
build on-tile as before.
- This bug existed since Phase 5 but only surfaced in Phase 6 because
Phase 5 demos ended at construction-complete; Phase 6 needed pawns to
walk away from finished walls toward the workbench.
Acceptance — MCP-verified end-to-end:
- 3 pawns boot with varied Crafting skills
- Construction priority wins first; all 48 build sites (23 walls + 1 door
+ 24 floors) complete. Pawns escape wall tiles safely (fix verified).
- Pawns transition to chop/mine, then crafting at the Carpenter workbench
- At tick 9215, 12 planks crafted with quality distribution matching
expected spread per skill: 1 SHODDY + 6 NORMAL + 4 EXCELLENT + 1
MASTERWORK. Quality-coloured borders visible on items.
- Smelter UNTIL_N=5 bill correctly idle (no stone consumed yet) because
CraftingProvider prefers closer workbench-ingredient pairs and the
carpenter+wood is closer to where pawns end up than smelter+stone
Phase 6 followups for later phases:
- on_craft_interrupted has no JobRunner hook — Phase 9 status interrupts
will need a 'cancel callback' on toils or wb.on_craft_interrupted will
leak current_bill/current_work_progress on canceled crafts
- Bill.from_dict reconstructs Recipe inline via Recipe.from_dict — Phase
16 may need a recipe registry for save-format stability across catalog
changes
- UNTIL_N's per-call World.items walk is O(items) — acceptable at MVP
scale; profile if it becomes hot
Delegation report this phase:
- Agent A: Recipe + Bill + RecipeCatalog + Item.quality + Pawn.skills + i18n
- Agent B: Workbench (one class, label_text-driven differentiation, no
Carpenter/Smelter subclass) + World registry
- Agent C: Toil.KIND_CRAFT + JobRunner._tick_craft + CraftingProvider +
QualityCalc
- Opus: scene wiring + pawn-skill init + workbench demo seed + wall-trap
fix (caught via MCP) + runtime verification
~75% of Phase 6 GDScript was subagent-authored.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Make the demo cabin readable as a real building so the rendering pattern
is solid before Phase 6+ adds more building types.
Demo seed (world.gd._seed_phase5_demo_buildings):
- 8×6 stone cabin at (44, 23) — 23 walls (perimeter minus door slot) +
1 door (south wall centre at (47, 28)) + 24 wood-floor designations
for the interior. ConstructionProvider picks them all up; pawns build
the whole thing.
- One pre-built crate inside at (50, 24) so the interior reads as a
furnished room on first frame.
- Two external stockpile-target crates unchanged at (17, 60) / (18, 60).
Door visual rewrite (door.gd):
- Was the old 16×24 bottom-anchored shape that encroached on the cell
above. Now fits strictly within its 16×16 tile, matching the wall's
3/4 band layout (5 px lit lintel + 11 px shaded frame + inset panel
+ hinge dot). Door and walls now share a top horizon line so they
line up visually.
Designation gained TOOL_BUILD_DOOR + atlas mapping; world.gd's
_on_designation_added now branches on build_door to spawn a Door entity.
THE DOUBLE-RENDER BUG (caught by MCP inspection):
- World.mark_floor_tile stamps the Floor TileMap with atlas (2, 0) which
is *stone-grey* in the placeholder atlas, regardless of material name.
- The Floor TileMap layer was visible=true with z_index=1, so it drew
ON TOP of the brown Floor entity sprites underneath.
- Result before fix: interior tiles looked gray-stone, not wood.
- Fix: set Floor TileMap layer visible=false (data-only, same as Wall).
Entities own the visual; the TileMap retains tile-level data for
Phase 13's room detection + Phase 16's save format.
Pattern locked for future building types: 'render at entity level,
TileMap layers are data-only'. Phase 13's roof and any future wall
materials follow the same template.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User feedback after first visual inspection: 'walls looked really thick
and there was hardly any inside space'. Five targeted fixes:
1. Wall._draw rewrite — proper within-tile 3/4 perspective. Previously the
wall was a 16w×32h bottom-anchored rect that rose UP into the cell
above (which encroached on the cabin interior whenever the wall faced
south). New layout: 16×16 fitting strictly inside its own tile, with
a lit-top 5px band + shaded-front 11px band + mortar lines + outline.
Reads as 'a wall standing up' without overlapping adjacent cells.
Same shape for stone and wood materials (different palettes).
2. Demo cabin expanded from 5×4 to 8×6 — interior went from 6 cells
(closet) to 24 cells (actual room).
3. Grass-tile border darken: 0.15 → 0.04. Killed the graph-paper effect
that dominated at 3×+ zoom. Tile boundary still readable when looking
carefully; doesn't dominate the visual.
4. Removed Phase 1 _paint_sample_walls() seed. That 8×8 stone ring lived
only on the Wall TileMap layer (set to visible=false in the Phase 5
rendering pivot) — so it was an invisible path obstacle. Cleaner to
not seed it at all now that walls render at entity level.
5. CameraRig default target_zoom: 1.0 → 2.5. At 1× on a 1280-px viewport
the world feels sparse and pawns become 6-pixel dots. 2.5× shows ~30
tiles wide which is the 'comfortable inspection' level.
MCP-verified visually: the cabin now reads as a proper 8×6 stone room
with raised 3/4 walls and a real 24-cell interior. Subtle grass field.
The 'commit to 3/4 perspective' rendering decision is now actually
visible (was conceptual-only before).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three gdscript-refactor agents in parallel + Opus integration.
Entities (scenes/entities/, Agent A — 3 scripts + 3 .tscn, ~460 lines):
- item.gd: 16-type StringName registry (matches design.md filter chips);
Node2D + _draw() colored square + stack-count badge; to_dict/from_dict
- tree.gd: class_name HarvestableTree (Godot 4 ships a built-in 'Tree'
Control class — renamed to avoid the shadow); CHOP_TICKS=80; on_chop_tick
advances progress, fells when complete, drops 3 wood items at tile +
walkable neighbours
- rock.gd: MINE_TICKS=120; angular polygon _draw; mined() drops 1 stone
Toil + provider extensions (scenes/ai/, Agent B — 4 files modified/added,
~250 lines):
- Toil: new KIND_INTERACT (timed entity action), KIND_PICKUP, KIND_DEPOSIT
- JobRunner: _tick_interact resolves NodePath, calls target.<method>()
each tick, marks done when is_choppable/is_mineable returns false;
_tick_pickup finds Item at pawn.tile, transfers to pawn.carried_item;
_tick_deposit places carried_item at pawn.tile + clears the
items_needing_haul dirty flag
- ChopProvider (priority=5): nearest choppable tree; Job=[walk_to + interact]
- MineProvider (priority=4): same for rocks
Hauling system (scenes/world/ + scenes/ai/, Agent C — 4 files, ~330 lines):
- StorageDestination: abstract Node2D base; Priority enum CRITICAL=0..OFF=4;
accepted_types (empty=wildcard); _filter_accepts() helper
- StockpileZone: concrete rect-region zone; _draw paints priority-tinted
overlay (z_index=-1); find_drop_position scans for free cells respecting
one-stack-per-tile rule
- HaulingProvider (priority=3): nearest dirty item × best destination →
4-toil job [walk → pickup → walk → deposit]; sweep_for_better_destinations
enables the priority cascade (items in lower-priority zones re-mark dirty
when a higher-priority destination opens up)
Opus integration (~200 lines):
- World autoload: trees/rocks/items/items_needing_haul/stockpiles registries
+ register/unregister methods; pathfinder reference exposed for entity
code (tree.fell needs is_walkable for neighbour drops)
- Pawn: carried_item slot + carry-indicator (small colored rect upper-right
of body) via queue_redraw in _on_sim_tick
- World scene: registers chop/mine/haul/rest providers; spawns 6 trees
(cluster east-north), 4 rocks (south-east), 2 stockpile zones (Zone A
wood-only NORMAL, Zone B wildcard HIGH); periodic
hauling_provider.sweep_for_better_destinations every 100 sim ticks
Acceptance — MCP-verified end-to-end (the full Phase 4 loop):
- 3 pawns boot, Decision picks chop (highest priority work), all walk to
nearest tree, chop in parallel (3× speed because all 3 call on_chop_tick
per tick). Trees fell, drop wood (18 items). Pawns move to rocks, mine,
drop stone (4 items). Total 22 items spawn.
- HaulingProvider routes wood + stone toward Zone B (wildcard HIGH > Zone
A's wood-only NORMAL). Pawns carry items one at a time, visual indicator
shows during transit. Items deposit, items_needing_haul dirty flag
clears.
- **Priority cascade test:** Zone A promoted from NORMAL to CRITICAL.
Manually-triggered sweep marks 3 wood items in Zone B for re-haul.
Within a few thousand ticks: Zone A has 5 wood (cascaded from Zone B),
Zone B has 4 stone only (wood left, stone stayed because Zone A rejects
stone). Filter + priority cascade working exactly per design.md spec.
Phase 4 gotchas (logged in implementation.md):
- 'Tree' shadows Godot 4's built-in Tree Control class — class_name had to
be renamed to HarvestableTree. Scene/file names stayed as 'tree' since
the game concept is still 'tree'; the rename only affects code-side
type references.
- draw_colored_polygon(points, color) takes a SINGLE Color, not a
PackedColorArray. Agent C had to be reminded; draw_polygon(points, colors)
is the variant that takes per-vertex colors.
- Godot's class-name cache lags behind file changes — a full editor scan
('godot --headless --editor --quit') is needed to flush. Even after
reload_project, type-annotation assignments can fail; duck-typed
variables ('var x = scene.instantiate()') sidestep the issue.
- JobRunner's _tick_deposit had to explicitly call
World.clear_item_haul_flag — the dirty set persisted otherwise and
items appeared 'needing haul' even after deposit.
Delegation report this phase:
- Agent A (Sonnet, gdscript-refactor): Tree + Rock + Item entities + i18n
keys. ~460 lines.
- Agent B (Sonnet, gdscript-refactor): Toil extensions + JobRunner handlers
+ ChopProvider + MineProvider. ~250 lines.
- Agent C (Sonnet, gdscript-refactor): StorageDestination + StockpileZone
+ HaulingProvider with cascade sweep. ~330 lines.
- Opus: World autoload extensions (entity registries + pathfinder ref),
Pawn carry slot + visual, world.tscn/gd wiring, the Tree rename, the
draw_colored_polygon fix, the dirty-set-clear fix, MCP-driven runtime
verification including the full chop-mine-haul loop and the priority
cascade demo.
~75% of Phase 4's GDScript was subagent-authored.
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>
Researcher (Haiku) scanned all 6 Ventilatore packs (Premium, Castles
and Fortresses, Medieval Interiors, Desert Oasis, Snow Adventures,
Turning of the Seasons). The bundle is character + terrain + decoration
only — no animal/creature sprites at all except player + slimes.
Wolf options now narrowed to: commission, CC0 source, or recolor-a-dog
placeholder until Phase 10. Ventilatore-search sub-option closed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wood walls (early game) custom-authored on top of FG_Houses warm-brown
timber palette to preserve the Stardew-cabin warmth that's the project's
aesthetic anchor. ~½ day of pixel art for corner/T/cap/cross variants.
Stone walls (upgrade material) imported from FG_Fortress autotile-solvable
as-is. Both materials plug into the same WallMaterial enum / construction
pipeline. Phase 5 estimate bumped from 2-3 wks to 2.5-3.5 wks.
Phase 1 wall-rendering test now specified: use FG_Fortress as the
drop-in test material; wood walls land in Phase 5 alongside the
authoring task.
Iconic Homestead $19.99 fallback formally not needed.
memory.md decisions table now includes the wall locks; the original
3-option open question collapsed to a back-reference. art.md got a
new 'Wall-material decision' section replacing the options block.
Wolf-sprite question still open; Ventilatore search dispatched.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>