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(
    behavior_type="passive",  # passive, hostile, friendly, merchant, etc.
    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 CharacterStatsComponent, CharacterInfoComponent

# Make NPC killable
npc.add(HealthComponent(current=100, maximum=100))

# Give NPC D&D-style ability scores
npc.add(CharacterStatsComponent(
    strength=14,
    dexterity=12,
    constitution=13,
))

# Class, race, and level live on CharacterInfoComponent
npc.add(CharacterInfoComponent(
    name="Town Guard",
    race="human",
    character_class="warrior",
    level=5,
))

# Give NPC items
npc.add(InventoryComponent(capacity=20))

Behavior Components

BehaviorConfig

BehaviorConfig describes how an NPC acts. Register it with the BehaviorSystem, which creates and tracks the NPC's runtime NPCState:

from maid_classic_rpg.models.entities.monster import BehaviorConfig, BehaviorType

behavior = BehaviorConfig(
    behavior_type=BehaviorType.GUARDIAN,
    aggro_range=3,              # Rooms to detect enemies
    home_room_id=guard_room.id,
)

# register_npc returns the runtime NPCState the system will process
npc_state = behavior_system.register_npc(
    entity_id=npc.id,
    behavior=behavior,
    territory_center=guard_room.id,
)

Aggression Levels

BehaviorType Behavior
PASSIVE Won't attack unless attacked
NEUTRAL Won't attack unless provoked
AGGRESSIVE Attacks on sight
TERRITORIAL Attacks if a player lingers
COWARDLY Flees when damaged
GUARDIAN Protects an area or entity
PACK Calls for help from nearby allies
AMBUSH Waits hidden, then attacks

Configure aggression via BehaviorConfig.behavior_type:

from maid_classic_rpg.models.entities.monster import BehaviorConfig, BehaviorType

behavior = BehaviorConfig(
    behavior_type=BehaviorType.AGGRESSIVE,  # attacks on sight
    aggro_range=3,                          # rooms to detect enemies
)
behavior_system.register_npc(entity_id=npc.id, behavior=behavior)

Threat and Memory

NPCs remember who attacked them. Always update threat through the behavior system's helpers — they store entity IDs as strings, which is what get_highest_threat_target() expects. Inserting a raw UUID key directly into threat_memory will crash get_highest_threat_target() (it calls UUID(key) on the stored key):

# Correct: go through the behavior system (stores str(entity_id) internally)
behavior_system.update_threat(npc_id=npc.id, entity_id=attacker.id, threat=10)
behavior_system.record_sighting(npc_id=npc.id, entity_id=attacker.id)

# Get highest threat target (returns a UUID | None)
state = behavior_system.get_npc_state(npc.id)
target_id = state.get_highest_threat_target() if state else None

When an NPC takes damage, BehaviorSystem also calls update_threat automatically from its DamageDealtEvent handler, so pack/call-for-help behavior is populated without manual wiring.


Dialogue Systems

The built-in live dialogue system (NPCDialogueSystem) is driven entirely by the DialogueComponent you attach to an NPC entity — see AI-Powered NPCs below. It handles player talk/ask/greet commands, LLM responses, and scripted fallbacks.

The NPCDialogue / DialogueLine models described next are data definitions only — they are not consumed by any built-in system. Use them if you write your own scripted-dialogue handler in a content pack; otherwise attach a DialogueComponent.

Static Dialogue (model definitions — not wired to the live system)

Not consumed by the built-in dialogue system

NPCDialogue and DialogueLine are plain models (not ECS components, so they cannot be attached to an entity with entity.add(...)). The built-in NPCDialogueSystem reads only DialogueComponent. The example below shows how to describe scripted trigger/response lines; wiring them to gameplay requires a custom system in your content pack.

For simple NPCs with predictable conversations, you can model the lines as data:

from maid_classic_rpg.models.npc.behavior import NPCDialogue, DialogueLine

# A data description of scripted lines — consumed only by a custom handler.
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

DialogueLine.trigger is a keyword/pattern string, and conditions/actions are string maps/lists your custom handler interprets:

# 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.models.entities.monster import BehaviorConfig, BehaviorType

# Define the patrol route as an ordered list of room IDs
patrol_rooms = [
    gate_room.id,
    wall_room_1.id,
    tower_room.id,
    wall_room_2.id,
]

# Patrol routes live on BehaviorConfig.patrol_rooms
behavior = BehaviorConfig(
    behavior_type=BehaviorType.GUARDIAN,
    home_room_id=gate_room.id,
    patrol_rooms=patrol_rooms,
)
behavior_system.register_npc(entity_id=npc.id, behavior=behavior)

Wandering Behavior

NPCs move randomly within a territory:

from maid_classic_rpg.models.entities.monster import BehaviorConfig, BehaviorType

# Wandering is driven by BehaviorConfig.wander_chance around a home room
behavior = BehaviorConfig(
    behavior_type=BehaviorType.NEUTRAL,
    home_room_id=tavern_room.id,
    wander_chance=0.1,  # 10% chance to wander each processing cycle
)
behavior_system.register_npc(
    entity_id=npc.id,
    behavior=behavior,
    territory_center=tavern_room.id,
)

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 uuid import uuid4
from maid_classic_rpg.models.economy.shop import Shop, ShopInventoryEntry, ShopType

# 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,
))

