Docs: DR-026 inventory/equipment/progression foundation + session log
DR-026 records the architecture (data-driven catalog + replicated buffers + reuse of the StatModifier/RPC machinery), the Phase 0-4 roadmap, the 7 validated decisions, and the deliberate gameplay choices (personal-harvest pivot, automation asymmetry, session-only inventory). Adds the Build-section inventory pointer to CLAUDE.md net-zero (39923 bytes, >1KB headroom) by condensing the persistence/world/HUD bullets; trimmed wording archived to the gotchas archive. 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: 2026-06-04 (first pass, M1–M6 long-form → archive) · 2026-06-07 (second pass, M7+/HUD/animation tightened; full pre-trim snapshot saved at the archive's tail).
|
||||
- Condensation history: 2026-06-04 (first pass, M1–M6 long-form → archive) · 2026-06-07 (second pass, M7+/HUD/animation tightened) · 2026-06-08 (inventory pointer net-zero; persistence + world-collision-rim detail → archive).
|
||||
|
||||
## Stack — Unity 6.4.7 (`6000.4.7f1`, stable) as of 2026-05-30
|
||||
|
||||
@@ -90,24 +90,25 @@ Long-form originals + the milestone each came from: `Docs/Vault/_Meta/CLAUDE_Bui
|
||||
- **Co-op placement atomicity:** commit the `StorageMath.Withdraw` + cell-reservation **in-place inside the RPC foreach** (only `Instantiate` goes through the ECB) so two same-tick requests for one cell can't both pass.
|
||||
- **Buildable turret = hitscan = reversed `EnemyAISystem`:** nearest living Husk in-region within Range, on `NextTick` cooldown append a direct `DamageEvent{Damage, SourceNetworkId=-1}` → reuses `HealthApplyDamageSystem`. No projectile → no tunnelling, no team model.
|
||||
- **Resource-gated ability tiers reuse `StatModifier`** — grow ONE `StatModifier{Target=Damage, Op=PercentAdd, SourceId=<sentinel>}` (replace-by-SourceId → bounded buffer); `StatRecomputeSystem` folds it into `EffectiveAbilityStats` on both worlds. `GoalProgress{[GhostField] int Charge, Target}` rides the global CycleDirector ghost.
|
||||
- **M7 Automation (server-only, never predicted) ★:** `Harvester`/`Conveyor`/`Fabricator` are buildable machines on the `PlacedStructure` ghost storing `PeriodTicks` + **server-only** `MachineInput`/`MachineOutput` buffers (NOT `[GhostField]`). Production runs in the plain server group `[UpdateAfter(PredictedSimulationSystemGroup)]` (Harvester→Conveyor→Fabricator), replicating only via the global ledger + `PlacedStructure`. Deterministic catch-up via `ProductionMath.CyclesDue` (**lower-bound 0, never 1**; period-0 guarded). Byte-only pure math is EditMode-tested; `ConveyorMath` is order-independent (stable-sort by `CellKey` → at-most-one claim → losers stall). `RuntimePlacedTag` marks player-built machines for the save-scan. See [[DR-020_M7_Automation_Production_Chains]].
|
||||
- **Disk persistence (`SaveData`, single-slot atomic JSON at `persistentDataPath`) ★:** versioned (null on bad version), schema **additive**. **Born-correct load** — `CycleDirectorSpawnSystem` applies a staged `PendingSave` AT SPAWN so the director ghost never replicates a default first. Autosave on the Siege→Calm checkpoint + quit-to-menu (`WorldLauncher.TrySaveFromServer`, host-only); `BaseRestoreSystem` replays saved structures **charge-free** with epoch-independent REMAINING-tick cooldowns; shared `SaveStructureScan.Collect` (one scan path). See [[DR-019_Frontend_Menu_Settings_Saves_Build]].
|
||||
- **M7 Automation (server-only, never predicted) ★:** `Harvester`/`Conveyor`/`Fabricator` are buildable machines on the `PlacedStructure` ghost storing `PeriodTicks` + **server-only** `MachineInput`/`MachineOutput` buffers (NOT `[GhostField]`). Production runs in the plain server group `[UpdateAfter(PredictedSimulationSystemGroup)]` (Harvester→Conveyor→Fabricator), replicating only via the global ledger + `PlacedStructure`. Deterministic catch-up via `ProductionMath.CyclesDue` (**lower-bound 0, never 1**; period-0 guarded). Byte-only EditMode-tested math; `ConveyorMath` order-independent (`CellKey` stable-sort → at-most-one claim → losers stall). `RuntimePlacedTag` = player-built (save-scan). See [[DR-020_M7_Automation_Production_Chains]].
|
||||
- **Per-player inventory + data-driven items ★:** harvest → the firing player's PERSONAL `InventorySlot` (`[GhostField]` `OwnerSendType.All`, a `StatModifier` twin; owner read via an OPTIONAL `ComponentLookup<GhostOwner>` in `ResourceHarvestSystem`, remainder/un-owned → the ledger). Items = an `ItemDatabase` blob (`AbilityDatabase` twin; `ushort ItemId` subsumes `ResourceId`; ID-keyed; `byte Category`/`Tier`). Deposit personal→ledger via the `G`-key `InventoryDepositRequest` RPC (the build economy spends the ledger). `InventoryMath`(stack/slot-cap) ≠ `StorageMath`. Session-only (no SaveData bump). See [[DR-026_Inventory_Equipment_Progression_Foundation]].
|
||||
- **Disk persistence (`SaveData`, single-slot atomic JSON, versioned/additive) ★:** **born-correct load** (`CycleDirectorSpawnSystem` applies a staged `PendingSave` AT SPAWN so the director never replicates a default first); host-only autosave on Siege→Calm + quit; `BaseRestoreSystem` replays structures **charge-free** with REMAINING-tick cooldowns (one `SaveStructureScan.Collect` path). See [[DR-019_Frontend_Menu_Settings_Saves_Build]].
|
||||
|
||||
### Presentation / juice / VFX
|
||||
- **All juice/HUD = client-only managed `SystemBase` in `PresentationSystemGroup`** (once/frame, no rollback double-fire) that OBSERVES replicated state, never mutates the sim. Read ECS via `SystemAPI.Query` + `EntityManager.CompleteDependencyBeforeRO<T>()` — NOT a MonoBehaviour `LateUpdate` (job-safety throw). `Entity` is a stable client dict key for a ghost's lifetime — **prune the cache each frame** (a pruned enemy = a kill → death VFX); **never `DestroyEntity` a ghost from the client** (`GhostDespawnSystem` owns despawn). Hit-stop = a camera punch, **never `Time.timeScale`** (corrupts the sim).
|
||||
- **Asset-free presentation:** procedural `AudioClip.Create` SFX; runtime `ParticleSystem` pool (Sprites/Default + HDR start color); code-built **UI Toolkit** HUD/menus. Edit a prefab asset's component in code via `PrefabUtility.LoadPrefabContents` → modify → **`SaveAsPrefabAsset(root, path)`** → `UnloadPrefabContents`. Watch **shared-material bleed** when re-tinting. ACES tonemapping needs URP color grading mode = HDR (`m_ColorGradingMode=1`).
|
||||
- **Prototype glue lives in `ProjectM.Client` as MonoBehaviours:** `PrototypeCameraRig` (player-following ARPG cam), `VFXConfig` (static `Instance` + prefab fields bridging authored VFX to `CombatFeedbackSystem`; keep a procedural fallback). A **static presentation bridge must reset on play-enter** via `[RuntimeInitializeOnLoadMethod(SubsystemRegistration)]` (statics survive fast-enter-playmode reloads → stale flash).
|
||||
- **UITK HUD + menus ★:** `MenuUi` owns the shared palette + element factories + `PanelSettings`/`EventSystem` plumbing + the canonical `Round`/`Border` helpers; `HudUi` is a thin extension. `HudSystem` is a `PresentationSystemGroup` observe-only `SystemBase` owning a runtime `UIDocument` (`sortingOrder 50`, behind the pause overlay's 100); builds the tree on the first frame `rootVisualElement != null`, root `pickingMode = Ignore` so the HUD never eats world clicks (only palette buttons opt back in). **Runtime UITK needs a `PanelSettings` WITH a `themeStyleSheet` AND an `EventSystem` + `InputSystemUIInputModule`** or buttons are silently dead. The **build palette** (lazy-built from the client `StructureCatalog`) drives click-to-place: green/red ground-ghost preview (`BuildPreviewMath`, the client mirror of the server check), left-click → `BuildPlaceRequest` RPC, right-click/Esc cancel, `[`/`]`/R rotate, `Fire` suppressed in build mode. See [[DR-021_HUD_UITK_BuildPalette]].
|
||||
- **Synty HUD skin via a build-safe `HudTheme` ★ (DR-024):** Synty sprites/fonts under `Assets/Synty/…` (NOT Resources) → a runtime name-string `Resources.Load` is **build-stripped**; use a curated `HudTheme : ScriptableObject` (`Assets/_Project/Resources/HudTheme.asset`) holding **serialized** refs, loaded null-safe via `HudTheme.Get()` (every consumer falls back to flat on a null ref). `unityBackgroundImageTintColor` MULTIPLIES (tint white skins); fonts = cached SDF, reset on `SubsystemRegistration`. Don't set `unitySlice*` on Synty 9-slice frame/bar sprites (per-element ERROR; DO set it for border-0 sprites). Some Synty sprites import as **Multiple** → `LoadAssetAtPath<Sprite>` null; verify. See [[DR-024_HUD_Synty_Skin_Theme]].
|
||||
- **UITK HUD + menus ★:** `MenuUi` owns the shared palette + factories + `PanelSettings`/`EventSystem` plumbing + `Round`/`Border` helpers; `HudUi`/`HudSystem` extend it. `HudSystem` = a `PresentationSystemGroup` observe-only `SystemBase` owning a runtime `UIDocument` (`sortingOrder 50`); builds the tree on the first frame `rootVisualElement != null`, root `pickingMode = Ignore` (only palette buttons opt back in). **Runtime UITK needs a `PanelSettings` WITH a `themeStyleSheet` AND an `EventSystem` + `InputSystemUIInputModule`** or buttons are silently dead. The **build palette** (lazy from the client `StructureCatalog`) drives click-to-place: green/red `BuildPreviewMath` ground-ghost, left-click → `BuildPlaceRequest` RPC, right-click/Esc cancel, `[`/`]`/R rotate, `Fire` suppressed. See [[DR-021_HUD_UITK_BuildPalette]].
|
||||
- **Synty HUD skin via a build-safe `HudTheme` ★ (DR-024):** Synty sprites/fonts under `Assets/Synty/…` → a runtime name-string `Resources.Load` is **build-stripped**; use a curated `HudTheme : ScriptableObject` (`Assets/_Project/Resources/HudTheme.asset`) of **serialized** refs, loaded null-safe via `HudTheme.Get()` (consumers fall back to flat on null). `unityBackgroundImageTintColor` MULTIPLIES (tint white skins). Don't set `unitySlice*` on 9-slice frame/bar sprites (per-element ERROR; DO for border-0). Some Synty sprites import as **Multiple** → `LoadAssetAtPath<Sprite>` null; verify. See [[DR-024_HUD_Synty_Skin_Theme]].
|
||||
|
||||
### Art import (HDRP store packs → URP)
|
||||
- BefourStudios art is **HDRP-authored** → magenta under URP 17.4 + Entities Graphics. **Convert, don't switch pipelines** (HDRP breaks EG): re-author to stock URP/Lit via `EnvArtTools.cs` (menu `ProjectM/Art/1. Convert Curated Env Materials`). Synty art is **URP-native — no conversion**.
|
||||
- **World = cosmetic Synty nature biomes ★ (DR-025):** `Game.unity` roots `BaseBiome`(Meadow_Forest)@origin + `ExpeditionBiome`(Arid_Desert)@+1000 — classic-URP cosmetics; ground = stock URP/Lit `Mat_Grass_Textures_01`/`sand 1` (NOT prop-atlas `S_General` mats). Global `Skybox/Procedural` (skydome MESH @origin can't span both regions); per-region fog/ambient cross-fade via client `WorldAtmosphereSystem` (camera X>500). **PNB fog/cloud-ring PREFABS = white torus — don't place.** See [[DR-025_World_Environment_Redo_Natural_Frontier]].
|
||||
- **World = cosmetic Synty nature biomes ★ (DR-025):** `Game.unity` roots `BaseBiome`(Meadow_Forest)@origin + `ExpeditionBiome`(Arid_Desert)@+1000 — classic-URP cosmetics, ground = stock URP/Lit (NOT prop-atlas `S_General` mats). Global `Skybox/Procedural`; per-region fog/ambient cross-fade via client `WorldAtmosphereSystem` (camera X>500). **PNB fog/cloud-ring PREFABS = white torus — don't place.** See [[DR-025_World_Environment_Redo_Natural_Frontier]].
|
||||
- **A dark-lit screenshot MASKS material bugs — verify material *values*.** `shader.GetPropertyType(idx)`-guard before `GetColor`/`GetFloat`/`GetTexture` (`S_General`'s `_BaseColorMultiply` is a float → `GetColor` returns black). Gate emission on the `_Emissive` flag + a fixture name; keep converted env metallic low (0.1–0.2).
|
||||
- **`VolumeProfile.Add<T>()` does NOT persist** (serializes `{fileID:0}`) — use `AssetDatabase.AddObjectToAsset(comp, profile)` + `SaveAssets`, verify on disk.
|
||||
- **A reverted engine/URP upgrade can stamp `URPGlobalSettings.asset` `m_AssetVersion` AHEAD of the package's `k_LastVersion`** (11>10, from the reverted 6.6 alpha); URP migrates forward-only so `URPPreprocessBuild` rejects it (*"not at last version"*) — **blocks player builds, not editor Play**. Fix: reflection-set `m_AssetVersion` back to `k_LastVersion` + `SaveAssets`.
|
||||
- **`LocalTransform.FromPosition()` resets Scale=1** — server spawners read the prefab's baked `LocalTransform`, override only Position (Scale is a `[GhostField]` → consistent-but-wrong).
|
||||
- **Static decor → gameplay subscene** (Entities Graphics renders only baked/EG-spawned entities); **strip colliders from cosmetic props** (else they bake into the PhysicsWorld the CC sweeps), no `GhostAuthoring` on scenery (classic-URP cosmetic colliders are **inert to the DOTS PhysicsWorld**). **World collision = subscene-only ★:** `Environment`-layer boundary ring + landmark box colliders (player blocked via the default layer matrix); enemies slide via a server `CollisionWorld.SphereCast` in `EnemyAISystem` (filter=`WorldCollisionConfig.EnvironmentMask`). Boundaries read as a **raised rock-cliff bowl rim** (`SM_Env_Rock_Cliff` ring ground-snapped at the collider radius, flat walkable interior) — top-down gates height as a hard vertical wall, never traversable slopes. See [[2026-06-08_World_Collision_HUD_Scaling]].
|
||||
- **Static decor → gameplay subscene** (EG renders only baked entities); **strip colliders from cosmetic props** + no `GhostAuthoring` on scenery (classic-URP cosmetic colliders are **inert to the DOTS PhysicsWorld**). **World collision = subscene-only ★:** `Environment`-layer boundary ring + landmark box colliders (player blocked via the default layer matrix); enemies slide via a server `CollisionWorld.SphereCast` in `EnemyAISystem` (filter=`WorldCollisionConfig.EnvironmentMask`). Boundary = a height-gated `SM_Env_Rock_Cliff` bowl rim, flat walkable interior. See [[2026-06-08_World_Collision_HUD_Scaling]].
|
||||
- **A GA "projectile" prefab self-propels** (non-kinematic `Rigidbody`+collider+`ProjectileMoveScript`) — strip to particles before `Start` (`CombatFeedbackSystem.StripCosmetic`). Verify *components*, not the name.
|
||||
|
||||
### Aim controls
|
||||
|
||||
Reference in New Issue
Block a user