106 lines
4.4 KiB
C#
106 lines
4.4 KiB
C#
using UnityEngine;
|
|
|
|
namespace Ashwild.Inventory
|
|
{
|
|
/// <summary>
|
|
/// The single source of truth for what happens when a stack meets another stack. Both rules below
|
|
/// are pure — they know nothing about containers, networking or authority — so the exact same
|
|
/// decision runs for an inventory-to-inventory drag (client-side), a chest-to-chest drag
|
|
/// (server-side) and an inventory-to-chest drag (server-side, against the payload in flight).
|
|
/// That is why a chest transfer behaves identically to moving an item inside the inventory.
|
|
///
|
|
/// There are exactly two operations, because the player expresses exactly two intents:
|
|
/// <see cref="Move"/> when they aim at a precise destination, and <see cref="TryStack"/> when they
|
|
/// let the game find a free spot (quick transfer, deposit-all, pickups).
|
|
/// </summary>
|
|
public static class SlotTransfer
|
|
{
|
|
/// <summary>
|
|
/// Moves a stack onto a destination the player explicitly aimed at. Empty destination = the whole
|
|
/// stack moves; same mergeable item = merge and leave the remainder in the source; anything else
|
|
/// (different item, non-stackable, uses-tracked, or a full stack) = swap the two slots. Uses ride
|
|
/// along with the content in every branch, so a worn item stays worn.
|
|
/// Returns whether anything actually changed.
|
|
/// </summary>
|
|
public static bool Move(ref SlotContent source, ref SlotContent target)
|
|
{
|
|
if (source.IsEmpty) return false;
|
|
|
|
if (target.IsEmpty)
|
|
{
|
|
target = source;
|
|
source = SlotContent.Empty;
|
|
return true;
|
|
}
|
|
|
|
if (CanMerge(source, target))
|
|
{
|
|
int space = target.Item.MaxStackSize - target.Quantity;
|
|
int moved = Mathf.Min(source.Quantity, space);
|
|
|
|
target.Quantity += moved;
|
|
source.Quantity -= moved;
|
|
if (source.Quantity <= 0) source = SlotContent.Empty;
|
|
|
|
return moved > 0;
|
|
}
|
|
|
|
SlotContent temp = source;
|
|
source = target;
|
|
target = temp;
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Auto-placement: pushes as much of <paramref name="incoming"/> into the target as it can accept,
|
|
/// and never swaps — the player did not pick this slot, so displacing what is already there would
|
|
/// be wrong. An empty target takes one instance of a uses-tracked item (each keeps its own uses
|
|
/// bar) or up to a full stack otherwise. Returns whether the target changed, leaving the leftover
|
|
/// in <paramref name="incoming"/> for the caller to keep placing.
|
|
/// </summary>
|
|
public static bool TryStack(ref SlotContent incoming, ref SlotContent target)
|
|
{
|
|
if (incoming.IsEmpty) return false;
|
|
|
|
ItemData item = incoming.Item;
|
|
|
|
if (target.IsEmpty)
|
|
{
|
|
int toPlace = item.HasUses || !item.IsStackable
|
|
? 1
|
|
: Mathf.Min(incoming.Quantity, item.MaxStackSize);
|
|
|
|
target = SlotContent.Of(item, toPlace, incoming.Uses);
|
|
incoming.Quantity -= toPlace;
|
|
if (incoming.Quantity <= 0) incoming = SlotContent.Empty;
|
|
return true;
|
|
}
|
|
|
|
if (!CanMerge(incoming, target)) return false;
|
|
|
|
int space = target.Item.MaxStackSize - target.Quantity;
|
|
int moved = Mathf.Min(incoming.Quantity, space);
|
|
if (moved <= 0) return false;
|
|
|
|
target.Quantity += moved;
|
|
incoming.Quantity -= moved;
|
|
if (incoming.Quantity <= 0) incoming = SlotContent.Empty;
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Two stacks merge only when they are the same stackable item that does not track uses and the
|
|
/// target still has room. Uses-tracked items never merge: each instance owns its uses bar, so
|
|
/// merging them would silently destroy one item's wear.
|
|
/// </summary>
|
|
private static bool CanMerge(SlotContent source, SlotContent target)
|
|
{
|
|
ItemData item = source.Item;
|
|
return target.Item == item
|
|
&& item.IsStackable
|
|
&& !item.HasUses
|
|
&& target.Quantity < item.MaxStackSize;
|
|
}
|
|
}
|
|
}
|