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: # Wall is drawn as a bottom-anchored 16×32 rect occupying the lower portion # of the virtual sprite area. The origin (0, 0) of _draw() sits at position, # which is at the tile's bottom-centre (tile_y * 16 + 16). # Drawing upward: Y goes from 0 to -32 in local space. var alpha: float = 1.0 if _completed else 0.4 if wall_material == MATERIAL_STONE: _draw_stone_wall(alpha) else: # Fallback: render as wood until art assets are wired in Phase 6+. _draw_wood_wall(alpha) func _draw_stone_wall(alpha: float) -> void: var base := Color(0.55, 0.55, 0.50, alpha) var mortar := Color(0.40, 0.40, 0.38, alpha) # Main body: 16 px wide, 32 px tall, origin at bottom-centre. draw_rect(Rect2(Vector2(-8.0, -32.0), Vector2(16.0, 32.0)), base) # Mortar / horizontal stone-course lines (4 lines spaced 8 px apart). for i in range(1, 5): var y: float = -8.0 * float(i) draw_line(Vector2(-8.0, y), Vector2(8.0, y), mortar, 1.0) # Outline. draw_rect(Rect2(Vector2(-8.0, -32.0), Vector2(16.0, 32.0)), Color(0.0, 0.0, 0.0, 0.5 * alpha), false, 1.0) func _draw_wood_wall(alpha: float) -> void: var base := Color(0.55, 0.40, 0.22, alpha) var plank := Color(0.45, 0.32, 0.16, alpha) draw_rect(Rect2(Vector2(-8.0, -32.0), Vector2(16.0, 32.0)), base) # Vertical plank lines. for x_offset in [-3.0, 2.0]: draw_line( Vector2(x_offset, -32.0), Vector2(x_offset, 0.0), plank, 1.0 ) draw_rect(Rect2(Vector2(-8.0, -32.0), Vector2(16.0, 32.0)), Color(0.0, 0.0, 0.0, 0.5 * alpha), 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])