Clean up contrib docs, autogeneration

This commit is contained in:
Griatch 2022-01-08 14:40:58 +01:00
parent b922cf9b3c
commit e96bbb4b86
94 changed files with 4126 additions and 2536 deletions

View 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.

View file

@ -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.

View file

@ -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.

View 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).