rimlike/scenes/world/world.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

530 lines
21 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.

extends Node2D
## Phase 4 world view. 80×80 TileMap, 6 layers, 3 pawns, full AI pipeline:
## RestProvider → ChopProvider → MineProvider → HaulingProvider → idle
## plus sample trees, rocks, and two stockpile zones with different priorities
## for the haul-cascade demo.
##
## TileMap layer indices follow docs/architecture.md:
## 0 Terrain · 1 Floor · 2 Wall · 3 Designation · 4 Roof · 5 Fog
const MAP_SIZE_TILES: Vector2i = Vector2i(80, 80)
const TILE_SIZE_PX: int = 16
const TILE_GRASS: Vector2i = Vector2i(0, 0)
const TILE_DIRT: Vector2i = Vector2i(1, 0)
const TILE_STONE: Vector2i = Vector2i(2, 0)
const TILE_STONE_DARK: Vector2i = Vector2i(3, 0)
const PLACEHOLDER_SOURCE_ID: int = 0
const PAWN_SCENE: PackedScene = preload("res://scenes/pawn/pawn.tscn")
const TREE_SCENE: PackedScene = preload("res://scenes/entities/tree.tscn")
const ROCK_SCENE: PackedScene = preload("res://scenes/entities/rock.tscn")
const STOCKPILE_SCENE: PackedScene = preload("res://scenes/world/stockpile_zone.tscn")
const WALL_SCENE: PackedScene = preload("res://scenes/entities/wall.tscn")
const FLOOR_SCENE: PackedScene = preload("res://scenes/entities/floor.tscn")
const DOOR_SCENE: PackedScene = preload("res://scenes/entities/door.tscn")
const CRATE_SCENE: PackedScene = preload("res://scenes/world/crate.tscn")
const WORKBENCH_SCENE: PackedScene = preload("res://scenes/entities/workbench.tscn")
const CROP_SCENE: PackedScene = preload("res://scenes/entities/crop.tscn")
const ITEM_SCENE: PackedScene = preload("res://scenes/entities/item.tscn")
const BED_SCENE: PackedScene = preload("res://scenes/entities/bed.tscn")
const TORCH_SCENE: PackedScene = preload("res://scenes/entities/torch.tscn")
# 3 starting pawns — Phase 2 demo. Phase 7+ replaces this with map-gen + name table.
const SAMPLE_PAWNS: Array[Dictionary] = [
# Phase 6 demo — varied Crafting skill so the quality system shows visible
# spread. Phase 7 adds Cooking skill for hearth recipe variety.
{"name": "Bram", "tile": Vector2i(20, 40), "crafting": 8, "cooking": 2}, # high crafter, weak cook
{"name": "Cora", "tile": Vector2i(25, 40), "crafting": 4, "cooking": 6}, # mid crafter, strong cook
{"name": "Edda", "tile": Vector2i(30, 40), "crafting": 0, "cooking": 1}, # low crafter, weak cook
]
# Phase 4 — sample harvestables. Trees clustered east, rocks south-east.
const SAMPLE_TREES: Array[Vector2i] = [
Vector2i(58, 30), Vector2i(60, 31), Vector2i(62, 30),
Vector2i(61, 33), Vector2i(63, 34), Vector2i(59, 35),
]
const SAMPLE_ROCKS: Array[Vector2i] = [
Vector2i(60, 60), Vector2i(62, 60), Vector2i(63, 62), Vector2i(58, 62),
]
# HaulingProvider re-flow cadence — every 5 sim seconds at 1× (100 ticks).
const HAUL_SWEEP_INTERVAL_TICKS: int = 100
# Phase 11 — global darkness tint. Day = white, night = deep cool blue.
# Driven by Clock.darkness_factor() (0..1) each sim tick.
const NIGHT_TINT: Color = Color(0.20, 0.22, 0.40, 1.0)
const DAY_TINT: Color = Color(1.0, 1.0, 1.0, 1.0)
@onready var dark_overlay: CanvasModulate = $DarkOverlay
@onready var terrain_layer: TileMapLayer = $Terrain
@onready var floor_layer: TileMapLayer = $Floor
@onready var wall_layer: TileMapLayer = $Wall
@onready var designation_layer: TileMapLayer = $Designation
@onready var roof_layer: TileMapLayer = $Roof
@onready var fog_layer: TileMapLayer = $Fog
@onready var pathfinder: Pathfinder = $Pathfinder
@onready var selection: Selection = $Selection
@onready var designation_ctl: Designation = $DesignationCtl
@onready var rest_provider: RestProvider = $RestProvider
@onready var chop_provider: ChopProvider = $ChopProvider
@onready var mine_provider: MineProvider = $MineProvider
@onready var hauling_provider: HaulingProvider = $HaulingProvider
@onready var construction_provider: ConstructionProvider = $ConstructionProvider
@onready var crafting_provider: CraftingProvider = $CraftingProvider
@onready var plant_provider: PlantProvider = $PlantProvider
@onready var eat_provider: EatProvider = $EatProvider
@onready var sleep_provider: SleepProvider = $SleepProvider
func _ready() -> void:
Audit.log("world", "Phase 4 — building %d×%d world + harvestables + AI." % [MAP_SIZE_TILES.x, MAP_SIZE_TILES.y])
var tileset := _build_placeholder_tileset()
for layer in [terrain_layer, floor_layer, wall_layer, designation_layer, roof_layer, fog_layer]:
layer.tile_set = tileset
_paint_terrain()
# Phase 5: removed _paint_sample_walls() — the old 8×8 stone ring rendered
# only on the (now-hidden) Wall TileMap layer. With the rendering pivot
# to entity-based walls, that ring was invisible to the player but still
# blocked pathing. Removing it cleans the world view; the Phase 5 cabin
# is now the only player-visible structure.
_apply_camera_bounds()
pathfinder.setup(MAP_SIZE_TILES)
_wire_walls_to_pathfinder()
selection.bind(pathfinder)
World.pathfinder = pathfinder # expose to entities (Tree.fell() walkability checks, etc.)
# Phase 5 — expose TileMap layer refs on the autoload so entity code
# (Wall._complete, Floor._complete) can stamp data-only tile state.
World.wall_layer = wall_layer
World.floor_layer = floor_layer
World.designation_layer = designation_layer
# Designation: bind the paint surface + the Selection guard.
designation_ctl.bind(designation_layer, selection)
# Register all 9 providers — Decision iterates by .priority desc.
# sleep=8 > eat=7 > construction=6 > chop=5 ≈ plant=5 > mine=4 ≈ crafting=4 > haul=3 > rest=0.
# Phase 17 will tune these via the work-priority matrix UI.
World.register_work_provider(sleep_provider)
World.register_work_provider(eat_provider)
World.register_work_provider(construction_provider)
World.register_work_provider(chop_provider)
World.register_work_provider(mine_provider)
World.register_work_provider(crafting_provider)
World.register_work_provider(plant_provider)
World.register_work_provider(hauling_provider)
World.register_work_provider(rest_provider)
# Phase 5: bridge designation paint events → spawn the ghost-state entity
# at that tile and register it as a build site.
EventBus.designation_added.connect(_on_designation_added)
EventBus.designation_cleared.connect(_on_designation_cleared)
_spawn_sample_pawns()
_spawn_sample_harvestables()
_spawn_sample_stockpiles()
_seed_phase5_demo_buildings()
_run_pathfinder_spike()
# Phase 4: every 5 in-game seconds (100 ticks), re-evaluate items in
# stockpiles in case a higher-priority destination opened up.
EventBus.sim_tick.connect(_on_sim_tick_world_sweep)
func world_bounds_px() -> Rect2:
return Rect2(Vector2.ZERO, Vector2(MAP_SIZE_TILES * TILE_SIZE_PX))
# ── tileset & map painting ──────────────────────────────────────────────────
func _build_placeholder_tileset() -> TileSet:
# Four 16×16 placeholder tiles laid out as a 4×1 atlas. Real ElvGames
# art replaces this in Phase 5 (wood walls + stone walls).
var ts := TileSet.new()
ts.tile_size = Vector2i(TILE_SIZE_PX, TILE_SIZE_PX)
var atlas_w := TILE_SIZE_PX * 4
var img := Image.create(atlas_w, TILE_SIZE_PX, false, Image.FORMAT_RGBA8)
var palette: Array[Color] = [
Color(0.45, 0.65, 0.30), # grass
Color(0.55, 0.45, 0.30), # dirt
Color(0.60, 0.60, 0.55), # stone
Color(0.30, 0.30, 0.32), # stone dark
]
for i in palette.size():
var base: Color = palette[i]
# Subtle border — was darkened(0.15) which made grass look like graph paper.
# darkened(0.04) keeps the tile boundary readable when squinting but doesn't
# dominate the visual.
var border: Color = base.darkened(0.04)
for px in TILE_SIZE_PX:
for py in TILE_SIZE_PX:
var on_border := (
px == 0 or px == TILE_SIZE_PX - 1
or py == 0 or py == TILE_SIZE_PX - 1
)
img.set_pixel(i * TILE_SIZE_PX + px, py, border if on_border else base)
var tex := ImageTexture.create_from_image(img)
var src := TileSetAtlasSource.new()
src.texture = tex
src.texture_region_size = Vector2i(TILE_SIZE_PX, TILE_SIZE_PX)
for i in palette.size():
src.create_tile(Vector2i(i, 0))
ts.add_source(src, PLACEHOLDER_SOURCE_ID)
return ts
func _paint_terrain() -> void:
for x in MAP_SIZE_TILES.x:
for y in MAP_SIZE_TILES.y:
terrain_layer.set_cell(Vector2i(x, y), PLACEHOLDER_SOURCE_ID, TILE_GRASS)
func _paint_sample_walls() -> void:
var origin := Vector2i(36, 36)
var size: int = 8
for i in size:
wall_layer.set_cell(origin + Vector2i(i, 0), PLACEHOLDER_SOURCE_ID, TILE_STONE)
wall_layer.set_cell(origin + Vector2i(i, size - 1), PLACEHOLDER_SOURCE_ID, TILE_STONE)
wall_layer.set_cell(origin + Vector2i(0, i), PLACEHOLDER_SOURCE_ID, TILE_STONE_DARK)
wall_layer.set_cell(origin + Vector2i(size - 1, i), PLACEHOLDER_SOURCE_ID, TILE_STONE_DARK)
# ── pathfinder + pawns ──────────────────────────────────────────────────────
func _wire_walls_to_pathfinder() -> void:
var wall_cells := wall_layer.get_used_cells()
for cell in wall_cells:
pathfinder.set_cell_walkable(cell, false)
Audit.log("world", "%d wall cells marked impassable" % wall_cells.size())
func _spawn_sample_pawns() -> void:
for spawn_data in SAMPLE_PAWNS:
var p: Pawn = PAWN_SCENE.instantiate()
add_child(p)
p.setup(spawn_data["name"], spawn_data["tile"])
# Phase 3: attach a JobRunner so Decision can hand it jobs.
var jr := JobRunner.new()
jr.name = "JobRunner"
p.add_child(jr)
jr.setup(p, pathfinder)
p.job_runner = jr
# Phase 6: seed Crafting skill so the quality demo shows variety.
if spawn_data.has("crafting"):
p.set_skill(Pawn.SKILL_CRAFTING, int(spawn_data["crafting"]))
# Phase 7: seed Cooking skill so hearth recipes show quality spread too.
if spawn_data.has("cooking"):
p.set_skill(Pawn.SKILL_COOKING, int(spawn_data["cooking"]))
World.register_pawn(p)
# ── Phase 4: harvestables + stockpile zones ─────────────────────────────────
func _spawn_sample_harvestables() -> void:
# Untyped vars — Godot's class-name cache for class_name'd classes is
# scan-time and intermittently lags behind file changes. Duck typing is
# safer here and the calls below are all spec'd on the entity types.
for t_tile in SAMPLE_TREES:
var tree = TREE_SCENE.instantiate()
add_child(tree)
tree.setup(t_tile)
for r_tile in SAMPLE_ROCKS:
var rock = ROCK_SCENE.instantiate()
add_child(rock)
rock.setup(r_tile)
Audit.log("world", "spawned %d trees + %d rocks" % [SAMPLE_TREES.size(), SAMPLE_ROCKS.size()])
func _seed_phase5_demo_buildings() -> void:
# Pre-queue a furnished cabin so the construction loop runs end-to-end
# without needing player-paint UI (deferred to Phase 17).
#
# Layout — 8×6 stone cabin at (44, 23), south door, wood floor inside:
# • Perimeter walls (skipping the door slot)
# • Door at (47, 28) — middle of the south wall
# • Wood floor across the 6×4 interior
# • One pre-built crate inside (north-east corner of the interior)
# • Two stockpile-target crates outside (Phase 4 hauling target)
var origin := Vector2i(44, 23)
var w := 8
var h := 6
var door_x := w / 2 - 1 # 3 → tile (47, y_bottom). Centred door.
var door_tile := origin + Vector2i(door_x, h - 1)
# Perimeter walls — skip the door slot in the bottom row.
var wall_count := 0
for x in w:
EventBus.designation_added.emit(origin + Vector2i(x, 0), Designation.TOOL_BUILD_WALL)
wall_count += 1
var bottom_tile := origin + Vector2i(x, h - 1)
if bottom_tile != door_tile:
EventBus.designation_added.emit(bottom_tile, Designation.TOOL_BUILD_WALL)
wall_count += 1
for y in range(1, h - 1): # left + right cols, skip corners (already painted as top/bottom rows)
EventBus.designation_added.emit(origin + Vector2i(0, y), Designation.TOOL_BUILD_WALL)
EventBus.designation_added.emit(origin + Vector2i(w - 1, y), Designation.TOOL_BUILD_WALL)
wall_count += 2
# Door at south wall centre.
EventBus.designation_added.emit(door_tile, Designation.TOOL_BUILD_DOOR)
# Wood floor across the interior — 6×4 = 24 cells.
var floor_count := 0
for x in range(1, w - 1):
for y in range(1, h - 1):
EventBus.designation_added.emit(origin + Vector2i(x, y), Designation.TOOL_BUILD_FLOOR)
floor_count += 1
# Pre-built crate inside the cabin (top-right corner of interior).
# Auto-built so the cabin shows furnished on first frame.
var interior_crate: Crate = CRATE_SCENE.instantiate()
add_child(interior_crate)
interior_crate.setup(origin + Vector2i(w - 2, 1)) # (50, 24)
while interior_crate.is_buildable():
interior_crate.on_build_tick()
# Two external stockpile-target crates south-west (Phase 4 haul destination).
var crate_tiles: Array[Vector2i] = [Vector2i(17, 60), Vector2i(18, 60)]
for t in crate_tiles:
var c: Crate = CRATE_SCENE.instantiate()
add_child(c)
c.setup(t)
while c.is_buildable():
c.on_build_tick()
Audit.log("world", "phase 5 demo: %d walls + 1 door + %d floors queued; %d crates pre-built" % [
wall_count, floor_count, 1 + crate_tiles.size()
])
# Phase 6 demo — two pre-built workbenches inside the cabin with bills.
# Carpenter at (46, 25) makes wood → plank, FOREVER.
# Smelter at (48, 25) makes stone → stone_block, UNTIL 5 in stockpiles.
var carpenter: Workbench = WORKBENCH_SCENE.instantiate()
add_child(carpenter)
carpenter.setup(Vector2i(46, 25))
carpenter.label_text = "Carpenter"
carpenter.accepted_skill = Pawn.SKILL_CRAFTING
while carpenter.is_buildable():
carpenter.on_build_tick()
var plank_bill := Bill.new()
plank_bill.recipe = RecipeCatalog.plank()
plank_bill.mode = Bill.Mode.FOREVER
carpenter.add_bill(plank_bill)
var smelter: Workbench = WORKBENCH_SCENE.instantiate()
add_child(smelter)
smelter.setup(Vector2i(48, 25))
smelter.label_text = "Smelter"
smelter.accepted_skill = Pawn.SKILL_CRAFTING
while smelter.is_buildable():
smelter.on_build_tick()
var block_bill := Bill.new()
block_bill.recipe = RecipeCatalog.stone_block()
block_bill.mode = Bill.Mode.UNTIL_N
block_bill.target_count = 5
smelter.add_bill(block_bill)
Audit.log("world", "phase 6 demo: 2 workbenches built (Carpenter→planks FOREVER, Smelter→blocks UNTIL_N=5)")
# Phase 7 demo — Millstone + Hearth inside the cabin (south row), so the
# grain→flour→bread chain runs visibly. Wheat crops outside the cabin to
# the east. A couple of pre-baked breads + a meal item near the cabin so
# the eat-loop is testable even before cooking completes.
var millstone: Workbench = WORKBENCH_SCENE.instantiate()
add_child(millstone)
millstone.setup(Vector2i(46, 27))
millstone.label_text = "Millstone"
millstone.accepted_skill = Pawn.SKILL_CRAFTING
while millstone.is_buildable():
millstone.on_build_tick()
var flour_bill := Bill.new()
flour_bill.recipe = RecipeCatalog.flour()
flour_bill.mode = Bill.Mode.FOREVER
millstone.add_bill(flour_bill)
var hearth: Workbench = WORKBENCH_SCENE.instantiate()
add_child(hearth)
hearth.setup(Vector2i(49, 27))
hearth.label_text = "Hearth"
hearth.accepted_skill = Pawn.SKILL_COOKING
while hearth.is_buildable():
hearth.on_build_tick()
var bread_bill := Bill.new()
bread_bill.recipe = RecipeCatalog.bread()
bread_bill.mode = Bill.Mode.FOREVER
hearth.add_bill(bread_bill)
var meal_bill := Bill.new()
meal_bill.recipe = RecipeCatalog.meal_from_vegetables()
meal_bill.mode = Bill.Mode.FOREVER
hearth.add_bill(meal_bill)
# Wheat crops east of the cabin, near the trees.
var crop_tiles: Array[Vector2i] = [
Vector2i(54, 24), Vector2i(54, 25), Vector2i(54, 26),
Vector2i(55, 24), Vector2i(55, 25), Vector2i(55, 26),
]
for ct in crop_tiles:
var c: Crop = CROP_SCENE.instantiate()
add_child(c)
c.setup(ct, Crop.KIND_WHEAT, Crop.Stage.SOWN)
# Pre-baked breads + a vegetable meal so pawns can eat before the
# full cooking chain finishes. Phase 17 may remove these as cooking
# becomes reliable; for Phase 7 demo they keep the loop unblocked.
var snack_tiles: Array[Vector2i] = [Vector2i(45, 21), Vector2i(50, 21)]
for st in snack_tiles:
var bread_item: Item = ITEM_SCENE.instantiate()
add_child(bread_item)
bread_item.setup(Item.TYPE_BREAD, 1, st)
bread_item.quality = Item.Quality.NORMAL
Audit.log("world", "phase 7 demo: Millstone+Hearth built, %d wheat crops sown, %d pre-baked breads placed" % [
crop_tiles.size(), snack_tiles.size()
])
# Phase 8 demo — 3 beds inside the cabin's north row. Beds are pre-built
# (skip construction) so pawns have somewhere to sleep on first tired tick.
# (45, 24), (47, 24), (49, 24) — leaves (50, 24) for the existing crate.
var bed_tiles: Array[Vector2i] = [Vector2i(45, 24), Vector2i(47, 24), Vector2i(49, 24)]
for bt in bed_tiles:
var bed: Bed = BED_SCENE.instantiate()
add_child(bed)
bed.setup(bt)
while bed.is_buildable():
bed.on_build_tick()
Audit.log("world", "phase 8 demo: %d beds pre-built inside cabin" % bed_tiles.size())
# Phase 11 demo — 2 torches inside cabin (north-east + south-west corners
# of interior) so the indoor area stays lit at night. Combined with the
# Hearth's light radius=5, the cabin interior should be mostly bright
# while the outdoors goes deep-blue tinted.
var torch_tiles: Array[Vector2i] = [Vector2i(46, 26), Vector2i(49, 26)]
for tt in torch_tiles:
var torch: Torch = TORCH_SCENE.instantiate()
add_child(torch)
torch.setup(tt)
while torch.is_buildable():
torch.on_build_tick()
Audit.log("world", "phase 11 demo: %d torches pre-built inside cabin" % torch_tiles.size())
func _spawn_sample_stockpiles() -> void:
# Two zones for the Phase 4 acceptance demo:
# - Zone A (north): wood-only filter, NORMAL priority (just a wood drop)
# - Zone B (south): wildcard, HIGH priority (the "watch wood flow upward" target)
# When the sweep runs, wood items in Zone A get re-marked for haul and
# eventually migrate to Zone B.
var zone_a: StockpileZone = STOCKPILE_SCENE.instantiate()
add_child(zone_a)
zone_a.region = Rect2i(15, 55, 4, 4)
zone_a.label = "Wood (Normal)"
zone_a.priority = StorageDestination.Priority.NORMAL
zone_a.accepted_types = [Item.TYPE_WOOD] as Array[StringName]
zone_a.queue_redraw()
var zone_b: StockpileZone = STOCKPILE_SCENE.instantiate()
add_child(zone_b)
zone_b.region = Rect2i(15, 62, 4, 4)
zone_b.label = "Anything (High)"
zone_b.priority = StorageDestination.Priority.HIGH
zone_b.accepted_types = [] as Array[StringName] # wildcard
zone_b.queue_redraw()
Audit.log("world", "spawned 2 stockpiles: %s + %s" % [zone_a.label, zone_b.label])
# ── Phase 5: designation → build-site spawn bridge ──────────────────────────
# Track build sites keyed by tile so we can find + queue_free them on cancel.
var _build_sites_by_tile: Dictionary = {}
func _on_designation_added(cell: Vector2i, tool: StringName) -> void:
if _build_sites_by_tile.has(cell):
return # already a build site here
var entity = null
match tool:
&"build_wall":
entity = WALL_SCENE.instantiate()
add_child(entity)
entity.setup(cell, &"stone")
&"build_floor":
entity = FLOOR_SCENE.instantiate()
add_child(entity)
entity.setup(cell, &"wood")
&"build_door":
entity = DOOR_SCENE.instantiate()
add_child(entity)
entity.setup(cell)
_:
Audit.log("world", "unknown designation tool: %s" % tool)
return
_build_sites_by_tile[cell] = entity
Audit.log("world", "queued %s at %s" % [tool, cell])
func _on_designation_cleared(cell: Vector2i) -> void:
if not _build_sites_by_tile.has(cell):
return
var entity = _build_sites_by_tile[cell]
_build_sites_by_tile.erase(cell)
if is_instance_valid(entity) and not entity.is_completed():
entity.queue_free()
# ── periodic re-flow (the "wood floats up" cascade) ─────────────────────────
func _on_sim_tick_world_sweep(tick_n: int) -> void:
_update_dark_overlay()
if tick_n % HAUL_SWEEP_INTERVAL_TICKS != 0:
return
hauling_provider.sweep_for_better_destinations()
# Phase 11 — interpolate CanvasModulate between DAY_TINT and NIGHT_TINT based
# on Clock.darkness_factor() (0 = full day, 1 = full night).
# Called every sim tick; Color.lerp is a handful of float ops — negligible cost.
func _update_dark_overlay() -> void:
var f := Clock.darkness_factor()
dark_overlay.color = DAY_TINT.lerp(NIGHT_TINT, f)
# ── spike: AStarGrid2D query timing at 80² ──────────────────────────────────
func _run_pathfinder_spike() -> void:
var corners := [
Vector2i(2, 2),
Vector2i(MAP_SIZE_TILES.x - 3, 2),
Vector2i(2, MAP_SIZE_TILES.y - 3),
Vector2i(MAP_SIZE_TILES.x - 3, MAP_SIZE_TILES.y - 3),
]
var pairs: Array = []
for a in corners:
for b in corners:
if a != b:
pairs.append([a, b])
var result: Dictionary = pathfinder.benchmark(pairs, 3)
Audit.log("world", "spike: %d paths min=%d us avg=%.1f us max=%d us" % [
result["total_paths"], result["min_us"], result["avg_us"], result["max_us"]
])
# ── camera bounds ───────────────────────────────────────────────────────────
func _apply_camera_bounds() -> void:
var cam := get_node_or_null("CameraRig")
if cam == null:
Audit.log("world", "no CameraRig child yet — bounds set later when camera lands.")
return
if not cam.has_method("set_world_bounds"):
Audit.log("world", "CameraRig present but missing set_world_bounds() — skipping.")
return
cam.set_world_bounds(world_bounds_px())