rimlike/scenes/ai/thought_catalog.gd
megaproxy 92f4e5c945 Phase 12: Seasons + Weather (rolls, rain, storm, wet/cold)
48-day year (4 seasons × 12 days), daily weather rolls, rain visual,
storm flash, Wet/Cold statuses with mood thoughts.

Three-agent fan-out — Opus prepped contracts up front (event_bus
signals, Clock season constants, Weather autoload stub) so the three
slices could run fully parallel and integrate on first try.

Calendar (Agent A):
- Clock season API — SEASON_SPRING/SUMMER/AUTUMN/WINTER constants,
  current_season(), current_season_index(), day_of_season(),
  current_year(), DAYS_PER_SEASON=12, DAYS_PER_YEAR=48
- EventBus.season_changed emitted on transition (mirrors phase_changed)
- Top-bar SeasonLabel ('Spring 1/12') with localized season names via
  Strings.t() — season.spring/summer/autumn/winter + season.format
- Terrain TileMapLayer seasonal palette modulate
  (spring=warm-green, summer=neutral, autumn=warm-orange, winter=cool-blue)

Weather (Agent B):
- autoload/weather.gd — daily roll triggered by Clock day-index change
  Probability tables per season (placeholders, tune Phase 20):
    spring  60% clear / 35% rain /  5% storm
    summer  75% clear / 18% rain /  7% storm
    autumn  50% clear / 35% rain / 12% storm /  3% cold_snap
    winter  55% clear / 15% rain / 10% storm / 20% cold_snap
- EventBus.weather_changed signal
- scenes/world/rain_overlay.tscn — procedural _draw() diagonal raindrops
  on a CanvasLayer (chosen over CPUParticles2D for pixel-art exactness
  and to colocate storm-flash logic in one weather-aware script)
- Storm flash — Tween-driven ColorRect at random 4-8s intervals
- Save round-trip preserves _last_day_index to prevent double-rolling

Wet + Cold + Mood (Agent C):
- StatusCatalog.wet(severity 1-2) — Damp at 25, Soaked at 60 (of 100)
- StatusCatalog.cold(severity 1-3) — Mild at 25, Severe at 60, Extreme at 85
- ThoughtCatalog.damp(-3), soaked(-6), cold_thought(-4)
- Pawn._wet_accum / _cold_accum floats, ticked in _process_statuses:
  +0.02/tick rain (×2 storm), -0.05/tick decay when sheltered
  +0.015/tick cold winter-or-snap (×2 cold_snap)
- _sync_wet_status / _sync_cold_status — severity-flip detection with
  one Audit line per transition
- _is_sheltered() v1: World.floor_layer.get_cell_source_id != -1
  Phase 13 replaces with proper Room BFS
- _wet_accum / _cold_accum round-trip through Pawn.to_dict / from_dict
- Persistent thought sync in _process_thoughts after in_darkness

MCP runtime verified:
- Top-bar 'Spring 1/12' renders; green seasonal terrain tint visible
- Rain droplets render across screen; storm flash captured mid-animation
- Bram wet=26 (Damp) → wet=65 (Soaked) with mood thought (-6), mood=30
- Cora cold=30 cold_snap → Cold status sev=1 + Cold thought (-4), mood=32
- Daily weather rolls visible day 0-5 (rain → clear → rain → clear → rain)

Quick-edit fixup mid-flight: Variant inference errors on
'var old_sev := s.severity' (untyped Array loop var). Same trap as
the Phase 7 crop fix; pattern is now to always explicit-type ':='
when the rhs is non-typed-Array element access.

Delegation: 3× gdscript-refactor agents in parallel, 1× quick-edit
for the Variant-inference fix; integration + MCP verify on Opus.

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

144 lines
4.6 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
## 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