rimlike/scenes/ai/thought_catalog.gd
megaproxy d9638a4ea4 fix six critical bugs from audit sprint
save/load round-trip: workbench bills, crop static-method, bed owner,
wolf target now all survive reload via Bill.from_dict reconstruction,
_spawn_crop using setup(), and a new _post_load_resolve_references pass.

PlantProvider: sow path added; consumes 1 grain on a TILLED crop tile.

CraftingProvider: ingredient2 supported via new KIND_DEPOSIT_AT_WB toil
and Workbench.deposited_inputs buffer. Cremation pyre now actually
consumes wood.

HaulingProvider: per-item haul_retry_count + haul_rejected after 3
orphan passes; new EventBus.stockpile_layout_changed resets rejects on
any player stockpile edit.

Storyteller: 14 stubbed event effects implemented. New buff registry
(add_buff/get_buff_multiplier/has_buff, day-prune, save/load) drives
seasonal/resource events. New request_pawn_spawn signal + WANDERER
table for arrivals. New SICK status + 3 mood thoughts.

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

340 lines
11 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
## ── Phase 17 — Storyteller event thoughts ──────────────────────────────────
## Positive mood boost after surviving a full year. Applied colony-wide by the
## "one_year_survived" milestone event.
## modifier=+6, max_stacks=1, EVENT, ~2 in-game days (9600 ticks at 20 Hz).
static func we_made_it() -> Thought:
var t := Thought.new()
t.id = &"we_made_it"
t.label = "We made it through a year"
t.modifier = 6
t.lifetime = Thought.Lifetime.EVENT
t.ticks_remaining = 9600
t.max_stacks = 1
return t
## Negative mood penalty after the colony turned away refugees.
## Applied colony-wide by the "refugee_family" wanderer event (refuse branch).
## modifier=-4, max_stacks=1, EVENT, ~1 in-game day (4800 ticks at 20 Hz).
static func refused_refugees() -> Thought:
var t := Thought.new()
t.id = &"refused_refugees"
t.label = "We turned away the refugees"
t.modifier = -4
t.lifetime = Thought.Lifetime.EVENT
t.ticks_remaining = 4800
t.max_stacks = 1
return t
## Positive mood boost when a newcomer joins the colony.
## Applied colony-wide (including the new pawn, after they are registered)
## by wanderer-accept events.
## modifier=+3, max_stacks=1, EVENT, ~1 in-game day (4800 ticks at 20 Hz).
static func hopeful_newcomer() -> Thought:
var t := Thought.new()
t.id = &"hopeful_newcomer"
t.label = "A new face among us"
t.modifier = 3
t.lifetime = Thought.Lifetime.EVENT
t.ticks_remaining = 4800
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