Files
Emberwild/Assets/GAME/Script/Inventory/SlotTransfer.cs
T
2026-07-22 12:56:13 +02:00

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;
}
}
}