rimlike/scenes/entities/wall.gd
megaproxy 6d04c8229b Phase 5 visual polish: real 3/4 walls, bigger cabin, subtle grass
User feedback after first visual inspection: 'walls looked really thick
and there was hardly any inside space'. Five targeted fixes:

1. Wall._draw rewrite — proper within-tile 3/4 perspective. Previously the
   wall was a 16w×32h bottom-anchored rect that rose UP into the cell
   above (which encroached on the cabin interior whenever the wall faced
   south). New layout: 16×16 fitting strictly inside its own tile, with
   a lit-top 5px band + shaded-front 11px band + mortar lines + outline.
   Reads as 'a wall standing up' without overlapping adjacent cells.
   Same shape for stone and wood materials (different palettes).

2. Demo cabin expanded from 5×4 to 8×6 — interior went from 6 cells
   (closet) to 24 cells (actual room).

3. Grass-tile border darken: 0.15 → 0.04. Killed the graph-paper effect
   that dominated at 3×+ zoom. Tile boundary still readable when looking
   carefully; doesn't dominate the visual.

4. Removed Phase 1 _paint_sample_walls() seed. That 8×8 stone ring lived
   only on the Wall TileMap layer (set to visible=false in the Phase 5
   rendering pivot) — so it was an invisible path obstacle. Cleaner to
   not seed it at all now that walls render at entity level.

5. CameraRig default target_zoom: 1.0 → 2.5. At 1× on a 1280-px viewport
   the world feels sparse and pawns become 6-pixel dots. 2.5× shows ~30
   tiles wide which is the 'comfortable inspection' level.

MCP-verified visually: the cabin now reads as a proper 8×6 stone room
with raised 3/4 walls and a real 24-cell interior. Subtle grass field.
The 'commit to 3/4 perspective' rendering decision is now actually
visible (was conceptual-only before).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:30:40 +01:00

182 lines
7.3 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
## 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])