329 lines
10 KiB
Python
329 lines
10 KiB
Python
"""
|
|
Basic Map - helpme 2022
|
|
|
|
This adds an ascii `map` to a given room which can be viewed with the `map` command.
|
|
You can easily alter it to add special characters, room colors etc. The map shown is
|
|
dynamically generated on use, and supports all compass directions and up/down. Other
|
|
directions are ignored.
|
|
|
|
If you don't expect the map to be updated frequently, you could choose to save the
|
|
calculated map as a .ndb value on the room and render that instead of running mapping
|
|
calculations anew each time.
|
|
|
|
An example map:
|
|
```
|
|
|
|
|
-[-]-
|
|
|
|
|
|
|
|
-[-]--[-]--[-]--[-]
|
|
| | | |
|
|
| | |
|
|
-[-]--[-] [-]
|
|
| \/ | |
|
|
\ | /\ |
|
|
-[-]--[-]
|
|
```
|
|
|
|
Installation:
|
|
|
|
Adding the `MapDisplayCmdSet` to the default character cmdset will add the `map` command.
|
|
|
|
Specifically, in `mygame/commands/default_cmdsets.py`:
|
|
|
|
```
|
|
...
|
|
from evennia.contrib.grid.ingame_map_display import MapDisplayCmdSet # <---
|
|
|
|
class CharacterCmdset(default_cmds.Character_CmdSet):
|
|
...
|
|
def at_cmdset_creation(self):
|
|
...
|
|
self.add(MapDisplayCmdSet) # <---
|
|
|
|
```
|
|
|
|
Then `reload` to make the new commands available.
|
|
|
|
Additional Settings:
|
|
|
|
In order to change your default map size, you can add to `mygame/server/settings.py`:
|
|
|
|
BASIC_MAP_SIZE = 5
|
|
|
|
This changes the default map width/height. 2-5 for most clients is sensible.
|
|
|
|
If you don't want the player to be able to specify the size of the map, ignore any
|
|
arguments passed into the Map command.
|
|
"""
|
|
|
|
import time
|
|
|
|
from django.conf import settings
|
|
|
|
from evennia import CmdSet
|
|
from evennia.commands.default.muxcommand import MuxCommand
|
|
|
|
_BASIC_MAP_SIZE = settings.BASIC_MAP_SIZE if hasattr(settings, "BASIC_MAP_SIZE") else 2
|
|
_MAX_MAP_SIZE = settings.BASIC_MAP_SIZE if hasattr(settings, "MAX_MAP_SIZE") else 10
|
|
|
|
# _COMPASS_DIRECTIONS specifies which way to move the pointer on the x/y axes and what characters to use to depict the exits on the map.
|
|
_COMPASS_DIRECTIONS = {
|
|
"north": (0, -3, " | "),
|
|
"south": (0, 3, " | "),
|
|
"east": (3, 0, "-"),
|
|
"west": (-3, 0, "-"),
|
|
"northeast": (3, -3, "/"),
|
|
"northwest": (-3, -3, "\\"),
|
|
"southeast": (3, 3, "\\"),
|
|
"southwest": (-3, 3, "/"),
|
|
"up": (0, 0, "^"),
|
|
"down": (0, 0, "v"),
|
|
}
|
|
|
|
|
|
class Map(object):
|
|
def __init__(self, caller, size=_BASIC_MAP_SIZE, location=None):
|
|
"""
|
|
Initializes the map.
|
|
|
|
Args:
|
|
caller (object): Any object, though generally a puppeted character.
|
|
size (int): The seed size of the map, which will be multiplied to get the final grid size.
|
|
location (object): The location at the map's center (will default to caller.location if none provided).
|
|
"""
|
|
self.start_time = time.time()
|
|
self.caller = caller
|
|
self.max_width = int(size * 2 + 1) * 5 # This must be an odd number
|
|
self.max_length = int(size * 2 + 1) * 3 # This must be an odd number
|
|
self.has_mapped = {}
|
|
self.curX = None
|
|
self.curY = None
|
|
self.size = size
|
|
self.location = location or caller.location
|
|
|
|
def create_grid(self):
|
|
"""
|
|
Create the empty grid for the map based on the configured size
|
|
|
|
Returns:
|
|
list: The created grid, a list of lists.
|
|
"""
|
|
board = []
|
|
for row in range(self.max_length):
|
|
board.append([])
|
|
for column in range(int(self.max_width / 5)):
|
|
board[row].extend([" ", " ", " "])
|
|
return board
|
|
|
|
def exit_name_as_ordinal(self, ex):
|
|
"""
|
|
Get the exit name as a compass direction if possible
|
|
|
|
Args:
|
|
ex (Exit): The current exit being mapped.
|
|
Returns:
|
|
string: The exit name as a compass direction or an empty string.
|
|
"""
|
|
exit_name = ex.name
|
|
if exit_name not in _COMPASS_DIRECTIONS:
|
|
compass_aliases = [
|
|
direction in ex.aliases.all() for direction in _COMPASS_DIRECTIONS.keys()
|
|
]
|
|
if compass_aliases[0]:
|
|
exit_name = compass_aliases[0]
|
|
if exit_name not in _COMPASS_DIRECTIONS:
|
|
return ""
|
|
return exit_name
|
|
|
|
def update_pos(self, room, exit_name):
|
|
"""
|
|
Update the position pointer.
|
|
|
|
Args:
|
|
room (Room): The current location.
|
|
exit_name (str): The name of the exit to to use in this room. This must
|
|
be a valid compass direction, or an error will be raised.
|
|
Raises:
|
|
KeyError: If providing a non-compass exit name.
|
|
"""
|
|
# Update the pointer
|
|
self.curX, self.curY = self.has_mapped[room][0], self.has_mapped[room][1]
|
|
|
|
# Move the pointer depending on which direction the exit lies
|
|
# exit_name has already been validated as an ordinal direction at this point
|
|
self.curY += _COMPASS_DIRECTIONS[exit_name][0]
|
|
self.curX += _COMPASS_DIRECTIONS[exit_name][1]
|
|
|
|
def has_drawn(self, room):
|
|
"""
|
|
Checks if the given room has already been drawn or not
|
|
|
|
Args:
|
|
room (Room): Room to check.
|
|
Returns:
|
|
bool: Whether or not the room has been drawn.
|
|
"""
|
|
return True if room in self.has_mapped.keys() else False
|
|
|
|
def draw_room_on_map(self, room, max_distance):
|
|
"""
|
|
Draw the room and its exits on the map recursively
|
|
|
|
Args:
|
|
room (Room): The room to draw out.
|
|
max_distance (int): How extensive the map is.
|
|
"""
|
|
self.draw(room)
|
|
self.draw_exits(room)
|
|
|
|
if max_distance == 0:
|
|
return
|
|
|
|
# Check if the caller has access to the room in question. If not, don't draw it.
|
|
# Additionally, if the name of the exit is not ordinal but an alias of it is, use that.
|
|
for ex in [x for x in room.exits if x.access(self.caller, "traverse")]:
|
|
ex_name = self.exit_name_as_ordinal(ex)
|
|
if not ex_name or ex_name in ["up", "down"]:
|
|
continue
|
|
if self.has_drawn(ex.destination):
|
|
continue
|
|
|
|
self.update_pos(room, ex_name.lower())
|
|
self.draw_room_on_map(ex.destination, max_distance - 1)
|
|
|
|
def draw_exits(self, room):
|
|
"""
|
|
Draw a given room's exit paths
|
|
|
|
Args:
|
|
room (Room): The room to draw exits of.
|
|
"""
|
|
x, y = self.curX, self.curY
|
|
for ex in room.exits:
|
|
ex_name = self.exit_name_as_ordinal(ex)
|
|
if not ex_name:
|
|
continue
|
|
|
|
ex_character = _COMPASS_DIRECTIONS[ex_name][2]
|
|
delta_x = int(_COMPASS_DIRECTIONS[ex_name][1] / 3)
|
|
delta_y = int(_COMPASS_DIRECTIONS[ex_name][0] / 3)
|
|
|
|
# Make modifications if the exit has BOTH up and down exits
|
|
if ex_name == "up":
|
|
if "v" in self.grid[x][y]:
|
|
self.render_room(room, x, y, p1="^", p2="v")
|
|
else:
|
|
self.render_room(room, x, y, here="^")
|
|
elif ex_name == "down":
|
|
if "^" in self.grid[x][y]:
|
|
self.render_room(room, x, y, p1="^", p2="v")
|
|
else:
|
|
self.render_room(room, x, y, here="v")
|
|
else:
|
|
self.grid[x + delta_x][y + delta_y] = ex_character
|
|
|
|
def draw(self, room):
|
|
"""
|
|
Draw the map starting from a given room and add it to the cache of mapped rooms
|
|
|
|
Args:
|
|
room (Room): The room to render.
|
|
"""
|
|
# draw initial caller location on map first!
|
|
if room == self.location:
|
|
self.start_loc_on_grid(room)
|
|
self.has_mapped[room] = [self.curX, self.curY]
|
|
else:
|
|
# map all other rooms
|
|
self.has_mapped[room] = [self.curX, self.curY]
|
|
self.render_room(room, self.curX, self.curY)
|
|
|
|
def render_room(self, room, x, y, p1="[", p2="]", here=None):
|
|
"""
|
|
Draw a given room with ascii characters
|
|
|
|
Args:
|
|
room (Room): The room to render.
|
|
x (int): The x-value of the room on the grid (horizontally, east/west).
|
|
y (int): The y-value of the room on the grid (vertically, north/south).
|
|
p1 (str): The first character of the 3-character room depiction.
|
|
p2 (str): The last character of the 3-character room depiction.
|
|
here (str): Defaults to none, a special character depicting the room.
|
|
"""
|
|
# Note: This is where you would set colors, symbols etc.
|
|
# Render the room
|
|
you = list("[ ]")
|
|
|
|
you[0] = f"{p1}|n"
|
|
you[1] = f"{here if here else you[1]}"
|
|
if room == self.caller.location:
|
|
you[1] = "|[x|co|n" # Highlight the location you are currently in
|
|
you[2] = f"{p2}|n"
|
|
|
|
self.grid[x][y] = "".join(you)
|
|
|
|
def start_loc_on_grid(self, room):
|
|
"""
|
|
Set the starting location on the grid based on the maximum width and length
|
|
|
|
Args:
|
|
room (Room): The room to begin with.
|
|
"""
|
|
x = int((self.max_width * 0.6 - 1) / 2)
|
|
y = int((self.max_length - 1) / 2)
|
|
|
|
self.render_room(room, x, y)
|
|
self.curX, self.curY = x, y
|
|
|
|
def show_map(self, debug=False):
|
|
"""
|
|
Create and show the map, piecing it all together in the end
|
|
|
|
Args:
|
|
debug (bool): Whether or not to return the time taken to build the map.
|
|
"""
|
|
map_string = ""
|
|
self.grid = self.create_grid()
|
|
self.draw_room_on_map(self.location, self.size)
|
|
|
|
for row in self.grid:
|
|
map_row = "".join(row)
|
|
if map_row.strip() != "":
|
|
map_string += f"{map_row}\n"
|
|
|
|
elapsed = time.time() - self.start_time
|
|
if debug:
|
|
map_string += f"\nTook {elapsed}ms to render the map.\n"
|
|
|
|
return "%s" % map_string
|
|
|
|
|
|
class CmdMap(MuxCommand):
|
|
"""
|
|
Check the local map around you.
|
|
|
|
Usage: map (optional size)
|
|
"""
|
|
|
|
key = "map"
|
|
|
|
def func(self):
|
|
size = _BASIC_MAP_SIZE
|
|
max_size = _MAX_MAP_SIZE
|
|
if self.args.isnumeric():
|
|
size = min(max_size, int(self.args))
|
|
|
|
# You can run show_map(debug=True) to see how long it takes.
|
|
map_here = Map(self.caller, size=size).show_map()
|
|
self.caller.msg((map_here, {"type": "map"}))
|
|
|
|
|
|
# CmdSet for easily install all commands
|
|
class MapDisplayCmdSet(CmdSet):
|
|
"""
|
|
The map command.
|
|
"""
|
|
|
|
def at_cmdset_creation(self):
|
|
self.add(CmdMap)
|