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.
92 lines
3 KiB
Python
92 lines
3 KiB
Python
"""Base entity class for characters in the world."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from typing import TYPE_CHECKING
|
|
|
|
from mudlib.object import Object
|
|
|
|
if TYPE_CHECKING:
|
|
from mudlib.npc_schedule import NpcSchedule
|
|
|
|
|
|
@dataclass(eq=False)
|
|
class Entity(Object):
|
|
"""Base class for anything with position and identity in the world.
|
|
|
|
Inherits name, location from Object. Narrows x, y to int (entities
|
|
always have spatial coordinates).
|
|
"""
|
|
|
|
# Entities always have spatial coordinates (override Object's int | None)
|
|
x: int = 0
|
|
y: int = 0
|
|
# Combat stats
|
|
pl: float = 100.0 # power level (health and damage multiplier)
|
|
max_pl: float = 100.0 # maximum power level
|
|
stamina: float = 100.0 # current stamina
|
|
max_stamina: float = 100.0 # stamina ceiling
|
|
defense_locked_until: float = 0.0 # monotonic time when defense recovery ends
|
|
resting: bool = False # whether this entity is currently resting
|
|
sleeping: bool = False # whether this entity is currently sleeping (deep rest)
|
|
_last_stamina_cue: float = 1.0 # Last stamina percentage that triggered a cue
|
|
|
|
@property
|
|
def posture(self) -> str:
|
|
"""Return entity's current posture for room display.
|
|
|
|
Priority order: unconscious > fighting > flying > sleeping > resting > standing
|
|
"""
|
|
# Unconscious (highest priority)
|
|
if self.pl <= 0 or self.stamina <= 0:
|
|
return "unconscious"
|
|
|
|
# Fighting (only for Player with mode="combat")
|
|
if hasattr(self, "mode") and self.mode == "combat":
|
|
return "fighting"
|
|
|
|
# Flying (only for Player)
|
|
if getattr(self, "flying", False):
|
|
return "flying"
|
|
|
|
# Sleeping (before resting since sleeping implies resting)
|
|
if self.sleeping:
|
|
return "sleeping"
|
|
|
|
# Resting
|
|
if self.resting:
|
|
return "resting"
|
|
|
|
# Default
|
|
return "standing"
|
|
|
|
def can_accept(self, obj: Object) -> bool:
|
|
"""Entities accept portable Things (inventory)."""
|
|
from mudlib.thing import Thing
|
|
|
|
return isinstance(obj, Thing) and obj.portable
|
|
|
|
async def send(self, message: str) -> None:
|
|
"""Send a message to this entity. Base implementation is a no-op."""
|
|
pass
|
|
|
|
|
|
@dataclass(eq=False)
|
|
class Mob(Entity):
|
|
"""Represents a non-player character (NPC) in the world."""
|
|
|
|
description: str = ""
|
|
alive: bool = True
|
|
moves: list[str] = field(default_factory=list)
|
|
next_action_at: float = 0.0
|
|
home_x_min: int | None = None
|
|
home_x_max: int | None = None
|
|
home_y_min: int | None = None
|
|
home_y_max: int | None = None
|
|
# NPC behavior state machine
|
|
behavior_state: str = "idle"
|
|
behavior_data: dict = field(default_factory=dict)
|
|
npc_name: str | None = None # links to dialogue tree
|
|
schedule: NpcSchedule | None = None # time-based behavior schedule
|
|
schedule_last_hour: int | None = None # last hour schedule was processed
|