mirror of
https://github.com/evennia/evennia.git
synced 2026-03-23 16:26:30 +01:00
Refactor 1.0 docs with new toctree structure and inheritance
This commit is contained in:
parent
62477eac50
commit
628afe9367
142 changed files with 3967 additions and 3024 deletions
116
docs/source/Howtos/Beginner-Tutorial/Beginner-Tutorial-Intro.md
Normal file
116
docs/source/Howtos/Beginner-Tutorial/Beginner-Tutorial-Intro.md
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
# Beginner Tutorial
|
||||
|
||||
```{eval-rst}
|
||||
.. sidebar:: Tutorial Parts
|
||||
|
||||
**Introduction**
|
||||
Getting set up.
|
||||
Part 1: `What we have <Part1/Beginner-Tutorial-Part1-Intro.html>`_
|
||||
A tour of Evennia and how to use the tools, including an introduction to Python.
|
||||
Part 2: `What we want <Part2/Beginner-Tutorial-Part2-Intro.html>`_
|
||||
Planning our tutorial game and what to think about when planning your own in the future.
|
||||
Part 3: `How we get there <Part3/Beginner-Tutorial-Part3-Intro.html>`_
|
||||
Getting down to the meat of extending Evennia to make our game
|
||||
Part 4: `Using what we created <Part4/Beginner-Tutorial-Part4-Intro.html>`_
|
||||
Building a tech-demo and world content to go with our code
|
||||
Part 5: `Showing the world <Part5/Beginner-Tutorial-Part5-Intro.html>`_
|
||||
Taking our new game online and let players try it out
|
||||
|
||||
```
|
||||
Welcome to Evennia! This multi-part Beginner Tutorial will help you get off the ground. It consists
|
||||
of five parts, each with several lessons. You can pick what seems interesting, but if you
|
||||
follow through to the end you will have created a little online game of your own to play
|
||||
and share with others!
|
||||
|
||||
Use the menu on the right to get the index of each tutorial-part. Use the [next](Part1/Beginner-Tutorial-Part1-Intro.md)
|
||||
and [previous](../Howtos-Overview.md) links to step from lesson to lesson.
|
||||
|
||||
## Things you need
|
||||
|
||||
- A Command line
|
||||
- A MUD client (or web browser)
|
||||
- A text-editor/IDE
|
||||
- Evennia installed and a game-dir initialized
|
||||
|
||||
### A Command line
|
||||
|
||||
You need to know how to find your Terminal/Console in your OS. The Evennia server can be controlled
|
||||
from in-game, but you _will_ need to use the command-line to get anywhere. Here are some starters:
|
||||
|
||||
- [Django-girls' Intro to the Command line for different OS:es](https://tutorial.djangogirls.org/en/intro_to_command_line/)
|
||||
|
||||
Note that we usually only show forward-slashes `/` for file system paths. Windows users should mentally convert this to
|
||||
back-slashes `\` instead.
|
||||
|
||||
### A MUD client
|
||||
|
||||
You might already have a MUD-client you prefer. Check out the [grid of supported clients](../../Setup/Client-Support-Grid.md) for aid.
|
||||
If telnet's not your thing, you can also just use Evennia's web client in your browser.
|
||||
|
||||
> In this documentation we often use the terms 'MUD', 'MU' or 'MU*' interchangeably
|
||||
to represent all the historically different forms of text-based multiplayer game-styles,
|
||||
like MUD, MUX, MUSH, MUCK, MOO and others. Evennia can be used to create all those game-styles
|
||||
and more.
|
||||
|
||||
### An Editor
|
||||
You need a text-editor to edit Python source files. Most everything that can edit and output raw
|
||||
text works (so not Word).
|
||||
|
||||
- [Here's a blog post summing up some of the alternatives](https://www.elegantthemes.com/blog/resources/best-code-editors) - these
|
||||
things don't change much from year to year. Popular choices for Python are PyCharm, VSCode, Atom, Sublime Text and Notepad++.
|
||||
Evennia is to a very large degree coded in VIM, but that's not suitable for beginners.
|
||||
|
||||
> Hint: When setting up your editor, make sure that pressing TAB inserts _4 spaces_ rather than a Tab-character. Since
|
||||
> Python is whitespace-aware, this will make your life a lot easier.
|
||||
|
||||
|
||||
### Set up a game dir for the tutorial
|
||||
|
||||
Next you should make sure you have [installed Evennia](../../Setup/Installation.md). If you followed the instructions
|
||||
you will already have created a game-dir. You could use that for this tutorial or you may want to do the
|
||||
tutorial in its own, isolated game dir; it's up to you.
|
||||
|
||||
- If you want a new gamedir for the tutorial game and already have Evennia running with another gamedir,
|
||||
first enter that gamedir and run
|
||||
|
||||
evennia stop
|
||||
|
||||
> If you want to run two parallel servers, that'd be fine too, but one would have to use
|
||||
> different ports from the defaults, or there'd be a clash. We will go into changing settings later.
|
||||
- Now go to where you want to create your tutorial-game. We will always refer to it as `mygame` so
|
||||
it may be convenient if you do too:
|
||||
|
||||
evennia --init mygame
|
||||
cd mygame
|
||||
evennia migrate
|
||||
evennia start --log
|
||||
|
||||
Add your superuser name and password at the prompt (email is optional). Make sure you can
|
||||
go to `localhost:4000` in your MUD client or to [http://localhost:4001](http://localhost:4001)
|
||||
in your web browser (Mac users: Try `127.0.0.1` instead of `localhost` if you have trouble).
|
||||
|
||||
The above `--log` flag will have Evennia output all its logs to the terminal. This will block
|
||||
the terminal from other input. To leave the log-view, press `Ctrl-C` (`Cmd-C` on Mac). To see
|
||||
the log again just run
|
||||
|
||||
evennia --log
|
||||
|
||||
You should now be good to go on to [the first part of the tutorial](Part1/Beginner-Tutorial-Part1-Intro.md).
|
||||
Good luck!
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
Click here to expand a list of all Beginner-Tutorial sections (all parts).
|
||||
</summary>
|
||||
|
||||
```{toctree}
|
||||
|
||||
Part1/Beginner-Tutorial-Part1-Intro
|
||||
Part2/Beginner-Tutorial-Part2-Intro
|
||||
Part3/Beginner-Tutorial-Part3-Intro
|
||||
Part4/Beginner-Tutorial-Part4-Intro
|
||||
Part5/Beginner-Tutorial-Part5-Intro
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
392
docs/source/Howtos/Beginner-Tutorial/Part1/Adding-Commands.md
Normal file
392
docs/source/Howtos/Beginner-Tutorial/Part1/Adding-Commands.md
Normal file
|
|
@ -0,0 +1,392 @@
|
|||
# Adding custom commands
|
||||
|
||||
In this lesson we'll learn how to create our own Evennia _Commands_. If you are new to Python you'll
|
||||
also learn some more basics about how to manipulate strings and get information out of Evennia.
|
||||
|
||||
A Command is something that handles the input from a user and causes a result to happen.
|
||||
An example is `look`, which examines your current location and tells how it looks like and
|
||||
what is in it.
|
||||
|
||||
```{sidebar} Commands are not typeclassed
|
||||
|
||||
If you just came from the previous lesson, you might want to know that Commands and
|
||||
CommandSets are not `typeclassed`. That is, instances of them are not saved to the
|
||||
database. They are "just" normal Python classes.
|
||||
```
|
||||
|
||||
In Evennia, a Command is a Python _class_. If you are unsure about what a class is, review the
|
||||
previous lessons! A Command inherits from `evennia.Command` or from one of the alternative command-
|
||||
classes, such as `MuxCommand` which is what most default commands use.
|
||||
|
||||
All Commands are in turn grouped in another class called a _Command Set_. Think of a Command Set
|
||||
as a bag holding many different commands. One CmdSet could for example hold all commands for
|
||||
combat, another for building etc. By default, Evennia groups all character-commands into one
|
||||
big cmdset.
|
||||
|
||||
Command-Sets are then associated with objects, for example with your Character. Doing so makes the
|
||||
commands in that cmdset available to the object. So, to summarize:
|
||||
|
||||
- Commands are classes
|
||||
- A group of Commands is stored in a CmdSet
|
||||
- CmdSets are stored on objects - this defines which commands are available to that object.
|
||||
|
||||
## Creating a custom command
|
||||
|
||||
Open `mygame/commands/command.py`:
|
||||
|
||||
```python
|
||||
"""
|
||||
(module docstring)
|
||||
"""
|
||||
|
||||
from evennia import Command as BaseCommand
|
||||
# from evennia import default_cmds
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""
|
||||
(class docstring)
|
||||
"""
|
||||
pass
|
||||
|
||||
# (lots of commented-out stuff)
|
||||
# ...
|
||||
```
|
||||
|
||||
Ignoring the docstrings (which you can read if you want), this is the only really active code in the module.
|
||||
|
||||
We can see that we import `Command` from `evennia` and use the `from ... import ... as ...` form to rename it
|
||||
to `BaseCommand`. This is so we can let our child class also be named `Command` for reference. The class
|
||||
itself doesn't do anything, it just has `pass`. So in the same way as `Object` in the previous lesson, this
|
||||
class is identical to its parent.
|
||||
|
||||
> The commented out `default_cmds` gives us access to Evennia's default commands for easy overriding. We'll try
|
||||
> that a little later.
|
||||
|
||||
We could modify this module directly, but to train imports we'll work in a separate module. Open a new file
|
||||
`mygame/commands/mycommands.py` and add the following code:
|
||||
|
||||
```python
|
||||
|
||||
from commands.command import Command
|
||||
|
||||
class CmdEcho(Command):
|
||||
key = "echo"
|
||||
|
||||
```
|
||||
|
||||
This is the simplest form of command you can imagine. It just gives itself a name, "echo". This is
|
||||
what you will use to call this command later.
|
||||
|
||||
Next we need to put this in a CmdSet. It will be a one-command CmdSet for now! Change your file as such:
|
||||
|
||||
|
||||
```python
|
||||
|
||||
from commands.command import Command
|
||||
from evennia import CmdSet
|
||||
|
||||
class CmdEcho(Command):
|
||||
key = "echo"
|
||||
|
||||
|
||||
class MyCmdSet(CmdSet):
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
self.add(CmdEcho)
|
||||
|
||||
```
|
||||
|
||||
Our `EchoCmdSet` class must have an `at_cmdset_creation` method, named exactly
|
||||
like this - this is what Evennia will be looking for when setting up the cmdset later, so
|
||||
if you didn't set it up, it will use the parent's version, which is empty. Inside we add the
|
||||
command class to the cmdset by `self.add()`. If you wanted to add more commands to this CmdSet you
|
||||
could just add more lines of `self.add` after this.
|
||||
|
||||
Finally, let's add this command to ourselves so we can try it out. In-game you can experiment with `py` again:
|
||||
|
||||
> py self.cmdset.add("commands.mycommands.MyCmdSet")
|
||||
|
||||
Now try
|
||||
|
||||
> echo
|
||||
Command echo has no defined `func()` - showing on-command variables:
|
||||
...
|
||||
...
|
||||
|
||||
You should be getting a long list of outputs. The reason for this is that your `echo` function is not really
|
||||
"doing" anything yet and the default function is then to show all useful resources available to you when you
|
||||
use your Command. Let's look at some of those listed:
|
||||
|
||||
Command echo has no defined `func()` - showing on-command variables:
|
||||
obj (<class 'typeclasses.characters.Character'>): YourName
|
||||
lockhandler (<class 'evennia.locks.lockhandler.LockHandler'>): cmd:all()
|
||||
caller (<class 'typeclasses.characters.Character'>): YourName
|
||||
cmdname (<class 'str'>): echo
|
||||
raw_cmdname (<class 'str'>): echo
|
||||
cmdstring (<class 'str'>): echo
|
||||
args (<class 'str'>):
|
||||
cmdset (<class 'evennia.commands.cmdset.CmdSet'>): @mail, about, access, accounts, addcom, alias, allcom, ban, batchcode, batchcommands, boot, cboot, ccreate,
|
||||
cdesc, cdestroy, cemit, channels, charcreate, chardelete, checklockstring, clientwidth, clock, cmdbare, cmdsets, color, copy, cpattr, create, cwho, delcom,
|
||||
desc, destroy, dig, dolphin, drop, echo, emit, examine, find, force, get, give, grapevine2chan, help, home, ic, inventory, irc2chan, ircstatus, link, lock,
|
||||
look, menutest, mudinfo, mvattr, name, nick, objects, ooc, open, option, page, password, perm, pose, public, py, quell, quit, reload, reset, rss2chan, say,
|
||||
script, scripts, server, service, sessions, set, setdesc, sethelp, sethome, shutdown, spawn, style, tag, tel, test2010, test2028, testrename, testtable,
|
||||
tickers, time, tunnel, typeclass, unban, unlink, up, up, userpassword, wall, whisper, who, wipe
|
||||
session (<class 'evennia.server.serversession.ServerSession'>): Griatch(#1)@1:2:7:.:0:.:0:.:1
|
||||
account (<class 'typeclasses.accounts.Account'>): Griatch(account 1)
|
||||
raw_string (<class 'str'>): echo
|
||||
|
||||
--------------------------------------------------
|
||||
echo - Command variables from evennia:
|
||||
--------------------------------------------------
|
||||
name of cmd (self.key): echo
|
||||
cmd aliases (self.aliases): []
|
||||
cmd locks (self.locks): cmd:all();
|
||||
help category (self.help_category): General
|
||||
object calling (self.caller): Griatch
|
||||
object storing cmdset (self.obj): Griatch
|
||||
command string given (self.cmdstring): echo
|
||||
current cmdset (self.cmdset): ChannelCmdSet
|
||||
|
||||
These are all properties you can access with `.` on the Command instance, such as `.key`, `.args` and so on.
|
||||
Evennia makes these available to you and they will be different every time a command is run. The most
|
||||
important ones we will make use of now are:
|
||||
|
||||
- `caller` - this is 'you', the person calling the command.
|
||||
- `args` - this is all arguments to the command. Now it's empty, but if you tried `echo foo bar` you'd find
|
||||
that this would be `" foo bar"`.
|
||||
- `obj` - this is object on which this Command (and CmdSet) "sits". So you, in this case.
|
||||
|
||||
The reason our command doesn't do anything yet is because it's missing a `func` method. This is what Evennia
|
||||
looks for to figure out what a Command actually does. Modify your `CmdEcho` class:
|
||||
|
||||
```python
|
||||
# ...
|
||||
|
||||
class CmdEcho(Command):
|
||||
"""
|
||||
A simple echo command
|
||||
|
||||
Usage:
|
||||
echo <something>
|
||||
|
||||
"""
|
||||
key = "echo"
|
||||
|
||||
def func(self):
|
||||
self.caller.msg(f"Echo: '{self.args}'")
|
||||
|
||||
# ...
|
||||
```
|
||||
|
||||
First we added a docstring. This is always a good thing to do in general, but for a Command class, it will also
|
||||
automatically become the in-game help entry! Next we add the `func` method. It has one active line where it
|
||||
makes use of some of those variables we found the Command offers to us. If you did the
|
||||
[basic Python tutorial](./Python-basic-introduction.md), you will recognize `.msg` - this will send a message
|
||||
to the object it is attached to us - in this case `self.caller`, that is, us. We grab `self.args` and includes
|
||||
that in the message.
|
||||
|
||||
Since we haven't changed `MyCmdSet`, that will work as before. Reload and re-add this command to ourselves to
|
||||
try out the new version:
|
||||
|
||||
> reload
|
||||
> py self.cmdset.add("commands.mycommands.MyCmdSet")
|
||||
> echo
|
||||
Echo: ''
|
||||
|
||||
Try to pass an argument:
|
||||
|
||||
> echo Woo Tang!
|
||||
Echo: ' Woo Tang!'
|
||||
|
||||
Note that there is an extra space before `Woo!`. That is because self.args contains the _everything_ after
|
||||
the command name, including spaces. Evennia will happily understand if you skip that space too:
|
||||
|
||||
> echoWoo Tang!
|
||||
Echo: 'Woo Tang!'
|
||||
|
||||
There are ways to force Evennia to _require_ an initial space, but right now we want to just ignore it since
|
||||
it looks a bit weird for our echo example. Tweak the code:
|
||||
|
||||
```python
|
||||
# ...
|
||||
|
||||
class CmdEcho(Command):
|
||||
"""
|
||||
A simple echo command
|
||||
|
||||
Usage:
|
||||
echo <something>
|
||||
|
||||
"""
|
||||
key = "echo"
|
||||
|
||||
def func(self):
|
||||
self.caller.msg(f"Echo: '{self.args.strip()}'")
|
||||
|
||||
# ...
|
||||
```
|
||||
|
||||
The only difference is that we called `.strip()` on `self.args`. This is a helper method available on all
|
||||
strings - it strips out all whitespace before and after the string. Now the Command-argument will no longer
|
||||
have any space in front of it.
|
||||
|
||||
> reload
|
||||
> py self.cmdset.add("commands.mycommands.MyCmdSet")
|
||||
> echo Woo Tang!
|
||||
Echo: 'Woo Tang!'
|
||||
|
||||
Don't forget to look at the help for the echo command:
|
||||
|
||||
> help echo
|
||||
|
||||
You will get the docstring you put in your Command-class.
|
||||
|
||||
### Making our cmdset persistent
|
||||
|
||||
It's getting a little annoying to have to re-add our cmdset every time we reload, right? It's simple
|
||||
enough to make `echo` a _persistent_ change though:
|
||||
|
||||
> py self.cmdset.add("commands.mycommands.MyCmdSet", persistent=True)
|
||||
|
||||
Now you can `reload` as much as you want and your code changes will be available directly without
|
||||
needing to re-add the MyCmdSet again. To remove the cmdset again, do
|
||||
|
||||
> py self.cmdset.remove("commands.mycommands.MyCmdSet")
|
||||
|
||||
But for now, keep it around, we'll expand it with some more examples.
|
||||
|
||||
### Figuring out who to hit
|
||||
|
||||
Let's try something a little more exciting than just echo. Let's make a `hit` command, for punching
|
||||
someone in the face! This is how we want it to work:
|
||||
|
||||
> hit <target>
|
||||
You hit <target> with full force!
|
||||
|
||||
Not only that, we want the <target> to see
|
||||
|
||||
You got hit by <hitter> with full force!
|
||||
|
||||
Here, `<hitter>` would be the one using the `hit` command and `<target>` is the one doing the punching.
|
||||
|
||||
Still in `mygame/commands/mycommands.py`, add a new class, between `CmdEcho` and `MyCmdSet`.
|
||||
|
||||
```python
|
||||
# ...
|
||||
|
||||
class CmdHit(Command):
|
||||
"""
|
||||
Hit a target.
|
||||
|
||||
Usage:
|
||||
hit <target>
|
||||
|
||||
"""
|
||||
key = "hit"
|
||||
|
||||
def func(self):
|
||||
args = self.args.strip()
|
||||
if not args:
|
||||
self.caller.msg("Who do you want to hit?")
|
||||
return
|
||||
target = self.caller.search(args)
|
||||
if not target:
|
||||
return
|
||||
self.caller.msg(f"You hit {target.key} with full force!")
|
||||
target.msg(f"You got hit by {self.caller.key} with full force!")
|
||||
# ...
|
||||
|
||||
```
|
||||
|
||||
A lot of things to dissect here:
|
||||
- **Line 4**: The normal `class` header. We inherit from `Command` which we imported at the top of this file.
|
||||
- **Lines 5**-11: The docstring and help-entry for the command. You could expand on this as much as you wanted.
|
||||
- **Line 12**: We want to write `hit` to use this command.
|
||||
- **Line 15**: We strip the whitespace from the argument like before. Since we don't want to have to do
|
||||
`self.args.strip()` over and over, we store the stripped version
|
||||
in a _local variable_ `args`. Note that we don't modify `self.args` by doing this, `self.args` will still
|
||||
have the whitespace and is not the same as `args` in this example.
|
||||
```{sidebar} if-statements
|
||||
|
||||
The full form of the if statement is
|
||||
|
||||
if condition:
|
||||
...
|
||||
elif othercondition:
|
||||
...
|
||||
else:
|
||||
...
|
||||
|
||||
There can be any number of `elifs` to mark when different branches of the code should run. If
|
||||
the `else` condition is given, it will run if none of the other conditions was truthy. In Python
|
||||
the `if..elif..else` structure also serves the same function as `case` in some other languages.
|
||||
|
||||
```
|
||||
- **Line 16** has our first _conditional_, an `if` statement. This is written on the form `if <condition>:` and only
|
||||
if that condition is 'truthy' will the indented code block under the `if` statement run. To learn what is truthy in
|
||||
Python it's usually easier to learn what is "falsy":
|
||||
- `False` - this is a reserved boolean word in Python. The opposite is `True`.
|
||||
- `None` - another reserved word. This represents nothing, a null-result or value.
|
||||
- `0` or `0.0`
|
||||
- The empty string `""` or `''` or `""""""` or `''''''`
|
||||
- Empty _iterables_ we haven't seen yet, like empty lists `[]`, empty tuples `()` and empty dicts `{}`.
|
||||
- Everything else is "truthy".
|
||||
|
||||
Line 16's condition is `not args`. The `not` _inverses_ the result, so if `args` is the empty string (falsy), the
|
||||
whole conditional becomes truthy. Let's continue in the code:
|
||||
- **Lines 17-18**: This code will only run if the `if` statement is truthy, in this case if `args` is the empty string.
|
||||
- **Line 18**: `return` is a reserved Python word that exits `func` immediately.
|
||||
- **Line 19**: We use `self.caller.search` to look for the target in the current location.
|
||||
- **Lines 20-21**: A feature of `.search` is that it will already inform `self.caller` if it couldn't find the target.
|
||||
In that case, `target` will be `None` and we should just directly `return`.
|
||||
- **Lines 22-23**: At this point we have a suitable target and can send our punching strings to each.
|
||||
|
||||
Finally we must also add this to a CmdSet. Let's add it to `MyCmdSet` which we made persistent earlier.
|
||||
|
||||
```python
|
||||
# ...
|
||||
|
||||
class MyCmdSet(CmdSet):
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
self.add(CmdEcho)
|
||||
self.add(CmdHit)
|
||||
|
||||
```
|
||||
|
||||
```{sidebar} Errors in your code
|
||||
|
||||
With longer code snippets to try, it gets more and more likely you'll
|
||||
make an error and get a `traceback` when you reload. This will either appear
|
||||
directly in-game or in your log (view it with `evennia -l` in a terminal).
|
||||
Don't panic; tracebacks are your friends - they are to be read bottom-up and usually describe
|
||||
exactly where your problem is. Refer to `The Python intro <Python-basic-introduction.html>`_ for
|
||||
more hints. If you get stuck, reach out to the Evennia community for help.
|
||||
|
||||
```
|
||||
|
||||
Next we reload to let Evennia know of these code changes and try it out:
|
||||
|
||||
> reload
|
||||
hit
|
||||
Who do you want to hit?
|
||||
hit me
|
||||
You hit YourName with full force!
|
||||
You got hit by YourName with full force!
|
||||
|
||||
Lacking a target, we hit ourselves. If you have one of the dragons still around from the previous lesson
|
||||
you could try to hit it (if you dare):
|
||||
|
||||
hit smaug
|
||||
You hit Smaug with full force!
|
||||
|
||||
You won't see the second string. Only Smaug sees that (and is not amused).
|
||||
|
||||
|
||||
## Summary
|
||||
|
||||
In this lesson we learned how to create our own Command, add it to a CmdSet and then to ourselves.
|
||||
We also upset a dragon.
|
||||
|
||||
In the next lesson we'll learn how to hit Smaug with different weapons. We'll also
|
||||
get into how we replace and extend Evennia's default Commands.
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
# Part 1: What we have
|
||||
|
||||
```{eval-rst}
|
||||
.. sidebar:: Beginner Tutorial Parts
|
||||
|
||||
`Introduction <../Beginner-Tutorial-Intro.html>`_
|
||||
Getting set up.
|
||||
**Part 1: What we have**
|
||||
A tour of Evennia and how to use the tools, including an introduction to Python.
|
||||
Part 2: `What we want <../Part2/Beginner-Tutorial-Part2-Intro.html>`_
|
||||
Planning our tutorial game and what to think about when planning your own in the future.
|
||||
Part 3: `How we get there <../Part3/Beginner-Tutorial-Part3-Intro.html>`_
|
||||
Getting down to the meat of extending Evennia to make our game
|
||||
Part 4: `Using what we created <../Part4/Beginner-Tutorial-Part4-Intro.html>`_
|
||||
Building a tech-demo and world content to go with our code
|
||||
Part 5: `Showing the world <../Part5/Beginner-Tutorial-Part5-Intro.html>`_
|
||||
Taking our new game online and let players try it out
|
||||
|
||||
```
|
||||
|
||||
In this first part we'll focus on what we get out of the box in Evennia - we'll get used to the tools,
|
||||
and how to find things we are looking for. We will also dive into some of things you'll
|
||||
need to know to fully utilize the system, including giving you a brief rundown of Python concepts. If you are
|
||||
an experienced Python programmer, some sections may feel a bit basic, but you will at least not have seen
|
||||
these concepts in the context of Evennia before.
|
||||
|
||||
## Lessons
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 1
|
||||
:numbered:
|
||||
|
||||
Building-Quickstart
|
||||
Tutorial-World
|
||||
Python-basic-introduction
|
||||
Gamedir-Overview
|
||||
Python-classes-and-objects
|
||||
Evennia-Library-Overview
|
||||
Learning-Typeclasses
|
||||
Adding-Commands
|
||||
More-on-Commands
|
||||
Creating-Things
|
||||
Searching-Things
|
||||
Django-queries
|
||||
|
||||
```
|
||||
|
||||
## Table of Contents
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 2
|
||||
|
||||
Building-Quickstart
|
||||
Tutorial-World
|
||||
Python-basic-introduction
|
||||
Gamedir-Overview
|
||||
Python-classes-and-objects
|
||||
Evennia-Library-Overview
|
||||
Learning-Typeclasses
|
||||
Adding-Commands
|
||||
More-on-Commands
|
||||
Creating-Things
|
||||
Searching-Things
|
||||
Django-queries
|
||||
|
||||
```
|
||||
|
|
@ -0,0 +1,311 @@
|
|||
# Using commands and building stuff
|
||||
|
||||
In this lesson we will test out what we can do in-game out-of-the-box. Evennia ships with
|
||||
[around 90 default commands](../../../Components/Default-Commands.md), and while you can override those as you please,
|
||||
they can be quite useful.
|
||||
|
||||
Connect and log into your new game and you will end up in the "Limbo" location. This
|
||||
is the only room in the game at this point. Let's explore the commands a little.
|
||||
|
||||
The default commands has syntax [similar to MUX](../../../Concepts/Using-MUX-as-a-Standard.md):
|
||||
|
||||
command[/switch/switch...] [arguments ...]
|
||||
|
||||
An example would be
|
||||
|
||||
create/drop box
|
||||
|
||||
A _/switch_ is a special, optional flag to the command to make it behave differently. It is always
|
||||
put directly after the command name, and begins with a forward slash (`/`). The _arguments_ are one
|
||||
or more inputs to the commands. It's common to use an equal sign (`=`) when assigning something to
|
||||
an object.
|
||||
|
||||
> Are you used to commands starting with @, like @create? That will work too. Evennia simply ignores
|
||||
> the preceeding @.
|
||||
|
||||
## Getting help
|
||||
|
||||
help
|
||||
|
||||
Will give you a list of all commands available to you. Use
|
||||
|
||||
help <commandname>
|
||||
|
||||
to see the in-game help for that command.
|
||||
|
||||
## Looking around
|
||||
|
||||
The most common comman is
|
||||
|
||||
look
|
||||
|
||||
This will show you the description of the current location. `l` is an alias.
|
||||
|
||||
When targeting objects in commands you have two special labels you can use, `here` for the current
|
||||
room or `me`/`self` to point back to yourself. So
|
||||
|
||||
look me
|
||||
|
||||
will give you your own description. `look here` is, in this case, the same as plain `look`.
|
||||
|
||||
|
||||
## Stepping Down From Godhood
|
||||
|
||||
If you just installed Evennia, your very first player account is called user #1, also known as the
|
||||
_superuser_ or _god user_. This user is very powerful, so powerful that it will override many game
|
||||
restrictions such as locks. This can be useful, but it also hides some functionality that you might
|
||||
want to test.
|
||||
|
||||
To temporarily step down from your superuser position you can use the `quell` command in-game:
|
||||
|
||||
quell
|
||||
|
||||
This will make you start using the permission of your current character's level instead of your
|
||||
superuser level. If you didn't change any settings your game Character should have an _Developer_
|
||||
level permission - high as can be without bypassing locks like the superuser does. This will work
|
||||
fine for the examples on this page. Use
|
||||
|
||||
unquell
|
||||
|
||||
to get superuser status again when you are done.
|
||||
|
||||
## Creating an Object
|
||||
|
||||
Basic objects can be anything -- swords, flowers and non-player characters. They are created using
|
||||
the `create` command:
|
||||
|
||||
create box
|
||||
|
||||
This created a new 'box' (of the default object type) in your inventory. Use the command `inventory`
|
||||
(or `i`) to see it. Now, 'box' is a rather short name, let's rename it and tack on a few aliases.
|
||||
|
||||
name box = very large box;box;very;crate
|
||||
|
||||
```{warning} MUD clients and semi-colon
|
||||
Some traditional MUD clients use the semi-colon `;` to separate client inputs. If so,
|
||||
the above line will give an error. You need to change your client to use another command-separator
|
||||
or to put it in 'verbatim' mode. If you still have trouble, use the Evennia web client instead.
|
||||
|
||||
```
|
||||
|
||||
|
||||
We now renamed the box to _very large box_ (and this is what we will see when looking at it), but we
|
||||
will also recognize it by any of the other names we give - like _crate_ or simply _box_ as before.
|
||||
We could have given these aliases directly after the name in the `create` command, this is true for
|
||||
all creation commands - you can always tag on a list of `;`-separated aliases to the name of your
|
||||
new object. If you had wanted to not change the name itself, but to only add aliases, you could have
|
||||
used the `alias` command.
|
||||
|
||||
We are currently carrying the box. Let's drop it (there is also a short cut to create and drop in
|
||||
one go by using the `/drop` switch, for example `create/drop box`).
|
||||
|
||||
drop box
|
||||
|
||||
Hey presto - there it is on the ground, in all its normality.
|
||||
|
||||
examine box
|
||||
|
||||
This will show some technical details about the box object. For now we will ignore what this
|
||||
information means.
|
||||
|
||||
Try to `look` at the box to see the (default) description.
|
||||
|
||||
look box
|
||||
You see nothing special.
|
||||
|
||||
The description you get is not very exciting. Let's add some flavor.
|
||||
|
||||
describe box = This is a large and very heavy box.
|
||||
|
||||
If you try the `get` command we will pick up the box. So far so good, but if we really want this to
|
||||
be a large and heavy box, people should _not_ be able to run off with it that easily. To prevent
|
||||
this we need to lock it down. This is done by assigning a _Lock_ to it. Make sure the box was
|
||||
dropped in the room, then try this:
|
||||
|
||||
lock box = get:false()
|
||||
|
||||
Locks represent a rather [big topic](../../../Components/Locks.md), but for now that will do what we want. This will lock
|
||||
the box so noone can lift it. The exception is superusers, they override all locks and will pick it
|
||||
up anyway. Make sure you are quelling your superuser powers and try to get the box now:
|
||||
|
||||
> get box
|
||||
You can't get that.
|
||||
|
||||
Think thís default error message looks dull? The `get` command looks for an [Attribute](../../../Components/Attributes.md)
|
||||
named `get_err_msg` for returning a nicer error message (we just happen to know this, you would need
|
||||
to peek into the
|
||||
[code](https://github.com/evennia/evennia/blob/master/evennia/commands/default/general.py#L235) for
|
||||
the `get` command to find out.). You set attributes using the `set` command:
|
||||
|
||||
set box/get_err_msg = It's way too heavy for you to lift.
|
||||
|
||||
Try to get it now and you should see a nicer error message echoed back to you. To see what this
|
||||
message string is in the future, you can use 'examine.'
|
||||
|
||||
examine box/get_err_msg
|
||||
|
||||
Examine will return the value of attributes, including color codes. `examine here/desc` would return
|
||||
the raw description of your current room (including color codes), so that you can copy-and-paste to
|
||||
set its description to something else.
|
||||
|
||||
You create new Commands (or modify existing ones) in Python outside the game. We will get to that
|
||||
later, in the [Commands tutorial](./Adding-Commands.md).
|
||||
|
||||
## Get a Personality
|
||||
|
||||
[Scripts](../../../Components/Scripts.md) are powerful out-of-character objects useful for many "under the hood" things.
|
||||
One of their optional abilities is to do things on a timer. To try out a first script, let's put one
|
||||
on ourselves. There is an example script in `evennia/contrib/tutorial_examples/bodyfunctions.py`
|
||||
that is called `BodyFunctions`. To add this to us we will use the `script` command:
|
||||
|
||||
script self = tutorial_examples.bodyfunctions.BodyFunctions
|
||||
|
||||
This string will tell Evennia to dig up the Python code at the place we indicate. It already knows
|
||||
to look in the `contrib/` folder, so we don't have to give the full path.
|
||||
|
||||
> Note also how we use `.` instead of `/` (or `\` on Windows). This is a so-called "Python path". In a Python-path,
|
||||
> you separate the parts of the path with `.` and skip the `.py` file-ending. Importantly, it also allows you to point to
|
||||
Python code _inside_ files, like the `BodyFunctions` class inside `bodyfunctions.py` (we'll get to classes later).
|
||||
These "Python-paths" are used extensively throughout Evennia.
|
||||
|
||||
Wait a while and you will notice yourself starting making random observations ...
|
||||
|
||||
script self
|
||||
|
||||
This will show details about scripts on yourself (also `examine` works). You will see how long it is
|
||||
until it "fires" next. Don't be alarmed if nothing happens when the countdown reaches zero - this
|
||||
particular script has a randomizer to determine if it will say something or not. So you will not see
|
||||
output every time it fires.
|
||||
|
||||
When you are tired of your character's "insights", kill the script with
|
||||
|
||||
script/stop self = tutorial_examples.bodyfunctions.BodyFunctions
|
||||
|
||||
You create your own scripts in Python, outside the game; the path you give to `script` is literally
|
||||
the Python path to your script file. The [Scripts](../../../Components/Scripts.md) page explains more details.
|
||||
|
||||
## Pushing Your Buttons
|
||||
|
||||
If we get back to the box we made, there is only so much fun you can have with it at this point. It's
|
||||
just a dumb generic object. If you renamed it to `stone` and changed its description, noone would be
|
||||
the wiser. However, with the combined use of custom [Typeclasses](../../../Components/Typeclasses.md), [Scripts](../../../Components/Scripts.md)
|
||||
and object-based [Commands](../../../Components/Commands.md), you could expand it and other items to be as unique, complex
|
||||
and interactive as you want.
|
||||
|
||||
Let's take an example. So far we have only created objects that use the default object typeclass
|
||||
named simply `Object`. Let's create an object that is a little more interesting. Under
|
||||
`evennia/contrib/tutorial_examples` there is a module `red_button.py`. It contains the enigmatic
|
||||
`RedButton` class.
|
||||
|
||||
Let's make us one of _those_!
|
||||
|
||||
create/drop button:tutorial_examples.red_button.RedButton
|
||||
|
||||
The same way we did with the Script Earler, we specify a "Python-path" to the Python code we want Evennia
|
||||
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/tutorials/red_button](evennia.contrib.tutorials.red_button)
|
||||
|
||||
If you wait for a while (make sure you dropped it!) the button will blink invitingly.
|
||||
|
||||
Why don't you try to push it ...?
|
||||
|
||||
Surely a big red button is meant to be pushed.
|
||||
|
||||
You know you want to.
|
||||
|
||||
```{warning} Don't press the invitingly blinking red button.
|
||||
```
|
||||
|
||||
## Making Yourself a House
|
||||
|
||||
The main command for shaping the game world is `dig`. For example, if you are standing in Limbo you
|
||||
can dig a route to your new house location like this:
|
||||
|
||||
dig house = large red door;door;in,to the outside;out
|
||||
|
||||
This will create a new room named 'house'. Spaces at the start/end of names and aliases are ignored
|
||||
so you could put more air if you wanted. This call will directly create an exit from your current
|
||||
location named 'large red door' and a corresponding exit named 'to the outside' in the house room
|
||||
leading back to Limbo. We also define a few aliases to those exits, so people don't have to write
|
||||
the full thing all the time.
|
||||
|
||||
If you wanted to use normal compass directions (north, west, southwest etc), you could do that with
|
||||
`dig` too. But Evennia also has a limited version of `dig` that helps for compass directions (and
|
||||
also up/down and in/out). It's called `tunnel`:
|
||||
|
||||
tunnel sw = cliff
|
||||
|
||||
This will create a new room "cliff" with an exit "southwest" leading there and a path "northeast"
|
||||
leading back from the cliff to your current location.
|
||||
|
||||
You can create new exits from where you are, using the `open` command:
|
||||
|
||||
open north;n = house
|
||||
|
||||
This opens an exit `north` (with an alias `n`) to the previously created room `house`.
|
||||
|
||||
If you have many rooms named `house` you will get a list of matches and have to select which one you
|
||||
want to link to.
|
||||
|
||||
Follow the north exit to your 'house' or `teleport` to it:
|
||||
|
||||
north
|
||||
|
||||
or:
|
||||
|
||||
teleport house
|
||||
|
||||
To manually open an exit back to Limbo (if you didn't do so with the `dig` command):
|
||||
|
||||
open door = limbo
|
||||
|
||||
(You can also us the #dbref of limbo, which you can find by using `examine here` when in limbo).
|
||||
|
||||
## Reshuffling the World
|
||||
|
||||
You can find things using the `find` command. Assuming you are back at `Limbo`, let's teleport the
|
||||
_large box_ to our house.
|
||||
|
||||
teleport box = house
|
||||
very large box is leaving Limbo, heading for house.
|
||||
Teleported very large box -> house.
|
||||
|
||||
We can still find the box by using find:
|
||||
|
||||
find box
|
||||
One Match(#1-#8):
|
||||
very large box(#8) - src.objects.objects.Object
|
||||
|
||||
Knowing the `#dbref` of the box (#8 in this example), you can grab the box and get it back here
|
||||
without actually yourself going to `house` first:
|
||||
|
||||
teleport #8 = here
|
||||
|
||||
As mentioned, `here` is an alias for 'your current location'. The box should now be back in Limbo with you.
|
||||
|
||||
We are getting tired of the box. Let's destroy it.
|
||||
|
||||
destroy box
|
||||
|
||||
It will ask you for confirmation. Once you give it, the box will be gone.
|
||||
|
||||
You can destroy many objects in one go by giving a comma-separated list of objects (or a range
|
||||
of #dbrefs, if they are not in the same location) to the command.
|
||||
|
||||
## Adding a Help Entry
|
||||
|
||||
The Command-help is something you modify in Python code. We'll get to that when we get to how to
|
||||
add Commands. But you can also add regular help entries, for example to explain something about
|
||||
the history of your game world:
|
||||
|
||||
sethelp/add History = At the dawn of time ...
|
||||
|
||||
You will now find your new `History` entry in the `help` list and read your help-text with `help History`.
|
||||
|
||||
## Adding a World
|
||||
|
||||
After this brief introduction to building and using in-game commands you may be ready to see a more fleshed-out
|
||||
example. Evennia comes with a tutorial world for you to explore. We will try that out in the next lesson.
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
# Creating things
|
||||
|
||||
We have already created some things - dragons for example. There are many different things to create
|
||||
in Evennia though. In the last lesson we learned about typeclasses, the way to make objects persistent in the database.
|
||||
|
||||
Given the path to a Typeclass, there are three ways to create an instance of it:
|
||||
|
||||
- Firstly, you can call the class directly, and then `.save()` it:
|
||||
|
||||
obj = SomeTypeClass(db_key=...)
|
||||
obj.save()
|
||||
|
||||
This has the drawback of being two operations; you must also import the class and have to pass
|
||||
the actual database field names, such as `db_key` instead of `key` as keyword arguments.
|
||||
- Secondly you can use the Evennia creation helpers:
|
||||
|
||||
obj = evennia.create_object(SomeTypeClass, key=...)
|
||||
|
||||
This is the recommended way if you are trying to create things in Python. The first argument can either be
|
||||
the class _or_ the python-path to the typeclass, like `"path.to.SomeTypeClass"`. It can also be `None` in which
|
||||
case the Evennia default will be used. While all the creation methods
|
||||
are available on `evennia`, they are actually implemented in [evennia/utils/create.py](../../../api/evennia.utils.create.md).
|
||||
- Finally, you can create objects using an in-game command, such as
|
||||
|
||||
create/drop obj:path.to.SomeTypeClass
|
||||
|
||||
As a developer you are usually best off using the two other methods, but a command is usually the only way
|
||||
to let regular players or builders without Python-access help build the game world.
|
||||
|
||||
## Creating Objects
|
||||
|
||||
This is one of the most common creation-types. These are entities that inherits from `DefaultObject` at any distance.
|
||||
They have an existence in the game world and includes rooms, characters, exits, weapons, flower pots and castles.
|
||||
|
||||
> py
|
||||
> import evennia
|
||||
> rose = evennia.create_object(key="rose")
|
||||
|
||||
Since we didn't specify the `typeclass` as the first argument, the default given by `settings.BASE_OBJECT_TYPECLASS`
|
||||
(`typeclasses.objects.Object`) will be used.
|
||||
|
||||
## Creating Accounts
|
||||
|
||||
An _Account_ is an out-of-character (OOC) entity, with no existence in the game world.
|
||||
You can find the parent class for Accounts in `typeclasses/accounts.py`.
|
||||
|
||||
_TODO_
|
||||
|
||||
397
docs/source/Howtos/Beginner-Tutorial/Part1/Django-queries.md
Normal file
397
docs/source/Howtos/Beginner-Tutorial/Part1/Django-queries.md
Normal file
|
|
@ -0,0 +1,397 @@
|
|||
# Advanced searching - Django Database queries
|
||||
|
||||
```{important} More advanced lesson!
|
||||
|
||||
Learning about Django's queryset language is very useful once you start doing more advanced things
|
||||
in Evennia. But it's not strictly needed out the box and can be a little overwhelming for a first
|
||||
reading. So if you are new to Python and Evennia, feel free to just skim this lesson and refer
|
||||
back to it later when you've gained more experience.
|
||||
```
|
||||
|
||||
The search functions and methods we used in the previous lesson are enough for most cases.
|
||||
But sometimes you need to be more specific:
|
||||
|
||||
- You want to find all `Characters` ...
|
||||
- ... who are in Rooms tagged as `moonlit` ...
|
||||
- ... _and_ who has the Attribute `lycantrophy` with a level higher than 2 ...
|
||||
- ... because they'll should immediately transform to werewolves!
|
||||
|
||||
In principle you could achieve this with the existing search functions combined with a lot of loops
|
||||
and if statements. But for something non-standard like this, querying the database directly will be
|
||||
much more efficient.
|
||||
|
||||
Evennia uses [Django](https://www.djangoproject.com/) to handle its connection to the database.
|
||||
A [django queryset](https://docs.djangoproject.com/en/3.0/ref/models/querysets/) represents
|
||||
a database query. One can add querysets together to build ever-more complicated queries. Only when
|
||||
you are trying to use the results of the queryset will it actually call the database.
|
||||
|
||||
The normal way to build a queryset is to define what class of entity you want to search by getting its
|
||||
`.objects` resource, and then call various methods on that. We've seen this one before:
|
||||
|
||||
all_weapons = Weapon.objects.all()
|
||||
|
||||
This is now a queryset representing all instances of `Weapon`. If `Weapon` had a subclass `Cannon` and we
|
||||
only wanted the cannons, we would do
|
||||
|
||||
all_cannons = Cannon.objects.all()
|
||||
|
||||
Note that `Weapon` and `Cannon` are different typeclasses. You won't find any `Cannon` instances in
|
||||
the `all_weapon` result above, confusing as that may sound. To get instances of a Typeclass _and_ the
|
||||
instances of all its children classes you need to use `_family`:
|
||||
|
||||
```{sidebar} _family
|
||||
|
||||
The all_family, filter_family etc is an Evennia-specific
|
||||
thing. It's not part of regular Django.
|
||||
|
||||
```
|
||||
|
||||
really_all_weapons = Weapon.objects.all_family()
|
||||
|
||||
This result now contains both `Weapon` and `Cannon` instances.
|
||||
|
||||
To limit your search by other criteria than the Typeclass you need to use `.filter`
|
||||
(or `.filter_family`) instead:
|
||||
|
||||
roses = Flower.objects.filter(db_key="rose")
|
||||
|
||||
This is a queryset representing all objects having a `db_key` equal to `"rose"`.
|
||||
Since this is a queryset you can keep adding to it; this will act as an `AND` condition.
|
||||
|
||||
local_roses = roses.filter(db_location=myroom)
|
||||
|
||||
We could also have written this in one statement:
|
||||
|
||||
local_roses = Flower.objects.filter(db_key="rose", db_location=myroom)
|
||||
|
||||
We can also `.exclude` something from results
|
||||
|
||||
local_non_red_roses = local_roses.exclude(db_key="red_rose")
|
||||
|
||||
Only until we actually try to examine the result will the database be called. Here it's called when we
|
||||
try to loop over the queryset:
|
||||
|
||||
for rose in local_non_red_roses:
|
||||
print(rose)
|
||||
|
||||
From now on, the queryset is _evaluated_ and we can't keep adding more queries to it - we'd need to
|
||||
create a new queryset if we wanted to find some other result. Other ways to evaluate the queryset is to
|
||||
print it, convert it to a list with `list()` and otherwise try to access its results.
|
||||
|
||||
Note how we use `db_key` and `db_location`. This is the actual names of these database fields. By convention
|
||||
Evennia uses `db_` in front of every database field. When you use the normal Evennia search helpers and objects
|
||||
you can skip the `db_` but here we are calling the database directly and need to use the 'real' names.
|
||||
|
||||
Here are the most commonly used methods to use with the `objects` managers:
|
||||
|
||||
- `filter` - query for a listing of objects based on search criteria. Gives empty queryset if none
|
||||
were found.
|
||||
- `get` - query for a single match - raises exception if none were found, or more than one was
|
||||
found.
|
||||
- `all` - get all instances of the particular type.
|
||||
- `filter_family` - like `filter`, but search all sub classes as well.
|
||||
- `get_family` - like `get`, but search all sub classes as well.
|
||||
- `all_family` - like `all`, but return entities of all subclasses as well.
|
||||
|
||||
> All of Evennia search functions use querysets under the hood. The `evennia.search_*` functions actually
|
||||
> return querysets, which means you could in principle keep adding queries to their results as well.
|
||||
|
||||
|
||||
## Queryset field lookups
|
||||
|
||||
Above we found roses with exactly the `db_key` `"rose"`. This is an _exact_ match that is _case sensitive_,
|
||||
so it would not find `"Rose"`.
|
||||
|
||||
# this is case-sensitive and the same as =
|
||||
roses = Flower.objects.filter(db_key__exact="rose"
|
||||
|
||||
# the i means it's case-insensitive
|
||||
roses = Flower.objects.filter(db_key__iexact="rose")
|
||||
|
||||
The Django field query language uses `__` in the same way as Python uses `.` to access resources. This
|
||||
is because `.` is not allowed in a function keyword.
|
||||
|
||||
roses = Flower.objects.filter(db_key__icontains="rose")
|
||||
|
||||
This will find all flowers whose name contains the string `"rose"`, like `"roses"`, `"wild rose"` etc. The
|
||||
`i` in the beginning makes the search case-insensitive. Other useful variations to use
|
||||
are `__istartswith` and `__iendswith`. You can also use `__gt`, `__ge` for "greater-than"/"greater-or-equal-than"
|
||||
comparisons (same for `__lt` and `__le`). There is also `__in`:
|
||||
|
||||
swords = Weapons.objects.filter(db_key__in=("rapier", "two-hander", "shortsword"))
|
||||
|
||||
One also uses `__` to access foreign objects like Tags. Let's for example assume this is how we identify mages:
|
||||
|
||||
char.tags.add("mage", category="profession")
|
||||
|
||||
Now, in this case we have an Evennia helper to do this search:
|
||||
|
||||
mages = evennia.search_tags("mage", category="profession")
|
||||
|
||||
But this will find all Objects with this tag+category. Maybe you are only looking for Vampire mages:
|
||||
|
||||
sparkly_mages = Vampire.objects.filter(db_tags__db_key="mage", db_tags__db_category="profession")
|
||||
|
||||
This looks at the `db_tags` field on the `Vampire` and filters on the values of each tag's
|
||||
`db_key` and `db_category` together.
|
||||
|
||||
For more field lookups, see the
|
||||
[django docs](https://docs.djangoproject.com/en/3.0/ref/models/querysets/#field-lookups) on the subject.
|
||||
|
||||
## Get that werewolf ...
|
||||
|
||||
Let's see if we can make a query for the werewolves in the moonlight we mentioned at the beginning
|
||||
of this section.
|
||||
|
||||
Firstly, we make ourselves and our current location match the criteria, so we can test:
|
||||
|
||||
> py here.tags.add("moonlit")
|
||||
> py me.db.lycantrophy = 3
|
||||
|
||||
This is an example of a more complex query. We'll consider it an example of what is
|
||||
possible.
|
||||
|
||||
```{sidebar} Line breaks
|
||||
|
||||
Note the way of writing this code. It would have been very hard to read if we just wrote it in
|
||||
one long line. But since we wrapped it in `(...)` we can spread it out over multiple lines
|
||||
without worrying about line breaks!
|
||||
```
|
||||
|
||||
```python
|
||||
from typeclasses.characters import Character
|
||||
|
||||
will_transform = (
|
||||
Character.objects
|
||||
.filter(
|
||||
db_location__db_tags__db_key__iexact="moonlit",
|
||||
db_attributes__db_key="lycantrophy",
|
||||
db_attributes__db_value__gt=2)
|
||||
)
|
||||
```
|
||||
|
||||
- **Line 3** - We want to find `Character`s, so we access `.objects` on the `Character` typeclass.
|
||||
- **Line 4** - We start to filter ...
|
||||
- **Line 5**
|
||||
- ... by accessing the `db_location` field (usually this is a Room)
|
||||
- ... and on that location, we get the value of `db_tags` (this is a _many-to-many_ database field
|
||||
that we can treat like an object for this purpose; it references all Tags on the location)
|
||||
- ... and from those `Tags`, we looking for `Tags` whose `db_key` is "monlit" (non-case sensitive).
|
||||
- **Line 6** - ... We also want only Characters with `Attributes` whose `db_key` is exactly `"lycantrophy"`
|
||||
- **Line 7** - ... at the same time as the `Attribute`'s `db_value` is greater-than 2.
|
||||
|
||||
Running this query makes our newly lycantrrophic Character appear in `will_transform`. Success!
|
||||
|
||||
> Don't confuse database fields with [Attributes](../../../Components/Attributes.md) you set via `obj.db.attr = 'foo'` or
|
||||
`obj.attributes.add()`. Attributes are custom database entities *linked* to an object. They are not
|
||||
separate fields *on* that object like `db_key` or `db_location` are.
|
||||
|
||||
## Complex queries
|
||||
|
||||
All examples so far used `AND` relations. The arguments to `.filter` are added together with `AND`
|
||||
("we want tag room to be "monlit" _and_ lycantrhopy be > 2").
|
||||
|
||||
For queries using `OR` and `NOT` we need Django's
|
||||
[Q object](https://docs.djangoproject.com/en/1.11/topics/db/queries/#complex-lookups-with-q-objects). It is
|
||||
imported from Django directly:
|
||||
|
||||
from django.db.models import Q
|
||||
|
||||
The `Q` is an object that is created with the same arguments as `.filter`, for example
|
||||
|
||||
Q(db_key="foo")
|
||||
|
||||
You can then use this `Q` instance as argument in a `filter`:
|
||||
|
||||
q1 = Q(db_key="foo")
|
||||
Character.objects.filter(q1)
|
||||
|
||||
|
||||
The useful thing about `Q` is that these objects can be chained together with special symbols (bit operators):
|
||||
`|` for `OR` and `&` for `AND`. A tilde `~` in front negates the expression inside the `Q` and thus
|
||||
works like `NOT`.
|
||||
|
||||
q1 = Q(db_key="Dalton")
|
||||
q2 = Q(db_location=prison)
|
||||
Character.objects.filter(q1 | ~q2)
|
||||
|
||||
Would get all Characters that are either named "Dalton" _or_ which is _not_ in prison. The result is a mix
|
||||
of Daltons and non-prisoners.
|
||||
|
||||
Let us expand our original werewolf query. Not only do we want to find all Characters in a moonlit room
|
||||
with a certain level of `lycanthrophy`. Now we also want the full moon to immediately transform people who were
|
||||
recently bitten, even if their `lycantrophy` level is not yet high enough (more dramatic this way!). Let's say there is
|
||||
a Tag "recently_bitten" that controls this.
|
||||
|
||||
This is how we'd change our query:
|
||||
|
||||
```python
|
||||
from django.db.models import Q
|
||||
|
||||
will_transform = (
|
||||
Character.objects
|
||||
.filter(
|
||||
Q(db_location__db_tags__db_key__iexact="moonlit")
|
||||
& (
|
||||
Q(db_attributes__db_key="lycantrophy",
|
||||
db_attributes__db_value__gt=2)
|
||||
| Q(db_tags__db_key__iexact="recently_bitten")
|
||||
))
|
||||
.distinct()
|
||||
)
|
||||
```
|
||||
|
||||
That's quite compact. It may be easier to see what's going on if written this way:
|
||||
|
||||
```python
|
||||
from django.db.models import Q
|
||||
|
||||
q_moonlit = Q(db_location__db_tags__db_key__iexact="moonlit")
|
||||
q_lycantropic = Q(db_attributes__db_key="lycantrophy", db_attributes__db_value__gt=2)
|
||||
q_recently_bitten = Q(db_tags__db_key__iexact="recently_bitten")
|
||||
|
||||
will_transform = (
|
||||
Character.objects
|
||||
.filter(q_moonlit & (q_lycantropic | q_recently_bitten))
|
||||
.distinct()
|
||||
)
|
||||
```
|
||||
|
||||
```{sidebar} SQL
|
||||
|
||||
These Python structures are internally converted to SQL, the native language of the database.
|
||||
If you are familiar with SQL, these are many-to-many tables joined with `LEFT OUTER JOIN`,
|
||||
which may lead to multiple merged rows combining the same object with different relations.
|
||||
|
||||
```
|
||||
|
||||
This reads as "Find all Characters in a moonlit room that either has the Attribute `lycantrophy` higher
|
||||
than two _or_ which has the Tag `recently_bitten`". With an OR-query like this it's possible to find the
|
||||
same Character via different paths, so we add `.distinct()` at the end. This makes sure that there is only
|
||||
one instance of each Character in the result.
|
||||
|
||||
## Annotations
|
||||
|
||||
What if we wanted to filter on some condition that isn't represented easily by a field on the
|
||||
object? Maybe we want to find rooms only containing five or more objects?
|
||||
|
||||
We *could* do it like this (don't actually do it this way!):
|
||||
|
||||
```python
|
||||
from typeclasses.rooms import Room
|
||||
|
||||
all_rooms = Rooms.objects.all()
|
||||
|
||||
rooms_with_five_objects = []
|
||||
for room in all_rooms:
|
||||
if len(room.contents) >= 5:
|
||||
rooms_with_five_objects.append(room)
|
||||
```
|
||||
|
||||
Above we get all rooms and then use `list.append()` to keep adding the right rooms
|
||||
to an ever-growing list. This is _not_ a good idea, once your database grows this will
|
||||
be unnecessarily computing-intensive. The database is much more suitable for this.
|
||||
|
||||
_Annotations_ allow you to set a 'variable' inside the query that you can
|
||||
then access from other parts of the query. Let's do the same example as before directly in the database:
|
||||
|
||||
```python
|
||||
from typeclasses.rooms import Room
|
||||
from django.db.models import Count
|
||||
|
||||
rooms = (
|
||||
Room.objects
|
||||
.annotate(
|
||||
num_objects=Count('locations_set'))
|
||||
.filter(num_objects__gte=5)
|
||||
)
|
||||
```
|
||||
|
||||
`Count` is a Django class for counting the number of things in the database.
|
||||
|
||||
Here we first create an annotation `num_objects` of type `Count`. It creates an in-database function
|
||||
that will count the number of results inside the database.
|
||||
|
||||
> Note the use of `location_set` in that `Count`. The `*_set` is a back-reference automatically created by
|
||||
Django. In this case it allows you to find all objects that *has the current object as location*.
|
||||
|
||||
Next we filter on this annotation, using the name `num_objects` as something we can filter for. We
|
||||
use `num_objects__gte=5` which means that `num_objects` should be greater than 5. This is a little
|
||||
harder to get one's head around but much more efficient than lopping over all objects in Python.
|
||||
|
||||
## F-objects
|
||||
|
||||
What if we wanted to compare two dynamic parameters against one another in a query? For example, what if
|
||||
instead of having 5 or more objects, we only wanted objects that had a bigger inventory than they had
|
||||
tags (silly example, but ...)? This can be with Django's
|
||||
[F objects](https://docs.djangoproject.com/en/1.11/ref/models/expressions/#f-expressions).
|
||||
So-called F expressions allow you to do a query that looks at a value of each object in the database.
|
||||
|
||||
```python
|
||||
from django.db.models import Count, F
|
||||
from typeclasses.rooms import Room
|
||||
|
||||
result = (
|
||||
Room.objects
|
||||
.annotate(
|
||||
num_objects=Count('locations_set'),
|
||||
num_tags=Count('db_tags'))
|
||||
.filter(num_objects__gt=F('num_tags'))
|
||||
)
|
||||
```
|
||||
|
||||
Here we used `.annotate` to create two in-query 'variables' `num_objects` and `num_tags`. We then
|
||||
directly use these results in the filter. Using `F()` allows for also the right-hand-side of the filter
|
||||
condition to be calculated on the fly, completely within the database.
|
||||
|
||||
## Grouping and returning only certain properties
|
||||
|
||||
Suppose you used tags to mark someone belonging to an organization. Now you want to make a list and
|
||||
need to get the membership count of every organization all at once.
|
||||
|
||||
The `.annotate`, `.values_list`, and `.order_by` queryset methods are useful for this. Normally when
|
||||
you run a `.filter`, what you get back is a bunch of full typeclass instances, like roses or swords.
|
||||
Using `.values_list` you can instead choose to only get back certain properties on objects.
|
||||
The `.order_by` method finally allows for sorting the results according to some criterion:
|
||||
|
||||
|
||||
```python
|
||||
from django.db.models import Count
|
||||
from typeclasses.rooms import Room
|
||||
|
||||
result = (
|
||||
Character.objects
|
||||
.filter(db_tags__db_category="organization")
|
||||
.annotate(tagcount=Count('id'))
|
||||
.order_by('-tagcount'))
|
||||
.values_list('db_tags__db_key', "tagcount")
|
||||
```
|
||||
|
||||
Here we fetch all Characters who ...
|
||||
- ... has a tag of category "organization" on them
|
||||
- ... along the way we count how many different Characters (each `id` is unique) we find for each organization
|
||||
and store it in a 'variable' `tagcount` using `.annotate` and `Count`
|
||||
- ... we use this count to sort the result in descending order of `tagcount` (descending because there is a minus sign,
|
||||
default is increasing order but we want the most popular organization to be first).
|
||||
- ... and finally we make sure to only return exactly the properties we want, namely the name of the organization tag
|
||||
and how many matches we found for that organization.
|
||||
|
||||
The result queryset will be a list of tuples ordered in descending order by the number of matches,
|
||||
in a format like the following:
|
||||
```
|
||||
[
|
||||
('Griatch's poets society', 3872),
|
||||
("Chainsol's Ainneve Testers", 2076),
|
||||
("Blaufeuer's Whitespace Fixers", 1903),
|
||||
("Volund's Bikeshed Design Crew", 1764),
|
||||
("Tehom's Glorious Misanthropes", 1763)
|
||||
]
|
||||
```
|
||||
|
||||
## Conclusions
|
||||
|
||||
We have covered a lot of ground in this lesson and covered several more complex topics. Knowing how to
|
||||
query using Django is a powerful skill to have.
|
||||
|
||||
This concludes the first part of the Evennia starting tutorial - "What we have". Now we have a good foundation
|
||||
to understand how to plan what our tutorial game will be about.
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
# Overview of the Evennia library
|
||||
|
||||
```{sidebar} API
|
||||
|
||||
API stands for `Application Programming Interface`, a description for how to access
|
||||
the resources of a program or library.
|
||||
```
|
||||
A good place to start exploring Evennia is the [Evenia-API frontpage](../../../Evennia-API.md).
|
||||
This page sums up the main components of Evennia with a short description of each. Try clicking through
|
||||
to a few entries - once you get deep enough you'll see full descriptions
|
||||
of each component along with their documentation. You can also click `[source]` to see the full Python source
|
||||
for each thing.
|
||||
|
||||
You can also browse [the evennia repository on github](https://github.com/evennia/evennia). This is exactly
|
||||
what you can download from us. The github repo is also searchable.
|
||||
|
||||
Finally, you can clone the evennia repo to your own computer and read the sources locally. This is necessary
|
||||
if you want to help with Evennia's development itself. See the
|
||||
[extended install instructions](../../../Setup/Installation-Git.md) if you want to do this.
|
||||
|
||||
## Where is it?
|
||||
|
||||
If Evennia is installed, you can import from it simply with
|
||||
|
||||
import evennia
|
||||
from evennia import some_module
|
||||
from evennia.some_module.other_module import SomeClass
|
||||
|
||||
and so on.
|
||||
|
||||
If you installed Evennia with `pip install`, the library folder will be installed deep inside your Python
|
||||
installation. If you cloned the repo there will be a folder `evennia` on your hard drive there.
|
||||
|
||||
If you cloned the repo or read the code on `github` you'll find this being the outermost structure:
|
||||
|
||||
evennia/
|
||||
bin/
|
||||
CHANGELOG.md
|
||||
...
|
||||
...
|
||||
docs/
|
||||
evennia/
|
||||
|
||||
This outer layer is for Evennia's installation and package distribution. That internal folder `evennia/evennia/` is
|
||||
the _actual_ library, the thing covered by the API auto-docs and what you get when you do `import evennia`.
|
||||
|
||||
> The `evennia/docs/` folder contains the sources for this documentation. See
|
||||
> [contributing to the docs](../../../Contributing-Docs.md) if you want to learn more about how this works.
|
||||
|
||||
This the the structure of the Evennia library:
|
||||
|
||||
- evennia
|
||||
- [`__init__.py`](../../../Evennia-API.md#shortcuts) - The "flat API" of Evennia resides here.
|
||||
- [`settings_default.py`](../../../Setup/Settings.md#settings-file) - Root settings of Evennia. Copy settings
|
||||
from here to `mygame/server/settings.py` file.
|
||||
- [`commands/`](../../../Components/Commands.md) - The command parser and handler.
|
||||
- `default/` - The [default commands](../../../Components/Default-Commands.md) and cmdsets.
|
||||
- [`comms/`](../../../Components/Channels.md) - Systems for communicating in-game.
|
||||
- `contrib/` - Optional plugins too game-specific for core Evennia.
|
||||
- `game_template/` - Copied to become the "game directory" when using `evennia --init`.
|
||||
- [`help/`](../../../Components/Help-System.md) - Handles the storage and creation of help entries.
|
||||
- `locale/` - Language files ([i18n](../../../Concepts/Internationalization.md)).
|
||||
- [`locks/`](../../../Components/Locks.md) - Lock system for restricting access to in-game entities.
|
||||
- [`objects/`](../../../Components/Objects.md) - In-game entities (all types of items and Characters).
|
||||
- [`prototypes/`](../../../Components/Prototypes.md) - Object Prototype/spawning system and OLC menu
|
||||
- [`accounts/`](../../../Components/Accounts.md) - Out-of-game Session-controlled entities (accounts, bots etc)
|
||||
- [`scripts/`](../../../Components/Scripts.md) - Out-of-game entities equivalence to Objects, also with timer support.
|
||||
- [`server/`](../../../Components/Portal-And-Server.md) - Core server code and Session handling.
|
||||
- `portal/` - Portal proxy and connection protocols.
|
||||
- [`typeclasses/`](../../../Components/Typeclasses.md) - Abstract classes for the typeclass storage and database system.
|
||||
- [`utils/`](../../../Components/Coding-Utils.md) - Various miscellaneous useful coding resources.
|
||||
- [`web/`](../../../Concepts/Web-Features.md) - Web resources and webserver. Partly copied into game directory on initialization.
|
||||
|
||||
```{sidebar} __init__.py
|
||||
|
||||
The `__init__.py` file is a special Python filename used to represent a Python 'package'.
|
||||
When you import `evennia` on its own, you import this file. When you do `evennia.foo` Python will
|
||||
first look for a property `.foo` in `__init__.py` and then for a module or folder of that name
|
||||
in the same location.
|
||||
|
||||
```
|
||||
|
||||
While all the actual Evennia code is found in the various folders, the `__init__.py` represents the entire
|
||||
package `evennia`. It contains "shortcuts" to code that is actually located elsewhere. Most of these shortcuts
|
||||
are listed if you [scroll down a bit](../../../Evennia-API.md) on the Evennia-API page.
|
||||
|
||||
## An example of exploring the library
|
||||
|
||||
In the previous lesson we took a brief look at `mygame/typeclasses/objects` as an example of a Python module. Let's
|
||||
open it again. Inside is the `Object` class, which inherits from `DefaultObject`.
|
||||
Near the top of the module is this line:
|
||||
|
||||
from evennia import DefaultObject
|
||||
|
||||
We want to figure out just what this DefaultObject offers. Since this is imported directly from `evennia`, we
|
||||
are actually importing from `evennia/__init__.py`.
|
||||
|
||||
[Look at Line 159](github:evennia/__init__.py#159) of `evennia/__init__.py` and you'll find this line:
|
||||
|
||||
from .objects.objects import DefaultObject
|
||||
|
||||
```{sidebar} Relative and absolute imports
|
||||
|
||||
The first full-stop in `from .objects.objects ...` means that
|
||||
we are importing from the current location. This is called a `relative import`.
|
||||
By comparison, `from evennia.objects.objects` is an `absolute import`. In this particular
|
||||
case, the two would give the same result.
|
||||
```
|
||||
|
||||
> You can also look at [the right section of the API frontpage](../../../Evennia-API.md#typeclasses) and click through
|
||||
> to the code that way.
|
||||
|
||||
The fact that `DefaultObject` is imported into `__init__.py` here is what makes it possible to also import
|
||||
it as `from evennia import DefaultObject` even though the code for the class is not actually here.
|
||||
|
||||
So to find the code for `DefaultObject` we need to look in `evennia/objects/objects.py`. Here's how
|
||||
to look it up in the docs:
|
||||
|
||||
1. Open the [API frontpage](../../../Evennia-API.md)
|
||||
2. Locate the link to [evennia.objects.objects](evennia.objects.objects) and click on it.
|
||||
3 You are now in the python module. Scroll down (or search in your web browser) to find the `DefaultObject` class.
|
||||
4 You can now read what this does and what methods are on it. If you want to see the full source, click the
|
||||
\[source\] link next to it.
|
||||
208
docs/source/Howtos/Beginner-Tutorial/Part1/Gamedir-Overview.md
Normal file
208
docs/source/Howtos/Beginner-Tutorial/Part1/Gamedir-Overview.md
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
# Overview of your new Game Dir
|
||||
|
||||
Next we will take a little detour to look at the _Tutorial World_. This is a little solo adventure
|
||||
that comes with Evennia, a showcase for some of the things that are possible.
|
||||
|
||||
Now we have 'run the game' a bit and started with our forays into Python from inside Evennia.
|
||||
It is time to start to look at how things look 'outside of the game'. Let's do a tour of your game-dir
|
||||
Like everywhere in the docs we'll assume it's called `mygame`.
|
||||
|
||||
> When looking through files, ignore files ending with `.pyc` and the
|
||||
`__pycache__` folder if it exists. This is internal Python compilation files that you should never
|
||||
> need to touch. Files `__init__.py` is also often empty and can be ignored (they have to do with
|
||||
> Python package management).
|
||||
|
||||
You may have noticed when we were building things in-game that we would often refer to code through
|
||||
"python paths", such as
|
||||
|
||||
```{sidebar} Python-paths
|
||||
|
||||
A 'python path' uses '.' instead of '/' or '`\\`' and
|
||||
skips the `.py` ending of files. It can also point to
|
||||
the code contents of python files. Since Evennia is already
|
||||
looking for code in your game dir, your python paths can start
|
||||
from there.
|
||||
|
||||
So a path `/home/foo/devel/mygame/commands/command.py`
|
||||
would translate to a Python-path `commands.command`.
|
||||
```
|
||||
|
||||
create/drop button:tutorial_examples.red_button.RedButton
|
||||
|
||||
This is a fundamental aspect of coding Evennia - _you create code and then you tell Evennia where that
|
||||
code is and when it should be used_. Above we told it to create a red button by pulling from specific code
|
||||
in the `contribs/` folder but the same principle is true everywhere. So it's important to know where code is
|
||||
and how you point to it correctly.
|
||||
|
||||
- `mygame/`
|
||||
- `commands/` - This holds all your custom commands (user-input handlers). You both add your own
|
||||
and override Evennia's defaults from here.
|
||||
- `server`/ - The structure of this folder should not change since Evennia expects it.
|
||||
- `conf/` - All server configuration files sits here. The most important file is `settings.py`.
|
||||
- `logs/` - Server log files are stored here. When you use `evennia --log` you are actually
|
||||
tailing the files in this directory.
|
||||
- `typeclasses/` - this holds empty templates describing all database-bound entities in the
|
||||
game, like Characters, Scripts, Accounts etc. Adding code here allows to customize and extend
|
||||
the defaults.
|
||||
- `web/` - This is where you override and extend the default templates, views and static files used
|
||||
for Evennia's web-presence, like the website and the HTML5 webclient.
|
||||
- `world/` - this is a "miscellaneous" folder holding everything related to the world you are
|
||||
building, such as build scripts and rules modules that don't fit with one of the other folders.
|
||||
|
||||
> The `server/` subfolder should remain the way it is - Evennia expects this. But you could in
|
||||
> principle change the structure of the rest of your game dir as best fits your preference.
|
||||
> Maybe you don't need a world/ folder but prefer many folders with different aspects of your world?
|
||||
> Or a new folder 'rules' for your RPG rules? This is fine. If you move things around you just need
|
||||
> to update Evennia's default settings to point to the right places in the new structure.
|
||||
|
||||
## commands/
|
||||
|
||||
The `commands/` folder holds Python modules related to creating and extending the [Commands](../../../Components/Commands.md)
|
||||
of Evennia. These manifest in game like the server understanding input like `look` or `dig`.
|
||||
|
||||
```{sidebar} Classes
|
||||
|
||||
A `class` is template for creating object-instances of a particular type
|
||||
in Python. We will explain classes in more detail in the next
|
||||
`python overview <Python-basic-tutorial-part-two>`_.
|
||||
|
||||
```
|
||||
- [command.py](github:evennia/game_template/commands/command.py) (Python-path: `commands.command`) - this contain the
|
||||
base _classes_ for designing new input commands, or override the defaults.
|
||||
- [default_cmdsets.py](github:evennia/game_template/commands/default_cmdsets.py) (Python path: `commands.default_commands`) -
|
||||
a cmdset (Command-Set) groups Commands together. Command-sets can be added and removed from objects on the fly,
|
||||
meaning a user could have a different set of commands (or versions of commands) available depending on their circumstance
|
||||
in the game. In order to add a new command to the game, it's common to import the new command-class
|
||||
from `command.py` and add it to one of the default cmdsets in this module.
|
||||
|
||||
## server/
|
||||
|
||||
This folder contains resource necessary for running Evennia. Contrary to the other folders, the structure
|
||||
of this should be kept the way it is.
|
||||
|
||||
- `evennia.db3` - you will only have this file if you are using the default SQLite3 database. This file
|
||||
contains the entire database. Just copy it to make a backup. For development you could also just
|
||||
make a copy once you have set up everything you need and just copy that back to 'reset' the state.
|
||||
If you delete this file you can easily recreate it by running `evennia migrate`.
|
||||
|
||||
### server/logs/
|
||||
|
||||
This holds the server logs. When you do `evennia --log`, the evennia program is in fact tailing and concatenating
|
||||
the `server.log` and `portal.log` files in this directory. The logs are rotated every week. Depending on your settings,
|
||||
other logs, like the webserver HTTP request log can also be found here.
|
||||
|
||||
### server/conf/
|
||||
|
||||
This contains all configuration files of the Evennia server. These are regular Python modules which
|
||||
means that they must be extended with valid Python. You can also add logic to them if you wanted to.
|
||||
|
||||
Common for the settings is that you generally will never them directly via their python-path; instead Evennia
|
||||
knows where they are and will read them to configure itself at startup.
|
||||
|
||||
- `settings.py` - this is by far the most important file. It's nearly empty by default, rather you
|
||||
are expected to copy&paste the changes you need from [evennia/default_settings.py](github:evennia/default_settings.py).
|
||||
The default settings file is extensively documented. Importing/accessing the values in the settings
|
||||
file is done in a special way, like this:
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
To get to the setting `TELNET_PORT` in the settings file you'd then do
|
||||
|
||||
telnet_port = settings.TELNET_PORT
|
||||
|
||||
You cannot assign to the settings file dynamically; you must change the `settings.py` file directly to
|
||||
change a setting.
|
||||
- `secret_settings.py` - If you are making your code effort public, you may not want to share all settings online.
|
||||
There may be server-specific secrets or just fine-tuning for your game systems that you prefer be kept secret
|
||||
from the players. Put such settings in here, it will override values in `settings.py` and not be included in
|
||||
version control.
|
||||
- `at_initial_setup.py` - When Evennia starts up for the very first time, it does some basic tasks, like creating the
|
||||
superuser and Limbo room. Adding to this file allows to add more actions for it to for first-startup.
|
||||
- `at_search.py` - When searching for objects and either finding no match or more than one match, it will
|
||||
respond by giving a warning or offering the user to differentiate between the multiple matches. Modifying
|
||||
the code here will change this behavior to your liking.
|
||||
- `at_server_startstop.py` - This allows to inject code to execute every time the server starts, stops or reloads
|
||||
in different ways.
|
||||
- `connection_screens.py` - This allows for changing the connection screen you see when you first connect to your
|
||||
game.
|
||||
- `inlinefuncs.py` - _Inlinefuncs_ are optional and limited 'functions' that can be embedded in any strings being
|
||||
sent to a player. They are written as `$funcname(args)` and are used to customize the output
|
||||
depending on the user receiving it. For example sending people the text `"Let's meet at $realtime(13:00, GMT)!`
|
||||
would show every player seeing that string the time given in their own time zone. The functions added to this
|
||||
module will become new inlinefuncs in the game.
|
||||
- `inputfucs.py` - When a command like `look` is received by the server, it is handled by an _inputfunc_
|
||||
that redirects it to the cmdhandler system. But there could be other inputs coming from the clients, like
|
||||
button-presses or the request to update a health-bar. While most common cases are already covered, this is
|
||||
where one adds new functions to process new types of input.
|
||||
- `lockfuncs.py` - _Locks_ restrict access to things in-game. Lock funcs are used in a mini-language
|
||||
to defined more complex locks. For example you could have a lockfunc that checks if the user is carrying
|
||||
a given item, is bleeding or has a certain skill value. New functions added in this modules will
|
||||
become available for use in lock definitions.
|
||||
- `mssp.py` - Mud Server Status Protocol is a way for online MUD archives/listings (which you usually have
|
||||
to sign up for) to track which MUDs are currently online, how many players they have etc. While Evennia handles
|
||||
the dynamic information automatically, this is were you set up the meta-info about your game, such as its
|
||||
theme, if player-killing is allowed and so on. This is a more generic form of the Evennia Game directory.
|
||||
- `portal_services_plugins.py` - If you want to add new external connection protocols to Evennia, this is the place
|
||||
to add them.
|
||||
- `server_services_plugins.py` - This allows to override internal server connection protocols.
|
||||
- `web_plugins.py` - This allows to add plugins to the Evennia webserver as it starts.
|
||||
|
||||
### typeclasses/
|
||||
|
||||
The [Typeclasses](../../../Components/Typeclasses.md) of Evennia are Evennia-specific Python classes whose instances save themselves
|
||||
to the database. This allows a Character to remain in the same place and your updated strength stat to still
|
||||
be the same after a server reboot.
|
||||
|
||||
- [accounts.py](github:evennia/game_template/typeclasses/accounts.py) (Python-path: `typeclasses.accounts`) - An
|
||||
[Account](../../../Components/Accounts.md) represents the player connecting to the game. It holds information like email,
|
||||
password and other out-of-character details.
|
||||
- [channels.py](github:evennia/game_template/typeclasses/channels.py) (Python-path: `typeclasses.channels`) -
|
||||
[Channels](../../../Components/Channels.md) are used to manage in-game communication between players.
|
||||
- [objects.py](github:evennia/game_template/typeclasses/objects.py) (Python-path: `typeclasses.objects`) -
|
||||
[Objects](../../../Components/Objects.md) represent all things having a location within the game world.
|
||||
- [characters.py](github:evennia/game_template/typeclasses/characters.py) (Python-path: `typeclasses.characters`) -
|
||||
The [Character](../../../Components/Objects.md#characters) is a subclass of Objects, controlled by Accounts - they are the player's
|
||||
avatars in the game world.
|
||||
- [rooms.py](github:evennia/game_template/typeclasses/rooms.py) (Python-path: `typeclasses.rooms`) - A
|
||||
[Room](../../../Components/Objects.md#rooms) is also a subclass of Object; describing discrete locations. While the traditional
|
||||
term is 'room', such a location can be anything and on any scale that fits your game, from a forest glade,
|
||||
an entire planet or an actual dungeon room.
|
||||
- [exits.py](github:evennia/game_template/typeclasses/exits.py) (Python-path: `typeclasses.exits`) -
|
||||
[Exits](../../../Components/Objects.md#exits) is another subclass of Object. Exits link one Room to another.
|
||||
- [scripts.py](github:evennia/game_template/typeclasses/scripts.py) (Python-path: `typeclasses.scripts`) -
|
||||
[Scripts](../../../Components/Scripts.md) are 'out-of-character' objects. They have no location in-game and can serve as basis for
|
||||
anything that needs database persistence, such as combat, weather, or economic systems. They also
|
||||
have the ability to execute code repeatedly, on a timer.
|
||||
|
||||
### web/
|
||||
|
||||
This folder contains folders for overriding the default web-presence of Evennia with your own designs.
|
||||
Most of these folders are empty except for a README file or a subset of other empty folders.
|
||||
|
||||
- `media/` - this empty folder is where you can place your own images or other media files you want the
|
||||
web server to serve. If you are releasing your game with a lot of media (especially if you want videos) you
|
||||
should consider re-pointing Evennia to use some external service to serve your media instead.
|
||||
- `static_overrides/` - 'static' files include fonts, CSS and JS. Within this folder you'll find sub-folders for
|
||||
overriding the static files for the `admin` (this is the Django web-admin), the `webclient` (this is thet
|
||||
HTML5 webclient) and the `website`. Adding files to this folder will replace same-named files in the
|
||||
default web presence.
|
||||
- `template_overrides/` - these are HTML files, for the `webclient` and the `website`. HTML files are written
|
||||
using [Jinja](https://jinja.palletsprojects.com/en/2.11.x/) templating, which means that one can override
|
||||
only particular parts of a default template without touching others.
|
||||
- `static/` - this is a work-directory for the web system and should _not_ be manually modified. Basically,
|
||||
Evennia will copy static data from `static_overrides` here when the server starts.
|
||||
- `urls.py` - this module links up the Python code to the URLs you go to in the browser.
|
||||
|
||||
### world/
|
||||
|
||||
This folder only contains some example files. It's meant to hold 'the rest' of your game implementation. Many
|
||||
people change and re-structure this in various ways to better fit their ideas.
|
||||
|
||||
- [batch_cmds.ev](github:evennia/game_template/world/batch_cmds.ev) - This is an `.ev` file, which is essentially
|
||||
just a list of Evennia commands to execute in sequence. This one is empty and ready to expand on. The
|
||||
[Tutorial World](./Tutorial-World.md) was built with such a batch-file.
|
||||
- [prototypes.py](github:evennia/game_template/world/prototypes.py) - A [prototype](../../../Components/Prototypes.md) is a way
|
||||
to easily vary objects without changing their base typeclass. For example, one could use prototypes to
|
||||
tell that Two goblins, while both of the class 'Goblin' (so they follow the same code logic), should have different
|
||||
equipment, stats and looks.
|
||||
|
||||
|
|
@ -0,0 +1,621 @@
|
|||
# Making objects persistent
|
||||
|
||||
Now that we have learned a little about how to find things in the Evennia library, let's use it.
|
||||
|
||||
In the [Python classes and objects](./Python-classes-and-objects.md) lesson we created the dragons Fluffy, Cuddly
|
||||
and Smaug and made them fly and breathe fire. So far our dragons are short-lived - whenever we `restart`
|
||||
the server or `quit()` out of python mode they are gone.
|
||||
|
||||
This is what you should have in `mygame/typeclasses/monsters.py` so far:
|
||||
|
||||
|
||||
```python
|
||||
|
||||
class Monster:
|
||||
"""
|
||||
This is a base class for Monsters.
|
||||
"""
|
||||
|
||||
def __init__(self, key):
|
||||
self.key = key
|
||||
|
||||
def move_around(self):
|
||||
print(f"{self.key} is moving!")
|
||||
|
||||
|
||||
class Dragon(Monster):
|
||||
"""
|
||||
This is a dragon-specific monster.
|
||||
"""
|
||||
|
||||
def move_around(self):
|
||||
super().move_around()
|
||||
print("The world trembles.")
|
||||
|
||||
def firebreath(self):
|
||||
"""
|
||||
Let our dragon breathe fire.
|
||||
"""
|
||||
print(f"{self.key} breathes fire!")
|
||||
|
||||
```
|
||||
|
||||
## Our first persistent object
|
||||
|
||||
At this point we should know enough to understand what is happening in `mygame/typeclasses/objects.py`. Let's
|
||||
open it:
|
||||
|
||||
```python
|
||||
"""
|
||||
module docstring
|
||||
"""
|
||||
from evennia import DefaultObject
|
||||
|
||||
class Object(DefaultObject):
|
||||
"""
|
||||
class docstring
|
||||
"""
|
||||
pass
|
||||
```
|
||||
|
||||
So we have a class `Object` that _inherits_ from `DefaultObject`, which we have imported from Evennia.
|
||||
The class itself doesn't do anything (it just `pass`es) but that doesn't mean it's useless. As we've seen,
|
||||
it inherits all the functionality of its parent. It's in fact an _exact replica_ of `DefaultObject` right now.
|
||||
If we knew what kind of methods and resources were available on `DefaultObject` we could add our own and
|
||||
change the way it works!
|
||||
|
||||
> Hint: We will get back to this, but to learn what resources an Evennia parent like `DefaultObject` offers,
|
||||
> easiest is to peek at its [API documentation](evennia.objects.objects.DefaultObject). The docstring for
|
||||
> the `Object` class can also help.
|
||||
|
||||
One thing that Evennia classes offers and which you don't get with vanilla Python classes is _persistence_. As
|
||||
you've found, Fluffy, Cuddly and Smaug are gone once we reload the server. Let's see if we can fix this.
|
||||
|
||||
Go back to `mygame/typeclasses/monsters.py`. Change it as follows:
|
||||
|
||||
```python
|
||||
|
||||
from typeclasses.objects import Object
|
||||
|
||||
class Monster(Object):
|
||||
"""
|
||||
This is a base class for Monsters.
|
||||
"""
|
||||
def move_around(self):
|
||||
print(f"{self.key} is moving!")
|
||||
|
||||
|
||||
class Dragon(Monster):
|
||||
"""
|
||||
This is a dragon-specific Monster.
|
||||
"""
|
||||
|
||||
def move_around(self):
|
||||
super().move_around()
|
||||
print("The world trembles.")
|
||||
|
||||
def firebreath(self):
|
||||
"""
|
||||
Let our dragon breathe fire.
|
||||
"""
|
||||
print(f"{self.key} breathes fire!")
|
||||
|
||||
```
|
||||
|
||||
Don't forget to save. We removed `Monster.__init__` and made `Monster` inherit from Evennia's `Object` (which in turn
|
||||
inherits from Evennia's `DefaultObject`, as we saw). By extension, this means that `Dragon` also inherits
|
||||
from `DefaultObject`, just from further away!
|
||||
|
||||
### Making a new object by calling the class
|
||||
|
||||
First reload the server as usual. We will need to create the dragon a little differently this time:
|
||||
|
||||
```{sidebar} Keyword arguments
|
||||
|
||||
Keyword arguments (like `db_key="Smaug"`) is a way to
|
||||
name the input arguments to a function or method. They make
|
||||
things easier to read but also allows for conveniently setting
|
||||
defaults for values not given explicitly.
|
||||
|
||||
```
|
||||
> py
|
||||
> from typeclasses.monsters import Dragon
|
||||
> smaug = Dragon(db_key="Smaug", db_location=here)
|
||||
> smaug.save()
|
||||
> smaug.move_around()
|
||||
Smaug is moving!
|
||||
The world trembles.
|
||||
|
||||
Smaug works the same as before, but we created him differently: first we used
|
||||
`Dragon(db_key="Smaug", db_location=here)` to create the object, and then we used `smaug.save()` afterwards.
|
||||
|
||||
> quit()
|
||||
Python Console is closing.
|
||||
> look
|
||||
|
||||
You should now see that Smaug _is in the room with you_. Woah!
|
||||
|
||||
> reload
|
||||
> look
|
||||
|
||||
_He's still there_... What we just did was to create a new entry in the database for Smaug. We gave the object
|
||||
its name (key) and set its location to our current location (remember that `here` is just something available
|
||||
in the `py` command, you can't use it elsewhere).
|
||||
|
||||
To make use of Smaug in code we must first find him in the database. For an object in the current
|
||||
location we can easily do this in `py` by using `me.search()`:
|
||||
|
||||
> py smaug = me.search("Smaug") ; smaug.firebreath()
|
||||
Smaug breathes fire!
|
||||
|
||||
### Creating using create_object
|
||||
|
||||
Creating Smaug like we did above is nice because it's similar to how we created non-database
|
||||
bound Python instances before. But you need to use `db_key` instead of `key` and you also have to
|
||||
remember to call `.save()` afterwards. Evennia has a helper function that is more common to use,
|
||||
called `create_object`:
|
||||
|
||||
> py fluffy = evennia.create_object('typeclases.monster.Monster', key="Fluffy", location=here)
|
||||
> look
|
||||
|
||||
Boom, Fluffy should now be in the room with you, a little less scary than Smaug. You specify the
|
||||
python-path to the code you want and then set the key and location. Evennia sets things up and saves for you.
|
||||
|
||||
If you want to find Fluffy from anywhere, you can use Evennia's `search_object` helper:
|
||||
|
||||
> fluffy = evennia.search_object("Fluffy")[0] ; fluffy.move_around()
|
||||
Fluffy is moving!
|
||||
|
||||
> The `[0]` is because `search_object` always returns a _list_ of zero, one or more found objects. The `[0]`
|
||||
means that we want the first element of this list (counting in Python always starts from 0). If there were
|
||||
multiple Fluffies we could get the second one with `[1]`.
|
||||
|
||||
### Creating using create-command
|
||||
|
||||
Finally, you can also create a new Dragon using the familiar builder-commands we explored a few lessons ago:
|
||||
|
||||
> create/drop Cuddly:typeclasses.monsters.Monster
|
||||
|
||||
Cuddly is now in the room. After learning about how objects are created you'll realize that all this command really
|
||||
does is to parse your input, figure out that `/drop` means to "give the object the same location as the caller",
|
||||
and then do a call akin to
|
||||
|
||||
evennia.create_object("typeclasses.monsters.Monster", key="Cuddly", location=here)
|
||||
|
||||
That's pretty much all there is to the mighty `create` command! The rest is just parsing for the command
|
||||
to understand just what the user wants to create.
|
||||
|
||||
## Typeclasses
|
||||
|
||||
The `Object` (and `DefafultObject` class we inherited from above is what we refer to as a _Typeclass_. This
|
||||
is an Evennia thing. The instance of a typeclass saves itself to the database when it is created, and after
|
||||
that you can just search for it to get it back. We use the term _typeclass_ or _typeclassed_ to differentiate
|
||||
these types of classes and objects from the normal Python classes, whose instances go away on a reload.
|
||||
|
||||
The number of typeclasses in Evennia are so few they can be learned by heart:
|
||||
|
||||
- `evennia.DefaultObject`: This is the parent of all in-game entities - everything with a location. Evennia makes
|
||||
a few very useful child classes of this class:
|
||||
- `evennia.DefaultCharacter`: The default entity represening a player avatar in-game.
|
||||
- `evennia.DefaultRoom`: A location in the game world.
|
||||
- `evennia.DefaultExit`: A link between locations.
|
||||
- `evennia.DefaultAccount`: The OOC representation of a player, holds password and account info.
|
||||
- `evennia.DefaultChannel`: In-game channels. These could be used for all sorts of in-game communication.
|
||||
- `evennia.DefaultScript`: Out-of-game objects, with no presence in the game world. Anything you want to create that
|
||||
needs to be persistent can be stored with these entities, such as combat state, economic systems or what have you.
|
||||
|
||||
If you take a look in `mygame/typeclasses/` you'll find modules for each of these. Each contains an empty child
|
||||
class ready that already inherits from the right parent, ready for you to modify or build from:
|
||||
|
||||
- `mygame/typeclasses/objects.py` has `class Object(DefaultObject)`, a class directly inheriting the basic in-game entity, this
|
||||
works as a base for any object.
|
||||
- `mygame/typeclasses/characters.py` has `class Character(DefaultCharacter)`
|
||||
- `mygame/typeclasses/rooms.py` has `class Room(DefaultRoom)`
|
||||
- `mygame/typeclasses/exits.py` has `class Exit(DefaultExit)`
|
||||
- `mygame/typeclasses/accounts.py` has `class Account(DefaultAccount)`
|
||||
- `mygame/typeclasses/channels.py` has `class Channel(DefaultChannel)`
|
||||
- `mygame/typeclasses/scripts.py` has `class Script(DefaultScript)`
|
||||
|
||||
> Notice that the classes in `mygame/typeclasses/` are _not inheriting from each other_. For example,
|
||||
> `Character` is inheriting from `evennia.DefaultCharacter` and not from `typeclasses.objects.Object`.
|
||||
> So if you change `Object` you will not cause any change in the `Character` class. If you want that you
|
||||
> can easily just change the child classes to inherit in that way instead; Evennia doesn't care.
|
||||
|
||||
As seen with our `Dragon` example, you don't _have_ to modify these modules directly. You can just make your
|
||||
own modules and import the base class.
|
||||
|
||||
### Examining and defaults
|
||||
|
||||
When you do
|
||||
|
||||
> create/drop giantess:typeclasses.monsters.Monster
|
||||
You create a new Monster: giantess.
|
||||
|
||||
or
|
||||
|
||||
> py evennia.create_object("typeclasses.monsters.Monster", key="Giantess", location=here)
|
||||
|
||||
You are specifying exactly which typeclass you want to use to build the Giantess. Let's examine the result:
|
||||
|
||||
> examine giantess
|
||||
-------------------------------------------------------------------------------
|
||||
Name/key: Giantess (#14)
|
||||
Typeclass: Monster (typeclasses.monsters.Monster)
|
||||
Location: Limbo (#2)
|
||||
Home: Limbo (#2)
|
||||
Permissions: <None>
|
||||
Locks: call:true(); control:id(1) or perm(Admin); delete:id(1) or perm(Admin);
|
||||
drop:holds(); edit:perm(Admin); examine:perm(Builder); get:all();
|
||||
puppet:pperm(Developer); tell:perm(Admin); view:all()
|
||||
Persistent attributes:
|
||||
desc = You see nothing special.
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
We used the `examine` command briefly in the [lesson about building in-game](./Building-Quickstart.md). Now these lines
|
||||
may be more useful to us:
|
||||
- **Name/key** - The name of this thing. The value `(#14)` is probably different for you. This is the
|
||||
unique 'primary key' or _dbref_ for this entity in the database.
|
||||
- **Typeclass**: This show the typeclass we specified, and the path to it.
|
||||
- **Location**: We are in Limbo. If you moved elsewhere you'll see that instead. Also the `#dbref` is shown.
|
||||
- **Permissions**: _Permissions_ are like the inverse to _Locks_ - they are like keys to unlock access to other things.
|
||||
The giantess have no such keys (maybe fortunately).
|
||||
- **Locks**: Locks are the inverse of _Permissions_ - specify what criterion _other_ objects must fulfill in order to
|
||||
access the `giantess` object. This uses a very flexible mini-language. For examine, the line `examine:perm(Builders)`
|
||||
is read as "Only those with permission _Builder_ or higher can _examine_ this object". Since we are the superuser
|
||||
we pass (even bypass) such locks with ease.
|
||||
- **Persistent attributes**: This allows for storing arbitrary, persistent data on the typeclassed entity. We'll get
|
||||
to those in the next section.
|
||||
|
||||
Note how the **Typeclass** line describes exactly where to find the code of this object? This is very useful for
|
||||
understanding how any object in Evennia works.
|
||||
|
||||
What happens if we _don't_ specify the typeclass though?
|
||||
|
||||
> create/drop box
|
||||
You create a new Object: box.
|
||||
|
||||
or
|
||||
|
||||
> py create.create_object(None, key="box", location=here)
|
||||
|
||||
Now check it out:
|
||||
|
||||
> examine box
|
||||
|
||||
You will find that the **Typeclass** line now reads
|
||||
|
||||
Typeclass: Object (typeclasses.objects.Object)
|
||||
|
||||
So when you didn't specify a typeclass, Evennia used a default, more specifically the (so far) empty `Object` class in
|
||||
`mygame/typeclasses/objects.py`. This is usually what you want, especially since you can tweak that class as much
|
||||
as you like.
|
||||
|
||||
But the reason Evennia knows to fall back to this class is not hard-coded - it's a setting. The default is
|
||||
in [evennia/settings_default.py](https://github.com/evennia/evennia/blob/master/evennia/settings_default.py#L465),
|
||||
with the name `BASE_OBJECT_TYPECLASS`, which is set to `typeclasses.objects.Object`.
|
||||
|
||||
```{sidebar} Changing things
|
||||
|
||||
While it's tempting to change folders around to your liking, this can
|
||||
make it harder to follow tutorials and may confuse if
|
||||
you are asking others for help. So don't overdo it unless you really
|
||||
know what you are doing.
|
||||
```
|
||||
|
||||
So if you wanted the creation commands and methods to default to some other class you could
|
||||
add your own `BASE_OBJECT_TYPECLASS` line to `mygame/server/conf/settings.py`. The same is true for all the other
|
||||
typeclasseses, like characters, rooms and accounts. This way you can change the
|
||||
layout of your game dir considerably if you wanted. You just need to tell Evennia where everything is.
|
||||
|
||||
## Modifying ourselves
|
||||
|
||||
Let's try to modify ourselves a little. Open up `mygame/typeclasses/characters.py`.
|
||||
|
||||
```python
|
||||
"""
|
||||
(module docstring)
|
||||
"""
|
||||
from evennia import DefaultCharacter
|
||||
|
||||
class Character(DefaultCharacter):
|
||||
"""
|
||||
(class docstring)
|
||||
"""
|
||||
pass
|
||||
```
|
||||
|
||||
This looks quite familiar now - an empty class inheriting from the Evennia base typeclass. As you would expect,
|
||||
this is also the default typeclass used for creating Characters if you don't specify it. You can verify it:
|
||||
|
||||
> examine me
|
||||
------------------------------------------------------------------------------
|
||||
Name/key: YourName (#1)
|
||||
Session id(s): #1
|
||||
Account: YourName
|
||||
Account Perms: <Superuser> (quelled)
|
||||
Typeclass: Character (typeclasses.characters.Character)
|
||||
Location: Limbo (#2)
|
||||
Home: Limbo (#2)
|
||||
Permissions: developer, player
|
||||
Locks: boot:false(); call:false(); control:perm(Developer); delete:false();
|
||||
drop:holds(); edit:false(); examine:perm(Developer); get:false();
|
||||
msg:all(); puppet:false(); tell:perm(Admin); view:all()
|
||||
Stored Cmdset(s):
|
||||
commands.default_cmdsets.CharacterCmdSet [DefaultCharacter] (Union, prio 0)
|
||||
Merged Cmdset(s):
|
||||
...
|
||||
Commands available to YourName (result of Merged CmdSets):
|
||||
...
|
||||
Persistent attributes:
|
||||
desc = This is User #1.
|
||||
prelogout_location = Limbo
|
||||
Non-Persistent attributes:
|
||||
last_cmd = None
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
You got a lot longer output this time. You have a lot more going on than a simple Object. Here are some new fields of note:
|
||||
- **Session id(s)**: This identifies the _Session_ (that is, the individual connection to a player's game client).
|
||||
- **Account** shows, well the `Account` object associated with this Character and Session.
|
||||
- **Stored/Merged Cmdsets** and **Commands available** is related to which _Commands_ are stored on you. We will
|
||||
get to them in the [next lesson](./Adding-Commands.md). For now it's enough to know these consitute all the
|
||||
commands available to you at a given moment.
|
||||
- **Non-Persistent attributes** are Attributes that are only stored temporarily and will go away on next reload.
|
||||
|
||||
Look at the **Typeclass** field and you'll find that it points to `typeclasses.character.Character` as expected.
|
||||
So if we modify this class we'll also modify ourselves.
|
||||
|
||||
### A method on ourselves
|
||||
|
||||
Let's try something simple first. Back in `mygame/typeclasses/characters.py`:
|
||||
|
||||
```python
|
||||
|
||||
class Character(DefaultCharacter):
|
||||
"""
|
||||
(class docstring)
|
||||
"""
|
||||
|
||||
str = 10
|
||||
dex = 12
|
||||
int = 15
|
||||
|
||||
def get_stats(self):
|
||||
"""
|
||||
Get the main stats of this character
|
||||
"""
|
||||
return self.str, self.dex, self.int
|
||||
|
||||
```
|
||||
|
||||
> reload
|
||||
> py self.get_stats()
|
||||
(10, 12, 15)
|
||||
|
||||
```{sidebar} Tuples and lists
|
||||
|
||||
- A `list` is written `[a, b, c, d, ...]`. It can be modified after creation.
|
||||
- A `tuple` is written `(a, b, c, ...)`. It cannot be modified once created.
|
||||
```
|
||||
We made a new method, gave it a docstring and had it `return` the RP-esque values we set. It comes back as a
|
||||
_tuple_ `(10, 12, 15)`. To get a specific value you could specify the _index_ of the value you want,
|
||||
starting from zero:
|
||||
|
||||
> py stats = self.get_stats() ; print(f"Strength is {stats[0]}.")
|
||||
Strength is 10.
|
||||
|
||||
### Attributes
|
||||
|
||||
So what happens when we increase our strength? This would be one way:
|
||||
|
||||
> py self.str = self.str + 1
|
||||
> py self.str
|
||||
11
|
||||
|
||||
Here we set the strength equal to its previous value + 1. A shorter way to write this is to use Python's `+=`
|
||||
operator:
|
||||
|
||||
> py self.str += 1
|
||||
> py self.str
|
||||
12
|
||||
> py self.get_stats()
|
||||
(12, 12, 15)
|
||||
|
||||
This looks correct! Try to change the values for dex and int too; it works fine. However:
|
||||
|
||||
> reload
|
||||
> py self.get_stats()
|
||||
(10, 12, 15)
|
||||
|
||||
After a reload all our changes were forgotten. When we change properties like this, it only changes in memory,
|
||||
not in the database (nor do we modify the python module's code). So when we reloaded, the 'fresh' `Character`
|
||||
class was loaded, and it still has the original stats we wrote to it.
|
||||
|
||||
In principle we could change the python code. But we don't want to do that manually every time. And more importantly
|
||||
since we have the stats hardcoded in the class, _every_ character instance in the game will have exactly the
|
||||
same `str`, `dex` and `int` now! This is clearly not what we want.
|
||||
|
||||
Evennia offers a special, persistent type of property for this, called an `Attribute`. Rework your
|
||||
`mygame/typeclasses/characters.py` like this:
|
||||
|
||||
```python
|
||||
|
||||
class Character(DefaultCharacter):
|
||||
"""
|
||||
(class docstring)
|
||||
"""
|
||||
|
||||
def get_stats(self):
|
||||
"""
|
||||
Get the main stats of this character
|
||||
"""
|
||||
return self.db.str, self.db.dex, self.db.int
|
||||
```
|
||||
|
||||
```{sidebar} Spaces in Attribute name?
|
||||
|
||||
What if you want spaces in your Attribute name? Or you want to assign the
|
||||
name of the Attribute on-the fly? Then you can use `.attributes.add(name, value)` instead,
|
||||
for example `self.attributes.add("str", 10)`.
|
||||
|
||||
```
|
||||
|
||||
We removed the hard-coded stats and added added `.db` for every stat. The `.db` handler makes the stat
|
||||
into an an Evennia `Attribute`.
|
||||
|
||||
> reload
|
||||
> py self.get_stats()
|
||||
(None, None, None)
|
||||
|
||||
Since we removed the hard-coded values, Evennia don't know what they should be (yet). So all we get back
|
||||
is `None`, which is a Python reserved word to represent nothing, a no-value. This is different from a normal python
|
||||
property:
|
||||
|
||||
> py self.str
|
||||
AttributeError: 'Character' object has no attribute 'str'
|
||||
> py self.db.str
|
||||
(nothing will be displayed, because it's None)
|
||||
|
||||
Trying to get an unknown normal Python property will give an error. Getting an unknown Evennia `Attribute` will
|
||||
never give an error, but only result in `None` being returned. This is often very practical.
|
||||
|
||||
> py self.db.str, self.db.dex, self.db.int = 10, 12, 15
|
||||
> py self.get_stats()
|
||||
(10, 12, 15)
|
||||
> reload
|
||||
> py self.get_stats()
|
||||
(10, 12, 15)
|
||||
|
||||
Now we set the Attributes to the right values. We can see that things work the same as before, also after a
|
||||
server reload. Let's modify the strength:
|
||||
|
||||
> py self.db.str += 2
|
||||
> py self.get_stats()
|
||||
(12, 12, 15)
|
||||
> reload
|
||||
> py self.get_stats()
|
||||
(12, 12, 15)
|
||||
|
||||
Our change now survives a reload since Evennia automatically saves the Attribute to the database for us.
|
||||
|
||||
### Setting things on new Characters
|
||||
|
||||
Things a looking better, but one thing remains strange - the stats start out with a value `None` and we
|
||||
have to manually set them to something reasonable. In a later lesson we will investigate character-creation
|
||||
in more detail. For now, let's give every new character some random stats to start with.
|
||||
|
||||
We want those stats to be set only once, when the object is first created. For the Character, this method
|
||||
is called `at_object_creation`.
|
||||
|
||||
```{sidebar} __init__ vs at_object_creation
|
||||
|
||||
For the `Monster` class we used `__init__` to set up the class. We can't use this
|
||||
for a typeclass because it will be called more than once, at the very least after
|
||||
every reload and maybe more depending on caching. Even if you are familiar with Python,
|
||||
avoid touching `__init__` for typeclasses, the results will not be what you expect.
|
||||
|
||||
```
|
||||
|
||||
```python
|
||||
# up by the other imports
|
||||
import random
|
||||
|
||||
class Character(DefaultCharacter):
|
||||
"""
|
||||
(class docstring)
|
||||
"""
|
||||
|
||||
def at_object_creation(self):
|
||||
self.db.str = random.randint(3, 18)
|
||||
self.db.dex = random.randint(3, 18)
|
||||
self.db.int = random.randint(3, 18)
|
||||
|
||||
def get_stats(self):
|
||||
"""
|
||||
Get the main stats of this character
|
||||
"""
|
||||
return self.db.str, self.db.dex, self.db.int
|
||||
```
|
||||
|
||||
We imported a new module, `random`. This is part of Python's standard library. We used `random.randint` to
|
||||
set a random value from 3 to 18 to each stat. Simple, but for some classical RPGs this is all you need!
|
||||
|
||||
> reload
|
||||
> py self.get_stats()
|
||||
(12, 12, 15)
|
||||
|
||||
Hm, this is the same values we set before. They are not random. The reason for this is of course that, as said,
|
||||
`at_object_creation` only runs _once_, the very first time a character is created. Our character object was already
|
||||
created long before, so it will not be called again.
|
||||
|
||||
It's simple enough to run it manually though:
|
||||
|
||||
> self.at_object_creation()
|
||||
> py self.get_stats()
|
||||
(5, 4, 8)
|
||||
|
||||
Lady luck didn't smile on us for this example; maybe you'll fare better. Evennia has a helper command
|
||||
`update` that re-runs the creation hook and also cleans up any other Attributes not re-created by `at_object_creation`:
|
||||
|
||||
> update self
|
||||
> py self.get_stats()
|
||||
(8, 16, 14)
|
||||
|
||||
### Updating all Characters in a loop
|
||||
|
||||
Needless to say, for your game you are wise to have a feel for what you want to go into the `at_object_creation` hook
|
||||
before you create a lot of objects (characters in this case). But should it come to that you don't want to have to
|
||||
go around and re-run the method on everyone manually. For the Python beginner, doing this will also give a chance to
|
||||
try out Python _loops_. We try them out in multi-line Python mode:
|
||||
|
||||
> py
|
||||
> for a in [1, 2, "foo"]: > print(a)
|
||||
1
|
||||
2
|
||||
foo
|
||||
|
||||
A python _for-loop_ allows us to loop over something. Above, we made a _list_ of two numbers and a string. In
|
||||
every iteration of the loop, the variable `a` becomes one element in turn, and we print that.
|
||||
|
||||
For our list, we want to loop over all Characters, and want to call `.at_object_creation` on each. This is how
|
||||
this is done (still in python multi-line mode):
|
||||
|
||||
> from typeclasses.characters import Character
|
||||
> for char in Character.objects.all()
|
||||
> char.at_object_creation()
|
||||
|
||||
```{sidebar} Database queries
|
||||
|
||||
`Character.objects.all()` is an example of a database query expressed in Python. This will be converted
|
||||
into a database query under the hood. This syntax is part of
|
||||
`Django's query language <https://docs.djangoproject.com/en/3.0/topics/db/queries/>`_. You don't need to
|
||||
know Django to use Evennia, but if you ever need more specific database queries, this is always available
|
||||
when you need it.
|
||||
|
||||
```
|
||||
We import the `Character` class and then we use `.objects.all()` to get all `Character` instances. Simplified,
|
||||
`.objects` is a resource from which one can _query_ for all `Characters`. Using `.all()` gets us a listing
|
||||
of all of them that we then immediately loop over. Boom, we just updated all Characters, including ourselves:
|
||||
|
||||
> quit()
|
||||
Closing the Python console.
|
||||
> self.get_stats()
|
||||
(3, 18, 10)
|
||||
|
||||
## Extra Credits
|
||||
|
||||
This principle is the same for other typeclasses. So using the tools explored in this lesson, try to expand
|
||||
the default room with an `is_dark` flag. It can be either `True` or `False`.
|
||||
Have all new rooms start with `is_dark = False` and make it so that once you change it, it survives a reload.
|
||||
Oh, and if you created any other rooms before, make sure they get the new flag too!
|
||||
|
||||
## Conclusions
|
||||
|
||||
In this lesson we created database-persistent dragons by having their classes inherit from one `Object`, one
|
||||
of Evennia's _typeclasses_. We explored where Evennia looks for typeclasses if we don't specify the path
|
||||
explicitly. We then modified ourselves - via the `Character` class - to give us some simple RPG stats. This
|
||||
led to the need to use Evennia's _Attributes_, settable via `.db` and to use a for-loop to update ourselves.
|
||||
|
||||
Typeclasses are a fundamental part of Evennia and we will see a lot of more uses of them in the course of
|
||||
this tutorial. But that's enough of them for now. It's time to take some action. Let's learn about _Commands_.
|
||||
|
||||
|
||||
497
docs/source/Howtos/Beginner-Tutorial/Part1/More-on-Commands.md
Normal file
497
docs/source/Howtos/Beginner-Tutorial/Part1/More-on-Commands.md
Normal file
|
|
@ -0,0 +1,497 @@
|
|||
# Parsing Command input
|
||||
|
||||
In this lesson we learn some basics about parsing the input of Commands. We will
|
||||
also learn how to add, modify and extend Evennia's default commands.
|
||||
|
||||
## More advanced parsing
|
||||
|
||||
In the last lesson we made a `hit` Command and hit a dragon with it. You should have the code
|
||||
from that still around.
|
||||
|
||||
Let's expand our simple `hit` command to accept a little more complex input:
|
||||
|
||||
hit <target> [[with] <weapon>]
|
||||
|
||||
That is, we want to support all of these forms
|
||||
|
||||
hit target
|
||||
hit target weapon
|
||||
hit target with weapon
|
||||
|
||||
If you don't specify a weapon you'll use your fists. It's also nice to be able to skip "with" if
|
||||
you are in a hurry. Time to modify `mygame/commands/mycommands.py` again. Let us break out the parsing
|
||||
a little, in a new method `parse`:
|
||||
|
||||
|
||||
```python
|
||||
#...
|
||||
|
||||
class CmdHit(Command):
|
||||
"""
|
||||
Hit a target.
|
||||
|
||||
Usage:
|
||||
hit <target>
|
||||
|
||||
"""
|
||||
key = "hit"
|
||||
|
||||
def parse(self):
|
||||
self.args = self.args.strip()
|
||||
target, *weapon = self.args.split(" with ", 1)
|
||||
if not weapon:
|
||||
target, *weapon = target.split(" ", 1)
|
||||
self.target = target.strip()
|
||||
if weapon:
|
||||
self.weapon = weapon.strip()
|
||||
else:
|
||||
self.weapon = ""
|
||||
|
||||
def func(self):
|
||||
if not self.args:
|
||||
self.caller.msg("Who do you want to hit?")
|
||||
return
|
||||
# get the target for the hit
|
||||
target = self.caller.search(self.target)
|
||||
if not target:
|
||||
return
|
||||
# get and handle the weapon
|
||||
weapon = None
|
||||
if self.weapon:
|
||||
weapon = self.caller.search(self.weapon)
|
||||
if weapon:
|
||||
weaponstr = f"{weapon.key}"
|
||||
else:
|
||||
weaponstr = "bare fists"
|
||||
|
||||
self.caller.msg(f"You hit {target.key} with {weaponstr}!")
|
||||
target.msg(f"You got hit by {self.caller.key} with {weaponstr}!")
|
||||
# ...
|
||||
|
||||
```
|
||||
|
||||
The `parse` method is called before `func` and has access to all the same on-command variables as in `func`. Using
|
||||
`parse` not only makes things a little easier to read, it also means you can easily let other Commands _inherit_
|
||||
your parsing - if you wanted some other Command to also understand input on the form `<arg> with <arg>` you'd inherit
|
||||
from this class and just implement the `func` needed for that command without implementing `parse` anew.
|
||||
|
||||
```{sidebar} Tuples and Lists
|
||||
|
||||
- A `list` is written as `[a, b, c, d, ...]`. You can add and grow/shrink a list after it was first created.
|
||||
- A `tuple` is written as `(a, b, c, d, ...)`. A tuple cannot be modified once it is created.
|
||||
|
||||
```
|
||||
- **Line 14** - We do the stripping of `self.args` once and for all here. We also store the stripped version back
|
||||
into `self.args`, overwriting it. So there is no way to get back the non-stripped version from here on, which is fine
|
||||
for this command.
|
||||
- **Line 15** - This makes use of the `.split` method of strings. `.split` will, well, split the string by some criterion.
|
||||
`.split(" with ", 1)` means "split the string once, around the substring `" with "` if it exists". The result
|
||||
of this split is a _list_. Just how that list looks depends on the string we are trying to split:
|
||||
1. If we entered just `hit smaug`, we'd be splitting just `"smaug"` which would give the result `["smaug"]`.
|
||||
2. `hit smaug sword` gives `["smaug sword"]`
|
||||
3. `hit smaug with sword` gives `["smaug", "sword"]`
|
||||
|
||||
So we get a list of 1 or 2 elements. We assign it to two variables like this, `target, *weapon = `. That
|
||||
asterisk in `*weapon` is a nifty trick - it will automatically become a list of _0 or more_ values. It sorts of
|
||||
"soaks" up everything left over.
|
||||
1. `target` becomes `"smaug"` and `weapon` becomes `[]`
|
||||
2. `target` becomes `"smaug sword"` and `weapon` becomes `[]`
|
||||
3. `target` becomes `"smaug"` and `weapon` becomes `sword`
|
||||
- **Lines 16-17** - In this `if` condition we check if `weapon` is falsy (that is, the empty list). This can happen
|
||||
under two conditions (from the example above):
|
||||
1. `target` is simply `smaug`
|
||||
2. `target` is `smaug sword`
|
||||
|
||||
To separate these cases we split `target` once again, this time by empty space `" "`. Again we store the
|
||||
result back with `target, *weapon =`. The result will be one of the following:
|
||||
1. `target` remains `smaug` and `weapon` remains `[]`
|
||||
2. `target` becomes `smaug` and `weapon` becomes `sword`
|
||||
- **Lines 18-22** - We now store `target` and `weapon` into `self.target` and `self.weapon`. We must do this in order
|
||||
for these local variables to made available in `func` later. Note how we need to check so `weapon` is not falsy
|
||||
before running `strip()` on it. This is because we know that if it's falsy, it's an empty list `[]` and lists
|
||||
don't have the `.strip()` method on them (so if we tried to use it, we'd get an error).
|
||||
|
||||
Now onto the `func` method. The main difference is we now have `self.target` and `self.weapon` available for
|
||||
convenient use.
|
||||
- **Lines 29 and 35** - We make use of the previously parsed search terms for the target and weapon to find the
|
||||
respective resource.
|
||||
- **Lines 34-39** - Since the weapon is optional, we need to supply a default (use our fists!) if it's not set. We
|
||||
use this to create a `weaponstr` that is different depending on if we have a weapon or not.
|
||||
- **Lines 41-42** - We merge the `weaponstr` with our attack text.
|
||||
|
||||
Let's try it out!
|
||||
|
||||
> reload
|
||||
> hit smaug with sword
|
||||
Could not find 'sword'.
|
||||
You hit smaug with bare fists!
|
||||
|
||||
Oops, our `self.caller.search(self.weapon)` is telling us that it found no sword. Since we are not `return`ing
|
||||
in this situation (like we do if failing to find `target`) we still continue fighting with our bare hands.
|
||||
This won't do. Let's make ourselves a sword.
|
||||
|
||||
> create sword
|
||||
|
||||
Since we didn't specify `/drop`, the sword will end up in our inventory and can seen with the `i` or
|
||||
`inventory` command. The `.search` helper will still find it there. There is no need to reload to see this
|
||||
change (no code changed, only stuff in the database).
|
||||
|
||||
> hit smaug with sword
|
||||
You hit smaug with sword!
|
||||
|
||||
|
||||
## Adding a Command to an object
|
||||
|
||||
The commands of a cmdset attached to an object with `obj.cmdset.add()` will by default be made available to that object
|
||||
but _also to those in the same location as that object_. If you did the [Building introduction](./Building-Quickstart.md)
|
||||
you've seen an example of this with the "Red Button" object. The [Tutorial world](./Tutorial-World.md)
|
||||
also has many examples of objects with commands on them.
|
||||
|
||||
To show how this could work, let's put our 'hit' Command on our simple `sword` object from the previous section.
|
||||
|
||||
> self.search("sword").cmdset.add("commands.mycommands.MyCmdSet", persistent=True)
|
||||
|
||||
We find the sword (it's still in our inventory so `self.search` should be able to find it), then
|
||||
add `MyCmdSet` to it. This actually adds both `hit` and `echo` to the sword, which is fine.
|
||||
|
||||
Let's try to swing it!
|
||||
|
||||
> hit
|
||||
More than one match for 'hit' (please narrow target):
|
||||
hit-1 (sword #11)
|
||||
hit-2
|
||||
|
||||
```{sidebar} Multi-matches
|
||||
|
||||
Some game engines will just pick the first hit when finding more than one.
|
||||
Evennia will always give you a choice. The reason for this is that Evennia
|
||||
cannot know if `hit` and `hit` are different or the same - maybe it behaves
|
||||
differently depending on the object it sits on? Besides, imagine if you had
|
||||
a red and a blue button both with the command `push` on it. Now you just write
|
||||
`push`. Wouldn't you prefer to be asked `which` button you really wanted to push?
|
||||
```
|
||||
Woah, that didn't go as planned. Evennia actually found _two_ `hit` commands to didn't know which one to use
|
||||
(_we_ know they are the same, but Evennia can't be sure of that). As we can see, `hit-1` is the one found on
|
||||
the sword. The other one is from adding `MyCmdSet` to ourself earlier. It's easy enough to tell Evennia which
|
||||
one you meant:
|
||||
|
||||
> hit-1
|
||||
Who do you want to hit?
|
||||
> hit-2
|
||||
Who do you want to hit?
|
||||
|
||||
In this case we don't need both command-sets, so let's just keep the one on the sword:
|
||||
|
||||
> self.cmdset.remove("commands.mycommands.MyCmdSet")
|
||||
> hit
|
||||
Who do you want to hit?
|
||||
|
||||
Now try this:
|
||||
|
||||
> tunnel n = kitchen
|
||||
> n
|
||||
> drop sword
|
||||
> s
|
||||
> hit
|
||||
Command 'hit' is not available. Maybe you meant ...
|
||||
> n
|
||||
> hit
|
||||
Who do you want to hit?
|
||||
|
||||
The `hit` command is now only available if you hold or are in the same room as the sword.
|
||||
|
||||
### You need to hold the sword!
|
||||
|
||||
Let's get a little ahead of ourselves and make it so you have to _hold_ the sword for the `hit` command to
|
||||
be available. This involves a _Lock_. We've cover locks in more detail later, just know that they are useful
|
||||
for limiting the kind of things you can do with an object, including limiting just when you can call commands on
|
||||
it.
|
||||
```{sidebar} Locks
|
||||
|
||||
Evennia Locks are defined as a mini-language defined in `lockstrings`. The lockstring
|
||||
is on a form `<situation>:<lockfuncs>`, where `situation` determines when this
|
||||
lock applies and the `lockfuncs` (there can be more than one) are run to determine
|
||||
if the lock-check passes or not depending on circumstance.
|
||||
```
|
||||
|
||||
> py self.search("sword").locks.add("call:holds()")
|
||||
|
||||
We added a new lock to the sword. The _lockstring_ `"call:holds()"` means that you can only _call_ commands on
|
||||
this object if you are _holding_ the object (that is, it's in your inventory).
|
||||
|
||||
For locks to work, you cannot be _superuser_, since the superuser passes all locks. You need to `quell` yourself
|
||||
first:
|
||||
```{sidebar} quell/unquell
|
||||
|
||||
Quelling allows you as a developer to take on the role of players with less
|
||||
priveleges. This is useful for testing and debugging, in particular since a
|
||||
superuser has a little `too` much power sometimes.
|
||||
Use `unquell` to get back to your normal self.
|
||||
```
|
||||
|
||||
> quell
|
||||
|
||||
If the sword lies on the ground, try
|
||||
|
||||
> hit
|
||||
Command 'hit' is not available. ..
|
||||
> get sword
|
||||
> hit
|
||||
> Who do you want to hit?
|
||||
|
||||
|
||||
Finally, we get rid of ours sword so we have a clean slate with no more `hit` commands floating around.
|
||||
We can do that in two ways:
|
||||
|
||||
delete sword
|
||||
|
||||
or
|
||||
|
||||
py self.search("sword").delete()
|
||||
|
||||
|
||||
## Adding the Command to a default Cmdset
|
||||
|
||||
|
||||
As we have seen we can use `obj.cmdset.add()` to add a new cmdset to objects, whether that object
|
||||
is ourself (`self`) or other objects like the `sword`.
|
||||
|
||||
This is how all commands in Evennia work, including default commands like `look`, `dig`, `inventory` and so on.
|
||||
All these commands are in just loaded on the default objects that Evennia provides out of the box.
|
||||
|
||||
- Characters (that is 'you' in the gameworld) has the `CharacterCmdSet`.
|
||||
- Accounts (the thing that represents your out-of-character existence on the server) has the `AccountCmdSet`
|
||||
- Sessions (representing one single client connection) has the `SessionCmdSet`
|
||||
- Before you log in (at the connection screen) you'll have access to the `UnloggedinCmdSet`.
|
||||
|
||||
The thing must commonly modified is the `CharacterCmdSet`.
|
||||
|
||||
The default cmdset are defined in `mygame/commands/default_cmdsets.py`. Open that file now:
|
||||
|
||||
```python
|
||||
"""
|
||||
(module docstring)
|
||||
"""
|
||||
|
||||
from evennia import default_cmds
|
||||
|
||||
class CharacterCmdSet(default_cmds.CharacterCmdSet):
|
||||
|
||||
key = "DefaultCharacter"
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
|
||||
super().at_cmdset_creation()
|
||||
#
|
||||
# any commands you add below will overload the default ones
|
||||
#
|
||||
|
||||
class AccountCmdSet(default_cmds.AccountCmdSet):
|
||||
|
||||
key = "DefaultAccount"
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
|
||||
super().at_cmdset_creation()
|
||||
#
|
||||
# any commands you add below will overload the default ones
|
||||
#
|
||||
|
||||
class UnloggedinCmdSet(default_cmds.UnloggedinCmdSet):
|
||||
|
||||
key = "DefaultUnloggedin"
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
|
||||
super().at_cmdset_creation()
|
||||
#
|
||||
# any commands you add below will overload the default ones
|
||||
#
|
||||
|
||||
class SessionCmdSet(default_cmds.SessionCmdSet):
|
||||
|
||||
key = "DefaultSession"
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
|
||||
super().at_cmdset_creation()
|
||||
#
|
||||
# any commands you add below will overload the default ones
|
||||
#
|
||||
```
|
||||
|
||||
```{sidebar} super()
|
||||
|
||||
The `super()` function refers to the parent of the current class and is commonly
|
||||
used to call same-named methods on the parent.
|
||||
```
|
||||
`evennia.default_cmds` is a container that holds all of Evennia's default commands and cmdsets. In this module
|
||||
we can see that this was imported and then a new child class was made for each cmdset. Each class looks familiar
|
||||
(except the `key`, that's mainly used to easily identify the cmdset in listings). In each `at_cmdset_creation` all
|
||||
we do is call `super().at_cmdset_creation` which means that we call `at_cmdset_creation() on the _parent_ CmdSet.
|
||||
This is what adds all the default commands to each CmdSet.
|
||||
|
||||
To add even more Commands to a default cmdset, we can just add them below the `super()` line. Usefully, if we were to
|
||||
add a Command with the same `.key` as a default command, it would completely replace that original. So if you were
|
||||
to add a command with a key `look`, the original `look` command would be replaced by your own version.
|
||||
|
||||
For now, let's add our own `hit` and `echo` commands to the `CharacterCmdSet`:
|
||||
|
||||
|
||||
```python
|
||||
# ...
|
||||
|
||||
from commands import mycommands
|
||||
|
||||
class CharacterCmdSet(default_cmds.CharacterCmdSet):
|
||||
|
||||
key = "DefaultCharacter"
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
|
||||
super().at_cmdset_creation()
|
||||
#
|
||||
# any commands you add below will overload the default ones
|
||||
#
|
||||
self.add(mycommands.CmdEcho)
|
||||
self.add(mycommands.CmdHit)
|
||||
|
||||
```
|
||||
|
||||
> reload
|
||||
> hit
|
||||
Who do you want to hit?
|
||||
|
||||
Your new commands are now available for all player characters in the game. There is another way to add a bunch
|
||||
of commands at once, and that is to add a _CmdSet_ to the other cmdset. All commands in that cmdset will then be added:
|
||||
|
||||
```python
|
||||
from commands import mycommands
|
||||
|
||||
class CharacterCmdSet(default_cmds.CharacterCmdSet):
|
||||
|
||||
key = "DefaultCharacter"
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
|
||||
super().at_cmdset_creation()
|
||||
#
|
||||
# any commands you add below will overload the default ones
|
||||
#
|
||||
self.add(mycommands.MyCmdSet)
|
||||
```
|
||||
|
||||
Which way you use depends on how much control you want, but if you already have a CmdSet,
|
||||
this is practical. A Command can be a part of any number of different CmdSets.
|
||||
|
||||
### Removing Commands
|
||||
|
||||
To remove your custom commands again, you of course just delete the change you did to
|
||||
`mygame/commands/default_cmdsets.py`. But what if you want to remove a default command?
|
||||
|
||||
We already know that we use `cmdset.remove()` to remove a cmdset. It turns out you can
|
||||
do the same in `at_cmdset_creation`. For example, let's remove the default `get` Command
|
||||
from Evennia. We happen to know this can be found as `default_cmds.CmdGet`.
|
||||
|
||||
|
||||
```python
|
||||
# ...
|
||||
from commands import mycommands
|
||||
|
||||
class CharacterCmdSet(default_cmds.CharacterCmdSet):
|
||||
|
||||
key = "DefaultCharacter"
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
|
||||
super().at_cmdset_creation()
|
||||
#
|
||||
# any commands you add below will overload the default ones
|
||||
#
|
||||
self.add(mycommands.MyCmdSet)
|
||||
self.remove(default_cmds.CmdGet)
|
||||
# ...
|
||||
```
|
||||
|
||||
> reload
|
||||
> get
|
||||
Command 'get' is not available ...
|
||||
|
||||
## Replace a default command
|
||||
|
||||
At this point you already have all the pieces for how to do this! We just need to add a new
|
||||
command with the same `key` in the `CharacterCmdSet` to replace the default one.
|
||||
|
||||
Let's combine this with what we know about classes and
|
||||
how to _override_ a parent class. Open `mygame/commands/mycommands.py` and lets override
|
||||
that `CmdGet` command.
|
||||
|
||||
```python
|
||||
# up top, by the other imports
|
||||
from evennia import default_cmds
|
||||
|
||||
# somewhere below
|
||||
class MyCmdGet(default_cmds.CmdGet):
|
||||
|
||||
def func(self):
|
||||
super().func()
|
||||
self.caller.msg(str(self.caller.location.contents))
|
||||
|
||||
```
|
||||
|
||||
- **Line2**: We import `default_cmds` so we can get the parent class.
|
||||
We made a new class and we make it _inherit_ `default_cmds.CmdGet`. We don't
|
||||
need to set `.key` or `.parse`, that's already handled by the parent.
|
||||
In `func` we call `super().func()` to let the parent do its normal thing,
|
||||
- **Line 7**: By adding our own `func` we replace the one in the parent.
|
||||
- **Line 8**: For this simple change we still want the command to work the
|
||||
same as before, so we use `super()` to call `func` on the parent.
|
||||
- **Line 9**: `.location` is the place an object is at. `.contents` contains, well, the
|
||||
contents of an object. If you tried `py self.contents` you'd get a list that equals
|
||||
your inventory. For a room, the contents is everything in it.
|
||||
So `self.caller.location.contents` gets the contents of our current location. This is
|
||||
a _list_. In order send this to us with `.msg` we turn the list into a string. Python
|
||||
has a special function `str()` to do this.
|
||||
|
||||
We now just have to add this so it replaces the default `get` command. Open
|
||||
`mygame/commands/default_cmdsets.py` again:
|
||||
|
||||
```python
|
||||
# ...
|
||||
from commands import mycommands
|
||||
|
||||
class CharacterCmdSet(default_cmds.CharacterCmdSet):
|
||||
|
||||
key = "DefaultCharacter"
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
|
||||
super().at_cmdset_creation()
|
||||
#
|
||||
# any commands you add below will overload the default ones
|
||||
#
|
||||
self.add(mycommands.MyCmdSet)
|
||||
self.add(mycommands.MyCmdGet)
|
||||
# ...
|
||||
```
|
||||
```{sidebar} Another way
|
||||
|
||||
Instead of adding `MyCmdGet` explicitly in default_cmdset.py,
|
||||
you could also add it to `mycommands.MyCmdSet` and let it be
|
||||
added automatically for you.
|
||||
```
|
||||
|
||||
> reload
|
||||
> get
|
||||
Get What?
|
||||
[smaug, fluffy, YourName, ...]
|
||||
|
||||
We just made a new `get`-command that tells us everything we could pick up (well, we can't pick up ourselves, so
|
||||
there's some room for improvement there).
|
||||
|
||||
## Summary
|
||||
|
||||
In this lesson we got into some more advanced string formatting - many of those tricks will help you a lot in
|
||||
the future! We also made a functional sword. Finally we got into how to add to, extend and replace a default
|
||||
command on ourselves.
|
||||
|
||||
|
|
@ -0,0 +1,653 @@
|
|||
# Intro to using Python with Evennia
|
||||
|
||||
Time to dip our toe into some coding! Evennia is written and extended in [Python](https://python.org),
|
||||
which is a mature and professional programming language that is very fast to work with.
|
||||
|
||||
That said, even though Python is widely considered easy to learn, we can only cover the most immediately
|
||||
important aspects of Python in this series of starting tutorials. Hopefully we can get you started
|
||||
but then you'll need to continue learning from there. See our [link section](../../../Links.md) for finding
|
||||
more reference material and dedicated Python tutorials.
|
||||
|
||||
> While this will be quite basic if you are an experienced developer, you may want to at least
|
||||
> stay around for the first few sections where we cover how to run Python from inside Evennia.
|
||||
|
||||
First, if you were quelling yourself to play the tutorial world, make sure to get your
|
||||
superuser powers back:
|
||||
|
||||
unquell
|
||||
|
||||
## Evennia Hello world
|
||||
|
||||
The `py` Command (or `!`, which is an alias) allows you as a superuser to execute raw Python from in-
|
||||
game. This is useful for quick testing. From the game's input line, enter the following:
|
||||
|
||||
> py print("Hello World!")
|
||||
|
||||
|
||||
```{sidebar} Command input
|
||||
|
||||
The line with `>` indicates input to enter in-game, while the lines below are the
|
||||
expected return from that input.
|
||||
```
|
||||
|
||||
You will see
|
||||
|
||||
> print("Hello world!")
|
||||
Hello World!
|
||||
|
||||
To understand what is going on: some extra info: The `print(...)` *function* is the basic, in-built
|
||||
way to output text in Python. We are sending "Hello World" as an _argument_ to this function. The quotes `"..."`
|
||||
mean that you are inputting a *string* (i.e. text). You could also have used single-quotes `'...'`,
|
||||
Python accepts both. A third variant is triple-quotes (`"""..."""` or `'''...'''`, which work across multiple
|
||||
lines and are common for larger text-blocks. The way we use the `py` command right now only supports
|
||||
single-line input however.
|
||||
|
||||
## Making some text 'graphics'
|
||||
|
||||
When making a text-game you will, unsurprisingly, be working a lot with text. Even if you have the occational
|
||||
button or even graphical element, the normal process is for the user to input commands as
|
||||
text and get text back. As we saw above, a piece of text is called a _string_ in Python and is enclosed in
|
||||
either single- or double-quotes.
|
||||
|
||||
Strings can be added together:
|
||||
|
||||
> py print("This is a " + "breaking change.")
|
||||
This is a breaking change.
|
||||
|
||||
A string multiplied with a number will repeat that string as many times:
|
||||
|
||||
> py print("|" + "-" * 40 + "|")
|
||||
|----------------------------------------|
|
||||
|
||||
or
|
||||
|
||||
> py print("A" + "a" * 5 + "rgh!")
|
||||
Aaaaaargh!
|
||||
|
||||
### .format()
|
||||
|
||||
While combining different strings is useful, even more powerful is the ability to modify the contents
|
||||
of the string in-place. There are several ways to do this in Python and we'll show two of them here. The first
|
||||
is to use the `.format` _method_ of the string:
|
||||
|
||||
> py print("This is a {} idea!".format("good"))
|
||||
This is a good idea!
|
||||
|
||||
```{eval-rst}
|
||||
.. sidebar:: Functions and Methods
|
||||
|
||||
Function:
|
||||
Something that performs and action when you `call` it with zero or more `arguments`. A function
|
||||
is stand-alone in a python module, like `print()`
|
||||
Method:
|
||||
A function that sits "on" an object, like `<string>.format()`.
|
||||
```
|
||||
|
||||
A method can be thought of as a resource "on" another object. The method knows on which object it
|
||||
sits and can thus affect it in various ways. You access it with the period `.`. In this case, the
|
||||
string has a resource `format(...)` that modifies it. More specifically, it replaced the `{}` marker
|
||||
inside the string with the value passed to the format. You can do so many times:
|
||||
|
||||
> py print("This is a {} idea!".format("bad"))
|
||||
This is a bad idea!
|
||||
|
||||
or
|
||||
|
||||
> py print("This is the {} and {} {} idea!".format("first", "second", "great"))
|
||||
This is the first and second great idea!
|
||||
|
||||
> Note the double-parenthesis at the end - the first closes the `format(...` method and the outermost
|
||||
closes the `print(...`. Not closing them will give you a scary `SyntaxError`. We will talk a
|
||||
little more about errors in the next section, for now just fix until it prints as expected.
|
||||
|
||||
Here we passed three comma-separated strings as _arguments_ to the string's `format` method. These
|
||||
replaced the `{}` markers in the same order as they were given.
|
||||
|
||||
The input does not have to be strings either:
|
||||
|
||||
> py print("STR: {}, DEX: {}, INT: {}".format(12, 14, 8))
|
||||
STR: 12, DEX: 14, INT: 8
|
||||
|
||||
To separate two Python instructions on the same line, you use the semi-colon, `;`. Try this:
|
||||
|
||||
> py a = "awesome sauce" ; print("This is {}!".format(a))
|
||||
This is awesome sauce!
|
||||
|
||||
```{warning} MUD clients and semi-colon
|
||||
|
||||
Some MUD clients use the semi-colon `;` to split client-inputs
|
||||
into separate sends. If so, the above will give an error. Most clients allow you to
|
||||
run in 'verbatim' mode or to remap to use some other separator than `;`. If you still have
|
||||
trouble, use the Evennia web client.
|
||||
```
|
||||
|
||||
What happened here was that we _assigned_ the string `"awesome sauce"` to a _variable_ we chose
|
||||
to name `a`. In the next statement, Python remembered what `a` was and we passed that into `format()`
|
||||
to get the output. If you replaced the value of `a` with something else in between, _that_ would be printed
|
||||
instead.
|
||||
|
||||
Here's the stat-example again, moving the stats to variables (here we just set them, but in a real
|
||||
game they may be changed over time, or modified by circumstance):
|
||||
|
||||
> py stren, dext, intel = 13, 14, 8 ; print("STR: {}, DEX: {}, INT: {}".format(stren, dext, intel))
|
||||
STR: 13, DEX: 14, INT: 8
|
||||
|
||||
The point is that even if the values of the stats change, the print() statement would not change - it just keeps
|
||||
pretty-printing whatever is given to it.
|
||||
|
||||
### f-strings
|
||||
|
||||
Using `.format()` is convenient (and there is a [lot more](https://www.w3schools.com/python/ref_string_format.asp)
|
||||
you can do with it). But the _f-string_ can be even more convenient. An
|
||||
f-string looks like a normal string ... except there is an `f` front of it, like this:
|
||||
|
||||
f"this is now an f-string."
|
||||
|
||||
An f-string on its own is just like any other string. But let's redo the example we did before, using an f-string:
|
||||
|
||||
> py a = "awesome sauce" ; print(f"This is {a}!")
|
||||
This is awesome sauce!
|
||||
|
||||
We could just insert that `a` variable directly into the f-string using `{a}`. Fewer parentheses to
|
||||
remember and arguable easier to read as well.
|
||||
|
||||
> py stren, dext, intel = 13, 14, 8 ; print(f"STR: {stren}, DEX: {dext}, INT: {intel}")
|
||||
STR: 13, DEX: 14, INT: 8
|
||||
|
||||
We will be exploring more complex string concepts when we get to creating Commands and need to
|
||||
parse and understand player input.
|
||||
|
||||
### Colored text
|
||||
|
||||
Python itself knows nothing about colored text, this is an Evennia thing. Evennia supports the
|
||||
standard color schemes of traditional MUDs.
|
||||
|
||||
> py print("|rThis is red text!|n This is normal color.")
|
||||
|
||||
Adding that `|r` at the start will turn our output bright red. `|R` will make it dark red. `|n`
|
||||
gives the normal text color. You can also use RGB (Red-Green-Blue) values from 0-5 (Xterm256 colors):
|
||||
|
||||
> py print("|043This is a blue-green color.|[530|003 Now dark blue text on orange background.")
|
||||
|
||||
> If you don't see the expected color, your client or terminal may not support Xterm256 (or
|
||||
color at all). Use the Evennia webclient.
|
||||
|
||||
Use the commands `color ansi` or `color xterm` to see which colors are available. Experiment!
|
||||
|
||||
## Importing code from other modules
|
||||
|
||||
As we saw in the previous sections, we used `.format` to format strings and `me.msg` to access
|
||||
the `msg` method on `me`. This use of the full-stop character is used to access all sorts of resources,
|
||||
including that in other Python modules.
|
||||
|
||||
Keep your game running, then open a text editor of your choice. If your game folder is called
|
||||
`mygame`, create a new text file `test.py` in the subfolder `mygame/world`. This is how the file
|
||||
structure should look:
|
||||
|
||||
```
|
||||
mygame/
|
||||
world/
|
||||
test.py
|
||||
```
|
||||
|
||||
For now, only add one line to `test.py`:
|
||||
|
||||
```python
|
||||
print("Hello World!")
|
||||
```
|
||||
|
||||
```{sidebar} Python module
|
||||
|
||||
This is a text file with the `.py` file ending. A module
|
||||
contains Python source code and from within Python one can
|
||||
access its contents by importing it via its python-path.
|
||||
```
|
||||
|
||||
Don't forget to _save_ the file. We just created our first Python _module_!
|
||||
To use this in-game we have to *import* it. Try this:
|
||||
|
||||
> py import world.test
|
||||
Hello World
|
||||
|
||||
If you make some error (we'll cover how to handle errors below), fix the error in the module and
|
||||
run the `reload` command in-game for your changes to take effect.
|
||||
|
||||
So importing `world.test` actually means importing `world/test.py`. Think of the period `.` as
|
||||
replacing `/` (or `\` for Windows) in your path. The `.py` ending of `test.py` is also never
|
||||
included in this "Python-path", but _only_ files with that ending can be imported this way.
|
||||
Where is `mygame` in that Python-path? The answer is that Evennia has already told Python that
|
||||
your `mygame` folder is a good place to look for imports. So we don't include `mygame` in the
|
||||
path - Evennia handles this for us.
|
||||
|
||||
When you import the module, the top "level" of it will execute. In this case, it will immediately
|
||||
print "Hello World".
|
||||
|
||||
Now try to run this a second time:
|
||||
|
||||
> py import world.test
|
||||
|
||||
You will *not* see any output this second time or any subsequent times! This is not a bug. Rather
|
||||
it is because of how Python importing works - it stores all imported modules and will
|
||||
avoid importing them more than once. So your `print` will only run the first time, when the module
|
||||
is first imported.
|
||||
|
||||
Try this:
|
||||
|
||||
> reload
|
||||
|
||||
And then
|
||||
|
||||
> py import world.test
|
||||
Hello World!
|
||||
|
||||
Now we see it again. The `reload` wiped the server's memory of what was imported, so it had to
|
||||
import it anew. You'd have to do this every time you wanted the print to show though, which is
|
||||
not very useful.
|
||||
|
||||
> We'll get back to more advanced ways to import code in later tutorial sections - this is an
|
||||
> important topic. But for now, let's press on and resolve this particular problem.
|
||||
|
||||
|
||||
### Our first own function
|
||||
|
||||
We want to be able to print our hello-world message at any time, not just once after a server
|
||||
reload. Change your `mygame/world/test.py` file to look like this:
|
||||
|
||||
```python
|
||||
def hello_world():
|
||||
print("Hello World!")
|
||||
```
|
||||
|
||||
As we are moving to multi-line Python code, there are some important things to remember:
|
||||
|
||||
- Capitalization matters in Python. It must be `def` and not `DEF`, `who` is not the same as `Who`.
|
||||
- Indentation matters in Python. The second line must be indented or it's not valid code. You should
|
||||
also use a consistent indentation length. We *strongly* recommend that you, for your own sanity's sake,
|
||||
set up your editor to always indent *4 spaces* (**not** a single tab-character) when you press the TAB key.
|
||||
|
||||
So about that function. Line 1:
|
||||
|
||||
- `def` is short for "define" and defines a *function* (or a *method*, if sitting on an object).
|
||||
This is a [reserved Python keyword](https://docs.python.org/2.5/ref/keywords.html); try not to use
|
||||
these words anywhere else.
|
||||
- A function name can not have spaces but otherwise we could have called it almost anything. We call
|
||||
it `hello_world`. Evennia follows [Python's standard naming style](https://github.com/evennia/evennia/blob/master/CODING_STYLE.md#a-quick-list-of-code-style-points)
|
||||
with lowercase letters and underscores. We recommend you do the same.
|
||||
- The colon (`:`) at the end of line 1 indicates that the header of the function is complete.
|
||||
|
||||
Line 2:
|
||||
|
||||
- The indentation marks the beginning of the actual operating code of the function (the function's
|
||||
*body*). If we wanted more lines to belong to this function those lines would all have to
|
||||
start at least at this indentation level.
|
||||
|
||||
Now let's try this out. First `reload` your game to have it pick up
|
||||
our updated Python module, then import it.
|
||||
|
||||
> reload
|
||||
> py import world.test
|
||||
|
||||
Nothing happened! That is because the function in our module won't do anything just by importing it (this
|
||||
is what we wanted). It will only act when we *call* it. So we need to first import the module and then access the
|
||||
function within:
|
||||
|
||||
> py import world.test ; world.test.hello_world()
|
||||
Hello world!
|
||||
|
||||
There is our "Hello World"! As mentioned earlier, use use semi-colon to put multiple
|
||||
Python-statements on one line. Note also the previous warning about mud-clients using the `;` to their
|
||||
own ends.
|
||||
|
||||
So what happened there? First we imported `world.test` as usual. But this time we continued and
|
||||
accessed the `hello_world` function _inside_ the newly imported module.
|
||||
|
||||
By adding `()` to the `hello_world` function we _call_ it, that is we run the body of the function and
|
||||
print our text. We can now redo this as many times as we want without having to `reload` in between:
|
||||
|
||||
|
||||
> py import world.test ; world.test.hello_world()
|
||||
Hello world!
|
||||
> py import world.test ; world.test.hello_world()
|
||||
Hello world!
|
||||
|
||||
## Sending text to others
|
||||
|
||||
The `print` command is a standard Python structure. We can use that here in the `py` command since
|
||||
we can se the output. It's great for debugging and quick testing. But if you need to send a text
|
||||
to an actual player, `print` won't do, because it doesn't know _who_ to send to. Try this:
|
||||
|
||||
> py me.msg("Hello world!")
|
||||
Hello world!
|
||||
|
||||
This looks the same as the `print` result, but we are now actually messaging a specific *object*,
|
||||
`me`. The `me` is a shortcut to 'us', the one running the `py` command. It is not some special
|
||||
Python thing, but something Evennia just makes available in the `py` command for convenience
|
||||
(`self` is an alias).
|
||||
|
||||
The `me` is an example of an *Object instance*. Objects are fundamental in Python and Evennia.
|
||||
The `me` object also contains a lot of useful resources for doing
|
||||
things with that object. We access those resources with '`.`'.
|
||||
|
||||
One such resource is `msg`, which works like `print` except it sends the text to the object it
|
||||
is attached to. So if we, for example, had an object `you`, doing `you.msg(...)` would send a message
|
||||
to the object `you`.
|
||||
|
||||
For now, `print` and `me.msg` behaves the same, just remember that `print` is mainly used for
|
||||
debugging and `.msg()` will be more useful for you in the future.
|
||||
|
||||
|
||||
## Parsing Python errors
|
||||
|
||||
Let's try this new text-sending in the function we just created. Go back to
|
||||
your `test.py` file and Replace the function with this instead:
|
||||
|
||||
```python
|
||||
def hello_world():
|
||||
me.msg("Hello World!")
|
||||
```
|
||||
|
||||
Save your file and `reload` your server to tell Evennia to re-import new code,
|
||||
then run it like before:
|
||||
|
||||
> py import world.test ; world.test.hello_world()
|
||||
|
||||
No go - this time you get an error!
|
||||
|
||||
```python
|
||||
File "./world/test.py", line 2, in hello_world
|
||||
me.msg("Hello World!")
|
||||
NameError: name 'me' is not defined
|
||||
```
|
||||
|
||||
```{sidebar} Errors in the logs
|
||||
|
||||
In regular use, tracebacks will often appear in the log rather than
|
||||
in the game. Use `evennia --log` to view the log in the terminal. Make
|
||||
sure to scroll back if you expect an error and don't see it. Use
|
||||
`Ctrl-C` (or `Cmd-C` on Mac) to exit the log-view.
|
||||
|
||||
```
|
||||
|
||||
This is called a *traceback*. Python's errors are very friendly and will most of the time tell you
|
||||
exactly what and where things go wrong. It's important that you learn to parse tracebacks so you
|
||||
know how to fix your code.
|
||||
|
||||
A traceback is to be read from the _bottom up_:
|
||||
|
||||
- (line 3) An error of type `NameError` is the problem ...
|
||||
- (line 3) ... more specifically it is due to the variable `me` not being defined.
|
||||
- (line 2) This happened on the line `me.msg("Hello world!")` ...
|
||||
- (line 1) ... which is on line `2` of the file `./world/test.py`.
|
||||
|
||||
In our case the traceback is short. There may be many more lines above it, tracking just how
|
||||
different modules called each other until the program got to the faulty line. That can
|
||||
sometimes be useful information, but reading from the bottom is always a good start.
|
||||
|
||||
The `NameError` we see here is due to a module being its own isolated thing. It knows nothing about
|
||||
the environment into which it is imported. It knew what `print` is because that is a special
|
||||
[reserved Python keyword](https://docs.python.org/2.5/ref/keywords.html). But `me` is *not* such a
|
||||
reserved word (as mentioned, it's just something Evennia came up with for convenience in the `py`
|
||||
command). As far as the module is concerned `me` is an unfamiliar name, appearing out of nowhere.
|
||||
Hence the `NameError`.
|
||||
|
||||
## Passing arguments to functions
|
||||
|
||||
We know that `me` exists at the point when we run the `py` command, because we can do `py me.msg("Hello World!")`
|
||||
with no problem. So let's _pass_ that me along to the function so it knows what it should be.
|
||||
Go back to your `test.py` and change it to this:
|
||||
|
||||
```python
|
||||
def hello_world(who):
|
||||
who.msg("Hello World!")
|
||||
```
|
||||
We now added an _argument_ to the function. We could have named it anything. Whatever `who` is,
|
||||
we will call a method `.msg()` on it.
|
||||
|
||||
As usual, `reload` the server to make sure the new code is available.
|
||||
|
||||
> py import world.test ; world.test.hello_world(me)
|
||||
Hello World!
|
||||
|
||||
Now it worked. We _passed_ `me` to our function. It will appear inside the function renamed as `who` and
|
||||
now the function works and prints as expected. Note how the `hello_world` function doesn't care _what_ you
|
||||
pass into it as long as it has a `.msg()` method on it. So you could reuse this function over and over for other
|
||||
suitable targets.
|
||||
|
||||
> **Extra Credit:** As an exercise, try to pass something else into `hello_world`. Try for example
|
||||
>to pass the number `5` or the string `"foo"`. You'll get errors telling you that they don't have
|
||||
>the attribute `msg`. They don't care about `me` itself not being a string or a number. If you are
|
||||
>familiar with other programming languages (especially C/Java) you may be tempted to start *validating*
|
||||
>`who` to make sure it's of the right type before you send it. This is usually not recommended in Python.
|
||||
>Python philosophy is to [handle](https://docs.python.org/2/tutorial/errors.html) the error if it happens
|
||||
>rather than to add a lot of code to prevent it from happening. See [duck typing](https://en.wikipedia.org/wiki/Duck_typing)
|
||||
>and the concept of _Leap before you Look_.
|
||||
|
||||
|
||||
## Finding others to send to
|
||||
|
||||
Let's wrap up this first Python `py` crash-course by finding someone else to send to.
|
||||
|
||||
In Evennia's `contrib/` folder (`evennia/contrib/tutorial_examples/mirror.py`) is a handy little
|
||||
object called the `TutorialMirror`. The mirror will echo whatever is being sent to it to
|
||||
the room it is in.
|
||||
|
||||
On the game command-line, let's create a mirror:
|
||||
|
||||
> create/drop mirror:contrib.tutorial_examples.mirror.TutorialMirror
|
||||
|
||||
```{sidebar} Creating objects
|
||||
|
||||
The `create` command was first used to create boxes in the
|
||||
`Building Stuff <Building-Quickstart>`_ tutorial. Note how it
|
||||
uses a "python-path" to describe where to load the mirror's code from.
|
||||
```
|
||||
|
||||
A mirror should appear in your location.
|
||||
|
||||
> look mirror
|
||||
mirror shows your reflection:
|
||||
This is User #1
|
||||
|
||||
What you are seeing is actually your own avatar in the game, the same thing that is available as `me` in the `py`
|
||||
command.
|
||||
|
||||
What we are aiming for now is the equivalent of `mirror.msg("Mirror Mirror on the wall")`. But the first thing that
|
||||
comes to mind will not work:
|
||||
|
||||
> py mirror.msg("Mirror, Mirror on the wall ...")
|
||||
NameError: name 'mirror' is not defined.
|
||||
|
||||
This is not surprising: Python knows nothing about "mirrors" or locations or anything. The `me` we've been using
|
||||
is, as mentioned, just a convenient thing the Evennia devs makes available to the `py` command. They couldn't possibly
|
||||
predict that you wanted to talk to mirrors.
|
||||
|
||||
Instead we will need to _search_ for that `mirror` object before we can send to it.
|
||||
Make sure you are in the same location as the mirror and try:
|
||||
|
||||
> py me.search("mirror")
|
||||
mirror
|
||||
|
||||
`me.search("name")` will, by default, search and _return_ an object with the given name found in _the same location_
|
||||
as the `me` object is. If it can't find anything you'll see an error.
|
||||
|
||||
```{sidebar} Function returns
|
||||
|
||||
Whereas a function like `print` only prints its arguments, it's very common
|
||||
for functions/methods to `return` a result of some kind. Think of the function
|
||||
as a machine - you put something in and out comes a result you can use. In the case
|
||||
of `me.search`, it will perform a database search and spit out the object it finds.
|
||||
```
|
||||
|
||||
> py me.search("dummy")
|
||||
Could not find 'dummy'.
|
||||
|
||||
Wanting to find things in the same location is very common, but as we continue we'll
|
||||
find that Evennia provides ample tools for tagging, searching and finding things from all over your game.
|
||||
|
||||
Now that we know how to find the 'mirror' object, we just need to use that instead of `me`!
|
||||
|
||||
> py mirror = self.search("mirror") ; mirror.msg("Mirror, Mirror on the wall ...")
|
||||
mirror echoes back to you:
|
||||
"Mirror, Mirror on the wall ..."
|
||||
|
||||
The mirror is useful for testing because its `.msg` method just echoes whatever is sent to it back to the room. More common
|
||||
would be to talk to a player character, in which case the text you sent would have appeared in their game client.
|
||||
|
||||
|
||||
## Multi-line py
|
||||
|
||||
So far we have use `py` in single-line mode, using `;` to separate multiple inputs. This is very convenient
|
||||
when you want to do some quick testing. But you can also start a full multi-line Python interactive interpreter
|
||||
inside Evennia.
|
||||
|
||||
> py
|
||||
Evennia Interactive Python mode
|
||||
Python 3.7.1 (default, Oct 22 2018, 11:21:55)
|
||||
[GCC 8.2.0] on Linux
|
||||
[py mode - quit() to exit]
|
||||
|
||||
(the details of the output will vary with your Python version and OS). You are now in python interpreter mode. It means
|
||||
that _everything_ you insert from now on will become a line of Python (you can no longer look around or do other
|
||||
commands).
|
||||
|
||||
> print("Hello World")
|
||||
|
||||
>>> print("Hello World")
|
||||
Hello World
|
||||
[py mode - quit() to exit]
|
||||
|
||||
Note that we didn't need to put `py` in front now. The system will also echo your input (that's the bit after
|
||||
the `>>>`). For brevity in this tutorual we'll turn the echo off. First exit `py` and then start again with the
|
||||
`/noecho` flag.
|
||||
|
||||
> quit()
|
||||
Closing the Python console.
|
||||
> py/noecho
|
||||
Evennia Interactive Python mode (no echoing of prompts)
|
||||
Python 3.7.1 (default, Oct 22 2018, 11:21:55)
|
||||
[GCC 8.2.0] on Linux
|
||||
[py mode - quit() to exit]
|
||||
|
||||
```{sidebar} interactive py
|
||||
|
||||
- Start with `py`.
|
||||
- Use `py/noecho` if you don't want your input to be echoed for every line.
|
||||
- All your inputs will now be interpreted as Python code.
|
||||
- Exit with `quit()`.
|
||||
```
|
||||
|
||||
We can now enter multi-line Python code:
|
||||
|
||||
> a = "Test"
|
||||
> print(f"This is a {a}."}
|
||||
This is a Test.
|
||||
|
||||
Let's try to define a function:
|
||||
|
||||
> def hello_world(who, txt):
|
||||
...
|
||||
> who.msg(txt)
|
||||
...
|
||||
>
|
||||
[py mode - quit() to exit]
|
||||
|
||||
Some important things above:
|
||||
|
||||
- Definining a function with `def` means we are starting a new code block. Python works so that you mark the content
|
||||
of the block with indention. So the next line must be manually indented (4 spaces is a good standard) in order
|
||||
for Python to know it's part of the function body.
|
||||
- We expand the `hello_world` function with another argument `txt`. This allows us to send any text, not just
|
||||
"Hello World" over and over.
|
||||
- To tell `py` that no more lines will be added to the function body, we end with an empty input. When
|
||||
the normal prompt on how to exit returns, we know we are done.
|
||||
|
||||
Now we have defined a new function. Let's try it out:
|
||||
|
||||
> hello_world(me, "Hello world to me!")
|
||||
Hello world to me!
|
||||
|
||||
The `me` is still available to us, so we pass that as the `who` argument, along with a little longer
|
||||
string. Let's combine this with searching for the mirror.
|
||||
|
||||
> mirror = me.search("mirror")
|
||||
> hello_world(mirror, "Mirror, Mirror on the wall ...")
|
||||
mirror echoes back to you:
|
||||
"Mirror, Mirror on the wall ..."
|
||||
|
||||
Exit the `py` mode with
|
||||
|
||||
> quit()
|
||||
Closing the Python console.
|
||||
|
||||
## Other ways to test Python code
|
||||
|
||||
The `py` command is very powerful for experimenting with Python in-game. It's great for quick testing.
|
||||
But you are still limited to working over telnet or the webclient, interfaces that doesn't know anything
|
||||
about Python per-se.
|
||||
|
||||
Outside the game, go to the terminal where you ran Evennia (or any terminal where the `evennia` command
|
||||
is available).
|
||||
|
||||
- `cd` to your game dir.
|
||||
- `evennia shell`
|
||||
|
||||
A Python shell opens. This works like `py` did inside the game, with the exception that you don't have
|
||||
`me` available out of the box. If you want `me`, you need to first find yourself:
|
||||
|
||||
> import evennia
|
||||
> me = evennia.search_object("YourChar")[0]
|
||||
|
||||
Here we make use of one of evennia's search functions, available by importing `evennia` directly.
|
||||
We will cover more advanced searching later, but suffice to say, you put your own character name instead of
|
||||
"YourChar" above.
|
||||
|
||||
> The `[0]` at the end is because `.search_object` returns a list of objects and we want to
|
||||
get at the first of them (counting starts from 0).
|
||||
|
||||
Use `Ctrl-D` (`Cmd-D` on Mac) or `quit()` to exit the Python console.
|
||||
|
||||
## ipython
|
||||
|
||||
The default Python shell is quite limited and ugly. It's *highly* recommended to install `ipython` instead. This
|
||||
is a much nicer, third-party Python interpreter with colors and many usability improvements.
|
||||
|
||||
pip install ipython
|
||||
|
||||
If `ipython` is installed, `evennia shell` will use it automatically.
|
||||
|
||||
evennia shell
|
||||
...
|
||||
IPython 7.4.0 -- An enhanced Interactive Python. Type '?' for help
|
||||
In [1]: You now have Tab-completion:
|
||||
|
||||
> import evennia
|
||||
> evennia.<TAB>
|
||||
|
||||
That is, enter `evennia.` and then press the TAB key - you will be given a list of all the resources
|
||||
available on the `evennia` object. This is great for exploring what Evennia has to offer. For example,
|
||||
use your arrow keys to scroll to `search_object()` to fill it in.
|
||||
|
||||
> evennia.search_object?
|
||||
|
||||
Adding a `?` and pressing return will give you the full documentation for `.search_object`. Use `??` if you
|
||||
want to see the entire source code.
|
||||
|
||||
As for the normal python interpreter, use `Ctrl-D`/`Cmd-D` or `quit()` to exit ipython.
|
||||
|
||||
```{important} Persistent code
|
||||
|
||||
Common for both `py` and `python`/`ipython` is that the code you write is not persistent - it will
|
||||
be gone after you shut down the interpreter (but ipython will remember your input history). For making long-lasting
|
||||
Python code, we need to save it in a Python module, like we did for `world/test.py`.
|
||||
```
|
||||
|
||||
|
||||
## Conclusions
|
||||
|
||||
This covers quite a lot of basic Python usage. We printed and formatted strings, defined our own
|
||||
first function, fixed an error and even searched and talked to a mirror! Being able to access
|
||||
python inside and outside of the game is an important skill for testing and debugging, but in
|
||||
practice you will be writing most your code in Python modules.
|
||||
|
||||
To that end we also created a first new Python module in the `mygame/` game dir, then imported and used it.
|
||||
Now let's look at the rest of the stuff you've got going on inside that `mygame/` folder ...
|
||||
|
|
@ -0,0 +1,415 @@
|
|||
# Introduction to Python classes and objects
|
||||
|
||||
We have now learned how to run some simple Python code from inside (and outside) your game server.
|
||||
We have also taken a look at what our game dir looks and what is where. Now we'll start to use it.
|
||||
|
||||
## Importing things
|
||||
|
||||
No one writes something as big as an online game in one single huge file. Instead one breaks up the
|
||||
code into separate files (modules). Each module is dedicated to different purposes. Not only does
|
||||
it make things cleaner, organized and easier to understand. It also makes it easier to re-use code -
|
||||
you just import the resources you need and know you only get just what you requested. This makes
|
||||
it much easier to find errors and to know what code is good and which has issues.
|
||||
|
||||
> Evennia itself uses your code in the same way - you just tell it where a particular type of code is,
|
||||
and it will import and use it (often instead of its defaults).
|
||||
|
||||
We have already successfully imported things, for example:
|
||||
|
||||
> py import world.test ; world.test.hello_world(me)
|
||||
Hello World!
|
||||
|
||||
In this example, on your hard drive, the files looks like this:
|
||||
|
||||
```
|
||||
mygame/
|
||||
world/
|
||||
test.py <- inside this file is a function hello_world
|
||||
|
||||
```
|
||||
If you followed earlier tutorial lessons, the `mygame/world/test.py` file should look like this (if
|
||||
not, make it so):
|
||||
|
||||
```python
|
||||
def hello_world(who):
|
||||
who.msg("Hello World!")
|
||||
```
|
||||
|
||||
```{eval-rst}
|
||||
|
||||
.. sidebar:: Remember:
|
||||
|
||||
- Indentation matters in Python
|
||||
- So does capitalization
|
||||
- Use 4 `spaces` to indent, not tabs
|
||||
- Empty lines are fine
|
||||
- Anything on a line after a `#` is a `comment`, ignored by Python
|
||||
```
|
||||
|
||||
The _python_path_ describes the relation between Python resources, both between and inside
|
||||
Python _modules_ (that is, files ending with .py). A python-path separates each part of the
|
||||
path `.` and always skips the `.py` file endings. Also, Evennia already knows to start looking
|
||||
for python resources inside `mygame/` so this should never be specified. Hence
|
||||
|
||||
import world.test
|
||||
|
||||
The `import` Python instruction loads `world.test` so you have it available. You can now go "into"
|
||||
this module to get to the function you want:
|
||||
|
||||
world.test.hello_world(me)
|
||||
|
||||
Using `import` like this means that you have to specify the full `world.test` every time you want
|
||||
to get to your function. Here's a more powerful form of import:
|
||||
|
||||
from world.test import hello_world
|
||||
|
||||
The `from ... import ...` is very, very common as long as you want to get something with a longer
|
||||
python path. It imports `hello_world` directly, so you can use it right away!
|
||||
|
||||
> py from world.test import hello_world ; hello_world(me)
|
||||
Hello World!
|
||||
|
||||
Let's say your `test.py` module had a bunch of interesting functions. You could then import them
|
||||
all one by one:
|
||||
|
||||
from world.test import hello_world, my_func, awesome_func
|
||||
|
||||
If there were _a lot_ of functions, you could instead just import `test` and get the function
|
||||
from there when you need (without having to give the full `world.test` every time):
|
||||
|
||||
> from world import test ; test.hello_world(me
|
||||
Hello World!
|
||||
|
||||
You can also _rename_ stuff you import. Say for example that the module you import to already
|
||||
has a function `hello_world` but we also want to use the one from `world/test.py`:
|
||||
|
||||
from world.test import hello_world as test_hello_world
|
||||
|
||||
The form `from ... import ... as ...` renames the import.
|
||||
|
||||
> from world.test import hello_world as hw ; hw(me)
|
||||
Hello World!
|
||||
|
||||
> Avoid renaming unless it's to avoid a name-collistion like above - you want to make things as
|
||||
> easy to read as possible, and renaming adds another layer of potential confusion.
|
||||
|
||||
In [the basic intro to Python](./Python-basic-introduction.md) we learned how to open the in-game
|
||||
multi-line interpreter.
|
||||
|
||||
> py
|
||||
Evennia Interactive Python mode
|
||||
Python 3.7.1 (default, Oct 22 2018, 11:21:55)
|
||||
[GCC 8.2.0] on Linux
|
||||
[py mode - quit() to exit]
|
||||
|
||||
You now only need to import once to use the imported function over and over.
|
||||
|
||||
> from world.test import hello_world
|
||||
> hello_world()
|
||||
Hello World!
|
||||
> hello_world()
|
||||
Hello World!
|
||||
> hello_world()
|
||||
Hello World!
|
||||
> quit()
|
||||
Closing the Python console.
|
||||
|
||||
The same goes when writing code in a module - in most Python modules you will see a bunch of
|
||||
imports at the top, resources that are then used by all code in that module.
|
||||
|
||||
## On classes and objects
|
||||
|
||||
Now that we know about imports, let look at a real Evennia module and try to understand it.
|
||||
|
||||
Open `mygame/typeclasses/objects.py` in your text editor of choice.
|
||||
|
||||
```python
|
||||
"""
|
||||
module docstring
|
||||
"""
|
||||
from evennia import DefaultObject
|
||||
|
||||
class Object(DefaultObject):
|
||||
"""
|
||||
class docstring
|
||||
"""
|
||||
pass
|
||||
```
|
||||
|
||||
```{sidebar} Docstrings vs Comments
|
||||
|
||||
A docstring is not the same as a comment (created by `#`). A
|
||||
docstring is not ignored by Python but is an integral part of the thing
|
||||
it is documenting (the module and the class in this case).
|
||||
```
|
||||
The real file is much longer but we can ignore the multi-line strings (`""" ... """`). These serve
|
||||
as documentation-strings, or _docstrings_ for the module (at the top) and the `class` below.
|
||||
|
||||
Below the module doc string we have the import. In this case we are importing a resource
|
||||
from the core `evennia` library itself. We will dive into this later, for now we just treat this
|
||||
as a black box.
|
||||
|
||||
Next we have a `class` named `Object`, which _inherits_ from `DefaultObject`. This class doesn't
|
||||
actually do anything on its own, its only code (except the docstring) is `pass` which means,
|
||||
well, to pass and don't do anything.
|
||||
|
||||
We will get back to this module in the [next lesson](./Learning-Typeclasses.md). First we need to do a
|
||||
little detour to understand what a 'class', an 'object' or 'instance' is. These are fundamental
|
||||
things to understand before you can use Evennia efficiently.
|
||||
```{sidebar} OOP
|
||||
|
||||
Classes, objects, instances and inheritance are fundamental to Python. This and some
|
||||
other concepts are often clumped together under the term Object-Oriented-Programming (OOP).
|
||||
```
|
||||
|
||||
### Classes and instances
|
||||
|
||||
A 'class' can be seen as a 'template' for a 'type' of object. The class describes the basic functionality
|
||||
of everyone of that class. For example, we could have a class `Monster` which has resources for moving itself
|
||||
from room to room.
|
||||
|
||||
Open a new file `mygame/typeclasses/monsters.py`. Add the following simple class:
|
||||
|
||||
```python
|
||||
|
||||
class Monster:
|
||||
|
||||
key = "Monster"
|
||||
|
||||
def move_around(self):
|
||||
print(f"{self.key} is moving!")
|
||||
|
||||
```
|
||||
|
||||
Above we have defined a `Monster` class with one variable `key` (that is, the name) and one
|
||||
_method_ on it. A method is like a function except it sits "on" the class. It also always has
|
||||
at least one argument (almost always written as `self` although you could in principle use
|
||||
another name), which is a reference back to itself. So when we print `self.key` we are referring
|
||||
back to the `key` on the class.
|
||||
|
||||
```{eval-rst}
|
||||
.. sidebar:: Terms
|
||||
|
||||
- A `class` is a code template describing a 'type' of something
|
||||
- An `object` is an `instance` of a `class`. Like using a mold to cast tin soldiers, one class can be `instantiated` into any number of object-instances.
|
||||
|
||||
```
|
||||
A class is just a template. Before it can be used, we must create an _instance_ of the class. If
|
||||
`Monster` is a class, then an instance is Fluffy, the individual red dragon. You instantiate
|
||||
by _calling_ the class, much like you would a function:
|
||||
|
||||
fluffy = Monster()
|
||||
|
||||
Let's try it in-game (we use multi-line mode, it's easier)
|
||||
|
||||
> py
|
||||
> from typeclasses.monsters import Monster
|
||||
> fluffy = Monster()
|
||||
> fluffy.move_around()
|
||||
Monster is moving!
|
||||
|
||||
We created an _instance_ of `Monster`, which we stored in the variable `fluffy`. We then
|
||||
called the `move_around` method on fluffy to get the printout.
|
||||
|
||||
> Note how we _didn't_ call the method as `fluffy.move_around(self)`. While the `self` has to be
|
||||
> there when defining the method, we _never_ add it explicitly when we call the method (Python
|
||||
> will add the correct `self` for us automatically behind the scenes).
|
||||
|
||||
Let's create the sibling of Fluffy, Cuddly:
|
||||
|
||||
> cuddly = Monster()
|
||||
> cuddly.move_around()
|
||||
Monster is moving!
|
||||
|
||||
We now have two dragons and they'll hang around until with call `quit()` to exit this Python
|
||||
instance. We can have them move as many times as we want. But no matter how many dragons we
|
||||
create, they will all show the same printout since `key` is always fixed as "Monster".
|
||||
|
||||
Let's make the class a little more flexible:
|
||||
|
||||
```python
|
||||
|
||||
class Monster:
|
||||
|
||||
def __init__(self, key):
|
||||
self.key = key
|
||||
|
||||
def move_around(self):
|
||||
print(f"{self.key} is moving!")
|
||||
|
||||
```
|
||||
|
||||
The `__init__` is a special method that Python recognizes. If given, this handles extra arguments
|
||||
when you instantiate a new Monster. We have it add an argument `key` that we store on `self`.
|
||||
|
||||
Now, for Evennia to see this code change, we need to reload the server. You can either do it this
|
||||
way:
|
||||
|
||||
> quit()
|
||||
Python Console is closing.
|
||||
> reload
|
||||
|
||||
Or you can use a separate terminal and restart from outside the game:
|
||||
```{sidebar} On reloading
|
||||
|
||||
Reloading with the python mode gets a little annoying since you need to redo everything
|
||||
after every reload. Just keep in mind that during regular development you will not be
|
||||
working this way. The in-game python mode is practical for quick fixes and experiments like
|
||||
this, but actual code is normally written externally, in python modules.
|
||||
```
|
||||
|
||||
$ evennia reload (or restart)
|
||||
|
||||
Either way you'll need to go into `py` again:
|
||||
|
||||
> py
|
||||
> from typeclasses.monsters import Monster
|
||||
fluffy = Monster("Fluffy")
|
||||
fluffy.move_around()
|
||||
Fluffy is moving!
|
||||
|
||||
Now we passed `"Fluffy"` as an argument to the class. This went into `__init__` and set `self.key`, which we
|
||||
later used to print with the right name! Again, note that we didn't include `self` when calling.
|
||||
|
||||
### What's so good about objects?
|
||||
|
||||
So far all we've seen a class do is to behave our first `hello_world` function but more complex. We
|
||||
could just have made a function:
|
||||
|
||||
```python
|
||||
def monster_move_around(key):
|
||||
print(f"{key} is moving!")
|
||||
```
|
||||
|
||||
The difference between the function and an instance of a class (the object), is that the
|
||||
object retains _state_. Once you called the function it forgets everything about what you called
|
||||
it with last time. The object, on the other hand, remembers changes:
|
||||
|
||||
> fluffy.key = "Cuddly"
|
||||
> fluffy.move_around()
|
||||
Cuddly is moving!
|
||||
|
||||
The `fluffy` object's `key` was changed to "Cuddly" for as long as it's around. This makes objects
|
||||
extremely useful for representing and remembering collections of data - some of which can be other
|
||||
objects in turn:
|
||||
|
||||
- A player character with all its stats
|
||||
- A monster with HP
|
||||
- A chest with a number of gold coins in it
|
||||
- A room with other objects inside it
|
||||
- The current policy positions of a political party
|
||||
- A rule with methods for resolving challenges or roll dice
|
||||
- A multi-dimenstional data-point for a complex economic simulation
|
||||
- And so much more!
|
||||
|
||||
### Classes can have children
|
||||
|
||||
Classes can _inherit_ from each other. A "child" class will inherit everything from its "parent" class. But if
|
||||
the child adds something with the same name as its parent, it will _override_ whatever it got from its parent.
|
||||
|
||||
Let's expand `mygame/typeclasses/monsters.py` with another class:
|
||||
|
||||
```python
|
||||
|
||||
class Monster:
|
||||
"""
|
||||
This is a base class for Monster.
|
||||
"""
|
||||
|
||||
def __init__(self, key):
|
||||
self.key = key
|
||||
|
||||
def move_around(self):
|
||||
print(f"{self.key} is moving!")
|
||||
|
||||
|
||||
class Dragon(Monster):
|
||||
"""
|
||||
This is a dragon-specific monster.
|
||||
"""
|
||||
|
||||
def move_around(self):
|
||||
print(f"{self.key} flies through the air high above!")
|
||||
|
||||
def firebreath(self):
|
||||
"""
|
||||
Let our dragon breathe fire.
|
||||
"""
|
||||
print(f"{self.key} breathes fire!")
|
||||
|
||||
```
|
||||
|
||||
We added some docstrings for clarity. It's always a good idea to add doc strings; you can do so also for methods,
|
||||
as exemplified for the new `firebreath` method.
|
||||
|
||||
We created the new class `Dragon` but we also specified that `Monster` is the _parent_ of `Dragon` but adding
|
||||
the parent in parenthesis. `class Classname(Parent)` is the way to do this.
|
||||
|
||||
```{sidebar} Multi-inheritance
|
||||
|
||||
It's possible to add more comma-separated parents to a class. You should usually avoid
|
||||
this until you `really` know what you are doing. A single parent will be enough for almost
|
||||
every case you'll need.
|
||||
|
||||
```
|
||||
|
||||
Let's try out our new class. First `reload` the server and the do
|
||||
|
||||
> py
|
||||
> from typeclasses.monsters import Dragon
|
||||
> smaug = Dragon("Smaug")
|
||||
> smaug.move_around()
|
||||
Smaug flies through the air high above!
|
||||
> smaug.firebreath()
|
||||
Smaug breathes fire!
|
||||
|
||||
Because we didn't implement `__init__` in `Dragon`, we got the one from `Monster` instead. But since we
|
||||
implemented our own `move_around` in `Dragon`, it _overrides_ the one in `Monster`. And `firebreath` is only
|
||||
available for `Dragon`s of course. Having that on `Monster` would not have made much sense, since not every monster
|
||||
can breathe fire.
|
||||
|
||||
One can also force a class to use resources from the parent even if you are overriding some of it. This is done
|
||||
with the `super()` method. Modify your `Dragon` class as follows:
|
||||
|
||||
|
||||
```python
|
||||
# ...
|
||||
|
||||
class Dragon(Monster):
|
||||
|
||||
def move_around(self):
|
||||
super().move_around()
|
||||
print("The world trembles.")
|
||||
|
||||
# ...
|
||||
```
|
||||
> Keep `Monster` and the `firebreath` method, `# ...` indicates the rest of the code is untouched.
|
||||
>
|
||||
|
||||
The `super().move_around()` line means that we are calling `move_around()` on the parent of the class. So in this
|
||||
case, we will call `Monster.move_around` first, before doing our own thing.
|
||||
|
||||
Now `reload` the server and then:
|
||||
|
||||
> py
|
||||
> from typeclasses.monsters import Dragon
|
||||
> smaug = Dragon("Smaug")
|
||||
> smaug.move_around()
|
||||
Smaug is moving!
|
||||
The world trembles.
|
||||
|
||||
We can see that `Monster.move_around()` is calls first and prints "Smaug is moving!", followed by the extra bit
|
||||
about the trembling world we added in the `Dragon` class.
|
||||
|
||||
Inheritance is very powerful because it allows you to organize and re-use code while only adding the special things
|
||||
you want to change. Evennia uses this concept a lot.
|
||||
|
||||
## Summary
|
||||
|
||||
We have created our first dragons from classes. We have learned a little about how you _instantiate_ a class
|
||||
into an _object_. We have seen some examples of _inheritance_ and we tested to _override_ a method in the parent
|
||||
with one in the child class. We also used `super()` to good effect.
|
||||
|
||||
We have used pretty much raw Python so far. In the coming lessons we'll start to look at the extra bits that Evennia
|
||||
provides. But first we need to learn just where to find everything.
|
||||
|
||||
261
docs/source/Howtos/Beginner-Tutorial/Part1/Searching-Things.md
Normal file
261
docs/source/Howtos/Beginner-Tutorial/Part1/Searching-Things.md
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
# Searching for things
|
||||
|
||||
We have gone through how to create the various entities in Evennia. But creating something is of little use
|
||||
if we cannot find and use it afterwards.
|
||||
|
||||
## Main search functions
|
||||
|
||||
The base tools are the `evennia.search_*` functions, such as `evennia.search_object`.
|
||||
|
||||
rose = evennia.search_object(key="rose")
|
||||
acct = evennia.search_account(key="MyAccountName", email="foo@bar.com")
|
||||
|
||||
```{sidebar} Querysets
|
||||
|
||||
What is returned from the main search functions is actually a `queryset`. They can be
|
||||
treated like lists except that they can't modified in-place. We'll discuss querysets in
|
||||
the `next lesson` <Django-queries>`_.
|
||||
```
|
||||
|
||||
Strings are always case-insensitive, so searching for `"rose"`, `"Rose"` or `"rOsE"` give the same results.
|
||||
It's important to remember that what is returned from these search methods is a _listing_ of 0, one or more
|
||||
elements - all the matches to your search. To get the first match:
|
||||
|
||||
rose = rose[0]
|
||||
|
||||
Often you really want all matches to the search parameters you specify. In other situations, having zero or
|
||||
more than one match is a sign of a problem and you need to handle this case yourself.
|
||||
|
||||
the_one_ring = evennia.search_object(key="The one Ring")
|
||||
if not the_one_ring:
|
||||
# handle not finding the ring at all
|
||||
elif len(the_one_ring) > 1:
|
||||
# handle finding more than one ring
|
||||
else:
|
||||
# ok - exactly one ring found
|
||||
the_one_ring = the_one_ring[0]
|
||||
|
||||
There are equivalent search functions for all the main resources. You can find a listing of them
|
||||
[in the Search functions section](../../../Evennia-API.md) of the API frontpage.
|
||||
|
||||
## Searching using Object.search
|
||||
|
||||
On the `DefaultObject` is a `.search` method which we have already tried out when we made Commands. For
|
||||
this to be used you must already have an object available:
|
||||
|
||||
rose = obj.search("rose")
|
||||
|
||||
The `.search` method wraps `evennia.search_object` and handles its output in various ways.
|
||||
|
||||
- By default it will always search for objects among those in `obj.location.contents` and `obj.contents` (that is,
|
||||
things in obj's inventory or in the same room).
|
||||
- It will always return exactly one match. If it found zero or more than one match, the return is `None`.
|
||||
- On a no-match or multimatch, `.search` will automatically send an error message to `obj`.
|
||||
|
||||
So this method handles error messaging for you. A very common way to use it is in commands:
|
||||
|
||||
```python
|
||||
from evennia import Command
|
||||
|
||||
class MyCommand(Command):
|
||||
|
||||
key = "findfoo"
|
||||
|
||||
def func(self):
|
||||
|
||||
foo = self.caller.search("foo")
|
||||
if not foo:
|
||||
return
|
||||
```
|
||||
|
||||
Remember, `self.caller` is the one calling the command. This is usually a Character, which
|
||||
inherits from `DefaultObject`! This (rather stupid) Command searches for an object named "foo" in
|
||||
the same location. If it can't find it, `foo` will be `None`. The error has already been reported
|
||||
to `self.caller` so we just abort with `return`.
|
||||
|
||||
You can use `.search` to find anything, not just stuff in the same room:
|
||||
|
||||
volcano = self.caller.search("Volcano", global=True)
|
||||
|
||||
If you only want to search for a specific list of things, you can do so too:
|
||||
|
||||
stone = self.caller.search("MyStone", candidates=[obj1, obj2, obj3, obj4])
|
||||
|
||||
This will only return a match if MyStone is one of the four provided candidate objects. This is quite powerful,
|
||||
here's how you'd find something only in your inventory:
|
||||
|
||||
potion = self.caller.search("Healing potion", candidates=self.caller.contents)
|
||||
|
||||
You can also turn off the automatic error handling:
|
||||
|
||||
swords = self.caller.search("Sword", quiet=True)
|
||||
|
||||
With `quiet=True` the user will not be notified on zero or multi-match errors. Instead you are expected to handle this
|
||||
yourself and what you get back is now a list of zero, one or more matches!
|
||||
|
||||
## What can be searched for
|
||||
|
||||
These are the main database entities one can search for:
|
||||
|
||||
- [Objects](../../../Components/Objects.md)
|
||||
- [Accounts](../../../Components/Accounts.md)
|
||||
- [Scripts](../../../Components/Scripts.md),
|
||||
- [Channels](../../../Components/Channels.md),
|
||||
- [Messages](../../../Components/Msg.md)
|
||||
- [Help Entries](../../../Components/Help-System.md).
|
||||
|
||||
Most of the time you'll likely spend your time searching for Objects and the occasional Accounts.
|
||||
|
||||
So to find an entity, what can be searched for?
|
||||
|
||||
### Search by key
|
||||
|
||||
The `key` is the name of the entity. Searching for this is always case-insensitive.
|
||||
|
||||
### Search by aliases
|
||||
|
||||
Objects and Accounts can have any number of aliases. When searching for `key` these will searched too,
|
||||
you can't easily search only for aliases.
|
||||
|
||||
rose.aliases.add("flower")
|
||||
|
||||
If the above `rose` has a `key` `"Rose"`, it can now also be found by searching for `flower`. In-game
|
||||
you can assign new aliases to things with the `alias` command.
|
||||
|
||||
### Search by location
|
||||
|
||||
Only Objects (things inheriting from `evennia.DefaultObject`) has a location. This is usually a room.
|
||||
The `Object.search` method will automatically limit it search by location, but it also works for the
|
||||
general search function. If we assume `room` is a particular Room instance,
|
||||
|
||||
chest = evennia.search_object("Treasure chest", location=room)
|
||||
|
||||
### Search by Tags
|
||||
|
||||
Think of a [Tag](../../../Components/Tags.md) as the label the airport puts on your luggage when flying.
|
||||
Everyone going on the same plane gets a tag grouping them together so the airport can know what should
|
||||
go to which plane. Entities in Evennia can be grouped in the same way. Any number of tags can be attached
|
||||
to each object.
|
||||
|
||||
rose.tags.add("flowers")
|
||||
daffodil.tags.add("flowers")
|
||||
tulip.tags.add("flowers")
|
||||
|
||||
You can now find all flowers using the `search_tag` function:
|
||||
|
||||
all_flowers = evennia.search_tag("flowers")
|
||||
|
||||
Tags can also have categories. By default this category is `None` which is also considered a category.
|
||||
|
||||
silmarillion.tags.add("fantasy", category="books")
|
||||
ice_and_fire.tags.add("fantasy", category="books")
|
||||
mona_lisa_overdrive.tags.add("cyberpunk", category="books")
|
||||
|
||||
Note that if you specify the tag you _must_ also include its category, otherwise that category
|
||||
will be `None` and find no matches.
|
||||
|
||||
all_fantasy_books = evennia.search_tag("fantasy") # no matches!
|
||||
all_fantasy_books = evennia.search_tag("fantasy", category="books")
|
||||
|
||||
Only the second line above returns the two fantasy books. If we specify a category however,
|
||||
we can get all tagged entities within that category:
|
||||
|
||||
all_books = evennia.search_tag(category="books")
|
||||
|
||||
This gets all three books.
|
||||
|
||||
### Search by Attribute
|
||||
|
||||
We can also search by the [Attributes](../../../Components/Attributes.md) associated with entities.
|
||||
|
||||
For example, let's give our rose thorns:
|
||||
|
||||
rose.db.has_thorns = True
|
||||
wines.db.has_thorns = True
|
||||
daffodil.db.has_thorns = False
|
||||
|
||||
Now we can find things attribute and the value we want it to have:
|
||||
|
||||
is_ouch = evennia.search_object_attribute("has_thorns", True)
|
||||
|
||||
This returns the rose and the wines.
|
||||
|
||||
> Searching by Attribute can be very practical. But if you plan to do a search very often, searching
|
||||
> by-tag is generally faster.
|
||||
|
||||
|
||||
### Search by Typeclass
|
||||
|
||||
Sometimes it's useful to find all objects of a specific Typeclass. All of Evennia's search tools support this.
|
||||
|
||||
all_roses = evennia.search_object(typeclass="typeclasses.flowers.Rose")
|
||||
|
||||
If you have the `Rose` class already imported you can also pass it directly:
|
||||
|
||||
all_roses = evennia.search_object(typeclass=Rose)
|
||||
|
||||
You can also search using the typeclass itself:
|
||||
|
||||
all_roses = Rose.objects.all()
|
||||
|
||||
This last way of searching is a simple form of a Django _query_. This is a way to express SQL queries using
|
||||
Python.
|
||||
|
||||
### Search by dbref
|
||||
|
||||
The database id or `#dbref` is unique and never-reused within each database table. In search methods you can
|
||||
replace the search for `key` with the dbref to search for. This must be written as a string `#dbref`:
|
||||
|
||||
the_answer = self.caller.search("#42")
|
||||
eightball = evennia.search_object("#8")
|
||||
|
||||
Since `#dbref` is always unique, this search is always global.
|
||||
|
||||
```{warning} Relying on #dbrefs
|
||||
|
||||
You may be used to using #dbrefs a lot from other codebases. It is however considered
|
||||
`bad practice` in Evennia to rely on hard-coded #dbrefs. It makes your code hard to maintain
|
||||
and tied to the exact layout of the database. In 99% of cases you should pass the actual objects
|
||||
around and search by key/tags/attribute instead.
|
||||
```
|
||||
|
||||
## Finding objects relative each other
|
||||
|
||||
Let's consider a `chest` with a `coin` inside it. The chests stand in a room `dungeon`. In the dungeon is also
|
||||
a `door`. This is an exit leading outside.
|
||||
|
||||
- `coin.location` is `chest`.
|
||||
- `chest.location` is `dungeon`.
|
||||
- `door.location` is `dungeon`.
|
||||
- `room.location` is `None` since it's not inside something else.
|
||||
|
||||
One can use this to find what is inside what. For example, `coin.location.location` is the `room`.
|
||||
We can also find what is inside each object. This is a list of things.
|
||||
|
||||
- `room.contents` is `[chest, door]`
|
||||
- `chest.contents` is `[coin]`
|
||||
- `coin.contents` is `[]`, the empty list since there's nothing 'inside' the coin.
|
||||
- `door.contents` is `[]` too.
|
||||
|
||||
A convenient helper is `.contents_get` - this allows to restrict what is returned:
|
||||
|
||||
- `room.contents_get(exclude=chest)` - this returns everything in the room except the chest (maybe it's hidden?)
|
||||
|
||||
There is a special property for finding exits:
|
||||
|
||||
- `room.exits` is `[door]`
|
||||
- `coin.exits` is `[]` (same for all the other objects)
|
||||
|
||||
There is a property `.destination` which is only used by exits:
|
||||
|
||||
- `door.destination` is `outside` (or wherever the door leads)
|
||||
- `room.destination` is `None` (same for all the other non-exit objects)
|
||||
|
||||
## Summary
|
||||
|
||||
Knowing how to find things is important and the tools from this section will serve you well. For most of your needs
|
||||
these tools will be all you need ...
|
||||
|
||||
... but not always. In the next lesson we will dive further into more complex searching when we look at
|
||||
Django queries and querysets in earnest.
|
||||
|
||||
123
docs/source/Howtos/Beginner-Tutorial/Part1/Tutorial-World.md
Normal file
123
docs/source/Howtos/Beginner-Tutorial/Part1/Tutorial-World.md
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
# The Tutorial World
|
||||
|
||||
The *Tutorial World* is a small and functioning MUD-style game world shipped with Evennia.
|
||||
It's a small showcase of what is possible. It can also be useful for those who have an easier
|
||||
time learning by deconstructing existing code.
|
||||
|
||||
Stand in the Limbo room and install it with
|
||||
|
||||
batchcommand tutorial_world.build
|
||||
|
||||
What this does is to run the build script
|
||||
[evennia/contrib/tutorial_world/build.ev](github:evennia/contrib/tutorial_world/build.ev).
|
||||
This is pretty much just a list of build-commands executed in sequence by the `batchcommand` command.
|
||||
Wait for the building to complete and don't run it twice.
|
||||
|
||||
> After having run the batchcommand, the `intro` command also becomes available in Limbo. Try it out to
|
||||
> for in-game help and to get an example of [EvMenu](../../../Components/EvMenu.md), Evennia's in-built
|
||||
> menu generation system!
|
||||
|
||||
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.
|
||||
|
||||
A new exit should have appeared named _Tutorial_. Enter by writing `tutorial`.
|
||||
|
||||
You will automatically `quell` when you enter (and `unquell` when you leave), so you can play the way it was intended.
|
||||
Both if you are triumphant or if you use the `give up` command you will eventually end up back in Limbo.
|
||||
|
||||
```{important}
|
||||
Only LOSERS and QUITTERS use the `give up` command.
|
||||
```
|
||||
|
||||
## 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 ...*
|
||||
|
||||
---
|
||||
|
||||
### Gameplay hints
|
||||
|
||||
- Use the command `tutorial` to get code insight behind the scenes of every room.
|
||||
- Look at everything. While a demo, the Tutorial World is not necessarily trivial to solve - it depends
|
||||
on your experience with text-based adventure games. Just remember that everything can be solved or bypassed.
|
||||
- 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.
|
||||
- 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.
|
||||
- Some things _cannot_ be hurt by mundane weapons. In that case it's OK to run away. Expect
|
||||
to be chased though.
|
||||
- Being defeated is a part of the experience. You can't actually die, but getting knocked out
|
||||
means being left in the dark ...
|
||||
|
||||
## Once you are done (or had enough)
|
||||
|
||||
Afterwards you'll either have conquered the old ruin and returned in glory and triumph ... or
|
||||
you returned limping and whimpering from the challenge by using the `give up` command.
|
||||
Either way you should now be back in Limbo, able to reflect on the experience.
|
||||
|
||||
Some features exemplified by the tutorial world:
|
||||
|
||||
- Rooms with custom ability to show details (like looking at the wall in the dark room)
|
||||
- Hidden or impassable exits until you fulfilled some criterion
|
||||
- Objects with multiple custom interactions (like swords, the well, the obelisk ...)
|
||||
- Large-area rooms (that bridge is actually only one room!)
|
||||
- Outdoor weather rooms with weather (the rain pummeling you)
|
||||
- Dark room, needing light source to reveal itself (the burning splinter even burns out after a while)
|
||||
- Puzzle object (the wines in the dark cell; hope you didn't get stuck!)
|
||||
- Multi-room puzzle (the obelisk and the crypt)
|
||||
- Aggressive mobile with roam, pursue and battle state-engine AI (quite deadly until you find the right weapon)
|
||||
- Weapons, also used by mobs (most are admittedly not that useful against the big baddie)
|
||||
- Simple combat system with attack/defend commands (teleporting on-defeat)
|
||||
- Object spawning (the weapons in the barrel and the final weapoon is actually randomized)
|
||||
- Teleporter trap rooms (if you fail the obelisk puzzle)
|
||||
|
||||
```{sidebar} Extra Credit
|
||||
|
||||
If you have previous programming experience (or after you have gone
|
||||
through this Starter tutorial) it may be instructive to dig a little deeper into the Tutorial-world
|
||||
code to learn how it achieves what it does. The code is heavily documented.
|
||||
You can find all the code in [evennia/contrib/tutorials/tutorial_world](evennia.contrib.tutorials.tutorial_world).
|
||||
The build-script is [here](github:evennia/contrib/tutorials/tutorial_world/build.ev).
|
||||
|
||||
|
||||
When reading the code, remember that the Tutorial World was designed to install easily and to not permanently modify
|
||||
the rest of the game. It therefore makes sure to only use temporary solutions and to clean up after itself. This is
|
||||
not something you will often need to worry about when making your own game.
|
||||
```
|
||||
|
||||
Quite a lot of stuff crammed in such a small area!
|
||||
|
||||
## Uninstall the tutorial world
|
||||
|
||||
Once are done playing with the tutorial world, let's uninstall it.
|
||||
Uninstalling the tutorial world basically means deleting all the rooms and objects it consists of.
|
||||
Make sure you are back in Limbo, then
|
||||
|
||||
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.
|
||||
|
||||
Even if the game-style of the Tutorial-world was not similar to the one you are interested in, it
|
||||
should hopefully have given you a little taste of some of the possibilities of Evennia. Now we'll
|
||||
move on with how to access this power through code.
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
# Part 2: What we want
|
||||
|
||||
```{eval-rst}
|
||||
.. sidebar:: Beginner Tutorial Parts
|
||||
|
||||
`Introduction <../Beginner-Tutorial-Intro.html>`_
|
||||
Getting set up.
|
||||
Part 1: `What we have <../Part1/Beginner-Tutorial-Part1-Intro.html>`_
|
||||
A tour of Evennia and how to use the tools, including an introduction to Python.
|
||||
**Part 2: What we want**
|
||||
Planning our tutorial game and what to think about when planning your own in the future.
|
||||
Part 3: `How we get there <../Part3/Beginner-Tutorial-Part3-Intro.html>`_
|
||||
Getting down to the meat of extending Evennia to make our game
|
||||
Part 4: `Using what we created <../Part4/Beginner-Tutorial-Part4-Intro.html>`_
|
||||
Building a tech-demo and world content to go with our code
|
||||
Part 5: `Showing the world <../Part5/Beginner-Tutorial-Part5-Intro.html>`_
|
||||
Taking our new game online and let players try it out
|
||||
```
|
||||
|
||||
In Part two of the Beginner Tutorial we'll take a step back and plan out the kind of tutorial
|
||||
game we want to make. This is a more 'theoretical' part where we won't do any hands-on
|
||||
programming.
|
||||
|
||||
In the process we'll go through the common questions of "where to start"
|
||||
and "what to think about" when creating a multiplayer online text game.
|
||||
|
||||
## Lessons
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 1
|
||||
|
||||
Planning-Where-Do-I-Begin.md
|
||||
Game-Planning.md
|
||||
Planning-Some-Useful-Contribs.md
|
||||
Planning-The-Tutorial-Game.md
|
||||
```
|
||||
|
||||
## Table of Contents
|
||||
|
||||
```{toctree}
|
||||
|
||||
Planning-Where-Do-I-Begin.md
|
||||
Game-Planning.md
|
||||
Planning-Some-Useful-Contribs.md
|
||||
Planning-The-Tutorial-Game.md
|
||||
```
|
||||
209
docs/source/Howtos/Beginner-Tutorial/Part2/Game-Planning.md
Normal file
209
docs/source/Howtos/Beginner-Tutorial/Part2/Game-Planning.md
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
# On Planning a Game
|
||||
|
||||
Last lesson we asked ourselves some questions about our motivation. In this one we'll present
|
||||
some more technical questions to consider. In the next lesson we'll answer them for the sake of
|
||||
our tutorial game.
|
||||
|
||||
Note that the suggestions on this page are just that - suggestions. Also, they are primarily aimed at a lone
|
||||
hobby designer or a small team developing a game in their free time.
|
||||
|
||||
```{important}
|
||||
|
||||
Your first all overshadowing goal is to beat the odds and get **something** out the door!
|
||||
Even if it's a scaled-down version of your dream game, lacking many "must-have" features!
|
||||
|
||||
```
|
||||
|
||||
Remember: *99.99999% of all great game ideas never lead to a game*. Especially not to an online
|
||||
game that people can actually play and enjoy. It's better to get your game out there and expand on it
|
||||
later than to code in isolation until you burn out, lose interest or your hard drive crashes.
|
||||
|
||||
- Keep the scope of your initial release down. Way down.
|
||||
- Start small, with an eye towards expansions later, after first release.
|
||||
- If the suggestions here seems boring or a chore to you, do it your way instead. Everyone's different.
|
||||
- Keep having _fun_. You must keep your motivation up, whichever way works for _you_.
|
||||
|
||||
|
||||
## The steps
|
||||
|
||||
Here are the rough steps towards your goal.
|
||||
|
||||
1. Planning
|
||||
2. Coding + Gradually building a tech-demo
|
||||
3. Building the actual game world
|
||||
4. Release
|
||||
5. Celebrate
|
||||
|
||||
## Planning
|
||||
|
||||
You need to have at least a rough idea about what you want to create. Some like a lot of planning, others
|
||||
do it more seat-of-the-pants style. Regardless, while _some_ planning is always good to do, it's common
|
||||
to have your plans change on you as you create your code prototypes. So don't get _too_ bogged down in
|
||||
the details out of the gate.
|
||||
|
||||
Many prospective game developers are very good at *parts* of this process, namely in defining what their
|
||||
world is "about": The theme, the world concept, cool monsters and so on. Such things are very important. But
|
||||
unfortunately, they are not enough to make your game. You need to figure out how to accomplish your ideas in
|
||||
Evennia.
|
||||
|
||||
Below are some questions to get you going. In the next lesson we will try to answer them for our particular
|
||||
tutorial game. There are of course many more questions you could be asking yourself.
|
||||
|
||||
### Administration
|
||||
|
||||
- Should your game rules be enforced by coded systems or by human game masters?
|
||||
- What is the staff hierarchy in your game? Is vanilla Evennia roles enough or do you need something else?
|
||||
- Should players be able to post out-of-characters on channels and via other means like bulletin-boards?
|
||||
|
||||
### Building
|
||||
|
||||
- How will the world be built? Traditionally (from in-game with build-commands) or externally (by batchcmds/code
|
||||
or directly with custom code)?
|
||||
- Can only privileged Builders create things or should regular players also have limited build-capability?
|
||||
|
||||
### Systems
|
||||
|
||||
- Do you base your game off an existing RPG system or make up your own?
|
||||
- What are the game mechanics? How do you decide if an action succeeds or fails?
|
||||
- Does the flow of time matter in your game - does night and day change? What about seasons?
|
||||
- Do you want changing, global weather or should weather just be set manually in roleplay?
|
||||
- Do you want a coded world-economy or just a simple barter system? Or no formal economy at all?
|
||||
- Do you have concepts like reputation and influence?
|
||||
- Will your characters be known by their name or only by their physical appearance?
|
||||
|
||||
### Rooms
|
||||
|
||||
- Is a simple room description enough or should the description be able to change (such as with time, by
|
||||
light conditions, weather or season)?
|
||||
- Should the room have different statuses? Can it have smells, sounds? Can it be affected by
|
||||
dramatic weather, fire or magical effects? If so, how would this affect things in the room? Or are
|
||||
these things something admins/game masters should handle manually?
|
||||
- Can objects be hidden in the room? Can a person hide in the room? How does the room display this?
|
||||
|
||||
### Objects / items
|
||||
|
||||
- How numerous are your objects? Do you want large loot-lists or are objects just role playing props
|
||||
created on demand?
|
||||
- If you use money, is each coin a separate object or do you just store a bank account value?
|
||||
- Do multiple similar objects form stacks and how are those stacks handled in that case?
|
||||
- Does an object have weight or volume (so you cannot carry an infinite amount of them)?
|
||||
- Can objects be broken? Can they be repaired?
|
||||
- Can you fight with a chair or a flower or must you use a specific 'weapon' kind of thing?
|
||||
- Will characters be able to craft new objects?
|
||||
- Should mobs/NPCs have some sort of AI?
|
||||
- Are NPCs and mobs different entities? How do they differ?
|
||||
- Should there be NPCs giving quests? If so, how do you track Quest status?
|
||||
|
||||
### Characters
|
||||
|
||||
- Can players have more than one Character active at a time or are they allowed to multi-play?
|
||||
- How does the character-generation work? Walk from room-to-room? A menu?
|
||||
- How do you implement different "classes" or "races"? Are they separate types of objects or do you
|
||||
simply load different stats on a basic object depending on what the Player wants?
|
||||
- If a Character can hide in a room, what skill will decide if they are detected?
|
||||
- What does the skill tree look like? Can a Character gain experience to improve? By killing
|
||||
enemies? Solving quests? By roleplaying?
|
||||
- May player-characters attack each other (PvP)?
|
||||
- What are the penalties of defeat? Permanent death? Quick respawn? Time in prison?
|
||||
|
||||
A MUD's a lot more involved than you would think and these things hang together in a complex web. It
|
||||
can easily become overwhelming and it's tempting to want *all* functionality right out of the door.
|
||||
Try to identify the basic things that "make" your game and focus *only* on them for your first
|
||||
release. Make a list. Keep future expansions in mind but limit yourself.
|
||||
|
||||
## Coding and Tech demo
|
||||
|
||||
This is the actual work of creating the "game" part of your game. As you code and test systems you should
|
||||
build a little "tech demo" along the way.
|
||||
|
||||
```{sidebar} Tech demo
|
||||
|
||||
With "tech demo" we mean a small example of your code in-action: A room with a mob,
|
||||
a way to jump into and test character-creation etc. The tech demo need not be pretty, it's
|
||||
there to test functionality. It's not the beginning of your game world (unless you find that
|
||||
to be more fun).
|
||||
|
||||
```
|
||||
|
||||
Try to avoid going wild with building a huge game world before you have a tech-demo showing off all parts
|
||||
you expect to have in the first version of your game. Otherwise you run the risk of having to redo it all
|
||||
again.
|
||||
|
||||
Evennia tries hard to make the coding easier for you, but there is no way around the fact that if you want
|
||||
anything but a basic chat room you *will* have to bite the bullet and code your game (or find a coder willing
|
||||
to do it for you).
|
||||
|
||||
> Even if you won't code anything yourself, as a designer you need to at least understand the basic
|
||||
paradigms and components of Evennia. It's recommended you look over the rest of this Beginner Tutorial to learn
|
||||
what tools you have available.
|
||||
|
||||
During Coding you look back at the things you wanted during the **Planning** phase and try to
|
||||
implement them. Don't be shy to update your plans if you find things easier/harder than you thought.
|
||||
The earlier you revise problems, the easier they will be to fix.
|
||||
|
||||
A good idea is to host your code online using _version control_. Github.com offers free Private repos
|
||||
these days if you don't want the world to learn your secrets. Not only version control
|
||||
make it easy for your team to collaborate, it also means
|
||||
your work is backed up at all times. The page on [Version Control](../../../Coding/Version-Control.md)
|
||||
will help you to setting up a sane developer environment with proper version control.
|
||||
|
||||
## World Building
|
||||
|
||||
Up until this point we've only had a few tech-demo objects in the database. This step is the act of
|
||||
populating the database with a larger, thematic world. Too many would-be developers jump to this
|
||||
stage too soon (skipping the **Coding** or even **Planning** stages). What if the rooms you build
|
||||
now doesn't include all the nice weather messages the code grows to support? Or the way you store
|
||||
data changes under the hood? Your building work would at best require some rework and at worst you
|
||||
would have to redo the whole thing. You could be in for a *lot* of unnecessary work if you build stuff
|
||||
en masse without having the underlying code systems in some reasonable shape first.
|
||||
|
||||
So before starting to build, the "game" bit (**Coding** + **Testing**) should be more or less
|
||||
**complete**, *at least to the level of your initial release*.
|
||||
|
||||
Make sure it is clear to yourself and your eventual builders just which parts of the world you want
|
||||
for your initial release. Establish for everyone which style, quality and level of detail you expect.
|
||||
|
||||
Your goal should *not* be to complete your entire world in one go. You want just enough to make the
|
||||
game's "feel" come across. You want a minimal but functioning world where the intended game play can
|
||||
be tested and roughly balanced. You can always add new areas later.
|
||||
|
||||
During building you get free and extensive testing of whatever custom build commands and systems you
|
||||
have made at this point. If Builders and coders are different people you also
|
||||
get a chance to hear if some things are hard to understand or non-intuitive. Make sure to respond
|
||||
to this feedback.
|
||||
|
||||
|
||||
## Alpha Release
|
||||
|
||||
As mentioned, don't hold onto your world more than necessary. *Get it out there* with a huge *Alpha*
|
||||
flag and let people try it!
|
||||
|
||||
Call upon your alpha-players to try everything - they *will* find ways to break your game in ways that
|
||||
you never could have imagined. In Alpha you might be best off to
|
||||
focus on inviting friends and maybe other MUD developers, people who you can pester to give proper
|
||||
feedback and bug reports (there *will* be bugs, there is no way around it).
|
||||
|
||||
Follow the quick instructions for [Online Setup](../../../Setup/Online-Setup.md) to make your
|
||||
game visible online.
|
||||
|
||||
If you hadn't already, make sure to put up your game on the
|
||||
[Evennia game index](http://games.evennia.com/) so people know it's in the works (actually, even
|
||||
pre-alpha games are allowed in the index so don't be shy)!
|
||||
|
||||
## Beta Release/Perpetual Beta
|
||||
|
||||
Once things stabilize in Alpha you can move to *Beta* and let more people in. Many MUDs are in
|
||||
[perpetual beta](https://en.wikipedia.org/wiki/Perpetual_beta), meaning they are never considered
|
||||
"finished", but just repeat the cycle of Planning, Coding, Testing and Building over and over as new
|
||||
features get implemented or Players come with suggestions. As the game designer it is now up to you
|
||||
to gradually perfect your vision.
|
||||
|
||||
## Congratulate yourself!
|
||||
|
||||
You are worthy of a celebration since at this point you have joined the small, exclusive crowd who
|
||||
have made their dream game a reality!
|
||||
|
||||
## Planning our tutorial game
|
||||
|
||||
In the next lesson we'll make use of these general points and try to plan out our tutorial game.
|
||||
|
||||
|
|
@ -0,0 +1,244 @@
|
|||
# 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.
|
||||
|
||||
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
|
||||
from to use, tweak or take inspiration from when you make your game.
|
||||
|
||||
The [Contrib overview](../../../Contribs/Contribs-Overview.md) page gives the full list of the current roster of contributions. On
|
||||
this page we will review a few contribs we will make use of for our game. We will do the actual installation
|
||||
of them when we start coding in the next part of this tutorial series. While we will introduce them here, you
|
||||
are wise to read their doc-strings yourself for the details.
|
||||
|
||||
This is the things we know we need:
|
||||
|
||||
- A barter system
|
||||
- Character generation
|
||||
- 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
|
||||
- Combat (with players and against monsters)
|
||||
|
||||
## Barter contrib
|
||||
|
||||
[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.
|
||||
|
||||
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.
|
||||
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!
|
||||
Tom > accept: Good! Nice doing business with you.
|
||||
<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
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
## Clothing contrib
|
||||
|
||||
[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:
|
||||
|
||||
> 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.
|
||||
|
||||
## Dice contrib
|
||||
|
||||
[source](../../../api/evennia.contrib.rpg.dice.md)
|
||||
|
||||
The dice contrib presents a general dice roller to use in game.
|
||||
|
||||
> roll 2d6
|
||||
Roll(s): 2 and 5. Total result is 7.
|
||||
> roll 1d100 + 2
|
||||
Roll(s): 43. Total result is 47
|
||||
> roll 1d20 > 12
|
||||
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
|
||||
or play a game, we will not need it for the core of our game.
|
||||
|
||||
## Extended room contrib
|
||||
|
||||
[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.
|
||||
|
||||
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.
|
||||
|
||||
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.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: <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.
|
||||
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: Sue grins. "I'm Angelica", she says.
|
||||
Tom: A muscular, blonde woman grins. "I'm Angelica", she says.
|
||||
Tom > recog muscular Angelica
|
||||
Tom > emote /me nods to /angelica: "I have a message for you ..."
|
||||
Tom: Tom nods to Angelica: "I have a message for you ..."
|
||||
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 `"..."`.
|
||||
|
||||
The RPSystem module also includes options for `poses`, which help to establish your position in the room
|
||||
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.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.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.
|
||||
|
||||
## Traits contrib
|
||||
|
||||
[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.
|
||||
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
|
||||
|
||||
> py self.traits.hp.value
|
||||
100
|
||||
> py self.traits.hp -= 20 # getting hurt
|
||||
> py self.traits.hp.value
|
||||
80
|
||||
> 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.
|
||||
|
||||
The contrib introduces three main Trait-classes
|
||||
|
||||
- _Static_ traits for single values like str, dex, things that at most gets a modifier.
|
||||
- _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
|
||||
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.ratetarget = 50 # stop at 50 hp
|
||||
# Wait 30s
|
||||
> py self.traits.hp.value
|
||||
70
|
||||
# Wait another 30s
|
||||
> py self.traits.hp.value
|
||||
50 # stopped at 50
|
||||
> py self.traits.hp.rate = 0 # no more poison
|
||||
> py self.traits.hp.rate = 5 # healing magic!
|
||||
# wait 5s
|
||||
> pyself.traits.hp.value
|
||||
75
|
||||
```
|
||||
|
||||
Traits will be very practical to use for our character sheets.
|
||||
|
||||
## Turnbattle contrib
|
||||
|
||||
[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.
|
||||
- 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
|
||||
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
|
||||
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.
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,425 @@
|
|||
# Planning our tutorial game
|
||||
|
||||
Using the general plan from last lesson we'll now establish what kind of game we want to create for this tutorial.
|
||||
Remembering that we need to keep the scope down, let's establish some parameters.
|
||||
Note that for your own
|
||||
game you don't _need_ to agree/adopt any of these. Many game-types need more or much less than this.
|
||||
But this makes for good, instructive examples.
|
||||
|
||||
- To have something to refer to rather than just saying "our tutorial game" over and over, we'll
|
||||
name it ... _EvAdventure_.
|
||||
- We want EvAdventure be a small game we can play ourselves for fun, but which could in principle be expanded
|
||||
to something more later.
|
||||
- Let's go with a fantasy theme, it's well understood.
|
||||
- We'll use some existing, simple RPG system.
|
||||
- We want to be able to create and customize a character of our own.
|
||||
- We want the tools to roleplay with other players.
|
||||
- We don't want to have to rely on a Game master to resolve things, but will rely on code for skill resolution
|
||||
and combat.
|
||||
- We want monsters to fight and NPCs we can talk to. So some sort of AI.
|
||||
- We want to be able to buy and sell stuff, both with NPCs and other players.
|
||||
- We want some sort of crafting system.
|
||||
- We want some sort of quest system.
|
||||
|
||||
Let's answer the questions from the previous lesson and discuss some of the possibilities.
|
||||
|
||||
## Administration
|
||||
|
||||
### Should your game rules be enforced by coded systems by human game masters?
|
||||
|
||||
Generally, the more work you expect human staffers/GMs to do, the less your code needs to work. To
|
||||
support GMs you'd need to design commands to support GM-specific actions and the type of game-mastering
|
||||
you want them to do. You may need to expand communication channels so you can easily
|
||||
talk to groups people in private and split off gaming groups from each other. RPG rules could be as simple
|
||||
as the GM sitting with the rule books and using a dice-roller for visibility.
|
||||
|
||||
GM:ing is work-intensive however, and even the most skilled and enthusiastic GM can't be awake all hours
|
||||
of the day to serve an international player base. The computer never needs sleep, so having the ability for
|
||||
players to "self-serve" their RP itch when no GMs are around is a good idea even for the most GM-heavy games.
|
||||
|
||||
On the other side of the spectrum are games with no GMs at all; all gameplay are driven either by the computer
|
||||
or by the interactions between players. Such games still need an active staff, but nowhere as much active
|
||||
involvement. Allowing for solo-play with the computer also allows players to have fun when the number of active
|
||||
players is low.
|
||||
|
||||
We want EvAdventure to work entirely without depending on human GMs. That said, there'd be nothing
|
||||
stopping a GM from stepping in and run an adventure for some players should they want to.
|
||||
|
||||
### What is the staff hierarchy in your game? Is vanilla Evennia roles enough or do you need something else?
|
||||
|
||||
The default hierarchy is
|
||||
|
||||
- `Player` - regular players
|
||||
- `Player Helper` - can create/edit help entries
|
||||
- `Builder` - can use build commands
|
||||
- `Admin` - can kick and ban accounts
|
||||
- `Developer` - full access, usually also trusted with server access
|
||||
|
||||
There is also the _superuser_, the "owner" of the game you create when you first set up your database. This user
|
||||
goes outside the regular hierarchy and should usually only.
|
||||
|
||||
We are okay with keeping this structure for our game.
|
||||
|
||||
### Should players be able to post out-of-characters on channels and via other means like bulletin-boards?
|
||||
|
||||
Evennia's _Channels_ are by default only available between _Accounts_. That is, for players to communicate with each
|
||||
other. By default, the `public` channel is created for general discourse.
|
||||
Channels are logged to a file and when you are coming back to the game you can view the history of a channel
|
||||
in case you missed something.
|
||||
|
||||
> public Hello world!
|
||||
[Public] MyName: Hello world!
|
||||
|
||||
But Channels can also be set up to work between Characters instead of Accounts. This would mean the channels
|
||||
would have an in-game meaning:
|
||||
|
||||
- Members of a guild could be linked telepathically.
|
||||
- Survivors of the apocalypse can communicate over walkie-talkies.
|
||||
- Radio stations you can tune into or have to discover.
|
||||
|
||||
_Bulletin boards_ are a sort of in-game forum where posts are made publicly or privately. Contrary to a channel,
|
||||
the messages are usually stored and are grouped into topics with replies. Evennia has no default bulletin-board
|
||||
system.
|
||||
|
||||
In EvAdventure we will just use the default inter-account channels. We will also not be implementing any
|
||||
bulletin boards.
|
||||
|
||||
## Building
|
||||
|
||||
### How will the world be built?
|
||||
|
||||
There are two main ways to handle this:
|
||||
- Traditionally, from in-game with build-commands: This means builders creating content in their game
|
||||
client. This has the advantage of not requiring Python skills nor server access. This can often be a quite
|
||||
intuitive way to build since you are sort-of walking around in your creation as you build it. However, the
|
||||
developer (you) must make sure to provide build-commands that are flexible enough for builders to be able to
|
||||
create the content you want for your game.
|
||||
- Externally (by batchcmds): Evennia's `batchcmd` takes a text file with Evennia Commands and executes them
|
||||
in sequence. This allows the build process to be repeated and applied quickly to a new database during development.
|
||||
It also allows builders to use proper text-editing tools rather than writing things line-by-line in their clients.
|
||||
The drawback is that for their changes to go live they either need server access or they need to send their
|
||||
batchcode to the game administrator so they can apply the changes. Or use version control.
|
||||
- Externally (with batchcode or custom code): This is the "professional game development" approach. This gives the
|
||||
builders maximum power by creating the content in Python using Evennia primitives. The `batchcode` processor
|
||||
allows Evennia to apply and re-apply build-scripts that are raw Python modules. Again, this would require the
|
||||
builder to have server access or to use version control to share their work with the rest of the development team.
|
||||
|
||||
In this tutorial, we will show examples of all these ways, but since we don't have a team of builders we'll
|
||||
build the brunt of things using Evennia's Batchcode system.
|
||||
|
||||
### Can only privileged Builders create things or should regular players also have limited build-capability?
|
||||
|
||||
In some game styles, players have the ability to create objects and even script them. While giving regular users
|
||||
the ability to create objects with in-built commands is easy and safe, actual code-creation (aka _softcode_ ) is
|
||||
not something Evennia supports natively. Regular, untrusted users should never be allowed to execute raw Python
|
||||
code (such as what you can do with the `py` command). You can
|
||||
[read more about Evennia's stance on softcode here](../../../Concepts/Soft-Code.md). If you want users to do limited scripting,
|
||||
it's suggested that this is accomplished by adding more powerful build-commands for them to use.
|
||||
|
||||
For our tutorial-game, we will only allow privileged builders to modify the world. The exception is crafting,
|
||||
which we will limit to repairing broken items by combining them with other repair-related items.
|
||||
|
||||
## Systems
|
||||
|
||||
### Do you base your game off an existing RPG system or make up your own?
|
||||
|
||||
We will make use of [Open Adventure](http://www.geekguild.com/openadventure/), a simple 'old school' RPG-system
|
||||
that is available for free under the Creative Commons license. We'll only use a subset of the rules from
|
||||
the blue "basic" book. For the sake of keeping down the length of this tutorial we will limit what features
|
||||
we will include:
|
||||
|
||||
- Only two 'archetypes' (classes) - Arcanist (wizard) and Warrior, these are examples of two different play
|
||||
styles.
|
||||
- Two races only (dwarves and elves), to show off how to implement races and race bonuses.
|
||||
- No extra features of the races/archetypes such as foci and special feats. While these are good for fleshing
|
||||
out a character, these will work the same as other bonuses and are thus not that instructive.
|
||||
- We will add only a small number of items/weapons from the Open Adventure rulebook to show how it's done.
|
||||
|
||||
### What are the game mechanics? How do you decide if an action succeeds or fails?
|
||||
|
||||
Open Adventure's conflict resolution is based on adding a trait (such as Strength) with a random number in
|
||||
order to beat a target. We will emulate this in code.
|
||||
|
||||
Having a "skill" means getting a bonus to that roll for a more narrow action.
|
||||
Since the computer will need to know exactly what those skills are, we will add them more explicitly than
|
||||
in the rules, but we will only add the minimum to show off the functionality we need.
|
||||
|
||||
### Does the flow of time matter in your game - does night and day change? What about seasons?
|
||||
|
||||
Most commonly, game-time runs faster than real-world time. There are
|
||||
a few advantages with this:
|
||||
|
||||
- Unlike in a single-player game, you can't fast-forward time in a multiplayer game if you are waiting for
|
||||
something, like NPC shops opening.
|
||||
- Healing and other things that we know takes time will go faster while still being reasonably 'realistic'.
|
||||
|
||||
The main drawback is for games with slower roleplay pace. While you are having a thoughtful roleplaying scene
|
||||
over dinner, the game world reports that two days have passed. Having a slower game time than real-time is
|
||||
a less common, but possible solution for such games.
|
||||
|
||||
It is however _not_ recommended to let game-time exactly equal the speed of real time. The reason for this
|
||||
is that people will join your game from all around the world, and they will often only be able to play at
|
||||
particular times of their day. With a game-time drifting relative real-time, everyone will eventually be
|
||||
able to experience both day and night in the game.
|
||||
|
||||
For this tutorial-game we will go with Evennia's default, which is that the game-time runs two times faster
|
||||
than real time.
|
||||
|
||||
### Do you want changing, global weather or should weather just be set manually in roleplay?
|
||||
|
||||
A weather system is a good example of a game-global system that affects a subset of game entities
|
||||
(outdoor rooms). We will not be doing any advanced weather simulation, but we'll show how to do
|
||||
random weather changes happening across the game world.
|
||||
|
||||
### Do you want a coded world-economy or just a simple barter system? Or no formal economy at all?
|
||||
|
||||
We will allow for money and barter/trade between NPCs/Players and Player/Player, but will not care about
|
||||
inflation. A real economic simulation could do things like modify shop prices based on supply and demand.
|
||||
We will not go down that rabbit hole.
|
||||
|
||||
### Do you have concepts like reputation and influence?
|
||||
|
||||
These are useful things for a more social-interaction heavy game. We will not include them for this
|
||||
tutorial however.
|
||||
|
||||
### Will your characters be known by their name or only by their physical appearance?
|
||||
|
||||
This is a common thing in RP-heavy games. Others will only see you as "The tall woman" until you
|
||||
introduce yourself and they 'recognize' you with a name. Linked to this is the concept of more complex
|
||||
emoting and posing.
|
||||
|
||||
Adding such a system from scratch is complex and way beyond the scope of this tutorial. However,
|
||||
there is an existing Evennia contrib that adds all of this functionality and more, so we will
|
||||
include that and explain briefly how it works.
|
||||
|
||||
## Rooms
|
||||
|
||||
### Is a simple room description enough or should the description be able to change?
|
||||
|
||||
Changing room descriptions for day and night, winder and summer is actually quite easy to do, but looks
|
||||
very impressive. We happen to know there is also a contrib that helps with this, so we'll show how to
|
||||
include that.
|
||||
|
||||
### Should the room have different statuses?
|
||||
|
||||
We will have different weather in outdoor rooms, but this will not have any gameplay effect - bow strings
|
||||
will not get wet and fireballs will not fizzle if it rains.
|
||||
|
||||
### Can objects be hidden in the room? Can a person hide in the room?
|
||||
|
||||
We will not model hiding and stealth. This will be a game of honorable face-to-face conflict.
|
||||
|
||||
## Objects
|
||||
|
||||
### How numerous are your objects? Do you want large loot-lists or are objects just role playing props?
|
||||
|
||||
Since we are not going for a pure freeform RPG here, we will want objects with properties, like weapons
|
||||
and potions and such. Monsters should drop loot even though our list of objects will not be huge.
|
||||
|
||||
### Is each coin a separate object or do you just store a bank account value?
|
||||
|
||||
Since we will use bartering, placing coin objects on one side of the barter makes for a simple way to
|
||||
handle payments. So we will use coins as-objects.
|
||||
|
||||
### Do multiple similar objects form stacks and how are those stacks handled in that case?
|
||||
|
||||
Since we'll use coins, it's practical to have them and other items stack together. While Evennia does not
|
||||
do this natively, we will make use of a contrib for this.
|
||||
|
||||
### Does an object have weight or volume (so you cannot carry an infinite amount of them)?
|
||||
|
||||
Limiting carrying weight is one way to stop players from hoarding. It also makes it more important
|
||||
for players to pick only the equipment they need. Carrying limits can easily come across as
|
||||
annoying to players though, so one needs to be careful with it.
|
||||
|
||||
Open Adventure rules include weight limits, so we will include them.
|
||||
|
||||
### Can objects be broken? Can they be repaired?
|
||||
|
||||
Item breakage is very useful for a game economy; breaking weapons adds tactical considerations (if it's not
|
||||
too common, then it becomes annoying) and repairing things gives work for crafting players.
|
||||
|
||||
We wanted a crafting system, so this is what we will limit it to - repairing items using some sort
|
||||
of raw materials.
|
||||
|
||||
### Can you fight with a chair or a flower or must you use a special 'weapon' kind of thing?
|
||||
|
||||
Traditionally, only 'weapons' could be used to fight with. In the past this was a useful
|
||||
simplification, but with Python classes and inheritance, it's not actually more work to just
|
||||
let all items in game work as a weapon in a pinch.
|
||||
|
||||
So for our game we will let a character use any item they want as a weapon. The difference will
|
||||
be that non-weapon items will do less damage and also break and become unusable much quicker.
|
||||
|
||||
### Will characters be able to craft new objects?
|
||||
|
||||
Crafting is a common feature in multiplayer games. In code it usually means using a skill-check
|
||||
to combine base ingredients from a fixed recipe in order to create a new item. The classic
|
||||
example is to combine _leather straps_, a _hilt_, a _pommel_ and a _blade_ to make a new _sword_.
|
||||
A full-fledged crafting system could require multiple levels of crafting, including having to mine
|
||||
for ore or cut down trees for wood.
|
||||
|
||||
In our case we will limit our crafting to repairing broken items. To show how it's done, we will require
|
||||
extra items (a recipe) in order to facilitate the repairs.
|
||||
|
||||
### Should mobs/NPCs have some sort of AI?
|
||||
|
||||
A rule of adding Artificial Intelligence is that with today's technology you should not hope to fool
|
||||
anyone with it anytime soon. Unless you have a side-gig as an AI researcher, users will likely
|
||||
not notice any practical difference between a simple state-machine and you spending a lot of time learning
|
||||
how to train a neural net.
|
||||
|
||||
For this tutorial, we will show how to add a simple state-machine for monsters. NPCs will only be
|
||||
shop-keepers and quest-gives so they won't need any real AI to speak of.
|
||||
|
||||
### Are NPCs and mobs different entities? How do they differ?
|
||||
|
||||
"Mobs" or "mobiles" are things that move around. This is traditionally monsters you can fight with, but could
|
||||
also be city guards or the baker going to chat with the neighbor. Back in the day, they were often fundamentally
|
||||
different these days it's often easier to just make NPCs and mobs essentially the same thing.
|
||||
|
||||
In EvAdventure, both Monsters and NPCs will be the same type of thing; A monster could give you a quest
|
||||
and an NPC might fight you as a mob as well as trade with you.
|
||||
|
||||
### _Should there be NPCs giving quests? If so, how do you track Quest status?
|
||||
|
||||
We will design a simple quest system to track the status of ongoing quests.
|
||||
|
||||
## Characters
|
||||
|
||||
### Can players have more than one Character active at a time or are they allowed to multi-play?
|
||||
|
||||
Since Evennia differentiates between `Sessions` (the client-connection to the game), `Accounts`
|
||||
and `Character`s, it natively supports multi-play. This is controlled by the `MULTISESSION_MODE`
|
||||
setting, which has a value from `0` (default) to `3`.
|
||||
|
||||
- `0`- One Character per Account and one Session per Account. This means that if you login to the same
|
||||
account from another client you'll be disconnected from the first. When creating a new account, a Character
|
||||
will be auto-created with the same name as your Account. This is default mode and mimics legacy code bases
|
||||
which had no separation between Account and Character.
|
||||
- `1` - One Character per Account, multiple Sessions per Account. So you can connect simultaneously from
|
||||
multiple clients and see the same output in all of them.
|
||||
- `2` - Multiple Characters per Account, one Session per Character. This will not auto-create a same-named
|
||||
Character for you, instead you get to create/choose between a number of Characters up to a max limit given by
|
||||
the `MAX_NR_CHARACTERS` setting (default 1). You can play them all simultaneously if you have multiple clients
|
||||
open, but only one client per Character.
|
||||
- `3` - Multiple Characters per Account, Multiple Sessions per Character. This is like mode 2, except players
|
||||
can control each Character from multiple clients, seeing the same output from each Character.
|
||||
|
||||
We will go with a multi-role game, so we will use `MULTISESSION_MODE=3` for this tutorial.
|
||||
|
||||
### How does the character-generation work?
|
||||
|
||||
There are a few common ways to do character generation:
|
||||
|
||||
- Rooms. This is the traditional way. Each room's description tells you what command to use to modify
|
||||
your character. When you are done you move to the next room. Only use this if you have another reason for
|
||||
using a room, like having a training dummy to test skills on, for example.
|
||||
- A Menu. The Evennia _EvMenu_ system allows you to code very flexible in-game menus without needing to walk
|
||||
between rooms. You can both have a step-by-step menu (a 'wizard') or allow the user to jump between the
|
||||
steps as they please. This tends to be a lot easier for newcomers to understand since it doesn't require
|
||||
using custom commands they will likely never use again after this.
|
||||
- Questions. A fun way to build a character is to answer a series of questions. This is usually implemented
|
||||
with a sequential menu.
|
||||
|
||||
For the tutorial we will use a menu to let the user modify each section of their character sheet in any order
|
||||
until they are happy.
|
||||
|
||||
### How do you implement different "classes" or "races"?
|
||||
|
||||
The way classes and races work in most RPGs (as well as in OpenAdventure) is that they act as static 'templates'
|
||||
that inform which bonuses and special abilities you have. This means that all we need to store on the
|
||||
Character is _which_ class and _which_ race they have; the actual logic can sit in Python code and just
|
||||
be looked up when we need it.
|
||||
|
||||
### If a Character can hide in a room, what skill will decide if they are detected?
|
||||
|
||||
Hiding means a few things.
|
||||
- The Character should not appear in the room's description / character list
|
||||
- Others hould not be able to interact with a hidden character. It'd be weird if you could do `attack <name>`
|
||||
or `look <name>` if the named character is in hiding.
|
||||
- There must be a way for the person to come out of hiding, and probably for others to search or accidentally
|
||||
find the person (probably based on skill checks).
|
||||
- The room will also need to be involved, maybe with some modifier as to how easy it is to hide in the room.
|
||||
|
||||
We will _not_ be including a hide-mechanic in EvAdventure though.
|
||||
|
||||
### What does the skill tree look like? Can a Character gain experience to improve? By killing enemies? Solving quests? By roleplaying?
|
||||
|
||||
Gaining experience points (XP) and improving one's character is a staple of roleplaying games. There are many
|
||||
ways to implement this:
|
||||
- Gaining XP from kills is very common; it's easy to let a monster be 'worth' a certain number of XP and it's
|
||||
easy to tell when you should gain it.
|
||||
- Gaining XP from quests is the same - each quest is 'worth' XP and you get them when completing the test.
|
||||
- Gaining XP from roleplay is harder to define. Different games have tried a lot of different ways to do this:
|
||||
- XP from being online - just being online gains you XP. This inflates player numbers but many players may
|
||||
just be lurking and not be actually playing the game at any given time.
|
||||
- XP from roleplaying scenes - you gain XP according to some algorithm analyzing your emotes for 'quality',
|
||||
how often you post, how long your emotes are etc.
|
||||
- XP from actions - you gain XP when doing things, anything. Maybe your XP is even specific to each action, so
|
||||
you gain XP only for running when you run, XP for your axe skill when you fight with an axe etc.
|
||||
- XP from fails - you only gain XP when failing rolls.
|
||||
- XP from other players - other players can award you XP for good RP.
|
||||
|
||||
For EvAdventure we will use Open Adventure's rules for XP, which will be driven by kills and quest successes.
|
||||
|
||||
### May player-characters attack each other (PvP)?
|
||||
|
||||
Deciding this affects the style of your entire game. PvP makes for exciting gameplay but it opens a whole new
|
||||
can of worms when it comes to "fairness". Players will usually accept dying to an overpowered NPC dragon. They
|
||||
will not be as accepting if they perceive another player is perceived as being overpowered. PvP means that you
|
||||
have to be very careful to balance the game - all characters does not have to be exactly equal but they should
|
||||
all be viable to play a fun game with. PvP does not only mean combat though. Players can compete in all sorts of ways, including gaining influence in
|
||||
a political game or gaining market share when selling their crafted merchandise.
|
||||
|
||||
For the EvAdventure we will support both Player-vs-environment combat and turn-based PvP. We will allow players
|
||||
to barter with each other (so potentially scam others?) but that's the extent of it. We will focus on showing
|
||||
off techniques and will not focus on making a balanced game.
|
||||
|
||||
### What are the penalties of defeat? Permanent death? Quick respawn? Time in prison?
|
||||
|
||||
This is another big decision that strongly affects the mood and style of your game.
|
||||
|
||||
Perma-death means that once your character dies, it's gone and you have to make a new one.
|
||||
|
||||
- It allows for true heroism. If you genuinely risk losing your character of two years to fight the dragon,
|
||||
your triumph is an actual feat.
|
||||
- It limits the old-timer dominance problem. If long-time players dies occationally, it will open things
|
||||
up for newcomers.
|
||||
- It lowers inflation, since the hoarded resources of a dead character can be removed.
|
||||
- It gives capital punishment genuine discouraging power.
|
||||
- It's realistic.
|
||||
|
||||
Perma-death comes with some severe disadvantages however.
|
||||
|
||||
- It's impopular. Many players will just not play a game where they risk losing their beloved character
|
||||
just like that.
|
||||
- Many players say they like the _idea_ of permadeath except when it could happen to them.
|
||||
- It can limit roleplaying freedom and make people refuse to take any risks.
|
||||
- It may make players even more reluctant to play conflict-driving 'bad guys'.
|
||||
- Game balance is much, much more important when results are "final". This escalates the severity of 'unfairness'
|
||||
a hundred-fold. Things like bugs or exploits can also lead to much more server effects.
|
||||
|
||||
For these reasons, it's very common to do hybrid systems. Some tried variations:
|
||||
|
||||
- NPCs cannot kill you, only other players can.
|
||||
- Death is permanent, but it's difficult to actually die - you are much more likely to end up being severely
|
||||
hurt/incapacitated.
|
||||
- You can pre-pay 'insurance' to magically/technologically avoid actually dying. Only if don't have insurance will
|
||||
you die permanently.
|
||||
- Death just means harsh penalties, not actual death.
|
||||
- When you die you can fight your way back to life from some sort of afterlife.
|
||||
- You'll only die permanently if you as a player explicitly allows it.
|
||||
|
||||
For our tutorial-game we will not be messing with perma-death; instead your defeat will mean you will re-spawn
|
||||
back at your home location with a fraction of your health.
|
||||
|
||||
## Conclusions
|
||||
|
||||
Going through the questions has helped us get a little bit more of a feel for the game we want to do. There are
|
||||
many other things we could ask ourselves, but if we can cover these points we will be a good way towards a complete,
|
||||
playable game!
|
||||
|
||||
Before starting to code in earnest a good coder should always do an inventory of all the stuff they _don't_ need
|
||||
to code themselves. So in the next lesson we will check out what help we have from Evennia's _contribs_.
|
||||
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
# Where do I begin?
|
||||
|
||||
The good news is that following this Starting tutorial is a great way to begin making an Evennia game.
|
||||
|
||||
The bad news is that everyone's different and when it comes to starting your own game there is no
|
||||
one-size-fits-all answer. Instead we will ask a series of questions
|
||||
to help you figure this out for yourself. It will also help you evaluate your own skills and maybe
|
||||
put some more realistic limits on how fast you can achieve your goals.
|
||||
|
||||
> The questions in this lesson do not really apply to our tutorial game since we know we are doing it
|
||||
> to learn Evennia. If you just want to follow along with the technical bits you can skip this lesson and
|
||||
> come back later when you feel ready to take on making your own game.
|
||||
|
||||
## What is your motivation for doing this?
|
||||
|
||||
So you want to make a game. First you need to make a few things clear to yourself.
|
||||
|
||||
Making a multiplayer online game is a _big_ undertaking. You will (if you are like most of us) be
|
||||
doing it as a hobby, without getting paid. And you’ll be doing it for a long time.
|
||||
|
||||
So the very first thing you should ask yourself (and your team, if you have any) is
|
||||
_why am I doing this_? Do some soul-searching here. Here are some possible answers:
|
||||
|
||||
- I want to earn recognition and fame from my online community and/or among my friends.
|
||||
- I want to build the game so I can play and enjoy it myself.
|
||||
- I want to build the same game I already play but without the bad people.
|
||||
- I want to create a game so that I can control it and be the head honcho.
|
||||
- A friend or online acquaintance talked me into working on it.
|
||||
- I work on this because I’m paid to (wow!)
|
||||
- I only build this for my own benefit or to see if I can pull it off.
|
||||
- I want to create something to give back to the community I love.
|
||||
- I want to use this project as a stepping-stone towards other projects (like a career in game design
|
||||
or programming).
|
||||
- I am interested in coding or server and network architectures, making a MUD just seems to be a good
|
||||
way to teach myself.
|
||||
- I want to build a commercial game and earn money.
|
||||
- I want to fulfill a life-long dream of game making.
|
||||
|
||||
There are many other possibilities. How “solid” your answer is for a long-term development project
|
||||
is up to you. The important point is that you ask yourself the question.
|
||||
|
||||
**Help someone else instead** - Maybe you should _not_ start a new project - maybe you're better off
|
||||
helping someone else or improve on something that already exists. Or maybe you find you are more of a
|
||||
game engine developer than a game designer.
|
||||
|
||||
**Driven by emotion** - Some answers may suggest that you are driven by emotions of revenge or disconcert. Be careful with that and
|
||||
check so that's not your _only_ driving force. Those emotions may have abated later when the project
|
||||
most needs your enthusiasm and motivation.
|
||||
|
||||
**Going commercial** - If your aim is to earn money, your design goals will likely be very different from
|
||||
those of a person who only creates as a hobby or for their own benefit. You may also have a much stricter
|
||||
timeline for release.
|
||||
|
||||
Whichever your motivation, you should at least have it clear in your own mind. It’s worth to make
|
||||
sure your eventual team is on the same page too.
|
||||
|
||||
## What are your skills?
|
||||
|
||||
Once you have your motivations straight you need to take a stock of your own skills and the skills
|
||||
available in your team, if you have any.
|
||||
|
||||
Your game will have two principal components and you will need skills to cater for both:
|
||||
|
||||
- The game engine / code base - Evennia in this case.
|
||||
- The assets created for using the game engine (“the game world”)
|
||||
|
||||
### The game engine
|
||||
|
||||
The game engine is maintained and modified by programmers (coders). It represents the infrastructure
|
||||
that runs the game - the network code, the protocol support, the handling of commands, scripting and
|
||||
data storage.
|
||||
|
||||
If you are just evaluating Evennia, it's worth to do the following:
|
||||
|
||||
- Hang out in the community/forums/chat. Expect to need to ask a lot of “stupid” questions as you start
|
||||
developing (hint: no question is stupid). Is this a community in which you would feel comfortable doing so?
|
||||
- Keep tabs on the manual (you're already here).
|
||||
- How's your Python skills? What are the skills in your team? Do you or your team already know it or are
|
||||
you willing to learn? Learning the language as you go is not too unusual with Evennia devs, but expect it
|
||||
to add development time. You will also be worse at predicting how 'hard' something is to do.
|
||||
- If you don’t know Python, you should have gotten a few tastes from the first part of this tutorial. But
|
||||
expect to have to refer to external online tutorials - there are many details of Python that will not be
|
||||
covered.
|
||||
|
||||
### Asset creation
|
||||
|
||||
Compared to the level of work needed to produce professional graphics for an MMORPG, detailed text
|
||||
assets for a mud are cheap to create. This is one of the many reasons muds are so well suited for a
|
||||
small team.
|
||||
|
||||
This is not to say that making “professional” text content is easy though. Knowing how to write
|
||||
imaginative and grammatically correct prose is only the minimal starting requirement. A good asset-
|
||||
creator (traditionally called a “builder”) must also be able to utilize the tools of the game engine
|
||||
to its fullest in order to script events, make quests, triggers and interactive, interesting
|
||||
environments.
|
||||
|
||||
Assuming you are not coding all alone, your team’s in-house builders will be the first ones to actually
|
||||
“use” your game framework and build tools. They will stumble on all the bugs. This means that you
|
||||
need people who are just not “artsy” or “good with words”. Assuming coders and builders are not the
|
||||
same people (common for early testing), builders need to be able to collaborate well and give clear
|
||||
and concise feedback.
|
||||
|
||||
If you know your builders are not tech-savvy, you may need to spend more time making easier
|
||||
build-tools and commands for them.
|
||||
|
||||
## So, where do I begin, then?
|
||||
|
||||
Right, after all this soul-searching and skill-inventory-checking, let’s go back to the original
|
||||
question. And maybe you’ll find that you have a better feeling for the answer yourself already:
|
||||
|
||||
- Keep following this tutorial and spend the time
|
||||
to really understand what is happening in the examples. Not only will this give you a better idea
|
||||
of how parts hang together, it may also give you ideas for what is possible. Maybe something
|
||||
is easier than you expected!
|
||||
- Introduce yourself in the IRC/Discord chat and don't be shy to ask questions as you go through
|
||||
the tutorial. Don't get hung up on trying to resolve something that a seasoned Evennia dev may
|
||||
clear up for you in five minutes. Also, not all errors are your faults - it's possible the
|
||||
tutorial is unclear or has bugs, asking will quickly bring those problems to light, if so.
|
||||
- If Python is new to you, you should complement the tutorial with third-party Python references
|
||||
so you can read, understand and replicate example code without being completely in the dark.
|
||||
|
||||
Once you are out of the starting tutorial, you'll be off to do your own thing.
|
||||
|
||||
- The starting tutorial cannot cover everything. Skim through the [Evennia docs](../../../index.md).
|
||||
Even if you don't read everything, it gives you a feeling for what's available should you need
|
||||
to look for something later. Make sure to use the search function.
|
||||
- You can now start by expanding on the tutorial-game we will have created. In the last part there
|
||||
there will be a list of possible future projects you could take on. Working on your own, without help
|
||||
from a tutorial is the next step.
|
||||
|
||||
As for your builders, they can start getting familiar with Evennia's default build commands ... but
|
||||
keep in mind that your game is not yet built! Don't set your builders off on creating large zone projects.
|
||||
If they build anything at all, it should be small test areas to agree on a homogenous form, mood
|
||||
and literary style.
|
||||
|
||||
## Conclusions
|
||||
|
||||
Remember that what kills a hobby game project will usually be your own lack of
|
||||
motivation. So do whatever you can to keep that motivation burning strong! Even if it means
|
||||
deviating from what you read in a tutorial like this one. Just get that game out there, whichever way
|
||||
works best for you.
|
||||
|
||||
In the next lesson we'll go through some of the technical questions you need to consider. This should
|
||||
hopefully help you figure out more about the game you want to make. In the lesson following that we'll
|
||||
then try to answer those questions for the sake of creating our little tutorial game.
|
||||
802
docs/source/Howtos/Beginner-Tutorial/Part3/A-Sittable-Object.md
Normal file
802
docs/source/Howtos/Beginner-Tutorial/Part3/A-Sittable-Object.md
Normal file
|
|
@ -0,0 +1,802 @@
|
|||
[prev lesson](../../../Unimplemented.md) | [next lesson](../../../Unimplemented.md)
|
||||
|
||||
# Making a sittable object
|
||||
|
||||
In this lesson we will go through how to make a chair you can sit on. Sounds easy, right?
|
||||
Well it is. But in the process of making the chair we will need to consider the various ways
|
||||
to do it depending on how we want our game to work.
|
||||
|
||||
The goals of this lesson are as follows:
|
||||
|
||||
- We want a new 'sittable' object, a Chair in particular".
|
||||
- We want to be able to use a command to sit in the chair.
|
||||
- Once we are sitting in the chair it should affect us somehow. To demonstrate this we'll
|
||||
set a flag "Resting" on the Character sitting in the Chair.
|
||||
- When you sit down you should not be able to walk to another room without first standing up.
|
||||
- A character should be able to stand up and move away from the chair.
|
||||
|
||||
There are two main ways to design the commands for sitting and standing up.
|
||||
- You can store the commands on the chair so they are only available when a chair is in the room
|
||||
- You can store the commands on the Character so they are always available and you must always specify
|
||||
which chair to sit on.
|
||||
|
||||
Both of these are very useful to know about, so in this lesson we'll try both. But first
|
||||
we need to handle some basics.
|
||||
|
||||
|
||||
## Don't move us when resting
|
||||
|
||||
When you are sitting in a chair you can't just walk off without first standing up.
|
||||
This requires a change to our Character typeclass. Open `mygame/typeclasses/characters.py`:
|
||||
|
||||
```python
|
||||
|
||||
# ...
|
||||
|
||||
class Character(DefaultCharacter):
|
||||
# ...
|
||||
|
||||
def at_pre_move(self, destination):
|
||||
"""
|
||||
Called by self.move_to when trying to move somewhere. If this returns
|
||||
False, the move is immediately cancelled.
|
||||
"""
|
||||
if self.db.is_resting:
|
||||
self.msg("You can't go anywhere while resting.")
|
||||
return False
|
||||
return True
|
||||
|
||||
```
|
||||
|
||||
When moving somewhere, [character.move_to](evennia.objects.objects.DefaultObject.move_to) is called. This in turn
|
||||
will call `character.at_pre_move`. Here we look for an Attribute `is_resting` (which we will assign below)
|
||||
to determine if we are stuck on the chair or not.
|
||||
|
||||
## Making the Chair itself
|
||||
|
||||
Next we need the Chair itself, or rather a whole family of "things you can sit on" that we will call
|
||||
_sittables_. We can't just use a default Object since we want a sittable to contain some custom code. We need
|
||||
a new, custom Typeclass. Create a new module `mygame/typeclasses/sittables.py` with the following content:
|
||||
|
||||
```python
|
||||
|
||||
from evennia import DefaultObject
|
||||
|
||||
class Sittable(DefaultObject):
|
||||
|
||||
def at_object_creation(self):
|
||||
self.db.sitter = None
|
||||
|
||||
def do_sit(self, sitter):
|
||||
"""
|
||||
Called when trying to sit on/in this object.
|
||||
|
||||
Args:
|
||||
sitter (Object): The one trying to sit down.
|
||||
|
||||
"""
|
||||
current = self.db.sitter
|
||||
if current:
|
||||
if current == sitter:
|
||||
sitter.msg("You are already sitting on {self.key}.")
|
||||
else:
|
||||
sitter.msg(f"You can't sit on {self.key} "
|
||||
f"- {current.key} is already sitting there!")
|
||||
return
|
||||
self.db.sitting = sitter
|
||||
sitter.db.is_resting = True
|
||||
sitter.msg(f"You sit on {self.key}")
|
||||
|
||||
def do_stand(self, stander):
|
||||
"""
|
||||
Called when trying to stand from this object.
|
||||
|
||||
Args:
|
||||
stander (Object): The one trying to stand up.
|
||||
|
||||
"""
|
||||
current = self.db.sitter
|
||||
if not stander == current:
|
||||
stander.msg(f"You are not sitting on {self.key}.")
|
||||
else:
|
||||
self.db.sitting = None
|
||||
stander.db.is_resting = False
|
||||
stander.msg(f"You stand up from {self.key}")
|
||||
```
|
||||
|
||||
Here we have a small Typeclass that handles someone trying to sit on it. It has two methods that we can simply
|
||||
call from a Command later. We set the `is_resting` Attribute on the one sitting down.
|
||||
|
||||
One could imagine that one could have the future `sit` command check if someone is already sitting in the
|
||||
chair instead. This would work too, but letting the `Sittable` class handle the logic around who can sit on it makes
|
||||
logical sense.
|
||||
|
||||
We let the typeclass handle the logic, and also let it do all the return messaging. This makes it easy to churn out
|
||||
a bunch of chairs for people to sit on. But it's not perfect. The `Sittable` class is general. What if you want to
|
||||
make an armchair. You sit "in" an armchair rather than "on" it. We _could_ make a child class of `Sittable` named
|
||||
`SittableIn` that makes this change, but that feels excessive. Instead we will make it so that Sittables can
|
||||
modify this per-instance:
|
||||
|
||||
|
||||
```python
|
||||
|
||||
from evennia import DefaultObject
|
||||
|
||||
class Sittable(DefaultObject):
|
||||
|
||||
def at_object_creation(self):
|
||||
self.db.sitter = None
|
||||
# do you sit "on" or "in" this object?
|
||||
self.db.adjective = "on"
|
||||
|
||||
def do_sit(self, sitter):
|
||||
"""
|
||||
Called when trying to sit on/in this object.
|
||||
|
||||
Args:
|
||||
sitter (Object): The one trying to sit down.
|
||||
|
||||
"""
|
||||
adjective = self.db.adjective
|
||||
current = self.db.sitter
|
||||
if current:
|
||||
if current == sitter:
|
||||
sitter.msg(f"You are already sitting {adjective} {self.key}.")
|
||||
else:
|
||||
sitter.msg(
|
||||
f"You can't sit {adjective} {self.key} "
|
||||
f"- {current.key} is already sitting there!")
|
||||
return
|
||||
self.db.sitting = sitter
|
||||
sitter.db.is_resting = True
|
||||
sitter.msg(f"You sit {adjective} {self.key}")
|
||||
|
||||
def do_stand(self, stander):
|
||||
"""
|
||||
Called when trying to stand from this object.
|
||||
|
||||
Args:
|
||||
stander (Object): The one trying to stand up.
|
||||
|
||||
"""
|
||||
current = self.db.sitter
|
||||
if not stander == current:
|
||||
stander.msg(f"You are not sitting {self.db.adjective} {self.key}.")
|
||||
else:
|
||||
self.db.sitting = None
|
||||
stander.db.is_resting = False
|
||||
stander.msg(f"You stand up from {self.key}")
|
||||
```
|
||||
|
||||
We added a new Attribute `adjective` which will probably usually be `in` or `on` but could also be `at` if you
|
||||
want to be able to sit _at a desk_ for example. A regular builder would use it like this:
|
||||
|
||||
> create/drop armchair : sittables.Sittable
|
||||
> set armchair/adjective = in
|
||||
|
||||
This is probably enough. But all those strings are hard-coded. What if we want some more dramatic flair when you
|
||||
sit down?
|
||||
|
||||
You sit down and a whoopie cushion makes a loud fart noise!
|
||||
|
||||
For this we need to allow some further customization. Let's let the current strings be defaults that
|
||||
we can replace.
|
||||
|
||||
```python
|
||||
|
||||
from evennia import DefaultObject
|
||||
|
||||
class Sittable(DefaultObject):
|
||||
"""
|
||||
An object one can sit on
|
||||
|
||||
Customizable Attributes:
|
||||
adjective: How to sit (on, in, at etc)
|
||||
Return messages (set as Attributes):
|
||||
msg_already_sitting: Already sitting here
|
||||
format tokens {adjective} and {key}
|
||||
msg_other_sitting: Someone else is sitting here.
|
||||
format tokens {adjective}, {key} and {other}
|
||||
msg_sitting_down: Successfully sit down
|
||||
format tokens {adjective}, {key}
|
||||
msg_standing_fail: Fail to stand because not sitting.
|
||||
format tokens {adjective}, {key}
|
||||
msg_standing_up: Successfully stand up
|
||||
format tokens {adjective}, {key}
|
||||
|
||||
"""
|
||||
def at_object_creation(self):
|
||||
self.db.sitter = None
|
||||
# do you sit "on" or "in" this object?
|
||||
self.db.adjective = "on"
|
||||
|
||||
def do_sit(self, sitter):
|
||||
"""
|
||||
Called when trying to sit on/in this object.
|
||||
|
||||
Args:
|
||||
sitter (Object): The one trying to sit down.
|
||||
|
||||
"""
|
||||
adjective = self.db.adjective
|
||||
current = self.db.sitter
|
||||
if current:
|
||||
if current == sitter:
|
||||
if self.db.msg_already_sitting:
|
||||
sitter.msg(
|
||||
self.db.msg_already_sitting.format(
|
||||
adjective=self.db.adjective, key=self.key))
|
||||
else:
|
||||
sitter.msg(f"You are already sitting {adjective} {self.key}.")
|
||||
else:
|
||||
if self.db.msg_other_sitting:
|
||||
sitter.msg(self.db.msg_already_sitting.format(
|
||||
other=current.key, adjective=self.db.adjective, key=self.key))
|
||||
else:
|
||||
sitter.msg(f"You can't sit {adjective} {self.key} "
|
||||
f"- {current.key} is already sitting there!")
|
||||
return
|
||||
self.db.sitting = sitter
|
||||
sitter.db.is_resting = True
|
||||
if self.db.msg_sitting_down:
|
||||
sitter.msg(self.db.msg_sitting_down.format(adjective=adjective, key=self.key))
|
||||
else:
|
||||
sitter.msg(f"You sit {adjective} {self.key}")
|
||||
|
||||
def do_stand(self, stander):
|
||||
"""
|
||||
Called when trying to stand from this object.
|
||||
|
||||
Args:
|
||||
stander (Object): The one trying to stand up.
|
||||
|
||||
"""
|
||||
current = self.db.sitter
|
||||
if not stander == current:
|
||||
if self.db.msg_standing_fail:
|
||||
stander.msg(self.db.msg_standing_fail.format(
|
||||
adjective=self.db.adjective, key=self.key))
|
||||
else:
|
||||
stander.msg(f"You are not sitting {self.db.adjective} {self.key}")
|
||||
else:
|
||||
self.db.sitting = None
|
||||
stander.db.is_resting = False
|
||||
if self.db.msg_standing_up:
|
||||
stander.msg(self.db.msg_standing_up.format(
|
||||
adjective=self.db.adjective, key=self.key))
|
||||
else:
|
||||
stander.msg(f"You stand up from {self.key}")
|
||||
```
|
||||
|
||||
Here we really went all out with flexibility. If you need this much is up to you.
|
||||
We added a bunch of optional Attributes to hold alternative versions of all the messages.
|
||||
There are some things to note:
|
||||
|
||||
- We don't actually initiate those Attributes in `at_object_creation`. This is a simple
|
||||
optimization. The assumption is that _most_ chairs will probably not be this customized.
|
||||
So initiating a bunch of Attributes to, say, empty strings would be a lot of useless database calls.
|
||||
The drawback is that the available Attributes become less visible when reading the code. So we add a long
|
||||
describing docstring to the end to explain all you can use.
|
||||
- We use `.format` to inject formatting-tokens in the text. The good thing about such formatting
|
||||
markers is that they are _optional_. They are there if you want them, but Python will not complain
|
||||
if you don't include some or any of them. Let's see an example:
|
||||
|
||||
> reload # if you have new code
|
||||
> create/drop armchair : sittables.Sittable
|
||||
> set armchair/adjective = in
|
||||
> set armchair/msg_sitting_down = As you sit down {adjective} {key}, life feels easier.
|
||||
> set armchair/msg_standing_up = You stand up from {key}. Life resumes.
|
||||
|
||||
The `{key}` and `{adjective}` are examples of optional formatting markers. Whenever the message is
|
||||
returned, the format-tokens within will be replaced with `armchair` and `in` respectively. Should we
|
||||
rename the chair later, this will show in the messages automatically (since `{key}` will change).
|
||||
|
||||
We have no Command to use this chair yet. But we can try it out with `py`:
|
||||
|
||||
> py self.search("armchair").do_sit(self)
|
||||
As you sit down in armchair, life feels easier.
|
||||
> self.db.resting
|
||||
True
|
||||
> py self.search("armchair").do_stand(self)
|
||||
You stand up from armchair. Life resumes
|
||||
> self.db.resting
|
||||
False
|
||||
|
||||
If you follow along and get a result like this, all seems to be working well!
|
||||
|
||||
## Command variant 1: Commands on the chair
|
||||
|
||||
This way to implement `sit` and `stand` puts new cmdsets on the Sittable itself.
|
||||
As we've learned before, commands on objects are made available to others in the room.
|
||||
This makes the command easy but instead adds some complexity in the management of the CmdSet.
|
||||
|
||||
This is how it will look if `armchair` is in the room:
|
||||
|
||||
> sit
|
||||
As you sit down in armchair, life feels easier.
|
||||
|
||||
What happens if there are sittables `sofa` and `barstool` also in the room? Evennia will automatically
|
||||
handle this for us and allow us to specify which one we want:
|
||||
|
||||
> sit
|
||||
More than one match for 'sit' (please narrow target):
|
||||
sit-1 (armchair)
|
||||
sit-2 (sofa)
|
||||
sit-3 (barstool)
|
||||
> sit-1
|
||||
As you sit down in armchair, life feels easier.
|
||||
|
||||
To keep things separate we'll make a new module `mygame/commands/sittables.py`:
|
||||
|
||||
```{sidebar} Separate Commands and Typeclasses?
|
||||
|
||||
You can organize these things as you like. If you wanted you could put the sit-command + cmdset
|
||||
together with the `Sittable` typeclass in `mygame/typeclasses/sittables.py`. That has the advantage of
|
||||
keeping everything related to sitting in one place. But there is also some organizational merit to
|
||||
keeping all Commands in one place as we do here.
|
||||
|
||||
```
|
||||
|
||||
```python
|
||||
from evennia import Command, CmdSet
|
||||
|
||||
class CmdSit(Command):
|
||||
"""
|
||||
Sit down.
|
||||
"""
|
||||
key = "sit"
|
||||
|
||||
def func(self):
|
||||
self.obj.do_sit(self.caller)
|
||||
|
||||
class CmdStand(Command):
|
||||
"""
|
||||
Stand up.
|
||||
"""
|
||||
key = "stand"
|
||||
def func(self):
|
||||
self.obj.do_stand(self.caller)
|
||||
|
||||
|
||||
class CmdSetSit(CmdSet):
|
||||
priority = 1
|
||||
def at_cmdset_creation(self):
|
||||
self.add(CmdSit)
|
||||
self.add(CmdStand)
|
||||
|
||||
```
|
||||
|
||||
As seen, the commands are nearly trivial. `self.obj` is the object to which we added the cmdset with this
|
||||
Command (so for example a chair). We just call the `do_sit/stand` on that object and the `Sittable` will
|
||||
do the rest.
|
||||
|
||||
Why that `priority = 1` on `CmdSetSit`? This makes same-named Commands from this cmdset merge with a bit higher
|
||||
priority than Commands from the Character-cmdset. Why this is a good idea will become clear shortly.
|
||||
|
||||
We also need to make a change to our `Sittable` typeclass. Open `mygame/typeclasses/sittables.py`:
|
||||
|
||||
```python
|
||||
from evennia import DefaultObject
|
||||
from commands.sittables import CmdSetSit # <- new
|
||||
|
||||
class Sittable(DefaultObject):
|
||||
"""
|
||||
(docstring)
|
||||
"""
|
||||
def at_object_creation(self):
|
||||
|
||||
self.db.sitter = None
|
||||
# do you sit "on" or "in" this object?
|
||||
self.db.adjective = "on"
|
||||
self.cmdset.add_default(CmdSetSit) # <- new
|
||||
```
|
||||
|
||||
Any _new_ Sittables will now have your `sit` Command. Your existing `armchair` will not,
|
||||
since `at_object_creation` will not re-run for already existing objects. We can update it manually:
|
||||
|
||||
> reload
|
||||
> update armchair
|
||||
|
||||
We could also update all existing sittables (all on one line):
|
||||
|
||||
> py from typeclasses.sittables import Sittable ;
|
||||
[sittable.at_object_creation() for sittable in Sittable.objects.all()]
|
||||
|
||||
> The above shows an example of a _list comprehension_. Think of it as an efficient way to construct a new list
|
||||
all in one line. You can read more about list comprehensions
|
||||
[here in the Python docs](https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions).
|
||||
|
||||
We should now be able to use `sit` while in the room with the armchair.
|
||||
|
||||
> sit
|
||||
As you sit down in armchair, life feels easier.
|
||||
> stand
|
||||
You stand up from armchair.
|
||||
|
||||
One issue with placing the `sit` (or `stand`) Command "on" the chair is that it will not be available when in a
|
||||
room without a Sittable object:
|
||||
|
||||
> sit
|
||||
Command 'sit' is not available. ...
|
||||
|
||||
This is practical but not so good-looking; it makes it harder for the user to know a `sit` action is at all
|
||||
possible. Here is a trick for fixing this. Let's add _another_ Command to the bottom
|
||||
of `mygame/commands/sittables.py`:
|
||||
|
||||
```python
|
||||
# ...
|
||||
|
||||
class CmdNoSitStand(Command):
|
||||
"""
|
||||
Sit down or Stand up
|
||||
"""
|
||||
key = "sit"
|
||||
aliases = ["stand"]
|
||||
|
||||
def func(self):
|
||||
if self.cmdname == "sit":
|
||||
self.msg("You have nothing to sit on.")
|
||||
else:
|
||||
self.msg("You are not sitting down.")
|
||||
|
||||
```
|
||||
|
||||
Here we have a Command that is actually two - it will answer to both `sit` and `stand` since we
|
||||
added `stand` to its `aliases`. In the command we look at `self.cmdname`, which is the string
|
||||
_actually used_ to call this command. We use this to return different messages.
|
||||
|
||||
We don't need a separate CmdSet for this, instead we will add this
|
||||
to the default Character cmdset. Open `mygame/commands/default_cmdsets.py`:
|
||||
|
||||
```python
|
||||
# ...
|
||||
from commands import sittables
|
||||
|
||||
class CharacterCmdSet(CmdSet):
|
||||
"""
|
||||
(docstring)
|
||||
"""
|
||||
def at_cmdset_creation(self):
|
||||
# ...
|
||||
self.add(sittables.CmdNoSitStand)
|
||||
|
||||
```
|
||||
|
||||
To test we'll build a new location without any comfy armchairs and go there:
|
||||
|
||||
> reload
|
||||
> tunnel n = kitchen
|
||||
north
|
||||
> sit
|
||||
You have nothing to sit on.
|
||||
> south
|
||||
sit
|
||||
As you sit down in armchair, life feels easier.
|
||||
|
||||
We now have a fully functioning `sit` action that is contained with the chair itself. When no chair is around, a
|
||||
default error message is shown.
|
||||
|
||||
How does this work? There are two cmdsets at play, both of which have a `sit` Command. As you may remember we
|
||||
set the chair's cmdset to `priority = 1`. This is where that matters. The default Character cmdset has a
|
||||
priority of 0. This means that whenever we enter a room with a Sittable thing, the `sit` command
|
||||
from _its_ cmdset will take _precedence_ over the Character cmdset's version. So we are actually picking
|
||||
_different_ `sit` commands depending on circumstance! The user will never be the wiser.
|
||||
|
||||
So this handles `sit`. What about `stand`? That will work just fine:
|
||||
|
||||
> stand
|
||||
You stand up from armchair.
|
||||
> north
|
||||
> stand
|
||||
You are not sitting down.
|
||||
|
||||
We have one remaining problem with `stand` though - what happens when you are sitting down and try to
|
||||
`stand` in a room with more than one chair:
|
||||
|
||||
> stand
|
||||
More than one match for 'stand' (please narrow target):
|
||||
stand-1 (armchair)
|
||||
stand-2 (sofa)
|
||||
stand-3 (barstool)
|
||||
|
||||
Since all the sittables have the `stand` Command on them, you'll get a multi-match error. This _works_ ... but
|
||||
you could pick _any_ of those sittables to "stand up from". That's really weird and non-intuitive. With `sit` it
|
||||
was okay to get a choice - Evennia can't know which chair we intended to sit on. But we know which chair we
|
||||
sit on so we should only get _its_ `stand` command.
|
||||
|
||||
We will fix this with a `lock` and a custom `lock function`. We want a lock on the `stand` Command that only
|
||||
makes it available when the caller is actually sitting on the chair the `stand` command is on.
|
||||
|
||||
First let's add the lock so we see what we want. Open `mygame/commands/sittables.py`:
|
||||
|
||||
```python
|
||||
# ...
|
||||
|
||||
class CmdStand(Command):
|
||||
"""
|
||||
Stand up.
|
||||
"""
|
||||
key = "stand"
|
||||
lock = "cmd:sitsonthis()" # < this is new
|
||||
|
||||
def func(self):
|
||||
self.obj.do_stand(self.caller)
|
||||
# ...
|
||||
```
|
||||
|
||||
We define a [Lock](../../../Components/Locks.md) on the command. The `cmd:` is in what situation Evennia will check
|
||||
the lock. The `cmd` means that it will check the lock when determining if a user has access to this command or not.
|
||||
What will be checked is the `sitsonthis` _lock function_ which doesn't exist yet.
|
||||
|
||||
Open `mygame/server/conf/lockfuncs.py` to add it!
|
||||
|
||||
```python
|
||||
"""
|
||||
(module lockstring)
|
||||
"""
|
||||
# ...
|
||||
|
||||
def sitsonthis(accessing_obj, accessed_obj, *args, **kwargs):
|
||||
"""
|
||||
True if accessing_obj is sitting on/in the accessed_obj.
|
||||
"""
|
||||
return accessed_obj.db.sitting == accessing_obj
|
||||
|
||||
# ...
|
||||
```
|
||||
|
||||
Evennia knows that all functions in `mygame/server/conf/lockfuncs` should be possible to use in a lock definition.
|
||||
The arguments are required and Evennia will pass all relevant objects to them:
|
||||
|
||||
```{sidebar} Lockfuncs
|
||||
|
||||
Evennia provides a large number of default lockfuncs, such as checking permission-levels,
|
||||
if you are carrying or are inside the accessed object etc. There is no concept of 'sitting'
|
||||
in default Evennia however, so this we need to specify ourselves.
|
||||
|
||||
```
|
||||
|
||||
- `accessing_obj` is the one trying to access the lock. So us, in this case.
|
||||
- `accessed_obj` is the entity we are trying to gain a particular type of access to. So the chair.
|
||||
- `args` is a tuple holding any arguments passed to the lockfunc. Since we use `sitsondthis()` this will
|
||||
be empty (and if we add anything, it will be ignored).
|
||||
- `kwargs` is a tuple of keyword arguments passed to the lockfuncs. This will be empty as well in our example.
|
||||
|
||||
If you are superuser, it's important that you `quell` yourself before trying this out. This is because the superuser
|
||||
bypasses all locks - it can never get locked out, but it means it will also not see the effects of a lock like this.
|
||||
|
||||
> reload
|
||||
> quell
|
||||
> stand
|
||||
You stand up from armchair
|
||||
|
||||
None of the other sittables' `stand` commands passed the lock and only the one we are actually sitting on did.
|
||||
|
||||
Adding a Command to the chair object like this is powerful and a good technique to know. It does come with some
|
||||
caveats though that one needs to keep in mind.
|
||||
|
||||
We'll now try another way to add the `sit/stand` commands.
|
||||
|
||||
## Command variant 2: Command on Character
|
||||
|
||||
Before we start with this, delete the chairs you've created (`del armchair` etc) and then do the following
|
||||
changes:
|
||||
|
||||
- In `mygame/typeclasses/sittables.py`, comment out the line `self.cmdset.add_default(CmdSetSit)`.
|
||||
- In `mygame/commands/default_cmdsets.py`, comment out the line `self.add(sittables.CmdNoSitStand)`.
|
||||
|
||||
This disables the on-object command solution so we can try an alternative. Make sure to `reload` so the
|
||||
changes are known to Evennia.
|
||||
|
||||
In this variation we will put the `sit` and `stand` commands on the `Character` instead of on the chair. This
|
||||
makes some things easier, but makes the Commands themselves more complex because they will not know which
|
||||
chair to sit on. We can't just do `sit` anymore. This is how it will work.
|
||||
|
||||
> sit <chair>
|
||||
You sit on chair.
|
||||
> stand
|
||||
You stand up from chair.
|
||||
|
||||
Open `mygame/commands.sittables.py` again. We'll add a new sit-command. We name the class `CmdSit2` since
|
||||
we already have `CmdSit` from the previous example. We put everything at the end of the module to
|
||||
keep it separate.
|
||||
|
||||
```python
|
||||
from evennia import Command, CmdSet
|
||||
from evennia import InterruptCommand # <- this is new
|
||||
|
||||
class CmdSit(Command):
|
||||
# ...
|
||||
|
||||
# ...
|
||||
|
||||
# new from here
|
||||
|
||||
class CmdSit2(Command):
|
||||
"""
|
||||
Sit down.
|
||||
|
||||
Usage:
|
||||
sit <sittable>
|
||||
|
||||
"""
|
||||
key = "sit"
|
||||
|
||||
def parse(self):
|
||||
self.args = self.args.strip()
|
||||
if not self.args:
|
||||
self.caller.msg("Sit on what?")
|
||||
raise InterruptCommand
|
||||
|
||||
def func(self):
|
||||
|
||||
# self.search handles all error messages etc.
|
||||
sittable = self.caller.search(self.args)
|
||||
if not sittable:
|
||||
return
|
||||
try:
|
||||
sittable.do_sit(self.caller)
|
||||
except AttributeError:
|
||||
self.caller.msg("You can't sit on that!")
|
||||
|
||||
```
|
||||
|
||||
With this Command-variation we need to search for the sittable. A series of methods on the Command
|
||||
are run in sequence:
|
||||
|
||||
1. `Command.at_pre_command` - this is not used by default
|
||||
2. `Command.parse` - this should parse the input
|
||||
3. `Command.func` - this should implement the actual Command functionality
|
||||
4. `Command.at_post_func` - this is not used by default
|
||||
|
||||
So if we just `return` in `.parse`, `.func` will still run, which is not what we want. To immediately
|
||||
abort this sequence we need to `raise InterruptCommand`.
|
||||
|
||||
```{sidebar} Raising exceptions
|
||||
|
||||
Raising an exception allows for immediately interrupting the current program flow. Python
|
||||
automatically raises error-exceptions when detecting problems with the code. It will be
|
||||
raised up through the sequence of called code (the 'stack') until it's either `caught` with
|
||||
a `try ... except` or reaches the outermost scope where it'll be logged or displayed.
|
||||
|
||||
```
|
||||
|
||||
`InterruptCommand` is an _exception_ that the Command-system catches with the understanding that we want
|
||||
to do a clean abort. In the `.parse` method we strip any whitespaces from the argument and
|
||||
sure there actuall _is_ an argument. We abort immediately if there isn't.
|
||||
|
||||
We we get to `.func` at all, we know that we have an argument. We search for this and abort if we there was
|
||||
a problem finding the target.
|
||||
|
||||
> We could have done `raise InterruptCommand` in `.func` as well, but `return` is a little shorter to write
|
||||
> and there is no harm done if `at_post_func` runs since it's empty.
|
||||
|
||||
Next we call the found sittable's `do_sit` method. Note that we wrap this call like this:
|
||||
|
||||
```python
|
||||
|
||||
try:
|
||||
# code
|
||||
except AttributeError:
|
||||
# stuff to do if AttributeError exception was raised
|
||||
```
|
||||
|
||||
The reason is that `caller.search` has no idea we are looking for a Sittable. The user could have tried
|
||||
`sit wall` or `sit sword`. These don't have a `do_sit` method _but we call it anyway and handle the error_.
|
||||
This is a very "Pythonic" thing to do. The concept is often called "leap before you look" or "it's easier to
|
||||
ask for forgiveness than for permission". If `sittable.do_sit` does not exist, Python will raise an `AttributeError`.
|
||||
We catch this with `try ... except AttributeError` and convert it to a proper error message.
|
||||
|
||||
While it's useful to learn about `try ... except`, there is also a way to leverage Evennia to do this without
|
||||
`try ... except`:
|
||||
|
||||
```python
|
||||
|
||||
# ...
|
||||
|
||||
def func(self):
|
||||
|
||||
# self.search handles all error messages etc.
|
||||
sittable = self.caller.search(
|
||||
self.args,
|
||||
typeclass="typeclasses.sittables.Sittable")
|
||||
if not sittable:
|
||||
return
|
||||
sittable.do_sit(self.caller)
|
||||
```
|
||||
|
||||
```{sidebar} Continuing across multiple lines
|
||||
|
||||
Note how the `.search()` method's arguments are spread out over multiple
|
||||
lines. This works for all lists, tuples and other listings and is
|
||||
a good way to avoid very long and hard-to-read lines.
|
||||
|
||||
```
|
||||
|
||||
The `caller.search` method has an keyword argument `typeclass` that can take either a python-path to a
|
||||
typeclass, the typeclass itself, or a list of either to widen the allowed options. In this case we know
|
||||
for sure that the `sittable` we get is actually a `Sittable` class and we can call `sittable.do_sit` without
|
||||
needing to worry about catching errors.
|
||||
|
||||
Let's do the `stand` command while we are at it. Again, since the Command is external to the chair we don't
|
||||
know which object we are sitting in and have to search for it.
|
||||
|
||||
```python
|
||||
|
||||
class CmdStand2(Command):
|
||||
"""
|
||||
Stand up.
|
||||
|
||||
Usage:
|
||||
stand
|
||||
|
||||
"""
|
||||
key = "stand"
|
||||
|
||||
def func(self):
|
||||
|
||||
caller = self.caller
|
||||
# find the thing we are sitting on/in, by finding the object
|
||||
# in the current location that as an Attribute "sitter" set
|
||||
# to the caller
|
||||
sittable = caller.search(
|
||||
caller,
|
||||
candidates=caller.location.contents,
|
||||
attribute_name="sitter",
|
||||
typeclass="typeclasses.sittables.Sittable")
|
||||
# if this is None, the error was already reported to user
|
||||
if not sittable:
|
||||
return
|
||||
|
||||
sittable.do_stand(caller)
|
||||
|
||||
```
|
||||
|
||||
This forced us to to use the full power of the `caller.search` method. If we wanted to search for something
|
||||
more complex we would likely need to break out a [Django query](../Part1/Django-queries.md) to do it. The key here is that
|
||||
we know that the object we are looking for is a `Sittable` and that it must have an Attribute named `sitter`
|
||||
which should be set to us, the one sitting on/in the thing. Once we have that we just call `.do_stand` on it
|
||||
and let the Typeclass handle the rest.
|
||||
|
||||
All that is left now is to make this available to us. This type of Command should be available to us all the time
|
||||
so we can put it in the default Cmdset` on the Character. Open `mygame/default_cmdsets.py`
|
||||
|
||||
|
||||
```python
|
||||
# ...
|
||||
from commands import sittables
|
||||
|
||||
class CharacterCmdSet(CmdSet):
|
||||
"""
|
||||
(docstring)
|
||||
"""
|
||||
def at_cmdset_creation(self):
|
||||
# ...
|
||||
self.add(sittables.CmdSit2)
|
||||
self.add(sittables.CmdStand2)
|
||||
|
||||
```
|
||||
|
||||
Now let's try it out:
|
||||
|
||||
> reload
|
||||
> create/drop sofa : sittables.Sittable
|
||||
> sit sofa
|
||||
You sit down on sofa.
|
||||
> stand
|
||||
You stand up from sofa.
|
||||
|
||||
|
||||
## Conclusions
|
||||
|
||||
In this lesson we accomplished quite a bit:
|
||||
|
||||
- We modified our `Character` class to avoid moving when sitting down.
|
||||
- We made a new `Sittable` typeclass
|
||||
- We tried two ways to allow a user to interact with sittables using `sit` and `stand` commands.
|
||||
|
||||
Eagle-eyed readers will notice that the `stand` command sitting "on" the chair (variant 1) would work just fine
|
||||
together with the `sit` command sitting "on" the Character (variant 2). There is nothing stopping you from
|
||||
mixing them, or even try a third solution that better fits what you have in mind.
|
||||
|
||||
[prev lesson](../../../Unimplemented.md) | [next lesson](../../../Unimplemented.md)
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
# Part 3: How we get there
|
||||
|
||||
```{eval-rst}
|
||||
.. sidebar:: Beginner Tutorial Parts
|
||||
|
||||
`Introduction <../Beginner-Tutorial-Intro.html>`_
|
||||
Getting set up.
|
||||
Part 1: `What we have <../Part1/Beginner-Tutorial-Part1-Intro.html>`_
|
||||
A tour of Evennia and how to use the tools, including an introduction to Python.
|
||||
Part 2: `What we want <../Part2/Beginner-Tutorial-Part2-Intro.html>`_
|
||||
Planning our tutorial game and what to think about when planning your own in the future.
|
||||
**Part 3: How we get there**
|
||||
Getting down to the meat of extending Evennia to make our game
|
||||
Part 4: `Using what we created <../Part4/Beginner-Tutorial-Part4-Intro.html>`_
|
||||
Building a tech-demo and world content to go with our code
|
||||
Part 5: `Showing the world <../Part5/Beginner-Tutorial-Part5-Intro.html>`_
|
||||
Taking our new game online and let players try it out
|
||||
```
|
||||
|
||||
In part three of the Evennia Beginner tutorial we will go through the creation of several key parts of our tutorial
|
||||
game _EvAdventure_. This is a pretty big part with plenty of examples.
|
||||
|
||||
If you followed the previous parts of this tutorial you will have some notions about Python and where to find
|
||||
and make use of things in Evennia. We also have a good idea of the type of game we want.
|
||||
Even if this is not the game-style you are interested in, following along will give you a lot of experience
|
||||
with using Evennia. This be of much use when doing your own thing later.
|
||||
|
||||
|
||||
## Lessons
|
||||
|
||||
_TODO_
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 1
|
||||
|
||||
Implementing-a-game-rule-system
|
||||
Turn-based-Combat-System
|
||||
A-Sittable-Object
|
||||
|
||||
```
|
||||
1. [Changing settings](../../../Unimplemented.md)
|
||||
1. [Applying contribs](../../../Unimplemented.md)
|
||||
1. [Creating a rule module](../../../Unimplemented.md)
|
||||
1. [Tweaking the base Typeclasses](../../../Unimplemented.md)
|
||||
1. [Character creation menu](../../../Unimplemented.md)
|
||||
1. [Wearing armor and wielding weapons](../../../Unimplemented.md)
|
||||
1. [Two types of combat](../../../Unimplemented.md)
|
||||
1. [Monsters and AI](../../../Unimplemented.md)
|
||||
1. [Questing and rewards](../../../Unimplemented.md)
|
||||
1. [Overview of Tech demo](../../../Unimplemented.md)
|
||||
|
||||
|
||||
## Table of Contents
|
||||
|
||||
_TODO_
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 1
|
||||
|
||||
Implementing-a-game-rule-system
|
||||
Turn-Based-Combat-System
|
||||
A-Sittable-Object
|
||||
```
|
||||
|
|
@ -0,0 +1,265 @@
|
|||
# Implementing a game rule system
|
||||
|
||||
|
||||
The simplest way to create an online roleplaying game (at least from a code perspective) is to
|
||||
simply grab a paperback RPG rule book, get a staff of game masters together and start to run scenes
|
||||
with whomever logs in. Game masters can roll their dice in front of their computers and tell the
|
||||
players the results. This is only one step away from a traditional tabletop game and puts heavy
|
||||
demands on the staff - it is unlikely staff will be able to keep up around the clock even if they
|
||||
are very dedicated.
|
||||
|
||||
Many games, even the most roleplay-dedicated, thus tend to allow for players to mediate themselves
|
||||
to some extent. A common way to do this is to introduce *coded systems* - that is, to let the
|
||||
computer do some of the heavy lifting. A basic thing is to add an online dice-roller so everyone can
|
||||
make rolls and make sure noone is cheating. Somewhere at this level you find the most bare-bones
|
||||
roleplaying MUSHes.
|
||||
|
||||
The advantage of a coded system is that as long as the rules are fair the computer is too - it makes
|
||||
no judgement calls and holds no personal grudges (and cannot be accused of holding any). Also, the
|
||||
computer doesn't need to sleep and can always be online regardless of when a player logs on. The
|
||||
drawback is that a coded system is not flexible and won't adapt to the unprogrammed actions human
|
||||
players may come up with in role play. For this reason many roleplay-heavy MUDs do a hybrid
|
||||
variation - they use coded systems for things like combat and skill progression but leave role play
|
||||
to be mostly freeform, overseen by staff game masters.
|
||||
|
||||
Finally, on the other end of the scale are less- or no-roleplay games, where game mechanics (and
|
||||
thus player fairness) is the most important aspect. In such games the only events with in-game value
|
||||
are those resulting from code. Such games are very common and include everything from hack-and-slash
|
||||
MUDs to various tactical simulations.
|
||||
|
||||
So your first decision needs to be just what type of system you are aiming for. This page will try
|
||||
to give some ideas for how to organize the "coded" part of your system, however big that may be.
|
||||
|
||||
## Overall system infrastructure
|
||||
|
||||
We strongly recommend that you code your rule system as stand-alone as possible. That is, don't
|
||||
spread your skill check code, race bonus calculation, die modifiers or what have you all over your
|
||||
game.
|
||||
|
||||
- Put everything you would need to look up in a rule book into a module in `mygame/world`. Hide away
|
||||
as much as you can. Think of it as a black box (or maybe the code representation of an all-knowing
|
||||
game master). The rest of your game will ask this black box questions and get answers back. Exactly
|
||||
how it arrives at those results should not need to be known outside the box. Doing it this way
|
||||
makes it easier to change and update things in one place later.
|
||||
- Store only the minimum stuff you need with each game object. That is, if your Characters need
|
||||
values for Health, a list of skills etc, store those things on the Character - don't store how to
|
||||
roll or change them.
|
||||
- Next is to determine just how you want to store things on your Objects and Characters. You can
|
||||
choose to either store things as individual [Attributes](../../../Components/Attributes.md), like `character.db.STR=34` and
|
||||
`character.db.Hunting_skill=20`. But you could also use some custom storage method, like a
|
||||
dictionary `character.db.skills = {"Hunting":34, "Fishing":20, ...}`. A much more fancy solution is
|
||||
to look at the Ainneve [Trait
|
||||
handler](https://github.com/evennia/ainneve/blob/master/world/traits.py). Finally you could even go
|
||||
with a [custom django model](../../../Concepts/New-Models.md). Which is the better depends on your game and the
|
||||
complexity of your system.
|
||||
- Make a clear [API](https://en.wikipedia.org/wiki/Application_programming_interface) into your
|
||||
rules. That is, make methods/functions that you feed with, say, your Character and which skill you
|
||||
want to check. That is, you want something similar to this:
|
||||
|
||||
```python
|
||||
from world import rules
|
||||
result = rules.roll_skill(character, "hunting")
|
||||
result = rules.roll_challenge(character1, character2, "swords")
|
||||
```
|
||||
|
||||
You might need to make these functions more or less complex depending on your game. For example the
|
||||
properties of the room might matter to the outcome of a roll (if the room is dark, burning etc).
|
||||
Establishing just what you need to send into your game mechanic module is a great way to also get a
|
||||
feel for what you need to add to your engine.
|
||||
|
||||
## Coded systems
|
||||
|
||||
Inspired by tabletop role playing games, most game systems mimic some sort of die mechanic. To this
|
||||
end Evennia offers a full [dice
|
||||
roller](https://github.com/evennia/evennia/blob/master/evennia/contrib/dice.py) in its `contrib`
|
||||
folder. For custom implementations, Python offers many ways to randomize a result using its in-built
|
||||
`random` module. No matter how it's implemented, we will in this text refer to the action of
|
||||
determining an outcome as a "roll".
|
||||
|
||||
In a freeform system, the result of the roll is just compared with values and people (or the game
|
||||
master) just agree on what it means. In a coded system the result now needs to be processed somehow.
|
||||
There are many things that may happen as a result of rule enforcement:
|
||||
|
||||
- Health may be added or deducted. This can effect the character in various ways.
|
||||
- Experience may need to be added, and if a level-based system is used, the player might need to be
|
||||
informed they have increased a level.
|
||||
- Room-wide effects need to be reported to the room, possibly affecting everyone in the room.
|
||||
|
||||
There are also a slew of other things that fall under "Coded systems", including things like
|
||||
weather, NPC artificial intelligence and game economy. Basically everything about the world that a
|
||||
Game master would control in a tabletop role playing game can be mimicked to some level by coded
|
||||
systems.
|
||||
|
||||
|
||||
## Example of Rule module
|
||||
|
||||
Here is a simple example of a rule module. This is what we assume about our simple example game:
|
||||
- Characters have only four numerical values:
|
||||
- Their `level`, which starts at 1.
|
||||
- A skill `combat`, which determines how good they are at hitting things. Starts between 5 and
|
||||
10.
|
||||
- Their Strength, `STR`, which determine how much damage they do. Starts between 1 and 10.
|
||||
- Their Health points, `HP`, which starts at 100.
|
||||
- When a Character reaches `HP = 0`, they are presumed "defeated". Their HP is reset and they get a
|
||||
failure message (as a stand-in for death code).
|
||||
- Abilities are stored as simple Attributes on the Character.
|
||||
- "Rolls" are done by rolling a 100-sided die. If the result is below the `combat` value, it's a
|
||||
success and damage is rolled. Damage is rolled as a six-sided die + the value of `STR` (for this
|
||||
example we ignore weapons and assume `STR` is all that matters).
|
||||
- Every successful `attack` roll gives 1-3 experience points (`XP`). Every time the number of `XP`
|
||||
reaches `(level + 1) ** 2`, the Character levels up. When leveling up, the Character's `combat`
|
||||
value goes up by 2 points and `STR` by one (this is a stand-in for a real progression system).
|
||||
|
||||
### Character
|
||||
|
||||
The Character typeclass is simple. It goes in `mygame/typeclasses/characters.py`. There is already
|
||||
an empty `Character` class there that Evennia will look to and use.
|
||||
|
||||
```python
|
||||
from random import randint
|
||||
from evennia import DefaultCharacter
|
||||
|
||||
class Character(DefaultCharacter):
|
||||
"""
|
||||
Custom rule-restricted character. We randomize
|
||||
the initial skill and ability values bettween 1-10.
|
||||
"""
|
||||
def at_object_creation(self):
|
||||
"Called only when first created"
|
||||
self.db.level = 1
|
||||
self.db.HP = 100
|
||||
self.db.XP = 0
|
||||
self.db.STR = randint(1, 10)
|
||||
self.db.combat = randint(5, 10)
|
||||
```
|
||||
|
||||
`@reload` the server to load up the new code. Doing `examine self` will however *not* show the new
|
||||
Attributes on yourself. This is because the `at_object_creation` hook is only called on *new*
|
||||
Characters. Your Character was already created and will thus not have them. To force a reload, use
|
||||
the following command:
|
||||
|
||||
```
|
||||
@typeclass/force/reset self
|
||||
```
|
||||
|
||||
The `examine self` command will now show the new Attributes.
|
||||
|
||||
### Rule module
|
||||
|
||||
This is a module `mygame/world/rules.py`.
|
||||
|
||||
```python
|
||||
from random import randint
|
||||
|
||||
def roll_hit():
|
||||
"Roll 1d100"
|
||||
return randint(1, 100)
|
||||
|
||||
def roll_dmg():
|
||||
"Roll 1d6"
|
||||
return randint(1, 6)
|
||||
|
||||
def check_defeat(character):
|
||||
"Checks if a character is 'defeated'."
|
||||
if character.db.HP <= 0:
|
||||
character.msg("You fall down, defeated!")
|
||||
character.db.HP = 100 # reset
|
||||
|
||||
def add_XP(character, amount):
|
||||
"Add XP to character, tracking level increases."
|
||||
character.db.XP += amount
|
||||
if character.db.XP >= (character.db.level + 1) ** 2:
|
||||
character.db.level += 1
|
||||
character.db.STR += 1
|
||||
character.db.combat += 2
|
||||
character.msg(f"You are now level {character.db.level}!")
|
||||
|
||||
def skill_combat(*args):
|
||||
"""
|
||||
This determines outcome of combat. The one who
|
||||
rolls under their combat skill AND higher than
|
||||
their opponent's roll hits.
|
||||
"""
|
||||
char1, char2 = args
|
||||
roll1, roll2 = roll_hit(), roll_hit()
|
||||
failtext_template = "You are hit by {attacker} for {dmg} damage!"
|
||||
wintext_template = "You hit {target} for {dmg} damage!"
|
||||
xp_gain = randint(1, 3)
|
||||
if char1.db.combat >= roll1 > roll2:
|
||||
# char 1 hits
|
||||
dmg = roll_dmg() + char1.db.STR
|
||||
char1.msg(wintext_template.format(target=char2, dmg=dmg))
|
||||
add_XP(char1, xp_gain)
|
||||
char2.msg(failtext_template.format(attacker=char1, dmg=dmg))
|
||||
char2.db.HP -= dmg
|
||||
check_defeat(char2)
|
||||
elif char2.db.combat >= roll2 > roll1:
|
||||
# char 2 hits
|
||||
dmg = roll_dmg() + char2.db.STR
|
||||
char1.msg(failtext_template.format(attacker=char2, dmg=dmg))
|
||||
char1.db.HP -= dmg
|
||||
check_defeat(char1)
|
||||
char2.msg(wintext_template.format(target=char1, dmg=dmg))
|
||||
add_XP(char2, xp_gain)
|
||||
else:
|
||||
# a draw
|
||||
drawtext = "Neither of you can find an opening."
|
||||
char1.msg(drawtext)
|
||||
char2.msg(drawtext)
|
||||
|
||||
SKILLS = {"combat": skill_combat}
|
||||
|
||||
def roll_challenge(character1, character2, skillname):
|
||||
"""
|
||||
Determine the outcome of a skill challenge between
|
||||
two characters based on the skillname given.
|
||||
"""
|
||||
if skillname in SKILLS:
|
||||
SKILLS[skillname](character1, character2)
|
||||
else:
|
||||
raise RunTimeError(f"Skillname {skillname} not found.")
|
||||
```
|
||||
|
||||
These few functions implement the entirety of our simple rule system. We have a function to check
|
||||
the "defeat" condition and reset the `HP` back to 100 again. We define a generic "skill" function.
|
||||
Multiple skills could all be added with the same signature; our `SKILLS` dictionary makes it easy to
|
||||
look up the skills regardless of what their actual functions are called. Finally, the access
|
||||
function `roll_challenge` just picks the skill and gets the result.
|
||||
|
||||
In this example, the skill function actually does a lot - it not only rolls results, it also informs
|
||||
everyone of their results via `character.msg()` calls.
|
||||
|
||||
Here is an example of usage in a game command:
|
||||
|
||||
```python
|
||||
from evennia import Command
|
||||
from world import rules
|
||||
|
||||
class CmdAttack(Command):
|
||||
"""
|
||||
attack an opponent
|
||||
|
||||
Usage:
|
||||
attack <target>
|
||||
|
||||
This will attack a target in the same room, dealing
|
||||
damage with your bare hands.
|
||||
"""
|
||||
def func(self):
|
||||
"Implementing combat"
|
||||
|
||||
caller = self.caller
|
||||
if not self.args:
|
||||
caller.msg("You need to pick a target to attack.")
|
||||
return
|
||||
|
||||
target = caller.search(self.args)
|
||||
if target:
|
||||
rules.roll_challenge(caller, target, "combat")
|
||||
```
|
||||
|
||||
Note how simple the command becomes and how generic you can make it. It becomes simple to offer any
|
||||
number of Combat commands by just extending this functionality - you can easily roll challenges and
|
||||
pick different skills to check. And if you ever decided to, say, change how to determine hit chance,
|
||||
you don't have to change every command, but need only change the single `roll_hit` function inside
|
||||
your `rules` module.
|
||||
|
|
@ -0,0 +1,521 @@
|
|||
# Turn based Combat System
|
||||
|
||||
|
||||
This tutorial gives an example of a full, if simplified, combat system for Evennia. It was inspired
|
||||
by the discussions held on the [mailing
|
||||
list](https://groups.google.com/forum/#!msg/evennia/wnJNM2sXSfs/-dbLRrgWnYMJ).
|
||||
|
||||
## Overview of combat system concepts
|
||||
|
||||
Most MUDs will use some sort of combat system. There are several main variations:
|
||||
|
||||
- _Freeform_ - the simplest form of combat to implement, common to MUSH-style roleplaying games.
|
||||
This means the system only supplies dice rollers or maybe commands to compare skills and spit out
|
||||
the result. Dice rolls are done to resolve combat according to the rules of the game and to direct
|
||||
the scene. A game master may be required to resolve rule disputes.
|
||||
- _Twitch_ - This is the traditional MUD hack&slash style combat. In a twitch system there is often
|
||||
no difference between your normal "move-around-and-explore mode" and the "combat mode". You enter an
|
||||
attack command and the system will calculate if the attack hits and how much damage was caused.
|
||||
Normally attack commands have some sort of timeout or notion of recovery/balance to reduce the
|
||||
advantage of spamming or client scripting. Whereas the simplest systems just means entering `kill
|
||||
<target>` over and over, more sophisticated twitch systems include anything from defensive stances
|
||||
to tactical positioning.
|
||||
- _Turn-based_ - a turn based system means that the system pauses to make sure all combatants can
|
||||
choose their actions before continuing. In some systems, such entered actions happen immediately
|
||||
(like twitch-based) whereas in others the resolution happens simultaneously at the end of the turn.
|
||||
The disadvantage of a turn-based system is that the game must switch to a "combat mode" and one also
|
||||
needs to take special care of how to handle new combatants and the passage of time. The advantage is
|
||||
that success is not dependent on typing speed or of setting up quick client macros. This potentially
|
||||
allows for emoting as part of combat which is an advantage for roleplay-heavy games.
|
||||
|
||||
To implement a freeform combat system all you need is a dice roller and a roleplaying rulebook. See
|
||||
[contrib/dice.py](https://github.com/evennia/evennia/blob/master/evennia/contrib/dice.py) for an
|
||||
example dice roller. To implement at twitch-based system you basically need a few combat
|
||||
[commands](../../../Components/Commands.md), possibly ones with a [cooldown](../../Command-Cooldown.md). You also need a [game rule
|
||||
module](./Implementing-a-game-rule-system.md) that makes use of it. We will focus on the turn-based
|
||||
variety here.
|
||||
|
||||
## Tutorial overview
|
||||
|
||||
This tutorial will implement the slightly more complex turn-based combat system. Our example has the
|
||||
following properties:
|
||||
|
||||
- Combat is initiated with `attack <target>`, this initiates the combat mode.
|
||||
- Characters may join an ongoing battle using `attack <target>` against a character already in
|
||||
combat.
|
||||
- Each turn every combating character will get to enter two commands, their internal order matters
|
||||
and they are compared one-to-one in the order given by each combatant. Use of `say` and `pose` is
|
||||
free.
|
||||
- The commands are (in our example) simple; they can either `hit <target>`, `feint <target>` or
|
||||
`parry <target>`. They can also `defend`, a generic passive defense. Finally they may choose to
|
||||
`disengage/flee`.
|
||||
- When attacking we use a classic [rock-paper-scissors](https://en.wikipedia.org/wiki/Rock-paper-
|
||||
scissors) mechanic to determine success: `hit` defeats `feint`, which defeats `parry` which defeats
|
||||
`hit`. `defend` is a general passive action that has a percentage chance to win against `hit`
|
||||
(only).
|
||||
- `disengage/flee` must be entered two times in a row and will only succeed if there is no `hit`
|
||||
against them in that time. If so they will leave combat mode.
|
||||
- Once every player has entered two commands, all commands are resolved in order and the result is
|
||||
reported. A new turn then begins.
|
||||
- If players are too slow the turn will time out and any unset commands will be set to `defend`.
|
||||
|
||||
For creating the combat system we will need the following components:
|
||||
|
||||
- A combat handler. This is the main mechanic of the system. This is a [Script](../../../Components/Scripts.md) object
|
||||
created for each combat. It is not assigned to a specific object but is shared by the combating
|
||||
characters and handles all the combat information. Since Scripts are database entities it also means
|
||||
that the combat will not be affected by a server reload.
|
||||
- A combat [command set](../../../Components/Command-Sets.md) with the relevant commands needed for combat, such as the
|
||||
various attack/defend options and the `flee/disengage` command to leave the combat mode.
|
||||
- A rule resolution system. The basics of making such a module is described in the [rule system
|
||||
tutorial](./Implementing-a-game-rule-system.md). We will only sketch such a module here for our end-turn
|
||||
combat resolution.
|
||||
- An `attack` [command](../../../Components/Commands.md) for initiating the combat mode. This is added to the default
|
||||
command set. It will create the combat handler and add the character(s) to it. It will also assign
|
||||
the combat command set to the characters.
|
||||
|
||||
## The combat handler
|
||||
|
||||
The _combat handler_ is implemented as a stand-alone [Script](../../../Components/Scripts.md). This Script is created when
|
||||
the first Character decides to attack another and is deleted when no one is fighting any more. Each
|
||||
handler represents one instance of combat and one combat only. Each instance of combat can hold any
|
||||
number of characters but each character can only be part of one combat at a time (a player would
|
||||
need to disengage from the first combat before they could join another).
|
||||
|
||||
The reason we don't store this Script "on" any specific character is because any character may leave
|
||||
the combat at any time. Instead the script holds references to all characters involved in the
|
||||
combat. Vice-versa, all characters holds a back-reference to the current combat handler. While we
|
||||
don't use this very much here this might allow the combat commands on the characters to access and
|
||||
update the combat handler state directly.
|
||||
|
||||
_Note: Another way to implement a combat handler would be to use a normal Python object and handle
|
||||
time-keeping with the [TickerHandler](../../../Components/TickerHandler.md). This would require either adding custom hook
|
||||
methods on the character or to implement a custom child of the TickerHandler class to track turns.
|
||||
Whereas the TickerHandler is easy to use, a Script offers more power in this case._
|
||||
|
||||
Here is a basic combat handler. Assuming our game folder is named `mygame`, we store it in
|
||||
`mygame/typeclasses/combat_handler.py`:
|
||||
|
||||
```python
|
||||
# mygame/typeclasses/combat_handler.py
|
||||
|
||||
import random
|
||||
from evennia import DefaultScript
|
||||
from world.rules import resolve_combat
|
||||
|
||||
class CombatHandler(DefaultScript):
|
||||
"""
|
||||
This implements the combat handler.
|
||||
"""
|
||||
|
||||
# standard Script hooks
|
||||
|
||||
def at_script_creation(self):
|
||||
"Called when script is first created"
|
||||
|
||||
self.key = f"combat_handler_{random.randint(1, 1000)}"
|
||||
self.desc = "handles combat"
|
||||
self.interval = 60 * 2 # two minute timeout
|
||||
self.start_delay = True
|
||||
self.persistent = True
|
||||
|
||||
# store all combatants
|
||||
self.db.characters = {}
|
||||
# store all actions for each turn
|
||||
self.db.turn_actions = {}
|
||||
# number of actions entered per combatant
|
||||
self.db.action_count = {}
|
||||
|
||||
def _init_character(self, character):
|
||||
"""
|
||||
This initializes handler back-reference
|
||||
and combat cmdset on a character
|
||||
"""
|
||||
character.ndb.combat_handler = self
|
||||
character.cmdset.add("commands.combat.CombatCmdSet")
|
||||
|
||||
def _cleanup_character(self, character):
|
||||
"""
|
||||
Remove character from handler and clean
|
||||
it of the back-reference and cmdset
|
||||
"""
|
||||
dbref = character.id
|
||||
del self.db.characters[dbref]
|
||||
del self.db.turn_actions[dbref]
|
||||
del self.db.action_count[dbref]
|
||||
del character.ndb.combat_handler
|
||||
character.cmdset.delete("commands.combat.CombatCmdSet")
|
||||
|
||||
def at_start(self):
|
||||
"""
|
||||
This is called on first start but also when the script is restarted
|
||||
after a server reboot. We need to re-assign this combat handler to
|
||||
all characters as well as re-assign the cmdset.
|
||||
"""
|
||||
for character in self.db.characters.values():
|
||||
self._init_character(character)
|
||||
|
||||
def at_stop(self):
|
||||
"Called just before the script is stopped/destroyed."
|
||||
for character in list(self.db.characters.values()):
|
||||
# note: the list() call above disconnects list from database
|
||||
self._cleanup_character(character)
|
||||
|
||||
def at_repeat(self):
|
||||
"""
|
||||
This is called every self.interval seconds (turn timeout) or
|
||||
when force_repeat is called (because everyone has entered their
|
||||
commands). We know this by checking the existence of the
|
||||
`normal_turn_end` NAttribute, set just before calling
|
||||
force_repeat.
|
||||
|
||||
"""
|
||||
if self.ndb.normal_turn_end:
|
||||
# we get here because the turn ended normally
|
||||
# (force_repeat was called) - no msg output
|
||||
del self.ndb.normal_turn_end
|
||||
else:
|
||||
# turn timeout
|
||||
self.msg_all("Turn timer timed out. Continuing.")
|
||||
self.end_turn()
|
||||
|
||||
# Combat-handler methods
|
||||
|
||||
def add_character(self, character):
|
||||
"Add combatant to handler"
|
||||
dbref = character.id
|
||||
self.db.characters[dbref] = character
|
||||
self.db.action_count[dbref] = 0
|
||||
self.db.turn_actions[dbref] = [("defend", character, None),
|
||||
("defend", character, None)]
|
||||
# set up back-reference
|
||||
self._init_character(character)
|
||||
|
||||
def remove_character(self, character):
|
||||
"Remove combatant from handler"
|
||||
if character.id in self.db.characters:
|
||||
self._cleanup_character(character)
|
||||
if not self.db.characters:
|
||||
# if no more characters in battle, kill this handler
|
||||
self.stop()
|
||||
|
||||
def msg_all(self, message):
|
||||
"Send message to all combatants"
|
||||
for character in self.db.characters.values():
|
||||
character.msg(message)
|
||||
|
||||
def add_action(self, action, character, target):
|
||||
"""
|
||||
Called by combat commands to register an action with the handler.
|
||||
|
||||
action - string identifying the action, like "hit" or "parry"
|
||||
character - the character performing the action
|
||||
target - the target character or None
|
||||
|
||||
actions are stored in a dictionary keyed to each character, each
|
||||
of which holds a list of max 2 actions. An action is stored as
|
||||
a tuple (character, action, target).
|
||||
"""
|
||||
dbref = character.id
|
||||
count = self.db.action_count[dbref]
|
||||
if 0 <= count <= 1: # only allow 2 actions
|
||||
self.db.turn_actions[dbref][count] = (action, character, target)
|
||||
else:
|
||||
# report if we already used too many actions
|
||||
return False
|
||||
self.db.action_count[dbref] += 1
|
||||
return True
|
||||
|
||||
def check_end_turn(self):
|
||||
"""
|
||||
Called by the command to eventually trigger
|
||||
the resolution of the turn. We check if everyone
|
||||
has added all their actions; if so we call force the
|
||||
script to repeat immediately (which will call
|
||||
`self.at_repeat()` while resetting all timers).
|
||||
"""
|
||||
if all(count > 1 for count in self.db.action_count.values()):
|
||||
self.ndb.normal_turn_end = True
|
||||
self.force_repeat()
|
||||
|
||||
def end_turn(self):
|
||||
"""
|
||||
This resolves all actions by calling the rules module.
|
||||
It then resets everything and starts the next turn. It
|
||||
is called by at_repeat().
|
||||
"""
|
||||
resolve_combat(self, self.db.turn_actions)
|
||||
|
||||
if len(self.db.characters) < 2:
|
||||
# less than 2 characters in battle, kill this handler
|
||||
self.msg_all("Combat has ended")
|
||||
self.stop()
|
||||
else:
|
||||
# reset counters before next turn
|
||||
for character in self.db.characters.values():
|
||||
self.db.characters[character.id] = character
|
||||
self.db.action_count[character.id] = 0
|
||||
self.db.turn_actions[character.id] = [("defend", character, None),
|
||||
("defend", character, None)]
|
||||
self.msg_all("Next turn begins ...")
|
||||
```
|
||||
|
||||
This implements all the useful properties of our combat handler. This Script will survive a reboot
|
||||
and will automatically re-assert itself when it comes back online. Even the current state of the
|
||||
combat should be unaffected since it is saved in Attributes at every turn. An important part to note
|
||||
is the use of the Script's standard `at_repeat` hook and the `force_repeat` method to end each turn.
|
||||
This allows for everything to go through the same mechanisms with minimal repetition of code.
|
||||
|
||||
What is not present in this handler is a way for players to view the actions they set or to change
|
||||
their actions once they have been added (but before the last one has added theirs). We leave this as
|
||||
an exercise.
|
||||
|
||||
## Combat commands
|
||||
|
||||
Our combat commands - the commands that are to be available to us during the combat - are (in our
|
||||
example) very simple. In a full implementation the commands available might be determined by the
|
||||
weapon(s) held by the player or by which skills they know.
|
||||
|
||||
We create them in `mygame/commands/combat.py`.
|
||||
|
||||
```python
|
||||
# mygame/commands/combat.py
|
||||
|
||||
from evennia import Command
|
||||
|
||||
class CmdHit(Command):
|
||||
"""
|
||||
hit an enemy
|
||||
|
||||
Usage:
|
||||
hit <target>
|
||||
|
||||
Strikes the given enemy with your current weapon.
|
||||
"""
|
||||
key = "hit"
|
||||
aliases = ["strike", "slash"]
|
||||
help_category = "combat"
|
||||
|
||||
def func(self):
|
||||
"Implements the command"
|
||||
if not self.args:
|
||||
self.caller.msg("Usage: hit <target>")
|
||||
return
|
||||
target = self.caller.search(self.args)
|
||||
if not target:
|
||||
return
|
||||
ok = self.caller.ndb.combat_handler.add_action("hit",
|
||||
self.caller,
|
||||
target)
|
||||
if ok:
|
||||
self.caller.msg("You add 'hit' to the combat queue")
|
||||
else:
|
||||
self.caller.msg("You can only queue two actions per turn!")
|
||||
|
||||
# tell the handler to check if turn is over
|
||||
self.caller.ndb.combat_handler.check_end_turn()
|
||||
```
|
||||
|
||||
The other commands `CmdParry`, `CmdFeint`, `CmdDefend` and `CmdDisengage` look basically the same.
|
||||
We should also add a custom `help` command to list all the available combat commands and what they
|
||||
do.
|
||||
|
||||
We just need to put them all in a cmdset. We do this at the end of the same module:
|
||||
|
||||
```python
|
||||
# mygame/commands/combat.py
|
||||
|
||||
from evennia import CmdSet
|
||||
from evennia import default_cmds
|
||||
|
||||
class CombatCmdSet(CmdSet):
|
||||
key = "combat_cmdset"
|
||||
mergetype = "Replace"
|
||||
priority = 10
|
||||
no_exits = True
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
self.add(CmdHit())
|
||||
self.add(CmdParry())
|
||||
self.add(CmdFeint())
|
||||
self.add(CmdDefend())
|
||||
self.add(CmdDisengage())
|
||||
self.add(CmdHelp())
|
||||
self.add(default_cmds.CmdPose())
|
||||
self.add(default_cmds.CmdSay())
|
||||
```
|
||||
|
||||
## Rules module
|
||||
|
||||
A general way to implement a rule module is found in the [rule system tutorial](Implementing-a-game-
|
||||
rule-system). Proper resolution would likely require us to change our Characters to store things
|
||||
like strength, weapon skills and so on. So for this example we will settle for a very simplistic
|
||||
rock-paper-scissors kind of setup with some randomness thrown in. We will not deal with damage here
|
||||
but just announce the results of each turn. In a real system the Character objects would hold stats
|
||||
to affect their skills, their chosen weapon affect the choices, they would be able to lose health
|
||||
etc.
|
||||
|
||||
Within each turn, there are "sub-turns", each consisting of one action per character. The actions
|
||||
within each sub-turn happens simultaneously and only once they have all been resolved we move on to
|
||||
the next sub-turn (or end the full turn).
|
||||
|
||||
*Note: In our simple example the sub-turns don't affect each other (except for `disengage/flee`),
|
||||
nor do any effects carry over between turns. The real power of a turn-based system would be to add
|
||||
real tactical possibilities here though; For example if your hit got parried you could be out of
|
||||
balance and your next action would be at a disadvantage. A successful feint would open up for a
|
||||
subsequent attack and so on ...*
|
||||
|
||||
Our rock-paper-scissor setup works like this:
|
||||
|
||||
- `hit` beats `feint` and `flee/disengage`. It has a random chance to fail against `defend`.
|
||||
- `parry` beats `hit`.
|
||||
- `feint` beats `parry` and is then counted as a `hit`.
|
||||
- `defend` does nothing but has a chance to beat `hit`.
|
||||
- `flee/disengage` must succeed two times in a row (i.e. not beaten by a `hit` once during the
|
||||
turn). If so the character leaves combat.
|
||||
|
||||
|
||||
```python
|
||||
# mygame/world/rules.py
|
||||
|
||||
import random
|
||||
|
||||
# messages
|
||||
|
||||
def resolve_combat(combat_handler, actiondict):
|
||||
"""
|
||||
This is called by the combat handler
|
||||
actiondict is a dictionary with a list of two actions
|
||||
for each character:
|
||||
{char.id:[(action1, char, target), (action2, char, target)], ...}
|
||||
"""
|
||||
flee = {} # track number of flee commands per character
|
||||
for isub in range(2):
|
||||
# loop over sub-turns
|
||||
messages = []
|
||||
for subturn in (sub[isub] for sub in actiondict.values()):
|
||||
# for each character, resolve the sub-turn
|
||||
action, char, target = subturn
|
||||
if target:
|
||||
taction, tchar, ttarget = actiondict[target.id][isub]
|
||||
if action == "hit":
|
||||
if taction == "parry" and ttarget == char:
|
||||
messages.append(
|
||||
f"{char} tries to hit {tchar}, but {tchar} parries the attack!"
|
||||
)
|
||||
elif taction == "defend" and random.random() < 0.5:
|
||||
messages.append(
|
||||
f"{tchar} defends against the attack by {char}."
|
||||
)
|
||||
elif taction == "flee":
|
||||
flee[tchar] = -2
|
||||
messages.append(
|
||||
f"{char} stops {tchar} from disengaging, with a hit!"
|
||||
)
|
||||
else:
|
||||
messages.append(
|
||||
f"{char} hits {tchar}, bypassing their {taction}!"
|
||||
)
|
||||
elif action == "parry":
|
||||
if taction == "hit":
|
||||
messages.append(f"{char} parries the attack by {tchar}.")
|
||||
elif taction == "feint":
|
||||
messages.append(
|
||||
f"{char} tries to parry, but {tchar} feints and hits!"
|
||||
)
|
||||
else:
|
||||
messages.append(f"{char} parries to no avail.")
|
||||
elif action == "feint":
|
||||
if taction == "parry":
|
||||
messages.append(
|
||||
f"{char} feints past {tchar}'s parry, landing a hit!"
|
||||
)
|
||||
elif taction == "hit":
|
||||
messages.append(f"{char} feints but is defeated by {tchar}'s hit!")
|
||||
else:
|
||||
messages.append(f"{char} feints to no avail.")
|
||||
elif action == "defend":
|
||||
messages.append(f"{char} defends.")
|
||||
elif action == "flee":
|
||||
if char in flee:
|
||||
flee[char] += 1
|
||||
else:
|
||||
flee[char] = 1
|
||||
messages.append(
|
||||
f"{char} tries to disengage (two subsequent turns needed)"
|
||||
)
|
||||
|
||||
# echo results of each subturn
|
||||
combat_handler.msg_all("\n".join(messages))
|
||||
|
||||
# at the end of both sub-turns, test if anyone fled
|
||||
for (char, fleevalue) in flee.items():
|
||||
if fleevalue == 2:
|
||||
combat_handler.msg_all(f"{char} withdraws from combat.")
|
||||
combat_handler.remove_character(char)
|
||||
```
|
||||
|
||||
To make it simple (and to save space), this example rule module actually resolves each interchange
|
||||
twice - first when it gets to each character and then again when handling the target. Also, since we
|
||||
use the combat handler's `msg_all` method here, the system will get pretty spammy. To clean it up,
|
||||
one could imagine tracking all the possible interactions to make sure each pair is only handled and
|
||||
reported once.
|
||||
|
||||
## Combat initiator command
|
||||
|
||||
This is the last component we need, a command to initiate combat. This will tie everything together.
|
||||
We store this with the other combat commands.
|
||||
|
||||
```python
|
||||
# mygame/commands/combat.py
|
||||
|
||||
from evennia import create_script
|
||||
|
||||
class CmdAttack(Command):
|
||||
"""
|
||||
initiates combat
|
||||
|
||||
Usage:
|
||||
attack <target>
|
||||
|
||||
This will initiate combat with <target>. If <target is
|
||||
already in combat, you will join the combat.
|
||||
"""
|
||||
key = "attack"
|
||||
help_category = "General"
|
||||
|
||||
def func(self):
|
||||
"Handle command"
|
||||
if not self.args:
|
||||
self.caller.msg("Usage: attack <target>")
|
||||
return
|
||||
target = self.caller.search(self.args)
|
||||
if not target:
|
||||
return
|
||||
# set up combat
|
||||
if target.ndb.combat_handler:
|
||||
# target is already in combat - join it
|
||||
target.ndb.combat_handler.add_character(self.caller)
|
||||
target.ndb.combat_handler.msg_all(f"{self.caller} joins combat!")
|
||||
else:
|
||||
# create a new combat handler
|
||||
chandler = create_script("combat_handler.CombatHandler")
|
||||
chandler.add_character(self.caller)
|
||||
chandler.add_character(target)
|
||||
self.caller.msg(f"You attack {target}! You are in combat.")
|
||||
target.msg(f"{self.caller} attacks you! You are in combat.")
|
||||
```
|
||||
|
||||
The `attack` command will not go into the combat cmdset but rather into the default cmdset. See e.g.
|
||||
the [Adding Command Tutorial](../Part1/Adding-Commands.md) if you are unsure about how to do this.
|
||||
|
||||
## Expanding the example
|
||||
|
||||
At this point you should have a simple but flexible turn-based combat system. We have taken several
|
||||
shortcuts and simplifications in this example. The output to the players is likely too verbose
|
||||
during combat and too limited when it comes to informing about things surrounding it. Methods for
|
||||
changing your commands or list them, view who is in combat etc is likely needed - this will require
|
||||
play testing for each game and style. There is also currently no information displayed for other
|
||||
people happening to be in the same room as the combat - some less detailed information should
|
||||
probably be echoed to the room to
|
||||
show others what's going on.
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
# Part 4: Using what we created
|
||||
|
||||
```{eval-rst}
|
||||
..sidebar:: Beginner Tutorial Parts
|
||||
|
||||
`Introduction <../Beginner-Tutorial-Intro.html>`_
|
||||
Getting set up.
|
||||
Part 1: `What we have <../Part1/Beginner-Tutorial-Part1-Intro.html>`_
|
||||
A tour of Evennia and how to use the tools, including an introduction to Python.
|
||||
Part 2: `What we want <../Part2/Beginner-Tutorial-Part2-Intro.html>`_
|
||||
Planning our tutorial game and what to think about when planning your own in the future.
|
||||
Part 3: `How we get there <../Part3/Beginner-Tutorial-Part3-Intro.html>`_
|
||||
Getting down to the meat of extending Evennia to make our game to make a tech-demo
|
||||
**Part 4: Using what we created**
|
||||
Using the tech-demo and world content to go with our code
|
||||
Part 5: `Showing the world <../Part5/Beginner-Tutorial-Part5-Intro.html>`_
|
||||
Taking our new game online and let players try it out
|
||||
```
|
||||
|
||||
We now have the code underpinnings of everything we need. We have also tested the various components
|
||||
and has a simple tech-demo to show it all works together. But there is no real coherence to it at this
|
||||
point - we need to actually make a world.
|
||||
In part four we will expand our tech demo into a more full-fledged (if small) game by use of batchcommand
|
||||
and batchcode processors.
|
||||
|
||||
## Lessons
|
||||
|
||||
_TODO_
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 1
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Table of Contents
|
||||
|
||||
_TODO_
|
||||
|
||||
```{toctree}
|
||||
|
||||
```
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
# Add a simple new web page
|
||||
|
||||
|
||||
Evennia leverages [Django](https://docs.djangoproject.com) which is a web development framework.
|
||||
Huge professional websites are made in Django and there is extensive documentation (and books) on it
|
||||
. You are encouraged to at least look at the Django basic tutorials. Here we will just give a brief
|
||||
introduction for how things hang together, to get you started.
|
||||
|
||||
We assume you have installed and set up Evennia to run. A webserver and website comes out of the
|
||||
box. You can get to that by entering `http://localhost:4001` in your web browser - you should see a
|
||||
welcome page with some game statistics and a link to the web client. Let us add a new page that you
|
||||
can get to by going to `http://localhost:4001/story`.
|
||||
|
||||
## Create the view
|
||||
|
||||
A django "view" is a normal Python function that django calls to render the HTML page you will see
|
||||
in the web browser. Here we will just have it spit back the raw html, but Django can do all sorts of
|
||||
cool stuff with the page in the view, like adding dynamic content or change it on the fly. Open
|
||||
`mygame/web` folder and add a new module there named `story.py` (you could also put it in its own
|
||||
folder if you wanted to be neat. Don't forget to add an empty `__init__.py` file if you do, to tell
|
||||
Python you can import from the new folder). Here's how it looks:
|
||||
|
||||
```python
|
||||
# in mygame/web/story.py
|
||||
|
||||
from django.shortcuts import render
|
||||
|
||||
def storypage(request):
|
||||
return render(request, "story.html")
|
||||
```
|
||||
|
||||
This view takes advantage of a shortcut provided to use by Django, _render_. This shortcut gives the
|
||||
template some information from the request, for instance, the game name, and then renders it.
|
||||
|
||||
## The HTML page
|
||||
|
||||
We need to find a place where Evennia (and Django) looks for html files (called *templates* in
|
||||
Django parlance). You can specify such places in your settings (see the `TEMPLATES` variable in
|
||||
`default_settings.py` for more info), but here we'll use an existing one. Go to
|
||||
`mygame/template/overrides/website/` and create a page `story.html` there.
|
||||
|
||||
This is not a HTML tutorial, so we'll go simple:
|
||||
|
||||
```html
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1>A story about a tree</h1>
|
||||
<p>
|
||||
This is a story about a tree, a classic tale ...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
Since we've used the _render_ shortcut, Django will allow us to extend our base styles easily.
|
||||
|
||||
If you'd rather not take advantage of Evennia's base styles, you can do something like this instead:
|
||||
|
||||
```html
|
||||
<html>
|
||||
<body>
|
||||
<h1>A story about a tree</h1>
|
||||
<p>
|
||||
This is a story about a tree, a classic tale ...
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
|
||||
## The URL
|
||||
|
||||
When you enter the address `http://localhost:4001/story` in your web browser, Django will parse that
|
||||
field to figure out which page you want to go to. You tell it which patterns are relevant in the
|
||||
file
|
||||
[mygame/web/urls.py](https://github.com/evennia/evennia/blob/master/evennia/game_template/web/urls.py).
|
||||
Open it now.
|
||||
|
||||
Django looks for the variable `urlpatterns` in this file. You want to add your new pattern to the
|
||||
`custom_patterns` list we have prepared - that is then merged with the default `urlpatterns`. Here's
|
||||
how it could look:
|
||||
|
||||
```python
|
||||
from web import story
|
||||
|
||||
# ...
|
||||
|
||||
custom_patterns = [
|
||||
url(r'story', story.storypage, name='Story'),
|
||||
]
|
||||
```
|
||||
|
||||
That is, we import our story view module from where we created it earlier and then create an `url`
|
||||
instance. The first argument to `url` is the pattern of the url we want to find (`"story"`) (this is
|
||||
a regular expression if you are familiar with those) and then our view function we want to direct
|
||||
to.
|
||||
|
||||
That should be it. Reload Evennia and you should be able to browse to your new story page!
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
# Part 5: Showing the world
|
||||
|
||||
```{eval-rst}
|
||||
.. sidebar:: Beginner Tutorial Parts
|
||||
|
||||
`Introduction <../Beginner-Tutorial-Intro.html>`_
|
||||
Getting set up.
|
||||
Part 1: `What we have <../Part1/Beginner-Tutorial-Part1-Intro.html>`_
|
||||
A tour of Evennia and how to use the tools, including an introduction to Python.
|
||||
Part 2: `What we want <../Part2/Beginner-Tutorial-Part2-Intro.html>`_
|
||||
Planning our tutorial game and what to think about when planning your own in the future.
|
||||
Part 3: `How we get there <../Part3/Beginner-Tutorial-Part3-Intro.html>`_
|
||||
Getting down to the meat of extending Evennia to make our game
|
||||
Part 4: `Using what we created <../Part4/Beginner-Tutorial-Part4-Intro.html>`_
|
||||
Building a tech-demo and world content to go with our code
|
||||
**Part 5: Showing the world**
|
||||
Taking our new game online and let players try it out
|
||||
```
|
||||
|
||||
You have a working game! In part five we will look at the web-components of Evennia and how to modify them
|
||||
to fit your game. We will also look at hosting your game and if you feel up to it we'll also go through how
|
||||
to bring your game online so you can invite your first players.
|
||||
|
||||
## Lessons
|
||||
|
||||
_TODO_
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 1
|
||||
|
||||
Add-a-simple-new-web-page.md
|
||||
Web-Tutorial.md
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Table of Contents
|
||||
|
||||
_TODO_
|
||||
|
||||
```{toctree}
|
||||
|
||||
Add-a-simple-new-web-page.md
|
||||
Web-Tutorial.md
|
||||
|
||||
```
|
||||
126
docs/source/Howtos/Beginner-Tutorial/Part5/Web-Tutorial.md
Normal file
126
docs/source/Howtos/Beginner-Tutorial/Part5/Web-Tutorial.md
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
# Web Tutorial
|
||||
|
||||
|
||||
Evennia uses the [Django](https://www.djangoproject.com/) web framework as the basis of both its
|
||||
database configuration and the website it provides. While a full understanding of Django requires
|
||||
reading the Django documentation, we have provided this tutorial to get you running with the basics
|
||||
and how they pertain to Evennia. This text details getting everything set up. The
|
||||
[Web-based Character view Tutorial](../../Web-Character-View-Tutorial.md) gives a more explicit example of making a
|
||||
custom web page connected to your game, and you may want to read that after finishing this guide.
|
||||
|
||||
## A Basic Overview
|
||||
|
||||
Django is a web framework. It gives you a set of development tools for building a website quickly
|
||||
and easily.
|
||||
|
||||
Django projects are split up into *apps* and these apps all contribute to one project. For instance,
|
||||
you might have an app for conducting polls, or an app for showing news posts or, like us, one for
|
||||
creating a web client.
|
||||
|
||||
Each of these applications has a `urls.py` file, which specifies what
|
||||
[URL](https://en.wikipedia.org/wiki/Uniform_resource_locator)s are used by the app, a `views.py` file
|
||||
for the code that the URLs activate, a `templates` directory for displaying the results of that code
|
||||
in [HTML](https://en.wikipedia.org/wiki/Html) for the user, and a `static` folder that holds assets
|
||||
like [CSS](https://en.wikipedia.org/wiki/CSS), [Javascript](https://en.wikipedia.org/wiki/Javascript),
|
||||
and Image files (You may note your mygame/web folder does not have a `static` or `template` folder.
|
||||
This is intended and explained further below). Django applications may also have a `models.py` file
|
||||
for storing information in the database. We will not change any models here, take a look at the
|
||||
[New Models](../../../Concepts/New-Models.md) page (as well as the [Django docs](https://docs.djangoproject.com/en/1.7/topics/db/models/) on models) if you are interested.
|
||||
|
||||
There is also a root `urls.py` that determines the URL structure for the entire project. A starter
|
||||
`urls.py` is included in the default game template, and automatically imports all of Evennia's
|
||||
default URLs for you. This is located in `web/urls.py`.
|
||||
|
||||
## Changing the logo on the front page
|
||||
|
||||
Evennia's default logo is a fun little googly-eyed snake wrapped around a gear globe. As cute as it
|
||||
is, it probably doesn't represent your game. So one of the first things you may wish to do is
|
||||
replace it with a logo of your own.
|
||||
|
||||
Django web apps all have _static assets_: CSS files, Javascript files, and Image files. In order to
|
||||
make sure the final project has all the static files it needs, the system collects the files from
|
||||
every app's `static` folder and places it in the `STATIC_ROOT` defined in `settings.py`. By default,
|
||||
the Evennia `STATIC_ROOT` is in `web/static`.
|
||||
|
||||
Because Django pulls files from all of those separate places and puts them in one folder, it's
|
||||
possible for one file to overwrite another. We will use this to plug in our own files without having
|
||||
to change anything in the Evennia itself.
|
||||
|
||||
By default, Evennia is configured to pull files you put in the `web/static_overrides` *after* all
|
||||
other static files. That means that files in `static_overrides` folder will overwrite any previously
|
||||
loaded files *having the same path under its static folder*. This last part is important to repeat:
|
||||
To overload the static resource from a standard `static` folder you need to replicate the path of
|
||||
folders and file names from that `static` folder in exactly the same way inside `static_overrides`.
|
||||
|
||||
Let's see how this works for our logo. The default web application is in the Evennia library itself,
|
||||
in `evennia/web/`. We can see that there is a `static` folder here. If we browse down, we'll
|
||||
eventually find the full path to the Evennia logo file:
|
||||
`evennia/web/static/evennia_general/images/evennia_logo.png`.
|
||||
|
||||
Inside our `static_overrides` we must replicate the part of the path inside the `static` folder, in
|
||||
other words, we must replicate `evennia_general/images/evennia_logo.png`.
|
||||
|
||||
So, to change the logo, we need to create the folder path `evennia_general/images/` in
|
||||
`static_overrides`. We then rename our own logo file to `evennia_logo.png` and copy it there. The
|
||||
final path for this file would thus be:
|
||||
`web/static_overrides/evennia_general/images/evennia_logo.png` in your local game folder.
|
||||
|
||||
To get this file pulled in, just change to your own game directory and reload the server:
|
||||
|
||||
```
|
||||
evennia reload
|
||||
```
|
||||
|
||||
This will reload the configuration and bring in the new static file(s). If you didn't want to reload
|
||||
the server you could instead use
|
||||
|
||||
```
|
||||
evennia collectstatic
|
||||
```
|
||||
|
||||
to only update the static files without any other changes.
|
||||
|
||||
> **Note**: Evennia will collect static files automatically during startup. So if `evennia
|
||||
collectstatic` reports finding 0 files to collect, make sure you didn't start the engine at some
|
||||
point - if so the collector has already done its work! To make sure, connect to the website and
|
||||
check so the logo has actually changed to your own version.
|
||||
|
||||
> **Note**: Sometimes the static asset collector can get confused. If no matter what you do, your
|
||||
overridden files aren't getting copied over the defaults, try removing the target file (or
|
||||
everything) in the `web/static` directory, and re-running `collectstatic` to gather everything from
|
||||
scratch.
|
||||
|
||||
## Changing the Front Page's Text
|
||||
|
||||
The default front page for Evennia contains information about the Evennia project. You'll probably
|
||||
want to replace this information with information about your own project. Changing the page template
|
||||
is done in a similar way to changing static resources.
|
||||
|
||||
Like static files, Django looks through a series of template folders to find the file it wants. The
|
||||
difference is that Django does not copy all of the template files into one place, it just searches
|
||||
through the template folders until it finds a template that matches what it's looking for. This
|
||||
means that when you edit a template, the changes are instant. You don't have to reload the server or
|
||||
run any extra commands to see these changes - reloading the web page in your browser is enough.
|
||||
|
||||
To replace the index page's text, we'll need to find the template for it. We'll go into more detail
|
||||
about how to determine which template is used for rendering a page in the
|
||||
[Web-based Character view Tutorial](../../Web-Character-View-Tutorial.md). For now, you should know that the template we want to change
|
||||
is stored in `evennia/web/website/templates/website/index.html`.
|
||||
|
||||
To replace this template file, you will put your changed template inside the
|
||||
`web/template_overrides/website` directory in your game folder. In the same way as with static
|
||||
resources you must replicate the path inside the default `template` directory exactly. So we must
|
||||
copy our replacement template named `index.html` there (or create the `website` directory in
|
||||
web/template_overrides` if it does not exist, first). The final path to the file should thus be:
|
||||
`web/template_overrides/website/index.html` within your game directory.
|
||||
|
||||
Note that it is usually easier to just copy the original template over and edit it in place. The
|
||||
original file already has all the markup and tags, ready for editing.
|
||||
|
||||
## Further reading
|
||||
|
||||
For further hints on working with the web presence, you could now continue to the
|
||||
[Web-based Character view Tutorial](../../Web-Character-View-Tutorial.md) where you learn to make a web page that
|
||||
displays in-game character stats. You can also look at [Django's own
|
||||
tutorial](https://docs.djangoproject.com/en/1.7/intro/tutorial01/) to get more insight in how Django
|
||||
works and what possibilities exist.
|
||||
Loading…
Add table
Add a link
Reference in a new issue