Fixes to goto functionality. Working well now.

This commit is contained in:
Griatch 2021-07-13 23:31:24 +02:00
parent 5edda10e81
commit 0686414d0f
4 changed files with 246 additions and 42 deletions

View file

@ -7,17 +7,23 @@ the commands with XYZ-aware equivalents.
""" """
from collections import namedtuple
from django.conf import settings from django.conf import settings
from evennia import InterruptCommand from evennia import InterruptCommand
from evennia import default_cmds, CmdSet from evennia import default_cmds, CmdSet
from evennia.commands.default import building from evennia.commands.default import building
from evennia.contrib.xyzgrid.xyzroom import XYZRoom from evennia.contrib.xyzgrid.xyzroom import XYZRoom
from evennia.contrib.xyzgrid.xyzgrid import get_xyzgrid from evennia.contrib.xyzgrid.xyzgrid import get_xyzgrid
from evennia.utils.utils import list_to_string, class_from_module, make_iter from evennia.utils import ansi
from evennia.utils.utils import list_to_string, class_from_module, delay
COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS) COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
# temporary store of goto/path data when using the auto-stepper
PathData = namedtuple("PathData", ("target", "xymap", "directions", "step_sequence", "task"))
class CmdXYZTeleport(building.CmdTeleport): class CmdXYZTeleport(building.CmdTeleport):
""" """
teleport object to another location teleport object to another location
@ -163,17 +169,19 @@ class CmdXYZOpen(building.CmdOpen):
class CmdGoto(COMMAND_DEFAULT_CLASS): class CmdGoto(COMMAND_DEFAULT_CLASS):
""" """
Go to a named location in this area. Go to a named location in this area via the shortest path.
Usage: Usage:
goto <location> - get path and start walking path <location> - find shortest path to target location (don't move)
path <location> - just check the path goto <location> - auto-move to target location, using shortest path
goto - abort current goto path - show current target location and shortest path
path - show current path goto - abort current goto, otherwise show current path
path clear - clear current path
This will find the shortest route to a location in your current area and Finds the shortest route to a location in your current area and
start automatically walk you there. Builders can also specify a specific grid can then automatically walk you there.
coordinate (X,Y).
Builders can optionally specify a specific grid coordinate (X,Y) to go to.
""" """
key = "goto" key = "goto"
@ -181,6 +189,9 @@ class CmdGoto(COMMAND_DEFAULT_CLASS):
help_category = "General" help_category = "General"
locks = "cmd:all()" locks = "cmd:all()"
# how quickly to step (seconds)
auto_step_delay = 2
def _search_by_xyz(self, inp, xyz_start): def _search_by_xyz(self, inp, xyz_start):
inp = inp.strip("()") inp = inp.strip("()")
X, Y = inp.split(",", 2) X, Y = inp.split(",", 2)
@ -198,27 +209,152 @@ class CmdGoto(COMMAND_DEFAULT_CLASS):
candidates = list(XYZRoom.objects.filter_xyz(xyz=('*', '*', Z))) candidates = list(XYZRoom.objects.filter_xyz(xyz=('*', '*', Z)))
return self.caller.search(inp, candidates=candidates) return self.caller.search(inp, candidates=candidates)
def _auto_step(self, caller, session, target=None,
xymap=None, directions=None, step_sequence=None, step=True):
path_data = caller.ndb.xy_path_data
if target:
# start/replace an old path if we provide the data for it
if path_data and path_data.task and path_data.task.active():
# stop any old task in its tracks
path_data.task.cancel()
path_data = caller.ndb.xy_path_data = PathData(
target=target, xymap=xymap, directions=directions,
step_sequence=step_sequence, task=None)
if step and path_data:
step_sequence = path_data.step_sequence
try:
direction = path_data.directions.pop(0)
current_node = path_data.step_sequence.pop(0)
first_link = path_data.step_sequence.pop(0)
except IndexError:
caller.msg("Target reached.", session=session)
caller.ndb.xy_path_data = None
return
# verfy our current location against the expected location
expected_xyz = (current_node.X, current_node.Y, current_node.Z)
location = caller.location
try:
xyz_start = location.xyz
except AttributeError:
caller.ndb.xy_path_data = None
caller.msg("Goto aborted - outside of area.", session=session)
return
if xyz_start != expected_xyz:
# we are not where we expected to be (maybe the user moved
# manually) - we must recalculate the path to target
caller.msg("Path changed - recalculating ('goto' to abort)", session=session)
try:
xyz_end = path_data.target.xyz
except AttributeError:
caller.ndb.xy_path_data = None
caller.msg("Goto aborted - target outside of area.", session=session)
return
if xyz_start[2] != xyz_end[2]:
# can't go to another map
caller.ndb.xy_path_data = None
caller.msg("Goto aborted - target outside of area.", session=session)
return
# recalculate path
xy_start = xyz_start[:2]
xy_end = xyz_end[:2]
directions, step_sequence = path_data.xymap.get_shortest_path(xy_start, xy_end)
# try again with this path, rebuilding the data
try:
direction = directions.pop(0)
current_node = step_sequence.pop(0)
first_link = step_sequence.pop(0)
except IndexError:
caller.msg("Target reached.", session=session)
caller.ndb.xy_path_data = None
return
path_data = caller.ndb.xy_path_data = PathData(
target=path_data.target,
xymap=path_data.xymap,
directions=directions,
step_sequence=step_sequence,
task=None
)
# pop any extra links up until the next node - these are
# not useful when dealing with exits
while step_sequence:
if hasattr(step_sequence[0], "node_index"):
break
step_sequence.pop(0)
# the exit name does not need to be the same as the cardinal direction!
exit_name, *_ = first_link.spawn_aliases.get(
direction, current_node.direction_spawn_defaults.get(direction, ('unknown', )))
if not caller.search(exit_name):
# extra safety measure to avoid trying to walk over and over
# if there's something wrong with the exit's name
caller.msg(f"No exit '{exit_name}' found at current location. Aborting goto.")
caller.ndb.xy_path_data = None
return
# do the actual move - we use the command to allow for more obvious overrides
caller.execute_cmd(exit_name, session=session)
# namedtuples are unmutables, so we recreate and store
# with the new task
caller.ndb.xy_path_data = PathData(
target=path_data.target,
xymap=path_data.xymap,
directions=path_data.directions,
step_sequence=path_data.step_sequence,
task=delay(self.auto_step_delay, self._auto_step, caller, session)
)
def func(self): def func(self):
""" """
Implement command Implement command
""" """
caller = self.caller caller = self.caller
current_target, *current_path = make_iter(caller.ndb.xy_current_goto)
goto_mode = self.cmdname == 'goto' goto_mode = self.cmdname == 'goto'
# check if we have an existing path
path_data = caller.ndb.xy_path_data
if not self.args: if not self.args:
if current_target: if path_data:
target_name = path_data.target.get_display_name(caller)
task = path_data.task
if goto_mode: if goto_mode:
caller.ndb.xy_current_goto_target = None if task and task.active():
caller.msg("Aborted goto.") task.cancel()
else: caller.msg(f"Aborted auto-walking to {target_name}.")
caller.msg(f"Remaining steps: {list_to_string(current_path)}") return
# goto/path-command will show current path
current_path = list_to_string(
[f"|w{step}|n" for step in path_data.directions])
moving = "(moving)" if task and task.active() else ""
caller.msg(f"Path to {target_name}{moving}: {current_path}")
else: else:
caller.msg("Usage: goto <location>") caller.msg("Usage: goto|path [<location>]")
return return
if not goto_mode and self.args == "clear" and path_data:
# in case there is a target location 'clear', this is only
# used if path data already exists.
caller.ndb.xy_path_data = None
caller.msg("Cleared goto-path.")
return
# find target
xyzgrid = get_xyzgrid() xyzgrid = get_xyzgrid()
try: try:
xyz_start = caller.location.xyz xyz_start = caller.location.xyz
@ -247,18 +383,61 @@ class CmdGoto(COMMAND_DEFAULT_CLASS):
# we only need the xy coords once we have the map # we only need the xy coords once we have the map
xy_start = xyz_start[:2] xy_start = xyz_start[:2]
xy_end = xyz_end[:2] xy_end = xyz_end[:2]
shortest_path, _ = xymap.get_shortest_path(xy_start, xy_end) directions, step_sequence = xymap.get_shortest_path(xy_start, xy_end)
caller.msg(f"There are {len(shortest_path)} steps to {target.get_display_name(caller)}: " caller.msg(f"There are {len(directions)} steps to {target.get_display_name(caller)}: "
f"|w{list_to_string(shortest_path, endsep='|nand finally|w')}|n") f"|w{list_to_string(directions, endsep='|n, and finally|w')}|n")
# store for use by the return_appearance hook on the XYZRoom # create data for display and start stepping if we used goto
caller.ndb.xy_current_goto = (xy_end, shortest_path) self._auto_step(caller, self.session, target=target, xymap=xymap,
directions=directions, step_sequence=step_sequence, step=goto_mode)
if self.cmdname == "goto":
# start actually walking right away class CmdMap(COMMAND_DEFAULT_CLASS):
self.msg("Walking ... eventually") """
pass Show a map of an area
Usage:
map [Zcoord]
map list
This is a builder-command.
"""
key = "map"
locks = "cmd:perm(Builders)"
def func(self):
"""Implement command"""
xyzgrid = get_xyzgrid()
Z = None
if not self.args:
# show current area's map
location = self.caller.location
try:
xyz = location.xyz
except AttributeError:
self.caller.msg("Your current location is not on the grid.")
return
Z = xyz[2]
elif self.args.strip().lower() == "list":
xymaps = "\n ".join(str(repr(xymap)) for xymap in xyzgrid.all_maps())
self.caller.msg(f"Maps (Z coords) on the grid:\n |w{xymaps}")
return
else:
Z = self.args
xymap = xyzgrid.get_map(Z)
if not xymap:
self.caller.msg(f"XYMap '{Z}' is not found on the grid. Try 'map list' to see "
"available maps/Zcoords.")
return
self.caller.msg(ansi.raw(xymap.mapstring))
class XYZGridCmdSet(CmdSet): class XYZGridCmdSet(CmdSet):
@ -272,3 +451,4 @@ class XYZGridCmdSet(CmdSet):
self.add(CmdXYZTeleport()) self.add(CmdXYZTeleport())
self.add(CmdXYZOpen()) self.add(CmdXYZOpen())
self.add(CmdGoto()) self.add(CmdGoto())
self.add(CmdMap())

View file

@ -258,6 +258,29 @@ class MapNode:
""" """
return self.X, self.Y, self.Z return self.X, self.Y, self.Z
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
def spawn(self): def spawn(self):
""" """
Build an actual in-game room from this node. Build an actual in-game room from this node.
@ -327,11 +350,7 @@ class MapNode:
maplinks = {} maplinks = {}
for direction, link in self.first_links.items(): for direction, link in self.first_links.items():
key, *aliases = ( key, *aliases = self.get_exit_spawn_name(direction)
link.spawn_aliases.get(direction, ('unknown',))
if link.spawn_aliases
else self.direction_spawn_defaults.get(direction, ('unknown',))
)
if not link.prototype.get('prototype_key'): if not link.prototype.get('prototype_key'):
# generate a deterministic prototype_key if it doesn't exist # generate a deterministic prototype_key if it doesn't exist
link.prototype['prototype_key'] = self.generate_prototype_key() link.prototype['prototype_key'] = self.generate_prototype_key()
@ -495,9 +514,11 @@ class MapLink:
on the game grid. This is only relevant for the *first* link out of a Node (the continuation on the game grid. This is only relevant for the *first* link out of a Node (the continuation
of the link is only used to determine its destination). This can be overridden on a of the link is only used to determine its destination). This can be overridden on a
per-direction basis. per-direction basis.
- `spawn_aliases` (list): A list of [key, alias, alias, ...] for the node to use when spawning - `spawn_aliases` (dict): A mapping {direction: (key, alias, alias, ...) to use when spawning
exits from this link. If not given, a sane set of defaults ((north, n) etc) will be used. This actual exits from this link. If not given, a sane set of defaults (n=(north, n) etc) will be
is required if you use any custom directions outside of the cardinal directions + up/down. used. This is required if you use any custom directions outside of the cardinal directions +
up/down. The exit's key (useful for auto-walk) is usually retrieved by calling
`node.get_exit_spawn_name(direction)`
""" """
# symbol for identifying this link on the map # symbol for identifying this link on the map
@ -535,10 +556,11 @@ class MapLink:
interrupt_path = False interrupt_path = False
# prototype for the first link out of a node. # prototype for the first link out of a node.
prototype = None prototype = None
# used for spawning, if the exit prototype doesn't contain an explicit key. # used for spawning and maps {direction: (key, alias, alias, ...) for use for the exits spawned
# if neither that nor this is not given, the central node's direction_aliases will be used. # in this direction. Used unless the exit's prototype contain an explicit key - then that will
# the first element of this list is the key, the others are the aliases. # take precedence. If neither that nor this is not given, sane defaults ('n'=('north','n'), etc)
spawn_aliases = [] # will be used.
spawn_aliases = {}
def __init__(self, x, y, Z, xymap=None): def __init__(self, x, y, Z, xymap=None):
""" """

View file

@ -717,7 +717,7 @@ class XYMap:
Returns: Returns:
tuple: Two lists, first containing the list of directions as strings (n, ne etc) and tuple: Two lists, first containing the list of directions as strings (n, ne etc) and
the second is a mixed list of MapNodes and string-directions in a sequence describing the second is a mixed list of MapNodes and all MapLinks in a sequence describing
the full path including the start- and end-node. the full path including the start- and end-node.
""" """
@ -909,7 +909,7 @@ class XYMap:
for node_or_link in path[1:]: for node_or_link in path[1:]:
if hasattr(node_or_link, "node_index"): if hasattr(node_or_link, "node_index"):
nsteps += 1 nsteps += 1
if nsteps >= maxstep: if nsteps > maxstep:
break break
# don't decorate current (character?) location # don't decorate current (character?) location
ix, iy = node_or_link.x, node_or_link.y ix, iy = node_or_link.x, node_or_link.y

View file

@ -430,14 +430,16 @@ class XYZRoom(DefaultRoom):
elif map_align == 'c': elif map_align == 'c':
map_indent = max(0, (display_width - map_width) // 2) map_indent = max(0, (display_width - map_width) // 2)
goto_target, *current_path = make_iter(looker.ndb.xy_current_goto) # data set by the goto/path-command, for displaying the shortest path
path_data = looker.ndb.xy_path_data
target_xy = path_data.target.xyz[:2] if path_data else None
# get visual range display from map # get visual range display from map
map_display = xymap.get_visual_range( map_display = xymap.get_visual_range(
(xyz[0], xyz[1]), (xyz[0], xyz[1]),
dist=visual_range, dist=visual_range,
mode=map_mode, mode=map_mode,
target=goto_target, target=target_xy,
target_path_style="|y{display_symbol}|n", target_path_style="|y{display_symbol}|n",
character=f"|g{character_symbol}|n", character=f"|g{character_symbol}|n",
max_size=(display_width, None), max_size=(display_width, None),