rimlike/scenes/ai/thought_catalog.gd
megaproxy 9cf9b7dbfd 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>
2026-05-11 17:19:23 +01:00

236 lines
7.3 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

class_name ThoughtCatalog
## Static factory registry for named thoughts.
##
## Phase 8 ships 5 thoughts (hungry, tired, well_rested, slept_on_floor,
## ate_meal). Phase 17 expands with: slept_in_good_bed (quality tiers),
## ate_raw_food, witnessed_corpse, in_darkness, cramped_quarters,
## beautiful_room, ugly_room, damp, soaked, cold.
##
## Usage pattern:
## pawn.add_thought(ThoughtCatalog.ate_meal())
##
## Each factory returns a fresh Thought with all fields set to correct defaults
## for that thought type. Callers must not mutate the returned object before
## passing it to add_thought() — add_thought() handles stack merging.
##
## docs/architecture.md "MoodSystem"; docs/design.md "Thought list (~13)".
# ── PERSISTENT thoughts ───────────────────────────────────────────────────────
# Pawn._refresh_persistent_thoughts adds / removes these based on live state.
# max_stacks=1 because each is binary (either hungry or not).
## Mood penalty while pawn.is_hungry() is true.
## modifier=-6, max_stacks=1, PERSISTENT.
static func hungry() -> Thought:
var t := Thought.new()
t.id = &"hungry"
t.label = "Hungry"
t.modifier = -6
t.lifetime = Thought.Lifetime.PERSISTENT
t.max_stacks = 1
return t
## Mood penalty while pawn.is_tired() is true.
## modifier=-4, max_stacks=1, PERSISTENT.
static func tired() -> Thought:
var t := Thought.new()
t.id = &"tired"
t.label = "Tired"
t.modifier = -4
t.lifetime = Thought.Lifetime.PERSISTENT
t.max_stacks = 1
return t
# ── EVENT thoughts ────────────────────────────────────────────────────────────
# Fire on a transition and decay after ticks_remaining reaches zero.
# ticks_remaining is in sim ticks at 1× speed (20 Hz).
# ~10 in-game min at 1× = 1200 ticks (20 ticks/s × 60 s/min × 10 min).
## Positive mood boost after waking from a full bed-sleep.
## Fires in _tick_sleep (Agent B) when had_bed=true.
## modifier=+5, max_stacks=1, EVENT, ~10 in-game min at 1×.
static func well_rested() -> Thought:
var t := Thought.new()
t.id = &"well_rested"
t.label = "Well rested"
t.modifier = 5
t.lifetime = Thought.Lifetime.EVENT
t.ticks_remaining = 1200
t.max_stacks = 1
return t
## Mood penalty after sleeping without a bed.
## Fires in _tick_sleep (Agent B) when had_bed=false.
## modifier=-5, max_stacks=1, EVENT, ~10 in-game min at 1×.
static func slept_on_floor() -> Thought:
var t := Thought.new()
t.id = &"slept_on_floor"
t.label = "Slept on the floor"
t.modifier = -5
t.lifetime = Thought.Lifetime.EVENT
t.ticks_remaining = 1200
t.max_stacks = 1
return t
## Mood penalty while a pawn is in an unlit tile at night.
## modifier=-3, max_stacks=1, PERSISTENT.
## Phase 17 polish may split into "outdoor dark" / "cave dark" tiers.
static func in_darkness() -> Thought:
var t := Thought.new()
t.id = &"in_darkness"
t.label = "In darkness"
t.modifier = -3
t.lifetime = Thought.Lifetime.PERSISTENT
t.max_stacks = 1
return t
## Mood penalty while wet accumulator is in Damp tier (2559).
## Driven by Pawn._sync_persistent_thought via wet status severity == 1.
## modifier=-3, max_stacks=1, PERSISTENT.
static func damp() -> Thought:
var t := Thought.new()
t.id = &"damp"
t.label = "Damp"
t.modifier = -3
t.lifetime = Thought.Lifetime.PERSISTENT
t.max_stacks = 1
return t
## Mood penalty while wet accumulator is in Soaked tier (60+).
## Replaces Damp — Pawn._sync_persistent_thought removes damp before adding soaked.
## modifier=-6, max_stacks=1, PERSISTENT.
static func soaked() -> Thought:
var t := Thought.new()
t.id = &"soaked"
t.label = "Soaked"
t.modifier = -6
t.lifetime = Thought.Lifetime.PERSISTENT
t.max_stacks = 1
return t
## Mood penalty while cold accumulator is active (any severity).
## Named cold_thought to avoid collision with StatusCatalog.cold() factory.
## modifier=-4, max_stacks=1, PERSISTENT.
static func cold_thought() -> Thought:
var t := Thought.new()
t.id = &"cold"
t.label = "Cold"
t.modifier = -4
t.lifetime = Thought.Lifetime.PERSISTENT
t.max_stacks = 1
return t
## ── Phase 13 — Room beauty / dirtiness thoughts ─────────────────────────────
# Synced in Pawn._process_thoughts() after the damp/soaked/cold block.
# All are PERSISTENT; the sync removes old ones before adding the active tier.
## Positive mood boost when average room beauty >= 4.0.
## modifier=+4, max_stacks=1, PERSISTENT.
static func beautiful_room() -> Thought:
var t := Thought.new()
t.id = &"beautiful_room"
t.label = "Beautiful room"
t.modifier = 4
t.lifetime = Thought.Lifetime.PERSISTENT
t.max_stacks = 1
return t
## Negative mood penalty when average room beauty < 0 (e.g. corpses present, Phase 14).
## modifier=-3, max_stacks=1, PERSISTENT.
static func ugly_room() -> Thought:
var t := Thought.new()
t.id = &"ugly_room"
t.label = "Ugly room"
t.modifier = -3
t.lifetime = Thought.Lifetime.PERSISTENT
t.max_stacks = 1
return t
## Positive mood boost when average room dirtiness < 25 (clean tier).
## modifier=+2, max_stacks=1, PERSISTENT.
static func clean_room() -> Thought:
var t := Thought.new()
t.id = &"clean_room"
t.label = "Clean room"
t.modifier = 2
t.lifetime = Thought.Lifetime.PERSISTENT
t.max_stacks = 1
return t
## Negative mood penalty when average room dirtiness is in dirty tier (25..60).
## modifier=-3, max_stacks=1, PERSISTENT.
static func dirty_room() -> Thought:
var t := Thought.new()
t.id = &"dirty_room"
t.label = "Dirty room"
t.modifier = -3
t.lifetime = Thought.Lifetime.PERSISTENT
t.max_stacks = 1
return t
## Strong negative mood penalty when average room dirtiness >= 60 (filthy tier).
## modifier=-6, max_stacks=1, PERSISTENT.
static func filthy_room() -> Thought:
var t := Thought.new()
t.id = &"filthy_room"
t.label = "Filthy room"
t.modifier = -6
t.lifetime = Thought.Lifetime.PERSISTENT
t.max_stacks = 1
return t
## Positive mood boost after sleeping in an indoor room.
## modifier=+3, max_stacks=1, EVENT, ~1200 ticks (~60 in-game sec at 1×).
## Phase 17 wires this into the sleep toil; factory added here for catalog completeness.
static func slept_in_room() -> Thought:
var t := Thought.new()
t.id = &"slept_in_room"
t.label = "Slept in a room"
t.modifier = 3
t.lifetime = Thought.Lifetime.EVENT
t.ticks_remaining = 1200
t.max_stacks = 1
return t
## Negative mood penalty for eating without a table nearby.
## modifier=-3, max_stacks=1, EVENT, ~800 ticks (~40 in-game sec at 1×).
## Phase 17 wires this into the eat toil; factory added here for catalog completeness.
static func ate_without_table() -> Thought:
var t := Thought.new()
t.id = &"ate_without_table"
t.label = "Ate without a table"
t.modifier = -3
t.lifetime = Thought.Lifetime.EVENT
t.ticks_remaining = 800
t.max_stacks = 1
return t
## Small mood boost after eating a cooked meal or bread.
## Fires in _tick_eat when item_type is TYPE_MEAL or TYPE_BREAD.
## Stacks up to 3 (multiple good meals compound, but cap at 3).
## modifier=+3, max_stacks=3, EVENT, ~800 ticks (~40 in-game sec at 1×).
static func ate_meal() -> Thought:
var t := Thought.new()
t.id = &"ate_meal"
t.label = "Ate a meal"
t.modifier = 3
t.lifetime = Thought.Lifetime.EVENT
t.ticks_remaining = 800
t.max_stacks = 3
return t