extends Node2D
## Phase 4 world view. 80×80 TileMap, 6 layers, 3 pawns, full AI pipeline:
##   RestProvider → ChopProvider → MineProvider → HaulingProvider → idle
## plus sample trees and rocks. No pre-made stockpiles — items sit where they
## are produced until the player paints storage (a stockpile zone or builds
## a crate). This matches Rimworld parity: storage is a player decision.
##
## 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 BEAUTY_SYSTEM_SCRIPT: Script = preload("res://scenes/world/beauty_system.gd")
const DIRTINESS_SYSTEM_SCRIPT: Script = preload("res://scenes/world/dirtiness_system.gd")
const CLEANING_PROVIDER_SCRIPT: Script = preload("res://scenes/ai/cleaning_provider.gd")
const INDOOR_TINT_SCRIPT: Script = preload("res://scenes/world/indoor_tint_overlay.gd")
# Phase 14 — grave / burial entities (scripts only; no .tscn needed).
const GRAVEYARD_ZONE_SCRIPT: Script = preload("res://scenes/world/graveyard_zone.gd")
const GRAVE_SLOT_SCRIPT: Script = preload("res://scenes/entities/grave_slot.gd")
# Phase 15 — EventCatalog (class_name in scenes/storyteller/) preloaded so the
# class is resolved before world._ready() fires. Avoids parse-order ambiguity.
const EVENT_CATALOG_SCRIPT: Script = preload("res://scenes/storyteller/event_catalog.gd")
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 BIG_ROCK_SCENE: PackedScene = preload("res://scenes/entities/big_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")

# Phase 14 — demo auto-kill toggle.
## Set to true to force-kill the first pawn 50 ticks after boot so MCP runtime
## tests can observe the full death → corpse pipeline at startup.
## LEAVE FALSE for normal play — a colonist dies every session otherwise.
## To enable for a test session: edit this constant, run, revert when done.
const DEMO_PHASE14_AUTOKILL: bool = false

# 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),
]

# 2×2 boulder formations (top-left anchor tiles). Two near the small-rock
# cluster so the player sees the size contrast on first scroll.
const SAMPLE_BIG_ROCKS: Array[Vector2i] = [
	Vector2i(65, 58),  # brown/gray (deterministic per-tile)
	Vector2i(56, 64),
]

# 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)

# Phase 12 — subtle seasonal modulate on the Terrain TileMapLayer only.
# Wall / Floor / Roof are entity-drawn (Y-sorted sprites); don't tint those.
const SEASON_TINTS: Dictionary = {
	&"spring": Color(0.96, 1.02, 0.94),  # slight warm-green
	&"summer": Color(1.0,  1.0,  1.0 ),  # neutral / bright
	&"autumn": Color(1.06, 0.95, 0.82),  # warm orange
	&"winter": Color(0.88, 0.92, 1.02),  # cool blue, slight desaturation
}

