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
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
---
|
||||
date: 2026-06-08
|
||||
type: session
|
||||
tags:
|
||||
- session
|
||||
- inventory
|
||||
- equipment
|
||||
- progression
|
||||
- items
|
||||
- netcode
|
||||
- ghostfield
|
||||
- harvesting
|
||||
- ultracode
|
||||
permalink: gamevault/07-sessions/2026/2026-06-08-inventory-equipment-progression-phase0
|
||||
---
|
||||
|
||||
# Session 2026-06-08 — Inventory · Equipment · Progression (Phase 0 backbone)
|
||||
|
||||
> Driven by `/dots-dev` in **ultracode** mode. Operator: *"Expand combat and harvesting for resources… first the
|
||||
> concept of an inventory and equipment (granting spells/affects); harvesting should require axes/pickaxes/tools;
|
||||
> this may require a progression system to be scalable — think carefully, handle scale + flexibility."*
|
||||
|
||||
## Decision & design
|
||||
|
||||
Architecture, phased roadmap, and the 7 Phase-0 decisions live in [[DR-026_Inventory_Equipment_Progression_Foundation]].
|
||||
Intake forks (one `AskUserQuestion`): **fully per-player inventory · gear-tier + crafting progression ·
|
||||
weapon-granted swappable abilities · plan + build the Phase 0 backbone**.
|
||||
|
||||
Before any code, a **5-lens adversarial design-review Workflow** (netcode/relevancy · determinism-Burst ·
|
||||
architecture-scale · economy-regression · test-strategy → synthesis) audited the design against the real code.
|
||||
Verdict **GO_WITH_CHANGES**. Key catches that changed the implementation:
|
||||
|
||||
- **Blocker (4/5 lenses):** the planned "add `RefRO<GhostOwner>` to the projectile query" would make GhostOwner a
|
||||
*required* component → the 8 existing harvest tests seed owner-less projectiles → they'd never match and **fail**,
|
||||
and the ledger-fallback never runs. **Fix:** optional cached `ComponentLookup<GhostOwner>`, query unchanged.
|
||||
- **Reachable deposit trigger required** — harvest→bag severs the build economy until deposit; the dev `#if` hook is
|
||||
not enough → added a real `G` key.
|
||||
- **Hoist** the catalog/owner-map/buffer lookups out of the per-hit sweep (stays `[BurstCompile]`).
|
||||
- **Mandatory player re-bake** (new `[GhostField]` buffer changes the ghost hash) + a **headless Play gate** (EditMode
|
||||
can't test replication; `NetCodeTestWorld` is internal).
|
||||
- **Scalability add-now:** bake `byte Tier` into `ItemDefBlob` (free re-bake later, but content-only Phase 1/2 now).
|
||||
|
||||
## What was built (code-complete, clean compile, 228/228 EditMode green)
|
||||
|
||||
**Simulation (`_Project/Scripts/Simulation/Items/`):** `ItemCategory` (byte consts), `ItemDefBlob`/`ItemDatabaseBlob`
|
||||
(ID-keyed), `ItemDatabase` (blob singleton), `InventorySlot` (`[GhostField]` `OwnerSendType.All` buffer, twin of
|
||||
`StatModifier`), `InventoryMath` (pure stack/cap/withdraw), `InventoryDepositRequest` (`IRpcCommand`). +
|
||||
`Tuning.InventoryMaxSlots=24` / `DefaultStackMax=999`.
|
||||
|
||||
**Authoring:** `ItemDefinition` SO (byte `Category`/`Tier` to dodge the MCP enum-drop + Burst-enum hazards) +
|
||||
`ItemDatabaseAuthoring` baker (mirrors `AbilityDatabaseAuthoring`). `PlayerAuthoring` now `AddBuffer<InventorySlot>`.
|
||||
|
||||
**Server:** `InventoryDepositSystem` (RPC → withdraw from bag, deposit to `ResourceLedger`, owner-map idiom,
|
||||
`ItemId==0` = deposit-all). `ResourceHarvestSystem` rerouted: owner read via optional `ComponentLookup<GhostOwner>`,
|
||||
deposit into the harvesting player's `InventorySlot` (stackMax from `ItemDatabase` if present), remainder/unresolvable
|
||||
→ ledger.
|
||||
|
||||
**Client:** `InventoryDepositSendSystem` (`G` = deposit all + `#if UNITY_EDITOR` static hook, mirrors
|
||||
`StorageOpSendSystem`). `HudSystem` folds in a read-only inventory panel (bottom-right, toggle `I`, names from the
|
||||
catalog, `G` hint) — null-safe, observe-only.
|
||||
|
||||
**Tests (`_Project/Tests/EditMode/`):** `InventoryMathTests`, `InventoryHarvestTests` (owned→bag / full-bag→ledger /
|
||||
no-matching-player→ledger), `InventoryDepositSystemTests` (specific / deposit-all / unresolvable). The 8
|
||||
`ResourceHarvestSystemTests` stay green via the genuine no-owner fallback.
|
||||
|
||||
## Play validation (done — task #9 closed)
|
||||
|
||||
Focused-editor finish completed this session:
|
||||
1. Authored 4 `ItemDefinition` assets (Aether/Ore/Biomass + Stone Pickaxe tool, id 10) under `Assets/_Project/Items/`
|
||||
and wired `ItemDatabaseAuthoring` (Items list assigned via `execute_code` to dodge the MCP ref-drop) into the
|
||||
`Gameplay` subscene; `refresh_unity scope=all force` re-baked.
|
||||
2. Headless `execute_code` Play gate (host+client), all green:
|
||||
- `ItemDatabase` baked into BOTH worlds (4 items); player ghost carries `InventorySlot` in both → **re-bake +
|
||||
handshake clean** (no ghost-hash desync).
|
||||
- Server write to the player bag (`InventoryMath.Deposit` Ore x7 + Aether x3) **replicated to the `ClientWorld`
|
||||
owner** — the proof EditMode can't make.
|
||||
- `InventoryDepositSendSystem.Deposit(0,0)` (the `G` path) **round-tripped**: server ledger ← bag, both
|
||||
inventories emptied, empty state replicated back.
|
||||
- HUD panel (`_invOpen` forced) rendered 4 rows with **catalog names** incl. `Stone Pickaxe x1` (non-resource
|
||||
lookup works).
|
||||
|
||||
## Deliberate decisions to remember (so a later session doesn't "fix" them)
|
||||
|
||||
- Manual harvest is personal-then-deposit; **automation stays base-direct** (asymmetry is intentional).
|
||||
- Inventory is **session-only** — undeposited haul is lost on Continue (Phase 3 adds `SaveData` v3).
|
||||
- Don't unify `ResourceLedger` / `SharedStorageContainer` / `InventorySlot` (separate decision); don't touch
|
||||
`SharedStorageContainer`.
|
||||
|
||||
## Next-session intent
|
||||
|
||||
Finish the focused-editor Play gate above, then **Phase 1 — Equipment slots + `EquipmentEffectSystem`** (weapon →
|
||||
`AbilityRef.Id`, gear → `StatModifier` bundle by slot `SourceId`), then **Phase 2 — tool-gated harvesting**.
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
---
|
||||
id: DR-026
|
||||
title: Inventory · Equipment · Progression foundation — data-driven ItemDatabase catalog + per-player replicated inventory, gear-tier progression, weapon-granted abilities (Phase 0 backbone)
|
||||
status: accepted
|
||||
date: 2026-06-08
|
||||
tags:
|
||||
- decision
|
||||
- inventory
|
||||
- equipment
|
||||
- progression
|
||||
- items
|
||||
- netcode
|
||||
- ghostfield
|
||||
- economy
|
||||
- harvesting
|
||||
- architecture
|
||||
permalink: gamevault/07-sessions/decisions/dr-026-inventory-equipment-progression-foundation
|
||||
---
|
||||
|
||||
# DR-026 — Inventory · Equipment · Progression foundation
|
||||
|
||||
## Context
|
||||
|
||||
Combat shipped with **one generic pre-assigned attack** (a single `AbilityRef.Id` byte → `AbilityFireSystem` →
|
||||
projectile), and harvesting was a side effect of that attack: a projectile hitting a `ResourceNode`/`BlightClutter`
|
||||
deposited yield straight into the **global shared `ResourceLedger`** ([[DR-018_World_Space_Cohesion_Pass]]). There was
|
||||
**no inventory, no items, no equipment, no tools, no character progression**. The operator asked to expand combat +
|
||||
harvesting into a system that "handles scale well and allows flexibility": inventory + equipment (granting spells /
|
||||
effects), tool-gated harvesting (axes/pickaxes), and a progression system.
|
||||
|
||||
Intake forks (one `AskUserQuestion`): **fully per-player inventory** (harvested resources land in each player's
|
||||
personal bag, then are deposited to base) · progression = **gear tiers + crafting** (reuses the Fabricator economy) ·
|
||||
"various spells" = **weapon-granted, swappable** ability now, multi-slot loadout later · this session = **plan + build
|
||||
the Phase 0 backbone**. A 5-lens adversarial design-review workflow (netcode / determinism-Burst / architecture-scale
|
||||
/ economy-regression / test-strategy) pressure-tested the Phase 0 design before any code — verdict **GO_WITH_CHANGES**,
|
||||
which this DR's decisions incorporate.
|
||||
|
||||
## Decision — the architecture (the "scale + flexibility" answer)
|
||||
|
||||
**Extend the existing data-driven spine; do not build a parallel system.** Four in-repo mechanisms do the work:
|
||||
|
||||
1. **`ItemDatabase` blob catalog** (sibling of `AbilityDatabase`) — the single source of truth for every item
|
||||
(resource, tool, weapon, gear, consumable). `ItemDefBlob { ushort ItemId; byte Category; byte Tier; int StackMax;
|
||||
FixedString64Bytes Name }`, **ID-keyed** `TryGetItem` (never index-keyed → inserting items never renumbers).
|
||||
Adding content = one authoring row + re-bake, **zero code**. This is the flexibility lever.
|
||||
2. **`ushort ItemId` id space subsumes the byte `ResourceId`** (Aether=1/Ore=2/Biomass=3 keep their ids; reserve
|
||||
>3 for new items; 0 = none). `StorageEntry.ItemId` / `StorageOpRequest.ItemId` are already `ushort` → no wire
|
||||
change. A resource is just a low-id item of `ItemCategory.Resource`.
|
||||
3. **Equipment effects fold through the existing `StatModifier` machinery** (Phase 1) — a gear piece is a bundle of
|
||||
`StatModifier`s tagged by a slot-derived `SourceId` (the M6 replace-by-SourceId pattern); a weapon sets the active
|
||||
`AbilityRef.Id`. No new folding code.
|
||||
4. **RPC-driven, server-authoritative mutation** — every player action (harvest deposit, equip, craft) is a blittable
|
||||
`IRpcCommand` applied server-only outside the predicted loop.
|
||||
|
||||
**Progression = gear tiers (catalog rows) crafted via the existing Fabricator economy.** `byte Tier` is baked into
|
||||
`ItemDefBlob` **now** (cheap; avoids a near-term re-bake) so Phase 2/3 tier gating is a content-only edit.
|
||||
|
||||
### Phased roadmap (each phase = its own approved slice)
|
||||
|
||||
- **Phase 0 — backbone (THIS session):** `ItemDatabase` catalog + per-player replicated `InventorySlot` buffer +
|
||||
harvest reroute to personal inventory + deposit-to-base RPC + read-only HUD inventory panel.
|
||||
- **Phase 1 — equipment + effects:** `EquipmentSlot` buffer (Weapon/Tool/Armor/Trinket); `EquipmentEffectSystem`
|
||||
syncs catalog modifiers into the `StatModifier` stack + sets `AbilityRef.Id` from the equipped weapon; equip RPC.
|
||||
- **Phase 2 — tool-gated harvesting:** `RequiredToolType`/`RequiredToolTier` baked on `ResourceNode`; harvest gates +
|
||||
scales yield by the owner's equipped tool tier.
|
||||
- **Phase 3 — progression / crafting:** gear-tier content; extend the Fabricator to craft *items* into inventory;
|
||||
equip-tier gates; per-player inventory persistence (additive `SaveData` v3).
|
||||
- **Phase 4 — (optional) multi-slot spell loadout:** expand `AbilityRef` into primary + hotkey spells.
|
||||
|
||||
## Phase 0 decisions (D1–D7, as validated/adjusted by the review)
|
||||
|
||||
- **D1 — `InventorySlot` is a `[GhostField]` `IBufferElementData` with `OwnerSendType.All`**, `[InternalBufferCapacity(24)]`,
|
||||
baked empty on the player (the exact `StatModifier` pattern). BOTH fields carry `[GhostField]` (the `[GhostComponent]`
|
||||
attribute alone does not replicate fields). Server-only writers → no predicted double-apply; the owner reads a pure
|
||||
snapshot and never mutates it locally. **Adding it changes the player ghost hash → a player-prefab re-bake is
|
||||
mandatory** (consistent across both worlds).
|
||||
- **D2 — harvest reroute (`ResourceHarvestSystem`, stays `[BurstCompile]`):** the projectile owner is read via an
|
||||
**optional cached `ComponentLookup<GhostOwner>`** (NOT a required query component — that would exclude owner-less
|
||||
projectiles and break the 8 existing harvest tests). Yield deposits into the owner's `InventorySlot` via
|
||||
`InventoryMath`; **unresolvable owner OR a full bag spills the remainder to the `ResourceLedger`** (no-loss valve).
|
||||
Owner-map + catalog + lookups are hoisted out of the per-hit sweep.
|
||||
- **D3 — `InventoryDepositRequest { ushort ItemId; int Count }` `IRpcCommand`** (`ItemId==0` = deposit all, handled
|
||||
before any withdraw; unconditional wire type). Server `InventoryDepositSystem` (plain server group, owner-map idiom)
|
||||
withdraws from the sender's bag and deposits into the **`ResourceLedger`** (what the build/upgrade/automation economy
|
||||
actually spends — NOT `SharedStorageContainer`). Client `InventoryDepositSendSystem` has a **real player key (`G` =
|
||||
deposit all)** plus an `#if UNITY_EDITOR` static hook — a reachable trigger is required or the economy is severed.
|
||||
- **D4 — inventory is session-only; `SaveData.CurrentVersion` is NOT bumped** (stays 2). **Deliberate, documented
|
||||
regression:** resources harvested-but-not-deposited are lost on a Continue (the haul-to-base loop means undeposited
|
||||
≠ committed). The `SharedStorageContainer` "no persistence yet" analogy was noted as imperfect (inventory now holds
|
||||
the whole manual-harvest economy); per-player persistence is an additive `SaveData` v3 in Phase 3.
|
||||
- **D5 — tests:** the 8 existing `ResourceHarvestSystemTests` stay green via the genuine no-owner ledger fallback (the
|
||||
projectile query is unchanged). New EditMode coverage: `InventoryMathTests`, owned-harvest→inventory,
|
||||
full-bag→ledger spill, owned-but-no-matching-player→ledger fallback, and the deposit-RPC server logic. Replication +
|
||||
RPC round-trip are **Play-only** (`NetCodeTestWorld` is internal) → a headless `execute_code` Play gate.
|
||||
- **D6 — `ushort ItemId` subsumes the `ResourceId` byte space** (validated; no collision, ids 1–3 stable).
|
||||
- **D7 — `InventoryMath` is a NEW pure helper** (does NOT collapse into `StorageMath`, which has no stack/slot cap):
|
||||
`Deposit(→remainder)` top-up-then-append with `stackMax`/`maxSlots`, `Withdraw(→taken)` back-to-front, `CountOf`.
|
||||
`Tuning.InventoryMaxSlots=24`, `Tuning.DefaultStackMax=999`.
|
||||
|
||||
## Deliberate gameplay consequences (stated, not accidental)
|
||||
|
||||
- **Personal-harvest pivot:** harvested resources no longer auto-credit the shared HUD strip / build affordability —
|
||||
they're personal until deposited (`G`). This is the intended gather→carry→deposit loop, not a bug.
|
||||
- **Automation asymmetry:** the Fabricator/machines still deposit **base-direct** to the ledger; only **manual** harvest
|
||||
is personal-then-deposit. Defensible (machines are base infrastructure) — do not "fix" the Fabricator to route
|
||||
through inventory.
|
||||
- **Co-op gap (latent):** each player's haul is private; there is no shared-deposit affordance yet (single-player-fine;
|
||||
flag for a co-op pass).
|
||||
|
||||
## What is locked / not touched
|
||||
|
||||
- Frozen: grid, `PlacedStructure` archetype, server-only production, `ResourceLedger` resolution discipline
|
||||
(`GetSingletonEntity<ResourceLedger>` → `GetBuffer<StorageEntry>`, **never** `GetSingleton<StorageEntry>`).
|
||||
- **Not unified this session:** the three buffers (`ResourceLedger` + `SharedStorageContainer` `StorageEntry`, and the
|
||||
new `InventorySlot`). `SharedStorageContainer` is a retirement candidate — a separate decision, untouched here.
|
||||
|
||||
## Validation status (2026-06-08)
|
||||
|
||||
Code-complete + clean compile. **228/228 EditMode tests pass** (server logic: InventoryMath, harvest reroute incl.
|
||||
the 8 legacy fallbacks, deposit RPC). **Play-validated (2026-06-08, host+client):** the catalog asset baked into BOTH
|
||||
worlds (`ItemDatabase` = 4 items); the re-baked player ghost carries `InventorySlot` in both worlds with a clean
|
||||
connect handshake; a server write to the player bag replicated to the `ClientWorld` owner (`Ore x7, Aether x3`); the
|
||||
`G` deposit-all RPC round-tripped (server ledger ← bag, both inventories emptied, empty state replicated back); and
|
||||
the HUD panel rendered the 4 carried items with names from the catalog — including the non-resource `Stone Pickaxe`
|
||||
(id 10, the catalog name lookup). The slice is shippable.
|
||||
|
||||
## Links
|
||||
|
||||
[[DR-004_Data_Driven_Abilities_Modifiers]] (StatModifier machinery reused for equipment) ·
|
||||
[[DR-008_M5_HomeBase_BaseLayer]] (StorageEntry / RPC storage pattern) ·
|
||||
[[DR-014_M6_Build_Structures_Automation_Foundation]] (data-driven catalog pattern) ·
|
||||
[[DR-018_World_Space_Cohesion_Pass]] (harvest sweep this reroutes) ·
|
||||
[[DR-020_M7_Automation_Production_Chains]] (server-only economy, Fabricator) ·
|
||||
[[DR-021_HUD_UITK_BuildPalette]] (HudSystem this extends).
|
||||
@@ -363,3 +363,13 @@ cosmetic classic-URP biomes in `Game.unity`; region-aware `WorldAtmosphereSystem
|
||||
restyled; knobs in a `WorldFeelConfig`-style MonoBehaviour with `SubsystemRegistration` static reset + null-safe
|
||||
fallbacks. Zero sim/netcode impact (writes only global managed `RenderSettings`; `ClientSimulation` filter keeps
|
||||
it off the server/headless world).
|
||||
## 2026-06-08 — CLAUDE.md condensation (inventory pointer added net-zero)
|
||||
|
||||
Added the **Per-player inventory + data-driven items ★** pointer (see [[DR-026_Inventory_Equipment_Progression_Foundation]]) to the *Build / structures / grid* section. Paid for it net-zero by trimming the wording below from CLAUDE.md — all of it survives in the linked DRs/session notes, so this is a pure terseness pass with no semantic loss:
|
||||
|
||||
- **Disk persistence ([[DR-019_Frontend_Menu_Settings_Saves_Build]]):** dropped the explicit "versioned (null on bad version)", "quit-to-menu (`WorldLauncher.TrySaveFromServer`)", and "epoch-independent REMAINING-tick" phrasings (kept REMAINING-tick + born-correct + the one `SaveStructureScan.Collect` path).
|
||||
- **World biomes ([[DR-025_World_Environment_Redo_Natural_Frontier]]):** dropped the ground mat names (`Mat_Grass_Textures_01` / `sand 1`) and the "skydome MESH @origin can't span both regions" rationale for the global `Skybox/Procedural`.
|
||||
- **World collision ([[2026-06-08_World_Collision_HUD_Scaling]]):** dropped the rim cosmetic detail — "`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" → kept "height-gated `SM_Env_Rock_Cliff` bowl rim".
|
||||
- **UITK HUD ([[DR-021_HUD_UITK_BuildPalette]]):** dropped "behind the pause overlay's 100" and "`BuildPreviewMath` = the client mirror of the server check".
|
||||
- **Synty HudTheme ([[DR-024_HUD_Synty_Skin_Theme]]):** dropped "fonts = cached SDF, reset on `SubsystemRegistration`" and the "(NOT Resources)" aside.
|
||||
- **M7 Automation ([[DR-020_M7_Automation_Production_Chains]]):** tightened wording only, no detail removed.
|
||||
|
||||
Reference in New Issue
Block a user