From db8b395257160e4cbc843a44b2d9e3b31395d1e2 Mon Sep 17 00:00:00 2001 From: Jared Miller Date: Mon, 16 Feb 2026 16:20:35 -0500 Subject: [PATCH] Add NPC/mob system documentation --- docs/how/npc-mobs.rst | 193 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 docs/how/npc-mobs.rst diff --git a/docs/how/npc-mobs.rst b/docs/how/npc-mobs.rst new file mode 100644 index 0000000..1b6a5de --- /dev/null +++ b/docs/how/npc-mobs.rst @@ -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