mirror of
https://github.com/evennia/evennia.git
synced 2026-03-20 06:46:31 +01:00
Merge pull request #2859 from aMiss-aWry/contrib
Basic Worming Map Contrib
This commit is contained in:
commit
2db753dba5
4 changed files with 415 additions and 0 deletions
51
evennia/contrib/grid/ingame_map_display/README.md
Normal file
51
evennia/contrib/grid/ingame_map_display/README.md
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# Basic Map
|
||||
|
||||
Contribution - 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.
|
||||
|
||||
## Installation:
|
||||
|
||||
Adding the `MapDisplayCmdSet` to the default character cmdset will add the `map` command.
|
||||
|
||||
Specifically, in `mygame/commands/default_cmdsets.py`:
|
||||
|
||||
```python
|
||||
...
|
||||
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.
|
||||
|
||||
## Settings:
|
||||
|
||||
In order to change your default map size, you can add to `mygame/server/settings.py`:
|
||||
|
||||
```python
|
||||
BASIC_MAP_SIZE = 5 # This changes the default map width/height.
|
||||
|
||||
```
|
||||
|
||||
## Features:
|
||||
|
||||
### ASCII map (and evennia supports UTF-8 characters and even emojis)
|
||||
|
||||
This produces an ASCII map for players of configurable size.
|
||||
|
||||
### New command
|
||||
|
||||
- `CmdMap` - view the map
|
||||
6
evennia/contrib/grid/ingame_map_display/__init__.py
Normal file
6
evennia/contrib/grid/ingame_map_display/__init__.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
"""
|
||||
Mapbuilder - helpme 2022
|
||||
|
||||
"""
|
||||
|
||||
from .ingame_map_display import MapDisplayCmdSet # noqa
|
||||
323
evennia/contrib/grid/ingame_map_display/ingame_map_display.py
Normal file
323
evennia/contrib/grid/ingame_map_display/ingame_map_display.py
Normal file
|
|
@ -0,0 +1,323 @@
|
|||
"""
|
||||
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)
|
||||
35
evennia/contrib/grid/ingame_map_display/tests.py
Normal file
35
evennia/contrib/grid/ingame_map_display/tests.py
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
"""
|
||||
Tests of ingame_map_display.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
from evennia.commands.default.tests import BaseEvenniaCommandTest
|
||||
from evennia.utils.create import create_object
|
||||
from typeclasses import rooms, exits
|
||||
from . import ingame_map_display
|
||||
|
||||
|
||||
class TestIngameMap(BaseEvenniaCommandTest):
|
||||
"""
|
||||
Test the ingame map display by building two rooms and checking their connections are found
|
||||
|
||||
Expected output:
|
||||
[ ]--[ ]
|
||||
"""
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.west_room = create_object(rooms.Room, key="Room 1")
|
||||
self.east_room = create_object(rooms.Room, key="Room 2")
|
||||
create_object(exits.Exit, key="east", aliases=["e"], location=self.west_room, destination=self.east_room)
|
||||
create_object(exits.Exit, key="west", aliases=["w"], location=self.east_room, destination=self.west_room)
|
||||
|
||||
def test_west_room_map_room(self):
|
||||
self.char1.location = self.west_room
|
||||
map_here = ingame_map_display.Map(self.char1).show_map()
|
||||
self.assertEqual(map_here.strip(), '[|n|[x|co|n]|n--[|n ]|n')
|
||||
|
||||
def test_east_room_map_room(self):
|
||||
self.char1.location = self.east_room
|
||||
map_here = ingame_map_display.Map(self.char1).show_map()
|
||||
self.assertEqual(map_here.strip(), '[|n ]|n--[|n|[x|co|n]|n')
|
||||
Loading…
Add table
Add a link
Reference in a new issue