class_name Wall extends Node2D ## Wall entity — built by a pawn with a Build job. Blocks pathfinding once ## complete. Rendered as a bottom-anchored tall sprite (Y-sorted) so it ## occludes pawns standing behind it, matching the project's 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 wall is ## complete: it stamps the data-layer TileMap (World.mark_wall_tile), blocks ## pathfinding, and transitions from ghost (40% alpha) to solid rendering. ## ## Material support: ## Phase 5 ships stone only. Wood constant is defined for Phase 6+ wiring ## (Pixel Crawler Walls.png asset crop session) without breaking the data model. ## ## World registration: World.register_build_site / World.unregister_build_site ## are called from _ready / _exit_tree. The actual World methods land in the ## Opus integration pass. const TILE_SIZE_PX: int = 16 ## Sim ticks to complete construction at 1× speed (100 ticks = 5 sim seconds). const BUILD_TICKS: int = 100 ## Supported materials. Phase 5 uses MATERIAL_STONE; MATERIAL_WOOD is reserved ## for the Phase 6+ art-authoring pass. const MATERIAL_STONE: StringName = &"stone" const MATERIAL_WOOD: StringName = &"wood" # ── state ────────────────────────────────────────────────────────────────────── @export var wall_material: StringName = MATERIAL_STONE @export var tile: Vector2i = Vector2i.ZERO ## 0..BUILD_TICKS. Advanced by on_build_tick(). Entity is in "ghost" state ## until build_progress reaches BUILD_TICKS. var build_progress: int = 0 var _completed: bool = false # ── 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, p_material: StringName) -> void: tile = p_tile wall_material = p_material # Bottom-anchor the sprite: position.y sits at the bottom of the tile so the # 16×32 virtual sprite "rises" into the cell above. Y-sort uses position.y. position = Vector2( tile.x * TILE_SIZE_PX + TILE_SIZE_PX / 2.0, tile.y * TILE_SIZE_PX + TILE_SIZE_PX ) queue_redraw() Audit.log("wall", "%s wall ghost placed at %s" % [wall_material, tile]) ## True while the wall still needs construction work. ## JobRunner's _tick_build checks this to decide when the toil is done. func is_buildable() -> bool: return not _completed ## Human-readable label for job descriptions and Audit logs. func label() -> String: return "%s wall" % wall_material ## Called by the BUILD toil in JobRunner once per sim tick while the pawn works. ## Advances build_progress and completes the wall when BUILD_TICKS is reached. func on_build_tick() -> void: if _completed: return build_progress += 1 queue_redraw() if build_progress >= BUILD_TICKS: _complete() ## True once the wall 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, "material": str(wall_material), "build_progress": build_progress, "completed": _completed, } static func from_dict(d: Dictionary) -> Dictionary: return { "tile_x": int(d.get("tile_x", 0)), "tile_y": int(d.get("tile_y", 0)), "material": str(d.get("material", "stone")), "build_progress": int(d.get("build_progress", 0)), "completed": bool(d.get("completed", false)), } # ── render ───────────────────────────────────────────────────────────────────── func _draw() -> void: # 3/4-perspective wall rendering — fits WITHIN the wall's own tile so it # never encroaches on adjacent floor/interior tiles. Two-band look: # Top band (lit) = the wall's "top surface" (looking down at it) # Bottom band (dark) = the wall's "front face" (looking at the side) # # Origin (0, 0) is at the tile's bottom-centre. Tile spans local Y: -16 to 0. # We draw entirely within that 16×16 box. var alpha: float = 1.0 if _completed else 0.4 if wall_material == MATERIAL_STONE: _draw_stone_wall(alpha) else: _draw_wood_wall(alpha) func _draw_stone_wall(alpha: float) -> void: var top_face := Color(0.65, 0.65, 0.60, alpha) # lit top surface var front_face := Color(0.42, 0.42, 0.38, alpha) # shaded front var mortar := Color(0.28, 0.28, 0.25, alpha) var outline := Color(0.18, 0.18, 0.16, 0.7 * alpha) # Top face — thin lit strip at upper-third of the tile. draw_rect(Rect2(Vector2(-8.0, -16.0), Vector2(16.0, 5.0)), top_face) # Front face — main wall body, lower two-thirds. draw_rect(Rect2(Vector2(-8.0, -11.0), Vector2(16.0, 11.0)), front_face) # Mortar lines on the front face only. draw_line(Vector2(-8.0, -7.0), Vector2(8.0, -7.0), mortar, 1.0) draw_line(Vector2(-8.0, -3.0), Vector2(8.0, -3.0), mortar, 1.0) # Border between top and front (gives the depth illusion). draw_line(Vector2(-8.0, -11.0), Vector2(8.0, -11.0), Color(0.20, 0.20, 0.18, alpha), 1.0) # Outline. draw_rect(Rect2(Vector2(-8.0, -16.0), Vector2(16.0, 16.0)), outline, false, 1.0) func _draw_wood_wall(alpha: float) -> void: var top_face := Color(0.62, 0.45, 0.25, alpha) var front_face := Color(0.42, 0.30, 0.16, alpha) var plank := Color(0.30, 0.20, 0.10, alpha) var outline := Color(0.16, 0.10, 0.04, 0.7 * alpha) draw_rect(Rect2(Vector2(-8.0, -16.0), Vector2(16.0, 5.0)), top_face) draw_rect(Rect2(Vector2(-8.0, -11.0), Vector2(16.0, 11.0)), front_face) # Vertical plank seams on the front face. for x_offset in [-3.0, 2.0]: draw_line(Vector2(x_offset, -11.0), Vector2(x_offset, 0.0), plank, 1.0) 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 # Block pathfinding — wall is now impassable. if World.pathfinder != null: World.pathfinder.set_cell_walkable(tile, false) # Stamp the data-layer TileMap so room / roof / save logic sees the wall. World.mark_wall_tile(tile, wall_material) queue_redraw() Audit.log("wall", "%s wall completed at %s" % [wall_material, tile])