rimlike/scenes/ai/thought_catalog.gd
megaproxy a1e5b38dd6 Phase 11 — Day/night cycle + Lighting (taken before Phase 9 per recommendation)
Three gdscript-refactor agents in parallel; Opus integrated and verified
the day-night transition + torch lighting via MCP runtime + screenshot.

Clock autoload (Agent A, autoload/clock.gd, ~138 lines):
- TICKS_PER_DAY = 4800 → 4 min/day at 1× / 48 s at Fast / 20 s at Ultra
- TICKS_PER_HOUR = 200 (so 60 min × ~3 ticks per minute)
- 4-phase day: night → dawn (5–7) → day (7–19) → dusk (19–22) → night
- darkness_factor() returns 0..1 with linear ramps across dawn/dusk
- phase_changed signal fires on phase transitions
- save_dict / apply_dict for save round-trip
- Boots at Day 1, 06:00 (mid-dawn for atmospheric start)
- Registered in project.godot autoload list (Opus)

Top-bar clock UI (Agent A):
- ClockLabel added to top_bar.tscn (center-anchored at ±80 px)
- _on_clock_refresh in top_bar.gd; early-out string compare to skip text
  assignments when unchanged (cheap per-tick)

Torch entity + lights registry (Agent B, scenes/entities/torch.{gd,tscn} +
workbench.gd + world.gd, ~210 lines):
- class Torch: buildable furniture, BUILD_TICKS=30, LIGHT_RADIUS=6
- Procedural radial gradient texture (64×64) generated at runtime with
  smoothstep falloff → no PNG dependency
- PointLight2D child with the gradient texture, warm fire tint, energy 1.2
- is_on / get_light_tile / get_light_radius duck-typed interface; same
  shape exposed by Workbench when label_text='Hearth' (HEARTH_LIGHT_RADIUS=5)
- World.light_sources registry + register/unregister + is_tile_lit(tile)
  (Manhattan distance, no occlusion — Phase 13 may add wall-occlusion)

CanvasModulate darkness + in_darkness thought (Agent C, ~30 lines mod +
new factory):
- DarkOverlay CanvasModulate node added to world.tscn (first child of
  World root so it tints all sibling layers + entities)
- world.gd._update_dark_overlay lerps DAY_TINT (white) ↔ NIGHT_TINT
  (0.20, 0.22, 0.40 deep cool blue) by Clock.darkness_factor() each tick
- ThoughtCatalog.in_darkness(): persistent, -3 mood, fires when
  darkness > 0.3 AND World.is_tile_lit(pawn.tile) is false
- Pawn._process_thoughts syncs in_darkness alongside hungry/tired

Opus integration:
- project.godot: Clock autoload registered
- world.tscn: DarkOverlay CanvasModulate node, plus the agent additions
- Demo seed: 2 torches inside cabin at (46, 26) + (49, 26), pre-built
- MCP-driven runtime test verified day→night transition + lighting
  effects:
  - Noon: world bright green, torches barely visible (over-bright at noon
    is minor polish — Phase 17 may scale torch energy by darkness)
  - Midnight: world deep blue/green tinted, torches cast yellow halos,
    Hearth ember glows orange, cabin interior warmly lit, exterior dark
- top_bar clock label updates each sim tick (early-out on no-change)

Phase 11 followups for later phases:
- Torch energy should scale with darkness — visible halos at noon are
  silly. Phase 17 will likely tie PointLight2D.energy to clamp(darkness,
  0.2, 1.0) so they're invisible at midday
- Wall-occlusion for light_map — Phase 13's room-detection BFS could
  treat completed wall tiles as occluders so light doesn't bleed through
- 'In darkness' thought currently treats ALL unlit cells as darkness;
  Phase 13's roof flag could differentiate 'indoors-dark' (different
  thought) from 'outdoors-dark'
- Light source visibility through CanvasModulate works correctly thanks
  to PointLight2D's additive blend mode

Acceptance — MCP-verified via play_scene + get_game_screenshot:
-  Day → Dusk → Night cycle visible (Clock.current_phase emits events)
-  CanvasModulate tints world deep blue at night
-  Torches cast visible yellow halos via PointLight2D additive blend
-  Hearth opts-in as a light source via label_text='Hearth' check
-  Top-bar clock shows 'Day N, HH:MM' format and updates each tick
-  in_darkness thought wires through _process_thoughts (would fire if
  a pawn were standing in an unlit night tile — demo didn't capture this
  specifically but the code path is verified)

Delegation report this phase:
- Agent A: Clock autoload + 4-phase day cycle + top-bar UI extension
- Agent B: Torch entity + PointLight2D + procedural radial texture +
  Workbench Hearth opt-in + World.light_sources registry
- Agent C: CanvasModulate world.tscn node + day/night colour lerp +
  in_darkness ThoughtCatalog entry + Pawn persistent thought sync
- Opus: Clock autoload registration in project.godot + 2 torches in
  demo seed + MCP runtime verification at midnight vs noon

~75% of Phase 11 GDScript was subagent-authored.

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

105 lines
3.5 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
## 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