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,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).