Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use when building inventory systems — Resource-based items, slot management, stacking, and UI binding
.claude/skills/jame581-inventory-system/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 138% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 103% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 90% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 143% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 99% | 0% |
All examples target Godot 4.3+ with no deprecated APIs. GDScript is shown first, then C#.
> Related skills: resource-pattern for custom Resource data containers, save-load for inventory serialization, event-bus for inventory change notifications, hud-system for inventory UI display, popochiu for adventure-game inventory.
┌─────────────────────────────────────────────────────────┐
│ UI Layer │
│ InventoryUI (Control) │
│ └─ GridContainer │
│ └─ SlotUI × N (Button) │
│ └─ TextureRect (icon) + Label (qty) │
│ │
│ Connects to: inventory_changed signal │
│ Drag-and-drop via _get_drag_data / _drop_data │
└───────────────────────┬─────────────────────────────────┘
│ reads / mutates
┌───────────────────────▼─────────────────────────────────┐
│ Inventory (Node) │
│ slots: Array[InventorySlot] │
│ add_item(item, qty) → leftover: int │
│ remove_item(item, qty) │
│ has_item(item, qty) → bool │
│ get_item_count(item) → int │
│ │
│ signals: inventory_changed │
│ item_added(item, quantity) │
│ item_removed(item, quantity) │
└───────────────────────┬─────────────────────────────────┘
│ references
┌───────────────────────▼─────────────────────────────────┐
│ Data Layer (Resources) │
│ ItemData (Resource) │
│ id, name, description, icon, max_stack_size, │
│ item_type enum │
│ │
│ InventorySlot (inner class / Resource) │
│ item: ItemData, quantity: int │
└─────────────────────────────────────────────────────────┘Define items as Resources so they live in .tres files, are shareable across scenes, and benefit from full editor integration.
gdscript# item_data.gd class_name ItemData extends Resource enum ItemType { CONSUMABLE, EQUIPMENT, MATERIAL, KEY_ITEM, } @export var id: String = "" @export var name: String = "" @export var description: String = "" @export var icon: Texture2D @export var max_stack_size: int = 99 @export var item_type: ItemType = ItemType.MATERIAL
Create item assets: res://items/potion_health.tres, set id = "potion_health", etc.
csharp// ItemData.cs using Godot; [GlobalClass] public partial class ItemData : Resource { public enum ItemType { Consumable, Equipment, Material, KeyItem, } [Export] public string Id { get; set; } = ""; [Export] public string Name { get; set; } = ""; [Export] public string Description { get; set; } = ""; [Export] public Texture2D Icon { get; set; } [Export] public int MaxStackSize { get; set; } = 99; [Export] public ItemType Type { get; set; } = ItemType.Material; }
> Use [GlobalClass] so the Inspector dropdown shows ItemData as a resource type when creating .tres files.
gdscript# inventory.gd class_name Inventory extends Node signal inventory_changed signal item_added(item: ItemData, quantity: int) signal item_removed(item: ItemData, quantity: int) @export var capacity: int = 20 var slots: Array[InventorySlot] = [] func _ready() -> void: slots.resize(capacity) for i in capacity: slots[i] = InventorySlot.new() # Returns the number of items that could NOT be added (leftover). func add_item(item: ItemData, quantity: int = 1) -> int: var remaining := quantity # Fill existing stacks first for slot in slots: if remaining <= 0: break if not slot.is_empty() and slot.item == item: remaining = slot.add_to_stack(remaining) # Open empty slots next for slot in slots: if remaining <= 0: break if slot.is_empty(): slot.item = item remaining = slot.add_to_stack(remaining) var added := quantity - remaining if added > 0: item_added.emit(item, added) inventory_changed.emit() return remaining func remove_item(item: ItemData, quantity: int = 1) -> void: var remaining := quantity for slot in slots: if remaining <= 0: break if not slot.is_empty() and slot.item == item: var removed := mini(slot.quantity, remaining) slot.remove_from_stack(removed) remaining -= removed var actually_removed := quantity - remaining if actually_removed > 0: item_removed.emit(item, actually_removed) inventory_changed.emit() func has_item(item: ItemData, quantity: int = 1) -> bool: return get_item_count(item) >= quantity func get_item_count(item: ItemData) -> int: var total := 0 for slot in slots: if not slot.is_empty() and slot.item == item: total += slot.quantity return total
csharp// Inventory.cs using Godot; using Godot.Collections; public partial class Inventory : Node { [Signal] public delegate void InventoryChangedEventHandler(); [Signal] public delegate void ItemAddedEventHandler(ItemData item, int quantity); [Signal] public delegate void ItemRemovedEventHandler(ItemData item, int quantity); [Export] public int Capacity { get; set; } = 20; public Array<InventorySlot> Slots { get; private set; } = new(); public override void _Ready() { for (int i = 0; i < Capacity; i++) Slots.Add(new InventorySlot()); } /// <summary>Returns the number of items that could NOT be added (leftover).</summary> public int AddItem(ItemData item, int quantity = 1) { int remaining = quantity; // Fill existing stacks first foreach (var slot in Slots) { if (remaining <= 0) break; if (!slot.IsEmpty() && slot.Item == item) remaining = slot.AddToStack(remaining); } // Open empty slots next foreach (var slot in Slots) { if (remaining <= 0) break; if (slot.IsEmpty()) { slot.Item = item; remaining = slot.AddToStack(remaining); } } int added = quantity - remaining; if (added > 0) { EmitSignal(SignalName.ItemAdded, item, added); EmitSignal(SignalName.InventoryChanged); } return remaining; } public void RemoveItem(ItemData item, int quantity = 1) { int remaining = quantity; foreach (var slot in Slots) { if (remaining <= 0) break; if (!slot.IsEmpty() && slot.Item == item) { int removed = Mathf.Min(slot.Quantity, remaining); slot.RemoveFromStack(removed); remaining -= removed; } } int actuallyRemoved = quantity - remaining; if (actuallyRemoved > 0) { EmitSignal(SignalName.ItemRemoved, item, actuallyRemoved); EmitSignal(SignalName.InventoryChanged); } } public bool HasItem(ItemData item, int quantity = 1) => GetItemCount(item) >= quantity; public int GetItemCount(ItemData item) { int total = 0; foreach (var slot in Slots) if (!slot.IsEmpty() && slot.Item == item) total += slot.Quantity; return total; } }
InventorySlot is a lightweight object tracking an item reference and its quantity. Define it as an inner class on Inventory (GDScript) or as a standalone RefCounted subclass (C#).
gdscript# inventory_slot.gd — or nest as inner class inside inventory.gd class_name InventorySlot extends RefCounted var item: ItemData = null var quantity: int = 0 func is_empty() -> bool: return item == null or quantity <= 0 func can_stack(new_item: ItemData) -> bool: return not is_empty() and item == new_item and quantity < item.max_stack_size # Adds amount to this slot, capped at max_stack_size. # Returns the leftover that did not fit. func add_to_stack(amount: int) -> int: if item == null: push_error("InventorySlot.add_to_stack: slot has no item assigned") return amount var space := item.max_stack_size - quantity var to_add := mini(amount, space) quantity += to_add return amount - to_add # Removes amount from this slot. Clears the slot when quantity reaches zero. func remove_from_stack(amount: int) -> void: quantity -= amount if quantity <= 0: quantity = 0 item = null
csharp// InventorySlot.cs using Godot; public partial class InventorySlot : RefCounted { public ItemData Item { get; set; } public int Quantity { get; set; } public bool IsEmpty() => Item == null || Quantity <= 0; public bool CanStack(ItemData newItem) => !IsEmpty() && Item == newItem && Quantity < Item.MaxStackSize; /// <summary>Adds amount to this slot. Returns leftover that did not fit.</summary> public int AddToStack(int amount) { if (Item == null) { GD.PushError("InventorySlot.AddToStack: slot has no item assigned"); return amount; } int space = Item.MaxStackSize - Quantity; int toAdd = Mathf.Min(amount, space); Quantity += toAdd; return amount - toAdd; } /// <summary>Removes amount from this slot. Clears when quantity reaches zero.</summary> public void RemoveFromStack(int amount) { Quantity -= amount; if (Quantity <= 0) { Quantity = 0; Item = null; } } }
ItemData extends Resource with a stable id string set in the InspectorItemData files live under res://items/ and are committed to version controlInventory.add_item() returns leftover count; callers handle a full inventoryinventory_changed signal drives all UI updates — UI never polls per-frameInventorySlot.remove_from_stack() clears item to null when quantity reaches 0SlotType enum, not by string, to catch typos at compile timeEquipment.get_total_stat() is called when stats are needed, not cached unless profiling demands itid + quantity only — never full ItemData objects or resource pathsItemRegistry loads items at startup; all deserialization goes through itinventory_changed oncemax_stack_size = 1 on EQUIPMENT and KEY_ITEM types to prevent stackingpush_error() messages include the class name and method for easy tracingAdd equipment slots (HEAD, CHEST, WEAPON, etc.) by extending the Inventory class with a typed slot map. Stat aggregation runs by summing ItemData.stats across equipped items; signal equipment_changed when slots change.
> See references/equipment.md for the full GDScript and C# Equipment class with EquipmentSlotType enum, equip / unequip API, and stat aggregation.
Slot-grid UI: a GridContainer of Panel slot widgets, each rendering one InventorySlot. Drag-and-drop uses _get_drag_data / _drop_data / _can_drop_data on the slot widget. The Inventory emits inventory_changed; the UI re-renders affected slots.
> See references/ui-binding.md for the full GDScript and C# slot widget (drag/drop, hover preview), inventory grid layout, and tooltip wiring.
Persist Inventory + Equipment as a Dictionary keyed by item resource path (since ItemData lives at res://items/<name>.tres). Reload by load(path) and reconstructing the slot list. Version field gates migration on load.
> See references/serialization.md for the GDScript and C# save/load implementation with version field and ConfigFile / JSON variants.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 9,957 | 6,173 | -38% | 1 | 1 | 0% | 1,904 | 4,523 | +138% | 0 | 0 | — |
case-02 | fail→fail | 16,279 | 15,077 | -7% | 1 | 1 | 0% | 3,394 | 6,413 | +89% | 0 | 0 | — |
case-03 | fail→pass | 14,649 | 12,263 | -16% | 1 | 1 | 0% | 2,957 | 5,988 | +103% | 0 | 0 | — |
case-04 | pass→pass | 9,638 | 8,036 | -17% | 1 | 1 | 0% | 1,753 | 4,827 | +175% | 0 | 0 | — |
case-05 | pass→pass | 14,778 | 10,242 | -31% | 1 | 1 | 0% | 2,895 | 5,349 | +85% | 0 | 0 | — |
case-06 | pass→pass | 14,102 | 10,318 | -27% | 1 | 1 | 0% | 2,659 | 5,368 | +102% | 0 | 0 | — |
case-07 | fail→pass | 16,783 | 14,108 | -16% | 1 | 1 | 0% | 3,329 | 6,310 | +90% | 0 | 0 | — |
case-08 | fail→pass | 10,935 | 7,517 | -31% | 1 | 1 | 0% | 1,970 | 4,781 | +143% | 0 | 0 | — |
case-09 | pass→pass | 14,967 | 13,150 | -12% | 1 | 1 | 0% | 2,823 | 6,079 | +115% | 0 | 0 | — |
case-10 | fail→fail | 12,698 | 16,159 | +27% | 1 | 1 | 0% | 2,535 | 6,650 | +162% | 0 | 0 | — |
case-11 | fail→pass | 18,566 | 15,555 | -16% | 1 | 1 | 0% | 3,353 | 6,677 | +99% | 0 | 0 | — |
case-12 | pass→pass | 15,030 | 12,996 | -14% | 1 | 1 | 0% | 2,438 | 5,535 | +127% | 0 | 0 | — |
case-13 | pass→pass | 13,633 | 13,389 | -2% | 1 | 1 | 0% | 2,546 | 5,890 | +131% | 0 | 0 | — |
case-14 | fail→pass | 14,152 | 12,629 | -11% | 1 | 1 | 0% | 2,356 | 5,642 | +139% | 0 | 0 | — |
case-15 | pass→pass | 9,794 | 5,510 | -44% | 1 | 1 | 0% | 1,712 | 4,325 | +153% | 0 | 0 | — |
case-16 | pass→pass | 12,145 | 6,457 | -47% | 1 | 1 | 0% | 2,111 | 4,572 | +117% | 0 | 0 | — |
case-17 | pass→pass | 15,117 | 18,656 | +23% | 1 | 1 | 0% | 2,850 | 6,952 | +144% | 0 | 0 | — |
case-18 | pass→pass | 10,553 | 6,004 | -43% | 1 | 1 | 0% | 1,868 | 4,434 | +137% | 0 | 0 | — |
case-19 | pass→pass | 13,342 | 6,133 | -54% | 1 | 1 | 0% | 2,405 | 4,419 | +84% | 0 | 0 | — |
case-20 | pass→pass | 11,011 | 14,186 | +29% | 1 | 1 | 0% | 2,053 | 6,468 | +215% | 0 | 0 | — |
case-21 | pass→pass | 17,197 | 19,724 | +15% | 1 | 1 | 0% | 3,284 | 7,248 | +121% | 0 | 0 | — |
case-22 | pass→pass | 13,216 | 16,948 | +28% | 1 | 1 | 0% | 2,320 | 6,604 | +185% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +27 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.