diff --git a/scenes/entities/door.gd b/scenes/entities/door.gd index 557af78..86b5d41 100644 --- a/scenes/entities/door.gd +++ b/scenes/entities/door.gd @@ -13,6 +13,13 @@ class_name Door extends Node2D ## 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. @@ -21,6 +28,13 @@ 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 @@ -52,10 +66,43 @@ func setup(p_tile: Vector2i) -> void: 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 @@ -107,29 +154,11 @@ static func from_dict(d: Dictionary) -> Dictionary: # ── 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) + # 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 ─────────────────────────────────────────────────────────────────── @@ -148,5 +177,9 @@ func _complete() -> void: # 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)