Creating NPCs Guide¶
This guide covers everything you need to know about creating Non-Player Characters (NPCs) in MAID, from simple shopkeepers to complex AI-powered characters.
Table of Contents¶
- Overview
- NPC Entity Structure
- Behavior Components
- Dialogue Systems
- Patrol and Wandering
- Shopkeepers and Traders
- Quest Givers
- AI-Powered NPCs
- Best Practices
Overview¶
NPCs in MAID are entities with specialized components that define their appearance, behavior, and interactions. You can create NPCs that:
- Stand guard at specific locations
- Patrol between waypoints
- Wander randomly within a territory
- Trade items with players
- Give quests and track progress
- Engage in combat when threatened
- Have conversations using AI or scripted dialogue
NPC Complexity Levels¶
| Level | Description | Use Case |
|---|---|---|
| Static | No behavior, just description | Background atmosphere |
| Scripted | Predefined responses and actions | Vendors, quest givers |
| Behavioral | Patrol, wander, react to events | Guards, monsters |
| AI-Powered | Dynamic conversations using LLMs | Important characters |
NPC Entity Structure¶
Basic NPC Creation¶
In-game command:
Core NPC Components¶
Every NPC needs these components:
from maid_stdlib.components import (
DescriptionComponent,
PositionComponent,
NPCComponent,
)
# Create NPC entity
npc = world.create_entity()
# Required: Description
npc.add(DescriptionComponent(
name="Town Guard",
short_desc="A stern guard in chainmail",
long_desc="A stern-looking guard stands at attention, his hand resting on his sword hilt.",
keywords=["guard", "soldier", "watchman"],
))
# Required: Position
npc.add(PositionComponent(room_id=town_gate_room.id))
# Required: NPC marker
npc.add(NPCComponent(
behavior_type="passive",
respawn_time=300.0, # 5 minutes
))
Optional Components¶
Add components based on NPC functionality:
from maid_stdlib.components import HealthComponent, InventoryComponent
from maid_classic_rpg.components import CombatStatsComponent
# Make NPC killable
npc.add(HealthComponent(maximum=100, current=100))
# Give NPC combat abilities
npc.add(CombatStatsComponent(
level=5,
strength=14,
dexterity=12,
attack_bonus=3,
armor_class=15,
))
# Give NPC items
npc.add(InventoryComponent(capacity=20))
Behavior Components¶
BehaviorConfig¶
The BehaviorConfig component defines how an NPC acts:
from maid_classic_rpg.models.npc.behavior import NPCState, PatrolState
# Runtime state (created by system)
npc_state = NPCState(
entity_id=npc.id,
patrol_state=PatrolState.IDLE,
in_combat=False,
territory_center=guard_room.id,
territory_radius=3,
)
Aggression Levels¶
| Level | Behavior |
|---|---|
| Passive | Never attacks, flees if attacked |
| Neutral | Attacks only when attacked first |
| Defensive | Attacks players who attack allies |
| Aggressive | Attacks players on sight |
| Hostile to Faction | Attacks specific faction members |
Configure in NPCComponent:
Threat and Memory¶
NPCs remember who attacked them:
# In behavior system
state.threat_memory[attacker_id] = threat_level
state.last_seen[attacker_id] = current_time
# Get highest threat target
target_id = state.get_highest_threat_target()
Dialogue Systems¶
MAID supports two dialogue approaches:
- Static Dialogue - Predefined trigger/response pairs
- AI Dialogue - Dynamic LLM-generated responses
Static Dialogue¶
For simple NPCs with predictable conversations:
from maid_classic_rpg.models.npc.behavior import NPCDialogue, DialogueLine
dialogue = NPCDialogue(
npc_id=guard.id,
greeting="Halt! State your business.",
farewell="Move along, citizen.",
default_response="I don't have time for idle chatter.",
lines=[
DialogueLine(
trigger="curfew",
response="Curfew is at sundown. No exceptions.",
),
DialogueLine(
trigger="trouble",
response="There have been rumors of goblins near the forest. Stay alert.",
),
DialogueLine(
trigger="captain",
response="The Captain is in the barracks. He doesn't see just anyone.",
conditions={"reputation": "friendly"},
),
DialogueLine(
trigger="bribe",
response="*looks around* I might forget I saw you... for a price.",
conditions={"reputation": "neutral"},
actions=["start_quest:bribe_guard"],
),
],
)
Dialogue Triggers¶
Triggers match player input:
# Exact match
DialogueLine(trigger="hello", response="Greetings!")
# Keyword in message
DialogueLine(trigger="help", response="What kind of help do you need?")
# Multiple triggers
DialogueLine(trigger="yes|agree|okay", response="Very well.")
Conditional Dialogue¶
Show different responses based on game state:
DialogueLine(
trigger="quest",
response="I have a task for you...",
conditions={
"quest_status:main_quest": "not_started",
"player_level": ">=5",
},
)
DialogueLine(
trigger="quest",
response="You're already on a mission. Focus!",
conditions={
"quest_status:main_quest": "in_progress",
},
)
DialogueLine(
trigger="quest",
response="You've done well. The town thanks you.",
conditions={
"quest_status:main_quest": "completed",
},
)
Dialogue Actions¶
Trigger game actions from dialogue:
DialogueLine(
trigger="accept",
response="Excellent! Here's what you need to know...",
actions=[
"start_quest:rescue_princess",
"give_item:old_map",
"set_flag:talked_to_king",
"teleport:throne_room",
],
)
Patrol and Wandering¶
Patrol Behavior¶
NPCs follow a predefined route:
from maid_classic_rpg.systems.npc import BehaviorSystem
# Define patrol route (list of room IDs)
patrol_route = [
gate_room.id,
wall_room_1.id,
tower_room.id,
wall_room_2.id,
# Returns to gate_room
]
# Configure NPC
npc_state.patrol_route = patrol_route
npc_state.patrol_state = PatrolState.PATROLLING
npc_state.patrol_speed = 30.0 # Seconds between moves
Wandering Behavior¶
NPCs move randomly within a territory:
# Configure territory
npc_state.territory_center = tavern_room.id
npc_state.territory_radius = 2 # Rooms from center
# Enable wandering
npc.add(NPCComponent(
wander_radius=2, # Rooms from spawn point
))
Returning Home¶
When combat ends, NPCs return to their origin:
# After combat ends
if npc_state.patrol_state == PatrolState.ENGAGED:
npc_state.patrol_state = PatrolState.RETURNING
# System will move NPC back to territory_center
Shopkeepers and Traders¶
Creating a Shop NPC¶
from maid_classic_rpg.models.economy.shop import ShopInventory, ShopItem
# Create shopkeeper
shopkeeper = world.create_entity()
shopkeeper.add(DescriptionComponent(
name="Marcus the Merchant",
short_desc="A portly merchant with a friendly smile",
keywords=["marcus", "merchant", "shopkeeper"],
))
shopkeeper.add(NPCComponent(
behavior_type="merchant",
is_merchant=True,
))
# Configure shop inventory
shop = ShopInventory(
owner_id=shopkeeper.id,
name="Marcus's General Goods",
buy_multiplier=0.5, # Pays 50% of item value
sell_multiplier=1.2, # Sells at 120% of value
items=[
ShopItem(item_template="torch", quantity=-1, price=1), # Unlimited
ShopItem(item_template="rope", quantity=10, price=5),
ShopItem(item_template="health_potion", quantity=5, price=50),
],
restock_interval=3600.0, # 1 hour
)
Shop Commands¶
Players interact with shops using:
list - Show shop inventory
buy <item> [count] - Purchase items
sell <item> [count] - Sell items to shop
appraise <item> - Check item's sell value
Dynamic Pricing¶
Adjust prices based on supply, demand, or reputation:
def calculate_price(base_price: int, player: Entity, shop: ShopInventory) -> int:
price = base_price * shop.sell_multiplier
# Reputation discount
rep = player.get(ReputationComponent)
if rep and rep.get_faction_rep("merchants") > 50:
price *= 0.9 # 10% discount
# Supply/demand
if shop.get_stock(item_type) < 3:
price *= 1.2 # Low stock premium
return int(price)
Quest Givers¶
Quest NPC Structure¶
from maid_classic_rpg.models.quest import Quest, QuestObjective, QuestReward
# Create quest giver
quest_giver = world.create_entity()
quest_giver.add(DescriptionComponent(
name="Captain Aldric",
short_desc="A grizzled veteran with a worried expression",
keywords=["captain", "aldric", "veteran"],
))
quest_giver.add(NPCComponent(
is_quest_giver=True,
))
Defining Quests¶
quest = Quest(
id="goblin_camp",
name="Clear the Goblin Camp",
description="A goblin camp has been spotted near the village. Eliminate the threat.",
giver_id=captain.id,
level_requirement=5,
objectives=[
QuestObjective(
id="kill_goblins",
type="kill",
target="goblin",
count=10,
description="Kill 10 goblins",
),
QuestObjective(
id="kill_chief",
type="kill",
target="goblin_chief",
count=1,
description="Kill the Goblin Chief",
),
],
rewards=QuestReward(
gold=500,
xp=1000,
items=["steel_sword"],
reputation={"town_guard": 100},
),
dialogue={
"offer": "Those goblins have been raiding our supplies. We need someone to clear them out.",
"progress": "How goes the hunt? Have you found their camp?",
"complete": "You've done the town a great service. Here's your reward.",
},
)
Quest Dialogue Integration¶
dialogue = NPCDialogue(
npc_id=captain.id,
greeting="Ah, an adventurer. Perhaps you can help.",
lines=[
# Quest not started
DialogueLine(
trigger="quest|help|job",
response=quest.dialogue["offer"],
conditions={"quest_status:goblin_camp": "not_started"},
actions=["offer_quest:goblin_camp"],
),
# Quest in progress
DialogueLine(
trigger="quest|report",
response=quest.dialogue["progress"],
conditions={"quest_status:goblin_camp": "in_progress"},
),
# Quest ready to turn in
DialogueLine(
trigger="quest|report|done",
response=quest.dialogue["complete"],
conditions={"quest_status:goblin_camp": "ready_to_complete"},
actions=["complete_quest:goblin_camp"],
),
],
)
AI-Powered NPCs¶
For important characters who need dynamic, contextual conversations, use AI dialogue.
Basic AI NPC Setup¶
from maid_stdlib.components import DialogueComponent
# Create NPC entity (same as before)
bartender = world.create_entity()
bartender.add(DescriptionComponent(
name="Old Grimsby",
short_desc="A grizzled bartender with knowing eyes",
keywords=["bartender", "grimsby", "barkeep"],
))
bartender.add(NPCComponent())
# Add AI dialogue
bartender.add(DialogueComponent(
ai_enabled=True,
personality=(
"A gruff but kind-hearted bartender who's seen it all. "
"Wise about the world but never preachy. Has a soft spot "
"for down-on-their-luck adventurers."
),
speaking_style=(
"Short sentences. Occasional grunts. Uses 'aye' and 'nay'. "
"Wipes glasses while talking. Speaks in a low voice."
),
npc_role="bartender at The Rusty Tankard tavern",
knowledge_domains=[
"local gossip",
"drinks and brewing",
"travelers' tales",
"regular customers",
],
secret_knowledge=[
"knows about the smuggler's tunnel beneath the tavern",
"witnessed the murder last winter",
],
will_discuss=["drinks", "gossip", "weather", "room rates"],
wont_discuss=["his past", "the tunnel", "the murder"],
greeting="*looks up from polishing a glass* What'll it be?",
farewell="*nods* Take care out there.",
max_response_tokens=100,
temperature=0.7,
))
AI Configuration¶
Control AI behavior with these settings:
| Setting | Range | Description |
|---|---|---|
temperature |
0.0-1.0 | Higher = more creative/varied |
max_response_tokens |
50-500 | Response length |
cooldown_seconds |
1.0+ | Time between responses |
Temperature guidelines:
| Value | Character Type |
|---|---|
| 0.3-0.4 | Guards, officials (formal, predictable) |
| 0.5-0.6 | Merchants, craftsmen (consistent) |
| 0.7-0.8 | Bartenders, adventurers (balanced) |
| 0.9-1.0 | Mystics, tricksters (unpredictable) |
Using Factory Functions¶
For common NPC types, use the provided factories:
from maid_classic_rpg.data.npcs.dialogue_configs import (
create_bartender_dialogue,
create_guard_dialogue,
create_merchant_dialogue,
create_quest_giver_dialogue,
)
# Quick bartender setup
bartender.add(create_bartender_dialogue(
tavern_name="The Golden Griffin",
temperature=0.8,
))
# Quick guard setup
guard.add(create_guard_dialogue(
location="the eastern watchtower",
))
Player Commands for AI NPCs¶
talk <npc> <message> - Talk to NPC
ask <npc> about <topic> - Ask about something
greet <npc> - Send greeting (aliases: hello, hi)
conversations - List active conversations
endconversation <npc> - End conversation (alias: bye)
See the AI-Powered NPC Dialogue Guide for complete documentation.
NPC Autonomy¶
Added in v3.1.
Autonomy components give NPCs independent goals, needs, and daily schedules so they behave proactively rather than only reacting to player input.
NeedsComponent¶
NeedsComponent tracks NPC needs across four categories:
| NeedCategory | Examples |
|---|---|
SURVIVAL |
Hunger, rest, safety |
ECONOMIC |
Income, stock, trade |
PURPOSE |
Duty, craft, patrol |
COMFORT |
Socializing, shelter, warmth |
Each need has a 0.0–1.0 satisfaction value that decays over time, driving the NPC to seek fulfillment.
GoalsComponent¶
GoalsComponent holds a prioritized list of goals. Each goal has a GoalCategory and a GoalPredicate protocol that determines when the goal is satisfied:
from maid_stdlib.models.npc.autonomy import GoalsComponent, Goal, GoalCategory
npc.add(GoalsComponent(goals=[
Goal(category=GoalCategory.ECONOMIC, description="Restock shop inventory", priority=0.8),
Goal(category=GoalCategory.PURPOSE, description="Patrol the market square", priority=0.5),
]))
ScheduleComponent¶
ScheduleComponent defines a daily routine as a list of ScheduleBlock entries:
from maid_stdlib.models.npc.autonomy import ScheduleComponent, ScheduleBlock
npc.add(ScheduleComponent(blocks=[
ScheduleBlock(start_hour=6, end_hour=8, activity="eat_breakfast", location="tavern"),
ScheduleBlock(start_hour=8, end_hour=17, activity="tend_shop", location="market"),
ScheduleBlock(start_hour=17, end_hour=20, activity="socialize", location="tavern"),
ScheduleBlock(start_hour=20, end_hour=6, activity="sleep", location="home"),
]))
NPC Archetypes¶
Added in v3.1.
Archetypes are YAML-based NPC templates that define default components, personality weights, and need priorities. They support single inheritance via the inherits field.
Archetype definitions live at data/npcs/archetypes.yaml:
shopkeeper:
personality: "A diligent merchant focused on fair trade."
needs:
ECONOMIC: 0.9
COMFORT: 0.5
PURPOSE: 0.7
SURVIVAL: 0.3
schedule:
- { start: 8, end: 18, activity: tend_shop }
- { start: 18, end: 22, activity: socialize }
- { start: 22, end: 8, activity: sleep }
blacksmith:
inherits: shopkeeper
personality: "A hardworking smith who takes pride in the forge."
needs:
PURPOSE: 0.9
When an NPC is created with an archetype, the loader merges inherited fields with the archetype's own overrides.
Bark System¶
Added in v3.1.
Barks are short ambient lines that NPCs emit without making LLM calls. They add flavor and make the world feel alive at zero API cost.
BarkTemplate¶
Each BarkTemplate has a text string and optional conditions (time of day, weather, NPC need state, nearby entity type):
# data/npcs/barks.yaml
shopkeeper:
- text: "*rearranges wares on the counter*"
conditions: { activity: tend_shop }
- text: "Fine goods for sale! Best prices in town!"
conditions: { time: day, activity: tend_shop }
- text: "*yawns and stretches*"
conditions: { time: night }
Bark definitions are loaded from data/npcs/barks.yaml. The NPC system randomly selects a matching bark at configurable intervals.
Spawner Integration¶
Added in v3.1.
When NPCs are spawned via the spawner system with an archetype assignment, autonomy components (NeedsComponent, GoalsComponent, ScheduleComponent) are automatically attached based on the archetype definition. No manual wiring is needed:
# Spawner assigns archetype and autonomy components automatically
spawner.spawn(template="guard_captain", archetype="guard", room=barracks)
# The spawned NPC will have needs, goals, and a patrol schedule from the "guard" archetype.
Best Practices¶
NPC Design Tips¶
- Give NPCs purpose: Every NPC should have a reason to exist (vendor, quest, lore)
- Consistent personality: Maintain character across all dialogue options
- Memorable quirks: Add unique speech patterns, catchphrases, or behaviors
- Appropriate knowledge: NPCs should know about their role, not everything
Performance Considerations¶
| NPC Count | Recommendations |
|---|---|
| < 50 | AI dialogue for all |
| 50-200 | AI for important NPCs, scripted for others |
| 200+ | Mostly scripted, AI for key characters |
AI Cost Management¶
- Set reasonable
max_response_tokens(100-150 for most NPCs) - Use
cooldown_secondsto prevent spam - Consider per-player daily token budgets
- Use local Ollama for development
Testing NPCs¶
# Test dialogue
talk <npc> Hello!
talk <npc> Tell me about yourself.
ask <npc> about local news
# Test shop
list
buy torch 5
sell sword
# Test quest
talk <npc> Do you have a quest?
# Complete objectives
talk <npc> I'm done.
Example: Complete Village NPCs¶
Here's a complete example of setting up a village with various NPC types:
# Bartender (AI-powered)
bartender = create_npc(
name="Old Tom",
room=tavern,
dialogue=create_bartender_dialogue("The Rusty Bucket"),
)
# Shopkeeper (scripted)
shopkeeper = create_npc(
name="Merchant Mary",
room=market,
shop=ShopInventory(
items=[
ShopItem("torch", -1, 1),
ShopItem("rope", 10, 5),
],
),
dialogue=NPCDialogue(
greeting="Welcome to my shop!",
lines=[
DialogueLine("buy", "Just say 'list' to see my wares."),
],
),
)
# Guard (behavioral, scripted dialogue)
guard = create_npc(
name="Gate Guard",
room=town_gate,
patrol_route=[gate, wall_1, tower, wall_2],
dialogue=create_guard_dialogue("the main gate"),
)
# Quest Giver (AI-powered)
mayor = create_npc(
name="Mayor Thornwood",
room=town_hall,
dialogue=DialogueComponent(
ai_enabled=True,
personality="A worried but determined leader facing a crisis.",
npc_role="mayor of the village",
knowledge_domains=["village history", "current crisis", "politics"],
),
quests=["goblin_threat", "missing_villagers"],
)
Related Documentation¶
- AI-Powered NPC Dialogue Guide - Complete AI dialogue documentation
- AI Configuration Reference - AI provider settings
- Combat Systems Guide - NPC combat configuration
- Building Worlds Guide - Placing NPCs in the world