Add NPC/mob system documentation

This commit is contained in:
Jared Miller 2026-02-16 16:20:35 -05:00
parent 50a1d356a8
commit db8b395257
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C

193
docs/how/npc-mobs.rst Normal file
View file

@ -0,0 +1,193 @@
================
npc/mob system
================
the npc/mob system handles non-player characters: enemies, allies, townsfolk. mobs can wander, patrol, converse, flee, and fight. they follow schedules and have behavior states that transition based on game events.
entity model
============
all characters (players and mobs) inherit from ``Entity`` in ``entity.py``. this gives them:
- ``pl`` (power level) - combat effectiveness
- ``stamina`` and ``max_stamina`` - resource for moves
- ``posture`` - computed @property with priority order: "unconscious", "fighting", "flying", "sleeping", "resting", "standing"
- ``x``, ``y``, ``zone`` - location in the world
the ``Mob`` subclass adds:
- ``description`` - what you see when you look at them
- ``alive`` - whether they're dead
- ``moves`` - list of combat move names they can use
- ``next_action_at`` - throttle for AI decisions
- ``home_x_min``, ``home_x_max``, ``home_y_min``, ``home_y_max`` - wander bounds
- ``behavior_state`` - current state: "idle", "patrol", "converse", "flee", "working"
- ``behavior_data`` - dict with state-specific data (waypoints, threat coords, etc)
- ``npc_name`` - key into dialogue tree registry
- ``schedule`` - ``NpcSchedule`` instance with hourly behavior changes
mob templates
=============
templates live in ``content/mobs/`` as TOML files. ``MobTemplate`` in ``mobs.py`` defines the schema::
name = "goblin"
description = "a snarling goblin with a crude club"
pl = 50.0
stamina = 40.0
max_stamina = 40.0
moves = ["punch left", "punch right", "sweep"]
[[loot]]
name = "crude club"
chance = 0.8
description = "a crude wooden club"
``spawn_mob(template, x, y, zone, home_region)`` creates a ``Mob`` from a template and registers it. ``despawn_mob(mob)`` marks it dead and removes it from the registry. ``get_nearby_mob(name, x, y, zone, radius)`` finds mobs by name within range (toroidal-aware).
behavior states
===============
the behavior system in ``npc_behavior.py`` is a state machine. ``process_behavior(mob)`` dispatches to handlers based on ``mob.behavior_state``:
- **idle** - default state, wanders toward home region center
- **patrol** - moves toward waypoints in ``behavior_data["waypoints"]``
- **converse** - locked in place, talking to a player
- **flee** - runs away from threat at ``behavior_data["flee_from"]``
- **working** - schedule-driven state (librarian at desk, blacksmith at forge)
transitions happen when:
- player starts conversation → "converse"
- schedule hour changes → state from active schedule entry
- conversation ends → restores ``previous_state`` from conversation data
- flee timeout expires → return to previous state
helper functions calculate movement direction:
- ``get_patrol_direction(mob)`` - toward next waypoint (toroidal)
- ``get_flee_direction(mob)`` - away from threat (toroidal)
schedules
=========
scheduled NPCs have ``NpcSchedule`` in ``npc_schedule.py``. a schedule is a list of ``ScheduleEntry``::
[[schedule]]
hour = 7
state = "working"
location = [10, 20, "town"]
[[schedule]]
hour = 21
state = "idle"
``get_active_entry(hour)`` returns the most recent entry at or before the given hour. ``process_schedules(game_time)`` is called from the game loop when the hour changes. it:
- finds all mobs with schedules
- skips dead mobs and mobs in conversation
- checks if the active entry changed
- teleports mob to new location if specified
- transitions to new state
- clears behavior_data if state changed
mob ai
======
combat ai
---------
``process_mobs()`` in ``mob_ai.py`` runs every tick (10/sec). for mobs in combat:
- **defender with incoming attack** - 40% chance of correct counter, 60% random affordable defense
- **attacker outside defend window** - random affordable attack
the ai respects throttles (``next_action_at``) and stamina costs. it uses ``get_affordable_moves()`` to filter by stamina.
movement ai
-----------
``process_mob_movement()`` runs every tick with a 3-second cooldown per mob. behavior-based movement:
- **patrol** - toward next waypoint, cycles through list
- **flee** - away from threat coordinates
- **idle** - toward home region center
- **working** - no movement
movement checks passability via ``world.is_passable(x, y, zone)``. broadcasts departure/arrival to nearby players. uses toroidal distance for direction calculation.
dialogue and conversation
=========================
``DialogueTree`` in ``dialogue.py`` has ``DialogueNode`` entries::
[dialogue.librarian.greeting]
text = "welcome to the library"
[[dialogue.librarian.greeting.choices]]
text = "what books do you have?"
next_node = "books"
``DialogueChoice`` has optional ``condition`` (python expression) and ``action`` (function name).
``ConversationState`` in ``conversation.py`` tracks:
- ``tree`` - the dialogue tree
- ``current_node`` - id of current node
- ``npc`` - the mob being talked to
- ``previous_state`` - mob's state before conversation
``start_conversation(player, npc)`` transitions the mob to "converse" state. ``end_conversation(player)`` restores the mob's previous state. active conversations are stored in a dict keyed by player name.
commands
========
talk and reply
--------------
``cmd_talk`` in ``commands/talk.py``:
- finds nearby NPC by name
- starts conversation
- shows greeting text and choices
``cmd_reply``:
- takes choice number as argument
- advances to next node
- ends conversation if no next node
spawn
-----
``cmd_spawn`` in ``commands/spawn.py``:
- takes template name
- creates mob at player location
- sets home region to 5-tile radius
game loop integration
=====================
the ``game_loop()`` function in ``server.py`` calls:
- ``process_mobs()`` - every tick (10/sec)
- ``process_mob_movement()`` - every tick (10/sec)
- ``process_schedules(game_time)`` - when hour changes
this keeps mob AI responsive while preventing spam. the throttles (``next_action_at`` for combat, 3s cooldown for movement) ensure mobs don't act too frequently.
code
====
- ``src/mudlib/entity.py`` - Entity and Mob classes
- ``src/mudlib/mobs.py`` - MobTemplate, spawn/despawn registry
- ``src/mudlib/npc_behavior.py`` - behavior state machine
- ``src/mudlib/npc_schedule.py`` - NpcSchedule and ScheduleEntry
- ``src/mudlib/mob_ai.py`` - combat and movement AI
- ``src/mudlib/dialogue.py`` - DialogueTree, DialogueNode, DialogueChoice
- ``src/mudlib/conversation.py`` - ConversationState, start/end conversation
- ``src/mudlib/commands/talk.py`` - talk and reply commands
- ``src/mudlib/commands/spawn.py`` - spawn command
- ``content/mobs/`` - mob template TOML files
- ``content/dialogue/`` - dialogue tree TOML files