rimlike/scenes/entities/door.gd
megaproxy f82807ff3d Phase 5 — Designation, BuildJob, Wall/Floor/Door, Crate; 3/4 perspective pivot
Three gdscript-refactor agents in parallel + Opus integration.

Architectural pivot (memory.md Decisions table updated):
- View: top-down grid for gameplay + 3/4-perspective rendering for vertical
  structures (Stardew/Going Medieval style). Walls/doors/crates are Y-sorted
  entity sprites, not TileMap cells.
- Wall TileMap layer (Layer 2) becomes data-only — used for room detection,
  roof BFS, save serialization. Visual rendering happens at entity level.
- Asset reality check baked into the decision: the entire asset library is
  RPG-style perspective art; pivoting the renderer is cheaper than authoring
  or commissioning top-down 47-tile autotile sets.

Designation paint (scenes/world/, Agent A — ~170 lines):
- class_name Designation extends Node, lives as DesignationCtl child of World
- TOOL_NONE / TOOL_BUILD_WALL / TOOL_BUILD_FLOOR
- _unhandled_input captures left-mouse press/drag/release
- Drag-paints ghost tiles on Layer 3 via paint_layer.set_cell
- Green/red modulate based on World.pathfinder.is_walkable + cell occupancy
- Emits EventBus.designation_added/cleared per cell
- Selection.designation_active guard prevents double-handling clicks

EventBus signals added:
- designation_added(cell: Vector2i, tool: StringName)
- designation_cleared(cell: Vector2i)

BuildJob + Wall/Floor/Door entities (scenes/ai/ + scenes/entities/, Agent B — ~530 lines):
- Toil.KIND_BUILD + Toil.build_at(target_path) factory
- JobRunner._tick_build: resolves NodePath target, calls on_build_tick() per
  sim tick, marks toil done when is_buildable() returns false
- ConstructionProvider (priority=6, highest): nearest is_buildable() site in
  World.build_queue → Job=[walk_to(site.tile), build_at(site.get_path())]
- Wall entity: BUILD_TICKS=100, 40% alpha ghost; _complete() calls
  pathfinder.set_cell_walkable(false) + World.mark_wall_tile + Audit.log
- Floor entity: BUILD_TICKS=30, ground-level (no y_sort), does NOT block
  pathfinding, calls mark_floor_tile on complete
- Door entity: BUILD_TICKS=80, bottom-anchored, walkable when built (no
  pathfinder block); registers with World.doors for Phase 7 open/close logic
- ALL wall/door scenes have y_sort_enabled=true on root; floors don't (always
  on ground plane)

