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>
105 lines
3.5 KiB
GDScript
105 lines
3.5 KiB
GDScript
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
|