diff --git a/CLAUDE.md b/CLAUDE.md index 022246b67..aae4b11d8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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`) **+ `Unity.Collections`** (baking source-gen). Never name a nested baker `Baker` (shadows `Baker`) — 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`, 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` (NEVER `GetSingleton`): 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` (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()` — 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) diff --git a/Docs/Vault/07_Sessions/2026/2026-07-04_Combat_Demo_Feel_Pass.md b/Docs/Vault/07_Sessions/2026/2026-07-04_Combat_Demo_Feel_Pass.md new file mode 100644 index 000000000..d3b9cf6aa --- /dev/null +++ b/Docs/Vault/07_Sessions/2026/2026-07-04_Combat_Demo_Feel_Pass.md @@ -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` 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). diff --git a/Docs/Vault/07_Sessions/_Decisions/DR-045_Combat_Demo_Feel_Boss_Fight.md b/Docs/Vault/07_Sessions/_Decisions/DR-045_Combat_Demo_Feel_Boss_Fight.md new file mode 100644 index 000000000..8fc9ffa34 --- /dev/null +++ b/Docs/Vault/07_Sessions/_Decisions/DR-045_Combat_Demo_Feel_Boss_Fight.md @@ -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()`. 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()`** — 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` 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).