Slice Combat Depth (MC-3 + wiring + review fixes): Spitter aim-line + player-hit punch, rigged enemies, in-band gate (DR-041)
Completes the Combat Depth slice on top of the MC-2 server spine (56cf60cce):
MC-3 impact juice (client, observe-only):
- 7 FeelConfig fields + ResetDefaults; magnitude-scaled player-dealt-hit camera
PunchFov on the enemy-Health-decrease edge (camera-only hit-stop, never timeScale).
- Spitter Kind==2 aim-LANE telegraph (BuildLaneMesh) — reads baked SpitterState
client-side, falls back to a fixed length. True freeze + material flash deferred.
Content / wiring:
- SpitterProjectilePrefabAuthoring (the SpitterProjectilePrefab singleton).
- Both directors rebuilt to a 4-entry KIND-INDEXED roster [Grunt,Charger,Spitter,
Swarmer] + mix/MaxAlive config + the SpitterProjectileConfig singleton in the subscene.
- Real rigged models: EnemySpitter (re-skinned Kaiju, ranged poker) + EnemySwarmerUndead
(Undead-Werewolf, fast/low-HP); grunt/charger keep Werewolf/ChargerMuscle. EnemySpit =
ownerless interpolated ghost (no Health, no collider).
Post-impl adversarial review fixes (wf_febdcfdb-665):
- [MED] in-band fire gate: the Spitter committed its telegraph from ANY range (fired while
advancing from far). Now commits only when sInBand || sCornered (gives CorneredRange a
real read site) — a Spitter out-of-band holds fire and repositions.
- [LOW] EnemyProjectileDamageSystem early-returns on !ServerTick.IsValid (sibling parity).
- [LOW] EnemyAuthoring bake-time guard: errors if a prefab composes both Charger+Spitter
(would match zero AI passes -> never move).
- [LOW] tests: Spitter brain fires from Expedition (kills the Base==0 region false-green);
a direct partition-exclusion test replaces the order-masked claim; added out-of-band +
cornered negative tests.
388/388 EditMode green + two Play smokes (clean boot, fire, swept-hit, region, server==
client; rigged Kaiju spitter bakes + fires with zero console errors). Accepted as-is
(documented in DR-041): global spit soft-cap, co-op punch attribution.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+33
@@ -0,0 +1,33 @@
|
||||
---
|
||||
title: Combat Depth Slice — Enemy Variety (MC-2) + Impact (MC-3) — Build
|
||||
date: 2026-06-24
|
||||
tags: [session, combat, enemies, netcode, juice, slice, rukhanka]
|
||||
permalink: gamevault/07-sessions/2026/2026-06-24-combat-depth-build
|
||||
---
|
||||
|
||||
# Combat Depth (MC-2 + MC-3) — Build session
|
||||
|
||||
Built the combat-depth slice the operator chose after Slice 3 ("the combat needs a lot more work"). Spec + forks + the full build record are in [[DR-041_Slice_Combat_Depth_Enemy_Variety_Impact]]. This log is the build narrative.
|
||||
|
||||
## What shipped
|
||||
- **Two new enemy questions:** the **Spitter** (ranged reposition — holds a preferred range-band, fires a telegraphed dodgeable spit) and the **Swarmer** (surround — deterministic cluster spawn). On top of the existing Grunt (walk-up melee) + Charger (committed lunge) → four distinct readable questions.
|
||||
- **4-type weighted mix** in BOTH directors (expedition `ZoneEnemyDirector` + base-siege `WaveSystem`, fork-4a) sharing one pure `ZoneEnemyMath` composition function, with the mandatory `MaxAlive` cap. Legacy size/charger curve kept + parity-tested.
|
||||
- **MC-3 impact:** magnitude-scaled player-dealt-hit camera punch + the Spitter aim-LANE telegraph (camera-only hit-stop, never `Time.timeScale`).
|
||||
- **Real rigged enemies:** Spitter = a re-skinned rigged Kaiju, Swarmer = a rigged Undead-Werewolf (fast/low-HP); grunt/charger unchanged. Spit projectile = `EnemySpit` ownerless interpolated ghost.
|
||||
|
||||
## How it went (verify ladder)
|
||||
- **MC-2 server spine** committed first as `56cf60cce` (already green at the time).
|
||||
- **MC-3 + tests + rigging + wiring** built via MCP. **388/388 EditMode** (added Spitter-brain, swarmer-cluster, projectile, mix, and 3 review-driven guard tests).
|
||||
- **Play smoke (×2):** clean two-world boot (no `ComponentSystemSorter` cycle, no Burst ICE), Spitter fires → spit sweeps → player damaged (HP 105→25) → `DamageEvent` drained, region-correct, **replicated server→client**. Re-ran with the rigged Kaiju spitter after the fix: spawns + fires + damages, **zero console errors** (Rukhanka rig baked intact).
|
||||
|
||||
## Post-impl adversarial review (`wf_febdcfdb-665`)
|
||||
3 lenses (netcode/relevancy · determinism/prediction · reuse/test-validity) → per-finding refutation. Caught **1 MED + 5 LOW**:
|
||||
- **[MED, FIXED] Spitter fired from any range** — the wind-up commit lacked the locked in-band gate, so it would telegraph+fire while still advancing from far (defeating the hold-range question). Fixed: commit only when `sInBand || sCornered` (which also gave the previously-dead `CorneredRange` a real read site). The review's value re-confirmed — a true behavioral deviation 388 green tests + a clean Play had NOT caught.
|
||||
- **[LOW, FIXED]** `EnemyProjectileDamageSystem` missing `!ServerTick.IsValid` guard; bake-time guard against a Charger+Spitter prefab; two test false-greens (Base==0 region assert → fire from Expedition; overstated partition claim → a direct partition-exclusion test).
|
||||
- **[LOW, DOCUMENTED]** global (not per-region) spit soft-cap; co-op camera-punch attribution. Both per the locked design / generous bounds.
|
||||
|
||||
## Gotchas worth remembering
|
||||
- **Director rosters are KIND-INDEXED (index == Kind 0..3), NOT round-robin.** The `WaveDirector` still held the OLD round-robin pool `[Werewolf, WerewolfUndead, Kaiju, ChargerMuscle]` — under MC-2 that silently maps the charger model into the Swarmer slot etc. Always rebuild the roster as `[Grunt,Charger,Spitter,Swarmer]` when adopting `KindForSlot`.
|
||||
- **A `DynamicBuffer` handle is invalidated by any later structural change** — in a test, create the prefab entities BEFORE `AddBuffer`, populate with no `CreateEntity`/`AddComponent` in between (the cluster-test setup bug).
|
||||
- **Reuse rigged variants, don't rebuild rigs.** A plain `AssetDatabase.CopyAsset` duplicate of a working Rukhanka prefab + adding a marker authoring bakes clean and dodges the `EnemyRigTools`/skeleton-rebuild risk entirely.
|
||||
- The fun-gate co-op playtest (Slice 3's too) is still **pending** — the open question is whether the fight is fun, not test counts.
|
||||
+17
-10
@@ -76,17 +76,24 @@ Adopt `WaveSlots`/`KindForSlot` + a 4-entry prefab buffer; **add `WaveDirector.M
|
||||
Pure-math: `WaveSlots`≥1, per-epoch ramps, `KindForSlot` determinism + composition counts, swarmer bucketing, `PackSizeForSlot`≥1, `IsChargerSlot` wrapper keeps 4 legacy assertions; **parity** test (`WaveSlots` reproduces legacy curve); `BandVelocity` (retreat/advance/in-band/Y-flat); `ClusterOffset` determinism. System: `EnemyProjectileMove` integrate+LastStep+range-expiry; `EnemyProjectileDamage` append+at-most-once + **tunnelling regression** (LastStep>radius still hits) + **region-filter** (Expedition spit doesn't damage Base target); Spitter brain (advance/retreat/in-band-commit-then-spawn-with-Region, soft-cap no-burn); cornered fallback; dash-through-spit negation; cluster spawn (PackSize spawned, 1 slot consumed, pack-over-MaxAlive defers); discriminator routing (no double-visit); 4-entry buffer guard. **Play-validation:** no sort-cycle at world-creation; no Burst ICE; Spitter end-to-end (holds range, aim-line telegraph, dodgeable+dash-negatable spit); region correctness (no cross-region see/damage); swarmer reads as a swarm + respects MaxAlive; mix ramp visibly shifts; MC-3 magnitude-scaled punch + predicted tick-rate UNAFFECTED (no timeScale); perf (live spits ≤24, stable frame time under base siege + expedition swarm).
|
||||
|
||||
## Consequences
|
||||
- **Deferred to a later slice:** DOTS `[MaterialProperty]` enemy hit-flash (ShaderGraph `_Flash*` + render-entity mapping); true freeze-frame hit-stop (gated off); Spitter in-band strafe (1b); player-shoots-spit (2b); Swarmer pack-size epoch ramp (field exposed, unwired).
|
||||
- **Deferred to a later slice:** DOTS `[MaterialProperty]` enemy hit-flash (ShaderGraph `_Flash*` + render-entity mapping); true freeze-frame hit-stop (gated off); Spitter in-band strafe (1b); player-shoots-spit (2b); Swarmer pack-size epoch ramp (field exposed, unwired); **per-region** spit soft-cap (v1 is GLOBAL — 24 is generous); a co-op **local-attribution gate** for the player-dealt-hit punch (v1 keys on any enemy-Health edge, so a teammate's hit nudges your camera a touch); **bespoke Spitter/Swarmer art** (v1 reuses the rigged Kaiju + Undead-Werewolf models for distinct silhouettes).
|
||||
- **Open (operator):** the combat fun-gate is a hands-on co-op playtest after build ("play with a friend and not want to stop"); the Slice 3 fun-gate still pending too.
|
||||
- **Status:** reviewed + locked; build IN FLIGHT (see below). Full review (verdicts/blockers/forks) in run transcript `wf_eb115556-8cc`.
|
||||
- **Status:** BUILT + post-impl-reviewed + Play-validated (2026-06-24). Pre-coding review `wf_eb115556-8cc`; post-impl adversarial review `wf_febdcfdb-665` (3 lenses → per-finding refutation; **1 MED + 3 LOW fixed in code, 2 LOW documented**). **388/388 EditMode** + a clean netcode Play smoke (boot, fire, swept-hit, region, server==client). Fun-gate co-op playtest still pending.
|
||||
|
||||
## Build progress (in flight — 2026-06-22)
|
||||
## Build record (complete — 2026-06-24)
|
||||
|
||||
**Done + compiling clean (368/368 EditMode still green, backward-compatible at epoch 1):**
|
||||
- Leaf components: `SpitterState`(+baked `WindupTicks`), `SwarmerTag`, `EnemyProjectile`, `SpitterProjectilePrefab`, `MixBands` (`Simulation/Combat/`).
|
||||
- Math: `EnemyAIMath.{BandVelocity, ClusterOffset}`; `ZoneEnemyMath` Kind consts + `WaveSlots/KindForSlot/PackSizeForSlot` (legacy `WaveSize`/`IsChargerSlot` kept intact for parity).
|
||||
- Systems: `EnemyProjectileMoveSystem` + `EnemyProjectileDamageSystem` (plain server group, LastStep swept, region filter); `EnemyAISystem` Spitter pass + partition guards (`WithNone<LungeState,SpitterState>` Grunt / `WithNone<SpitterState>` Charger) + `m_EnemyProjectiles` cache.
|
||||
- Discriminator: `EnemyTelegraph.IsCharger→byte Kind`; `EnemyAuthoring` bakes Kind from sibling authoring.
|
||||
- Authoring: `SpitterAuthoring`, `SwarmerAuthoring`, `EnemyProjectileAuthoring`; both director components (`ZoneEnemyDirector`, `WaveDirector`) + their authoring gained the mix/cluster fields + (Wave) mandatory `MaxAlive`. Base siege adopts `WaveSlots`/`KindForSlot`/cluster (fork 4a); defaults keep the size curve (≈+1 charger +1 spitter/wave) so the END-game stays bounded.
|
||||
Built through the full `/dots-dev` ladder: **388/388 EditMode green** + a Play smoke (clean two-world boot, no sort-cycle/ICE; Spitter fires → spit sweeps → player damaged → drained, region-correct, **server==client**) + the post-impl adversarial review (`wf_febdcfdb-665`). Two commits: the **MC-2 server spine** (`56cf60cce`) and the **MC-3 + tests + wiring + review-fixes** (one commit, on operator go).
|
||||
|
||||
**Remaining:** MC-3 client juice (FeelConfig fields + `CombatFeedbackSystem` player-hit camera punch + Spitter `Kind==2` aim-line); the additive EditMode tests (per the test plan above); prefab + subscene wiring (Spitter/Swarmer/EnemyProjectile ghosts via the new-ghost recipe, 4-entry director rosters, `SpitterProjectilePrefab` singleton, MixBands/MaxAlive on both directors); then the verify ladder + Play smoke + post-impl review + doc bookend + commit. **Resume from here if compacted.**
|
||||
**Shipped (MC-2 spine):** `SpitterState`, `SwarmerTag`, `EnemyProjectile`, `SpitterProjectilePrefab`, `MixBands`; `EnemyAIMath.{BandVelocity,ClusterOffset}`; `ZoneEnemyMath` Kind consts + `WaveSlots/KindForSlot/PackSizeForSlot` (legacy `WaveSize`/`IsChargerSlot` kept + parity-tested); `EnemyProjectileMove/DamageSystem` (plain server group, LastStep swept, region filter, at-most-once destroy); `EnemyAISystem` Spitter pass + partition guards; `EnemyTelegraph.IsCharger→byte Kind` + `EnemyAuthoring` Kind baking.
|
||||
|
||||
**Shipped (MC-3 juice):** 7 `FeelConfig` fields (+ `ResetDefaults`); magnitude-scaled **player-dealt-hit `PunchFov`** on the enemy-`Health`-decrease edge; the **Spitter `Kind==2` aim-LANE** telegraph (`BuildLaneMesh`; reads baked `SpitterState` client-side, falls back to a fixed length). True freeze-frame + `[MaterialProperty]` enemy hit-flash DEFERRED (gated off / own ShaderGraph slice).
|
||||
|
||||
**Shipped (content):** `SpitterProjectilePrefabAuthoring` (the singleton); both directors carry a 4-entry **kind-indexed** roster `[Grunt,Charger,Spitter,Swarmer]` + mix/MaxAlive config (fork-4a base siege included) + the `SpitterProjectileConfig` singleton. **Real rigged models:** Spitter = `EnemySpitter` (rigged Kaiju, dialed to a ranged poker — HP 28/spd 2.8/cd 66), Swarmer = `EnemySwarmerUndead` (rigged Undead-Werewolf — HP 8/spd 6.5/cd 24, scale 0.6); grunt/charger keep Werewolf/ChargerMuscle. Spit = `EnemySpit` (ownerless interpolated ghost, no `Health`, no collider). All bake clean (zero console errors, Rukhanka rig intact). *(The simple-mesh `Enemy*` prototypes were left untouched; `EnemySwarmer.prefab` reverted.)*
|
||||
|
||||
**Post-impl review fixes (`wf_febdcfdb-665`, all re-validated 388/388 + a re-Play):**
|
||||
- **[MED] in-band fire gate** — the Spitter committed its telegraph from ANY range (would fire while advancing from far, defeating the hold-range question). Fixed: commit only when `sInBand || sCornered` (which gives the once-dead `CorneredRange` a real read site — a Spitter advancing-from-far OR retreating-from-too-close now holds fire and repositions). Guarded by new `Spitter_OutOfBand_DoesNotCommitWindup` + `Spitter_Cornered_CommitsWindupPointBlank`.
|
||||
- **[LOW] `EnemyProjectileDamageSystem`** now early-returns on `!ServerTick.IsValid` (sibling-system parity).
|
||||
- **[LOW] bake-time Charger+Spitter guard** — `EnemyAuthoring.Bake` `Debug.LogError`s a prefab composing BOTH (it would match zero AI passes → never move).
|
||||
- **[LOW] test quality** — the Spitter brain test now fires from **Expedition** (a dropped `Region` copy = 0 = Base would fail it, killing the `Base==0` false-green); the overstated "proves the partition" claim is replaced by a **direct** partition-exclusion test (`Spitter_IsExcludedFromGruntAndChargerPasses`).
|
||||
|
||||
**Accepted (documented, not defects):** the `MaxLiveProjectiles` soft-cap is GLOBAL across regions (24 generous; per-region deferred); the player-dealt-hit punch keys on the enemy-`Health` edge per the locked design (a teammate's hit nudges your camera a touch in co-op — a future local-attribution gate). See the Deferred list above.
|
||||
|
||||
Reference in New Issue
Block a user