@onready var dark_overlay: CanvasModulate = $DarkOverlay
@onready var terrain_layer: TileMapLayer = $Terrain
@onready var decoration_layer: TileMapLayer = $Decoration
@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
@onready var doctor_provider: DoctorProvider = $DoctorProvider
@onready var room_detector = $RoomDetector  # RoomDetector — duck-typed (class_name scan-time window)


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()
	# Decoration overlay — sparse grass tufts + flowers from the ElvGames bundle.
	# Lives on its own TileMapLayer above Terrain so a future season swap is one
	# tileset reassignment.
	decoration_layer.tile_set = _build_decoration_tileset()
	_paint_decorations()
	# 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)
	# Bind the camera so Tab-cycle and center-on-selection can pan to pawns.
	var camera_rig := get_node_or_null("CameraRig")
	if camera_rig != null:
		selection.bind_camera(camera_rig)
	else:
		Audit.log("world", "selection.bind_camera: CameraRig not found")
	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.terrain_layer = terrain_layer
	World.wall_layer = wall_layer
	World.floor_layer = floor_layer
	World.designation_layer = designation_layer
	World.roof_layer = roof_layer

	# Phase 13 — wire RoomDetector; setup with map size so BFS knows map bounds.
	room_detector.setup(MAP_SIZE_TILES)
	World.room_detector = room_detector

	# Phase 13 — instantiate BeautySystem + DirtinessSystem + CleaningProvider as
	# runtime children (no .tscn entry needed; they're stateful Nodes with no
	# editor-tunable exports). Autoload refs let entity code reach them.
	var beauty := Node.new()
	beauty.set_script(BEAUTY_SYSTEM_SCRIPT)
	beauty.name = "BeautySystem"
	add_child(beauty)
	World.beauty_system = beauty

	var dirtiness := Node.new()
	dirtiness.set_script(DIRTINESS_SYSTEM_SCRIPT)
	dirtiness.name = "DirtinessSystem"
	add_child(dirtiness)
	World.dirtiness_system = dirtiness

	var cleaning := Node.new()
	cleaning.set_script(CLEANING_PROVIDER_SCRIPT)
	cleaning.name = "CleaningProvider"
	add_child(cleaning)

	# Phase 13 — instantiate IndoorTintOverlay so roofed rooms get a subtle warm
	# overlay. Node2D, z_index 3 (set in its _ready). Self-listens to room_changed.
	var indoor_tint := Node2D.new()
	indoor_tint.set_script(INDOOR_TINT_SCRIPT)
	indoor_tint.name = "IndoorTintOverlay"
	add_child(indoor_tint)

	# Designation: bind the paint surface + the Selection guard.
	designation_ctl.bind(designation_layer, selection)
	# Expose on the autoload so entities can clear their ghost stamp on
	# completion (Tree.fell, Wall._complete, etc.).
	World.designation_ctl = designation_ctl

	# Register all 10 providers — Decision iterates by .priority desc.
	# doctor=9 > 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(doctor_provider)
	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(cleaning)  # priority 2 — between haul (3) and rest (0)
	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()
	# No pre-made stockpiles: the player must paint their own storage. Items
	# from chop/mine/crafting sit where they're produced until a player-made
	# stockpile or crate exists for hauling. (2026-05-15 — _spawn_sample_stockpiles
	# removed; was leftover Phase 4 acceptance scaffolding south-west of the cabin.)
	_seed_phase5_demo_buildings()
	# Phase 13 — pre-stamp the cabin walls + floors on the TileMap data layers
	# so RoomDetector can see a completed enclosure at boot without waiting for
	# pawns to finish the build queue.  Mirrors the layout in _seed_phase5_demo_buildings.
	_prestamp_cabin_for_room_detector()
	_prestamp_test_shed_for_room_detector()
	room_detector.recompute_around(Vector2i(47, 25))
	room_detector.recompute_around(Vector2i(36, 25))  # tiny shed centroid

	# Phase 13 — register existing prebuilt furniture with BeautySystem so it has
	# a beauty score baseline at boot (the post-_complete hooks already fire for
	# the cabin's beds/torches/workbenches, but pawns may path before the first
	# recompute, so seed the map here).
	for ws in World.workbenches:
		beauty.register_furniture(ws)
	for b in World.beds:
		beauty.register_furniture(b)
	for ls in World.light_sources:
		beauty.register_furniture(ls)
	beauty.recompute_all()
	_run_pathfinder_spike()

	# Phase 15 — register all 25 Storyteller events now that World state and
	# autoloads are fully mounted (trigger predicates read Clock, World, Storyteller).
	EVENT_CATALOG_SCRIPT.register_all(Storyteller)

	# 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)

	# Phase 12 — season tint: apply on boot then update on each season transition.
	_apply_season_tint(Clock.current_season())
	EventBus.season_changed.connect(_apply_season_tint)

	# Phase 14 — demo auto-kill (disabled by default — see DEMO_PHASE14_AUTOKILL).
	if DEMO_PHASE14_AUTOKILL:
		EventBus.sim_tick.connect(_demo_phase14_autokill_hook)


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)


# ── decoration overlay (ElvGames Grasslands sprinkle) ────────────────────────

