Fixes to goto functionality. Working well now.
This commit is contained in:
parent
5edda10e81
commit
0686414d0f
4 changed files with 246 additions and 42 deletions
|
|
@ -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())
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue