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:
@@ -9,7 +9,7 @@ Multiplayer game on **Unity DOTS (Entities) + Netcode for Entities** — server-
|
||||
- **Size check** — bash: `wc -c CLAUDE.md` · PowerShell: `(Get-Item CLAUDE.md).Length`. Must be `< 40960`.
|
||||
- **Archive, don't delete.** When trimming, append the verbose / least-hot detail to the obsidian reference note `Docs/Vault/_Meta/CLAUDE_Build_Gotchas_Archive.md` under a **new dated heading** (never overwrite an older snapshot), and leave a one-line pointer + the relevant `[[DR-###]]` link here. Design rationale already lives in the per-milestone DRs (`Docs/Vault/07_Sessions/_Decisions/DR-###`).
|
||||
- **Net-zero rule:** every addition is paid for by a condensation elsewhere. Keep only the hottest, highest-recurrence operational rules inline (flag them **★**); depth lives in the archive + DRs.
|
||||
- Condensation history: 06-04 → 06-17 (M1–END-2 long-form + 6.5 stack swap → archive) · 07-04 (base-mining core-loop bullet + stack-history sentence → archive; DR-044 supersedes).
|
||||
- Condensation history: 06-04 → 06-17 (M1–END-2 long-form + 6.5 stack swap → archive) · 07-04 (base-mining core-loop bullet + stack-history sentence → archive; DR-044 supersedes) · 07-04b (EB-1/2 · END-1/2 bullets condensed → DR pointers, paying for the DR-045 combat-demo additions).
|
||||
|
||||
## Stack — Unity 6.5.1 (`6000.5.1f1`, stable) as of 2026-06-27
|
||||
|
||||
@@ -48,7 +48,7 @@ Root namespace: **`ProjectM`**. Code lives under `Assets/_Project/Scripts/` in f
|
||||
Long-form originals + the milestone each came from: `Docs/Vault/_Meta/CLAUDE_Build_Gotchas_Archive.md`. The highest-recurrence hazards are flagged **★**.
|
||||
|
||||
### Assemblies, asmdefs & source-gen
|
||||
- **`Unity.Transforms` must be a DIRECT asmdef reference** for any assembly whose source-gen'd systems touch `LocalTransform`/`LocalToWorld` — transitive visibility compiles hand-written code but the generator emits **CS0246** in `*.g.cs`.
|
||||
- **`Unity.Transforms` must be a DIRECT asmdef reference** for any assembly whose source-gen'd systems touch `LocalTransform`/`LocalToWorld` — transitive visibility compiles hand-written code but the generator emits **CS0246** in `*.g.cs` — SAME failure if the consuming FILE omits `using Unity.Transforms;` (source-gen copies the file's usings into `*.g.cs`; adding a `LocalTransform` query to a system that lacked the using breaks only `*.g.cs`).
|
||||
- **`Unity.Physics` must ALSO be a DIRECT asmdef ref** for any assembly whose source-gen touches `KinematicCharacterBody` (it nests `Unity.Physics.ColliderKey`) → else CS8377/CS0012 in `*.g.cs` (same class as the Transforms rule).
|
||||
- **Authoring asmdefs need `Unity.Entities.Hybrid`** (`Baker<T>`) **+ `Unity.Collections`** (baking source-gen). Never name a nested baker `Baker` (shadows `Baker<T>`) — use `FooBaker`.
|
||||
- **Never name an `IComponentData` `PlayerInput`** and don't `using UnityEngine.InputSystem;` in a file referencing such a component — collides with the managed `UnityEngine.InputSystem.PlayerInput`, generator binds `RefRW<…>` to the class → misleading **CS8377**. Fully-qualify Input System types instead.
|
||||
@@ -88,14 +88,14 @@ Long-form originals + the milestone each came from: `Docs/Vault/_Meta/CLAUDE_Bui
|
||||
- **Build-grid math must be deterministic + integer-stable:** corner-origin, center-returning, **half-open** cell bounds, `math.floor`. Lock `CellSize`/`PlotSize` as a coordinate space once (`BaseGridMath`) — changing them invalidates placed structures.
|
||||
- **`PlacedStructure{[GhostField] byte Type; int2 Cell (server-only); uint NextTick; uint LastProcessedTick}`** on an ownerless interpolated ghost. **Bake the tick fields** (catch-up linchpin); only `Type` replicates (client derives `Cell`). **Occupancy is DERIVED** by scanning live ghosts into a Temp `NativeHashSet<int2>`, never baked. See [[DR-014_M6_Build_Structures_Automation_Foundation]].
|
||||
- **Co-op placement atomicity:** commit `StorageMath.Withdraw` + cell-reservation **in-place in the RPC foreach** (only `Instantiate` via ECB) so two same-tick requests for one cell can't both pass.
|
||||
- **EB-1 machines can die ★ (DR-032; Fabricator parity restored 07-04 — a Health-less machine silently drops OUT of the AI aggro snapshot):** structures bake `Health`(`[GhostField]`)+`DamageEvent`+a `Destructible` tag; `HealthApplyDamageSystem` destroys a `Destructible` at 0 (NOT bare `PlacedStructure`; occupancy auto-frees). `EnemyAISystem` fortress-targets weighted-nearest players+structures (`EnemyAIMath.PickWeightedNearest`; snapshot ABOVE the early-return; `StructureAggroWeight`<1, SQUARED). See [[DR-032_EB1_Machines_Can_Die]].
|
||||
- **EB-2 felt spend ★ (DR-033):** turret ammo = shared `Charge`(`ResourceId` **4**) on the `[GhostField] StorageEntry` ledger. `TurretFireSystem` spends from the ONE `GetSingletonEntity<ResourceLedger>` (NEVER `GetSingleton<StorageEntry>`): afford→fire+cooldown, else **SOFT-FAIL** (no cooldown-burn). `Fabricator.InputFromLedger` reads the ledger **LIVE in-loop** (no hoist → machines split a finite pool). See [[DR-033_EB2_Felt_Spend_Charge_Economy]].
|
||||
- **END-1 losable Core ★ (DR-034):** `CoreIntegrity{[GhostField] int Current,Max; uint OverrunTick}` on the GLOBAL CycleDirector ghost. `CoreDamageSystem`/`CoreRestoreSystem` (server): a Husk near `PlotCenter` drains+despawns; regen ONLY in Calm. SOFT-loss edge IN `CyclePhaseSystem` (sole Phase writer): `Current<=0` in Siege → Calm (**NO** reward; drain+despawn; transient `OverrunTick`, NOT latching). Core = `EnemyAISystem` **FALLBACK** target. SaveData **v4**. See [[DR-034_END1_Losable_Core]].
|
||||
- **END-2 win/lose ★ (DR-036):** terminal run on the CycleDirector — server-only `RunPhase` (writer `GoalReachedSystem`, after CyclePhase) + **REPLICATED `RunOutcome{[GhostField] byte}`** (writer `CyclePhaseSystem`; replicate for the banner, do NOT client-derive). `GoalReached` arms a final siege ×`FinalSiegeMultiplier` at `Charge>=Target` (once)+FinalDefense; latch Victory/Loss+halt; **SiegeTimeout OFF in the final**; SaveData **v5**. See [[DR-036_END2_Final_Siege_Win_Lose]].
|
||||
- **EB-1 machines can die ★ (DR-032):** structures bake `Health`(`[GhostField]`)+`DamageEvent`+`Destructible`; `HealthApplyDamageSystem` destroys a `Destructible` at 0 (occupancy auto-frees). `EnemyAISystem` fortress-targets weighted-nearest players+structures (`EnemyAIMath.PickWeightedNearest`; snapshot ABOVE the early-return; `StructureAggroWeight`<1 SQUARED). ★ a Health-less machine silently drops OUT of the aggro snapshot. See [[DR-032_EB1_Machines_Can_Die]].
|
||||
- **EB-2 felt spend ★ (DR-033):** turret ammo = shared `Charge`(`ResourceId` **4**) on the `[GhostField] StorageEntry` ledger; `TurretFireSystem` spends from the ONE `GetSingletonEntity<ResourceLedger>` (afford→fire+cooldown, else SOFT-FAIL, no cooldown-burn). `Fabricator.InputFromLedger` reads it LIVE in-loop (no hoist → machines split a finite pool). See [[DR-033_EB2_Felt_Spend_Charge_Economy]].
|
||||
- **END-1 losable Core ★ (DR-034):** `CoreIntegrity{[GhostField] int Current,Max; uint OverrunTick}` on the GLOBAL CycleDirector ghost; `CoreDamageSystem`/`CoreRestoreSystem` (server) drain near `PlotCenter`, regen ONLY in Calm. SOFT-loss edge IN `CyclePhaseSystem` (sole Phase writer): `Current<=0` in Siege → Calm (NO reward; transient `OverrunTick`, NOT latching). Core = `EnemyAISystem` FALLBACK target. SaveData **v4**. See [[DR-034_END1_Losable_Core]].
|
||||
- **END-2 win/lose ★ (DR-036):** server-only `RunPhase` (writer `GoalReachedSystem`) + **REPLICATED `RunOutcome{[GhostField] byte}`** (writer `CyclePhaseSystem`; do NOT client-derive the banner). `GoalReached` arms a final siege ×`FinalSiegeMultiplier` at `Charge>=Target` (once)+FinalDefense; latch Victory/Loss+halt; SiegeTimeout OFF in the final; SaveData **v5**. See [[DR-036_END2_Final_Siege_Win_Lose]].
|
||||
- **`GoalProgress{[GhostField] int Charge,Target}`** (the goal meter — ≠ EB-2 `ResourceId.Charge` ammo) rides the CycleDirector ghost. Resource-gated ability tiers/buffs reuse `StatModifier` (`StatRecomputeSystem`→`EffectiveAbilityStats`).
|
||||
- **M7 Automation (server-only) ★:** `Harvester`/`Conveyor` TRIMMED from the palette (code intact), `Fabricator` LIVE (EB-2); plain server group; catch-up `ProductionMath.CyclesDue` (**lower-bound 0**); `RuntimePlacedTag`=player-built. See [[DR-020_M7_Automation_Production_Chains]].
|
||||
- **Harvest routes by node region ★; inventory/equipment PAUSED:** in-run (Expedition) nodes→PERSONAL `InventorySlot` (`[GhostField] OwnerSendType.All`, spill→ledger); `G`=deposit at base. (Base nodes are GONE — DR-044.) Items/equip — **full detail in the gotchas archive (2026-06-12)**. See [[DR-026_Inventory_Equipment_Progression_Foundation]].
|
||||
- **Disk persistence (`SaveData`, single-slot atomic JSON, versioned/additive) ★:** **born-correct load** — `CycleDirectorSpawnSystem` stages `PendingSave` AT SPAWN; `BaseRestoreSystem` replays structures charge-free + REMAINING-tick cooldowns + per-structure HP. `SaveService.Load` = additive floor `[MinLoadableVersion=2, Current]` (old saves load; missing field 0-defaults). See [[DR-019_Frontend_Menu_Settings_Saves_Build]].
|
||||
- **M7 Automation (server-only) ★:** `Harvester`/`Conveyor` palette-TRIMMED (code intact), `Fabricator` LIVE (EB-2); catch-up `ProductionMath.CyclesDue` (**lower-bound 0**); `RuntimePlacedTag`=player-built. See [[DR-020_M7_Automation_Production_Chains]].
|
||||
- **Harvest routes by node region ★; inventory/equipment PAUSED:** in-run nodes→PERSONAL `InventorySlot` (`[GhostField] OwnerSendType.All`, spill→ledger); `G`=deposit at base (base nodes GONE — DR-044). Items/equip detail → gotchas archive (2026-06-12). See [[DR-026_Inventory_Equipment_Progression_Foundation]].
|
||||
- **Disk persistence (`SaveData`, single-slot atomic JSON, versioned/additive) ★:** **born-correct load** — `CycleDirectorSpawnSystem` stages `PendingSave` AT SPAWN; `BaseRestoreSystem` replays structures charge-free + REMAINING-tick cooldowns + HP. `SaveService.Load` = additive floor `[MinLoadableVersion=2, Current]` (old saves load; missing field 0-defaults). See [[DR-019_Frontend_Menu_Settings_Saves_Build]].
|
||||
|
||||
### Presentation / juice / VFX
|
||||
- **All juice/HUD = client-only observe-only `SystemBase` in `PresentationSystemGroup`** (once/frame, no rollback double-fire), never mutates the sim. Read ECS via `SystemAPI.Query` + `EntityManager.CompleteDependencyBeforeRO<T>()` — NOT MonoBehaviour `LateUpdate` (job-safety throw). `Entity` = a stable client dict key per ghost lifetime — **prune the cache each frame** (a pruned ghost = a kill/loss → death VFX); **never `DestroyEntity` a ghost client-side** (`GhostDespawnSystem` owns despawn). Hit-stop = camera punch, **never `Time.timeScale`**.
|
||||
@@ -141,7 +141,7 @@ Full rationale: [[DR-022_Animation_Pipeline_Rukhanka_Synty]] · [[DR-023_Enemy_A
|
||||
|
||||
- `ProjectM.Simulation.GameBootstrap : ClientServerBootstrap` overrides `Initialize` with `AutoConnectPort = 0` (M4 — listen/connect is explicit via the `ConnectionConfig` singleton + per-world ConnectionControlSystems). **Editor default = instant-into-game + MPPM** (creates `ServerWorld` (`WorldFlags.GameServer`) + `ClientWorld` (`WorldFlags.GameClient`)); the `ProjectM/Boot Into Menu (Editor)` EditorPref flips the MAIN editor to the frontend path. **Player builds boot the UITK frontend menu** (`return false` → one menu world, no netcode worlds until a menu choice). See [[DR-019_Frontend_Menu_Settings_Saves_Build]].
|
||||
- **Scenes:** `Assets/Scenes/MainMenu.unity` (build index 0) boots the UITK frontend (menu world only); `Assets/Scenes/Game.unity` (index 1) holds gameplay with `Assets/_Project/Subscenes/Gameplay.unity` wired in as the baked subscene (GameObject `GameplaySubScene`). `SampleScene`/`DevSandbox` are kept as reference/dev scenes. The on-demand lifecycle (`WorldLauncher`/`SessionRunner`/`MainMenuController`) creates the right worlds per menu choice (Single/Host/Join), THEN `LoadScene(Game)` (subscene-streaming rule above).
|
||||
- **Core loop = ready-check multi-room RUNS ★ (DR-044; DR-031/042 base-mining text archived 07-04):** base = pure SPEND hub (no base nodes; `Tuning.StartingOre` grubstake). `RunDirectorSystem` = sole `RunInfo` writer (`[UpdateBefore(CyclePhaseSystem)]`; NOTHING else in the room chain touches CyclePhase); banks Charge+retaliation once-per-`RunEpoch` on boss-clear ONLY; strips the boon band + every `BoonOffer` on exit; teleports ONLY launch-stamped **`RunParticipant`s** (released on Returning — dead-respawned re-conscript, late joiners stay home). Rooms spawn per `RoomEpoch` at ping-pong sub-slots, `RoomTag`-torn-down; first slot waits `Tuning.RoomEntryGraceTicks`. ★ a serialized prefab bool ignores the C# initializer — flip `CycleDirector.prefab`. See [[DR-044_Expedition_Redesign_Shipped_Demo_Polish]].
|
||||
- **Core loop = ready-check multi-room RUNS ★ (DR-044; DR-031/042 base-mining text archived 07-04):** base = pure SPEND hub (no base nodes; `Tuning.StartingOre` grubstake). `RunDirectorSystem` = sole `RunInfo` writer (`[UpdateBefore(CyclePhaseSystem)]`; NOTHING else in the room chain touches CyclePhase); banks Charge+retaliation once-per-`RunEpoch` on boss-clear ONLY; strips the boon band + every `BoonOffer` on exit; teleports ONLY launch-stamped **`RunParticipant`s** (released on Returning — dead-respawned re-conscript, late joiners stay home). Rooms spawn per `RoomEpoch` at ping-pong sub-slots, `RoomTag`-torn-down; first slot waits `Tuning.RoomEntryGraceTicks`. ★ a serialized prefab bool ignores the C# initializer — flip `CycleDirector.prefab`. **Demo (DR-045): `GoalProgress.Target`=2; Boss = scaled Charger + server-only `BossState` kit (`BossAISystem` sole mover; radial SLAM via `AttackWindup`; phase-2 summon; knockback-immune; excluded from the Charger pass; client bar = `Max×BossHealthMultiplier`, `Health.Max` unreplicated). Grunt windup commits ~last 30%; Charger stagger roots.** See [[DR-044_Expedition_Redesign_Shipped_Demo_Polish]] · [[DR-045_Combat_Demo_Feel_Boss_Fight]].
|
||||
|
||||
## DOTS / ECS conventions (authoritative summary)
|
||||
|
||||
|
||||
@@ -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).
|
||||
Reference in New Issue
Block a user