## Source id for the decoration tileset (separate atlas from PLACEHOLDER_SOURCE_ID).
const DECORATION_SOURCE_ID: int = 10

## Atlas-tile coords inside FG_Grasslands_Spring.png for overlay variants.
## Each is a 16×16 sprite on transparent background, picked by visual eyeball
## (see /tmp/deco_review.png in the 2026-05-12 visual-pass session). Mix of
## small-grass sprouts, flowers, and scattered-tuft sprites — all sit cleanly
## on top of the base grass tile without occluding pawns.
const DECORATION_TILES: Array[Vector2i] = [
	Vector2i(12, 0),  # grass sprout A
	Vector2i(13, 0),  # grass sprout B
	Vector2i(14, 0),  # scattered grass dots — small
	Vector2i(16, 0),  # white flower / red center
	Vector2i(17, 0),  # white flower / yellow center
	Vector2i(18, 0),  # white flower / purple center
	Vector2i(16, 2),  # scattered grass dots — medium
	Vector2i(17, 2),  # scattered grass dots — large
]

## How often a tile gets a decoration. 0.08 = ~8% density — visually rich but
## not so dense that pawns/items get hidden under it. Tunable; tested at
## ~500 decorations on the 80×80 map.
const DECORATION_DENSITY: float = 0.08


func _build_decoration_tileset() -> TileSet:
	var ts := TileSet.new()
	ts.tile_size = Vector2i(TILE_SIZE_PX, TILE_SIZE_PX)

	var tex: Texture2D = load("res://art/tiles/FG_Grasslands_Spring.png")
	if tex == null:
		Audit.log("world", "decoration: FG_Grasslands_Spring.png not loadable; skipping overlay")
		return ts

	var src := TileSetAtlasSource.new()
	src.texture = tex
	src.texture_region_size = Vector2i(TILE_SIZE_PX, TILE_SIZE_PX)
	for coord in DECORATION_TILES:
		src.create_tile(coord)
	ts.add_source(src, DECORATION_SOURCE_ID)
	return ts


