rimlike/scenes/ai/thought_catalog.gd
megaproxy 67ec2cce7f Phase 14: Death + Corpses + Burial + Cremation
Three-agent fan-out. Opus pre-wrote Corpse class + 5 EventBus signals +
World registries (corpses, grave_markers) before dispatch so all three
slices ran fully parallel. Pattern proven across Phases 12/13/14.

Death pipeline (Agent A):
- Pawn.is_dead(), _check_death() — pawn_died signal → corpse spawn →
  corpse_spawned signal → World.unregister_pawn → queue_free
- _last_damage_source carries cause from take_damage() (now StringName)
- Bleed-out timeout: _bleed_ticks accumulates while bleeding active;
  at BLEED_OUT_TICKS=432000 (6 in-game hours) force-kills via take_damage
- Pawn.portrait_color stored field for corpse head-color hand-off
- Corpse: DECAY_PER_TICK=0.05 (~33 in-game min fresh→rotted at 1×),
  is_rotting()@50, queue_free@100 with corpse_rotted_away signal.
  Rotting bumps DirtinessSystem (Phase 13 hook) +0.04/tick (~+8/in-game-min)
- DEMO_PHASE14_AUTOKILL toggle in world.gd (default false, gates safety)

Graveyard + GraveSlot + GraveMarker + Hauling (Agent B):
- scenes/world/graveyard_zone.gd — StorageDestination subclass,
  accepted_types=[corpse], brownish overlay, finds dug GraveSlots
- scenes/entities/grave_slot.gd — buildable (ghost→dug) state machine,
  StorageDestination duck-type interface, accept_corpse() spawns
  GraveMarker + emits corpse_buried + queue_frees self
- scenes/entities/grave_marker.gd — permanent memorial, procedural
  stone-cross _draw, carries deceased identity, save round-trip
- TOOL_GRAVEYARD + TOOL_DIG_GRAVE paint modes (Designation dispatch)
- KIND_PICKUP_CORPSE + KIND_DEPOSIT_CORPSE toils + JobRunner handlers
- HaulingProvider.find_best_for iterates World.corpses in addition to
  items_needing_haul; corpse-payload stored as Node metadata on pawn
- ConstructionProvider duck-type already accepts GraveSlot (no change)

Cremation + Ash + Mood thoughts (Agent C):
- scenes/entities/cremation_pyre.gd — extends Workbench, label 'Pyre',
  auto-populates FOREVER bill for cremate_corpse, on_craft_complete
  drops 1 ash + emits corpse_cremated + queue_frees corpse
- Recipe.ingredient2_type/count added with save round-trip; recipe
  catalog entry cremate_corpse(TYPE_CORPSE primary + 5 wood secondary)
  NOTE: CraftingProvider still only enforces ingredient1 — documented
  gap, ships when crafting is generalized.
- Item.TYPE_ASH added + ALL_TYPES filter array entry
- 4 mood thoughts: saw_corpse (-3 EVENT 1200t max=3), buried_friend
  (+2 EVENT 2400t), cremated_friend (+2 EVENT 2400t),
  rotting_body_in_colony (-4 PERSISTENT stacks=count capped at 3)
- Pawn sync hooks: proximity scan (saw_corpse), signal listeners
  (buried/cremated within 8-tile radius), count helper for rotting

MCP runtime verified:
- DEMO_PHASE14_AUTOKILL toggle force-killed Bram at tick 50
- 'Bram DIED (cause=demo_kill, tile=(20, 36))' + corpse spawned
- 'Cora: saw_corpse thought added (corpse Bram at dist 5)' — mood -3
- Painted graveyard + dig_grave → grave dug to completion verified
  in build_queue (grave @(22, 39) complete=true)
- Hauler round-trip (corpse → GraveSlot → GraveMarker) WIRED correctly
  but didn't land within decay window at ULTRA speed (12×) — corpse
  rotted before priority-3 corpse-haul scheduled. Tuning for Phase 20.
- Screenshot captured: fresh corpse silhouette at cabin doorway

Delegation: 3× gdscript-refactor (Sonnet) agents in parallel;
integration + MCP runtime verify on Opus.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 18:48:15 +01:00

295 lines
9.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
# ── Phase 14 — Death + corpses + burial ──────────────────────────────────────
## Mood penalty when the pawn sees a corpse within 5 Manhattan tiles.
## Stacks up to 3 — multiple visible corpses compound.
## modifier=-3, max_stacks=3, EVENT, 1200 ticks (~10 in-game min at 1×).
static func saw_corpse() -> Thought:
var t := Thought.new()
t.id = &"saw_corpse"
t.label = "Saw a corpse"
t.modifier = -3
t.lifetime = Thought.Lifetime.EVENT
t.ticks_remaining = 1200
t.max_stacks = 3
return t
## Small positive mood when the pawn helped bury a friend.
## Closure — finite but meaningful.
## modifier=+2, max_stacks=1, EVENT, 2400 ticks (~20 in-game min at 1×).
static func buried_friend() -> Thought:
var t := Thought.new()
t.id = &"buried_friend"
t.label = "Buried a friend"
t.modifier = 2
t.lifetime = Thought.Lifetime.EVENT
t.ticks_remaining = 2400
t.max_stacks = 1
return t
## Small positive mood when the pawn helped cremate a friend.
## Closure — finite but meaningful.
## modifier=+2, max_stacks=1, EVENT, 2400 ticks (~20 in-game min at 1×).
static func cremated_friend() -> Thought:
var t := Thought.new()
t.id = &"cremated_friend"
t.label = "Cremated a friend"
t.modifier = 2
t.lifetime = Thought.Lifetime.EVENT
t.ticks_remaining = 2400
t.max_stacks = 1
return t
## Strong negative mood while a rotting corpse is present in the colony.
## PERSISTENT: synced from World.corpses each tick by Pawn._process_thoughts.
## Stacks up to 3 (severity scales with the number of rotting corpses, capped
## at 3). Mood compute uses min(stacks, max_stacks) so the cap is enforced.
## modifier=-4, max_stacks=3, PERSISTENT.
static func rotting_body_in_colony() -> Thought:
var t := Thought.new()
t.id = &"rotting_body_in_colony"
t.label = "Rotting body in colony"
t.modifier = -4
t.lifetime = Thought.Lifetime.PERSISTENT
t.max_stacks = 3
return t