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 32×32 closed-wood-door sprite from FG_Fortress (atlas (4..5, 19..20)) ## is bottom-anchored on the door tile. The sprite is 2 tiles wide so the stone ## arch extends 8 px into each flanking tile — which is correct, since walls ## typically sit on those tiles and the arch is supposed to merge into them. ## The sprite is 2 tiles tall so the lintel rises one tile above the door tile ## (matching Wall's bottom-anchored sprite convention). ## ## 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 Fortress atlas. Stone-arched closed wooden door — top-left of the ## 32×32 region at (4, 19); covers tiles (4..5, 19..20). const _DOOR_TEX: Texture2D = preload("res://art/tiles/FG_Fortress.png") const _DOOR_COORD: Vector2i = Vector2i(4, 19) const _DOOR_W_TILES: int = 2 const _DOOR_H_TILES: int = 2 # ── 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 32×32 FG_Fortress closed-door Sprite2D child. Bottom-anchored ## so it sits flush with the door tile's bottom edge — the lintel rises into ## the tile above and the stone arch extends one half-tile into each adjacent ## tile. 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 var pixels_w: int = TILE_SIZE_PX * _DOOR_W_TILES var pixels_h: int = TILE_SIZE_PX * _DOOR_H_TILES sprite.region_rect = Rect2( _DOOR_COORD.x * TILE_SIZE_PX, _DOOR_COORD.y * TILE_SIZE_PX, pixels_w, pixels_h, ) sprite.centered = true # Centred at (0, -16): bottom edge of sprite at y=0 (= tile bottom), top at # y=-32. Horizontally centred so the 32-wide sprite extends ±16 px around # the door tile's centre, overlapping the two neighbouring wall tiles. sprite.offset = Vector2(0.0, -float(pixels_h) / 2.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_Fortress closed-door sprite already # shows the wood panels, stone arch, and lintel. 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() Audit.log("door", "door completed at %s" % tile)