func _paint_decorations() -> void:
	# Deterministic-ish seed so the layout is stable across runs (cosmetic only;
	# not save-round-tripped). Mix world size into seed for "feels random but
	# reproducible." Use a fresh RNG so we don't perturb the global state.
	var rng := RandomNumberGenerator.new()
	rng.seed = MAP_SIZE_TILES.x * 31 + MAP_SIZE_TILES.y * 17
	var placed: int = 0
	for x in MAP_SIZE_TILES.x:
		for y in MAP_SIZE_TILES.y:
			if rng.randf() >= DECORATION_DENSITY:
				continue
			var coord: Vector2i = DECORATION_TILES[rng.randi() % DECORATION_TILES.size()]
			decoration_layer.set_cell(Vector2i(x, y), DECORATION_SOURCE_ID, coord)
			placed += 1
	Audit.log("world", "decoration: painted %d overlay cells" % placed)


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-zone seeding removed 2026-05-15) ───────

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.
	# Boot seed auto-designates so the production-chain demo runs end-to-end
	# without requiring a player to paint chop/mine first. Real player-painted
	# trees / rocks still gate on chop_designated / mine_designated (Rimworld parity).
	for t_tile in SAMPLE_TREES:
		var tree = TREE_SCENE.instantiate()
		add_child(tree)
		tree.setup(t_tile)
		tree.chop_designated = true
	for r_tile in SAMPLE_ROCKS:
		var rock = ROCK_SCENE.instantiate()
		add_child(rock)
		rock.setup(r_tile)
		rock.mine_designated = true
	for br_origin in SAMPLE_BIG_ROCKS:
		var big = BIG_ROCK_SCENE.instantiate()
		add_child(big)
		big.setup(br_origin)
		big.mine_designated = true
	Audit.log("world", "spawned %d trees + %d rocks + %d big rocks" % [
		SAMPLE_TREES.size(), SAMPLE_ROCKS.size(), SAMPLE_BIG_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×7 stone cabin at (44, 22), 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×5 interior (rows 23..27)
	#   • One pre-built crate inside (north-east corner of the interior — the
	#     cabin's starting amenity; player paints additional storage later)
	# Bumped from 8×6 → 8×7 so the north interior row (23) is free for the
	# bed sprites to extend into (beds are 1×2, anchored at the foot, head
	# extends one tile up). Previously the headboards clipped into the wall.
	var origin := Vector2i(44, 22)
	var w := 8
	var h := 7
	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, 23) — top-right of interior
	while interior_crate.is_buildable():
		interior_crate.on_build_tick()

	# (2026-05-15) Two external SW crates removed alongside _spawn_sample_stockpiles:
	# they were Phase 4 haul-destination scaffolding, no longer needed now that
	# the player paints their own storage. The interior cabin crate above stays
	# as a starting amenity.

	Audit.log("world", "phase 5 demo: %d walls + 1 door + %d floors queued; 1 interior crate pre-built" % [
		wall_count, floor_count
	])

	# 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)

	# Mixed crops east of the cabin, near the trees. One column per kind so the
	# player sees the four atlas variants side-by-side from the boot demo.
	var crop_plan: Array = [
		[Vector2i(54, 24), Crop.KIND_WHEAT],
		[Vector2i(54, 25), Crop.KIND_WHEAT],
		[Vector2i(54, 26), Crop.KIND_WHEAT],
		[Vector2i(55, 24), Crop.KIND_POTATO],
		[Vector2i(55, 25), Crop.KIND_POTATO],
		[Vector2i(55, 26), Crop.KIND_POTATO],
		[Vector2i(56, 24), Crop.KIND_CORN],
		[Vector2i(56, 25), Crop.KIND_CORN],
		[Vector2i(56, 26), Crop.KIND_CORN],
		[Vector2i(57, 24), Crop.KIND_STRAWBERRY],
		[Vector2i(57, 25), Crop.KIND_STRAWBERRY],
		[Vector2i(57, 26), Crop.KIND_STRAWBERRY],
	]
	var crop_tiles: Array[Vector2i] = []
	for entry in crop_plan:
		var ct: Vector2i = entry[0]
		var kind: StringName = entry[1]
		crop_tiles.append(ct)
		var c: Crop = CROP_SCENE.instantiate()
		add_child(c)
		c.setup(ct, kind, 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 crops sown (mixed kinds), %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.
	# 3 beds in cabin north row. Phase 9 — middle bed is the medical bed so
	# the doctor flow has a treatment target.
	var bed_tiles: Array[Vector2i] = [Vector2i(45, 24), Vector2i(47, 24), Vector2i(49, 24)]
	for i in bed_tiles.size():
		var bt: Vector2i = bed_tiles[i]
		var bed: Bed = BED_SCENE.instantiate()
		add_child(bed)
		bed.setup(bt)
		if i == 1:  # middle bed = medical
			bed.is_medical = true
		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())


# ── 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:
	# Replacement rule: painting a door on a tile occupied by a Wall (ghost or
	# completed) demolishes the wall in place so the door build can proceed.
	# Mirrors the Going-Medieval / Rimworld "door replaces wall" convention.
	# Source of truth is World.build_queue — pre-built walls (test shed, cabin
	# seed) aren't in _build_sites_by_tile but ARE in build_queue via _ready.
	if tool == &"build_door":
		var wall_at_cell = _find_wall_at(cell)
		if wall_at_cell != null:
			_demolish_wall_in_place(wall_at_cell, cell)
			_build_sites_by_tile.erase(cell)  # may not be present; erase is no-op
			# Fall through to spawn the door ghost below.
	if _build_sites_by_tile.has(cell):
		return  # already a build site here
	# Phase 17 — read material override from the Designation controller (may be "").
	var mat: StringName = designation_ctl.tool_material if designation_ctl != null else &""
	var entity = null
	match tool:
		&"build_wall":
			var wall_mat: StringName = mat if mat != &"" else &"stone"
			entity = WALL_SCENE.instantiate()
			add_child(entity)
			entity.setup(cell, wall_mat)
		&"build_floor":
			var floor_mat: StringName = mat if mat != &"" else &"wood"
			entity = FLOOR_SCENE.instantiate()
			add_child(entity)
			entity.setup(cell, floor_mat)
		&"build_door":
			entity = DOOR_SCENE.instantiate()
			add_child(entity)
			entity.setup(cell)
		# Phase 14 — graveyard zone: paint a single-cell GraveyardZone.
		&"graveyard":
			var gz := Node2D.new()
			gz.set_script(GRAVEYARD_ZONE_SCRIPT)
			gz.name = "GraveyardZone_%d_%d" % [cell.x, cell.y]
			add_child(gz)
			gz.region = Rect2i(cell.x, cell.y, 1, 1)
			gz.label = "Graveyard"
			gz.queue_redraw()
			entity = gz
		# Phase 14 — dig grave: spawn a GraveSlot ghost and queue it for build.
		&"dig_grave":
			var gs := Node2D.new()
			gs.set_script(GRAVE_SLOT_SCRIPT)
			gs.name = "GraveSlot_%d_%d" % [cell.x, cell.y]
			add_child(gs)
			gs.setup(cell)
			entity = gs
		# Chop / mine: flag the Tree / Rock entity at this tile so ChopProvider /
		# MineProvider treat it as work. Designation ghost on the TileMap is the
		# visual cue; _build_sites_by_tile holds a sentinel so cancel can clear.
		&"chop":
			for t in World.trees:
				if t.tile == cell:
					t.chop_designated = true
					break
			_build_sites_by_tile[cell] = null
			Audit.log("world", "chop designation at %s" % cell)
			return
		&"mine":
			for r in World.rocks:
				# BigRock occupies a 2×2 footprint; flag if cell is any of them.
				if r.has_method("footprint_tiles"):
					if cell in r.footprint_tiles():
						r.mine_designated = true
						break
				elif r.tile == cell:
					r.mine_designated = true
					break
			_build_sites_by_tile[cell] = null
			Audit.log("world", "mine designation at %s" % cell)
			return
		# Phase 17 — crate.
		&"build_crate":
			entity = CRATE_SCENE.instantiate()
			add_child(entity)
			entity.setup(cell)
		# Phase 17 — bed.
		&"build_bed":
			entity = BED_SCENE.instantiate()
			add_child(entity)
			entity.setup(cell)
		# Phase 17 — torch.
		&"build_torch":
			entity = TORCH_SCENE.instantiate()
			add_child(entity)
			entity.setup(cell)
		# Phase 17 — workbench variants.
		&"build_workbench_carpenter":
			entity = _spawn_workbench(cell, "Carpenter", Pawn.SKILL_CRAFTING)
		&"build_workbench_smelter":
			entity = _spawn_workbench(cell, "Smelter", Pawn.SKILL_CRAFTING)
		&"build_workbench_millstone":
			entity = _spawn_workbench(cell, "Millstone", Pawn.SKILL_CRAFTING)
		&"build_workbench_hearth":
			entity = _spawn_workbench(cell, "Hearth", Pawn.SKILL_COOKING)
		&"build_workbench_cremation_pyre":
			entity = _spawn_workbench(cell, "Cremation Pyre", Pawn.SKILL_CRAFTING)
		# Phase 17 — general stockpile paint: 1-cell stockpile zone, accepts all.
		&"paint_stockpile":
			var sz: StockpileZone = STOCKPILE_SCENE.instantiate()
			add_child(sz)
			sz.region = Rect2i(cell.x, cell.y, 1, 1)
			sz.label = "Stockpile"
			sz.priority = StorageDestination.Priority.NORMAL
			sz.accepted_types = [] as Array[StringName]  # wildcard
			sz.queue_redraw()
			entity = sz
		_:
			Audit.log("world", "unknown designation tool: %s" % tool)
			return
	_build_sites_by_tile[cell] = entity
	Audit.log("world", "queued %s at %s" % [tool, cell])


## True if `entity` is a Wall (ghost or completed). Duck-typed by script path
## to avoid registration-order issues with class_name Wall.
func _entity_is_wall(entity) -> bool:
	if entity == null:
		return false
	var s = entity.get_script()
	return s != null and s.resource_path.ends_with("/wall.gd")


## Finds the Wall entity at `cell` (ghost or completed), or null. Searches
## World.build_queue first since every Wall self-registers there on _ready;
## that covers both designation-painted walls and pre-built _spawn_complete_wall
## seeds.
func _find_wall_at(cell: Vector2i):
	for site in World.build_queue:
		if not is_instance_valid(site):
			continue
		if site.get("tile") != cell:
			continue
		if _entity_is_wall(site):
			return site
	return null


## Atomically remove a Wall entity at `cell`. Reverses Wall._complete() effects
## if the wall was already built: unstamps the wall_layer TileMap, marks the
## pathfinder cell walkable, and triggers a room recompute. Then frees the
## entity. Used by the door-replaces-wall replacement rule.
func _demolish_wall_in_place(wall, cell: Vector2i) -> void:
	var was_completed: bool = wall.has_method("is_completed") and wall.is_completed()
	if was_completed:
		wall_layer.erase_cell(cell)
		pathfinder.set_cell_walkable(cell, true)
		if room_detector != null:
			room_detector.recompute_around(cell)
	wall.queue_free()
	Audit.log("world", "wall at %s demolished (replaced by door designation)" % cell)


## Instantiate a Workbench ghost (in build-queue state) at `tile` with the
## given label and accepted skill. Returns the entity (already add_child'd).
func _spawn_workbench(tile: Vector2i, label: String, skill: StringName):
	var wb: Workbench = WORKBENCH_SCENE.instantiate()
	add_child(wb)
	wb.setup(tile)
	wb.label_text = label
	wb.accepted_skill = skill
	return wb


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)
	# Phase 17 — chop/mine designations store null as their sentinel; nothing to
	# free, but clear the corresponding flag on any Tree / Rock at that tile.
	if entity == null:
		for t in World.trees:
			if t.tile == cell:
				t.chop_designated = false
				break
		for r in World.rocks:
			if r.has_method("footprint_tiles"):
				if cell in r.footprint_tiles():
					r.mine_designated = false
					break
			elif r.tile == cell:
				r.mine_designated = false
				break
		return
	if not is_instance_valid(entity):
		return
	# For build-queue entities (Wall, Floor, Door, GraveSlot): only free if not
	# yet completed (a completed wall should not be torn down by a cancel signal).
	# For zone-overlay entities (GraveyardZone) that have no is_completed(): always free.
	var can_complete: bool = entity.has_method("is_completed")
	if not can_complete or 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)


