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.
|
||||
Reference in New Issue
Block a user