mirror of
https://github.com/evennia/evennia.git
synced 2026-03-29 20:17:16 +02:00
Clean up contrib docs, autogeneration
This commit is contained in:
parent
b922cf9b3c
commit
e96bbb4b86
94 changed files with 4126 additions and 2536 deletions
494
docs/source/Howto/Dynamic-In-Game-Map.md
Normal file
494
docs/source/Howto/Dynamic-In-Game-Map.md
Normal file
|
|
@ -0,0 +1,494 @@
|
|||
# Dynamic In Game Map
|
||||
|
||||
## Introduction
|
||||
|
||||
An often desired feature in a MUD is to show an in-game map to help navigation. The [Static in-game
|
||||
map](../Contribs/Contrib-Mapbuilder.md) tutorial solves this by creating a *static* map, meaning the map is pre-
|
||||
drawn once and for all - the rooms are then created to match that map. When walking around, parts of
|
||||
the static map is then cut out and displayed next to the room description.
|
||||
|
||||
In this tutorial we'll instead do it the other way around; We will dynamically draw the map based on
|
||||
the relationships we find between already existing rooms.
|
||||
|
||||
## The Grid of Rooms
|
||||
|
||||
There are at least two requirements needed for this tutorial to work.
|
||||
|
||||
1. The structure of your mud has to follow a logical layout. Evennia supports the layout of your
|
||||
world to be 'logically' impossible with rooms looping to themselves or exits leading to the other
|
||||
side of the map. Exits can also be named anything, from "jumping out the window" to "into the fifth
|
||||
dimension". This tutorial assumes you can only move in the cardinal directions (N, E, S and W).
|
||||
2. Rooms must be connected and linked together for the map to be generated correctly. Vanilla
|
||||
Evennia comes with a admin command [tunnel](evennia.commands.default.building.CmdTunnel) that allows a
|
||||
user to create rooms in the cardinal directions, but additional work is needed to assure that rooms
|
||||
are connected. For example, if you `tunnel east` and then immediately do `tunnel west` you'll find
|
||||
that you have created two completely stand-alone rooms. So care is needed if you want to create a
|
||||
"logical" layout. In this tutorial we assume you have such a grid of rooms that we can generate the
|
||||
map from.
|
||||
|
||||
## Concept
|
||||
|
||||
Before getting into the code, it is beneficial to understand and conceptualize how this is going to
|
||||
work. The idea is analogous to a worm that starts at your current position. It chooses a direction
|
||||
and 'walks' outward from it, mapping its route as it goes. Once it has traveled a pre-set distance
|
||||
it stops and starts over in another direction. An important note is that we want a system which is
|
||||
easily callable and not too complicated. Therefore we will wrap this entire code into a custom
|
||||
Python class (not a typeclass as this doesn't use any core objects from evennia itself).
|
||||
|
||||
We are going to create something that displays like this when you type 'look':
|
||||
|
||||
```
|
||||
Hallway
|
||||
|
||||
[.] [.]
|
||||
[@][.][.][.][.]
|
||||
[.] [.] [.]
|
||||
|
||||
The distant echoes of the forgotten
|
||||
wail throughout the empty halls.
|
||||
|
||||
Exits: North, East, South
|
||||
```
|
||||
|
||||
Your current location is defined by `[@]` while the `[.]`s are other rooms that the "worm" has seen
|
||||
since departing from your location.
|
||||
|
||||
## Setting up the Map Display
|
||||
|
||||
First we must define the components for displaying the map. For the "worm" to know what symbol to
|
||||
draw on the map we will have it check an Attribute on the room it visits called `sector_type`. For
|
||||
this tutorial we understand two symbols - a normal room and the room with us in it. We also define a
|
||||
fallback symbol for rooms without said Attribute - that way the map will still work even if we
|
||||
didn't prepare the room correctly. Assuming your game folder is named `mygame`, we create this code
|
||||
in `mygame/world/map.py.`
|
||||
|
||||
```python
|
||||
# in mygame/world/map.py
|
||||
|
||||
# the symbol is identified with a key "sector_type" on the
|
||||
# Room. Keys None and "you" must always exist.
|
||||
SYMBOLS = { None : ' . ', # for rooms without sector_type Attribute
|
||||
'you' : '[@]',
|
||||
'SECT_INSIDE': '[.]' }
|
||||
```
|
||||
|
||||
Since trying to access an unset Attribute returns `None`, this means rooms without the `sector_type`
|
||||
Atttribute will show as ` . `. Next we start building the custom class `Map`. It will hold all
|
||||
methods we need.
|
||||
|
||||
```python
|
||||
# in mygame/world/map.py
|
||||
|
||||
class Map(object):
|
||||
|
||||
def __init__(self, caller, max_width=9, max_length=9):
|
||||
self.caller = caller
|
||||
self.max_width = max_width
|
||||
self.max_length = max_length
|
||||
self.worm_has_mapped = {}
|
||||
self.curX = None
|
||||
self.curY = None
|
||||
```
|
||||
|
||||
- `self.caller` is normally your Character object, the one using the map.
|
||||
- `self.max_width/length` determine the max width and length of the map that will be generated. Note
|
||||
that it's important that these variables are set to *odd* numbers to make sure the display area has
|
||||
a center point.
|
||||
- ` self.worm_has_mapped` is building off the worm analogy above. This dictionary will store all
|
||||
rooms the "worm" has mapped as well as its relative position within the grid. This is the most
|
||||
important variable as it acts as a 'checker' and 'address book' that is able to tell us where the
|
||||
worm has been and what it has mapped so far.
|
||||
- `self.curX/Y` are coordinates representing the worm's current location on the grid.
|
||||
|
||||
|
||||
Before any sort of mapping can actually be done we need to create an empty display area and do some
|
||||
sanity checks on it by using the following methods.
|
||||
|
||||
```python
|
||||
# in mygame/world/map.py
|
||||
|
||||
class Map(object):
|
||||
# [... continued]
|
||||
|
||||
def create_grid(self):
|
||||
# This method simply creates an empty grid/display area
|
||||
# with the specified variables from __init__(self):
|
||||
board = []
|
||||
for row in range(self.max_width):
|
||||
board.append([])
|
||||
for column in range(self.max_length):
|
||||
board[row].append(' ')
|
||||
return board
|
||||
|
||||
def check_grid(self):
|
||||
# this method simply checks the grid to make sure
|
||||
# that both max_l and max_w are odd numbers.
|
||||
return True if self.max_length % 2 != 0 or self.max_width % 2 != 0\
|
||||
else False
|
||||
```
|
||||
|
||||
Before we can set our worm on its way, we need to know some of the computer science behind all this
|
||||
called 'Graph Traversing'. In Pseudo code what we are trying to accomplish is this:
|
||||
|
||||
```python
|
||||
# pseudo code
|
||||
|
||||
def draw_room_on_map(room, max_distance):
|
||||
self.draw(room)
|
||||
|
||||
if max_distance == 0:
|
||||
return
|
||||
|
||||
for exit in room.exits:
|
||||
if self.has_drawn(exit.destination):
|
||||
# skip drawing if we already visited the destination
|
||||
continue
|
||||
else:
|
||||
# first time here!
|
||||
self.draw_room_on_map(exit.destination, max_distance - 1)
|
||||
```
|
||||
|
||||
The beauty of Python is that our actual code of doing this doesn't differ much if at all from this
|
||||
Pseudo code example.
|
||||
|
||||
- `max_distance` is a variable indicating to our Worm how many rooms AWAY from your current location
|
||||
will it map. Obviously the larger the number the more time it will take if your current location has
|
||||
many many rooms around you.
|
||||
|
||||
The first hurdle here is what value to use for 'max_distance'. There is no reason for the worm to
|
||||
travel further than what is actually displayed to you. For example, if your current location is
|
||||
placed in the center of a display area of size `max_length = max_width = 9`, then the worm need only
|
||||
go `4` spaces in either direction:
|
||||
|
||||
```
|
||||
[.][.][.][.][@][.][.][.][.]
|
||||
4 3 2 1 0 1 2 3 4
|
||||
```
|
||||
|
||||
The `max_distance` can be set dynamically based on the size of the display area. As your width/length
|
||||
changes it becomes a simple algebraic linear relationship which is simply `max_distance =
|
||||
(min(max_width, max_length) -1) / 2`.
|
||||
|
||||
## Building the Mapper
|
||||
|
||||
Now we can start to fill our Map object with some methods. We are still missing a few methods that
|
||||
are very important:
|
||||
|
||||
* `self.draw(self, room)` - responsible for actually drawing room to grid.
|
||||
* `self.has_drawn(self, room)` - checks to see if the room has been mapped and worm has already been
|
||||
here.
|
||||
* `self.median(self, number)` - a simple utility method that finds the median (middle point) from 0,
|
||||
n
|
||||
* `self.update_pos(self, room, exit_name)` - updates the worm's physical position by reassigning
|
||||
self.curX/Y. .accordingly
|
||||
* `self.start_loc_on_grid(self)` - the very first initial draw on the grid representing your
|
||||
location in the middle of the grid
|
||||
* `self.show_map` - after everything is done convert the map into a readable string
|
||||
* `self.draw_room_on_map(self, room, max_distance)` - the main method that ties it all together.
|
||||
|
||||
|
||||
Now that we know which methods we need, let's refine our initial `__init__(self)` to pass some
|
||||
conditional statements and set it up to start building the display.
|
||||
|
||||
|
||||
```python
|
||||
#mygame/world/map.py
|
||||
|
||||
class Map(object):
|
||||
|
||||
def __init__(self, caller, max_width=9, max_length=9):
|
||||
self.caller = caller
|
||||
self.max_width = max_width
|
||||
self.max_length = max_length
|
||||
self.worm_has_mapped = {}
|
||||
self.curX = None
|
||||
self.curY = None
|
||||
|
||||
if self.check_grid():
|
||||
# we have to store the grid into a variable
|
||||
self.grid = self.create_grid()
|
||||
# we use the algebraic relationship
|
||||
self.draw_room_on_map(caller.location,
|
||||
((min(max_width, max_length) -1 ) / 2)
|
||||
|
||||
```
|
||||
|
||||
Here we check to see if the parameters for the grid are okay, then we create an empty canvas and map
|
||||
our initial location as the first room!
|
||||
|
||||
As mentioned above, the code for the `self.draw_room_on_map()` is not much different than the Pseudo
|
||||
code. The method is shown below:
|
||||
|
||||
```python
|
||||
# in mygame/world/map.py, in the Map class
|
||||
|
||||
def draw_room_on_map(self, room, max_distance):
|
||||
self.draw(room)
|
||||
|
||||
if max_distance == 0:
|
||||
return
|
||||
|
||||
for exit in room.exits:
|
||||
if exit.name not in ("north", "east", "west", "south"):
|
||||
# we only map in the cardinal directions. Mapping up/down would be
|
||||
# an interesting learning project for someone who wanted to try it.
|
||||
continue
|
||||
if self.has_drawn(exit.destination):
|
||||
# we've been to the destination already, skip ahead.
|
||||
continue
|
||||
|
||||
self.update_pos(room, exit.name.lower())
|
||||
self.draw_room_on_map(exit.destination, max_distance - 1)
|
||||
```
|
||||
|
||||
The first thing the "worm" does is to draw your current location in `self.draw`. Lets define that...
|
||||
|
||||
```python
|
||||
#in mygame/word/map.py, in the Map class
|
||||
|
||||
def draw(self, room):
|
||||
# draw initial ch location on map first!
|
||||
if room == self.caller.location:
|
||||
self.start_loc_on_grid()
|
||||
self.worm_has_mapped[room] = [self.curX, self.curY]
|
||||
else:
|
||||
# map all other rooms
|
||||
self.worm_has_mapped[room] = [self.curX, self.curY]
|
||||
# this will use the sector_type Attribute or None if not set.
|
||||
self.grid[self.curX][self.curY] = SYMBOLS[room.db.sector_type]
|
||||
```
|
||||
|
||||
In `self.start_loc_on_grid()`:
|
||||
|
||||
```python
|
||||
def median(self, num):
|
||||
lst = sorted(range(0, num))
|
||||
n = len(lst)
|
||||
m = n -1
|
||||
return (lst[n//2] + lst[m//2]) / 2.0
|
||||
|
||||
def start_loc_on_grid(self):
|
||||
x = self.median(self.max_width)
|
||||
y = self.median(self.max_length)
|
||||
# x and y are floats by default, can't index lists with float types
|
||||
x, y = int(x), int(y)
|
||||
|
||||
self.grid[x][y] = SYMBOLS['you']
|
||||
self.curX, self.curY = x, y # updating worms current location
|
||||
```
|
||||
|
||||
After the system has drawn the current map it checks to see if the `max_distance` is `0` (since this
|
||||
is the inital start phase it is not). Now we handle the iteration once we have each individual exit
|
||||
in the room. The first thing it does is check if the room the Worm is in has been mapped already..
|
||||
lets define that...
|
||||
|
||||
|
||||
```python
|
||||
def has_drawn(self, room):
|
||||
return True if room in self.worm_has_mapped.keys() else False
|
||||
```
|
||||
|
||||
If `has_drawn` returns `False` that means the worm has found a room that hasn't been mapped yet. It
|
||||
will then 'move' there. The self.curX/Y sort of lags behind, so we have to make sure to track the
|
||||
position of the worm; we do this in `self.update_pos()` below.
|
||||
|
||||
```python
|
||||
def update_pos(self, room, exit_name):
|
||||
# this ensures the coordinates stays up to date
|
||||
# to where the worm is currently at.
|
||||
self.curX, self.curY = \
|
||||
self.worm_has_mapped[room][0], self.worm_has_mapped[room][1]
|
||||
|
||||
# now we have to actually move the pointer
|
||||
# variables depending on which 'exit' it found
|
||||
if exit_name == 'east':
|
||||
self.curY += 1
|
||||
elif exit_name == 'west':
|
||||
self.curY -= 1
|
||||
elif exit_name == 'north':
|
||||
self.curX -= 1
|
||||
elif exit_name == 'south':
|
||||
self.curX += 1
|
||||
```
|
||||
|
||||
Once the system updates the position of the worm it feeds the new room back into the original
|
||||
`draw_room_on_map()` and starts the process all over again..
|
||||
|
||||
That is essentially the entire thing. The final method is to bring it all together and make a nice
|
||||
presentational string out of it using the `self.show_map()` method.
|
||||
|
||||
```python
|
||||
def show_map(self):
|
||||
map_string = ""
|
||||
for row in self.grid:
|
||||
map_string += " ".join(row)
|
||||
map_string += "\n"
|
||||
|
||||
return map_string
|
||||
```
|
||||
|
||||
## Using the Map
|
||||
|
||||
In order for the map to get triggered we store it on the Room typeclass. If we put it in
|
||||
`return_appearance` we will get the map back every time we look at the room.
|
||||
|
||||
> `return_appearance` is a default Evennia hook available on all objects; it is called e.g. by the
|
||||
`look` command to get the description of something (the room in this case).
|
||||
|
||||
```python
|
||||
# in mygame/typeclasses/rooms.py
|
||||
|
||||
from evennia import DefaultRoom
|
||||
from world.map import Map
|
||||
|
||||
class Room(DefaultRoom):
|
||||
|
||||
def return_appearance(self, looker):
|
||||
# [...]
|
||||
string = f"{Map(looker).show_map()}\n"
|
||||
# Add all the normal stuff like room description,
|
||||
# contents, exits etc.
|
||||
string += "\n" + super().return_appearance(looker)
|
||||
return string
|
||||
```
|
||||
|
||||
Obviously this method of generating maps doesn't take into account of any doors or exits that are
|
||||
hidden.. etc.. but hopefully it serves as a good base to start with. Like previously mentioned, it
|
||||
is very important to have a solid foundation on rooms before implementing this. You can try this on
|
||||
vanilla evennia by using @tunnel and essentially you can just create a long straight/edgy non-
|
||||
looping rooms that will show on your in-game map.
|
||||
|
||||
The above example will display the map above the room description. You could also use an
|
||||
[EvTable](github:evennia.utils.evtable) to place description and map next to each other. Some other
|
||||
things you can do is to have a [Command](../Components/Commands.md) that displays with a larger radius, maybe with a
|
||||
legend and other features.
|
||||
|
||||
Below is the whole `map.py` for your reference. You need to update your `Room` typeclass (see above)
|
||||
to actually call it. Remember that to see different symbols for a location you also need to set the
|
||||
`sector_type` Attribute on the room to one of the keys in the `SYMBOLS` dictionary. So in this
|
||||
example, to make a room be mapped as `[.]` you would set the room's `sector_type` to
|
||||
`"SECT_INSIDE"`. Try it out with `@set here/sector_type = "SECT_INSIDE"`. If you wanted all new
|
||||
rooms to have a given sector symbol, you could change the default in the `SYMBOLS` dictionary below,
|
||||
or you could add the Attribute in the Room's `at_object_creation` method.
|
||||
|
||||
```python
|
||||
# mygame/world/map.py
|
||||
|
||||
# These are keys set with the Attribute sector_type on the room.
|
||||
# The keys None and "you" must always exist.
|
||||
SYMBOLS = { None : ' . ', # for rooms without a sector_type attr
|
||||
'you' : '[@]',
|
||||
'SECT_INSIDE': '[.]' }
|
||||
|
||||
class Map(object):
|
||||
|
||||
def __init__(self, caller, max_width=9, max_length=9):
|
||||
self.caller = caller
|
||||
self.max_width = max_width
|
||||
self.max_length = max_length
|
||||
self.worm_has_mapped = {}
|
||||
self.curX = None
|
||||
self.curY = None
|
||||
|
||||
if self.check_grid():
|
||||
# we actually have to store the grid into a variable
|
||||
self.grid = self.create_grid()
|
||||
self.draw_room_on_map(caller.location,
|
||||
((min(max_width, max_length) -1 ) / 2))
|
||||
|
||||
def update_pos(self, room, exit_name):
|
||||
# this ensures the pointer variables always
|
||||
# stays up to date to where the worm is currently at.
|
||||
self.curX, self.curY = \
|
||||
self.worm_has_mapped[room][0], self.worm_has_mapped[room][1]
|
||||
|
||||
# now we have to actually move the pointer
|
||||
# variables depending on which 'exit' it found
|
||||
if exit_name == 'east':
|
||||
self.curY += 1
|
||||
elif exit_name == 'west':
|
||||
self.curY -= 1
|
||||
elif exit_name == 'north':
|
||||
self.curX -= 1
|
||||
elif exit_name == 'south':
|
||||
self.curX += 1
|
||||
|
||||
def draw_room_on_map(self, room, max_distance):
|
||||
self.draw(room)
|
||||
|
||||
if max_distance == 0:
|
||||
return
|
||||
|
||||
for exit in room.exits:
|
||||
if exit.name not in ("north", "east", "west", "south"):
|
||||
# we only map in the cardinal directions. Mapping up/down would be
|
||||
# an interesting learning project for someone who wanted to try it.
|
||||
continue
|
||||
if self.has_drawn(exit.destination):
|
||||
# we've been to the destination already, skip ahead.
|
||||
continue
|
||||
|
||||
self.update_pos(room, exit.name.lower())
|
||||
self.draw_room_on_map(exit.destination, max_distance - 1)
|
||||
|
||||
def draw(self, room):
|
||||
# draw initial caller location on map first!
|
||||
if room == self.caller.location:
|
||||
self.start_loc_on_grid()
|
||||
self.worm_has_mapped[room] = [self.curX, self.curY]
|
||||
else:
|
||||
# map all other rooms
|
||||
self.worm_has_mapped[room] = [self.curX, self.curY]
|
||||
# this will use the sector_type Attribute or None if not set.
|
||||
self.grid[self.curX][self.curY] = SYMBOLS[room.db.sector_type]
|
||||
|
||||
def median(self, num):
|
||||
lst = sorted(range(0, num))
|
||||
n = len(lst)
|
||||
m = n -1
|
||||
return (lst[n//2] + lst[m//2]) / 2.0
|
||||
|
||||
def start_loc_on_grid(self):
|
||||
x = self.median(self.max_width)
|
||||
y = self.median(self.max_length)
|
||||
# x and y are floats by default, can't index lists with float types
|
||||
x, y = int(x), int(y)
|
||||
|
||||
self.grid[x][y] = SYMBOLS['you']
|
||||
self.curX, self.curY = x, y # updating worms current location
|
||||
|
||||
|
||||
def has_drawn(self, room):
|
||||
return True if room in self.worm_has_mapped.keys() else False
|
||||
|
||||
|
||||
def create_grid(self):
|
||||
# This method simply creates an empty grid
|
||||
# with the specified variables from __init__(self):
|
||||
board = []
|
||||
for row in range(self.max_width):
|
||||
board.append([])
|
||||
for column in range(self.max_length):
|
||||
board[row].append(' ')
|
||||
return board
|
||||
|
||||
def check_grid(self):
|
||||
# this method simply checks the grid to make sure
|
||||
# both max_l and max_w are odd numbers
|
||||
return True if self.max_length % 2 != 0 or \
|
||||
self.max_width % 2 != 0 else False
|
||||
|
||||
def show_map(self):
|
||||
map_string = ""
|
||||
for row in self.grid:
|
||||
map_string += " ".join(row)
|
||||
map_string += "\n"
|
||||
|
||||
return map_string
|
||||
```
|
||||
|
||||
## Final Comments
|
||||
|
||||
The Dynamic map could be expanded with further capabilities. For example, it could mark exits or
|
||||
allow NE, SE etc directions as well. It could have colors for different terrain types. One could
|
||||
also look into up/down directions and figure out how to display that in a good way.
|
||||
|
|
@ -207,7 +207,7 @@ to use for creating the object. There you go - one red button.
|
|||
|
||||
The RedButton is an example object intended to show off a few of Evennia's features. You will find
|
||||
that the [Typeclass](../../../Components/Typeclasses.md) and [Commands](../../../Components/Commands.md) controlling it are
|
||||
inside [evennia/contrib/tutorial_examples](../../../api/evennia.contrib.tutorial_examples.md)
|
||||
inside [evennia/contrib/tutorials/red_button](evennia.contrib.tutorials.red_button)
|
||||
|
||||
If you wait for a while (make sure you dropped it!) the button will blink invitingly.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
# Planning the use of some useful contribs
|
||||
|
||||
Evennia is deliberately bare-bones out of the box. The idea is that you should be as unrestricted as possible
|
||||
in designing your game. This is why you can easily replace the few defaults we have and why we don't try to
|
||||
prescribe any major game systems on you.
|
||||
in designing your game. This is why you can easily replace the few defaults we have and why we don't try to
|
||||
prescribe any major game systems on you.
|
||||
|
||||
That said, Evennia _does_ offer some more game-opinionated _optional_ stuff. These are referred to as _Contribs_
|
||||
and is an ever-growing treasure trove of code snippets, concepts and even full systems you can pick and choose
|
||||
and is an ever-growing treasure trove of code snippets, concepts and even full systems you can pick and choose
|
||||
from to use, tweak or take inspiration from when you make your game.
|
||||
|
||||
The [Contrib overview](../../../Contribs/Contrib-Overview.md) page gives the full list of the current roster of contributions. On
|
||||
|
|
@ -17,70 +17,62 @@ This is the things we know we need:
|
|||
|
||||
- A barter system
|
||||
- Character generation
|
||||
- Some concept of wearing armor
|
||||
- Some concept of wearing armor
|
||||
- The ability to roll dice
|
||||
- Rooms with awareness of day, night and season
|
||||
- Roleplaying with short-descs, poses and emotes
|
||||
- Quests
|
||||
- Quests
|
||||
- Combat (with players and against monsters)
|
||||
|
||||
## Barter contrib
|
||||
## Barter contrib
|
||||
|
||||
[source](../../../api/evennia.contrib.barter.md)
|
||||
[source](../../../api/evennia.contrib.game_systems.barter.md)
|
||||
|
||||
Reviewing this contrib suggests that it allows for safe trading between two parties. The basic principle
|
||||
is that the parties puts up the stuff they want to sell and the system will guarantee that these systems are
|
||||
exactly what is being offered. Both sides can modify their offers (bartering) until both mark themselves happy
|
||||
with the deal. Only then the deal is sealed and the objects are exchanged automatically. Interestingly, this
|
||||
works just fine for money too - just put coin objects on one side of the transaction.
|
||||
with the deal. Only then the deal is sealed and the objects are exchanged automatically. Interestingly, this
|
||||
works just fine for money too - just put coin objects on one side of the transaction.
|
||||
|
||||
Sue > trade Tom: Hi, I have a necklace to sell; wanna trade for a healing potion?
|
||||
Tom > trade Sue: Hm, I could use a necklace ...
|
||||
<both accepted trade. Start trade>
|
||||
Sue > offer necklace: This necklace is really worth it.
|
||||
<both accepted trade. Start trade>
|
||||
Sue > offer necklace: This necklace is really worth it.
|
||||
Tom > evaluate necklace:
|
||||
<Tom sees necklace stats>
|
||||
Tom > offer ration: I don't have a healing potion, but I'll trade you an iron ration!
|
||||
Sue > Hey, this is a nice necklace, I need more than a ration for it...
|
||||
Tom > offer ration, 10gold: Ok, a ration and 10 gold as well.
|
||||
Sue > accept: Ok, that sounds fair!
|
||||
Sue > accept: Ok, that sounds fair!
|
||||
Tom > accept: Good! Nice doing business with you.
|
||||
<goods change hands automatically. Trade ends>
|
||||
<goods change hands automatically. Trade ends>
|
||||
|
||||
Arguably, in a small game you are just fine to just talk to people and use `give` to do the exchange. The
|
||||
Arguably, in a small game you are just fine to just talk to people and use `give` to do the exchange. The
|
||||
barter system guarantees trading safety if you don't trust your counterpart to try to give you the wrong thing or
|
||||
to run away with your money.
|
||||
to run away with your money.
|
||||
|
||||
We will use the barter contrib as an optional feature for player-player bartering. More importantly we can
|
||||
add it for NPC shopkeepers and expand it with a little AI, which allows them to potentially trade in other
|
||||
We will use the barter contrib as an optional feature for player-player bartering. More importantly we can
|
||||
add it for NPC shopkeepers and expand it with a little AI, which allows them to potentially trade in other
|
||||
things than boring gold coin.
|
||||
|
||||
## Character generation contrib
|
||||
|
||||
[source](../../../api/evennia.contrib.chargen.md)
|
||||
|
||||
This contrib is an example module for creating characters. Since we will be using `MULTISESSION_MODE=3` we will
|
||||
get a selection screen like this automatically. We also plan to use a proper menu to build our character, so
|
||||
we will _not_ be using this contrib.
|
||||
|
||||
## Clothing contrib
|
||||
|
||||
[source](../../../api/evennia.contrib.clothing.md)
|
||||
[source](../../../api/evennia.contrib.game_systems.clothing.md)
|
||||
|
||||
This contrib provides a full system primarily aimed at wearing clothes, but it could also work for armor. You wear
|
||||
an object in a particular location and this will then be reflected in your character's description. You can
|
||||
also add roleplaying flavor:
|
||||
an object in a particular location and this will then be reflected in your character's description. You can
|
||||
also add roleplaying flavor:
|
||||
|
||||
> wear helmet slightly askew on her head
|
||||
look self
|
||||
Username is wearing a helmet slightly askew on her head.
|
||||
|
||||
|
||||
By default there are no 'body locations' in this contrib, we will need to expand on it a little to make it useful
|
||||
for things like armor. It's a good contrib to build from though, so that's what we'll do.
|
||||
for things like armor. It's a good contrib to build from though, so that's what we'll do.
|
||||
|
||||
## Dice contrib
|
||||
## Dice contrib
|
||||
|
||||
[source](../../../api/evennia.contrib.dice.md)
|
||||
[source](../../../api/evennia.contrib.rpg.dice.md)
|
||||
|
||||
The dice contrib presents a general dice roller to use in game.
|
||||
|
||||
|
|
@ -92,51 +84,51 @@ The dice contrib presents a general dice roller to use in game.
|
|||
Roll(s): 7. Total result is 7. This is a failure (by 5)
|
||||
> roll/hidden 1d20 > 12
|
||||
Roll(s): 18. Total result is 17. This is a success (by 6). (not echoed)
|
||||
|
||||
The contrib also has a python function for producing these results in-code. However, while
|
||||
we will emulate rolls for our rule system, we'll do this as simply as possible with Python's `random`
|
||||
module.
|
||||
|
||||
So while this contrib is fun to have around for GMs or for players who want to get a random result
|
||||
The contrib also has a python function for producing these results in-code. However, while
|
||||
we will emulate rolls for our rule system, we'll do this as simply as possible with Python's `random`
|
||||
module.
|
||||
|
||||
So while this contrib is fun to have around for GMs or for players who want to get a random result
|
||||
or play a game, we will not need it for the core of our game.
|
||||
|
||||
## Extended room contrib
|
||||
## Extended room contrib
|
||||
|
||||
[source](../../../api/evennia.contrib.extended_room.md)
|
||||
[source](../../../api/evennia.contrib.grid.extended_room.md)
|
||||
|
||||
This is a custom Room typeclass that changes its description based on time of day and season.
|
||||
|
||||
For example, at night, in wintertime you could show the room as being dark and frost-covered while in daylight
|
||||
at summer it could describe a flowering meadow. The description can also contain special markers, so
|
||||
`<morning> ... </morning>` would include text only visible at morning.
|
||||
at summer it could describe a flowering meadow. The description can also contain special markers, so
|
||||
`<morning> ... </morning>` would include text only visible at morning.
|
||||
|
||||
The extended room also supports _details_, which are things to "look at" in the room without there having
|
||||
to be a separate database object created for it. For example, a player in a church may do `look window` and
|
||||
get a description of the windows without there needing to be an actual `window` object in the room.
|
||||
|
||||
Adding all those extra descriptions can be a lot of work, so they are optional; if not given the room works
|
||||
like a normal room.
|
||||
like a normal room.
|
||||
|
||||
The contrib is simple to add and provides a lot of optional flexibility, so we'll add it to our
|
||||
game, why not!
|
||||
The contrib is simple to add and provides a lot of optional flexibility, so we'll add it to our
|
||||
game, why not!
|
||||
|
||||
## RP-System contrib
|
||||
|
||||
[source](../../../api/evennia.contrib.rpsystem.md)
|
||||
[source](../../../api/evennia.contrib.rpg.rpsystem.md)
|
||||
|
||||
This contrib adds a full roleplaying subsystem to your game. It gives every character a "short-description"
|
||||
(sdesc) that is what people will see when first meeting them. Let's say Tom has an sdesc "A tall man" and
|
||||
Sue has the sdesc "A muscular, blonde woman"
|
||||
|
||||
Tom > look
|
||||
Tom > look
|
||||
Tom: <room desc> ... You see: A muscular, blonde woman
|
||||
Tom > emote /me smiles to /muscular.
|
||||
Tom: Tom smiles to A muscular, blonde woman.
|
||||
Sue: A tall man smiles to Sue.
|
||||
Sue: A tall man smiles to Sue.
|
||||
Tom > emote Leaning forward, /me says, "Well hello, what's yer name?"
|
||||
Tom: Leaning forward, Tom says, "Well hello..."
|
||||
Sue: Leaning forward, A tall man says, "Well hello, what's yer name?"
|
||||
Sue > emote /me grins. "I'm Angelica", she says.
|
||||
Sue > emote /me grins. "I'm Angelica", she says.
|
||||
Sue: Sue grins. "I'm Angelica", she says.
|
||||
Tom: A muscular, blonde woman grins. "I'm Angelica", she says.
|
||||
Tom > recog muscular Angelica
|
||||
|
|
@ -145,42 +137,42 @@ Sue has the sdesc "A muscular, blonde woman"
|
|||
Sue: A tall man nods to Sue: "I have a message for you ..."
|
||||
|
||||
Above, Sue introduces herself as "Angelica" and Tom uses this info to `recoc` her as "Angelica" hereafter. He
|
||||
could have `recoc`-ed her with whatever name he liked - it's only for his own benefit. There is no separate
|
||||
`say`, the spoken words are embedded in the emotes in quotes `"..."`.
|
||||
could have `recoc`-ed her with whatever name he liked - it's only for his own benefit. There is no separate
|
||||
`say`, the spoken words are embedded in the emotes in quotes `"..."`.
|
||||
|
||||
The RPSystem module also includes options for `poses`, which help to establish your position in the room
|
||||
when others look at you.
|
||||
when others look at you.
|
||||
|
||||
Tom > pose stands by the bar, looking bored.
|
||||
Sue > look
|
||||
Sue: <room desc> ... A tall man stands by the bar, looking bored.
|
||||
|
||||
You can also wear a mask to hide your identity; your sdesc will then be changed to the sdesc of the mask,
|
||||
like `a person with a mask`.
|
||||
|
||||
The RPSystem gives a lot of roleplaying power out of the box, so we will add it. There is also a separate
|
||||
[rplanguage](../../../api/evennia.contrib.rplanguage.md) module that integrates with the spoken words in your emotes and garbles them if you don't understand
|
||||
You can also wear a mask to hide your identity; your sdesc will then be changed to the sdesc of the mask,
|
||||
like `a person with a mask`.
|
||||
|
||||
The RPSystem gives a lot of roleplaying power out of the box, so we will add it. There is also a separate
|
||||
[rplanguage](../../../api/evennia.contrib.rpg.rpsystem.md) module that integrates with the spoken words in your emotes and garbles them if you don't understand
|
||||
the language spoken. In order to restrict the scope we will not include languages for the tutorial game.
|
||||
|
||||
|
||||
## Talking NPC contrib
|
||||
|
||||
[source](../../../api/evennia.contrib.talking_npc.md)
|
||||
[source](../../../api/evennia.contrib.tutorials.talking_npc.md)
|
||||
|
||||
This exemplifies an NPC with a menu-driven dialogue tree. We will not use this contrib explicitly, but it's
|
||||
good as inspiration for how we'll do quest-givers later.
|
||||
This exemplifies an NPC with a menu-driven dialogue tree. We will not use this contrib explicitly, but it's
|
||||
good as inspiration for how we'll do quest-givers later.
|
||||
|
||||
## Traits contrib
|
||||
|
||||
[source](../../../api/evennia.contrib.traits.md)
|
||||
[source](../../../api/evennia.contrib.rpg.traits.md)
|
||||
|
||||
An issue with dealing with roleplaying attributes like strength, dexterity, or skills like hunting, sword etc
|
||||
is how to keep track of the values in the moment. Your strength may temporarily be buffed by a strength-potion.
|
||||
is how to keep track of the values in the moment. Your strength may temporarily be buffed by a strength-potion.
|
||||
Your swordmanship may be worse because you are encumbered. And when you drink your health potion you must make
|
||||
sure that those +20 health does not bring your health higher than its maximum. All this adds complexity.
|
||||
|
||||
The _Traits_ contrib consists of several types of objects to help track and manage values like this. When
|
||||
installed, the traits are accessed on a new handler `.traits`, for example
|
||||
The _Traits_ contrib consists of several types of objects to help track and manage values like this. When
|
||||
installed, the traits are accessed on a new handler `.traits`, for example
|
||||
|
||||
> py self.traits.hp.value
|
||||
100
|
||||
|
|
@ -190,9 +182,9 @@ installed, the traits are accessed on a new handler `.traits`, for example
|
|||
> py self.traits.hp.reset() # drink a potion
|
||||
> py self.traits.hp.value
|
||||
100
|
||||
|
||||
A Trait is persistent (it uses an Attribute under the hood) and tracks changes, min/max and other things
|
||||
automatically. They can also be added together in various mathematical operations.
|
||||
|
||||
A Trait is persistent (it uses an Attribute under the hood) and tracks changes, min/max and other things
|
||||
automatically. They can also be added together in various mathematical operations.
|
||||
|
||||
The contrib introduces three main Trait-classes
|
||||
|
||||
|
|
@ -200,14 +192,14 @@ The contrib introduces three main Trait-classes
|
|||
- _Counters_ is a value that never moves outside a given range, even with modifiers. For example a skill
|
||||
that can at most get a maximum amount of buff. Counters can also easily be _timed_ so that they decrease
|
||||
or increase with a certain rate per second. This could be good for a time-limited curse for example.
|
||||
- _Gauge_ is like a fuel-gauge; it starts at a max value and then empties gradually. This is perfect for
|
||||
- _Gauge_ is like a fuel-gauge; it starts at a max value and then empties gradually. This is perfect for
|
||||
things like health, stamina and the like. Gauges can also change with a rate, which works well for the
|
||||
effects of slow poisons and healing both.
|
||||
|
||||
```
|
||||
> py self.traits.hp.value
|
||||
100
|
||||
> py self.traits.hp.rate = -1 # poisoned!
|
||||
> py self.traits.hp.rate = -1 # poisoned!
|
||||
> py self.traits.hp.ratetarget = 50 # stop at 50 hp
|
||||
# Wait 30s
|
||||
> py self.traits.hp.value
|
||||
|
|
@ -217,36 +209,36 @@ effects of slow poisons and healing both.
|
|||
50 # stopped at 50
|
||||
> py self.traits.hp.rate = 0 # no more poison
|
||||
> py self.traits.hp.rate = 5 # healing magic!
|
||||
# wait 5s
|
||||
# wait 5s
|
||||
> pyself.traits.hp.value
|
||||
75
|
||||
```
|
||||
|
||||
Traits will be very practical to use for our character sheets.
|
||||
|
||||
Traits will be very practical to use for our character sheets.
|
||||
|
||||
## Turnbattle contrib
|
||||
|
||||
[source](../../../api/evennia.contrib.turnbattle.md)
|
||||
[source](../../../api/evennia.contrib.game_systems.turnbattle.md)
|
||||
|
||||
This contrib consists of several implementations of a turn-based combat system, divivided into complexity:
|
||||
|
||||
- basic - initiative and turn order, attacks against defense values, damage.
|
||||
- equip - considers weapons and armor, wielding and weapon accuracy.
|
||||
- items - adds usable items with conditions and status effects
|
||||
- magic - adds spellcasting system using MP.
|
||||
- magic - adds spellcasting system using MP.
|
||||
- range - adds abstract positioning and 1D movement to differentiate between melee and ranged attacks.
|
||||
|
||||
The turnbattle system is comprehensive, but it's meant as a base to start from rather than offer
|
||||
The turnbattle system is comprehensive, but it's meant as a base to start from rather than offer
|
||||
a complete system. It's also not built with _Traits_ in mind, so we will need to adjust it for that.
|
||||
|
||||
## Conclusions
|
||||
|
||||
With some contribs selected, we have pieces to build from and don't have to write everything from scratch.
|
||||
We will need Quests and will likely need to do a bunch of work on Combat to adapt the combat contrib
|
||||
We will need Quests and will likely need to do a bunch of work on Combat to adapt the combat contrib
|
||||
to our needs.
|
||||
|
||||
We will now move into actually starting to implement our tutorial game
|
||||
in the next part of this tutorial series. When doing this for yourself, remember to refer
|
||||
back to your planning and adjust it as you learn what works and what does not.
|
||||
We will now move into actually starting to implement our tutorial game
|
||||
in the next part of this tutorial series. When doing this for yourself, remember to refer
|
||||
back to your planning and adjust it as you learn what works and what does not.
|
||||
|
||||
|
||||
|
|
|
|||
416
docs/source/Howto/Static-In-Game-Map.md
Normal file
416
docs/source/Howto/Static-In-Game-Map.md
Normal file
|
|
@ -0,0 +1,416 @@
|
|||
# Static In Game Map
|
||||
|
||||
|
||||
## Introduction
|
||||
|
||||
This tutorial describes the creation of an in-game map display based on a pre-drawn map. It also
|
||||
details how to use the [Batch code processor](../Components/Batch-Code-Processor.md) for advanced building. There is
|
||||
also the [Dynamic in-game map tutorial](./Dynamic-In-Game-Map.md) that works in the opposite direction,
|
||||
by generating a map from an existing grid of rooms.
|
||||
|
||||
Evennia does not require its rooms to be positioned in a "logical" way. Your exits could be named
|
||||
anything. You could make an exit "west" that leads to a room described to be in the far north. You
|
||||
could have rooms inside one another, exits leading back to the same room or describing spatial
|
||||
geometries impossible in the real world.
|
||||
|
||||
That said, most games *do* organize their rooms in a logical fashion, if nothing else to retain the
|
||||
sanity of their players. And when they do, the game becomes possible to map. This tutorial will give
|
||||
an example of a simple but flexible in-game map system to further help player's to navigate. We will
|
||||
|
||||
To simplify development and error-checking we'll break down the work into bite-size chunks, each
|
||||
building on what came before. For this we'll make extensive use of the [Batch code processor](Batch-
|
||||
Code-Processor), so you may want to familiarize yourself with that.
|
||||
|
||||
1. **Planning the map** - Here we'll come up with a small example map to use for the rest of the
|
||||
tutorial.
|
||||
2. **Making a map object** - This will showcase how to make a static in-game "map" object a
|
||||
Character could pick up and look at.
|
||||
3. **Building the map areas** - Here we'll actually create the small example area according to the
|
||||
map we designed before.
|
||||
4. **Map code** - This will link the map to the location so our output looks something like this:
|
||||
|
||||
```
|
||||
crossroads(#3)
|
||||
↑╚∞╝↑
|
||||
≈↑│↑∩ The merger of two roads. To the north looms a mighty castle.
|
||||
O─O─O To the south, the glow of a campfire can be seen. To the east lie
|
||||
≈↑│↑∩ the vast mountains and to the west is heard the waves of the sea.
|
||||
↑▲O▲↑
|
||||
|
||||
Exits: north(#8), east(#9), south(#10), west(#11)
|
||||
```
|
||||
|
||||
We will henceforth assume your game folder is name named `mygame` and that you haven't modified the
|
||||
default commands. We will also not be using [Colors](../Concepts/Colors.md) for our map since they
|
||||
don't show in the documentation wiki.
|
||||
|
||||
## Planning the Map
|
||||
|
||||
Let's begin with the fun part! Maps in MUDs come in many different [shapes and
|
||||
sizes](http://journal.imaginary-realities.com/volume-05/issue-01/modern-interface-modern-
|
||||
mud/index.html). Some appear as just boxes connected by lines. Others have complex graphics that are
|
||||
external to the game itself.
|
||||
|
||||
Our map will be in-game text but that doesn't mean we're restricted to the normal alphabet! If
|
||||
you've ever selected the [Wingdings font](https://en.wikipedia.org/wiki/Wingdings) in Microsoft Word
|
||||
you will know there are a multitude of other characters around to use. When creating your game with
|
||||
Evennia you have access to the [UTF-8 character encoding](https://en.wikipedia.org/wiki/UTF-8) which
|
||||
put at your disposal [thousands of letters, number and geometric shapes](https://mcdlr.com/utf-8/#1).
|
||||
|
||||
For this exercise, we've copy-and-pasted from the pallet of special characters used over at
|
||||
[Dwarf Fortress](https://dwarffortresswiki.org/index.php/Character_table) to create what is hopefully
|
||||
a pleasing and easy to understood landscape:
|
||||
|
||||
```
|
||||
≈≈↑↑↑↑↑∩∩
|
||||
≈≈↑╔═╗↑∩∩ Places the account can visit are indicated by "O".
|
||||
≈≈↑║O║↑∩∩ Up the top is a castle visitable by the account.
|
||||
≈≈↑╚∞╝↑∩∩ To the right is a cottage and to the left the beach.
|
||||
≈≈≈↑│↑∩∩∩ And down the bottom is a camp site with tents.
|
||||
≈≈O─O─O⌂∩ In the center is the starting location, a crossroads
|
||||
≈≈≈↑│↑∩∩∩ which connect the four other areas.
|
||||
≈≈↑▲O▲↑∩∩
|
||||
≈≈↑↑▲↑↑∩∩
|
||||
≈≈↑↑↑↑↑∩∩
|
||||
```
|
||||
There are many considerations when making a game map depending on the play style and requirements
|
||||
you intend to implement. Here we will display a 5x5 character map of the area surrounding the
|
||||
account. This means making sure to account for 2 characters around every visitable location. Good
|
||||
planning at this stage can solve many problems before they happen.
|
||||
|
||||
## Creating a Map Object
|
||||
|
||||
In this section we will try to create an actual "map" object that an account can pick up and look
|
||||
at.
|
||||
|
||||
Evennia offers a range of [default commands](../Components/Default-Commands.md) for
|
||||
[creating objects and rooms in-game](Starting/Part1/Building-Quickstart.md). While readily accessible, these commands are made to do very
|
||||
specific, restricted things and will thus not offer as much flexibility to experiment (for an
|
||||
advanced exception see [the FuncParser](../Components/FuncParser.md)). Additionally, entering long
|
||||
descriptions and properties over and over in the game client can become tedious; especially when
|
||||
testing and you may want to delete and recreate things over and over.
|
||||
|
||||
To overcome this, Evennia offers [batch processors](../Components/Batch-Processors.md) that work as input-files
|
||||
created out-of-game. In this tutorial we'll be using the more powerful of the two available batch
|
||||
processors, the [Batch Code Processor ](../Components/Batch-Code-Processor.md), called with the `@batchcode` command.
|
||||
This is a very powerful tool. It allows you to craft Python files to act as blueprints of your
|
||||
entire game world. These files have access to use Evennia's Python API directly. Batchcode allows
|
||||
for easy editing and creation in whatever text editor you prefer, avoiding having to manually build
|
||||
the world line-by-line inside the game.
|
||||
|
||||
> Important warning: `@batchcode`'s power is only rivaled by the `@py` command. Batchcode is so
|
||||
powerful it should be reserved only for the [superuser](../Concepts/Building-Permissions.md). Think carefully
|
||||
before you let others (such as `Developer`- level staff) run `@batchcode` on their own - make sure
|
||||
you are okay with them running *arbitrary Python code* on your server.
|
||||
|
||||
While a simple example, the map object it serves as good way to try out `@batchcode`. Go to
|
||||
`mygame/world` and create a new file there named `batchcode_map.py`:
|
||||
|
||||
```Python
|
||||
# mygame/world/batchcode_map.py
|
||||
|
||||
from evennia import create_object
|
||||
from evennia import DefaultObject
|
||||
|
||||
# We use the create_object function to call into existence a
|
||||
# DefaultObject named "Map" wherever you are standing.
|
||||
|
||||
map = create_object(DefaultObject, key="Map", location=caller.location)
|
||||
|
||||
# We then access its description directly to make it our map.
|
||||
|
||||
map.db.desc = """
|
||||
≈≈↑↑↑↑↑∩∩
|
||||
≈≈↑╔═╗↑∩∩
|
||||
≈≈↑║O║↑∩∩
|
||||
≈≈↑╚∞╝↑∩∩
|
||||
≈≈≈↑│↑∩∩∩
|
||||
≈≈O─O─O⌂∩
|
||||
≈≈≈↑│↑∩∩∩
|
||||
≈≈↑▲O▲↑∩∩
|
||||
≈≈↑↑▲↑↑∩∩
|
||||
≈≈↑↑↑↑↑∩∩
|
||||
"""
|
||||
|
||||
# This message lets us know our map was created successfully.
|
||||
caller.msg("A map appears out of thin air and falls to the ground.")
|
||||
```
|
||||
|
||||
Log into your game project as the superuser and run the command
|
||||
|
||||
```
|
||||
@batchcode batchcode_map
|
||||
```
|
||||
|
||||
This will load your `batchcode_map.py` file and execute the code (Evennia will look in your `world/`
|
||||
folder automatically so you don't need to specify it).
|
||||
|
||||
A new map object should have appeared on the ground. You can view the map by using `look map`. Let's
|
||||
take it with the `get map` command. We'll need it in case we get lost!
|
||||
|
||||
## Building the map areas
|
||||
|
||||
We've just used batchcode to create an object useful for our adventures. But the locations on that
|
||||
map does not actually exist yet - we're all mapped up with nowhere to go! Let's use batchcode to
|
||||
build a game area based on our map. We have five areas outlined: a castle, a cottage, a campsite, a
|
||||
coastal beach and the crossroads which connects them. Create a new batchcode file for this in
|
||||
`mygame/world`, named `batchcode_world.py`.
|
||||
|
||||
```Python
|
||||
# mygame/world/batchcode_world.py
|
||||
|
||||
from evennia import create_object, search_object
|
||||
from typeclasses import rooms, exits
|
||||
|
||||
# We begin by creating our rooms so we can detail them later.
|
||||
|
||||
centre = create_object(rooms.Room, key="crossroads")
|
||||
north = create_object(rooms.Room, key="castle")
|
||||
east = create_object(rooms.Room, key="cottage")
|
||||
south = create_object(rooms.Room, key="camp")
|
||||
west = create_object(rooms.Room, key="coast")
|
||||
|
||||
# This is where we set up the cross roads.
|
||||
# The rooms description is what we see with the 'look' command.
|
||||
|
||||
centre.db.desc = """
|
||||
The merger of two roads. A single lamp post dimly illuminates the lonely crossroads.
|
||||
To the north looms a mighty castle. To the south the glow of a campfire can be seen.
|
||||
To the east lie a wall of mountains and to the west the dull roar of the open sea.
|
||||
"""
|
||||
|
||||
# Here we are creating exits from the centre "crossroads" location to
|
||||
# destinations to the north, east, south, and west. We will be able
|
||||
# to use the exit by typing it's key e.g. "north" or an alias e.g. "n".
|
||||
|
||||
centre_north = create_object(exits.Exit, key="north",
|
||||
aliases=["n"], location=centre, destination=north)
|
||||
centre_east = create_object(exits.Exit, key="east",
|
||||
aliases=["e"], location=centre, destination=east)
|
||||
centre_south = create_object(exits.Exit, key="south",
|
||||
aliases=["s"], location=centre, destination=south)
|
||||
centre_west = create_object(exits.Exit, key="west",
|
||||
aliases=["w"], location=centre, destination=west)
|
||||
|
||||
# Now we repeat this for the other rooms we'll be implementing.
|
||||
# This is where we set up the northern castle.
|
||||
|
||||
north.db.desc = "An impressive castle surrounds you. " \
|
||||
"There might be a princess in one of these towers."
|
||||
north_south = create_object(exits.Exit, key="south",
|
||||
aliases=["s"], location=north, destination=centre)
|
||||
|
||||
# This is where we set up the eastern cottage.
|
||||
|
||||
east.db.desc = "A cosy cottage nestled among mountains " \
|
||||
"stretching east as far as the eye can see."
|
||||
east_west = create_object(exits.Exit, key="west",
|
||||
aliases=["w"], location=east, destination=centre)
|
||||
|
||||
# This is where we set up the southern camp.
|
||||
|
||||
south.db.desc = "Surrounding a clearing are a number of " \
|
||||
"tribal tents and at their centre a roaring fire."
|
||||
south_north = create_object(exits.Exit, key="north",
|
||||
aliases=["n"], location=south, destination=centre)
|
||||
|
||||
# This is where we set up the western coast.
|
||||
|
||||
west.db.desc = "The dark forest halts to a sandy beach. " \
|
||||
"The sound of crashing waves calms the soul."
|
||||
west_east = create_object(exits.Exit, key="east",
|
||||
aliases=["e"], location=west, destination=centre)
|
||||
|
||||
# Lastly, lets make an entrance to our world from the default Limbo room.
|
||||
|
||||
limbo = search_object('Limbo')[0]
|
||||
limbo_exit = create_object(exits.Exit, key="enter world",
|
||||
aliases=["enter"], location=limbo, destination=centre)
|
||||
|
||||
```
|
||||
|
||||
Apply this new batch code with `@batchcode batchcode_world`. If there are no errors in the code we
|
||||
now have a nice mini-world to explore. Remember that if you get lost you can look at the map we
|
||||
created!
|
||||
|
||||
## In-game minimap
|
||||
|
||||
Now we have a landscape and matching map, but what we really want is a mini-map that displays
|
||||
whenever we move to a room or use the `look` command.
|
||||
|
||||
We *could* manually enter a part of the map into the description of every room like we did our map
|
||||
object description. But some MUDs have tens of thousands of rooms! Besides, if we ever changed our
|
||||
map we would have to potentially alter a lot of those room descriptions manually to match the
|
||||
change. So instead we will make one central module to hold our map. Rooms will reference this
|
||||
central location on creation and the map changes will thus come into effect when next running our
|
||||
batchcode.
|
||||
|
||||
To make our mini-map we need to be able to cut our full map into parts. To do this we need to put it
|
||||
in a format which allows us to do that easily. Luckily, python allows us to treat strings as lists
|
||||
of characters allowing us to pick out the characters we need.
|
||||
|
||||
`mygame/world/map_module.py`
|
||||
```Python
|
||||
# We place our map into a sting here.
|
||||
world_map = """\
|
||||
≈≈↑↑↑↑↑∩∩
|
||||
≈≈↑╔═╗↑∩∩
|
||||
≈≈↑║O║↑∩∩
|
||||
≈≈↑╚∞╝↑∩∩
|
||||
≈≈≈↑│↑∩∩∩
|
||||
≈≈O─O─O⌂∩
|
||||
≈≈≈↑│↑∩∩∩
|
||||
≈≈↑▲O▲↑∩∩
|
||||
≈≈↑↑▲↑↑∩∩
|
||||
≈≈↑↑↑↑↑∩∩
|
||||
"""
|
||||
|
||||
# This turns our map string into a list of rows. Because python
|
||||
# allows us to treat strings as a list of characters, we can access
|
||||
# those characters with world_map[5][5] where world_map[row][column].
|
||||
world_map = world_map.split('\n')
|
||||
|
||||
def return_map():
|
||||
"""
|
||||
This function returns the whole map
|
||||
"""
|
||||
map = ""
|
||||
|
||||
#For each row in our map, add it to map
|
||||
for valuey in world_map:
|
||||
map += valuey
|
||||
map += "\n"
|
||||
|
||||
return map
|
||||
|
||||
def return_minimap(x, y, radius = 2):
|
||||
"""
|
||||
This function returns only part of the map.
|
||||
Returning all chars in a 2 char radius from (x,y)
|
||||
"""
|
||||
map = ""
|
||||
|
||||
#For each row we need, add the characters we need.
|
||||
for valuey in world_map[y-radius:y+radius+1]: for valuex in valuey[x-radius:x+radius+1]:
|
||||
map += valuex
|
||||
map += "\n"
|
||||
|
||||
return map
|
||||
```
|
||||
|
||||
With our map_module set up, let's replace our hardcoded map in `mygame/world/batchcode_map.py` with
|
||||
a reference to our map module. Make sure to import our map_module!
|
||||
|
||||
```python
|
||||
# mygame/world/batchcode_map.py
|
||||
|
||||
from evennia import create_object
|
||||
from evennia import DefaultObject
|
||||
from world import map_module
|
||||
|
||||
map = create_object(DefaultObject, key="Map", location=caller.location)
|
||||
|
||||
map.db.desc = map_module.return_map()
|
||||
|
||||
caller.msg("A map appears out of thin air and falls to the ground.")
|
||||
```
|
||||
|
||||
Log into Evennia as the superuser and run this batchcode. If everything worked our new map should
|
||||
look exactly the same as the old map - you can use `@delete` to delete the old one (use a number to
|
||||
pick which to delete).
|
||||
|
||||
Now, lets turn our attention towards our game's rooms. Let's use the `return_minimap` method we
|
||||
created above in order to include a minimap in our room descriptions. This is a little more
|
||||
complicated.
|
||||
|
||||
By itself we would have to settle for either the map being *above* the description with
|
||||
`room.db.desc = map_string + description_string`, or the map going *below* by reversing their order.
|
||||
Both options are rather unsatisfactory - we would like to have the map next to the text! For this
|
||||
solution we'll explore the utilities that ship with Evennia. Tucked away in `evennia\evennia\utils`
|
||||
is a little module called [EvTable](github:evennia.utils.evtable) . This is an advanced ASCII table
|
||||
creator for you to utilize in your game. We'll use it by creating a basic table with 1 row and two
|
||||
columns (one for our map and one for our text) whilst also hiding the borders. Open the batchfile
|
||||
again
|
||||
|
||||
```python
|
||||
# mygame\world\batchcode_world.py
|
||||
|
||||
# Add to imports
|
||||
from evennia.utils import evtable
|
||||
from world import map_module
|
||||
|
||||
# [...]
|
||||
|
||||
# Replace the descriptions with the below code.
|
||||
|
||||
# The cross roads.
|
||||
# We pass what we want in our table and EvTable does the rest.
|
||||
# Passing two arguments will create two columns but we could add more.
|
||||
# We also specify no border.
|
||||
centre.db.desc = evtable.EvTable(map_module.return_minimap(4,5),
|
||||
"The merger of two roads. A single lamp post dimly " \
|
||||
"illuminates the lonely crossroads. To the north " \
|
||||
"looms a mighty castle. To the south the glow of " \
|
||||
"a campfire can be seen. To the east lie a wall of " \
|
||||
"mountains and to the west the dull roar of the open sea.",
|
||||
border=None)
|
||||
# EvTable allows formatting individual columns and cells. We use that here
|
||||
# to set a maximum width for our description, but letting the map fill
|
||||
# whatever space it needs.
|
||||
centre.db.desc.reformat_column(1, width=70)
|
||||
|
||||
# [...]
|
||||
|
||||
# The northern castle.
|
||||
north.db.desc = evtable.EvTable(map_module.return_minimap(4,2),
|
||||
"An impressive castle surrounds you. There might be " \
|
||||
"a princess in one of these towers.",
|
||||
border=None)
|
||||
north.db.desc.reformat_column(1, width=70)
|
||||
|
||||
# [...]
|
||||
|
||||
# The eastern cottage.
|
||||
east.db.desc = evtable.EvTable(map_module.return_minimap(6,5),
|
||||
"A cosy cottage nestled among mountains stretching " \
|
||||
"east as far as the eye can see.",
|
||||
border=None)
|
||||
east.db.desc.reformat_column(1, width=70)
|
||||
|
||||
# [...]
|
||||
|
||||
# The southern camp.
|
||||
south.db.desc = evtable.EvTable(map_module.return_minimap(4,7),
|
||||
"Surrounding a clearing are a number of tribal tents " \
|
||||
"and at their centre a roaring fire.",
|
||||
border=None)
|
||||
south.db.desc.reformat_column(1, width=70)
|
||||
|
||||
# [...]
|
||||
|
||||
# The western coast.
|
||||
west.db.desc = evtable.EvTable(map_module.return_minimap(2,5),
|
||||
"The dark forest halts to a sandy beach. The sound of " \
|
||||
"crashing waves calms the soul.",
|
||||
border=None)
|
||||
west.db.desc.reformat_column(1, width=70)
|
||||
```
|
||||
|
||||
Before we run our new batchcode, if you are anything like me you would have something like 100 maps
|
||||
lying around and 3-4 different versions of our rooms extending from limbo. Let's wipe it all and
|
||||
start with a clean slate. In Command Prompt you can run `evennia flush` to clear the database and
|
||||
start anew. It won't reset dbref values however, so if you are at #100 it will start from there.
|
||||
Alternatively you can navigate to `mygame/server` and delete the `evennia.db3` file. Now in Command
|
||||
Prompt use `evennia migrate` to have a completely freshly made database.
|
||||
|
||||
Log in to evennia and run `@batchcode batchcode_world` and you'll have a little world to explore.
|
||||
|
||||
## Conclusions
|
||||
|
||||
You should now have a mapped little world and a basic understanding of batchcode, EvTable and how
|
||||
easily new game defining features can be added to Evennia.
|
||||
|
||||
You can easily build from this tutorial by expanding the map and creating more rooms to explore. Why
|
||||
not add more features to your game by trying other tutorials: [Add weather to your world](Weather-
|
||||
Tutorial), [fill your world with NPC's](./Tutorial-Aggressive-NPCs.md) or
|
||||
[implement a combat system](Starting/Part3/Turn-based-Combat-System.md).
|
||||
Loading…
Add table
Add a link
Reference in a new issue