Lua for Game Development โ€” Chapter 13: NPCs, Town Simulation, Factions & AI Routines

NPC behaviors, towns, factions, daily routines, merchants, economy simulation, and social systems using Lua.

NPCs breathe life into any game world:

  • villagers with schedules
  • merchants with stock
  • guards who patrol
  • hostile factions
  • relationships
  • dialogue that reacts to story flags
  • simulation (hunger, work, needs)
  • towns that evolve over time

Lua is perfect for simulation layers:

  • lightweight logic
  • hot reload
  • easy-to-edit behavior packages
  • clean data-driven content
  • integrates with quests, story, and world triggers

This chapter builds:

  1. NPC definitions
  2. Behavior packages (wander, talk, patrol, work)
  3. Daily schedules (day/night cycles)
  4. Merchant system
  5. Factions & reputation
  6. Town simulation
  7. Social interactions

1. NPC Definitions (Data-Driven)

npcs/
villagers.lua
merchants.lua
guards.lua

1.1 Example NPC Definition

return {
  elder = {
    name = "Village Elder",
    sprite = "npc_elder",
    faction = "village",
    behaviors = {"talk", "idle"},
    dialogue = "elder_intro",
    schedule = {
      {time=0,   state="sleep"},
      {time=6,   state="idle"},
      {time=12,  state="walk_square"},
      {time=18,  state="home"},
      {time=22,  state="sleep"},
    }
  },
}

NPC = data + behaviors + dialogue + schedule.

2. NPC Object & Behavior System

NPC behaviors are modular, like animation states.

2.1 NPC Object Class

local NPC = {}
NPC.__index = NPC

function NPC.new(data)
  return setmetatable({
    data = data,
    x = 0, y = 0,
    state = "idle",
    schedule_index = 1,
  }, NPC)
end

2.2 Behavior Packages

Stored in behaviors/.

wander.lua

return {
  update = function(npc, dt)
    npc.x = npc.x + math.random(-20,20) * dt
    npc.y = npc.y + math.random(-20,20) * dt
  end
}

talk.lua

return {
  interact = function(npc, player)
    DialogueSystem:start(npc.data.dialogue)
  end
}

patrol.lua

return {
  update = function(npc, dt)
    -- follow waypoints
    local wp = npc.data.waypoints[npc.waypoint_index]
    npc:move_to(wp, dt)
  end
}

All behaviors share a standard interface.

2.3 NPC Update

function NPC:update(dt)
  local behaviors = self.data.behaviors
  for _, b in ipairs(behaviors) do
    local pkg = require("behaviors."..b)
    if pkg.update then
      pkg.update(self, dt)
    end
  end
end

3. Daily Schedules (Day/Night Simulation)

NPCs follow time-based routines.

3.1 Update Schedule Based on Time

function NPC:apply_schedule(hour)
  for i = #self.data.schedule,1,-1 do
    if hour >= self.data.schedule[i].time then
      self.state = self.data.schedule[i].state
      return
    end
  end
end

3.2 Day/Night Integration

Called every in-game minute:

for _, npc in pairs(NPC_LIST) do
  npc:apply_schedule(WorldHour)
end

Common routines:

  • villagers sleep at night
  • guards patrol at night
  • merchants open shops during day
  • farmers work during morning

4. Merchant System (Economy Basics)

Defined with:

shops/
  potions.lua
  weapons.lua

4.1 Example Merchant Data

return {
  items = {
    {id="potion_small", price=20},
    {id="potion_large", price=50},
  },
  restock_rate = 24, -- in-game hours
}

4.2 Merchant NPC Behavior

return {
  interact = function(npc, player)
    ShopUI:open(npc.data.shop_id)
  end
}

4.3 Restocking

function Shop:restock(shop_data)
  for _, item in ipairs(shop_data.items) do
    item.stock = math.random(1,5)
  end
end

Triggered by:

