Inventory and item systems are essential for:
- RPGs
- Action RPGs
- Survival games
- Roguelikes
- Adventure/Metroidvania
- Open-world games
- SLG/Strategy games
- Loot-based games
Lua is perfect for building these systems:
- Data-driven items (Lua tables)
- Hot-reloadable item definitions
- Easy extensions (rarities, tags, effects)
- Modular inventory logic
- Crafting formulas as tables
- Shops using item IDs
- Persistent save/load
This chapter builds a complete, modern item ecosystem.
1. Item Definitions (Data-Driven)
All items are defined as Lua tables.
This is how most professional games implement content pipelines.
items/
weapons.lua
armor.lua
consumables.lua
materials.lua
loot_tables.lua
1.1 Example: weapons.lua
return {
sword_1 = {
name = "Iron Sword",
type = "weapon",
damage = 12,
rarity = "common",
},
sword_2 = {
name = "Flame Blade",
type = "weapon",
damage = 20,
fire_damage = 5,
rarity = "rare",
}
}
1.2 Example: consumables.lua
return {
potion_small = {
name = "Small Health Potion",
type = "consumable",
heal = 30
}
}
1.3 Loading All Item Definitions
local Items = {}
local function load_group(path)
local t = require(path)
for id, data in pairs(t) do
Items[id] = data
end
end
load_group("items.weapons")
load_group("items.armor")
load_group("items.consumables")
load_group("items.materials")
return Items
Now Items[“sword_2”] returns a complete data table.
2. Inventory System
Inventory must support:
- slots
- stacking
- swapping
- merging
- removing
- weight limits (optional)
- slot-based or grid-based inventory
We build a slot-based inventory.
2.1 Inventory Object
local Inventory = {}
Inventory.__index = Inventory
function Inventory.new(size)
return setmetatable({
size = size,
slots = {} -- { {id="potion", count=3}, ... }
}, Inventory)
end
2.2 Find Free Slot
function Inventory:first_empty()
for i=1,self.size do
if not self.slots[i] then return i end
end
end
2.3 Add Item
Supports stacking.
function Inventory:add(id, count, items)
count = count or 1
-- stack with existing
for i=1,self.size do
local slot = self.slots[i]
if slot and slot.id == id then
slot.count = slot.count + count
return true
end
end
-- new stack
local idx = self:first_empty()
if not idx then return false end
self.slots[idx] = {id=id, count=count}
return true
end
2.4 Remove Item
function Inventory:remove(id, count)
count = count or 1
for i=1,self.size do
local slot = self.slots[i]
if slot and slot.id == id then
slot.count = slot.count - count
if slot.count <= 0 then
self.slots[i] = nil
end
return true
end
end
return false
end
2.5 Swap Slots
function Inventory:swap(a, b)
self.slots[a], self.slots[b] = self.slots[b], self.slots[a]
end
3. Equipment System
Common slots:
- weapon
- offhand
- head
- body
- legs
- accessories
3.1 Equipment Object
local Equipment = {}
Equipment.__index = Equipment
function Equipment.new()
return setmetatable({
weapon = nil,
head = nil,
body = nil,
}, Equipment)
end
function Equipment:equip(slot, id)
self[slot] = id
end
function Equipment:unequip(slot)
local old = self[slot]
self[slot] = nil
return old
end
return Equipment
3.2 Applying Equipment Stats
function Equipment:apply_to(stats, Items)
for slot, id in pairs(self) do
local item = Items[id]
if item and item.damage then
stats.attack.flat = stats.attack.flat + item.damage
end
if item and item.def then
stats.def.flat = stats.def.flat + item.def
end
end
end
Called each time you compute stats.
This integrates perfectly with the Stat system from Chapter 5.
4. Loot Tables
Loot tables define drops.
They can be:
- weighted
- random
- rarity-based
- level-scaled
- table-based (like Slay the Spire)
4.1 Loot Table Structure
return {
{id="potion_small", weight=50},
{id="sword_1", weight=5},
{id="material_iron",weight=30},
}
4.2 Weighted Random Select
local function random_loot(table)
local total = 0
for _, e in ipairs(table) do total = total + e.weight end
local r = math.random() * total
for _, e in ipairs(table) do
if r < e.weight then return e.id end
r = r - e.weight
end
end
4.3 Drop Item
function drop_loot(enemy, inventory, LootTables)
local tbl = LootTables[enemy.type]
if not tbl then return end
local id = random_loot(tbl)
inventory:add(id)
end
5. Crafting System
Crafting formulas are pure Lua.
5.1 Crafting Recipe
recipes = {
iron_sword = {
requires = {
{id="material_iron", count=2},
{id="wood", count=1},
},
result = {id="sword_1", count=1}
}
}
5.2 Craft Function
function craft(inventory, recipe)
for _, req in ipairs(recipe.requires) do
if not inventory:remove(req.id, req.count) then
return false
end
end
inventory:add(recipe.result.id, recipe.result.count)
return true
end
Simple and scalable.
6. Shops & Vendors
Shop inventory is also data-driven.
6.1 Shop Definitions
shop = {
items = {
{id="potion_small", price=20},
{id="material_iron", price=15},
{id="sword_1", price=120},
}
}
6.2 Buy Item
function buy(player, item)
if player.gold >= item.price then
player.gold = player.gold - item.price
player.inventory:add(item.id)
return true
end
return false
end
7. Inventory UI (Hugo-friendly listing)
A typical inventory UI:
- grid of slots
- drag & drop
- double-click = use
- right-click = drop/equip
- tooltip on hover
In Lua:
function draw_inventory(inv)
for i=1,inv.size do
local slot = inv.slots[i]
local x = ((i-1)%5)*80
local y = math.floor((i-1)/5)*80
draw_slot(x,y)
if slot then
draw_item_icon(slot.id, x, y)
draw_text(slot.count, x+60, y+60)
end
end
end
8. Save / Load (Persistence)
Items and inventory must be included in save files.
8.1 Save Inventory
function save_inventory(inv)
local data = {}
for i=1,inv.size do
local slot = inv.slots[i]
if slot then
data[i] = {id=slot.id, count=slot.count}
end
end
return data
end
8.2 Load Inventory
function load_inventory(inv, data)
for i=1,inv.size do
if data[i] then
inv.slots[i] = {id=data[i].id, count=data[i].count}
end
end
end
9. Putting It All Together: A Complete Item Flow
-- load item definitions
local Items = require("items.index")
-- player inventory
local inv = Inventory.new(20)
-- pick up loot
inv:add("material_iron", 1)
-- craft sword
craft(inv, recipes.iron_sword)
-- equip it
player.equipment:equip("weapon", "sword_1")
-- apply stats
player.equipment:apply_to(player.stats, Items)
-- show UI
draw_inventory(inv)
A full gameplay loop powered entirely by Lua.
10. Summary of Chapter 11
You now understand how to build:
- Data-driven item definitions
- Slot-based inventory
- Equipment and stat modifiers
- Loot tables & weighted randomness
- Crafting system
- Shops & vendors
- Inventory UI
- Persistence for save/load
- Modular content pipelines
Lua is one of the best languages for content-driven systems, allowing huge games to scale cleanly.
Keep Reading
Follow the engineering thread
Get the next practical Birdor note, or browse the archive for related systems, tooling, and architecture work.