# Item template IDs come from your loaded item templates
torch_id, rope_id, potion_id = uuid4(), uuid4(), uuid4()

# Build the shop and register it with the ShopSystem
shop = Shop(
    name="Marcus's General Goods",
    owner_id=shopkeeper.id,
    room_id=market_room.id,
    shop_type=ShopType.GENERAL,
    buy_multiplier=1.2,   # Player pays 120% of value when buying
    sell_multiplier=0.5,  # Player receives 50% of value when selling
    inventory=[
        ShopInventoryEntry(item_template_id=torch_id, base_price=1, quantity=-1),  # Unlimited
        ShopInventoryEntry(item_template_id=rope_id, base_price=5, quantity=10),
        ShopInventoryEntry(item_template_id=potion_id, base_price=50, quantity=5),
    ],
)

# ShopSystem is registered by the maid-classic-rpg content pack
shop_system.manager.register_shop(shop)

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:

from maid_classic_rpg.models.social.faction import CharacterReputation

# Shops price items from a base value, the buyer's charisma modifier, and their
# reputation with the shop's faction. Both helpers live on the Shop model.
reputation = CharacterReputation(faction_id=merchants_faction_id, value=60)

buy_price = shop.calculate_buy_price(
    base_value=50,
    charisma_modifier=2,          # from the buyer's CharacterStatsComponent
    reputation=reputation.value,  # 0-100
)
sell_price = shop.calculate_sell_price(
    base_value=50,
    charisma_modifier=2,
    reputation=reputation.value,
)

Quest Givers

Quest NPC Structure

from maid_stdlib.models.quest import (
    Quest,
    QuestObjective,
    QuestReward,
    QuestPrerequisite,
    ObjectiveType,
)

# 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(
    behavior_type="friendly",
    is_quest_giver=True,
))

Defining Quests

quest = Quest(
    name="Clear the Goblin Camp",
    quest_id="goblin_camp",
    title="Clear the Goblin Camp",
    description="A goblin camp has been spotted near the village. Eliminate the threat.",
    giver_npc_id=quest_giver.id,
    prerequisites=QuestPrerequisite(min_level=5),
    objectives=[
        QuestObjective(
            objective_id="kill_goblins",
            objective_type=ObjectiveType.KILL,
            description="Kill 10 goblins",
            target_name="goblin",
            target_count=10,
        ),
        QuestObjective(
            objective_id="kill_chief",
            objective_type=ObjectiveType.KILL,
            description="Kill the Goblin Chief",
            target_name="goblin_chief",
            target_count=1,
        ),
    ],
    rewards=QuestReward(
        experience=1000,
        gold=500,
        item_template_ids=[steel_sword_template_id],  # reward item template UUIDs
        reputation_changes={"town_guard": 100},
    ),
    accept_dialogue="Those goblins have been raiding our supplies. We need someone to clear them out.",
    progress_dialogue="How goes the hunt? Have you found their camp?",
    complete_dialogue="You've done the town a great service. Here's your reward.",
)

