Inventory: per-player items backbone (DR-026 Phase 0)

Data-driven ItemDatabase catalog + per-player replicated InventorySlot ([GhostField] OwnerSendType.All, a StatModifier twin). Harvest reroutes to the firing player's personal inventory (optional ComponentLookup<GhostOwner> in ResourceHarvestSystem; remainder/un-owned -> ledger); the G-key InventoryDepositRequest RPC moves the bag into the shared ledger the build economy spends. Catalog asset (Aether/Ore/Biomass + Stone Pickaxe) wired into the Gameplay subscene; read-only HUD inventory panel. ushort ItemId subsumes ResourceId; byte Category/Tier baked for gear-tier progression. Session-only (no SaveData bump).

Play-validated host+client: catalog baked into both worlds, the re-baked player ghost carries InventorySlot with a clean handshake, a server write replicates to the client owner, the deposit RPC round-trips, and the HUD renders catalog names. See DR-026.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-08 09:43:31 -07:00
parent 1ed2aa46c5
commit 599b9b4255
37 changed files with 848 additions and 1 deletions
@@ -70,6 +70,9 @@ namespace ProjectM.Client
VisualElement _vignette, _downed;
float _prevHp, _flash;
bool _haveHp;
// personal inventory panel (read-only; toggled with I)
VisualElement _invPanel, _invList;
bool _invOpen;
EntityQuery _huskQuery;
@@ -290,6 +293,35 @@ namespace ProjectM.Client
_vignette.style.display = DisplayStyle.None;
_downed.style.display = DisplayStyle.None;
}
// ---- Personal inventory (read-only; toggle with I, deposit-all with G via InventoryDepositSendSystem) ----
var invKb = UnityEngine.InputSystem.Keyboard.current;
if (invKb != null && invKb.iKey.wasPressedThisFrame) _invOpen = !_invOpen;
if (_invOpen && found)
{
EntityManager.CompleteDependencyBeforeRO<InventorySlot>();
bool haveItemDb = SystemAPI.TryGetSingleton<ItemDatabase>(out var itemDb);
_invPanel.style.display = DisplayStyle.Flex;
_invList.Clear();
int shown = 0;
foreach (var bag in SystemAPI.Query<DynamicBuffer<InventorySlot>>()
.WithAll<GhostOwnerIsLocal, PlayerTag>())
{
for (int i = 0; i < bag.Length; i++)
{
var slot = bag[i];
if (slot.ItemId == 0 || slot.Count <= 0) continue;
AddInvRow(ItemName(haveItemDb, itemDb, slot.ItemId), ItemTint(slot.ItemId), slot.Count);
shown++;
}
break;
}
if (shown == 0)
_invList.Add(HudUi.Text("(empty)", 13, MenuUi.SubCol, TextAnchor.MiddleLeft));
}
else
{
_invPanel.style.display = DisplayStyle.None;
}
}
// ---- per-frame helpers ----
@@ -467,6 +499,7 @@ namespace ProjectM.Client
BuildPaletteRow(root);
BuildHintBar(root);
BuildDowned(root);
BuildInventory(root);
}
void BuildVignette(VisualElement root)
@@ -724,6 +757,67 @@ namespace ProjectM.Client
root.Add(_downed);
}
void BuildInventory(VisualElement root)
{
_invPanel = HudUi.Panel(PanelDark);
_invPanel.style.position = Position.Absolute;
_invPanel.style.right = 40; _invPanel.style.bottom = 40;
_invPanel.style.minWidth = 224;
_invPanel.style.paddingLeft = 14; _invPanel.style.paddingRight = 14;
_invPanel.style.paddingTop = 10; _invPanel.style.paddingBottom = 10;
_invPanel.style.alignItems = Align.FlexStart;
_invPanel.pickingMode = PickingMode.Ignore;
var header = HudUi.Display("INVENTORY", 16, AetherCyan, TextAnchor.MiddleLeft);
header.style.marginBottom = 6;
_invPanel.Add(header);
_invList = new VisualElement();
_invList.pickingMode = PickingMode.Ignore;
_invPanel.Add(_invList);
var hint = HudUi.Text("I close - G deposit all at base", 11, MenuUi.SubCol, TextAnchor.MiddleLeft);
hint.style.marginTop = 8;
_invPanel.Add(hint);
_invPanel.style.display = DisplayStyle.None;
root.Add(_invPanel);
}
void AddInvRow(string name, Color tint, int count)
{
var row = new VisualElement();
row.style.flexDirection = FlexDirection.Row;
row.style.justifyContent = Justify.SpaceBetween;
row.style.minWidth = 196;
row.style.marginTop = 2;
row.pickingMode = PickingMode.Ignore;
row.Add(HudUi.Text(name, 13, tint, TextAnchor.MiddleLeft));
row.Add(HudUi.Display("x" + count, 13, Color.white, TextAnchor.MiddleRight));
_invList.Add(row);
}
static string ItemName(bool haveDb, ItemDatabase db, ushort id)
{
if (haveDb && db.Value.IsCreated)
{
ref var blob = ref db.Value.Value;
if (blob.TryGetItem(id, out var def)) return def.Name.ToString();
}
if (id == ResourceId.Aether) return "Aether";
if (id == ResourceId.Ore) return "Ore";
if (id == ResourceId.Biomass) return "Biomass";
return "Item " + id;
}
static Color ItemTint(ushort id)
{
if (id == ResourceId.Aether) return AetherCyan;
if (id == ResourceId.Ore) return OreAmber;
if (id == ResourceId.Biomass) return BioGreen;
return new Color(0.85f, 0.85f, 0.9f);
}
static Color ResourceTint(byte resId)
=> resId == ResourceId.Aether ? AetherCyan : resId == ResourceId.Biomass ? BioGreen : OreAmber;