mirror of
https://github.com/evennia/evennia.git
synced 2026-03-18 22:06:30 +01:00
Updated HTML docs.
This commit is contained in:
parent
59e50f3fa5
commit
06bc3c8bcd
663 changed files with 2 additions and 61705 deletions
|
|
@ -1,114 +0,0 @@
|
|||
# Beginner Tutorial
|
||||
|
||||
```{sidebar} Beginner Tutorial Parts
|
||||
- **[Introduction](./Beginner-Tutorial-Intro.md)**
|
||||
<br>Getting set up.
|
||||
- Part 1: [What we have](Part1/Beginner-Tutorial-Part1-Intro.md)
|
||||
<br>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.md)
|
||||
<br>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.md)
|
||||
<br>Getting down to the meat of extending Evennia to make our game
|
||||
- Part 4: [Using what we created](Part4/Beginner-Tutorial-Part4-Intro.md)
|
||||
<br>Building a tech-demo and world content to go with our code
|
||||
- Part 5: [Showing the world](Part5/Beginner-Tutorial-Part5-Intro.md)
|
||||
<br>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>
|
||||
|
|
@ -1,394 +0,0 @@
|
|||
# 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](./Beginner-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`.
|
||||
|
||||
```{code-block} python
|
||||
:linenos:
|
||||
|
||||
# ...
|
||||
|
||||
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 3**: The normal `class` header. We inherit from `Command` which we imported at the top of this file.
|
||||
- **Lines 4-10**: The docstring and help-entry for the command. You could expand on this as much as you wanted.
|
||||
- **Line 11**: We want to write `hit` to use this command.
|
||||
- **Line 14**: 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 15** 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 16-17**: This code will only run if the `if` statement is truthy, in this case if `args` is the empty string.
|
||||
- **Line 17**: `return` is a reserved Python word that exits `func` immediately.
|
||||
- **Line 18**: We use `self.caller.search` to look for the target in the current location.
|
||||
- **Lines 19-20**: 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 21-22**: 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.
|
||||
|
|
@ -1,311 +0,0 @@
|
|||
# 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.
|
||||
|
||||
desc 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](./Beginner-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/tutorials/bodyfunctions/bodyfunctions.py`
|
||||
that is called `BodyFunctions`. To add this to us we will use the `script` command:
|
||||
|
||||
script self = tutorials.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 = tutorials.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:tutorials.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](../../../api/evennia.contrib.tutorials.red_button.md)
|
||||
|
||||
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 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.
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
# 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_
|
||||
|
||||
|
|
@ -1,432 +0,0 @@
|
|||
# Advanced searching - Django Database queries
|
||||
|
||||
```{important} More advanced lesson!
|
||||
|
||||
Learning about Django's query 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 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. This means that you
|
||||
won't find any `Weapon`-typeclassed results in `all_cannons`. Vice-versa, you
|
||||
won't find any `Cannon`-typeclassed results in `all_weapons`. This may not be
|
||||
what you expect.
|
||||
|
||||
If you want to get all entities with typeclass `Weapon` _as well_ as all the
|
||||
subclasses of `Weapon`, such as `Cannon`, you need to use the `_family` type of
|
||||
query:
|
||||
|
||||
```{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 (and any other
|
||||
entities whose typeclasses inherit at any distance from `Weapon`, like `Musket` or
|
||||
`Sword`).
|
||||
|
||||
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 flowers 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")
|
||||
|
||||
It's important to note that we haven't called the database yet! Not until we
|
||||
actually try to examine the result will the database be called. Here the
|
||||
database is called when we try to loop over it (because now we need to actually
|
||||
get results out of it to be able to loop):
|
||||
|
||||
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.
|
||||
|
||||
```{sidebar} database fields
|
||||
Each database table have only a few fields. For `Objects`, the most common ones
|
||||
are `db_key`, `db_location` and `db_destination`. When accessing them they are
|
||||
normally accessed just as `obj.key`, `obj.location` and `obj.destination`. You
|
||||
only need to remember the `db_` when using them in database queries. The object
|
||||
description, `obj.db.desc` is not such a hard-coded field, but one of many
|
||||
arbitrary Attributes attached to the Object.
|
||||
|
||||
```
|
||||
|
||||
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 `__` similarly to how 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 have identified 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 lesson.
|
||||
|
||||
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
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
- We want to find `Character`s, so we access `.objects` on the `Character` typeclass.
|
||||
- We start to filter ...
|
||||
-
|
||||
- ... 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).
|
||||
- ... We also want only Characters with `Attributes` whose `db_key` is exactly `"lycantrophy"`
|
||||
- ... at the same time as the `Attribute`'s `db_value` is greater-than 2.
|
||||
|
||||
Running this query makes our newly lycantrophic Character appear in `will_transform` so we
|
||||
know to transform it. 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!). When you get bitten, you'll get a Tag `recently_bitten` put on you to
|
||||
indicate 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 or equal to 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.
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
# 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](../../../api/evennia.objects.objects.md) 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.
|
||||
|
|
@ -1,199 +0,0 @@
|
|||
# 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 lesson.
|
||||
|
||||
```
|
||||
- [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](./Beginner-Tutorial-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.
|
||||
|
||||
|
|
@ -1,605 +0,0 @@
|
|||
# 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](./Beginner-Tutorial-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](./Beginner-Tutorial-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](./Beginner-Tutorial-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/4.1/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_.
|
||||
|
||||
|
||||
|
|
@ -1,483 +0,0 @@
|
|||
# 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](./Beginner-Tutorial-Building-Quickstart.md)
|
||||
you've seen an example of this with the "Red Button" object. The [Tutorial world](./Beginner-Tutorial-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 here 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.
|
||||
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
# Part 1: What we have
|
||||
|
||||
```{sidebar} Beginner Tutorial Parts
|
||||
- [Introduction](../Beginner-Tutorial-Intro.md)
|
||||
<br>Getting set up.
|
||||
- Part 1: **[What we have](./Beginner-Tutorial-Part1-Intro.md)**
|
||||
<br>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.md)
|
||||
<br>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.md)
|
||||
<br>Getting down to the meat of extending Evennia to make our game
|
||||
- Part 4: [Using what we created](../Part4/Beginner-Tutorial-Part4-Intro.md)
|
||||
<br>Building a tech-demo and world content to go with our code
|
||||
- Part 5: [Showing the world](../Part5/Beginner-Tutorial-Part5-Intro.md)
|
||||
<br>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:
|
||||
|
||||
Beginner-Tutorial-Building-Quickstart
|
||||
Beginner-Tutorial-Tutorial-World
|
||||
Beginner-Tutorial-Python-basic-introduction
|
||||
Beginner-Tutorial-Gamedir-Overview
|
||||
Beginner-Tutorial-Python-classes-and-objects
|
||||
Beginner-Tutorial-Evennia-Library-Overview
|
||||
Beginner-Tutorial-Learning-Typeclasses
|
||||
Beginner-Tutorial-Adding-Commands
|
||||
Beginner-Tutorial-More-on-Commands
|
||||
Beginner-Tutorial-Creating-Things
|
||||
Beginner-Tutorial-Searching-Things
|
||||
Beginner-Tutorial-Django-queries
|
||||
|
||||
```
|
||||
|
||||
## Table of Contents
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 2
|
||||
|
||||
Beginner-Tutorial-Building-Quickstart
|
||||
Beginner-Tutorial-Tutorial-World
|
||||
Beginner-Tutorial-Python-basic-introduction
|
||||
Beginner-Tutorial-Gamedir-Overview
|
||||
Beginner-Tutorial-Python-classes-and-objects
|
||||
Beginner-Tutorial-Evennia-Library-Overview
|
||||
Beginner-Tutorial-Learning-Typeclasses
|
||||
Beginner-Tutorial-Adding-Commands
|
||||
Beginner-Tutorial-More-on-Commands
|
||||
Beginner-Tutorial-Creating-Things
|
||||
Beginner-Tutorial-Searching-Things
|
||||
Beginner-Tutorial-Django-queries
|
||||
|
||||
```
|
||||
|
|
@ -1,649 +0,0 @@
|
|||
# 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!
|
||||
|
||||
```{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 `obj.msg()`.
|
||||
|
||||
```
|
||||
|
||||
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 ...
|
||||
|
|
@ -1,404 +0,0 @@
|
|||
# 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!")
|
||||
```
|
||||
|
||||
```{sidebar} Whitespace matters in Python!
|
||||
|
||||
- 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](./Beginner-Tutorial-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](./Beginner-Tutorial-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.
|
||||
|
||||
```{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 in 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.
|
||||
|
||||
|
|
@ -1,259 +0,0 @@
|
|||
# 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.
|
||||
|
||||
|
|
@ -1,123 +0,0 @@
|
|||
# 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](../../../api/evennia.contrib.tutorials.tutorial_world.md).
|
||||
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.
|
||||
|
||||
|
||||
|
|
@ -1,206 +0,0 @@
|
|||
# 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.
|
||||
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
# Part 2: What we want
|
||||
|
||||
```{sidebar} Beginner Tutorial Parts
|
||||
- [Introduction](../Beginner-Tutorial-Intro.md)
|
||||
<br>Getting set up.
|
||||
- Part 1: [What we have](../Part1/Beginner-Tutorial-Part1-Intro.md)
|
||||
<br>A tour of Evennia and how to use the tools, including an introduction to Python.
|
||||
- **Part 2: [What we want](./Beginner-Tutorial-Part2-Intro.md)**
|
||||
<br>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.md)
|
||||
<br>Getting down to the meat of extending Evennia to make our game
|
||||
- Part 4: [Using what we created](../Part4/Beginner-Tutorial-Part4-Intro.md)
|
||||
<br>Building a tech-demo and world content to go with our code
|
||||
- Part 5: [Showing the world](../Part5/Beginner-Tutorial-Part5-Intro.md)
|
||||
<br>Taking our new game online and let players try it out
|
||||
```
|
||||
|
||||
In Part two of the Evennia 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
|
||||
|
||||
Beginner-Tutorial-Planning-Where-Do-I-Begin.md
|
||||
Beginner-Tutorial-Game-Planning.md
|
||||
Beginner-Tutorial-Planning-The-Tutorial-Game.md
|
||||
```
|
||||
|
||||
## Table of Contents
|
||||
|
||||
```{toctree}
|
||||
|
||||
Beginner-Tutorial-Planning-Where-Do-I-Begin.md
|
||||
Beginner-Tutorial-Game-Planning.md
|
||||
Beginner-Tutorial-Planning-The-Tutorial-Game.md
|
||||
```
|
||||
|
|
@ -1,462 +0,0 @@
|
|||
# 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. We'll call it ... _EvAdventure_.
|
||||
Remembering that we need to keep the scope down, let's establish some parameters.
|
||||
|
||||
- We want EvAdventure be a small game we can play ourselves for fun, but which could in principle be expanded to something more later.
|
||||
- We want to have a clear game-loop, with clear goals.
|
||||
- Let's go with a fantasy theme, it's well understood.
|
||||
- We will use a small, existing tabletop RPG rule set ([Knave](https://www.drivethrurpg.com/product/250888/Knave), more info later)
|
||||
- We want to be able to create and customize a character of our own.
|
||||
- While not roleplay-focused, it should still be possible to socialize and to collaborate.
|
||||
- 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 some sort of quest system and merchants to buy stuff from.
|
||||
|
||||
|
||||
## Game concept
|
||||
|
||||
With these points in mind, here's a quick blurb for our game:
|
||||
|
||||
_Recently, the nearby village discovered that the old abandoned well contained a dark secret. The bottom of the well led to a previously undiscovered dungeon of ever shifting passages. No one knew why it was there or what its purpose was, but local rumors abound. The first adventurer that went down didn't come back. The second ... brought back a handful of glittering riches._
|
||||
|
||||
_Now the rush is on - there's a dungeon to explore and coin to earn. Knaves, cutthroats, adventurers and maybe even a hero or two are coming from all over the realm to challenge whatever lurks at the bottom of that well._
|
||||
|
||||
_Local merchants and opportunists have seen a chance for profit. A camp of tents has sprung up around the old well, providing food and drink, equipment, entertainment and rumors for a price. It's a festival to enjoy before paying the entrance fee for dropping down the well to find your fate among the shadows below ..._
|
||||
|
||||
Our game will consist of two main game modes - above ground and below. The player starts above ground and is expected to do 'expeditions' into the dark. The design goal is for them to be forced back up again when their health, equipment and luck is about to run out.
|
||||
- Above, in the "dungeon festival", the player can restock and heal up, buy things and do a small set of quests. It's the only place where the characters can sleep and fully heal. They also need to spend coin here to gain XP and levels. This is a place for players to socialize and RP. There is no combat above ground except for an optional spot for non-lethal PvP.
|
||||
- Below is the mysterious dungeon. This is a procedurally generated set of rooms. Players can collaborate if they go down the well together, they will not be able to run into each other otherwise (so this works as an instance). Each room generally presents some challenge (normally a battle). Pushing deeper is more dangerous but can grant greater rewards. While the rooms could in theory go on forever, there should be a boss encounter once a player reaches deep enough.
|
||||
|
||||
Here's an overview of the topside camp for inspiration (quickly thrown together in the free version of [Inkarnate](https://inkarnate.com/)). We'll explore how to break this up into "rooms" (locations) when we get to creating the game world later.
|
||||
|
||||

|
||||
|
||||
For the rest of this lesson we'll answer and reason around the specific questions posed in the previous [Game Planning](./Beginner-Tutorial-Game-Planning.md) lesson.
|
||||
|
||||
## 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.
|
||||
|
||||
**EvAdventure Answer:**
|
||||
|
||||
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.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
We are okay with keeping the default permission 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.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
In EvAdventure we will just use the default inter-account channels. We will also not be implementing any bulletin boards; instead the merchant NPCs will act as quest givers.
|
||||
|
||||
## 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.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
For EvAdventure, we will build the above-ground part of the game world using batch-scripts. The world below-ground we will build procedurally, using raw code.
|
||||
|
||||
### 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.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
For our tutorial-game, we will only allow privileged builders and admins to modify the world.
|
||||
|
||||
## Systems
|
||||
|
||||
### Do you base your game off an existing RPG system or make up your own?
|
||||
|
||||
There is a plethora of options out there, and what you choose depends on the game you want. It can be tempting to grab a short free-form ruleset, but remember that the computer does not have any intuitiion or common sense to interpret the rules like a human GM could. Conversely, if you pick a very 'crunchy' game system, with detailed simulation of the real world, remember that you'll need to actually _code_ all those exceptions and tables yourself.
|
||||
|
||||
For speediest development, what you want is a game with a _consolidated_ resolution mechanic - one you can code once and then use in a lot of situations. But you still want enough rules to help telling the computer how various situations should be resolved (combat is the most common system that needs such structure).
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
For this tutorial, we will make use of [Knave](https://www.drivethrurpg.com/product/250888/Knave), a very light [OSR](https://en.wikipedia.org/wiki/Old_School_Renaissance) ruleset by Ben Milton. It's only a few pages long but highly compatible with old-school D&D games. It's consolidates all rules around a few opposed d20 rolls and includes clear rules for combat, inventory, equipment and so on. Since _Knave_ is a tabletop RPG, we will have to do some minor changes here and there to fit it to the computer medium.
|
||||
|
||||
_Knave_ is available under a Creative Commons Attributions 4.0 License, meaning it can be used for derivative work (even commercially). The above link allows you to purchase the PDF and supporting the author. Alternatively you can find unofficial fan releases of the rules [on this page](https://dungeonsandpossums.com/2020/04/some-great-knave-rpg-resources/).
|
||||
|
||||
|
||||
### What are the game mechanics? How do you decide if an action succeeds or fails?
|
||||
|
||||
This follows from the RPG system decided upon in the previous question.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
_Knave_ gives every character a set of six traditional stats: Strength, Intelligence, Dexterity, Constitution, Intelligence, Wisdom and Charisma. Each has a value from +1 to +10. To find its "Defense" value, you add 10.
|
||||
|
||||
You have Strength +1. Your Strength-Defense is 10 + 1 = 11
|
||||
|
||||
To make a check, say an arm-wrestling challenge you roll a twenty-sided die (d20) and add your stat. You have to roll higher than the opponents defense for that stat.
|
||||
|
||||
I have Strength +1, my opponent has a Strength of +2. To beat them in arm wrestling I must roll d20 + 1 and hope to get higher than 12, which is their Strength defense (10 + 2).
|
||||
|
||||
If you attack someone you do the same, except you roll against their `Armor` defense. If you rolled higher, you roll for how much damage you do (depends on your weapon).
|
||||
You can have _advantage_ or _disadvantage_ on a roll. This means rolling 2d20 and picking highest or lowest value.
|
||||
|
||||
In Knave, combat is turn-based. In our implementation we'll also play turn-based, but we'll resolve everything _simultaneously_. This changes _Knave_'s feel quite a bit, but is a case where the computer can do things not practical to do when playing around a table.
|
||||
|
||||
There are also a few tables we'll need to implement. For example, if you lose all health, there's a one-in-six chance you'll die outright. We'll keep this perma-death aspect, but make it very easy to start a new character and jump back in.
|
||||
|
||||
> In this tutorial we will not add opportunities to make use of all of the character stats, making some, like strength, intelligence and dexterity more useful than others. In a full game, one would want to expand so a user can utilize all of their character's strengths.
|
||||
|
||||
### 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.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
The passage of time will have no impact on our particular game example, so we'll 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).
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
We'll not change the weather, but will add some random messages to echo through
|
||||
the game world at random intervals just to show the principle.
|
||||
|
||||
### Do you want a coded world-economy or just a simple barter system? Or no formal economy at all?
|
||||
This is a big question and depends on how deep and interconnected the virtual transactions are that are happening in the game. Shop prices could rice and drop due to supply and demand, supply chains could involve crafting and production. One also could consider adding money sinks and manipulate the in-game market to combat inflation.
|
||||
|
||||
The [Barter](../../../Contribs/Contrib-Barter.md) contrib provides a full interface for trading with another player in a safe way.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
We will not deal with any of this complexity. We will allow for players to buy from npc sellers and players will be able to trade using the normal `give` command.
|
||||
|
||||
### Do you have concepts like reputation and influence?
|
||||
|
||||
These are useful things for a more social-interaction heavy game.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
We will not include them for this tutorial. Adding the Barter contrib is simple though.
|
||||
|
||||
### 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.
|
||||
|
||||
Implementing such a system is not trivial, but the [RPsystem](../../../Contribs/Contrib-RPSystem.md) Evennia contrib offers a ready system with everything needed for free emoting, recognizing people by their appearance and more.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
We will not use any special RP systems for this tutorial. Adding the RPSystem contrib is a good extra expansion though!
|
||||
|
||||
## 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.
|
||||
|
||||
There is an [Extended Room](../../../Contribs/Contrib-Extended-Room.md) contrib that adds a Room type that is aware of the time-of-day as well as seasonal variations.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
We will stick to a normal room in this tutorial and let the world be in a perpetual daylight. Making Rooms into ExtendedRooms is not hard though.
|
||||
|
||||
### Should the room have different statuses?
|
||||
|
||||
One could picture weather making outdoor rooms wet, cold or burnt. In rain, bow strings could get wet and fireballs fizz out. In a hot room, characters could require drinking more water, or even take damage if not finding shelter.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
For the above-ground we need to be able to disable combat all rooms except for the PvP location. We also need to consider how to auto-generate the rooms under ground. So we probably will need some statuses to control that.
|
||||
|
||||
Since each room under ground should present some sort of challenge, we may need a few different room types different from the above-ground Rooms.
|
||||
|
||||
### Can objects be hidden in the room? Can a person hide in the room?
|
||||
|
||||
This ties into if you have hide/stealth mechanics. Maybe you could evesdrop or attack out of hiding.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
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?
|
||||
|
||||
This also depends on the type of game. In a pure freeform RPG, most objects may be 'imaginary' and just appearing in fiction. If the game is more coded, you want objects with properties that the computer can measure, track and calculate. In many roleplaying-heavy games, you find a mixture of the two, with players imagining items for roleplaying scenes, but only using 'real' objects to resolve conflicts.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
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 in this example game.
|
||||
|
||||
### Is each coin a separate object or do you just store a bank account value?
|
||||
|
||||
The advantage of having multiple items is that it can be more immersive. The drawback is that it's also very fiddly to deal with individual coins, especially if you have to deal with different currencies.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
_Knave_ uses the "copper" as the base coin and so will we. Knave considers the weight of coin and one inventory "slot" can hold 100 coins. So we'll implement a "coin item" to represent many coins.
|
||||
|
||||
### Do multiple similar objects form stack and how are those stacks handled in that case?
|
||||
|
||||
If you drop two identical apples on the ground, Evennia will default to show this in the room as "two apples", but this is just a visual effect - there are still two apple-objects in the room. One could picture instead merging the two into a single object "X nr of apples" when you drop the apples.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
We will keep Evennia's default.
|
||||
|
||||
### 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.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
_Knave_ limits your inventory to `Constitution + 10` "slots", where most items take up one slot and some large things, like armor, uses two. Small items (like rings) can fit 2-10 per slot and you can fit 100 coins in a slot. This is an important game mechanic to limit players from hoarding. Especially since you need coin to level up.
|
||||
|
||||
### 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.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
In _Knave_, items will break if you make a critical failure on using them (rolls a native 1 on d20). This means they lose a level of `quality` and once at 0, it's unusable. We will not allow players to repair, but we could allow merchants to repair items for a fee.
|
||||
|
||||
### 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.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
Since _Knave_ deals with weapon lists and positions where items can be wielded, we will have a separate "Weapon" class for everything you can use for fighting. So, you won't be able to fight with a chair (unless we make it a weapon-inherited chair).
|
||||
|
||||
### 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.
|
||||
|
||||
Evennia's [Crafting](../../../Contribs/Contrib-Crafting.md) contrib adds a full crafting system to any game. It's based on [Tags](../../../Components/Tags.md), meaning that pretty much any object can be made usable for crafting, even used in an unexpected way.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
In our case we will not add any crafting in order to limit the scope of our game. Maybe NPCs will be able to repair items - for a cost?
|
||||
|
||||
### Should mobs/NPCs have some sort of AI?
|
||||
|
||||
As a rule, you should not hope to fool anyone into thinking your AI is actually intelligent. The best you will be able to do is to give interesting results and 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.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
For this tutorial, we will show how to add a simple state-machine AI 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.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
In EvAdventure, Monsters and NPCs do very different things, so they will be different classes, sharing some code where possible.
|
||||
|
||||
### _Should there be NPCs giving quests? If so, how do you track Quest status?
|
||||
|
||||
Quests are a staple of many classic RPGs.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
We will design a simple quest system with some simple conditions for success, like carrying the right item or items back to the quest giver.
|
||||
|
||||
## 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.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
Due to the nature of _Knave_, characters are squishy and probably short-lived. So it makes little sense to keep a stable of them. We'll use use mode 0 or 1.
|
||||
|
||||
### 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.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
Knave randomizes almost aspects of the Character generation. We'll use a menu to let the player add their name and sex as well as do the minor re-assignment of stats allowed by the rules.
|
||||
|
||||
### How do you implement different "classes" or "races"?
|
||||
|
||||
The way classes and races work in most RPGs is that they act as static 'templates' that inform which bonuses and special abilities you have. Much of this only comes into play during character generation or when leveling up.
|
||||
|
||||
Often 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.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
There are no races and no classes in _Knave_. Every character is a human.
|
||||
|
||||
### 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.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
We will not be including a hide-mechanic in EvAdventure.
|
||||
|
||||
### 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.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
We will use an alternative rule in _Knave_, where Characters gain XP by spending coins they carry back from their adventures. The above-ground merchants will allow you to spend your coins and exchange them for XP 1:1. Each level costs 1000 coins. Every level you have `1d8 * new level` (minimum what you had before + 1) HP, and can raise 3 different ability scores by 1 (max +10). There are no skills in _Knave_, but the principle of increasing them would be the same.
|
||||
|
||||
### 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 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.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
We will allow PvP only in one place - a special Dueling location where players can play-fight each other for training and prestige, but not actually get killed. Otherwise no PvP will be allowed. Note that without a full Barter system in place (just regular `give`, it makes it theoretically easier for players to scam one another.
|
||||
|
||||
### 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.
|
||||
|
||||
- Many players say they like the _idea_ of permadeath except when it could happen to them.
|
||||
- Some players refuse to take any risks if death is permanent.
|
||||
- It may make players even more reluctant to play conflict-driving 'bad guys'.
|
||||
- Balancing PvP becomes very hard. Fairness and avoiding exploits becomes critical when the outcome
|
||||
is permanent.
|
||||
|
||||
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.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
In _Knave_, when you hit 0 HP, you roll on a death table, with a 1/8 chance of immediate death (otherwise you lose
|
||||
points in a random stat). We will offer an "Insurance" that allows you to resurrect if you carry enough coin on you when
|
||||
you die. If not, you are perma-dead and have to create a new character (which is easy and quick since it's mostly
|
||||
randomized).
|
||||
|
||||
## 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, 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!
|
||||
|
||||
In the last of these planning lessons we'll sketch out how these ideas will map to Evennia.
|
||||
|
|
@ -1,145 +0,0 @@
|
|||
# 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.
|
||||
|
|
@ -1,414 +0,0 @@
|
|||
# Player Characters
|
||||
|
||||
In the [previous lesson about rules and dice rolling](./Beginner-Tutorial-Rules.md) we made some
|
||||
assumptions about the "Player Character" entity:
|
||||
|
||||
- It should store Abilities on itself as `character.strength`, `character.constitution` etc.
|
||||
- It should have a `.heal(amount)` method.
|
||||
|
||||
So we have some guidelines of how it should look! A Character is a database entity with values that
|
||||
should be able to be changed over time. It makes sense to base it off Evennia's
|
||||
[DefaultCharacter Typeclass](../../../Components/Typeclasses.md). The Character class is like a 'character sheet' in a tabletop
|
||||
RPG, it will hold everything relevant to that PC.
|
||||
|
||||
## Inheritance structure
|
||||
|
||||
Player Characters (PCs) are not the only "living" things in our world. We also have _NPCs_
|
||||
(like shopkeepers and other friendlies) as well as _monsters_ (mobs) that can attack us.
|
||||
|
||||
In code, there are a few ways we could structure this. If NPCs/monsters were just special cases of PCs,
|
||||
we could use a class inheritance like this:
|
||||
|
||||
```python
|
||||
from evennia import DefaultCharacter
|
||||
|
||||
class EvAdventureCharacter(DefaultCharacter):
|
||||
# stuff
|
||||
|
||||
class EvAdventureNPC(EvAdventureCharacter):
|
||||
# more stuff
|
||||
|
||||
class EvAdventureMob(EvAdventureNPC):
|
||||
# more stuff
|
||||
```
|
||||
|
||||
All code we put on the `Character` class would now be inherited to `NPC` and `Mob` automatically.
|
||||
|
||||
However, in _Knave_, NPCs and particularly monsters are _not_ using the same rules as PCs - they are
|
||||
simplified to use a Hit-Die (HD) concept. So while still character-like, NPCs should be separate from
|
||||
PCs like this:
|
||||
|
||||
```python
|
||||
from evennia import DefaultCharacter
|
||||
|
||||
class EvAdventureCharacter(DefaultCharacter):
|
||||
# stuff
|
||||
|
||||
class EvAdventureNPC(DefaultCharacter):
|
||||
# separate stuff
|
||||
|
||||
class EvAdventureMob(EvadventureNPC):
|
||||
# more separate stuff
|
||||
```
|
||||
|
||||
Nevertheless, there are some things that _should_ be common for all 'living things':
|
||||
|
||||
- All can take damage.
|
||||
- All can die.
|
||||
- All can heal
|
||||
- All can hold and lose coins
|
||||
- All can loot their fallen foes.
|
||||
- All can get looted when defeated.
|
||||
|
||||
We don't want to code this separately for every class but we no longer have a common parent
|
||||
class to put it on. So instead we'll use the concept of a _mixin_ class:
|
||||
|
||||
```python
|
||||
from evennia import DefaultCharacter
|
||||
|
||||
class LivingMixin:
|
||||
# stuff common for all living things
|
||||
|
||||
class EvAdventureCharacter(LivingMixin, DefaultCharacter):
|
||||
# stuff
|
||||
|
||||
class EvAdventureNPC(LivingMixin, DefaultCharacter):
|
||||
# stuff
|
||||
|
||||
class EvAdventureMob(LivingMixin, EvadventureNPC):
|
||||
# more stuff
|
||||
```
|
||||
|
||||
```{sidebar}
|
||||
In [evennia/contrib/tutorials/evadventure/characters.py](../../../api/evennia.contrib.tutorials.evadventure.characters.md)
|
||||
is an example of a character class structure.
|
||||
```
|
||||
Above, the `LivingMixin` class cannot work on its own - it just 'patches' the other classes with some
|
||||
extra functionality all living things should be able to do. This is an example of
|
||||
_multiple inheritance_. It's useful to know about, but one should not over-do multiple inheritance
|
||||
since it can also get confusing to follow the code.
|
||||
|
||||
## Living mixin class
|
||||
|
||||
> Create a new module `mygame/evadventure/characters.py`
|
||||
|
||||
Let's get some useful common methods all living things should have in our game.
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/characters.py
|
||||
|
||||
from .rules import dice
|
||||
|
||||
class LivingMixin:
|
||||
|
||||
# makes it easy for mobs to know to attack PCs
|
||||
is_pc = False
|
||||
|
||||
def heal(self, hp):
|
||||
"""
|
||||
Heal hp amount of health, not allowing to exceed our max hp
|
||||
|
||||
"""
|
||||
damage = self.hp_max - self.hp
|
||||
healed = min(damage, hp)
|
||||
self.hp += healed
|
||||
|
||||
self.msg("You heal for {healed} HP.")
|
||||
|
||||
def at_pay(self, amount):
|
||||
"""When paying coins, make sure to never detract more than we have"""
|
||||
amount = min(amount, self.coins)
|
||||
self.coins -= amount
|
||||
return amount
|
||||
|
||||
def at_damage(self, damage, attacker=None):
|
||||
"""Called when attacked and taking damage."""
|
||||
self.hp -= damage
|
||||
|
||||
def at_defeat(self):
|
||||
"""Called when defeated. By default this means death."""
|
||||
self.at_death()
|
||||
|
||||
def at_death(self):
|
||||
"""Called when this thing dies."""
|
||||
# this will mean different things for different living things
|
||||
pass
|
||||
|
||||
def at_do_loot(self, looted):
|
||||
"""Called when looting another entity"""
|
||||
looted.at_looted(self)
|
||||
|
||||
def at_looted(self, looter):
|
||||
"""Called when looted by another entity"""
|
||||
|
||||
# default to stealing some coins
|
||||
max_steal = dice.roll("1d10")
|
||||
stolen = self.at_pay(max_steal)
|
||||
looter.coins += stolen
|
||||
|
||||
```
|
||||
Most of these are empty since they will behave differently for characters and npcs. But having them
|
||||
in the mixin means we can expect these methods to be available for all living things.
|
||||
|
||||
|
||||
## Character class
|
||||
|
||||
We will now start making the basic Character class, based on what we need from _Knave_.
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/characters.py
|
||||
|
||||
from evennia import DefaultCharacter, AttributeProperty
|
||||
from .rules import dice
|
||||
|
||||
class LivingMixin:
|
||||
# ...
|
||||
|
||||
|
||||
class EvAdventureCharacter(LivingMixin, DefaultCharacter):
|
||||
"""
|
||||
A character to use for EvAdventure.
|
||||
"""
|
||||
is_pc = True
|
||||
|
||||
strength = AttributeProperty(1)
|
||||
dexterity = AttributeProperty(1)
|
||||
constitution = AttributeProperty(1)
|
||||
intelligence = AttributeProperty(1)
|
||||
wisdom = AttributeProperty(1)
|
||||
charisma = AttributeProperty(1)
|
||||
|
||||
hp = AttributeProperty(8)
|
||||
hp_max = AttributeProperty(8)
|
||||
|
||||
level = AttributeProperty(1)
|
||||
xp = AttributeProperty(0)
|
||||
coins = AttributeProperty(0)
|
||||
|
||||
def at_defeat(self):
|
||||
"""Characters roll on the death table"""
|
||||
if self.location.allow_death:
|
||||
# this allow rooms to have non-lethal battles
|
||||
dice.roll_death(self)
|
||||
else:
|
||||
self.location.msg_contents(
|
||||
"$You() $conj(collapse) in a heap, alive but beaten.",
|
||||
from_obj=self)
|
||||
self.heal(self.hp_max)
|
||||
|
||||
def at_death(self):
|
||||
"""We rolled 'dead' on the death table."""
|
||||
self.location.msg_contents(
|
||||
"$You() collapse in a heap, embraced by death.",
|
||||
from_obj=self)
|
||||
# TODO - go back into chargen to make a new character!
|
||||
```
|
||||
|
||||
We make an assumption about our rooms here - that they have a property `.allow_death`. We need
|
||||
to make a note to actually add such a property to rooms later!
|
||||
|
||||
In our `Character` class we implement all attributes we want to simulate from the _Knave_ ruleset.
|
||||
The `AttributeProperty` is one way to add an Attribute in a field-like way; these will be accessible
|
||||
on every character in several ways:
|
||||
|
||||
- As `character.strength`
|
||||
- As `character.db.strength`
|
||||
- As `character.attributes.get("strength")`
|
||||
|
||||
See [Attributes](../../../Components/Attributes.md) for seeing how Attributes work.
|
||||
|
||||
Unlike in base _Knave_, we store `coins` as a separate Attribute rather than as items in the inventory,
|
||||
this makes it easier to handle barter and trading later.
|
||||
|
||||
We implement the Player Character versions of `at_defeat` and `at_death`. We also make use of `.heal()`
|
||||
from the `LivingMixin` class.
|
||||
|
||||
### Funcparser inlines
|
||||
|
||||
This piece of code is worth some more explanation:
|
||||
|
||||
```python
|
||||
self.location.msg_contents(
|
||||
"$You() $conj(collapse) in a heap, alive but beaten.",
|
||||
from_obj=self)
|
||||
```
|
||||
|
||||
Remember that `self` is the Character instance here. So `self.location.msg_contents` means "send a
|
||||
message to everything inside my current location". In other words, send a message to everyone
|
||||
in the same place as the character.
|
||||
|
||||
The `$You() $conj(collapse)` are [FuncParser inlines](../../../Components/FuncParser.md). These are functions that
|
||||
execute
|
||||
in the string. The resulting string may look different for different audiences. The `$You()` inline
|
||||
function will use `from_obj` to figure out who 'you' are and either show your name or 'You'.
|
||||
The `$conj()` (verb conjugator) will tweak the (English) verb to match.
|
||||
|
||||
- You will see: `"You collapse in a heap, alive but beaten."`
|
||||
- Others in the room will see: `"Thomas collapses in a heap, alive but beaten."`
|
||||
|
||||
Note how `$conj()` chose `collapse/collapses` to make the sentences grammatically correct.
|
||||
|
||||
### Backtracking
|
||||
|
||||
We make our first use of the `rules.dice` roller to roll on the death table! As you may recall, in the
|
||||
previous lesson, we didn't know just what to do when rolling 'dead' on this table. Now we know - we
|
||||
should be calling `at_death` on the character. So let's add that where we had TODOs before:
|
||||
|
||||
```python
|
||||
# mygame/evadventure/rules.py
|
||||
|
||||
class EvAdventureRollEngine:
|
||||
|
||||
# ...
|
||||
|
||||
def roll_death(self, character):
|
||||
ability_name = self.roll_random_table("1d8", death_table)
|
||||
|
||||
if ability_name == "dead":
|
||||
# kill the character!
|
||||
character.at_death() # <------ TODO no more
|
||||
else:
|
||||
# ...
|
||||
|
||||
if current_ability < -10:
|
||||
# kill the character!
|
||||
character.at_death() # <------- TODO no more
|
||||
else:
|
||||
# ...
|
||||
```
|
||||
|
||||
## Connecting the Character with Evennia
|
||||
|
||||
You can easily make yourself an `EvAdventureCharacter` in-game by using the
|
||||
`type` command:
|
||||
|
||||
type self = evadventure.characters.EvAdventureCharacter
|
||||
|
||||
You can now do `examine self` to check your type updated.
|
||||
|
||||
If you want _all_ new Characters to be of this type you need to tell Evennia about it. Evennia
|
||||
uses a global setting `BASE_CHARACTER_TYPECLASS` to know which typeclass to use when creating
|
||||
Characters (when logging in, for example). This defaults to `typeclasses.characters.Character` (that is,
|
||||
the `Character` class in `mygame/typeclasses/characters.py`).
|
||||
|
||||
There are thus two ways to weave your new Character class into Evennia:
|
||||
|
||||
1. Change `mygame/server/conf/settings.py` and add `BASE_CHARACTER_CLASS = "evadventure.characters.EvAdventureCharacter"`.
|
||||
2. Or, change `typeclasses.characters.Character` to inherit from `EvAdventureCharacter`.
|
||||
|
||||
You must always reload the server for changes like this to take effect.
|
||||
|
||||
```{important}
|
||||
In this tutorial we are making all changes in a folder `mygame/evadventure/`. This means we can isolate
|
||||
our code but means we need to do some extra steps to tie the character (and other objects) into Evennia.
|
||||
For your own game it would be just fine to start editing `mygame/typeclasses/characters.py` directly
|
||||
instead.
|
||||
```
|
||||
|
||||
|
||||
## Unit Testing
|
||||
|
||||
> Create a new module `mygame/evadventure/tests/test_characters.py`
|
||||
|
||||
For testing, we just need to create a new EvAdventure character and check
|
||||
that calling the methods on it doesn't error out.
|
||||
|
||||
```python
|
||||
# mygame/evadventure/tests/test_characters.py
|
||||
|
||||
from evennia.utils import create
|
||||
from evennia.utils.test_resources import BaseEvenniaTest
|
||||
|
||||
from ..characters import EvAdventureCharacter
|
||||
|
||||
class TestCharacters(BaseEvenniaTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.character = create.create_object(EvAdventureCharacter, key="testchar")
|
||||
|
||||
def test_heal(self):
|
||||
self.character.hp = 0
|
||||
self.character.hp_max = 8
|
||||
|
||||
self.character.heal(1)
|
||||
self.assertEqual(self.character.hp, 1)
|
||||
# make sure we can't heal more than max
|
||||
self.character.heal(100)
|
||||
self.assertEqual(self.character.hp, 8)
|
||||
|
||||
def test_at_pay(self):
|
||||
self.character.coins = 100
|
||||
|
||||
result = self.character.at_pay(60)
|
||||
self.assertEqual(result, 60)
|
||||
self.assertEqual(self.character.coins, 40)
|
||||
|
||||
# can't get more coins than we have
|
||||
result = self.character.at_pay(100)
|
||||
self.assertEqual(result, 40)
|
||||
self.assertEqual(self.character.coins, 0)
|
||||
|
||||
# tests for other methods ...
|
||||
|
||||
```
|
||||
If you followed the previous lessons, these tests should look familiar. Consider adding
|
||||
tests for other methods as practice. Refer to previous lessons for details.
|
||||
|
||||
For running the tests you do:
|
||||
|
||||
evennia test --settings settings.py .evadventure.tests.test_character
|
||||
|
||||
|
||||
## About races and classes
|
||||
|
||||
_Knave_ doesn't have any D&D-style _classes_ (like Thief, Fighter etc). It also does not bother with
|
||||
_races_ (like dwarves, elves etc). This makes the tutorial shorter, but you may ask yourself how you'd
|
||||
add these functions.
|
||||
|
||||
In the framework we have sketched out for _Knave_, it would be simple - you'd add your race/class as
|
||||
an Attribute on your Character:
|
||||
|
||||
```python
|
||||
# mygame/evadventure/characters.py
|
||||
|
||||
from evennia import DefaultCharacter, AttributeProperty
|
||||
# ...
|
||||
|
||||
class EvAdventureCharacter(LivingMixin, DefaultCharacter):
|
||||
|
||||
# ...
|
||||
|
||||
charclass = AttributeProperty("Fighter")
|
||||
charrace = AttributeProperty("Human")
|
||||
|
||||
```
|
||||
We use `charclass` rather than `class` here, because `class` is a reserved Python keyword. Naming
|
||||
`race` as `charrace` thus matches in style.
|
||||
|
||||
We'd then need to expand our [rules module](./Beginner-Tutorial-Rules.md) (and later
|
||||
[character generation](./Beginner-Tutorial-Chargen.md) to check and include what these classes mean.
|
||||
|
||||
|
||||
## Summary
|
||||
|
||||
|
||||
With the `EvAdventureCharacter` class in place, we have a better understanding of how our PCs will look
|
||||
like under _Knave_.
|
||||
|
||||
For now, we only have bits and pieces and haven't been testing this code in-game. But if you want
|
||||
you can swap yourself into `EvAdventureCharacter` right now. Log into your game and run
|
||||
the command
|
||||
|
||||
type self = evadventure.characters.EvAdventureCharacter
|
||||
|
||||
If all went well, `ex self` will now show your typeclass as being `EvAdventureCharacter`.
|
||||
Check out your strength with
|
||||
|
||||
py self.strength = 3
|
||||
|
||||
```{important}
|
||||
When doing `ex self` you will _not_ see all your Abilities listed yet. That's because
|
||||
Attributes added with `AttributeProperty` are not available until they have been accessed at
|
||||
least once. So once you set (or look at) `.strength` above, `strength` will show in `examine` from
|
||||
then on.
|
||||
```
|
||||
|
|
@ -1,711 +0,0 @@
|
|||
# Character Generation
|
||||
|
||||
In previous lessons we have established how a character looks. Now we need to give the player a
|
||||
chance to create one.
|
||||
|
||||
## How it will work
|
||||
|
||||
A fresh Evennia install will automatically create a new Character with the same name as your
|
||||
Account when you log in. This is quick and simple and mimics older MUD styles. You could picture
|
||||
doing this, and then customizing the Character in-place.
|
||||
|
||||
We will be a little more sophisticated though. We want the user to be able to create a character
|
||||
using a menu when they log in.
|
||||
|
||||
We do this by editing `mygame/server/conf/settings.py` and adding the line
|
||||
|
||||
AUTO_CREATE_CHARACTER_WITH_ACCOUNT = False
|
||||
|
||||
When doing this, connecting with the game with a new account will land you in "OOC" mode. The
|
||||
ooc-version of `look` (sitting in the Account cmdset) will show a list of available characters
|
||||
if you have any. You can also enter `charcreate` to make a new character. The `charcreate` is a
|
||||
simple command coming with Evennia that just lets you make a new character with a given name and
|
||||
description. We will later modify that to kick off our chargen. For now we'll just keep in mind
|
||||
that's how we'll start off the menu.
|
||||
|
||||
In _Knave_, most of the character-generation is random. This means this tutorial can be pretty
|
||||
compact while still showing the basic idea. What we will create is a menu looking like this:
|
||||
|
||||
|
||||
```
|
||||
Silas
|
||||
|
||||
STR +1
|
||||
DEX +2
|
||||
CON +1
|
||||
INT +3
|
||||
WIS +1
|
||||
CHA +2
|
||||
|
||||
You are lanky with a sunken face and filthy hair, breathy speech, and foreign clothing.
|
||||
You were a herbalist, but you were pursued and ended up a knave. You are honest but also
|
||||
suspicious. You are of the neutral alignment.
|
||||
|
||||
Your belongings:
|
||||
Brigandine armor, ration, ration, sword, torch, torch, torch, torch, torch,
|
||||
tinderbox, chisel, whistle
|
||||
|
||||
----------------------------------------------------------------------------------------
|
||||
1. Change your name
|
||||
2. Swap two of your ability scores (once)
|
||||
3. Accept and create character
|
||||
```
|
||||
|
||||
If you select 1, you get a new menu node:
|
||||
|
||||
```
|
||||
Your current name is Silas. Enter a new name or leave empty to abort.
|
||||
-----------------------------------------------------------------------------------------
|
||||
```
|
||||
You can now enter a new name. When pressing return you'll get back to the first menu node
|
||||
showing your character, now with the new name.
|
||||
|
||||
If you select 2, you go to another menu node:
|
||||
|
||||
```
|
||||
Your current abilities:
|
||||
|
||||
STR +1
|
||||
DEX +2
|
||||
CON +1
|
||||
INT +3
|
||||
WIS +1
|
||||
CHA +2
|
||||
|
||||
You can swap the values of two abilities around.
|
||||
You can only do this once, so choose carefully!
|
||||
|
||||
To swap the values of e.g. STR and INT, write 'STR INT'. Empty to abort.
|
||||
------------------------------------------------------------------------------------------
|
||||
```
|
||||
If you enter `WIS CHA` here, WIS will become `+2` and `CHA` `+1`. You will then again go back
|
||||
to the main node to see your new character, but this time the option to swap will no longer be
|
||||
available (you can only do it once).
|
||||
|
||||
If you finally select the `Accept and create character` option, the character will be created
|
||||
and you'll leave the menu;
|
||||
|
||||
Character was created!
|
||||
|
||||
## Random tables
|
||||
|
||||
```{sidebar}
|
||||
Full Knave random tables are found in
|
||||
[evennia/contrib/tutorials/evadventure/random_tables.py](../../../api/evennia.contrib.tutorials.evadventure.random_tables.md).
|
||||
```
|
||||
|
||||
> Make a new module `mygame/evadventure/random_tables.py`.
|
||||
|
||||
Since most of _Knave_'s character generation is random we will need to roll on random tables
|
||||
from the _Knave_ rulebook. While we added the ability to roll on a random table back in the
|
||||
[Rules Tutorial](./Beginner-Tutorial-Rules.md), we haven't added the relevant tables yet.
|
||||
|
||||
```
|
||||
# in mygame/evadventure/random_tables.py
|
||||
|
||||
chargen_tables = {
|
||||
"physique": [
|
||||
"athletic", "brawny", "corpulent", "delicate", "gaunt", "hulking", "lanky",
|
||||
"ripped", "rugged", "scrawny", "short", "sinewy", "slender", "flabby",
|
||||
"statuesque", "stout", "tiny", "towering", "willowy", "wiry",
|
||||
],
|
||||
"face": [
|
||||
"bloated", "blunt", "bony", # ...
|
||||
], # ...
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
The tables are just copied from the _Knave_ rules. We group the aspects in a dict
|
||||
`character_generation` to separate chargen-only tables from other random tables we'll also
|
||||
keep in here.
|
||||
|
||||
## Storing state of the menu
|
||||
|
||||
```{sidebar}
|
||||
There is a full implementation of the chargen in
|
||||
[evennia/contrib/tutorials/evadventure/chargen.py](../../../api/evennia.contrib.tutorials.evadventure.chargen.md).
|
||||
```
|
||||
> create a new module `mygame/evadventure/chargen.py`.
|
||||
|
||||
During character generation we will need an entity to store/retain the changes, like a
|
||||
'temporary character sheet'.
|
||||
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/chargen.py
|
||||
|
||||
from .random_tables import chargen_tables
|
||||
from .rules import dice
|
||||
|
||||
class TemporaryCharacterSheet:
|
||||
|
||||
def _random_ability(self):
|
||||
return min(dice.roll("1d6"), dice.roll("1d6"), dice.roll("1d6"))
|
||||
|
||||
def __init__(self):
|
||||
self.ability_changes = 0 # how many times we tried swap abilities
|
||||
|
||||
# name will likely be modified later
|
||||
self.name = dice.roll_random_table("1d282", chargen_tables["name"])
|
||||
|
||||
# base attribute values
|
||||
self.strength = self._random_ability()
|
||||
self.dexterity = self._random_ability()
|
||||
self.constitution = self._random_ability()
|
||||
self.intelligence = self._random_ability()
|
||||
self.wisdom = self._random_ability()
|
||||
self.charisma = self._random_ability()
|
||||
|
||||
# physical attributes (only for rp purposes)
|
||||
physique = dice.roll_random_table("1d20", chargen_tables["physique"])
|
||||
face = dice.roll_random_table("1d20", chargen_tables["face"])
|
||||
skin = dice.roll_random_table("1d20", chargen_tables["skin"])
|
||||
hair = dice.roll_random_table("1d20", chargen_tables["hair"])
|
||||
clothing = dice.roll_random_table("1d20", chargen_tables["clothing"])
|
||||
speech = dice.roll_random_table("1d20", chargen_tables["speech"])
|
||||
virtue = dice.roll_random_table("1d20", chargen_tables["virtue"])
|
||||
vice = dice.roll_random_table("1d20", chargen_tables["vice"])
|
||||
background = dice.roll_random_table("1d20", chargen_tables["background"])
|
||||
misfortune = dice.roll_random_table("1d20", chargen_tables["misfortune"])
|
||||
alignment = dice.roll_random_table("1d20", chargen_tables["alignment"])
|
||||
|
||||
self.desc = (
|
||||
f"You are {physique} with a {face} face, {skin} skin, {hair} hair, {speech} speech,"
|
||||
f" and {clothing} clothing. You were a {background.title()}, but you were"
|
||||
f" {misfortune} and ended up a knave. You are {virtue} but also {vice}. You are of the"
|
||||
f" {alignment} alignment."
|
||||
)
|
||||
|
||||
#
|
||||
self.hp_max = max(5, dice.roll("1d8"))
|
||||
self.hp = self.hp_max
|
||||
self.xp = 0
|
||||
self.level = 1
|
||||
|
||||
# random equipment
|
||||
self.armor = dice.roll_random_table("1d20", chargen_tables["armor"])
|
||||
|
||||
_helmet_and_shield = dice.roll_random_table("1d20", chargen_tables["helmets and shields"])
|
||||
self.helmet = "helmet" if "helmet" in _helmet_and_shield else "none"
|
||||
self.shield = "shield" if "shield" in _helmet_and_shield else "none"
|
||||
|
||||
self.weapon = dice.roll_random_table("1d20", chargen_tables["starting weapon"])
|
||||
|
||||
self.backpack = [
|
||||
"ration",
|
||||
"ration",
|
||||
dice.roll_random_table("1d20", chargen_tables["dungeoning gear"]),
|
||||
dice.roll_random_table("1d20", chargen_tables["dungeoning gear"]),
|
||||
dice.roll_random_table("1d20", chargen_tables["general gear 1"]),
|
||||
dice.roll_random_table("1d20", chargen_tables["general gear 2"]),
|
||||
]
|
||||
```
|
||||
|
||||
Here we have followed the _Knave_ rulebook to randomize abilities, description and equipment.
|
||||
The `dice.roll()` and `dice.roll_random_table` methods now become very useful! Everything here
|
||||
should be easy to follow.
|
||||
|
||||
The main difference from baseline _Knave_ is that we make a table of "starting weapon" (in Knave
|
||||
you can pick whatever you like).
|
||||
|
||||
We also initialize `.ability_changes = 0`. Knave only allows us to swap the values of two
|
||||
Abilities _once_. We will use this to know if it has been done or not.
|
||||
|
||||
### Showing the sheet
|
||||
|
||||
Now that we have our temporary character sheet, we should make it easy to visualize it.
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/chargen.py
|
||||
|
||||
_TEMP_SHEET = """
|
||||
{name}
|
||||
|
||||
STR +{strength}
|
||||
DEX +{dexterity}
|
||||
CON +{constitution}
|
||||
INT +{intelligence}
|
||||
WIS +{wisdom}
|
||||
CHA +{charisma}
|
||||
|
||||
{description}
|
||||
|
||||
Your belongings:
|
||||
{equipment}
|
||||
"""
|
||||
|
||||
class TemporaryCharacterSheet:
|
||||
|
||||
# ...
|
||||
|
||||
def show_sheet(self):
|
||||
equipment = (
|
||||
str(item)
|
||||
for item in [self.armor, self.helmet, self.shield, self.weapon] + self.backpack
|
||||
if item
|
||||
)
|
||||
|
||||
return _TEMP_SHEET.format(
|
||||
name=self.name,
|
||||
strength=self.strength,
|
||||
dexterity=self.dexterity,
|
||||
constitution=self.constitution,
|
||||
intelligence=self.intelligence,
|
||||
wisdom=self.wisdom,
|
||||
charisma=self.charisma,
|
||||
description=self.desc,
|
||||
equipment=", ".join(equipment),
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
The new `show_sheet` method collect the data from the temporary sheet and return it in a pretty
|
||||
form. Making a 'template' string like `_TEMP_SHEET` makes it easier to change things later if you want
|
||||
to change how things look.
|
||||
|
||||
### Apply character
|
||||
|
||||
Once we are happy with our character, we need to actually create it with the stats we chose.
|
||||
This is a bit more involved.
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/chargen.py
|
||||
|
||||
# ...
|
||||
|
||||
from .characters import EvAdventureCharacter
|
||||
from evennia import create_object
|
||||
from evennia.prototypes.spawner import spawn
|
||||
|
||||
|
||||
class TemporaryCharacterSheet:
|
||||
|
||||
# ...
|
||||
|
||||
def apply(self):
|
||||
# create character object with given abilities
|
||||
new_character = create_object(
|
||||
EvAdventureCharacter,
|
||||
key=self.name,
|
||||
attrs=(
|
||||
("strength", self.strength),
|
||||
("dexterity", self.dexterity),
|
||||
("constitution", self.constitution),
|
||||
("intelligence", self.intelligence),
|
||||
("wisdom", self.wisdom),
|
||||
("charisma", self.wisdom),
|
||||
("hp", self.hp),
|
||||
("hp_max", self.hp_max),
|
||||
("desc", self.desc),
|
||||
),
|
||||
)
|
||||
# spawn equipment (will require prototypes created before it works)
|
||||
if self.weapon:
|
||||
weapon = spawn(self.weapon)
|
||||
new_character.equipment.move(weapon)
|
||||
if self.shield:
|
||||
shield = spawn(self.shield)
|
||||
new_character.equipment.move(shield)
|
||||
if self.armor:
|
||||
armor = spawn(self.armor)
|
||||
new_character.equipment.move(armor)
|
||||
if self.helmet:
|
||||
helmet = spawn(self.helmet)
|
||||
new_character.equipment.move(helmet)
|
||||
|
||||
for item in self.backpack:
|
||||
item = spawn(item)
|
||||
new_character.equipment.store(item)
|
||||
|
||||
return new_character
|
||||
```
|
||||
|
||||
We use `create_object` to create a new `EvAdventureCharacter`. We feed it with all relevant data
|
||||
from the temporary character sheet. This is when these become an actual character.
|
||||
|
||||
```{sidebar}
|
||||
A prototype is basically a `dict` describing how the object should be created. Since
|
||||
it's just a piece of code, it can stored in a Python module and used to quickly _spawn_ (create)
|
||||
things from those prototypes.
|
||||
```
|
||||
|
||||
Each piece of equipment is an object in in its own right. We will here assume that all game
|
||||
items are defined as [Prototypes](../../../Components/Prototypes.md) keyed to its name, such as "sword", "brigandine
|
||||
armor" etc.
|
||||
|
||||
We haven't actually created those prototypes yet, so for now we'll need to assume they are there.
|
||||
Once a piece of equipment has been spawned, we make sure to move it into the `EquipmentHandler` we
|
||||
created in the [Equipment lesson](./Beginner-Tutorial-Equipment.md).
|
||||
|
||||
|
||||
## Initializing EvMenu
|
||||
|
||||
Evennia comes with a full menu-generation system based on [Command sets](../../../Components/Command-Sets.md), called
|
||||
[EvMenu](../../../Components/EvMenu.md).
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/chargen.py
|
||||
|
||||
from evennia import EvMenu
|
||||
|
||||
# ...
|
||||
|
||||
# chargen menu
|
||||
|
||||
|
||||
# this goes to the bottom of the module
|
||||
|
||||
def start_chargen(caller, session=None):
|
||||
"""
|
||||
This is a start point for spinning up the chargen from a command later.
|
||||
|
||||
"""
|
||||
|
||||
menutree = {} # TODO!
|
||||
|
||||
# this generates all random components of the character
|
||||
tmp_character = TemporaryCharacterSheet()
|
||||
|
||||
EvMenu(caller, menutree, session=session, tmp_character=tmp_character)
|
||||
|
||||
```
|
||||
|
||||
This first function is what we will call from elsewhere (for example from a custom `charcreate`
|
||||
command) to kick the menu into gear.
|
||||
|
||||
It takes the `caller` (the one to want to start the menu) and a `session` argument. The latter will help
|
||||
track just which client-connection we are using (depending on Evennia settings, you could be
|
||||
connecting with multiple clients).
|
||||
|
||||
We create a `TemporaryCharacterSheet` and call `.generate()` to make a random character. We then
|
||||
feed all this into `EvMenu`.
|
||||
|
||||
The moment this happens, the user will be in the menu, there are no further steps needed.
|
||||
|
||||
The `menutree` is what we'll create next. It describes which menu 'nodes' are available to jump
|
||||
between.
|
||||
|
||||
## Main Node: Choosing what to do
|
||||
|
||||
This is the first menu node. It will act as a central hub, from which one can choose different
|
||||
actions.
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/chargen.py
|
||||
|
||||
# ...
|
||||
|
||||
# at the end of the module, but before the `start_chargen` function
|
||||
|
||||
def node_chargen(caller, raw_string, **kwargs):
|
||||
|
||||
tmp_character = kwargs["tmp_character"]
|
||||
|
||||
text = tmp_character.show_sheet()
|
||||
|
||||
options = [
|
||||
{
|
||||
"desc": "Change your name",
|
||||
"goto": ("node_change_name", kwargs)
|
||||
}
|
||||
]
|
||||
if tmp_character.ability_changes <= 0:
|
||||
options.append(
|
||||
{
|
||||
"desc": "Swap two of your ability scores (once)",
|
||||
"goto": ("node_swap_abilities", kwargs),
|
||||
}
|
||||
)
|
||||
options.append(
|
||||
{
|
||||
"desc": "Accept and create character",
|
||||
"goto": ("node_apply_character", kwargs)
|
||||
},
|
||||
)
|
||||
|
||||
return text, options
|
||||
|
||||
# ...
|
||||
```
|
||||
|
||||
A lot to unpack here! In Evennia, it's convention to name your node-functions `node_*`. While
|
||||
not required, it helps you track what is a node and not.
|
||||
|
||||
Every menu-node, should accept `caller, raw_string, **kwargs` as arguments. Here `caller` is the
|
||||
`caller` you passed into the `EvMenu` call. `raw_string` is the input given by the user in order
|
||||
to _get to this node_, so currently empty. The `**kwargs` are all extra keyword arguments passed
|
||||
into `EvMenu`. They can also be passed between nodes. In this case, we passed the
|
||||
keyword `tmp_character` to `EvMenu`. We now have the temporary character sheet available in the
|
||||
node!
|
||||
|
||||
An `EvMenu` node must always return two things - `text` and `options`. The `text` is what will
|
||||
show to the user when looking at this node. The `options` are, well, what options should be
|
||||
presented to move on from here to some other place.
|
||||
|
||||
For the text, we simply get a pretty-print of the temporary character sheet. A single option is
|
||||
defined as a `dict` like this:
|
||||
|
||||
```python
|
||||
{
|
||||
"key": ("name". "alias1", "alias2", ...), # if skipped, auto-show a number
|
||||
"desc": "text to describe what happens when selecting option",.
|
||||
"goto": ("name of node or a callable", kwargs_to_pass_into_next_node_or_callable)
|
||||
}
|
||||
```
|
||||
|
||||
Multiple option-dicts are returned in a list or tuple. The `goto` option-key is important to
|
||||
understand. The job of this is to either point directly to another node (by giving its name), or
|
||||
by pointing to a Python callable (like a function) _that then returns that name_. You can also
|
||||
pass kwargs (as a dict). This will be made available as `**kwargs` in the callable or next node.
|
||||
|
||||
While an option can have a `key`, you can also skip it to just get a running number.
|
||||
|
||||
In our `node_chargen` node, we point to three nodes by name: `node_change_name`,
|
||||
`node_swap_abilities`, and `node_apply_character`. We also make sure to pass along `kwargs`
|
||||
to each node, since that contains our temporary character sheet.
|
||||
|
||||
The middle of these options only appear if we haven't already switched two abilities around - to
|
||||
know this, we check the `.ability_changes` property to make sure it's still 0.
|
||||
|
||||
|
||||
## Node: Changing your name
|
||||
|
||||
This is where you end up if you opted to change your name in `node_chargen`.
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/chargen.py
|
||||
|
||||
# ...
|
||||
|
||||
# after previous node
|
||||
|
||||
def _update_name(caller, raw_string, **kwargs):
|
||||
"""
|
||||
Used by node_change_name below to check what user
|
||||
entered and update the name if appropriate.
|
||||
|
||||
"""
|
||||
if raw_string:
|
||||
tmp_character = kwargs["tmp_character"]
|
||||
tmp_character.name = raw_string.lower().capitalize()
|
||||
|
||||
return "node_chargen", kwargs
|
||||
|
||||
|
||||
def node_change_name(caller, raw_string, **kwargs):
|
||||
"""
|
||||
Change the random name of the character.
|
||||
|
||||
"""
|
||||
tmp_character = kwargs["tmp_character"]
|
||||
|
||||
text = (
|
||||
f"Your current name is |w{tmp_character.name}|n. "
|
||||
"Enter a new name or leave empty to abort."
|
||||
)
|
||||
|
||||
options = {
|
||||
"key": "_default",
|
||||
"goto": (_update_name, kwargs)
|
||||
}
|
||||
|
||||
return text, options
|
||||
```
|
||||
|
||||
There are two functions here - the menu node itself (`node_change_name`) and a
|
||||
helper _goto_function_ (`_update_name`) to handle the user's input.
|
||||
|
||||
For the (single) option, we use a special `key` named `_default`. This makes this option
|
||||
a catch-all: If the user enters something that does not match any other option, this is
|
||||
the option that will be used.
|
||||
Since we have no other options here, we will always use this option no matter what the user enters.
|
||||
|
||||
Also note that the `goto` part of the option points to the `_update_name` callable rather than to
|
||||
the name of a node. It's important we keep passing `kwargs` along to it!
|
||||
|
||||
When a user writes anything at this node, the `_update_name` callable will be called. This has
|
||||
the same arguments as a node, but it is _not_ a node - we will only use it to _figure out_ which
|
||||
node to go to next.
|
||||
|
||||
In `_update_name` we now have a use for the `raw_string` argument - this is what was written by
|
||||
the user on the previous node, remember? This is now either an empty string (meaning to ignore
|
||||
it) or the new name of the character.
|
||||
|
||||
A goto-function like `_update_name` must return the name of the next node to use. It can also
|
||||
optionally return the `kwargs` to pass into that node - we want to always do this, so we don't
|
||||
loose our temporary character sheet. Here we will always go back to the `node_chargen`.
|
||||
|
||||
> Hint: If returning `None` from a goto-callable, you will always return to the last node you
|
||||
> were at.
|
||||
|
||||
## Node: Swapping Abilities around
|
||||
|
||||
You get here by selecting the second option from the `node_chargen` node.
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/chargen.py
|
||||
|
||||
# ...
|
||||
|
||||
# after previous node
|
||||
|
||||
_ABILITIES = {
|
||||
"STR": "strength",
|
||||
"DEX": "dexterity",
|
||||
"CON": "constitution",
|
||||
"INT": "intelligence",
|
||||
"WIS": "wisdom",
|
||||
"CHA": "charisma",
|
||||
}
|
||||
|
||||
|
||||
def _swap_abilities(caller, raw_string, **kwargs):
|
||||
"""
|
||||
Used by node_swap_abilities to parse the user's input and swap ability
|
||||
values.
|
||||
|
||||
"""
|
||||
if raw_string:
|
||||
abi1, *abi2 = raw_string.split(" ", 1)
|
||||
if not abi2:
|
||||
caller.msg("That doesn't look right.")
|
||||
return None, kwargs
|
||||
abi2 = abi2[0]
|
||||
abi1, abi2 = abi1.upper().strip(), abi2.upper().strip()
|
||||
if abi1 not in _ABILITIES or abi2 not in _ABILITIES:
|
||||
caller.msg("Not a familiar set of abilites.")
|
||||
return None, kwargs
|
||||
|
||||
# looks okay = swap values. We need to convert STR to strength etc
|
||||
tmp_character = kwargs["tmp_character"]
|
||||
abi1 = _ABILITIES[abi1]
|
||||
abi2 = _ABILITIES[abi2]
|
||||
abival1 = getattr(tmp_character, abi1)
|
||||
abival2 = getattr(tmp_character, abi2)
|
||||
|
||||
setattr(tmp_character, abi1, abival2)
|
||||
setattr(tmp_character, abi2, abival1)
|
||||
|
||||
tmp_character.ability_changes += 1
|
||||
|
||||
return "node_chargen", kwargs
|
||||
|
||||
|
||||
def node_swap_abilities(caller, raw_string, **kwargs):
|
||||
"""
|
||||
One is allowed to swap the values of two abilities around, once.
|
||||
|
||||
"""
|
||||
tmp_character = kwargs["tmp_character"]
|
||||
|
||||
text = f"""
|
||||
Your current abilities:
|
||||
|
||||
STR +{tmp_character.strength}
|
||||
DEX +{tmp_character.dexterity}
|
||||
CON +{tmp_character.constitution}
|
||||
INT +{tmp_character.intelligence}
|
||||
WIS +{tmp_character.wisdom}
|
||||
CHA +{tmp_character.charisma}
|
||||
|
||||
You can swap the values of two abilities around.
|
||||
You can only do this once, so choose carefully!
|
||||
|
||||
To swap the values of e.g. STR and INT, write |wSTR INT|n. Empty to abort.
|
||||
"""
|
||||
|
||||
options = {"key": "_default", "goto": (_swap_abilities, kwargs)}
|
||||
|
||||
return text, options
|
||||
```
|
||||
|
||||
This is more code, but the logic is the same - we have a node (`node_swap_abilities`) and
|
||||
and a goto-callable helper (`_swap_abilities`). We catch everything the user writes on the
|
||||
node (such as `WIS CON`) and feed it into the helper.
|
||||
|
||||
In `_swap_abilities`, we need to analyze the `raw_string` from the user to see what they
|
||||
want to do.
|
||||
|
||||
Most code in the helper is validating the user didn't enter nonsense. If they did,
|
||||
we use `caller.msg()` to tell them and then return `None, kwargs`, which re-runs the same node (the
|
||||
name-selection) all over again.
|
||||
|
||||
Since we want users to be able to write "CON" instead of the longer "constitution", we need a
|
||||
mapping `_ABILITIES` to easily convert between the two (it's stored as `consitution` on the
|
||||
temporary character sheet). Once we know which abilities they want to swap, we do so and tick up
|
||||
the `.ability_changes` counter. This means this option will no longer be available from the main
|
||||
node.
|
||||
|
||||
Finally, we return to `node_chargen` again.
|
||||
|
||||
## Node: Creating the Character
|
||||
|
||||
We get here from the main node by opting to finish chargen.
|
||||
|
||||
```python
|
||||
node_apply_character(caller, raw_string, **kwargs):
|
||||
"""
|
||||
End chargen and create the character. We will also puppet it.
|
||||
|
||||
"""
|
||||
tmp_character = kwargs["tmp_character"]
|
||||
new_character = tmp_character.apply(caller)
|
||||
|
||||
caller.account.db._playable_characters = [new_character]
|
||||
|
||||
text = "Character created!"
|
||||
|
||||
return text, None
|
||||
```
|
||||
When entering the node, we will take the Temporary character sheet and use its `.appy` method to
|
||||
create a new Character with all equipment.
|
||||
|
||||
This is what is called an _end node_, because it returns `None` instead of options. After this,
|
||||
the menu will exit. We will be back to the default character selection screen. The characters
|
||||
found on that screen are the ones listed in the `_playable_characters` Attribute, so we need to
|
||||
also the new character to it.
|
||||
|
||||
|
||||
## Tying the nodes together
|
||||
|
||||
```python
|
||||
def start_chargen(caller, session=None):
|
||||
"""
|
||||
This is a start point for spinning up the chargen from a command later.
|
||||
|
||||
"""
|
||||
menutree = { # <----- can now add this!
|
||||
"node_chargen": node_chargen,
|
||||
"node_change_name": node_change_name,
|
||||
"node_swap_abilities": node_swap_abilities,
|
||||
"node_apply_character": node_apply_character
|
||||
}
|
||||
|
||||
# this generates all random components of the character
|
||||
tmp_character = TemporaryCharacterSheet()
|
||||
tmp_character.generate()
|
||||
|
||||
EvMenu(caller, menutree, session=session,
|
||||
startnode="node_chargen", # <-----
|
||||
tmp_character=tmp_character)
|
||||
|
||||
```
|
||||
|
||||
Now that we have all the nodes, we add them to the `menutree` we left empty before. We only add
|
||||
the nodes, _not_ the goto-helpers! The keys we set in the `menutree` dictionary are the names we
|
||||
should use to point to nodes from inside the menu (and we did).
|
||||
|
||||
We also add a keyword argument `startnode` pointing to the `node_chargen` node. This tells EvMenu
|
||||
to first jump into that node when the menu is starting up.
|
||||
|
||||
## Conclusions
|
||||
|
||||
This lesson taught us how to use `EvMenu` to make an interactive character generator. In an RPG
|
||||
more complex than _Knave_, the menu would be bigger and more intricate, but the same principles
|
||||
apply.
|
||||
|
||||
Together with the previous lessons we have now fished most of the basics around player
|
||||
characters - how they store their stats, handle their equipment and how to create them.
|
||||
|
||||
In the next lesson we'll address how EvAdventure _Rooms_ work.
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
# In-game Commands
|
||||
|
||||
```{warning}
|
||||
This part of the Beginner tutorial is still being developed.
|
||||
```
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
# Dynamically generated Dungeon
|
||||
|
||||
```{warning}
|
||||
This part of the Beginner tutorial is still being developed.
|
||||
```
|
||||
|
|
@ -1,599 +0,0 @@
|
|||
# Handling Equipment
|
||||
|
||||
In _Knave_, you have a certain number of inventory "slots". The amount of slots is given by `CON + 10`.
|
||||
All items (except coins) have a `size`, indicating how many slots it uses. You can't carry more items
|
||||
than you have slot-space for. Also items wielded or worn count towards the slots.
|
||||
|
||||
We still need to track what the character is using however: What weapon they have readied affects the damage
|
||||
they can do. The shield, helmet and armor they use affects their defense.
|
||||
|
||||
We have already set up the possible 'wear/wield locations' when we defined our Objects
|
||||
[in the previous lesson](./Beginner-Tutorial-Objects.md). This is what we have in `enums.py`:
|
||||
|
||||
```python
|
||||
# mygame/evadventure/enums.py
|
||||
|
||||
# ...
|
||||
|
||||
class WieldLocation(Enum):
|
||||
|
||||
BACKPACK = "backpack"
|
||||
WEAPON_HAND = "weapon_hand"
|
||||
SHIELD_HAND = "shield_hand"
|
||||
TWO_HANDS = "two_handed_weapons"
|
||||
BODY = "body" # armor
|
||||
HEAD = "head" # helmets
|
||||
```
|
||||
|
||||
Basically, all the weapon/armor locations are exclusive - you can only have one item in each (or none).
|
||||
The BACKPACK is special - it contains any number of items (up to the maximum slot usage).
|
||||
|
||||
## EquipmentHandler that saves
|
||||
|
||||
> Create a new module `mygame/evadventure/equipment.py`.
|
||||
|
||||
```{sidebar}
|
||||
If you want to understand more about behind how Evennia uses handlers, there is a
|
||||
[dedicated tutorial](../../Tutorial-Persistent-Handler.md) talking about the principle.
|
||||
```
|
||||
In default Evennia, everything you pick up will end up "inside" your character object (that is, have
|
||||
you as its `.location`). This is called your _inventory_ and has no limit. We will keep 'moving items into us'
|
||||
when we pick them up, but we will add more functionality using an _Equipment handler_.
|
||||
|
||||
A handler is (for our purposes) an object that sits "on" another entity, containing functionality
|
||||
for doing one specific thing (managing equipment, in our case).
|
||||
|
||||
This is the start of our handler:
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/equipment.py
|
||||
|
||||
from .enums import WieldLocation
|
||||
|
||||
class EquipmentHandler:
|
||||
save_attribute = "inventory_slots"
|
||||
|
||||
def __init__(self, obj):
|
||||
# here obj is the character we store the handler on
|
||||
self.obj = obj
|
||||
self._load()
|
||||
|
||||
def _load(self):
|
||||
"""Load our data from an Attribute on `self.obj`"""
|
||||
self.slots = self.obj.attributes.get(
|
||||
self.save_attribute,
|
||||
category="inventory",
|
||||
default={
|
||||
WieldLocation.WEAPON_HAND: None,
|
||||
WieldLocation.SHIELD_HAND: None,
|
||||
WieldLocation.TWO_HANDS: None,
|
||||
WieldLocation.BODY: None,
|
||||
WieldLocation.HEAD: None,
|
||||
WieldLocation.BACKPACK: []
|
||||
}
|
||||
)
|
||||
|
||||
def _save(self):
|
||||
"""Save our data back to the same Attribute"""
|
||||
self.obj.attributes.add(self.save_attribute, self.slots, category="inventory")
|
||||
```
|
||||
|
||||
This is a compact and functional little handler. Before analyzing how it works, this is how
|
||||
we will add it to the Character:
|
||||
|
||||
```python
|
||||
# mygame/evadventure/characters.py
|
||||
|
||||
# ...
|
||||
|
||||
from evennia.utils.utils import lazy_property
|
||||
from .equipment import EquipmentHandler
|
||||
|
||||
# ...
|
||||
|
||||
class EvAdventureCharacter(LivingMixin, DefaultCharacter):
|
||||
|
||||
# ...
|
||||
|
||||
@lazy_property
|
||||
def equipment(self):
|
||||
return EquipmentHandler(self)
|
||||
```
|
||||
|
||||
After reloading the server, the equipment-handler will now be accessible on character-instances as
|
||||
|
||||
character.equipment
|
||||
|
||||
The `@lazy_property` works such that it will not load the handler until someone actually tries to
|
||||
fetch it with `character.equipment`. When that
|
||||
happens, we start up the handler and feed it `self` (the `Character` instance itself). This is what
|
||||
enters `__init__` as `.obj` in the `EquipmentHandler` code above.
|
||||
|
||||
So we now have a handler on the character, and the handler has a back-reference to the character it sits
|
||||
on.
|
||||
|
||||
Since the handler itself is just a regular Python object, we need to use the `Character` to store
|
||||
our data - our _Knave_ "slots". We must save them to the database, because we want the server to remember
|
||||
them even after reloading.
|
||||
|
||||
Using `self.obj.attributes.add()` and `.get()` we save the data to the Character in a specially named
|
||||
[Attribute](../../../Components/Attributes.md). Since we use a `category`, we are unlikely to collide with
|
||||
other Attributes.
|
||||
|
||||
Our storage structure is a `dict` with keys after our available `WieldLocation` enums. Each can only
|
||||
have one item except `WieldLocation.BACKPACK`, which is a list.
|
||||
|
||||
## Connecting the EquipmentHandler
|
||||
|
||||
Whenever an object leaves from one location to the next, Evennia will call a set of _hooks_ (methods) on the
|
||||
object that moves, on the source-location and on its destination. This is the same for all moving things -
|
||||
whether it's a character moving between rooms or an item being dropping from your hand to the ground.
|
||||
|
||||
We need to tie our new `EquipmentHandler` into this system. By reading the doc page on [Objects](../../../Components/Objects.md),
|
||||
or looking at the [DefaultObject.move_to](evennia.objects.objects.DefaultObject.move_to) docstring, we'll
|
||||
find out what hooks Evennia will call. Here `self` is the object being moved from
|
||||
`source_location` to `destination`:
|
||||
|
||||
|
||||
1. `self.at_pre_move(destination)` (abort if return False)
|
||||
2. `source_location.at_pre_object_leave(self, destination)` (abort if return False)
|
||||
3. `destination.at_pre_object_receive(self, source_location)` (abort if return False)
|
||||
4. `source_location.at_object_leave(self, destination)`
|
||||
5. `self.announce_move_from(destination)`
|
||||
6. (move happens here)
|
||||
7. `self.announce_move_to(source_location)`
|
||||
8. `destination.at_object_receive(self, source_location)`
|
||||
9. `self.at_post_move(source_location)`
|
||||
|
||||
All of these hooks can be overridden to customize movement behavior. In this case we are interested in
|
||||
controlling how items 'enter' and 'leave' our character - being 'inside' the character is the same as
|
||||
them 'carrying' it. We have three good hook-candidates to use for this.
|
||||
|
||||
- `.at_pre_object_receive` - used to check if you can actually pick something up, or if your equipment-store is full.
|
||||
- `.at_object_receive` - used to add the item to the equipmenthandler
|
||||
- `.at_object_leave` - used to remove the item from the equipmenthandler
|
||||
|
||||
You could also picture using `.at_pre_object_leave` to restrict dropping (cursed?) items, but
|
||||
we will skip that for this tutorial.
|
||||
|
||||
```python
|
||||
# mygame/evadventure/character.py
|
||||
|
||||
# ...
|
||||
|
||||
class EvAdventureCharacter(LivingMixin, DefaultCharacter):
|
||||
|
||||
# ...
|
||||
|
||||
def at_pre_object_receive(self, moved_object, source_location, **kwargs):
|
||||
"""Called by Evennia before object arrives 'in' this character (that is,
|
||||
if they pick up something). If it returns False, move is aborted.
|
||||
|
||||
"""
|
||||
return self.equipment.validate_slot_usage(moved_object)
|
||||
|
||||
def at_object_receive(self, moved_object, source_location, **kwargs):
|
||||
"""
|
||||
Called by Evennia when an object arrives 'in' the character.
|
||||
|
||||
"""
|
||||
self.equipment.add(moved_object)
|
||||
|
||||
def at_object_leave(self, moved_object, destination, **kwargs):
|
||||
"""
|
||||
Called by Evennia when object leaves the Character.
|
||||
|
||||
"""
|
||||
self.equipment.remove(moved_object)
|
||||
```
|
||||
|
||||
Above we have assumed the `EquipmentHandler` (`.equipment`) has methods `.validate_slot_usage`,
|
||||
`.add` and `.remove`. But we haven't actually added them yet - we just put some reasonable names! Before
|
||||
we can use this, we need to go actually adding those methods.
|
||||
|
||||
## Expanding the Equipmenthandler
|
||||
|
||||
## `.validate_slot_usage`
|
||||
|
||||
Let's start with implementing the first method we came up with above, `validate_slot_usage`:
|
||||
|
||||
```python
|
||||
# mygame/evadventure/equipment.py
|
||||
|
||||
from .enums import WieldLocation, Ability
|
||||
|
||||
class EquipmentError(TypeError):
|
||||
"""All types of equipment-errors"""
|
||||
pass
|
||||
|
||||
class EquipmentHandler:
|
||||
|
||||
# ...
|
||||
|
||||
@property
|
||||
def max_slots(self):
|
||||
"""Max amount of slots, based on CON defense (CON + 10)"""
|
||||
return getattr(self.obj, Ability.CON.value, 1) + 10
|
||||
|
||||
def count_slots(self):
|
||||
"""Count current slot usage"""
|
||||
slots = self.slots
|
||||
wield_usage = sum(
|
||||
getattr(slotobj, "size", 0) or 0
|
||||
for slot, slotobj in slots.items()
|
||||
if slot is not WieldLocation.BACKPACK
|
||||
)
|
||||
backpack_usage = sum(
|
||||
getattr(slotobj, "size", 0) or 0 for slotobj in slots[WieldLocation.BACKPACK]
|
||||
)
|
||||
return wield_usage + backpack_usage
|
||||
|
||||
def validate_slot_usage(self, obj):
|
||||
"""
|
||||
Check if obj can fit in equipment, based on its size.
|
||||
|
||||
"""
|
||||
if not inherits_from(obj, EvAdventureObject):
|
||||
# in case we mix with non-evadventure objects
|
||||
raise EquipmentError(f"{obj.key} is not something that can be equipped.")
|
||||
|
||||
size = obj.size
|
||||
max_slots = self.max_slots
|
||||
current_slot_usage = self.count_slots()
|
||||
return current_slot_usage + size <= max_slots:
|
||||
|
||||
```
|
||||
|
||||
```{sidebar}
|
||||
The `@property` decorator turns a method into a property so you don't need to 'call' it.
|
||||
That is, you can access `.max_slots` instead of `.max_slots()`. In this case, it's just a
|
||||
little less to type.
|
||||
```
|
||||
We add two helpers - the `max_slots` _property_ and `count_slots`, a method that calculate the current
|
||||
slots being in use. Let's figure out how they work.
|
||||
|
||||
### `.max_slots`
|
||||
|
||||
For `max_slots`, remember that `.obj` on the handler is a back-reference to the `EvAdventureCharacter` we
|
||||
put this handler on. `getattr` is a Python method for retrieving a named property on an object.
|
||||
The `Enum` `Ability.CON.value` is the string `Constitution` (check out the
|
||||
[first Utility and Enums tutorial](./Beginner-Tutorial-Utilities.md) if you don't recall).
|
||||
|
||||
So to be clear,
|
||||
|
||||
```python
|
||||
getattr(self.obj, Ability.CON.value) + 10
|
||||
```
|
||||
is the same as writing
|
||||
|
||||
```python
|
||||
getattr(your_character, "Constitution") + 10
|
||||
```
|
||||
|
||||
which is the same as doing something like this:
|
||||
|
||||
```python
|
||||
your_character.Constitution + 10
|
||||
```
|
||||
|
||||
In our code we write `getattr(self.obj, Ability.CON.value, 1)` - that extra `1` means that if there
|
||||
should happen to _not_ be a property "Constitution" on `self.obj`, we should not error out but just
|
||||
return 1.
|
||||
|
||||
|
||||
### `.count_slots`
|
||||
|
||||
In this helper we use two Python tools - the `sum()` function and a
|
||||
[list comprehension](https://www.w3schools.com/python/python_lists_comprehension.asp). The former
|
||||
simply adds the values of any iterable together. The latter is a more efficient way to create a list:
|
||||
|
||||
new_list = [item for item in some_iterable if condition]
|
||||
all_above_5 = [num for num in range(10) if num > 5] # [6, 7, 8, 9]
|
||||
all_below_5 = [num for num in range(10) if num < 5] # [0, 1, 2, 3, 4]
|
||||
|
||||
To make it easier to understand, try reading the last line above as "for every number in the range 0-9,
|
||||
pick all with a value below 5 and make a list of them". You can also embed such comprehensions
|
||||
directly in a function call like `sum()` without using `[]` around it.
|
||||
|
||||
In `count_slots` we have this code:
|
||||
|
||||
```python
|
||||
wield_usage = sum(
|
||||
getattr(slotobj, "size", 0)
|
||||
for slot, slotobj in slots.items()
|
||||
if slot is not WieldLocation.BACKPACK
|
||||
)
|
||||
```
|
||||
|
||||
We should be able to follow all except `slots.items()`. Since `slots` is a `dict`, we can use `.items()`
|
||||
to get a sequence of `(key, value)` pairs. We store these in `slot` and `slotobj`. So the above can
|
||||
be understood as "for every `slot` and `slotobj`-pair in `slots`, check which slot location it is.
|
||||
If it is _not_ in the backpack, get its size and add it to the list. Sum over all these
|
||||
sizes".
|
||||
|
||||
A less compact but maybe more readonable way to write this would be:
|
||||
|
||||
```python
|
||||
backpack_item_sizes = []
|
||||
for slot, slotobj in slots.items():
|
||||
if slot is not WieldLocation.BACKPACK:
|
||||
size = getattr(slotobj, "size", 0)
|
||||
backpack_item_sizes.append(size)
|
||||
wield_usage = sum(backpack_item_sizes)
|
||||
```
|
||||
|
||||
The same is done for the items actually in the BACKPACK slot. The total sizes are added
|
||||
together.
|
||||
|
||||
### Validating slots
|
||||
|
||||
With these helpers in place, `validate_slot_usage` now becomes simple. We use `max_slots` to see how much we can carry.
|
||||
We then get how many slots we are already using (with `count_slots`) and see if our new `obj`'s size
|
||||
would be too much for us.
|
||||
|
||||
## `.add` and `.remove`
|
||||
|
||||
We will make it so `.add` puts something in the `BACKPACK` location and `remove` drops it, wherever
|
||||
it is (even if it was in your hands).
|
||||
|
||||
```python
|
||||
# mygame/evadventure/equipment.py
|
||||
|
||||
from .enums import WieldLocation, Ability
|
||||
|
||||
# ...
|
||||
|
||||
class EquipmentHandler:
|
||||
|
||||
# ...
|
||||
|
||||
def add(self, obj):
|
||||
"""
|
||||
Put something in the backpack.
|
||||
"""
|
||||
self.validate_slot_usage(obj)
|
||||
self.slots[WieldLocation.BACKPACK].append(obj)
|
||||
self._save()
|
||||
|
||||
def remove(self, slot):
|
||||
"""
|
||||
Remove contents of a particular slot, for
|
||||
example `equipment.remove(WieldLocation.SHIELD_HAND)`
|
||||
"""
|
||||
slots = self.slots
|
||||
ret = []
|
||||
if slot is WieldLocation.BACKPACK:
|
||||
# empty entire backpack!
|
||||
ret.extend(slots[slot])
|
||||
slots[slot] = []
|
||||
else:
|
||||
ret.append(slots[slot])
|
||||
slots[slot] = None
|
||||
if ret:
|
||||
self._save()
|
||||
return ret
|
||||
```
|
||||
|
||||
Both of these should be straight forward to follow. In `.add`, we make use of `validate_slot_usage` to
|
||||
double-check we can actually fit the thing, then we add the item to the backpack.
|
||||
|
||||
In `.delete`, we allow emptying by `WieldLocation` - we figure out what slot it is and return
|
||||
the item within (if any). If we gave `BACKPACK` as the slot, we empty the backpack and
|
||||
return all items.
|
||||
|
||||
Whenever we change the equipment loadout we must make sure to `._save()` the result, or it will
|
||||
be lost after a server reload.
|
||||
|
||||
## Moving things around
|
||||
|
||||
With the help of `.remove()` and `.add()` we can get things in and out of the `BACKPACK` equipment
|
||||
location. We also need to grab stuff from the backpack and wield or wear it. We add a `.move` method
|
||||
on the `EquipmentHandler` to do this:
|
||||
|
||||
```python
|
||||
# mygame/evadventure/equipment.py
|
||||
|
||||
from .enums import WieldLocation, Ability
|
||||
|
||||
# ...
|
||||
|
||||
class EquipmentHandler:
|
||||
|
||||
# ...
|
||||
|
||||
def move(self, obj):
|
||||
"""Move object from backpack to its intended `inventory_use_slot`."""
|
||||
|
||||
# make sure to remove from equipment/backpack first, to avoid double-adding
|
||||
self.remove(obj)
|
||||
|
||||
slots = self.slots
|
||||
use_slot = getattr(obj, "inventory_use_slot", WieldLocation.BACKPACK)
|
||||
|
||||
to_backpack = []
|
||||
if use_slot is WieldLocation.TWO_HANDS:
|
||||
# two-handed weapons can't co-exist with weapon/shield-hand used items
|
||||
to_backpack = [slots[WieldLocation.WEAPON_HAND], slots[WieldLocation.SHIELD_HAND]]
|
||||
slots[WieldLocation.WEAPON_HAND] = slots[WieldLocation.SHIELD_HAND] = None
|
||||
slots[use_slot] = obj
|
||||
elif use_slot in (WieldLocation.WEAPON_HAND, WieldLocation.SHIELD_HAND):
|
||||
# can't keep a two-handed weapon if adding a one-handed weapon or shield
|
||||
to_backpack = [slots[WieldLocation.TWO_HANDS]]
|
||||
slots[WieldLocation.TWO_HANDS] = None
|
||||
slots[use_slot] = obj
|
||||
elif use_slot is WieldLocation.BACKPACK:
|
||||
# it belongs in backpack, so goes back to it
|
||||
to_backpack = [obj]
|
||||
else:
|
||||
# for others (body, head), just replace whatever's there
|
||||
replaced = [obj]
|
||||
slots[use_slot] = obj
|
||||
|
||||
for to_backpack_obj in to_backpack:
|
||||
# put stuff in backpack
|
||||
slots[use_slot].append(to_backpack_obj)
|
||||
|
||||
# store new state
|
||||
self._save()
|
||||
```
|
||||
|
||||
Here we remember that every `EvAdventureObject` has an `inventory_use_slot` property that tells us where
|
||||
it goes. So we just need to move the object to that slot, replacing whatever is in that place
|
||||
from before. Anything we replace goes back to the backpack.
|
||||
|
||||
## Get everything
|
||||
|
||||
In order to visualize our inventory, we need some method to get everything we are carrying.
|
||||
|
||||
|
||||
```python
|
||||
# mygame/evadventure/equipment.py
|
||||
|
||||
from .enums import WieldLocation, Ability
|
||||
|
||||
# ...
|
||||
|
||||
class EquipmentHandler:
|
||||
|
||||
# ...
|
||||
|
||||
def all(self):
|
||||
"""
|
||||
Get all objects in inventory, regardless of location.
|
||||
"""
|
||||
slots = self.slots
|
||||
lst = [
|
||||
(slots[WieldLocation.WEAPON_HAND], WieldLocation.WEAPON_HAND),
|
||||
(slots[WieldLocation.SHIELD_HAND], WieldLocation.SHIELD_HAND),
|
||||
(slots[WieldLocation.TWO_HANDS], WieldLocation.TWO_HANDS),
|
||||
(slots[WieldLocation.BODY], WieldLocation.BODY),
|
||||
(slots[WieldLocation.HEAD], WieldLocation.HEAD),
|
||||
] + [(item, WieldLocation.BACKPACK) for item in slots[WieldLocation.BACKPACK]]
|
||||
return lst
|
||||
```
|
||||
|
||||
Here we get all the equipment locations and add their contents together into a list of tuples
|
||||
`[(item, WieldLocation), ...]`. This is convenient for display.
|
||||
|
||||
## Weapon and armor
|
||||
|
||||
It's convenient to have the `EquipmentHandler` easily tell you what weapon is currently wielded
|
||||
and what _armor_ level all worn equipment provides. Otherwise you'd need to figure out what item is
|
||||
in which wield-slot and to add up armor slots manually every time you need to know.
|
||||
|
||||
|
||||
```python
|
||||
# mygame/evadventure/equipment.py
|
||||
|
||||
from .objects import WeaponEmptyHand
|
||||
from .enums import WieldLocation, Ability
|
||||
|
||||
# ...
|
||||
|
||||
class EquipmentHandler:
|
||||
|
||||
# ...
|
||||
|
||||
@property
|
||||
def armor(self):
|
||||
slots = self.slots
|
||||
return sum(
|
||||
(
|
||||
# armor is listed using its defense, so we remove 10 from it
|
||||
# (11 is base no-armor value in Knave)
|
||||
getattr(slots[WieldLocation.BODY], "armor", 1),
|
||||
# shields and helmets are listed by their bonus to armor
|
||||
getattr(slots[WieldLocation.SHIELD_HAND], "armor", 0),
|
||||
getattr(slots[WieldLocation.HEAD], "armor", 0),
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def weapon(self):
|
||||
# first checks two-handed wield, then one-handed; the two
|
||||
# should never appear simultaneously anyhow (checked in `move` method).
|
||||
slots = self.slots
|
||||
weapon = slots[WieldLocation.TWO_HANDS]
|
||||
if not weapon:
|
||||
weapon = slots[WieldLocation.WEAPON_HAND]
|
||||
if not weapon:
|
||||
weapon = WeaponEmptyHand()
|
||||
return weapon
|
||||
|
||||
```
|
||||
|
||||
In the `.armor()` method we get the item (if any) out of each relevant wield-slot (body, shield, head),
|
||||
and grab their `armor` Attribute. We then `sum()` them all up.
|
||||
|
||||
In `.weapon()`, we simply check which of the possible weapon slots (weapon-hand or two-hands) have
|
||||
something in them. If not we fall back to the 'fake' weapon `WeaponEmptyHand` which is just a 'dummy'
|
||||
object that represents your bare hands with damage and all.
|
||||
(created in [The Object tutorial](./Beginner-Tutorial-Objects.md#your-bare-hands) earlier).
|
||||
|
||||
|
||||
## Extra credits
|
||||
|
||||
This covers the basic functionality of the equipment handler. There are other useful methods that
|
||||
can be added:
|
||||
|
||||
- Given an item, figure out which equipment slot it is currently in
|
||||
- Make a string representing the current loadout
|
||||
- Get everything in the backpack (only)
|
||||
- Get all wieldable items (weapons, shields) from backpack
|
||||
- Get all usable items (items with a use-location of `BACKPACK`) from the backpack
|
||||
|
||||
Experiment with adding those. A full example is found in
|
||||
[evennia/contrib/tutorials/evadventure/equipment.py](../../../api/evennia.contrib.tutorials.evadventure.equipment.md).
|
||||
|
||||
## Unit Testing
|
||||
|
||||
> Create a new module `mygame/evadventure/tests/test_equipment.py`.
|
||||
|
||||
```{sidebar}
|
||||
See [evennia/contrib/tutorials/evadventure/tests/test_equipment.py](../../../api/evennia.contrib.tutorials.evadventure.tests.test_equipment.md)
|
||||
for a finished testing example.
|
||||
```
|
||||
|
||||
To test the `EquipmentHandler`, easiest is create an `EvAdventureCharacter` (this should by now
|
||||
have `EquipmentHandler` available on itself as `.equipment`) and a few test objects; then test
|
||||
passing these into the handler's methods.
|
||||
|
||||
|
||||
```python
|
||||
# mygame/evadventure/tests/test_equipment.py
|
||||
|
||||
from evennia.utils import create
|
||||
from evennia.utils.test_resources import BaseEvenniaTest
|
||||
|
||||
from ..objects import EvAdventureRoom
|
||||
from ..enums import WieldLocation
|
||||
|
||||
class TestEquipment(BaseEvenniaTest):
|
||||
|
||||
def setUp(self):
|
||||
self.character = create.create_object(EvAdventureCharacter, key='testchar')
|
||||
self.helmet = create.create_object(EvAdventureHelmet, key="helmet")
|
||||
self.weapon = create.create_object(EvAdventureWeapon, key="weapon")
|
||||
|
||||
def test_add_remove):
|
||||
self.character.equipment.add(self.helmet)
|
||||
self.assertEqual(
|
||||
self.character.equipment.slots[WieldLocation.BACKPACK],
|
||||
[self.helmet]
|
||||
)
|
||||
self.character.equipment.remove(self.helmet)
|
||||
self.assertEqual(self.character.equipment.slots[WieldLocation.BACKPACK], [])
|
||||
|
||||
# ...
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
_Handlers_ are useful for grouping functionality together. Now that we spent our time making the
|
||||
`EquipmentHandler`, we shouldn't need to worry about item-slots anymore - the handler 'handles' all
|
||||
the details for us. As long as we call its methods, the details can be forgotten about.
|
||||
|
||||
We also learned to use _hooks_ to tie _Knave_'s custom equipment handling into Evennia.
|
||||
|
||||
With `Characters`, `Objects` and now `Equipment` in place, we should be able to move on to character
|
||||
generation - where players get to make their own character!
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
# Non-Player-Characters (NPCs)
|
||||
|
||||
```{warning}
|
||||
This part of the Beginner tutorial is still being developed.
|
||||
```
|
||||
|
|
@ -1,360 +0,0 @@
|
|||
# In-game Objects and items
|
||||
|
||||
In the previous lesson we established what a 'Character' is in our game. Before we continue
|
||||
we also need to have a notion what an 'item' or 'object' is.
|
||||
|
||||
Looking at _Knave_'s item lists, we can get some ideas of what we need to track:
|
||||
|
||||
- `size` - this is how many 'slots' the item uses in the character's inventory.
|
||||
- `value` - a base value if we want to sell or buy the item.
|
||||
- `inventory_use_slot` - some items can be worn or wielded. For example, a helmet needs to be
|
||||
worn on the head and a shield in the shield hand. Some items can't be used this way at all, but
|
||||
only belong in the backpack.
|
||||
- `obj_type` - Which 'type' of item this is.
|
||||
|
||||
|
||||
## New Enums
|
||||
|
||||
We added a few enumberations for Abilities back in the [Utilities tutorial](./Beginner-Tutorial-Utilities.md).
|
||||
Before we continue, let's expand with enums for use-slots and object types.
|
||||
|
||||
```python
|
||||
# mygame/evadventure/enums.py
|
||||
|
||||
# ...
|
||||
|
||||
class WieldLocation(Enum):
|
||||
|
||||
BACKPACK = "backpack"
|
||||
WEAPON_HAND = "weapon_hand"
|
||||
SHIELD_HAND = "shield_hand"
|
||||
TWO_HANDS = "two_handed_weapons"
|
||||
BODY = "body" # armor
|
||||
HEAD = "head" # helmets
|
||||
|
||||
class ObjType(Enum):
|
||||
|
||||
WEAPON = "weapon"
|
||||
ARMOR = "armor"
|
||||
SHIELD = "shield"
|
||||
HELMET = "helmet"
|
||||
CONSUMABLE = "consumable"
|
||||
GEAR = "gear"
|
||||
MAGIC = "magic"
|
||||
QUEST = "quest"
|
||||
TREASURE = "treasure"
|
||||
```
|
||||
|
||||
Once we have these enums, we will use them for referencing things.
|
||||
|
||||
## The base object
|
||||
|
||||
> Create a new module `mygame/evadventure/objects.py`
|
||||
|
||||
```{sidebar}
|
||||
[evennia/contrib/tutorials/evadventure/objects.py](../../../api/evennia.contrib.tutorials.evadventure.objects.md) has
|
||||
a full set of objects implemented.
|
||||
```
|
||||
<div style="clear: right;"></div>
|
||||
|
||||
We will make a base `EvAdventureObject` class off Evennia's standard `DefaultObject`. We will then add
|
||||
child classes to represent the relevant types:
|
||||
|
||||
```python
|
||||
# mygame/evadventure/objects.py
|
||||
|
||||
from evennia import AttributeProperty, DefaultObject
|
||||
from evennia.utils.utils import make_iter
|
||||
from .utils import get_obj_stats
|
||||
from .enums import WieldLocation, ObjType
|
||||
|
||||
|
||||
class EvAdventureObject(DefaultObject):
|
||||
"""
|
||||
Base for all evadventure objects.
|
||||
|
||||
"""
|
||||
inventory_use_slot = WieldLocation.BACKPACK
|
||||
size = AttributeProperty(1, autocreate=False)
|
||||
value = AttributeProperty(0, autocreate=False)
|
||||
|
||||
# this can be either a single type or a list of types (for objects able to be
|
||||
# act as multiple). This is used to tag this object during creation.
|
||||
obj_type = ObjType.GEAR
|
||||
|
||||
def at_object_creation(self):
|
||||
"""Called when this object is first created. We convert the .obj_type
|
||||
property to a database tag."""
|
||||
|
||||
for obj_type in make_iter(self.obj_type):
|
||||
self.tags.add(self.obj_type.value, category="obj_type")
|
||||
|
||||
def get_help(self):
|
||||
"""Get any help text for this item"""
|
||||
return "No help for this item"
|
||||
```
|
||||
|
||||
### Using Attributes or not
|
||||
|
||||
In theory, `size` and `value` does not change and _could_ also be just set as a regular Python
|
||||
property on the class:
|
||||
|
||||
```python
|
||||
class EvAdventureObject(DefaultObject):
|
||||
inventory_use_slot = WieldLocation.BACKPACK
|
||||
size = 1
|
||||
value = 0
|
||||
```
|
||||
|
||||
The problem with this is that if we want to make a new object of `size 3` and `value 20`, we have to
|
||||
make a new class for it. We can't change it on the fly because the change would only be in memory and
|
||||
be lost on next server reload.
|
||||
|
||||
Because we use `AttributeProperties`, we can set `size` and `value` to whatever we like when we
|
||||
create the object (or later), and the Attributes will remember our changes to that object indefinitely.
|
||||
|
||||
To make this a little more efficient, we use `autocreate=False`. Normally when you create a
|
||||
new object with defined `AttributeProperties`, a matching `Attribute` is immediately created at
|
||||
the same time. So normally, the object would be created along with two Attributes `size` and `value`.
|
||||
With `autocreate=False`, no Attribute will be created _unless the default is changed_. That is, as
|
||||
long as your object has `size=1` no database `Attribute` will be created at all. This saves time and
|
||||
resources when creating large number of objects.
|
||||
|
||||
The drawback is that since no Attribute is created you can't refer to it
|
||||
with `obj.db.size` or `obj.attributes.get("size")` _unless you change its default_. You also can't query
|
||||
the database for all objects with `size=1`, since most objects would not yet have an in-database
|
||||
`size` Attribute to search for.
|
||||
|
||||
In our case, we'll only refer to these properties as `obj.size` etc, and have no need to find
|
||||
all objects of a particular size. So we should be safe.
|
||||
|
||||
### Creating tags in `at_object_creation`
|
||||
|
||||
The `at_object_creation` is a method Evennia calls on every child of `DefaultObject` whenever it is
|
||||
first created.
|
||||
|
||||
We do a tricky thing here, converting our `.obj_type` to one or more [Tags](../../../Components/Tags.md). Tagging the
|
||||
object like this means you can later efficiently find all objects of a given type (or combination of
|
||||
types) with Evennia's search functions:
|
||||
|
||||
```python
|
||||
from .enums import ObjType
|
||||
from evennia.utils import search
|
||||
|
||||
# get all shields in the game
|
||||
all_shields = search.search_object_by_tag(ObjType.SHIELD.value, category="obj_type")
|
||||
```
|
||||
|
||||
We allow `.obj_type` to be given as a single value or a list of values. We use `make_iter` from the
|
||||
evennia utility library to make sure we don't balk at either. This means you could have a Shield that
|
||||
is also Magical, for example.
|
||||
|
||||
## Other object types
|
||||
|
||||
Some of the other object types are very simple so far.
|
||||
|
||||
```python
|
||||
# mygame/evadventure/objects.py
|
||||
|
||||
from evennia import AttributeProperty, DefaultObject
|
||||
from .enums import ObjType
|
||||
|
||||
class EvAdventureObject(DefaultObject):
|
||||
# ...
|
||||
|
||||
|
||||
class EvAdventureQuestObject(EvAdventureObject):
|
||||
"""Quest objects should usually not be possible to sell or trade."""
|
||||
obj_type = ObjType.QUEST
|
||||
|
||||
class EvAdventureTreasure(EvAdventureObject):
|
||||
"""Treasure is usually just for selling for coin"""
|
||||
obj_type = ObjType.TREASURE
|
||||
value = AttributeProperty(100, autocreate=False)
|
||||
|
||||
```
|
||||
|
||||
## Consumables
|
||||
|
||||
A 'consumable' is an item that has a certain number of 'uses'. Once fully consumed, it can't be used
|
||||
anymore. An example would be a health potion.
|
||||
|
||||
|
||||
```python
|
||||
# mygame/evadventure/objects.py
|
||||
|
||||
# ...
|
||||
|
||||
class EvAdventureConsumable(EvAdventureObject):
|
||||
"""An item that can be used up"""
|
||||
|
||||
obj_type = ObjType.CONSUMABLE
|
||||
value = AttributeProperty(0.25, autocreate=False)
|
||||
uses = AttributeProperty(1, autocreate=False)
|
||||
|
||||
def at_pre_use(self, user, *args, **kwargs):
|
||||
"""Called before using. If returning False, abort use."""
|
||||
return uses > 0
|
||||
|
||||
def at_use(self, user, *args, **kwargs):
|
||||
"""Called when using the item"""
|
||||
pass
|
||||
|
||||
def at_post_use(self. user, *args, **kwargs):
|
||||
"""Called after using the item"""
|
||||
# detract a usage, deleting the item if used up.
|
||||
self.uses -= 1
|
||||
if self.uses <= 0:
|
||||
user.msg(f"{self.key} was used up.")
|
||||
self.delete()
|
||||
```
|
||||
|
||||
What exactly each consumable does will vary - we will need to implement children of this class
|
||||
later, overriding `at_use` with different effects.
|
||||
|
||||
## Weapons
|
||||
|
||||
All weapons need properties that describe how efficient they are in battle.
|
||||
|
||||
```python
|
||||
# mygame/evadventure/objects.py
|
||||
|
||||
from .enums import WieldLocation, ObjType, Ability
|
||||
|
||||
# ...
|
||||
|
||||
class EvAdventureWeapon(EvAdventureObject):
|
||||
"""Base class for all weapons"""
|
||||
|
||||
obj_type = ObjType.WEAPON
|
||||
inventory_use_slot = AttributeProperty(WieldLocation.WEAPON_HAND, autocreate=False)
|
||||
quality = AttributeProperty(3, autocreate=False)
|
||||
|
||||
attack_type = AttibuteProperty(Ability.STR, autocreate=False)
|
||||
defend_type = AttibuteProperty(Ability.ARMOR, autocreate=False)
|
||||
|
||||
damage_roll = AttibuteProperty("1d6", autocreate=False)
|
||||
```
|
||||
|
||||
The `quality` is something we need to track in _Knave_. When getting critical failures on attacks,
|
||||
a weapon's quality will go down. When it reaches 0, it will break.
|
||||
|
||||
The attack/defend type tracks how we resolve attacks with the weapon, like `roll + STR vs ARMOR + 10`.
|
||||
|
||||
## Magic
|
||||
|
||||
In _Knave_, anyone can use magic if they are wielding a rune stone (our name for spell books) in both
|
||||
hands. You can only use a rune stone once per rest. So a rune stone is an example of a 'magical weapon'
|
||||
that is also a 'consumable' of sorts.
|
||||
|
||||
|
||||
```python
|
||||
# mygame/evadventure/objects.py
|
||||
|
||||
# ...
|
||||
class EvAdventureConsumable(EvAdventureObject):
|
||||
# ...
|
||||
|
||||
class EvAdventureWeapon(EvAdventureObject):
|
||||
# ...
|
||||
|
||||
class EvAdventureRuneStone(EvAdventureWeapon, EvAdventureConsumable):
|
||||
"""Base for all magical rune stones"""
|
||||
|
||||
obj_type = (ObjType.WEAPON, ObjType.MAGIC)
|
||||
inventory_use_slot = WieldLocation.TWO_HANDS # always two hands for magic
|
||||
quality = AttributeProperty(3, autocreate=False)
|
||||
|
||||
attack_type = AttibuteProperty(Ability.INT, autocreate=False)
|
||||
defend_type = AttibuteProperty(Ability.DEX, autocreate=False)
|
||||
|
||||
damage_roll = AttibuteProperty("1d8", autocreate=False)
|
||||
|
||||
def at_post_use(self, user, *args, **kwargs):
|
||||
"""Called after usage/spell was cast"""
|
||||
self.uses -= 1
|
||||
# we don't delete the rune stone here, but
|
||||
# it must be reset on next rest.
|
||||
|
||||
def refresh(self):
|
||||
"""Refresh the rune stone (normally after rest)"""
|
||||
self.uses = 1
|
||||
```
|
||||
|
||||
We make the rune stone a mix of weapon and consumable. Note that we don't have to add `.uses`
|
||||
again, it's inherited from `EvAdventureConsumable` parent. The `at_pre_use` and `at_use` methods
|
||||
are also inherited; we only override `at_post_use` since we don't want the runestone to be deleted
|
||||
when it runs out of uses.
|
||||
|
||||
We add a little convenience method `refresh` - we should call this when the character rests, to
|
||||
make the runestone active again.
|
||||
|
||||
Exactly what rune stones _do_ will be implemented in the `at_use` methods of subclasses to this
|
||||
base class. Since magic in _Knave_ tends to be pretty custom, it makes sense that it will lead to a lot
|
||||
of custom code.
|
||||
|
||||
|
||||
## Armor
|
||||
|
||||
Armor, shields and helmets increase the `ARMOR` stat of the character. In _Knave_, what is stored is the
|
||||
defense value of the armor (values 11-20). We will instead store the 'armor bonus' (1-10). As we know,
|
||||
defending is always `bonus + 10`, so the result will be the same - this means
|
||||
we can use `Ability.ARMOR` as any other defensive ability without worrying about a special case.
|
||||
|
||||
``
|
||||
```python
|
||||
# mygame/evadventure/objects.py
|
||||
|
||||
# ...
|
||||
|
||||
class EvAdventureAmor(EvAdventureObject):
|
||||
obj_type = ObjType.ARMOR
|
||||
inventory_use_slot = WieldLocation.BODY
|
||||
|
||||
armor = AttributeProperty(1, autocreate=False)
|
||||
quality = AttributeProperty(3, autocreate=False)
|
||||
|
||||
|
||||
class EvAdventureShield(EvAdventureArmor):
|
||||
obj_type = ObjType.SHIELD
|
||||
inventory_use_slot = WieldLocation.SHIELD_HAND
|
||||
|
||||
|
||||
class EvAdventureHelmet(EvAdventureArmor):
|
||||
obj_type = ObjType.HELMET
|
||||
inventory_use_slot = WieldLocation.HEAD
|
||||
```
|
||||
|
||||
## Your Bare hands
|
||||
|
||||
This is a 'dummy' object that is not stored in the database. We will use this in the upcoming
|
||||
[Equipment tutorial lesson](./Beginner-Tutorial-Equipment.md) to represent when you have 'nothing'
|
||||
in your hands. This way we don't need to add any special case for this.
|
||||
|
||||
```python
|
||||
class WeaponEmptyHand:
|
||||
obj_type = ObjType.WEAPON
|
||||
key = "Empty Fists"
|
||||
inventory_use_slot = WieldLocation.WEAPON_HAND
|
||||
attack_type = Ability.STR
|
||||
defense_type = Ability.ARMOR
|
||||
damage_roll = "1d4"
|
||||
quality = 100000 # let's assume fists are always available ...
|
||||
|
||||
def __repr__(self):
|
||||
return "<WeaponEmptyHand>"
|
||||
```
|
||||
|
||||
## Testing and Extra credits
|
||||
|
||||
Remember the `get_obj_stats` function from the [Utility Tutorial](./Beginner-Tutorial-Utilities.md) earlier?
|
||||
We had to use dummy-values since we didn't yet know how we would store properties on Objects in the game.
|
||||
|
||||
Well, we just figured out all we need! You can go back and update `get_obj_stats` to properly read the data
|
||||
from the object it receives.
|
||||
|
||||
When you change this function you must also update the related unit test - so your existing test becomes a
|
||||
nice way to test your new Objects as well! Add more tests showing the output of feeding different object-types
|
||||
to `get_obj_stats`.
|
||||
|
||||
Try it out yourself. If you need help, a finished utility example is found in [evennia/contrib/tutorials/evadventure/utils.py](get_obj_stats).
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
# Part 3: How we get there
|
||||
|
||||
```{warning}
|
||||
The tutorial game is under development and is not yet complete, nor tested. Use the existing
|
||||
lessons as inspiration and to help get you going, but don't expect out-of-the-box perfection
|
||||
from it at this time.
|
||||
```
|
||||
|
||||
```{sidebar} Beginner Tutorial Parts
|
||||
- [Introduction](../Beginner-Tutorial-Intro.md)
|
||||
<br>Getting set up.
|
||||
- Part 1: [What we have](../Part1/Beginner-Tutorial-Part1-Intro.md)
|
||||
<br>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.md)
|
||||
<br>Planning our tutorial game and what to think about when planning your own in the future.
|
||||
- **Part 3: [How we get there](./Beginner-Tutorial-Part3-Intro.md)**
|
||||
<br>Getting down to the meat of extending Evennia to make our game
|
||||
- Part 4: [Using what we created](../Part4/Beginner-Tutorial-Part4-Intro.md)
|
||||
<br>Building a tech-demo and world content to go with our code
|
||||
- Part 5: [Showing the world](../Part5/Beginner-Tutorial-Part5-Intro.md)
|
||||
<br>Taking our new game online and let players try it out
|
||||
```
|
||||
|
||||
In part three of the Evennia Beginner tutorial we will go through the actual creation of
|
||||
our tutorial game _EvAdventure_, based on the [Knave](https://www.drivethrurpg.com/product/250888/Knave)
|
||||
RPG ruleset.
|
||||
|
||||
This is a big part. You'll be seeing a lot of code and there are plenty of lessons to go through.
|
||||
Take your time!
|
||||
|
||||
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 will
|
||||
create.
|
||||
|
||||
Even if this is not the game-style you are interested in, following along will give you a lot
|
||||
of experience using Evennia and be really helpful for doing your own thing later!
|
||||
|
||||
Fully coded examples of all code we make in this part can be found in the
|
||||
[evennia/contrib/tutorials/evadventure](../../../api/evennia.contrib.tutorials.evadventure.md) package.
|
||||
|
||||
## Lessons
|
||||
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 1
|
||||
|
||||
Beginner-Tutorial-Utilities
|
||||
Beginner-Tutorial-Rules
|
||||
Beginner-Tutorial-Characters
|
||||
Beginner-Tutorial-Objects
|
||||
Beginner-Tutorial-Equipment
|
||||
Beginner-Tutorial-Chargen
|
||||
Beginner-Tutorial-Rooms
|
||||
Beginner-Tutorial-NPCs
|
||||
Beginner-Tutorial-Turnbased-Combat
|
||||
Beginner-Tutorial-Quests
|
||||
Beginner-Tutorial-Shops
|
||||
Beginner-Tutorial-Dungeon
|
||||
Beginner-Tutorial-Commands
|
||||
```
|
||||
|
||||
## Table of Contents
|
||||
|
||||
```{toctree}
|
||||
|
||||
Beginner-Tutorial-Utilities
|
||||
Beginner-Tutorial-Rules
|
||||
Beginner-Tutorial-Characters
|
||||
Beginner-Tutorial-Objects
|
||||
Beginner-Tutorial-Equipment
|
||||
Beginner-Tutorial-Chargen
|
||||
Beginner-Tutorial-Rooms
|
||||
Beginner-Tutorial-NPCs
|
||||
Beginner-Tutorial-Turnbased-Combat
|
||||
Beginner-Tutorial-Quests
|
||||
Beginner-Tutorial-Shops
|
||||
Beginner-Tutorial-Dungeon
|
||||
Beginner-Tutorial-Commands
|
||||
```
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
# Game Quests
|
||||
|
||||
```{warning}
|
||||
This part of the Beginner tutorial is still being developed.
|
||||
```
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
# In-game Rooms
|
||||
|
||||
```{warning}
|
||||
This part of the Beginner tutorial is still being developed.
|
||||
```
|
||||
|
|
@ -1,633 +0,0 @@
|
|||
# Rules and dice rolling
|
||||
|
||||
In _EvAdventure_ we have decided to use the [Knave](https://www.drivethrurpg.com/product/250888/Knave)
|
||||
RPG ruleset. This is commercial, but released under Creative Commons 4.0, meaning it's okay to share and
|
||||
adapt _Knave_ for any purpose, even commercially. If you don't want to buy it but still follow
|
||||
along, you can find a [free fan-version here](http://abominablefancy.blogspot.com/2018/10/knaves-fancypants.html).
|
||||
|
||||
## Summary of _Knave_ rules
|
||||
|
||||
Knave, being inspired by early Dungeons & Dragons, is very simple.
|
||||
|
||||
- It uses six Ability bonuses
|
||||
_Strength_ (STR), _Dexterity_ (DEX), _Constitution_ (CON), _Intelligence_ (INT), _Wisdom_ (WIS)
|
||||
and _Charisma_ (CHA). These are rated from `+1` to `+10`.
|
||||
- Rolls are made with a twenty-sided die (`1d20`), usually adding a suitable Ability bonus to the roll.
|
||||
- If you roll _with advantage_, you roll `2d20` and pick the
|
||||
_highest_ value, If you roll _with disadvantage_, you roll `2d20` and pick the _lowest_.
|
||||
- Rolling a natural `1` is a _critical failure_. A natural `20` is a _critical success_. Rolling such
|
||||
in combat means your weapon or armor loses quality, which will eventually destroy it.
|
||||
- A _saving throw_ (trying to succeed against the environment) means making a roll to beat `15` (always).
|
||||
So if you are lifting a heavy stone and have `STR +2`, you'd roll `1d20 + 2` and hope the result
|
||||
is higher than `15`.
|
||||
- An _opposed saving throw_ means beating the enemy's suitable Ability 'defense', which is always their
|
||||
`Ability bonus + 10`. So if you have `STR +1` and are arm wrestling someone with `STR +2`, you roll
|
||||
`1d20 + 1` and hope to roll higher than `2 + 10 = 12`.
|
||||
- A special bonus is `Armor`, `+1` is unarmored, additional armor is given by equipment. Melee attacks
|
||||
test `STR` versus the `Armor` defense value while ranged attacks uses `WIS` vs `Armor`.
|
||||
- _Knave_ has no skills or classes. Everyone can use all items and using magic means having a special
|
||||
'rune stone' in your hands; one spell per stone and day.
|
||||
- A character has `CON + 10` carry 'slots'. Most normal items uses one slot, armor and large weapons uses
|
||||
two or three.
|
||||
- Healing is random, `1d8 + CON` health healed after food and sleep.
|
||||
- Monster difficulty is listed by hy many 1d8 HP they have; this is called their "hit die" or HD. If
|
||||
needing to test Abilities, monsters have HD bonus in every Ability.
|
||||
- Monsters have a _morale rating_. When things go bad, they have a chance to panic and flee if
|
||||
rolling `2d6` over their morale rating.
|
||||
- All Characters in _Knave_ are mostly randomly generated. HP is `<level>d8` but we give every
|
||||
new character max HP to start.
|
||||
- _Knave_ also have random tables, such as for starting equipment and to see if dying when
|
||||
hitting 0. Death, if it happens, is permanent.
|
||||
|
||||
|
||||
## Making a rule module
|
||||
|
||||
> Create a new module mygame/evadventure/rules.py
|
||||
|
||||
```{sidebar}
|
||||
A complete version of the rule module is found in
|
||||
[evennia/contrib/tutorials/evadventure/rules.py](../../../api/evennia.contrib.tutorials.evadventure.rules.md).
|
||||
```
|
||||
There are three broad sets of rules for most RPGS:
|
||||
|
||||
- Character generation rules, often only used during character creation
|
||||
- Regular gameplay rules - rolling dice and resolving game situations
|
||||
- Character improvement - getting and spending experience to improve the character
|
||||
|
||||
We want our `rules` module to cover as many aspeects of what we'd otherwise would have to look up
|
||||
in a rulebook.
|
||||
|
||||
|
||||
## Rolling dice
|
||||
|
||||
We will start by making a dice roller. Let's group all of our dice rolling into a structure like this
|
||||
(not functional code yet):
|
||||
|
||||
```python
|
||||
class EvAdventureRollEngine:
|
||||
|
||||
def roll(...):
|
||||
# get result of one generic roll, for any type and number of dice
|
||||
|
||||
def roll_with_advantage_or_disadvantage(...)
|
||||
# get result of normal d20 roll, with advantage/disadvantage (or not)
|
||||
|
||||
def saving_throw(...):
|
||||
# do a saving throw against a specific target number
|
||||
|
||||
def opposed_saving_throw(...):
|
||||
# do an opposed saving throw against a target's defense
|
||||
|
||||
def roll_random_table(...):
|
||||
# make a roll against a random table (loaded elsewere)
|
||||
|
||||
def morale_check(...):
|
||||
# roll a 2d6 morale check for a target
|
||||
|
||||
def heal_from_rest(...):
|
||||
# heal 1d8 when resting+eating, but not more than max value.
|
||||
|
||||
def roll_death(...):
|
||||
# roll to determine penalty when hitting 0 HP.
|
||||
|
||||
|
||||
dice = EvAdventureRollEngine()
|
||||
|
||||
```
|
||||
```{sidebar}
|
||||
This groups all dice-related code into one 'container' that is easy to import. But it's mostly a matter
|
||||
of taste. You _could_ also break up the class' methods into normal functions at the top-level of the
|
||||
module if you wanted.
|
||||
```
|
||||
|
||||
This structure (called a _singleton_) means we group all dice rolls into one class that we then initiate
|
||||
into a variable `dice` at the end of the module. This means that we can do the following from other
|
||||
modules:
|
||||
|
||||
```python
|
||||
from .rules import dice
|
||||
|
||||
dice.roll("1d8")
|
||||
```
|
||||
|
||||
### Generic dice roller
|
||||
|
||||
We want to be able to do `roll("1d20")` and get a random result back from the roll.
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/rules.py
|
||||
|
||||
from random import randint
|
||||
|
||||
class EvAdventureRollEngine:
|
||||
|
||||
def roll(self, roll_string):
|
||||
"""
|
||||
Roll XdY dice, where X is the number of dice
|
||||
and Y the number of sides per die.
|
||||
|
||||
Args:
|
||||
roll_string (str): A dice string on the form XdY.
|
||||
Returns:
|
||||
int: The result of the roll.
|
||||
|
||||
"""
|
||||
|
||||
# split the XdY input on the 'd' one time
|
||||
number, diesize = roll_string.split("d", 1)
|
||||
|
||||
# convert from string to integers
|
||||
number = int(number)
|
||||
diesize = int(diesize)
|
||||
|
||||
# make the roll
|
||||
return sum(randint(1, diesize) for _ in range(number))
|
||||
```
|
||||
|
||||
```{sidebar}
|
||||
For this tutorial we have opted to not use any contribs, so we create
|
||||
our own dice roller. But normally you could instead use the [dice](../../../Contribs/Contrib-Dice.md) contrib for this.
|
||||
We'll point out possible helpful contribs in sidebars as we proceed.
|
||||
```
|
||||
|
||||
The `randint` standard Python library module produces a random integer
|
||||
in a specific range. The line
|
||||
|
||||
```python
|
||||
sum(randint(1, diesize) for _ in range(number))
|
||||
```
|
||||
works like this:
|
||||
|
||||
- For a certain `number` of times ...
|
||||
- ... create a random integer between `1` and `diesize` ...
|
||||
- ... and `sum` all those integers together.
|
||||
|
||||
You could write the same thing less compactly like this:
|
||||
|
||||
```python
|
||||
rolls = []
|
||||
for _ in range(number):
|
||||
random_result = randint(1, diesize)
|
||||
rolls.append(random_result)
|
||||
return sum(rolls)
|
||||
```
|
||||
|
||||
```{sidebar}
|
||||
Note that `range` generates a value `0...number-1`. We use `_` in the `for` loop to
|
||||
indicate we don't really care what this value is - we just want to repeat the loop
|
||||
a certain amount of times.
|
||||
```
|
||||
|
||||
We don't ever expect end users to call this method; if we did, we would have to validate the inputs
|
||||
much more - We would have to make sure that `number` or `diesize` are valid inputs and not
|
||||
crazy big so the loop takes forever!
|
||||
|
||||
### Rolling with advantage
|
||||
|
||||
Now that we have the generic roller, we can start using it to do a more complex roll.
|
||||
|
||||
```
|
||||
# in mygame/evadventure/rules.py
|
||||
|
||||
# ...
|
||||
|
||||
class EvAdventureRollEngine:
|
||||
|
||||
def roll(roll_string):
|
||||
# ...
|
||||
|
||||
def roll_with_advantage_or_disadvantage(self, advantage=False, disadvantage=False):
|
||||
|
||||
if not (advantage or disadvantage) or (advantage and disadvantage):
|
||||
# normal roll - advantage/disadvantage not set or they cancel
|
||||
# each other out
|
||||
return self.roll("1d20")
|
||||
elif advantage:
|
||||
# highest of two d20 rolls
|
||||
return max(self.roll("1d20"), self.roll("1d20"))
|
||||
else:
|
||||
# disadvantage - lowest of two d20 rolls
|
||||
return min(self.roll("1d20"), self.roll("1d20"))
|
||||
```
|
||||
|
||||
The `min()` and `max()` functions are standard Python fare for getting the biggest/smallest
|
||||
of two arguments.
|
||||
|
||||
### Saving throws
|
||||
|
||||
We want the saving throw to itself figure out if it succeeded or not. This means it needs to know
|
||||
the Ability bonus (like STR `+1`). It would be convenient if we could just pass the entity
|
||||
doing the saving throw to this method, tell it what type of save was needed, and then
|
||||
have it figure things out:
|
||||
|
||||
```python
|
||||
result, quality = dice.saving_throw(character, Ability.STR)
|
||||
```
|
||||
The return will be a boolean `True/False` if they pass, as well as a `quality` that tells us if
|
||||
a perfect fail/success was rolled or not.
|
||||
|
||||
To make the saving throw method this clever, we need to think some more about how we want to store our
|
||||
data on the character.
|
||||
|
||||
For our purposes it sounds reasonable that we will be using [Attributes](../../../Components/Attributes.md) for storing
|
||||
the Ability scores. To make it easy, we will name them the same as the
|
||||
[Enum values](./Beginner-Tutorial-Utilities.md#enums) we set up in the previous lesson. So if we have
|
||||
an enum `STR = "strength"`, we want to store the Ability on the character as an Attribute `strength`.
|
||||
|
||||
From the Attribute documentation, we can see that we can use `AttributeProperty` to make it so the
|
||||
Attribute is available as `character.strength`, and this is what we will do.
|
||||
|
||||
So, in short, we'll create the saving throws method with the assumption that we will be able to do
|
||||
`character.strength`, `character.constitution`, `character.charisma` etc to get the relevant Abilities.
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/rules.py
|
||||
# ...
|
||||
from .enums import Ability
|
||||
|
||||
class EvAdventureRollEngine:
|
||||
|
||||
def roll(...)
|
||||
# ...
|
||||
|
||||
def roll_with_advantage_or_disadvantage(...)
|
||||
# ...
|
||||
|
||||
def saving_throw(self, character, bonus_type=Ability.STR, target=15,
|
||||
advantage=False, disadvantage=False):
|
||||
"""
|
||||
Do a saving throw, trying to beat a target.
|
||||
|
||||
Args:
|
||||
character (Character): A character (assumed to have Ability bonuses
|
||||
stored on itself as Attributes).
|
||||
bonus_type (Ability): A valid Ability bonus enum.
|
||||
target (int): The target number to beat. Always 15 in Knave.
|
||||
advantage (bool): If character has advantage on this roll.
|
||||
disadvantage (bool): If character has disadvantage on this roll.
|
||||
|
||||
Returns:
|
||||
tuple: A tuple (bool, Ability), showing if the throw succeeded and
|
||||
the quality is one of None or Ability.CRITICAL_FAILURE/SUCCESS
|
||||
|
||||
"""
|
||||
|
||||
# make a roll
|
||||
dice_roll = self.roll_with_advantage_or_disadvantage(advantage, disadvantage)
|
||||
|
||||
# figure out if we had critical failure/success
|
||||
quality = None
|
||||
if dice_roll == 1:
|
||||
quality = Ability.CRITICAL_FAILURE
|
||||
elif dice_roll == 20:
|
||||
quality = Ability.CRITICAL_SUCCESS
|
||||
|
||||
# figure out bonus
|
||||
bonus = getattr(character, bonus_type.value, 1)
|
||||
|
||||
# return a tuple (bool, quality)
|
||||
return (dice_roll + bonus) > target, quality
|
||||
```
|
||||
|
||||
The `getattr(obj, attrname, default)` function is a very useful Python tool for getting an attribute
|
||||
off an object and getting a default value if the attribute is not defined.
|
||||
|
||||
### Opposed saving throw
|
||||
|
||||
With the building pieces we already created, this method is simple. Remember that the defense you have
|
||||
to beat is always the relevant bonus + 10 in _Knave_. So if the enemy defends with `STR +3`, you must
|
||||
roll higher than `13`.
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/rules.py
|
||||
|
||||
from .enums import Ability
|
||||
|
||||
class EvAdventureRollEngine:
|
||||
|
||||
def roll(...):
|
||||
# ...
|
||||
|
||||
def roll_with_advantage_or_disadvantage(...):
|
||||
# ...
|
||||
|
||||
def saving_throw(...):
|
||||
# ...
|
||||
|
||||
def opposed_saving_throw(self, attacker, defender,
|
||||
attack_type=Ability.STR, defense_type=Ability.ARMOR,
|
||||
advantage=False, disadvantage=False):
|
||||
defender_defense = getattr(defender, defense_type.value, 1) + 10
|
||||
result, quality = self.saving_throw(attacker, bonus_type=attack_type,
|
||||
target=defender_defense,
|
||||
advantage=advantave, disadvantage=disadvantage)
|
||||
|
||||
return result, quality
|
||||
```
|
||||
|
||||
### Morale check
|
||||
|
||||
We will make the assumption that the `morale` value is available from the creature simply as
|
||||
`monster.morale` - we need to remember to make this so later!
|
||||
|
||||
In _Knave_, a creature have roll with `2d6` equal or under its morale to not flee or surrender
|
||||
when things go south. The standard morale value is 9.
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/rules.py
|
||||
|
||||
class EvAdventureRollEngine:
|
||||
|
||||
# ...
|
||||
|
||||
def morale_check(self, defender):
|
||||
return self.roll("2d6") <= getattr(defender, "morale", 9)
|
||||
|
||||
```
|
||||
|
||||
### Roll for Healing
|
||||
|
||||
To be able to handle healing, we need to make some more assumptions about how we store
|
||||
health on game entities. We will need `hp_max` (the total amount of available HP) and `hp`
|
||||
(the current health value). We again assume these will be available as `obj.hp` and `obj.hp_max`.
|
||||
|
||||
According to the rules, after consuming a ration and having a full night's sleep, a character regains
|
||||
`1d8 + CON` HP.
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/rules.py
|
||||
|
||||
from .enums import Ability
|
||||
|
||||
class EvAdventureRollEngine:
|
||||
|
||||
# ...
|
||||
|
||||
def heal_from_rest(self, character):
|
||||
"""
|
||||
A night's rest retains 1d8 + CON HP
|
||||
|
||||
"""
|
||||
con_bonus = getattr(character, Ability.CON.value, 1)
|
||||
character.heal(self.roll("1d8") + con_bonus)
|
||||
```
|
||||
|
||||
We make another assumption here - that `character.heal()` is a thing. We tell this function how
|
||||
much the character should heal, and it will do so, making sure to not heal more than its max
|
||||
number of HPs
|
||||
|
||||
> Knowing what is available on the character and what rule rolls we need is a bit of a chicken-and-egg
|
||||
> problem. We will make sure to implement the matching _Character_ class next lesson.
|
||||
|
||||
|
||||
### Rolling on a table
|
||||
|
||||
We occasionally need to roll on a 'table' - a selection of choices. There are two main table-types
|
||||
we need to support:
|
||||
|
||||
Simply one element per row of the table (same odds to get each result).
|
||||
|
||||
| Result |
|
||||
|:------:|
|
||||
| item1 |
|
||||
| item2 |
|
||||
| item3 |
|
||||
| item4 |
|
||||
|
||||
This we will simply represent as a plain list
|
||||
|
||||
```python
|
||||
["item1", "item2", "item3", "item4"]
|
||||
```
|
||||
|
||||
Ranges per item (varying odds per result):
|
||||
|
||||
| Range | Result |
|
||||
|:-----:|:------:|
|
||||
| 1-5 | item1 |
|
||||
| 6-15 | item2 |
|
||||
| 16-19 | item3 |
|
||||
| 20 | item4 |
|
||||
|
||||
This we will represent as a list of tuples:
|
||||
|
||||
```python
|
||||
[("1-5", "item1"), ("6-15", "item2"), ("16-19", "item4"), ("20", "item5")]
|
||||
```
|
||||
|
||||
We also need to know what die to roll to get a result on the table (it may not always
|
||||
be obvious, and in some games you could be asked to roll a lower dice to only get
|
||||
early table results, for example).
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/rules.py
|
||||
|
||||
from random import randint, choice
|
||||
|
||||
class EvAdventureRollEngine:
|
||||
|
||||
# ...
|
||||
|
||||
def roll_random_table(self, dieroll, table_choices):
|
||||
"""
|
||||
Args:
|
||||
dieroll (str): A die roll string, like "1d20".
|
||||
table_choices (iterable): A list of either single elements or
|
||||
of tuples.
|
||||
Returns:
|
||||
Any: A random result from the given list of choices.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If rolling dice giving results outside the table.
|
||||
|
||||
"""
|
||||
roll_result = self.roll(dieroll)
|
||||
|
||||
if isinstance(table_choices[0], (tuple, list)):
|
||||
# the first element is a tuple/list; treat as on the form [("1-5", "item"),...]
|
||||
for (valrange, choice) in table_choices:
|
||||
minval, *maxval = valrange.split("-", 1)
|
||||
minval = abs(int(minval))
|
||||
maxval = abs(int(maxval[0]) if maxval else minval)
|
||||
|
||||
if minval <= roll_result <= maxval:
|
||||
return choice
|
||||
|
||||
# if we get here we must have set a dieroll producing a value
|
||||
# outside of the table boundaries - raise error
|
||||
raise RuntimeError("roll_random_table: Invalid die roll")
|
||||
else:
|
||||
# a simple regular list
|
||||
roll_result = max(1, min(len(table_choices), roll_result))
|
||||
return table_choices[roll_result - 1]
|
||||
```
|
||||
Check that you understand what this does.
|
||||
|
||||
This may be confusing:
|
||||
```python
|
||||
minval, *maxval = valrange.split("-", 1)
|
||||
minval = abs(int(minval))
|
||||
maxval = abs(int(maxval[0]) if maxval else minval)
|
||||
```
|
||||
|
||||
If `valrange` is the string `1-5`, then `valrange.split("-", 1)` would result in a tuple `("1", "5")`.
|
||||
But if the string was in fact just `"20"` (possible for a single entry in an RPG table), this would
|
||||
lead to an error since it would only split out a single element - and we expected two.
|
||||
|
||||
By using `*maxval` (with the `*`), `maxval` is told to expect _0 or more_ elements in a tuple.
|
||||
So the result for `1-5` will be `("1", ("5",))` and for `20` it will become `("20", ())`. In the line
|
||||
|
||||
```python
|
||||
maxval = abs(int(maxval[0]) if maxval else minval)
|
||||
```
|
||||
|
||||
we check if `maxval` actually has a value `("5",)` or if its empty `()`. The result is either
|
||||
`"5"` or the value of `minval`.
|
||||
|
||||
|
||||
### Roll for death
|
||||
|
||||
While original Knave suggests hitting 0 HP means insta-death, we will grab the optional "death table"
|
||||
from the "prettified" Knave's optional rules to make it a little less punishing. We also changed the
|
||||
result of `2` to 'dead' since we don't simulate 'dismemberment' in this tutorial:
|
||||
|
||||
| Roll | Result | -1d4 Loss of Ability |
|
||||
|:---: |:--------:|:--------------------:|
|
||||
| 1-2 | dead | -
|
||||
| 3 | weakened | STR |
|
||||
|4 | unsteady | DEX |
|
||||
| 5 | sickly | CON |
|
||||
| 6 | addled | INT |
|
||||
| 7 | rattled | WIS |
|
||||
| 8 | disfigured | CHA |
|
||||
|
||||
All the non-dead values map to a loss of 1d4 in one of the six Abilities (but you get HP back).
|
||||
We need to map back to this from the above table. One also cannot have less than -10 Ability bonus,
|
||||
if you do, you die too.
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/rules.py
|
||||
|
||||
death_table = (
|
||||
("1-2", "dead"),
|
||||
("3": "strength",
|
||||
("4": "dexterity"),
|
||||
("5": "constitution"),
|
||||
("6": "intelligence"),
|
||||
("7": "wisdom"),
|
||||
("8": "charisma"),
|
||||
)
|
||||
|
||||
|
||||
class EvAdventureRollEngine:
|
||||
|
||||
# ...
|
||||
|
||||
def roll_random_table(...)
|
||||
# ...
|
||||
|
||||
def roll_death(self, character):
|
||||
ability_name = self.roll_random_table("1d8", death_table)
|
||||
|
||||
if ability_name == "dead":
|
||||
# TODO - kill the character!
|
||||
pass
|
||||
else:
|
||||
loss = self.roll("1d4")
|
||||
|
||||
current_ability = getattr(character, ability_name)
|
||||
current_ability -= loss
|
||||
|
||||
if current_ability < -10:
|
||||
# TODO - kill the character!
|
||||
pass
|
||||
else:
|
||||
# refresh 1d4 health, but suffer 1d4 ability loss
|
||||
self.heal(character, self.roll("1d4")
|
||||
setattr(character, ability_name, current_ability)
|
||||
|
||||
character.msg(
|
||||
"You survive your brush with death, and while you recover "
|
||||
f"some health, you permanently lose {loss} {ability_name} instead."
|
||||
)
|
||||
|
||||
dice = EvAdventureRollEngine()
|
||||
```
|
||||
|
||||
Here we roll on the 'death table' from the rules to see what happens. We give the character
|
||||
a message if they survive, to let them know what happened.
|
||||
|
||||
We don't yet know what 'killing the character' technically means, so we mark this as `TODO` and
|
||||
return to it in a later lesson. We just know that we need to do _something_ here to kill off the
|
||||
character!
|
||||
|
||||
## Testing
|
||||
|
||||
> Make a new module `mygame/evadventure/tests/test_rules.py`
|
||||
|
||||
Testing the `rules` module will also showcase some very useful tools when testing.
|
||||
|
||||
```python
|
||||
# mygame/evadventure/tests/test_rules.py
|
||||
|
||||
from unittest.mock import patch
|
||||
from evennia.utils.test_resources import BaseEvenniaTest
|
||||
from .. import rules
|
||||
|
||||
class TestEvAdventureRuleEngine(BaseEvenniaTest):
|
||||
|
||||
def setUp(self):
|
||||
"""Called before every test method"""
|
||||
super().setUp()
|
||||
self.roll_engine = rules.EvAdventureRollEngine()
|
||||
|
||||
@patch("evadventure.rules.randint")
|
||||
def test_roll(self, mock_randint):
|
||||
mock_randint.return_value = 4
|
||||
self.assertEqual(self.roll_engine.roll("1d6", 4)
|
||||
self.assertEqual(self.roll_engine.roll("2d6", 2 * 4)
|
||||
|
||||
# test of the other rule methods below ...
|
||||
```
|
||||
|
||||
As before, run the specific test with
|
||||
|
||||
evennia test --settings settings.py .evadventure.tests.test_rules
|
||||
|
||||
### Mocking and patching
|
||||
|
||||
```{sidebar}
|
||||
In [evennia/contrib/tutorials/evadventure/tests/test_rules.py](../../../api/evennia.contrib.tutorials.evadventure.tests.test_rules.md)
|
||||
has a complete example of rule testing.
|
||||
```
|
||||
The `setUp` method is a special method of the testing class. It will be run before every
|
||||
test method. We use `super().setUp()` to make sure the parent class' version of this method
|
||||
always fire. Then we create a fresh `EvAdventureRollEngine` we can test with.
|
||||
|
||||
In our test, we import `patch` from the `unittest.mock` library. This is a very useful tool for testing.
|
||||
Normally the `randint` function we imported in `rules` will return a random value. That's very hard to
|
||||
test for, since the value will be different every test.
|
||||
|
||||
With `@patch` (this is called a _decorator_), we temporarily replace `rules.randint` with a 'mock' - a
|
||||
dummy entity. This mock is passed into the testing method. We then take this `mock_randint` and set
|
||||
`.return_value = 4` on it.
|
||||
|
||||
Adding `return_value` to the mock means that every time this mock is called, it will return 4. For the
|
||||
duration of the test we can now check with `self.assertEqual` that our `roll` method always returns a
|
||||
result as-if the random result was 4.
|
||||
|
||||
There are [many resources for understanding mock](https://realpython.com/python-mock-library/), refer to
|
||||
them for further help.
|
||||
|
||||
> The `EvAdventureRollEngine` have many methods to test. We leave this as an extra exercise!
|
||||
|
||||
## Summary
|
||||
|
||||
This concludes all the core rule mechanics of _Knave_ - the rules used during play. We noticed here
|
||||
that we are going to soon need to establish how our _Character_ actually stores data. So we will
|
||||
address that next.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
# In-game Shops
|
||||
|
||||
```{warning}
|
||||
This part of the Beginner tutorial is still being developed.
|
||||
```
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
# Turn-based combat
|
||||
|
||||
```{warning}
|
||||
This part of the Beginner tutorial is still being developed.
|
||||
```
|
||||
|
|
@ -1,322 +0,0 @@
|
|||
# Code structure and Utilities
|
||||
|
||||
In this lesson we will set up the file structure for _EvAdventure_. We will make some
|
||||
utilities that will be useful later. We will also learn how to write _tests_.
|
||||
|
||||
## Folder structure
|
||||
|
||||
Create a new folder under your `mygame` folder, named `evadventure`. Inside it, create
|
||||
another folder `tests/` and make sure to put empty `__init__.py` files in both. This turns both
|
||||
folders into packages Python understands to import from.
|
||||
|
||||
```
|
||||
mygame/
|
||||
commands/
|
||||
evadventure/ <---
|
||||
__init__.py <---
|
||||
tests/ <---
|
||||
__init__.py <---
|
||||
__init__.py
|
||||
README.md
|
||||
server/
|
||||
typeclasses/
|
||||
web/
|
||||
world/
|
||||
|
||||
```
|
||||
|
||||
Importing anything from inside this folder from anywhere else under `mygame` will be done by
|
||||
|
||||
```python
|
||||
# from anywhere in mygame/
|
||||
from evadventure.yourmodulename import whatever
|
||||
```
|
||||
|
||||
This is the 'absolute path` type of import.
|
||||
|
||||
Between two modules both in `evadventure/`, you can use a 'relative' import with `.`:
|
||||
|
||||
```python
|
||||
# from a module inside mygame/evadventure
|
||||
from .yourmodulename import whatever
|
||||
```
|
||||
|
||||
From e.g. inside `mygame/evadventure/tests/` you can import from one level above using `..`:
|
||||
|
||||
```python
|
||||
# from mygame/evadventure/tests/
|
||||
from ..yourmodulename import whatever
|
||||
```
|
||||
|
||||
|
||||
## Enums
|
||||
|
||||
```{sidebar}
|
||||
A full example of the enum module is found in
|
||||
[evennia/contrib/tutorials/evadventure/enums.py](../../../api/evennia.contrib.tutorials.evadventure.enums.md).
|
||||
```
|
||||
Create a new file `mygame/evadventure/enums.py`.
|
||||
|
||||
An [enum](https://docs.python.org/3/library/enum.html) (enumeration) is a way to establish constants
|
||||
in Python. Best is to show an example:
|
||||
|
||||
```python
|
||||
# in a file mygame/evadventure/enums.py
|
||||
|
||||
from enum import Enum
|
||||
|
||||
class Ability(Enum):
|
||||
|
||||
STR = "strength"
|
||||
|
||||
```
|
||||
|
||||
You access an enum like this:
|
||||
|
||||
```
|
||||
# from another module in mygame/evadventure
|
||||
|
||||
from .enums import Ability
|
||||
|
||||
Ability.STR # the enum itself
|
||||
Ability.STR.value # this is the string "strength"
|
||||
|
||||
```
|
||||
|
||||
Having enums is recommended practice. With them set up, it means we can make sure to refer to the
|
||||
same thing every time. Having all enums in one place also means you have a good overview of the
|
||||
constants you are dealing with.
|
||||
|
||||
The alternative would be to for example pass around a string `"constitution"`. If you mis-spell
|
||||
this (`"consitution"`), you would not necessarily know it right away - the error would happen later
|
||||
when the string is not recognized. If you make a typo getting `Ability.COM` instead of `Ability.CON`,
|
||||
Python will immediately raise an error since this enum is not recognized.
|
||||
|
||||
With enums you can also do nice direct comparisons like `if ability is Ability.WIS: <do stuff>`.
|
||||
|
||||
Note that the `Ability.STR` enum does not have the actual _value_ of e.g. your Strength.
|
||||
It's just a fixed label for the Strength ability.
|
||||
|
||||
Here is the `enum.py` module needed for _Knave_. It covers the basic aspects of
|
||||
rule systems we need to track (check out the _Knave_ rules. If you use another rule system you'll
|
||||
likely gradually expand on your enums as you figure out what you'll need).
|
||||
|
||||
```python
|
||||
# mygame/evadventure/enums.py
|
||||
|
||||
class Ability(Enum):
|
||||
"""
|
||||
The six base ability-bonuses and other
|
||||
abilities
|
||||
|
||||
"""
|
||||
|
||||
STR = "strength"
|
||||
DEX = "dexterity"
|
||||
CON = "constitution"
|
||||
INT = "intelligence"
|
||||
WIS = "wisdom"
|
||||
CHA = "charisma"
|
||||
|
||||
ARMOR = "armor"
|
||||
|
||||
CRITICAL_FAILURE = "critical_failure"
|
||||
CRITICAL_SUCCESS = "critical_success"
|
||||
|
||||
ALLEGIANCE_HOSTILE = "hostile"
|
||||
ALLEGIANCE_NEUTRAL = "neutral"
|
||||
ALLEGIANCE_FRIENDLY = "friendly"
|
||||
|
||||
|
||||
```
|
||||
|
||||
Here the `Ability` class holds basic properties of a character sheet.
|
||||
|
||||
|
||||
## Utility module
|
||||
|
||||
> Create a new module `mygame/evadventure/utils.py`
|
||||
|
||||
```{sidebar}
|
||||
An example of the utility module is found in
|
||||
[evennia/contrib/tutorials/evadventure/utils.py](../../../api/evennia.contrib.tutorials.evadventure.utils.md)
|
||||
```
|
||||
|
||||
This is for general functions we may need from all over. In this case we only picture one utility,
|
||||
a function that produces a pretty display of any object we pass to it.
|
||||
|
||||
This is an example of the string we want to see:
|
||||
|
||||
```
|
||||
Chipped Sword
|
||||
Value: ~10 coins [wielded in Weapon hand]
|
||||
|
||||
A simple sword used by mercenaries all over
|
||||
the world.
|
||||
|
||||
Slots: 1, Used from: weapon hand
|
||||
Quality: 3, Uses: None
|
||||
Attacks using strength against armor.
|
||||
Damage roll: 1d6
|
||||
```
|
||||
|
||||
Here's the start of how the function could look:
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/utils.py
|
||||
|
||||
_OBJ_STATS = """
|
||||
|c{key}|n
|
||||
Value: ~|y{value}|n coins{carried}
|
||||
|
||||
{desc}
|
||||
|
||||
Slots: |w{size}|n, Used from: |w{use_slot_name}|n
|
||||
Quality: |w{quality}|n, Uses: |wuses|n
|
||||
Attacks using |w{attack_type_name}|n against |w{defense_type_name}|n
|
||||
Damage roll: |w{damage_roll}|n
|
||||
""".strip()
|
||||
|
||||
|
||||
def get_obj_stats(obj, owner=None):
|
||||
"""
|
||||
Get a string of stats about the object.
|
||||
|
||||
Args:
|
||||
obj (Object): The object to get stats for.
|
||||
owner (Object): The one currently owning/carrying `obj`, if any. Can be
|
||||
used to show e.g. where they are wielding it.
|
||||
Returns:
|
||||
str: A nice info string to display about the object.
|
||||
|
||||
"""
|
||||
return _OBJ_STATS.format(
|
||||
key=obj.key,
|
||||
value=10,
|
||||
carried="[Not carried]",
|
||||
desc=obj.db.desc,
|
||||
size=1,
|
||||
quality=3,
|
||||
uses="infinite"
|
||||
use_slot_name="backpack",
|
||||
attack_type_name="strength"
|
||||
defense_type_name="armor"
|
||||
damage_roll="1d6"
|
||||
)
|
||||
```
|
||||
Here we set up the string template with place holders for where every piece of info should go.
|
||||
Study this string so you understand what it does. The `|c`, `|y`, `|w` and `|n` markers are
|
||||
[Evennia color markup](../../../Concepts/Colors.md) for making the text cyan, yellow, white and neutral-color respectively.
|
||||
|
||||
We can guess some things, such that `obj.key` is the name of the object, and that `obj.db.desc` will
|
||||
hold its description (this is how it is in default Evennia).
|
||||
|
||||
But so far we have not established how to get any of the other properties like `size` or `attack_type`.
|
||||
So we just set them to dummy values. We'll need to get back to this when we have more code in place!
|
||||
|
||||
## Testing
|
||||
|
||||
```{important}
|
||||
It's useful for any game dev to know how to effectively test their code. So we'll try to include a
|
||||
*Testing* section at the end of each of the implementation lessons to follow. Writing tests for your code
|
||||
is optional but highly recommended; it can feel a little cumbersome at first, but you'll thank yourself later.
|
||||
```
|
||||
|
||||
> create a new module `mygame/evadventure/tests/test_utils.py`
|
||||
|
||||
How do you know if you made a typo in the code above? You could _manually_ test it by reloading your
|
||||
Evennia server and do the following from in-game:
|
||||
|
||||
py from evadventure.utils import get_obj_stats;print(get_obj_stats(self))
|
||||
|
||||
You should get back a nice string about yourself! If that works, great! But you'll need to remember
|
||||
doing that test when you change this code later.
|
||||
|
||||
```{sidebar}
|
||||
In [evennia/contrib/tutorials/evadventure/tests/test_utils.py](evennia.contrib.tutorials.
|
||||
evadventure.tests.test_utils)
|
||||
is an example of the testing module. To dive deeper into unit testing in Evennia, see the
|
||||
[Unit testing](../../../Coding/Unit-Testing.md) documentation.
|
||||
```
|
||||
|
||||
A _unit test_ allows you to set up automated testing of code. Once you've written your test you
|
||||
can run it over and over and make sure later changes to your code didn't break things.
|
||||
|
||||
In this particular case, we _expect_ to later have to update the test when `get_obj_stats` becomes more
|
||||
complete and returns more reasonable data.
|
||||
|
||||
Evennia comes with extensive functionality to help you test your code. Here's a module for
|
||||
testing `get_obj_stats`.
|
||||
|
||||
```python
|
||||
# mygame/evadventure/tests/test_utils.py
|
||||
|
||||
from evennia.utils import create
|
||||
from evennia.utils.test_resources import BaseEvenniaTest
|
||||
|
||||
from ..import utils
|
||||
|
||||
class TestUtils(BaseEvenniaTest):
|
||||
def test_get_obj_stats(self):
|
||||
# make a simple object to test with
|
||||
obj = create.create_object(
|
||||
key="testobj",
|
||||
attributes=(("desc", "A test object"),)
|
||||
)
|
||||
# run it through the function
|
||||
result = utils.get_obj_stats(obj)
|
||||
# check that the result is what we expected
|
||||
self.assertEqual(
|
||||
result,
|
||||
"""
|
||||
|ctestobj|n
|
||||
Value: ~|y10|n coins
|
||||
|
||||
A test object
|
||||
|
||||
Slots: |w1|n, Used from: |wbackpack|n
|
||||
Quality: |w3|n, Uses: |winfinite|n
|
||||
Attacks using |wstrength|n against |warmor|n
|
||||
Damage roll: |w1d6|n
|
||||
""".strip()
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
What happens here is that we create a new test-class `TestUtils` that inherits from `BaseEvenniaTest`.
|
||||
This inheritance is what makes this a testing class.
|
||||
|
||||
We can have any number of methods on this class. To have a method recognized as one containing
|
||||
code to test, its name _must_ start with `test_`. We have one - `test_get_obj_stats`.
|
||||
|
||||
In this method we create a dummy `obj` and gives it a `key` "testobj". Note how we add the
|
||||
`desc` [Attribute](../../../Components/Attributes.md) directly in the `create_object` call by specifying the attribute as a
|
||||
tuple `(name, value)`!
|
||||
|
||||
We then get the result of passing this dummy-object through `get_obj_stats` we imported earlier.
|
||||
|
||||
The `assertEqual` method is available on all testing classes and checks that the `result` is equal
|
||||
to the string we specify. If they are the same, the test _passes_, otherwise it _fails_ and we
|
||||
need to investigate what went wrong.
|
||||
|
||||
### Running your test
|
||||
|
||||
To run your test you need to stand inside your `mygame` folder and execute the following command:
|
||||
|
||||
evennia test --settings settings.py .evadventure.tests
|
||||
|
||||
This will run all your `evadventure` tests (if you had more of them). To only run your utility tests
|
||||
you could do
|
||||
|
||||
evennia test --settings settings.py .evadventure.tests.test_utils
|
||||
|
||||
If all goes well, you should get an `OK` back. Otherwise you need to check the failure, maybe
|
||||
your return string doesn't quite match what you expected.
|
||||
|
||||
## Summary
|
||||
|
||||
It's very important to understand how you import code between modules in Python, so if this is still
|
||||
confusing to you, it's worth to read up on this more.
|
||||
|
||||
That said, many newcomers are confused with how to begin, so by creating the folder structure, some
|
||||
small modules and even making your first unit test, you are off to a great start!
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
# Part 4: Using what we created
|
||||
|
||||
```{sidebar} Beginner Tutorial Parts
|
||||
- [Introduction](../Beginner-Tutorial-Intro.md)
|
||||
<br>Getting set up.
|
||||
- Part 1: [What we have](../Part1/Beginner-Tutorial-Part1-Intro.md)
|
||||
<br>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.md)
|
||||
<br>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.md)
|
||||
<br>Getting down to the meat of extending Evennia to make our game
|
||||
- **Part 4: [Using what we created](./Beginner-Tutorial-Part4-Intro.md)**
|
||||
<br>Building a tech-demo and world content to go with our code
|
||||
- Part 5: [Showing the world](../Part5/Beginner-Tutorial-Part5-Intro.md)
|
||||
<br>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}
|
||||
|
||||
```
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
# 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!
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
# Part 5: Showing the world
|
||||
|
||||
|
||||
```{sidebar} Beginner Tutorial Parts
|
||||
- [Introduction](../Beginner-Tutorial-Intro.md)
|
||||
<br>Getting set up.
|
||||
- Part 1: [What we have](../Part1/Beginner-Tutorial-Part1-Intro.md)
|
||||
<br>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.md)
|
||||
<br>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.md)
|
||||
<br>Getting down to the meat of extending Evennia to make our game
|
||||
- Part 4: [Using what we created](../Part4/Beginner-Tutorial-Part4-Intro.md)
|
||||
<br>Building a tech-demo and world content to go with our code
|
||||
- **Part 5: [Showing the world](./Beginner-Tutorial-Part5-Intro.md)**
|
||||
<br>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
|
||||
|
||||
```
|
||||
|
|
@ -1,126 +0,0 @@
|
|||
# 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