Phase 13: Rooms + Auto-roof + Beauty + Dirtiness + Cleaning

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>
This commit is contained in:
megaproxy 2026-05-11 17:19:23 +01:00
parent 92f4e5c945
commit 9cf9b7dbfd
28 changed files with 1286 additions and 28 deletions

View file

@ -17,6 +17,10 @@ const TILE_STONE_DARK: Vector2i = Vector2i(3, 0)
const PLACEHOLDER_SOURCE_ID: int = 0
const BEAUTY_SYSTEM_SCRIPT: Script = preload("res://scenes/world/beauty_system.gd")
const DIRTINESS_SYSTEM_SCRIPT: Script = preload("res://scenes/world/dirtiness_system.gd")
const CLEANING_PROVIDER_SCRIPT: Script = preload("res://scenes/ai/cleaning_provider.gd")
const INDOOR_TINT_SCRIPT: Script = preload("res://scenes/world/indoor_tint_overlay.gd")
const PAWN_SCENE: PackedScene = preload("res://scenes/pawn/pawn.tscn")
const TREE_SCENE: PackedScene = preload("res://scenes/entities/tree.tscn")
const ROCK_SCENE: PackedScene = preload("res://scenes/entities/rock.tscn")
@ -86,6 +90,7 @@ const SEASON_TINTS: Dictionary = {
@onready var eat_provider: EatProvider = $EatProvider
@onready var sleep_provider: SleepProvider = $SleepProvider
@onready var doctor_provider: DoctorProvider = $DoctorProvider
@onready var room_detector = $RoomDetector # RoomDetector — duck-typed (class_name scan-time window)
func _ready() -> void:
@ -112,6 +117,37 @@ func _ready() -> void:
World.floor_layer = floor_layer
World.designation_layer = designation_layer
# Phase 13 — wire RoomDetector; setup with map size so BFS knows map bounds.
room_detector.setup(MAP_SIZE_TILES)
World.room_detector = room_detector
# Phase 13 — instantiate BeautySystem + DirtinessSystem + CleaningProvider as
# runtime children (no .tscn entry needed; they're stateful Nodes with no
# editor-tunable exports). Autoload refs let entity code reach them.
var beauty := Node.new()
beauty.set_script(BEAUTY_SYSTEM_SCRIPT)
beauty.name = "BeautySystem"
add_child(beauty)
World.beauty_system = beauty
var dirtiness := Node.new()
dirtiness.set_script(DIRTINESS_SYSTEM_SCRIPT)
dirtiness.name = "DirtinessSystem"
add_child(dirtiness)
World.dirtiness_system = dirtiness
var cleaning := Node.new()
cleaning.set_script(CLEANING_PROVIDER_SCRIPT)
cleaning.name = "CleaningProvider"
add_child(cleaning)
# Phase 13 — instantiate IndoorTintOverlay so roofed rooms get a subtle warm
# overlay. Node2D, z_index 3 (set in its _ready). Self-listens to room_changed.
var indoor_tint := Node2D.new()
indoor_tint.set_script(INDOOR_TINT_SCRIPT)
indoor_tint.name = "IndoorTintOverlay"
add_child(indoor_tint)
# Designation: bind the paint surface + the Selection guard.
designation_ctl.bind(designation_layer, selection)
@ -128,6 +164,7 @@ func _ready() -> void:
World.register_work_provider(crafting_provider)
World.register_work_provider(plant_provider)
World.register_work_provider(hauling_provider)
World.register_work_provider(cleaning) # priority 2 — between haul (3) and rest (0)
World.register_work_provider(rest_provider)
# Phase 5: bridge designation paint events → spawn the ghost-state entity
@ -139,6 +176,25 @@ func _ready() -> void:
_spawn_sample_harvestables()
_spawn_sample_stockpiles()
_seed_phase5_demo_buildings()
# Phase 13 — pre-stamp the cabin walls + floors on the TileMap data layers
# so RoomDetector can see a completed enclosure at boot without waiting for
# pawns to finish the build queue. Mirrors the layout in _seed_phase5_demo_buildings.
_prestamp_cabin_for_room_detector()
_prestamp_test_shed_for_room_detector()
room_detector.recompute_around(Vector2i(47, 25))
room_detector.recompute_around(Vector2i(36, 25)) # tiny shed centroid
# Phase 13 — register existing prebuilt furniture with BeautySystem so it has
# a beauty score baseline at boot (the post-_complete hooks already fire for
# the cabin's beds/torches/workbenches, but pawns may path before the first
# recompute, so seed the map here).
for ws in World.workbenches:
beauty.register_furniture(ws)
for b in World.beds:
beauty.register_furniture(b)
for ls in World.light_sources:
beauty.register_furniture(ls)
beauty.recompute_all()
_run_pathfinder_spike()
# Phase 4: every 5 in-game seconds (100 ticks), re-evaluate items in
@ -527,6 +583,88 @@ func _apply_season_tint(season: StringName) -> void:
Audit.log("world", "season tint → %s (%s)" % [season, tint])
# ── Phase 13: demo seed room helper ─────────────────────────────────────────
# Directly stamps the cabin perimeter walls and interior floors on the data
# TileMap layers so RoomDetector can detect the enclosure at boot.
# The ghost entities are already queued — this only writes the data layer
# (same as Wall._complete / Floor._complete call). Without this,
# RoomDetector would have to wait until pawns finish building the walls
# (many seconds of real time) before the first room is visible.
#
# Layout mirrors _seed_phase5_demo_buildings: 8×6 cabin at (44, 23),
# door at (47, 28), 6×4 wood floor interior.
func _prestamp_cabin_for_room_detector() -> void:
var origin := Vector2i(44, 23)
var w := 8
var h := 6
var door_x := w / 2 - 1 # 3 → tile x=47
var door_tile := origin + Vector2i(door_x, h - 1)
# Stamp perimeter walls (skipping the door slot).
for x in w:
World.mark_wall_tile(origin + Vector2i(x, 0), &"stone")
var bottom := origin + Vector2i(x, h - 1)
if bottom != door_tile:
World.mark_wall_tile(bottom, &"stone")
for y in range(1, h - 1):
World.mark_wall_tile(origin + Vector2i(0, y), &"stone")
World.mark_wall_tile(origin + Vector2i(w - 1, y), &"stone")
# Stamp interior floors.
for x in range(1, w - 1):
for y in range(1, h - 1):
World.mark_floor_tile(origin + Vector2i(x, y), &"wood")
# Stamp the door slot as a wall so the perimeter is fully closed and BFS
# terminates cleanly. Door._complete() erases this wall stamp and registers
# the door entity; the follow-up recompute_around picks it up as a boundary.
World.mark_wall_tile(door_tile, &"stone")
Audit.log("world", "phase 13 demo: cabin walls+floors pre-stamped for RoomDetector")
# Tiny 5×5 walled shed with 3×3 interior (= 9 floor tiles) — under the 16-cap
# auto-roof threshold, so this room WILL roof. Used to exercise the indoor /
# shelter / room-thoughts pipeline without resizing the main cabin.
func _prestamp_test_shed_for_room_detector() -> void:
var origin := Vector2i(34, 23) # left of cabin, on the grass plain
var w := 5
var h := 5
# Perimeter walls — instantiate complete Wall entities so they're visible.
for x in w:
_spawn_complete_wall(origin + Vector2i(x, 0))
_spawn_complete_wall(origin + Vector2i(x, h - 1))
for y in range(1, h - 1):
_spawn_complete_wall(origin + Vector2i(0, y))
_spawn_complete_wall(origin + Vector2i(w - 1, y))
# Interior floors — instantiate complete Floor entities.
for x in range(1, w - 1):
for y in range(1, h - 1):
_spawn_complete_floor(origin + Vector2i(x, y))
Audit.log("world", "phase 13 demo: 5×5 test shed pre-built (interior 9 tiles, auto-roofed)")
# Instantiate a Wall entity in completed state at `tile`. Bypasses the build
# queue. Used by the Phase 13 test shed seed.
func _spawn_complete_wall(tile: Vector2i) -> void:
var w = WALL_SCENE.instantiate()
w.setup(tile, &"stone")
add_child(w)
w.build_progress = w.BUILD_TICKS
w.on_build_tick() # triggers _complete() and the data-layer stamp + room recompute
# Instantiate a Floor entity in completed state at `tile`.
func _spawn_complete_floor(tile: Vector2i) -> void:
var f = FLOOR_SCENE.instantiate()
f.setup(tile, &"wood")
add_child(f)
f.build_progress = f.BUILD_TICKS
f.on_build_tick()
# ── spike: AStarGrid2D query timing at 80² ──────────────────────────────────
func _run_pathfinder_spike() -> void: