Files
Project-M/Assets/_Project/Scripts/Simulation/Items/InventoryMath.cs
T
kronic 43f355c06b Equipment: weapon-granted abilities + gear mods (DR-027 Phase 1)
Equipment slots reusing the AbilityRef/StatModifier machinery: EquipmentSlot [GhostField] buffer (index=slot), server-only event-driven EquipSystem (RPC). Weapon -> AbilityRef.Id swaps the attack (prefab + base stats, prediction-correct); gear -> StatModifiers tagged a reserved per-slot EquipSourceId, stripped target-agnostically via RemoveBySourceId. Item mods are INLINE on ItemDefBlob (a nested BlobArray reads empty under the by-value TryGetItem copy). Atomic equip-over swap (no item loss); DefaultAbility restores the unarmed ability on weapon-unequip. Client keys + build-safe hooks; HUD equipment panel + click-to-equip. 4 catalog weapon/gear items wired + re-baked.

Play-validated host+client: weapon equip swaps AbilityRef on both worlds, gear folds into EffectiveCharacterStats, unequip reverses + restores DefaultAbility, all replicated to the owner. See DR-027.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 11:09:25 -07:00

111 lines
5.0 KiB
C#

using Unity.Entities;
namespace ProjectM.Simulation
{
/// <summary>
/// Pure, deterministic stacking logic for a player's <see cref="InventorySlot"/> buffer (no RNG /
/// wall-clock / singleton access, so server and any future prediction agree). Parallels
/// <see cref="StorageMath"/> 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.
/// </summary>
public static class InventoryMath
{
/// <summary>
/// Add <paramref name="count"/> of <paramref name="itemId"/>: first tops up existing non-full stacks
/// of that item, then appends new stacks (each capped at <paramref name="stackMax"/>) while a free slot
/// remains (buffer length &lt; <paramref name="maxSlots"/>). Returns the REMAINDER that did not fit
/// (0 if everything was deposited). No-op (returns 0) for count &lt;= 0; a positive count of itemId 0
/// returns the full count (nothing deposited — never writes a 0-id row). stackMax &lt; 1 = unbounded.
/// </summary>
public static int Deposit(DynamicBuffer<InventorySlot> 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;
}
/// <summary>
/// Remove up to <paramref name="count"/> of <paramref name="itemId"/> 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 &lt;= 0 or itemId 0.
/// </summary>
public static int Withdraw(DynamicBuffer<InventorySlot> 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;
}
/// <summary>
/// Non-mutating check: would depositing <paramref name="count"/> of <paramref name="itemId"/> FULLY fit
/// (top-up existing stacks + new stacks within <paramref name="maxSlots"/>)? Used by the equip swap to
/// guarantee the swapped-out item has room BEFORE any withdrawal (no item loss). Mirrors Deposit's space math.
/// </summary>
public static bool CanDeposit(DynamicBuffer<InventorySlot> buffer, ushort itemId, int count, int stackMax, int maxSlots)
{
if (count <= 0) return true;
if (itemId == 0) return false;
if (stackMax < 1) stackMax = int.MaxValue;
long space = 0;
for (int i = 0; i < buffer.Length; i++)
if (buffer[i].ItemId == itemId)
space += stackMax - buffer[i].Count;
int freeSlots = maxSlots - buffer.Length;
if (freeSlots > 0)
space += (long)freeSlots * stackMax;
return space >= count;
}
/// <summary>Total quantity of <paramref name="itemId"/> across all stacks (0 if absent).</summary>
public static int CountOf(DynamicBuffer<InventorySlot> 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;
}
}
}