Docs: DR-045 combat demo pass + session log + CLAUDE.md gotchas

DR-045 (real boss, GoalTarget=2, readable-but-fair) + session log. CLAUDE.md:
core-loop demo note (GoalTarget=2, boss kit), source-gen file-using gotcha,
EB-1/2/END-1/2 bullets condensed to DR pointers to stay under the 40KB budget.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-05 20:21:51 -07:00
parent ab657fa578
commit 35357faa7a
3 changed files with 216 additions and 10 deletions
@@ -0,0 +1,74 @@
---
title: Combat Demo-Readiness Pass — Real Boss Fight, Readable-but-Fair Threat, Onboarding + Loop Flow
date: 2026-07-04
tags: [session, combat, boss, demo, feel, onboarding, netcode, dots-dev, ultracode]
permalink: gamevault/07-sessions/2026/2026-07-04-combat-demo-feel-pass
---
# Combat Demo-Readiness Pass (/dots-dev, ultracode)
Operator brief: *"comprehensive pass of combat to make it demo-ready — adjust/improve/add/tune, nothing off limits, fun
& fluid; also fix onboarding + the overall game loop to make logical sense for a demo."* Decisions + full change list:
[[DR-045_Combat_Demo_Feel_Boss_Fight]]. (Second session of the day — see also [[2026-07-04_Demo_Polish_Pass]].)
## How it went
1. **Ground fan-out** (`wf_3bb890df-74e`, 5 read-only lenses; 4 done, the tuning lens quota-died but was fully covered
by the enemy + player lenses' file:line tables). Surfaced the headline bugs: the boss health bar pegs full for 7/8 of
the fight (`Health.Max` not replicated, boss ×8s both Current+Max), boss spawns on the party's heads, boss hitbox
unscaled, solo-melee **perma-stunlocks any enemy incl. the boss** (every hit zeroes windup), grunts can't catch a
moving player (speed 4.2 < 6 + windup cancels on step-back), the Charger "stagger" doesn't stagger, the onboarding
Fabricator beat is unaffordable on lap 1 (50 Ore 40 Turret = 10 < 30) so it silently times out, the crystal-mining
lesson never shows, and Return false-completes on death.
2. **Forks → operator** (AskUserQuestion): all three recommended — real boss, GoalTarget=2, readable-but-fair.
3. **Pre-code adversarial review** (`wf_1dcddfa5-c0e`, 3 lenses + critics). Two confirmed (cone knockback must be
HasComponent-guarded + inside `isServer`; Return must NOT gate on the 1-tick `Returning` edge). The critic swarm hit
the Fable/session limit mid-run — I self-adjudicated all 23 candidates against the code (matched the 2 confirmed) and
folded the fixes in. Key design simplification it drove: **drop the boss lunge** → seek + slam + summon only, which
removed the whole `LungeState`/`IsLunging` double-writer entanglement.
4. **Serial MCP implementation** (~24 edits + 4 new files) in compile-checked batches: server/sim (BossState, BossAISystem,
EnemyMoveUtil, ZoneEnemySpawnUtil, EnemyAISystem, RoomEnemyDirectorSystem, knockback immunity ×3, DashSystem, Tuning,
GoalTarget) → client (HudSystem, CombatFeedbackSystem, EnemyAnimationDriveSystem, EnemyHitFlashSystem, FeelConfig,
PrototypeCameraRig) → onboarding (OnboardingStepMath, OnboardingSystem, OnboardingState, SettingsScreen) → prefab
(grunt speed). Zero new `[GhostField]`; the only re-bakes are prefab/const VALUE edits (no ghost-hash change).
5. **Verify**: 456/456 EditMode (2 onboarding tests updated to the new D2/D3 contracts). Live Play smoke — forced a Boss
room and confirmed the boss spawns correct (360/360 HP, 1.28 hitbox, 1.6 scale, BossState), seeks the player, and its
slam fires; `GoalProgress.Target=2` server==client; console clean.
6. **Post-impl diff review** (`wf_71f634ea-3fa`, clean run, 0 failures): 4 candidates, only 1 confirmed — cosmetic
orphaned doc-comments + a stray `;` from the anchor-edit tooling (fixed). No functional/netcode/determinism/
simplification regressions survived. Re-ran tests → still 456/456.
## Gotchas worth remembering
- **The SystemAPI-source-generator copies the source file's `using` directives into `*.g.cs`.** Adding a
`RefRO<LocalTransform>` query to `HudSystem` (which lacked `using Unity.Transforms;`) compiled the hand-written call but
emitted CS0246 for `LocalTransform` only in the generated file. Fix = add the using to the FILE (the asmdef already
referenced Unity.Transforms directly — this is the file-using flavor of the known asmdef-direct-ref gotcha).
- **`script_apply_edits` anchors are REGEX** — unescaped `()` in an anchor silently matches zero (empty group), so a
whole multi-op call fails "anchor not found." Escape `\(` `\)` (and `.` where it matters). `anchor_insert position:after`
on a code line that has a trailing `// comment` lands the inserted text BETWEEN the code and its comment → orphaned
comments (the post-impl review caught 3). Prefer anchoring on the comment-free token or clean up after.
- **A runtime-added replicated component does NOT replicate** (ghost composition is baked) — so a server-only boss
discriminator (`BossState`, runtime `AddComponent`) is the right shape; client boss identity comes from the
already-replicated `RunInfo.CurrentRoomType==Boss` + baked `EnemyTelegraph.Kind` + position, never a new ghost field.
- **`Health.Max` is not a `[GhostField]`** — a server-side `hp.Max *= N` never reaches the client. Reconstruct
client-side (`bakedMax × multiplier`) rather than adding a ghost field (kept the whole pass re-bake-free).
- **A workflow's critic swarm can die on the session/Fable limit mid-run** — the completed lenses' findings are still in
`journal.jsonl`; self-adjudicate the un-critiqued candidates against the code rather than treating the empty result as
a clean pass.
- **`GoalProgress.Target` is a baker LITERAL** (not a serialized authoring field) so editing the const + recompile
re-bakes it (no "serialized value wins over the initializer" trap); `CycleDirectorSpawnSystem` clamps a saved Target to
the baked value, so old saves roll forward to 2 cleanly. Verified `Target=2` on both worlds at Play.
## Validation
456/456 EditMode · live Play netcode smoke (boss spawn/seek/slam + GoalTarget=2 server==client + console clean) · two
adversarial reviews (pre-code folded, post-impl clean bar 1 cosmetic finding fixed). L3 visual/fun-gate (the boss fight
feel, the onboarding first-lap) is the operator's playtest.
## Next-session intent
Operator fun-gate: play the boss fight (slam dodge window, phase-two adds, is it a memorable climax?) + the first-lap
onboarding (does the Fabricator beat land now?) + a 2-run demo timing. Live-tune `BossSlam*/BossPhase2*/BossSummon*`
(Tuning consts, recompile) + the finisher hit-stop frames (FeelConfig, live). Then the standalone 2-instance LAN smoke
(DR-044 §5, still open).
@@ -0,0 +1,132 @@
---
id: DR-045
title: Combat Demo-Readiness Pass — Real Boss Fight, Readable-but-Fair Threat, 2-Run Demo
date: 2026-07-04
status: locked
tags:
- decision
- combat
- boss
- demo
- feel
- onboarding
- netcode
supersedes: DR-044 §5 GoalProgress.Target demo-pacing (4→2); extends the combat-feel line of DR-041
permalink: gamevault/07-sessions/decisions/dr-045-combat-demo-feel-boss-fight
---
# DR-045 — Combat Demo-Readiness Pass (Real Boss + Readable-but-Fair + 2-Run Demo)
Operator brief (ultracode): *"comprehensive pass of combat to make it demo-ready — adjust/improve/add/tune, nothing off
limits, fun & fluid; also fix onboarding + the overall game loop to make logical sense for a demo."* Full session log:
[[2026-07-04_Combat_Demo_Feel_Pass]]. Demo = hands-off public co-op play (DR-044 definition).
## 1. Operator-locked forks (AskUserQuestion)
- **Boss = a REAL fight** (not the bugged stat-sack): fix all bugs AND give the boss a signature kit — a telegraphed
**radial SLAM** + a **phase-two** (≤50% HP) speed-up that **summons swarmer adds** + a correct health bar + a scaled
hitbox + knockback-resistance + an arena-anchored spawn.
- **Demo length = `GoalProgress.Target` 2** (was 4) — ~10-20 min hands-off: two full expeditions → the final siege → win.
- **Difficulty = readable-but-fair** — enemies actually pressure a moving player, but i-frames / 3-s respawn / soft-loss
stay generous so a casual public player still progresses.
## 2. The boss (server-only, ZERO new ghost surface)
Design principle held all pass: **no new `[GhostField]`, no ghost-hash change, no re-bake of a ghost's serializer.**
- **`BossState`** (`Simulation/Combat/BossState.cs`) — server-only working state `{byte Phase; uint SlamReadyTick,
SummonReadyTick}`, added at spawn via ECB (a runtime-added replicated component would NOT replicate anyway; this is
deliberately server-only, like `LungeState`/`KnockbackState`). PRESENCE is the boss discriminator.
- **`BossAISystem`** (`Server/Combat/`, server-only, plain `SimulationSystemGroup`, `[UpdateAfter(EnemyAISystem)]`) = the
SOLE mover/attacker of `.WithAll<EnemyTag, BossState>()`. Seek nearest living **expedition** player → telegraphed
radial SLAM (the client cue rides the already-replicated `AttackWindup` [GhostField]; on elapse, AoE `DamageEvent` to
players within `Tuning.BossSlamRadius`, no player-knockback) → phase two (<50% Current/Max): `×BossPhase2SpeedMult`
speed, tighter slam cooldown, and periodic swarmer summons (capped `< BossSummonLiveCap`). The boss does **not** lunge
→ its baked `LungeState` stays idle → EnemyAISystem's `IsLunging` derive sees `UntilTick==0` → bit off (single writer,
harmless). All ticks via `TickUtil.NonZero` + `NetworkTick`.
- **`EnemyAISystem` Charger MOVE pass now `.WithNone<BossState>()`** — so exactly one system writes the boss's
Position/Rotation/AttackWindup (the sole-writer invariant). It is already excluded from the Grunt pass (has LungeState)
and the Spitter pass (no SpitterState).
- **`RoomEnemyDirectorSystem`** boss branch: spawn at `origin + (0,0,12)` (**across the arena, not on the party's
landing** — the old on-the-head spawn), tag `BossState{Phase=1}`, scale `Health` (×`BossHealthMultiplier`=8),
`HitRadius` (×`BossScaleMultiplier`=1.6, so player hits register on the big model) and `EnemyStats.AttackRange` (×1.6).
- **Boss knockback-immune** — the THREE `KnockbackState` stamp sites (`ProjectileDamageSystem`, `MeleeComboSystem`,
`AbilityFireSystem` cone) skip a `BossState` target (`HasComponent` guarded — dummies lack `KnockbackState` → an
unguarded stamp throws at ECB playback). Kills the solo-melee perma-stunlock that reduced the boss to free wailing.
- **Client boss bar (A6, `HudSystem`)** — `Health.Max` is NOT replicated, so the client reconstructs the true max as
`bakedMax × Tuning.BossHealthMultiplier` over the highest-`Max` enemy that is `EnemyTelegraph.Kind==KindCharger` AND at
expedition-region X (`pos.x > ExpeditionRegionXMin`). The Kind + position filter excludes phase-two summoned swarmers
AND a base-region siege enemy a dead teammate can see. Fixes the bar that pegged full for 7/8 of the fight.
- **Client slam telegraph (A7, `CombatFeedbackSystem`)** — in a Boss room the Charger-kind enemy renders a FULL ground
RING at `BossSlamRadius` (not a forward melee wedge that would lie about a radial AoE), ramping over the boss's real
slam wind-up. `EnemyAnimationDriveSystem.IsAttacking = windup || isLunging` (the committed lunge that zeroes
AttackWindup now still animates as an attack — fixes the Charger/boss lunge-anim gap).
**Reviewed edge (post-impl, refuted-but-noted):** the boss carries `ZoneEnemyTag` so it counts in `liveZone` → phase-two
summons top out at `BossSummonLiveCap-1` adds *plus* the boss. Intended: the cap bounds *live zone enemies*, boss
included, so a boss room never floods past readable.
## 3. Readable-but-fair enemy threat
- **Grunt windup COMMITS in its final ~30%** (`EnemyAISystem`): leaving range / knockback no longer cancels a nearly-done
swing (a last-instant step-back won't save you — dash i-frames or an early exit will); a committed swing the player
dodged out of **WHIFFS** at elapse (still burns cooldown) = the punish window, mirroring the Charger. Grunts (speed now
**5.2**, still < player 6) can finally land on a lingering player instead of being trivially kited. Remaining-ticks via
`NetworkTick.TicksSince`, never raw uint.
- **Charger stagger now ROOTS the Charger** (skip seek while `StaggerUntilTick` active) — the advertised whiff-punish
window is finally legible (the player sees it stop). Was: only an attack-cooldown, so it kept seeking at full speed.
- **Swarmer telegraph honest** — the baked `EnemyTelegraph.WindupTicks` was `6` while the server windup is
`Tuning.AttackWindupTicks`(22) → the danger cone snapped from 0→full. Now matches (`EnemyAuthoring`).
## 4. General combat feel
- **Dash toward MOVEMENT when moving, else facing** (`DashSystem`) — a KBM "panic dash away" while aiming at the threat
now dashes away, not into it. Pure fn of replicated input → idempotent under rollback.
- **Player body hit-flash + hurt color** — generalized `EnemyHitFlashSystem` to `WithAny<EnemyTag, PlayerTag>` with a
per-entry `IsPlayer` flag → distinct `FeelConfig.PlayerHurtFlashColor` (red) vs the enemy white overdrive; edge
threshold raised 0.001→0.5 so predicted-health reconciliation jitter can't spuriously flash the local player.
- **Warrior cone parity** — the cone now stamps `KnockbackState` like the melee cleave (server, guarded), and the client
suppresses the projectile muzzle-flash/zap for a Cone-archetype shot (the dedicated cone arc is its cue).
- **True hit-stop, minimal + conservative (C4)** — `PrototypeCameraRig.Hold(frames)` freezes the follow-cam for
`HitStopMaxFrames`(2 ≈33 ms) on the **combo finisher only** (behind `FeelConfig.HitStopFreezeEnabled`, flipped true).
Finisher-only avoids per-kill horde stutter; presentation-only, **never `Time.timeScale`** (DR-041 deferral honored as
a bounded, disable-able implementation).
- **C5 retaliation-siege scaling CUT** — the infra exists stubbed at ×0, and at GoalTarget=2 there is ~1 retaliation, so
the curve is unfelt; not worth the surface for the demo.
## 5. Onboarding + loop demo logic
- **Lap-1 Fabricator affordable** — `Tuning.StartingOre` 50→**90** so a first-timer can build a Turret (40) AND a
Fabricator (30) and SEE turrets get fed (the taught lesson). The old dead "build a Fabricator" beat silently timed out.
- **Crystal-mining lesson shows IN the room (D2)** — `Rooms` satisfies on `OnExpedition && StepElapsed >= RoomsSeconds`
(7 s) instead of instantly on teleport, so the prompt + ▶ node pointer actually display in the first room.
- **Return no longer false-completes on death (D3)** — satisfies on `(WasOnExpedition && !OnExpedition) || StepElapsed >=
ReturnMaxSeconds`; a `_wasOnExpedition` latch (mirrors `_sawSiege`) prevents a start-at-base Return from instantly
satisfying, and the soft-timeout guarantees no softlock. **Never gate on the 1-tick `Returning` edge** (a dropped
snapshot under tick-batching would stall the tutorial — pre-code review, confirmed).
- **HUD never hides critical cues during combat (D4)** — `OnboardingState.SuppressLocationLine` (early base steps only)
replaces the blanket blank, so "TURRETS OUT OF AMMO" / room / siege cues survive during the Defend/Rooms steps.
- **Welcome not skipped by movement (D5)** — dismisses only on an explicit confirm (Space/Enter/click/South), so the
win-condition strip is read.
- **READY panel reflects final defense (D6)** — gated `!goalFull` (replicated `GoalProgress.Charge >= Target`, since
`RunPhase` is server-only); a "GOAL REACHED — FINAL DEFENSE INCOMING" line replaces the inert READY panel that silently
refused to launch.
- **Dev "Force Each Launch" hidden from public Settings (D7)** — `#if UNITY_EDITOR` so a demo player can't lock into
permanent tutorial. **"Ammo" wording (D8)** distinguishes turret ammo from the GOAL win-meter. Copy updated for 2 runs
+ "ALPHA HUSK" (D9).
## 6. Validation
456/456 EditMode (two onboarding tests updated to the D2/D3 contracts). Live Play netcode smoke: `GoalProgress.Target=2`
**server==client**; forced a Boss room → boss spawned with Health 360/360, HitRadius 1.28, AttackRange 2.72, Scale 1.6,
`BossState`, seeked the player (moved toward it), and its **slam fired** (`SlamReadyTick` set); console clean (only the
pre-existing editor tick-batching warnings). Pre-code adversarial review (`wf_1dcddfa5-c0e`, 3 lenses + critics; 2
confirmed folded into the plan, 21 self-adjudicated after the critics hit the session limit). Post-impl diff review
(`wf_71f634ea-3fa`, clean run, 0 failures): 4 candidates, 1 confirmed (cosmetic doc-comment/`;` artifacts) — fixed;
3 refuted (boss self-counts in its own summon cap = intended; Rooms no-timeout = reachable-by-design; cap doc wording).
## 7. Open / operator-side (unchanged fun-gates)
Boss-fight fun-gate + the onboarding fun-gate are still the operator's real playtest (the visual/feel L3). Standalone
2-instance LAN smoke (DR-044 §5) still pending. Tunables to feel-check live: `BossSlam*`, `BossPhase2*`, `BossSummon*`
(Tuning consts — recompile per tweak), grunt speed, the finisher hit-stop frames (`FeelConfig`, live).