mirror of
https://github.com/evennia/evennia.git
synced 2026-03-23 08:16:30 +01:00
Simplified map-transition logic using a transition-node rather than a -link
This commit is contained in:
parent
aaa67218d6
commit
ae2f856200
4 changed files with 91 additions and 137 deletions
|
|
@ -49,6 +49,11 @@ class MapNode:
|
|||
(such as a guard or locked gate etc).
|
||||
- `prototype` (dict) - The default `prototype` dict to use for reproducing this map component
|
||||
on the game grid. This is used if not overridden specifically for this coordinate.
|
||||
- `deferred` (bool): A deferred node is used to indicate a link (currently) pointing to nowhere
|
||||
because the end node is not yet available - usually because that node is on another map
|
||||
and won't be available until the full grid has loaded. A deferred node doesn't need a symbol
|
||||
but is returned from links. Links pointing to deferred nodes will be re-parsed once the entire
|
||||
grid has been built, in order to correctly link maps together.
|
||||
|
||||
"""
|
||||
# symbol used to identify this link on the map
|
||||
|
|
@ -68,6 +73,7 @@ class MapNode:
|
|||
# the prototype to use for mapping this to the grid.
|
||||
prototype = None
|
||||
|
||||
|
||||
def __init__(self, x, y, node_index=0, xymap=None):
|
||||
"""
|
||||
Initialize the mapnode.
|
||||
|
|
@ -118,7 +124,7 @@ class MapNode:
|
|||
def __repr__(self):
|
||||
return str(self)
|
||||
|
||||
def scan_all_directions(self, xygrid):
|
||||
def build_links(self, xygrid):
|
||||
"""
|
||||
This is called by the map parser when this node is encountered. It tells the node
|
||||
to scan in all directions and follow any found links to other nodes. Since there
|
||||
|
|
@ -286,6 +292,42 @@ class MapNode:
|
|||
maplinks[direction].prototype, objects=[linkobj], exact=False)
|
||||
|
||||
|
||||
class TransitionMapNode(MapNode):
|
||||
"""
|
||||
Entering this node teleports the user to another Map (this is completely handled by the
|
||||
prototyped Room class). This teleportation is not understood by the pathfinder, so why it will
|
||||
be possible to pathfind to this node, it really represents a map transition. Only a single link
|
||||
must ever be connected to this node.
|
||||
|
||||
Properties:
|
||||
- `linked_map_name` (str) - the map you will move to when entering this node.
|
||||
- `linked_coords` (tuple) - the XY coordinates *on the linked* map this node
|
||||
will teleport to. This must be another node that is not a TransitionMapNode.
|
||||
Note that for the trip to be two-way, a similar set up must be created from the
|
||||
other map.
|
||||
|
||||
Examples:
|
||||
::
|
||||
|
||||
map1 map2
|
||||
|
||||
#-T #- - one-way transition from map1 -> map2.
|
||||
#-T T-# - two-way. Both ExternalMapNodes links to the coords of the
|
||||
`#` (NOT the `T`) on the other map!
|
||||
|
||||
"""
|
||||
symbol = 'T'
|
||||
display_symbol = ' '
|
||||
linked_map_name = ""
|
||||
linked_map_coords = None
|
||||
|
||||
def build_links(self, xygrid):
|
||||
"""Check so we don't have too many links"""
|
||||
super().build_links(xygrid)
|
||||
if len(self.links) > 1:
|
||||
raise MapParserError("may have at most one link connecting to it.", self)
|
||||
|
||||
|
||||
class MapLink:
|
||||
"""
|
||||
This represents one or more links between an 'incoming direction'
|
||||
|
|
@ -338,6 +380,10 @@ 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.
|
||||
- `requires_grid` (bool): If set, it indicates this component requires the full grid (multiple
|
||||
maps to be available before it can be processed. This is usually only needed for
|
||||
inter-map traversal links where the other map must already be ready. Note that this is
|
||||
*only* relevant for the *first* link out of a node.
|
||||
|
||||
"""
|
||||
# symbol for identifying this link on the map
|
||||
|
|
@ -375,8 +421,7 @@ class MapLink:
|
|||
interrupt_path = False
|
||||
# prototype for the first link out of a node.
|
||||
prototype = None
|
||||
# only traverse this after all of the grid is complete
|
||||
delay_traversal = False
|
||||
|
||||
|
||||
def __init__(self, x, y, xymap=None):
|
||||
"""
|
||||
|
|
@ -447,10 +492,11 @@ class MapLink:
|
|||
raise MapParserError(
|
||||
f"points to empty space in the direction {end_direction}!", self)
|
||||
|
||||
if next_target.xymap.name != self.xymap.name:
|
||||
# this target is on another map. Immediately exit the traversal
|
||||
# and set a high weight.
|
||||
return (next_target, BIGVAL, [start_direction])
|
||||
if ((hasattr(next_target, "deferred") and next_target.deferred)
|
||||
or (next_target.xymap.name != self.xymap.name)):
|
||||
# this target is either deferred until grid exists, or sits on another map. Immediately
|
||||
# exit the traversal and set a high weight.
|
||||
return (next_target, BIGVAL, [self])
|
||||
|
||||
_weight += self.get_weight(start_direction, xygrid, _weight)
|
||||
if _steps is None:
|
||||
|
|
@ -577,6 +623,7 @@ class MapLink:
|
|||
"""
|
||||
return self.symbol if self.display_symbol is None else self.display_symbol
|
||||
|
||||
|
||||
class SmartRerouterMapLink(MapLink):
|
||||
r"""
|
||||
A 'smart' link without visible direction, but which uses its topological surroundings
|
||||
|
|
@ -753,86 +800,6 @@ class TeleporterMapLink(MapLink):
|
|||
return self.directions.get(start_direction)
|
||||
|
||||
|
||||
class MapTransitionLink(TeleporterMapLink):
|
||||
"""
|
||||
This link teleports the user to another map and lets them continue moving
|
||||
from there. Like the TeleporterMapLink, the map-transition symbol must connect to only one other
|
||||
link (not directly to a node).
|
||||
|
||||
The other map will be scanned for a matching `.symbol` that must also be a MapTransitionLink.
|
||||
The link is always two-way, but the link connecting to the transition can be one-way to create
|
||||
a one-way transition. Make new links with different symbols (like A, B, C, ...) to link
|
||||
multiple maps together.
|
||||
|
||||
Note that unlike for teleports, pathfinding will *not* work across the map-transition.
|
||||
|
||||
Examples:
|
||||
::
|
||||
|
||||
map1 map2
|
||||
|
||||
T
|
||||
/ T-# - movement to the transition-link will continue on the other map.
|
||||
-#
|
||||
|
||||
T
|
||||
/
|
||||
-# T># - one-way link from map1 to map2
|
||||
|
||||
-#t - invalid, may only connect to another link
|
||||
|
||||
-#-t-# - invalid, only one connected link is allowed.
|
||||
|
||||
"""
|
||||
symbol = 'T'
|
||||
display_symbol = ' '
|
||||
direction_name = 'transition'
|
||||
interrupt_path = True
|
||||
|
||||
target_map = 'map2'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.paired_map_link = None
|
||||
|
||||
def at_empty_target(self, start_direction, end_direction, xygrid):
|
||||
"""
|
||||
This is called by .traverse when it finds this link pointing to nowhere.
|
||||
|
||||
Args:
|
||||
start_direction (str): The direction (n, ne etc) from which
|
||||
this traversal originates for this link.
|
||||
end_direction (str): The direction found from `get_direction` earlier.
|
||||
xygrid (dict): 2D dict with x,y coordinates as keys.
|
||||
|
||||
"""
|
||||
if not self.paired_map_link:
|
||||
try:
|
||||
grid = self.xymap.grid.grid
|
||||
except AttributeError:
|
||||
raise MapParserError(f"requires this map being set up within an XYZgrid. No grid "
|
||||
"was found (maybe it was not passed during XYMap initialization?",
|
||||
self)
|
||||
try:
|
||||
target_map = grid[self.target_map]
|
||||
except KeyError:
|
||||
raise MapParserError(f"cannot find target_map '{self.target_map}' "
|
||||
f"on the grid.", self)
|
||||
|
||||
# find the matching link on the other side
|
||||
link = target_map.get_components_with_symbol(self.symbol)
|
||||
if not link:
|
||||
raise MapParserError(f"must have a matching '{self.symbol}' on "
|
||||
f"its target_map `{self.target_map}`.", self)
|
||||
if len(link) > 1:
|
||||
raise MapParserError(f"must have a singl mathing '{self.symbol}' on "
|
||||
f"its target_map (found {len(link)}): {link}")
|
||||
# this is a link on another map
|
||||
self.paired_map_link = link[0]
|
||||
|
||||
return self.paired_map_link
|
||||
|
||||
|
||||
class SmartMapLink(MapLink):
|
||||
"""
|
||||
A 'smart' link withot visible direction, but which uses its topological surroundings
|
||||
|
|
@ -985,6 +952,15 @@ class BasicMapNode(MapNode):
|
|||
symbol = "#"
|
||||
|
||||
|
||||
class MapTransitionMapNode(TransitionMapNode):
|
||||
"""Teleports entering players to other map"""
|
||||
symbol = "T"
|
||||
display_symbol = " "
|
||||
interrupt_path = True
|
||||
linked_map_name = ""
|
||||
linked_map_coords = None
|
||||
|
||||
|
||||
class InterruptMapNode(MapNode):
|
||||
"""A point of interest, where pathfinder will stop"""
|
||||
symbol = "I"
|
||||
|
|
|
|||
|
|
@ -1027,14 +1027,16 @@ class TestMapStressTest(TestCase):
|
|||
|
||||
|
||||
# map transitions
|
||||
class Map12aTransition(map_legend.MapTransitionLink):
|
||||
class Map12aTransition(map_legend.MapTransitionMapNode):
|
||||
symbol = "T"
|
||||
target_map = "map12b"
|
||||
linked_map_name = "map12b"
|
||||
linked_map_coords = (1, 0)
|
||||
|
||||
|
||||
class Map12bTransition(map_legend.MapTransitionLink):
|
||||
class Map12bTransition(map_legend.MapTransitionMapNode):
|
||||
symbol = "T"
|
||||
target_map = "map12a"
|
||||
linked_map_name= "map12a"
|
||||
linked_map_coords = (0, 1)
|
||||
|
||||
|
||||
class TestXYZGrid(TestCase):
|
||||
|
|
@ -1063,8 +1065,8 @@ class TestXYZGrid(TestCase):
|
|||
self.grid.delete()
|
||||
|
||||
@parameterized.expand([
|
||||
((1, 0), (1, 1), ('e', 'nw', 'e')),
|
||||
((1, 1), (0, 0), ('w', 'se', 'w')),
|
||||
((1, 0), (1, 1), ('w', 'n', 'e')),
|
||||
((1, 1), (1, 0), ('w', 's', 'e')),
|
||||
])
|
||||
def test_shortest_path(self, startcoord, endcoord, expected_directions):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -114,6 +114,7 @@ _CACHE_DIR = settings.CACHE_DIR
|
|||
# these are all symbols used for x,y coordinate spots
|
||||
DEFAULT_LEGEND = {
|
||||
"#": map_legend.BasicMapNode,
|
||||
"T": map_legend.MapTransitionMapNode,
|
||||
"I": map_legend.InterruptMapNode,
|
||||
"|": map_legend.NSMapLink,
|
||||
"-": map_legend.EWMapLink,
|
||||
|
|
@ -131,7 +132,6 @@ DEFAULT_LEGEND = {
|
|||
"b": map_legend.BlockedMapLink,
|
||||
"i": map_legend.InterruptMapLink,
|
||||
't': map_legend.TeleporterMapLink,
|
||||
'T': map_legend.MapTransitionLink,
|
||||
}
|
||||
|
||||
# --------------------------------------------
|
||||
|
|
@ -196,8 +196,7 @@ class XYMap:
|
|||
more than one map. Used when referencing this map during map transitions,
|
||||
baking of pathfinding matrices etc. This will be overridden by any 'name' given
|
||||
in the MAP_DATA itself.
|
||||
grid (xyzgrid.XYZGrid, optional): Reference to the top-level grid object, which
|
||||
stores all maps. This is necessary for transitioning from map to another.
|
||||
grid (.xyzgrid.XYZgrid): A top-level grid this map is a part of.
|
||||
|
||||
Notes:
|
||||
The map deals with two sets of coorinate systems:
|
||||
|
|
@ -219,6 +218,7 @@ class XYMap:
|
|||
|
||||
self.grid = grid
|
||||
self.prototypes = None
|
||||
|
||||
# transitional mapping
|
||||
self.symbol_map = None
|
||||
|
||||
|
|
@ -257,7 +257,7 @@ class XYMap:
|
|||
def __repr__(self):
|
||||
return f"<Map {self.max_X + 1}x{self.max_Y + 1}, {len(self.node_index_map)} nodes>"
|
||||
|
||||
def parse_first_pass(self):
|
||||
def parse(self):
|
||||
"""
|
||||
Parses the numerical grid from the string. The first pass means parsing out
|
||||
all nodes. The linking-together of nodes is not happening until the second pass
|
||||
|
|
@ -368,33 +368,11 @@ class XYMap:
|
|||
# store the symbol mapping for transition lookups
|
||||
symbol_map[char].append(xygrid[ix][iy])
|
||||
|
||||
# store results
|
||||
self.max_x, self.max_y = max_x, max_y
|
||||
self.xygrid = xygrid
|
||||
# second pass - link all nodes of the map except the inter-map traversals.
|
||||
|
||||
self.max_X, self.max_Y = max_X, max_Y
|
||||
self.XYgrid = XYgrid
|
||||
|
||||
self.node_index_map = node_index_map
|
||||
self.symbol_map = symbol_map
|
||||
|
||||
def parse_second_pass(self):
|
||||
"""
|
||||
Parsing, second pass. Here we loop over all nodes and have them connect to each other via
|
||||
the detected linkages. For multi-map grids (that links to one another), this must run after
|
||||
all maps have run through the first pass of their parsing.
|
||||
|
||||
This will create the linkages, build the display map for visualization and validate
|
||||
all prototypes for the nodes and their connected links.
|
||||
|
||||
"""
|
||||
node_index_map = self.node_index_map
|
||||
max_x, max_y = self.max_x, self.max_y
|
||||
xygrid = self.xygrid
|
||||
|
||||
# build all links
|
||||
# build all links except the transitional links
|
||||
for node in node_index_map.values():
|
||||
node.scan_all_directions(xygrid)
|
||||
node.build_links(xygrid)
|
||||
|
||||
# build display map
|
||||
display_map = [[" "] * (max_x + 1) for _ in range(max_y + 1)]
|
||||
|
|
@ -412,12 +390,16 @@ class XYMap:
|
|||
maplink.prototype = self.prototypes.get(node_coord + (direction,), maplink.prototype)
|
||||
|
||||
# store results
|
||||
self.display_map = display_map
|
||||
self.max_x, self.max_y = max_x, max_y
|
||||
self.xygrid = xygrid
|
||||
|
||||
def parse(self):
|
||||
"""Shortcut for running the full parsing of a single map. Useful for testing."""
|
||||
self.parse_first_pass()
|
||||
self.parse_second_pass()
|
||||
self.max_X, self.max_Y = max_X, max_Y
|
||||
self.XYgrid = XYgrid
|
||||
|
||||
self.node_index_map = node_index_map
|
||||
self.symbol_map = symbol_map
|
||||
|
||||
self.display_map = display_map
|
||||
|
||||
def _get_topology_around_coord(self, coord, dist=2):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -50,25 +50,19 @@ class XYZGrid(DefaultScript):
|
|||
|
||||
"""
|
||||
logger.log_info("[grid] (Re)loading grid ...")
|
||||
grid = {}
|
||||
self.ndb.grid = {}
|
||||
nmaps = 0
|
||||
# generate all Maps - this will also initialize their components
|
||||
# and bake any pathfinding paths (or load from disk-cache)
|
||||
for mapname, mapdata in self.db.map_data.items():
|
||||
logger.log_info(f"[grid] Loading map '{mapname}'...")
|
||||
xymap = XYMap(dict(mapdata), name=mapname, grid=self)
|
||||
xymap.parse_first_pass()
|
||||
grid[mapname] = xymap
|
||||
xymap.parse()
|
||||
xymap.calculate_path_matrix()
|
||||
self.ndb.grid[mapname] = xymap
|
||||
nmaps += 1
|
||||
|
||||
# link maps together across grid
|
||||
logger.log_info("[grid] Link {nmaps} maps (may be slow first time a map has changed) ...")
|
||||
for name, xymap in grid.items():
|
||||
xymap.parse_second_pass()
|
||||
xymap.calculate_path_matrix()
|
||||
|
||||
# store
|
||||
self.ndb.grid = grid
|
||||
logger.log_info(f"[grid] Loaded and linked {nmaps} map(s).")
|
||||
|
||||
def at_init(self):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue