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:
2026-06-08 09:44:03 -07:00
parent e23bebc84b
commit 510c12a980
4 changed files with 245 additions and 8 deletions
@@ -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**.
@@ -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 (D1D7, 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 13 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).
@@ -362,4 +362,14 @@ cosmetic classic-URP biomes in `Game.unity`; region-aware `WorldAtmosphereSystem
(mirrors `HudSystem.ExpeditionRegionXMin`). No ECS query, observe-only, `"Game"`-scene guarded so the menu isn't
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).
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.