rimlike/scenes/entities/wall.gd
megaproxy 2de5130ae0 Visual pass 2: tree + rock + stone wall sprite swaps
Replaces three procedural _draw() entities with bundle sprites:

- Tree: was draw_rect trunk + draw_circle canopy. Now Sprite2D using
  FG_Tree_Spring.png (64x80, 4 variants picked deterministically from
  tile coord). Bottom-anchored so trunk base sits at tile bottom, canopy
  rises into the cell above; y_sort_enabled so canopies tuck behind
  pawns south of the trunk. Chop-progress notch overlay retained.
- Rock: was draw_colored_polygon hex. Now Sprite2D reading from the
  existing FG_Grasslands_Spring.png decoration atlas at three eyeballed
  coords (2 gray boulders, 1 brown rock pile). Variant deterministic
  per tile. Mine-progress crack overlay retained.
- Stone wall: was procedural top-band + front-band + mortar lines. Now
  Sprite2D from FG_Fortress.png at (1,1) — clean tan-stone brick fill.
  Bottom-anchored (offset.y=-8) so the 16x16 sprite spans y=-16..0,
  matching the procedural draw box exactly. Ghost state via modulate.a.
  Wood walls still use procedural _draw_wood_wall — no clean 16x16 wood
  tile found in the bundle yet (Pixel Crawler Walls.png is 32x32, would
  need crop+rescale).

Asset additions:
- art/sprites/FG_Tree_Spring.png (Tier 1, Grasslands pack)
- FG_Fortress.png and FG_Grasslands_Spring.png were already in art/tiles
  from earlier passes; this commit just consumes them from new sites.

Headless boots clean, runtime verified: trees look like chunky pixel-art
trees with root flare, rocks read as real boulders, cabin walls show
proper brick texture.

License: all ElvGames Humble bundle — commercial OK with credit.
Credit-string compilation still open.

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

222 lines
8.7 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 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 (1, 1) is a plain tan-stone fill tile.
## Eyeballed from /tmp/walls/probe.png in the 2026-05-12 visual pass.
## We use a single sprite per material (Phase 5 lock: no autotile yet).
const _STONE_TEX: Texture2D = preload("res://art/tiles/FG_Fortress.png")
const _STONE_FILL_COORD: Vector2i = Vector2i(1, 1)
## 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)
# 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()
Audit.log("wall", "%s wall completed at %s" % [wall_material, tile])