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. ## ## 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 # ── 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 ) queue_redraw() Audit.log("door", "door ghost placed at %s" % tile) ## 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: # 3/4-perspective door — fits strictly within the tile (16×16) so it # slots cleanly between adjacent walls. Origin (0,0) is at the tile's # bottom-centre. Tile spans local Y: -16 to 0. var alpha: float = 1.0 if _completed else 0.4 # Matches Wall's 3/4-band layout so doors and walls share a top horizon. var lintel_color := Color(0.55, 0.40, 0.22, alpha) # stone-warmed top lintel var frame_color := Color(0.32, 0.22, 0.10, alpha) var panel_color := Color(0.52, 0.36, 0.18, alpha) var hinge_color := Color(0.20, 0.18, 0.16, alpha) var outline := Color(0.16, 0.10, 0.04, 0.7 * alpha) # Top lintel band (matches wall top-face height of 5 px so it lines up). draw_rect(Rect2(Vector2(-8.0, -16.0), Vector2(16.0, 5.0)), lintel_color) # Door frame — fills the front-face band (matches wall front 11 px). draw_rect(Rect2(Vector2(-8.0, -11.0), Vector2(16.0, 11.0)), frame_color) # Door panel (inset 1 px each side, leaves 2 px frame visible left/right). draw_rect(Rect2(Vector2(-6.0, -10.0), Vector2(12.0, 10.0)), panel_color) # Hinge dot. draw_circle(Vector2(-5.0, -5.0), 1.0, hinge_color) # Top/bottom borders + tile outline. draw_line(Vector2(-8.0, -11.0), Vector2(8.0, -11.0), Color(0.20, 0.14, 0.06, alpha), 1.0) draw_rect(Rect2(Vector2(-8.0, -16.0), Vector2(16.0, 16.0)), outline, false, 1.0) # ── 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) queue_redraw() Audit.log("door", "door completed at %s" % tile)