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:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9a4badbd1733bfa4daeb671385fad693
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,64 @@
|
||||
using ProjectM.Simulation;
|
||||
using Unity.Entities;
|
||||
using Unity.NetCode;
|
||||
|
||||
namespace ProjectM.Client
|
||||
{
|
||||
/// <summary>
|
||||
/// Client-only sender for <see cref="InventoryDepositRequest"/> RPCs (move the local player's PERSONAL
|
||||
/// inventory into the shared base stockpile / global ledger). A one-off action (not per-tick predicted
|
||||
/// input), so it is an RPC: on the deposit key edge (G = deposit ALL) it creates the request entity
|
||||
/// targeted at the server connection, and the server applies it authoritatively in
|
||||
/// <see cref="ProjectM.Server.InventoryDepositSystem"/>. Managed SystemBase because it reads the managed
|
||||
/// Input System; Input System types are fully qualified and <c>using UnityEngine.InputSystem;</c> is
|
||||
/// intentionally omitted (that namespace defines a PlayerInput type that collides with
|
||||
/// <see cref="ProjectM.Simulation.PlayerInput"/>). An editor-only static hook (<see cref="Deposit"/>)
|
||||
/// drives the same path from execute_code for headless validation without a focused Game view. The wire
|
||||
/// type is unconditional; only this send SYSTEM is build-time managed.
|
||||
/// </summary>
|
||||
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)]
|
||||
public partial class InventoryDepositSendSystem : SystemBase
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
struct PendingDeposit { public ushort ItemId; public int Count; }
|
||||
|
||||
static readonly System.Collections.Generic.Queue<PendingDeposit> s_Pending =
|
||||
new System.Collections.Generic.Queue<PendingDeposit>();
|
||||
|
||||
/// <summary>EDITOR / execute_code hook: queue a deposit (ItemId 0 = deposit all; Count <= 0 = all of that item).</summary>
|
||||
public static void Deposit(ushort itemId = 0, int count = 0) =>
|
||||
s_Pending.Enqueue(new PendingDeposit { ItemId = itemId, Count = count });
|
||||
#endif
|
||||
|
||||
protected override void OnCreate()
|
||||
{
|
||||
RequireForUpdate<NetworkId>();
|
||||
}
|
||||
|
||||
protected override void OnUpdate()
|
||||
{
|
||||
// Need the server connection to target the RPC; bail (keeping any queued ops) until connected.
|
||||
if (!SystemAPI.TryGetSingletonEntity<NetworkId>(out var connection))
|
||||
return;
|
||||
|
||||
var keyboard = UnityEngine.InputSystem.Keyboard.current;
|
||||
if (keyboard != null && keyboard.gKey.wasPressedThisFrame)
|
||||
Send(connection, 0, 0); // G -> deposit everything to the base stockpile
|
||||
|
||||
#if UNITY_EDITOR
|
||||
while (s_Pending.Count > 0)
|
||||
{
|
||||
var d = s_Pending.Dequeue();
|
||||
Send(connection, d.ItemId, d.Count);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void Send(Entity connection, ushort itemId, int count)
|
||||
{
|
||||
var request = EntityManager.CreateEntity();
|
||||
EntityManager.AddComponentData(request, new InventoryDepositRequest { ItemId = itemId, Count = count });
|
||||
EntityManager.AddComponentData(request, new SendRpcCommandRequest { TargetConnection = connection });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9298a5924b4920b4db8ff2f48732a662
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user