diff --git a/Assets/_Project/Items.meta b/Assets/_Project/Items.meta
new file mode 100644
index 000000000..f2638c9ca
--- /dev/null
+++ b/Assets/_Project/Items.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 7994449a235d329439443860dfb4ec82
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Items/Item_Aether.asset b/Assets/_Project/Items/Item_Aether.asset
new file mode 100644
index 000000000..24bb2e180
--- /dev/null
+++ b/Assets/_Project/Items/Item_Aether.asset
@@ -0,0 +1,19 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &11400000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 84295e2f852afac4fa4b7384857281d9, type: 3}
+ m_Name: Item_Aether
+ m_EditorClassIdentifier: ProjectM.Authoring::ProjectM.Authoring.ItemDefinition
+ ItemId: 1
+ DisplayName: Aether
+ Category: 0
+ Tier: 0
+ StackMax: 999
diff --git a/Assets/_Project/Items/Item_Aether.asset.meta b/Assets/_Project/Items/Item_Aether.asset.meta
new file mode 100644
index 000000000..0a8253f9b
--- /dev/null
+++ b/Assets/_Project/Items/Item_Aether.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 938a882b6fbd50d4c9a051ddabc5829b
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 11400000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Items/Item_Biomass.asset b/Assets/_Project/Items/Item_Biomass.asset
new file mode 100644
index 000000000..cd71939c2
--- /dev/null
+++ b/Assets/_Project/Items/Item_Biomass.asset
@@ -0,0 +1,19 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &11400000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 84295e2f852afac4fa4b7384857281d9, type: 3}
+ m_Name: Item_Biomass
+ m_EditorClassIdentifier: ProjectM.Authoring::ProjectM.Authoring.ItemDefinition
+ ItemId: 3
+ DisplayName: Biomass
+ Category: 0
+ Tier: 0
+ StackMax: 999
diff --git a/Assets/_Project/Items/Item_Biomass.asset.meta b/Assets/_Project/Items/Item_Biomass.asset.meta
new file mode 100644
index 000000000..033cc9041
--- /dev/null
+++ b/Assets/_Project/Items/Item_Biomass.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: a2a0e6d3ae0218d458d2b5305891ce89
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 11400000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Items/Item_Ore.asset b/Assets/_Project/Items/Item_Ore.asset
new file mode 100644
index 000000000..5a9eca760
--- /dev/null
+++ b/Assets/_Project/Items/Item_Ore.asset
@@ -0,0 +1,19 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &11400000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 84295e2f852afac4fa4b7384857281d9, type: 3}
+ m_Name: Item_Ore
+ m_EditorClassIdentifier: ProjectM.Authoring::ProjectM.Authoring.ItemDefinition
+ ItemId: 2
+ DisplayName: Ore
+ Category: 0
+ Tier: 0
+ StackMax: 999
diff --git a/Assets/_Project/Items/Item_Ore.asset.meta b/Assets/_Project/Items/Item_Ore.asset.meta
new file mode 100644
index 000000000..7ea0eb459
--- /dev/null
+++ b/Assets/_Project/Items/Item_Ore.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 4ed399a329eb6d847921aef05bde213d
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 11400000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Items/Item_StonePickaxe.asset b/Assets/_Project/Items/Item_StonePickaxe.asset
new file mode 100644
index 000000000..6deeb7a62
--- /dev/null
+++ b/Assets/_Project/Items/Item_StonePickaxe.asset
@@ -0,0 +1,19 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &11400000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 84295e2f852afac4fa4b7384857281d9, type: 3}
+ m_Name: Item_StonePickaxe
+ m_EditorClassIdentifier: ProjectM.Authoring::ProjectM.Authoring.ItemDefinition
+ ItemId: 10
+ DisplayName: Stone Pickaxe
+ Category: 1
+ Tier: 1
+ StackMax: 1
diff --git a/Assets/_Project/Items/Item_StonePickaxe.asset.meta b/Assets/_Project/Items/Item_StonePickaxe.asset.meta
new file mode 100644
index 000000000..eac97a324
--- /dev/null
+++ b/Assets/_Project/Items/Item_StonePickaxe.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 31ec40cfc6e81ef4ab9bbce57a40621e
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 11400000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Scripts/Authoring/Items.meta b/Assets/_Project/Scripts/Authoring/Items.meta
new file mode 100644
index 000000000..b681fbeb2
--- /dev/null
+++ b/Assets/_Project/Scripts/Authoring/Items.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 2251d22676b666d48ac5e9e32b70391c
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Scripts/Authoring/Items/ItemDatabaseAuthoring.cs b/Assets/_Project/Scripts/Authoring/Items/ItemDatabaseAuthoring.cs
new file mode 100644
index 000000000..75413bad2
--- /dev/null
+++ b/Assets/_Project/Scripts/Authoring/Items/ItemDatabaseAuthoring.cs
@@ -0,0 +1,54 @@
+using System.Collections.Generic;
+using ProjectM.Simulation;
+using Unity.Collections;
+using Unity.Entities;
+using UnityEngine;
+
+namespace ProjectM.Authoring
+{
+ ///
+ /// Bakes the designer-authored item definitions into a single ItemDatabase blob singleton (immutable,
+ /// shared, Burst-fast), mirroring AbilityDatabaseAuthoring. Place ONE in the gameplay subscene; it streams
+ /// identically into the client and server worlds (config, not replicated). DependsOn each definition so a
+ /// value change re-bakes the blob. Runtime lookup is ID-keyed (ItemDatabaseBlob.TryGetItem), so the list
+ /// order here does not matter and inserting an item never renumbers existing ids.
+ ///
+ public class ItemDatabaseAuthoring : MonoBehaviour
+ {
+ [Tooltip("All item definitions in the game (resources + tools/gear). Looked up at runtime by ItemId.")]
+ public List Items = new List();
+
+ private class DatabaseBaker : Baker
+ {
+ public override void Bake(ItemDatabaseAuthoring authoring)
+ {
+ var entity = GetEntity(TransformUsageFlags.None);
+
+ int count = authoring.Items != null ? authoring.Items.Count : 0;
+
+ var builder = new BlobBuilder(Allocator.Temp);
+ ref var root = ref builder.ConstructRoot();
+ var arr = builder.Allocate(ref root.Items, count);
+ for (int i = 0; i < count; i++)
+ {
+ var def = authoring.Items[i];
+ if (def == null) { arr[i] = default; continue; }
+ DependsOn(def);
+ arr[i] = new ItemDefBlob
+ {
+ ItemId = (ushort)def.ItemId,
+ Category = def.Category,
+ Tier = def.Tier,
+ StackMax = def.StackMax,
+ Name = def.DisplayName,
+ };
+ }
+
+ var blob = builder.CreateBlobAssetReference(Allocator.Persistent);
+ builder.Dispose();
+ AddBlobAsset(ref blob, out _);
+ AddComponent(entity, new ItemDatabase { Value = blob });
+ }
+ }
+ }
+}
diff --git a/Assets/_Project/Scripts/Authoring/Items/ItemDatabaseAuthoring.cs.meta b/Assets/_Project/Scripts/Authoring/Items/ItemDatabaseAuthoring.cs.meta
new file mode 100644
index 000000000..7dcd14c91
--- /dev/null
+++ b/Assets/_Project/Scripts/Authoring/Items/ItemDatabaseAuthoring.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 5ee44dc3bc9f3164592195d4068be8d1
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Authoring/Items/ItemDefinition.cs b/Assets/_Project/Scripts/Authoring/Items/ItemDefinition.cs
new file mode 100644
index 000000000..922269ee2
--- /dev/null
+++ b/Assets/_Project/Scripts/Authoring/Items/ItemDefinition.cs
@@ -0,0 +1,31 @@
+using ProjectM.Simulation;
+using UnityEngine;
+
+namespace ProjectM.Authoring
+{
+ ///
+ /// Designer-facing definition of one item (resource, tool, weapon, gear, consumable). Numeric/byte fields
+ /// are baked into the ItemDatabase blob (immutable, Burst-fast runtime config). Category and Tier are
+ /// BYTES, not enums, to dodge BOTH the MCP enum-drop hazard (manage_* silently drop enum fields when set
+ /// via tooling) AND the cross-assembly enum-in-Burst hazard. UI icon/description are deferred to a later
+ /// managed lookup keyed by id, exactly like AbilityDefinition.
+ ///
+ [CreateAssetMenu(menuName = "Project M/Item Definition", fileName = "Item_")]
+ public class ItemDefinition : ScriptableObject
+ {
+ [Tooltip("Stable item id (ushort range). Keep 1=Aether, 2=Ore, 3=Biomass; reserve >3 for new items; 0 = none.")]
+ public int ItemId = 4;
+
+ public string DisplayName = "Item";
+
+ [Tooltip("ItemCategory byte: 0=Resource, 1=Tool, 2=Weapon, 3=Gear, 4=Consumable.")]
+ public byte Category = ItemCategory.Resource;
+
+ [Tooltip("Progression tier (0 = base). Higher-tier tools harvest higher-tier nodes / hit harder.")]
+ public byte Tier = 0;
+
+ [Min(1)]
+ [Tooltip("Max units that stack in one inventory slot (1 for non-stacking equipment).")]
+ public int StackMax = 999;
+ }
+}
diff --git a/Assets/_Project/Scripts/Authoring/Items/ItemDefinition.cs.meta b/Assets/_Project/Scripts/Authoring/Items/ItemDefinition.cs.meta
new file mode 100644
index 000000000..0a7a91fba
--- /dev/null
+++ b/Assets/_Project/Scripts/Authoring/Items/ItemDefinition.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 84295e2f852afac4fa4b7384857281d9
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Authoring/Player/PlayerAuthoring.cs b/Assets/_Project/Scripts/Authoring/Player/PlayerAuthoring.cs
index ed7ecff2d..71faae6bb 100644
--- a/Assets/_Project/Scripts/Authoring/Player/PlayerAuthoring.cs
+++ b/Assets/_Project/Scripts/Authoring/Player/PlayerAuthoring.cs
@@ -67,6 +67,8 @@ namespace ProjectM.Authoring
// Empty replicated modifier stack (grown by upgrades/pickups/debug hook, server-authoritative).
AddBuffer(entity);
+ // Empty replicated personal inventory (server-authoritative; harvest yield + deposit RPC land here).
+ AddBuffer(entity);
// Server-only expiry tracker for timed buffs (paired with a StatModifier by SourceId; not replicated).
AddBuffer(entity);
diff --git a/Assets/_Project/Scripts/Client/Economy.meta b/Assets/_Project/Scripts/Client/Economy.meta
new file mode 100644
index 000000000..5ff449904
--- /dev/null
+++ b/Assets/_Project/Scripts/Client/Economy.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 9a4badbd1733bfa4daeb671385fad693
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Scripts/Client/Economy/InventoryDepositSendSystem.cs b/Assets/_Project/Scripts/Client/Economy/InventoryDepositSendSystem.cs
new file mode 100644
index 000000000..f777e4c16
--- /dev/null
+++ b/Assets/_Project/Scripts/Client/Economy/InventoryDepositSendSystem.cs
@@ -0,0 +1,64 @@
+using ProjectM.Simulation;
+using Unity.Entities;
+using Unity.NetCode;
+
+namespace ProjectM.Client
+{
+ ///
+ /// Client-only sender for 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
+ /// . Managed SystemBase because it reads the managed
+ /// Input System; Input System types are fully qualified and using UnityEngine.InputSystem; is
+ /// intentionally omitted (that namespace defines a PlayerInput type that collides with
+ /// ). An editor-only static hook ()
+ /// 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.
+ ///
+ [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 s_Pending =
+ new System.Collections.Generic.Queue();
+
+ /// EDITOR / execute_code hook: queue a deposit (ItemId 0 = deposit all; Count <= 0 = all of that item).
+ 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();
+ }
+
+ protected override void OnUpdate()
+ {
+ // Need the server connection to target the RPC; bail (keeping any queued ops) until connected.
+ if (!SystemAPI.TryGetSingletonEntity(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 });
+ }
+ }
+}
diff --git a/Assets/_Project/Scripts/Client/Economy/InventoryDepositSendSystem.cs.meta b/Assets/_Project/Scripts/Client/Economy/InventoryDepositSendSystem.cs.meta
new file mode 100644
index 000000000..d63db2d3b
--- /dev/null
+++ b/Assets/_Project/Scripts/Client/Economy/InventoryDepositSendSystem.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 9298a5924b4920b4db8ff2f48732a662
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Client/Presentation/HudSystem.cs b/Assets/_Project/Scripts/Client/Presentation/HudSystem.cs
index d1c0848f2..f7c2921dd 100644
--- a/Assets/_Project/Scripts/Client/Presentation/HudSystem.cs
+++ b/Assets/_Project/Scripts/Client/Presentation/HudSystem.cs
@@ -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();
+ bool haveItemDb = SystemAPI.TryGetSingleton(out var itemDb);
+ _invPanel.style.display = DisplayStyle.Flex;
+ _invList.Clear();
+ int shown = 0;
+ foreach (var bag in SystemAPI.Query>()
+ .WithAll())
+ {
+ 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;
diff --git a/Assets/_Project/Scripts/Server/Economy/InventoryDepositSystem.cs b/Assets/_Project/Scripts/Server/Economy/InventoryDepositSystem.cs
new file mode 100644
index 000000000..ccea5857e
--- /dev/null
+++ b/Assets/_Project/Scripts/Server/Economy/InventoryDepositSystem.cs
@@ -0,0 +1,84 @@
+using ProjectM.Simulation;
+using Unity.Burst;
+using Unity.Collections;
+using Unity.Entities;
+using Unity.NetCode;
+
+namespace ProjectM.Server
+{
+ ///
+ /// Server-authoritative handler for RPCs: moves items from the
+ /// sender's PERSONAL inventory into the shared base stockpile (the global
+ /// the build/upgrade/automation economy spends from). Resolves the sender's
+ /// player (SourceConnection -> NetworkId -> GhostOwner) via the AbilityUpgradeSystem owner-map idiom,
+ /// then withdraws from the player's inventory and deposits into the ledger IN-PLACE (buffer mutation is not
+ /// a structural change). ItemId == 0 ("deposit all") is handled BEFORE any per-item withdraw and
+ /// never writes a 0-id row. Resolves the ledger via GetSingletonEntity<ResourceLedger>() then
+ /// GetBuffer<StorageEntry>() — NEVER GetSingleton<StorageEntry> (the base
+ /// container owns a second StorageEntry buffer). Plain server SimulationSystemGroup (not predicted, so the
+ /// effect applies exactly once — no rollback double-apply).
+ ///
+ [BurstCompile]
+ [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
+ public partial struct InventoryDepositSystem : ISystem
+ {
+ [BurstCompile]
+ public void OnCreate(ref SystemState state)
+ {
+ state.RequireForUpdate();
+ var builder = new EntityQueryBuilder(Allocator.Temp)
+ .WithAll();
+ state.RequireForUpdate(state.GetEntityQuery(builder));
+ }
+
+ [BurstCompile]
+ public void OnUpdate(ref SystemState state)
+ {
+ var ledger = SystemAPI.GetBuffer(SystemAPI.GetSingletonEntity());
+
+ var playerByConn = new NativeHashMap(8, Allocator.Temp);
+ foreach (var (owner, entity) in
+ SystemAPI.Query>().WithAll().WithEntityAccess())
+ playerByConn[owner.ValueRO.NetworkId] = entity;
+
+ var ecb = new EntityCommandBuffer(Allocator.Temp);
+
+ foreach (var (request, receive, requestEntity) in
+ SystemAPI.Query, RefRO>().WithEntityAccess())
+ {
+ var conn = receive.ValueRO.SourceConnection;
+ if (SystemAPI.HasComponent(conn)
+ && playerByConn.TryGetValue(SystemAPI.GetComponent(conn).Value, out var player))
+ {
+ var inv = SystemAPI.GetBuffer(player);
+ var req = request.ValueRO;
+
+ if (req.ItemId == 0)
+ {
+ // Deposit EVERYTHING: drain each non-empty stack into the ledger, then clear the bag.
+ for (int i = 0; i < inv.Length; i++)
+ {
+ var slot = inv[i];
+ if (slot.ItemId != 0 && slot.Count > 0)
+ StorageMath.Deposit(ledger, slot.ItemId, slot.Count);
+ }
+ inv.Clear();
+ }
+ else
+ {
+ // Count <= 0 means "all of that item"; Withdraw clamps to what is available.
+ int want = req.Count <= 0 ? int.MaxValue : req.Count;
+ int moved = InventoryMath.Withdraw(inv, req.ItemId, want);
+ if (moved > 0)
+ StorageMath.Deposit(ledger, req.ItemId, moved);
+ }
+ }
+
+ ecb.DestroyEntity(requestEntity);
+ }
+
+ ecb.Playback(state.EntityManager);
+ playerByConn.Dispose();
+ }
+ }
+}
diff --git a/Assets/_Project/Scripts/Server/Economy/InventoryDepositSystem.cs.meta b/Assets/_Project/Scripts/Server/Economy/InventoryDepositSystem.cs.meta
new file mode 100644
index 000000000..539d80e3e
--- /dev/null
+++ b/Assets/_Project/Scripts/Server/Economy/InventoryDepositSystem.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 25b1bdf13ad8a6d4ca48bef112b98d28
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Server/Economy/ResourceHarvestSystem.cs b/Assets/_Project/Scripts/Server/Economy/ResourceHarvestSystem.cs
index f0ffe7683..6769ed557 100644
--- a/Assets/_Project/Scripts/Server/Economy/ResourceHarvestSystem.cs
+++ b/Assets/_Project/Scripts/Server/Economy/ResourceHarvestSystem.cs
@@ -33,11 +33,16 @@ namespace ProjectM.Server
{
const float k_ProjectileRadius = Tuning.HarvestProjectileRadius;
+ ComponentLookup m_GhostOwnerLookup;
+ BufferLookup m_InvLookup;
+
[BurstCompile]
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate();
state.RequireForUpdate();
+ m_GhostOwnerLookup = state.GetComponentLookup(true);
+ m_InvLookup = state.GetBufferLookup(false);
}
[BurstCompile]
@@ -46,6 +51,18 @@ namespace ProjectM.Server
var ledgerEntity = SystemAPI.GetSingletonEntity();
var ledger = SystemAPI.GetBuffer(ledgerEntity);
+ // Resolve the harvesting player from the projectile's GhostOwner so yield lands in their PERSONAL
+ // inventory. Owner read via a cached lookup (optional); the owner->player map + item catalog are
+ // hoisted out of the per-hit sweep (invariant for the tick).
+ m_GhostOwnerLookup.Update(ref state);
+ m_InvLookup.Update(ref state);
+ bool haveDb = SystemAPI.TryGetSingleton(out var itemDb);
+
+ var playerByConn = new NativeHashMap(8, Allocator.Temp);
+ foreach (var (owner, playerEntity) in
+ SystemAPI.Query>().WithAll().WithEntityAccess())
+ playerByConn[owner.ValueRO.NetworkId] = playerEntity;
+
// Snapshot all harvest/clear targets (nodes + clutter) once this tick into a UNIFIED set.
var tgtEntity = new NativeList(Allocator.Temp);
var tgtPos = new NativeList(Allocator.Temp);
@@ -121,7 +138,30 @@ namespace ProjectM.Server
// A positive baked yield must always make progress: a raw (int) truncation of a sub-1.0 per-hit
// value would deposit 0 AND never decrement Remaining -> an immortal target that silently eats shots.
int amount = math.max(1, (int)tgtYieldPerHit[bestIdx]);
- StorageMath.Deposit(ledger, tgtYieldId[bestIdx], amount);
+ byte yieldId = tgtYieldId[bestIdx];
+
+ // Route the yield into the HARVESTING player's PERSONAL inventory. The projectile carries the
+ // firing player's GhostOwner (AbilityFireSystem); the owner is read OPTIONALLY (cached lookup) so
+ // an un-owned projectile (or a test projectile with no GhostOwner) falls through to the ledger.
+ int remainder = amount;
+ if (m_GhostOwnerLookup.HasComponent(projEntity)
+ && playerByConn.TryGetValue(m_GhostOwnerLookup[projEntity].NetworkId, out var player)
+ && m_InvLookup.HasBuffer(player))
+ {
+ int stackMax = Tuning.DefaultStackMax;
+ if (haveDb && itemDb.Value.IsCreated)
+ {
+ ref var itemBlob = ref itemDb.Value.Value;
+ if (itemBlob.TryGetItem(yieldId, out var def) && def.StackMax > 0)
+ stackMax = def.StackMax;
+ }
+ var inv = m_InvLookup[player];
+ remainder = InventoryMath.Deposit(inv, yieldId, amount, stackMax, Tuning.InventoryMaxSlots);
+ }
+
+ // Unresolvable owner or a full bag: the remainder credits the shared ledger (no-loss valve).
+ if (remainder > 0)
+ StorageMath.Deposit(ledger, yieldId, remainder);
int rem = tgtRemaining[bestIdx] - amount;
tgtRemaining[bestIdx] = rem;
ecb.DestroyEntity(projEntity);
@@ -158,6 +198,7 @@ namespace ProjectM.Server
ecb.Playback(state.EntityManager);
ecb.Dispose();
+ playerByConn.Dispose();
destroyed.Dispose();
tgtEntity.Dispose();
tgtPos.Dispose();
diff --git a/Assets/_Project/Scripts/Simulation/Items.meta b/Assets/_Project/Scripts/Simulation/Items.meta
new file mode 100644
index 000000000..52d31ddd8
--- /dev/null
+++ b/Assets/_Project/Scripts/Simulation/Items.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 0d59e1972aec03a4399355f38b87d5be
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Project/Scripts/Simulation/Items/InventoryDepositRequest.cs b/Assets/_Project/Scripts/Simulation/Items/InventoryDepositRequest.cs
new file mode 100644
index 000000000..26a3ca809
--- /dev/null
+++ b/Assets/_Project/Scripts/Simulation/Items/InventoryDepositRequest.cs
@@ -0,0 +1,23 @@
+using Unity.NetCode;
+
+namespace ProjectM.Simulation
+{
+ ///
+ /// Client -> server request to move items from the sender's PERSONAL inventory into the shared base
+ /// stockpile (the global the build/upgrade/automation economy spends from).
+ /// A one-off action, so it is an RPC (not a per-tick predicted input), and the server applies it exactly
+ /// once in the plain SimulationSystemGroup (no rollback double-apply). Payload is plain blittable scalars
+ /// (no entity refs, no enum): the server resolves the sender's player from the RPC's SourceConnection. The
+ /// wire type is UNCONDITIONAL (never #if-gated) so the RpcCollection hash matches across release/dev peers;
+ /// only the send/receive SYSTEMS may be #if-gated.
+ ///
+ public struct InventoryDepositRequest : IRpcCommand
+ {
+ /// Item to deposit, or 0 to deposit EVERYTHING the player is carrying. The server branches on
+ /// 0 BEFORE any per-item withdraw and never writes a 0-id row.
+ public ushort ItemId;
+
+ /// Quantity to deposit; <= 0 means "all of that item" (ignored when ItemId is 0).
+ public int Count;
+ }
+}
diff --git a/Assets/_Project/Scripts/Simulation/Items/InventoryDepositRequest.cs.meta b/Assets/_Project/Scripts/Simulation/Items/InventoryDepositRequest.cs.meta
new file mode 100644
index 000000000..1bf61bf2b
--- /dev/null
+++ b/Assets/_Project/Scripts/Simulation/Items/InventoryDepositRequest.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: a815da9a948230e46bc4f7154887613e
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Simulation/Items/InventoryMath.cs b/Assets/_Project/Scripts/Simulation/Items/InventoryMath.cs
new file mode 100644
index 000000000..92af415e2
--- /dev/null
+++ b/Assets/_Project/Scripts/Simulation/Items/InventoryMath.cs
@@ -0,0 +1,89 @@
+using Unity.Entities;
+
+namespace ProjectM.Simulation
+{
+ ///
+ /// Pure, deterministic stacking logic for a player's buffer (no RNG /
+ /// wall-clock / singleton access, so server and any future prediction agree). Parallels
+ /// in spirit but does NOT collapse into it: StorageMath is an unbounded
+ /// single-row merge, whereas this enforces a per-item stack cap and a max slot count and supports multiple
+ /// stacks of the same item once a stack fills. DynamicBuffer is a handle, so mutations apply to the
+ /// underlying entity buffer; growing it (buffer.Add) is a resize, NOT a structural change, so it is safe to
+ /// call while iterating a different query. Unit-tested in EditMode via a plain Entities world.
+ ///
+ public static class InventoryMath
+ {
+ ///
+ /// Add of : first tops up existing non-full stacks
+ /// of that item, then appends new stacks (each capped at ) while a free slot
+ /// remains (buffer length < ). Returns the REMAINDER that did not fit
+ /// (0 if everything was deposited). No-op (returns 0) for count <= 0; a positive count of itemId 0
+ /// returns the full count (nothing deposited — never writes a 0-id row). stackMax < 1 = unbounded.
+ ///
+ public static int Deposit(DynamicBuffer buffer, ushort itemId, int count, int stackMax, int maxSlots)
+ {
+ if (count <= 0) return 0;
+ if (itemId == 0) return count;
+ if (stackMax < 1) stackMax = int.MaxValue;
+
+ // Top up existing stacks of this item.
+ for (int i = 0; i < buffer.Length && count > 0; i++)
+ {
+ if (buffer[i].ItemId != itemId) continue;
+ var e = buffer[i];
+ int space = stackMax - e.Count;
+ if (space <= 0) continue;
+ int add = space < count ? space : count;
+ e.Count += add;
+ buffer[i] = e;
+ count -= add;
+ }
+
+ // Append new stacks while a slot is free.
+ while (count > 0 && buffer.Length < maxSlots)
+ {
+ int add = stackMax < count ? stackMax : count;
+ buffer.Add(new InventorySlot { ItemId = itemId, Count = add });
+ count -= add;
+ }
+
+ return count;
+ }
+
+ ///
+ /// Remove up to of across all its stacks, clamped to
+ /// what is available; drops a stack that reaches zero. Returns the amount actually withdrawn (0 if none).
+ /// Iterates back-to-front so RemoveAt does not skip a stack. No-op for count <= 0 or itemId 0.
+ ///
+ public static int Withdraw(DynamicBuffer buffer, ushort itemId, int count)
+ {
+ if (count <= 0 || itemId == 0) return 0;
+
+ int taken = 0;
+ for (int i = buffer.Length - 1; i >= 0 && count > 0; i--)
+ {
+ if (buffer[i].ItemId != itemId) continue;
+ var e = buffer[i];
+ int t = e.Count < count ? e.Count : count;
+ e.Count -= t;
+ taken += t;
+ count -= t;
+ if (e.Count <= 0)
+ buffer.RemoveAt(i);
+ else
+ buffer[i] = e;
+ }
+ return taken;
+ }
+
+ /// Total quantity of across all stacks (0 if absent).
+ public static int CountOf(DynamicBuffer buffer, ushort itemId)
+ {
+ int total = 0;
+ for (int i = 0; i < buffer.Length; i++)
+ if (buffer[i].ItemId == itemId)
+ total += buffer[i].Count;
+ return total;
+ }
+ }
+}
diff --git a/Assets/_Project/Scripts/Simulation/Items/InventoryMath.cs.meta b/Assets/_Project/Scripts/Simulation/Items/InventoryMath.cs.meta
new file mode 100644
index 000000000..e46f72a25
--- /dev/null
+++ b/Assets/_Project/Scripts/Simulation/Items/InventoryMath.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: c8560f6c2e717b943bed78d40ea87404
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Simulation/Items/InventorySlot.cs b/Assets/_Project/Scripts/Simulation/Items/InventorySlot.cs
new file mode 100644
index 000000000..bb9f84dc6
--- /dev/null
+++ b/Assets/_Project/Scripts/Simulation/Items/InventorySlot.cs
@@ -0,0 +1,35 @@
+using Unity.Entities;
+using Unity.NetCode;
+
+namespace ProjectM.Simulation
+{
+ ///
+ /// One (item, count) row in a player's PERSONAL inventory. The per-player DynamicBuffer of these is the
+ /// server-authoritative source of what that player is carrying. A structural twin of
+ /// : a [GhostField] buffer with so the owning
+ /// (predicting) client receives its own inventory — without it the owner, being the owner, would not get
+ /// the owner-typed buffer at all and the HUD would read empty. BOTH fields carry [GhostField]; the
+ /// [GhostComponent] attribute alone does NOT auto-replicate fields (an un-annotated field ships as a
+ /// silent zero), so the annotations mirror field-for-field.
+ ///
+ /// REPLICATION DISCIPLINE — the ONLY writers are server-only:
+ /// (harvest yield) and the deposit-to-base RPC handler, both in the plain server SimulationSystemGroup. So
+ /// there is no predicted-loop double-apply and the owner never mispredicts its inventory — it is a pure
+ /// server-authored snapshot. NEVER mutate this from a client predicted system (that would reintroduce a
+ /// double-apply / mispredict path). ItemId is the same opaque ushort id space as
+ /// and the catalog.
+ ///
+ /// NOTE: adding this [GhostField] buffer CHANGES the player ghost serialization hash — the player prefab /
+ /// subscene MUST be re-baked (consistently in both worlds) or the connect handshake desyncs.
+ ///
+ [GhostComponent(OwnerSendType = SendToOwnerType.All)]
+ [InternalBufferCapacity(24)]
+ public struct InventorySlot : IBufferElementData
+ {
+ /// Item carried in this slot (0 = empty/unused; aligns with InventoryMath's 0-id no-op).
+ [GhostField] public ushort ItemId;
+
+ /// Quantity in this slot (bounded by the item's StackMax when deposited via InventoryMath).
+ [GhostField] public int Count;
+ }
+}
diff --git a/Assets/_Project/Scripts/Simulation/Items/InventorySlot.cs.meta b/Assets/_Project/Scripts/Simulation/Items/InventorySlot.cs.meta
new file mode 100644
index 000000000..c572f67be
--- /dev/null
+++ b/Assets/_Project/Scripts/Simulation/Items/InventorySlot.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: f151c780df9917d4089b2944f3ffb12d
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Simulation/Items/ItemCategory.cs b/Assets/_Project/Scripts/Simulation/Items/ItemCategory.cs
new file mode 100644
index 000000000..e88978457
--- /dev/null
+++ b/Assets/_Project/Scripts/Simulation/Items/ItemCategory.cs
@@ -0,0 +1,26 @@
+namespace ProjectM.Simulation
+{
+ ///
+ /// Broad item-category ids (a byte, not an enum, per the cross-assembly enum-in-Burst hazard that
+ /// already de-Bursted ProjectileClassificationSystem). The category lets systems and UI treat an item
+ /// generically (a resource stacks and is spendable at the base; a tool/weapon is equippable; a
+ /// consumable is used) without a per-id switch. Stored in the ItemDatabase blob's .
+ ///
+ public static class ItemCategory
+ {
+ /// Stackable raw material (Aether/Ore/Biomass). Spendable at the base / for crafting.
+ public const byte Resource = 0;
+
+ /// Gathering tool (axe/pickaxe). Equippable; gates + scales harvesting (Phase 2).
+ public const byte Tool = 1;
+
+ /// Weapon. Equipping it grants its ability + stat modifiers (Phase 1).
+ public const byte Weapon = 2;
+
+ /// Wearable gear (armour/trinket). Equipping it grants stat modifiers (Phase 1).
+ public const byte Gear = 3;
+
+ /// One-shot consumable (potion/charge). Used from the inventory (later phase).
+ public const byte Consumable = 4;
+ }
+}
diff --git a/Assets/_Project/Scripts/Simulation/Items/ItemCategory.cs.meta b/Assets/_Project/Scripts/Simulation/Items/ItemCategory.cs.meta
new file mode 100644
index 000000000..3e2f36f17
--- /dev/null
+++ b/Assets/_Project/Scripts/Simulation/Items/ItemCategory.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: fa1718754184d2a418b1099ef7e3ae34
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Simulation/Items/ItemDatabase.cs b/Assets/_Project/Scripts/Simulation/Items/ItemDatabase.cs
new file mode 100644
index 000000000..11e185d41
--- /dev/null
+++ b/Assets/_Project/Scripts/Simulation/Items/ItemDatabase.cs
@@ -0,0 +1,17 @@
+using Unity.Entities;
+
+namespace ProjectM.Simulation
+{
+ ///
+ /// Singleton handle to the baked item-definition database (config, not replicated — baked identically
+ /// into both worlds from the gameplay subscene, exactly like ). A distinct
+ /// component type, so GetSingleton<ItemDatabase>() resolves independently of the ability
+ /// database (singleton-ness is per type). Optional at runtime: consumers that read it use
+ /// TryGetSingleton and fall back to defaults (e.g. Tuning.DefaultStackMax) so the sim still
+ /// runs before the catalog is authored.
+ ///
+ public struct ItemDatabase : IComponentData
+ {
+ public BlobAssetReference Value;
+ }
+}
diff --git a/Assets/_Project/Scripts/Simulation/Items/ItemDatabase.cs.meta b/Assets/_Project/Scripts/Simulation/Items/ItemDatabase.cs.meta
new file mode 100644
index 000000000..34e81bb20
--- /dev/null
+++ b/Assets/_Project/Scripts/Simulation/Items/ItemDatabase.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 6b355888562be7349b8754168375db9b
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Simulation/Items/ItemDatabaseBlob.cs b/Assets/_Project/Scripts/Simulation/Items/ItemDatabaseBlob.cs
new file mode 100644
index 000000000..eda274d99
--- /dev/null
+++ b/Assets/_Project/Scripts/Simulation/Items/ItemDatabaseBlob.cs
@@ -0,0 +1,69 @@
+using Unity.Collections;
+using Unity.Entities;
+
+namespace ProjectM.Simulation
+{
+ ///
+ /// One authored item definition, baked immutable into the blob. This is the
+ /// single source of truth for everything an item IS — resources, tools, weapons, gear, consumables — so
+ /// adding game content is an authoring row + re-bake, with no code change. Id space is the SAME
+ /// ushort space as / , and it
+ /// SUBSUMES the low byte ids (Aether=1/Ore=2/Biomass=3) — a resource is just a
+ /// low-id item of . KEEP ids 1-3 stable for the existing resources and
+ /// reserve new item ids > 3; 0 = none. Entity/prefab refs do NOT live here (blobs don't remap entity
+ /// refs) — a future companion buffer carries those, exactly like AbilityPrefabElement.
+ ///
+ /// The blob is config (baked identically into both worlds, NOT replicated, NOT in SaveData), so growing
+ /// this struct later (a granted-ability id, a StatModifier-spec array, a slot id for Phase 1/2/3) is a
+ /// pure re-bake with zero migration: no SaveData version bump, no ghost-hash change, no desync.
+ /// is baked NOW because the project's progression axis is gear tiers, so Phase 2/3 tier gating is a
+ /// content-only edit.
+ ///
+ public struct ItemDefBlob
+ {
+ /// Stable item id (ushort; 1-3 reserved for the existing resources, keep stable for saves).
+ public ushort ItemId;
+
+ /// Broad category (see ), stored as a byte.
+ public byte Category;
+
+ /// Progression tier (0 = base). Higher-tier tools harvest higher-tier nodes / hit harder (Phase 2/3).
+ public byte Tier;
+
+ /// Max units that stack in a single inventory slot (1 for non-stacking equipment).
+ public int StackMax;
+
+ /// Designer-facing display name (shown in the HUD inventory panel).
+ public FixedString64Bytes Name;
+ }
+
+ ///
+ /// Immutable designer-authored item database, baked from ScriptableObjects to a blob asset and shared by
+ /// every entity (Burst-fast, zero per-instance cost). Looked up by stable
+ /// — ID-KEYED, never by array index, so inserting a new item never renumbers existing ids.
+ ///
+ /// NOTE: the lookup is intentionally NOT a 'readonly' method. A readonly struct method forces a defensive
+ /// copy of a field when calling a non-readonly member on it; copying a BlobArray breaks its relative-offset
+ /// pointer, so the array would read as empty. A plain (non-readonly) method accesses the BlobArray in place.
+ /// Always reach this through 'ref blob.Value' (mirrors ).
+ ///
+ public struct ItemDatabaseBlob
+ {
+ public BlobArray Items;
+
+ /// Linear lookup by item id (the array is tiny). Returns false if not present.
+ public bool TryGetItem(ushort id, out ItemDefBlob def)
+ {
+ for (int i = 0; i < Items.Length; i++)
+ {
+ if (Items[i].ItemId == id)
+ {
+ def = Items[i];
+ return true;
+ }
+ }
+ def = default;
+ return false;
+ }
+ }
+}
diff --git a/Assets/_Project/Scripts/Simulation/Items/ItemDatabaseBlob.cs.meta b/Assets/_Project/Scripts/Simulation/Items/ItemDatabaseBlob.cs.meta
new file mode 100644
index 000000000..46dde8b2e
--- /dev/null
+++ b/Assets/_Project/Scripts/Simulation/Items/ItemDatabaseBlob.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 0ae0cc6bfb8578c42bff8e300078cf2d
\ No newline at end of file
diff --git a/Assets/_Project/Scripts/Simulation/Tuning.cs b/Assets/_Project/Scripts/Simulation/Tuning.cs
index a62be4e5d..ee597812f 100644
--- a/Assets/_Project/Scripts/Simulation/Tuning.cs
+++ b/Assets/_Project/Scripts/Simulation/Tuning.cs
@@ -56,5 +56,13 @@ namespace ProjectM.Simulation
/// Max production cycles a single machine awards in one process (bounds within-session
/// catch-up after any skipped ticks; restore re-seats the baseline so this never reflects wall-clock).
public const int MaxProductionCatchup = 600;
+
+ // ---- Inventory (per-player bag; InventoryMath / ResourceHarvestSystem / InventoryDepositSystem) ----
+
+ /// Max stacks a player can carry; InventoryMath rejects deposits past this and the harvest remainder spills to the global ledger.
+ public const int InventoryMaxSlots = 24;
+
+ /// Default per-slot stack cap when an item has no ItemDatabase entry (the catalog is optional at runtime).
+ public const int DefaultStackMax = 999;
}
}
diff --git a/Assets/_Project/Subscenes/Gameplay.unity b/Assets/_Project/Subscenes/Gameplay.unity
index 3020b02c6..749e47058 100644
--- a/Assets/_Project/Subscenes/Gameplay.unity
+++ b/Assets/_Project/Subscenes/Gameplay.unity
@@ -1449,6 +1449,55 @@ BoxCollider:
serializedVersion: 3
m_Size: {x: 3, y: 2.5, z: 3}
m_Center: {x: 0, y: 1.25, z: 0}
+--- !u!1 &722706768
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 722706770}
+ - component: {fileID: 722706769}
+ m_Layer: 0
+ m_Name: ItemDatabase
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!114 &722706769
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 722706768}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 5ee44dc3bc9f3164592195d4068be8d1, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: ProjectM.Authoring::ProjectM.Authoring.ItemDatabaseAuthoring
+ Items:
+ - {fileID: 11400000, guid: 938a882b6fbd50d4c9a051ddabc5829b, type: 2}
+ - {fileID: 11400000, guid: a2a0e6d3ae0218d458d2b5305891ce89, type: 2}
+ - {fileID: 11400000, guid: 4ed399a329eb6d847921aef05bde213d, type: 2}
+ - {fileID: 11400000, guid: 31ec40cfc6e81ef4ab9bbce57a40621e, type: 2}
+--- !u!4 &722706770
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 722706768}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &842917524
GameObject:
m_ObjectHideFlags: 0
@@ -3685,3 +3734,4 @@ SceneRoots:
- {fileID: 1698085403}
- {fileID: 450973138}
- {fileID: 1268920429}
+ - {fileID: 722706770}