if WorldHour % shop.restock_rate == 0 then
  Shop:restock(shop_data)
end

5. Factions & Reputation

Faction examples:

  • village
  • bandits
  • merchants
  • kingdom
  • demons
  • wilderness creatures

5.1 Faction Data

factions = {
  village = {name="Village", hostility=0},
  bandits = {name="Bandits", hostility=1},
}

5.2 Reputation System

local Reputation = {}
Reputation.__index = Reputation

function Reputation.new()
  return setmetatable({rep={}}, Reputation)
end

function Reputation:add(faction, amount)
  self.rep[faction] = (self.rep[faction] or 0) + amount
end

return Reputation

5.3 NPC Reaction Based on Reputation

function NPC:reaction(player)
  local rep = player.reputation.rep[self.data.faction] or 0
  if rep < -10 then
    return "hostile"
  elseif rep < 0 then
    return "unfriendly"
  else
    return "neutral"
  end
end

6. Guard & Aggro Behavior (AI Combat)

If faction is hostile, NPC attacks.

if npc:reaction(player) == "hostile" then
  npc.state = "attack"
end

Attack behavior:

return {
  update=function(npc, dt)
    local dx = player.x - npc.x
    local dy = player.y - npc.y
    local d = math.sqrt(dx*dx + dy*dy)

    if d < 50 then
      player:take_damage(5)
    else
      npc.x = npc.x + dx/d * dt * 50
      npc.y = npc.y + dy/d * dt * 50
    end
  end
}

7. Town Simulation (Lightweight Economy)

Simple simulation:

  • villagers produce goods
  • merchants consume goods
  • food supply changes with population
  • shops restock based on supply

7.1 Town Resource Data

Town = {
  food = 100,
  goods = 20,
  population = 12,
}

7.2 Daily Update

function Town:update_daily()
  self.food = self.food - self.population * 2
  self.goods = self.goods + math.random(0,2)

  if self.food < 0 then
    event:emit("town_starving")
  end
end

Called every 24 in-game hours.

8. NPC Social Interactions

NPCs interact with each other:

  • greetings
  • gossip
  • sharing rumors
  • giving quest hints
  • reacting to player reputation

This is lightweight but adds life.

8.1 NPC Gossip System

function NPC:gossip(other)
  if math.random() < 0.1 then
    -- trade rumor
    local rumor = "weather_rain" -- example
    npc:learn_rumor(rumor)
  end
end

8.2 NPC-to-NPC Interaction Update

for _, a in ipairs(NPC_LIST) do
  for _, b in ipairs(NPC_LIST) do
    if a ~= b and distance(a, b) < 40 then
      a:gossip(b)
    end
  end
end

9. Story Integration

NPCs react to story flags.

9.1 Dialogue Changes

if quest_manager.flags["main_01"] then
  npc.dialogue = "elder_after_quest"
end

9.2 World State Changes

if Town.food < 10 then
  spawn("starving_villager", 200, 200)
end

10. Putting It All Together โ€” Example Village Simulation

-- daily town tick
Town:update_daily()

-- update NPC behaviors
for _, npc in pairs(NPC_LIST) do
  npc:apply_schedule(WorldHour)
  npc:update(dt)
end

-- social interactions
NPCSocial:update(dt)

-- reactions to player
for _, npc in pairs(NPC_LIST) do
  if npc:reaction(player) == "hostile" then
    npc.state = "attack"
  end
end

This simulates a living village.

11. Summary of Chapter 13

You now understand:

  • NPC definition & modular behavior system
  • Daily routines and schedule-driven AI
  • Merchant system & restocking
  • Town economy simulation
  • Faction and reputation logic
  • Hostile/neutral/friendly reactions
  • NPC-to-NPC interactions
  • Story integration

This system supports:

  • RPG villages
  • Survival colony sims
  • Open-world games
  • Town building games
  • Strategy games with civilian life

Lua handles these simulation layers elegantly.

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