Skip to content

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

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:

@create npc Town Guard
@describe npc:Guard A stern-looking guard in chainmail armor.

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(
    is_aggressive=False,
    respawns=True,
    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(max_hp=100, current_hp=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:

npc.add(NPCComponent(
    is_aggressive=True,
    aggro_radius=3,  # Rooms
    faction="orcs",
    hostile_factions=["humans", "elves"],
))

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:

  1. Static Dialogue - Predefined trigger/response pairs
  2. 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=True,
    wander_chance=0.1,  # 10% chance each tick
))

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(
    is_aggressive=False,
    shop_enabled=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(
    quest_giver=True,
    available_quests=["goblin_camp", "missing_patrol"],
))

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.


Best Practices

NPC Design Tips

  1. Give NPCs purpose: Every NPC should have a reason to exist (vendor, quest, lore)
  2. Consistent personality: Maintain character across all dialogue options
  3. Memorable quirks: Add unique speech patterns, catchphrases, or behaviors
  4. 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_seconds to 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"],
)