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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user