mud/src/mudlib/mobs.py
Jared Miller f5646589b5
Migrate look to use player.location (Zone)
- 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
2026-02-11 19:36:46 -05:00

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