Lua for Game Development โ€” Chapter 11: Inventory, Items, Equipment, Crafting & Data-Driven Content

A complete and production-ready inventory, item, equipment, crafting, and loot table system powered by Lua.

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.

Join newsletter Browse articles