Crate furniture (scenes/world/, Agent C — ~270 lines):
- class_name Crate extends StorageDestination (Phase 4's abstract base)
- CAPACITY=4 stacks; accepts() gates on _completed + _filter_accepts + room
- find_drop_position returns tile when room exists, (-1,-1) otherwise
- BUILD_TICKS=60; on_build_tick mirrors Wall's pattern
- _draw procedural brown crate body + slat lines + fill-level dots

World autoload extensions (Opus):
- build_queue: Array — Wall/Floor/Door/Crate ghost entities awaiting
  construction. ConstructionProvider iterates by .priority desc; Phase 6+
  prepends material-haul toils.
- doors: Array — completed doors for future open/close (Phase 7+)
- wall_layer / floor_layer / designation_layer refs exposed for entity code
- mark_wall_tile(tile, material) / mark_floor_tile(tile, material) —
  stamps data-only TileMap layer with material-encoded atlas coord
- stockpile_at_tile(tile) — finds StockpileZone OR Crate covering a tile;
  used by JobRunner._tick_deposit to route Crate deposits

JobRunner._tick_deposit refactor (Opus):
- After clearing the haul-dirty flag, looks up stockpile_at_tile(pawn.tile)
- If destination is a Crate (has_method('register_item')), calls
  dest.register_item(item) so the crate's _contents tracks the stack

World scene integration (Opus):
- y_sort_enabled=true on World root so all entity sprites sort correctly
- DesignationCtl, ConstructionProvider, Wall TileMap (data-only, visible=false)
- World._ready wires:
  * World.wall_layer / floor_layer / designation_layer
  * designation.bind(designation_layer, selection)
  * Register 5 work providers (construction=6 > chop=5 > mine=4 > haul=3 > rest=0)
  * EventBus.designation_added → _on_designation_added (spawns Wall/Floor entity)
- _seed_phase5_demo_buildings: pre-queues 14 wall designations forming a
  5×4 cabin outline at (45, 25) so pawns visibly construct walls without
  player-paint UI (deferred to Phase 17). Spawns 2 fully-built crates at
  (17-18, 60) for hauling routing.

Acceptance — MCP-verified end-to-end:
- 14 wall designations seeded at boot, 2 crates pre-built
- All 3 pawns picked construction (highest priority work) and walked to
  build sites (paths 37/32/27 from spawn). Walls built one by one.
- Wall layer post-construction has 42 cells: 28 (Phase 1 stone ring) +
  14 (Phase 5 cabin) — both rendering paths (placeholder TileMap from
  Phase 1, plus new entity sprites from Phase 5) coexist correctly.
- Pathfinder set_cell_walkable(false) fired on each wall completion.
- Pawns transitioned from construction to hauling once all walls done.
- Final visual: 5×4 stone-walled cabin with mortar lines, Y-sorted entity
  rendering, wood items scattered east of the cabin awaiting haul.

Phase 5 gotchas (logged):
- 'material' as a member var shadows CanvasItem.material (Node2D inherits
  it). Renamed to wall_material / floor_material via quick-edit agent.
  Save-format dict KEYS stay as 'material' for stability.
- Class-name registration cache lag bit again (Tree/Pawn pattern from
  earlier phases). Workflow stays: agent writes class_name file → MCP
  reload_project → godot --headless --editor --quit → headless validate.
- ConstructionProvider scans build_queue every tick including completed
  walls; is_buildable() filters them out but the queue keeps growing.
  Phase 16+ should add an unregister_build_site call from _complete or
  a periodic queue compaction.

Delegation report this phase:
- Agent A (Sonnet, gdscript-refactor): Designation paint mode + EventBus
  signals + Selection guard. ~180 lines.
- Agent B (Sonnet, gdscript-refactor): Toil.KIND_BUILD + JobRunner._tick_build
  + ConstructionProvider + Wall/Floor/Door entities + scenes. ~530 lines.
- Agent C (Sonnet, gdscript-refactor): Crate furniture extending
  StorageDestination. ~270 lines.
- quick-edit (Haiku): material → wall_material/floor_material rename. ~14
  occurrences across 2 files.
- Opus: World autoload extensions + JobRunner _tick_deposit refactor +
  World scene integration (DesignationCtl + ConstructionProvider + new
  scene preloads + _seed_phase5_demo_buildings) + MCP runtime verification
  + the material-shadow + class-cache-lag debugging.

Pivot decision worth flagging: the asset library audit revealed that no
pack we own ships top-down 47-tile autotile walls. After multiple
researcher-overpromise cycles, the pragmatic call was to pivot the
rendering model itself. Walls now render as bottom-anchored tall sprites
with Y-sort; the entire asset library becomes usable as-is. Phase 17
polish will swap procedural _draw() with AtlasTexture regions from
Pixel Crawler / FG_Houses / Ventilatore Castle_Building.

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

138 lines
5.1 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 Door extends Node2D
## Door entity — built by a pawn with a Build job. Unlike walls, doors remain
## walkable once complete. Phase 5: always-open (always walkable). Phase 7+
## will add open/close animation and toggling.
##
## Door is rendered as a bottom-anchored tall sprite (Y-sorted), same as Wall,
## so it occludes pawns walking behind it — matching the 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 door is
## complete: it registers with World for future open/close logic, does NOT
## call set_cell_walkable(false), and transitions from ghost to solid.
##
## World registration: World.register_build_site / World.unregister_build_site
## are called from _ready / _exit_tree. Methods land in the Opus integration pass.
const TILE_SIZE_PX: int = 16
## Sim ticks to build a door at 1× speed (80 ticks = 4 sim seconds).
const BUILD_TICKS: int = 80
# ── state ──────────────────────────────────────────────────────────────────────
@export var tile: Vector2i = Vector2i.ZERO
## 0..BUILD_TICKS.
var build_progress: int = 0
var _completed: bool = false
## Phase 5: always-open. Phase 7+ toggles this for open/close animation.
var is_open: bool = true
# ── 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) -> void:
tile = p_tile
# Bottom-anchor: same as Wall so it Y-sorts correctly with pawns.
position = Vector2(
tile.x * TILE_SIZE_PX + TILE_SIZE_PX / 2.0,
tile.y * TILE_SIZE_PX + TILE_SIZE_PX
)
queue_redraw()
Audit.log("door", "door ghost placed at %s" % tile)
## True while the door still needs construction work.
func is_buildable() -> bool:
return not _completed
## Human-readable label for job descriptions and Audit logs.
func label() -> String:
return "door"
## Called by the BUILD toil in JobRunner once per sim tick.
func on_build_tick() -> void:
if _completed:
return
build_progress += 1
queue_redraw()
if build_progress >= BUILD_TICKS:
_complete()
## True once the door 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,
"build_progress": build_progress,
"completed": _completed,
"is_open": is_open,
}
static func from_dict(d: Dictionary) -> Dictionary:
return {
"tile_x": int(d.get("tile_x", 0)),
"tile_y": int(d.get("tile_y", 0)),
"build_progress": int(d.get("build_progress", 0)),
"completed": bool(d.get("completed", false)),
"is_open": bool(d.get("is_open", true)),
}
# ── render ─────────────────────────────────────────────────────────────────────
func _draw() -> void:
# Door: 16 px wide, 24 px tall, bottom-anchored. Origin at bottom-centre.
# (Shorter than the full 32 px wall height — visually reads as a door opening.)
var alpha: float = 1.0 if _completed else 0.4
var frame_color := Color(0.32, 0.22, 0.10, alpha)
var panel_color := Color(0.52, 0.36, 0.18, alpha)
var hinge_color := Color(0.20, 0.18, 0.16, alpha)
# Door frame (slightly wider than the panel).
draw_rect(Rect2(Vector2(-8.0, -24.0), Vector2(16.0, 24.0)), frame_color)
# Door panel (inset 2 px on each side).
draw_rect(Rect2(Vector2(-6.0, -22.0), Vector2(12.0, 20.0)), panel_color)
# Hinge dot on the left side.
draw_circle(Vector2(-6.0, -18.0), 1.5, hinge_color)
# Outline.
draw_rect(Rect2(Vector2(-8.0, -24.0), Vector2(16.0, 24.0)), Color(0.0, 0.0, 0.0, 0.5 * alpha), false, 1.0)
# ── internal ───────────────────────────────────────────────────────────────────
func _complete() -> void:
_completed = true
# Doors are walkable — do NOT call set_cell_walkable(false).
# Register so future open/close logic can locate this door by tile.
World.register_door(self)
queue_redraw()
Audit.log("door", "door completed at %s" % tile)