Source code for evennia.contrib.grid.xyzgrid.xymap_legend

"""
# Map legend components

Each map-legend component is either a 'mapnode' - something that represents and actual in-game
location (usually a room) or a 'maplink' - something connecting nodes together. The start of a link
usually shows as an Exit, but the length of the link has no in-game equivalent.

----

"""

try:
    from numpy import zeros
except ImportError as err:
    raise ImportError(
        f"{err}\nThe XYZgrid contrib requires the SciPy package. Install with `pip install scipy'."
    )

import uuid
from collections import defaultdict

from django.core import exceptions as django_exceptions

from evennia.prototypes import spawner
from evennia.utils.utils import class_from_module

from .utils import BIGVAL, MAPSCAN, REVERSE_DIRECTIONS, MapError, MapParserError

NodeTypeclass = None
ExitTypeclass = None


UUID_XYZ_NAMESPACE = uuid.uuid5(uuid.UUID(int=0), "xyzgrid")


# Nodes/Links


[docs]class MapNode: """ This represents a 'room' node on the map. Note that the map system deals with two grids, the finer `xygrid`, which is the per-character grid on the map, and the `XYgrid` which contains only the even-integer coordinates and also represents in-game coordinates/rooms. MapNodes are always located on even X,Y coordinates on the map grid and in-game. MapNodes will also handle the syncing of themselves and all outgoing links to the grid. Attributes on the node class: - `symbol` (str) - The character to parse from the map into this node. By default this is '#' and must be a single character, with the exception of `\\ - `display_symbol` (str or `None`) - This is what is used to visualize this node later. This symbol must still only have a visual size of 1, but you could e.g. use some fancy unicode character (be aware of encodings to different clients though) or, commonly, add color tags around it. For further customization, the `.get_display_symbol` method can return a dynamically determined display symbol. If set to `None`, the `symbol` is used. - `interrupt_path` (bool): If this is set, the shortest-path algorithm will include this node as normally, but the auto-stepper will stop when reaching it, even if not having reached its target yet. This is useful for marking 'points of interest' along a route, or places where you are not expected to be able to continue without some further in-game action not covered by the map (such as a guard or locked gate etc). - `prototype` (dict) - The default `prototype` dict to use for reproducing this map component on the game grid. This is used if not overridden specifically for this coordinate. If this is not given, nothing will be spawned for this coordinate (a 'virtual' node can be useful for various reasons, mostly map-transitions). """ # symbol used to identify this link on the map symbol = "#" # if printing this node should show another symbol. If set # to the empty string, use `symbol`. display_symbol = None # this will interrupt a shortest-path step (useful for 'points' of interest, stop before # a door etc). interrupt_path = False # the prototype to use for mapping this to the grid. prototype = None # internal use. Set during generation, but is also used for identification of the node node_index = None # this should always be left True for Nodes and avoids inifinite loops during querying. multilink = True # default values to use if the exit doesn't have a 'spawn_aliases' iterable direction_spawn_defaults = { "n": ("north", "n"), "ne": ("northeast", "ne", "north-east"), "e": ("east", "e"), "se": ("southeast", "se", "south-east"), "s": ("south", "s"), "sw": ("southwest", "sw", "south-west"), "w": ("west", "w"), "nw": ("northwest", "nw", "north-west"), "d": ("down", "d", "do"), "u": ("up", "u"), }
[docs] def __init__(self, x, y, Z, node_index=0, symbol=None, xymap=None): """ Initialize the mapnode. Args: x (int): Coordinate on xygrid. y (int): Coordinate on xygrid. Z (int or str): Name/Z-pos of this map. node_index (int): This identifies this node with a running index number required for pathfinding. This is used internally and should not be set manually. symbol (str, optional): Set during parsing - allows to override the symbol based on what's set in the legend. xymap (XYMap, optional): The map object this sits on. """ self.x = x self.y = y # map name, usually self.xymap = xymap # XYgrid coordinate self.X = x // 2 self.Y = y // 2 self.Z = Z self.node_index = node_index if symbol is not None: self.symbol = symbol # this indicates linkage in 8 cardinal directions on the string-map, # n,ne,e,se,s,sw,w,nw and link that to a node (always) self.links = {} # first MapLink in each direction - used by grid syncing self.first_links = {} # this maps self.weights = {} # lowest direction to a given neighbor self.shortest_route_to_node = {} # maps the directions (on the xygrid NOT on XYgrid!) taken if stepping # out from this node in a given direction until you get to the end node. # This catches eventual longer link chains that would otherwise be lost # {startdirection: [direction, ...], ...} # where the directional path-lists also include the start-direction self.xy_steps_to_node = {} # direction-names of the closest neighbors to the node self.closest_neighbor_names = {}
def __str__(self): return f"<MapNode '{self.symbol}' {self.node_index} XY=({self.X},{self.Y})" def __repr__(self): return str(self)
[docs] def log(self, msg): """log messages using the xygrid parent""" self.xymap.log(msg)
[docs] def generate_prototype_key(self): """ Generate a deterministic prototype key to allow for users to apply prototypes without needing a separate new name for every one. """ return str(uuid.uuid5(UUID_XYZ_NAMESPACE, str((self.X, self.Y, self.Z))))
[docs] def linkweights(self, nnodes): """ Retrieve all the weights for the direct links to all other nodes. This is used for the efficient generation of shortest-paths. Args: nnodes (int): The total number of nodes Returns: scipy.array: Array of weights of the direct links to other nodes. The weight will be 0 for nodes not directly connected to one another. Notes: A node can at most have 8 connections (the cardinal directions). """ link_graph = zeros(nnodes) for node_index, weight in self.weights.items(): link_graph[node_index] = weight return link_graph
[docs] def get_display_symbol(self): """ Hook to override for customizing how the display_symbol is determined. Returns: str: The display-symbol to use. This must visually be a single character but could have color markers, use a unicode font etc. Notes: By default, just setting .display_symbol is enough. """ return self.symbol if self.display_symbol is None else self.display_symbol
[docs] def get_spawn_xyz(self): """ This should return the XYZ-coordinates for spawning this node. This normally the XYZ of the current map, but for traversal-nodes, it can also be the location on another map. Returns: tuple: The (X, Y, Z) coords to spawn this node at. """ return self.X, self.Y, self.Z
[docs] def get_exit_spawn_name(self, direction, return_aliases=True): """ Retrieve the spawn name for the exit being created by this link. Args: direction (str): The cardinal direction (n,ne etc) the want the exit name/aliases for. return_aliases (bool, optional): Also return all aliases. Returns: str or tuple: The key of the spawned exit, or a tuple (key, alias, alias, ...) """ key, *aliases = self.first_links[direction].spawn_aliases.get( direction, self.direction_spawn_defaults.get(direction, ("unknown",)) ) if return_aliases: return (key, *aliases) return key
[docs] def spawn(self): """ Build an actual in-game room from this node. This should be called as part of the node-sync step of the map sync. The reason is that the exits (next step) requires all nodes to exist before they can link up to their destinations. """ global NodeTypeclass if not NodeTypeclass: from .xyzroom import XYZRoom as NodeTypeclass if not self.prototype: # no prototype means we can't spawn anything - # a 'virtual' node. return xyz = self.get_spawn_xyz() try: nodeobj = NodeTypeclass.objects.get_xyz(xyz=xyz) except django_exceptions.ObjectDoesNotExist: # create a new entity, using the specified typeclass (if there's one) and # with proper coordinates etc typeclass = self.prototype.get("typeclass") if typeclass is None: raise MapError( f"The prototype {self.prototype} for this node has no 'typeclass' key.", self ) self.log(f" spawning room at xyz={xyz} ({typeclass})") Typeclass = class_from_module(typeclass) nodeobj, err = Typeclass.create(self.prototype.get("key", "An empty room"), xyz=xyz) if err: raise RuntimeError(err) except django_exceptions.MultipleObjectsReturned: raise MapError( f"Multiple objects found: {NodeTypeclass.objects.filter_xyz(xyz=xyz)}. " "This may be due to manual creation of XYZRooms at this position. " "Delete duplicates.", self, ) else: self.log(f" updating existing room (if changed) at xyz={xyz}") if not self.prototype.get("prototype_key"): # make sure there is a prototype_key in prototype self.prototype["prototype_key"] = self.generate_prototype_key() # apply prototype to node. This will not override the XYZ tags since # these are not in the prototype and exact=False spawner.batch_update_objects_with_prototype(self.prototype, objects=[nodeobj], exact=False)
[docs] def unspawn(self): """ Remove all spawned objects related to this node and all links. """ global NodeTypeclass if not NodeTypeclass: from .room import XYZRoom as NodeTypeclass xyz = (self.X, self.Y, self.Z) try: nodeobj = NodeTypeclass.objects.get_xyz(xyz=xyz) except django_exceptions.ObjectDoesNotExist: # no object exists pass else: nodeobj.delete()
[docs]class TransitionMapNode(MapNode): """ This node acts as an end-node for a link that actually leads to a specific node on another map. It is not actually represented by a separate room in-game. This teleportation is not understood by the pathfinder, so why it will be possible to pathfind to this node, it really represents a map transition. Only a single link must ever be connected to this node. Properties: - `target_map_xyz` (tuple) - the (X, Y, Z) coordinate of a node on the other map to teleport to when moving to this node. This should not be another TransitionMapNode (see below for how to make a two-way link). Examples: :: map1 map2 #-T #- - one-way transition from map1 -> map2. #-T T-# - two-way. Both TransitionMapNodes links to the coords of the actual rooms (`#`) on the other map (NOT to the `T`s)! """ symbol = "T" display_symbol = " " # X,Y,Z coordinates of target node taget_map_xyz = (None, None, None)
[docs] def get_spawn_xyz(self): """ Make sure to return the coord of the *target* - this will be used when building the exit to this node (since the prototype is None, this node itself will not be built). """ if any(True for coord in self.target_map_xyz if coord in (None, "unset")): raise MapParserError( f"(Z={self.xymap.Z}) has not defined its " "`.target_map_xyz` property. It must point " "to another valid xymap (Z coordinate).", self, ) return self.target_map_xyz
# ---------------------------------- # Default nodes and link classes
[docs]class BasicMapNode(MapNode): """A map node/room""" symbol = "#" prototype = "xyz_room"
[docs]class InterruptMapNode(MapNode): """A point of interest node/room. Pathfinder will ignore but auto-stepper will stop here if passing through. Starting from here is fine.""" symbol = "I" display_symbol = "#" interrupt_path = True prototype = "xyz_room"
[docs]class MapTransitionNode(TransitionMapNode): """Transition-target node to other map. This is not actually spawned in-game.""" symbol = "T" display_symbol = " " prototype = None # important to leave None! target_map_xyz = (None, None, None) # must be set manually
# all map components; used as base if not overridden LEGEND = { # nodes "#": BasicMapNode, "T": MapTransitionNode, "I": InterruptMapNode, # links "|": NSMapLink, "-": EWMapLink, "/": NESWMapLink, "\\": SENWMapLink, "x": CrossMapLink, "+": PlusMapLink, "v": NSOneWayMapLink, "^": SNOneWayMapLink, "<": EWOneWayMapLink, ">": WEOneWayMapLink, "o": RouterMapLink, "u": UpMapLink, "d": DownMapLink, "b": BlockedMapLink, "i": InterruptMapLink, "t": TeleporterMapLink, }