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>
This commit is contained in:
parent
43e52ffe75
commit
a1e5b38dd6
16 changed files with 606 additions and 10 deletions
|
|
@ -31,6 +31,17 @@ const TILE_SIZE_PX: int = 16
|
|||
## Sim ticks to build a workbench (90 ticks ≈ 4.5 sim seconds at 1×).
|
||||
const BUILD_TICKS: int = 90
|
||||
|
||||
# ── Phase 11: light-source support ───────────────────────────────────────────
|
||||
## Workbenches whose label_text is in this list emit light when completed.
|
||||
## Currently only the Hearth (open-fire cooking) qualifies.
|
||||
const LIGHT_EMITTING_LABELS: Array[String] = ["Hearth"]
|
||||
|
||||
## Sim-side Manhattan-distance light radius for the Hearth (tiles). Max 8.
|
||||
const HEARTH_LIGHT_RADIUS: int = 5
|
||||
|
||||
## Pixel size of the procedural radial gradient used for PointLight2D.
|
||||
const LIGHT_TEXTURE_SIZE: int = 64
|
||||
|
||||
# ── exports ───────────────────────────────────────────────────────────────────
|
||||
|
||||
## Tile position of this workbench in world-tile coordinates.
|
||||
|
|
@ -68,6 +79,11 @@ var current_bill = null
|
|||
## JobRunner reads this to decide whether the recipe's work_ticks are done.
|
||||
var current_work_progress: int = 0
|
||||
|
||||
## PointLight2D child for workbenches that emit light (Hearth). null for all
|
||||
## others. Built in _ready() when label_text is in LIGHT_EMITTING_LABELS;
|
||||
## enabled only after _complete() fires.
|
||||
var _light: PointLight2D = null
|
||||
|
||||
|
||||
# ── lifecycle ─────────────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -78,11 +94,20 @@ func _ready() -> void:
|
|||
tile.y * TILE_SIZE_PX + TILE_SIZE_PX
|
||||
)
|
||||
World.register_workbench(self)
|
||||
# Phase 11: light-emitting workbenches register with the light-source registry.
|
||||
# All workbenches register; non-emitters return false from is_on() so
|
||||
# World.is_tile_lit() skips them at zero cost.
|
||||
World.register_light_source(self)
|
||||
if label_text in LIGHT_EMITTING_LABELS:
|
||||
_light = _build_point_light_2d()
|
||||
add_child(_light)
|
||||
_light.enabled = false # dark until built
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
World.unregister_workbench(self)
|
||||
World.unregister_light_source(self)
|
||||
|
||||
|
||||
## One-shot initialiser. Call after add_child() so _ready() has fired.
|
||||
|
|
@ -180,6 +205,29 @@ func on_craft_interrupted() -> void:
|
|||
current_work_progress = 0
|
||||
|
||||
|
||||
# ── Phase 11: light-source duck-type interface ────────────────────────────────
|
||||
## Shared by Torch. World.is_tile_lit() and the "in darkness" thought call these.
|
||||
|
||||
## True when this workbench emits light and has finished construction.
|
||||
## Non-emitting workbenches (Carpenter, Smelter, Millstone) always return false.
|
||||
func is_on() -> bool:
|
||||
return _completed and label_text in LIGHT_EMITTING_LABELS
|
||||
|
||||
|
||||
## The tile this light source occupies (for Manhattan-distance calculation).
|
||||
func get_light_tile() -> Vector2i:
|
||||
return tile
|
||||
|
||||
|
||||
## The sim-side Manhattan-distance radius of this light source.
|
||||
## Returns 0 for non-emitting workbenches; World.is_tile_lit() still calls this
|
||||
## but d <= 0 is never true for a non-adjacent tile so it's a no-op in practice.
|
||||
func get_light_radius() -> int:
|
||||
if label_text in LIGHT_EMITTING_LABELS:
|
||||
return HEARTH_LIGHT_RADIUS
|
||||
return 0
|
||||
|
||||
|
||||
# ── save / load ───────────────────────────────────────────────────────────────
|
||||
|
||||
## Serialise workbench state for World save (wired in Phase 16).
|
||||
|
|
@ -352,5 +400,43 @@ func _draw_generic(alpha: float) -> void:
|
|||
|
||||
func _complete() -> void:
|
||||
_completed = true
|
||||
# Phase 11: enable PointLight2D for light-emitting workbenches on completion.
|
||||
if _light != null:
|
||||
_light.enabled = is_on()
|
||||
queue_redraw()
|
||||
Audit.log("workbench", "%s built at %s" % [label_text, tile])
|
||||
|
||||
|
||||
# ── Phase 11: internal light helpers ─────────────────────────────────────────
|
||||
|
||||
## Construct and return the PointLight2D that provides Godot-side visual lighting.
|
||||
## Mirrors Torch._build_point_light_2d(). Duplicated here to avoid a cross-file
|
||||
## dependency; the ~20 lines of duplication is the lesser cost.
|
||||
func _build_point_light_2d() -> PointLight2D:
|
||||
var p := PointLight2D.new()
|
||||
p.texture = _build_radial_light_texture(LIGHT_TEXTURE_SIZE)
|
||||
# Scale so the texture radius covers HEARTH_LIGHT_RADIUS tiles in world-pixels.
|
||||
p.texture_scale = float(HEARTH_LIGHT_RADIUS) * float(TILE_SIZE_PX) / float(LIGHT_TEXTURE_SIZE) * 2.0
|
||||
p.color = Color(1.0, 0.80, 0.50, 1.0) # warm hearthfire tint, slightly redder than torch
|
||||
p.energy = 1.0
|
||||
# Offset upward so the light originates from the flame area, not the tile base.
|
||||
p.position = Vector2(0.0, -10.0)
|
||||
return p
|
||||
|
||||
|
||||
## Build a soft radial gradient Image and return it as an ImageTexture.
|
||||
## White centre fades to transparent at the edge via smoothstep falloff.
|
||||
static func _build_radial_light_texture(size: int) -> Texture2D:
|
||||
var img := Image.create(size, size, false, Image.FORMAT_RGBA8)
|
||||
var cx: float = float(size) / 2.0
|
||||
var cy: float = float(size) / 2.0
|
||||
var max_r: float = float(size) / 2.0
|
||||
for x in size:
|
||||
for y in size:
|
||||
var dx: float = float(x) - cx
|
||||
var dy: float = float(y) - cy
|
||||
var d: float = sqrt(dx * dx + dy * dy)
|
||||
var t: float = clampf(1.0 - d / max_r, 0.0, 1.0)
|
||||
var a: float = t * t * (3.0 - 2.0 * t)
|
||||
img.set_pixel(x, y, Color(1.0, 1.0, 1.0, a))
|
||||
return ImageTexture.create_from_image(img)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue