Lua for Game Development — Chapter 11: Inventory, Items, Equipment, Crafting & Data-Driven Content
Leeting Yan
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.