Skills/godot-inventory-economy/SKILL.md

315 lines
8.9 KiB
Markdown

---
name: godot-inventory-economy
description: "Inventory and economy patterns for Godot 4.x: resource-based item definitions, stackable/unique items, drag-drop UI, equip systems, currency management, and save integration. Use when building item systems, shops, or economies in Godot games."
license: MIT
compatibility:
- godot-4.0
- godot-4.1
- godot-4.2
- godot-4.3
- godot-4.4
- godot-4.5
- godot-4.6
- godot-4.7
metadata:
author: vl4dt
version: 0.1.0
tags:
- godot
- inventory
- economy
- items
- equip
created: 2026-06-21
---
# Inventory and Economy Systems in Godot
Patterns for item management, equipping, and game economies.
## Resource-Based Item Definitions
Define all items as `Resource` files — no code changes needed for new items:
```gdscript
class_name ItemData extends Resource
enum ItemType { CONSUMABLE, EQUIPMENT, KEY_ITEM, MATERIAL }
enum EquipmentSlot { HEAD, CHEST, HANDS, FEET, WEAPON, ACCESSORY }
@export var item_id: String = ""
@export var display_name: String = ""
@export var description: String = ""
@export var icon: Texture2D = null
@export var item_type: ItemType = ItemType.MATERIAL
@export var stack_size: int = 1 # Max per stack, 0 = unique
@export var value: int = 0 # Gold/sell value
@export var equipment_slot: EquipmentSlot = EquipmentSlot.WEAPON
# Stat modifiers (for equipment)
@export var stat_modifiers: Dictionary = {} # {"speed": 10, "defense": 5}
# Consumable effects
@export var heal_amount: int = 0
@export var effect_script: Script = null # Optional custom effect
```
### Item Instance (Runtime)
```gdscript
class_name InventoryItem extends Resource
@export var item_data: ItemData
@export var quantity: int = 1
@export var durability: float = 1.0 # 0.0 to 1.0, -1 = unbreakable
@export var is_equipped: bool = false
```
## Inventory Manager
Core inventory system with stack management:
```gdscript
class_name Inventory extends Node
signal item_added(item: InventoryItem)
signal item_removed(item: InventoryItem)
signal item_equipped(item: InventoryItem, slot: ItemData.EquipmentSlot)
signal item_unequipped(slot: ItemData.EquipmentSlot)
signal inventory_changed()
var items: Array[InventoryItem] = []
var equipped: Dictionary = {} # slot -> InventoryItem
@export var max_slots: int = 20
func add_item(data: ItemData, quantity: int = 1) -> bool:
if items.size() >= max_slots:
return false
# Try to stack with existing item
for inv_item in items:
if inv_item.item_data == data and data.stack_size > 1:
var remaining = data.stack_size - inv_item.quantity
if remaining > 0:
var add_amount = min(quantity, remaining)
inv_item.quantity += add_amount
quantity -= add_amount
item_added.emit(inv_item)
inventory_changed.emit()
if quantity <= 0:
return true
# Create new stack
while quantity > 0:
var new_item = InventoryItem.new()
new_item.item_data = data
var add_amount = min(quantity, data.stack_size if data.stack_size > 0 else 1)
new_item.quantity = add_amount
new_item.durability = data.stack_size > 1 ? 1.0 : -1.0
items.append(new_item)
item_added.emit(new_item)
quantity -= add_amount
inventory_changed.emit()
return true
func remove_item(index: int, quantity: int = 1) -> bool:
if index < 0 or index >= items.size():
return false
var inv_item = items[index]
if inv_item.quantity <= quantity:
item_removed.emit(inv_item)
items.remove_at(index)
else:
inv_item.quantity -= quantity
item_removed.emit(inv_item)
inventory_changed.emit()
return true
func equip(item_index: int) -> bool:
var inv_item = items[item_index]
var slot = inv_item.item_data.equipment_slot
# Unequip current item in slot
if equipped.has(slot):
unequip(slot)
inv_item.is_equipped = true
equipped[slot] = inv_item
item_equipped.emit(inv_item, slot)
return true
func unequip(slot: ItemData.EquipmentSlot) -> bool:
if not equipped.has(slot):
return false
var inv_item = equipped[slot]
inv_item.is_equipped = false
equipped.erase(slot)
item_unequipped.emit(slot)
return true
```
## Drag-and-Drop Inventory UI
Grid-based inventory with drag support:
```gdscript
class_name InventorySlot extends Container
signal slot_clicked(index: int)
signal drag_started(item: InventoryItem, index: int)
signal drop_requested(from_index: int, to_index: int)
var item: InventoryItem = null
var slot_index: int = 0
func _gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton and event.pressed:
if event.button_index == MOUSE_BUTTON_LEFT and item:
drag_started.emit(item, slot_index)
# Handle dropping another item into this slot
func accept_drop(from_slot: InventorySlot) -> bool:
if not item:
# Empty slot — move item here
drop_requested.emit(from_slot.slot_index, slot_index)
return true
elif from_slot.item and not item:
drop_requested.emit(from_slot.slot_index, slot_index)
return true
return false
func update_display() -> void:
# Update child controls (icon, quantity label) based on item
pass
```
### Inventory Grid
```gdscript
class_name InventoryGrid extends GridContainer
@export var inventory: Inventory
@export var slot_scene: PackedScene
func _ready():
for i in range(inventory.max_slots):
var slot = slot_scene.instantiate() as InventorySlot
slot.slot_index = i
slot.drag_started.connect(_on_drag_started)
slot.drop_requested.connect(_on_drop_requested)
add_child(slot)
refresh()
func refresh() -> void:
for child in get_children():
if child is InventorySlot:
var slot = child as InventorySlot
if slot.slot_index < inventory.items.size():
slot.item = inventory.items[slot.slot_index]
else:
slot.item = null
slot.update_display()
func _on_drop_requested(from: int, to: int) -> void:
# Swap items between slots
if from >= inventory.items.size() or to >= inventory.items.size():
return
var temp = inventory.items[from]
inventory.items[from] = inventory.items[to]
inventory.items[to] = temp
refresh()
```
## Currency and Shop System
```gdscript
class_name EconomyManager extends Node
signal gold_changed(amount: int)
signal item_purchased(item: ItemData, cost: int)
signal item_sold(item: ItemData, price: int)
var gold: int = 0
func add_gold(amount: int) -> void:
gold += amount
gold_changed.emit(gold)
func spend_gold(amount: int) -> bool:
if gold < amount:
return false
gold -= amount
gold_changed.emit(gold)
return true
# Shop transaction
func buy_item(inventory: Inventory, item_data: ItemData, price: int) -> bool:
if not spend_gold(price):
return false
inventory.add_item(item_data)
item_purchased.emit(item_data, price)
return true
func sell_item(inventory: Inventory, item_index: int, price: int) -> bool:
var inv_item = inventory.items[item_index]
if not inv_item:
return false
inventory.remove_item(item_index, 1)
add_gold(price)
item_sold.emit(inv_item.item_data, price)
return true
```
## Save Integration
Serialize inventory for save systems:
```gdscript
func serialize_inventory() -> Dictionary:
var result = []
for inv_item in items:
result.append({
"item_id": inv_item.item_data.item_id,
"quantity": inv_item.quantity,
"durability": inv_item.durability,
"equipped": inv_item.is_equipped
})
return result
func deserialize_inventory(data: Array) -> void:
items.clear()
for entry in data:
var item_data = load("res://items/" + entry["item_id"] + ".tres") as ItemData
if item_data:
var inv_item = InventoryItem.new()
inv_item.item_data = item_data
inv_item.quantity = entry["quantity"]
inv_item.durability = entry.get("durability", 1.0)
items.append(inv_item)
inventory_changed.emit()
```
## Godot 4.7 Economy Notes
- **ResourceCache** — Preload item resources at game start for instant UI updates
- **tween_await()** — Animate gold counter changes or item pickup effects
## MCP Bridge Tools (Optional — Live Editor Integration)
When the MCP bridge is running (Phase 2), these tools can inspect and modify your project live:
- **`script_read`** / **`script_edit`** — Read and modify inventory scripts
- **`scene_tree`** — Inspect inventory UI node hierarchy
> **Note:** MCP tools require the Godot editor to be running with the MCP plugin enabled. Skills work independently without the bridge.
## References
- [Item Resource Design](references/item-resources.md)
- [Inventory UI Patterns](references/inventory-ui.md)
- [Economy Balance Guide](references/economy-balance.md)