# Phase 12 — apply a subtle hue tint to the Terrain layer to signal season.
# Only Terrain is tinted; Wall/Floor/Roof are entity sprites (Y-sorted) — leave them neutral.
# Called once at boot (from _ready) and on every EventBus.season_changed signal.
func _apply_season_tint(season: StringName) -> void:
	var tint: Color = SEASON_TINTS.get(season, Color.WHITE)
	terrain_layer.modulate = tint
	Audit.log("world", "season tint → %s (%s)" % [season, tint])


# ── Phase 13: demo seed room helper ─────────────────────────────────────────

# Directly stamps the cabin perimeter walls and interior floors on the data
# TileMap layers so RoomDetector can detect the enclosure at boot.
# The ghost entities are already queued — this only writes the data layer
# (same as Wall._complete / Floor._complete call).  Without this,
# RoomDetector would have to wait until pawns finish building the walls
# (many seconds of real time) before the first room is visible.
#
# Layout mirrors _seed_phase5_demo_buildings: 8×6 cabin at (44, 23),
# door at (47, 28), 6×4 wood floor interior.
func _prestamp_cabin_for_room_detector() -> void:
	# Matches _seed_phase5_demo_buildings: 8×7 cabin at (44, 22), 6×5 interior.
	var origin := Vector2i(44, 22)
	var w := 8
	var h := 7
	var door_x := w / 2 - 1  # 3 → tile x=47
	var door_tile := origin + Vector2i(door_x, h - 1)

	# Stamp perimeter walls (skipping the door slot).
	for x in w:
		World.mark_wall_tile(origin + Vector2i(x, 0), &"stone")
		var bottom := origin + Vector2i(x, h - 1)
		if bottom != door_tile:
			World.mark_wall_tile(bottom, &"stone")
	for y in range(1, h - 1):
		World.mark_wall_tile(origin + Vector2i(0, y), &"stone")
		World.mark_wall_tile(origin + Vector2i(w - 1, y), &"stone")

	# Stamp interior floors.
	for x in range(1, w - 1):
		for y in range(1, h - 1):
			World.mark_floor_tile(origin + Vector2i(x, y), &"wood")

	# Stamp the door slot as a wall so the perimeter is fully closed and BFS
	# terminates cleanly.  Door._complete() erases this wall stamp and registers
	# the door entity; the follow-up recompute_around picks it up as a boundary.
	World.mark_wall_tile(door_tile, &"stone")

	Audit.log("world", "phase 13 demo: cabin walls+floors pre-stamped for RoomDetector")


