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:
@@ -0,0 +1,59 @@
|
||||
---
|
||||
date: 2026-06-11
|
||||
type: session
|
||||
tags:
|
||||
- session
|
||||
- combat
|
||||
- structures
|
||||
- enemy-ai
|
||||
- persistence
|
||||
- netcode
|
||||
- eb-1
|
||||
permalink: gamevault/07-sessions/2026/2026-06-11-eb1-machines-can-die
|
||||
---
|
||||
|
||||
# EB-1 — machines can die (structures get HP, Husks raze the base, a wounded base persists)
|
||||
|
||||
> Operator: "Completely implement EB-1." Path A milestone; forks locked in [[DR-029_Path_A_Fork_Locks]] (Husks push for the base/structures, you defend; a wounded base persists across save/quit; structure destruction is EB-1's job — the END-1 Core bar is later). Full [[dots-dev]] Feature track under **ultracode**: mandatory pre-code adversarial review (caught 1 blocker + 8 majors) + post-code adversarial review (CLEAN). Locked → [[DR-032_EB1_Machines_Can_Die]].
|
||||
|
||||
## What shipped — reuse the combat spine
|
||||
EB-1 reuses the exact ownerless-interpolated-ghost + server-written `Health.Current` + `DamageEvent`-buffer spine that Husks already prove in production. No parallel structure-health system.
|
||||
|
||||
### Structures get HP + can die
|
||||
- `TurretAuthoring` (MaxHp 120) + `StructureAuthoring` (Wall/Pylon MaxHp 150) now bake `Health{Current=Max=MaxHp}` + `AddBuffer<DamageEvent>` + a new empty **`Destructible`** tag. **No `HitRadius`** (deliberate — `ProjectileDamageSystem` needs Health+HitRadius, so player shots never friendly-fire your own turret). Re-baked (Health.Current is a `[GhostField]` → hash change; structures are runtime-spawned so no prespawn-baseline trap).
|
||||
- `HealthApplyDamageSystem` destroy gate += `HasComponent<Destructible>`. Structures carry **no `EffectiveCharacterStats`**, so they take the `math.max(0,..)` branch and CAN reach 0 (giving a structure stats would make it immortal — a gate comment records this). Occupancy auto-frees (`BuildPlaceSystem` derives it from live ghosts). At-most-once destroy (N Husk hits sum, one `DestroyEntity`).
|
||||
- **`Destructible` tag, NOT bare `PlacedStructure`:** that identity is shared by the reserved M7 automation machines whose conveyor cargo-drop teardown is unhandled — the tag lets each destructible opt in.
|
||||
|
||||
### Fortress targeting (the locked aggro fork)
|
||||
- `EnemyAISystem` now snapshots live structures (`PlacedStructure`+`Health`, skip HP<=0) **ABOVE** the early-return (guard → `players==0 && structures==0`) so Husks keep razing the base with every player dead/away.
|
||||
- Both the Grunt and Charger passes call the shared pure `EnemyAIMath.PickWeightedNearest(from, players, structures, weight, out isStructure, out index)` — the weight is a **squared** factor on structure distance (so `<1` prefers structures, a closer player "in the way" still wins). The strike appends to the resolved `targetEntity` (player or structure, which now has the buffer).
|
||||
- **`StructureAggroWeight`** is a `TuningConfig` knob (default 0.7, **value-clamp ≥0** — the default `≥1` branch would have silently floored 0.7 to 1.0 and killed the behavior), threaded through all five projections + `DebugTuningReport` + a `DebugOverlay` row (live-tunable, MC-0 pattern).
|
||||
|
||||
### Persistence v3 (wounded base persists)
|
||||
- `StructureSave.HP` + `PendingStructure.HP`; `SaveStructureScan` reads `Health.Current` **guarded by `HasComponent<Health>`** (an automation machine without Health would crash the autosave path, which has no try/catch); `BaseRestoreSystem` adds a `ComponentLookup<Health>` and sets `Health{Current = p.HP>0 ? p.HP : baked Max, Max = baked}` in the **SAME ecb** as Instantiate (born-correct GhostField; a deferred set would leak baked Max for one snapshot). `SaveData.CurrentVersion 2→3` + `MinLoadableVersion=2`; `SaveService.Load` gate is now a **floor `[2,3]`** so OLD v2 saves still load (a missing HP field 0-defaults → the `0→Max` restore guard → full HP). `WorldLauncher` uses the extracted pure `SaveApply.ToPending(StructureSave)` — the **5th persistence site** the pre-code review caught (omitting HP there silently restores every structure at full HP, a bug no JSON round-trip test catches).
|
||||
|
||||
### Loss has weight (feedback)
|
||||
- `CombatFeedbackSystem` suppresses structures (`bool isStructure = HasComponent<PlacedStructure>`; the hit-spark/damage-number line AND the player-death line are gated `&& !isStructure` — else a dying wall fired the HUMAN player-death cue + a cyan damage-number storm).
|
||||
- New client-only `StructureFeedbackSystem` (observe-only, PresentationSystemGroup): an amber chip on an HP decrease (camera-SILENT so a siege's hits never sustain shake), a LOUD red-orange burst + camera punch on destruction. **Proximity-gated** (copy of `WorldFeedbackSystem`'s range gate) so the base→expedition RegionRelevancy despawn (every base structure drops at once) stays silent. De-duped (an HP<=0 edge sets `DeathFired` so the prune-cleanup skips it). `StructureFeelConfig` static bridge with a `[RuntimeInitializeOnLoadMethod]` reset.
|
||||
|
||||
## Adversarial reviews (both workflowed, ultracode)
|
||||
- **Pre-code** (6 lenses → synthesis): caught the **server-crash BLOCKER** (appending a `DamageEvent` to a structure whose prefab lacks the buffer → ECB playback throws mid-wave) + 8 majors (the `Destructible`-not-`PlacedStructure` M7 trap; the early-return move; the `ClampKnob` value-vs-tick misclassification; the `SaveService` floor not blanket; the born-correct same-ECB Health restore; the 5-site HP flow incl. `WorldLauncher`; the `CombatFeedbackSystem` suppression; the `StructureFeedbackSystem` proximity gate). **All folded in before a line of code** — this is why it landed clean.
|
||||
- **Post-code** (3 lenses → adversarial verify → synthesis): **CLEAN** — all 5 candidate findings refuted, 0 confirmed bugs, no changes required.
|
||||
|
||||
## Validation
|
||||
- **EditMode: 312/312** (302 prior + 10 new: HealthApplyDamage Destructible ×2, PickWeightedNearest ×5, persistence v3/backward-compat/staging-map ×3; + the `StructureAggroWeight` default pin). Zero console errors at every compile.
|
||||
- **Play (live netcode, focused editor):** the **re-bake** is confirmed — all 3 catalog prefabs carry `Health`(120/150/150)+`Destructible`+`DamageEvent`; `StructureAggroWeight=0.7`; an end-to-end **spawn turret → append lethal damage → destroyed** worked in the running ServerWorld; **0 console errors/exceptions**.
|
||||
- **Operator hands-on fun-gate is OPEN** ("loses have weight"): build a turret → survive into a Siege → watch Husks push for + raze it → feel the loss → it persists wounded across Continue. Tune `StructureAggroWeight` (overlay "Struct aggro w") + the per-prefab `MaxHp` to taste.
|
||||
|
||||
## Deliberate cuts (kept END-1 + scope out)
|
||||
- **Wall physical-blocking of Husks** — out of scope (structures are ghosts inert to the DOTS PhysicsWorld; a damage-sponge AI target fully delivers "machines can die"). A wall is still meaningful: Husks prefer the nearest structure, so a forward wall soaks strikes that would hit a turret.
|
||||
- **No aggregate base-health HUD meter / replicated loss counter** — that is END-1 Core integrity, kept firmly OUT. Feedback is purely per-structure + client-local; the only new wire surface is the per-structure `Health.Current` `[GhostField]`.
|
||||
- **Per-structure health bar / progressive damage tint** — demoted to optional-for-EB-1 (recommended next if the bare chip+burst doesn't read enough anticipation).
|
||||
|
||||
## Files
|
||||
- New: `Simulation/Combat/Destructible.cs`; `Client/Presentation/StructureFeedbackSystem.cs`, `StructureFeelConfig.cs`.
|
||||
- Modified: `Authoring/Building/TurretAuthoring.cs`, `StructureAuthoring.cs`; `Server/Combat/HealthApplyDamageSystem.cs`, `EnemyAISystem.cs`; `Simulation/Combat/EnemyAIMath.cs`; `Simulation/Debug/TuningConfig.cs`, `Client/Debug/DebugOverlay.cs`; `Simulation/Persistence/SaveData.cs`, `SaveComponents.cs`, `SaveStructureScan.cs`, `SaveApply.cs`, `SaveService.cs`; `Server/Automation/BaseRestoreSystem.cs`; `Client/UI/WorldLauncher.cs`; `Client/Presentation/CombatFeedbackSystem.cs`.
|
||||
- Tests: `HealthApplyDamageSystemTests.cs`, `EnemyAIMathTests.cs`, `SavePersistenceTests.cs`, `TuningConfigTests.cs`.
|
||||
|
||||
## Next-session intent
|
||||
Operator runs the EB-1 fun-gate (does a structure loss have weight?). If it lands, the natural next Path A braid is **EB-2 (felt spend — turret ammo from harvest)** which closes the factory→defense pipe so the mined Ore is *spent*, then **END-1 (losable Core integrity bar)** + **END-2 (win/lose)**. If the loss doesn't read, add the per-structure damage tint (pattern in `CombatFeedbackSystem`) before moving on.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user