diff --git a/evennia/contrib/xyzgrid/commands.py b/evennia/contrib/xyzgrid/commands.py index 044f11e2c6..7420bd158b 100644 --- a/evennia/contrib/xyzgrid/commands.py +++ b/evennia/contrib/xyzgrid/commands.py @@ -7,38 +7,15 @@ the commands with XYZ-aware equivalents. """ +from django.conf import settings from evennia import InterruptCommand from evennia import default_cmds, CmdSet -from evennia.commands.default import building, general +from evennia.commands.default import building from evennia.contrib.xyzgrid.xyzroom import XYZRoom -from evennia.utils.utils import inherits_from +from evennia.contrib.xyzgrid.xyzgrid import get_xyzgrid +from evennia.utils.utils import list_to_string, class_from_module, make_iter - -class CmdXYZLook(general.CmdLook): - - character = '@' - visual_range = 2 - map_mode = 'nodes' # or 'scan' - - def func(self): - """ - Add xymap display before the normal look command. - - """ - location = self.caller.location - if inherits_from(location, XYZRoom): - xyz = location.xyz - xymap = location.xyzgrid.get_map(xyz[2]) - map_display = xymap.get_visual_range( - (xyz[0], xyz[1]), - dist=self.visual_range, - mode=self.map_mode) - 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) - # now run the normal look command - super().func() +COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS) class CmdXYZTeleport(building.CmdTeleport): @@ -184,6 +161,106 @@ class CmdXYZOpen(building.CmdOpen): self.exit_typeclass = self.lhs_objs[0]["option"] +class CmdGoto(COMMAND_DEFAULT_CLASS): + """ + Go to a named location in this area. + + Usage: + goto - get path and start walking + path - just check the path + goto - abort current goto + path - show current path + + This will find the shortest route to a location in your current area and + start automatically walk you there. Builders can also specify a specific grid + coordinate (X,Y). + + """ + key = "goto" + aliases = "path" + help_category = "General" + locks = "cmd:all()" + + def _search_by_xyz(self, inp, xyz_start): + inp = inp.strip("()") + X, Y = inp.split(",", 2) + Z = xyz_start[2] + # search by coordinate + X, Y, Z = str(X).strip(), str(Y).strip(), str(Z).strip() + try: + return XYZRoom.objects.get_xyz(xyz=(X, Y, Z)) + except XYZRoom.DoesNotExist: + self.caller.msg(f"Could not find a room at ({X},{Y}) (Z={Z}).") + return None + + def _search_by_key_and_alias(self, inp, xyz_start): + Z = xyz_start[2] + candidates = list(XYZRoom.objects.filter_xyz(xyz=('*', '*', Z))) + return self.caller.search(inp, candidates=candidates) + + def func(self): + """ + Implement command + """ + + caller = self.caller + + current_target, *current_path = make_iter(caller.ndb.xy_current_goto) + goto_mode = self.cmdname == 'goto' + + if not self.args: + if current_target: + if goto_mode: + caller.ndb.xy_current_goto_target = None + caller.msg("Aborted goto.") + else: + caller.msg(f"Remaining steps: {list_to_string(current_path)}") + else: + caller.msg("Usage: goto ") + return + + xyzgrid = get_xyzgrid() + try: + xyz_start = caller.location.xyz + except AttributeError: + self.caller.msg("Cannot path-find since the current location is not on the grid.") + return + + allow_xyz_query = caller.locks.check_lockstring(caller, "perm(Builder)") + if allow_xyz_query and all(char in self.args for char in ("(", ")", ",")): + # search by (X,Y) + target = self._search_by_xyz(self.args, xyz_start) + if not target: + return + else: + # search by normal key/alias + target = self._search_by_key_and_alias(self.args, xyz_start) + if not target: + return + try: + xyz_end = target.xyz + except AttributeError: + self.caller.msg("Target location is not on the grid and cannot be auto-walked to.") + return + + xymap = xyzgrid.get_map(xyz_start[2]) + # we only need the xy coords once we have the map + xy_start = xyz_start[:2] + xy_end = xyz_end[:2] + shortest_path, _ = xymap.get_shortest_path(xy_start, xy_end) + + caller.msg(f"There are {len(shortest_path)} steps to {target.get_display_name(caller)}: " + f"|w{list_to_string(shortest_path, endsep='|nand finally|w')}|n") + + # store for use by the return_appearance hook on the XYZRoom + caller.ndb.xy_current_goto = (xy_end, shortest_path) + + if self.cmdname == "goto": + # start actually walking right away + self.msg("Walking ... eventually") + pass + + class XYZGridCmdSet(CmdSet): """ Cmdset for easily adding the above cmds to the character cmdset. @@ -194,3 +271,4 @@ class XYZGridCmdSet(CmdSet): def at_cmdset_creation(self): self.add(CmdXYZTeleport()) self.add(CmdXYZOpen()) + self.add(CmdGoto()) diff --git a/evennia/contrib/xyzgrid/launchcmd.py b/evennia/contrib/xyzgrid/launchcmd.py index e46e395216..d3506f3aa0 100644 --- a/evennia/contrib/xyzgrid/launchcmd.py +++ b/evennia/contrib/xyzgrid/launchcmd.py @@ -320,7 +320,7 @@ def _option_delete(*suboptions): if not suboptions: repl = input("WARNING: This will delete the ENTIRE Grid and wipe all rooms/exits!" "\nObjects/Chars inside deleted rooms will be moved to their home locations." - "\nThis can't be undone. Are you sure you want to continue? Y/[N]?") + "\nThis can't be undone. Are you sure you want to continue? Y/[N]? ") if repl.lower() not in ('yes', 'y'): print("Aborted.") return @@ -342,7 +342,7 @@ def _option_delete(*suboptions): repl = input("This will delete map(s) {', '.join(zcoords)} and wipe all corresponding\n" "rooms/exits!" "\nObjects/Chars inside deleted rooms will be moved to their home locations." - "\nThis can't be undone. Are you sure you want to continue? Y/[N]?") + "\nThis can't be undone. Are you sure you want to continue? Y/[N]? ") if repl.lower() not in ('yes', 'y'): print("Aborted.") return @@ -378,3 +378,6 @@ def xyzcommand(*args): _option_initpath(*suboptions) elif option == 'delete': _option_delete(*suboptions) + else: + print(f"Unknown option '{option}'. Use 'evennia xyzgrid help' for valid arguments.") + diff --git a/evennia/contrib/xyzgrid/map_legend.py b/evennia/contrib/xyzgrid/map_legend.py index c3eeec328e..2d8e49d045 100644 --- a/evennia/contrib/xyzgrid/map_legend.py +++ b/evennia/contrib/xyzgrid/map_legend.py @@ -326,8 +326,9 @@ class MapNode: maplinks = {} for direction, link in self.first_links.items(): + key, *aliases = ( - make_iter(link.spawn_aliases) + link.spawn_aliases.get(direction, ('unknown',)) if link.spawn_aliases else self.direction_spawn_defaults.get(direction, ('unknown',)) ) @@ -368,7 +369,7 @@ class MapNode: if err: raise RuntimeError(err) linkobjs[key.lower()] = exi - self.log(f" spawning/updating exit xyz={xyz}, direction={direction}") + self.log(f" spawning/updating exit xyz={xyz}, direction={key}") # apply prototypes to catch any changes for key, linkobj in linkobjs.items(): @@ -1175,7 +1176,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} + spawn_aliases = {direction: ("down", "d") for direction in direction_aliases} prototype = "xyz_exit" diff --git a/evennia/contrib/xyzgrid/xymap.py b/evennia/contrib/xyzgrid/xymap.py index c7c808c2ed..fcea6dc0d9 100644 --- a/evennia/contrib/xyzgrid/xymap.py +++ b/evennia/contrib/xyzgrid/xymap.py @@ -772,7 +772,8 @@ class XYMap: def get_visual_range(self, xy, dist=2, mode='nodes', character='@', - target=None, target_path_style="|y{display_symbol}|n", + target=None, + target_path_style="|y{display_symbol}|n", max_size=None, indent=0, return_str=True): @@ -797,7 +798,7 @@ class XYMap: (or the beginning of said path, if outside of visual range) will be marked according to `target_path_style`. target_path_style (str or callable, optional): This is use for marking the path - found when `path_to_coord` is given. If a string, it accepts a formatting marker + found when `target` is given. If a string, it accepts a formatting marker `display_symbol` which will be filled with the `display_symbol` of each node/link the path passes through. This allows e.g. to color the path. If a callable, this will receive the MapNode or MapLink object for every step of the path and and diff --git a/evennia/contrib/xyzgrid/xyzroom.py b/evennia/contrib/xyzgrid/xyzroom.py index 1274a7abda..7b0614bc93 100644 --- a/evennia/contrib/xyzgrid/xyzroom.py +++ b/evennia/contrib/xyzgrid/xyzroom.py @@ -8,9 +8,9 @@ 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 +from evennia.utils.utils import make_iter # name of all tag categories. Note that the Z-coordinate is # the `map_name` of the XYZgrid @@ -328,6 +328,25 @@ class XYZRoom(DefaultRoom): return DefaultRoom.create(key, account=account, tags=tags, typeclass=cls, **kwargs) + def get_display_name(self, looker, **kwargs): + """ + Shows both the #dbref and the xyz coord to staff. + + Args: + looker (TypedObject): The object or account that is looking + at/getting inforamtion for this object. + + Returns: + name (str): A string containing the name of the object, + including the DBREF and XYZ coord if this user is + privileged to control the room. + + """ + if self.locks.check_lockstring(looker, "perm(Builder)"): + x, y, z = self.xyz + return f"{self.name}[#{self.id}({x},{y},{z})]" + return self.name + def return_appearance(self, looker, **kwargs): """ Displays the map in addition to the room description @@ -411,12 +430,16 @@ class XYZRoom(DefaultRoom): elif map_align == 'c': map_indent = max(0, (display_width - map_width) // 2) + goto_target, *current_path = make_iter(looker.ndb.xy_current_goto) + # 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, + target=goto_target, + target_path_style="|y{display_symbol}|n", + character=f"|g{character_symbol}|n", max_size=(display_width, None), indent=map_indent ) diff --git a/evennia/typeclasses/attributes.py b/evennia/typeclasses/attributes.py index f1bec510bd..d31e21e12d 100644 --- a/evennia/typeclasses/attributes.py +++ b/evennia/typeclasses/attributes.py @@ -1260,7 +1260,7 @@ class DbHolder: _GA(self, _GA(self, "name")).remove(attrname) def get_all(self): - return _GA(self, _GA(self, "name")).get_all_attributes() + return _GA(self, _GA(self, "name")).backend.get_all_attributes() all = property(get_all)