rimlike/scenes/entities/grave_slot.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

228 lines
8.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 GraveSlot extends Node2D
## Phase 14 — per-tile grave slot entity. Spawned by the designation system when
## the player paints TOOL_DIG_GRAVE. Progresses through a two-state build model:
##
## ghost (BUILD_TICKS not yet reached)
## ↓ ConstructionProvider builds it via on_build_tick()
## dug (open hole, ready for a corpse)
## ↓ HaulingProvider routes a corpse here; accept_corpse() is called
## (GraveSlot queue_free-s itself; GraveMarker spawned at same tile)
##
## Build contract: same duck-type interface as Wall / Floor / Door so that
## ConstructionProvider's untyped iteration in World.build_queue picks it up
## automatically without any dispatch addition.
## site.tile: Vector2i
## site.is_buildable() -> bool
## site.label() -> String
## site.get_path() -> NodePath
## site.blocks_pathing_when_complete() -> bool (returns false — grave stays walkable)
##
## StorageDestination duck-type interface (registered in World.stockpiles):
## accepts(item) -> bool
## find_drop_position(item) -> Vector2i
## covers_tile(tile) -> bool
## is_grave_slot_dug() -> bool (queried by GraveyardZone._find_open_grave_slot)
##
## See docs/implementation.md Phase 14 "GraveSlot".
const TILE_SIZE_PX: int = 16
## Sim ticks to dig the grave (80 ticks ≈ 4 s at 1×).
const BUILD_TICKS: int = 80
## Visual colours.
const _GHOST_FILL := Color(0.38, 0.28, 0.16, 0.35) # pale dirt, transparent
const _DUG_FILL := Color(0.28, 0.18, 0.08, 0.85) # dark open earth
const _OUTLINE := Color(0.18, 0.12, 0.06, 0.80)
## Tile position.
@export var tile: Vector2i = Vector2i.ZERO
var build_progress: int = 0
var _dug: bool = false # true once BUILD_TICKS reached (ghost→dug transition)
# ── lifecycle ─────────────────────────────────────────────────────────────────
func _ready() -> void:
# Register as a build site so ConstructionProvider picks it up.
World.register_build_site(self)
# Register as a stockpile destination so HaulingProvider can route corpses here.
World.register_stockpile(self)
z_index = 1 # above terrain, below items
func _exit_tree() -> void:
World.unregister_build_site(self)
World.unregister_stockpile(self)
# ── public setup ──────────────────────────────────────────────────────────────
## One-shot initialiser. Call after add_child(). Sets tile + world position.
func setup(p_tile: Vector2i) -> void:
tile = p_tile
# Centre-align within the tile (same convention as Corpse, Item).
global_position = Vector2(
tile.x * TILE_SIZE_PX + TILE_SIZE_PX / 2.0,
tile.y * TILE_SIZE_PX + TILE_SIZE_PX / 2.0
)
queue_redraw()
Audit.log("grave_slot", "ghost placed at %s" % tile)
# ── ConstructionProvider duck-type interface ──────────────────────────────────
## True while the slot still needs digging. ConstructionProvider checks this.
func is_buildable() -> bool:
return not _dug
## GraveSlot stays walkable after being dug — the pawn stands on it to deposit.
func blocks_pathing_when_complete() -> bool:
return false
## Human-readable job label used by ConstructionProvider and Audit logs.
func label() -> String:
return "grave"
## Called by the BUILD toil in JobRunner once per sim tick while the pawn digs.
func on_build_tick() -> void:
if _dug:
return
build_progress += 1
queue_redraw()
if build_progress >= BUILD_TICKS:
_complete_dig()
## True once the slot has finished the ghost-phase (for World._on_designation_cleared).
func is_completed() -> bool:
return _dug
# ── StorageDestination duck-type interface ────────────────────────────────────
## Returns true if this slot is dug (open) and ready to accept a corpse.
## GraveyardZone calls this to verify an open slot exists in the region.
func is_grave_slot_dug() -> bool:
return _dug
## Returns true if `item` is a Corpse entity AND this slot is dug.
func accepts(item) -> bool:
if not _dug:
return false
return item.has_method("get_item_type") and item.get_item_type() == &"corpse"
## Returns the slot's own tile if it is dug and available; Vector2i(-1,-1) otherwise.
func find_drop_position(item) -> Vector2i:
if not accepts(item):
return Vector2i(-1, -1)
return tile
## True when `p_tile` matches this slot's tile.
func covers_tile(p_tile: Vector2i) -> bool:
return p_tile == tile
## Priority mirrors StorageDestination enum so HaulingProvider's priority
## comparison works cleanly. NORMAL for grave slots.
var priority: int = StorageDestination.Priority.NORMAL
# ── burial ─────────────────────────────────────────────────────────────────────
## Called by JobRunner._tick_deposit_corpse when a pawn has carried a corpse
## here and is standing on this tile. Instantiates a GraveMarker at this tile,
## emits EventBus.corpse_buried, then frees both the corpse and this slot.
func accept_corpse(corpse, world_parent: Node) -> void:
if not _dug:
Audit.log("grave_slot", "accept_corpse called before slot is dug at %s — ignoring" % tile)
return
Audit.log("grave_slot", "burying '%s' at %s" % [corpse.deceased_name, tile])
# Spawn permanent GraveMarker.
var marker_script: Script = load("res://scenes/entities/grave_marker.gd")
var marker := Node2D.new()
marker.set_script(marker_script)
marker.name = "GraveMarker_%s_%s" % [tile.x, tile.y]
world_parent.add_child(marker)
marker.setup(tile, corpse.deceased_name, corpse.deceased_portrait_color,
corpse.death_cause, corpse.death_tick)
Audit.log("grave_slot", "marker placed for '%s' (cause=%s death_tick=%d)" % [
corpse.deceased_name, corpse.death_cause, corpse.death_tick
])
EventBus.corpse_buried.emit(corpse, marker)
# Free the corpse and the slot — marker persists.
corpse.queue_free()
queue_free()
# ── visual ────────────────────────────────────────────────────────────────────
func _draw() -> void:
# Drawn centred in tile (origin = tile centre).
if _dug:
_draw_open_grave()
else:
_draw_ghost()
func _draw_ghost() -> void:
# Transparent dashed outline: dug-grave shape as a ghost.
draw_rect(Rect2(-6.0, -4.0, 12.0, 8.0), _GHOST_FILL, true)
draw_rect(Rect2(-6.0, -4.0, 12.0, 8.0), Color(_OUTLINE.r, _OUTLINE.g, _OUTLINE.b, 0.40), false, 1.0)
# Progress bar — thin strip across bottom.
if BUILD_TICKS > 0 and build_progress > 0:
var pct := float(build_progress) / float(BUILD_TICKS)
var bar_w := 12.0 * pct
draw_rect(Rect2(-6.0, 3.0, bar_w, 1.5), Color(0.6, 0.5, 0.3, 0.6), true)
func _draw_open_grave() -> void:
# Dark open pit: recessed rectangle to suggest depth.
draw_rect(Rect2(-6.0, -4.0, 12.0, 8.0), _DUG_FILL, true)
# Lighter near-edge to suggest rim of earth.
draw_rect(Rect2(-6.0, -4.0, 12.0, 2.0), Color(0.45, 0.33, 0.18, 0.70), true)
draw_rect(Rect2(-6.0, -4.0, 12.0, 8.0), _OUTLINE, false, 1.0)
# ── save / load ───────────────────────────────────────────────────────────────
func to_dict() -> Dictionary:
return {
"class_id": &"grave_slot",
"tile_x": tile.x,
"tile_y": tile.y,
"build_progress": build_progress,
"dug": _dug,
}
func from_dict(d: Dictionary) -> void:
tile = Vector2i(int(d.get("tile_x", 0)), int(d.get("tile_y", 0)))
build_progress = int(d.get("build_progress", 0))
_dug = bool(d.get("dug", false))
global_position = Vector2(
tile.x * TILE_SIZE_PX + TILE_SIZE_PX / 2.0,
tile.y * TILE_SIZE_PX + TILE_SIZE_PX / 2.0
)
queue_redraw()
# ── internal ─────────────────────────────────────────────────────────────────
func _complete_dig() -> void:
_dug = true
queue_redraw()
World.clear_designation_at(tile)
Audit.log("grave_slot", "grave dug at %s (ready for burial)" % tile)