mirror of
https://github.com/evennia/evennia.git
synced 2026-03-24 08:46:31 +01:00
Fixes to goto functionality. Working well now.
This commit is contained in:
parent
8f228327c1
commit
417c52e871
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 evennia import InterruptCommand
|
||||
from evennia import default_cmds, CmdSet
|
||||
from evennia.commands.default import building
|
||||
from evennia.contrib.xyzgrid.xyzroom import XYZRoom
|
||||
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)
|
||||
|
||||
|
||||
# temporary store of goto/path data when using the auto-stepper
|
||||
PathData = namedtuple("PathData", ("target", "xymap", "directions", "step_sequence", "task"))
|
||||
|
||||
|
||||
class CmdXYZTeleport(building.CmdTeleport):
|
||||
"""
|
||||
teleport object to another location
|
||||
|
|
@ -163,17 +169,19 @@ class CmdXYZOpen(building.CmdOpen):
|
|||
|
||||
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:
|
||||
goto <location> - get path and start walking
|
||||
path <location> - just check the path
|
||||
goto - abort current goto
|
||||
path - show current path
|
||||
path <location> - find shortest path to target location (don't move)
|
||||
goto <location> - auto-move to target location, using shortest path
|
||||
path - show current target location and shortest 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
|
||||
start automatically walk you there. Builders can also specify a specific grid
|
||||
coordinate (X,Y).
|
||||
Finds the shortest route to a location in your current area and
|
||||
can then automatically walk you there.
|
||||
|
||||
Builders can optionally specify a specific grid coordinate (X,Y) to go to.
|
||||
|
||||
"""
|
||||
key = "goto"
|
||||
|
|
@ -181,6 +189,9 @@ class CmdGoto(COMMAND_DEFAULT_CLASS):
|
|||
help_category = "General"
|
||||
locks = "cmd:all()"
|
||||
|
||||
# how quickly to step (seconds)
|
||||
auto_step_delay = 2
|
||||
|
||||
def _search_by_xyz(self, inp, xyz_start):
|
||||
inp = inp.strip("()")
|
||||
X, Y = inp.split(",", 2)
|
||||
|
|
@ -198,27 +209,152 @@ class CmdGoto(COMMAND_DEFAULT_CLASS):
|
|||
candidates = list(XYZRoom.objects.filter_xyz(xyz=('*', '*', Z)))
|
||||
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):
|
||||
"""
|
||||
Implement command
|
||||
"""
|
||||
|
||||
caller = self.caller
|
||||
|
||||
current_target, *current_path = make_iter(caller.ndb.xy_current_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 current_target:
|
||||
if path_data:
|
||||
target_name = path_data.target.get_display_name(caller)
|
||||
task = path_data.task
|
||||
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)}")
|
||||
if task and task.active():
|
||||
task.cancel()
|
||||
caller.msg(f"Aborted auto-walking to {target_name}.")
|
||||
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:
|
||||
caller.msg("Usage: goto <location>")
|
||||
caller.msg("Usage: goto|path [<location>]")
|
||||
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()
|
||||
try:
|
||||
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
|
||||
xy_start = xyz_start[: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)}: "
|
||||
f"|w{list_to_string(shortest_path, endsep='|nand finally|w')}|n")
|
||||
caller.msg(f"There are {len(directions)} steps to {target.get_display_name(caller)}: "
|
||||
f"|w{list_to_string(directions, endsep='|n, and finally|w')}|n")
|
||||
|
||||
# store for use by the return_appearance hook on the XYZRoom
|
||||
caller.ndb.xy_current_goto = (xy_end, shortest_path)
|
||||
# create data for display and start stepping if we used goto
|
||||
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
|
||||
self.msg("Walking ... eventually")
|
||||
pass
|
||||
|
||||
class CmdMap(COMMAND_DEFAULT_CLASS):
|
||||
"""
|
||||
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):
|
||||
|
|
@ -272,3 +451,4 @@ class XYZGridCmdSet(CmdSet):
|
|||
self.add(CmdXYZTeleport())
|
||||
self.add(CmdXYZOpen())
|
||||
self.add(CmdGoto())
|
||||
self.add(CmdMap())
|
||||
|
|
|
|||
|
|
@ -258,6 +258,29 @@ class MapNode:
|
|||
"""
|
||||
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):
|
||||
"""
|
||||
Build an actual in-game room from this node.
|
||||
|
|
@ -327,11 +350,7 @@ class MapNode:
|
|||
maplinks = {}
|
||||
for direction, link in self.first_links.items():
|
||||
|
||||
key, *aliases = (
|
||||
link.spawn_aliases.get(direction, ('unknown',))
|
||||
if link.spawn_aliases
|
||||
else self.direction_spawn_defaults.get(direction, ('unknown',))
|
||||
)
|
||||
key, *aliases = self.get_exit_spawn_name(direction)
|
||||
if not link.prototype.get('prototype_key'):
|
||||
# generate a deterministic prototype_key if it doesn't exist
|
||||
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
|
||||
of the link is only used to determine its destination). This can be overridden on a
|
||||
per-direction basis.
|
||||
- `spawn_aliases` (list): A list of [key, alias, alias, ...] for the node to use when spawning
|
||||
exits from this link. If not given, a sane set of defaults ((north, n) etc) will be used. This
|
||||
is required if you use any custom directions outside of the cardinal directions + up/down.
|
||||
- `spawn_aliases` (dict): A mapping {direction: (key, alias, alias, ...) to use when spawning
|
||||
actual exits from this link. If not given, a sane set of defaults (n=(north, n) etc) will be
|
||||
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
|
||||
|
|
@ -535,10 +556,11 @@ class MapLink:
|
|||
interrupt_path = False
|
||||
# prototype for the first link out of a node.
|
||||
prototype = None
|
||||
# used for spawning, if the exit prototype doesn't contain an explicit key.
|
||||
# if neither that nor this is not given, the central node's direction_aliases will be used.
|
||||
# the first element of this list is the key, the others are the aliases.
|
||||
spawn_aliases = []
|
||||
# used for spawning and maps {direction: (key, alias, alias, ...) for use for the exits spawned
|
||||
# in this direction. Used unless the exit's prototype contain an explicit key - then that will
|
||||
# take precedence. If neither that nor this is not given, sane defaults ('n'=('north','n'), etc)
|
||||
# will be used.
|
||||
spawn_aliases = {}
|
||||
|
||||
def __init__(self, x, y, Z, xymap=None):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -717,7 +717,7 @@ class XYMap:
|
|||
|
||||
Returns:
|
||||
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.
|
||||
|
||||
"""
|
||||
|
|
@ -909,7 +909,7 @@ class XYMap:
|
|||
for node_or_link in path[1:]:
|
||||
if hasattr(node_or_link, "node_index"):
|
||||
nsteps += 1
|
||||
if nsteps >= maxstep:
|
||||
if nsteps > maxstep:
|
||||
break
|
||||
# don't decorate current (character?) location
|
||||
ix, iy = node_or_link.x, node_or_link.y
|
||||
|
|
|
|||
|
|
@ -430,14 +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)
|
||||
# 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
|
||||
map_display = xymap.get_visual_range(
|
||||
(xyz[0], xyz[1]),
|
||||
dist=visual_range,
|
||||
mode=map_mode,
|
||||
target=goto_target,
|
||||
target=target_xy,
|
||||
target_path_style="|y{display_symbol}|n",
|
||||
character=f"|g{character_symbol}|n",
|
||||
max_size=(display_width, None),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue