"""Zone — a spatial area with a terrain grid.""" from __future__ import annotations import random from dataclasses import dataclass, field from mudlib.object import Object @dataclass(eq=False) class BoundaryRegion: """A rectangular region within a zone that can trigger effects. Used for anti-theft detection, level gates, messages, etc. """ name: str x_min: int x_max: int y_min: int y_max: int on_enter_message: str | None = None on_exit_message: str | None = None on_exit_check: str | None = None on_exit_fail: str | None = None def contains(self, x: int, y: int) -> bool: """Check if a position is inside this boundary region.""" return self.x_min <= x <= self.x_max and self.y_min <= y <= self.y_max @dataclass(eq=False) class SpawnRule: """Configuration for spawning mobs in a zone. Defines what mob should spawn, how many can exist at once, and how long to wait before respawning after one dies. """ mob: str max_count: int = 1 respawn_seconds: int = 300 home_region: dict | None = None @dataclass(eq=False) 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. """ description: str = "" width: int = 0 height: int = 0 toroidal: bool = True terrain: list[list[str]] = field(default_factory=list) impassable: set[str] = field(default_factory=lambda: {"^", "~"}) spawn_x: int = 0 spawn_y: int = 0 ambient_messages: list[str] = field(default_factory=list) ambient_interval: int = 120 spawn_rules: list[SpawnRule] = field(default_factory=list) safe: bool = False boundaries: list[BoundaryRegion] = field(default_factory=list) 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 def get_ambient_message(zone: Zone) -> str | None: """Return a random ambient message from the zone's list. Args: zone: The zone to get an ambient message from Returns: A random message from the zone's ambient_messages list, or None if the list is empty """ if not zone.ambient_messages: return None return random.choice(zone.ambient_messages)