Docs: EB-1 session log + DR-032; CLAUDE.md machines-can-die

Session log + DR-032 (structures reuse Health/DamageEvent, Destructible-not-PlacedStructure, fortress targeting, persistence v3, loss feedback; both adversarial reviews). CLAUDE.md: EB-1 build-gotcha bullet + persistence v3 floor-gate; net condensation of M7/inventory/UITK/juice reference bullets.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-11 23:53:50 -07:00
parent 66edbdec69
commit e04cdea44f
3 changed files with 114 additions and 5 deletions
@@ -0,0 +1,49 @@
---
id: DR-032
title: EB-1 = machines can die — structures reuse the Health/DamageEvent spine, Husks fortress-target them, a wounded base persists (SaveData v3)
status: accepted
date: 2026-06-11
tags:
- decision
- design
- combat
- structures
- netcode
- persistence
- eb-1
permalink: gamevault/07-sessions/decisions/dr-032-eb1-machines-can-die
---
# DR-032 — EB-1: Machines Can Die
## Context
Session [[2026-06-11_EB1_Machines_Can_Die]] · forks locked [[DR-029_Path_A_Fork_Locks]] · combat thesis [[DR-028_Combat_Primary_Verb_Depth_First]] · base loop [[DR-031_Base_Mining_Loop_Cohesion]] · build/structures [[DR-014_M6_Build_Structures_Automation_Foundation]] · persistence [[DR-019_Frontend_Menu_Settings_Saves_Build]].
Operator: "Completely implement EB-1." The base loop (DR-031) lets you mine→build, but a built structure had no stakes — it could not be lost. EB-1's locked forks (DR-029): Husks **push for the base/structures** (you defend; live-singleton tunable); a **wounded base persists** across save/quit (structure HP in SaveData v3); structure destruction is EB-1's job (the END-1 Core integrity bar is later). Ran the standing **pre-code adversarial review** (1 blocker + 8 majors, all folded in) and a **post-code** review (CLEAN).
## Decision
**1. Structures reuse the existing combat spine — no parallel system.** `Health{[GhostField] Current; Max}` + a `DamageEvent` buffer are baked onto the Turret/Wall/Pylon ghost prefabs (the same ownerless-interpolated-ghost shape Husks use). The strike append + drain + clamp + destroy all reuse `HealthApplyDamageSystem`. Rejected a parallel `StructureHealth`/`StructureDamage` system (would duplicate the proven spine + lose the `Health.Current` `[GhostField]` the feedback edge-detect needs).
**2. A `Destructible` tag gates destruction, NOT bare `PlacedStructure`.** `HealthApplyDamageSystem`'s destroy gate becomes `TrainingDummyTag || EnemyTag || HasComponent<Destructible>`. `PlacedStructure` is a SHARED identity worn by the reserved M7 automation machines (Harvester/Fabricator/Conveyor) whose conveyor cargo-drop teardown is unhandled — a bare-`PlacedStructure` gate would make every machine destructible the instant it got Health. The tag (zero-size, ghost-hash-neutral) lets each destructible opt in. Structures carry **no `EffectiveCharacterStats`** so they take the `math.max(0,..)` clamp branch and CAN die — adding stats would make them immortal (a gate comment records this anti-pattern). **No `HitRadius`** on structures (deliberate): `ProjectileDamageSystem` needs Health+HitRadius, so player shots never friendly-fire a turret.
**3. Fortress targeting via one shared, weighted helper.** `EnemyAISystem` snapshots live structures ABOVE the early-return (guard `players==0 && structures==0`) so Husks raze an undefended base. Both the Grunt and Charger passes call the pure `EnemyAIMath.PickWeightedNearest(from, players, structures, weight, out isStructure, out index)` — the weight is a **squared** factor on structure distance (a linear weight in a `lengthsq` comparison is mathematically wrong), so `<1` prefers structures while a sufficiently-closer player "in the way" still wins. One helper makes both archetypes aggro bit-identically and is unit-testable. `StructureAggroWeight` is a **`TuningConfig` knob** (default 0.7), NOT a standalone singleton (a separate singleton duplicates the SetTuning/broadcast/overlay plumbing and never reaches MPPM thin clients). It is a **value knob (clamp ≥0)** — the default `≥1` clamp branch would silently floor 0.7 to 1.0 and kill the "structures preferred" behavior.
**4. Persistence v3 is additive and threads HP through FIVE sites.** `StructureSave.HP`, `PendingStructure.HP`, `SaveStructureScan` (read `Health.Current`, guarded by `HasComponent<Health>` so an automation machine doesn't crash the try/catch-less autosave path), `BaseRestoreSystem` (set Health in the SAME ecb as Instantiate — born-correct `[GhostField]`; Max + the `0→Max` fallback sourced from the BAKED prefab, never the save), and `WorldLauncher.StagePendingSave` (via the extracted pure `SaveApply.ToPending`). `SaveData.CurrentVersion 2→3` + `MinLoadableVersion=2`; the `SaveService.Load` gate is a **floor `[2,3]`** (a blanket `<=Current` would accept v0/v1 garbage into a restore that assumes the structure-array shape), so old v2 saves still load — a missing HP field 0-defaults and the `0→Max` guard restores them full.
**5. Loss has weight without an aggregate meter.** `CombatFeedbackSystem` is suppressed for structures (else a dying wall fires the HUMAN player-death cue + a cyan damage-number storm — both gated `&& !isStructure`). A new client-only `StructureFeedbackSystem` observes `PlacedStructure`+`Health`: a camera-silent amber chip on a HP decrease, a LOUD burst + camera punch on destruction, **proximity-gated** (the base→expedition RegionRelevancy despawn drops every base structure at once — a naive prune=death would storm a false "base wiped") and de-duped (`DeathFired`). No aggregate base-health HUD / replicated loss counter — that is END-1 Core integrity, kept OUT; the only new wire surface is the per-structure `Health.Current` `[GhostField]`.
## Consequences
- A built structure now has stakes: Husks prefer it, it takes damage, it can die, and the wound persists across Continue — closing the "build has weight" half of the loop. A forward wall soaks strikes that would hit a turret (meaningful even without physical blocking).
- 312/312 EditMode (10 new); live netcode Play clean (re-bake verified, end-to-end destroy proven, 0 errors). **Operator fun-gate OPEN** ("does the loss have weight?"). Live knobs: `StructureAggroWeight` (overlay) + per-prefab `MaxHp`.
- The pre-code review's 1-blocker-8-majors → a CLEAN post-code review is the strongest evidence yet that the mandatory pre-code adversarial pass on a netcode-heavy slice prevents bugs rather than finding them late.
## Alternatives considered (rejected)
- **Bare `HasComponent<PlacedStructure>` destroy gate** — shared with reserved automation machines (unhandled cargo teardown); `Destructible` tag instead.
- **A parallel `StructureHealth`/`StructureDamage` system** — duplicates the spine + loses the `[GhostField]` the feedback needs.
- **A standalone `StructureAggroWeight` singleton** — duplicates the live-tuning plumbing; a `TuningConfig` knob instead.
- **Wall physical-blocking of Husks** — a conscious cut (needs a dynamic `CollisionWorld` of player-built structures); a damage-sponge AI target suffices for "machines can die". Revisit in a later EB.
- **Adding `EffectiveCharacterStats` to structures for an HP ceiling** — anti-pattern; it would clamp to a non-zero floor and never die.