Quest Dialogue Integration

The Quest model already carries the strings the quest system uses at each stage — accept_dialogue, progress_dialogue, and complete_dialogue (set in Defining Quests above). These are the supported, wired-up fields.

If you want the quest giver to volunteer those lines in response to player keywords, you can describe them with NPCDialogue/DialogueLine — but, as noted under Static Dialogue above, those models are not consumed by the built-in dialogue system and require a custom handler:

# Illustrative data only — requires a custom scripted-dialogue handler.
dialogue = NPCDialogue(
    npc_id=quest_giver.id,
    greeting="Ah, an adventurer. Perhaps you can help.",
    lines=[
        # Quest not started
        DialogueLine(
            trigger="quest|help|job",
            response=quest.accept_dialogue,
            conditions={"quest_status:goblin_camp": "not_started"},
            actions=["offer_quest:goblin_camp"],
        ),
        # Quest in progress
        DialogueLine(
            trigger="quest|report",
            response=quest.progress_dialogue,
            conditions={"quest_status:goblin_camp": "in_progress"},
        ),
        # Quest ready to turn in
        DialogueLine(
            trigger="quest|report|done",
            response=quest.complete_dialogue,
            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(active_goals=[
    Goal(category=GoalCategory.ACQUIRE, description="Restock shop inventory", priority=0.8),
    Goal(category=GoalCategory.DUTY, 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, ActivityType

npc.add(ScheduleComponent(blocks=[
    ScheduleBlock(start_hour=6, end_hour=8, activity=ActivityType.EAT, location="tavern", priority=1.0),
    ScheduleBlock(start_hour=8, end_hour=17, activity=ActivityType.TRADE, location="market", priority=1.0),
    ScheduleBlock(start_hour=17, end_hour=20, activity=ActivityType.SOCIALIZE, location="tavern", priority=1.0),
    ScheduleBlock(start_hour=20, end_hour=6, activity=ActivityType.SLEEP, location="home", priority=1.0),
]))

NPC Archetypes

Added in v3.1.

Archetypes are YAML-based NPC templates that define personality weights, need weights and decay rates, a default daily schedule, and innate/forbidden goals. They support single inheritance via the parent field.

The maid-classic-rpg pack loads its archetypes from the bundled maid_classic_rpg/data/npcs/archetypes.yaml. The file has a top-level archetypes: list, and each entry is keyed by archetype_id:

archetypes:
  - archetype_id: shopkeeper
    display_name: Shopkeeper
    need_weights:
      survival: 0.3
      economic: 0.9
      purpose: 0.7
      comfort: 0.5
    default_schedule:
      - start_hour: 8
        end_hour: 18
        activity: work
        location: workplace
        priority: 0.9
      - start_hour: 18
        end_hour: 22
        activity: socialize
        location: tavern
        priority: 0.7
      - start_hour: 22
        end_hour: 8
        activity: sleep
        location: home
        priority: 1.0
    innate_goals:
      - category: duty
        description: Keep the shop running.
        priority: 0.7
    personality:
      diligence: 0.8
      greed: 0.6
      sociability: 0.7

  - archetype_id: blacksmith
    display_name: Blacksmith
    parent: shopkeeper
    need_weights:
      purpose: 0.9

When an NPC is created with an archetype, the loader merges inherited fields (from parent) 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 gating fields: the need that triggers it (trigger_need), a need_threshold, an allowed mood_range, and a per-line cooldown_minutes. The YAML is a top-level map whose keys are bark pools:

# maid_classic_rpg/data/npcs/barks.yaml
default:
  - text: "Fine goods for sale! Best prices in town!"
    trigger_need: economic
    need_threshold: 0.5
    mood_range: [0.3, 1.0]
    cooldown_minutes: 8
  - text: "*rearranges wares on the counter*"
    trigger_need: purpose
    need_threshold: 0.4
    mood_range: [0.2, 1.0]
    cooldown_minutes: 12

Bark definitions are loaded by the maid-classic-rpg pack from the bundled maid_classic_rpg/data/npcs/barks.yaml. For each NPC, BarkSystem selects from the pool named str(NPCComponent.template_id) plus the default pool, filtering by the NPC's needs, mood, and each line's cooldown.

Archetype-named bark pools do not match spawned NPCs

BarkSystem looks up bark pools by str(NPCComponent.template_id) — the spawned NPC's MonsterTemplate UUID, not a human-readable archetype name. A pool keyed by a name such as merchant: therefore never matches a spawner-created NPC (whose template_id is a UUID). Put barks you want every NPC to use under the default: key, as shown above. (Autonomy components map templates to archetypes via MonsterTemplate.metadata["archetype_id"], but the bark system does not use that mapping.)


Spawner Integration

Added in v3.1.

When NPCs are spawned via the spawner system, autonomy components (NeedsComponent, GoalsComponent, ScheduleComponent) are attached automatically based on the archetype inferred from the spawned template — no manual wiring is needed.

Spawning itself is driven by registered spawn points and templates rather than a direct spawn call. You register them with the SpawnerSystem, which spawns and respawns entities during tick processing:

from maid_classic_rpg.models.world.spawn import SpawnPoint

# Register the monster template and a spawn point that references it
spawner_system.register_template(guard_captain_template)  # a MonsterTemplate
spawner_system.register_spawn_point(SpawnPoint(
    room_id=barracks.id,
    template_id=guard_captain_template.id,
    max_count=1,
))
# On its next processing cycle the SpawnerSystem spawns the NPC and, if the
# template maps to an archetype, attaches its needs, goals, and schedule.

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:

from uuid import uuid4
from maid_stdlib.components import (
    DescriptionComponent, PositionComponent, NPCComponent, DialogueComponent,
)
from maid_classic_rpg.models.economy.shop import Shop, ShopInventoryEntry
from maid_classic_rpg.models.entities.monster import BehaviorConfig, BehaviorType
from maid_classic_rpg.data.npcs.dialogue_configs import (
    create_bartender_dialogue, create_guard_dialogue,
)

# Bartender (AI-powered, using a real dialogue factory)
bartender = world.create_entity()
bartender.add(DescriptionComponent(name="Old Tom", keywords=["tom", "bartender"]))
bartender.add(PositionComponent(room_id=tavern.id))
bartender.add(NPCComponent(behavior_type="friendly"))
bartender.add(create_bartender_dialogue(tavern_name="The Rusty Bucket"))

# Shopkeeper (scripted shop registered with the ShopSystem)
shopkeeper = world.create_entity()
shopkeeper.add(DescriptionComponent(name="Merchant Mary", keywords=["mary", "merchant"]))
shopkeeper.add(PositionComponent(room_id=market.id))
shopkeeper.add(NPCComponent(behavior_type="merchant", is_merchant=True))
shop_system.manager.register_shop(Shop(
    name="Mary's Wares",
    owner_id=shopkeeper.id,
    room_id=market.id,
    inventory=[
        ShopInventoryEntry(item_template_id=uuid4(), base_price=1, quantity=-1),
        ShopInventoryEntry(item_template_id=uuid4(), base_price=5, quantity=10),
    ],
))

# Guard (behavioral patrol + scripted dialogue)
guard = world.create_entity()
guard.add(DescriptionComponent(name="Gate Guard", keywords=["guard"]))
guard.add(PositionComponent(room_id=town_gate.id))
guard.add(NPCComponent(behavior_type="passive"))
behavior_system.register_npc(
    entity_id=guard.id,
    behavior=BehaviorConfig(
        behavior_type=BehaviorType.GUARDIAN,
        home_room_id=town_gate.id,
        patrol_rooms=[town_gate.id, wall_1.id, tower.id, wall_2.id],
    ),
)
guard.add(create_guard_dialogue(location="the main gate"))

# Quest Giver (AI-powered)
mayor = world.create_entity()
mayor.add(DescriptionComponent(name="Mayor Thornwood", keywords=["mayor", "thornwood"]))
mayor.add(PositionComponent(room_id=town_hall.id))
mayor.add(NPCComponent(behavior_type="friendly", is_quest_giver=True))
mayor.add(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"],
))