103 lines
3.3 KiB
Python
103 lines
3.3 KiB
Python
"""Zone — a spatial area with a terrain grid."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
|
|
from mudlib.object import Object
|
|
|
|
|
|
@dataclass
|
|
class Zone(Object):
|
|
"""A spatial area with a grid of terrain tiles.
|
|
|
|
Zones are top-level containers (location=None). Everything in the world
|
|
lives inside a zone — players, mobs, items, fixtures. The overworld is
|
|
a zone. A tavern interior is a zone. A pocket dimension is a zone.
|
|
"""
|
|
|
|
width: int = 0
|
|
height: int = 0
|
|
toroidal: bool = True
|
|
terrain: list[list[str]] = field(default_factory=list)
|
|
impassable: set[str] = field(default_factory=lambda: {"^", "~"})
|
|
|
|
def can_accept(self, obj: Object) -> bool:
|
|
"""Zones accept everything."""
|
|
return True
|
|
|
|
def wrap(self, x: int, y: int) -> tuple[int, int]:
|
|
"""Wrap or clamp coordinates depending on zone type."""
|
|
if self.toroidal:
|
|
return x % self.width, y % self.height
|
|
else:
|
|
return (
|
|
max(0, min(x, self.width - 1)),
|
|
max(0, min(y, self.height - 1)),
|
|
)
|
|
|
|
def get_tile(self, x: int, y: int) -> str:
|
|
"""Return terrain character at position (wrapping/clamping)."""
|
|
wx, wy = self.wrap(x, y)
|
|
return self.terrain[wy][wx]
|
|
|
|
def is_passable(self, x: int, y: int) -> bool:
|
|
"""Check if position is passable (wrapping/clamping)."""
|
|
return self.get_tile(x, y) not in self.impassable
|
|
|
|
def get_viewport(
|
|
self, cx: int, cy: int, width: int, height: int
|
|
) -> list[list[str]]:
|
|
"""Return 2D slice of terrain centered on (cx, cy)."""
|
|
viewport = []
|
|
half_w = width // 2
|
|
half_h = height // 2
|
|
start_x = cx - half_w
|
|
start_y = cy - half_h
|
|
|
|
for dy in range(height):
|
|
row = []
|
|
for dx in range(width):
|
|
wx, wy = self.wrap(start_x + dx, start_y + dy)
|
|
row.append(self.terrain[wy][wx])
|
|
viewport.append(row)
|
|
|
|
return viewport
|
|
|
|
def contents_at(self, x: int, y: int) -> list[Object]:
|
|
"""Return all contents at the given coordinates."""
|
|
return [obj for obj in self._contents if obj.x == x and obj.y == y]
|
|
|
|
def contents_near(self, x: int, y: int, range_: int) -> list[Object]:
|
|
"""Return all contents within range_ tiles of (x, y).
|
|
|
|
Uses wrapping-aware Manhattan distance for toroidal zones.
|
|
Includes objects at exactly (x, y).
|
|
|
|
Args:
|
|
x: X coordinate of center point
|
|
y: Y coordinate of center point
|
|
range_: Maximum Manhattan distance (inclusive)
|
|
|
|
Returns:
|
|
List of all objects within range
|
|
"""
|
|
nearby = []
|
|
for obj in self._contents:
|
|
# Skip objects without coordinates
|
|
if obj.x is None or obj.y is None:
|
|
continue
|
|
|
|
dx_dist = abs(obj.x - x)
|
|
dy_dist = abs(obj.y - y)
|
|
|
|
if self.toroidal:
|
|
# Use wrapping-aware distance
|
|
dx_dist = min(dx_dist, self.width - dx_dist)
|
|
dy_dist = min(dy_dist, self.height - dy_dist)
|
|
|
|
manhattan_dist = dx_dist + dy_dist
|
|
if manhattan_dist <= range_:
|
|
nearby.append(obj)
|
|
|
|
return nearby
|