rimlike/scenes/ai/recipe.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

74 lines
2.9 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 Recipe extends RefCounted
## A recipe describes one production step: what raw material goes in, what
## product comes out, how long it takes, and what skill is required.
##
## Phase 6 ships single-ingredient / single-output recipes. Multi-input is
## a Phase 7+ expansion — the dict seam in to_dict/from_dict already uses
## the same key names as the full architecture.md spec so no migration is needed.
##
## Save/load contract:
## var r2 := Recipe.from_dict(r.to_dict())
## assert(r2.id == r.id and r2.work_ticks == r.work_ticks)
const SKILL_CRAFTING: StringName = &"crafting"
const SKILL_COOKING: StringName = &"cooking"
## Unique identifier — e.g. &"plank", &"stone_block", &"basic_meal".
var id: StringName = &""
## Item type consumed by this recipe (single-ingredient for Phase 6).
var ingredient_type: StringName = &""
## Phase 14 — optional secondary ingredient. Empty string = no secondary.
## CraftingProvider Phase 14 follow-up: enforce pickup of ingredient2 before
## assigning a pawn to this bill (currently stub — only ingredient_type enforced).
var ingredient2_type: StringName = &""
var ingredient2_count: int = 0
## Item type produced by this recipe.
var output_type: StringName = &""
## Sim ticks needed at 1× skill (no pawn modifier applied here).
var work_ticks: int = 100
## Which skill governs this recipe's quality roll and speed.
var required_skill: StringName = SKILL_CRAFTING
## Pawn's skill level must be >= this to be assigned this bill.
## Per design.md: skills modify duration and quality, never permission — so
## this threshold defaults to 0 (no gate); bill UI lets the player raise it.
var skill_threshold: int = 0
## Human-readable label for Audit logs and (later) bill UI. Not i18n'd here;
## call Strings.t("item." + id) for player-visible text.
var label: String = ""
# ── save / load ───────────────────────────────────────────────────────────────
func to_dict() -> Dictionary:
return {
"id": String(id),
"ingredient_type": String(ingredient_type),
"ingredient2_type": String(ingredient2_type),
"ingredient2_count": ingredient2_count,
"output_type": String(output_type),
"work_ticks": work_ticks,
"required_skill": String(required_skill),
"skill_threshold": skill_threshold,
"label": label,
}
static func from_dict(d: Dictionary) -> Recipe:
var r := Recipe.new()
r.id = StringName(d.get("id", ""))
r.ingredient_type = StringName(d.get("ingredient_type", ""))
r.ingredient2_type = StringName(d.get("ingredient2_type", ""))
r.ingredient2_count = int(d.get("ingredient2_count", 0))
r.output_type = StringName(d.get("output_type", ""))
r.work_ticks = int(d.get("work_ticks", 100))
r.required_skill = StringName(d.get("required_skill", str(SKILL_CRAFTING)))
r.skill_threshold = int(d.get("skill_threshold", 0))
r.label = str(d.get("label", ""))
return r