Make the demo cabin readable as a real building so the rendering pattern is solid before Phase 6+ adds more building types. Demo seed (world.gd._seed_phase5_demo_buildings): - 8×6 stone cabin at (44, 23) — 23 walls (perimeter minus door slot) + 1 door (south wall centre at (47, 28)) + 24 wood-floor designations for the interior. ConstructionProvider picks them all up; pawns build the whole thing. - One pre-built crate inside at (50, 24) so the interior reads as a furnished room on first frame. - Two external stockpile-target crates unchanged at (17, 60) / (18, 60). Door visual rewrite (door.gd): - Was the old 16×24 bottom-anchored shape that encroached on the cell above. Now fits strictly within its 16×16 tile, matching the wall's 3/4 band layout (5 px lit lintel + 11 px shaded frame + inset panel + hinge dot). Door and walls now share a top horizon line so they line up visually. Designation gained TOOL_BUILD_DOOR + atlas mapping; world.gd's _on_designation_added now branches on build_door to spawn a Door entity. THE DOUBLE-RENDER BUG (caught by MCP inspection): - World.mark_floor_tile stamps the Floor TileMap with atlas (2, 0) which is *stone-grey* in the placeholder atlas, regardless of material name. - The Floor TileMap layer was visible=true with z_index=1, so it drew ON TOP of the brown Floor entity sprites underneath. - Result before fix: interior tiles looked gray-stone, not wood. - Fix: set Floor TileMap layer visible=false (data-only, same as Wall). Entities own the visual; the TileMap retains tile-level data for Phase 13's room detection + Phase 16's save format. Pattern locked for future building types: 'render at entity level, TileMap layers are data-only'. Phase 13's roof and any future wall materials follow the same template. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
142 lines
5.6 KiB
GDScript
142 lines
5.6 KiB
GDScript
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 {
|
||
"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).
|
||
# Register so future open/close logic can locate this door by tile.
|
||
World.register_door(self)
|
||
queue_redraw()
|
||
Audit.log("door", "door completed at %s" % tile)
|