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>
82 lines
2.9 KiB
GDScript
82 lines
2.9 KiB
GDScript
extends CanvasLayer
|
|
## Top-bar HUD: speed/pause buttons and tick counter.
|
|
##
|
|
## Buttons call Sim.set_speed(); active button is yellow-tinted.
|
|
## Tick label updates on every EventBus.sim_tick signal.
|
|
## Keyboard shortcuts (pause / speed_normal / speed_fast / speed_ultra) are
|
|
## handled here so the bar is the single owner of speed-input logic.
|
|
|
|
const ACTIVE_MODULATE := Color(1.2, 1.2, 0.8)
|
|
const IDLE_MODULATE := Color.WHITE
|
|
|
|
@onready var pause_btn : Button = $Anchor/ButtonRow/PauseBtn
|
|
@onready var normal_btn : Button = $Anchor/ButtonRow/NormalBtn
|
|
@onready var fast_btn : Button = $Anchor/ButtonRow/FastBtn
|
|
@onready var ultra_btn : Button = $Anchor/ButtonRow/UltraBtn
|
|
@onready var tick_label : Label = $Anchor/TickLabel
|
|
@onready var clock_label : Label = $Anchor/ClockLabel
|
|
|
|
# Maps Speed enum value → the corresponding Button node.
|
|
var _speed_buttons: Dictionary = {}
|
|
|
|
# Early-out cache: only set clock_label.text when the string changes.
|
|
var _last_clock_text: String = ""
|
|
|
|
|
|
func _ready() -> void:
|
|
pause_btn.text = Strings.t(&"speed.pause")
|
|
normal_btn.text = Strings.t(&"speed.normal")
|
|
fast_btn.text = Strings.t(&"speed.fast")
|
|
ultra_btn.text = Strings.t(&"speed.ultra")
|
|
tick_label.text = "(boot)"
|
|
clock_label.text = Strings.t(&"clock.format").format({"d": 1, "t": "06:00"})
|
|
|
|
_speed_buttons = {
|
|
Sim.Speed.PAUSE: pause_btn,
|
|
Sim.Speed.NORMAL: normal_btn,
|
|
Sim.Speed.FAST: fast_btn,
|
|
Sim.Speed.ULTRA: ultra_btn,
|
|
}
|
|
|
|
pause_btn.pressed.connect(func() -> void: Sim.set_speed(Sim.Speed.PAUSE))
|
|
normal_btn.pressed.connect(func() -> void: Sim.set_speed(Sim.Speed.NORMAL))
|
|
fast_btn.pressed.connect(func() -> void: Sim.set_speed(Sim.Speed.FAST))
|
|
ultra_btn.pressed.connect(func() -> void: Sim.set_speed(Sim.Speed.ULTRA))
|
|
|
|
EventBus.speed_changed.connect(_on_speed_changed)
|
|
EventBus.sim_tick.connect(_on_sim_tick)
|
|
EventBus.sim_tick.connect(_on_clock_refresh)
|
|
|
|
# Reflect the initial speed state without emitting a signal.
|
|
_apply_highlight(Sim.current_speed)
|
|
|
|
|
|
func _unhandled_input(event: InputEvent) -> void:
|
|
if event.is_action_pressed("pause"):
|
|
Sim.set_speed(Sim.Speed.PAUSE)
|
|
elif event.is_action_pressed("speed_normal"):
|
|
Sim.set_speed(Sim.Speed.NORMAL)
|
|
elif event.is_action_pressed("speed_fast"):
|
|
Sim.set_speed(Sim.Speed.FAST)
|
|
elif event.is_action_pressed("speed_ultra"):
|
|
Sim.set_speed(Sim.Speed.ULTRA)
|
|
|
|
|
|
func _on_speed_changed(new_speed: int) -> void:
|
|
_apply_highlight(new_speed as Sim.Speed)
|
|
|
|
|
|
func _on_sim_tick(tick_number: int) -> void:
|
|
tick_label.text = Strings.t(&"hud.tick").format({"n": tick_number})
|
|
|
|
|
|
func _on_clock_refresh(_n: int) -> void:
|
|
var t: String = Strings.t(&"clock.format").format({"d": Clock.current_day(), "t": Clock.time_string()})
|
|
if t != _last_clock_text:
|
|
_last_clock_text = t
|
|
clock_label.text = t
|
|
|
|
|
|
func _apply_highlight(speed: Sim.Speed) -> void:
|
|
for s: int in _speed_buttons:
|
|
_speed_buttons[s].modulate = ACTIVE_MODULATE if s == speed else IDLE_MODULATE
|