extends Node ## Runtime entity registry + tile-related sim state. ## ## All gameplay entities (pawns, items, furniture, animals, corpses) live here. ## TileMap data is owned by the world-view scene; World holds the *indirect* ## state (designation queue, dirty-haul set, zone records, etc.) that doesn't ## belong on the TileMap itself. ## ## See docs/architecture.md. # Phase 2 — pawn registry. items/furniture/animals/corpses arrive in later phases. var pawns: Array[Pawn] = [] # Phase 3 — work providers (e.g. RestProvider, ChopProvider, HaulingProvider). # World scene registers them on _ready. Decision.pick_next_job() iterates by .priority desc. var work_providers: Array = [] # Phase 4 — harvestables + items + stockpiles. Entities call register_*/unregister_* # from their _ready/_exit_tree. Phase 16 will add stable IDs and persistence wiring. var trees: Array = [] # Array of Tree var rocks: Array = [] # Array of Rock var items: Array = [] # Array of Item (on-floor stacks) var stockpiles: Array = [] # Array of StorageDestination (StockpileZone for now; containers Phase 5) # Phase 4 — pathfinder reference exposed for entity code that needs walkability # checks (e.g. Tree.fell() picking neighbour tiles for wood drops). The actual # Pathfinder node lives on the World scene as a child; the scene sets this in # its _ready(). Don't access before the world scene is mounted. var pathfinder = null # Phase 5 — build queue. Holds Wall/Floor/Door/Crate ghost entities (not yet # completed). ConstructionProvider iterates this for the nearest buildable site. # Entities call register_build_site() in _ready and unregister_build_site() when # they finish or are cancelled. var build_queue: Array = [] # Phase 5 — completed Door entities, keyed for future open/close logic. # Door._complete() calls register_door(); Phase 7+ uses this for toggling. var doors: Array = [] # Phase 6 — workbench entities. Workbench._ready() calls register_workbench(); # _exit_tree() calls unregister_workbench(). CraftingProvider iterates this # to find bench+bill pairs for eligible pawns. var workbenches: Array = [] # Phase 7 — crop entities. Crop._ready() calls register_crop(); # _exit_tree() calls unregister_crop(). PlantProvider iterates this to find # harvestable (READY) and sowable (TILLED) crops for eligible pawns. var crops: Array = [] # Phase 8 — bed entities. Bed._ready() calls register_bed(); # _exit_tree() calls unregister_bed(). SleepProvider iterates this to find # available (completed, unoccupied) beds for tired pawns. # Storyteller also reads beds.size() for the "First Beds" state predicate. var beds: Array = [] # Phase 11 — light-source entities (Torch + Hearth workbench). Entities call # register_light_source() in _ready and unregister_light_source() in _exit_tree. # is_tile_lit() is queried by the "in darkness" thought and any future # darkness-rendering shader bridge. All entries expose the duck-type interface: # is_on() → bool | get_light_tile() → Vector2i | get_light_radius() → int var light_sources: Array = [] # Phase 10 — wolf entities. Wolf._ready() calls register_wolf(); # Wolf._exit_tree() calls unregister_wolf(). WolfSpawner reads/writes # nothing from this array directly — it only add_child()s new wolves. # CombatSystem (Phase 10) will iterate this for threat detection. # Untyped array — avoids class_name ordering window (Phase 2 gotcha). var wolves: Array = [] # Phase 4 — hauling dirty set. Keys are Items, value is unused (we just use .keys()). # An Item is added when it spawns (Tree.fell, Rock.mined, workbench drop, ...) # and removed when it lands at its highest-priority valid destination. # HaulingProvider.sweep_for_better_destinations() re-marks items when a higher # priority stockpile opens up (the priority cascade per design.md). var items_needing_haul: Dictionary = {} func register_work_provider(wp) -> void: assert(wp != null, "World.register_work_provider: provider is null") if not work_providers.has(wp): work_providers.append(wp) func clear_work_providers() -> void: work_providers.clear() func register_pawn(p: Pawn) -> void: assert(p != null, "World.register_pawn: pawn is null") if pawns.has(p): return pawns.append(p) func unregister_pawn(p: Pawn) -> void: pawns.erase(p) func pawn_at_tile(tile: Vector2i) -> Pawn: for p in pawns: if p.tile == tile: return p return null func clear_pawns() -> void: # For save-load / new-game flow in Phase 16. pawns.clear() # ── Phase 4: harvestables + items + stockpiles ────────────────────────────── func register_tree(t) -> void: if not trees.has(t): trees.append(t) func unregister_tree(t) -> void: trees.erase(t) func register_rock(r) -> void: if not rocks.has(r): rocks.append(r) func unregister_rock(r) -> void: rocks.erase(r) func register_item(it) -> void: if items.has(it): return items.append(it) # Newly-spawned items always start as "needs haul" — HaulingProvider will # clear the flag once the item lands in its highest-priority destination. items_needing_haul[it] = true func unregister_item(it) -> void: items.erase(it) items_needing_haul.erase(it) func register_stockpile(s) -> void: if not stockpiles.has(s): stockpiles.append(s) func unregister_stockpile(s) -> void: stockpiles.erase(s) func mark_item_needs_haul(it) -> void: items_needing_haul[it] = true func clear_item_haul_flag(it) -> void: items_needing_haul.erase(it) # ── Phase 5: build queue + tile-data stamping for walls / floors ──────────── func register_build_site(entity) -> void: if not build_queue.has(entity): build_queue.append(entity) func unregister_build_site(entity) -> void: build_queue.erase(entity) func register_door(d) -> void: if not doors.has(d): doors.append(d) func unregister_door(d) -> void: doors.erase(d) func register_workbench(wb) -> void: if not workbenches.has(wb): workbenches.append(wb) func unregister_workbench(wb) -> void: workbenches.erase(wb) func register_crop(c) -> void: if not crops.has(c): crops.append(c) func unregister_crop(c) -> void: crops.erase(c) func register_bed(b) -> void: if not beds.has(b): beds.append(b) func unregister_bed(b) -> void: beds.erase(b) # ── Phase 11: light-source registry ──────────────────────────────────────── func register_light_source(ls) -> void: if not light_sources.has(ls): light_sources.append(ls) func unregister_light_source(ls) -> void: light_sources.erase(ls) # ── Phase 10: wolf registry ──────────────────────────────────────────────── func register_wolf(w) -> void: if not wolves.has(w): wolves.append(w) func unregister_wolf(w) -> void: wolves.erase(w) ## Returns true if `tile` is within get_light_radius() of any is_on() light ## source. Uses Manhattan distance (no wall-occlusion in Phase 11; Phase 13 ## may add BFS-based occlusion through the room/roof system). ## ## Called by the "in darkness" Thought trigger on each pawn sim tick. ## O(light_sources) per call; trivial at our scale (< 50 sources in MVP). func is_tile_lit(p_tile: Vector2i) -> bool: for ls in light_sources: if not ls.is_on(): continue var d: int = abs(ls.get_light_tile().x - p_tile.x) + abs(ls.get_light_tile().y - p_tile.y) if d <= ls.get_light_radius(): return true return false # Called by Wall.on_build_tick() when construction completes. # Stamps the data-only Wall TileMap layer so room/roof/save logic sees the # wall. World scene exposes wall_layer via a getter set during _ready. var wall_layer = null var floor_layer = null var designation_layer = null func mark_wall_tile(tile: Vector2i, material: StringName) -> void: if wall_layer == null: Audit.log("world", "mark_wall_tile: layer not yet wired — skipping") return # Atlas coord encodes material — for Phase 5 placeholder atlas: # stone → (2, 0), dark stone → (3, 0) # Real material→atlas mapping lands when assets are imported. var atlas := Vector2i(2, 0) if material == &"stone" else Vector2i(3, 0) wall_layer.set_cell(tile, 0, atlas) func mark_floor_tile(tile: Vector2i, material: StringName) -> void: if floor_layer == null: return var atlas := Vector2i(1, 0) if material == &"dirt" else Vector2i(2, 0) floor_layer.set_cell(tile, 0, atlas) # Returns the first StockpileZone OR Crate covering `tile`, or null. # Used by JobRunner._tick_deposit (Phase 5 refactor) to route deposits into # Crate contents when applicable. func stockpile_at_tile(tile: Vector2i): for sp in stockpiles: if sp.covers_tile(tile): return sp return null