mirror of
https://github.com/evennia/evennia.git
synced 2026-03-17 21:36:30 +01:00
Reorganize docs into flat folder layout
This commit is contained in:
parent
106558cec0
commit
892d8efb93
135 changed files with 34 additions and 1180 deletions
100
docs/source/Howto/StartingTutorial/Add-a-simple-new-web-page.md
Normal file
100
docs/source/Howto/StartingTutorial/Add-a-simple-new-web-page.md
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
# Add a simple new web page
|
||||
|
||||
|
||||
Evennia leverages [Django](https://docs.djangoproject.com) which is a web development framework.
|
||||
Huge professional websites are made in Django and there is extensive documentation (and books) on it
|
||||
. You are encouraged to at least look at the Django basic tutorials. Here we will just give a brief
|
||||
introduction for how things hang together, to get you started.
|
||||
|
||||
We assume you have installed and set up Evennia to run. A webserver and website comes out of the
|
||||
box. You can get to that by entering `http://localhost:4001` in your web browser - you should see a
|
||||
welcome page with some game statistics and a link to the web client. Let us add a new page that you
|
||||
can get to by going to `http://localhost:4001/story`.
|
||||
|
||||
### Create the view
|
||||
|
||||
A django "view" is a normal Python function that django calls to render the HTML page you will see
|
||||
in the web browser. Here we will just have it spit back the raw html, but Django can do all sorts of
|
||||
cool stuff with the page in the view, like adding dynamic content or change it on the fly. Open
|
||||
`mygame/web` folder and add a new module there named `story.py` (you could also put it in its own
|
||||
folder if you wanted to be neat. Don't forget to add an empty `__init__.py` file if you do, to tell
|
||||
Python you can import from the new folder). Here's how it looks:
|
||||
|
||||
```python
|
||||
# in mygame/web/story.py
|
||||
|
||||
from django.shortcuts import render
|
||||
|
||||
def storypage(request):
|
||||
return render(request, "story.html")
|
||||
```
|
||||
|
||||
This view takes advantage of a shortcut provided to use by Django, _render_. This shortcut gives the
|
||||
template some information from the request, for instance, the game name, and then renders it.
|
||||
|
||||
### The HTML page
|
||||
|
||||
We need to find a place where Evennia (and Django) looks for html files (called *templates* in
|
||||
Django parlance). You can specify such places in your settings (see the `TEMPLATES` variable in
|
||||
`default_settings.py` for more info), but here we'll use an existing one. Go to
|
||||
`mygame/template/overrides/website/` and create a page `story.html` there.
|
||||
|
||||
This is not a HTML tutorial, so we'll go simple:
|
||||
|
||||
```html
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1>A story about a tree</h1>
|
||||
<p>
|
||||
This is a story about a tree, a classic tale ...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
Since we've used the _render_ shortcut, Django will allow us to extend our base styles easily.
|
||||
|
||||
If you'd rather not take advantage of Evennia's base styles, you can do something like this instead:
|
||||
|
||||
```html
|
||||
<html>
|
||||
<body>
|
||||
<h1>A story about a tree</h1>
|
||||
<p>
|
||||
This is a story about a tree, a classic tale ...
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
|
||||
### The URL
|
||||
|
||||
When you enter the address `http://localhost:4001/story` in your web browser, Django will parse that
|
||||
field to figure out which page you want to go to. You tell it which patterns are relevant in the
|
||||
file
|
||||
[mygame/web/urls.py](https://github.com/evennia/evennia/blob/master/evennia/game_template/web/urls.py).
|
||||
Open it now.
|
||||
|
||||
Django looks for the variable `urlpatterns` in this file. You want to add your new pattern to the
|
||||
`custom_patterns` list we have prepared - that is then merged with the default `urlpatterns`. Here's
|
||||
how it could look:
|
||||
|
||||
```python
|
||||
from web import story
|
||||
|
||||
# ...
|
||||
|
||||
custom_patterns = [
|
||||
url(r'story', story.storypage, name='Story'),
|
||||
]
|
||||
```
|
||||
|
||||
That is, we import our story view module from where we created it earlier and then create an `url`
|
||||
instance. The first argument to `url` is the pattern of the url we want to find (`"story"`) (this is
|
||||
a regular expression if you are familiar with those) and then our view function we want to direct
|
||||
to.
|
||||
|
||||
That should be it. Reload Evennia and you should be able to browse to your new story page!
|
||||
171
docs/source/Howto/StartingTutorial/Adding-Command-Tutorial.md
Normal file
171
docs/source/Howto/StartingTutorial/Adding-Command-Tutorial.md
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
# Adding Command Tutorial
|
||||
|
||||
This is a quick first-time tutorial expanding on the [Commands](Commands) documentation.
|
||||
|
||||
Let's assume you have just downloaded Evennia, installed it and created your game folder (let's call
|
||||
it just `mygame` here). Now you want to try to add a new command. This is the fastest way to do it.
|
||||
|
||||
## Step 1: Creating a custom command
|
||||
|
||||
1. Open `mygame/commands/command.py` in a text editor. This is just one place commands could be
|
||||
placed but you get it setup from the onset as an easy place to start. It also already contains some
|
||||
example code.
|
||||
1. Create a new class in `command.py` inheriting from `default_cmds.MuxCommand`. Let's call it
|
||||
`CmdEcho` in this example.
|
||||
1. Set the class variable `key` to a good command name, like `echo`.
|
||||
1. Give your class a useful _docstring_. A docstring is the string at the very top of a class or
|
||||
function/method. The docstring at the top of the command class is read by Evennia to become the help
|
||||
entry for the Command (see
|
||||
[Command Auto-help](Help-System#command-auto-help-system)).
|
||||
1. Define a class method `func(self)` that echoes your input back to you.
|
||||
|
||||
Below is an example how this all could look for the echo command:
|
||||
|
||||
```python
|
||||
# file mygame/commands/command.py
|
||||
#[...]
|
||||
from evennia import default_cmds
|
||||
class CmdEcho(default_cmds.MuxCommand):
|
||||
"""
|
||||
Simple command example
|
||||
|
||||
Usage:
|
||||
echo [text]
|
||||
|
||||
This command simply echoes text back to the caller.
|
||||
"""
|
||||
|
||||
key = "echo"
|
||||
|
||||
def func(self):
|
||||
"This actually does things"
|
||||
if not self.args:
|
||||
self.caller.msg("You didn't enter anything!")
|
||||
else:
|
||||
self.caller.msg("You gave the string: '%s'" % self.args)
|
||||
```
|
||||
|
||||
## Step 2: Adding the Command to a default Cmdset
|
||||
|
||||
The command is not available to use until it is part of a [Command Set](Command-Sets). In this
|
||||
example we will go the easiest route and add it to the default Character commandset that already
|
||||
exists.
|
||||
|
||||
1. Edit `mygame/commands/default_cmdsets.py`
|
||||
1. Import your new command with `from commands.command import CmdEcho`.
|
||||
1. Add a line `self.add(CmdEcho())` to `CharacterCmdSet`, in the `at_cmdset_creation` method (the
|
||||
template tells you where).
|
||||
|
||||
This is approximately how it should look at this point:
|
||||
|
||||
```python
|
||||
# file mygame/commands/default_cmdsets.py
|
||||
#[...]
|
||||
from commands.command import CmdEcho
|
||||
#[...]
|
||||
class CharacterCmdSet(default_cmds.CharacterCmdSet):
|
||||
|
||||
key = "DefaultCharacter"
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
|
||||
# this first adds all default commands
|
||||
super(DefaultSet, self).at_cmdset_creation()
|
||||
|
||||
# all commands added after this point will extend or
|
||||
# overwrite the default commands.
|
||||
self.add(CmdEcho())
|
||||
```
|
||||
|
||||
Next, run the `@reload` command. You should now be able to use your new `echo` command from inside
|
||||
the game. Use `help echo` to see the documentation for the command.
|
||||
|
||||
If you have trouble, make sure to check the log for error messages (probably due to syntax errors in
|
||||
your command definition).
|
||||
|
||||
> Note: Typing `echotest` will also work. It will be handled as the command `echo` directly followed
|
||||
by
|
||||
its argument `test` (which will end up in `self.args). To change this behavior, you can add the
|
||||
`arg_regex` property alongside `key`, `help_category` etc. [See the arg_regex
|
||||
documentation](Commands#on-arg_regex) for more info.
|
||||
|
||||
If you want to overload existing default commands (such as `look` or `get`), just add your new
|
||||
command with the same key as the old one - it will then replace it. Just remember that you must use
|
||||
`@reload` to see any changes.
|
||||
|
||||
See [Commands](Commands) for many more details and possibilities when defining Commands and using
|
||||
Cmdsets in various ways.
|
||||
|
||||
|
||||
## Adding the command to specific object types
|
||||
|
||||
Adding your Command to the `CharacterCmdSet` is just one easy exapmple. The cmdset system is very
|
||||
generic. You can create your own cmdsets (let's say in a module `mycmdsets.py`) and add them to
|
||||
objects as you please (how to control their merging is described in detail in the [Command Set
|
||||
documentation](Command-Sets)).
|
||||
|
||||
```python
|
||||
# file mygame/commands/mycmdsets.py
|
||||
#[...]
|
||||
from commands.command import CmdEcho
|
||||
from evennia import CmdSet
|
||||
#[...]
|
||||
class MyCmdSet(CmdSet):
|
||||
|
||||
key = "MyCmdSet"
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
self.add(CmdEcho())
|
||||
```
|
||||
Now you just need to add this to an object. To test things (as superuser) you can do
|
||||
|
||||
@py self.cmdset.add("mycmdsets.MyCmdSet")
|
||||
|
||||
This will add this cmdset (along with its echo command) to yourself so you can test it. Note that
|
||||
you cannot add a single Command to an object on its own, it must be part of a CommandSet in order to
|
||||
do so.
|
||||
|
||||
The Command you added is not there permanently at this point. If you do a `@reload` the merger will
|
||||
be gone. You *could* add the `permanent=True` keyword to the `cmdset.add` call. This will however
|
||||
only make the new merged cmdset permanent on that *single* object. Often you want *all* objects of
|
||||
this particular class to have this cmdset.
|
||||
|
||||
To make sure all new created objects get your new merged set, put the `cmdset.add` call in your
|
||||
custom [Typeclasses](Typeclasses)' `at_object_creation` method:
|
||||
|
||||
```python
|
||||
# e.g. in mygame/typeclasses/objects.py
|
||||
|
||||
from evennia import DefaultObject
|
||||
class MyObject(DefaultObject):
|
||||
|
||||
def at_object_creation(self):
|
||||
"called when the object is first created"
|
||||
self.cmdset.add("mycmdset.MyCmdSet", permanent=True)
|
||||
```
|
||||
|
||||
All new objects of this typeclass will now start with this cmdset and it will survive a `@reload`.
|
||||
|
||||
*Note:* An important caveat with this is that `at_object_creation` is only called *once*, when the
|
||||
object is first created. This means that if you already have existing objects in your databases
|
||||
using that typeclass, they will not have been initiated the same way. There are many ways to update
|
||||
them; since it's a one-time update you can usually just simply loop through them. As superuser, try
|
||||
the following:
|
||||
|
||||
@py from typeclasses.objects import MyObject; [o.cmdset.add("mycmdset.MyCmdSet") for o in
|
||||
MyObject.objects.all()]
|
||||
|
||||
This goes through all objects in your database having the right typeclass, adding the new cmdset to
|
||||
each. The good news is that you only have to do this if you want to post-add *cmdsets*. If you just
|
||||
want to add a new *command*, you can simply add that command to the cmdset's `at_cmdset_creation`
|
||||
and `@reload` to make the Command immediately available.
|
||||
|
||||
## Change where Evennia looks for command sets
|
||||
|
||||
Evennia uses settings variables to know where to look for its default command sets. These are
|
||||
normally not changed unless you want to re-organize your game folder in some way. For example, the
|
||||
default character cmdset defaults to being defined as
|
||||
|
||||
CMDSET_CHARACTER="commands.default_cmdset.CharacterCmdSet"
|
||||
|
||||
See `evennia/settings_default.py` for the other settings.
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
# Adding Object Typeclass Tutorial
|
||||
|
||||
Evennia comes with a few very basic classes of in-game entities:
|
||||
|
||||
DefaultObject
|
||||
|
|
||||
DefaultCharacter
|
||||
DefaultRoom
|
||||
DefaultExit
|
||||
DefaultChannel
|
||||
|
||||
When you create a new Evennia game (with for example `evennia --init mygame`) Evennia will
|
||||
automatically create empty child classes `Object`, `Character`, `Room` and `Exit` respectively. They
|
||||
are found `mygame/typeclasses/objects.py`, `mygame/typeclasses/rooms.py` etc.
|
||||
|
||||
> Technically these are all [Typeclassed](Typeclasses), which can be ignored for now. In
|
||||
> `mygame/typeclasses` are also base typeclasses for out-of-character things, notably
|
||||
> [Channels](Communications), [Accounts](Accounts) and [Scripts](Scripts). We don't cover those in
|
||||
> this tutorial.
|
||||
|
||||
For your own game you will most likely want to expand on these very simple beginnings. It's normal
|
||||
to want your Characters to have various attributes, for example. Maybe Rooms should hold extra
|
||||
information or even *all* Objects in your game should have properties not included in basic Evennia.
|
||||
|
||||
## Change Default Rooms, Exits, Character Typeclass
|
||||
|
||||
This is the simplest case.
|
||||
|
||||
The default build commands of a new Evennia game is set up to use the `Room`, `Exit` and `Character`
|
||||
classes found in the same-named modules under `mygame/typeclasses/`. By default these are empty and
|
||||
just implements the default parents from the Evennia library (`DefaultRoom`etc). Just add the
|
||||
changes you want to these classes and run `@reload` to add your new functionality.
|
||||
|
||||
## Create a new type of object
|
||||
|
||||
Say you want to create a new "Heavy" object-type that characters should not have the ability to pick
|
||||
up.
|
||||
|
||||
1. Edit `mygame/typeclasses/objects.py` (you could also create a new module there, named something
|
||||
like `heavy.py`, that's up to how you want to organize things).
|
||||
1. Create a new class inheriting at any distance from `DefaultObject`. It could look something like
|
||||
this:
|
||||
```python
|
||||
# end of file mygame/typeclasses/objects.py
|
||||
from evennia import DefaultObject
|
||||
|
||||
class Heavy(DefaultObject):
|
||||
"Heavy object"
|
||||
def at_object_creation(self):
|
||||
"Called whenever a new object is created"
|
||||
# lock the object down by default
|
||||
self.locks.add("get:false()")
|
||||
# the default "get" command looks for this Attribute in order
|
||||
# to return a customized error message (we just happen to know
|
||||
# this, you'd have to look at the code of the 'get' command to
|
||||
# find out).
|
||||
self.db.get_err_msg = "This is too heavy to pick up."
|
||||
```
|
||||
1. Once you are done, log into the game with a build-capable account and do `@create/drop
|
||||
rock:objects.Heavy` to drop a new heavy "rock" object in your location. Next try to pick it up
|
||||
(`@quell` yourself first if you are a superuser). If you get errors, look at your log files where
|
||||
you will find the traceback. The most common error is that you have some sort of syntax error in
|
||||
your class.
|
||||
|
||||
Note that the [Locks](Locks) and [Attribute](Attributes) which are set in the typeclass could just
|
||||
as well have been set using commands in-game, so this is a *very* simple example.
|
||||
|
||||
## Storing data on initialization
|
||||
|
||||
The `at_object_creation` is only called once, when the object is first created. This makes it ideal
|
||||
for database-bound things like [Attributes](Attributes). But sometimes you want to create temporary
|
||||
properties (things that are not to be stored in the database but still always exist every time the
|
||||
object is created). Such properties can be initialized in the `at_init` method on the object.
|
||||
`at_init` is called every time the object is loaded into memory.
|
||||
|
||||
> Note: It's usually pointless and wasteful to assign database data in `at_init`, since this will
|
||||
> hit the database with the same value over and over. Put those in `at_object_creation` instead.
|
||||
|
||||
You are wise to use `ndb` (non-database Attributes) to store these non-persistent properties, since
|
||||
ndb-properties are protected against being cached out in various ways and also allows you to list
|
||||
them using various in-game tools:
|
||||
|
||||
```python
|
||||
def at_init(self):
|
||||
self.ndb.counter = 0
|
||||
self.ndb.mylist = []
|
||||
```
|
||||
|
||||
> Note: As mentioned in the [Typeclasses](Typeclasses) documentation, `at_init` replaces the use of
|
||||
> the standard `__init__` method of typeclasses due to how the latter may be called in situations
|
||||
> other than you'd expect. So use `at_init` where you would normally use `__init__`.
|
||||
|
||||
|
||||
## Updating existing objects
|
||||
|
||||
If you already have some `Heavy` objects created and you add a new `Attribute` in
|
||||
`at_object_creation`, you will find that those existing objects will not have this Attribute. This
|
||||
is not so strange, since `at_object_creation` is only called once, it will not be called again just
|
||||
because you update it. You need to update existing objects manually.
|
||||
|
||||
If the number of objects is limited, you can use `@typeclass/force/reload objectname` to force a
|
||||
re-load of the `at_object_creation` method (only) on the object. This case is common enough that
|
||||
there is an alias `@update objectname` you can use to get the same effect. If there are multiple
|
||||
objects you can use `@py` to loop over the objects you need:
|
||||
|
||||
```
|
||||
@py from typeclasses.objects import Heavy; [obj.at_object_creation() for obj in Heavy.objects.all()]
|
||||
|
||||
```
|
||||
274
docs/source/Howto/StartingTutorial/Building-Quickstart.md
Normal file
274
docs/source/Howto/StartingTutorial/Building-Quickstart.md
Normal file
|
|
@ -0,0 +1,274 @@
|
|||
# Building Quickstart
|
||||
|
||||
|
||||
The [default command](Default-Command-Help) definitions coming with Evennia
|
||||
follows a style [similar](Using-MUX-as-a-Standard) to that of MUX, so the
|
||||
commands should be familiar if you used any such code bases before.
|
||||
|
||||
> Throughout the larger documentation you may come across commands prefixed
|
||||
> with `@`. This is just an optional marker used in some places to make a
|
||||
> command stand out. Evennia defaults to ignoring the use of `@` in front of
|
||||
> your command (so entering `dig` is the same as entering `@dig`).
|
||||
|
||||
The default commands have the following style (where `[...]` marks optional parts):
|
||||
|
||||
command[/switch/switch...] [arguments ...]
|
||||
|
||||
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.
|
||||
|
||||
Below are some examples of commands you can try when logged in to the game. Use `help <command>` for
|
||||
learning more about each command and their detailed options.
|
||||
|
||||
## 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 back to superuser status again afterwards.
|
||||
|
||||
## 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
|
||||
|
||||
We now renamed the box to _very large box_ (and this is what we will see when looking at it), but we
|
||||
will also recognize it by any of the other names we give - like _crate_ or simply _box_ as before.
|
||||
We could have given these aliases directly after the name in the `create` command, this is true for
|
||||
all creation commands - you can always tag on a list of `;`-separated aliases to the name of your
|
||||
new object. If you had wanted to not change the name itself, but to only add aliases, you could have
|
||||
used the `alias` command.
|
||||
|
||||
We are currently carrying the box. Let's drop it (there is also a short cut to create and drop in
|
||||
one go by using the `/drop` switch, for example `create/drop box`).
|
||||
|
||||
drop box
|
||||
|
||||
Hey presto - there it is on the ground, in all its normality.
|
||||
|
||||
examine box
|
||||
|
||||
This will show some technical details about the box object. For now we will ignore what this
|
||||
information means.
|
||||
|
||||
Try to `look` at the box to see the (default) description.
|
||||
|
||||
look box
|
||||
You see nothing special.
|
||||
|
||||
The description you get is not very exciting. Let's add some flavor.
|
||||
|
||||
describe box = This is a large and very heavy box.
|
||||
|
||||
If you try the `get` command we will pick up the box. So far so good, but if we really want this to
|
||||
be a large and heavy box, people should _not_ be able to run off with it that easily. To prevent
|
||||
this we need to lock it down. This is done by assigning a _Lock_ to it. Make sure the box was
|
||||
dropped in the room, then try this:
|
||||
|
||||
lock box = get:false()
|
||||
|
||||
Locks represent a rather [big topic](Locks), 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](Attributes)
|
||||
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. See the [Adding
|
||||
Commands tutorial](Adding-Command-Tutorial) for help with creating your first own Command.
|
||||
|
||||
## Get a Personality
|
||||
|
||||
[Scripts](Scripts) are powerful out-of-character objects useful for many "under the hood" things.
|
||||
One of their optional abilities is to do things on a timer. To try out a first script, let's put one
|
||||
on ourselves. There is an example script in `evennia/contrib/tutorial_examples/bodyfunctions.py`
|
||||
that is called `BodyFunctions`. To add this to us we will use the `script` command:
|
||||
|
||||
script self = tutorial_examples.bodyfunctions.BodyFunctions
|
||||
|
||||
(note that you don't have to give the full path as long as you are pointing to a place inside the
|
||||
`contrib` directory, it's one of the places Evennia looks for Scripts). Wait a while and you will
|
||||
notice yourself starting making random observations.
|
||||
|
||||
script self
|
||||
|
||||
This will show details about scripts on yourself (also `examine` works). You will see how long it is
|
||||
until it "fires" next. Don't be alarmed if nothing happens when the countdown reaches zero - this
|
||||
particular script has a randomizer to determine if it will say something or not. So you will not see
|
||||
output every time it fires.
|
||||
|
||||
When you are tired of your character's "insights", kill the script with
|
||||
|
||||
script/stop self = tutorial_examples.bodyfunctions.BodyFunctions
|
||||
|
||||
You create your own scripts in Python, outside the game; the path you give to `script` is literally
|
||||
the Python path to your script file. The [Scripts](Scripts) page explains more details.
|
||||
|
||||
## Pushing Your Buttons
|
||||
|
||||
If we get back to the box we made, there is only so much fun you can do 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](Typeclasses), [Scripts](Scripts)
|
||||
and object-based [Commands](Commands), 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` typeclass.
|
||||
|
||||
Let's make us one of _those_!
|
||||
|
||||
create/drop button:tutorial_examples.red_button.RedButton
|
||||
|
||||
We import the RedButton python class the same way you would import it in Python except Evennia makes
|
||||
sure to look in`evennia/contrib/` so you don't have to write the full path every time. 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](Typeclasses) and [Commands](Commands) controlling it are inside
|
||||
`evennia/contrib/tutorial_examples/`.
|
||||
|
||||
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.
|
||||
|
||||
## 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. You can also give its database (#dbref) number, which is unique to every object.
|
||||
This can be found with the `examine` command or by looking at the latest constructions with
|
||||
`objects`.
|
||||
|
||||
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
|
||||
|
||||
(or give limbo's dbref which is #2)
|
||||
|
||||
## 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
|
||||
|
||||
(You can usually use `here` to refer to your current location. To refer to yourself you can use
|
||||
`self` or `me`). The box should now be back in Limbo with you.
|
||||
|
||||
We are getting tired of the box. Let's destroy it.
|
||||
|
||||
destroy box
|
||||
|
||||
You can destroy many objects in one go by giving a comma-separated list of objects (or their
|
||||
#dbrefs, if they are not in the same location) to the command.
|
||||
|
||||
## Adding a Help Entry
|
||||
|
||||
An important part of building is keeping the help files updated. You can add, delete and append to
|
||||
existing help entries using the `sethelp` command.
|
||||
|
||||
sethelp/add MyTopic = This help topic is about ...
|
||||
|
||||
## Adding a World
|
||||
|
||||
After this brief introduction to building you may be ready to see a more fleshed-out example.
|
||||
Evennia comes with a tutorial world for you to explore.
|
||||
|
||||
First you need to switch back to _superuser_ by using the `unquell` command. Next, place yourself in
|
||||
`Limbo` and run the following command:
|
||||
|
||||
batchcommand tutorial_world.build
|
||||
|
||||
This will take a while (be patient and don't re-run the command). You will see all the commands used
|
||||
to build the world scroll by as the world is built for you.
|
||||
|
||||
You will end up with a new exit from Limbo named _tutorial_. Apart from being a little solo-
|
||||
adventure in its own right, the tutorial world is a good source for learning Evennia building (and
|
||||
coding).
|
||||
|
||||
Read [the batch
|
||||
file](https://github.com/evennia/evennia/blob/master/evennia/contrib/tutorial_world/build.ev) to see
|
||||
exactly how it's built, step by step. See also more info about the tutorial world [here](Tutorial-
|
||||
World-Introduction).
|
||||
104
docs/source/Howto/StartingTutorial/Coding-Introduction.md
Normal file
104
docs/source/Howto/StartingTutorial/Coding-Introduction.md
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
# Coding Introduction
|
||||
|
||||
|
||||
Evennia allows for a lot of freedom when designing your game - but to code efficiently you still
|
||||
need to adopt some best practices as well as find a good place to start to learn.
|
||||
|
||||
Here are some pointers to get you going.
|
||||
|
||||
### Python
|
||||
|
||||
Evennia is developed using Python. Even if you are more of a designer than a coder, it is wise to
|
||||
learn how to read and understand basic Python code. If you are new to Python, or need a refresher,
|
||||
take a look at our two-part [Python introduction](Python-basic-introduction).
|
||||
|
||||
### Explore Evennia interactively
|
||||
|
||||
When new to Evennia it can be hard to find things or figure out what is available. Evennia offers a
|
||||
special interactive python shell that allows you to experiment and try out things. It's recommended
|
||||
to use [ipython](http://ipython.org/) for this since the vanilla python prompt is very limited. Here
|
||||
are some simple commands to get started:
|
||||
|
||||
# [open a new console/terminal]
|
||||
# [activate your evennia virtualenv in this console/terminal]
|
||||
pip install ipython # [only needed the first time]
|
||||
cd mygame
|
||||
evennia shell
|
||||
|
||||
This will open an Evennia-aware python shell (using ipython). From within this shell, try
|
||||
|
||||
import evennia
|
||||
evennia.<TAB>
|
||||
|
||||
That is, enter `evennia.` and press the `<TAB>` key. This will show you all the resources made
|
||||
available at the top level of Evennia's "flat API". See the [flat API](Evennia-API) page for more
|
||||
info on how to explore it efficiently.
|
||||
|
||||
You can complement your exploration by peeking at the sections of the much more detailed [Developer
|
||||
Central](Developer-Central). The [Tutorials](Tutorials) section also contains a growing collection
|
||||
of system- or implementation-specific help.
|
||||
|
||||
### Use a python syntax checker
|
||||
|
||||
Evennia works by importing your own modules and running them as part of the server. Whereas Evennia
|
||||
should just gracefully tell you what errors it finds, it can nevertheless be a good idea for you to
|
||||
check your code for simple syntax errors *before* you load it into the running server. There are
|
||||
many python syntax checkers out there. A fast and easy one is
|
||||
[pyflakes](https://pypi.python.org/pypi/pyflakes), a more verbose one is
|
||||
[pylint](http://www.pylint.org/). You can also check so that your code looks up to snuff using
|
||||
[pep8](https://pypi.python.org/pypi/pep8). Even with a syntax checker you will not be able to catch
|
||||
every possible problem - some bugs or problems will only appear when you actually run the code. But
|
||||
using such a checker can be a good start to weed out the simple problems.
|
||||
|
||||
### Plan before you code
|
||||
|
||||
Before you start coding away at your dream game, take a look at our [Game Planning](Game-Planning)
|
||||
page. It might hopefully help you avoid some common pitfalls and time sinks.
|
||||
|
||||
### Code in your game folder, not in the evennia/ repository
|
||||
|
||||
As part of the Evennia setup you will create a game folder to host your game code. This is your
|
||||
home. You should *never* need to modify anything in the `evennia` library (anything you download
|
||||
from us, really). You import useful functionality from here and if you see code you like, copy&paste
|
||||
it out into your game folder and edit it there.
|
||||
|
||||
If you find that Evennia doesn't support some functionality you need, make a [Feature
|
||||
Request](feature-request) about it. Same goes for [bugs][bug]. If you add features or fix bugs
|
||||
yourself, please consider [Contributing](Contributing) your changes upstream!
|
||||
|
||||
### Learn to read tracebacks
|
||||
|
||||
Python is very good at reporting when and where things go wrong. A *traceback* shows everything you
|
||||
need to know about crashing code. The text can be pretty long, but you usually are only interested
|
||||
in the last bit, where it says what the error is and at which module and line number it happened -
|
||||
armed with this info you can resolve most problems.
|
||||
|
||||
Evennia will usually not show the full traceback in-game though. Instead the server outputs errors
|
||||
to the terminal/console from which you started Evennia in the first place. If you want more to show
|
||||
in-game you can add `IN_GAME_ERRORS = True` to your settings file. This will echo most (but not all)
|
||||
tracebacks both in-game as well as to the terminal/console. This is a potential security problem
|
||||
though, so don't keep this active when your game goes into production.
|
||||
|
||||
> A common confusing error is finding that objects in-game are suddenly of the type `DefaultObject`
|
||||
rather than your custom typeclass. This happens when you introduce a critical Syntax error to the
|
||||
module holding your custom class. Since such a module is not valid Python, Evennia can't load it at
|
||||
all. Instead of crashing, Evennia will then print the full traceback to the terminal/console and
|
||||
temporarily fall back to the safe `DefaultObject` until you fix the problem and reload.
|
||||
|
||||
### Docs are here to help you
|
||||
|
||||
Some people find reading documentation extremely dull and shun it out of principle. That's your
|
||||
call, but reading docs really *does* help you, promise! Evennia's documentation is pretty thorough
|
||||
and knowing what is possible can often give you a lot of new cool game ideas. That said, if you
|
||||
can't find the answer in the docs, don't be shy to ask questions! The [discussion
|
||||
group](https://sites.google.com/site/evenniaserver/discussions) and the [irc
|
||||
chat](http://webchat.freenode.net/?channels=evennia) are also there for you.
|
||||
|
||||
### The most important point
|
||||
|
||||
And finally, of course, have fun!
|
||||
|
||||
[feature-request]:
|
||||
(https://github.com/evennia/evennia/issues/new?title=Feature+Request%3a+%3Cdescriptive+title+here%3E&body=%23%23%23%23+Description+of+the+suggested+feature+and+how+it+is+supposed+to+work+for+the+admin%2fend+user%3a%0D%0A%0D%0A%0D%0A%23%23%23%23+A+list+of+arguments+for+why+you+think+this+new+feature+should+be+included+in+Evennia%3a%0D%0A%0D%0A1.%0D%0A2.%0D%0A%0D%0A%23%23%23%23+Extra+information%2c+such+as+requirements+or+ideas+on+implementation%3a%0D%0A%0D%0A
|
||||
[bug]:
|
||||
https://github.com/evennia/evennia/issues/new?title=Bug%3a+%3Cdescriptive+title+here%3E&body=%23%23%23%23+Steps+to+reproduce+the+issue%3a%0D%0A%0D%0A1.+%0D%0A2.+%0D%0A3.+%0D%0A%0D%0A%23%23%23%23+What+I+expect+to+see+and+what+I+actually+see+%28tracebacks%2c+error+messages+etc%29%3a%0D%0A%0D%0A%0D%0A%0D%0A%23%23%23%23+Extra+information%2c+such+as+Evennia+revision%2frepo%2fbranch%2c+operating+system+and+ideas+for+how+to+solve%3a%0D%0A%0D%0A
|
||||
120
docs/source/Howto/StartingTutorial/Execute-Python-Code.md
Normal file
120
docs/source/Howto/StartingTutorial/Execute-Python-Code.md
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
# Execute Python Code
|
||||
|
||||
|
||||
The `@py` command supplied with the default command set of Evennia allows you to execute Python
|
||||
commands directly from inside the game. An alias to `@py` is simply "`!`". *Access to the `@py`
|
||||
command should be severely restricted*. This is no joke - being able to execute arbitrary Python
|
||||
code on the server is not something you should entrust to just anybody.
|
||||
|
||||
@py 1+2
|
||||
<<< 3
|
||||
|
||||
## Available variables
|
||||
|
||||
A few local variables are made available when running `@py`. These offer entry into the running
|
||||
system.
|
||||
|
||||
- **self** / **me** - the calling object (i.e. you)
|
||||
- **here** - the current caller's location
|
||||
- **obj** - a dummy [Object](Objects) instance
|
||||
- **evennia** - Evennia's [flat API](Evennia-API) - through this you can access all of Evennia.
|
||||
|
||||
For accessing other objects in the same room you need to use `self.search(name)`. For objects in
|
||||
other locations, use one of the `evennia.search_*` methods. See [below](Execute-Python-Code#finding-
|
||||
objects).
|
||||
|
||||
## Returning output
|
||||
|
||||
This is an example where we import and test one of Evennia's utilities found in
|
||||
`src/utils/utils.py`, but also accessible through `ev.utils`:
|
||||
|
||||
@py from ev import utils; utils.time_format(33333)
|
||||
<<< Done.
|
||||
|
||||
Note that we didn't get any return value, all we where told is that the code finished executing
|
||||
without error. This is often the case in more complex pieces of code which has no single obvious
|
||||
return value. To see the output from the `time_format()` function we need to tell the system to
|
||||
echo it to us explicitly with `self.msg()`.
|
||||
|
||||
@py from ev import utils; self.msg(str(utils.time_format(33333)))
|
||||
09:15
|
||||
<<< Done.
|
||||
|
||||
> Warning: When using the `msg` function wrap our argument in `str()` to convert it into a string
|
||||
above. This is not strictly necessary for most types of data (Evennia will usually convert to a
|
||||
string behind the scenes for you). But for *lists* and *tuples* you will be confused by the output
|
||||
if you don't wrap them in `str()`: only the first item of the iterable will be returned. This is
|
||||
because doing `msg(text)` is actually just a convenience shortcut; the full argument that `msg`
|
||||
accepts is something called an *outputfunc* on the form `(cmdname, (args), {kwargs})` (see [the
|
||||
message path](Messagepath) for more info). Sending a list/tuple confuses Evennia to think you are
|
||||
sending such a structure. Converting it to a string however makes it clear it should just be
|
||||
displayed as-is.
|
||||
|
||||
If you were to use Python's standard `print`, you will see the result in your current `stdout` (your
|
||||
terminal by default, otherwise your log file).
|
||||
|
||||
## Finding objects
|
||||
|
||||
A common use for `@py` is to explore objects in the database, for debugging and performing specific
|
||||
operations that are not covered by a particular command.
|
||||
|
||||
Locating an object is best done using `self.search()`:
|
||||
|
||||
@py self.search("red_ball")
|
||||
<<< Ball
|
||||
|
||||
@py self.search("red_ball").db.color = "red"
|
||||
<<< Done.
|
||||
|
||||
@py self.search("red_ball").db.color
|
||||
<<< red
|
||||
|
||||
`self.search()` is by far the most used case, but you can also search other database tables for
|
||||
other Evennia entities like scripts or configuration entities. To do this you can use the generic
|
||||
search entries found in `ev.search_*`.
|
||||
|
||||
@py evennia.search_script("sys_game_time")
|
||||
<<< [<src.utils.gametime.GameTime object at 0x852be2c>]
|
||||
|
||||
(Note that since this becomes a simple statement, we don't have to wrap it in `self.msg()` to get
|
||||
the output). You can also use the database model managers directly (accessible through the `objects`
|
||||
properties of database models or as `evennia.managers.*`). This is a bit more flexible since it
|
||||
gives you access to the full range of database search methods defined in each manager.
|
||||
|
||||
@py evennia.managers.scripts.script_search("sys_game_time")
|
||||
<<< [<src.utils.gametime.GameTime object at 0x852be2c>]
|
||||
|
||||
The managers are useful for all sorts of database studies.
|
||||
|
||||
@py ev.managers.configvalues.all()
|
||||
<<< [<ConfigValue: default_home]>, <ConfigValue:site_name>, ...]
|
||||
|
||||
## Testing code outside the game
|
||||
|
||||
`@py` has the advantage of operating inside a running server (sharing the same process), where you
|
||||
can test things in real time. Much of this *can* be done from the outside too though.
|
||||
|
||||
In a terminal, cd to the top of your game directory (this bit is important since we need access to
|
||||
your config file) and run
|
||||
|
||||
evennia shell
|
||||
|
||||
Your default Python interpreter will start up, configured to be able to work with and import all
|
||||
modules of your Evennia installation. From here you can explore the database and test-run individual
|
||||
modules as desired.
|
||||
|
||||
It's recommended that you get a more fully featured Python interpreter like
|
||||
[iPython](http://ipython.scipy.org/moin/). If you use a virtual environment, you can just get it
|
||||
with `pip install ipython`. IPython allows you to better work over several lines, and also has a lot
|
||||
of other editing features, such as tab-completion and `__doc__`-string reading.
|
||||
|
||||
$ evennia shell
|
||||
|
||||
IPython 0.10 -- An enhanced Interactive Python
|
||||
...
|
||||
|
||||
In [1]: import evennia
|
||||
In [2]: evennia.managers.objects.all()
|
||||
Out[3]: [<ObjectDB: Harry>, <ObjectDB: Limbo>, ...]
|
||||
|
||||
See the page about the [Evennia-API](Evennia-API) for more things to explore.
|
||||
292
docs/source/Howto/StartingTutorial/First-Steps-Coding.md
Normal file
292
docs/source/Howto/StartingTutorial/First-Steps-Coding.md
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
# First Steps Coding
|
||||
|
||||
|
||||
This section gives a brief step-by-step introduction on how to set up Evennia for the first time so
|
||||
you can modify and overload the defaults easily. You should only need to do these steps once. It
|
||||
also walks through you making your first few tweaks.
|
||||
|
||||
Before continuing, make sure you have Evennia installed and running by following the [Getting
|
||||
Started](Getting-Started) instructions. You should have initialized a new game folder with the
|
||||
`evennia --init foldername` command. We will in the following assume this folder is called
|
||||
"mygame".
|
||||
|
||||
It might be a good idea to eye through the brief [Coding Introduction](Coding-Introduction) too
|
||||
(especially the recommendations in the section about the evennia "flat" API and about using `evennia
|
||||
shell` will help you here and in the future).
|
||||
|
||||
To follow this tutorial you also need to know the basics of operating your computer's
|
||||
terminal/command line. You also need to have a text editor to edit and create source text files.
|
||||
There are plenty of online tutorials on how to use the terminal and plenty of good free text
|
||||
editors. We will assume these things are already familiar to you henceforth.
|
||||
|
||||
|
||||
## Your First Changes
|
||||
|
||||
Below are some first things to try with your new custom modules. You can test these to get a feel
|
||||
for the system. See also [Tutorials](Tutorials) for more step-by-step help and special cases.
|
||||
|
||||
### Tweak Default Character
|
||||
|
||||
We will add some simple rpg attributes to our default Character. In the next section we will follow
|
||||
up with a new command to view those attributes.
|
||||
|
||||
1. Edit `mygame/typeclasses/characters.py` and modify the `Character` class. The
|
||||
`at_object_creation` method also exists on the `DefaultCharacter` parent and will overload it. The
|
||||
`get_abilities` method is unique to our version of `Character`.
|
||||
|
||||
```python
|
||||
class Character(DefaultCharacter):
|
||||
# [...]
|
||||
def at_object_creation(self):
|
||||
"""
|
||||
Called only at initial creation. This is a rather silly
|
||||
example since ability scores should vary from Character to
|
||||
Character and is usually set during some character
|
||||
generation step instead.
|
||||
"""
|
||||
#set persistent attributes
|
||||
self.db.strength = 5
|
||||
self.db.agility = 4
|
||||
self.db.magic = 2
|
||||
|
||||
def get_abilities(self):
|
||||
"""
|
||||
Simple access method to return ability
|
||||
scores as a tuple (str,agi,mag)
|
||||
"""
|
||||
return self.db.strength, self.db.agility, self.db.magic
|
||||
```
|
||||
|
||||
1. [Reload](Start-Stop-Reload) the server (you will still be connected to the game after doing
|
||||
this). Note that if you examine *yourself* you will *not* see any new Attributes appear yet. Read
|
||||
the next section to understand why.
|
||||
|
||||
#### Updating Yourself
|
||||
|
||||
It's important to note that the new [Attributes](Attributes) we added above will only be stored on
|
||||
*newly* created characters. The reason for this is simple: The `at_object_creation` method, where we
|
||||
added those Attributes, is per definition only called when the object is *first created*, then never
|
||||
again. This is usually a good thing since those Attributes may change over time - calling that hook
|
||||
would reset them back to start values. But it also means that your existing character doesn't have
|
||||
them yet. You can see this by calling the `get_abilities` hook on yourself at this point:
|
||||
|
||||
```
|
||||
# (you have to be superuser to use @py)
|
||||
@py self.get_abilities()
|
||||
<<< (None, None, None)
|
||||
```
|
||||
|
||||
This is easily remedied.
|
||||
|
||||
```
|
||||
@update self
|
||||
```
|
||||
|
||||
This will (only) re-run `at_object_creation` on yourself. You should henceforth be able to get the
|
||||
abilities successfully:
|
||||
|
||||
```
|
||||
@py self.get_abilities()
|
||||
<<< (5, 4, 2)
|
||||
```
|
||||
|
||||
This is something to keep in mind if you start building your world before your code is stable -
|
||||
startup-hooks will not (and should not) automatically run on *existing* objects - you have to update
|
||||
your existing objects manually. Luckily this is a one-time thing and pretty simple to do. If the
|
||||
typeclass you want to update is in `typeclasses.myclass.MyClass`, you can do the following (e.g.
|
||||
from `evennia shell`):
|
||||
|
||||
```python
|
||||
from typeclasses.myclass import MyClass
|
||||
# loop over all MyClass instances in the database
|
||||
# and call .swap_typeclass on them
|
||||
for obj in MyClass.objects.all():
|
||||
obj.swap_typeclass(MyClass, run_start_hooks="at_object_creation")
|
||||
```
|
||||
|
||||
Using `swap_typeclass` to the same typeclass we already have will re-run the creation hooks (this is
|
||||
what the `@update` command does under the hood). From in-game you can do the same with `@py`:
|
||||
|
||||
```
|
||||
@py typeclasses.myclass import MyClass;[obj.swap_typeclass(MyClass) for obj in
|
||||
MyClass.objects.all()]
|
||||
```
|
||||
|
||||
See the [Object Typeclass tutorial](Adding-Object-Typeclass-Tutorial) for more help and the
|
||||
[Typeclasses](Typeclasses) and [Attributes](Attributes) page for detailed documentation about
|
||||
Typeclasses and Attributes.
|
||||
|
||||
#### Troubleshooting: Updating Yourself
|
||||
|
||||
One may experience errors for a number of reasons. Common beginner errors are spelling mistakes,
|
||||
wrong indentations or code omissions leading to a `SyntaxError`. Let's say you leave out a colon
|
||||
from the end of a class function like so: ```def at_object_creation(self)```. The client will reload
|
||||
without issue. *However*, if you look at the terminal/console (i.e. not in-game), you will see
|
||||
Evennia complaining (this is called a *traceback*):
|
||||
|
||||
```
|
||||
Traceback (most recent call last):
|
||||
File "C:\mygame\typeclasses\characters.py", line 33
|
||||
def at_object_creation(self)
|
||||
^
|
||||
SyntaxError: invalid syntax
|
||||
```
|
||||
|
||||
Evennia will still be restarting and following the tutorial, doing `@py self.get_abilities()` will
|
||||
return the right response `(None, None, None)`. But when attempting to `@typeclass/force self` you
|
||||
will get this response:
|
||||
|
||||
```python
|
||||
AttributeError: 'DefaultObject' object has no attribute 'get_abilities'
|
||||
```
|
||||
|
||||
The full error will show in the terminal/console but this is confusing since you did add
|
||||
`get_abilities` before. Note however what the error says - you (`self`) should be a `Character` but
|
||||
the error talks about `DefaultObject`. What has happened is that due to your unhandled `SyntaxError`
|
||||
earlier, Evennia could not load the `character.py` module at all (it's not valid Python). Rather
|
||||
than crashing, Evennia handles this by temporarily falling back to a safe default - `DefaultObject`
|
||||
- in order to keep your MUD running. Fix the original `SyntaxError` and reload the server. Evennia
|
||||
will then be able to use your modified `Character` class again and things should work.
|
||||
|
||||
> Note: Learning how to interpret an error traceback is a critical skill for anyone learning Python.
|
||||
Full tracebacks will appear in the terminal/Console you started Evennia from. The traceback text can
|
||||
sometimes be quite long, but you are usually just looking for the last few lines: The description of
|
||||
the error and the filename + line number for where the error occurred. In the example above, we see
|
||||
it's a `SyntaxError` happening at `line 33` of `mygame\typeclasses\characters.py`. In this case it
|
||||
even points out *where* on the line it encountered the error (the missing colon). Learn to read
|
||||
tracebacks and you'll be able to resolve the vast majority of common errors easily.
|
||||
|
||||
### Add a New Default Command
|
||||
|
||||
The `@py` command used above is only available to privileged users. We want any player to be able to
|
||||
see their stats. Let's add a new [command](Commands) to list the abilities we added in the previous
|
||||
section.
|
||||
|
||||
1. Open `mygame/commands/command.py`. You could in principle put your command anywhere but this
|
||||
module has all the imports already set up along with some useful documentation. Make a new class at
|
||||
the bottom of this file:
|
||||
|
||||
```python
|
||||
class CmdAbilities(Command):
|
||||
"""
|
||||
List abilities
|
||||
|
||||
Usage:
|
||||
abilities
|
||||
|
||||
Displays a list of your current ability values.
|
||||
"""
|
||||
key = "abilities"
|
||||
aliases = ["abi"]
|
||||
lock = "cmd:all()"
|
||||
help_category = "General"
|
||||
|
||||
def func(self):
|
||||
"implements the actual functionality"
|
||||
|
||||
str, agi, mag = self.caller.get_abilities()
|
||||
string = "STR: %s, AGI: %s, MAG: %s" % (str, agi, mag)
|
||||
self.caller.msg(string)
|
||||
```
|
||||
|
||||
1. Next you edit `mygame/commands/default_cmdsets.py` and add a new import to it near the top:
|
||||
|
||||
```python
|
||||
from commands.command import CmdAbilities
|
||||
```
|
||||
|
||||
1. In the `CharacterCmdSet` class, add the following near the bottom (it says where):
|
||||
|
||||
```python
|
||||
self.add(CmdAbilities())
|
||||
```
|
||||
|
||||
1. [Reload](Start-Stop-Reload) the server (noone will be disconnected by doing this).
|
||||
|
||||
You (and anyone else) should now be able to use `abilities` (or its alias `abi`) as part of your
|
||||
normal commands in-game:
|
||||
|
||||
```
|
||||
abilities
|
||||
STR: 5, AGI: 4, MAG: 2
|
||||
```
|
||||
|
||||
See the [Adding a Command tutorial](Adding-Command-Tutorial) for more examples and the
|
||||
[Commands](Commands) section for detailed documentation about the Command system.
|
||||
|
||||
### Make a New Type of Object
|
||||
|
||||
Let's test to make a new type of object. This example is an "wise stone" object that returns some
|
||||
random comment when you look at it, like this:
|
||||
|
||||
> look stone
|
||||
|
||||
A very wise stone
|
||||
|
||||
This is a very wise old stone.
|
||||
It grumbles and says: 'The world is like a rock of chocolate.'
|
||||
|
||||
1. Create a new module in `mygame/typeclasses/`. Name it `wiseobject.py` for this example.
|
||||
1. In the module import the base `Object` (`typeclasses.objects.Object`). This is empty by default,
|
||||
meaning it is just a proxy for the default `evennia.DefaultObject`.
|
||||
1. Make a new class in your module inheriting from `Object`. Overload hooks on it to add new
|
||||
functionality. Here is an example of how the file could look:
|
||||
|
||||
```python
|
||||
from random import choice
|
||||
from typeclasses.objects import Object
|
||||
|
||||
class WiseObject(Object):
|
||||
"""
|
||||
An object speaking when someone looks at it. We
|
||||
assume it looks like a stone in this example.
|
||||
"""
|
||||
def at_object_creation(self):
|
||||
"Called when object is first created"
|
||||
self.db.wise_texts = \
|
||||
["Stones have feelings too.",
|
||||
"To live like a stone is to not have lived at all.",
|
||||
"The world is like a rock of chocolate."]
|
||||
|
||||
def return_appearance(self, looker):
|
||||
"""
|
||||
Called by the look command. We want to return
|
||||
a wisdom when we get looked at.
|
||||
"""
|
||||
# first get the base string from the
|
||||
# parent's return_appearance.
|
||||
string = super().return_appearance(looker)
|
||||
wisewords = "\n\nIt grumbles and says: '%s'"
|
||||
wisewords = wisewords % choice(self.db.wise_texts)
|
||||
return string + wisewords
|
||||
```
|
||||
|
||||
1. Check your code for bugs. Tracebacks will appear on your command line or log. If you have a grave
|
||||
Syntax Error in your code, the source file itself will fail to load which can cause issues with the
|
||||
entire cmdset. If so, fix your bug and [reload the server from the command line](Start-Stop-Reload)
|
||||
(noone will be disconnected by doing this).
|
||||
1. Use `@create/drop stone:wiseobject.WiseObject` to create a talkative stone. If the `@create`
|
||||
command spits out a warning or cannot find the typeclass (it will tell you which paths it searched),
|
||||
re-check your code for bugs and that you gave the correct path. The `@create` command starts looking
|
||||
for Typeclasses in `mygame/typeclasses/`.
|
||||
1. Use `look stone` to test. You will see the default description ("You see nothing special")
|
||||
followed by a random message of stony wisdom. Use `@desc stone = This is a wise old stone.` to make
|
||||
it look nicer. See the [Builder Docs](Builder-Docs) for more information.
|
||||
|
||||
Note that `at_object_creation` is only called once, when the stone is first created. If you make
|
||||
changes to this method later, already existing stones will not see those changes. As with the
|
||||
`Character` example above you can use `@typeclass/force` to tell the stone to re-run its
|
||||
initialization.
|
||||
|
||||
The `at_object_creation` is a special case though. Changing most other aspects of the typeclass does
|
||||
*not* require manual updating like this - you just need to `@reload` to have all changes applied
|
||||
automatically to all existing objects.
|
||||
|
||||
## Where to Go From Here?
|
||||
|
||||
There are more [Tutorials](Tutorials), including one for building a [whole little MUSH-like
|
||||
game](Tutorial-for-basic-MUSH-like-game) - that is instructive also if you have no interest in
|
||||
MUSHes per se. A good idea is to also get onto the [IRC
|
||||
chat](http://webchat.freenode.net/?channels=evennia) and the [mailing
|
||||
list](https://groups.google.com/forum/#!forum/evennia) to get in touch with the community and other
|
||||
developers.
|
||||
|
|
@ -0,0 +1,748 @@
|
|||
# Parsing command arguments, theory and best practices
|
||||
|
||||
|
||||
This tutorial will elaborate on the many ways one can parse command arguments. The first step after
|
||||
[adding a command](Adding-Command-Tutorial) usually is to parse its arguments. There are lots of
|
||||
ways to do it, but some are indeed better than others and this tutorial will try to present them.
|
||||
|
||||
If you're a Python beginner, this tutorial might help you a lot. If you're already familiar with
|
||||
Python syntax, this tutorial might still contain useful information. There are still a lot of
|
||||
things I find in the standard library that come as a surprise, though they were there all along.
|
||||
This might be true for others.
|
||||
|
||||
In this tutorial we will:
|
||||
|
||||
- Parse arguments with numbers.
|
||||
- Parse arguments with delimiters.
|
||||
- Take a look at optional arguments.
|
||||
- Parse argument containing object names.
|
||||
|
||||
## What are command arguments?
|
||||
|
||||
I'm going to talk about command arguments and parsing a lot in this tutorial. So let's be sure we
|
||||
talk about the same thing before going any further:
|
||||
|
||||
> A command is an Evennia object that handles specific user input.
|
||||
|
||||
For instance, the default `look` is a command. After having created your Evennia game, and
|
||||
connected to it, you should be able to type `look` to see what's around. In this context, `look` is
|
||||
a command.
|
||||
|
||||
> Command arguments are additional text passed after the command.
|
||||
|
||||
Following the same example, you can type `look self` to look at yourself. In this context, `self`
|
||||
is the text specified after `look`. `" self"` is the argument to the `look` command.
|
||||
|
||||
Part of our task as a game developer is to connect user inputs (mostly commands) with actions in the
|
||||
game. And most of the time, entering commands is not enough, we have to rely on arguments for
|
||||
specifying actions with more accuracy.
|
||||
|
||||
Take the `say` command. If you couldn't specify what to say as a command argument (`say hello!`),
|
||||
you would have trouble communicating with others in the game. One would need to create a different
|
||||
command for every kind of word or sentence, which is, of course, not practical.
|
||||
|
||||
Last thing: what is parsing?
|
||||
|
||||
> In our case, parsing is the process by which we convert command arguments into something we can
|
||||
work with.
|
||||
|
||||
We don't usually use the command argument as is (which is just text, of type `str` in Python). We
|
||||
need to extract useful information. We might want to ask the user for a number, or the name of
|
||||
another character present in the same room. We're going to see how to do all that now.
|
||||
|
||||
## Working with strings
|
||||
|
||||
In object terms, when you write a command in Evennia (when you write the Python class), the
|
||||
arguments are stored in the `args` attribute. Which is to say, inside your `func` method, you can
|
||||
access the command arguments in `self.args`.
|
||||
|
||||
### self.args
|
||||
|
||||
To begin with, look at this example:
|
||||
|
||||
```python
|
||||
class CmdTest(Command):
|
||||
|
||||
"""
|
||||
Test command.
|
||||
|
||||
Syntax:
|
||||
test [argument]
|
||||
|
||||
Enter any argument after test.
|
||||
|
||||
"""
|
||||
|
||||
key = "test"
|
||||
|
||||
def func(self):
|
||||
self.msg(f"You have entered: {self.args}.")
|
||||
```
|
||||
|
||||
If you add this command and test it, you will receive exactly what you have entered without any
|
||||
parsing:
|
||||
|
||||
```
|
||||
> test Whatever
|
||||
You have entered: Whatever.
|
||||
> test
|
||||
You have entered: .
|
||||
```
|
||||
|
||||
> The lines starting with `>` indicate what you enter into your client. The other lines are what
|
||||
you receive from the game server.
|
||||
|
||||
Notice two things here:
|
||||
|
||||
1. The left space between our command key ("test", here) and our command argument is not removed.
|
||||
That's why there are two spaces in our output at line 2. Try entering something like "testok".
|
||||
2. Even if you don't enter command arguments, the command will still be called with an empty string
|
||||
in `self.args`.
|
||||
|
||||
Perhaps a slight modification to our code would be appropriate to see what's happening. We will
|
||||
force Python to display the command arguments as a debug string using a little shortcut.
|
||||
|
||||
```python
|
||||
class CmdTest(Command):
|
||||
|
||||
"""
|
||||
Test command.
|
||||
|
||||
Syntax:
|
||||
test [argument]
|
||||
|
||||
Enter any argument after test.
|
||||
|
||||
"""
|
||||
|
||||
key = "test"
|
||||
|
||||
def func(self):
|
||||
self.msg(f"You have entered: {self.args!r}.")
|
||||
```
|
||||
|
||||
The only line we have changed is the last one, and we have added `!r` between our braces to tell
|
||||
Python to print the debug version of the argument (the repr-ed version). Let's see the result:
|
||||
|
||||
```
|
||||
> test Whatever
|
||||
You have entered: ' Whatever'.
|
||||
> test
|
||||
You have entered: ''.
|
||||
> test And something with '?
|
||||
You have entered: " And something with '?".
|
||||
```
|
||||
|
||||
This displays the string in a way you could see in the Python interpreter. It might be easier to
|
||||
read... to debug, anyway.
|
||||
|
||||
I insist so much on that point because it's crucial: the command argument is just a string (of type
|
||||
`str`) and we will use this to parse it. What you will see is mostly not Evennia-specific, it's
|
||||
Python-specific and could be used in any other project where you have the same need.
|
||||
|
||||
### Stripping
|
||||
|
||||
As you've seen, our command arguments are stored with the space. And the space between the command
|
||||
and the arguments is often of no importance.
|
||||
|
||||
> Why is it ever there?
|
||||
|
||||
Evennia will try its best to find a matching command. If the user enters your command key with
|
||||
arguments (but omits the space), Evennia will still be able to find and call the command. You might
|
||||
have seen what happened if the user entered `testok`. In this case, `testok` could very well be a
|
||||
command (Evennia checks for that) but seeing none, and because there's a `test` command, Evennia
|
||||
calls it with the arguments `"ok"`.
|
||||
|
||||
But most of the time, we don't really care about this left space, so you will often see code to
|
||||
remove it. There are different ways to do it in Python, but a command use case is the `strip`
|
||||
method on `str` and its cousins, `lstrip` and `rstrip`.
|
||||
|
||||
- `strip`: removes one or more characters (either spaces or other characters) from both ends of the
|
||||
string.
|
||||
- `lstrip`: same thing but only removes from the left end (left strip) of the string.
|
||||
- `rstrip`: same thing but only removes from the right end (right strip) of the string.
|
||||
|
||||
Some Python examples might help:
|
||||
|
||||
```python
|
||||
>>> ' this is '.strip() # remove spaces by default
|
||||
'this is'
|
||||
>>> " What if I'm right? ".lstrip() # strip spaces from the left
|
||||
"What if I'm right? "
|
||||
>>> 'Looks good to me...'.strip('.') # removes '.'
|
||||
'Looks good to me'
|
||||
>>> '"Now, what is it?"'.strip('"?') # removes '"' and '?' from both ends
|
||||
'Now, what is it'
|
||||
```
|
||||
|
||||
Usually, since we don't need the space separator, but still want our command to work if there's no
|
||||
separator, we call `lstrip` on the command arguments:
|
||||
|
||||
```python
|
||||
class CmdTest(Command):
|
||||
|
||||
"""
|
||||
Test command.
|
||||
|
||||
Syntax:
|
||||
test [argument]
|
||||
|
||||
Enter any argument after test.
|
||||
|
||||
"""
|
||||
|
||||
key = "test"
|
||||
|
||||
def parse(self):
|
||||
"""Parse arguments, just strip them."""
|
||||
self.args = self.args.lstrip()
|
||||
|
||||
def func(self):
|
||||
self.msg(f"You have entered: {self.args!r}.")
|
||||
```
|
||||
|
||||
> We are now beginning to override the command's `parse` method, which is typically useful just for
|
||||
argument parsing. This method is executed before `func` and so `self.args` in `func()` will contain
|
||||
our `self.args.lstrip()`.
|
||||
|
||||
Let's try it:
|
||||
|
||||
```
|
||||
> test Whatever
|
||||
You have entered: 'Whatever'.
|
||||
> test
|
||||
You have entered: ''.
|
||||
> test And something with '?
|
||||
You have entered: "And something with '?".
|
||||
> test And something with lots of spaces
|
||||
You have entered: 'And something with lots of spaces'.
|
||||
```
|
||||
|
||||
Spaces at the end of the string are kept, but all spaces at the beginning are removed:
|
||||
|
||||
> `strip`, `lstrip` and `rstrip` without arguments will strip spaces, line breaks and other common
|
||||
separators. You can specify one or more characters as a parameter. If you specify more than one
|
||||
character, all of them will be stripped from your original string.
|
||||
|
||||
### Convert arguments to numbers
|
||||
|
||||
As pointed out, `self.args` is a string (of type `str`). What if we want the user to enter a
|
||||
number?
|
||||
|
||||
Let's take a very simple example: creating a command, `roll`, that allows to roll a six-sided die.
|
||||
The player has to guess the number, specifying the number as argument. To win, the player has to
|
||||
match the number with the die. Let's see an example:
|
||||
|
||||
```
|
||||
> roll 3
|
||||
You roll a die. It lands on the number 4.
|
||||
You played 3, you have lost.
|
||||
> dice 1
|
||||
You roll a die. It lands on the number 2.
|
||||
You played 1, you have lost.
|
||||
> dice 1
|
||||
You roll a die. It lands on the number 1.
|
||||
You played 1, you have won!
|
||||
```
|
||||
|
||||
If that's your first command, it's a good opportunity to try to write it. A command with a simple
|
||||
and finite role always is a good starting choice. Here's how we could (first) write it... but it
|
||||
won't work as is, I warn you:
|
||||
|
||||
```python
|
||||
from random import randint
|
||||
|
||||
from evennia import Command
|
||||
|
||||
class CmdRoll(Command):
|
||||
|
||||
"""
|
||||
Play random, enter a number and try your luck.
|
||||
|
||||
Usage:
|
||||
roll <number>
|
||||
|
||||
Enter a valid number as argument. A random die will be rolled and you
|
||||
will win if you have specified the correct number.
|
||||
|
||||
Example:
|
||||
roll 3
|
||||
|
||||
"""
|
||||
|
||||
key = "roll"
|
||||
|
||||
def parse(self):
|
||||
"""Convert the argument to a number."""
|
||||
self.args = self.args.lstrip()
|
||||
|
||||
def func(self):
|
||||
# Roll a random die
|
||||
figure = randint(1, 6) # return a pseudo-random number between 1 and 6, including both
|
||||
self.msg(f"You roll a die. It lands on the number {figure}.")
|
||||
|
||||
if self.args == figure: # THAT WILL BREAK!
|
||||
self.msg(f"You played {self.args}, you have won!")
|
||||
else:
|
||||
self.msg(f"You played {self.args}, you have lost.")
|
||||
```
|
||||
|
||||
If you try this code, Python will complain that you try to compare a number with a string: `figure`
|
||||
is a number and `self.args` is a string and can't be compared as-is in Python. Python doesn't do
|
||||
"implicit converting" as some languages do. By the way, this might be annoying sometimes, and other
|
||||
times you will be glad it tries to encourage you to be explicit rather than implicit about what to
|
||||
do. This is an ongoing debate between programmers. Let's move on!
|
||||
|
||||
So we need to convert the command argument from a `str` into an `int`. There are a few ways to do
|
||||
it. But the proper way is to try to convert and deal with the `ValueError` Python exception.
|
||||
|
||||
Converting a `str` into an `int` in Python is extremely simple: just use the `int` function, give it
|
||||
the string and it returns an integer, if it could. If it can't, it will raise `ValueError`. So
|
||||
we'll need to catch that. However, we also have to indicate to Evennia that, should the number be
|
||||
invalid, no further parsing should be done. Here's a new attempt at our command with this
|
||||
converting:
|
||||
|
||||
```python
|
||||
from random import randint
|
||||
|
||||
from evennia import Command, InterruptCommand
|
||||
|
||||
class CmdRoll(Command):
|
||||
|
||||
"""
|
||||
Play random, enter a number and try your luck.
|
||||
|
||||
Usage:
|
||||
roll <number>
|
||||
|
||||
Enter a valid number as argument. A random die will be rolled and you
|
||||
will win if you have specified the correct number.
|
||||
|
||||
Example:
|
||||
roll 3
|
||||
|
||||
"""
|
||||
|
||||
key = "roll"
|
||||
|
||||
def parse(self):
|
||||
"""Convert the argument to number if possible."""
|
||||
args = self.args.lstrip()
|
||||
|
||||
# Convert to int if possible
|
||||
# If not, raise InterruptCommand. Evennia will catch this
|
||||
# exception and not call the 'func' method.
|
||||
try:
|
||||
self.entered = int(args)
|
||||
except ValueError:
|
||||
self.msg(f"{args} is not a valid number.")
|
||||
raise InterruptCommand
|
||||
|
||||
def func(self):
|
||||
# Roll a random die
|
||||
figure = randint(1, 6) # return a pseudo-random number between 1 and 6, including both
|
||||
self.msg(f"You roll a die. It lands on the number {figure}.")
|
||||
|
||||
if self.entered == figure:
|
||||
self.msg(f"You played {self.entered}, you have won!")
|
||||
else:
|
||||
self.msg(f"You played {self.entered}, you have lost.")
|
||||
```
|
||||
|
||||
Before enjoying the result, let's examine the `parse` method a little more: what it does is try to
|
||||
convert the entered argument from a `str` to an `int`. This might fail (if a user enters `roll
|
||||
something`). In such a case, Python raises a `ValueError` exception. We catch it in our
|
||||
`try/except` block, send a message to the user and raise the `InterruptCommand` exception in
|
||||
response to tell Evennia to not run `func()`, since we have no valid number to give it.
|
||||
|
||||
In the `func` method, instead of using `self.args`, we use `self.entered` which we have defined in
|
||||
our `parse` method. You can expect that, if `func()` is run, then `self.entered` contains a valid
|
||||
number.
|
||||
|
||||
If you try this command, it will work as expected this time: the number is converted as it should
|
||||
and compared to the die roll. You might spend some minutes playing this game. Time out!
|
||||
|
||||
Something else we could want to address: in our small example, we only want the user to enter a
|
||||
positive number between 1 and 6. And the user can enter `roll 0` or `roll -8` or `roll 208` for
|
||||
that matter, the game still works. It might be worth addressing. Again, you could write a
|
||||
condition to do that, but since we're catching an exception, we might end up with something cleaner
|
||||
by grouping:
|
||||
|
||||
```python
|
||||
from random import randint
|
||||
|
||||
from evennia import Command, InterruptCommand
|
||||
|
||||
class CmdRoll(Command):
|
||||
|
||||
"""
|
||||
Play random, enter a number and try your luck.
|
||||
|
||||
Usage:
|
||||
roll <number>
|
||||
|
||||
Enter a valid number as argument. A random die will be rolled and you
|
||||
will win if you have specified the correct number.
|
||||
|
||||
Example:
|
||||
roll 3
|
||||
|
||||
"""
|
||||
|
||||
key = "roll"
|
||||
|
||||
def parse(self):
|
||||
"""Convert the argument to number if possible."""
|
||||
args = self.args.lstrip()
|
||||
|
||||
# Convert to int if possible
|
||||
try:
|
||||
self.entered = int(args)
|
||||
if not 1 <= self.entered <= 6:
|
||||
# self.entered is not between 1 and 6 (including both)
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
self.msg(f"{args} is not a valid number.")
|
||||
raise InterruptCommand
|
||||
|
||||
def func(self):
|
||||
# Roll a random die
|
||||
figure = randint(1, 6) # return a pseudo-random number between 1 and 6, including both
|
||||
self.msg(f"You roll a die. It lands on the number {figure}.")
|
||||
|
||||
if self.entered == figure:
|
||||
self.msg(f"You played {self.entered}, you have won!")
|
||||
else:
|
||||
self.msg(f"You played {self.entered}, you have lost.")
|
||||
```
|
||||
|
||||
Using grouped exceptions like that makes our code easier to read, but if you feel more comfortable
|
||||
checking, afterward, that the number the user entered is in the right range, you can do so in a
|
||||
latter condition.
|
||||
|
||||
> Notice that we have updated our `parse` method only in this last attempt, not our `func()` method
|
||||
which remains the same. This is one goal of separating argument parsing from command processing,
|
||||
these two actions are best kept isolated.
|
||||
|
||||
### Working with several arguments
|
||||
|
||||
Often a command expects several arguments. So far, in our example with the "roll" command, we only
|
||||
expect one argument: a number and just a number. What if we want the user to specify several
|
||||
numbers? First the number of dice to roll, then the guess?
|
||||
|
||||
> You won't win often if you roll 5 dice but that's for the example.
|
||||
|
||||
So we would like to interpret a command like this:
|
||||
|
||||
> roll 3 12
|
||||
|
||||
(To be understood: roll 3 dice, my guess is the total number will be 12.)
|
||||
|
||||
What we need is to cut our command argument, which is a `str`, break it at the space (we use the
|
||||
space as a delimiter). Python provides the `str.split` method which we'll use. Again, here are
|
||||
some examples from the Python interpreter:
|
||||
|
||||
>>> args = "3 12"
|
||||
>>> args.split(" ")
|
||||
['3', '12']
|
||||
>>> args = "a command with several arguments"
|
||||
>>> args.split(" ")
|
||||
['a', 'command', 'with', 'several', 'arguments']
|
||||
>>>
|
||||
|
||||
As you can see, `str.split` will "convert" our strings into a list of strings. The specified
|
||||
argument (`" "` in our case) is used as delimiter. So Python browses our original string. When it
|
||||
sees a delimiter, it takes whatever is before this delimiter and append it to a list.
|
||||
|
||||
The point here is that `str.split` will be used to split our argument. But, as you can see from the
|
||||
above output, we can never be sure of the length of the list at this point:
|
||||
|
||||
>>> args = "something"
|
||||
>>> args.split(" ")
|
||||
['something']
|
||||
>>> args = ""
|
||||
>>> args.split(" ")
|
||||
['']
|
||||
>>>
|
||||
|
||||
Again we could use a condition to check the number of split arguments, but Python offers a better
|
||||
approach, making use of its exception mechanism. We'll give a second argument to `str.split`, the
|
||||
maximum number of splits to do. Let's see an example, this feature might be confusing at first
|
||||
glance:
|
||||
|
||||
>>> args = "that is something great"
|
||||
>>> args.split(" ", 1) # one split, that is a list with two elements (before, after)
|
||||
['that', 'is something great']
|
||||
>>>
|
||||
|
||||
Read this example as many times as needed to understand it. The second argument we give to
|
||||
`str.split` is not the length of the list that should be returned, but the number of times we have
|
||||
to split. Therefore, we specify 1 here, but we get a list of two elements (before the separator,
|
||||
after the separator).
|
||||
|
||||
> What will happen if Python can't split the number of times we ask?
|
||||
|
||||
It won't:
|
||||
|
||||
>>> args = "whatever"
|
||||
>>> args.split(" ", 1) # there isn't even a space here...
|
||||
['whatever']
|
||||
>>>
|
||||
|
||||
This is one moment I would have hoped for an exception and didn't get one. But there's another way
|
||||
which will raise an exception if there is an error: variable unpacking.
|
||||
|
||||
We won't talk about this feature in details here. It would be complicated. But the code is really
|
||||
straightforward to use. Let's take our example of the roll command but let's add a first argument:
|
||||
the number of dice to roll.
|
||||
|
||||
```python
|
||||
from random import randint
|
||||
|
||||
from evennia import Command, InterruptCommand
|
||||
|
||||
class CmdRoll(Command):
|
||||
|
||||
"""
|
||||
Play random, enter a number and try your luck.
|
||||
|
||||
Specify two numbers separated by a space. The first number is the
|
||||
number of dice to roll (1, 2, 3) and the second is the expected sum
|
||||
of the roll.
|
||||
|
||||
Usage:
|
||||
roll <dice> <number>
|
||||
|
||||
For instance, to roll two 6-figure dice, enter 2 as first argument.
|
||||
If you think the sum of these two dice roll will be 10, you could enter:
|
||||
|
||||
roll 2 10
|
||||
|
||||
"""
|
||||
|
||||
key = "roll"
|
||||
|
||||
def parse(self):
|
||||
"""Split the arguments and convert them."""
|
||||
args = self.args.lstrip()
|
||||
|
||||
# Split: we expect two arguments separated by a space
|
||||
try:
|
||||
number, guess = args.split(" ", 1)
|
||||
except ValueError:
|
||||
self.msg("Invalid usage. Enter two numbers separated by a space.")
|
||||
raise InterruptCommand
|
||||
|
||||
# Convert the entered number (first argument)
|
||||
try:
|
||||
self.number = int(number)
|
||||
if self.number <= 0:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
self.msg(f"{number} is not a valid number of dice.")
|
||||
raise InterruptCommand
|
||||
|
||||
# Convert the entered guess (second argument)
|
||||
try:
|
||||
self.guess = int(guess)
|
||||
if not 1 <= self.guess <= self.number * 6:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
self.msg(f"{self.guess} is not a valid guess.")
|
||||
raise InterruptCommand
|
||||
|
||||
def func(self):
|
||||
# Roll a random die X times (X being self.number)
|
||||
figure = 0
|
||||
for _ in range(self.number):
|
||||
figure += randint(1, 6)
|
||||
|
||||
self.msg(f"You roll {self.number} dice and obtain the sum {figure}.")
|
||||
|
||||
if self.guess == figure:
|
||||
self.msg(f"You played {self.guess}, you have won!")
|
||||
else:
|
||||
self.msg(f"You played {self.guess}, you have lost.")
|
||||
```
|
||||
|
||||
The beginning of the `parse()` method is what interests us most:
|
||||
|
||||
```python
|
||||
try:
|
||||
number, guess = args.split(" ", 1)
|
||||
except ValueError:
|
||||
self.msg("Invalid usage. Enter two numbers separated by a space.")
|
||||
raise InterruptCommand
|
||||
```
|
||||
|
||||
We split the argument using `str.split` but we capture the result in two variables. Python is smart
|
||||
enough to know that we want what's left of the space in the first variable, what's right of the
|
||||
space in the second variable. If there is not even a space in the string, Python will raise a
|
||||
`ValueError` exception.
|
||||
|
||||
This code is much easier to read than browsing through the returned strings of `str.split`. We can
|
||||
convert both variables the way we did previously. Actually there are not so many changes in this
|
||||
version and the previous one, most of it is due to name changes for clarity.
|
||||
|
||||
> Splitting a string with a maximum of splits is a common occurrence while parsing command
|
||||
arguments. You can also see the `str.rspli8t` method that does the same thing but from the right of
|
||||
the string. Therefore, it will attempt to find delimiters at the end of the string and work toward
|
||||
the beginning of it.
|
||||
|
||||
We have used a space as a delimiter. This is absolutely not necessary. You might remember that
|
||||
most default Evennia commands can take an `=` sign as a delimiter. Now you know how to parse them
|
||||
as well:
|
||||
|
||||
>>> cmd_key = "tel"
|
||||
>>> cmd_args = "book = chest"
|
||||
>>> left, right = cmd_args.split("=") # mighht raise ValueError!
|
||||
>>> left
|
||||
'book '
|
||||
>>> right
|
||||
' chest'
|
||||
>>>
|
||||
|
||||
### Optional arguments
|
||||
|
||||
Sometimes, you'll come across commands that have optional arguments. These arguments are not
|
||||
necessary but they can be set if more information is needed. I will not provide the entire command
|
||||
code here but just enough code to show the mechanism in Python:
|
||||
|
||||
Again, we'll use `str.split`, knowing that we might not have any delimiter at all. For instance,
|
||||
the player could enter the "tel" command like this:
|
||||
|
||||
> tel book
|
||||
> tell book = chest
|
||||
|
||||
The equal sign is optional along with whatever is specified after it. A possible solution in our
|
||||
`parse` method would be:
|
||||
|
||||
```python
|
||||
def parse(self):
|
||||
args = self.args.lstrip()
|
||||
|
||||
# = is optional
|
||||
try:
|
||||
obj, destination = args.split("=", 1)
|
||||
except ValueError:
|
||||
obj = args
|
||||
destination = None
|
||||
```
|
||||
|
||||
This code would place everything the user entered in `obj` if she didn't specify any equal sign.
|
||||
Otherwise, what's before the equal sign will go in `obj`, what's after the equal sign will go in
|
||||
`destination`. This makes for quick testing after that, more robust code with less conditions that
|
||||
might too easily break your code if you're not careful.
|
||||
|
||||
> Again, here we specified a maximum numbers of splits. If the users enters:
|
||||
|
||||
> tel book = chest = chair
|
||||
|
||||
Then `destination` will contain: `" chest = chair"`. This is often desired, but it's up to you to
|
||||
set parsing however you like.
|
||||
|
||||
## Evennia searches
|
||||
|
||||
After this quick tour of some `str` methods, we'll take a look at some Evennia-specific features
|
||||
that you won't find in standard Python.
|
||||
|
||||
One very common task is to convert a `str` into an Evennia object. Take the previous example:
|
||||
having `"book"` in a variable is great, but we would prefer to know what the user is talking
|
||||
about... what is this `"book"`?
|
||||
|
||||
To get an object from a string, we perform an Evennia search. Evennia provides a `search` method on
|
||||
all typeclassed objects (you will most likely use the one on characters or accounts). This method
|
||||
supports a very wide array of arguments and has [its own tutorial](Tutorial-Searching-For-Objects).
|
||||
Some examples of useful cases follow:
|
||||
|
||||
### Local searches
|
||||
|
||||
When an account or a character enters a command, the account or character is found in the `caller`
|
||||
attribute. Therefore, `self.caller` will contain an account or a character (or a session if that's
|
||||
a session command, though that's not as frequent). The `search` method will be available on this
|
||||
caller.
|
||||
|
||||
Let's take the same example of our little "tel" command. The user can specify an object as
|
||||
argument:
|
||||
|
||||
```python
|
||||
def parse(self):
|
||||
name = self.args.lstrip()
|
||||
```
|
||||
|
||||
We then need to "convert" this string into an Evennia object. The Evennia object will be searched
|
||||
in the caller's location and its contents by default (that is to say, if the command has been
|
||||
entered by a character, it will search the object in the character's room and the character's
|
||||
inventory).
|
||||
|
||||
```python
|
||||
def parse(self):
|
||||
name = self.args.lstrip()
|
||||
|
||||
self.obj = self.caller.search(name)
|
||||
```
|
||||
|
||||
We specify only one argument to the `search` method here: the string to search. If Evennia finds a
|
||||
match, it will return it and we keep it in the `obj` attribute. If it can't find anything, it will
|
||||
return `None` so we need to check for that:
|
||||
|
||||
```python
|
||||
def parse(self):
|
||||
name = self.args.lstrip()
|
||||
|
||||
self.obj = self.caller.search(name)
|
||||
if self.obj is None:
|
||||
# A proper error message has already been sent to the caller
|
||||
raise InterruptCommand
|
||||
```
|
||||
|
||||
That's it. After this condition, you know that whatever is in `self.obj` is a valid Evennia object
|
||||
(another character, an object, an exit...).
|
||||
|
||||
### Quiet searches
|
||||
|
||||
By default, Evennia will handle the case when more than one match is found in the search. The user
|
||||
will be asked to narrow down and re-enter the command. You can, however, ask to be returned the
|
||||
list of matches and handle this list yourself:
|
||||
|
||||
```python
|
||||
def parse(self):
|
||||
name = self.args.lstrip()
|
||||
|
||||
objs = self.caller.search(name, quiet=True)
|
||||
if not objs:
|
||||
# This is an empty list, so no match
|
||||
self.msg(f"No {name!r} was found.")
|
||||
raise InterruptCommand
|
||||
|
||||
self.obj = objs[0] # Take the first match even if there are several
|
||||
```
|
||||
|
||||
All we have changed to obtain a list is a keyword argument in the `search` method: `quiet`. If set
|
||||
to `True`, then errors are ignored and a list is always returned, so we need to handle it as such.
|
||||
Notice in this example, `self.obj` will contain a valid object too, but if several matches are
|
||||
found, `self.obj` will contain the first one, even if more matches are available.
|
||||
|
||||
### Global searches
|
||||
|
||||
By default, Evennia will perform a local search, that is, a search limited by the location in which
|
||||
the caller is. If you want to perform a global search (search in the entire database), just set the
|
||||
`global_search` keyword argument to `True`:
|
||||
|
||||
```python
|
||||
def parse(self):
|
||||
name = self.args.lstrip()
|
||||
self.obj = self.caller.search(name, global_search=True)
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
Parsing command arguments is vital for most game designers. If you design "intelligent" commands,
|
||||
users should be able to guess how to use them without reading the help, or with a very quick peek at
|
||||
said help. Good commands are intuitive to users. Better commands do what they're told to do. For
|
||||
game designers working on MUDs, commands are the main entry point for users into your game. This is
|
||||
no trivial. If commands execute correctly (if their argument is parsed, if they don't behave in
|
||||
unexpected ways and report back the right errors), you will have happier players that might stay
|
||||
longer on your game. I hope this tutorial gave you some pointers on ways to improve your command
|
||||
parsing. There are, of course, other ways you will discover, or ways you are already using in your
|
||||
code.
|
||||
267
docs/source/Howto/StartingTutorial/Python-basic-introduction.md
Normal file
267
docs/source/Howto/StartingTutorial/Python-basic-introduction.md
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
# Python basic introduction
|
||||
|
||||
This is the first part of our beginner's guide to the basics of using Python with Evennia. It's
|
||||
aimed at you with limited or no programming/Python experience. But also if you are an experienced
|
||||
programmer new to Evennia or Python you might still pick up a thing or two. It is by necessity brief
|
||||
and low on detail. There are countless Python guides and tutorials, books and videos out there for
|
||||
learning more in-depth - use them!
|
||||
|
||||
**Contents:**
|
||||
- [Evennia Hello world](Python-basic-introduction#evennia-hello-world)
|
||||
- [Importing modules](Python-basic-introduction#importing-modules)
|
||||
- [Parsing Python errors](Python-basic-introduction#parsing-python-errors)
|
||||
- [Our first function](Python-basic-introduction#our-first-function)
|
||||
- [Looking at the log](Python-basic-introduction#looking-at-the-log)
|
||||
- (continued in [part 2](Python-basic-tutorial-part-two))
|
||||
|
||||
This quickstart assumes you have [gotten Evennia started](Getting-Started). You should make sure
|
||||
that you are able to see the output from the server in the console from which you started it. Log
|
||||
into the game either with a mud client on `localhost:4000` or by pointing a web browser to
|
||||
`localhost:4001/webclient`. Log in as your superuser (the user you created during install).
|
||||
|
||||
Below, lines starting with a single `>` means command input.
|
||||
|
||||
### Evennia Hello world
|
||||
|
||||
The `py` (or `!` which is an alias) command allows you as a superuser to run raw Python from in-
|
||||
game. From the game's input line, enter the following:
|
||||
|
||||
> py print("Hello World!")
|
||||
|
||||
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. The quotes `"..."` means you are inputing a *string* (i.e. text). You
|
||||
could also have used single-quotes `'...'`, Python accepts both.
|
||||
|
||||
The first return line (with `>>>`) is just `py` echoing what you input (we won't include that in the
|
||||
examples henceforth).
|
||||
|
||||
> Note: You may sometimes see people/docs refer to `@py` or other commands starting with `@`.
|
||||
Evennia ignores `@` by default, so `@py` is the exact same thing as `py`.
|
||||
|
||||
The `print` command is a standard Python structure. We can use that here in the `py` command, and
|
||||
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 something uniquely available in the `py` command (we could also use `self`, it's
|
||||
an alias). It represents "us", the ones calling the `py` command. The `me` is an example of an
|
||||
*Object instance*. Objects are fundamental in Python and Evennia. The `me` object not only
|
||||
represents the character we play in the game, it also contains a lot of useful resources for doing
|
||||
things with that Object. One such resource is `msg`. `msg` 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`.
|
||||
|
||||
You access an Object's resources by using the full-stop character `.`. So `self.msg` accesses the
|
||||
`msg` resource and then we call it like we did print, with our "Hello World!" greeting in
|
||||
parentheses.
|
||||
|
||||
> Important: something like `print(...)` we refer to as a *function*, while `msg(...)` which sits on
|
||||
an object is called a *method*.
|
||||
|
||||
For now, `print` and `me.msg` behaves the same, just remember that you're going to mostly be using
|
||||
the latter in the future. Try printing other things. Also try to include `|r` at the start of your
|
||||
string to make the output red in-game. Use `color` to learn more color tags.
|
||||
|
||||
### Importing 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!")
|
||||
```
|
||||
|
||||
Don't forget to save the file. A file with the ending `.py` is referred to as a Python *module*. To
|
||||
use this in-game we have to *import* it. Try this:
|
||||
|
||||
```python
|
||||
> @py import world.test
|
||||
Hello World
|
||||
```
|
||||
If you make some error (we'll cover how to handle errors below) you may need to run the `@reload`
|
||||
command 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".
|
||||
|
||||
> If you look in the folder you'll also often find new files ending with `.pyc`. These are compiled
|
||||
Python binaries that Python auto-creates when running code. Just ignore them, you should never edit
|
||||
those anyway.
|
||||
|
||||
Now try to run this a second time:
|
||||
|
||||
```python
|
||||
> 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 Python is being clever - it stores all imported modules and to be efficient it will
|
||||
avoid importing them more than once. So your `print` will only run the first time, when the module
|
||||
is first imported. To see it again you need to `@reload` first, so Python forgets about the module
|
||||
and has to import it again.
|
||||
|
||||
We'll get back to importing code in the second part of this tutorial. For now, let's press on.
|
||||
|
||||
### Parsing Python errors
|
||||
|
||||
Next, erase the single `print` statement you had in `test.py` and replace it with this instead:
|
||||
|
||||
```python
|
||||
me.msg("Hello World!")
|
||||
```
|
||||
|
||||
As you recall we used this from `py` earlier - it echoed "Hello World!" in-game.
|
||||
Save your file and `reload` your server - this makes sure Evennia sees the new version of your code.
|
||||
Try to import it from `py` in the same way as earlier:
|
||||
|
||||
```python
|
||||
> py import world.test
|
||||
```
|
||||
|
||||
No go - this time you get an error!
|
||||
|
||||
```python
|
||||
File "./world/test.py", line 1, in <module>
|
||||
me.msg("Hello world!")
|
||||
NameError: name 'me' is not defined
|
||||
```
|
||||
|
||||
This is called a *traceback*. Python's errors are very friendly and will most of the time tell you
|
||||
exactly what and where things are wrong. It's important that you learn to parse tracebacks so you
|
||||
can fix your code. Let's look at this one. A traceback is to be read from the _bottom up_. The last
|
||||
line is the error Python balked at, while the two lines above it details exactly where that error
|
||||
was encountered.
|
||||
|
||||
1. An error of type `NameError` is the problem ...
|
||||
2. ... more specifically it is due to the variable `me` not being defined.
|
||||
3. This happened on the line `me.msg("Hello world!")` ...
|
||||
4. ... which is on line `1` 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 it 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 far as the module is concerned `me` is just there out of nowhere. Hence the
|
||||
`NameError`.
|
||||
|
||||
### Our first function
|
||||
|
||||
Let's see if we can resolve that `NameError` from the previous section. We know that `me` is defined
|
||||
at the time we use the `@py` command because if we do `py me.msg("Hello World!")` directly in-game
|
||||
it works fine. What if we could *send* that `me` to the `test.py` module so it knows what it is? One
|
||||
way to do this is with a *function*.
|
||||
|
||||
Change your `mygame/world/test.py` file to look like this:
|
||||
|
||||
```python
|
||||
def hello_world(who):
|
||||
who.msg("Hello World!")
|
||||
```
|
||||
|
||||
Now that we are moving onto 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`
|
||||
etc.
|
||||
- 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 set up your editor to
|
||||
always indent *4 spaces* (**not** a single tab-character) when you press the TAB key - it will make
|
||||
your life a lot easier.
|
||||
- `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. Use this style for now.
|
||||
- `who` is what we call the *argument* to our function. Arguments are variables we pass to the
|
||||
function. We could have named it anything and we could also have multiple arguments separated by
|
||||
commas. What `who` is depends on what we pass to this function when we *call* it later (hint: we'll
|
||||
pass `me` to it).
|
||||
- The colon (`:`) at the end of the first line indicates that the header of the function is
|
||||
complete.
|
||||
- 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 have to
|
||||
start at this indentation level.
|
||||
- In the function body we take the `who` argument and treat it as we would have treated `me` earlier
|
||||
- we expect it to have a `.msg` method we can use to send "Hello World" to.
|
||||
|
||||
First, `reload` your game to make it aware of the updated Python module. Now we have defined our
|
||||
first function, let's use it.
|
||||
|
||||
> reload
|
||||
> py import world.test
|
||||
|
||||
Nothing happened! That is because the function in our module won't do anything just by importing it.
|
||||
It will only act when we *call* it. We will need to enter the module we just imported and do so.
|
||||
|
||||
> py import world.test ; world.test.hello_world(me)
|
||||
Hello world!
|
||||
|
||||
There is our "Hello World"! The `;` is the way to put multiple Python-statements on one line.
|
||||
|
||||
> Some MUD clients use `;` for their own purposes to separate client-inputs. If so you'll get a
|
||||
`NameError` stating that `world` is not defined. Check so you understand why this is! Change the use
|
||||
of `;` in your client or use the Evennia web client if this is a problem.
|
||||
|
||||
In the second statement we access the module path we imported (`world.test`) and reach for the
|
||||
`hello_world` function within. We *call* the function with `me`, which becomes the `who` variable we
|
||||
use inside the `hello_function`.
|
||||
|
||||
> As an exercise, try to pass something else into `hello_world`. Try for example to pass _who_ as
|
||||
the number `5` or the simple string `"foo"`. You'll get errors that they don't have the attribute
|
||||
`msg`. As we've seen, `me` *does* make `msg` available which is why it works (you'll learn more
|
||||
about Objects like `me` in the next part of this tutorial). If you are familiar with other
|
||||
programming languages you may be tempted to start *validating* `who` to make sure it works as
|
||||
expected. This is usually not recommended in Python which suggests it's better to
|
||||
[handle](https://docs.python.org/2/tutorial/errors.html) the error if it happens rather than to make
|
||||
a lot of code to prevent it from happening. See also [duck
|
||||
typing](https://en.wikipedia.org/wiki/Duck_typing).
|
||||
|
||||
# Looking at the log
|
||||
|
||||
As you start to explore Evennia, it's important that you know where to look when things go wrong.
|
||||
While using the friendly `py` command you'll see errors directly in-game. But if something goes
|
||||
wrong in your code while the game runs, you must know where to find the _log_.
|
||||
|
||||
Open a terminal (or go back to the terminal you started Evennia in), make sure your `virtualenv` is
|
||||
active and that you are standing in your game directory (the one created with `evennia --init`
|
||||
during installation). Enter
|
||||
|
||||
```
|
||||
evennia --log
|
||||
```
|
||||
(or `evennia -l`)
|
||||
|
||||
This will show the log. New entries will show up in real time. Whenever you want to leave the log,
|
||||
enter `Ctrl-C` or `Cmd-C` depending on your system. As a game dev it is important to look at the
|
||||
log output when working in Evennia - many errors will only appear with full details here. You may
|
||||
sometimes have to scroll up in the history if you miss it.
|
||||
|
||||
This tutorial is continued in [Part 2](Python-basic-tutorial-part-two), where we'll start learning
|
||||
about objects and to explore the Evennia library.
|
||||
|
|
@ -0,0 +1,506 @@
|
|||
# Python basic tutorial part two
|
||||
|
||||
[In the first part](Python-basic-introduction) of this Python-for-Evennia basic tutorial we learned
|
||||
how to run some simple Python code from inside the game. We also made our first new *module*
|
||||
containing a *function* that we called. Now we're going to start exploring the very important
|
||||
subject of *objects*.
|
||||
|
||||
**Contents:**
|
||||
- [On the subject of objects](Python-basic-tutorial-part-two#on-the-subject-of-objects)
|
||||
- [Exploring the Evennia library](Python-basic-tutorial-part-two#exploring-the-evennia-library)
|
||||
- [Tweaking our Character class](Python-basic-tutorial-part-two#tweaking-our-character-class)
|
||||
- [The Evennia shell](Python-basic-tutorial-part-two#the-evennia-shell)
|
||||
- [Where to go from here](Python-basic-tutorial-part-two#where-to-go-from-here)
|
||||
|
||||
### On the subject of objects
|
||||
|
||||
In the first part of the tutorial we did things like
|
||||
|
||||
> py me.msg("Hello World!")
|
||||
|
||||
To learn about functions and imports we also passed that `me` on to a function `hello_world` in
|
||||
another module.
|
||||
|
||||
Let's learn some more about this `me` thing we are passing around all over the place. In the
|
||||
following we assume that we named our superuser Character "Christine".
|
||||
|
||||
> py me
|
||||
Christine
|
||||
> py me.key
|
||||
Christine
|
||||
|
||||
These returns look the same at first glance, but not if we examine them more closely:
|
||||
|
||||
> py type(me)
|
||||
<class 'typeclasses.characters.Character'>
|
||||
> py type(me.key)
|
||||
<type str>
|
||||
|
||||
> Note: In some MU clients, such as Mudlet and MUSHclient simply returning `type(me)`, you may not
|
||||
see the proper return from the above commands. This is likely due to the HTML-like tags `<...>`,
|
||||
being swallowed by the client.
|
||||
|
||||
The `type` function is, like `print`, another in-built function in Python. It
|
||||
tells us that we (`me`) are of the *class* `typeclasses.characters.Character`.
|
||||
Meanwhile `me.key` is a *property* on us, a string. It holds the name of this
|
||||
object.
|
||||
|
||||
> When you do `py me`, the `me` is defined in such a way that it will use its `.key` property to
|
||||
represent itself. That is why the result is the same as when doing `py me.key`. Also, remember that
|
||||
as noted in the first part of the tutorial, the `me` is *not* a reserved Python word; it was just
|
||||
defined by the Evennia developers as a convenient short-hand when creating the `py` command. So
|
||||
don't expect `me` to be available elsewhere.
|
||||
|
||||
A *class* is like a "factory" or blueprint. From a class you then create individual *instances*. So
|
||||
if class is`Dog`, an instance of `Dog` might be `fido`. Our in-game persona is of a class
|
||||
`Character`. The superuser `christine` is an *instance* of the `Character` class (an instance is
|
||||
also often referred to as an *object*). This is an important concept in *object oriented
|
||||
programming*. You are wise to [familiarize yourself with it](https://en.wikipedia.org/wiki/Class-
|
||||
based_programming) a little.
|
||||
|
||||
> In other terms:
|
||||
> * class: A description of a thing, all the methods (code) and data (information)
|
||||
> * object: A thing, defined as an *instance* of a class.
|
||||
>
|
||||
> So in "Fido is a Dog", "Fido" is an object--a unique thing--and "Dog" is a class. Coders would
|
||||
also say, "Fido is an instance of Dog". There can be other dogs too, such as Butch and Fifi. They,
|
||||
too, would be instances of Dog.
|
||||
>
|
||||
> As another example: "Christine is a Character", or "Christine is an instance of
|
||||
typeclasses.characters.Character". To start, all characters will be instances of
|
||||
typeclass.characters.Character.
|
||||
>
|
||||
> You'll be writing your own class soon! The important thing to know here is how classes and objects
|
||||
relate.
|
||||
|
||||
The string `'typeclasses.characters.Character'` we got from the `type()` function is not arbitrary.
|
||||
You'll recognize this from when we _imported_ `world.test` in part one. This is a _path_ exactly
|
||||
describing where to find the python code describing this class. Python treats source code files on
|
||||
your hard drive (known as *modules*) as well as folders (known as *packages*) as objects that you
|
||||
access with the `.` operator. It starts looking at a place that Evennia has set up for you - namely
|
||||
the root of your own game directory.
|
||||
|
||||
Open and look at your game folder (named `mygame` if you exactly followed the Getting Started
|
||||
instructions) in a file editor or in a new terminal/console. Locate the file
|
||||
`mygame/typeclasses/characters.py`
|
||||
|
||||
```
|
||||
mygame/
|
||||
typeclasses
|
||||
characters.py
|
||||
```
|
||||
|
||||
This represents the first part of the python path - `typeclasses.characters` (the `.py` file ending
|
||||
is never included in the python path). The last bit, `.Character` is the actual class name inside
|
||||
the `characters.py` module. Open that file in a text editor and you will see something like this:
|
||||
|
||||
```python
|
||||
"""
|
||||
(Doc string for module)
|
||||
"""
|
||||
|
||||
from evennia import DefaultCharacter
|
||||
|
||||
class Character(DefaultCharacter):
|
||||
"""
|
||||
(Doc string for class)
|
||||
"""
|
||||
pass
|
||||
|
||||
```
|
||||
|
||||
There is `Character`, the last part of the path. Note how empty this file is. At first glance one
|
||||
would think a Character had no functionality at all. But from what we have used already we know it
|
||||
has at least the `key` property and the method `msg`! Where is the code? The answer is that this
|
||||
'emptiness' is an illusion caused by something called *inheritance*. Read on.
|
||||
|
||||
Firstly, in the same way as the little `hello.py` we did in the first part of the tutorial, this is
|
||||
an example of full, multi-line Python code. Those triple-quoted strings are used for strings that
|
||||
have line breaks in them. When they appear on their own like this, at the top of a python module,
|
||||
class or similar they are called *doc strings*. Doc strings are read by Python and is used for
|
||||
producing online help about the function/method/class/module. By contrast, a line starting with `#`
|
||||
is a *comment*. It is ignored completely by Python and is only useful to help guide a human to
|
||||
understand the code.
|
||||
|
||||
The line
|
||||
|
||||
```python
|
||||
class Character(DefaultCharacter):
|
||||
```
|
||||
|
||||
means that the class `Character` is a *child* of the class `DefaultCharacter`. This is called
|
||||
*inheritance* and is another fundamental concept. The answer to the question "where is the code?" is
|
||||
that the code is *inherited* from its parent, `DefaultCharacter`. And that in turn may inherit code
|
||||
from *its* parent(s) and so on. Since our child, `Character` is empty, its functionality is *exactly
|
||||
identical* to that of its parent. The moment we add new things to Character, these will take
|
||||
precedence. And if we add something that already existed in the parent, our child-version will
|
||||
*override* the version in the parent. This is very practical: It means that we can let the parent do
|
||||
the heavy lifting and only tweak the things we want to change. It also means that we could easily
|
||||
have many different Character classes, all inheriting from `DefaultCharacter` but changing different
|
||||
things. And those can in turn also have children ...
|
||||
|
||||
Let's go on an expedition up the inheritance tree.
|
||||
|
||||
### Exploring the Evennia library
|
||||
|
||||
Let's figure out how to tweak `Character`. Right now we don't know much about `DefaultCharacter`
|
||||
though. Without knowing that we won't know what to override. At the top of the file you find
|
||||
|
||||
```python
|
||||
from evennia import DefaultCharacter
|
||||
```
|
||||
|
||||
This is an `import` statement again, but on a different form to what we've seen before. `from ...
|
||||
import ...` is very commonly used and allows you to precisely dip into a module to extract just the
|
||||
component you need to use. In this case we head into the `evennia` package to get
|
||||
`DefaultCharacter`.
|
||||
|
||||
Where is `evennia`? To find it you need to go to the `evennia` folder (repository) you originally
|
||||
cloned from us. If you open it, this is how it looks:
|
||||
|
||||
```
|
||||
evennia/
|
||||
__init__.py
|
||||
bin/
|
||||
CHANGELOG.txt etc.
|
||||
...
|
||||
evennia/
|
||||
...
|
||||
```
|
||||
There are lots of things in there. There are some docs but most of those have to do with the
|
||||
distribution of Evennia and does not concern us right now. The `evennia` subfolder is what we are
|
||||
looking for. *This* is what you are accessing when you do `from evennia import ...`. It's set up by
|
||||
Evennia as a good place to find modules when the server starts. The exact layout of the Evennia
|
||||
library [is covered by our directory overview](Directory-Overview#evennia-library-layout). You can
|
||||
also explore it [online on github](https://github.com/evennia/evennia/tree/master/evennia).
|
||||
|
||||
The structure of the library directly reflects how you import from it.
|
||||
|
||||
- To, for example, import [the text justify
|
||||
function](https://github.com/evennia/evennia/blob/master/evennia/utils/utils.py#L201) from
|
||||
`evennia/utils/utils.py` you would do `from evennia.utils.utils import justify`. In your code you
|
||||
could then just call `justify(...)` to access its functionality.
|
||||
- You could also do `from evennia.utils import utils`. In code you would then have to write
|
||||
`utils.justify(...)`. This is practical if want a lot of stuff from that `utils.py` module and don't
|
||||
want to import each component separately.
|
||||
- You could also do `import evennia`. You would then have to enter the full
|
||||
`evennia.utils.utils.justify(...)` every time you use it. Using `from` to only import the things you
|
||||
need is usually easier and more readable.
|
||||
- See [this overview](http://effbot.org/zone/import-confusion.htm) about the different ways to
|
||||
import in Python.
|
||||
|
||||
Now, remember that our `characters.py` module did `from evennia import DefaultCharacter`. But if we
|
||||
look at the contents of the `evennia` folder, there is no `DefaultCharacter` anywhere! This is
|
||||
because Evennia gives a large number of optional "shortcuts", known as [the "flat" API](Evennia-
|
||||
API). The intention is to make it easier to remember where to find stuff. The flat API is defined in
|
||||
that weirdly named `__init__.py` file. This file just basically imports useful things from all over
|
||||
Evennia so you can more easily find them in one place.
|
||||
|
||||
We could [just look at the documenation](github:evennia#typeclasses) to find out where we can look
|
||||
at our `DefaultCharacter` parent. But for practice, let's figure it out. Here is where
|
||||
`DefaultCharacter` [is imported
|
||||
from](https://github.com/evennia/evennia/blob/master/evennia/__init__.py#L188) inside `__init__.py`:
|
||||
|
||||
```python
|
||||
from .objects.objects import DefaultCharacter
|
||||
```
|
||||
|
||||
The period at the start means that it imports beginning from the same location this module sits(i.e.
|
||||
the `evennia` folder). The full python-path accessible from the outside is thus
|
||||
`evennia.objects.objects.DefaultCharacter`. So to import this into our game it'd be perfectly valid
|
||||
to do
|
||||
|
||||
```python
|
||||
from evennia.objects.objects import DefaultCharacter
|
||||
```
|
||||
|
||||
Using
|
||||
|
||||
```python
|
||||
from evennia import DefaultCharacter
|
||||
```
|
||||
|
||||
is the same thing, just a little easier to remember.
|
||||
|
||||
> To access the shortcuts of the flat API you *must* use `from evennia import
|
||||
> ...`. Using something like `import evennia.DefaultCharacter` will not work.
|
||||
> See [more about the Flat API here](Evennia-API).
|
||||
|
||||
|
||||
### Tweaking our Character class
|
||||
|
||||
In the previous section we traced the parent of our `Character` class to be
|
||||
`DefaultCharacter` in
|
||||
[evennia/objects/objects.py](https://github.com/evennia/evennia/blob/master/evennia/objects/objects.py).
|
||||
Open that file and locate the `DefaultCharacter` class. It's quite a bit down
|
||||
in this module so you might want to search using your editor's (or browser's)
|
||||
search function. Once you find it, you'll find that the class starts like this:
|
||||
|
||||
```python
|
||||
|
||||
class DefaultCharacter(DefaultObject):
|
||||
"""
|
||||
This implements an Object puppeted by a Session - that is, a character
|
||||
avatar controlled by an account.
|
||||
"""
|
||||
|
||||
def basetype_setup(self):
|
||||
"""
|
||||
Setup character-specific security.
|
||||
You should normally not need to overload this, but if you do,
|
||||
make sure to reproduce at least the two last commands in this
|
||||
method (unless you want to fundamentally change how a
|
||||
Character object works).
|
||||
"""
|
||||
super().basetype_setup()
|
||||
self.locks.add(";".join(["get:false()", # noone can pick up the character
|
||||
"call:false()"])) # no commands can be called on character from
|
||||
outside
|
||||
# add the default cmdset
|
||||
self.cmdset.add_default(settings.CMDSET_CHARACTER, permanent=True)
|
||||
|
||||
def at_after_move(self, source_location, **kwargs):
|
||||
"""
|
||||
We make sure to look around after a move.
|
||||
"""
|
||||
if self.location.access(self, "view"):
|
||||
self.msg(self.at_look(self.location))
|
||||
|
||||
def at_pre_puppet(self, account, session=None, **kwargs):
|
||||
"""
|
||||
Return the character from storage in None location in `at_post_unpuppet`.
|
||||
"""
|
||||
|
||||
# ...
|
||||
|
||||
```
|
||||
|
||||
... And so on (you can see the full [class online
|
||||
here](https://github.com/evennia/evennia/blob/master/evennia/objects/objects.py#L1915)). Here we
|
||||
have functional code! These methods may not be directly visible in `Character` back in our game dir,
|
||||
but they are still available since `Character` is a child of `DefaultCharacter` above. Here is a
|
||||
brief summary of the methods we find in `DefaultCharacter` (follow in the code to see if you can see
|
||||
roughly where things happen)::
|
||||
|
||||
- `basetype_setup` is called by Evennia only once, when a Character is first created. In the
|
||||
`DefaultCharacter` class it sets some particular [Locks](Locks) so that people can't pick up and
|
||||
puppet Characters just like that. It also adds the [Character Cmdset](Command-Sets) so that
|
||||
Characters always can accept command-input (this should usually not be modified - the normal hook to
|
||||
override is `at_object_creation`, which is called after `basetype_setup` (it's in the parent)).
|
||||
- `at_after_move` makes it so that every time the Character moves, the `look` command is
|
||||
automatically fired (this would not make sense for just any regular Object).
|
||||
- `at_pre_puppet` is called when an Account begins to puppet this Character. When not puppeted, the
|
||||
Character is hidden away to a `None` location. This brings it back to the location it was in before.
|
||||
Without this, "headless" Characters would remain in the game world just standing around.
|
||||
- `at_post_puppet` is called when puppeting is complete. It echoes a message to the room that his
|
||||
Character has now connected.
|
||||
- `at_post_unpuppet` is called once stopping puppeting of the Character. This hides away the
|
||||
Character to a `None` location again.
|
||||
- There are also some utility properties which makes it easier to get some time stamps from the
|
||||
Character.
|
||||
|
||||
Reading the class we notice another thing:
|
||||
|
||||
```python
|
||||
class DefaultCharacter(DefaultObject):
|
||||
# ...
|
||||
```
|
||||
|
||||
This means that `DefaultCharacter` is in *itself* a child of something called `DefaultObject`! Let's
|
||||
see what this parent class provides. It's in the same module as `DefaultCharacter`, you just need to
|
||||
[scroll up near the
|
||||
top](https://github.com/evennia/evennia/blob/master/evennia/objects/objects.py#L182):
|
||||
|
||||
```python
|
||||
class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)):
|
||||
# ...
|
||||
```
|
||||
|
||||
This is a really big class where the bulk of code defining an in-game object resides. It consists of
|
||||
a large number of methods, all of which thus also becomes available on the `DefaultCharacter` class
|
||||
below *and* by extension in your `Character` class over in your game dir. In this class you can for
|
||||
example find the `msg` method we have been using before.
|
||||
|
||||
> You should probably not expect to understand all details yet, but as an exercise, find and read
|
||||
the doc string of `msg`.
|
||||
|
||||
> As seen, `DefaultObject` actually has multiple parents. In one of those the basic `key` property
|
||||
is defined, but we won't travel further up the inheritance tree in this tutorial. If you are
|
||||
interested to see them, you can find `TypeclassBase` in
|
||||
[evennia/typeclasses/models.py](https://github.com/evennia/evennia/blob/master/evennia/typeclasses/models.py#L93)
|
||||
and `ObjectDB` in
|
||||
[evennia/objects/models.py](https://github.com/evennia/evennia/blob/master/evennia/objects/models.py#L121).
|
||||
We will also not go into the details of [Multiple
|
||||
Inheritance](https://docs.python.org/2/tutorial/classes.html#multiple-inheritance) or
|
||||
[Metaclasses](http://www.onlamp.com/pub/a/python/2003/04/17/metaclasses.html) here. The general rule
|
||||
is that if you realize that you need these features, you already know enough to use them.
|
||||
|
||||
Remember the `at_pre_puppet` method we looked at in `DefaultCharacter`? If you look at the
|
||||
`at_pre_puppet` hook as defined in `DefaultObject` you'll find it to be completely empty (just a
|
||||
`pass`). So if you puppet a regular object it won't be hiding/retrieving the object when you
|
||||
unpuppet it. The `DefaultCharacter` class *overrides* its parent's functionality with a version of
|
||||
its own. And since it's `DefaultCharacter` that our `Character` class inherits back in our game dir,
|
||||
it's *that* version of `at_pre_puppet` we'll get. Anything not explicitly overridden will be passed
|
||||
down as-is.
|
||||
|
||||
While it's useful to read the code, we should never actually modify anything inside the `evennia`
|
||||
folder. Only time you would want that is if you are planning to release a bug fix or new feature for
|
||||
Evennia itself. Instead you *override* the default functionality inside your game dir.
|
||||
|
||||
So to conclude our little foray into classes, objects and inheritance, locate the simple little
|
||||
`at_before_say` method in the `DefaultObject` class:
|
||||
|
||||
```python
|
||||
def at_before_say(self, message, **kwargs):
|
||||
"""
|
||||
(doc string here)
|
||||
"""
|
||||
return message
|
||||
```
|
||||
|
||||
If you read the doc string you'll find that this can be used to modify the output of `say` before it
|
||||
goes out. You can think of it like this: Evennia knows the name of this method, and when someone
|
||||
speaks, Evennia will make sure to redirect the outgoing message through this method. It makes it
|
||||
ripe for us to replace with a version of our own.
|
||||
|
||||
> In the Evennia documentation you may sometimes see the term *hook* used for a method explicitly
|
||||
meant to be overridden like this.
|
||||
|
||||
As you can see, the first argument to `at_before_say` is `self`. In Python, the first argument of a
|
||||
method is *always a back-reference to the object instance on which the method is defined*. By
|
||||
convention this argument is always called `self` but it could in principle be named anything. The
|
||||
`self` is very useful. If you wanted to, say, send a message to the same object from inside
|
||||
`at_before_say`, you would do `self.msg(...)`.
|
||||
|
||||
What can trip up newcomers is that you *don't* include `self` when you *call* the method. Try:
|
||||
|
||||
> @py me.at_before_say("Hello World!")
|
||||
Hello World!
|
||||
|
||||
Note that we don't send `self` but only the `message` argument. Python will automatically add `self`
|
||||
for us. In this case, `self` will become equal to the Character instance `me`.
|
||||
|
||||
By default the `at_before_say` method doesn't do anything. It just takes the `message` input and
|
||||
`return`s it just the way it was (the `return` is another reserved Python word).
|
||||
|
||||
> We won't go into `**kwargs` here, but it (and its sibling `*args`) is also important to
|
||||
understand, extra reading is [here for
|
||||
`**kwargs`](https://stackoverflow.com/questions/1769403/understanding-kwargs-in-python).
|
||||
|
||||
Now, open your game folder and edit `mygame/typeclasses/characters.py`. Locate your `Character`
|
||||
class and modify it as such:
|
||||
|
||||
```python
|
||||
class Character(DefaultCharacter):
|
||||
"""
|
||||
(docstring here)
|
||||
"""
|
||||
def at_before_say(self, message, **kwargs):
|
||||
"Called before say, allows for tweaking message"
|
||||
return f"{message} ..."
|
||||
```
|
||||
|
||||
So we add our own version of `at_before_say`, duplicating the `def` line from the parent but putting
|
||||
new code in it. All we do in this tutorial is to add an ellipsis (`...`) to the message as it passes
|
||||
through the method.
|
||||
|
||||
Note that `f` in front of the string, it means we turned the string into a 'formatted string'. We
|
||||
can now easily inject stuff directly into the string by wrapping them in curly brackets `{ }`. In
|
||||
this example, we put the incoming `message` into the string, followed by an ellipsis. This is only
|
||||
one way to format a string. Python has very powerful [string
|
||||
formatting](https://docs.python.org/2/library/string.html#format-specification-mini-language) and
|
||||
you are wise to learn it well, considering your game will be mainly text-based.
|
||||
|
||||
> You could also copy & paste the relevant method from `DefaultObject` here to get the full doc
|
||||
string. For more complex methods, or if you only want to change some small part of the default
|
||||
behavior, copy & pasting will eliminate the need to constantly look up the original method and keep
|
||||
you sane.
|
||||
|
||||
In-game, now try
|
||||
|
||||
> @reload
|
||||
> say Hello
|
||||
You say, "Hello ..."
|
||||
|
||||
An ellipsis `...` is added to what you said! This is a silly example but you have just made your
|
||||
first code change to core functionality - without touching any of Evennia's original code! We just
|
||||
plugged in our own version of the `at_before_say` method and it replaced the default one. Evennia
|
||||
happily redirected the message through our version and we got a different output.
|
||||
|
||||
> For sane overriding of parent methods you should also be aware of Python's
|
||||
[super](https://docs.python.org/3/library/functions.html#super), which allows you to call the
|
||||
methods defined on a parent in your child class.
|
||||
|
||||
### The Evennia shell
|
||||
|
||||
Now on to some generally useful tools as you continue learning Python and Evennia. We have so far
|
||||
explored using `py` and have inserted Python code directly in-game. We have also modified Evennia's
|
||||
behavior by overriding default functionality with our own. There is a third way to conveniently
|
||||
explore Evennia and Python - the Evennia shell.
|
||||
|
||||
Outside of your game, `cd` to your mygame folder and make sure any needed virtualenv is running.
|
||||
Next:
|
||||
|
||||
> pip install ipython # only needed once
|
||||
|
||||
The [`IPython`](https://en.wikipedia.org/wiki/IPython) program is just a nicer interface to the
|
||||
Python interpreter - you only need to install it once, after which Evennia will use it
|
||||
automatically.
|
||||
|
||||
> evennia shell
|
||||
|
||||
If you did this call from your game dir you will now be in a Python prompt managed by the IPython
|
||||
program.
|
||||
|
||||
IPython ...
|
||||
...
|
||||
In [1]:
|
||||
|
||||
IPython has some very nice ways to explore what Evennia has to offer.
|
||||
|
||||
> import evennia
|
||||
> evennia.<TAB>
|
||||
|
||||
That is, write `evennia.` and press the Tab key. You will be presented with a list of all available
|
||||
resources in the Evennia Flat API. We looked at the `__init__.py` file in the `evennia` folder
|
||||
earlier, so some of what you see should be familiar. From the IPython prompt, do:
|
||||
|
||||
> from evennia import DefaultCharacter
|
||||
> DefaultCharacter.at_before_say?
|
||||
|
||||
Don't forget that you can use `<TAB>` to auto-complete code as you write. Appending a single `?` to
|
||||
the end will show you the doc-string for `at_before_say` we looked at earlier. Use `??` to get the
|
||||
whole source code.
|
||||
|
||||
Let's look at our over-ridden version instead. Since we started the `evennia shell` from our game
|
||||
dir we can easily get to our code too:
|
||||
|
||||
> from typeclasses.characters import Character
|
||||
> Character.at_before_say??
|
||||
|
||||
This will show us the changed code we just did. Having a window with IPython running is very
|
||||
convenient for quickly exploring code without having to go digging through the file structure!
|
||||
|
||||
### Where to go from here
|
||||
|
||||
This should give you a running start using Python with Evennia. If you are completely new to
|
||||
programming or Python you might want to look at a more formal Python tutorial. You can find links
|
||||
and resources [on our link page](Links).
|
||||
|
||||
We have touched upon many of the concepts here but to use Evennia and to be able to follow along in
|
||||
the code, you will need basic understanding of Python
|
||||
[modules](http://docs.python.org/2/tutorial/modules.html),
|
||||
[variables](http://www.tutorialspoint.com/python/python_variable_types.htm), [conditional
|
||||
statements](http://docs.python.org/tutorial/controlflow.html#if-statements),
|
||||
[loops](http://docs.python.org/tutorial/controlflow.html#for-statements),
|
||||
[functions](http://docs.python.org/tutorial/controlflow.html#defining-functions), [lists,
|
||||
dictionaries, list comprehensions](http://docs.python.org/tutorial/datastructures.html) and [string
|
||||
formatting](http://docs.python.org/tutorial/introduction.html#strings). You should also have a basic
|
||||
understanding of [object-oriented
|
||||
programming](http://www.tutorialspoint.com/python/python_classes_objects.htm) and what Python
|
||||
[Classes](http://docs.python.org/tutorial/classes.html) are.
|
||||
|
||||
Once you have familiarized yourself, or if you prefer to pick Python up as you go, continue to one
|
||||
of the beginning-level [Evennia tutorials](Tutorials) to gradually build up your understanding.
|
||||
|
||||
Good luck!
|
||||
520
docs/source/Howto/StartingTutorial/Turn-based-Combat-System.md
Normal file
520
docs/source/Howto/StartingTutorial/Turn-based-Combat-System.md
Normal file
|
|
@ -0,0 +1,520 @@
|
|||
# Turn based Combat System
|
||||
|
||||
|
||||
This tutorial gives an example of a full, if simplified, combat system for Evennia. It was inspired
|
||||
by the discussions held on the [mailing
|
||||
list](https://groups.google.com/forum/#!msg/evennia/wnJNM2sXSfs/-dbLRrgWnYMJ).
|
||||
|
||||
## Overview of combat system concepts
|
||||
|
||||
Most MUDs will use some sort of combat system. There are several main variations:
|
||||
|
||||
- _Freeform_ - the simplest form of combat to implement, common to MUSH-style roleplaying games.
|
||||
This means the system only supplies dice rollers or maybe commands to compare skills and spit out
|
||||
the result. Dice rolls are done to resolve combat according to the rules of the game and to direct
|
||||
the scene. A game master may be required to resolve rule disputes.
|
||||
- _Twitch_ - This is the traditional MUD hack&slash style combat. In a twitch system there is often
|
||||
no difference between your normal "move-around-and-explore mode" and the "combat mode". You enter an
|
||||
attack command and the system will calculate if the attack hits and how much damage was caused.
|
||||
Normally attack commands have some sort of timeout or notion of recovery/balance to reduce the
|
||||
advantage of spamming or client scripting. Whereas the simplest systems just means entering `kill
|
||||
<target>` over and over, more sophisticated twitch systems include anything from defensive stances
|
||||
to tactical positioning.
|
||||
- _Turn-based_ - a turn based system means that the system pauses to make sure all combatants can
|
||||
choose their actions before continuing. In some systems, such entered actions happen immediately
|
||||
(like twitch-based) whereas in others the resolution happens simultaneously at the end of the turn.
|
||||
The disadvantage of a turn-based system is that the game must switch to a "combat mode" and one also
|
||||
needs to take special care of how to handle new combatants and the passage of time. The advantage is
|
||||
that success is not dependent on typing speed or of setting up quick client macros. This potentially
|
||||
allows for emoting as part of combat which is an advantage for roleplay-heavy games.
|
||||
|
||||
To implement a freeform combat system all you need is a dice roller and a roleplaying rulebook. See
|
||||
[contrib/dice.py](https://github.com/evennia/evennia/blob/master/evennia/contrib/dice.py) for an
|
||||
example dice roller. To implement at twitch-based system you basically need a few combat
|
||||
[commands](Commands), possibly ones with a [cooldown](Command-Cooldown). You also need a [game rule
|
||||
module](Implementing-a-game-rule-system) that makes use of it. We will focus on the turn-based
|
||||
variety here.
|
||||
|
||||
## Tutorial overview
|
||||
|
||||
This tutorial will implement the slightly more complex turn-based combat system. Our example has the
|
||||
following properties:
|
||||
|
||||
- Combat is initiated with `attack <target>`, this initiates the combat mode.
|
||||
- Characters may join an ongoing battle using `attack <target>` against a character already in
|
||||
combat.
|
||||
- Each turn every combating character will get to enter two commands, their internal order matters
|
||||
and they are compared one-to-one in the order given by each combatant. Use of `say` and `pose` is
|
||||
free.
|
||||
- The commands are (in our example) simple; they can either `hit <target>`, `feint <target>` or
|
||||
`parry <target>`. They can also `defend`, a generic passive defense. Finally they may choose to
|
||||
`disengage/flee`.
|
||||
- When attacking we use a classic [rock-paper-scissors](https://en.wikipedia.org/wiki/Rock-paper-
|
||||
scissors) mechanic to determine success: `hit` defeats `feint`, which defeats `parry` which defeats
|
||||
`hit`. `defend` is a general passive action that has a percentage chance to win against `hit`
|
||||
(only).
|
||||
- `disengage/flee` must be entered two times in a row and will only succeed if there is no `hit`
|
||||
against them in that time. If so they will leave combat mode.
|
||||
- Once every player has entered two commands, all commands are resolved in order and the result is
|
||||
reported. A new turn then begins.
|
||||
- If players are too slow the turn will time out and any unset commands will be set to `defend`.
|
||||
|
||||
For creating the combat system we will need the following components:
|
||||
|
||||
- A combat handler. This is the main mechanic of the system. This is a [Script](Scripts) object
|
||||
created for each combat. It is not assigned to a specific object but is shared by the combating
|
||||
characters and handles all the combat information. Since Scripts are database entities it also means
|
||||
that the combat will not be affected by a server reload.
|
||||
- A combat [command set](Command-Sets) with the relevant commands needed for combat, such as the
|
||||
various attack/defend options and the `flee/disengage` command to leave the combat mode.
|
||||
- A rule resolution system. The basics of making such a module is described in the [rule system
|
||||
tutorial](Implementing-a-game-rule-system). We will only sketch such a module here for our end-turn
|
||||
combat resolution.
|
||||
- An `attack` [command](Commands) for initiating the combat mode. This is added to the default
|
||||
command set. It will create the combat handler and add the character(s) to it. It will also assign
|
||||
the combat command set to the characters.
|
||||
|
||||
## The combat handler
|
||||
|
||||
The _combat handler_ is implemented as a stand-alone [Script](Scripts). This Script is created when
|
||||
the first Character decides to attack another and is deleted when no one is fighting any more. Each
|
||||
handler represents one instance of combat and one combat only. Each instance of combat can hold any
|
||||
number of characters but each character can only be part of one combat at a time (a player would
|
||||
need to disengage from the first combat before they could join another).
|
||||
|
||||
The reason we don't store this Script "on" any specific character is because any character may leave
|
||||
the combat at any time. Instead the script holds references to all characters involved in the
|
||||
combat. Vice-versa, all characters holds a back-reference to the current combat handler. While we
|
||||
don't use this very much here this might allow the combat commands on the characters to access and
|
||||
update the combat handler state directly.
|
||||
|
||||
_Note: Another way to implement a combat handler would be to use a normal Python object and handle
|
||||
time-keeping with the [TickerHandler](TickerHandler). This would require either adding custom hook
|
||||
methods on the character or to implement a custom child of the TickerHandler class to track turns.
|
||||
Whereas the TickerHandler is easy to use, a Script offers more power in this case._
|
||||
|
||||
Here is a basic combat handler. Assuming our game folder is named `mygame`, we store it in
|
||||
`mygame/typeclasses/combat_handler.py`:
|
||||
|
||||
```python
|
||||
# mygame/typeclasses/combat_handler.py
|
||||
|
||||
import random
|
||||
from evennia import DefaultScript
|
||||
from world.rules import resolve_combat
|
||||
|
||||
class CombatHandler(DefaultScript):
|
||||
"""
|
||||
This implements the combat handler.
|
||||
"""
|
||||
|
||||
# standard Script hooks
|
||||
|
||||
def at_script_creation(self):
|
||||
"Called when script is first created"
|
||||
|
||||
self.key = "combat_handler_%i" % random.randint(1, 1000)
|
||||
self.desc = "handles combat"
|
||||
self.interval = 60 * 2 # two minute timeout
|
||||
self.start_delay = True
|
||||
self.persistent = True
|
||||
|
||||
# store all combatants
|
||||
self.db.characters = {}
|
||||
# store all actions for each turn
|
||||
self.db.turn_actions = {}
|
||||
# number of actions entered per combatant
|
||||
self.db.action_count = {}
|
||||
|
||||
def _init_character(self, character):
|
||||
"""
|
||||
This initializes handler back-reference
|
||||
and combat cmdset on a character
|
||||
"""
|
||||
character.ndb.combat_handler = self
|
||||
character.cmdset.add("commands.combat.CombatCmdSet")
|
||||
|
||||
def _cleanup_character(self, character):
|
||||
"""
|
||||
Remove character from handler and clean
|
||||
it of the back-reference and cmdset
|
||||
"""
|
||||
dbref = character.id
|
||||
del self.db.characters[dbref]
|
||||
del self.db.turn_actions[dbref]
|
||||
del self.db.action_count[dbref]
|
||||
del character.ndb.combat_handler
|
||||
character.cmdset.delete("commands.combat.CombatCmdSet")
|
||||
|
||||
def at_start(self):
|
||||
"""
|
||||
This is called on first start but also when the script is restarted
|
||||
after a server reboot. We need to re-assign this combat handler to
|
||||
all characters as well as re-assign the cmdset.
|
||||
"""
|
||||
for character in self.db.characters.values():
|
||||
self._init_character(character)
|
||||
|
||||
def at_stop(self):
|
||||
"Called just before the script is stopped/destroyed."
|
||||
for character in list(self.db.characters.values()):
|
||||
# note: the list() call above disconnects list from database
|
||||
self._cleanup_character(character)
|
||||
|
||||
def at_repeat(self):
|
||||
"""
|
||||
This is called every self.interval seconds (turn timeout) or
|
||||
when force_repeat is called (because everyone has entered their
|
||||
commands). We know this by checking the existence of the
|
||||
`normal_turn_end` NAttribute, set just before calling
|
||||
force_repeat.
|
||||
|
||||
"""
|
||||
if self.ndb.normal_turn_end:
|
||||
# we get here because the turn ended normally
|
||||
# (force_repeat was called) - no msg output
|
||||
del self.ndb.normal_turn_end
|
||||
else:
|
||||
# turn timeout
|
||||
self.msg_all("Turn timer timed out. Continuing.")
|
||||
self.end_turn()
|
||||
|
||||
# Combat-handler methods
|
||||
|
||||
def add_character(self, character):
|
||||
"Add combatant to handler"
|
||||
dbref = character.id
|
||||
self.db.characters[dbref] = character
|
||||
self.db.action_count[dbref] = 0
|
||||
self.db.turn_actions[dbref] = [("defend", character, None),
|
||||
("defend", character, None)]
|
||||
# set up back-reference
|
||||
self._init_character(character)
|
||||
|
||||
def remove_character(self, character):
|
||||
"Remove combatant from handler"
|
||||
if character.id in self.db.characters:
|
||||
self._cleanup_character(character)
|
||||
if not self.db.characters:
|
||||
# if no more characters in battle, kill this handler
|
||||
self.stop()
|
||||
|
||||
def msg_all(self, message):
|
||||
"Send message to all combatants"
|
||||
for character in self.db.characters.values():
|
||||
character.msg(message)
|
||||
|
||||
def add_action(self, action, character, target):
|
||||
"""
|
||||
Called by combat commands to register an action with the handler.
|
||||
|
||||
action - string identifying the action, like "hit" or "parry"
|
||||
character - the character performing the action
|
||||
target - the target character or None
|
||||
|
||||
actions are stored in a dictionary keyed to each character, each
|
||||
of which holds a list of max 2 actions. An action is stored as
|
||||
a tuple (character, action, target).
|
||||
"""
|
||||
dbref = character.id
|
||||
count = self.db.action_count[dbref]
|
||||
if 0 <= count <= 1: # only allow 2 actions
|
||||
self.db.turn_actions[dbref][count] = (action, character, target)
|
||||
else:
|
||||
# report if we already used too many actions
|
||||
return False
|
||||
self.db.action_count[dbref] += 1
|
||||
return True
|
||||
|
||||
def check_end_turn(self):
|
||||
"""
|
||||
Called by the command to eventually trigger
|
||||
the resolution of the turn. We check if everyone
|
||||
has added all their actions; if so we call force the
|
||||
script to repeat immediately (which will call
|
||||
`self.at_repeat()` while resetting all timers).
|
||||
"""
|
||||
if all(count > 1 for count in self.db.action_count.values()):
|
||||
self.ndb.normal_turn_end = True
|
||||
self.force_repeat()
|
||||
|
||||
def end_turn(self):
|
||||
"""
|
||||
This resolves all actions by calling the rules module.
|
||||
It then resets everything and starts the next turn. It
|
||||
is called by at_repeat().
|
||||
"""
|
||||
resolve_combat(self, self.db.turn_actions)
|
||||
|
||||
if len(self.db.characters) < 2:
|
||||
# less than 2 characters in battle, kill this handler
|
||||
self.msg_all("Combat has ended")
|
||||
self.stop()
|
||||
else:
|
||||
# reset counters before next turn
|
||||
for character in self.db.characters.values():
|
||||
self.db.characters[character.id] = character
|
||||
self.db.action_count[character.id] = 0
|
||||
self.db.turn_actions[character.id] = [("defend", character, None),
|
||||
("defend", character, None)]
|
||||
self.msg_all("Next turn begins ...")
|
||||
```
|
||||
|
||||
This implements all the useful properties of our combat handler. This Script will survive a reboot
|
||||
and will automatically re-assert itself when it comes back online. Even the current state of the
|
||||
combat should be unaffected since it is saved in Attributes at every turn. An important part to note
|
||||
is the use of the Script's standard `at_repeat` hook and the `force_repeat` method to end each turn.
|
||||
This allows for everything to go through the same mechanisms with minimal repetition of code.
|
||||
|
||||
What is not present in this handler is a way for players to view the actions they set or to change
|
||||
their actions once they have been added (but before the last one has added theirs). We leave this as
|
||||
an exercise.
|
||||
|
||||
## Combat commands
|
||||
|
||||
Our combat commands - the commands that are to be available to us during the combat - are (in our
|
||||
example) very simple. In a full implementation the commands available might be determined by the
|
||||
weapon(s) held by the player or by which skills they know.
|
||||
|
||||
We create them in `mygame/commands/combat.py`.
|
||||
|
||||
```python
|
||||
# mygame/commands/combat.py
|
||||
|
||||
from evennia import Command
|
||||
|
||||
class CmdHit(Command):
|
||||
"""
|
||||
hit an enemy
|
||||
|
||||
Usage:
|
||||
hit <target>
|
||||
|
||||
Strikes the given enemy with your current weapon.
|
||||
"""
|
||||
key = "hit"
|
||||
aliases = ["strike", "slash"]
|
||||
help_category = "combat"
|
||||
|
||||
def func(self):
|
||||
"Implements the command"
|
||||
if not self.args:
|
||||
self.caller.msg("Usage: hit <target>")
|
||||
return
|
||||
target = self.caller.search(self.args)
|
||||
if not target:
|
||||
return
|
||||
ok = self.caller.ndb.combat_handler.add_action("hit",
|
||||
self.caller,
|
||||
target)
|
||||
if ok:
|
||||
self.caller.msg("You add 'hit' to the combat queue")
|
||||
else:
|
||||
self.caller.msg("You can only queue two actions per turn!")
|
||||
|
||||
# tell the handler to check if turn is over
|
||||
self.caller.ndb.combat_handler.check_end_turn()
|
||||
```
|
||||
|
||||
The other commands `CmdParry`, `CmdFeint`, `CmdDefend` and `CmdDisengage` look basically the same.
|
||||
We should also add a custom `help` command to list all the available combat commands and what they
|
||||
do.
|
||||
|
||||
We just need to put them all in a cmdset. We do this at the end of the same module:
|
||||
|
||||
```python
|
||||
# mygame/commands/combat.py
|
||||
|
||||
from evennia import CmdSet
|
||||
from evennia import default_cmds
|
||||
|
||||
class CombatCmdSet(CmdSet):
|
||||
key = "combat_cmdset"
|
||||
mergetype = "Replace"
|
||||
priority = 10
|
||||
no_exits = True
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
self.add(CmdHit())
|
||||
self.add(CmdParry())
|
||||
self.add(CmdFeint())
|
||||
self.add(CmdDefend())
|
||||
self.add(CmdDisengage())
|
||||
self.add(CmdHelp())
|
||||
self.add(default_cmds.CmdPose())
|
||||
self.add(default_cmds.CmdSay())
|
||||
```
|
||||
|
||||
## Rules module
|
||||
|
||||
A general way to implement a rule module is found in the [rule system tutorial](Implementing-a-game-
|
||||
rule-system). Proper resolution would likely require us to change our Characters to store things
|
||||
like strength, weapon skills and so on. So for this example we will settle for a very simplistic
|
||||
rock-paper-scissors kind of setup with some randomness thrown in. We will not deal with damage here
|
||||
but just announce the results of each turn. In a real system the Character objects would hold stats
|
||||
to affect their skills, their chosen weapon affect the choices, they would be able to lose health
|
||||
etc.
|
||||
|
||||
Within each turn, there are "sub-turns", each consisting of one action per character. The actions
|
||||
within each sub-turn happens simultaneously and only once they have all been resolved we move on to
|
||||
the next sub-turn (or end the full turn).
|
||||
|
||||
*Note: In our simple example the sub-turns don't affect each other (except for `disengage/flee`),
|
||||
nor do any effects carry over between turns. The real power of a turn-based system would be to add
|
||||
real tactical possibilities here though; For example if your hit got parried you could be out of
|
||||
balance and your next action would be at a disadvantage. A successful feint would open up for a
|
||||
subsequent attack and so on ...*
|
||||
|
||||
Our rock-paper-scissor setup works like this:
|
||||
|
||||
- `hit` beats `feint` and `flee/disengage`. It has a random chance to fail against `defend`.
|
||||
- `parry` beats `hit`.
|
||||
- `feint` beats `parry` and is then counted as a `hit`.
|
||||
- `defend` does nothing but has a chance to beat `hit`.
|
||||
- `flee/disengage` must succeed two times in a row (i.e. not beaten by a `hit` once during the
|
||||
turn). If so the character leaves combat.
|
||||
|
||||
|
||||
```python
|
||||
# mygame/world/rules.py
|
||||
|
||||
import random
|
||||
|
||||
# messages
|
||||
|
||||
def resolve_combat(combat_handler, actiondict):
|
||||
"""
|
||||
This is called by the combat handler
|
||||
actiondict is a dictionary with a list of two actions
|
||||
for each character:
|
||||
{char.id:[(action1, char, target), (action2, char, target)], ...}
|
||||
"""
|
||||
flee = {} # track number of flee commands per character
|
||||
for isub in range(2):
|
||||
# loop over sub-turns
|
||||
messages = []
|
||||
for subturn in (sub[isub] for sub in actiondict.values()):
|
||||
# for each character, resolve the sub-turn
|
||||
action, char, target = subturn
|
||||
if target:
|
||||
taction, tchar, ttarget = actiondict[target.id][isub]
|
||||
if action == "hit":
|
||||
if taction == "parry" and ttarget == char:
|
||||
msg = "%s tries to hit %s, but %s parries the attack!"
|
||||
messages.append(msg % (char, tchar, tchar))
|
||||
elif taction == "defend" and random.random() < 0.5:
|
||||
msg = "%s defends against the attack by %s."
|
||||
messages.append(msg % (tchar, char))
|
||||
elif taction == "flee":
|
||||
msg = "%s stops %s from disengaging, with a hit!"
|
||||
flee[tchar] = -2
|
||||
messages.append(msg % (char, tchar))
|
||||
else:
|
||||
msg = "%s hits %s, bypassing their %s!"
|
||||
messages.append(msg % (char, tchar, taction))
|
||||
elif action == "parry":
|
||||
if taction == "hit":
|
||||
msg = "%s parries the attack by %s."
|
||||
messages.append(msg % (char, tchar))
|
||||
elif taction == "feint":
|
||||
msg = "%s tries to parry, but %s feints and hits!"
|
||||
messages.append(msg % (char, tchar))
|
||||
else:
|
||||
msg = "%s parries to no avail."
|
||||
messages.append(msg % char)
|
||||
elif action == "feint":
|
||||
if taction == "parry":
|
||||
msg = "%s feints past %s's parry, landing a hit!"
|
||||
messages.append(msg % (char, tchar))
|
||||
elif taction == "hit":
|
||||
msg = "%s feints but is defeated by %s hit!"
|
||||
messages.append(msg % (char, tchar))
|
||||
else:
|
||||
msg = "%s feints to no avail."
|
||||
messages.append(msg % char)
|
||||
elif action == "defend":
|
||||
msg = "%s defends."
|
||||
messages.append(msg % char)
|
||||
elif action == "flee":
|
||||
if char in flee:
|
||||
flee[char] += 1
|
||||
else:
|
||||
flee[char] = 1
|
||||
msg = "%s tries to disengage (two subsequent turns needed)"
|
||||
messages.append(msg % char)
|
||||
|
||||
# echo results of each subturn
|
||||
combat_handler.msg_all("\n".join(messages))
|
||||
|
||||
# at the end of both sub-turns, test if anyone fled
|
||||
msg = "%s withdraws from combat."
|
||||
for (char, fleevalue) in flee.items():
|
||||
if fleevalue == 2:
|
||||
combat_handler.msg_all(msg % char)
|
||||
combat_handler.remove_character(char)
|
||||
```
|
||||
|
||||
To make it simple (and to save space), this example rule module actually resolves each interchange
|
||||
twice - first when it gets to each character and then again when handling the target. Also, since we
|
||||
use the combat handler's `msg_all` method here, the system will get pretty spammy. To clean it up,
|
||||
one could imagine tracking all the possible interactions to make sure each pair is only handled and
|
||||
reported once.
|
||||
|
||||
## Combat initiator command
|
||||
|
||||
This is the last component we need, a command to initiate combat. This will tie everything together.
|
||||
We store this with the other combat commands.
|
||||
|
||||
```python
|
||||
# mygame/commands/combat.py
|
||||
|
||||
from evennia import create_script
|
||||
|
||||
class CmdAttack(Command):
|
||||
"""
|
||||
initiates combat
|
||||
|
||||
Usage:
|
||||
attack <target>
|
||||
|
||||
This will initiate combat with <target>. If <target is
|
||||
already in combat, you will join the combat.
|
||||
"""
|
||||
key = "attack"
|
||||
help_category = "General"
|
||||
|
||||
def func(self):
|
||||
"Handle command"
|
||||
if not self.args:
|
||||
self.caller.msg("Usage: attack <target>")
|
||||
return
|
||||
target = self.caller.search(self.args)
|
||||
if not target:
|
||||
return
|
||||
# set up combat
|
||||
if target.ndb.combat_handler:
|
||||
# target is already in combat - join it
|
||||
target.ndb.combat_handler.add_character(self.caller)
|
||||
target.ndb.combat_handler.msg_all("%s joins combat!" % self.caller)
|
||||
else:
|
||||
# create a new combat handler
|
||||
chandler = create_script("combat_handler.CombatHandler")
|
||||
chandler.add_character(self.caller)
|
||||
chandler.add_character(target)
|
||||
self.caller.msg("You attack %s! You are in combat." % target)
|
||||
target.msg("%s attacks you! You are in combat." % self.caller)
|
||||
```
|
||||
|
||||
The `attack` command will not go into the combat cmdset but rather into the default cmdset. See e.g.
|
||||
the [Adding Command Tutorial](Adding-Command-Tutorial) if you are unsure about how to do this.
|
||||
|
||||
## Expanding the example
|
||||
|
||||
At this point you should have a simple but flexible turn-based combat system. We have taken several
|
||||
shortcuts and simplifications in this example. The output to the players is likely too verbose
|
||||
during combat and too limited when it comes to informing about things surrounding it. Methods for
|
||||
changing your commands or list them, view who is in combat etc is likely needed - this will require
|
||||
play testing for each game and style. There is also currently no information displayed for other
|
||||
people happening to be in the same room as the combat - some less detailed information should
|
||||
probably be echoed to the room to
|
||||
show others what's going on.
|
||||
|
|
@ -0,0 +1,431 @@
|
|||
# Tutorial Searching For Objects
|
||||
|
||||
|
||||
You will often want to operate on a specific object in the database. For example when a player
|
||||
attacks a named target you'll need to find that target so it can be attacked. Or when a rain storm
|
||||
draws in you need to find all outdoor-rooms so you can show it raining in them. This tutorial
|
||||
explains Evennia's tools for searching.
|
||||
|
||||
## Things to search for
|
||||
|
||||
The first thing to consider is the base type of the thing you are searching for. Evennia organizes
|
||||
its database into a few main tables: [Objects](Objects), [Accounts](Accounts), [Scripts](Scripts),
|
||||
[Channels](Communications#channels), [Messages](Communication#Msg) and [Help Entries](Help-System).
|
||||
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?
|
||||
|
||||
- The `key` is the name of the entity. While you can get this from `obj.key` the *database field*
|
||||
is actually named `obj.db_key` - this is useful to know only when you do [direct database
|
||||
queries](Tutorial-Searching-For-Objects#queries-in-django). The one exception is `Accounts`, where
|
||||
the database field for `.key` is instead named `username` (this is a Django requirement). When you
|
||||
don't specify search-type, you'll usually search based on key. *Aliases* are extra names given to
|
||||
Objects using something like `@alias` or `obj.aliases.add('name')`. The main search functions (see
|
||||
below) will automatically search for aliases whenever you search by-key.
|
||||
- [Tags](Tags) are the main way to group and identify objects in Evennia. Tags can most often be
|
||||
used (sometimes together with keys) to uniquely identify an object. For example, even though you
|
||||
have two locations with the same name, you can separate them by their tagging (this is how Evennia
|
||||
implements 'zones' seen in other systems). Tags can also have categories, to further organize your
|
||||
data for quick lookups.
|
||||
- An object's [Attributes](Attributes) can also used to find an object. This can be very useful but
|
||||
since Attributes can store almost any data they are far less optimized to search for than Tags or
|
||||
keys.
|
||||
- The object's [Typeclass](Typeclasses) indicate the sub-type of entity. A Character, Flower or
|
||||
Sword are all types of Objects. A Bot is a kind of Account. The database field is called
|
||||
`typeclass_path` and holds the full Python-path to the class. You can usually specify the
|
||||
`typeclass` as an argument to Evennia's search functions as well as use the class directly to limit
|
||||
queries.
|
||||
- The `location` is only relevant for [Objects](Objects) but is a very common way to weed down the
|
||||
number of candidates before starting to search. The reason is that most in-game commands tend to
|
||||
operate on things nearby (in the same room) so the choices can be limited from the start.
|
||||
- The database id or the '#dbref' is unique (and never re-used) within each database table. So while
|
||||
there is one and only one Object with dbref `#42` there could also be an Account or Script with the
|
||||
dbref `#42` at the same time. In almost all search methods you can replace the "key" search
|
||||
criterion with `"#dbref"` to search for that id. This can occasionally be practical and may be what
|
||||
you are used to from other code bases. But it is considered *bad practice* in Evennia to rely on
|
||||
hard-coded #dbrefs to do your searches. It makes your code tied to the exact layout of the database.
|
||||
It's also not very maintainable to have to remember abstract numbers. Passing the actual objects
|
||||
around and searching by Tags and/or keys will usually get you what you need.
|
||||
|
||||
|
||||
## Getting objects inside another
|
||||
|
||||
All in-game [Objects](Objects) have a `.contents` property that returns all objects 'inside' them
|
||||
(that is, all objects which has its `.location` property set to that object. This is a simple way to
|
||||
get everything in a room and is also faster since this lookup is cached and won't hit the database.
|
||||
|
||||
- `roomobj.contents` returns a list of all objects inside `roomobj`.
|
||||
- `obj.contents` same as for a room, except this usually represents the object's inventory
|
||||
- `obj.location.contents` gets everything in `obj`'s location (including `obj` itself).
|
||||
- `roomobj.exits` returns all exits starting from `roomobj` (Exits are here defined as Objects with
|
||||
their `destination` field set).
|
||||
- `obj.location.contents_get(exclude=obj)` - this helper method returns all objects in `obj`'s
|
||||
location except `obj`.
|
||||
|
||||
## Searching using `Object.search`
|
||||
|
||||
Say you have a [command](Commands), and you want it to do something to a target. You might be
|
||||
wondering how you retrieve that target in code, and that's where Evennia's search utilities come in.
|
||||
In the most common case, you'll often use the `search` method of the `Object` or `Account`
|
||||
typeclasses. In a command, the `.caller` property will refer back to the object using the command
|
||||
(usually a `Character`, which is a type of `Object`) while `.args` will contain Command's arguments:
|
||||
|
||||
```python
|
||||
# e.g. in file mygame/commands/command.py
|
||||
|
||||
from evennia import default_cmds
|
||||
|
||||
class CmdPoke(default_cmds.MuxCommand):
|
||||
"""
|
||||
Pokes someone.
|
||||
|
||||
Usage: poke <target>
|
||||
"""
|
||||
key = "poke"
|
||||
|
||||
def func(self):
|
||||
"""Executes poke command"""
|
||||
target = self.caller.search(self.args)
|
||||
if not target:
|
||||
# we didn't find anyone, but search has already let the
|
||||
# caller know. We'll just return, since we're done
|
||||
return
|
||||
# we found a target! we'll do stuff to them.
|
||||
target.msg("You have been poked by %s." % self.caller)
|
||||
self.caller.msg("You have poked %s." % target)
|
||||
```
|
||||
By default, the search method of a Character will attempt to find a unique object match for the
|
||||
string sent to it (`self.args`, in this case, which is the arguments passed to the command by the
|
||||
player) in the surroundings of the Character - the room or their inventory. If there is no match
|
||||
found, the return value (which is assigned to `target`) will be `None`, and an appropriate failure
|
||||
message will be sent to the Character. If there's not a unique match, `None` will again be returned,
|
||||
and a different error message will be sent asking them to disambiguate the multi-match. By default,
|
||||
the user can then pick out a specific match using with a number and dash preceding the name of the
|
||||
object: `character.search("2-pink unicorn")` will try to find the second pink unicorn in the room.
|
||||
|
||||
The search method has many [arguments](github:evennia.objects.objects#defaultcharactersearch) that
|
||||
allow you to refine the search, such as by designating the location to search in or only matching
|
||||
specific typeclasses.
|
||||
|
||||
## Searching using `utils.search`
|
||||
|
||||
Sometimes you will want to find something that isn't tied to the search methods of a character or
|
||||
account. In these cases, Evennia provides a [utility module with a number of search
|
||||
functions](github:evennia.utils.search). For example, suppose you want a command that will find and
|
||||
display all the rooms that are tagged as a 'hangout', for people to gather by. Here's a simple
|
||||
Command to do this:
|
||||
|
||||
```python
|
||||
# e.g. in file mygame/commands/command.py
|
||||
|
||||
from evennia import default_cmds
|
||||
from evennia.utils.search import search_tag
|
||||
|
||||
class CmdListHangouts(default_cmds.MuxCommand):
|
||||
"""Lists hangouts"""
|
||||
key = "hangouts"
|
||||
|
||||
def func(self):
|
||||
"""Executes 'hangouts' command"""
|
||||
hangouts = search_tag(key="hangout",
|
||||
category="location tags")
|
||||
self.caller.msg("Hangouts available: {}".format(
|
||||
", ".join(str(ob) for ob in hangouts)))
|
||||
```
|
||||
|
||||
This uses the `search_tag` function to find all objects previously tagged with [Tags](Tags)
|
||||
"hangout" and with category "location tags".
|
||||
|
||||
Other important search methods in `utils.search` are
|
||||
|
||||
- `search_object`
|
||||
- `search_account`
|
||||
- `search_scripts`
|
||||
- `search_channel`
|
||||
- `search_message`
|
||||
- `search_help`
|
||||
- `search_tag` - find Objects with a given Tag.
|
||||
- `search_account_tag` - find Accounts with a given Tag.
|
||||
- `search_script_tag` - find Scripts with a given Tag.
|
||||
- `search_channel_tag` - find Channels with a given Tag.
|
||||
- `search_object_attribute` - find Objects with a given Attribute.
|
||||
- `search_account_attribute` - find Accounts with a given Attribute.
|
||||
- `search_attribute_object` - this returns the actual Attribute, not the object it sits on.
|
||||
|
||||
> Note: All search functions return a Django `queryset` which is technically a list-like
|
||||
representation of the database-query it's about to do. Only when you convert it to a real list, loop
|
||||
over it or try to slice or access any of its contents will the datbase-lookup happen. This means you
|
||||
could yourself customize the query further if you know what you are doing (see the next section).
|
||||
|
||||
## Queries in Django
|
||||
|
||||
*This is an advanced topic.*
|
||||
|
||||
Evennia's search methods should be sufficient for the vast majority of situations. But eventually
|
||||
you might find yourself trying to figure out how to get searches for unusual circumstances: Maybe
|
||||
you want to find all characters who are *not* in rooms tagged as hangouts *and* have the lycanthrope
|
||||
tag *and* whose names start with a vowel, but *not* with 'Ab', and *only if* they have 3 or more
|
||||
objects in their inventory ... You could in principle use one of the earlier search methods to find
|
||||
all candidates and then loop over them with a lot of if statements in raw Python. But you can do
|
||||
this much more efficiently by querying the database directly.
|
||||
|
||||
Enter [django's querysets](https://docs.djangoproject.com/en/1.11/ref/models/querysets/). A QuerySet
|
||||
is the representation of a database query and can be modified as desired. Only once one tries to
|
||||
retrieve the data of that query is it *evaluated* and does an actual database request. This is
|
||||
useful because it means you can modify a query as much as you want (even pass it around) and only
|
||||
hit the database once you are happy with it.
|
||||
Evennia's search functions are themselves an even higher level wrapper around Django's queries, and
|
||||
many search methods return querysets. That means that you could get the result from a search
|
||||
function and modify the resulting query to your own ends to further tweak what you search for.
|
||||
|
||||
Evaluated querysets can either contain objects such as Character objects, or lists of values derived
|
||||
from the objects. Queries usually use the 'manager' object of a class, which by convention is the
|
||||
`.objects` attribute of a class. For example, a query of Accounts that contain the letter 'a' could
|
||||
be:
|
||||
|
||||
```python
|
||||
from typeclasses.accounts import Account
|
||||
|
||||
queryset = Account.objects.filter(username__contains='a')
|
||||
|
||||
```
|
||||
|
||||
The `filter` method of a manager takes arguments that allow you to define the query, and you can
|
||||
continue to refine the query by calling additional methods until you evaluate the queryset, causing
|
||||
the query to be executed and return a result. For example, if you have the result above, you could,
|
||||
without causing the queryset to be evaluated yet, get rid of matches that contain the letter 'e by
|
||||
doing this:
|
||||
|
||||
```python
|
||||
queryset = result.exclude(username__contains='e')
|
||||
|
||||
```
|
||||
|
||||
> You could also have chained `.exclude` directly to the end of the previous line.
|
||||
|
||||
Once you try to access the result, the queryset will be evaluated automatically under the hood:
|
||||
|
||||
```python
|
||||
accounts = list(queryset) # this fills list with matches
|
||||
|
||||
for account in queryset:
|
||||
# do something with account
|
||||
|
||||
accounts = queryset[:4] # get first four matches
|
||||
account = queryset[0] # get first match
|
||||
# etc
|
||||
|
||||
```
|
||||
|
||||
### Limiting by typeclass
|
||||
|
||||
Although `Character`s, `Exit`s, `Room`s, and other children of `DefaultObject` all shares the same
|
||||
underlying database table, Evennia provides a shortcut to do more specific queries only for those
|
||||
typeclasses. For example, to find only `Character`s whose names start with 'A', you might do:
|
||||
|
||||
```python
|
||||
Character.objects.filter(db_key__startswith="A")
|
||||
|
||||
```
|
||||
|
||||
If Character has a subclass `Npc` and you wanted to find only Npc's you'd instead do
|
||||
|
||||
```python
|
||||
Npc.objects.filter(db_key__startswith="A")
|
||||
|
||||
```
|
||||
|
||||
If you wanted to search both Characters and all its subclasses (like Npc) you use the `*_family`
|
||||
method which is added by Evennia:
|
||||
|
||||
|
||||
```python
|
||||
Character.objects.filter_family(db_key__startswith="A")
|
||||
```
|
||||
|
||||
The higher up in the inheritance hierarchy you go the more objects will be included in these
|
||||
searches. There is one special case, if you really want to include *everything* from a given
|
||||
database table. You do that by searching on the database model itself. These are named `ObjectDB`,
|
||||
`AccountDB`, `ScriptDB` etc.
|
||||
|
||||
```python
|
||||
from evennia import AccountDB
|
||||
|
||||
# all Accounts in the database, regardless of typeclass
|
||||
all = AccountDB.objects.all()
|
||||
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
## Multiple conditions
|
||||
|
||||
If you pass more than one keyword argument to a query method, the query becomes an `AND`
|
||||
relationship. For example, if we want to find characters whose names start with "A" *and* are also
|
||||
werewolves (have the `lycanthrope` tag), we might do:
|
||||
|
||||
```python
|
||||
queryset = Character.objects.filter(db_key__startswith="A", db_tags__db_key="lycanthrope")
|
||||
```
|
||||
|
||||
To exclude lycanthropes currently in rooms tagged as hangouts, we might tack on an `.exclude` as
|
||||
before:
|
||||
|
||||
```python
|
||||
queryset = quersyet.exclude(db_location__db_tags__db_key="hangout")
|
||||
```
|
||||
|
||||
Note the syntax of the keywords in building the queryset. For example, `db_location` is the name of
|
||||
the database field sitting on (in this case) the `Character` (Object). Double underscore `__` works
|
||||
like dot-notation in normal Python (it's used since dots are not allowed in keyword names). So the
|
||||
instruction `db_location__db_tags__db_key="hangout"` should be read as such:
|
||||
|
||||
1. "On the `Character` object ... (this comes from us building this queryset using the
|
||||
`Character.objects` manager)
|
||||
2. ... get the value of the `db_location` field ... (this references a Room object, normally)
|
||||
3. ... on that location, get the value of the `db_tags` field ... (this is a many-to-many field that
|
||||
can be treated like an object for this purpose. It references all tags on the location)
|
||||
4. ... through the `db_tag` manager, find all Tags having a field `db_key` set to the value
|
||||
"hangout"."
|
||||
|
||||
This may seem a little complex at first, but this syntax will work the same for all queries. Just
|
||||
remember that all *database-fields* in Evennia are prefaced with `db_`. So even though Evennia is
|
||||
nice enough to alias the `db_key` field so you can normally just do `char.key` to get a character's
|
||||
name, the database field is actually called `db_key` and the real name must be used for the purpose
|
||||
of building a query.
|
||||
|
||||
> Don't confuse database fields with [Attributes](Attributes) 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. You can get attached Attributes
|
||||
manually through the `db_attributes` many-to-many field in the same way as `db_tags` above.
|
||||
|
||||
### Complex queries
|
||||
|
||||
What if you want to have a query with with `OR` conditions or negated requirements (`NOT`)? Enter
|
||||
Django's Complex Query object,
|
||||
[Q](https://docs.djangoproject.com/en/1.11/topics/db/queries/#complex-lookups-with-q-objects). `Q()`
|
||||
objects take a normal django keyword query as its arguments. The special thing is that these Q
|
||||
objects can then be chained together with set operations: `|` for OR, `&` for AND, and preceded with
|
||||
`~` for NOT to build a combined, complex query.
|
||||
|
||||
In our original Lycanthrope example we wanted our werewolves to have names that could start with any
|
||||
vowel except for the specific beginning "ab".
|
||||
|
||||
```python
|
||||
from django.db.models import Q
|
||||
from typeclasses.characters import Character
|
||||
|
||||
query = Q()
|
||||
for letter in ("aeiouy"):
|
||||
query |= Q(db_key__istartswith=letter)
|
||||
query &= ~Q(db_key__istartswith="ab")
|
||||
query = Character.objects.filter(query)
|
||||
|
||||
list_of_lycanthropes = list(query)
|
||||
```
|
||||
|
||||
In the above example, we construct our query our of several Q objects that each represent one part
|
||||
of the query. We iterate over the list of vowels, and add an `OR` condition to the query using `|=`
|
||||
(this is the same idea as using `+=` which may be more familiar). Each `OR` condition checks that
|
||||
the name starts with one of the valid vowels. Afterwards, we add (using `&=`) an `AND` condition
|
||||
that is negated with the `~` symbol. In other words we require that any match should *not* start
|
||||
with the string "ab". Note that we don't actually hit the database until we convert the query to a
|
||||
list at the end (we didn't need to do that either, but could just have kept the query until we
|
||||
needed to do something with the matches).
|
||||
|
||||
### Annotations and `F` objects
|
||||
|
||||
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* retrieve all interesting candidates and run them through a for-loop to get and count
|
||||
their `.content` properties. We'd then just return a list of only those objects with enough
|
||||
contents. It would look something like this (note: don't actually do this!):
|
||||
|
||||
```python
|
||||
# probably not a good idea to do it this way
|
||||
|
||||
from typeclasses.rooms import Room
|
||||
|
||||
queryset = Room.objects.all() # get all Rooms
|
||||
rooms = [room for room in queryset if len(room.contents) >= 5]
|
||||
|
||||
```
|
||||
|
||||
Once the number of rooms in your game increases, this could become quite expensive. Additionally, in
|
||||
some particular contexts, like when using the web features of Evennia, you must have the result as a
|
||||
queryset in order to use it in operations, such as in Django's admin interface when creating list
|
||||
filters.
|
||||
|
||||
Enter [F objects](https://docs.djangoproject.com/en/1.11/ref/models/expressions/#f-expressions) and
|
||||
*annotations*. So-called F expressions allow you to do a query that looks at a value of each object
|
||||
in the database, while annotations allow you to calculate and attach a value to a query. So, let's
|
||||
do the same example as before directly in the database:
|
||||
|
||||
```python
|
||||
from typeclasses.rooms import Room
|
||||
from django.db.models import Count
|
||||
|
||||
room_count = Room.objects.annotate(num_objects=Count('locations_set'))
|
||||
queryset = room_count.filter(num_objects__gte=5)
|
||||
|
||||
rooms = (Room.objects.annotate(num_objects=Count('locations_set'))
|
||||
.filter(num_objects__gte=5))
|
||||
|
||||
rooms = list(rooms)
|
||||
|
||||
```
|
||||
Here we first create an annotation `num_objects` of type `Count`, which is a Django class. Note that
|
||||
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*.
|
||||
Once we have those, they are counted.
|
||||
Next we filter on this annotation, using the name `num_objects` as something we can filter for. We
|
||||
use `num_objects__gte=5` which means that `num_objects` should be greater than 5. This is a little
|
||||
harder to get one's head around but much more efficient than lopping over all objects in Python.
|
||||
|
||||
What if we wanted to compare two 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? Here an F-object comes in handy:
|
||||
|
||||
```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')))
|
||||
```
|
||||
|
||||
F-objects allows for wrapping an annotated structure on the right-hand-side of the expression. It
|
||||
will be evaluated on-the-fly as needed.
|
||||
|
||||
### Grouping By and Values
|
||||
|
||||
Suppose you used tags to mark someone belonging an organization. Now you want to make a list and
|
||||
need to get the membership count of every organization all at once. That's where annotations and the
|
||||
`.values_list` queryset method come in. Values/Values Lists are an alternate way of returning a
|
||||
queryset - instead of objects, you get a list of dicts or tuples that hold selected properties from
|
||||
the the matches. It also allows you a way to 'group up' queries for returning information. For
|
||||
example, to get a display about each tag per Character and the names of the tag:
|
||||
|
||||
```python
|
||||
result = (Character.objects.filter(db_tags__db_category="organization")
|
||||
.values_list('db_tags__db_key')
|
||||
.annotate(cnt=Count('id'))
|
||||
.order_by('-cnt'))
|
||||
```
|
||||
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 Fanclub', 3872), ("Chainsol's Ainneve Testers", 2076), ("Blaufeuer's Whitespace Fixers",
|
||||
1903),
|
||||
("Volund's Bikeshed Design Crew", 1764), ("Tehom's Misanthropes", 1)]
|
||||
|
|
@ -0,0 +1,654 @@
|
|||
# Tutorial for basic MUSH like game
|
||||
|
||||
|
||||
This tutorial lets you code a small but complete and functioning MUSH-like game in Evennia. A
|
||||
[MUSH](http://en.wikipedia.org/wiki/MUSH) is, for our purposes, a class of roleplay-centric games
|
||||
focused on free form storytelling. Even if you are not interested in MUSH:es, this is still a good
|
||||
first game-type to try since it's not so code heavy. You will be able to use the same principles for
|
||||
building other types of games.
|
||||
|
||||
The tutorial starts from scratch. If you did the [First Steps Coding](First-Steps-Coding) tutorial
|
||||
already you should have some ideas about how to do some of the steps already.
|
||||
|
||||
The following are the (very simplistic and cut-down) features we will implement (this was taken from
|
||||
a feature request from a MUSH user new to Evennia). A Character in this system should:
|
||||
|
||||
- Have a “Power” score from 1 to 10 that measures how strong they are (stand-in for the stat
|
||||
system).
|
||||
- Have a command (e.g. `+setpower 4`) that sets their power (stand-in for character generation
|
||||
code).
|
||||
- Have a command (e.g. `+attack`) that lets them roll their power and produce a "Combat Score"
|
||||
between `1` and `10*Power`, displaying the result and editing their object to record this number
|
||||
(stand-in for `+actions` in the command code).
|
||||
- Have a command that displays everyone in the room and what their most recent "Combat Score" roll
|
||||
was (stand-in for the combat code).
|
||||
- Have a command (e.g. `+createNPC Jenkins`) that creates an NPC with full abilities.
|
||||
- Have a command to control NPCs, such as `+npc/cmd (name)=(command)` (stand-in for the NPC
|
||||
controlling code).
|
||||
|
||||
In this tutorial we will assume you are starting from an empty database without any previous
|
||||
modifications.
|
||||
|
||||
## Server Settings
|
||||
|
||||
To emulate a MUSH, the default `MULTISESSION_MODE=0` is enough (one unique session per
|
||||
account/character). This is the default so you don't need to change anything. You will still be able
|
||||
to puppet/unpuppet objects you have permission to, but there is no character selection out of the
|
||||
box in this mode.
|
||||
|
||||
We will assume our game folder is called `mygame` henceforth. You should be fine with the default
|
||||
SQLite3 database.
|
||||
|
||||
## Creating the Character
|
||||
|
||||
First thing is to choose how our Character class works. We don't need to define a special NPC object
|
||||
-- an NPC is after all just a Character without an Account currently controlling them.
|
||||
|
||||
Make your changes in the `mygame/typeclasses/characters.py` file:
|
||||
|
||||
```python
|
||||
# mygame/typeclasses/characters.py
|
||||
|
||||
from evennia import DefaultCharacter
|
||||
|
||||
class Character(DefaultCharacter):
|
||||
"""
|
||||
[...]
|
||||
"""
|
||||
def at_object_creation(self):
|
||||
"This is called when object is first created, only."
|
||||
self.db.power = 1
|
||||
self.db.combat_score = 1
|
||||
```
|
||||
|
||||
We defined two new [Attributes](Attributes) `power` and `combat_score` and set them to default
|
||||
values. Make sure to `@reload` the server if you had it already running (you need to reload every
|
||||
time you update your python code, don't worry, no accounts will be disconnected by the reload).
|
||||
|
||||
Note that only *new* characters will see your new Attributes (since the `at_object_creation` hook is
|
||||
called when the object is first created, existing Characters won't have it). To update yourself,
|
||||
run
|
||||
|
||||
@typeclass/force self
|
||||
|
||||
This resets your own typeclass (the `/force` switch is a safety measure to not do this
|
||||
accidentally), this means that `at_object_creation` is re-run.
|
||||
|
||||
examine self
|
||||
|
||||
Under the "Persistent attributes" heading you should now find the new Attributes `power` and `score`
|
||||
set on yourself by `at_object_creation`. If you don't, first make sure you `@reload`ed into the new
|
||||
code, next look at your server log (in the terminal/console) to see if there were any syntax errors
|
||||
in your code that may have stopped your new code from loading correctly.
|
||||
|
||||
## Character Generation
|
||||
|
||||
We assume in this example that Accounts first connect into a "character generation area". Evennia
|
||||
also supports full OOC menu-driven character generation, but for this example, a simple start room
|
||||
is enough. When in this room (or rooms) we allow character generation commands. In fact, character
|
||||
generation commands will *only* be available in such rooms.
|
||||
|
||||
Note that this again is made so as to be easy to expand to a full-fledged game. With our simple
|
||||
example, we could simply set an `is_in_chargen` flag on the account and have the `+setpower` command
|
||||
check it. Using this method however will make it easy to add more functionality later.
|
||||
|
||||
What we need are the following:
|
||||
|
||||
- One character generation [Command](Commands) to set the "Power" on the `Character`.
|
||||
- A chargen [CmdSet](Command-Sets) to hold this command. Lets call it `ChargenCmdset`.
|
||||
- A custom `ChargenRoom` type that makes this set of commands available to players in such rooms.
|
||||
- One such room to test things in.
|
||||
|
||||
### The +setpower command
|
||||
|
||||
For this tutorial we will add all our new commands to `mygame/commands/command.py` but you could
|
||||
split your commands into multiple module if you prefered.
|
||||
|
||||
For this tutorial character generation will only consist of one [Command](Commands) to set the
|
||||
Character s "power" stat. It will be called on the following MUSH-like form:
|
||||
|
||||
+setpower 4
|
||||
|
||||
Open `command.py` file. It contains documented empty templates for the base command and the
|
||||
"MuxCommand" type used by default in Evennia. We will use the plain `Command` type here, the
|
||||
`MuxCommand` class offers some extra features like stripping whitespace that may be useful - if so,
|
||||
just import from that instead.
|
||||
|
||||
Add the following to the end of the `command.py` file:
|
||||
|
||||
```python
|
||||
# end of command.py
|
||||
from evennia import Command # just for clarity; already imported above
|
||||
|
||||
class CmdSetPower(Command):
|
||||
"""
|
||||
set the power of a character
|
||||
|
||||
Usage:
|
||||
+setpower <1-10>
|
||||
|
||||
This sets the power of the current character. This can only be
|
||||
used during character generation.
|
||||
"""
|
||||
|
||||
key = "+setpower"
|
||||
help_category = "mush"
|
||||
|
||||
def func(self):
|
||||
"This performs the actual command"
|
||||
errmsg = "You must supply a number between 1 and 10."
|
||||
if not self.args:
|
||||
self.caller.msg(errmsg)
|
||||
return
|
||||
try:
|
||||
power = int(self.args)
|
||||
except ValueError:
|
||||
self.caller.msg(errmsg)
|
||||
return
|
||||
if not (1 <= power <= 10):
|
||||
self.caller.msg(errmsg)
|
||||
return
|
||||
# at this point the argument is tested as valid. Let's set it.
|
||||
self.caller.db.power = power
|
||||
self.caller.msg("Your Power was set to %i." % power)
|
||||
```
|
||||
This is a pretty straightforward command. We do some error checking, then set the power on ourself.
|
||||
We use a `help_category` of "mush" for all our commands, just so they are easy to find and separate
|
||||
in the help list.
|
||||
|
||||
Save the file. We will now add it to a new [CmdSet](Command-Sets) so it can be accessed (in a full
|
||||
chargen system you would of course have more than one command here).
|
||||
|
||||
Open `mygame/commands/default_cmdsets.py` and import your `command.py` module at the top. We also
|
||||
import the default `CmdSet` class for the next step:
|
||||
|
||||
```python
|
||||
from evennia import CmdSet
|
||||
from commands import command
|
||||
```
|
||||
|
||||
Next scroll down and define a new command set (based on the base `CmdSet` class we just imported at
|
||||
the end of this file, to hold only our chargen-specific command(s):
|
||||
|
||||
```python
|
||||
# end of default_cmdsets.py
|
||||
|
||||
class ChargenCmdset(CmdSet):
|
||||
"""
|
||||
This cmdset it used in character generation areas.
|
||||
"""
|
||||
key = "Chargen"
|
||||
def at_cmdset_creation(self):
|
||||
"This is called at initialization"
|
||||
self.add(command.CmdSetPower())
|
||||
```
|
||||
|
||||
In the future you can add any number of commands to this cmdset, to expand your character generation
|
||||
system as you desire. Now we need to actually put that cmdset on something so it's made available to
|
||||
users. We could put it directly on the Character, but that would make it available all the time.
|
||||
It's cleaner to put it on a room, so it's only available when players are in that room.
|
||||
|
||||
### Chargen areas
|
||||
|
||||
We will create a simple Room typeclass to act as a template for all our Chargen areas. Edit
|
||||
`mygame/typeclasses/rooms.py` next:
|
||||
|
||||
```python
|
||||
from commands.default_cmdsets import ChargenCmdset
|
||||
|
||||
# ...
|
||||
# down at the end of rooms.py
|
||||
|
||||
class ChargenRoom(Room):
|
||||
"""
|
||||
This room class is used by character-generation rooms. It makes
|
||||
the ChargenCmdset available.
|
||||
"""
|
||||
def at_object_creation(self):
|
||||
"this is called only at first creation"
|
||||
self.cmdset.add(ChargenCmdset, permanent=True)
|
||||
```
|
||||
Note how new rooms created with this typeclass will always start with `ChargenCmdset` on themselves.
|
||||
Don't forget the `permanent=True` keyword or you will lose the cmdset after a server reload. For
|
||||
more information about [Command Sets](Command-Sets) and [Commands](Commands), see the respective
|
||||
links.
|
||||
|
||||
### Testing chargen
|
||||
|
||||
First, make sure you have `@reload`ed the server (or use `evennia reload` from the terminal) to have
|
||||
your new python code added to the game. Check your terminal and fix any errors you see - the error
|
||||
traceback lists exactly where the error is found - look line numbers in files you have changed.
|
||||
|
||||
We can't test things unless we have some chargen areas to test. Log into the game (you should at
|
||||
this point be using the new, custom Character class). Let's dig a chargen area to test.
|
||||
|
||||
@dig chargen:rooms.ChargenRoom = chargen,finish
|
||||
|
||||
If you read the help for `@dig` you will find that this will create a new room named `chargen`. The
|
||||
part after the `:` is the python-path to the Typeclass you want to use. Since Evennia will
|
||||
automatically try the `typeclasses` folder of our game directory, we just specify
|
||||
`rooms.ChargenRoom`, meaning it will look inside the module `rooms.py` for a class named
|
||||
`ChargenRoom` (which is what we created above). The names given after `=` are the names of exits to
|
||||
and from the room from your current location. You could also append aliases to each one name, such
|
||||
as `chargen;character generation`.
|
||||
|
||||
So in summary, this will create a new room of type ChargenRoom and open an exit `chargen` to it and
|
||||
an exit back here named `finish`. If you see errors at this stage, you must fix them in your code.
|
||||
`@reload`
|
||||
between fixes. Don't continue until the creation seems to have worked okay.
|
||||
|
||||
chargen
|
||||
|
||||
This should bring you to the chargen room. Being in there you should now have the `+setpower`
|
||||
command available, so test it out. When you leave (via the `finish` exit), the command will go away
|
||||
and trying `+setpower` should now give you a command-not-found error. Use `ex me` (as a privileged
|
||||
user) to check so the `Power` [Attribute](Attributes) has been set correctly.
|
||||
|
||||
If things are not working, make sure your typeclasses and commands are free of bugs and that you
|
||||
have entered the paths to the various command sets and commands correctly. Check the logs or command
|
||||
line for tracebacks and errors.
|
||||
|
||||
## Combat System
|
||||
|
||||
We will add our combat command to the default command set, meaning it will be available to everyone
|
||||
at all times. The combat system consists of a `+attack` command to get how successful our attack is.
|
||||
We also change the default `look` command to display the current combat score.
|
||||
|
||||
|
||||
### Attacking with the +attack command
|
||||
|
||||
Attacking in this simple system means rolling a random "combat score" influenced by the `power` stat
|
||||
set during Character generation:
|
||||
|
||||
> +attack
|
||||
You +attack with a combat score of 12!
|
||||
|
||||
Go back to `mygame/commands/command.py` and add the command to the end like this:
|
||||
|
||||
```python
|
||||
import random
|
||||
|
||||
# ...
|
||||
|
||||
class CmdAttack(Command):
|
||||
"""
|
||||
issues an attack
|
||||
|
||||
Usage:
|
||||
+attack
|
||||
|
||||
This will calculate a new combat score based on your Power.
|
||||
Your combat score is visible to everyone in the same location.
|
||||
"""
|
||||
key = "+attack"
|
||||
help_category = "mush"
|
||||
|
||||
def func(self):
|
||||
"Calculate the random score between 1-10*Power"
|
||||
caller = self.caller
|
||||
power = caller.db.power
|
||||
if not power:
|
||||
# this can happen if caller is not of
|
||||
# our custom Character typeclass
|
||||
power = 1
|
||||
combat_score = random.randint(1, 10 * power)
|
||||
caller.db.combat_score = combat_score
|
||||
|
||||
# announce
|
||||
message = "%s +attack%s with a combat score of %s!"
|
||||
caller.msg(message % ("You", "", combat_score))
|
||||
caller.location.msg_contents(message %
|
||||
(caller.key, "s", combat_score),
|
||||
exclude=caller)
|
||||
```
|
||||
|
||||
What we do here is simply to generate a "combat score" using Python's inbuilt `random.randint()`
|
||||
function. We then store that and echo the result to everyone involved.
|
||||
|
||||
To make the `+attack` command available to you in game, go back to
|
||||
`mygame/commands/default_cmdsets.py` and scroll down to the `CharacterCmdSet` class. At the correct
|
||||
place add this line:
|
||||
|
||||
```python
|
||||
self.add(command.CmdAttack())
|
||||
```
|
||||
|
||||
`@reload` Evennia and the `+attack` command should be available to you. Run it and use e.g. `@ex` to
|
||||
make sure the `combat_score` attribute is saved correctly.
|
||||
|
||||
### Have "look" show combat scores
|
||||
|
||||
Players should be able to view all current combat scores in the room. We could do this by simply
|
||||
adding a second command named something like `+combatscores`, but we will instead let the default
|
||||
`look` command do the heavy lifting for us and display our scores as part of its normal output, like
|
||||
this:
|
||||
|
||||
> look Tom
|
||||
Tom (combat score: 3)
|
||||
This is a great warrior.
|
||||
|
||||
We don't actually have to modify the `look` command itself however. To understand why, take a look
|
||||
at how the default `look` is actually defined. It sits in `evennia/commands/default/general.py` (or
|
||||
browse it online
|
||||
[here](https://github.com/evennia/evennia/blob/master/evennia/commands/default/general.py#L44)).
|
||||
You will find that the actual return text is done by the `look` command calling a *hook method*
|
||||
named `return_appearance` on the object looked at. All the `look` does is to echo whatever this hook
|
||||
returns. So what we need to do is to edit our custom Character typeclass and overload its
|
||||
`return_appearance` to return what we want (this is where the advantage of having a custom typeclass
|
||||
comes into play for real).
|
||||
|
||||
Go back to your custom Character typeclass in `mygame/typeclasses/characters.py`. The default
|
||||
implementation of `return appearance` is found in `evennia.DefaultCharacter` (or online
|
||||
[here](https://github.com/evennia/evennia/blob/master/evennia/objects/objects.py#L1438)). If you
|
||||
want to make bigger changes you could copy & paste the whole default thing into our overloading
|
||||
method. In our case the change is small though:
|
||||
|
||||
```python
|
||||
class Character(DefaultCharacter):
|
||||
"""
|
||||
[...]
|
||||
"""
|
||||
def at_object_creation(self):
|
||||
"This is called when object is first created, only."
|
||||
self.db.power = 1
|
||||
self.db.combat_score = 1
|
||||
|
||||
def return_appearance(self, looker):
|
||||
"""
|
||||
The return from this method is what
|
||||
looker sees when looking at this object.
|
||||
"""
|
||||
text = super().return_appearance(looker)
|
||||
cscore = " (combat score: %s)" % self.db.combat_score
|
||||
if "\n" in text:
|
||||
# text is multi-line, add score after first line
|
||||
first_line, rest = text.split("\n", 1)
|
||||
text = first_line + cscore + "\n" + rest
|
||||
else:
|
||||
# text is only one line; add score to end
|
||||
text += cscore
|
||||
return text
|
||||
```
|
||||
|
||||
What we do is to simply let the default `return_appearance` do its thing (`super` will call the
|
||||
parent's version of the same method). We then split out the first line of this text, append our
|
||||
`combat_score` and put it back together again.
|
||||
|
||||
`@reload` the server and you should be able to look at other Characters and see their current combat
|
||||
scores.
|
||||
|
||||
> Note: A potentially more useful way to do this would be to overload the entire `return_appearance`
|
||||
of the `Room`s of your mush and change how they list their contents; in that way one could see all
|
||||
combat scores of all present Characters at the same time as looking at the room. We leave this as an
|
||||
exercise.
|
||||
|
||||
## NPC system
|
||||
|
||||
Here we will re-use the Character class by introducing a command that can create NPC objects. We
|
||||
should also be able to set its Power and order it around.
|
||||
|
||||
There are a few ways to define the NPC class. We could in theory create a custom typeclass for it
|
||||
and put a custom NPC-specific cmdset on all NPCs. This cmdset could hold all manipulation commands.
|
||||
Since we expect NPC manipulation to be a common occurrence among the user base however, we will
|
||||
instead put all relevant NPC commands in the default command set and limit eventual access with
|
||||
[Permissions and Locks](Locks#Permissions).
|
||||
|
||||
### Creating an NPC with +createNPC
|
||||
|
||||
We need a command for creating the NPC, this is a very straightforward command:
|
||||
|
||||
> +createnpc Anna
|
||||
You created the NPC 'Anna'.
|
||||
|
||||
At the end of `command.py`, create our new command:
|
||||
|
||||
```python
|
||||
from evennia import create_object
|
||||
|
||||
class CmdCreateNPC(Command):
|
||||
"""
|
||||
create a new npc
|
||||
|
||||
Usage:
|
||||
+createNPC <name>
|
||||
|
||||
Creates a new, named NPC. The NPC will start with a Power of 1.
|
||||
"""
|
||||
key = "+createnpc"
|
||||
aliases = ["+createNPC"]
|
||||
locks = "call:not perm(nonpcs)"
|
||||
help_category = "mush"
|
||||
|
||||
def func(self):
|
||||
"creates the object and names it"
|
||||
caller = self.caller
|
||||
if not self.args:
|
||||
caller.msg("Usage: +createNPC <name>")
|
||||
return
|
||||
if not caller.location:
|
||||
# may not create npc when OOC
|
||||
caller.msg("You must have a location to create an npc.")
|
||||
return
|
||||
# make name always start with capital letter
|
||||
name = self.args.strip().capitalize()
|
||||
# create npc in caller's location
|
||||
npc = create_object("characters.Character",
|
||||
key=name,
|
||||
location=caller.location,
|
||||
locks="edit:id(%i) and perm(Builders);call:false()" % caller.id)
|
||||
# announce
|
||||
message = "%s created the NPC '%s'."
|
||||
caller.msg(message % ("You", name))
|
||||
caller.location.msg_contents(message % (caller.key, name),
|
||||
exclude=caller)
|
||||
```
|
||||
Here we define a `+createnpc` (`+createNPC` works too) that is callable by everyone *not* having the
|
||||
`nonpcs` "[permission](Locks#Permissions)" (in Evennia, a "permission" can just as well be used to
|
||||
block access, it depends on the lock we define). We create the NPC object in the caller's current
|
||||
location, using our custom `Character` typeclass to do so.
|
||||
|
||||
We set an extra lock condition on the NPC, which we will use to check who may edit the NPC later --
|
||||
we allow the creator to do so, and anyone with the Builders permission (or higher). See
|
||||
[Locks](Locks) for more information about the lock system.
|
||||
|
||||
Note that we just give the object default permissions (by not specifying the `permissions` keyword
|
||||
to the `create_object()` call). In some games one might want to give the NPC the same permissions
|
||||
as the Character creating them, this might be a security risk though.
|
||||
|
||||
Add this command to your default cmdset the same way you did the `+attack` command earlier.
|
||||
`@reload` and it will be available to test.
|
||||
|
||||
### Editing the NPC with +editNPC
|
||||
|
||||
Since we re-used our custom character typeclass, our new NPC already has a *Power* value - it
|
||||
defaults to 1. How do we change this?
|
||||
|
||||
There are a few ways we can do this. The easiest is to remember that the `power` attribute is just a
|
||||
simple [Attribute](Attributes) stored on the NPC object. So as a Builder or Admin we could set this
|
||||
right away with the default `@set` command:
|
||||
|
||||
@set mynpc/power = 6
|
||||
|
||||
The `@set` command is too generally powerful though, and thus only available to staff. We will add a
|
||||
custom command that only changes the things we want players to be allowed to change. We could in
|
||||
principle re-work our old `+setpower` command, but let's try something more useful. Let's make a
|
||||
`+editNPC` command.
|
||||
|
||||
> +editNPC Anna/power = 10
|
||||
Set Anna's property 'power' to 10.
|
||||
|
||||
This is a slightly more complex command. It goes at the end of your `command.py` file as before.
|
||||
|
||||
```python
|
||||
class CmdEditNPC(Command):
|
||||
"""
|
||||
edit an existing NPC
|
||||
|
||||
Usage:
|
||||
+editnpc <name>[/<attribute> [= value]]
|
||||
|
||||
Examples:
|
||||
+editnpc mynpc/power = 5
|
||||
+editnpc mynpc/power - displays power value
|
||||
+editnpc mynpc - shows all editable
|
||||
attributes and values
|
||||
|
||||
This command edits an existing NPC. You must have
|
||||
permission to edit the NPC to use this.
|
||||
"""
|
||||
key = "+editnpc"
|
||||
aliases = ["+editNPC"]
|
||||
locks = "cmd:not perm(nonpcs)"
|
||||
help_category = "mush"
|
||||
|
||||
def parse(self):
|
||||
"We need to do some parsing here"
|
||||
args = self.args
|
||||
propname, propval = None, None
|
||||
if "=" in args:
|
||||
args, propval = [part.strip() for part in args.rsplit("=", 1)]
|
||||
if "/" in args:
|
||||
args, propname = [part.strip() for part in args.rsplit("/", 1)]
|
||||
# store, so we can access it below in func()
|
||||
self.name = args
|
||||
self.propname = propname
|
||||
# a propval without a propname is meaningless
|
||||
self.propval = propval if propname else None
|
||||
|
||||
def func(self):
|
||||
"do the editing"
|
||||
|
||||
allowed_propnames = ("power", "attribute1", "attribute2")
|
||||
|
||||
caller = self.caller
|
||||
if not self.args or not self.name:
|
||||
caller.msg("Usage: +editnpc name[/propname][=propval]")
|
||||
return
|
||||
npc = caller.search(self.name)
|
||||
if not npc:
|
||||
return
|
||||
if not npc.access(caller, "edit"):
|
||||
caller.msg("You cannot change this NPC.")
|
||||
return
|
||||
if not self.propname:
|
||||
# this means we just list the values
|
||||
output = "Properties of %s:" % npc.key
|
||||
for propname in allowed_propnames:
|
||||
propvalue = npc.attributes.get(propname, default="N/A")
|
||||
output += "\n %s = %s" % (propname, propvalue)
|
||||
caller.msg(output)
|
||||
elif self.propname not in allowed_propnames:
|
||||
caller.msg("You may only change %s." %
|
||||
", ".join(allowed_propnames))
|
||||
elif self.propval:
|
||||
# assigning a new propvalue
|
||||
# in this example, the properties are all integers...
|
||||
intpropval = int(self.propval)
|
||||
npc.attributes.add(self.propname, intpropval)
|
||||
caller.msg("Set %s's property '%s' to %s" %
|
||||
(npc.key, self.propname, self.propval))
|
||||
else:
|
||||
# propname set, but not propval - show current value
|
||||
caller.msg("%s has property %s = %s" %
|
||||
(npc.key, self.propname,
|
||||
npc.attributes.get(self.propname, default="N/A")))
|
||||
```
|
||||
|
||||
This command example shows off the use of more advanced parsing but otherwise it's mostly error
|
||||
checking. It searches for the given npc in the same room, and checks so the caller actually has
|
||||
permission to "edit" it before continuing. An account without the proper permission won't even be
|
||||
able to view the properties on the given NPC. It's up to each game if this is the way it should be.
|
||||
|
||||
Add this to the default command set like before and you should be able to try it out.
|
||||
|
||||
_Note: If you wanted a player to use this command to change an on-object property like the NPC's
|
||||
name (the `key` property), you'd need to modify the command since "key" is not an Attribute (it is
|
||||
not retrievable via `npc.attributes.get` but directly via `npc.key`). We leave this as an optional
|
||||
exercise._
|
||||
|
||||
### Making the NPC do stuff - the +npc command
|
||||
|
||||
Finally, we will make a command to order our NPC around. For now, we will limit this command to only
|
||||
be usable by those having the "edit" permission on the NPC. This can be changed if it's possible for
|
||||
anyone to use the NPC.
|
||||
|
||||
The NPC, since it inherited our Character typeclass has access to most commands a player does. What
|
||||
it doesn't have access to are Session and Player-based cmdsets (which means, among other things that
|
||||
they cannot chat on channels, but they could do that if you just added those commands). This makes
|
||||
the `+npc` command simple:
|
||||
|
||||
+npc Anna = say Hello!
|
||||
Anna says, 'Hello!'
|
||||
|
||||
Again, add to the end of your `command.py` module:
|
||||
|
||||
```python
|
||||
class CmdNPC(Command):
|
||||
"""
|
||||
controls an NPC
|
||||
|
||||
Usage:
|
||||
+npc <name> = <command>
|
||||
|
||||
This causes the npc to perform a command as itself. It will do so
|
||||
with its own permissions and accesses.
|
||||
"""
|
||||
key = "+npc"
|
||||
locks = "call:not perm(nonpcs)"
|
||||
help_category = "mush"
|
||||
|
||||
def parse(self):
|
||||
"Simple split of the = sign"
|
||||
name, cmdname = None, None
|
||||
if "=" in self.args:
|
||||
name, cmdname = [part.strip()
|
||||
for part in self.args.rsplit("=", 1)]
|
||||
self.name, self.cmdname = name, cmdname
|
||||
|
||||
def func(self):
|
||||
"Run the command"
|
||||
caller = self.caller
|
||||
if not self.cmdname:
|
||||
caller.msg("Usage: +npc <name> = <command>")
|
||||
return
|
||||
npc = caller.search(self.name)
|
||||
if not npc:
|
||||
return
|
||||
if not npc.access(caller, "edit"):
|
||||
caller.msg("You may not order this NPC to do anything.")
|
||||
return
|
||||
# send the command order
|
||||
npc.execute_cmd(self.cmdname)
|
||||
caller.msg("You told %s to do '%s'." % (npc.key, self.cmdname))
|
||||
```
|
||||
|
||||
Note that if you give an erroneous command, you will not see any error message, since that error
|
||||
will be returned to the npc object, not to you. If you want players to see this, you can give the
|
||||
caller's session ID to the `execute_cmd` call, like this:
|
||||
|
||||
```python
|
||||
npc.execute_cmd(self.cmdname, sessid=self.caller.sessid)
|
||||
```
|
||||
|
||||
Another thing to remember is however that this is a very simplistic way to control NPCs. Evennia
|
||||
supports full puppeting very easily. An Account (assuming the "puppet" permission was set correctly)
|
||||
could simply do `@ic mynpc` and be able to play the game "as" that NPC. This is in fact just what
|
||||
happens when an Account takes control of their normal Character as well.
|
||||
|
||||
## Concluding remarks
|
||||
|
||||
This ends the tutorial. It looks like a lot of text but the amount of code you have to write is
|
||||
actually relatively short. At this point you should have a basic skeleton of a game and a feel for
|
||||
what is involved in coding your game.
|
||||
|
||||
From here on you could build a few more ChargenRooms and link that to a bigger grid. The `+setpower`
|
||||
command can either be built upon or accompanied by many more to get a more elaborate character
|
||||
generation.
|
||||
|
||||
The simple "Power" game mechanic should be easily expandable to something more full-fledged and
|
||||
useful, same is true for the combat score principle. The `+attack` could be made to target a
|
||||
specific player (or npc) and automatically compare their relevant attributes to determine a result.
|
||||
|
||||
To continue from here, you can take a look at the [Tutorial World](Tutorial-World-Introduction). For
|
||||
more specific ideas, see the [other tutorials and hints](Tutorials) as well
|
||||
as the [Developer Central](Developer-Central).
|
||||
127
docs/source/Howto/StartingTutorial/Web-Tutorial.md
Normal file
127
docs/source/Howto/StartingTutorial/Web-Tutorial.md
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
# 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) 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](http://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](http://en.wikipedia.org/wiki/Html) for the user, and a `static` folder that holds assets
|
||||
like [CSS](http://en.wikipedia.org/wiki/CSS), [Javascript](http://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](New-Models) 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). 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) 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