# Tiny 5×5 walled shed with 3×3 interior (= 9 floor tiles) — under the 16-cap
# auto-roof threshold, so this room WILL roof. Used to exercise the indoor /
# shelter / room-thoughts pipeline without resizing the main cabin.
func _prestamp_test_shed_for_room_detector() -> void:
	var origin := Vector2i(34, 23)  # left of cabin, on the grass plain
	var w := 5
	var h := 5
	# Perimeter walls — instantiate complete Wall entities so they're visible.
	for x in w:
		_spawn_complete_wall(origin + Vector2i(x, 0))
		_spawn_complete_wall(origin + Vector2i(x, h - 1))
	for y in range(1, h - 1):
		_spawn_complete_wall(origin + Vector2i(0, y))
		_spawn_complete_wall(origin + Vector2i(w - 1, y))
	# Interior floors — instantiate complete Floor entities.
	for x in range(1, w - 1):
		for y in range(1, h - 1):
			_spawn_complete_floor(origin + Vector2i(x, y))

	Audit.log("world", "phase 13 demo: 5×5 test shed pre-built (interior 9 tiles, auto-roofed)")


# Instantiate a Wall entity in completed state at `tile`. Bypasses the build
# queue. Used by the Phase 13 test shed seed.
func _spawn_complete_wall(tile: Vector2i) -> void:
	var w = WALL_SCENE.instantiate()
	w.setup(tile, &"stone")
	add_child(w)
	w.build_progress = w.BUILD_TICKS
	w.on_build_tick()  # triggers _complete() and the data-layer stamp + room recompute


# Instantiate a Floor entity in completed state at `tile`.
func _spawn_complete_floor(tile: Vector2i) -> void:
	var f = FLOOR_SCENE.instantiate()
	f.setup(tile, &"wood")
	add_child(f)
	f.build_progress = f.BUILD_TICKS
	f.on_build_tick()


# ── 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"]
	])


# ── Phase 14: demo auto-kill hook ───────────────────────────────────────────

## Connected to EventBus.sim_tick when DEMO_PHASE14_AUTOKILL is true.
## Force-kills the first pawn 50 ticks after boot so the death→corpse pipeline
## can be tested in a normal play session. LEAVE DEMO_PHASE14_AUTOKILL = false
## for normal play.
func _demo_phase14_autokill_hook(tick_n: int) -> void:
	if tick_n != 50:
		return
	if World.pawns.is_empty():
		return
	var victim = World.pawns[0]
	if victim.has_method("take_damage"):
		Audit.log("world", "phase 14 demo: force-killing '%s' at tick %d" % [victim.pawn_name, tick_n])
		victim.take_damage(victim.hp, &"demo_kill")
	EventBus.sim_tick.disconnect(_demo_phase14_autokill_hook)


# ── 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())
