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.
101 lines
3.2 KiB
Python
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
|