NPC AI & World Simulation
A comprehensive reference covering mob artificial intelligence, world simulation mechanics, dynamic content generation, mob programming systems, and spawn/respawn architecture as implemented across the MUD tradition.
Table of Contents
- Mob AI Patterns
- Advanced Mob AI
- World Simulation
- Dynamic Content
- Mob Programs (Mobprogs)
- Spawn and Respawn Systems
1. Mob AI Patterns
Traditional MUD mob behavior is driven by act flags – bitfield flags set on each mobile that determine its baseline behavior. These flags originated in DikuMUD and were refined through Merc, ROM, SMAUG, and their descendants.
1.1 Core Behavior Flags
| Flag | Numeric (Envy) | Behavior |
|---|---|---|
SENTINEL | 2 | Mob stays in its room; never wanders |
SCAVENGER | 4 | Picks up objects from the room floor |
AGGRESSIVE | 32 | Attacks players within its level range on sight |
STAY_AREA | 64 | Will not leave its area’s vnum range |
WIMPY | 128 | Flees when HP drops below ~20% |
AGGRESSIVE_GOOD | – | Attacks good-aligned characters only |
AGGRESSIVE_NEUTRAL | – | Attacks neutral-aligned characters only |
AGGRESSIVE_EVIL | – | Attacks evil-aligned characters only |
1.2 Aggro (Aggressive Behavior)
Aggro is the most fundamental combat AI pattern. When a player enters a room containing an aggressive mob, the mob initiates combat.
Variations:
- Level-range aggro: Only attacks players within a configurable level range (prevents newbie slaughter or pointless attacks on high-level players)
- Alignment-based aggro: Selective aggression based on good/neutral/evil alignment
- Race-based aggro: Attacks specific races (e.g., orcs attack elves)
- Faction-based aggro: Aggro driven by reputation/faction standing
Pseudocode:
on_player_enters_room(player):
if mob.has_flag(AGGRESSIVE):
if player.level in mob.aggro_range:
if not mob.is_fighting():
mob.attack(player)
1.3 Assist Behavior
Mobs help other mobs in combat. CoffeeMUD defines the most granular taxonomy:
| Behavior | Description |
|---|---|
BrotherHelper | Assists identical mobs or those from the same spawn room |
RaceHelper | Assists mobs of the same race |
AlignHelper | Assists mobs of the same alignment |
ClanHelper | Assists mobs belonging to the same clan |
MOBHelper | Defends any non-player mob under attack |
CombatAssister | Defends mobs matching a configurable mask |
Guard | Defends any attacked mob or player in the room |
Typical implementation: Each combat tick, non-fighting mobs in the room scan for allies in combat. If a matching ally is found, the mob joins the fight on their side.
on_combat_tick():
for each mob in room:
if mob.is_fighting():
continue
for each combatant in room.fighting_list:
if combatant.is_ally(mob) and combatant.is_defending():
mob.attack(combatant.attacker)
break
1.4 Wander (Mobile Behavior)
Mobs without the SENTINEL flag move randomly through connected rooms at regular intervals. CoffeeMUD’s Mobile behavior adds:
- Frequency control: How often the mob attempts to move
- Locale restrictions: Only wander through certain sector types (forest, city, etc.)
- Leash limits: Maximum distance from home room
- Patrolling: Follow a fixed path of room IDs or cardinal directions (
Patrollerbehavior)
LPC approach (from Discworld MUD docs): Place an identification function on your mobs (e.g., query_is_area_monster() returns 1), then add exit checks in boundary rooms that block mobs with that tag from leaving.
Aardwolf Lua approach (entry trigger):
local lastroom = self.lastroom
if lastroom ~= nil then
if self.room.sector ~= SECT_RIVER
and self.room.sector ~= SECT_LAKE then
rgoto(lastroom) -- snap back if wrong sector
end
end
1.5 Sentinel
The simplest behavior: the mob does not move. Used for:
- Shopkeepers who must stay at their counter
- Guards at a specific post
- Quest NPCs that players need to find reliably
- Boss mobs in their lair
1.6 Scavenger
The mob picks up objects from the room floor. Typically runs on the same tick as wander behavior. Useful for:
- Janitor mobs that clean up abandoned equipment
- Thieves who pocket loose items
- Animals that eat food items left on the ground
1.7 Memory and Grudges
Mobs can remember players who attacked them and react on future encounters:
ROM/SMAUG mobprog approach:
MOB REMEMBER $n -- remember the attacker as $q
Later, on a greet trigger:
if istarget $n
say You again! I haven't forgotten what you did!
MOB KILL $n
endif
Aardwolf Lua approach:
remember(ch) -- store ch as self.target
adddelay(300) -- set a timer
-- later, in delay trigger:
if self.target ~= nil then
local victim = getmob(self.target.name, nil, LP_PLRONLY)
if victim ~= nil then
kill(victim)
end
end
forget() -- clear memory
1.8 Flee / Wimpy
When a mob’s HP drops below a threshold (typically 20%), it attempts to flee combat and move to a random adjacent room.
Implementation layers:
- Flag-based: The
WIMPYact flag triggers automatic flee at 20% HP - Trigger-based: The
HPCNTtrigger fires during combat when HP falls below a percentage, allowing scripted responses (casting heal, calling for help, surrendering, changing tactics) - Behavioral: CoffeeMUD’s
FightFleebehavior adds configurable timing parameters
Practical constraints (from EQ/MUD hybrid designs):
- Fleeing NPCs must have active aggro on someone; clearing aggro stops the flee
- NPCs mid-cast do not flee until the cast completes
- If the NPC regenerates above ~30% HP, it stops fleeing and re-engages
- Multiple
hpcnttriggers should be listed in increasing order (40% fires before 20%)
1.9 Shop / Quest-Giver Behavior
Shop and quest NPCs combine several patterns:
Shopkeeper:
SENTINELflag (stays put)- Buy/sell command handlers
- Inventory management on area reset
- Price calculation based on item type, condition, and charisma
- CoffeeMUD:
ShopKeeperinterface with configurable buy/sell types, pricing, and prejudice masks
Quest-Giver:
- Speech/give triggers that detect quest items or keywords
- State tracking (quest not started / in progress / complete)
- Reward distribution (XP, gold, items)
Aardwolf Lua example (bribe-triggered quest reward):
-- bribe trigger: fires when gold given exceeds threshold
if isplayer(ch) then
if self.gold > 3000 then
emote("smiles broadly.")
say("You may well be the one fated to restore the Balance...")
local newobj = oload("onslaught-15")
give(newobj, ch)
else
emote("thank", ch)
say("Generous, but not enough to prove your commitment.")
end
end
Ranvier YAML-based quest NPC:
- id: rat
keywords: ['rat']
name: 'Rat'
level: 2
script: '1-rat'
items: ['limbo:sliceofcheese']
quests: ['limbo:onecheeseplease']
attributes:
health: 100
speed: 2.5
damage: 1-7
2. Advanced Mob AI
2.1 Scripted Behavior Systems
Mobprogs (ROM/SMAUG)
The original mob scripting system, using a command-based language similar to what players type. Triggers fire on game events and execute a sequence of MOB commands with simple if/else/endif control flow. See Section 5 for full details.
Lua (Aardwolf)
Aardwolf replaced mobprogs with a full Lua compiler integrated into the MUD engine. The system exposes five global variables:
self– the mob running the programch– the character that triggered the programobj– relevant object (if any)room– the room (for room progs)mud– global MUD state (time of day, season, player count, etc.)
Lua provides real loops, functions, string manipulation, math, and table data structures – a massive improvement over mobprog if/else chains.
LPC (LPMud Family)
In LPC MUDs (Genesis, Discworld, Dead Souls), NPCs are full objects written in the LPC language. Behavior is implemented via:
- Inheriting from
/std/monsterand overriding methods - The heartbeat system:
heart_beat()is called every ~1 second on registered objects, driving healing, combat, wandering, and AI decisions - The reset system:
reset()is called periodically by the driver, allowing rooms to respawn monsters
// Basic LPC monster creation in a room's reset()
void reset() {
object ob;
ob = new("/std/monster");
ob->set_name("goblin");
ob->set_level(5);
ob->set_aggressive(1);
ob->move(this_object());
}
Event-Driven Scripting (Ranvier)
Ranvier uses Node.js event emitters. All entities (NPCs, items, rooms, areas) can have scripts attached:
Unique script (one per entity):
'use strict';
module.exports = {
listeners: {
playerEnter: state => (room, player) => {
// react to player entering
},
hit: state => (damage, target, attacker) => {
// custom combat logic
},
},
};
Behavior (reusable, configurable):
'use strict';
module.exports = {
listeners: {
playerEnter: state => (config, room, player) => {
// config comes from the NPC's YAML definition
if (config.hostile) {
// attack logic
}
}
}
};
Multiple behaviors can be attached to a single NPC, composed from the YAML definition:
- id: 3
name: "Training Dummy"
behaviors:
lootable:
table:
pools:
- "limbo:junk"
- "limbo:sliceofcheese": 25
aggressive:
range: 3
2.2 State Machines
Finite State Machines (FSMs) are the most common AI architecture in games. A mob exists in exactly one state at a time, with transitions triggered by inputs.
Basic enum approach:
enum MobState { IDLE, PATROL, CHASE, FIGHT, FLEE, RETURN }
on_tick(mob):
switch mob.state:
case IDLE:
if player_in_range(mob):
mob.state = CHASE
elif mob.has_patrol_route:
mob.state = PATROL
case PATROL:
move_along_route(mob)
if player_in_range(mob):
mob.state = CHASE
case CHASE:
move_toward(mob, mob.target)
if adjacent_to(mob, mob.target):
mob.state = FIGHT
elif distance(mob, mob.target) > mob.leash_range:
mob.state = RETURN
case FIGHT:
execute_combat(mob)
if mob.hp_percent < 20:
mob.state = FLEE
elif mob.target.is_dead:
mob.state = RETURN
case FLEE:
move_away_from(mob, mob.target)
if mob.hp_percent > 30:
mob.state = FIGHT
elif safe_distance(mob):
mob.state = RETURN
case RETURN:
move_toward(mob, mob.home_room)
if mob.at_home:
mob.state = IDLE
mob.hp = mob.max_hp
Advanced FSM variants:
- Hierarchical State Machines: Superstates contain substates. An “OnGround” superstate handles jumping for both “Standing” and “Crouching” substates, reducing duplication.
- Pushdown Automata: A stack of states. Push a new state (e.g., “casting spell”) on top; when it completes, pop it and resume the previous state (e.g., “fighting”). Solves the “what was I doing before?” problem.
- Concurrent State Machines: Separate FSMs for independent concerns (movement vs. combat stance vs. dialogue state). Reduces state explosion from N*M combinations to N+M states.
2.3 Behavior Trees
Behavior trees became the industry standard after Halo 2 (2004) because they scale better than FSMs for complex behavior.
Core node types:
- Selector (OR): Try children left-to-right, succeed on first success
- Sequence (AND): Run children left-to-right, fail on first failure
- Decorator: Modify child behavior (invert, repeat, cooldown)
- Leaf: Actual actions or conditions
Root (Selector)
├── Sequence: "Fight"
│ ├── Condition: enemy_visible?
│ ├── Condition: hp > 20%?
│ ├── Action: move_to_enemy
│ └── Action: attack
├── Sequence: "Flee"
│ ├── Condition: hp <= 20%?
│ └── Action: flee_to_safety
├── Sequence: "Patrol"
│ ├── Condition: has_patrol_route?
│ └── Action: follow_route
└── Action: idle
Advantages over FSMs:
- State transition logic is centralized in the tree structure, not dispersed across states
- Highly modular – subtrees can be reused across mob types
- Easy to extend without rewriting existing logic
2.4 Group / Pack AI
Assist chains: When one mob is attacked, nearby mobs of the same type join combat (the BrotherHelper pattern). ROM implements this via the ASSIST flag family.
Pack behavior patterns:
- Alpha/follower: One mob leads, others follow. If the alpha dies, the next-highest-level mob becomes alpha.
- Coordinated attacks: Pack members flank or surround the target. One mob tanks while others deal damage.
- Summoning: A mob under attack calls for reinforcements via
MOB MLOADor equivalent. - Retreat signal: When the alpha flees, the pack flees.
Mobprog example (kill trigger – calling for help):
-- kill trigger on a guard mob
if mobhere guard
mob echoaround $n The guard shouts for backup!
mob force guard kill $n
endif
mob kill $n
2.5 Boss Mechanics
Boss mobs combine multiple AI systems:
Phase transitions (using hpcnt triggers):
-- Phase 1: Normal combat
hpcnt 75:
say "You think you can defeat me?"
mob cast 'fireball' $n
-- Phase 2: Enraged
hpcnt 50:
say "NOW YOU FACE MY TRUE POWER!"
mob cast 'sanctuary' self
-- increase damage output, change attack pattern
-- Phase 3: Desperate
hpcnt 25:
say "I will take you all with me!"
mob mload minion_vnum -- summon adds
mob mload minion_vnum
mob cast 'earthquake' $n
Common boss patterns:
- Minion summoning: Load additional mobs at HP thresholds
- AoE attacks:
mob damage all min maxhits everyone in the room - Protection checks: Require specific items to survive special attacks
- Enrage timers: After N minutes, boss does dramatically more damage
- Room mechanics: Transfer players, lock doors, change room descriptions
Aardwolf Lua boss example (protection check):
-- fight trigger on a god-level boss
if isplayer(ch) then
echoat(ch, "@CThe presence of the Gods sends out unimaginable energy...")
if wears(ch, "onslaught-23") then
echoat(ch, "@WThe energy passes harmlessly around you.@w")
else
damage(ch, 30000, 40000, LP_SEEALL) -- instant kill without ward
end
end
2.6 Reactive / Adaptive AI
Threat tables: Track damage dealt by each player. Mob targets the highest-threat player (tank/healer management).
Learning AI: Track player strategies across encounters. If players always use fire spells, the mob begins casting fire resistance.
LLM integration (CoffeeMUD): Modern CoffeeMUD integrates with LLMs via LangChain4J. The MPLLM scriptable feature supplements MudChat’s pattern-matching with AI-generated responses, allowing NPCs to hold freeform conversations while still executing game commands.
2.7 Dialog Trees
Pattern-matching (MudChat): CoffeeMUD’s MudChat behavior matches player speech against patterns in a chat.dat file and returns canned responses. QuestChat extends this with quest-state-aware responses.
Keyword-based (Mobprogs): Speech triggers match keywords in player dialogue:
-- speech trigger on an NPC
speech tell me about the quest
say "The dragon has stolen the king's crown..."
say "Bring it back and you shall be rewarded."
speech yes
say "Brave soul! Head north to the Dragon's Lair."
mob oload quest_map
give map $n
Aardwolf Lua dialog (speech triggers with state):
-- speechexact trigger matching "yes"
if isplayer(ch) then
if carries(ch, "quest-token") then
say("You already have a task. Complete it first.")
else
say("Very well. Take this token and find the lost artifact.")
local token = oload("quest-token")
give(token, ch)
end
end
Ranvier approach: Dialog is handled through quest definitions and NPC script events. The playerInteract event fires when a player talks to an NPC, and the script checks quest state via the quest manager.
3. World Simulation
3.1 Area Resets / Repops
Area resets are the heartbeat of a MUD’s world persistence. At regular intervals, areas “repop” – restoring mobs, objects, and door states to their defined defaults.
CircleMUD/ROM Reset System
Zone header format:
#<zone_vnum>
<zone_name>~
<top_room_vnum> <lifespan_minutes> <reset_mode>
Reset modes:
| Mode | Behavior |
|---|---|
| 0 | Never reset (lifespan ignored) |
| 1 | Reset after lifespan AND zone is empty of players |
| 2 | Reset immediately on lifespan expiry (most common) |
Reset commands:
| Cmd | Format | Purpose |
|---|---|---|
M | M <if> <mob_vnum> <max_exist> <room_vnum> | Load mob into room |
O | O <if> <obj_vnum> <max_exist> <room_vnum> | Load object on ground |
G | G <if> <obj_vnum> <max_exist> | Give object to last-loaded mob |
E | E <if> <obj_vnum> <max_exist> <equip_pos> | Equip object on last-loaded mob |
P | P <if> <obj_vnum1> <max_exist> <obj_vnum2> | Put object inside container |
D | D <if> <room_vnum> <exit_num> <state> | Set door state (0=open, 1=closed, 2=locked) |
R | R <if> <room_vnum> <obj_vnum> | Remove/purge object from room |
The if-flag creates dependency chains: if set to 1, the command only executes if the previous command succeeded. This creates conditional sequences like “load mob, THEN give it a sword, THEN equip it with armor.”
The max_existing argument caps how many copies of a mob/object can exist in the entire world. If the cap is reached, the reset command silently skips.
Example reset block:
M 0 3010 5 3001 -- Load guard (max 5) in room 3001
E 1 3020 5 16 -- Equip guard with sword (wield position)
E 1 3021 5 3 -- Equip guard with shield (shield position)
G 1 3022 10 -- Give guard a key
M 0 3011 3 3002 -- Load shopkeeper (max 3) in room 3002
O 0 3050 1 3003 -- Load treasure chest in room 3003
P 1 3051 1 3050 -- Put gold inside chest
D 0 3004 2 2 -- Lock the south door of room 3004
S -- End of zone commands
SMAUG INSTAZONE
SMAUG provides the INSTAZONE command that automatically generates reset lists from the current world state: place mobs in their rooms wearing their equipment, then run INSTAZONE and it writes all the M/E/G/O/P commands for you.
Ranvier Repop Trigger
Ranvier supports room-level repop triggers that fire on area reset:
add repop [prog_id] [optional_percent] [optional_actor]
3.2 Weather Systems
MUD weather systems range from cosmetic text to gameplay-affecting mechanics.
Algorithmic approach:
- Use sine waves keyed to in-game date for annual temperature curves (peak in summer, trough in winter)
- Layer smaller sine waves for daily variation (warmer at noon, cooler at midnight)
- Add random perturbation within bounds for unpredictability
- Seed a PRNG with the in-game date for deterministic, reproducible weather
Gameplay integration:
- Fog reduces visibility between rooms (can’t see mob descriptions in adjacent rooms)
- Rain extinguishes torches (Nemesis MUD implemented wet torches that dry later)
- Temperature affects healing rates or spell effectiveness
- Storms block outdoor travel or flight
- Snow accumulation changes room descriptions and movement costs
Pseudocode:
on_weather_tick():
for each area in world:
base_temp = sine(day_of_year / 365 * 2pi) * seasonal_amplitude
daily_mod = sine(hour / 24 * 2pi) * daily_amplitude
area.temperature = base_temp + daily_mod + random(-3, 3)
if area.temperature < FREEZE_THRESHOLD:
area.precipitation_type = SNOW
else:
area.precipitation_type = RAIN
area.precipitation_chance = calculate_from_pressure(area)
if random(100) < area.precipitation_chance:
area.weather_state = PRECIPITATING
apply_weather_effects(area)
3.3 Day/Night Cycles
Most MUDs run an accelerated clock (typically 1 real minute = 1 MUD hour, so a full day passes in ~24 real minutes).
Effects:
- Room descriptions change (torch-lit at night, sunlit by day)
- Nocturnal mobs become active; diurnal mobs sleep
- Shops close at night (CoffeeMUD’s
GateGuardlocks doors at night) - Visibility reduces outdoors at night (need light source)
- Certain spells or abilities are stronger/weaker based on time
Aardwolf Lua time access:
local current_hour = mud.hour -- current in-game hour
if current_hour >= 20 or current_hour <= 5 then
-- nighttime behavior
echoat(ch, "The creature's eyes glow in the darkness.")
end
Mobprog time check:
if hour $i == 6
say The sun rises! Time to open the shop.
mob open north
mob unlock north
endif
if hour $i == 20
say Closing time!
mob close north
mob lock north
endif
3.4 Seasonal Events
Implementation patterns:
Calendar-driven: Check the in-game date against a schedule. During event windows, swap area descriptions, load special mobs, activate event quests.
Timer-driven: An AI director tracks time elapsed and triggers events at intervals. Events can scale based on player population.
Condition-driven: Events trigger when world state meets criteria (e.g., total monster kills exceed a threshold, triggering an invasion).
Aardwolf Lua date handling:
local now = mud.unixtime()
local xmas = mud.unixtime("07:00 12/25/2025")
local diff = xmas - now
if diff > 0 and diff < 60*60*24*7 then
-- Christmas week: activate holiday content
say("Holiday cheer fills the air!")
end
3.5 Economy Simulation
MUD economies are notoriously difficult to balance. The core problem: money hemorrhages out of monsters (faucets) faster than it leaves the game (sinks), causing perpetual inflation.
Faucets (money enters the game):
- Monster drops / loot
- Quest rewards
- Selling items to NPC shops (infinite demand at fixed prices)
Sinks (money leaves the game):
- Buying from NPC shops
- Training costs / skill purchases
- Equipment repair
- Death penalties (gold lost on corpse)
- Taxes / tithes
- Consumables (potions, scrolls, food)
Alter Aeon’s feedback control system:
- Track total currency in the game in real-time
- Dynamically adjust monster drop rates and shop prices based on total money supply
- Tax players holding over 1 million gold at 2% of excess per period
- Result: total currency fluctuates by less than 10% over two years
Shattered World’s loans-based economy:
- Currency equation:
Bank Loans = Bank Deposits + Currency in Circulation - All shops, banks, and businesses are player-owned and operated
- Owners set their own buy/sell prices
- Currency enters through player loans secured against property
- The economy self-adjusts based on the number of players willing to borrow
Common design rules:
- If an item is endlessly generated by the game, shops should refuse to buy it (or buy at near-zero prices)
- Hard-to-get quest items have intrinsic value; common drops don’t
- Administrators should participate in the economy as players, not use unlimited creation powers
- Player-to-player trading should be the primary exchange mechanism for valuable items
4. Dynamic Content
4.1 Procedural Quest Generation
Template-based approach: Define quest templates with variable slots:
Template: FETCH_QUEST
giver: {npc matching criteria}
target_item: {item matching criteria}
target_location: {area matching criteria}
reward: {scaled to player level}
narrative: "{giver.name} asks you to retrieve {item.name}
from {location.name}."
NPC desire-driven approach: NPCs have dynamic desire models with parameters like satisfaction, wealth, and friendship. Quest generation draws from unmet NPC needs:
- A hungry NPC generates a “bring food” quest
- A threatened NPC generates a “kill the bandits” quest
- A wealthy NPC generates a “deliver this package” quest
Parameters change dynamically based on quest completion, world state, and time passage, creating emergent quest chains.
StoryWorld framework: Generates procedural quests rooted in world logic – completing or failing quests permanently affects the world state, making the quest system feel consequential rather than arbitrary.
GenMUD approach: A fully generated world where NPCs are given names, society affiliations, and professions. Side quests generate procedurally based on local events and historical context. The world runs dynamic simulations even when no players are present.
4.2 Random Encounters
Zone-based encounter tables:
encounter_table "dark_forest":
30% - 2-4 wolves (level 5-8)
20% - 1 bear (level 10)
15% - 1-3 bandits (level 6-9)
10% - 1 dire wolf (level 12)
5% - 1 forest spirit (level 15, quest-giver)
20% - nothing
on_player_moves(player, room):
if room.sector == FOREST:
roll = random(100)
encounter = lookup(encounter_table["dark_forest"], roll)
if encounter:
spawn_encounter(encounter, room)
Scaling encounters: Adjust encounter difficulty based on player level, party size, or recent activity. High-traffic areas can have depleted encounter rates (the monsters have been scared off).
4.3 World Events
Types:
- Scheduled: Holiday events, seasonal festivals, monthly tournaments
- Triggered: Player actions cause world-state changes (killing a faction leader starts a war)
- Random: Periodic invasions, natural disasters, wandering boss spawns
- Cascading: One event triggers others (drought -> famine -> refugee NPCs in cities -> new quests)
AI Director pattern: An overarching system monitors world state and player activity, deciding when to trigger events:
ai_director_tick():
if time_since_last_event > MIN_EVENT_GAP:
tension = calculate_tension(player_activity, world_state)
if tension > EVENT_THRESHOLD:
event = select_appropriate_event(tension, world_state)
trigger_event(event)
reset_tension()
4.4 Player-Triggered Events
Kill-chain events: Killing N mobs of a type triggers a response:
on_mob_death(mob):
area.kill_count[mob.type] += 1
if area.kill_count[mob.type] >= INVASION_THRESHOLD:
trigger_invasion(area, mob.faction)
area.kill_count[mob.type] = 0
Discovery events: Entering a hidden room, finding a rare item, or solving a puzzle triggers world-state changes.
Faction tipping points: When a player’s faction reputation crosses a threshold, NPCs throughout the world change behavior (new dialog options, shop access, quest availability).
4.5 Faction / Reputation Systems
Core architecture:
Reputation Tiers:
WAR [-1000, -600) -- Kill on sight
HOSTILE [-600, -200) -- Will not interact
NEUTRAL [-200, 200) -- Default standing
FRIENDLY [ 200, 600) -- Discounts, new quests
ALLIANCE [ 600, 1000] -- Full benefits, allied troops
Reputation changes:
- Killing faction members: large negative
- Completing faction quests: positive
- Completing rival faction quests: negative (cross-faction penalty)
- Trading with faction merchants: small positive
- Passive decay: reputation slowly drifts toward neutral over time
Advanced features:
- Propagation: Gaining rep with faction A gives partial rep with allies, partial negative with enemies
- Diminishing returns: Repeated identical actions give less reputation over time
- Faction relationship matrix: An N x N matrix defining how factions relate to each other (allied, neutral, hostile, at war)
Gameplay impact:
- Area access (certain zones require minimum faction standing)
- NPC behavior changes (aggro mobs become friendly, or vice versa)
- Exclusive vendors with unique inventory
- Allied NPCs assist the player in combat
- Faction-specific quest lines unlock
5. Mob Programs (Mobprogs)
5.1 ROM/SMAUG Mobprogs
Mobprogs originated as a snippet for Merc/ROM that became standard in SMAUG and its descendants. They provide a command-based scripting language where mobs react to game events.
Trigger Types
| Trigger | When It Fires | Argument |
|---|---|---|
ACT | Any act() message appears in room | keyword phrase |
SPEECH | Player speaks matching phrase | keyword phrase |
RANDOM | Each PULSE_MOBILE tick | percent chance |
GREET | Player enters room (mob must see them) | percent chance |
GRALL | Player enters room (always fires) | percent chance |
ENTRY | Mob enters a new room | percent chance |
EXIT | Player leaves through specific exit (mob must see) | direction |
EXALL | Player leaves through exit (always fires) | direction |
GIVE | Object given to mob | object keyword or “all” |
BRIBE | Gold given exceeds threshold | minimum gold |
KILL | Player initiates attack (fires once) | percent chance |
FIGHT | Each PULSE_VIOLENCE during combat | percent chance |
HPCNT | Mob HP below threshold during combat | HP percentage |
DEATH | Mob dies (before corpse created) | percent chance |
DELAY | Delay timer expires | percent chance |
SURR | Player surrenders | percent chance |
Variables
| Variable | Expansion |
|---|---|
$i / $I | Mob’s name / short description |
$n / $N | Triggering character’s name / name with title |
$t / $T | Secondary target name / description |
$r / $R | Random PC in room / description |
$q / $Q | Remembered target / description |
$o / $O | Primary object name / description |
$p / $P | Secondary object name / description |
$j/$e | He/she/it (mob/trigger char, nominative) |
$k/$m | Him/her/it (mob/trigger char, objective) |
$l/$s | His/hers/its (mob/trigger char, possessive) |
Control Flow
if {check} {argument} [{operator} {value}]
[or {check} ...]
[and {check} ...]
{commands}
[else
{commands}]
endif
Operators: ==, !=, >, <, >=, <=
break / end – exit the entire program immediately.
If-Checks (Selection)
Condition checks on characters:
isnpc $n/ispc $n– NPC or player?isgood $n/isneutral $n/isevil $n– alignmentisimmort $n– level > LEVEL_HEROlevel $n > 10– level comparisonhpcnt $n < 50– HP percentageclass $n == 'warrior'– class checkrace $n == 'elf'– race checkclan $n == 'shadow'– clan checksex $n == 1– gender (0=neuter, 1=male, 2=female)align $n > 500– alignment value
Room/world checks:
hour $i == 12– game timepeople == 3– total characters in roomplayers == 2– player count in roommobhere guard– mob present (by name or vnum)objhere sword– object presentrand 25– 25% random chance
Inventory checks:
carries $n 'quest_item'– character has itemwears $n 'magic_ring'– character wearing itemhas $n 'weapon'– character has item type
MOB Commands
Communication:
MOB ECHO/ECHOAT/ECHOAROUND– room messagesMOB ASOUND– message to adjacent roomsMOB ZECHO– message to entire zoneMOB GECHO– global message
Loading/Destroying:
MOB MLOAD <vnum>– create NPCMOB OLOAD <vnum> [level] [room|wear]– create objectMOB PURGE [target]– remove NPC/objectMOB JUNK <object>– destroy from inventoryMOB REMOVE <victim> <vnum|all>– strip and destroy equipment
Combat:
MOB KILL <victim>– attackMOB FLEE– unconditional retreatMOB CAST <spell> <target>– cast without mana costMOB DAMAGE <victim|all> <min> <max> [lethal]– direct damage
Movement:
MOB GOTO <location>– silent teleportMOB TRANSFER <victim|all> [location]– move character(s)MOB GTRANSFER– transfer with groupMOB AT <location> <command>– act in another room
Memory/Timing:
MOB REMEMBER <victim>– set $q targetMOB FORGET– clear targetMOB DELAY <seconds>– set delay trigger timerMOB CANCEL– clear timer
Control:
MOB FORCE <victim|all> <command>– compel actionMOB CALL <vnum> [victim] [target1] [target2]– call subroutine (max 5 deep)
Complete Example
-- Area file trigger assignment:
M act 1000 pokes you in the ribs.~
-- Program #1000:
if isnpc $n
chuckle
poke $n
break
else
if level $n <= 5
or isgood $n
tell $n I would rather you didnt poke me.
else
if level $n > 15
scream
say Ya know $n. I hate being poked!!!
if mobhere guard
mob force guard kill $n
endif
kill $n
break
endif
slap $n
shout MOMMY!!! $N is poking me.
endif
endif
This program: chuckles and pokes back at NPCs, warns low-level/good players, calls guards and attacks high-level players, and shouts for help against mid-level players.
5.2 Lua in Aardwolf
Aardwolf’s Lua system is a full replacement for mobprogs, providing a real programming language with access to MUD internals.
Architecture
Five global variables provide access to the MUD:
self– the mob executing the programch– the triggering character (nil for timer/random triggers)obj– relevant objectroom– room referencemud– global state (time, season, online count)
Trigger Types
Aardwolf Lua supports all legacy mobprog triggers plus additions:
Character triggers: act, bribe, death, delay, entry, exit, exall, fight, give, greet, grall, hpcnt, kill, listen, random, speech, speechexact, tell, tellexact, transfer, timer
Room triggers: command, after, repop, cmdabbrev
Key additions over mobprogs:
timer– fires every N seconds (minimum 60), with fixed timing unlike random’s probabilistic checkcmdabbrev– intercepts actual MUD commands with abbreviation awareness. Can block commands by returningtrue.command– arbitrary custom commands in rooms (e.g., “pick apple”, “search rubble”)after– runs a MUD command first, then executes the prog
Function Library (Selection)
Test functions:
affected(ch, "flag")– check affectcarries(ch, "item", opts)– inventory checkisplayer(ch)– player vs NPCmobexists("key", [room])– mob existence checkwearsobj(ch, "type")– equipment check
Action functions:
say(),emote(),echo(),echoat()– communicationrgoto(),transfer(),summon()– movementoload(),mload()– creation (return references!)kill(),damage(),cast()– combatgiveexp(),giveqp()– rewardsremember(),forget(),adddelay()– memory/timingcall("progid")– invoke another Lua program
Practical Examples
Race-gated door (cmdabbrev on “open”):
if charg == nil then return false end
if room.south.open == 1 then return false end
local i = string.find("south", "^" .. string.lower(charg))
if i ~= nil then
if ch.race ~= RACE_GIANT then
send(ch, "Only a giant is strong enough to clear the way South.")
return true -- block the command
else
return false -- allow normal open
end
else
return false
end
Multi-stage quest with delays:
-- death trigger on a boss mob
if isplayer(ch) then
if self.target == nil then
say("You have done it! By slaying the beast, you have saved us...")
remember(ch)
adddelay(4)
purgeobj("all", LP_WORNONLY)
purgeobj("all", LP_CARRIEDONLY)
end
end
-- delay trigger (4 seconds later):
if self.target ~= nil then
echoat(self.target, "The world shimmers as reality reforms...")
giveexp(self.target, 50000)
transfer(self.target, "onslaught-1")
forget()
end
Room cleanup when empty:
-- random trigger on a boss room controller
if room.playercount == 0 then
if mobexists("onslaught-58") then
purgemob("onslaught-58", LP_SEEALL + LP_WORLD + LP_ALLMOBS)
end
self.hp = self.maxhp -- full heal when no one is around
end
5.3 LPC Mob Scripting
In LPC MUDs, there is no separate “mobprog” language. NPCs are LPC objects that inherit from a base class (e.g., /std/monster) and override methods.
Key mechanisms:
heart_beat()– called every ~1 second on objects with heartbeats enabled. This is where combat, healing, wandering, and AI decisions happen.reset()– called periodically by the driver. Rooms use this to respawn monsters.init()– called when a living object enters the environment. Used to add attack commands, check aggro, etc.
Performance note: Objects with heartbeats are the major CPU hog in LPC MUDs. Minimize the number of objects with active heartbeats. Disable heartbeats on idle mobs and re-enable when players are present.
// Simple LPC monster in a room
inherit "/std/room";
void reset() {
// Only respawn if the mob isn't already here
if (!present("goblin")) {
object mob = clone_object("/monsters/goblin");
mob->move(this_object());
}
}
// In /monsters/goblin.c:
inherit "/std/monster";
void create() {
::create();
set_name("goblin");
set_short("a sneaky goblin");
set_long("A small, green-skinned creature with sharp teeth.\n");
set_level(5);
set_race("goblin");
set_aggressive(1);
set_heart_beat(1);
}
void heart_beat() {
::heart_beat(); // handle combat, healing
// Custom wander behavior
if (!query_attack() && random(10) < 2) {
// 20% chance to wander each heartbeat
string *exits = environment()->query_exits();
if (sizeof(exits)) {
command(exits[random(sizeof(exits))]);
}
}
}
5.4 Ranvier Event-Driven Scripting
Ranvier takes a modern approach: everything is an event. There are no “trigger types” in the mobprog sense – you listen for any event the engine emits.
Script file structure:
// scripts/npcs/areas/limbo/1-rat.js
'use strict';
module.exports = {
listeners: {
// fires when the NPC is hit
hit: state => (damage, target, attacker) => {
if (target.getAttribute('health') < target.getMaxAttribute('health') * 0.2) {
// flee at 20% HP
state.CommandManager.get('flee').execute(null, target, state);
}
},
// fires when a player enters the room
playerEnter: state => (room, player) => {
state.ChannelManager.get('say').send(state, target, 'Squeak!');
},
// fires when the NPC dies
deathblow: state => (victim, killer) => {
// custom death behavior
}
}
};
Behaviors are the reusable equivalent of mob flags:
# npcs.yml
- id: wolf
name: "Gray Wolf"
behaviors:
ranvier-aggro:
range: 3
ranvier-wander:
interval: 30
restricted: true
lootable:
table:
pools:
- "forest:wolf-pelt": 50
- "forest:wolf-fang": 25
// behaviors/npcs/ranvier-wander.js
'use strict';
module.exports = {
listeners: {
updateTick: state => (config) => function () {
if (this.isInCombat()) return;
const room = this.room;
const exits = room.getExits();
if (!exits.length) return;
const randomExit = exits[Math.floor(Math.random() * exits.length)];
// move the NPC
state.CommandManager.get(randomExit.direction).execute(null, this, state);
}
}
};
6. Spawn and Respawn Systems
6.1 Reset-Based Spawning (Traditional)
The traditional MUD spawn system is the area reset, a periodic process that restores an area to its designed state.
How it works:
- A timer counts down for each area (typically 3-15 minutes real-time)
- When the timer expires, the reset mode is checked:
- Mode 1: Only reset if no players are in the area
- Mode 2: Always reset
- The engine iterates through the area’s reset command list (M, O, G, E, P, D, R commands)
- Each command checks its
max_existingcap before executing - If a mob/object already exists at its designated location and hasn’t been killed/looted, the reset skips that entry
Key property: Resets are additive, not destructive. They fill in what’s missing, not replace what’s there. A mob that wandered to a different room is not teleported back – a new copy may spawn in its designated room if the max cap allows.
6.2 Reset Command Chains
The if-flag system creates equipment dependencies:
M 0 5001 1 5100 -- Load orc chief in room 5100 (max 1 in world)
E 1 5010 1 16 -- IF chief loaded: equip battle axe (wield)
E 1 5011 1 17 -- IF axe equipped: equip shield (hold)
G 1 5012 1 -- IF shield equipped: give dungeon key
M 0 5002 4 5101 -- Load orc grunt in room 5101 (max 4)
E 1 5013 4 16 -- IF grunt loaded: equip rusty sword
O 0 5020 1 5102 -- Load chest in room 5102 (always, max 1)
P 1 5021 1 5020 -- IF chest loaded: put gold inside
D 0 5103 0 2 -- Set north door of 5103 to locked (always)
6.3 Dynamic Spawn Rates
More sophisticated systems adjust spawn rates based on game conditions:
Player density scaling:
spawn_interval = base_interval * (1 / max(1, players_in_area))
More players = faster respawns to prevent resource starvation.
Feedback-controlled spawning (Alter Aeon approach):
- Track global resource levels (total gold, total items of each type)
- If resources are below target, increase drop rates and spawn rates
- If resources exceed target, decrease rates
- Apply diminishing returns to prevent exploitation
Time-of-day modulation:
- Nocturnal mobs spawn more frequently at night
- Undead spawn rates increase during in-game midnight
- Market NPCs only appear during daytime hours
6.4 Rare / Boss Timers
Boss and rare mob respawns use different mechanics than trash mobs:
Fixed timer: Boss respawns exactly N minutes after death. Players can calculate and camp the spawn.
Window timer: Boss respawns randomly within a time window (e.g., between 4-8 hours after death). Prevents precise camping while maintaining predictability.
Condition-based: Boss spawns when specific conditions are met:
on_area_tick():
if boss_is_dead and
kill_count["trash_mob_type"] >= KILL_THRESHOLD and
time_since_death >= MIN_RESPAWN:
spawn_boss()
reset_kill_count()
Triggered spawn: Boss appears in response to player action (using an item, completing a quest, reaching a specific room during an event).
6.5 Instanced vs. Shared World
Shared world (traditional MUD model):
- All players share one copy of every area
- Mobs exist once; first-come, first-served
- Competition for spawns drives social dynamics (camping, kill-stealing)
- Reset system keeps things populated
- Boss access is competitive
Instanced dungeons (MMORPG-inspired):
- A new copy of the area is created for each group that enters
- Every group gets fresh mobs and bosses
- No competition for spawns
- More predictable difficulty scaling
- Higher server resource cost (multiple copies of everything)
- Less social friction, but also less emergent social gameplay
Hybrid approaches:
- Instance only boss encounters, keep the open world shared
- Create instances only when the area is overcrowded
- Time-limited instances that merge back into the shared world after completion
Implementation considerations for a new MUD:
- Shared world is simpler to implement and more true to the MUD tradition
- Instancing requires tracking per-group state: mob HP, loot tables, reset state, door states
- Instanced content benefits from stronger narrative design (each run tells a story)
- Shared world benefits from emergent social dynamics (guild cooperation, spawn negotiation)
6.6 Spawn Architecture Pseudocode
class AreaResetManager:
areas: Map<AreaId, AreaState>
on_tick():
for area in areas:
area.timer -= tick_interval
if area.timer <= 0:
if area.reset_mode == RESET_ALWAYS:
execute_resets(area)
area.timer = area.lifespan
elif area.reset_mode == RESET_WHEN_EMPTY:
if area.player_count == 0:
execute_resets(area)
area.timer = area.lifespan
execute_resets(area):
last_mob = null
for cmd in area.reset_commands:
success = false
if cmd.if_flag and not last_success:
continue
switch cmd.type:
case 'M':
if world.count(cmd.mob_vnum) < cmd.max_existing:
last_mob = spawn_mob(cmd.mob_vnum, cmd.room)
success = true
case 'E':
if last_mob and world.count(cmd.obj_vnum) < cmd.max:
obj = create_obj(cmd.obj_vnum)
last_mob.equip(obj, cmd.position)
success = true
case 'G':
if last_mob and world.count(cmd.obj_vnum) < cmd.max:
obj = create_obj(cmd.obj_vnum)
last_mob.inventory.add(obj)
success = true
case 'O':
if world.count(cmd.obj_vnum) < cmd.max_existing:
obj = create_obj(cmd.obj_vnum)
room.add_object(obj)
success = true
case 'D':
room = get_room(cmd.room_vnum)
room.set_door_state(cmd.exit, cmd.state)
success = true
last_success = success
Sources
Mob AI and Behavior Flags
- Ansalon MUD MEDIT Documentation
- EnvyMUD Area Format (Act Flags)
- CoffeeMUD Behaviors Reference
- Advanced AI in MUDs - Top Mud Sites Forum
- Realistic MUDs: Mob Behavior (rec.games.mud.admin)
Mobprogs and Scripting
- ROM24 Mobprog Documentation
- SMAUG Building Pages - Programs
- Mprog Compendium - Cleft of Dimensions
- Aardwolf Lua Scripting Overview
- Aardwolf Lua Triggers
- Aardwolf Lua Functions
- Aardwolf Lua Howto and Snippets
- Aardwolf Onslaught of Chaos (Lua Examples)
- Aardwolf Lua/MUD Integration
- Aardwolf Area Building
LPC
Ranvier
State Machines and Behavior Trees
- Game Programming Patterns: State
- Behavior Trees for Computer Games (ResearchGate)
- Designing Game AI with FSMs (Game Developer)
Reset and Spawn Systems
Economy
Procedural Generation and Dynamic Content
- GenMUD - Procedural Content Generation Wiki
- Procedural Generation - Muds Wiki
- StoryWorld: Procedural Quest Generation (ACM)
- QuestWeaver - Procedural Quest Framework (GitHub)
- MUD Cookbook: Design Meets Implementation
- NPC Conversation Techniques (GameDev.net)