class_name Door extends Node2D ## Door entity — built by a pawn with a Build job. Unlike walls, doors remain ## walkable once complete. Phase 5: always-open (always walkable). Phase 7+ ## will add open/close animation and toggling. ## ## Door is rendered as a bottom-anchored tall sprite (Y-sorted), same as Wall, ## so it occludes pawns walking behind it — matching the 3/4-perspective ## rendering pivot (see memory.md Decisions: "Wall layer rendering"). ## ## Build model (docs/implementation.md Phase 5): ## A ConstructionProvider creates a Job whose BUILD toil calls on_build_tick() ## once per sim tick via JobRunner. After BUILD_TICKS ticks the door is ## complete: it registers with World for future open/close logic, does NOT ## call set_cell_walkable(false), and transitions from ghost to solid. ## ## Render: a 16×16 olive-wood cabin door sprite from FG_Village (atlas (3, 24)) ## is bottom-anchored on the door tile. The sprite occupies exactly one tile — ## same footprint as the surrounding wall, no overhang into neighbours — so ## a 1-tile gap in a wall reads cleanly as "door here". ## ## The previous pick was the 32×32 FG_Fortress stone-arch gate at (4, 19): a ## proper castle entrance, far too imposing for a cabin's front door. Swapped ## 2026-05-12 after the user flagged it as "a door for a entrance to a castle". ## ## World registration: World.register_build_site / World.unregister_build_site ## are called from _ready / _exit_tree. Methods land in the Opus integration pass. const TILE_SIZE_PX: int = 16 ## Sim ticks to build a door at 1× speed (80 ticks = 4 sim seconds). const BUILD_TICKS: int = 80 ## ElvGames Village atlas. Single-tile olive-wood cabin door with a white ## U-shaped handle at (3, 24) — extracted from the red-roofed cottage template ## on FG_Village. See /tmp/village_just_3_24.png for the standalone preview. const _DOOR_TEX: Texture2D = preload("res://art/tiles/FG_Village.png") const _DOOR_COORD: Vector2i = Vector2i(3, 24) # ── state ────────────────────────────────────────────────────────────────────── @export var tile: Vector2i = Vector2i.ZERO ## 0..BUILD_TICKS. var build_progress: int = 0 var _completed: bool = false ## Phase 5: always-open. Phase 7+ toggles this for open/close animation. var is_open: bool = true # ── lifecycle ────────────────────────────────────────────────────────────────── func _ready() -> void: World.register_build_site(self) func _exit_tree() -> void: World.unregister_build_site(self) # ── public API ───────────────────────────────────────────────────────────────── ## One-shot initialiser. Call after add_child() so _ready() has already fired. func setup(p_tile: Vector2i) -> void: tile = p_tile # Bottom-anchor: same as Wall so it Y-sorts correctly with pawns. position = Vector2( tile.x * TILE_SIZE_PX + TILE_SIZE_PX / 2.0, tile.y * TILE_SIZE_PX + TILE_SIZE_PX ) y_sort_enabled = true _build_sprite() queue_redraw() Audit.log("door", "door ghost placed at %s" % tile) ## Builds the 16×16 FG_Village cabin-door Sprite2D child. Bottom-anchored so the ## door sits flush with the tile's bottom edge, matching Wall's convention so ## Y-sort layers pawns correctly relative to it. Idempotent: re-running drops ## the previous sprite first. func _build_sprite() -> void: var prev := get_node_or_null("Sprite") if prev != null: prev.queue_free() var sprite := Sprite2D.new() sprite.name = "Sprite" sprite.texture = _DOOR_TEX sprite.region_enabled = true sprite.region_rect = Rect2( _DOOR_COORD.x * TILE_SIZE_PX, _DOOR_COORD.y * TILE_SIZE_PX, TILE_SIZE_PX, TILE_SIZE_PX, ) sprite.centered = true # Sprite center at y=-8 so the 16×16 region spans y=-16..0 — flush against # the bottom edge of the door tile (matches Wall._build_stone_sprite). No # horizontal overhang into neighbouring tiles. sprite.offset = Vector2(0.0, -8.0) sprite.z_index = 0 # Ghost state — translucent until built. Solidified in _complete(). sprite.modulate.a = 1.0 if _completed else 0.4 add_child(sprite) ## True while the door still needs construction work. func is_buildable() -> bool: return not _completed ## Human-readable label for job descriptions and Audit logs. func label() -> String: return "door" ## Called by the BUILD toil in JobRunner once per sim tick. func on_build_tick() -> void: if _completed: return build_progress += 1 queue_redraw() if build_progress >= BUILD_TICKS: _complete() ## True once the door has been fully built. func is_completed() -> bool: return _completed # ── save / load ──────────────────────────────────────────────────────────────── func to_dict() -> Dictionary: return { "class_id": &"door", "tile_x": tile.x, "tile_y": tile.y, "build_progress": build_progress, "completed": _completed, "is_open": is_open, } static func from_dict(d: Dictionary) -> Dictionary: return { "tile_x": int(d.get("tile_x", 0)), "tile_y": int(d.get("tile_y", 0)), "build_progress": int(d.get("build_progress", 0)), "completed": bool(d.get("completed", false)), "is_open": bool(d.get("is_open", true)), } # ── render ───────────────────────────────────────────────────────────────────── func _draw() -> void: # Door body is rendered by the Sprite2D child (see _build_sprite()). # No procedural overlay needed — the FG_Village cabin-door sprite already # shows the wood planks and U-handle. Ghost alpha is handled via # sprite.modulate.a in _build_sprite / _complete. pass # ── internal ─────────────────────────────────────────────────────────────────── func _complete() -> void: _completed = true # Doors are walkable — do NOT call set_cell_walkable(false). # Phase 13 — erase any wall-layer stamp at this tile. The demo seed # pre-stamps the door slot as a wall so BFS can detect the cabin at boot; # the real door completing supersedes that. Must happen before register_door # so the BFS in mark_door_tile sees the correct wall-layer state. if World.wall_layer != null: World.wall_layer.erase_cell(tile) # Register so future open/close logic can locate this door by tile. World.register_door(self) # Phase 13 — notify RoomDetector so the door tile is eligible as an # interior boundary tile for room BFS. World.mark_door_tile(tile) # Solidify the ghost sprite from 40% to full opacity. var sprite: Sprite2D = get_node_or_null("Sprite") if sprite != null: sprite.modulate.a = 1.0 queue_redraw() World.clear_designation_at(tile) Audit.log("door", "door completed at %s" % tile)