Start creating top-level grid class
This commit is contained in:
parent
f40e8c1168
commit
bab2f962f5
8 changed files with 405 additions and 138 deletions
|
|
@ -37,7 +37,7 @@ advanced tools like pathfinding will only operate within each XY `Map`.
|
||||||
map/location/Z-coordinate.
|
map/location/Z-coordinate.
|
||||||
2. The Map Legend - describes how to parse each symbol in the map string to a
|
2. The Map Legend - describes how to parse each symbol in the map string to a
|
||||||
topological relation, such as 'a room' or 'a two-way link east-west'.
|
topological relation, such as 'a room' or 'a two-way link east-west'.
|
||||||
3. The Map - combines the Map String and Legend into a parsed object with
|
3. The XYMap - combines the Map String and Legend into a parsed object with
|
||||||
pathfinding and visual-range handling.
|
pathfinding and visual-range handling.
|
||||||
4. The MultiMap - tracks multiple maps
|
4. The MultiMap - tracks multiple maps
|
||||||
5. Rooms, Exits and Prototypes - custom Typeclasses that understands XYZ coordinates.
|
5. Rooms, Exits and Prototypes - custom Typeclasses that understands XYZ coordinates.
|
||||||
|
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
"""
|
|
||||||
The grid
|
|
||||||
|
|
||||||
This represents the full XYZ grid, which consists of
|
|
||||||
|
|
||||||
- 2D `Map`-objects parsed from Map strings and Map-legend components. Each represents one
|
|
||||||
Z-coordinate or location.
|
|
||||||
- `Prototypes` for how to build each XYZ component into 'real' rooms and exits.
|
|
||||||
- Actual in-game rooms and exits, mapped to the game based on Map data.
|
|
||||||
|
|
||||||
The grid has three main functions:
|
|
||||||
- Building new rooms/exits from scratch based on one or more Maps.
|
|
||||||
- Updating the rooms/exits tied to an existing Map when the Map string
|
|
||||||
of that map changes.
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
class XYZGrid:
|
|
||||||
pass
|
|
||||||
|
|
@ -14,8 +14,12 @@ except ImportError as err:
|
||||||
f"{err}\nThe XYZgrid contrib requires "
|
f"{err}\nThe XYZgrid contrib requires "
|
||||||
"the SciPy package. Install with `pip install scipy'.")
|
"the SciPy package. Install with `pip install scipy'.")
|
||||||
|
|
||||||
|
from evennia.prototypes import spawner
|
||||||
from .utils import MAPSCAN, REVERSE_DIRECTIONS, MapParserError, BIGVAL
|
from .utils import MAPSCAN, REVERSE_DIRECTIONS, MapParserError, BIGVAL
|
||||||
|
|
||||||
|
NodeTypeclass = None
|
||||||
|
ExitTypeclass = None
|
||||||
|
|
||||||
|
|
||||||
# Nodes/Links
|
# Nodes/Links
|
||||||
|
|
||||||
|
|
@ -26,6 +30,8 @@ class MapNode:
|
||||||
the even-integer coordinates and also represents in-game coordinates/rooms. MapNodes are always
|
the even-integer coordinates and also represents in-game coordinates/rooms. MapNodes are always
|
||||||
located on even X,Y coordinates on the map grid and in-game.
|
located on even X,Y coordinates on the map grid and in-game.
|
||||||
|
|
||||||
|
MapNodes will also handle the syncing of themselves and all outgoing links to the grid.
|
||||||
|
|
||||||
Attributes on the node class:
|
Attributes on the node class:
|
||||||
|
|
||||||
- `symbol` (str) - The character to parse from the map into this node. By default this
|
- `symbol` (str) - The character to parse from the map into this node. By default this
|
||||||
|
|
@ -41,6 +47,8 @@ class MapNode:
|
||||||
is useful for marking 'points of interest' along a route, or places where you are not
|
is useful for marking 'points of interest' along a route, or places where you are not
|
||||||
expected to be able to continue without some further in-game action not covered by the map
|
expected to be able to continue without some further in-game action not covered by the map
|
||||||
(such as a guard or locked gate etc).
|
(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.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# symbol used to identify this link on the map
|
# symbol used to identify this link on the map
|
||||||
|
|
@ -57,8 +65,10 @@ class MapNode:
|
||||||
# this will interrupt a shortest-path step (useful for 'points' of interest, stop before
|
# this will interrupt a shortest-path step (useful for 'points' of interest, stop before
|
||||||
# a door etc).
|
# a door etc).
|
||||||
interrupt_path = False
|
interrupt_path = False
|
||||||
|
# the prototype to use for mapping this to the grid.
|
||||||
|
prototype = None
|
||||||
|
|
||||||
def __init__(self, x, y, node_index=0):
|
def __init__(self, x, y, node_index=0, xymap=None):
|
||||||
"""
|
"""
|
||||||
Initialize the mapnode.
|
Initialize the mapnode.
|
||||||
|
|
||||||
|
|
@ -68,12 +78,16 @@ class MapNode:
|
||||||
node_index (int): This identifies this node with a running
|
node_index (int): This identifies this node with a running
|
||||||
index number required for pathfinding. This is used
|
index number required for pathfinding. This is used
|
||||||
internally and should not be set manually.
|
internally and should not be set manually.
|
||||||
|
xymap (XYMap, optional): The map object this sits on.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.x = x
|
self.x = x
|
||||||
self.y = y
|
self.y = y
|
||||||
|
|
||||||
|
# map name, usually
|
||||||
|
self.xymap = xymap
|
||||||
|
|
||||||
# XYgrid coordinate
|
# XYgrid coordinate
|
||||||
self.X = x // 2
|
self.X = x // 2
|
||||||
self.Y = y // 2
|
self.Y = y // 2
|
||||||
|
|
@ -83,6 +97,8 @@ class MapNode:
|
||||||
# this indicates linkage in 8 cardinal directions on the string-map,
|
# this indicates linkage in 8 cardinal directions on the string-map,
|
||||||
# n,ne,e,se,s,sw,w,nw and link that to a node (always)
|
# n,ne,e,se,s,sw,w,nw and link that to a node (always)
|
||||||
self.links = {}
|
self.links = {}
|
||||||
|
# first MapLink in each direction - used by grid syncing
|
||||||
|
self.first_links = {}
|
||||||
# this maps
|
# this maps
|
||||||
self.weights = {}
|
self.weights = {}
|
||||||
# lowest direction to a given neighbor
|
# lowest direction to a given neighbor
|
||||||
|
|
@ -136,6 +152,8 @@ class MapNode:
|
||||||
if end_node:
|
if end_node:
|
||||||
# the link could be followed to an end node!
|
# the link could be followed to an end node!
|
||||||
|
|
||||||
|
self.first_links[direction] = link
|
||||||
|
|
||||||
# check the actual direction-alias to use, since this may be
|
# check the actual direction-alias to use, since this may be
|
||||||
# different than the xygrid cardinal directions. There must be
|
# different than the xygrid cardinal directions. There must be
|
||||||
# no duplicates out of this node or there will be a
|
# no duplicates out of this node or there will be a
|
||||||
|
|
@ -183,12 +201,13 @@ class MapNode:
|
||||||
link_graph[node_index] = weight
|
link_graph[node_index] = weight
|
||||||
return link_graph
|
return link_graph
|
||||||
|
|
||||||
def get_display_symbol(self, xygrid, **kwargs):
|
def get_display_symbol(self, xygrid, xymap=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
Hook to override for customizing how the display_symbol is determined.
|
Hook to override for customizing how the display_symbol is determined.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
xygrid (dict): 2D dict with x,y coordinates as keys.
|
xygrid (dict): 2D dict with x,y coordinates as keys.
|
||||||
|
xymap (XYMap): Main Map object.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: The display-symbol to use. This must visually be a single character
|
str: The display-symbol to use. This must visually be a single character
|
||||||
|
|
@ -200,6 +219,72 @@ class MapNode:
|
||||||
"""
|
"""
|
||||||
return self.symbol if self.display_symbol is None else self.display_symbol
|
return self.symbol if self.display_symbol is None else self.display_symbol
|
||||||
|
|
||||||
|
def sync_node_to_grid(self):
|
||||||
|
"""
|
||||||
|
This should be called as part of the node-sync step of the map sync. The reason is
|
||||||
|
that the exits (next step) requires all nodes to exist before they can link up
|
||||||
|
to their destinations.
|
||||||
|
|
||||||
|
"""
|
||||||
|
global NodeTypeclass
|
||||||
|
if not NodeTypeclass:
|
||||||
|
from .room import XYZRoom as NodeTypeclass
|
||||||
|
|
||||||
|
coord = (self.X, self.Y, self.xymap.name)
|
||||||
|
|
||||||
|
try:
|
||||||
|
nodeobj = NodeTypeclass.objects.get_xyz(coord=coord)
|
||||||
|
except NodeTypeclass.DoesNotExist:
|
||||||
|
# create a new entity with proper coordinates etc
|
||||||
|
nodeobj = NodeTypeclass.create(
|
||||||
|
self.prototype.get('key', 'An Empty room'),
|
||||||
|
coord=coord
|
||||||
|
)
|
||||||
|
# apply prototype to node. This will not override the XYZ tags since
|
||||||
|
# these are not in the prototype and exact=False
|
||||||
|
spawner.batch_update_objects_with_prototype(
|
||||||
|
self.prototype, objects=[nodeobj], exact=False)
|
||||||
|
|
||||||
|
def sync_links_to_grid(self):
|
||||||
|
"""
|
||||||
|
This should be called after all `sync_node_to_grid` operations have finished across
|
||||||
|
the entire XYZgrid. This creates/syncs all exits to their locations and destinations.
|
||||||
|
|
||||||
|
"""
|
||||||
|
coord = (self.X, self.Y, self.xymap.name)
|
||||||
|
|
||||||
|
global ExitTypeclass
|
||||||
|
if not ExitTypeclass:
|
||||||
|
from .room import XYZExit as ExitTypeclass
|
||||||
|
|
||||||
|
maplinks = self.first_links
|
||||||
|
# we need to search for exits in all directions since some
|
||||||
|
# may have been removed since last sync
|
||||||
|
linkobjs = {exi.db_key: exi for exi in ExitTypeclass.filter_xyz(coord=coord)}
|
||||||
|
|
||||||
|
# figure out if the topology changed between grid and map (will always
|
||||||
|
# build all exits first run)
|
||||||
|
differing_directions = set(maplinks.keys()).symmetric_difference(set(linkobjs.keys()))
|
||||||
|
for direction in differing_directions:
|
||||||
|
if direction in linkobjs:
|
||||||
|
# an exit without a maplink - delete the exit
|
||||||
|
linkobjs.pop(direction).delete()
|
||||||
|
else:
|
||||||
|
# a maplink without an exit - create the exit
|
||||||
|
|
||||||
|
link = maplinks[direction]
|
||||||
|
exitnode = self.links[direction]
|
||||||
|
|
||||||
|
linkobjs[direction] = ExitTypeclass.create(
|
||||||
|
link.prototype.get('key', direction),
|
||||||
|
coord=coord,
|
||||||
|
destination_coord=(exitnode.X, exitnode.Y, exitnode.xymap.name)
|
||||||
|
)
|
||||||
|
# apply prototypes to catch any changes
|
||||||
|
for direction, linkobj in linkobjs:
|
||||||
|
spawner.batch_update_objects_with_prototype(
|
||||||
|
maplinks[direction].prototype, objects=[linkobj], exact=False)
|
||||||
|
|
||||||
|
|
||||||
class MapLink:
|
class MapLink:
|
||||||
"""
|
"""
|
||||||
|
|
@ -249,6 +334,10 @@ class MapLink:
|
||||||
setting is necessary to avoid infinite loops when such multilinks are next to each other.
|
setting is necessary to avoid infinite loops when such multilinks are next to each other.
|
||||||
- `interrupt_path` (bool): If set, a shortest-path solution will include this link as normal,
|
- `interrupt_path` (bool): If set, a shortest-path solution will include this link as normal,
|
||||||
but will stop short of actually moving past this link.
|
but will stop short of actually moving past this link.
|
||||||
|
- `prototype` (dict) - The default `prototype` dict to use for reproducing this map component
|
||||||
|
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.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# symbol for identifying this link on the map
|
# symbol for identifying this link on the map
|
||||||
|
|
@ -284,19 +373,26 @@ class MapLink:
|
||||||
# this link does not block/reroute pathfinding, but makes the actual path always stop when
|
# this link does not block/reroute pathfinding, but makes the actual path always stop when
|
||||||
# trying to cross it.
|
# trying to cross it.
|
||||||
interrupt_path = False
|
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):
|
def __init__(self, x, y, xymap=None):
|
||||||
"""
|
"""
|
||||||
Initialize the link.
|
Initialize the link.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
x (int): The xygrid x coordinate
|
x (int): The xygrid x coordinate
|
||||||
y (int): The xygrid y coordinate.
|
y (int): The xygrid y coordinate.
|
||||||
|
xymap (XYMap, optional): The map object this sits on.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.x = x
|
self.x = x
|
||||||
self.y = y
|
self.y = y
|
||||||
|
|
||||||
|
self.xymap = xymap
|
||||||
|
|
||||||
self.X = x / 2
|
self.X = x / 2
|
||||||
self.Y = y / 2
|
self.Y = y / 2
|
||||||
|
|
||||||
|
|
@ -351,6 +447,11 @@ class MapLink:
|
||||||
raise MapParserError(
|
raise MapParserError(
|
||||||
f"points to empty space in the direction {end_direction}!", self)
|
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])
|
||||||
|
|
||||||
_weight += self.get_weight(start_direction, xygrid, _weight)
|
_weight += self.get_weight(start_direction, xygrid, _weight)
|
||||||
if _steps is None:
|
if _steps is None:
|
||||||
_steps = []
|
_steps = []
|
||||||
|
|
@ -457,16 +558,14 @@ class MapLink:
|
||||||
"""
|
"""
|
||||||
return self.weights.get(start_direction, self.default_weight)
|
return self.weights.get(start_direction, self.default_weight)
|
||||||
|
|
||||||
def get_display_symbol(self, xygrid, **kwargs):
|
def get_display_symbol(self, xygrid, xymap=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
Hook to override for customizing how the display_symbol is determined.
|
Hook to override for customizing how the display_symbol is determined.
|
||||||
This is called after all other hooks, at map visualization.
|
This is called after all other hooks, at map visualization.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
xygrid (dict): 2D dict with x,y coordinates as keys.
|
xygrid (dict): 2D dict with x,y coordinates as keys.
|
||||||
|
xymap (XYMap): The map object this sits on.
|
||||||
Kwargs:
|
|
||||||
mapinstance (Map): The current Map instance.
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: The display-symbol to use. This must visually be a single character
|
str: The display-symbol to use. This must visually be a single character
|
||||||
|
|
@ -576,8 +675,8 @@ class TeleporterMapLink(MapLink):
|
||||||
display_symbol = ' '
|
display_symbol = ' '
|
||||||
direction_name = 'teleport'
|
direction_name = 'teleport'
|
||||||
|
|
||||||
def __init__(self, *args):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args)
|
super().__init__(*args, **kwargs)
|
||||||
self.paired_teleporter = None
|
self.paired_teleporter = None
|
||||||
|
|
||||||
def at_empty_target(self, start_direction, xygrid):
|
def at_empty_target(self, start_direction, xygrid):
|
||||||
|
|
@ -690,13 +789,11 @@ class MapTransitionLink(TeleporterMapLink):
|
||||||
direction_name = 'transition'
|
direction_name = 'transition'
|
||||||
interrupt_path = True
|
interrupt_path = True
|
||||||
|
|
||||||
map1_name = 'map'
|
target_map = 'map2'
|
||||||
map2_name = 'map'
|
|
||||||
|
|
||||||
def __init__(self, *args):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args)
|
super().__init__(*args, **kwargs)
|
||||||
self.map1 = None
|
self.paired_map_link = None
|
||||||
self.map2 = None
|
|
||||||
|
|
||||||
def at_empty_target(self, start_direction, end_direction, xygrid):
|
def at_empty_target(self, start_direction, end_direction, xygrid):
|
||||||
"""
|
"""
|
||||||
|
|
@ -709,7 +806,26 @@ class MapTransitionLink(TeleporterMapLink):
|
||||||
xygrid (dict): 2D dict with x,y coordinates as keys.
|
xygrid (dict): 2D dict with x,y coordinates as keys.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# TODO - this needs some higher-level handler to work.
|
if not self.paired_map_link:
|
||||||
|
grid = self.xymap.grid.grid
|
||||||
|
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):
|
class SmartMapLink(MapLink):
|
||||||
|
|
@ -826,7 +942,7 @@ class InvisibleSmartMapLink(SmartMapLink):
|
||||||
(('ne', 'sw'), ('sw', 'ne')): '/',
|
(('ne', 'sw'), ('sw', 'ne')): '/',
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_display_symbol(self, xygrid, **kwargs):
|
def get_display_symbol(self, xygrid, xymap=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
The SmartMapLink already calculated the directions before this, so we
|
The SmartMapLink already calculated the directions before this, so we
|
||||||
just need to figure out what to replace this with in order to make this 'invisible'
|
just need to figure out what to replace this with in order to make this 'invisible'
|
||||||
|
|
@ -836,9 +952,7 @@ class InvisibleSmartMapLink(SmartMapLink):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if not hasattr(self, "_cached_display_symbol"):
|
if not hasattr(self, "_cached_display_symbol"):
|
||||||
mapinstance = kwargs['mapinstance']
|
legend = xymap.legend
|
||||||
|
|
||||||
legend = mapinstance.legend
|
|
||||||
default_symbol = (
|
default_symbol = (
|
||||||
self.symbol if self.display_symbol is None else self.display_symbol)
|
self.symbol if self.display_symbol is None else self.display_symbol)
|
||||||
self._cached_display_symbol = default_symbol
|
self._cached_display_symbol = default_symbol
|
||||||
|
|
@ -854,7 +968,7 @@ class InvisibleSmartMapLink(SmartMapLink):
|
||||||
# initiate class in the current location and run get_display_symbol
|
# initiate class in the current location and run get_display_symbol
|
||||||
# to get what it would show.
|
# to get what it would show.
|
||||||
self._cached_display_symbol = node_or_link_class(
|
self._cached_display_symbol = node_or_link_class(
|
||||||
self.x, self.y).get_display_symbol(xygrid, **kwargs)
|
self.x, self.y).get_display_symbol(xygrid, xymap=xymap, **kwargs)
|
||||||
return self._cached_display_symbol
|
return self._cached_display_symbol
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
"""
|
|
||||||
Over-arching map system for representing a larger number of Maps linked together with transitions.
|
|
||||||
|
|
||||||
"""
|
|
||||||
from .map_single import SingleMap
|
|
||||||
|
|
||||||
|
|
||||||
class MultiMap:
|
|
||||||
"""
|
|
||||||
Coordinate multiple maps.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.maps = {}
|
|
||||||
|
|
||||||
def add_map(self, map_module_or_dict, name="map"):
|
|
||||||
"""
|
|
||||||
Add a new map to the multimap store.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
map_module_or_dict (str, module or dict): Path or module pointing to a map. If a dict,
|
|
||||||
this should be a dict with a key 'map' and optionally a 'legend', 'name' and
|
|
||||||
`prototypes` keys.
|
|
||||||
name (str): Unique identifier for this map. Needed if the game uses
|
|
||||||
more than one map. Used when referencing this map during map transitions,
|
|
||||||
baking of pathfinding matrices etc.
|
|
||||||
|
|
||||||
"""
|
|
||||||
self.maps[name] = SingleMap(map_module_or_dict, name=name, other_maps=self.maps)
|
|
||||||
|
|
@ -8,7 +8,7 @@ from time import time
|
||||||
from random import randint
|
from random import randint
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
from parameterized import parameterized
|
from parameterized import parameterized
|
||||||
from . import map_single
|
from . import xymap
|
||||||
|
|
||||||
MAP1 = """
|
MAP1 = """
|
||||||
|
|
||||||
|
|
@ -320,7 +320,8 @@ class TestMap1(TestCase):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.map = map_single.SingleMap({"map": MAP1}, name="testmap")
|
self.map = xymap.XYMap({"map": MAP1}, name="testmap")
|
||||||
|
self.map.parse()
|
||||||
|
|
||||||
def test_str_output(self):
|
def test_str_output(self):
|
||||||
"""Check the display_map"""
|
"""Check the display_map"""
|
||||||
|
|
@ -404,7 +405,8 @@ class TestMap2(TestCase):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.map = map_single.SingleMap({"map": MAP2}, name="testmap")
|
self.map = xymap.XYMap({"map": MAP2}, name="testmap")
|
||||||
|
self.map.parse()
|
||||||
|
|
||||||
def test_str_output(self):
|
def test_str_output(self):
|
||||||
"""Check the display_map"""
|
"""Check the display_map"""
|
||||||
|
|
@ -514,7 +516,8 @@ class TestMap3(TestCase):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.map = map_single.SingleMap({"map": MAP3}, name="testmap")
|
self.map = xymap.XYMap({"map": MAP3}, name="testmap")
|
||||||
|
self.map.parse()
|
||||||
|
|
||||||
def test_str_output(self):
|
def test_str_output(self):
|
||||||
"""Check the display_map"""
|
"""Check the display_map"""
|
||||||
|
|
@ -563,7 +566,8 @@ class TestMap4(TestCase):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.map = map_single.SingleMap({"map": MAP4}, name="testmap")
|
self.map = xymap.XYMap({"map": MAP4}, name="testmap")
|
||||||
|
self.map.parse()
|
||||||
|
|
||||||
def test_str_output(self):
|
def test_str_output(self):
|
||||||
"""Check the display_map"""
|
"""Check the display_map"""
|
||||||
|
|
@ -593,7 +597,8 @@ class TestMap5(TestCase):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.map = map_single.SingleMap({"map": MAP5}, name="testmap")
|
self.map = xymap.XYMap({"map": MAP5}, name="testmap")
|
||||||
|
self.map.parse()
|
||||||
|
|
||||||
def test_str_output(self):
|
def test_str_output(self):
|
||||||
"""Check the display_map"""
|
"""Check the display_map"""
|
||||||
|
|
@ -621,7 +626,8 @@ class TestMap6(TestCase):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.map = map_single.SingleMap({"map": MAP6}, name="testmap")
|
self.map = xymap.XYMap({"map": MAP6}, name="testmap")
|
||||||
|
self.map.parse()
|
||||||
|
|
||||||
def test_str_output(self):
|
def test_str_output(self):
|
||||||
"""Check the display_map"""
|
"""Check the display_map"""
|
||||||
|
|
@ -653,7 +659,8 @@ class TestMap7(TestCase):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.map = map_single.SingleMap({"map": MAP7}, name="testmap")
|
self.map = xymap.XYMap({"map": MAP7}, name="testmap")
|
||||||
|
self.map.parse()
|
||||||
|
|
||||||
def test_str_output(self):
|
def test_str_output(self):
|
||||||
"""Check the display_map"""
|
"""Check the display_map"""
|
||||||
|
|
@ -681,7 +688,8 @@ class TestMap8(TestCase):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.map = map_single.SingleMap({"map": MAP8}, name="testmap")
|
self.map = xymap.XYMap({"map": MAP8}, name="testmap")
|
||||||
|
self.map.parse()
|
||||||
|
|
||||||
def test_str_output(self):
|
def test_str_output(self):
|
||||||
"""Check the display_map"""
|
"""Check the display_map"""
|
||||||
|
|
@ -747,7 +755,8 @@ class TestMap9(TestCase):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.map = map_single.SingleMap({"map": MAP9}, name="testmap")
|
self.map = xymap.XYMap({"map": MAP9}, name="testmap")
|
||||||
|
self.map.parse()
|
||||||
|
|
||||||
def test_str_output(self):
|
def test_str_output(self):
|
||||||
"""Check the display_map"""
|
"""Check the display_map"""
|
||||||
|
|
@ -776,7 +785,8 @@ class TestMap10(TestCase):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.map = map_single.SingleMap({"map": MAP10}, name="testmap")
|
self.map = xymap.XYMap({"map": MAP10}, name="testmap")
|
||||||
|
self.map.parse()
|
||||||
|
|
||||||
def test_str_output(self):
|
def test_str_output(self):
|
||||||
"""Check the display_map"""
|
"""Check the display_map"""
|
||||||
|
|
@ -824,7 +834,8 @@ class TestMap11(TestCase):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.map = map_single.SingleMap({"map": MAP11}, name="testmap")
|
self.map = xymap.XYMap({"map": MAP11}, name="testmap")
|
||||||
|
self.map.parse()
|
||||||
|
|
||||||
def test_str_output(self):
|
def test_str_output(self):
|
||||||
"""Check the display_map"""
|
"""Check the display_map"""
|
||||||
|
|
@ -914,7 +925,8 @@ class TestMapStressTest(TestCase):
|
||||||
grid = self._get_grid(Xmax, Ymax)
|
grid = self._get_grid(Xmax, Ymax)
|
||||||
# print(f"\n\n{grid}\n")
|
# print(f"\n\n{grid}\n")
|
||||||
t0 = time()
|
t0 = time()
|
||||||
map_single.SingleMap({'map': grid}, name="testmap")
|
mapobj = xymap.XYMap({'map': grid}, name="testmap")
|
||||||
|
mapobj.parse()
|
||||||
t1 = time()
|
t1 = time()
|
||||||
self.assertLess(t1 - t0, max_time, f"Map creation of ({Xmax}x{Ymax}) grid slower "
|
self.assertLess(t1 - t0, max_time, f"Map creation of ({Xmax}x{Ymax}) grid slower "
|
||||||
f"than expected {max_time}s.")
|
f"than expected {max_time}s.")
|
||||||
|
|
@ -930,7 +942,8 @@ class TestMapStressTest(TestCase):
|
||||||
"""
|
"""
|
||||||
Xmax, Ymax = gridsize
|
Xmax, Ymax = gridsize
|
||||||
grid = self._get_grid(Xmax, Ymax)
|
grid = self._get_grid(Xmax, Ymax)
|
||||||
mapobj = map_single.SingleMap({'map': grid}, name="testmap")
|
mapobj = xymap.XYMap({'map': grid}, name="testmap")
|
||||||
|
mapobj.parse()
|
||||||
|
|
||||||
t0 = time()
|
t0 = time()
|
||||||
mapobj._calculate_path_matrix()
|
mapobj._calculate_path_matrix()
|
||||||
|
|
@ -962,7 +975,8 @@ class TestMapStressTest(TestCase):
|
||||||
"""
|
"""
|
||||||
Xmax, Ymax = gridsize
|
Xmax, Ymax = gridsize
|
||||||
grid = self._get_grid(Xmax, Ymax)
|
grid = self._get_grid(Xmax, Ymax)
|
||||||
mapobj = map_single.SingleMap({'map': grid}, name="testmap")
|
mapobj = xymap.XYMap({'map': grid}, name="testmap")
|
||||||
|
mapobj.parse()
|
||||||
|
|
||||||
t0 = time()
|
t0 = time()
|
||||||
mapobj._calculate_path_matrix()
|
mapobj._calculate_path_matrix()
|
||||||
|
|
|
||||||
|
|
@ -46,4 +46,10 @@ class MapParserError(MapError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MapTransition(RuntimeWarning):
|
||||||
|
"""
|
||||||
|
Used when signaling to the parser that a link
|
||||||
|
leads to another map.
|
||||||
|
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
r"""
|
r"""
|
||||||
# Map
|
# XYMap
|
||||||
|
|
||||||
The `Map` class represents one XY-grid of interconnected map-legend components. It's built from an
|
The `XYMap` class represents one XY-grid of interconnected map-legend components. It's built from an
|
||||||
ASCII representation, where unique characters represents each type of component. The Map parses the
|
ASCII representation, where unique characters represents each type of component. The Map parses the
|
||||||
map into an internal graph that can be efficiently used for pathfinding the shortest route between
|
map into an internal graph that can be efficiently used for pathfinding the shortest route between
|
||||||
any two nodes (rooms).
|
any two nodes (rooms).
|
||||||
|
|
@ -53,6 +53,13 @@ as up and down. These are indicated in code as 'n', 'ne', 'e', 'se', 's', 'sw',
|
||||||
MAP_DATA = {
|
MAP_DATA = {
|
||||||
"map": MAP,
|
"map": MAP,
|
||||||
"legend": LEGEND,
|
"legend": LEGEND,
|
||||||
|
"name": "City of Foo",
|
||||||
|
"prototypes": {
|
||||||
|
(0,1): { ... },
|
||||||
|
(1,3): { ... },
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
@ -131,7 +138,7 @@ DEFAULT_LEGEND = {
|
||||||
# Map parser implementation
|
# Map parser implementation
|
||||||
|
|
||||||
|
|
||||||
class SingleMap:
|
class XYMap:
|
||||||
r"""
|
r"""
|
||||||
This represents a single map of interconnected nodes/rooms, parsed from a ASCII map
|
This represents a single map of interconnected nodes/rooms, parsed from a ASCII map
|
||||||
representation.
|
representation.
|
||||||
|
|
@ -177,7 +184,7 @@ class SingleMap:
|
||||||
# we normally only accept one single character for the legend key
|
# we normally only accept one single character for the legend key
|
||||||
legend_key_exceptions = ("\\")
|
legend_key_exceptions = ("\\")
|
||||||
|
|
||||||
def __init__(self, map_module_or_dict, name="map", other_maps=None):
|
def __init__(self, map_module_or_dict, name="map", grid=None):
|
||||||
"""
|
"""
|
||||||
Initialize the map parser by feeding it the map.
|
Initialize the map parser by feeding it the map.
|
||||||
|
|
||||||
|
|
@ -189,9 +196,8 @@ class SingleMap:
|
||||||
more than one map. Used when referencing this map during map transitions,
|
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
|
baking of pathfinding matrices etc. This will be overridden by any 'name' given
|
||||||
in the MAP_DATA itself.
|
in the MAP_DATA itself.
|
||||||
other_maps (dict, optional): Reference to mapping {name: SingleMap, ...} representing
|
grid (xyzgrid.XYZGrid, optional): Reference to the top-level grid object, which
|
||||||
all possible maps one could potentially reach from this map. This is usually
|
stores all maps. This is necessary for transitioning from map to another.
|
||||||
provided by the MutlMap handler.
|
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
The map deals with two sets of coorinate systems:
|
The map deals with two sets of coorinate systems:
|
||||||
|
|
@ -211,8 +217,10 @@ class SingleMap:
|
||||||
# store so we can reload
|
# store so we can reload
|
||||||
self.map_module_or_dict = map_module_or_dict
|
self.map_module_or_dict = map_module_or_dict
|
||||||
|
|
||||||
self.other_maps = other_maps
|
self.grid = grid
|
||||||
self.room_prototypes = None
|
self.prototypes = None
|
||||||
|
# transitional mapping
|
||||||
|
self.symbol_map = None
|
||||||
|
|
||||||
# map setup
|
# map setup
|
||||||
self.xygrid = None
|
self.xygrid = None
|
||||||
|
|
@ -249,12 +257,14 @@ class SingleMap:
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<Map {self.max_X + 1}x{self.max_Y + 1}, {len(self.node_index_map)} nodes>"
|
return f"<Map {self.max_X + 1}x{self.max_Y + 1}, {len(self.node_index_map)} nodes>"
|
||||||
|
|
||||||
def _parse(self):
|
def parse_first_pass(self):
|
||||||
"""
|
"""
|
||||||
Parses the numerical grid from the string. The result of this is a 2D array
|
Parses the numerical grid from the string. The first pass means parsing out
|
||||||
of [[MapNode,...], [MapNode, ...]] with MapLinks inside them describing their
|
all nodes. The linking-together of nodes is not happening until the second pass
|
||||||
linkage to other nodes. See the class docstring for details of how the grid
|
(the reason for this is that maps can also link to other maps, so all maps need
|
||||||
should be defined.
|
to have gone through their first parsing-passes before they can be linked together).
|
||||||
|
|
||||||
|
See the class docstring for details of how the grid should be defined.
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
In this parsing, the 'xygrid' is the full range of chraracters read from
|
In this parsing, the 'xygrid' is the full range of chraracters read from
|
||||||
|
|
@ -269,6 +279,8 @@ class SingleMap:
|
||||||
XYgrid = defaultdict(dict)
|
XYgrid = defaultdict(dict)
|
||||||
# needed by pathfinder
|
# needed by pathfinder
|
||||||
node_index_map = {}
|
node_index_map = {}
|
||||||
|
# used by transitions
|
||||||
|
symbol_map = defaultdict(list)
|
||||||
|
|
||||||
mapstring = self.mapstring
|
mapstring = self.mapstring
|
||||||
if mapstring.count(mapcorner_symbol) < 2:
|
if mapstring.count(mapcorner_symbol) < 2:
|
||||||
|
|
@ -347,44 +359,16 @@ class SingleMap:
|
||||||
node_index += 1
|
node_index += 1
|
||||||
|
|
||||||
xygrid[ix][iy] = XYgrid[iX][iY] = node_index_map[node_index] = \
|
xygrid[ix][iy] = XYgrid[iX][iY] = node_index_map[node_index] = \
|
||||||
mapnode_or_link_class(node_index=node_index, x=ix, y=iy)
|
mapnode_or_link_class(node_index=node_index, x=ix, y=iy, xymap=self)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# we have a link at this xygrid position (this is ok everywhere)
|
# we have a link at this xygrid position (this is ok everywhere)
|
||||||
xygrid[ix][iy] = mapnode_or_link_class(ix, iy)
|
xygrid[ix][iy] = mapnode_or_link_class(ix, iy, xymap=self)
|
||||||
|
|
||||||
# second pass: Here we loop over all nodes and have them connect to each other
|
# store the symbol mapping for transition lookups
|
||||||
# via the detected linkages.
|
symbol_map[char].append(xygrid[ix][iy])
|
||||||
for node in node_index_map.values():
|
|
||||||
node.scan_all_directions(xygrid)
|
|
||||||
|
|
||||||
# build display map
|
# store results
|
||||||
display_map = [[" "] * (max_x + 1) for _ in range(max_y + 1)]
|
|
||||||
for ix, ydct in xygrid.items():
|
|
||||||
for iy, node_or_link in ydct.items():
|
|
||||||
display_map[iy][ix] = node_or_link.get_display_symbol(xygrid, mapinstance=self)
|
|
||||||
|
|
||||||
if self.room_prototypes:
|
|
||||||
# validate that prototypes are actually all represented by a node on the grid.
|
|
||||||
node_positions = []
|
|
||||||
for node in node_index_map:
|
|
||||||
# check so every node has a prototype
|
|
||||||
node_coord = (node.X, node.Y)
|
|
||||||
node_positions.append(node_coord)
|
|
||||||
if node_coord not in self.room_prototypes:
|
|
||||||
raise MapParserError(
|
|
||||||
f"Symbol '{char}' on XY=({node_coord[0]},{node_coord[1]}) has "
|
|
||||||
"no corresponding entry in the `rooms` prototype dictionary."
|
|
||||||
)
|
|
||||||
for (iX, iY) in self.room_prototypes:
|
|
||||||
# also check in the reverse direction - so every prototype has a node
|
|
||||||
if (iX, iY) not in node_positions:
|
|
||||||
raise MapParserError(
|
|
||||||
f"There is a room prototype for XY=({iX},{iY}), but that position "
|
|
||||||
"of the map grid lacks a node."
|
|
||||||
)
|
|
||||||
|
|
||||||
# store
|
|
||||||
self.max_x, self.max_y = max_x, max_y
|
self.max_x, self.max_y = max_x, max_y
|
||||||
self.xygrid = xygrid
|
self.xygrid = xygrid
|
||||||
|
|
||||||
|
|
@ -392,8 +376,49 @@ class SingleMap:
|
||||||
self.XYgrid = XYgrid
|
self.XYgrid = XYgrid
|
||||||
|
|
||||||
self.node_index_map = node_index_map
|
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
|
||||||
|
for node in node_index_map.values():
|
||||||
|
node.scan_all_directions(xygrid)
|
||||||
|
|
||||||
|
# build display map
|
||||||
|
display_map = [[" "] * (max_x + 1) for _ in range(max_y + 1)]
|
||||||
|
for ix, ydct in xygrid.items():
|
||||||
|
for iy, node_or_link in ydct.items():
|
||||||
|
display_map[iy][ix] = node_or_link.get_display_symbol(xygrid, xymap=self)
|
||||||
|
|
||||||
|
# validate and make sure all nodes/links have prototypes
|
||||||
|
for node in node_index_map.values():
|
||||||
|
node_coord = (node.X, node.Y)
|
||||||
|
# load prototype from override, or use default
|
||||||
|
node.prototype = self.prototypes.get(node_coord, node.prototype)
|
||||||
|
# do the same for links (x, y, direction) coords
|
||||||
|
for direction, maplink in node.first_links.items():
|
||||||
|
maplink.prototype = self.prototypes.get(node_coord + (direction,), maplink.prototype)
|
||||||
|
|
||||||
|
# store results
|
||||||
self.display_map = display_map
|
self.display_map = display_map
|
||||||
|
|
||||||
|
def parse(self):
|
||||||
|
"""Shortcut for running the full parsing of a single map. Useful for testing."""
|
||||||
|
self.parse_first_pass()
|
||||||
|
self.parse_second_pass()
|
||||||
|
|
||||||
def _get_topology_around_coord(self, coord, dist=2):
|
def _get_topology_around_coord(self, coord, dist=2):
|
||||||
"""
|
"""
|
||||||
Get all links and nodes up to a certain distance from an XY coordinate.
|
Get all links and nodes up to a certain distance from an XY coordinate.
|
||||||
|
|
@ -518,6 +543,7 @@ class SingleMap:
|
||||||
mapdata['map'] = variable_from_module(mod, "MAP")
|
mapdata['map'] = variable_from_module(mod, "MAP")
|
||||||
mapdata['legend'] = variable_from_module(mod, "LEGEND", default=DEFAULT_LEGEND)
|
mapdata['legend'] = variable_from_module(mod, "LEGEND", default=DEFAULT_LEGEND)
|
||||||
mapdata['rooms'] = variable_from_module(mod, "ROOMS")
|
mapdata['rooms'] = variable_from_module(mod, "ROOMS")
|
||||||
|
mapdata['prototypes'] = variable_from_module(mod, "PROTOTYPES", default={})
|
||||||
|
|
||||||
# validate
|
# validate
|
||||||
for key in mapdata.get('legend', DEFAULT_LEGEND):
|
for key in mapdata.get('legend', DEFAULT_LEGEND):
|
||||||
|
|
@ -536,13 +562,11 @@ class SingleMap:
|
||||||
# store/update result
|
# store/update result
|
||||||
self.name = mapdata.get('name', self.name)
|
self.name = mapdata.get('name', self.name)
|
||||||
self.mapstring = mapdata['map']
|
self.mapstring = mapdata['map']
|
||||||
|
self.prototypes = mapdata.get('prototypes', {})
|
||||||
# merge the custom legend onto the default legend to allow easily
|
# merge the custom legend onto the default legend to allow easily
|
||||||
# overriding only parts of it
|
# overriding only parts of it
|
||||||
self.legend = {**DEFAULT_LEGEND, **map_module_or_dict.get("legend", DEFAULT_LEGEND)}
|
self.legend = {**DEFAULT_LEGEND, **map_module_or_dict.get("legend", DEFAULT_LEGEND)}
|
||||||
|
|
||||||
# process the new(?) data
|
|
||||||
self._parse()
|
|
||||||
|
|
||||||
def get_node_from_coord(self, coords):
|
def get_node_from_coord(self, coords):
|
||||||
"""
|
"""
|
||||||
Get a MapNode from a coordinate.
|
Get a MapNode from a coordinate.
|
||||||
|
|
@ -571,6 +595,19 @@ class SingleMap:
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def get_components_with_symbol(self, symbol):
|
||||||
|
"""
|
||||||
|
Find all map components (nodes, links) with a given symbol in this map.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
symbol (char): A single character-symbol to search for.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: A list of MapNodes and/or MapLinks found with the matching symbol.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self.symbol_map.get(symbol, [])
|
||||||
|
|
||||||
def get_shortest_path(self, startcoord, endcoord):
|
def get_shortest_path(self, startcoord, endcoord):
|
||||||
"""
|
"""
|
||||||
Get the shortest route between two points on the grid.
|
Get the shortest route between two points on the grid.
|
||||||
146
evennia/contrib/xyzgrid/xyzgrid.py
Normal file
146
evennia/contrib/xyzgrid/xyzgrid.py
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
"""
|
||||||
|
The grid
|
||||||
|
|
||||||
|
This represents the full XYZ grid, which consists of
|
||||||
|
|
||||||
|
- 2D `Map`-objects parsed from Map strings and Map-legend components. Each represents one
|
||||||
|
Z-coordinate or location.
|
||||||
|
- `Prototypes` for how to build each XYZ component into 'real' rooms and exits.
|
||||||
|
- Actual in-game rooms and exits, mapped to the game based on Map data.
|
||||||
|
|
||||||
|
The grid has three main functions:
|
||||||
|
- Building new rooms/exits from scratch based on one or more Maps.
|
||||||
|
- Updating the rooms/exits tied to an existing Map when the Map string
|
||||||
|
of that map changes.
|
||||||
|
- Fascilitate communication between the in-game entities and their Map.
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
import itertools
|
||||||
|
from evennia.scripts.scripts import DefaultScript
|
||||||
|
from evennia.utils import logger
|
||||||
|
from .xymap import XYMap
|
||||||
|
|
||||||
|
|
||||||
|
class XYZGrid(DefaultScript):
|
||||||
|
"""
|
||||||
|
Main grid class. This organizes the Maps based on their name/Z-coordinate.
|
||||||
|
|
||||||
|
"""
|
||||||
|
def at_script_creation(self):
|
||||||
|
"""
|
||||||
|
What we store persistently is the module-paths to each map.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.db.map_data = {}
|
||||||
|
|
||||||
|
def reload(self):
|
||||||
|
"""
|
||||||
|
Reload the grid. This is done on a server reload and is also necessary if adding a new map
|
||||||
|
since this may introduce new between-map traversals.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# build the nodes of each map
|
||||||
|
for name, xymap in self.grid:
|
||||||
|
xymap.parse_first_pass()
|
||||||
|
# link everything together
|
||||||
|
for name, xymap in self.grid:
|
||||||
|
xymap.parse_second_pass()
|
||||||
|
|
||||||
|
def add_map(self, mapdata, new=True):
|
||||||
|
"""
|
||||||
|
Add new map to the grid.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mapdata (dict): A structure `{"map": <mapstr>, "legend": <legenddict>,
|
||||||
|
"name": <name>, "prototypes": <dict-of-dicts>}`. The `prototypes are
|
||||||
|
coordinate-specific overrides for nodes/links on the map, keyed with their
|
||||||
|
(X,Y) coordinate (use .5 for link-positions between nodes).
|
||||||
|
new (bool, optional): If the data should be resaved.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: If mapdata is malformed.
|
||||||
|
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
After this, you need to run `.sync_to_grid` to make the new map actually
|
||||||
|
available in-game.
|
||||||
|
|
||||||
|
"""
|
||||||
|
name = mapdata.get('name')
|
||||||
|
if not name:
|
||||||
|
raise RuntimeError("XYZGrid.add_map data must contain 'name'.")
|
||||||
|
|
||||||
|
# this will raise MapErrors if there are issues with the map
|
||||||
|
self.grid[name] = XYMap(mapdata, name=name, grid=self)
|
||||||
|
if new:
|
||||||
|
self.db.map_data[name] = mapdata
|
||||||
|
|
||||||
|
def remove_map(self, zcoord, remove_objects=False):
|
||||||
|
"""
|
||||||
|
Remove a map from the grid.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): The map to remove.
|
||||||
|
remove_objects (bool, optional): If the synced database objects (rooms/exits) should
|
||||||
|
be removed alongside this map.
|
||||||
|
"""
|
||||||
|
if zcoord in self.grid:
|
||||||
|
self.db.map_data.pop(zcoord)
|
||||||
|
self.grid.pop(zcoord)
|
||||||
|
|
||||||
|
if remove_objects:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def sync_to_grid(self, coord=(None, None, None), direction=None):
|
||||||
|
"""
|
||||||
|
Create/recreate/update the in-game grid based on the stored Maps or for a specific Map
|
||||||
|
or coordinate.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
coord (tuple, optional): An (X,Y,Z) coordinate, where Z is the name of the map. `None`
|
||||||
|
acts as a wildcard.
|
||||||
|
direction (str, optional): A cardinal direction ('n', 'ne' etc). If given, sync only the
|
||||||
|
exit in the given direction. `None` acts as a wildcard.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- `coord=(1, 3, 'foo')` - sync a specific element of map 'foo' only.
|
||||||
|
- `coord=(None, None, 'foo') - sync all elements of map 'foo'
|
||||||
|
- `coord=(1, 3, None) - sync all (1,3) coordinates on all maps (rarely useful)
|
||||||
|
- `coord=(None, None, None)` - sync all maps.
|
||||||
|
- `coord=(1, 3, 'foo')`, `direction='ne'` - sync only the north-eastern exit
|
||||||
|
out of the specific node on map 'foo'.
|
||||||
|
|
||||||
|
"""
|
||||||
|
x, y, z = coord
|
||||||
|
|
||||||
|
if z is None:
|
||||||
|
xymaps = self.grid
|
||||||
|
elif z in self.grid:
|
||||||
|
xymaps = [self.grid[z]]
|
||||||
|
else:
|
||||||
|
raise RuntimeError(f"The 'z' coordinate/name '{z}' is not found on the grid.")
|
||||||
|
|
||||||
|
# first syncing pass (only nodes/rooms)
|
||||||
|
synced = []
|
||||||
|
for xymap in xymaps:
|
||||||
|
for node in xymap.node_index_map.values():
|
||||||
|
if (x is None or x == node.X) and (y is None or y == node.Y):
|
||||||
|
node.sync_node_to_grid()
|
||||||
|
synced.append(node)
|
||||||
|
# second pass (links/exits)
|
||||||
|
for node in synced:
|
||||||
|
node.sync_links_to_grid()
|
||||||
|
|
||||||
|
def at_init(self):
|
||||||
|
"""
|
||||||
|
Called when the script loads into memory after a reload. This will load all map data into
|
||||||
|
memory.
|
||||||
|
|
||||||
|
"""
|
||||||
|
nmaps = 0
|
||||||
|
for mapname, mapdata in self.db.map_data:
|
||||||
|
self.add_map(mapdata, new=False)
|
||||||
|
nmaps += 1
|
||||||
|
self.reload()
|
||||||
|
logger.log_info(f"Loaded {nmaps} map(s) onto the grid.")
|
||||||
Loading…
Add table
Add a link
Reference in a new issue