diff --git a/evennia/commands/default/general.py b/evennia/commands/default/general.py index 8c838ff099..e6c4eb2aa0 100644 --- a/evennia/commands/default/general.py +++ b/evennia/commands/default/general.py @@ -83,7 +83,10 @@ class CmdLook(COMMAND_DEFAULT_CLASS): target = caller.search(self.args) if not target: return - self.msg((caller.at_look(target), {"type": "look"}), options=None) + desc = caller.at_look(target) + # add the type=look to the outputfunc to make it + # easy to separate this output in client. + self.msg(text=(desc, {"type": "look"}), options=None) class CmdNick(COMMAND_DEFAULT_CLASS): diff --git a/evennia/contrib/xyzgrid/commands.py b/evennia/contrib/xyzgrid/commands.py index eb59067750..044f11e2c6 100644 --- a/evennia/contrib/xyzgrid/commands.py +++ b/evennia/contrib/xyzgrid/commands.py @@ -36,7 +36,7 @@ class CmdXYZLook(general.CmdLook): maxw = min(xymap.max_x, self.client_width()) sep = "~" * maxw map_display = f"|x{sep}|n\n{map_display}\n|x{sep}" - self.msg(map_display, {"type", "xymap"}, options=None) + self.msg((map_display, {"type": "xymap"}), options=None) # now run the normal look command super().func() @@ -55,6 +55,7 @@ class CmdXYZTeleport(building.CmdTeleport): tel/tonone box tel (3, 3, the small cave) tel (4, 1) # on the same map + tel/map Z|mapname Switches: quiet - don't echo leave/arrive messages to the source/target @@ -67,6 +68,7 @@ class CmdXYZTeleport(building.CmdTeleport): an object from a None location is by direct #dbref reference. A puppeted object cannot be moved to None. loc - teleport object to the target's location instead of its contents + map - show coordinate map of given Zcoord/mapname. Teleports an object somewhere. If no object is given, you yourself are teleported to the target location. If (X,Y) or (X,Y,Z) coordinates @@ -74,6 +76,7 @@ class CmdXYZTeleport(building.CmdTeleport): """ def _search_by_xyz(self, inp): + inp = inp.strip("()") X, Y, *Z = inp.split(",", 2) if Z: # Z was specified @@ -93,7 +96,7 @@ class CmdXYZTeleport(building.CmdTeleport): try: self.destination = XYZRoom.objects.get_xyz(xyz=(X, Y, Z)) except XYZRoom.DoesNotExist: - self.caller.msg("Found no target XYZRoom at ({X},{Y},{Y}).") + self.caller.msg(f"Found no target XYZRoom at ({X},{Y},{Z}).") raise InterruptCommand def parse(self): @@ -114,7 +117,7 @@ class CmdXYZTeleport(building.CmdTeleport): self.destination = self.caller.search(self.rhs, global_search=True) elif self.lhs: - if all(char in self.rhs for char in ("(", ")", ",")): + if all(char in self.lhs for char in ("(", ")", ",")): self._search_by_xyz(self.lhs) else: self.destination = self.caller.search(self.lhs, global_search=True) @@ -189,6 +192,5 @@ class XYZGridCmdSet(CmdSet): key = "xyzgrid_cmdset" def at_cmdset_creation(self): - self.add(CmdXYZLook()) self.add(CmdXYZTeleport()) self.add(CmdXYZOpen()) diff --git a/evennia/contrib/xyzgrid/launchcmd.py b/evennia/contrib/xyzgrid/launchcmd.py index 467cbc1d03..e46e395216 100644 --- a/evennia/contrib/xyzgrid/launchcmd.py +++ b/evennia/contrib/xyzgrid/launchcmd.py @@ -18,6 +18,7 @@ Use `evennia xyzgrid help` for usage help. from os.path import join as pathjoin from django.conf import settings +from evennia.utils import ansi from evennia.contrib.xyzgrid.xyzgrid import get_xyzgrid _HELP_SHORT = """ @@ -187,7 +188,7 @@ def _option_list(*suboptions): print("XYMaps stored in grid:") for zcoord, xymap in sorted(xymap_data.items(), key=lambda tup: tup[0]): print("\n" + str(repr(xymap)) + ":\n") - print(str(xymap)) + print(ansi.parse_ansi(str(xymap))) return for zcoord in suboptions: @@ -216,7 +217,7 @@ def _option_list(*suboptions): print("Note: This check is not complete; it does not consider changed map " "topology\nlike relocated nodes/rooms and new/removed links/exits - this " "is calculated only during a build.") - print("\nDisplayed map (as appearing in-game):\n\n" + str(xymap)) + print("\nDisplayed map (as appearing in-game):\n\n" + ansi.parse_ansi(str(xymap))) print("\nRaw map string (including axes and invisible nodes/links):\n" + str(xymap.mapstring)) legend = [] diff --git a/evennia/contrib/xyzgrid/map_legend.py b/evennia/contrib/xyzgrid/map_legend.py index c98d7d5dcc..c3eeec328e 100644 --- a/evennia/contrib/xyzgrid/map_legend.py +++ b/evennia/contrib/xyzgrid/map_legend.py @@ -1088,6 +1088,7 @@ class MapTransitionMapNode(TransitionMapNode): class NSMapLink(MapLink): """Two-way, North-South link""" symbol = "|" + display_symbol = "||" directions = {"n": "s", "s": "n"} prototype = "xyz_exit" @@ -1164,6 +1165,7 @@ class UpMapLink(SmartMapLink): # all movement over this link is 'up', regardless of where on the xygrid we move. direction_aliases = {'n': symbol, 'ne': symbol, 'e': symbol, 'se': symbol, 's': symbol, 'sw': symbol, 'w': symbol, 'nw': symbol} + spawn_aliases = {direction: ("up", "u") for direction in direction_aliases} prototype = "xyz_exit" @@ -1173,6 +1175,7 @@ class DownMapLink(UpMapLink): # all movement over this link is 'down', regardless of where on the xygrid we move. direction_aliases = {'n': symbol, 'ne': symbol, 'e': symbol, 'se': symbol, 's': symbol, 'sw': symbol, 'w': symbol, 'nw': symbol} + spawn_aliases = {direction: ("down", "do") for direction in direction_aliases} prototype = "xyz_exit" diff --git a/evennia/contrib/xyzgrid/xymap.py b/evennia/contrib/xyzgrid/xymap.py index 90f7012820..c7c808c2ed 100644 --- a/evennia/contrib/xyzgrid/xymap.py +++ b/evennia/contrib/xyzgrid/xymap.py @@ -774,6 +774,7 @@ class XYMap: character='@', target=None, target_path_style="|y{display_symbol}|n", max_size=None, + indent=0, return_str=True): """ Get a part of the grid centered on a specific point and extended a certain number @@ -791,7 +792,7 @@ class XYMap: nodes are reachable or not. If 'nodes', distance measure how many linked nodes away from the center coordinate to display. character (str, optional): Place this symbol at the `xy` position - of the displayed map. The center node' symbol is shown if this is falsy. + of the displayed map. The center node's symbol is shown if this is falsy. target (tuple, optional): A target XY coordinate to go to. The path to this (or the beginning of said path, if outside of visual range) will be marked according to `target_path_style`. @@ -802,8 +803,11 @@ class XYMap: will receive the MapNode or MapLink object for every step of the path and and must return the suitable string to display at the position of the node/link. max_size (tuple, optional): A max `(width, height)` to crop the displayed - return to. Make both odd numbers to get a perfect center. - If unset, display-size can grow up to the full size of the grid. + return to. Make both odd numbers to get a perfect center. Set either of + the tuple values to `None` to make that coordinate unlimited. Set entire + tuple to None let display-size able to grow up to full size of grid. + indent (int, optional): How far to the right to indent the map area (only + applies to `return_str=True`). return_str (bool, optional): Return result as an already formatted string or a 2D list. @@ -914,12 +918,15 @@ class XYMap: if max_size: # crop grid to make sure it doesn't grow too far max_x, max_y = max_size + max_x = self.max_x if max_x is None else max_x + max_y = self.max_y if max_y is None else max_y xmin, xmax = max(0, ixc - max_x // 2), min(width, ixc + max_x // 2 + 1) ymin, ymax = max(0, iyc - max_y // 2), min(height, iyc + max_y // 2 + 1) gridmap = [line[xmin:xmax] for line in gridmap[ymin:ymax]] if return_str: # we must flip the y-axis before returning the string - return "\n".join("".join(line) for line in gridmap[::-1]) + indent = indent * " " + return indent + f"\n{indent}".join("".join(line) for line in gridmap[::-1]) else: return gridmap diff --git a/evennia/contrib/xyzgrid/xyzgrid.py b/evennia/contrib/xyzgrid/xyzgrid.py index 003e63208c..0bf3f5f5a3 100644 --- a/evennia/contrib/xyzgrid/xyzgrid.py +++ b/evennia/contrib/xyzgrid/xyzgrid.py @@ -34,6 +34,7 @@ class XYZGrid(DefaultScript): """ self.db.map_data = {} + self.desc = "Manages maps for XYZ-grid" @property def grid(self): diff --git a/evennia/contrib/xyzgrid/xyzroom.py b/evennia/contrib/xyzgrid/xyzroom.py index a0648ada15..1274a7abda 100644 --- a/evennia/contrib/xyzgrid/xyzroom.py +++ b/evennia/contrib/xyzgrid/xyzroom.py @@ -8,6 +8,7 @@ used as stand-alone XYZ-coordinate-aware rooms. """ from django.db.models import Q +from django.conf import settings from evennia.objects.objects import DefaultRoom, DefaultExit from evennia.objects.manager import ObjectManager @@ -226,11 +227,34 @@ class XYZRoom(DefaultRoom): """ A game location aware of its XYZ-position. + Special properties: + map_display (bool): If the return_appearance of the room should + show the map or not. + map_mode (str): One of 'nodes' or 'scan'. See `return_apperance` + for examples of how they differ. + map_visual_range (int): How far on the map one can see. This is a + fixed value here, but could also be dynamic based on skills, + light etc. + map_character_symbol (str): The character symbol to use to show + the character position. Can contain color info. Default is + the @-character. + map_area_client (bool): If True, map area will always fill the entire + client width. If False, the map area's width will vary with the + width of the currently displayed location description. + """ # makes the `room.objects.filter_xymap` available objects = XYZManager() + # default settings for map visualization + map_display = True + map_mode = 'nodes' # or 'scan' + map_visual_range = 2 + map_character_symbol = "@" + map_align = 'c' + map_area_client = True + def __str__(self): return repr(self) @@ -252,10 +276,12 @@ class XYZRoom(DefaultRoom): y = self.tags.get(category=MAP_Y_TAG_CATEGORY, return_list=False) z = self.tags.get(category=MAP_Z_TAG_CATEGORY, return_list=False) if x is None or y is None or z is None: - # don't cache unfinished coordinate - return (x, y, z) - # cache result - self._xyz = (x, y, z) + # don't cache unfinished coordinate (probably tags have not finished saving) + return tuple(int(coord) if coord is not None and coord.isdigit() else coord + for coord in (x, y, z)) + # cache result, convert to correct types (tags are strings) + self._xyz = tuple(int(coord) if coord.isdigit() else coord for coord in (x, y, z)) + return self._xyz @classmethod @@ -302,6 +328,106 @@ class XYZRoom(DefaultRoom): return DefaultRoom.create(key, account=account, tags=tags, typeclass=cls, **kwargs) + def return_appearance(self, looker, **kwargs): + """ + Displays the map in addition to the room description + + Args: + looker (Object): The one looking. + + Keyword Args: + map_display (bool): Turn on/off map display. + map_visual_range (int): How 'far' one can see on the map. For + 'nodes' mode, this is how many connected nodes away, for + 'scan' mode, this is number of characters away on the map. + Default is a visual range of 2 (nodes). + map_mode (str): One of 'node' (default) or 'scan'. + map_character_symbol (str): The character symbol to use. Defaults to '@'. + This can also be colored with standard color tags. Set to `None` + to just show the current node. + + Examples: + + Assume this is the full map (where '@' is the character location): + :: + #----------------# + | | + | | + # @------------#-# + | | + #----------------# + + This is how it will look in 'nodes' mode with `visual_range=2`: + :: + @------------#-# + + And in 'scan' mode with `visual_range=2`: + :: + | + | + # @-- + | + #---- + + Notes: + The map kwargs default to values with the same names set on the + XYZRoom class; these can be changed by overriding the room. + + We return the map display as a separate msg() call here, in order + to make it easier to break this out into a client pane etc. The + map is tagged with type='xymap'. + + """ + + # normal get_appearance of a room + room_desc = super().return_appearance(looker, **kwargs) + + if kwargs.get('map_display', self.map_display): + # show the near-area map. + + character_symbol = kwargs.get('map_character_symbol', self.map_character_symbol) + visual_range = kwargs.get("visual_range", self.map_visual_range) + map_mode = kwargs.get("map_mode", self.map_mode) + map_align = kwargs.get("map_align", self.map_align) + map_area_client = kwargs.get("fill_width", self.map_area_client) + client_width, _ = looker.sessions.get()[0].get_client_size() + + # get current xymap + xyz = self.xyz + xymap = self.xyzgrid.get_map(xyz[2]) + map_width = xymap.max_x + + if map_area_client: + display_width = client_width + else: + display_width = max(map_width, + max(len(line) for line in room_desc.split("\n"))) + + # align map + map_indent = 0 + sep_width = display_width + if map_align == 'r': + map_indent = max(0, display_width - map_width) + elif map_align == 'c': + map_indent = max(0, (display_width - map_width) // 2) + + # get visual range display from map + map_display = xymap.get_visual_range( + (xyz[0], xyz[1]), + dist=visual_range, + mode=map_mode, + character=character_symbol, + max_size=(display_width, None), + indent=map_indent + ) + sep = "~" * sep_width + map_display = f"|x{sep}|n\n{map_display}\n|x{sep}" + + # echo directly to make easier to separate in client + looker.msg(text=(map_display, {"type": "xymap"}), options=None) + + return room_desc + class XYZExit(DefaultExit): """ diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index 1026b7199a..3c54697e2b 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -2269,7 +2269,7 @@ class DefaultCharacter(DefaultObject): """ if self.location.access(self, "view"): - self.msg(self.at_look(self.location)) + self.msg(text=(self.at_look(self.location), {"type": "look"})) def at_pre_puppet(self, account, session=None, **kwargs): """