- Removed world module-level variable from look.py - look.cmd_look() now uses player.location.get_viewport() instead of world.get_viewport() - look.cmd_look() uses zone.contents_near() to find nearby entities instead of iterating global players/mobs lists - Wrapping calculations use zone.width/height/toroidal instead of world properties - Added type check for player.location being a Zone instance - Removed look.world injection from server.py - Updated all tests to remove look.world injection - spawn_mob() and combat commands also migrated to use Zone (player.location) - Removed orphaned code from test_mob_ai.py and test_variant_prefix.py
126 lines
3.3 KiB
Python
126 lines
3.3 KiB
Python
"""Mob template loading, global registry, and spawn/despawn/query."""
|
|
|
|
import tomllib
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
|
|
from mudlib.entity import Mob
|
|
from mudlib.zone import Zone
|
|
|
|
|
|
@dataclass
|
|
class MobTemplate:
|
|
"""Definition loaded from TOML — used to spawn Mob instances."""
|
|
|
|
name: str
|
|
description: str
|
|
pl: float
|
|
stamina: float
|
|
max_stamina: float
|
|
moves: list[str] = field(default_factory=list)
|
|
|
|
|
|
# Module-level registries
|
|
mob_templates: dict[str, MobTemplate] = {}
|
|
mobs: list[Mob] = []
|
|
|
|
|
|
def load_mob_template(path: Path) -> MobTemplate:
|
|
"""Parse a mob TOML file into a MobTemplate."""
|
|
with open(path, "rb") as f:
|
|
data = tomllib.load(f)
|
|
return MobTemplate(
|
|
name=data["name"],
|
|
description=data["description"],
|
|
pl=data["pl"],
|
|
stamina=data["stamina"],
|
|
max_stamina=data["max_stamina"],
|
|
moves=data.get("moves", []),
|
|
)
|
|
|
|
|
|
def load_mob_templates(directory: Path) -> dict[str, MobTemplate]:
|
|
"""Load all .toml files in a directory into a dict keyed by name."""
|
|
templates: dict[str, MobTemplate] = {}
|
|
for path in sorted(directory.glob("*.toml")):
|
|
template = load_mob_template(path)
|
|
templates[template.name] = template
|
|
return templates
|
|
|
|
|
|
def spawn_mob(template: MobTemplate, x: int, y: int, zone: Zone) -> Mob:
|
|
"""Create a Mob instance from a template at the given position.
|
|
|
|
Args:
|
|
template: The mob template to spawn from
|
|
x: X coordinate in the zone
|
|
y: Y coordinate in the zone
|
|
zone: The zone where the mob will be spawned
|
|
|
|
Returns:
|
|
The spawned Mob instance
|
|
"""
|
|
mob = Mob(
|
|
name=template.name,
|
|
location=zone,
|
|
x=x,
|
|
y=y,
|
|
pl=template.pl,
|
|
stamina=template.stamina,
|
|
max_stamina=template.max_stamina,
|
|
description=template.description,
|
|
moves=list(template.moves),
|
|
)
|
|
mobs.append(mob)
|
|
return mob
|
|
|
|
|
|
def despawn_mob(mob: Mob) -> None:
|
|
"""Remove a mob from the registry and mark it dead."""
|
|
mob.alive = False
|
|
if mob in mobs:
|
|
mobs.remove(mob)
|
|
|
|
|
|
def get_nearby_mob(
|
|
name: str, x: int, y: int, zone: Zone, range_: int = 10
|
|
) -> Mob | None:
|
|
"""Find the closest alive mob matching name within range.
|
|
|
|
Uses zone.contents_near() to find all nearby objects, then filters
|
|
for alive mobs matching the name and picks the closest.
|
|
|
|
Args:
|
|
name: Name of the mob to find
|
|
x: X coordinate of the search center
|
|
y: Y coordinate of the search center
|
|
zone: The zone to search in
|
|
range_: Maximum Manhattan distance (default 10)
|
|
|
|
Returns:
|
|
The closest matching mob, or None if none found
|
|
"""
|
|
best: Mob | None = None
|
|
best_dist = float("inf")
|
|
|
|
# Get all nearby objects from the zone
|
|
nearby = zone.contents_near(x, y, range_)
|
|
|
|
for obj in nearby:
|
|
# Filter for alive mobs matching the name
|
|
if not isinstance(obj, Mob) or not obj.alive or obj.name != name:
|
|
continue
|
|
|
|
# Calculate wrapping-aware distance to find closest
|
|
dx = abs(obj.x - x)
|
|
dy = abs(obj.y - y)
|
|
if zone.toroidal:
|
|
dx = min(dx, zone.width - dx)
|
|
dy = min(dy, zone.height - dy)
|
|
|
|
dist = dx + dy
|
|
if dist < best_dist:
|
|
best = obj
|
|
best_dist = dist
|
|
|
|
return best
|