mud/src/mudlib/npc_schedule.py
Jared Miller 52f49104eb
Add NPC schedule system with game time
Implements time-based behavior transitions for NPCs:
- GameTime converts real time to game time (1 real min = 1 game hour)
- ScheduleEntry defines hour/state/location/data transitions
- NpcSchedule manages multiple entries with midnight wrapping
- process_schedules() applies transitions when game hour changes
- TOML support for schedule data in mob templates
- Integrated into game loop with hourly checks

Tests cover schedule transitions, game time calculation, TOML loading, and preventing duplicate processing.
2026-02-14 14:31:39 -05:00

101 lines
3.2 KiB
Python

"""NPC schedule system for time-based behavior transitions."""
from dataclasses import dataclass
from mudlib.entity import Mob
from mudlib.npc_behavior import transition_state
@dataclass
class ScheduleEntry:
"""A single schedule entry for a specific hour."""
hour: int # Game hour (0-23) when this entry activates
state: str # Behavior state to transition to
location: dict | None = None # Optional {"x": int, "y": int} to move to
data: dict | None = None # Optional behavior_data to set
@dataclass
class NpcSchedule:
"""Schedule for an NPC with multiple time-based entries."""
entries: list[ScheduleEntry]
def get_active_entry(self, hour: int) -> ScheduleEntry:
"""Get the schedule entry that should be active at the given hour.
Returns the most recent entry at or before the given hour,
wrapping around midnight if necessary.
Args:
hour: Game hour (0-23)
Returns:
The active ScheduleEntry for this hour
"""
if not self.entries:
raise ValueError("Schedule has no entries")
# Find the most recent entry at or before this hour
best_entry = None
best_hour = -1
for entry in self.entries:
if entry.hour <= hour and entry.hour > best_hour:
best_entry = entry
best_hour = entry.hour
# If no entry found before current hour, wrap around to last entry
if best_entry is None:
# Find the entry with the highest hour
best_entry = max(self.entries, key=lambda e: e.hour)
return best_entry
def process_schedules(mob_list: list[Mob], game_hour: int) -> None:
"""Process NPC schedules and apply transitions if hour has changed.
Args:
mob_list: List of mobs to process
game_hour: Current game hour (0-23)
"""
for mob in mob_list:
if not mob.alive:
continue
# Skip mobs in conversation (don't interrupt active player interaction)
if mob.behavior_state == "converse":
continue
schedule = mob.schedule
if schedule is None or not isinstance(schedule, NpcSchedule):
continue
# Check if we've already processed this hour
if mob.schedule_last_hour == game_hour:
continue
# Get the active entry for this hour
entry = schedule.get_active_entry(game_hour)
# Check if anything needs to change
needs_state_change = mob.behavior_state != entry.state
needs_location_change = entry.location is not None and (
mob.x != entry.location["x"] or mob.y != entry.location["y"]
)
needs_data_change = entry.data is not None
# Only apply changes if something actually changed
if needs_state_change or needs_location_change or needs_data_change:
# Transition state (this also sets behavior_data if entry.data is set)
transition_state(mob, entry.state, entry.data)
# Move mob if location specified
if entry.location is not None:
mob.x = entry.location["x"]
mob.y = entry.location["y"]
# Mark this hour as processed
mob.schedule_last_hour = game_hour