mirror of
https://github.com/evennia/evennia.git
synced 2026-03-16 21:06:30 +01:00
Reorganize docs into flat folder layout
This commit is contained in:
parent
106558cec0
commit
892d8efb93
135 changed files with 34 additions and 1180 deletions
436
docs/source/Contrib/A-voice-operated-elevator-using-events.md
Normal file
436
docs/source/Contrib/A-voice-operated-elevator-using-events.md
Normal file
|
|
@ -0,0 +1,436 @@
|
|||
# A voice operated elevator using events
|
||||
|
||||
|
||||
- Previous tutorial: [Adding dialogues in events](Dialogues-in-events)
|
||||
|
||||
This tutorial will walk you through the steps to create a voice-operated elevator, using the [in-
|
||||
game Python
|
||||
system](https://github.com/evennia/evennia/blob/master/evennia/contrib/ingame_python/README.md).
|
||||
This tutorial assumes the in-game Python system is installed in your game. If it isn't, you can
|
||||
follow the installation steps given in [the documentation on in-game
|
||||
Python](https://github.com/evennia/evennia/blob/master/evennia/contrib/ingame_python/README.md), and
|
||||
come back on this tutorial once the system is installed. **You do not need to read** the entire
|
||||
documentation, it's a good reference, but not the easiest way to learn about it. Hence these
|
||||
tutorials.
|
||||
|
||||
The in-game Python system allows to run code on individual objects in some situations. You don't
|
||||
have to modify the source code to add these features, past the installation. The entire system
|
||||
makes it easy to add specific features to some objects, but not all.
|
||||
|
||||
> What will we try to do?
|
||||
|
||||
In this tutorial, we are going to create a simple voice-operated elevator. In terms of features, we
|
||||
will:
|
||||
|
||||
- Explore events with parameters.
|
||||
- Work on more interesting callbacks.
|
||||
- Learn about chained events.
|
||||
- Play with variable modification in callbacks.
|
||||
|
||||
## Our study case
|
||||
|
||||
Let's summarize what we want to achieve first. We would like to create a room that will represent
|
||||
the inside of our elevator. In this room, a character could just say "1", "2" or "3", and the
|
||||
elevator will start moving. The doors will close and open on the new floor (the exits leading in
|
||||
and out of the elevator will be modified).
|
||||
|
||||
We will work on basic features first, and then will adjust some, showing you how easy and powerfully
|
||||
independent actions can be configured through the in-game Python system.
|
||||
|
||||
## Creating the rooms and exits we need
|
||||
|
||||
We'll create an elevator right in our room (generally called "Limbo", of ID 2). You could easily
|
||||
adapt the following instructions if you already have some rooms and exits, of course, just remember
|
||||
to check the IDs.
|
||||
|
||||
> Note: the in-game Python system uses IDs for a lot of things. While it is not mandatory, it is
|
||||
good practice to know the IDs you have for your callbacks, because it will make manipulation much
|
||||
quicker. There are other ways to identify objects, but as they depend on many factors, IDs are
|
||||
usually the safest path in our callbacks.
|
||||
|
||||
Let's go into limbo (`#2`) to add our elevator. We'll add it to the north. To create this room,
|
||||
in-game you could type:
|
||||
|
||||
tunnel n = Inside of an elevator
|
||||
|
||||
The game should respond by telling you:
|
||||
|
||||
Created room Inside of an elevator(#3) of type typeclasses.rooms.Room.
|
||||
Created Exit from Limbo to Inside of an elevator: north(#4) (n).
|
||||
Created Exit back from Inside of an elevator to Limbo: south(#5) (s).
|
||||
|
||||
Note the given IDs:
|
||||
|
||||
- `#2` is limbo, the first room the system created.
|
||||
- `#3` is our room inside of an elevator.
|
||||
- `#4` is the north exit from Limbo to our elevator.
|
||||
- `#5` is the south exit from an elevator to Limbo.
|
||||
|
||||
Keep these IDs somewhere for the demonstration. You will shortly see why they are important.
|
||||
|
||||
> Why have we created exits to our elevator and back to Limbo? Isn't the elevator supposed to move?
|
||||
|
||||
It is. But we need to have exits that will represent the way inside the elevator and out. What we
|
||||
will do, at every floor, will be to change these exits so they become connected to the right room.
|
||||
You'll see this process a bit later.
|
||||
|
||||
We have two more rooms to create: our floor 2 and 3. This time, we'll use `dig`, because we don't
|
||||
need exits leading there, not yet anyway.
|
||||
|
||||
dig The second floor
|
||||
dig The third floor
|
||||
|
||||
Evennia should answer with:
|
||||
|
||||
Created room The second floor(#6) of type typeclasses.rooms.Room.
|
||||
Created room The third floor(#7) of type typeclasses.rooms.Room.
|
||||
|
||||
Add these IDs to your list, we will use them too.
|
||||
|
||||
## Our first callback in the elevator
|
||||
|
||||
Let's go to the elevator (you could use `tel #3` if you have the same IDs I have).
|
||||
|
||||
This is our elevator room. It looks a bit empty, feel free to add a prettier description or other
|
||||
things to decorate it a bit.
|
||||
|
||||
But what we want now is to be able to say "1", "2" or "3" and have the elevator move in that
|
||||
direction.
|
||||
|
||||
If you have read [the previous tutorial about adding dialogues in events](Dialogues-in-events), you
|
||||
may remember what we need to do. If not, here's a summary: we need to run some code when somebody
|
||||
speaks in the room. So we need to create a callback (the callback will contain our lines of code).
|
||||
We just need to know on which event this should be set. You can enter `call here` to see the
|
||||
possible events in this room.
|
||||
|
||||
In the table, you should see the "say" event, which is called when somebody says something in the
|
||||
room. So we'll need to add a callback to this event. Don't worry if you're a bit lost, just follow
|
||||
the following steps, the way they connect together will become more obvious.
|
||||
|
||||
call/add here = say 1, 2, 3
|
||||
|
||||
1. We need to add a callback. A callback contains the code that will be executed at a given time.
|
||||
So we use the `call/add` command and switch.
|
||||
2. `here` is our object, the room in which we are.
|
||||
3. An equal sign.
|
||||
4. The name of the event to which the callback should be connected. Here, the event is "say".
|
||||
Meaning this callback will be executed every time somebody says something in the room.
|
||||
5. But we add an event parameter to indicate the keywords said in the room that should execute our
|
||||
callback. Otherwise, our callback would be called every time somebody speaks, no matter what. Here
|
||||
we limit, indicating our callback should be executed only if the spoken message contains "1", "2" or
|
||||
"3".
|
||||
|
||||
An editor should open, inviting you to enter the Python code that should be executed. The first
|
||||
thing to remember is to read the text provided (it can contain important information) and, most of
|
||||
all, the list of variables that are available in this callback:
|
||||
|
||||
```
|
||||
Variables you can use in this event:
|
||||
|
||||
character: the character having spoken in this room.
|
||||
room: the room connected to this event.
|
||||
message: the text having been spoken by the character.
|
||||
|
||||
----------Line Editor [Callback say of Inside of an elevator]---------------------
|
||||
01|
|
||||
----------[l:01 w:000 c:0000]------------(:h for help)----------------------------
|
||||
```
|
||||
|
||||
This is important, in order to know what variables we can use in our callback out-of-the-box. Let's
|
||||
write a single line to be sure our callback is called when we expect it to:
|
||||
|
||||
```python
|
||||
character.msg("You just said {}.".format(message))
|
||||
```
|
||||
|
||||
You can paste this line in-game, then type the `:wq` command to exit the editor and save your
|
||||
modifications.
|
||||
|
||||
Let's check. Try to say "hello" in the room. You should see the standard message, but nothing
|
||||
more. Now try to say "1". Below the standard message, you should see:
|
||||
|
||||
You just said 1.
|
||||
|
||||
You can try it. Our callback is only called when we say "1", "2" or "3". Which is just what we
|
||||
want.
|
||||
|
||||
Let's go back in our code editor and add something more useful.
|
||||
|
||||
call/edit here = say
|
||||
|
||||
> Notice that we used the "edit" switch this time, since the callback exists, we just want to edit
|
||||
it.
|
||||
|
||||
The editor opens again. Let's empty it first:
|
||||
|
||||
:DD
|
||||
|
||||
And turn off automatic indentation, which will help us:
|
||||
|
||||
:=
|
||||
|
||||
> Auto-indentation is an interesting feature of the code editor, but we'd better not use it at this
|
||||
point, it will make copy/pasting more complicated.
|
||||
|
||||
## Our entire callback in the elevator
|
||||
|
||||
So here's the time to truly code our callback in-game. Here's a little reminder:
|
||||
|
||||
1. We have all the IDs of our three rooms and two exits.
|
||||
2. When we say "1", "2" or "3", the elevator should move to the right room, that is change the
|
||||
exits. Remember, we already have the exits, we just need to change their location and destination.
|
||||
|
||||
It's a good idea to try to write this callback yourself, but don't feel bad about checking the
|
||||
solution right now. Here's a possible code that you could paste in the code editor:
|
||||
|
||||
```python
|
||||
# First let's have some constants
|
||||
ELEVATOR = get(id=3)
|
||||
FLOORS = {
|
||||
"1": get(id=2),
|
||||
"2": get(id=6),
|
||||
"3": get(id=7),
|
||||
}
|
||||
TO_EXIT = get(id=4)
|
||||
BACK_EXIT = get(id=5)
|
||||
|
||||
# Now we check that the elevator isn't already at this floor
|
||||
floor = FLOORS.get(message)
|
||||
if floor is None:
|
||||
character.msg("Which floor do you want?")
|
||||
elif TO_EXIT.location is floor:
|
||||
character.msg("The elevator already is at this floor.")
|
||||
else:
|
||||
# 'floor' contains the new room where the elevator should be
|
||||
room.msg_contents("The doors of the elevator close with a clank.")
|
||||
TO_EXIT.location = floor
|
||||
BACK_EXIT.destination = floor
|
||||
room.msg_contents("The doors of the elevator open to {floor}.",
|
||||
mapping=dict(floor=floor))
|
||||
```
|
||||
|
||||
Let's review this longer callback:
|
||||
|
||||
1. We first obtain the objects of both exits and our three floors. We use the `get()` eventfunc,
|
||||
which is a shortcut to obtaining objects. We usually use it to retrieve specific objects with an
|
||||
ID. We put the floors in a dictionary. The keys of the dictionary are the floor number (as str),
|
||||
the values are room objects.
|
||||
2. Remember, the `message` variable contains the message spoken in the room. So either "1", "2", or
|
||||
"3". We still need to check it, however, because if the character says something like "1 2" in the
|
||||
room, our callback will be executed. Let's be sure what she says is a floor number.
|
||||
3. We then check if the elevator is already at this floor. Notice that we use `TO_EXIT.location`.
|
||||
`TO_EXIT` contains our "north" exit, leading inside of our elevator. Therefore, its `location` will
|
||||
be the room where the elevator currently is.
|
||||
4. If the floor is a different one, have the elevator "move", changing just the location and
|
||||
destination of both exits.
|
||||
- The `BACK_EXIT` (that is "north") should change its location. The elevator shouldn't be
|
||||
accessible through our old floor.
|
||||
- The `TO_EXIT` (that is "south", the exit leading out of the elevator) should have a different
|
||||
destination. When we go out of the elevator, we should find ourselves in the new floor, not the old
|
||||
one.
|
||||
|
||||
Feel free to expand on this example, changing messages, making further checks. Usage and practice
|
||||
are keys.
|
||||
|
||||
You can quit the editor as usual with `:wq` and test it out.
|
||||
|
||||
## Adding a pause in our callback
|
||||
|
||||
Let's improve our callback. One thing that's worth adding would be a pause: for the time being,
|
||||
when we say the floor number in the elevator, the doors close and open right away. It would be
|
||||
better to have a pause of several seconds. More logical.
|
||||
|
||||
This is a great opportunity to learn about chained events. Chained events are very useful to create
|
||||
pauses. Contrary to the events we have seen so far, chained events aren't called automatically.
|
||||
They must be called by you, and can be called after some time.
|
||||
|
||||
- Chained events always have the name "chain_X". Usually, X is a number, but you can give the
|
||||
chained event a more explicit name.
|
||||
- In our original callback, we will call our chained events in, say, 15 seconds.
|
||||
- We'll also have to make sure the elevator isn't already moving.
|
||||
|
||||
Other than that, a chained event can be connected to a callback as usual. We'll create a chained
|
||||
event in our elevator, that will only contain the code necessary to open the doors to the new floor.
|
||||
|
||||
call/add here = chain_1
|
||||
|
||||
The callback is added to the "chain_1" event, an event that will not be automatically called by the
|
||||
system when something happens. Inside this event, you can paste the code to open the doors at the
|
||||
new floor. You can notice a few differences:
|
||||
|
||||
```python
|
||||
TO_EXIT.location = floor
|
||||
TO_EXIT.destination = ELEVATOR
|
||||
BACK_EXIT.location = ELEVATOR
|
||||
BACK_EXIT.destination = floor
|
||||
room.msg_contents("The doors of the elevator open to {floor}.",
|
||||
mapping=dict(floor=floor))
|
||||
```
|
||||
|
||||
Paste this code into the editor, then use `:wq` to save and quit the editor.
|
||||
|
||||
Now let's edit our callback in the "say" event. We'll have to change it a bit:
|
||||
|
||||
- The callback will have to check the elevator isn't already moving.
|
||||
- It must change the exits when the elevator move.
|
||||
- It has to call the "chain_1" event we have defined. It should call it 15 seconds later.
|
||||
|
||||
Let's see the code in our callback.
|
||||
|
||||
call/edit here = say
|
||||
|
||||
Remove the current code and disable auto-indentation again:
|
||||
|
||||
:DD
|
||||
:=
|
||||
|
||||
And you can paste instead the following code. Notice the differences with our first attempt:
|
||||
|
||||
```python
|
||||
# First let's have some constants
|
||||
ELEVATOR = get(id=3)
|
||||
FLOORS = {
|
||||
"1": get(id=2),
|
||||
"2": get(id=6),
|
||||
"3": get(id=7),
|
||||
}
|
||||
TO_EXIT = get(id=4)
|
||||
BACK_EXIT = get(id=5)
|
||||
|
||||
# Now we check that the elevator isn't already at this floor
|
||||
floor = FLOORS.get(message)
|
||||
if floor is None:
|
||||
character.msg("Which floor do you want?")
|
||||
elif BACK_EXIT.location is None:
|
||||
character.msg("The elevator is between floors.")
|
||||
elif TO_EXIT.location is floor:
|
||||
character.msg("The elevator already is at this floor.")
|
||||
else:
|
||||
# 'floor' contains the new room where the elevator should be
|
||||
room.msg_contents("The doors of the elevator close with a clank.")
|
||||
TO_EXIT.location = None
|
||||
BACK_EXIT.location = None
|
||||
call_event(room, "chain_1", 15)
|
||||
```
|
||||
|
||||
What changed?
|
||||
|
||||
1. We added a little test to make sure the elevator wasn't already moving. If it is, the
|
||||
`BACK_EXIT.location` (the "south" exit leading out of the elevator) should be `None`. We'll remove
|
||||
the exit while the elevator is moving.
|
||||
2. When the doors close, we set both exits' `location` to `None`. Which "removes" them from their
|
||||
room but doesn't destroy them. The exits still exist but they don't connect anything. If you say
|
||||
"2" in the elevator and look around while the elevator is moving, you won't see any exits.
|
||||
3. Instead of opening the doors immediately, we call `call_event`. We give it the object containing
|
||||
the event to be called (here, our elevator), the name of the event to be called (here, "chain_1")
|
||||
and the number of seconds from now when the event should be called (here, `15`).
|
||||
4. The `chain_1` callback we have created contains the code to "re-open" the elevator doors. That
|
||||
is, besides displaying a message, it reset the exits' `location` and `destination`.
|
||||
|
||||
If you try to say "3" in the elevator, you should see the doors closing. Look around you and you
|
||||
won't see any exit. Then, 15 seconds later, the doors should open, and you can leave the elevator
|
||||
to go to the third floor. While the elevator is moving, the exit leading to it will be
|
||||
inaccessible.
|
||||
|
||||
> Note: we don't define the variables again in our chained event, we just call them. When we
|
||||
execute `call_event`, a copy of our current variables is placed in the database. These variables
|
||||
will be restored and accessible again when the chained event is called.
|
||||
|
||||
You can use the `call/tasks` command to see the tasks waiting to be executed. For instance, say "2"
|
||||
in the room, notice the doors closing, and then type the `call/tasks` command. You will see a task
|
||||
in the elevator, waiting to call the `chain_1` event.
|
||||
|
||||
## Changing exit messages
|
||||
|
||||
Here's another nice little feature of events: you can modify the message of a single exit without
|
||||
altering the others. In this case, when someone goes north into our elevator, we'd like to see
|
||||
something like: "someone walks into the elevator." Something similar for the back exit would be
|
||||
great too.
|
||||
|
||||
Inside of the elevator, you can look at the available events on the exit leading outside (south).
|
||||
|
||||
call south
|
||||
|
||||
You should see two interesting rows in this table:
|
||||
|
||||
```
|
||||
| msg_arrive | 0 (0) | Customize the message when a character |
|
||||
| | | arrives through this exit. |
|
||||
| msg_leave | 0 (0) | Customize the message when a character leaves |
|
||||
| | | through this exit. |
|
||||
```
|
||||
|
||||
So we can change the message others see when a character leaves, by editing the "msg_leave" event.
|
||||
Let's do that:
|
||||
|
||||
call/add south = msg_leave
|
||||
|
||||
Take the time to read the help. It gives you all the information you should need. We'll need to
|
||||
change the "message" variable, and use custom mapping (between braces) to alter the message. We're
|
||||
given an example, let's use it. In the code editor, you can paste the following line:
|
||||
|
||||
```python
|
||||
message = "{character} walks out of the elevator."
|
||||
```
|
||||
|
||||
Again, save and quit the editor by entering `:wq`. You can create a new character to see it leave.
|
||||
|
||||
charcreate A beggar
|
||||
tel #8 = here
|
||||
|
||||
(Obviously, adapt the ID if necessary.)
|
||||
|
||||
py self.search("beggar").move_to(self.search("south"))
|
||||
|
||||
This is a crude way to force our beggar out of the elevator, but it allows us to test. You should
|
||||
see:
|
||||
|
||||
A beggar(#8) walks out of the elevator.
|
||||
|
||||
Great! Let's do the same thing for the exit leading inside of the elevator. Follow the beggar,
|
||||
then edit "msg_leave" of "north":
|
||||
|
||||
call/add north = msg_leave
|
||||
|
||||
```python
|
||||
message = "{character} walks into the elevator."
|
||||
```
|
||||
|
||||
Again, you can force our beggar to move and see the message we have just set. This modification
|
||||
applies to these two exits, obviously: the custom message won't be used for other exits. Since we
|
||||
use the same exits for every floor, this will be available no matter at what floor the elevator is,
|
||||
which is pretty neat!
|
||||
|
||||
## Tutorial F.A.Q.
|
||||
|
||||
- **Q:** what happens if the game reloads or shuts down while a task is waiting to happen?
|
||||
- **A:** if your game reloads while a task is in pause (like our elevator between floors), when the
|
||||
game is accessible again, the task will be called (if necessary, with a new time difference to take
|
||||
into account the reload). If the server shuts down, obviously, the task will not be called, but
|
||||
will be stored and executed when the server is up again.
|
||||
- **Q:** can I use all kinds of variables in my callback? Whether chained or not?
|
||||
- **A:** you can use every variable type you like in your original callback. However, if you
|
||||
execute `call_event`, since your variables are stored in the database, they will need to respect the
|
||||
constraints on persistent attributes. A callback will not be stored in this way, for instance.
|
||||
This variable will not be available in your chained event.
|
||||
- **Q:** when you say I can call my chained events something else than "chain_1", "chain_2" and
|
||||
such, what is the naming convention?
|
||||
- **A:** chained events have names beginning by "chain_". This is useful for you and for the
|
||||
system. But after the underscore, you can give a more useful name, like "chain_open_doors" in our
|
||||
case.
|
||||
- **Q:** do I have to pause several seconds to call a chained event?
|
||||
- **A:** no, you can call it right away. Just leave the third parameter of `call_event` out (it
|
||||
will default to 0, meaning the chained event will be called right away). This will not create a
|
||||
task.
|
||||
- **Q:** can I have chained events calling themselves?
|
||||
- **A:** you can. There's no limitation. Just be careful, a callback that calls itself,
|
||||
particularly without delay, might be a good recipe for an infinite loop. However, in some cases, it
|
||||
is useful to have chained events calling themselves, to do the same repeated action every X seconds
|
||||
for instance.
|
||||
- **Q:** what if I need several elevators, do I need to copy/paste these callbacks each time?
|
||||
- **A:** not advisable. There are definitely better ways to handle this situation. One of them is
|
||||
to consider adding the code in the source itself. Another possibility is to call chained events
|
||||
with the expected behavior, which makes porting code very easy. This side of chained events will be
|
||||
shown in the next tutorial.
|
||||
|
||||
- Previous tutorial: [Adding dialogues in events](Dialogues-in-events)
|
||||
1233
docs/source/Contrib/Building-menus.md
Normal file
1233
docs/source/Contrib/Building-menus.md
Normal file
File diff suppressed because it is too large
Load diff
249
docs/source/Contrib/Dialogues-in-events.md
Normal file
249
docs/source/Contrib/Dialogues-in-events.md
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
# Dialogues in events
|
||||
|
||||
|
||||
- Next tutorial: [adding a voice-operated elevator with events](A-voice-operated-elevator-using-
|
||||
events).
|
||||
|
||||
This tutorial will walk you through the steps to create several dialogues with characters, using the
|
||||
[in-game Python
|
||||
system](https://github.com/evennia/evennia/blob/master/evennia/contrib/ingame_python/README.md).
|
||||
This tutorial assumes the in-game Python system is installed in your game. If it isn't, you can
|
||||
follow the installation steps given in [the documentation on in-game
|
||||
Python](https://github.com/evennia/evennia/blob/master/evennia/contrib/ingame_python/README.md), and
|
||||
come back on this tutorial once the system is installed. **You do not need to read** the entire
|
||||
documentation, it's a good reference, but not the easiest way to learn about it. Hence these
|
||||
tutorials.
|
||||
|
||||
The in-game Python system allows to run code on individual objects in some situations. You don't
|
||||
have to modify the source code to add these features, past the installation. The entire system
|
||||
makes it easy to add specific features to some objects, but not all. This is why it can be very
|
||||
useful to create a dialogue system taking advantage of the in-game Python system.
|
||||
|
||||
> What will we try to do?
|
||||
|
||||
In this tutorial, we are going to create a basic dialogue to have several characters automatically
|
||||
respond to specific messages said by others.
|
||||
|
||||
## A first example with a first character
|
||||
|
||||
Let's create a character to begin with.
|
||||
|
||||
@charcreate a merchant
|
||||
|
||||
This will create a merchant in the room where you currently are. It doesn't have anything, like a
|
||||
description, you can decorate it a bit if you like.
|
||||
|
||||
As said above, the in-game Python system consists in linking objects with arbitrary code. This code
|
||||
will be executed in some circumstances. Here, the circumstance is "when someone says something in
|
||||
the same room", and might be more specific like "when someone says hello". We'll decide what code
|
||||
to run (we'll actually type the code in-game). Using the vocabulary of the in-game Python system,
|
||||
we'll create a callback: a callback is just a set of lines of code that will run under some
|
||||
conditions.
|
||||
|
||||
You can have an overview of every "conditions" in which callbacks can be created using the `@call`
|
||||
command (short for `@callback`). You need to give it an object as argument. Here for instance, we
|
||||
could do:
|
||||
|
||||
@call a merchant
|
||||
|
||||
You should see a table with three columns, showing the list of events existing on our newly-created
|
||||
merchant. There are quite a lot of them, as it is, althougn no line of code has been set yet. For
|
||||
our system, you might be more interested by the line describing the `say` event:
|
||||
|
||||
| say | 0 (0) | After another character has said something in |
|
||||
| | | the character's room. |
|
||||
|
||||
We'll create a callback on the `say` event, called when we say "hello" in the merchant's room:
|
||||
|
||||
@call/add a merchant = say hello
|
||||
|
||||
Before seeing what this command displays, let's see the command syntax itself:
|
||||
|
||||
- `@call` is the command name, `/add` is a switch. You can read the help of the command to get the
|
||||
help of available switches and a brief overview of syntax.
|
||||
- We then enter the object's name, here "a merchant". You can enter the ID too ("#3" in my case),
|
||||
which is useful to edit the object when you're not in the same room. You can even enter part of the
|
||||
name, as usual.
|
||||
- An equal sign, a simple separator.
|
||||
- The event's name. Here, it's "say". The available events are displayed when you use `@call`
|
||||
without switch.
|
||||
- After a space, we enter the conditions in which this callback should be called. Here, the
|
||||
conditions represent what the other character should say. We enter "hello". Meaning that if
|
||||
someone says something containing "hello" in the room, the callback we are now creating will be
|
||||
called.
|
||||
|
||||
When you enter this command, you should see something like this:
|
||||
|
||||
```
|
||||
After another character has said something in the character's room.
|
||||
This event is called right after another character has said
|
||||
something in the same location. The action cannot be prevented
|
||||
at this moment. Instead, this event is ideal to create keywords
|
||||
that would trigger a character (like a NPC) in doing something
|
||||
if a specific phrase is spoken in the same location.
|
||||
|
||||
To use this event, you have to specify a list of keywords as
|
||||
parameters that should be present, as separate words, in the
|
||||
spoken phrase. For instance, you can set a callback that would
|
||||
fire if the phrase spoken by the character contains "menu" or
|
||||
"dinner" or "lunch":
|
||||
@call/add ... = say menu, dinner, lunch
|
||||
Then if one of the words is present in what the character says,
|
||||
this callback will fire.
|
||||
|
||||
Variables you can use in this event:
|
||||
speaker: the character speaking in this room.
|
||||
character: the character connected to this event.
|
||||
message: the text having been spoken by the character.
|
||||
```
|
||||
|
||||
That's some list of information. What's most important to us now is:
|
||||
|
||||
- The "say" event is called whenever someone else speaks in the room.
|
||||
- We can set callbacks to fire when specific keywords are present in the phrase by putting them as
|
||||
additional parameters. Here we have set this parameter to "hello". We can have several keywords
|
||||
separated by a comma (we'll see this in more details later).
|
||||
- We have three default variables we can use in this callback: `speaker` which contains the
|
||||
character who speaks, `character` which contains the character who's modified by the in-game Python
|
||||
system (here, or merchant), and `message` which contains the spoken phrase.
|
||||
|
||||
This concept of variables is important. If it makes things more simple to you, think of them as
|
||||
parameters in a function: they can be used inside of the function body because they have been set
|
||||
when the function was called.
|
||||
|
||||
This command has opened an editor where we can type our Python code.
|
||||
|
||||
```
|
||||
----------Line Editor [Callback say of a merchant]--------------------------------
|
||||
01|
|
||||
----------[l:01 w:000 c:0000]------------(:h for help)----------------------------
|
||||
```
|
||||
|
||||
For our first test, let's type something like:
|
||||
|
||||
```python
|
||||
character.location.msg_contents("{character} shrugs and says: 'well, yes, hello to you!'",
|
||||
mapping=dict(character=character))
|
||||
```
|
||||
|
||||
Once you have entered this line, you can type `:wq` to save the editor and quit it.
|
||||
|
||||
And now if you use the "say" command with a message containing "hello":
|
||||
|
||||
```
|
||||
You say, "Hello sir merchant!"
|
||||
a merchant(#3) shrugs and says: 'well, yes, hello to you!'
|
||||
```
|
||||
|
||||
If you say something that doesn't contain "hello", our callback won't execute.
|
||||
|
||||
**In summary**:
|
||||
|
||||
1. When we say something in the room, using the "say" command, the "say" event of all characters
|
||||
(except us) is called.
|
||||
2. The in-game Python system looks at what we have said, and checks whether one of our callbacks in
|
||||
the "say" event contains a keyword that we have spoken.
|
||||
3. If so, call it, defining the event variables as we have seen.
|
||||
4. The callback is then executed as normal Python code. Here we have called the `msg_contents`
|
||||
method on the character's location (probably a room) to display a message to the entire room. We
|
||||
have also used mapping to easily display the character's name. This is not specific to the in-game
|
||||
Python system. If you feel overwhelmed by the code we've used, just shorten it and use something
|
||||
more simple, for instance:
|
||||
|
||||
```python
|
||||
speaker.msg("You have said something to me.")
|
||||
```
|
||||
|
||||
## The same callback for several keywords
|
||||
|
||||
It's easy to create a callback that will be triggered if the sentence contains one of several
|
||||
keywords.
|
||||
|
||||
@call/add merchant = say trade, trader, goods
|
||||
|
||||
And in the editor that opens:
|
||||
|
||||
```python
|
||||
character.location.msg_contents("{character} says: 'Ho well, trade's fine as long as roads are
|
||||
safe.'", mapping=dict(character=character))
|
||||
```
|
||||
|
||||
Then you can say something with either "trade", "trader" or "goods" in your sentence, which should
|
||||
call the callback:
|
||||
|
||||
```
|
||||
You say, "and how is your trade going?"
|
||||
a merchant(#3) says: 'Ho well, trade's fine as long as roads are safe.'
|
||||
```
|
||||
|
||||
We can set several keywords when adding the callback. We just need to separate them with commas.
|
||||
|
||||
## A longer callback
|
||||
|
||||
So far, we have only set one line in our callbacks. Which is useful, but we often need more. For
|
||||
an entire dialogue, you might want to do a bit more than that.
|
||||
|
||||
@call/add merchant = say bandit, bandits
|
||||
|
||||
And in the editor you can paste the following lines:
|
||||
|
||||
```python
|
||||
character.location.msg_contents("{character} says: 'Bandits he?'",
|
||||
mapping=dict(character=character))
|
||||
character.location.msg_contents("{character} scratches his head, considering.",
|
||||
mapping=dict(character=character))
|
||||
character.location.msg_contents("{character} whispers: 'Aye, saw some of them, north from here. No
|
||||
trouble o' mine, but...'", mapping=dict(character=character))
|
||||
speaker.msg("{character} looks at you more
|
||||
closely.".format(character=character.get_display_name(speaker)))
|
||||
speaker.msg("{character} continues in a low voice: 'Ain't my place to say, but if you need to find
|
||||
'em, they're encamped some distance away from the road, I guess near a cave or
|
||||
something.'.".format(character=character.get_display_name(speaker)))
|
||||
```
|
||||
|
||||
Now try to ask the merchant about bandits:
|
||||
|
||||
```
|
||||
You say, "have you seen bandits?"
|
||||
a merchant(#3) says: 'Bandits he?'
|
||||
a merchant(#3) scratches his head, considering.
|
||||
a merchant(#3) whispers: 'Aye, saw some of them, north from here. No trouble o' mine, but...'
|
||||
a merchant(#3) looks at you more closely.
|
||||
a merchant(#3) continues in a low voice: 'Ain't my place to say, but if you need to find 'em,
|
||||
they're encamped some distance away from the road, I guess near a cave or something.'.
|
||||
```
|
||||
|
||||
Notice here that the first lines of dialogue are spoken to the entire room, but then the merchant is
|
||||
talking directly to the speaker, and only the speaker hears it. There's no real limit to what you
|
||||
can do with this.
|
||||
|
||||
- You can set a mood system, storing attributes in the NPC itself to tell you in what mood he is,
|
||||
which will influence the information he will give... perhaps the accuracy of it as well.
|
||||
- You can add random phrases spoken in some context.
|
||||
- You can use other actions (you're not limited to having the merchant say something, you can ask
|
||||
him to move, gives you something, attack if you have a combat system, or whatever else).
|
||||
- The callbacks are in pure Python, so you can write conditions or loops.
|
||||
- You can add in "pauses" between some instructions using chained events. This tutorial won't
|
||||
describe how to do that however. You already have a lot to play with.
|
||||
|
||||
## Tutorial F.A.Q.
|
||||
|
||||
- **Q:** can I create several characters who would answer to specific dialogue?
|
||||
- **A:** of course. Te in-game Python system is so powerful because you can set unique code for
|
||||
various objects. You can have several characters answering to different things. You can even have
|
||||
different characters in the room answering to greetings. All callbacks will be executed one after
|
||||
another.
|
||||
- **Q:** can I have two characters answering to the same dialogue in exactly the same way?
|
||||
- **A:** It's possible but not so easy to do. Usually, event grouping is set in code, and depends
|
||||
on different games. However, if it is for some infrequent occurrences, it's easy to do using
|
||||
[chained
|
||||
events](https://github.com/evennia/evennia/blob/master/evennia/contrib/ingame_python/README.md#chained-
|
||||
events).
|
||||
- **Q:** is it possible to deploy callbacks on all characters sharing the same prototype?
|
||||
- **A:** not out of the box. This depends on individual settings in code. One can imagine that all
|
||||
characters of some type would share some events, but this is game-specific. Rooms of the same zone
|
||||
could share the same events as well. It is possible to do but requires modification of the source
|
||||
code.
|
||||
|
||||
- Next tutorial: [adding a voice-operated elevator with events](A-voice-operated-elevator-using-
|
||||
events).
|
||||
495
docs/source/Contrib/Dynamic-In-Game-Map.md
Normal file
495
docs/source/Contrib/Dynamic-In-Game-Map.md
Normal file
|
|
@ -0,0 +1,495 @@
|
|||
# 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](Static-In-Game-Map) 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](Default-Command-Help#tunnel-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 = "%s\n" % Map(looker).show_map()
|
||||
# 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](Commands) 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.
|
||||
417
docs/source/Contrib/Static-In-Game-Map.md
Normal file
417
docs/source/Contrib/Static-In-Game-Map.md
Normal file
|
|
@ -0,0 +1,417 @@
|
|||
# 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](Batch-Code-Processor) for advanced building. There is
|
||||
also the [Dynamic in-game map tutorial](Dynamic-In-Game-Map) 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](TextTags#colored-text) 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](http://mcdlr.com/utf-8/#1).
|
||||
|
||||
For this exercise, we've copy-and-pasted from the pallet of special characters used over at [Dwarf
|
||||
Fortress](http://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](Default-Command-Help) for [creating objects and rooms
|
||||
in-game](Building-Quickstart). 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 [in-line functions](TextTags#new-inlinefuncs)). 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](Batch-Processors) 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 ](Batch-Code-Processor), 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](Building-Permissions). 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) or [implement a combat
|
||||
system](Turn-based-Combat-System).
|
||||
106
docs/source/Contrib/Tutorial-World-Introduction.md
Normal file
106
docs/source/Contrib/Tutorial-World-Introduction.md
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
# Tutorial World Introduction
|
||||
|
||||
|
||||
The *Tutorial World* is a small and functioning MUD-style game world. It is intended to be
|
||||
deconstructed and used as a way to learn Evennia. The game consists of a single-player quest and
|
||||
has some 20 rooms that you can explore as you seek to discover the whereabouts of a mythical weapon.
|
||||
|
||||
The source code is fully documented. You can find the whole thing in
|
||||
`evennia/contrib/tutorial_world/`.
|
||||
|
||||
Some features exemplified by the tutorial world:
|
||||
|
||||
- Tutorial command, giving "behind-the-scenes" help for every room and some of the special objects
|
||||
- Rooms with custom `return_appearance` to show details.
|
||||
- Hidden exits
|
||||
- Objects with multiple custom interactions
|
||||
- Large-area rooms
|
||||
- Outdoor weather rooms
|
||||
- Dark room, needing light source
|
||||
- Puzzle object
|
||||
- Multi-room puzzle
|
||||
- Aggressive mobile with roam, pursue and battle state-engine AI
|
||||
- Weapons, also used by mobs
|
||||
- Simple combat system with attack/defend commands
|
||||
- Object spawning
|
||||
- Teleporter trap rooms
|
||||
|
||||
|
||||
## Install
|
||||
|
||||
The tutorial world consists of a few modules in `evennia/contrib/tutorial_world/` containing custom
|
||||
[Typeclasses](Typeclasses) for [rooms and objects](Objects) and associated [Commands](Commands).
|
||||
|
||||
These reusable bits and pieces are then put together into a functioning game area ("world" is maybe
|
||||
too big a word for such a small zone) using a [batch script](Batch-Processors) called `build.ev`. To
|
||||
install, log into the server as the superuser (user #1) and run:
|
||||
|
||||
@batchcommand tutorial_world.build
|
||||
|
||||
The world will be built (this might take a while, so don't rerun the command even if it seems the
|
||||
system has frozen). After finishing you will end up back in Limbo with a new exit called `tutorial`.
|
||||
|
||||
An alternative is
|
||||
|
||||
@batchcommand/interactive tutorial_world.build
|
||||
|
||||
with the /interactive switch you are able to step through the building process at your own pace to
|
||||
see what happens in detail.
|
||||
|
||||
To play the tutorial "correctly", you should *not* do so as superuser. The reason for this is that
|
||||
many game systems ignore the presence of a superuser and will thus not work as normal. Use the
|
||||
`@quell` command to limit your powers or log out and reconnect as a different user. As superuser you
|
||||
can of course examine things "under the hood" later if you want.
|
||||
|
||||
## Gameplay
|
||||
|
||||

|
||||
|
||||
|
||||
*To get into the mood of this miniature quest, imagine you are an adventurer out to find fame and
|
||||
fortune. You have heard rumours of an old castle ruin by the coast. In its depth a warrior princess
|
||||
was buried together with her powerful magical weapon - a valuable prize, if it's true. Of course
|
||||
this is a chance to adventure that you cannot turn down!*
|
||||
|
||||
*You reach the ocean in the midst of a raging thunderstorm. With wind and rain screaming in your
|
||||
face you stand where the moor meets the sea along a high, rocky coast ...*
|
||||
|
||||
- Look at everything.
|
||||
- Some objects are interactive in more than one way. Use the normal `help` command to get a feel for
|
||||
which commands are available at any given time. (use the command `tutorial` to get insight behind
|
||||
the scenes of the tutorial).
|
||||
|
||||
- In order to fight, you need to first find some type of weapon.
|
||||
- *slash* is a normal attack
|
||||
- *stab* launches an attack that makes more damage but has a lower chance to hit.
|
||||
- *defend* will lower the chance to taking damage on your enemy's next attack.
|
||||
- You *can* run from a fight that feels too deadly. Expect to be chased though.
|
||||
- Being defeated is a part of the experience ...
|
||||
|
||||
## Uninstall
|
||||
|
||||
Uninstalling the tutorial world basically means deleting all the rooms and objects it consists of.
|
||||
First, move out of the tutorial area.
|
||||
|
||||
@find tut#01
|
||||
@find tut#16
|
||||
|
||||
This should locate the first and last rooms created by `build.ev` - *Intro* and *Outro*. If you
|
||||
installed normally, everything created between these two numbers should be part of the tutorial.
|
||||
Note their dbref numbers, for example 5 and 80. Next we just delete all objects in that range:
|
||||
|
||||
@del 5-80
|
||||
|
||||
You will see some errors since some objects are auto-deleted and so cannot be found when the delete
|
||||
mechanism gets to them. That's fine. You should have removed the tutorial completely once the
|
||||
command finishes.
|
||||
|
||||
## Notes
|
||||
|
||||
When reading and learning from the code, keep in mind that *Tutorial World* was created with a very
|
||||
specific goal: to install easily and to not permanently modify the rest of the server. It therefore
|
||||
goes to some length to use only temporary solutions and to clean up after
|
||||
itself.
|
||||
Loading…
Add table
Add a link
Reference in a new issue