diff --git a/scripts/inventory/IItemContainer.cs b/scripts/inventory/IItemContainer.cs
index 5adcedb..d68cab5 100644
--- a/scripts/inventory/IItemContainer.cs
+++ b/scripts/inventory/IItemContainer.cs
@@ -3,6 +3,7 @@ using System.Collections;
using System.Collections.Generic;
using ColdMint.scripts.item;
+using ColdMint.scripts.item.itemStacks;
using Godot;
diff --git a/scripts/inventory/ItemSlotNode.cs b/scripts/inventory/ItemSlotNode.cs
index f6b27d4..4cf060b 100644
--- a/scripts/inventory/ItemSlotNode.cs
+++ b/scripts/inventory/ItemSlotNode.cs
@@ -1,4 +1,5 @@
using ColdMint.scripts.item;
+using ColdMint.scripts.item.itemStacks;
using ColdMint.scripts.utils;
using Godot;
@@ -256,7 +257,7 @@ public partial class ItemSlotNode : MarginContainer
var debugText = TranslationServerUtils.Translate("item_prompt_debug");
if (debugText != null)
{
- _control.TooltipText = string.Format(debugText, _itemStack.Id,
+ _control.TooltipText = string.Format(debugText, _itemStack.GetItem()?.Id,
TranslationServerUtils.Translate(_itemStack.Name),
_itemStack.Quantity, _itemStack.MaxQuantity, _itemStack.GetType().Name,
TranslationServerUtils.Translate(_itemStack.Description));
diff --git a/scripts/inventory/Packsack.cs b/scripts/inventory/Packsack.cs
index bd517da..3e2a42b 100644
--- a/scripts/inventory/Packsack.cs
+++ b/scripts/inventory/Packsack.cs
@@ -37,6 +37,8 @@ public partial class Packsack : RigidBody2D, IItem
QueueFree();
}
+ public bool CanStackWith(IItem item) => false;
+
private IItemContainer? _itemContainer;
public override void _Ready()
diff --git a/scripts/inventory/UniversalItemContainer.cs b/scripts/inventory/UniversalItemContainer.cs
index 998cda1..da798f7 100644
--- a/scripts/inventory/UniversalItemContainer.cs
+++ b/scripts/inventory/UniversalItemContainer.cs
@@ -6,6 +6,7 @@ using System.Linq;
using ColdMint.scripts.character;
using ColdMint.scripts.debug;
using ColdMint.scripts.item;
+using ColdMint.scripts.item.itemStacks;
using ColdMint.scripts.utils;
using Godot;
diff --git a/scripts/item/ICommonItem.cs b/scripts/item/ICommonItem.cs
new file mode 100644
index 0000000..4f50c0e
--- /dev/null
+++ b/scripts/item/ICommonItem.cs
@@ -0,0 +1,28 @@
+using ColdMint.scripts.item.itemStacks;
+
+namespace ColdMint.scripts.item;
+
+///
+/// Special item interface that make item common, which means will stack in a
+/// 该特殊的物品接口使得物品成为平凡物品,换言之,将会堆叠在中。
+///
+///
+/// Make this the class itself
+/// 应当为当前类自身
+///
+///
+///
+/// Notice when you implement: To avoid unexpected behavior, unless you understand what you're doing, the method
+/// of an item that implements the interface must only match its own exact same instance.
+///
+/// 实现时注意:为避免意外行为,除非你明白自己在做什么,否则实现接口的物品的方法必须只和自己完全相同的实例匹配。
+///
+public interface ICommonItem : IItem
+{
+ ///
+ /// Method to copy an instance same with self. Will be used to pick out item instance from a
+ /// 复制与自身相同的实例的方法。将用于从 中拿取新的物品实例。
+ ///
+ ///
+ ICommonItem CopyInstance();
+}
\ No newline at end of file
diff --git a/scripts/item/IItem.cs b/scripts/item/IItem.cs
index 8c9efc5..fe49c47 100644
--- a/scripts/item/IItem.cs
+++ b/scripts/item/IItem.cs
@@ -1,4 +1,6 @@
-using Godot;
+using ColdMint.scripts.item.itemStacks;
+
+using Godot;
namespace ColdMint.scripts.item;
@@ -32,4 +34,41 @@ public interface IItem
/// Execute when current item be removed from game.
///
void Destroy();
+
+ ///
+ /// Return true if this item can be stacked with the given item in one stack
+ /// 若该物品是否能与给定物品堆叠在同一个物品堆上,返回true
+ ///
+ ///
+ ///
+ /// ! Note in the implementation: the correspondence of this predicate must be an equivalence relation over the full set of stackable items
+ /// (i.e., be able to derive a division into the full set of stackable items).
+ /// Or if the item is not stackable, make it always return false.
+ ///
+ ///
+ /// !实现时注意:该谓词的对应关系必须是在全部可堆叠的物品集合上的等价关系(即能导出一个对可堆叠物品全集的划分)。
+ /// 或如果该物品不可堆叠,令其永远返回false。
+ ///
+ ///
+ /// If it is necessary to implement special stacking relationships (e.g. containers that can be stacked to hold items),
+ /// customize the special type, implement the special stacking determination in it by overriding ,
+ /// and override the method so that it returns an instance of that custom ItemStack.
+ ///
+ ///
+ /// 如果有必要实现特殊的堆叠关系(如可以用堆叠来收纳物品的容器),请自定义特殊的类型,在其中重写以实现特殊的堆叠判定,
+ /// 并重写方法使其返回该自定义ItemStack实例。
+ ///
+ ///
+ bool CanStackWith(IItem item);
+
+ ///
+ /// If this item need a special stack type, return the special item stack instance that contains the item. If else, just leave this null.
+ /// 如果该项目需要特殊的物品堆类型,重写此方法来返回包含该物品的特殊物品堆实例。否则,保留原本的null返回值。
+ ///
+ ///
+ /// DO NOT use this method to create stack from item, use instead
+ /// **不要**使用此方法从一个物品创建堆,请使用 。
+ ///
+ ///
+ IItemStack? SpecialStack() => null;
}
\ No newline at end of file
diff --git a/scripts/item/itemStacks/CommonItemStack.cs b/scripts/item/itemStacks/CommonItemStack.cs
new file mode 100644
index 0000000..443790e
--- /dev/null
+++ b/scripts/item/itemStacks/CommonItemStack.cs
@@ -0,0 +1,89 @@
+using System;
+
+using Godot;
+
+namespace ColdMint.scripts.item.itemStacks;
+
+///
+///
+/// one of the basic item stacks, where there is only one instance of an item actually held in the stack,
+/// meaning that all items are identical (or completely random in some ways)
+///
+/// 平凡物品堆,基础物品堆之一,堆中实际保存的物品实例仅有一个,意味着所有物品都完全一致(或某些方面完全随机)
+///
+///
+///
+public class CommonItemStack(ICommonItem innerItem) : IItemStack
+{
+ public int MaxQuantity { get; } = ItemTypeManager.MaxStackQuantityOf(innerItem.Id);
+ public int Quantity { get; private set; } = 1;
+ public bool Empty => Quantity == 0;
+ public Texture2D Icon => innerItem.Icon;
+ public string Name => $"{innerItem.Name}({Quantity})";
+ public string? Description => innerItem.Description;
+
+ public bool CanAddItem(IItem item1)
+ {
+ return innerItem.CanStackWith(item1) && (Quantity < MaxQuantity);
+ }
+
+ public bool AddItem(IItem item)
+ {
+ if (!CanAddItem(item)) return false;
+ Quantity++;
+ return true;
+ }
+
+ public int CanTakeFrom(IItemStack itemStack)
+ {
+ if (itemStack.Empty || !innerItem.CanStackWith(itemStack.GetItem()!)) return 0;
+ return Math.Min(itemStack.Quantity, MaxQuantity - Quantity);
+ }
+
+ public bool TakeFrom(IItemStack itemStack)
+ {
+ var number = CanTakeFrom(itemStack);
+ itemStack.RemoveItem(number);
+ Quantity += number;
+ return itemStack.Empty;
+ }
+
+ public IItem? GetItem()
+ {
+ return Empty ? null : innerItem;
+ }
+
+ public IItem? PickItem()
+ {
+ if (Empty) return null;
+ Quantity--;
+ if (Empty) innerItem.Destroy();
+ return innerItem.CopyInstance();
+ }
+
+ public IItemStack? PickItems(int value)
+ {
+ if (Empty) return null;
+ var result = new CommonItemStack(innerItem.CopyInstance());
+ var n = Math.Min(Quantity, value);
+ result.Quantity = n;
+ Quantity -= n;
+ if (Empty) innerItem.Destroy();
+ return result;
+ }
+
+ public int RemoveItem(int number)
+ {
+ var n = Math.Min(number, Quantity);
+ Quantity -= n;
+ if (Empty) innerItem.Destroy();
+ return number - n;
+ }
+
+ public void ClearStack()
+ {
+ if (Empty) return;
+ Quantity = 0;
+ innerItem.Destroy();
+ }
+}
\ No newline at end of file
diff --git a/scripts/item/IItemStack.cs b/scripts/item/itemStacks/IItemStack.cs
similarity index 89%
rename from scripts/item/IItemStack.cs
rename to scripts/item/itemStacks/IItemStack.cs
index e5ab606..8e77872 100644
--- a/scripts/item/IItemStack.cs
+++ b/scripts/item/itemStacks/IItemStack.cs
@@ -1,20 +1,14 @@
using System;
-using System.Diagnostics.CodeAnalysis;
using Godot;
-namespace ColdMint.scripts.item;
+namespace ColdMint.scripts.item.itemStacks;
///
/// Item stack in an inventory slot
///
public interface IItemStack
{
- ///
- /// ID of items inside current stack
- ///
- string Id { get; }
-
///
/// Max number of current stack
///
@@ -25,6 +19,11 @@ public interface IItemStack
///
int Quantity { get; }
+ ///
+ /// True if this stack is empty
+ ///
+ bool Empty { get; }
+
///
/// Icon of current item
///
@@ -134,10 +133,12 @@ public interface IItemStack
///
/// Create a new ItemStack with the given item as the first item
///
- public static IItemStack FromItem(IItem item) => ItemTypeManager.MaxStackQuantityOf(item.Id) switch
- {
- 1 => new SingleItemStack(item),
- > 1 => throw new NotImplementedException(),
- var other => throw new ArgumentException($"item {item} of type '{item.Id}' has unexpected max stack quantity {other}")
- };
+ public static IItemStack FromItem(IItem item) =>
+ item.SpecialStack() ??
+ ItemTypeManager.MaxStackQuantityOf(item.Id) switch
+ {
+ 1 => new SingleItemStack(item),
+ > 1 => item is ICommonItem commonItem ? new CommonItemStack(commonItem) : new UniqueItemStack(item),
+ var other => throw new ArgumentException($"item {item} of type '{item.Id}' has unexpected max stack quantity {other}")
+ };
}
\ No newline at end of file
diff --git a/scripts/item/SingleItemStack.cs b/scripts/item/itemStacks/SingleItemStack.cs
similarity index 57%
rename from scripts/item/SingleItemStack.cs
rename to scripts/item/itemStacks/SingleItemStack.cs
index a384172..61fc628 100644
--- a/scripts/item/SingleItemStack.cs
+++ b/scripts/item/itemStacks/SingleItemStack.cs
@@ -1,22 +1,21 @@
using System;
-using ColdMint.scripts.inventory;
-
using Godot;
-namespace ColdMint.scripts.item;
+namespace ColdMint.scripts.item.itemStacks;
///
-/// Item stack of single item
+/// One of the basic item stacks, there are always one item in stack
+/// 单身狗物品堆,基础物品堆之一,堆中永远只会有一个物品
///
-//maybe we'd move this into inventory namespace
+///
public class SingleItemStack(IItem item) : IItemStack
{
public IItem Item { get; init; } = item;
- public string Id => Item.Id;
public int MaxQuantity => 1;
- public int Quantity { get; set; } = 1;
+ public int Quantity => 1;
+ public bool Empty { get; private set; } = false;
public Texture2D Icon => Item.Icon;
public string Name => Item.Name;
public string? Description => Item.Description;
@@ -31,29 +30,28 @@ public class SingleItemStack(IItem item) : IItemStack
public IItem? GetItem()
{
- return Quantity == 1 ? Item : null;
+ return Empty ? null : Item;
}
public IItem? PickItem()
{
- Quantity = 0;
+ if (Empty) return null;
+ Empty = true;
return Item;
}
public IItemStack? PickItems(int value)
{
- if (value == 0) return null;
- else
- {
- Quantity = 0;
- return new SingleItemStack(Item);
- }
+ if (value == 0 || Empty) return null;
+
+ Empty = true;
+ return new SingleItemStack(Item);
}
public int RemoveItem(int number)
{
- if (number == 0) return 0;
- Quantity = 0;
+ if (number == 0 || Empty) return 0;
+ Empty = true;
Item.Destroy();
return Math.Max(number - 1, 0);
}
diff --git a/scripts/item/itemStacks/UniqueItemStack.cs b/scripts/item/itemStacks/UniqueItemStack.cs
new file mode 100644
index 0000000..7f9af3f
--- /dev/null
+++ b/scripts/item/itemStacks/UniqueItemStack.cs
@@ -0,0 +1,123 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+using Godot;
+
+namespace ColdMint.scripts.item.itemStacks;
+
+///
+/// One of the basic item stacks, where each item in the stack maintains its original state (allowing for items that are not identical to each other)
+/// 独特物品堆,基础物品堆之一,堆中的每一个物品会保持各自原本的状态(允许互不相同的物品存在)
+///
+///
+public class UniqueItemStack : IItemStack
+{
+ private readonly Stack _items;
+
+ public UniqueItemStack(IItem item)
+ {
+ _items = [];
+ _items.Push(item);
+ MaxQuantity = ItemTypeManager.MaxStackQuantityOf(item.Id);
+ }
+
+ private UniqueItemStack(UniqueItemStack from)
+ {
+ _items = from._items;
+ MaxQuantity = from.MaxQuantity;
+ Quantity = from.Quantity;
+ from.Quantity = 0;
+ }
+
+ public int MaxQuantity { get; }
+ public int Quantity { get; private set; } = 1;
+ public bool Empty => Quantity == 0;
+ public Texture2D Icon => GetItem()?.Icon ?? new PlaceholderTexture2D();
+ public string Name => $"{GetItem()?.Name}({Quantity})";
+ public string? Description => GetItem()?.Description;
+
+ public bool CanAddItem(IItem item)
+ {
+ //当两个物品相容,且当前堆未满时,我们返回true
+ return (GetItem()?.CanStackWith(item) ?? false) && (Quantity < MaxQuantity);
+ }
+
+ public bool AddItem(IItem item)
+ {
+ if (!CanAddItem(item)) return false;
+ _items.Push(item);
+ Quantity++;
+ return true;
+ }
+
+ public int CanTakeFrom(IItemStack itemStack)
+ {
+ if (!(itemStack.GetItem() is { } item)) return 0;
+ //如果两个物品相容,那么可以获取的数量取决于当前物品堆空位的大小和对方物品数量中较小的一方
+ if (CanAddItem(item))
+ return Mathf.Min(itemStack.Quantity, MaxQuantity - Quantity);
+ else return 0;
+ }
+
+ public bool TakeFrom(IItemStack itemStack)
+ {
+ var value = CanTakeFrom(itemStack);
+ for (int i = 0; i < value; i++)
+ {
+ //一个如果代码没有出错就用不到的安全检查
+ if (!AddItem(itemStack.PickItem()!)) break;
+ }
+
+ return itemStack.Empty;
+ }
+
+ public IItem? GetItem()
+ {
+ return Empty ? null : _items.Peek();
+ }
+
+ public IItem? PickItem()
+ {
+ if (Empty) return null;
+ Quantity--;
+ return _items.Pop();
+ }
+
+ public IItemStack? PickItems(int value)
+ {
+ if (Empty) return null;
+
+ if (value < 0) value = Quantity;
+
+ var result = new UniqueItemStack(PickItem()!);
+ //计算剩余的要取出的数量
+ var restToMove = Math.Min(value - 1, Quantity);
+ for (int i = 0; i < restToMove; i++)
+ {
+ result.AddItem(PickItem()!);
+ }
+
+ return result;
+ }
+
+ public int RemoveItem(int number)
+ {
+ if (number < 0) number = Quantity;
+ while (number > 0 && Quantity > 0)
+ {
+ PickItem()!.Destroy();
+ number--;
+ }
+
+ return number;
+ }
+
+ public void ClearStack()
+ {
+ while (Quantity > 0)
+ {
+ PickItem()!.Destroy();
+ }
+ }
+}
\ No newline at end of file
diff --git a/scripts/item/weapon/WeaponTemplate.cs b/scripts/item/weapon/WeaponTemplate.cs
index b4be423..d934fd7 100644
--- a/scripts/item/weapon/WeaponTemplate.cs
+++ b/scripts/item/weapon/WeaponTemplate.cs
@@ -40,6 +40,8 @@ public abstract partial class WeaponTemplate : RigidBody2D, IItem
QueueFree();
}
+ public bool CanStackWith(IItem item) => false;
+
///
/// Whether the weapon is currently picked up