rimlike/scenes/entities/wall.gd
megaproxy f67c12c51f Clear designation tile-highlight when jobs complete
Each entity completion handler (wall/floor/door/bed/torch/workbench/crate
/tree/rock/big_rock/grave_slot) now calls World.clear_designation_at(tile)
so the orange/blue/etc. highlight overlay disappears with the job.
BigRock iterates its footprint to clear all four tiles.

World.designation_ctl is set during the scene boot wire-up; the helper
no-ops when the controller is absent (e.g. headless tests).
2026-05-15 19:31:55 +01:00

245 lines
9.9 KiB
GDScript
Raw Permalink 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 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
## ElvGames Fortress tileset — coord (13, 4) is a dark brick wall with a
## visible capstone on top. It's the middle column of a 3-tile-wide capped
## wall autotile; used as a non-autotile single sprite it reads as a proper
## medieval stone wall with a 3D cap and brick coursing below.
##
## The original pick was (1, 1), but that tile is actually a tan stone FLOOR
## (no brick texture, no cap) — rows of (1, 1) looked like a pavement, not a
## wall. Replaced 2026-05-12 after the user flagged "walls don't look great";
## /tmp/wall_rows.png from the same session shows the side-by-side simulation.
##
## We use a single sprite per material (Phase 5 lock: no autotile yet, so
## every wall renders identically regardless of neighbour configuration).
const _STONE_TEX: Texture2D = preload("res://art/tiles/FG_Fortress.png")
const _STONE_FILL_COORD: Vector2i = Vector2i(13, 4)
## 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
)
# Stone uses a sprite from FG_Fortress; wood still draws procedurally below
# until we find a 16×16 wood-wall tile that fits the perspective.
if wall_material == MATERIAL_STONE:
_build_stone_sprite()
queue_redraw()
Audit.log("wall", "%s wall ghost placed at %s" % [wall_material, tile])
## Builds the stone-fill Sprite2D child. Bottom-anchored so it sits flush with
## the tile's bottom edge (matching the procedural draw box y=-16..0).
func _build_stone_sprite() -> void:
var sprite := Sprite2D.new()
sprite.name = "Sprite"
sprite.texture = _STONE_TEX
sprite.region_enabled = true
sprite.region_rect = Rect2(
_STONE_FILL_COORD.x * TILE_SIZE_PX,
_STONE_FILL_COORD.y * TILE_SIZE_PX,
TILE_SIZE_PX,
TILE_SIZE_PX,
)
sprite.centered = true
# Sprite center at y=-8 so 16×16 sprite spans y=-16..0 (matches procedural).
sprite.offset = Vector2(0, -8)
# Ghost state — translucent until built.
sprite.modulate.a = 1.0 if _completed else 0.4
add_child(sprite)
## 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
## Construction-provider hint: walls become impassable when built, so pawns
## must stand on an adjacent tile while building. Phase 6 fix for the
## "pawn-trapped-on-wall" bug. Floors / Doors / Crates / Workbenches don't
## need this since they remain walkable after completion.
func blocks_pathing_when_complete() -> bool:
return true
## 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 {
"class_id": &"wall",
"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:
# Stone walls render via the Sprite2D child (see _build_stone_sprite).
# Wood walls still draw procedurally until a wood-wall sprite lands.
var alpha: float = 1.0 if _completed else 0.4
if wall_material == MATERIAL_WOOD:
_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)
# Dislodge any pawn standing on this tile (Phase 6 wall-trap fix only
# protected the BUILDING pawn via adjacent-stand; bystanders weren't
# guarded, so a pawn idling on a queued build site would get stranded).
for p in World.pawns:
if p.tile == tile:
var safe: Vector2i = World.pathfinder.find_nearest_walkable(tile)
if safe != tile:
p.tile = safe
p.position = Vector2(safe.x * 16 + 8, safe.y * 16 + 8)
p._path.clear()
if p.job_runner != null:
p.job_runner.cancel_job()
Audit.log("wall", "dislodged %s from %s%s on wall complete" % [p.pawn_name, tile, safe])
# Stamp the data-layer TileMap so room / roof / save logic sees the wall.
World.mark_wall_tile(tile, wall_material)
# Solidify the ghost: sprite (if any) → full opacity; wood _draw rereads alpha.
var sprite: Sprite2D = get_node_or_null("Sprite")
if sprite != null:
sprite.modulate.a = 1.0
queue_redraw()
queue_redraw()
World.clear_designation_at(tile)
Audit.log("wall", "%s wall completed at %s" % [wall_material, tile])