rimlike/scenes/entities/grave_slot.gd
megaproxy 67ec2cce7f Phase 14: Death + Corpses + Burial + Cremation
Three-agent fan-out. Opus pre-wrote Corpse class + 5 EventBus signals +
World registries (corpses, grave_markers) before dispatch so all three
slices ran fully parallel. Pattern proven across Phases 12/13/14.

Death pipeline (Agent A):
- Pawn.is_dead(), _check_death() — pawn_died signal → corpse spawn →
  corpse_spawned signal → World.unregister_pawn → queue_free
- _last_damage_source carries cause from take_damage() (now StringName)
- Bleed-out timeout: _bleed_ticks accumulates while bleeding active;
  at BLEED_OUT_TICKS=432000 (6 in-game hours) force-kills via take_damage
- Pawn.portrait_color stored field for corpse head-color hand-off
- Corpse: DECAY_PER_TICK=0.05 (~33 in-game min fresh→rotted at 1×),
  is_rotting()@50, queue_free@100 with corpse_rotted_away signal.
  Rotting bumps DirtinessSystem (Phase 13 hook) +0.04/tick (~+8/in-game-min)
- DEMO_PHASE14_AUTOKILL toggle in world.gd (default false, gates safety)

Graveyard + GraveSlot + GraveMarker + Hauling (Agent B):
- scenes/world/graveyard_zone.gd — StorageDestination subclass,
  accepted_types=[corpse], brownish overlay, finds dug GraveSlots
- scenes/entities/grave_slot.gd — buildable (ghost→dug) state machine,
  StorageDestination duck-type interface, accept_corpse() spawns
  GraveMarker + emits corpse_buried + queue_frees self
- scenes/entities/grave_marker.gd — permanent memorial, procedural
  stone-cross _draw, carries deceased identity, save round-trip
- TOOL_GRAVEYARD + TOOL_DIG_GRAVE paint modes (Designation dispatch)
- KIND_PICKUP_CORPSE + KIND_DEPOSIT_CORPSE toils + JobRunner handlers
- HaulingProvider.find_best_for iterates World.corpses in addition to
  items_needing_haul; corpse-payload stored as Node metadata on pawn
- ConstructionProvider duck-type already accepts GraveSlot (no change)

Cremation + Ash + Mood thoughts (Agent C):
- scenes/entities/cremation_pyre.gd — extends Workbench, label 'Pyre',
  auto-populates FOREVER bill for cremate_corpse, on_craft_complete
  drops 1 ash + emits corpse_cremated + queue_frees corpse
- Recipe.ingredient2_type/count added with save round-trip; recipe
  catalog entry cremate_corpse(TYPE_CORPSE primary + 5 wood secondary)
  NOTE: CraftingProvider still only enforces ingredient1 — documented
  gap, ships when crafting is generalized.
- Item.TYPE_ASH added + ALL_TYPES filter array entry
- 4 mood thoughts: saw_corpse (-3 EVENT 1200t max=3), buried_friend
  (+2 EVENT 2400t), cremated_friend (+2 EVENT 2400t),
  rotting_body_in_colony (-4 PERSISTENT stacks=count capped at 3)
- Pawn sync hooks: proximity scan (saw_corpse), signal listeners
  (buried/cremated within 8-tile radius), count helper for rotting

MCP runtime verified:
- DEMO_PHASE14_AUTOKILL toggle force-killed Bram at tick 50
- 'Bram DIED (cause=demo_kill, tile=(20, 36))' + corpse spawned
- 'Cora: saw_corpse thought added (corpse Bram at dist 5)' — mood -3
- Painted graveyard + dig_grave → grave dug to completion verified
  in build_queue (grave @(22, 39) complete=true)
- Hauler round-trip (corpse → GraveSlot → GraveMarker) WIRED correctly
  but didn't land within decay window at ULTRA speed (12×) — corpse
  rotted before priority-3 corpse-haul scheduled. Tuning for Phase 20.
- Screenshot captured: fresh corpse silhouette at cabin doorway

Delegation: 3× gdscript-refactor (Sonnet) agents in parallel;
integration + MCP runtime verify on Opus.

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

204 lines
7.6 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)
# ── internal ─────────────────────────────────────────────────────────────────
func _complete_dig() -> void:
_dug = true
queue_redraw()
Audit.log("grave_slot", "grave dug at %s (ready for burial)" % tile)