Formalize inventory system with Item, ItemStack, and ItemSlot classes#202
Formalize inventory system with Item, ItemStack, and ItemSlot classes#202
Conversation
Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
Co-authored-by: dmccoystephenson <21204351+dmccoystephenson@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR refactors Beyond Nations’ inventory from a flat Dictionary<ItemType,int> into a structured model (Item → ItemStack → ItemSlot → Inventory) and adds a world-drop entity for item stacks.
Changes:
- Introduces
Item,ItemStack, andItemSlotdomain classes and wiresInventoryto use slot-based storage. - Adds
ItemDropas a newEntitytype to represent item stacks in the world. - Expands the test suite and adds documentation for the new inventory architecture.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 14 comments.
Show a summary per file
| File | Description |
|---|---|
| src/c#/main/inventory/Inventory.cs | Refactors inventory internals to a List<ItemSlot> and adds getSlots() |
| src/c#/main/item/Item.cs | Adds base Item type wrapper |
| src/c#/main/item/ItemStack.cs | Adds stack abstraction around item + quantity |
| src/c#/main/item/ItemSlot.cs | Adds slot container for optional stacks |
| src/c#/main/entity/entities/ItemDrop.cs | Adds world entity representation for an ItemStack |
| src/c#/main/entity/EntityType.cs | Adds ITEM_DROP enum value |
| src/c#/tests/inventory/TestInventory.cs | Adds/extends inventory tests for new behaviors and slot access |
| src/c#/tests/item/TestItem.cs | Adds tests for Item |
| src/c#/tests/item/TestItemStack.cs | Adds tests for ItemStack behaviors |
| src/c#/tests/item/TestItemSlot.cs | Adds tests for ItemSlot behaviors |
| src/c#/tests/Tests.cs | Registers new item-related tests in the test runner |
| docs/INVENTORY_SYSTEM.md | Documents the new inventory system architecture and usage |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| public class ItemDrop : Entity { | ||
| private ItemStack itemStack; | ||
|
|
||
| public ItemDrop(Vector3 position, ItemStack itemStack) : base(EntityType.ITEM_DROP, getItemDropName(itemStack)) { |
There was a problem hiding this comment.
ItemDrop assumes itemStack is non-null (it’s dereferenced in getItemDropName and createGameObject). Add an explicit null check in the constructor (and throw a clear exception) to avoid hard-to-diagnose runtime crashes if a caller passes null.
| public ItemDrop(Vector3 position, ItemStack itemStack) : base(EntityType.ITEM_DROP, getItemDropName(itemStack)) { | |
| public ItemDrop(Vector3 position, ItemStack itemStack) : base(EntityType.ITEM_DROP, itemStack != null ? getItemDropName(itemStack) : "Invalid ItemDrop") { | |
| if (itemStack == null) { | |
| throw new System.ArgumentNullException(nameof(itemStack), "ItemDrop requires a non-null ItemStack."); | |
| } |
| public void transferContentsOfInventory(Inventory otherInventory) { | ||
| items[ItemType.COIN] += otherInventory.getNumItems(ItemType.COIN); | ||
| items[ItemType.WOOD] += otherInventory.getNumItems(ItemType.WOOD); | ||
| items[ItemType.STONE] += otherInventory.getNumItems(ItemType.STONE); | ||
| items[ItemType.APPLE] += otherInventory.getNumItems(ItemType.APPLE); | ||
| items[ItemType.SAPLING] += otherInventory.getNumItems(ItemType.SAPLING); | ||
| foreach (ItemSlot otherSlot in otherInventory.getSlots()) { | ||
| if (!otherSlot.isEmpty()) { | ||
| ItemType itemType = otherSlot.getItemStack().getItemType(); | ||
| int quantity = otherSlot.getQuantity(); |
There was a problem hiding this comment.
transferContentsOfInventory will behave badly (and can throw due to collection modification during foreach) if otherInventory is the same instance as this. Consider adding a guard (if (otherInventory == this) return; or throw) or copying the source slots before iterating.
| public List<ItemSlot> getSlots() { | ||
| return slots; |
There was a problem hiding this comment.
getSlots() returns the backing List<ItemSlot> directly, which allows callers to add/remove/reorder slots (or create duplicate item types). That can silently break invariants used by getNumItems/addItem/removeItem. Consider returning an IReadOnlyList<ItemSlot>/IEnumerable<ItemSlot> or a defensive copy so external code can enumerate slots without mutating the inventory structure.
| public List<ItemSlot> getSlots() { | |
| return slots; | |
| public IReadOnlyList<ItemSlot> getSlots() { | |
| return slots.AsReadOnly(); |
| namespace beyondnations { | ||
| /// <summary> |
There was a problem hiding this comment.
Unity scripts in this repo appear to be checked in with .meta files (e.g., ItemType.cs.meta, Inventory.cs.meta), but the new scripts under src/c#/main/item/ don’t have .meta files. Please add the corresponding .meta files to avoid GUID churn and broken references in Unity projects.
| using UnityEngine; | ||
|
|
||
| using beyondnations; |
There was a problem hiding this comment.
The new test scripts under src/c#/tests/item/ don’t include .meta files, while other tests do (e.g., TestInventory.cs.meta). In a Unity project this can cause GUID changes and reference issues—please add the generated .meta files for these new test assets.
| foreach (ItemSlot slot in slots) { | ||
| if (slot.hasItemOfType(itemType)) { | ||
| return slot.getQuantity(); | ||
| } | ||
| } | ||
| return 0; |
There was a problem hiding this comment.
getNumItems returns the quantity from the first slot that matches itemType. If the inventory ever contains multiple stacks of the same type (possible if getSlots() is mutated externally, or if future stack limits require splitting across slots), this will undercount. Either enforce “at most one slot per item type” or sum quantities across all matching slots.
| foreach (ItemSlot slot in slots) { | |
| if (slot.hasItemOfType(itemType)) { | |
| return slot.getQuantity(); | |
| } | |
| } | |
| return 0; | |
| int total = 0; | |
| foreach (ItemSlot slot in slots) { | |
| if (slot.hasItemOfType(itemType)) { | |
| total += slot.getQuantity(); | |
| } | |
| } | |
| return total; |
| // Initialize slots for each item type | ||
| // Coins slot | ||
| if (numGoldCoins > 0) { | ||
| slots.Add(new ItemSlot(new ItemStack(ItemType.COIN, numGoldCoins))); | ||
| } else { | ||
| slots.Add(new ItemSlot()); |
There was a problem hiding this comment.
The constructor comments/structure imply a dedicated “coins slot” and fixed slot-to-item-type mapping, but addItem will fill the first empty slot (so an empty coin slot can later become WOOD/STONE/etc.). Either enforce stable per-type slots (e.g., by indexing by ItemType) or update the comments/docs/tests so they don’t imply reserved ordering.
| slot.getItemStack().removeQuantity(numItems); | ||
| // Clear the slot if empty (quantity <= 0) | ||
| if (slot.getItemStack().isEmpty()) { | ||
| slot.clear(); | ||
| } |
There was a problem hiding this comment.
removeItem clears the slot when the quantity becomes <= 0, which changes semantics vs the previous dictionary implementation (where counts could go negative and getNumItems would reflect that). If callers rely on exact counts (including underflow), avoid clearing and preserve the resulting quantity (or clamp to 0 consistently and update the backward-compatibility claim/docs).
| public bool isEmpty() { | ||
| return itemStack == null || itemStack.isEmpty(); | ||
| } | ||
|
|
||
| public ItemStack getItemStack() { |
There was a problem hiding this comment.
isEmpty() can return true while itemStack remains non-null (e.g., quantity == 0), so getItemStack() may return a non-null stack for an “empty” slot. This is easy to misuse and conflicts with the “optional ItemStack” model. Consider normalizing empty stacks to null (e.g., clear when setting or when quantity becomes <= 0) or make getItemStack() return null when isEmpty() is true.
| private Item item; | ||
| private int quantity; | ||
|
|
||
| public ItemStack(Item item, int quantity) { |
There was a problem hiding this comment.
ItemStack(Item item, int quantity) allows item to be null, but getItemType() and canStackWith() dereference item, leading to a guaranteed NullReferenceException for a null input. Either validate and throw an ArgumentNullException in the constructor, or make methods null-safe if null items are intended.
| public ItemStack(Item item, int quantity) { | |
| public ItemStack(Item item, int quantity) { | |
| if (item == null) { | |
| throw new System.ArgumentNullException(nameof(item)); | |
| } |
The inventory system used a flat
Dictionary<ItemType, int>structure. This PR formalizes the concept hierarchy: Item → ItemStack → ItemSlot → Inventory, enabling item stacks to exist as world drops and providing extensibility for future features (stack limits, item metadata, equipment slots).Architecture
Before:
After:
Changes
Backward Compatibility
All existing Inventory methods (
getNumItems,addItem,removeItem, etc.) work identically. No call sites require changes.Example Usage
Tests
Added 25+ test cases covering Item, ItemStack, ItemSlot classes and enhanced Inventory tests for edge cases (null handling, negative quantities, slot management).
Original prompt
💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.