rimlike/scenes/entities/door.gd
megaproxy ac21443037 Door sprite — swap procedural draw for FG_Fortress arched door
Closed wooden door with stone arch from FG_Fortress (4..5, 19..20) — a 32×32
sprite bottom-anchored on the door tile. The 2-tile-wide sprite extends 8 px
into each flanking tile so the stone arch merges into adjacent wall sprites,
and the lintel rises one tile above the door tile (Y-sorted, occludes pawns).

Ghost state stays at 40% alpha until build completes, matching the Wall/Bed/
Workbench convention. _draw() is now a no-op; the sprite handles everything.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 15:49:50 +01:00

185 lines
7.2 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)