mirror of
https://github.com/evennia/evennia.git
synced 2026-03-16 21:06:30 +01:00
Reorganize docs into flat folder layout
This commit is contained in:
parent
106558cec0
commit
892d8efb93
135 changed files with 34 additions and 1180 deletions
232
docs/source/Howto/Add-a-wiki-on-your-website.md
Normal file
232
docs/source/Howto/Add-a-wiki-on-your-website.md
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
# Add a wiki on your website
|
||||
|
||||
|
||||
**Before doing this tutorial you will probably want to read the intro in
|
||||
[Basic Web tutorial](Web-Tutorial).** Reading the three first parts of the
|
||||
[Django tutorial](https://docs.djangoproject.com/en/1.9/intro/tutorial01/) might help as well.
|
||||
|
||||
This tutorial will provide a step-by-step process to installing a wiki on your website.
|
||||
Fortunately, you don't have to create the features manually, since it has been done by others, and
|
||||
we can integrate their work quite easily with Django. I have decided to focus on
|
||||
the [Django-wiki](http://django-wiki.readthedocs.io/).
|
||||
|
||||
> Note: this article has been updated for Evennia 0.9. If you're not yet using this version, be
|
||||
careful, as the django wiki doesn't support Python 2 anymore. (Remove this note when enough time
|
||||
has passed.)
|
||||
|
||||
The [Django-wiki](http://django-wiki.readthedocs.io/) offers a lot of features associated with
|
||||
wikis, is
|
||||
actively maintained (at this time, anyway), and isn't too difficult to install in Evennia. You can
|
||||
see a [demonstration of Django-wiki here](https://demo.django.wiki).
|
||||
|
||||
## Basic installation
|
||||
|
||||
You should begin by shutting down the Evennia server if it is running. We will run migrations and
|
||||
alter the virtual environment just a bit. Open a terminal and activate your Python environment, the
|
||||
one you use to run the `evennia` command.
|
||||
|
||||
* On Linux:
|
||||
```
|
||||
source evenv/bin/activate
|
||||
```
|
||||
* Or Windows:
|
||||
```
|
||||
evenv\bin\activate
|
||||
```
|
||||
|
||||
### Installing with pip
|
||||
|
||||
Install the wiki using pip:
|
||||
|
||||
pip install wiki
|
||||
|
||||
> Note: this will install the last version of Django wiki. Version >0.4 doesn't support Python 2, so
|
||||
install wiki 0.3 if you haven't updated to Python 3 yet.
|
||||
|
||||
It might take some time, the Django-wiki having some dependencies.
|
||||
|
||||
### Adding the wiki in the settings
|
||||
|
||||
You will need to add a few settings to have the wiki app on your website. Open your
|
||||
`server/conf/settings.py` file and add the following at the bottom (but before importing
|
||||
`secret_settings`). Here's what you'll find in my own setting file (add the whole Django-wiki
|
||||
section):
|
||||
|
||||
```python
|
||||
r"""
|
||||
Evennia settings file.
|
||||
|
||||
...
|
||||
|
||||
"""
|
||||
|
||||
# Use the defaults from Evennia unless explicitly overridden
|
||||
from evennia.settings_default import *
|
||||
|
||||
######################################################################
|
||||
# Evennia base server config
|
||||
######################################################################
|
||||
|
||||
# This is the name of your game. Make it catchy!
|
||||
SERVERNAME = "demowiki"
|
||||
|
||||
######################################################################
|
||||
# Django-wiki settings
|
||||
######################################################################
|
||||
INSTALLED_APPS += (
|
||||
'django.contrib.humanize.apps.HumanizeConfig',
|
||||
'django_nyt.apps.DjangoNytConfig',
|
||||
'mptt',
|
||||
'sorl.thumbnail',
|
||||
'wiki.apps.WikiConfig',
|
||||
'wiki.plugins.attachments.apps.AttachmentsConfig',
|
||||
'wiki.plugins.notifications.apps.NotificationsConfig',
|
||||
'wiki.plugins.images.apps.ImagesConfig',
|
||||
'wiki.plugins.macros.apps.MacrosConfig',
|
||||
)
|
||||
|
||||
# Disable wiki handling of login/signup
|
||||
WIKI_ACCOUNT_HANDLING = False
|
||||
WIKI_ACCOUNT_SIGNUP_ALLOWED = False
|
||||
|
||||
######################################################################
|
||||
# Settings given in secret_settings.py override those in this file.
|
||||
######################################################################
|
||||
try:
|
||||
from server.conf.secret_settings import *
|
||||
except ImportError:
|
||||
print("secret_settings.py file not found or failed to import.")
|
||||
```
|
||||
|
||||
### Adding the new URLs
|
||||
|
||||
Next we need to add two URLs in our `web/urls.py` file. Open it and compare the following output:
|
||||
you will need to add two URLs in `custom_patterns` and add one import line:
|
||||
|
||||
```python
|
||||
from django.conf.urls import url, include
|
||||
from django.urls import path # NEW!
|
||||
|
||||
# default evenni a patterns
|
||||
from evennia.web.urls import urlpatterns
|
||||
|
||||
# eventual custom patterns
|
||||
custom_patterns = [
|
||||
# url(r'/desired/url/', view, name='example'),
|
||||
url('notifications/', include('django_nyt.urls')), # NEW!
|
||||
url('wiki/', include('wiki.urls')), # NEW!
|
||||
]
|
||||
|
||||
# this is required by Django.
|
||||
urlpatterns = custom_patterns + urlpatterns
|
||||
```
|
||||
|
||||
You will probably need to copy line 2, 10, and 11. Be sure to place them correctly, as shown in
|
||||
the example above.
|
||||
|
||||
### Running migrations
|
||||
|
||||
It's time to run the new migrations. The wiki app adds a few tables in our database. We'll need to
|
||||
run:
|
||||
|
||||
evennia migrate
|
||||
|
||||
And that's it, you can start the server. If you go to http://localhost:4001/wiki , you should see
|
||||
the wiki. Use your account's username and password to connect to it. That's how simple it is.
|
||||
|
||||
## Customizing privileges
|
||||
|
||||
A wiki can be a great collaborative tool, but who can see it? Who can modify it? Django-wiki comes
|
||||
with a privilege system centered around four values per wiki page. The owner of an article can
|
||||
always read and write in it (which is somewhat logical). The group of the article defines who can
|
||||
read and who can write, if the user seeing the page belongs to this group. The topic of groups in
|
||||
wiki pages will not be discussed here. A last setting determines which other user (that is, these
|
||||
who aren't in the groups, and aren't the article's owner) can read and write. Each article has
|
||||
these four settings (group read, group write, other read, other write). Depending on your purpose,
|
||||
it might not be a good default choice, particularly if you have to remind every builder to keep the
|
||||
pages private. Fortunately, Django-wiki gives us additional settings to customize who can read, and
|
||||
who can write, a specific article.
|
||||
|
||||
These settings must be placed, as usual, in your `server/conf/settings.py` file. They take a
|
||||
function as argument, said function (or callback) will be called with the article and the user.
|
||||
Remember, a Django user, for us, is an account. So we could check lockstrings on them if needed.
|
||||
Here is a default setting to restrict the wiki: only builders can write in it, but anyone (including
|
||||
non-logged in users) can read it. The superuser has some additional privileges.
|
||||
|
||||
```python
|
||||
# In server/conf/settings.py
|
||||
# ...
|
||||
|
||||
def is_superuser(article, user):
|
||||
"""Return True if user is a superuser, False otherwise."""
|
||||
return not user.is_anonymous() and user.is_superuser
|
||||
|
||||
def is_builder(article, user):
|
||||
"""Return True if user is a builder, False otherwise."""
|
||||
return not user.is_anonymous() and user.locks.check_lockstring(user, "perm(Builders)")
|
||||
|
||||
def is_anyone(article, user):
|
||||
"""Return True even if the user is anonymous."""
|
||||
return True
|
||||
|
||||
# Who can create new groups and users from the wiki?
|
||||
WIKI_CAN_ADMIN = is_superuser
|
||||
# Who can change owner and group membership?
|
||||
WIKI_CAN_ASSIGN = is_superuser
|
||||
# Who can change group membership?
|
||||
WIKI_CAN_ASSIGN_OWNER = is_superuser
|
||||
# Who can change read/write access to groups or others?
|
||||
WIKI_CAN_CHANGE_PERMISSIONS = is_superuser
|
||||
# Who can soft-delete an article?
|
||||
WIKI_CAN_DELETE = is_builder
|
||||
# Who can lock an article and permanently delete it?
|
||||
WIKI_CAN_MODERATE = is_superuser
|
||||
# Who can edit articles?
|
||||
WIKI_CAN_WRITE = is_builder
|
||||
# Who can read articles?
|
||||
WIKI_CAN_READ = is_anyone
|
||||
```
|
||||
|
||||
Here, we have created three functions: one to return `True` if the user is the superuser, one to
|
||||
return `True` if the user is a builder, one to return `True` no matter what (this includes if the
|
||||
user is anonymous, E.G. if it's not logged-in). We then change settings to allow either the
|
||||
superuser or
|
||||
each builder to moderate, read, write, delete, and more. You can, of course, add more functions,
|
||||
adapting them to your need. This is just a demonstration.
|
||||
|
||||
Providing the `WIKI_CAN*...` settings will bypass the original permission system. The superuser
|
||||
could change permissions of an article, but still, only builders would be able to write it. If you
|
||||
need something more custom, you will have to expand on the functions you use.
|
||||
|
||||
### Managing wiki pages from Evennia
|
||||
|
||||
Unfortunately, Django wiki doesn't provide a clear and clean entry point to read and write articles
|
||||
from Evennia and it doesn't seem to be a very high priority. If you really need to keep Django wiki
|
||||
and to create and manage wiki pages from your code, you can do so, but this article won't elaborate,
|
||||
as this is somewhat more technical.
|
||||
|
||||
However, it is a good opportunity to present a small project that has been created more recently:
|
||||
[evennia-wiki](https://github.com/vincent-lg/evennia-wiki) has been created to provide a simple
|
||||
wiki, more tailored to Evennia and easier to connect. It doesn't, as yet, provide as many options
|
||||
as does Django wiki, but it's perfectly usable:
|
||||
|
||||
- Pages have an inherent and much-easier to understand hierarchy based on URLs.
|
||||
- Article permissions are connected to Evennia groups and are much easier to accommodate specific
|
||||
requirements.
|
||||
- Articles can easily be created, read or updated from the Evennia code itself.
|
||||
- Markdown is fully-supported with a default integration to Bootstrap to look good on an Evennia
|
||||
website. Tables and table of contents are supported as well as wiki links.
|
||||
- The process to override wiki templates makes full use of the `template_overrides` directory.
|
||||
|
||||
However evennia-wiki doesn't yet support:
|
||||
|
||||
- Images in markdown and the uploading schema. If images are important to you, please consider
|
||||
contributing to this new project.
|
||||
- Modifying permissions on a per page/setting basis.
|
||||
- Moving pages to new locations.
|
||||
- Viewing page history.
|
||||
|
||||
Considering the list of features in Django wiki, obviously other things could be added to the list.
|
||||
However, these features may be the most important and useful. Additional ones might not be that
|
||||
necessary. If you're interested in supporting this little project, you are more than welcome to
|
||||
[contribute to it](https://github.com/vincent-lg/evennia-wiki). Thanks!
|
||||
236
docs/source/Howto/Building-a-mech-tutorial.md
Normal file
236
docs/source/Howto/Building-a-mech-tutorial.md
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
# Building a mech tutorial
|
||||
|
||||
> This page was adapted from the article "Building a Giant Mech in Evennia" by Griatch, published in
|
||||
Imaginary Realities Volume 6, issue 1, 2014. The original article is no longer available online,
|
||||
this is a version adopted to be compatible with the latest Evennia.
|
||||
|
||||
## Creating the Mech
|
||||
|
||||
Let us create a functioning giant mech using the Python MUD-creation system Evennia. Everyone likes
|
||||
a giant mech, right? Start in-game as a character with build privileges (or the superuser).
|
||||
|
||||
@create/drop Giant Mech ; mech
|
||||
|
||||
Boom. We created a Giant Mech Object and dropped it in the room. We also gave it an alias *mech*.
|
||||
Let’s describe it.
|
||||
|
||||
@desc mech = This is a huge mech. It has missiles and stuff.
|
||||
|
||||
Next we define who can “puppet” the mech object.
|
||||
|
||||
@lock mech = puppet:all()
|
||||
|
||||
This makes it so that everyone can control the mech. More mechs to the people! (Note that whereas
|
||||
Evennia’s default commands may look vaguely MUX-like, you can change the syntax to look like
|
||||
whatever interface style you prefer.)
|
||||
|
||||
Before we continue, let’s make a brief detour. Evennia is very flexible about its objects and even
|
||||
more flexible about using and adding commands to those objects. Here are some ground rules well
|
||||
worth remembering for the remainder of this article:
|
||||
|
||||
- The [Account](Accounts) represents the real person logging in and has no game-world existence.
|
||||
- Any [Object](Objects) can be puppeted by an Account (with proper permissions).
|
||||
- [Characters](Objects#characters), [Rooms](Objects#rooms), and [Exits](Objects#exits) are just
|
||||
children of normal Objects.
|
||||
- Any Object can be inside another (except if it creates a loop).
|
||||
- Any Object can store custom sets of commands on it. Those commands can:
|
||||
- be made available to the puppeteer (Account),
|
||||
- be made available to anyone in the same location as the Object, and
|
||||
- be made available to anyone “inside” the Object
|
||||
- Also Accounts can store commands on themselves. Account commands are always available unless
|
||||
commands on a puppeted Object explicitly override them.
|
||||
|
||||
In Evennia, using the `@ic` command will allow you to puppet a given Object (assuming you have
|
||||
puppet-access to do so). As mentioned above, the bog-standard Character class is in fact like any
|
||||
Object: it is auto-puppeted when logging in and just has a command set on it containing the normal
|
||||
in-game commands, like look, inventory, get and so on.
|
||||
|
||||
@ic mech
|
||||
|
||||
You just jumped out of your Character and *are* now the mech! If people look at you in-game, they
|
||||
will look at a mech. The problem at this point is that the mech Object has no commands of its own.
|
||||
The usual things like look, inventory and get sat on the Character object, remember? So at the
|
||||
moment the mech is not quite as cool as it could be.
|
||||
|
||||
@ic <Your old Character>
|
||||
|
||||
You just jumped back to puppeting your normal, mundane Character again. All is well.
|
||||
|
||||
> (But, you ask, where did that `@ic` command come from, if the mech had no commands on it? The
|
||||
answer is that it came from the Account's command set. This is important. Without the Account being
|
||||
the one with the `@ic` command, we would not have been able to get back out of our mech again.)
|
||||
|
||||
|
||||
### Arming the Mech
|
||||
|
||||
Let us make the mech a little more interesting. In our favorite text editor, we will create some new
|
||||
mech-suitable commands. In Evennia, commands are defined as Python classes.
|
||||
|
||||
```python
|
||||
# in a new file mygame/commands/mechcommands.py
|
||||
|
||||
from evennia import Command
|
||||
|
||||
class CmdShoot(Command):
|
||||
"""
|
||||
Firing the mech’s gun
|
||||
|
||||
Usage:
|
||||
shoot [target]
|
||||
|
||||
This will fire your mech’s main gun. If no
|
||||
target is given, you will shoot in the air.
|
||||
"""
|
||||
key = "shoot"
|
||||
aliases = ["fire", "fire!"]
|
||||
|
||||
def func(self):
|
||||
"This actually does the shooting"
|
||||
|
||||
caller = self.caller
|
||||
location = caller.location
|
||||
|
||||
if not self.args:
|
||||
# no argument given to command - shoot in the air
|
||||
message = "BOOM! The mech fires its gun in the air!"
|
||||
location.msg_contents(message)
|
||||
return
|
||||
|
||||
# we have an argument, search for target
|
||||
target = caller.search(self.args.strip())
|
||||
if target:
|
||||
message = "BOOM! The mech fires its gun at %s" % target.key
|
||||
location.msg_contents(message)
|
||||
|
||||
class CmdLaunch(Command):
|
||||
# make your own 'launch'-command here as an exercise!
|
||||
# (it's very similar to the 'shoot' command above).
|
||||
|
||||
```
|
||||
|
||||
This is saved as a normal Python module (let’s call it `mechcommands.py`), in a place Evennia looks
|
||||
for such modules (`mygame/commands/`). This command will trigger when the player gives the command
|
||||
“shoot”, “fire,” or even “fire!” with an exclamation mark. The mech can shoot in the air or at a
|
||||
target if you give one. In a real game the gun would probably be given a chance to hit and give
|
||||
damage to the target, but this is enough for now.
|
||||
|
||||
We also make a second command for launching missiles (`CmdLaunch`). To save
|
||||
space we won’t describe it here; it looks the same except it returns a text
|
||||
about the missiles being fired and has different `key` and `aliases`. We leave
|
||||
that up to you to create as an exercise. You could have it print "WOOSH! The
|
||||
mech launches missiles against <target>!", for example.
|
||||
|
||||
Now we shove our commands into a command set. A [Command Set](Command-Sets) (CmdSet) is a container
|
||||
holding any number of commands. The command set is what we will store on the mech.
|
||||
|
||||
```python
|
||||
# in the same file mygame/commands/mechcommands.py
|
||||
|
||||
from evennia import CmdSet
|
||||
from evennia import default_cmds
|
||||
|
||||
class MechCmdSet(CmdSet):
|
||||
"""
|
||||
This allows mechs to do do mech stuff.
|
||||
"""
|
||||
key = "mechcmdset"
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
"Called once, when cmdset is first created"
|
||||
self.add(CmdShoot())
|
||||
self.add(CmdLaunch())
|
||||
```
|
||||
|
||||
This simply groups all the commands we want. We add our new shoot/launch commands. Let’s head back
|
||||
into the game. For testing we will manually attach our new CmdSet to the mech.
|
||||
|
||||
@py self.search("mech").cmdset.add("commands.mechcommands.MechCmdSet")
|
||||
|
||||
This is a little Python snippet (run from the command line as an admin) that searches for the mech
|
||||
in our current location and attaches our new MechCmdSet to it. What we add is actually the Python
|
||||
path to our cmdset class. Evennia will import and initialize it behind the scenes.
|
||||
|
||||
@ic mech
|
||||
|
||||
We are back as the mech! Let’s do some shooting!
|
||||
|
||||
fire!
|
||||
BOOM! The mech fires its gun in the air!
|
||||
|
||||
There we go, one functioning mech. Try your own `launch` command and see that it works too. We can
|
||||
not only walk around as the mech — since the CharacterCmdSet is included in our MechCmdSet, the mech
|
||||
can also do everything a Character could do, like look around, pick up stuff, and have an inventory.
|
||||
We could now shoot the gun at a target or try the missile launch command. Once you have your own
|
||||
mech, what else do you need?
|
||||
|
||||
> Note: You'll find that the mech's commands are available to you by just standing in the same
|
||||
location (not just by puppeting it). We'll solve this with a *lock* in the next section.
|
||||
|
||||
## Making a Mech production line
|
||||
|
||||
What we’ve done so far is just to make a normal Object, describe it and put some commands on it.
|
||||
This is great for testing. The way we added it, the MechCmdSet will even go away if we reload the
|
||||
server. Now we want to make the mech an actual object “type” so we can create mechs without those
|
||||
extra steps. For this we need to create a new Typeclass.
|
||||
|
||||
A [Typeclass](Typeclasses) is a near-normal Python class that stores its existence to the database
|
||||
behind the scenes. A Typeclass is created in a normal Python source file:
|
||||
|
||||
```python
|
||||
# in the new file mygame/typeclasses/mech.py
|
||||
|
||||
from typeclasses.objects import Object
|
||||
from commands.mechcommands import MechCmdSet
|
||||
from evennia import default_cmds
|
||||
|
||||
class Mech(Object):
|
||||
"""
|
||||
This typeclass describes an armed Mech.
|
||||
"""
|
||||
def at_object_creation(self):
|
||||
"This is called only when object is first created"
|
||||
self.cmdset.add_default(default_cmds.CharacterCmdSet)
|
||||
self.cmdset.add(MechCmdSet, permanent=True)
|
||||
self.locks.add("puppet:all();call:false()")
|
||||
self.db.desc = "This is a huge mech. It has missiles and stuff."
|
||||
```
|
||||
|
||||
For convenience we include the full contents of the default `CharacterCmdSet` in there. This will
|
||||
make a Character’s normal commands available to the mech. We also add the mech-commands from before,
|
||||
making sure they are stored persistently in the database. The locks specify that anyone can puppet
|
||||
the meck and no-one can "call" the mech's Commands from 'outside' it - you have to puppet it to be
|
||||
able to shoot.
|
||||
|
||||
That’s it. When Objects of this type are created, they will always start out with the mech’s command
|
||||
set and the correct lock. We set a default description, but you would probably change this with
|
||||
`@desc` to individualize your mechs as you build them.
|
||||
|
||||
Back in the game, just exit the old mech (`@ic` back to your old character) then do
|
||||
|
||||
@create/drop The Bigger Mech ; bigmech : mech.Mech
|
||||
|
||||
We create a new, bigger mech with an alias bigmech. Note how we give the python-path to our
|
||||
Typeclass at the end — this tells Evennia to create the new object based on that class (we don't
|
||||
have to give the full path in our game dir `typeclasses.mech.Mech` because Evennia knows to look in
|
||||
the `typeclasses` folder already). A shining new mech will appear in the room! Just use
|
||||
|
||||
@ic bigmech
|
||||
|
||||
to take it on a test drive.
|
||||
|
||||
## Future Mechs
|
||||
|
||||
To expand on this you could add more commands to the mech and remove others. Maybe the mech
|
||||
shouldn’t work just like a Character after all. Maybe it makes loud noises every time it passes from
|
||||
room to room. Maybe it cannot pick up things without crushing them. Maybe it needs fuel, ammo and
|
||||
repairs. Maybe you’ll lock it down so it can only be puppeted by emo teenagers.
|
||||
|
||||
Having you puppet the mech-object directly is also just one way to implement a giant mech in
|
||||
Evennia.
|
||||
|
||||
For example, you could instead picture a mech as a “vehicle” that you “enter” as your normal
|
||||
Character (since any Object can move inside another). In that case the “insides” of the mech Object
|
||||
could be the “cockpit”. The cockpit would have the `MechCommandSet` stored on itself and all the
|
||||
shooting goodness would be made available to you only when you enter it.
|
||||
|
||||
And of course you could put more guns on it. And make it fly.
|
||||
378
docs/source/Howto/Coding-FAQ.md
Normal file
378
docs/source/Howto/Coding-FAQ.md
Normal file
|
|
@ -0,0 +1,378 @@
|
|||
# Coding FAQ
|
||||
|
||||
*This FAQ page is for users to share their solutions to coding problems. Keep it brief and link to
|
||||
the docs if you can rather than too lengthy explanations. Don't forget to check if an answer already
|
||||
exists before answering - maybe you can clarify that answer rather than to make a new Q&A section.*
|
||||
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Removing default commands](Coding-FAQ#removing-default-commands)
|
||||
- [Preventing character from moving based on a condition](Coding-FAQ#preventing-character-from-
|
||||
moving-based-on-a-condition)
|
||||
- [Reference initiating object in an EvMenu command](Coding-FAQ#reference-initiating-object-in-an-
|
||||
evmenu-command)
|
||||
- [Adding color to default Evennia Channels](Coding-FAQ#adding-color-to-default-evennia-channels)
|
||||
- [Selectively turn off commands in a room](Coding-FAQ#selectively-turn-off-commands-in-a-room)
|
||||
- [Select Command based on a condition](Coding-FAQ#select-command-based-on-a-condition)
|
||||
- [Automatically updating code when reloading](Coding-FAQ#automatically-updating-code-when-
|
||||
reloading)
|
||||
- [Changing all exit messages](Coding-FAQ#changing-all-exit-messages)
|
||||
- [Add parsing with the "to" delimiter](Coding-FAQ#add-parsing-with-the-to-delimiter)
|
||||
- [Store last used session IP address](Coding-FAQ#store-last-used-session-ip-address)
|
||||
- [Use wide characters with EvTable](Coding-FAQ#non-latin-characters-in-evtable)
|
||||
|
||||
## Removing default commands
|
||||
**Q:** How does one *remove* (not replace) e.g. the default `get` [Command](Commands) from the
|
||||
Character [Command Set](Command-Sets)?
|
||||
|
||||
**A:** Go to `mygame/commands/default_cmdsets.py`. Find the `CharacterCmdSet` class. It has one
|
||||
method named `at_cmdset_creation`. At the end of that method, add the following line:
|
||||
`self.remove(default_cmds.CmdGet())`. See the [Adding Commands Tutorial](Adding-Command-Tutorial)
|
||||
for more info.
|
||||
|
||||
## Preventing character from moving based on a condition
|
||||
**Q:** How does one keep a character from using any exit, if they meet a certain condition? (I.E. in
|
||||
combat, immobilized, etc.)
|
||||
|
||||
**A:** The `at_before_move` hook is called by Evennia just before performing any move. If it returns
|
||||
`False`, the move is aborted. Let's say we want to check for an [Attribute](Attributes) `cantmove`.
|
||||
Add the following code to the `Character` class:
|
||||
|
||||
```python
|
||||
def at_before_move(self, destination):
|
||||
"Called just before trying to move"
|
||||
if self.db.cantmove: # replace with condition you want to test
|
||||
self.msg("Something is preventing you from moving!")
|
||||
return False
|
||||
return True
|
||||
```
|
||||
|
||||
## Reference initiating object in an EvMenu command.
|
||||
**Q:** An object has a Command on it starts up an EvMenu instance. How do I capture a reference to
|
||||
that object for use in the menu?
|
||||
|
||||
**A:** When an [EvMenu](EvMenu) is started, the menu object is stored as `caller.ndb._menutree`.
|
||||
This is a good place to store menu-specific things since it will clean itself up when the menu
|
||||
closes. When initiating the menu, any additional keywords you give will be available for you as
|
||||
properties on this menu object:
|
||||
|
||||
```python
|
||||
class MyObjectCommand(Command):
|
||||
# A Command stored on an object (the object is always accessible from
|
||||
# the Command as self.obj)
|
||||
def func(self):
|
||||
# add the object as the stored_obj menu property
|
||||
EvMenu(caller, ..., stored_obj=self.obj)
|
||||
|
||||
```
|
||||
|
||||
Inside the menu you can now access the object through `caller.ndb._menutree.stored_obj`.
|
||||
|
||||
|
||||
## Adding color to default Evennia Channels
|
||||
**Q:** How do I add colors to the names of Evennia channels?
|
||||
|
||||
**A:** The Channel typeclass' `channel_prefix` method decides what is shown at the beginning of a
|
||||
channel send. Edit `mygame/typeclasses/channels.py` (and then `@reload`):
|
||||
|
||||
```python
|
||||
# define our custom color names
|
||||
CHANNEL_COLORS = {'public': '|015Public|n',
|
||||
'newbie': '|550N|n|551e|n|552w|n|553b|n|554i|n|555e|n',
|
||||
'staff': '|010S|n|020t|n|030a|n|040f|n|050f|n'}
|
||||
|
||||
# Add to the Channel class
|
||||
# ...
|
||||
def channel_prefix(self, msg, emit=False):
|
||||
prefix_string = ""
|
||||
if self.key in COLORS:
|
||||
prefix_string = "[%s] " % CHANNEL_COLORS.get(self.key.lower())
|
||||
else:
|
||||
prefix_string = "[%s] " % self.key.capitalize()
|
||||
return prefix_string
|
||||
```
|
||||
Additional hint: To make colors easier to change from one place you could instead put the
|
||||
`CHANNEL_COLORS` dict in your settings file and import it as `from django.conf.settings import
|
||||
CHANNEL_COLORS`.
|
||||
|
||||
|
||||
## Selectively turn off commands in a room
|
||||
**Q:** I want certain commands to turn off in a given room. They should still work normally for
|
||||
staff.
|
||||
|
||||
**A:** This is done using a custom cmdset on a room [locked with the 'call' lock type](Locks). Only
|
||||
if this lock is passed will the commands on the room be made available to an object inside it. Here
|
||||
is an example of a room where certain commands are disabled for non-staff:
|
||||
|
||||
```python
|
||||
# in mygame/typeclasses/rooms.py
|
||||
|
||||
from evennia import default_commands, CmdSet
|
||||
|
||||
class CmdBlocking(default_commands.MuxCommand):
|
||||
# block commands give, get, inventory and drop
|
||||
key = "give"
|
||||
aliases = ["get", "inventory", "drop"]
|
||||
def func(self):
|
||||
self.caller.msg("You cannot do that in this room.")
|
||||
|
||||
class BlockingCmdSet(CmdSet):
|
||||
key = "blocking_cmdset"
|
||||
# default commands have prio 0
|
||||
priority = 1
|
||||
def at_cmdset_creation(self):
|
||||
self.add(CmdBlocking())
|
||||
|
||||
class BlockingRoom(Room):
|
||||
def at_object_creation(self):
|
||||
self.cmdset.add(BlockingCmdSet, permanent=True)
|
||||
# only share commands with players in the room that
|
||||
# are NOT Builders or higher
|
||||
self.locks.add("call:not perm(Builders)")
|
||||
```
|
||||
After `@reload`, make some `BlockingRooms` (or switch a room to it with `@typeclass`). Entering one
|
||||
will now replace the given commands for anyone that does not have the `Builders` or higher
|
||||
permission. Note that the 'call' lock is special in that even the superuser will be affected by it
|
||||
(otherwise superusers would always see other player's cmdsets and a game would be unplayable for
|
||||
superusers).
|
||||
|
||||
## Select Command based on a condition
|
||||
**Q:** I want a command to be available only based on a condition. For example I want the "werewolf"
|
||||
command to only be available on a full moon, from midnight to three in-game time.
|
||||
|
||||
**A:** This is easiest accomplished by putting the "werewolf" command on the Character as normal,
|
||||
but to [lock](Locks) it with the "cmd" type lock. Only if the "cmd" lock type is passed will the
|
||||
command be available.
|
||||
|
||||
```python
|
||||
# in mygame/commands/command.py
|
||||
|
||||
from evennia import Command
|
||||
|
||||
class CmdWerewolf(Command):
|
||||
key = "werewolf"
|
||||
# lock full moon, between 00:00 (midnight) and 03:00.
|
||||
locks = "cmd:is_full_moon(0, 3)"
|
||||
def func(self):
|
||||
# ...
|
||||
```
|
||||
Add this to the [default cmdset as usual](Adding-Command-Tutorial). The `is_full_moon` [lock
|
||||
function](Locks#lock-functions) does not yet exist. We must create that:
|
||||
|
||||
```python
|
||||
# in mygame/server/conf/lockfuncs.py
|
||||
|
||||
def is_full_moon(accessing_obj, accessed_obj,
|
||||
starthour, endhour, *args, **kwargs):
|
||||
# calculate if the moon is full here and
|
||||
# if current game time is between starthour and endhour
|
||||
# return True or False
|
||||
|
||||
```
|
||||
After a `@reload`, the `werewolf` command will be available only at the right time, that is when the
|
||||
`is_full_moon` lock function returns True.
|
||||
|
||||
## Automatically updating code when reloading
|
||||
**Q:** I have a development server running Evennia. Can I have the server update its code-base when
|
||||
I reload?
|
||||
|
||||
**A:** Having a development server that pulls updated code whenever you reload it can be really
|
||||
useful if you have limited shell access to your server, or want to have it done automatically. If
|
||||
you have your project in a configured Git environment, it's a matter of automatically calling `git
|
||||
pull` when you reload. And that's pretty straightforward:
|
||||
|
||||
In `/server/conf/at_server_startstop.py`:
|
||||
|
||||
```python
|
||||
import subprocess
|
||||
|
||||
# ... other hooks ...
|
||||
|
||||
def at_server_reload_stop():
|
||||
"""
|
||||
This is called only time the server stops before a reload.
|
||||
"""
|
||||
print("Pulling from the game repository...")
|
||||
process = subprocess.call(["git", "pull"], shell=False)
|
||||
```
|
||||
|
||||
That's all. We call `subprocess` to execute a shell command (that code works on Windows and Linux,
|
||||
assuming the current directory is your game directory, which is probably the case when you run
|
||||
Evennia). `call` waits for the process to complete, because otherwise, Evennia would reload on
|
||||
partially-modified code, which would be problematic.
|
||||
|
||||
Now, when you enter `@reload` on your development server, the game repository is updated from the
|
||||
configured remote repository (Github, for instance). Your development cycle could resemble
|
||||
something like:
|
||||
|
||||
1. Coding on the local machine.
|
||||
2. Testing modifications.
|
||||
3. Committing once, twice or more (being sure the code is still working, unittests are pretty useful
|
||||
here).
|
||||
4. When the time comes, login to the development server and run `@reload`.
|
||||
|
||||
The reloading might take one or two additional seconds, since Evennia will pull from your remote Git
|
||||
repository. But it will reload on it and you will have your modifications ready, without needing
|
||||
connecting to your server using SSH or something similar.
|
||||
|
||||
## Changing all exit messages
|
||||
**Q:** How can I change the default exit messages to something like "XXX leaves east" or "XXX
|
||||
arrives from the west"?
|
||||
|
||||
**A:** the default exit messages are stored in two hooks, namely `announce_move_from` and
|
||||
`announce_move_to`, on the `Character` typeclass (if what you want to change is the message other
|
||||
characters will see when a character exits).
|
||||
|
||||
These two hooks provide some useful features to easily update the message to be displayed. They
|
||||
take both the default message and mapping as argument. You can easily call the parent hook with
|
||||
these information:
|
||||
|
||||
* The message represents the string of characters sent to characters in the room when a character
|
||||
leaves.
|
||||
* The mapping is a dictionary containing additional mappings (you will probably not need it for
|
||||
simple customization).
|
||||
|
||||
It is advisable to look in the [code of both
|
||||
hooks](https://github.com/evennia/evennia/tree/master/evennia/objects/objects.py), and read the
|
||||
hooks' documentation. The explanations on how to quickly update the message are shown below:
|
||||
|
||||
```python
|
||||
# In typeclasses/characters.py
|
||||
"""
|
||||
Characters
|
||||
|
||||
"""
|
||||
from evennia import DefaultCharacter
|
||||
|
||||
class Character(DefaultCharacter):
|
||||
"""
|
||||
The default character class.
|
||||
|
||||
...
|
||||
"""
|
||||
|
||||
def announce_move_from(self, destination, msg=None, mapping=None):
|
||||
"""
|
||||
Called if the move is to be announced. This is
|
||||
called while we are still standing in the old
|
||||
location.
|
||||
|
||||
Args:
|
||||
destination (Object): The place we are going to.
|
||||
msg (str, optional): a replacement message.
|
||||
mapping (dict, optional): additional mapping objects.
|
||||
|
||||
You can override this method and call its parent with a
|
||||
message to simply change the default message. In the string,
|
||||
you can use the following as mappings (between braces):
|
||||
object: the object which is moving.
|
||||
exit: the exit from which the object is moving (if found).
|
||||
origin: the location of the object before the move.
|
||||
destination: the location of the object after moving.
|
||||
|
||||
"""
|
||||
super().announce_move_from(destination, msg="{object} leaves {exit}.")
|
||||
|
||||
def announce_move_to(self, source_location, msg=None, mapping=None):
|
||||
"""
|
||||
Called after the move if the move was not quiet. At this point
|
||||
we are standing in the new location.
|
||||
|
||||
Args:
|
||||
source_location (Object): The place we came from
|
||||
msg (str, optional): the replacement message if location.
|
||||
mapping (dict, optional): additional mapping objects.
|
||||
|
||||
You can override this method and call its parent with a
|
||||
message to simply change the default message. In the string,
|
||||
you can use the following as mappings (between braces):
|
||||
object: the object which is moving.
|
||||
exit: the exit from which the object is moving (if found).
|
||||
origin: the location of the object before the move.
|
||||
destination: the location of the object after moving.
|
||||
|
||||
"""
|
||||
super().announce_move_to(source_location, msg="{object} arrives from the {exit}.")
|
||||
```
|
||||
|
||||
We override both hooks, but call the parent hook to display a different message. If you read the
|
||||
provided docstrings, you will better understand why and how we use mappings (information between
|
||||
braces). You can provide additional mappings as well, if you want to set a verb to move, for
|
||||
instance, or other, extra information.
|
||||
|
||||
## Add parsing with the "to" delimiter
|
||||
|
||||
**Q:** How do I change commands to undestand say `give obj to target` as well as the default `give
|
||||
obj = target`?
|
||||
|
||||
**A:** You can make change the default `MuxCommand` parent with your own class making a small change
|
||||
in its `parse` method:
|
||||
|
||||
```python
|
||||
# in mygame/commands/command.py
|
||||
from evennia import default_cmds
|
||||
class MuxCommand(default_cmds.MuxCommand):
|
||||
def parse(self):
|
||||
"""Implement an additional parsing of 'to'"""
|
||||
super().parse()
|
||||
if " to " in self.args:
|
||||
self.lhs, self.rhs = self.args.split(" to ", 1)
|
||||
```
|
||||
Next you change the parent of the default commands in settings:
|
||||
|
||||
```python
|
||||
COMMAND_DEFAULT_CLASS = "commands.command.MuxCommand"
|
||||
```
|
||||
|
||||
Do a `@reload` and all default commands will now use your new tweaked parent class. A copy of the
|
||||
MuxCommand class is also found commented-out in the `mygame/commands/command.py` file.
|
||||
|
||||
## Store last used session IP address
|
||||
|
||||
**Q:** If a user has already logged out of an Evennia account, their IP is no longer visible to
|
||||
staff that wants to ban-by-ip (instead of the user) with `@ban/ip`?
|
||||
|
||||
**A:** One approach is to write the IP from the last session onto the "account" account object.
|
||||
|
||||
`typeclasses/accounts.py`
|
||||
```python
|
||||
def at_post_login(self, session=None, **kwargs):
|
||||
super().at_post_login(session=session, **kwargs)
|
||||
self.db.lastsite = self.sessions.all()[-1].address
|
||||
```
|
||||
Adding timestamp for login time and appending to a list to keep the last N login IP addresses and
|
||||
timestamps is possible, also. Additionally, if you don't want the list to grow beyond a
|
||||
`do_not_exceed` length, conditionally pop a value after you've added it, if the length has grown too
|
||||
long.
|
||||
|
||||
**NOTE:** You'll need to add `import time` to generate the login timestamp.
|
||||
```python
|
||||
def at_post_login(self, session=None, **kwargs):
|
||||
super().at_post_login(session=session, **kwargs)
|
||||
do_not_exceed = 24 # Keep the last two dozen entries
|
||||
session = self.sessions.all()[-1] # Most recent session
|
||||
if not self.db.lastsite:
|
||||
self.db.lastsite = []
|
||||
self.db.lastsite.insert(0, (session.address, int(time.time())))
|
||||
if len(self.db.lastsite) > do_not_exceed:
|
||||
self.db.lastsite.pop()
|
||||
```
|
||||
This only stores the data. You may want to interface the `@ban` command or make a menu-driven viewer
|
||||
for staff to browse the list and display how long ago the login occurred.
|
||||
|
||||
## Non-latin characters in EvTable
|
||||
|
||||
**Q:** When using e.g. Chinese characters in EvTable, some lines appear to be too wide, for example
|
||||
```
|
||||
+------+------+
|
||||
| | |
|
||||
| 测试 | 测试 |
|
||||
| | |
|
||||
+~~~~~~+~~~~~~+
|
||||
```
|
||||
**A:** The reason for this is because certain non-latin characters are *visually* much wider than
|
||||
their len() suggests. There is little Evennia can (reliably) do about this. If you are using such
|
||||
characters, you need to make sure to use a suitable mono-spaced font where are width are equal. You
|
||||
can set this in your web client and need to recommend it for telnet-client users. See [this
|
||||
discussion](https://github.com/evennia/evennia/issues/1522) where some suitable fonts are suggested.
|
||||
98
docs/source/Howto/Command-Cooldown.md
Normal file
98
docs/source/Howto/Command-Cooldown.md
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
# Command Cooldown
|
||||
|
||||
|
||||
Some types of games want to limit how often a command can be run. If a
|
||||
character casts the spell *Firestorm*, you might not want them to spam that
|
||||
command over and over. Or in an advanced combat system, a massive swing may
|
||||
offer a chance of lots of damage at the cost of not being able to re-do it for
|
||||
a while. Such effects are called *cooldowns*.
|
||||
|
||||
This page exemplifies a very resource-efficient way to do cooldowns. A more
|
||||
'active' way is to use asynchronous delays as in the [command duration
|
||||
tutorial](Command-Duration#Blocking-Commands), the two might be useful to
|
||||
combine if you want to echo some message to the user after the cooldown ends.
|
||||
|
||||
## Non-persistent cooldown
|
||||
|
||||
This little recipe will limit how often a particular command can be run. Since
|
||||
Commands are class instances, and those are cached in memory, a command
|
||||
instance will remember things you store on it. So just store the current time
|
||||
of execution! Next time the command is run, it just needs to check if it has
|
||||
that time stored, and compare it with the current time to see if a desired
|
||||
delay has passed.
|
||||
|
||||
```python
|
||||
import time
|
||||
from evennia import default_cmds
|
||||
|
||||
class CmdSpellFirestorm(default_cmds.MuxCommand):
|
||||
"""
|
||||
Spell - Firestorm
|
||||
|
||||
Usage:
|
||||
cast firestorm <target>
|
||||
|
||||
This will unleash a storm of flame. You can only release one
|
||||
firestorm every five minutes (assuming you have the mana).
|
||||
"""
|
||||
key = "cast firestorm"
|
||||
locks = "cmd:isFireMage()"
|
||||
|
||||
def func(self):
|
||||
"Implement the spell"
|
||||
|
||||
# check cooldown (5 minute cooldown)
|
||||
now = time.time()
|
||||
if hasattr(self, "lastcast") and \
|
||||
now - self.lastcast < 5 * 60:
|
||||
message = "You cannot cast this spell again yet."
|
||||
self.caller.msg(message)
|
||||
return
|
||||
|
||||
#[the spell effect is implemented]
|
||||
|
||||
# if the spell was successfully cast, store the casting time
|
||||
self.lastcast = now
|
||||
```
|
||||
|
||||
We just check the `lastcast` flag, and update it if everything works out.
|
||||
Simple and very effective since everything is just stored in memory. The
|
||||
drawback of this simple scheme is that it's non-persistent. If you do
|
||||
`@reload`, the cache is cleaned and all such ongoing cooldowns will be
|
||||
forgotten. It is also limited only to this one command, other commands cannot
|
||||
(easily) check for this value.
|
||||
|
||||
## Persistent cooldown
|
||||
|
||||
This is essentially the same mechanism as the simple one above, except we use
|
||||
the database to store the information which means the cooldown will survive a
|
||||
server reload/reboot. Since commands themselves have no representation in the
|
||||
database, you need to use the caster for the storage.
|
||||
|
||||
```python
|
||||
# inside the func() of CmdSpellFirestorm as above
|
||||
|
||||
# check cooldown (5 minute cooldown)
|
||||
|
||||
now = time.time()
|
||||
lastcast = self.caller.db.firestorm_lastcast
|
||||
|
||||
if lastcast and now - lastcast < 5 * 60:
|
||||
message = "You need to wait before casting this spell again."
|
||||
self.caller.msg(message)
|
||||
return
|
||||
|
||||
#[the spell effect is implemented]
|
||||
|
||||
# if the spell was successfully cast, store the casting time
|
||||
self.caller.db.firestorm_lastcast = now
|
||||
```
|
||||
|
||||
Since we are storing as an [Attribute](Attributes), we need to identify the
|
||||
variable as `firestorm_lastcast` so we are sure we get the right one (we'll
|
||||
likely have other skills with cooldowns after all). But this method of
|
||||
using cooldowns also has the advantage of working *between* commands - you can
|
||||
for example let all fire-related spells check the same cooldown to make sure
|
||||
the casting of *Firestorm* blocks all fire-related spells for a while. Or, in
|
||||
the case of taking that big swing with the sword, this could now block all
|
||||
other types of attacks for a while before the warrior can recover.
|
||||
403
docs/source/Howto/Command-Duration.md
Normal file
403
docs/source/Howto/Command-Duration.md
Normal file
|
|
@ -0,0 +1,403 @@
|
|||
# Command Duration
|
||||
|
||||
|
||||
Before reading this tutorial, if you haven't done so already, you might want to
|
||||
read [the documentation on commands](Commands) to get a basic understanding of
|
||||
how commands work in Evennia.
|
||||
|
||||
In some types of games a command should not start and finish immediately.
|
||||
Loading a crossbow might take a bit of time to do - time you don't have when
|
||||
the enemy comes rushing at you. Crafting that armour will not be immediate
|
||||
either. For some types of games the very act of moving or changing pose all
|
||||
comes with a certain time associated with it.
|
||||
|
||||
## The simple way to pause commands with yield
|
||||
|
||||
Evennia allows a shortcut in syntax to create simple pauses in commands. This
|
||||
syntax uses the `yield` keyword. The `yield` keyword is used in Python to
|
||||
create generators, although you don't need to know what generators are to use
|
||||
this syntax. A short example will probably make it clear:
|
||||
|
||||
```python
|
||||
class CmdTest(Command):
|
||||
|
||||
"""
|
||||
A test command just to test waiting.
|
||||
|
||||
Usage:
|
||||
test
|
||||
|
||||
"""
|
||||
|
||||
key = "test"
|
||||
locks = "cmd:all()"
|
||||
|
||||
def func(self):
|
||||
self.msg("Before ten seconds...")
|
||||
yield 10
|
||||
self.msg("Afterwards.")
|
||||
```
|
||||
> Important: The `yield` functionality will *only* work in the `func` method of
|
||||
> Commands. It only works because Evennia has especially
|
||||
> catered for it in Commands. If you want the same functionality elsewhere you
|
||||
> must use the [interactive decorator](Async-Process#The-@interactive-decorator).
|
||||
|
||||
The important line is the `yield 10`. It tells Evennia to "pause" the command
|
||||
and to wait for 10 seconds to execute the rest. If you add this command and
|
||||
run it, you'll see the first message, then, after a pause of ten seconds, the
|
||||
next message. You can use `yield` several times in your command.
|
||||
|
||||
This syntax will not "freeze" all commands. While the command is "pausing",
|
||||
you can execute other commands (or even call the same command again). And
|
||||
other players aren't frozen either.
|
||||
|
||||
> Note: this will not save anything in the database. If you reload the game
|
||||
> while a command is "paused", it will not resume after the server has
|
||||
> reloaded.
|
||||
|
||||
|
||||
## The more advanced way with utils.delay
|
||||
|
||||
The `yield` syntax is easy to read, easy to understand, easy to use. But it's not that flexible if
|
||||
you want more advanced options. Learning to use alternatives might be much worth it in the end.
|
||||
|
||||
Below is a simple command example for adding a duration for a command to finish.
|
||||
|
||||
```python
|
||||
from evennia import default_cmds, utils
|
||||
|
||||
class CmdEcho(default_cmds.MuxCommand):
|
||||
"""
|
||||
wait for an echo
|
||||
|
||||
Usage:
|
||||
echo <string>
|
||||
|
||||
Calls and waits for an echo
|
||||
"""
|
||||
key = "echo"
|
||||
locks = "cmd:all()"
|
||||
|
||||
def func(self):
|
||||
"""
|
||||
This is called at the initial shout.
|
||||
"""
|
||||
self.caller.msg("You shout '%s' and wait for an echo ..." % self.args)
|
||||
# this waits non-blocking for 10 seconds, then calls self.echo
|
||||
utils.delay(10, self.echo) # call echo after 10 seconds
|
||||
|
||||
def echo(self):
|
||||
"Called after 10 seconds."
|
||||
shout = self.args
|
||||
string = "You hear an echo: %s ... %s ... %s"
|
||||
string = string % (shout.upper(), shout.capitalize(), shout.lower())
|
||||
self.caller.msg(string)
|
||||
```
|
||||
|
||||
Import this new echo command into the default command set and reload the server. You will find that
|
||||
it will take 10 seconds before you see your shout coming back. You will also find that this is a
|
||||
*non-blocking* effect; you can issue other commands in the interim and the game will go on as usual.
|
||||
The echo will come back to you in its own time.
|
||||
|
||||
### About utils.delay()
|
||||
|
||||
`utils.delay(timedelay, callback, persistent=False, *args, **kwargs)` is a useful function. It will
|
||||
wait `timedelay` seconds, then call the `callback` function, optionally passing to it the arguments
|
||||
provided to utils.delay by way of *args and/or **kwargs`.
|
||||
|
||||
> Note: The callback argument should be provided with a python path to the desired function, for
|
||||
instance `my_object.my_function` instead of `my_object.my_function()`. Otherwise my_function would
|
||||
get called and run immediately upon attempting to pass it to the delay function.
|
||||
If you want to provide arguments for utils.delay to use, when calling your callback function, you
|
||||
have to do it separatly, for instance using the utils.delay *args and/or **kwargs, as mentioned
|
||||
above.
|
||||
|
||||
> If you are not familiar with the syntax `*args` and `**kwargs`, [see the Python documentation
|
||||
here](https://docs.python.org/2/tutorial/controlflow.html#arbitrary-argument-lists).
|
||||
|
||||
Looking at it you might think that `utils.delay(10, callback)` in the code above is just an
|
||||
alternative to some more familiar thing like `time.sleep(10)`. This is *not* the case. If you do
|
||||
`time.sleep(10)` you will in fact freeze the *entire server* for ten seconds! The `utils.delay()`is
|
||||
a thin wrapper around a Twisted
|
||||
[Deferred](http://twistedmatrix.com/documents/11.0.0/core/howto/defer.html) that will delay
|
||||
execution until 10 seconds have passed, but will do so asynchronously, without bothering anyone else
|
||||
(not even you - you can continue to do stuff normally while it waits to continue).
|
||||
|
||||
The point to remember here is that the `delay()` call will not "pause" at that point when it is
|
||||
called (the way `yield` does in the previous section). The lines after the `delay()` call will
|
||||
actually execute *right away*. What you must do is to tell it which function to call *after the time
|
||||
has passed* (its "callback"). This may sound strange at first, but it is normal practice in
|
||||
asynchronous systems. You can also link such calls together as seen below:
|
||||
|
||||
```python
|
||||
from evennia import default_cmds, utils
|
||||
|
||||
class CmdEcho(default_cmds.MuxCommand):
|
||||
"""
|
||||
waits for an echo
|
||||
|
||||
Usage:
|
||||
echo <string>
|
||||
|
||||
Calls and waits for an echo
|
||||
"""
|
||||
key = "echo"
|
||||
locks = "cmd:all()"
|
||||
|
||||
def func(self):
|
||||
"This sets off a chain of delayed calls"
|
||||
self.caller.msg("You shout '%s', waiting for an echo ..." % self.args)
|
||||
|
||||
# wait 2 seconds before calling self.echo1
|
||||
utils.delay(2, self.echo1)
|
||||
|
||||
# callback chain, started above
|
||||
def echo1(self):
|
||||
"First echo"
|
||||
self.caller.msg("... %s" % self.args.upper())
|
||||
# wait 2 seconds for the next one
|
||||
utils.delay(2, self.echo2)
|
||||
|
||||
def echo2(self):
|
||||
"Second echo"
|
||||
self.caller.msg("... %s" % self.args.capitalize())
|
||||
# wait another 2 seconds
|
||||
utils.delay(2, callback=self.echo3)
|
||||
|
||||
def echo3(self):
|
||||
"Last echo"
|
||||
self.caller.msg("... %s ..." % self.args.lower())
|
||||
```
|
||||
|
||||
The above version will have the echoes arrive one after another, each separated by a two second
|
||||
delay.
|
||||
|
||||
> echo Hello!
|
||||
... HELLO!
|
||||
... Hello!
|
||||
... hello! ...
|
||||
|
||||
## Blocking commands
|
||||
|
||||
As mentioned, a great thing about the delay introduced by `yield` or `utils.delay()` is that it does
|
||||
not block. It just goes on in the background and you are free to play normally in the interim. In
|
||||
some cases this is not what you want however. Some commands should simply "block" other commands
|
||||
while they are running. If you are in the process of crafting a helmet you shouldn't be able to also
|
||||
start crafting a shield at the same time, or if you just did a huge power-swing with your weapon you
|
||||
should not be able to do it again immediately.
|
||||
|
||||
The simplest way of implementing blocking is to use the technique covered in the [Command
|
||||
Cooldown](Command-Cooldown) tutorial. In that tutorial we implemented cooldowns by having the
|
||||
Command store the current time. Next time the Command was called, we compared the current time to
|
||||
the stored time to determine if enough time had passed for a renewed use. This is a *very*
|
||||
efficient, reliable and passive solution. The drawback is that there is nothing to tell the Player
|
||||
when enough time has passed unless they keep trying.
|
||||
|
||||
Here is an example where we will use `utils.delay` to tell the player when the cooldown has passed:
|
||||
|
||||
```python
|
||||
from evennia import utils, default_cmds
|
||||
|
||||
class CmdBigSwing(default_cmds.MuxCommand):
|
||||
"""
|
||||
swing your weapon in a big way
|
||||
|
||||
Usage:
|
||||
swing <target>
|
||||
|
||||
Makes a mighty swing. Doing so will make you vulnerable
|
||||
to counter-attacks before you can recover.
|
||||
"""
|
||||
key = "bigswing"
|
||||
locks = "cmd:all()"
|
||||
|
||||
def func(self):
|
||||
"Makes the swing"
|
||||
|
||||
if self.caller.ndb.off_balance:
|
||||
# we are still off-balance.
|
||||
self.caller.msg("You are off balance and need time to recover!")
|
||||
return
|
||||
|
||||
# [attack/hit code goes here ...]
|
||||
self.caller.msg("You swing big! You are off balance now.")
|
||||
|
||||
# set the off-balance flag
|
||||
self.caller.ndb.off_balance = True
|
||||
|
||||
# wait 8 seconds before we can recover. During this time
|
||||
# we won't be able to swing again due to the check at the top.
|
||||
utils.delay(8, self.recover)
|
||||
|
||||
def recover(self):
|
||||
"This will be called after 8 secs"
|
||||
del self.caller.ndb.off_balance
|
||||
self.caller.msg("You regain your balance.")
|
||||
```
|
||||
|
||||
Note how, after the cooldown, the user will get a message telling them they are now ready for
|
||||
another swing.
|
||||
|
||||
By storing the `off_balance` flag on the character (rather than on, say, the Command instance
|
||||
itself) it can be accessed by other Commands too. Other attacks may also not work when you are off
|
||||
balance. You could also have an enemy Command check your `off_balance` status to gain bonuses, to
|
||||
take another example.
|
||||
|
||||
## Abortable commands
|
||||
|
||||
One can imagine that you will want to abort a long-running command before it has a time to finish.
|
||||
If you are in the middle of crafting your armor you will probably want to stop doing that when a
|
||||
monster enters your smithy.
|
||||
|
||||
You can implement this in the same way as you do the "blocking" command above, just in reverse.
|
||||
Below is an example of a crafting command that can be aborted by starting a fight:
|
||||
|
||||
```python
|
||||
from evennia import utils, default_cmds
|
||||
|
||||
class CmdCraftArmour(default_cmds.MuxCommand):
|
||||
"""
|
||||
Craft armour
|
||||
|
||||
Usage:
|
||||
craft <name of armour>
|
||||
|
||||
This will craft a suit of armour, assuming you
|
||||
have all the components and tools. Doing some
|
||||
other action (such as attacking someone) will
|
||||
abort the crafting process.
|
||||
"""
|
||||
key = "craft"
|
||||
locks = "cmd:all()"
|
||||
|
||||
def func(self):
|
||||
"starts crafting"
|
||||
|
||||
if self.caller.ndb.is_crafting:
|
||||
self.caller.msg("You are already crafting!")
|
||||
return
|
||||
if self._is_fighting():
|
||||
self.caller.msg("You can't start to craft "
|
||||
"in the middle of a fight!")
|
||||
return
|
||||
|
||||
# [Crafting code, checking of components, skills etc]
|
||||
|
||||
# Start crafting
|
||||
self.caller.ndb.is_crafting = True
|
||||
self.caller.msg("You start crafting ...")
|
||||
utils.delay(60, self.step1)
|
||||
|
||||
def _is_fighting(self):
|
||||
"checks if we are in a fight."
|
||||
if self.caller.ndb.is_fighting:
|
||||
del self.caller.ndb.is_crafting
|
||||
return True
|
||||
|
||||
def step1(self):
|
||||
"first step of armour construction"
|
||||
if self._is_fighting():
|
||||
return
|
||||
self.msg("You create the first part of the armour.")
|
||||
utils.delay(60, callback=self.step2)
|
||||
|
||||
def step2(self):
|
||||
"second step of armour construction"
|
||||
if self._is_fighting():
|
||||
return
|
||||
self.msg("You create the second part of the armour.")
|
||||
utils.delay(60, step3)
|
||||
|
||||
def step3(self):
|
||||
"last step of armour construction"
|
||||
if self._is_fighting():
|
||||
return
|
||||
|
||||
# [code for creating the armour object etc]
|
||||
|
||||
del self.caller.ndb.is_crafting
|
||||
self.msg("You finalize your armour.")
|
||||
|
||||
|
||||
# example of a command that aborts crafting
|
||||
|
||||
class CmdAttack(default_cmds.MuxCommand):
|
||||
"""
|
||||
attack someone
|
||||
|
||||
Usage:
|
||||
attack <target>
|
||||
|
||||
Try to cause harm to someone. This will abort
|
||||
eventual crafting you may be currently doing.
|
||||
"""
|
||||
key = "attack"
|
||||
aliases = ["hit", "stab"]
|
||||
locks = "cmd:all()"
|
||||
|
||||
def func(self):
|
||||
"Implements the command"
|
||||
|
||||
self.caller.ndb.is_fighting = True
|
||||
|
||||
# [...]
|
||||
```
|
||||
|
||||
The above code creates a delayed crafting command that will gradually create the armour. If the
|
||||
`attack` command is issued during this process it will set a flag that causes the crafting to be
|
||||
quietly canceled next time it tries to update.
|
||||
|
||||
## Persistent delays
|
||||
|
||||
In the latter examples above we used `.ndb` storage. This is fast and easy but it will reset all
|
||||
cooldowns/blocks/crafting etc if you reload the server. If you don't want that you can replace
|
||||
`.ndb` with `.db`. But even this won't help because the `yield` keyword is not persisent and nor is
|
||||
the use of `delay` shown above. To resolve this you can use `delay` with the `persistent=True`
|
||||
keyword. But wait! Making something persistent will add some extra complications, because now you
|
||||
must make sure Evennia can properly store things to the database.
|
||||
|
||||
Here is the original echo-command reworked to function with persistence:
|
||||
```python
|
||||
from evennia import default_cmds, utils
|
||||
|
||||
# this is now in the outermost scope and takes two args!
|
||||
def echo(caller, args):
|
||||
"Called after 10 seconds."
|
||||
shout = args
|
||||
string = "You hear an echo: %s ... %s ... %s"
|
||||
string = string % (shout.upper(), shout.capitalize(), shout.lower())
|
||||
caller.msg(string)
|
||||
|
||||
class CmdEcho(default_cmds.MuxCommand):
|
||||
"""
|
||||
wait for an echo
|
||||
|
||||
Usage:
|
||||
echo <string>
|
||||
|
||||
Calls and waits for an echo
|
||||
"""
|
||||
key = "echo"
|
||||
locks = "cmd:all()"
|
||||
|
||||
def func(self):
|
||||
"""
|
||||
This is called at the initial shout.
|
||||
"""
|
||||
self.caller.msg("You shout '%s' and wait for an echo ..." % self.args)
|
||||
# this waits non-blocking for 10 seconds, then calls echo(self.caller, self.args)
|
||||
utils.delay(10, echo, self.caller, self.args, persistent=True) # changes!
|
||||
|
||||
```
|
||||
|
||||
Above you notice two changes:
|
||||
- The callback (`echo`) was moved out of the class and became its own stand-alone function in the
|
||||
outermost scope of the module. It also now takes `caller` and `args` as arguments (it doesn't have
|
||||
access to them directly since this is now a stand-alone function).
|
||||
- `utils.delay` specifies the `echo` function (not `self.echo` - it's no longer a method!) and sends
|
||||
`self.caller` and `self.args` as arguments for it to use. We also set `persistent=True`.
|
||||
|
||||
The reason for this change is because Evennia needs to `pickle` the callback into storage and it
|
||||
cannot do this correctly when the method sits on the command class. Now this behave the same as the
|
||||
first version except if you reload (or even shut down) the server mid-delay it will still fire the
|
||||
callback when the server comes back up (it will resume the countdown and ignore the downtime).
|
||||
129
docs/source/Howto/Command-Prompt.md
Normal file
129
docs/source/Howto/Command-Prompt.md
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
# Command Prompt
|
||||
|
||||
|
||||
A *prompt* is quite common in MUDs. The prompt display useful details about your character that you
|
||||
are likely to want to keep tabs on at all times, such as health, magical power etc. It might also
|
||||
show things like in-game time, weather and so on. Many modern MUD clients (including Evennia's own
|
||||
webclient) allows for identifying the prompt and have it appear in a correct location (usually just
|
||||
above the input line). Usually it will remain like that until it is explicitly updated.
|
||||
|
||||
## Sending a prompt
|
||||
|
||||
A prompt is sent using the `prompt` keyword to the `msg()` method on objects. The prompt will be
|
||||
sent without any line breaks.
|
||||
|
||||
```python
|
||||
self.msg(prompt="HP: 5, MP: 2, SP: 8")
|
||||
```
|
||||
You can combine the sending of normal text with the sending (updating of the prompt):
|
||||
|
||||
```python
|
||||
self.msg("This is a text", prompt="This is a prompt")
|
||||
```
|
||||
|
||||
You can update the prompt on demand, this is normally done using [OOB](OOB)-tracking of the relevant
|
||||
Attributes (like the character's health). You could also make sure that attacking commands update
|
||||
the prompt when they cause a change in health, for example.
|
||||
|
||||
Here is a simple example of the prompt sent/updated from a command class:
|
||||
|
||||
```python
|
||||
from evennia import Command
|
||||
|
||||
class CmdDiagnose(Command):
|
||||
"""
|
||||
see how hurt your are
|
||||
|
||||
Usage:
|
||||
diagnose [target]
|
||||
|
||||
This will give an estimate of the target's health. Also
|
||||
the target's prompt will be updated.
|
||||
"""
|
||||
key = "diagnose"
|
||||
|
||||
def func(self):
|
||||
if not self.args:
|
||||
target = self.caller
|
||||
else:
|
||||
target = self.search(self.args)
|
||||
if not target:
|
||||
return
|
||||
# try to get health, mana and stamina
|
||||
hp = target.db.hp
|
||||
mp = target.db.mp
|
||||
sp = target.db.sp
|
||||
|
||||
if None in (hp, mp, sp):
|
||||
# Attributes not defined
|
||||
self.caller.msg("Not a valid target!")
|
||||
return
|
||||
|
||||
text = "You diagnose %s as having " \
|
||||
"%i health, %i mana and %i stamina." \
|
||||
% (hp, mp, sp)
|
||||
prompt = "%i HP, %i MP, %i SP" % (hp, mp, sp)
|
||||
self.caller.msg(text, prompt=prompt)
|
||||
```
|
||||
## A prompt sent with every command
|
||||
|
||||
The prompt sent as described above uses a standard telnet instruction (the Evennia web client gets a
|
||||
special flag). Most MUD telnet clients will understand and allow users to catch this and keep the
|
||||
prompt in place until it updates. So *in principle* you'd not need to update the prompt every
|
||||
command.
|
||||
|
||||
However, with a varying user base it can be unclear which clients are used and which skill level the
|
||||
users have. So sending a prompt with every command is a safe catch-all. You don't need to manually
|
||||
go in and edit every command you have though. Instead you edit the base command class for your
|
||||
custom commands (like `MuxCommand` in your `mygame/commands/command.py` folder) and overload the
|
||||
`at_post_cmd()` hook. This hook is always called *after* the main `func()` method of the Command.
|
||||
|
||||
```python
|
||||
from evennia import default_cmds
|
||||
|
||||
class MuxCommand(default_cmds.MuxCommand):
|
||||
# ...
|
||||
def at_post_cmd(self):
|
||||
"called after self.func()."
|
||||
caller = self.caller
|
||||
prompt = "%i HP, %i MP, %i SP" % (caller.db.hp,
|
||||
caller.db.mp,
|
||||
caller.db.sp)
|
||||
caller.msg(prompt=prompt)
|
||||
|
||||
```
|
||||
|
||||
### Modifying default commands
|
||||
|
||||
If you want to add something small like this to Evennia's default commands without modifying them
|
||||
directly the easiest way is to just wrap those with a multiple inheritance to your own base class:
|
||||
|
||||
```python
|
||||
# in (for example) mygame/commands/mycommands.py
|
||||
|
||||
from evennia import default_cmds
|
||||
# our custom MuxCommand with at_post_cmd hook
|
||||
from commands.command import MuxCommand
|
||||
|
||||
# overloading the look command
|
||||
class CmdLook(default_cmds.CmdLook, MuxCommand):
|
||||
pass
|
||||
```
|
||||
|
||||
The result of this is that the hooks from your custom `MuxCommand` will be mixed into the default
|
||||
`CmdLook` through multiple inheritance. Next you just add this to your default command set:
|
||||
|
||||
```python
|
||||
# in mygame/commands/default_cmdsets.py
|
||||
|
||||
from evennia import default_cmds
|
||||
from commands import mycommands
|
||||
|
||||
class CharacterCmdSet(default_cmds.CharacterCmdSet):
|
||||
# ...
|
||||
def at_cmdset_creation(self):
|
||||
# ...
|
||||
self.add(mycommands.CmdLook())
|
||||
```
|
||||
|
||||
This will automatically replace the default `look` command in your game with your own version.
|
||||
349
docs/source/Howto/Coordinates.md
Normal file
349
docs/source/Howto/Coordinates.md
Normal file
|
|
@ -0,0 +1,349 @@
|
|||
# Coordinates
|
||||
|
||||
# Adding room coordinates in your game
|
||||
|
||||
This tutorial is moderately difficult in content. You might want to be familiar and at ease with
|
||||
some Python concepts (like properties) and possibly Django concepts (like queries), although this
|
||||
tutorial will try to walk you through the process and give enough explanations each time. If you
|
||||
don't feel very confident with math, don't hesitate to pause, go to the example section, which shows
|
||||
a tiny map, and try to walk around the code or read the explanation.
|
||||
|
||||
Evennia doesn't have a coordinate system by default. Rooms and other objects are linked by location
|
||||
and content:
|
||||
|
||||
- An object can be in a location, that is, another object. Like an exit in a room.
|
||||
- An object can access its content. A room can see what objects uses it as location (that would
|
||||
include exits, rooms, characters and so on).
|
||||
|
||||
This system allows for a lot of flexibility and, fortunately, can be extended by other systems.
|
||||
Here, I offer you a way to add coordinates to every room in a way most compliant with Evennia
|
||||
design. This will also show you how to use coordinates, find rooms around a given point for
|
||||
instance.
|
||||
|
||||
## Coordinates as tags
|
||||
|
||||
The first concept might be the most surprising at first glance: we will create coordinates as
|
||||
[tags](Tags).
|
||||
|
||||
> Why not attributes, wouldn't that be easier?
|
||||
|
||||
It would. We could just do something like `room.db.x = 3`. The advantage of using tags is that it
|
||||
will be easy and effective to search. Although this might not seem like a huge advantage right now,
|
||||
with a database of thousands of rooms, it might make a difference, particularly if you have a lot of
|
||||
things based on coordinates.
|
||||
|
||||
Rather than giving you a step-by-step process, I'll show you the code. Notice that we use
|
||||
properties to easily access and update coordinates. This is a Pythonic approach. Here's our first
|
||||
`Room` class, that you can modify in `typeclasses/rooms.py`:
|
||||
|
||||
```python
|
||||
# in typeclasses/rooms.py
|
||||
|
||||
from evennia import DefaultRoom
|
||||
|
||||
class Room(DefaultRoom):
|
||||
"""
|
||||
Rooms are like any Object, except their location is None
|
||||
(which is default). They also use basetype_setup() to
|
||||
add locks so they cannot be puppeted or picked up.
|
||||
(to change that, use at_object_creation instead)
|
||||
|
||||
See examples/object.py for a list of
|
||||
properties and methods available on all Objects.
|
||||
"""
|
||||
|
||||
@property
|
||||
def x(self):
|
||||
"""Return the X coordinate or None."""
|
||||
x = self.tags.get(category="coordx")
|
||||
return int(x) if isinstance(x, str) else None
|
||||
|
||||
@x.setter
|
||||
def x(self, x):
|
||||
"""Change the X coordinate."""
|
||||
old = self.tags.get(category="coordx")
|
||||
if old is not None:
|
||||
self.tags.remove(old, category="coordx")
|
||||
if x is not None:
|
||||
self.tags.add(str(x), category="coordx")
|
||||
|
||||
@property
|
||||
def y(self):
|
||||
"""Return the Y coordinate or None."""
|
||||
y = self.tags.get(category="coordy")
|
||||
return int(y) if isinstance(y, str) else None
|
||||
|
||||
@y.setter
|
||||
def y(self, y):
|
||||
"""Change the Y coordinate."""
|
||||
old = self.tags.get(category="coordy")
|
||||
if old is not None:
|
||||
self.tags.remove(old, category="coordy")
|
||||
if y is not None:
|
||||
self.tags.add(str(y), category="coordy")
|
||||
|
||||
@property
|
||||
def z(self):
|
||||
"""Return the Z coordinate or None."""
|
||||
z = self.tags.get(category="coordz")
|
||||
return int(z) if isinstance(z, str) else None
|
||||
|
||||
@z.setter
|
||||
def z(self, z):
|
||||
"""Change the Z coordinate."""
|
||||
old = self.tags.get(category="coordz")
|
||||
if old is not None:
|
||||
self.tags.remove(old, category="coordz")
|
||||
if z is not None:
|
||||
self.tags.add(str(z), category="coordz")
|
||||
```
|
||||
|
||||
If you aren't familiar with the concept of properties in Python, I encourage you to read a good
|
||||
tutorial on the subject. [This article on Python properties](https://www.programiz.com/python-
|
||||
programming/property)
|
||||
is well-explained and should help you understand the idea.
|
||||
|
||||
Let's look at our properties for `x`. First of all is the read property.
|
||||
|
||||
```python
|
||||
@property
|
||||
def x(self):
|
||||
"""Return the X coordinate or None."""
|
||||
x = self.tags.get(category="coordx")
|
||||
return int(x) if isinstance(x, str) else None
|
||||
```
|
||||
|
||||
What it does is pretty simple:
|
||||
|
||||
1. It gets the tag of category `"coordx"`. It's the tag category where we store our X coordinate.
|
||||
The `tags.get` method will return `None` if the tag can't be found.
|
||||
2. We convert the value to an integer, if it's a `str`. Remember that tags can only contain `str`,
|
||||
so we'll need to convert it.
|
||||
|
||||
> I thought tags couldn't contain values?
|
||||
|
||||
Well, technically, they can't: they're either here or not. But using tag categories, as we have
|
||||
done, we get a tag, knowing only its category. That's the basic approach to coordinates in this
|
||||
tutorial.
|
||||
|
||||
Now, let's look at the method that will be called when we wish to set `x` in our room:
|
||||
|
||||
```python
|
||||
@x.setter
|
||||
def x(self, x):
|
||||
"""Change the X coordinate."""
|
||||
old = self.tags.get(category="coordx")
|
||||
if old is not None:
|
||||
self.tags.remove(old, category="coordx")
|
||||
if x is not None:
|
||||
self.tags.add(str(x), category="coordx")
|
||||
```
|
||||
|
||||
1. First, we remove the old X coordinate, if it exists. Otherwise, we'd end up with two tags in our
|
||||
room with "coordx" as their category, which wouldn't do at all.
|
||||
2. Then we add the new tag, giving it the proper category.
|
||||
|
||||
> Now what?
|
||||
|
||||
If you add this code and reload your game, once you're logged in with a character in a room as its
|
||||
location, you can play around:
|
||||
|
||||
```
|
||||
@py here.x
|
||||
@py here.x = 0
|
||||
@py here.y = 3
|
||||
@py here.z = -2
|
||||
@py here.z = None
|
||||
```
|
||||
|
||||
The code might not be that easy to read, but you have to admit it's fairly easy to use.
|
||||
|
||||
## Some additional searches
|
||||
|
||||
Having coordinates is useful for several reasons:
|
||||
|
||||
1. It can help in shaping a truly logical world, in its geography, at least.
|
||||
2. It can allow to look for specific rooms at given coordinates.
|
||||
3. It can be good in order to quickly find the rooms around a location.
|
||||
4. It can even be great in path-finding (finding the shortest path between two rooms).
|
||||
|
||||
So far, our coordinate system can help with 1., but not much else. Here are some methods that we
|
||||
could add to the `Room` typeclass. These methods will just be search methods. Notice that they are
|
||||
class methods, since we want to get rooms.
|
||||
|
||||
### Finding one room
|
||||
|
||||
First, a simple one: how to find a room at a given coordinate? Say, what is the room at X=0, Y=0,
|
||||
Z=0?
|
||||
|
||||
```python
|
||||
class Room(DefaultRoom):
|
||||
# ...
|
||||
@classmethod
|
||||
def get_room_at(cls, x, y, z):
|
||||
"""
|
||||
Return the room at the given location or None if not found.
|
||||
|
||||
Args:
|
||||
x (int): the X coord.
|
||||
y (int): the Y coord.
|
||||
z (int): the Z coord.
|
||||
|
||||
Return:
|
||||
The room at this location (Room) or None if not found.
|
||||
|
||||
"""
|
||||
rooms = cls.objects.filter(
|
||||
db_tags__db_key=str(x), db_tags__db_category="coordx").filter(
|
||||
db_tags__db_key=str(y), db_tags__db_category="coordy").filter(
|
||||
db_tags__db_key=str(z), db_tags__db_category="coordz")
|
||||
if rooms:
|
||||
return rooms[0]
|
||||
|
||||
return None
|
||||
```
|
||||
|
||||
This solution includes a bit of [Django
|
||||
queries](https://docs.djangoproject.com/en/1.11/topics/db/queries/).
|
||||
Basically, what we do is reach for the object manager and search for objects with the matching tags.
|
||||
Again, don't spend too much time worrying about the mechanism, the method is quite easy to use:
|
||||
|
||||
```
|
||||
Room.get_room_at(5, 2, -3)
|
||||
```
|
||||
|
||||
Notice that this is a class method: you will call it from `Room` (the class), not an instance.
|
||||
Though you still can:
|
||||
|
||||
@py here.get_room_at(3, 8, 0)
|
||||
|
||||
### Finding several rooms
|
||||
|
||||
Here's another useful method that allows us to look for rooms around a given coordinate. This is
|
||||
more advanced search and doing some calculation, beware! Look at the following section if you're
|
||||
lost.
|
||||
|
||||
```python
|
||||
from math import sqrt
|
||||
|
||||
class Room(DefaultRoom):
|
||||
|
||||
# ...
|
||||
|
||||
@classmethod
|
||||
def get_rooms_around(cls, x, y, z, distance):
|
||||
"""
|
||||
Return the list of rooms around the given coordinates.
|
||||
|
||||
This method returns a list of tuples (distance, room) that
|
||||
can easily be browsed. This list is sorted by distance (the
|
||||
closest room to the specified position is always at the top
|
||||
of the list).
|
||||
|
||||
Args:
|
||||
x (int): the X coord.
|
||||
y (int): the Y coord.
|
||||
z (int): the Z coord.
|
||||
distance (int): the maximum distance to the specified position.
|
||||
|
||||
Returns:
|
||||
A list of tuples containing the distance to the specified
|
||||
position and the room at this distance. Several rooms
|
||||
can be at equal distance from the position.
|
||||
|
||||
"""
|
||||
# Performs a quick search to only get rooms in a square
|
||||
x_r = list(reversed([str(x - i) for i in range(0, distance + 1)]))
|
||||
x_r += [str(x + i) for i in range(1, distance + 1)]
|
||||
y_r = list(reversed([str(y - i) for i in range(0, distance + 1)]))
|
||||
y_r += [str(y + i) for i in range(1, distance + 1)]
|
||||
z_r = list(reversed([str(z - i) for i in range(0, distance + 1)]))
|
||||
z_r += [str(z + i) for i in range(1, distance + 1)]
|
||||
wide = cls.objects.filter(
|
||||
db_tags__db_key__in=x_r, db_tags__db_category="coordx").filter(
|
||||
db_tags__db_key__in=y_r, db_tags__db_category="coordy").filter(
|
||||
db_tags__db_key__in=z_r, db_tags__db_category="coordz")
|
||||
|
||||
# We now need to filter down this list to find out whether
|
||||
# these rooms are really close enough, and at what distance
|
||||
# In short: we change the square to a circle.
|
||||
rooms = []
|
||||
for room in wide:
|
||||
x2 = int(room.tags.get(category="coordx"))
|
||||
y2 = int(room.tags.get(category="coordy"))
|
||||
z2 = int(room.tags.get(category="coordz"))
|
||||
distance_to_room = sqrt(
|
||||
(x2 - x) ** 2 + (y2 - y) ** 2 + (z2 - z) ** 2)
|
||||
if distance_to_room <= distance:
|
||||
rooms.append((distance_to_room, room))
|
||||
|
||||
# Finally sort the rooms by distance
|
||||
rooms.sort(key=lambda tup: tup[0])
|
||||
return rooms
|
||||
```
|
||||
|
||||
This gets more serious.
|
||||
|
||||
1. We have specified coordinates as parameters. We determine a broad range using the distance.
|
||||
That is, for each coordinate, we create a list of possible matches. See the example below.
|
||||
2. We then search for the rooms within this broader range. It gives us a square
|
||||
around our location. Some rooms are definitely outside the range. Again, see the example below
|
||||
to follow the logic.
|
||||
3. We filter down the list and sort it by distance from the specified coordinates.
|
||||
|
||||
Notice that we only search starting at step 2. Thus, the Django search doesn't look and cache all
|
||||
objects, just a wider range than what would be really necessary. This method returns a circle of
|
||||
coordinates around a specified point. Django looks for a square. What wouldn't fit in the circle
|
||||
is removed at step 3, which is the only part that includes systematic calculation. This method is
|
||||
optimized to be quick and efficient.
|
||||
|
||||
### An example
|
||||
|
||||
An example might help. Consider this very simple map (a textual description follows):
|
||||
|
||||
```
|
||||
4 A B C D
|
||||
3 E F G H
|
||||
2 I J K L
|
||||
1 M N O P
|
||||
1 2 3 4
|
||||
```
|
||||
|
||||
The X coordinates are given below. The Y coordinates are given on the left. This is a simple
|
||||
square with 16 rooms: 4 on each line, 4 lines of them. All the rooms are identified by letters in
|
||||
this example: the first line at the top has rooms A to D, the second E to H, the third I to L and
|
||||
the fourth M to P. The bottom-left room, X=1 and Y=1, is M. The upper-right room X=4 and Y=4 is D.
|
||||
|
||||
So let's say we want to find all the neighbors, distance 1, from the room J. J is at X=2, Y=2.
|
||||
|
||||
So we use:
|
||||
|
||||
Room.get_rooms_around(x=2, y=2, z=0, distance=1)
|
||||
# we'll assume a z coordinate of 0 for simplicity
|
||||
|
||||
1. First, this method gets all the rooms in a square around J. So it gets E F G, I J K, M N O. If
|
||||
you want, draw the square around these coordinates to see what's happening.
|
||||
2. Next, we browse over this list and check the real distance between J (X=2, Y=2) and the room.
|
||||
The four corners of the square are not in this circle. For instance, the distance between J and M
|
||||
is not 1. If you draw a circle of center J and radius 1, you'll notice that the four corners of our
|
||||
square (E, G, M and O) are not in this circle. So we remove them.
|
||||
3. We sort by distance from J.
|
||||
|
||||
So in the end we might obtain something like this:
|
||||
|
||||
```
|
||||
[
|
||||
(0, J), # yes, J is part of this circle after all, with a distance of 0
|
||||
(1, F),
|
||||
(1, I),
|
||||
(1, K),
|
||||
(1, N),
|
||||
]
|
||||
```
|
||||
|
||||
You can try with more examples if you want to see this in action.
|
||||
|
||||
### To conclude
|
||||
|
||||
You can definitely use this system to map other objects, not just rooms. You can easily remove the
|
||||
`Z coordinate too, if you simply need X and Y.
|
||||
484
docs/source/Howto/Customize-channels.md
Normal file
484
docs/source/Howto/Customize-channels.md
Normal file
|
|
@ -0,0 +1,484 @@
|
|||
# Customize channels
|
||||
|
||||
|
||||
# Channel commands in Evennia
|
||||
|
||||
By default, Evennia's default channel commands are inspired by MUX. They all
|
||||
begin with "c" followed by the action to perform (like "ccreate" or "cdesc").
|
||||
If this default seems strange to you compared to other Evennia commands that
|
||||
rely on switches, you might want to check this tutorial out.
|
||||
|
||||
This tutorial will also give you insight into the workings of the channel system.
|
||||
So it may be useful even if you don't plan to make the exact changes shown here.
|
||||
|
||||
## What we will try to do
|
||||
|
||||
Our mission: change the default channel commands to have a different syntax.
|
||||
|
||||
This tutorial will do the following changes:
|
||||
|
||||
- Remove all the default commands to handle channels.
|
||||
- Add a `+` and `-` command to join and leave a channel. So, assuming there is
|
||||
a `public` channel on your game (most often the case), you could type `+public`
|
||||
to join it and `-public` to leave it.
|
||||
- Group the commands to manipulate channels under the channel name, after a
|
||||
switch. For instance, instead of writing `cdesc public = My public channel`,
|
||||
you would write `public/desc My public channel`.
|
||||
|
||||
|
||||
> I listed removing the default Evennia commands as a first step in the
|
||||
> process. Actually, we'll move it at the very bottom of the list, since we
|
||||
> still want to use them, we might get it wrong and rely on Evennia commands
|
||||
> for a while longer.
|
||||
|
||||
## A command to join, another to leave
|
||||
|
||||
We'll do the most simple task at first: create two commands, one to join a
|
||||
channel, one to leave.
|
||||
|
||||
> Why not have them as switches? `public/join` and `public/leave` for instance?
|
||||
|
||||
For security reasons, I will hide channels to which the caller is not
|
||||
connected. It means that if the caller is not connected to the "public"
|
||||
channel, he won't be able to use the "public" command. This is somewhat
|
||||
standard: if we create an administrator-only channel, we don't want players to
|
||||
try (or even know) the channel command. Again, you could design it a different
|
||||
way should you want to.
|
||||
|
||||
First create a file named `comms.py` in your `commands` package. It's
|
||||
a rather logical place, since we'll write different commands to handle
|
||||
communication.
|
||||
|
||||
Okay, let's add the first command to join a channel:
|
||||
|
||||
```python
|
||||
# in commands/comms.py
|
||||
from evennia.utils.search import search_channel
|
||||
from commands.command import Command
|
||||
|
||||
class CmdConnect(Command):
|
||||
"""
|
||||
Connect to a channel.
|
||||
"""
|
||||
|
||||
key = "+"
|
||||
help_category = "Comms"
|
||||
locks = "cmd:not pperm(channel_banned)"
|
||||
auto_help = False
|
||||
|
||||
def func(self):
|
||||
"""Implement the command"""
|
||||
caller = self.caller
|
||||
args = self.args
|
||||
if not args:
|
||||
self.msg("Which channel do you want to connect to?")
|
||||
return
|
||||
|
||||
channelname = self.args
|
||||
channel = search_channel(channelname)
|
||||
if not channel:
|
||||
return
|
||||
|
||||
# Check permissions
|
||||
if not channel.access(caller, 'listen'):
|
||||
self.msg("%s: You are not allowed to listen to this channel." % channel.key)
|
||||
return
|
||||
|
||||
# If not connected to the channel, try to connect
|
||||
if not channel.has_connection(caller):
|
||||
if not channel.connect(caller):
|
||||
self.msg("%s: You are not allowed to join this channel." % channel.key)
|
||||
return
|
||||
else:
|
||||
self.msg("You now are connected to the %s channel. " % channel.key.lower())
|
||||
else:
|
||||
self.msg("You already are connected to the %s channel. " % channel.key.lower())
|
||||
```
|
||||
|
||||
Okay, let's review this code, but if you're used to Evennia commands, it shouldn't be too strange:
|
||||
|
||||
1. We import `search_channel`. This is a little helper function that we will use to search for
|
||||
channels by name and aliases, found in `evennia.utils.search`. It's just more convenient.
|
||||
2. Our class `CmdConnect` contains the body of our command to join a channel.
|
||||
3. Notice the key of this command is simply `"+"`. When you enter `+something` in the game, it will
|
||||
try to find a command key `+something`. Failing that, it will look at other potential matches.
|
||||
Evennia is smart enough to understand that when we type `+something`, `+` is the command key and
|
||||
`something` is the command argument. This will, of course, fail if you have a command beginning by
|
||||
`+` conflicting with the `CmdConnect` key.
|
||||
4. We have altered some class attributes, like `auto_help`. If you want to know what they do and
|
||||
why they have changed here, you can check the [documentation on commands](Commands).
|
||||
5. In the command body, we begin by extracting the channel name. Remember that this name should be
|
||||
in the command arguments (that is, in `self.args`). Following the same example, if a player enters
|
||||
`+something`, `self.args` should contain `"something"`. We use `search_channel` to see if this
|
||||
channel exists.
|
||||
6. We then check the access level of the channel, to see if the caller can listen to it (not
|
||||
necessarily use it to speak, mind you, just listen to others speak, as these are two different locks
|
||||
on Evennia).
|
||||
7. Finally, we connect the caller if he's not already connected to the channel. We use the
|
||||
channel's `connect` method to do this. Pretty straightforward eh?
|
||||
|
||||
Now we'll add a command to leave a channel. It's almost the same, turned upside down:
|
||||
|
||||
```python
|
||||
class CmdDisconnect(Command):
|
||||
"""
|
||||
Disconnect from a channel.
|
||||
"""
|
||||
|
||||
key = "-"
|
||||
help_category = "Comms"
|
||||
locks = "cmd:not pperm(channel_banned)"
|
||||
auto_help = False
|
||||
|
||||
def func(self):
|
||||
"""Implement the command"""
|
||||
caller = self.caller
|
||||
args = self.args
|
||||
if not args:
|
||||
self.msg("Which channel do you want to disconnect from?")
|
||||
return
|
||||
|
||||
channelname = self.args
|
||||
channel = search_channel(channelname)
|
||||
if not channel:
|
||||
return
|
||||
|
||||
# If connected to the channel, try to disconnect
|
||||
if channel.has_connection(caller):
|
||||
if not channel.disconnect(caller):
|
||||
self.msg("%s: You are not allowed to disconnect from this channel." % channel.key)
|
||||
return
|
||||
else:
|
||||
self.msg("You stop listening to the %s channel. " % channel.key.lower())
|
||||
else:
|
||||
self.msg("You are not connected to the %s channel. " % channel.key.lower())
|
||||
```
|
||||
|
||||
So far, you shouldn't have trouble following what this command does: it's
|
||||
pretty much the same as the `CmdConnect` class in logic, though it accomplishes
|
||||
the opposite. If you are connected to the channel `public` you could
|
||||
disconnect from it using `-public`. Remember, you can use channel aliases too
|
||||
(`+pub` and `-pub` will also work, assuming you have the alias `pub` on the
|
||||
`public` channel).
|
||||
|
||||
It's time to test this code, and to do so, you will need to add these two
|
||||
commands. Here is a good time to say it: by default, Evennia connects accounts
|
||||
to channels. Some other games (usually with a higher multisession mode) will
|
||||
want to connect characters instead of accounts, so that several characters in
|
||||
the same account can be connected to various channels. You can definitely add
|
||||
these commands either in the `AccountCmdSet` or `CharacterCmdSet`, the caller
|
||||
will be different and the command will add or remove accounts of characters.
|
||||
If you decide to install these commands on the `CharacterCmdSet`, you might
|
||||
have to disconnect your superuser account (account #1) from the channel before
|
||||
joining it with your characters, as Evennia tends to subscribe all accounts
|
||||
automatically if you don't tell it otherwise.
|
||||
|
||||
So here's an example of how to add these commands into your `AccountCmdSet`.
|
||||
Edit the file `commands/default_cmdsets.py` to change a few things:
|
||||
|
||||
```python
|
||||
# In commands/default_cmdsets.py
|
||||
from evennia import default_cmds
|
||||
from commands.comms import CmdConnect, CmdDisconnect
|
||||
|
||||
|
||||
# ... Skip to the AccountCmdSet class ...
|
||||
|
||||
class AccountCmdSet(default_cmds.AccountCmdSet):
|
||||
"""
|
||||
This is the cmdset available to the Account at all times. It is
|
||||
combined with the `CharacterCmdSet` when the Account puppets a
|
||||
Character. It holds game-account-specific commands, channel
|
||||
commands, etc.
|
||||
"""
|
||||
key = "DefaultAccount"
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
"""
|
||||
Populates the cmdset
|
||||
"""
|
||||
super().at_cmdset_creation()
|
||||
|
||||
# Channel commands
|
||||
self.add(CmdConnect())
|
||||
self.add(CmdDisconnect())
|
||||
```
|
||||
|
||||
Save, reload your game, and you should be able to use `+public` and `-public`
|
||||
now!
|
||||
|
||||
## A generic channel command with switches
|
||||
|
||||
It's time to dive a little deeper into channel processing. What happens in
|
||||
Evennia when a player enters `public Hello everybody!`?
|
||||
|
||||
Like exits, channels are a particular command that Evennia automatically
|
||||
creates and attaches to individual channels. So when you enter `public
|
||||
message` in your game, Evennia calls the `public` command.
|
||||
|
||||
> But I didn't add any public command...
|
||||
|
||||
Evennia will just create these commands automatically based on the existing
|
||||
channels. The base command is the command we'll need to edit.
|
||||
|
||||
> Why edit it? It works just fine to talk.
|
||||
|
||||
Unfortunately, if we want to add switches to our channel names, we'll have to
|
||||
edit this command. It's not too hard, however, we'll just start writing a
|
||||
standard command with minor twitches.
|
||||
|
||||
### Some additional imports
|
||||
|
||||
You'll need to add a line of import in your `commands/comms.py` file. We'll
|
||||
see why this import is important when diving in the command itself:
|
||||
|
||||
```python
|
||||
from evennia.comms.models import ChannelDB
|
||||
```
|
||||
|
||||
### The class layout
|
||||
|
||||
```python
|
||||
# In commands/comms.py
|
||||
class ChannelCommand(Command):
|
||||
"""
|
||||
{channelkey} channel
|
||||
|
||||
{channeldesc}
|
||||
|
||||
Usage:
|
||||
{lower_channelkey} <message>
|
||||
{lower_channelkey}/history [start]
|
||||
{lower_channelkey}/me <message>
|
||||
{lower_channelkey}/who
|
||||
|
||||
Switch:
|
||||
history: View 20 previous messages, either from the end or
|
||||
from <start> number of messages from the end.
|
||||
me: Perform an emote on this channel.
|
||||
who: View who is connected to this channel.
|
||||
|
||||
Example:
|
||||
{lower_channelkey} Hello World!
|
||||
{lower_channelkey}/history
|
||||
{lower_channelkey}/history 30
|
||||
{lower_channelkey}/me grins.
|
||||
{lower_channelkey}/who
|
||||
"""
|
||||
# note that channeldesc and lower_channelkey will be filled
|
||||
# automatically by ChannelHandler
|
||||
|
||||
# this flag is what identifies this cmd as a channel cmd
|
||||
# and branches off to the system send-to-channel command
|
||||
# (which is customizable by admin)
|
||||
is_channel = True
|
||||
key = "general"
|
||||
help_category = "Channel Names"
|
||||
obj = None
|
||||
arg_regex = ""
|
||||
```
|
||||
|
||||
There are some differences here compared to most common commands.
|
||||
|
||||
- There is something disconcerting in the class docstring. Some information is
|
||||
between curly braces. This is a format-style which is only used for channel
|
||||
commands. `{channelkey}` will be replaced by the actual channel key (like
|
||||
public). `{channeldesc}` will be replaced by the channel description (like
|
||||
"public channel"). And `{lower_channelkey}`.
|
||||
- We have set `is_channel` to `True` in the command class variables. You
|
||||
shouldn't worry too much about that: it just tells Evennia this is a special
|
||||
command just for channels.
|
||||
- `key` is a bit misleading because it will be replaced eventually. So we
|
||||
could set it to virtually anything.
|
||||
- The `obj` class variable is another one we won't detail right now.
|
||||
- `arg_regex` is important: the default `arg_regex` in the channel command will
|
||||
forbid to use switches (a slash just after the channel name is not allowed).
|
||||
That's why we enforce it here, we allow any syntax.
|
||||
|
||||
> What will become of this command?
|
||||
|
||||
Well, when we'll be through with it, and once we'll add it as the default
|
||||
command to handle channels, Evennia will create one per existing channel. For
|
||||
instance, the public channel will receive one command of this class, with `key`
|
||||
set to `public` and `aliases` set to the channel aliases (like `['pub']`).
|
||||
|
||||
> Can I see it work?
|
||||
|
||||
Not just yet, there's still a lot of code needed.
|
||||
|
||||
Okay we have the command structure but it's rather empty.
|
||||
|
||||
### The parse method
|
||||
|
||||
The `parse` method is called before `func` in every command. Its job is to
|
||||
parse arguments and in our case, we will analyze switches here.
|
||||
|
||||
```python
|
||||
# ...
|
||||
def parse(self):
|
||||
"""
|
||||
Simple parser
|
||||
"""
|
||||
# channel-handler sends channame:msg here.
|
||||
channelname, msg = self.args.split(":", 1)
|
||||
self.switch = None
|
||||
if msg.startswith("/"):
|
||||
try:
|
||||
switch, msg = msg[1:].split(" ", 1)
|
||||
except ValueError:
|
||||
switch = msg[1:]
|
||||
msg = ""
|
||||
|
||||
self.switch = switch.lower().strip()
|
||||
|
||||
self.args = (channelname.strip(), msg.strip())
|
||||
```
|
||||
|
||||
Reading the comments we see that the channel handler will send the command in a
|
||||
strange way: a string with the channel name, a colon and the actual message
|
||||
entered by the player. So if the player enters "public hello", the command
|
||||
`args` will contain `"public:hello"`. You can look at the way the channel name
|
||||
and message are parsed, this can be used in a lot of different commands.
|
||||
|
||||
Next we check if there's any switch, that is, if the message starts with a
|
||||
slash. This would be the case if a player entered `public/me jumps up and
|
||||
down`, for instance. If there is a switch, we save it in `self.switch`. We
|
||||
alter `self.args` at the end to contain a tuple with two values: the channel
|
||||
name, and the message (if a switch was used, notice that the switch will be
|
||||
stored in `self.switch`, not in the second element of `self.args`).
|
||||
|
||||
### The command func
|
||||
|
||||
Finally, let's see the `func` method in the command class. It will have to
|
||||
handle switches and also the raw message to send if no switch was used.
|
||||
|
||||
|
||||
```python
|
||||
# ...
|
||||
def func(self):
|
||||
"""
|
||||
Create a new message and send it to channel, using
|
||||
the already formatted input.
|
||||
"""
|
||||
channelkey, msg = self.args
|
||||
caller = self.caller
|
||||
channel = ChannelDB.objects.get_channel(channelkey)
|
||||
|
||||
# Check that the channel exists
|
||||
if not channel:
|
||||
self.msg(_("Channel '%s' not found.") % channelkey)
|
||||
return
|
||||
|
||||
# Check that the caller is connected
|
||||
if not channel.has_connection(caller):
|
||||
string = "You are not connected to channel '%s'."
|
||||
self.msg(string % channelkey)
|
||||
return
|
||||
|
||||
# Check that the caller has send access
|
||||
if not channel.access(caller, 'send'):
|
||||
string = "You are not permitted to send to channel '%s'."
|
||||
self.msg(string % channelkey)
|
||||
return
|
||||
|
||||
# Handle the various switches
|
||||
if self.switch == "me":
|
||||
if not msg:
|
||||
self.msg("What do you want to do on this channel?")
|
||||
else:
|
||||
msg = "{} {}".format(caller.key, msg)
|
||||
channel.msg(msg, online=True)
|
||||
elif self.switch:
|
||||
self.msg("{}: Invalid switch {}.".format(channel.key, self.switch))
|
||||
elif not msg:
|
||||
self.msg("Say what?")
|
||||
else:
|
||||
if caller in channel.mutelist:
|
||||
self.msg("You currently have %s muted." % channel)
|
||||
return
|
||||
channel.msg(msg, senders=self.caller, online=True)
|
||||
```
|
||||
|
||||
- First of all, we try to get the channel object from the channel name we have
|
||||
in the `self.args` tuple. We use `ChannelDB.objects.get_channel` this time
|
||||
because we know the channel name isn't an alias (that was part of the deal,
|
||||
`channelname` in the `parse` method contains a command key).
|
||||
- We check that the channel does exist.
|
||||
- We then check that the caller is connected to the channel. Remember, if the
|
||||
caller isn't connected, we shouldn't allow him to use this command (that
|
||||
includes the switches on channels).
|
||||
- We then check that the caller has access to the channel's `send` lock. This
|
||||
time, we make sure the caller can send messages to the channel, no matter what
|
||||
operation he's trying to perform.
|
||||
- Finally we handle switches. We try only one switch: `me`. This switch would
|
||||
be used if a player entered `public/me jumps up and down` (to do a channel
|
||||
emote).
|
||||
- We handle the case where the switch is unknown and where there's no switch
|
||||
(the player simply wants to talk on this channel).
|
||||
|
||||
The good news: The code is not too complicated by itself. The bad news is that
|
||||
this is just an abridged version of the code. If you want to handle all the
|
||||
switches mentioned in the command help, you will have more code to write. This
|
||||
is left as an exercise.
|
||||
|
||||
### End of class
|
||||
|
||||
It's almost done, but we need to add a method in this command class that isn't
|
||||
often used. I won't detail it's usage too much, just know that Evennia will use
|
||||
it and will get angry if you don't add it. So at the end of your class, just
|
||||
add:
|
||||
|
||||
```python
|
||||
# ...
|
||||
def get_extra_info(self, caller, **kwargs):
|
||||
"""
|
||||
Let users know that this command is for communicating on a channel.
|
||||
|
||||
Args:
|
||||
caller (TypedObject): A Character or Account who has entered an ambiguous command.
|
||||
|
||||
Returns:
|
||||
A string with identifying information to disambiguate the object, conventionally with a
|
||||
preceding space.
|
||||
"""
|
||||
return " (channel)"
|
||||
```
|
||||
|
||||
### Adding this channel command
|
||||
|
||||
Contrary to most Evennia commands, we won't add our `ChannelCommand` to a
|
||||
`CmdSet`. Instead we need to tell Evennia that it should use the command we
|
||||
just created instead of its default channel-command.
|
||||
|
||||
In your `server/conf/settings.py` file, add a new setting:
|
||||
|
||||
```python
|
||||
# Channel options
|
||||
CHANNEL_COMMAND_CLASS = "commands.comms.ChannelCommand"
|
||||
```
|
||||
|
||||
Then you can reload your game. Try to type `public hello` and `public/me jumps
|
||||
up and down`. Don't forget to enter `help public` to see if your command has
|
||||
truly been added.
|
||||
|
||||
## Conclusion and full code
|
||||
|
||||
That was some adventure! And there's still things to do! But hopefully, this
|
||||
tutorial will have helped you in designing your own channel system. Here are a
|
||||
few things to do:
|
||||
|
||||
- Add more switches to handle various actions, like changing the description of
|
||||
a channel for instance, or listing the connected participants.
|
||||
- Remove the default Evennia commands to handle channels.
|
||||
- Alter the behavior of the channel system so it better aligns with what you
|
||||
want to do.
|
||||
|
||||
As a special bonus, you can find a full, working example of a communication
|
||||
system similar to the one I've shown you: this is a working example, it
|
||||
integrates all switches and does ever some extra checking, but it's also very
|
||||
close from the code I've provided here. Notice, however, that this resource is
|
||||
external to Evennia and not maintained by anyone but the original author of
|
||||
this article.
|
||||
|
||||
[Read the full example on Github](https://github.com/vincent-
|
||||
lg/avenew/blob/master/commands/comms.py)
|
||||
122
docs/source/Howto/Default-Exit-Errors.md
Normal file
122
docs/source/Howto/Default-Exit-Errors.md
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
# Default Exit Errors
|
||||
|
||||
|
||||
Evennia allows for exits to have any name. The command "kitchen" is a valid exit name as well as
|
||||
"jump out the window" or "north". An exit actually consists of two parts: an [Exit Object](Objects)
|
||||
and an [Exit Command](Commands) stored on said exit object. The command has the same key and aliases
|
||||
as the object, which is why you can see the exit in the room and just write its name to traverse it.
|
||||
|
||||
If you try to enter the name of a non-existing exit, it is thus the same as trying a non-exising
|
||||
command; Evennia doesn't care about the difference:
|
||||
|
||||
> jump out the window
|
||||
Command 'jump out the window' is not available. Type "help" for help.
|
||||
|
||||
Many games don't need this type of freedom however. They define only the cardinal directions as
|
||||
valid exit names (Evennia's `@tunnel` command also offers this functionality). In this case, the
|
||||
error starts to look less logical:
|
||||
|
||||
> west
|
||||
Command 'west' is not available. Maybe you meant "@set" or "@reset"?
|
||||
|
||||
Since we for our particular game *know* that west is an exit direction, it would be better if the
|
||||
error message just told us that we couldn't go there.
|
||||
|
||||
## Adding default error commands
|
||||
|
||||
To solve this you need to be aware of how to [write and add new commands](Adding-Command-Tutorial).
|
||||
What you need to do is to create new commands for all directions you want to support in your game.
|
||||
In this example all we'll do is echo an error message, but you could certainly consider more
|
||||
advanced uses. You add these commands to the default command set. Here is an example of such a set
|
||||
of commands:
|
||||
|
||||
```python
|
||||
# for example in a file mygame/commands/movecommands.py
|
||||
|
||||
from evennia import default_cmds
|
||||
|
||||
class CmdExitError(default_cmds.MuxCommand):
|
||||
"Parent class for all exit-errors."
|
||||
locks = "cmd:all()"
|
||||
arg_regex = r"\s|$"
|
||||
auto_help = False
|
||||
def func(self):
|
||||
"returns the error"
|
||||
self.caller.msg("You cannot move %s." % self.key)
|
||||
|
||||
class CmdExitErrorNorth(CmdExitError):
|
||||
key = "north"
|
||||
aliases = ["n"]
|
||||
|
||||
class CmdExitErrorEast(CmdExitError):
|
||||
key = "east"
|
||||
aliases = ["e"]
|
||||
|
||||
class CmdExitErrorSouth(CmdExitError):
|
||||
key = "south"
|
||||
aliases = ["s"]
|
||||
|
||||
class CmdExitErrorWest(CmdExitError):
|
||||
key = "west"
|
||||
aliases = ["w"]
|
||||
```
|
||||
|
||||
Make sure to add the directional commands (not their parent) to the `CharacterCmdSet` class in
|
||||
`mygame/commands/default_cmdsets.py`:
|
||||
|
||||
```python
|
||||
# in mygame/commands/default_cmdsets.py
|
||||
|
||||
from commands import movecommands
|
||||
|
||||
# [...]
|
||||
class CharacterCmdSet(default_cmds.CharacterCmdSet):
|
||||
# [...]
|
||||
def at_cmdset_creation(self):
|
||||
# [...]
|
||||
self.add(movecommands.CmdExitErrorNorth())
|
||||
self.add(movecommands.CmdExitErrorEast())
|
||||
self.add(movecommands.CmdExitErrorSouth())
|
||||
self.add(movecommands.CmdExitErrorWest())
|
||||
```
|
||||
|
||||
After a `@reload` these commands (assuming you don't get any errors - check your log) will be
|
||||
loaded. What happens henceforth is that if you are in a room with an Exitobject (let's say it's
|
||||
"north"), the proper Exit-command will overload your error command (also named "north"). But if you
|
||||
enter an direction without having a matching exit for it, you will fallback to your default error
|
||||
commands:
|
||||
|
||||
> east
|
||||
You cannot move east.
|
||||
|
||||
Further expansions by the exit system (including manipulating the way the Exit command itself is
|
||||
created) can be done by modifying the [Exit typeclass](Typeclasses) directly.
|
||||
|
||||
## Additional Comments
|
||||
|
||||
So why didn't we create a single error command above? Something like this:
|
||||
|
||||
```python
|
||||
class CmdExitError(default_cmds.MuxCommand):
|
||||
"Handles all exit-errors."
|
||||
key = "error_cmd"
|
||||
aliases = ["north", "n",
|
||||
"east", "e",
|
||||
"south", "s",
|
||||
"west", "w"]
|
||||
#[...]
|
||||
```
|
||||
The anwer is that this would *not* work and understanding why is important in order to not be
|
||||
confused when working with commands and command sets.
|
||||
|
||||
The reason it doesn't work is because Evennia's [command system](Commands) compares commands *both*
|
||||
by `key` and by `aliases`. If *either* of those match, the two commands are considered *identical*
|
||||
as far as cmdset merging system is concerned.
|
||||
|
||||
So the above example would work fine as long as there were no Exits at all in the room. But what
|
||||
happens when we enter a room with an exit "north"? The Exit's cmdset is merged onto the default one,
|
||||
and since there is an alias match, the system determines our `CmdExitError` to be identical. It is
|
||||
thus overloaded by the Exit command (which also correctly defaults to a higher priority). The result
|
||||
is that you can go through the north exit normally but none of the error messages for the other
|
||||
directions are available since the single error command was completely overloaded by the single
|
||||
matching "north" exit-command.
|
||||
199
docs/source/Howto/Evennia-for-Diku-Users.md
Normal file
199
docs/source/Howto/Evennia-for-Diku-Users.md
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
# Evennia for Diku Users
|
||||
|
||||
|
||||
Evennia represents a learning curve for those who used to code on
|
||||
[Diku](https://en.wikipedia.org/wiki/DikuMUD) type MUDs. While coding in Python is easy if you
|
||||
already know C, the main effort is to get rid of old C programming habits. Trying to code Python the
|
||||
way you code C will not only look ugly, it will lead to less optimal and harder to maintain code.
|
||||
Reading Evennia example code is a good way to get a feel for how different problems are approached
|
||||
in Python.
|
||||
|
||||
Overall, Python offers an extensive library of resources, safe memory management and excellent
|
||||
handling of errors. While Python code does not run as fast as raw C code does, the difference is not
|
||||
all that important for a text-based game. The main advantage of Python is an extremely fast
|
||||
development cycle with and easy ways to create game systems that would take many times more code and
|
||||
be much harder to make stable and maintainable in C.
|
||||
|
||||
### Core Differences
|
||||
|
||||
- As mentioned, the main difference between Evennia and a Diku-derived codebase is that Evennia is
|
||||
written purely in Python. Since Python is an interpreted language there is no compile stage. It is
|
||||
modified and extended by the server loading Python modules at run-time. It also runs on all computer
|
||||
platforms Python runs on (which is basically everywhere).
|
||||
- Vanilla Diku type engines save their data in custom *flat file* type storage solutions. By
|
||||
contrast, Evennia stores all game data in one of several supported SQL databases. Whereas flat files
|
||||
have the advantage of being easier to implement, they (normally) lack many expected safety features
|
||||
and ways to effectively extract subsets of the stored data. For example, if the server loses power
|
||||
while writing to a flatfile it may become corrupt and the data lost. A proper database solution is
|
||||
not susceptible to this - at no point is the data in a state where it cannot be recovered. Databases
|
||||
are also highly optimized for querying large data sets efficiently.
|
||||
|
||||
### Some Familiar Things
|
||||
|
||||
Diku expresses the character object referenced normally by:
|
||||
|
||||
`struct char ch*` then all character-related fields can be accessed by `ch->`. In Evennia, one must
|
||||
pay attention to what object you are using, and when you are accessing another through back-
|
||||
handling, that you are accessing the right object. In Diku C, accessing character object is normally
|
||||
done by:
|
||||
|
||||
```c
|
||||
/* creating pointer of both character and room struct */
|
||||
|
||||
void(struct char ch*, struct room room*){
|
||||
int dam;
|
||||
if (ROOM_FLAGGED(room, ROOM_LAVA)){
|
||||
dam = 100
|
||||
ch->damage_taken = dam
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
As an example for creating Commands in Evennia via the `from evennia import Command` the character
|
||||
object that calls the command is denoted by a class property as `self.caller`. In this example
|
||||
`self.caller` is essentially the 'object' that has called the Command, but most of the time it is an
|
||||
Account object. For a more familiar Diku feel, create a variable that becomes the account object as:
|
||||
|
||||
```python
|
||||
#mygame/commands/command.py
|
||||
|
||||
from evennia import Command
|
||||
|
||||
class CmdMyCmd(Command):
|
||||
"""
|
||||
This is a Command Evennia Object
|
||||
"""
|
||||
|
||||
[...]
|
||||
|
||||
def func(self):
|
||||
ch = self.caller
|
||||
# then you can access the account object directly by using the familiar ch.
|
||||
ch.msg("...")
|
||||
account_name = ch.name
|
||||
race = ch.db.race
|
||||
|
||||
```
|
||||
|
||||
As mentioned above, care must be taken what specific object you are working with. If focused on a
|
||||
room object and you need to access the account object:
|
||||
|
||||
```python
|
||||
#mygame/typeclasses/room.py
|
||||
|
||||
from evennia import DefaultRoom
|
||||
|
||||
class MyRoom(DefaultRoom):
|
||||
[...]
|
||||
|
||||
def is_account_object(self, object):
|
||||
# a test to see if object is an account
|
||||
[...]
|
||||
|
||||
def myMethod(self):
|
||||
#self.caller would not make any sense, since self refers to the
|
||||
# object of 'DefaultRoom', you must find the character obj first:
|
||||
for ch in self.contents:
|
||||
if self.is_account_object(ch):
|
||||
# now you can access the account object with ch:
|
||||
account_name = ch.name
|
||||
race = ch.db.race
|
||||
```
|
||||
|
||||
|
||||
## Emulating Evennia to Look and Feel Like A Diku/ROM
|
||||
|
||||
To emulate a Diku Mud on Evennia some work has to be done before hand. If there is anything that all
|
||||
coders and builders remember from Diku/Rom days is the presence of VNUMs. Essentially all data was
|
||||
saved in flat files and indexed by VNUMs for easy access. Evennia has the ability to emulate VNUMS
|
||||
to the extent of categorising rooms/mobs/objs/trigger/zones[...] into vnum ranges.
|
||||
|
||||
Evennia has objects that are called Scripts. As defined, they are the 'out of game' instances that
|
||||
exist within the mud, but never directly interacted with. Scripts can be used for timers, mob AI,
|
||||
and even a stand alone databases.
|
||||
|
||||
Because of their wonderful structure all mob, room, zone, triggers, etc.. data can be saved in
|
||||
independently created global scripts.
|
||||
|
||||
Here is a sample mob file from a Diku Derived flat file.
|
||||
|
||||
```text
|
||||
#0
|
||||
mob0~
|
||||
mob0~
|
||||
mob0
|
||||
~
|
||||
Mob0
|
||||
~
|
||||
10 0 0 0 0 0 0 0 0 E
|
||||
1 20 9 0d0+10 1d2+0
|
||||
10 100
|
||||
8 8 0
|
||||
E
|
||||
#1
|
||||
Puff dragon fractal~
|
||||
Puff~
|
||||
Puff the Fractal Dragon is here, contemplating a higher reality.
|
||||
~
|
||||
Is that some type of differential curve involving some strange, and unknown
|
||||
calculus that she seems to be made out of?
|
||||
~
|
||||
516106 0 0 0 2128 0 0 0 1000 E
|
||||
34 9 -10 6d6+340 5d5+5
|
||||
340 115600
|
||||
8 8 2
|
||||
BareHandAttack: 12
|
||||
E
|
||||
T 95
|
||||
```
|
||||
Each line represents something that the MUD reads in and does something with it. This isn't easy to
|
||||
read, but let's see if we can emulate this as a dictionary to be stored on a database script created
|
||||
in Evennia.
|
||||
|
||||
First, let's create a global script that does absolutely nothing and isn't attached to anything. You
|
||||
can either create this directly in-game with the @py command or create it in another file to do some
|
||||
checks and balances if for whatever reason the script needs to be created again. Progmatically it
|
||||
can be done like so:
|
||||
|
||||
```python
|
||||
from evennia import create_script
|
||||
|
||||
mob_db = create_script("typeclasses.scripts.DefaultScript", key="mobdb",
|
||||
persistent=True, obj=None)
|
||||
mob_db.db.vnums = {}
|
||||
```
|
||||
Just by creating a simple script object and assigning it a 'vnums' attribute as a type dictionary.
|
||||
Next we have to create the mob layout..
|
||||
|
||||
```python
|
||||
# vnum : mob_data
|
||||
|
||||
mob_vnum_1 = {
|
||||
'key' : 'puff',
|
||||
'sdesc' : 'puff the fractal dragon',
|
||||
'ldesc' : 'Puff the Fractal Dragon is here, ' \
|
||||
'contemplating a higher reality.',
|
||||
'ddesc' : ' Is that some type of differential curve ' \
|
||||
'involving some strange, and unknown calculus ' \
|
||||
'that she seems to be made out of?',
|
||||
[...]
|
||||
}
|
||||
|
||||
# Then saving it to the data, assuming you have the script obj stored in a variable.
|
||||
mob_db.db.vnums[1] = mob_vnum_1
|
||||
```
|
||||
|
||||
This is a very 'caveman' example, but it gets the idea across. You can use the keys in the
|
||||
`mob_db.vnums` to act as the mob vnum while the rest contains the data..
|
||||
|
||||
Much simpler to read and edit. If you plan on taking this route, you must keep in mind that by
|
||||
default evennia 'looks' at different properties when using the `look` command for instance. If you
|
||||
create an instance of this mob and make its `self.key = 1`, by default evennia will say
|
||||
|
||||
`Here is : 1`
|
||||
|
||||
You must restructure all default commands so that the mud looks at different properties defined on
|
||||
your mob.
|
||||
|
||||
|
||||
|
||||
222
docs/source/Howto/Evennia-for-MUSH-Users.md
Normal file
222
docs/source/Howto/Evennia-for-MUSH-Users.md
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
# Evennia for MUSH Users
|
||||
|
||||
*This page is adopted from an article originally posted for the MUSH community [here on
|
||||
musoapbox.net](http://musoapbox.net/topic/1150/evennia-for-mushers).*
|
||||
|
||||
[MUSH](https://en.wikipedia.org/wiki/MUSH)es are text multiplayer games traditionally used for
|
||||
heavily roleplay-focused game styles. They are often (but not always) utilizing game masters and
|
||||
human oversight over code automation. MUSHes are traditionally built on the TinyMUSH-family of game
|
||||
servers, like PennMUSH, TinyMUSH, TinyMUX and RhostMUSH. Also their siblings
|
||||
[MUCK](https://en.wikipedia.org/wiki/TinyMUCK) and [MOO](https://en.wikipedia.org/wiki/MOO) are
|
||||
often mentioned together with MUSH since they all inherit from the same
|
||||
[TinyMUD](https://en.wikipedia.org/wiki/MUD_trees#TinyMUD_family_tree) base. A major feature is the
|
||||
ability to modify and program the game world from inside the game by using a custom scripting
|
||||
language. We will refer to this online scripting as *softcode* here.
|
||||
|
||||
Evennia works quite differently from a MUSH both in its overall design and under the hood. The same
|
||||
things are achievable, just in a different way. Here are some fundamental differences to keep in
|
||||
mind if you are coming from the MUSH world.
|
||||
|
||||
## Developers vs Players
|
||||
|
||||
In MUSH, users tend to code and expand all aspects of the game from inside it using softcode. A MUSH
|
||||
can thus be said to be managed solely by *Players* with different levels of access. Evennia on the
|
||||
other hand, differentiates between the role of the *Player* and the *Developer*.
|
||||
|
||||
- An Evennia *Developer* works in Python from *outside* the game, in what MUSH would consider
|
||||
“hardcode”. Developers implement larger-scale code changes and can fundamentally change how the game
|
||||
works. They then load their changes into the running Evennia server. Such changes will usually not
|
||||
drop any connected players.
|
||||
- An Evennia *Player* operates from *inside* the game. Some staff-level players are likely to double
|
||||
as developers. Depending on access level, players can modify and expand the game's world by digging
|
||||
new rooms, creating new objects, alias commands, customize their experience and so on. Trusted staff
|
||||
may get access to Python via the `@py` command, but this would be a security risk for normal Players
|
||||
to use. So the *Player* usually operates by making use of the tools prepared for them by the
|
||||
*Developer* - tools that can be as rigid or flexible as the developer desires.
|
||||
|
||||
## Collaborating on a game - Python vs Softcode
|
||||
|
||||
For a *Player*, collaborating on a game need not be too different between MUSH and Evennia. The
|
||||
building and description of the game world can still happen mostly in-game using build commands,
|
||||
using text tags and [inline functions](TextTags#inline-functions) to prettify and customize the
|
||||
experience. Evennia offers external ways to build a world but those are optional. There is also
|
||||
nothing *in principle* stopping a Developer from offering a softcode-like language to Players if
|
||||
that is deemed necessary.
|
||||
|
||||
For *Developers* of the game, the difference is larger: Code is mainly written outside the game in
|
||||
Python modules rather than in-game on the command line. Python is a very popular and well-supported
|
||||
language with tons of documentation and help to be found. The Python standard library is also a
|
||||
great help for not having to reinvent the wheel. But that said, while Python is considered one of
|
||||
the easier languages to learn and use it is undoubtedly very different from MUSH softcode.
|
||||
|
||||
While softcode allows collaboration in-game, Evennia's external coding instead opens up the
|
||||
possibility for collaboration using professional version control tools and bug tracking using
|
||||
websites like github (or bitbucket for a free private repo). Source code can be written in proper
|
||||
text editors and IDEs with refactoring, syntax highlighting and all other conveniences. In short,
|
||||
collaborative development of an Evennia game is done in the same way most professional collaborative
|
||||
development is done in the world, meaning all the best tools can be used.
|
||||
|
||||
|
||||
## `@parent` vs `@typeclass` and `@spawn`
|
||||
|
||||
Inheritance works differently in Python than in softcode. Evennia has no concept of a "master
|
||||
object" that other objects inherit from. There is in fact no reason at all to introduce "virtual
|
||||
objects" in the game world - code and data are kept separate from one another.
|
||||
|
||||
In Python (which is an [object oriented](https://en.wikipedia.org/wiki/Object-oriented_programming)
|
||||
language) one instead creates *classes* - these are like blueprints from which you spawn any number
|
||||
of *object instances*. Evennia also adds the extra feature that every instance is persistent in the
|
||||
database (this means no SQL is ever needed). To take one example, a unique character in Evennia is
|
||||
an instances of the class `Character`.
|
||||
|
||||
One parallel to MUSH's `@parent` command may be Evennia's `@typeclass` command, which changes which
|
||||
class an already existing object is an instance of. This way you can literally turn a `Character`
|
||||
into a `Flowerpot` on the spot.
|
||||
|
||||
if you are new to object oriented design it's important to note that all object instances of a class
|
||||
does *not* have to be identical. If they did, all Characters would be named the same. Evennia allows
|
||||
to customize individual objects in many different ways. One way is through *Attributes*, which are
|
||||
database-bound properties that can be linked to any object. For example, you could have an `Orc`
|
||||
class that defines all the stuff an Orc should be able to do (probably in turn inheriting from some
|
||||
`Monster` class shared by all monsters). Setting different Attributes on different instances
|
||||
(different strength, equipment, looks etc) would make each Orc unique despite all sharing the same
|
||||
class.
|
||||
|
||||
The `@spawn` command allows one to conveniently choose between different "sets" of Attributes to
|
||||
put on each new Orc (like the "warrior" set or "shaman" set) . Such sets can even inherit one
|
||||
another which is again somewhat remniscent at least of the *effect* of `@parent` and the object-
|
||||
based inheritance of MUSH.
|
||||
|
||||
There are other differences for sure, but that should give some feel for things. Enough with the
|
||||
theory. Let's get down to more practical matters next. To install, see the [Getting Started
|
||||
instructions](Getting-Started).
|
||||
|
||||
## A first step making things more familiar
|
||||
|
||||
We will here give two examples of customizing Evennia to be more familiar to a MUSH *Player*.
|
||||
|
||||
### Activating a multi-descer
|
||||
|
||||
By default Evennia’s `desc` command updates your description and that’s it. There is a more feature-
|
||||
rich optional “multi-descer” in `evennia/contrib/multidesc.py` though. This alternative allows for
|
||||
managing and combining a multitude of keyed descriptions.
|
||||
|
||||
To activate the multi-descer, `cd` to your game folder and into the `commands` sub-folder. There
|
||||
you’ll find the file `default_cmdsets.py`. In Python lingo all `*.py` files are called *modules*.
|
||||
Open the module in a text editor. We won’t go into Evennia in-game *Commands* and *Command sets*
|
||||
further here, but suffice to say Evennia allows you to change which commands (or versions of
|
||||
commands) are available to the player from moment to moment depending on circumstance.
|
||||
|
||||
Add two new lines to the module as seen below:
|
||||
|
||||
```python
|
||||
# the file mygame/commands/default_cmdsets.py
|
||||
# [...]
|
||||
|
||||
from evennia.contrib import multidescer # <- added now
|
||||
|
||||
class CharacterCmdSet(default_cmds.CharacterCmdSet):
|
||||
"""
|
||||
The CharacterCmdSet contains general in-game commands like look,
|
||||
get etc available on in-game Character objects. It is merged with
|
||||
the AccountCmdSet when an Account puppets a Character.
|
||||
"""
|
||||
key = "DefaultCharacter"
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
"""
|
||||
Populates the cmdset
|
||||
"""
|
||||
super().at_cmdset_creation()
|
||||
#
|
||||
# any commands you add below will overload the default ones.
|
||||
#
|
||||
self.add(multidescer.CmdMultiDesc()) # <- added now
|
||||
# [...]
|
||||
```
|
||||
|
||||
Note that Python cares about indentation, so make sure to indent with the same number of spaces as
|
||||
shown above!
|
||||
|
||||
So what happens above? We [import the
|
||||
module](http://www.linuxtopia.org/online_books/programming_books/python_programming/python_ch28s03.html)
|
||||
`evennia/contrib/multidescer.py` at the top. Once imported we can access stuff inside that module
|
||||
using full stop (`.`). The multidescer is defined as a class `CmdMultiDesc` (we could find this out
|
||||
by opening said module in a text editor). At the bottom we create a new instance of this class and
|
||||
add it to the `CharacterCmdSet` class. For the sake of this tutorial we only need to know that
|
||||
`CharacterCmdSet` contains all commands that should be be available to the `Character` by default.
|
||||
|
||||
This whole thing will be triggered when the command set is first created, which happens on server
|
||||
start. So we need to reload Evennia with `@reload` - no one will be disconnected by doing this. If
|
||||
all went well you should now be able to use `desc` (or `+desc`) and find that you have more
|
||||
possibilities:
|
||||
|
||||
```text
|
||||
> help +desc # get help on the command
|
||||
> +desc eyes = His eyes are blue.
|
||||
> +desc basic = A big guy.
|
||||
> +desc/set basic + + eyes # we add an extra space between
|
||||
> look me
|
||||
A big guy. His eyes are blue.
|
||||
```
|
||||
|
||||
If there are errors, a *traceback* will show in the server log - several lines of text showing
|
||||
where the error occurred. Find where the error is by locating the line number related to the
|
||||
`default_cmdsets.py` file (it's the only one you've changed so far). Most likely you mis-spelled
|
||||
something or missed the indentation. Fix it and either `@reload` again or run `evennia start` as
|
||||
needed.
|
||||
|
||||
### Customizing the multidescer syntax
|
||||
|
||||
As seen above the multidescer uses syntax like this (where `|/` are Evennia's tags for line breaks)
|
||||
:
|
||||
|
||||
```text
|
||||
> +desc/set basic + |/|/ + cape + footwear + |/|/ + attitude
|
||||
```
|
||||
|
||||
This use of `+ ` was prescribed by the *Developer* that coded this `+desc` command. What if the
|
||||
*Player* doesn’t like this syntax though? Do players need to pester the dev to change it? Not
|
||||
necessarily. While Evennia does not allow the player to build their own multi-descer on the command
|
||||
line, it does allow for *re-mapping* the command syntax to one they prefer. This is done using the
|
||||
`nick` command.
|
||||
|
||||
Here’s a nick that changes how to input the command above:
|
||||
|
||||
```text
|
||||
> nick setdesc $1 $2 $3 $4 = +desc/set $1 + |/|/ + $2 + $3 + |/|/ + $4
|
||||
```
|
||||
|
||||
The string on the left will be matched against your input and if matching, it will be replaced with
|
||||
the string on the right. The `$`-type tags will store space-separated arguments and put them into
|
||||
the replacement. The nick allows [shell-like wildcards](http://www.linfo.org/wildcard.html), so you
|
||||
can use `*`, `?`, `[...]`, `[!...]` etc to match parts of the input.
|
||||
|
||||
The same description as before can now be set as
|
||||
|
||||
```text
|
||||
> setdesc basic cape footwear attitude
|
||||
```
|
||||
|
||||
With the `nick` functionality players can mitigate a lot of syntax dislikes even without the
|
||||
developer changing the underlying Python code.
|
||||
|
||||
## Next steps
|
||||
|
||||
If you are a *Developer* and are interested in making a more MUSH-like Evennia game, a good start is
|
||||
to look into the Evennia [Tutorial for a first MUSH-like game](Tutorial-for-basic-MUSH-like-game).
|
||||
That steps through building a simple little game from scratch and helps to acquaint you with the
|
||||
various corners of Evennia. There is also the [Tutorial for running roleplaying sessions](Evennia-
|
||||
for-roleplaying-sessions) that can be of interest.
|
||||
|
||||
An important aspect of making things more familiar for *Players* is adding new and tweaking existing
|
||||
commands. How this is done is covered by the [Tutorial on adding new commands](Adding-Command-
|
||||
Tutorial). You may also find it useful to shop through the `evennia/contrib/` folder. The [Tutorial
|
||||
world](Tutorial-World-Introduction) is a small single-player quest you can try (it’s not very MUSH-
|
||||
like but it does show many Evennia concepts in action). Beyond that there are [many more
|
||||
tutorials](Tutorials) to try out. If you feel you want a more visual overview you can also look at
|
||||
[Evennia in pictures](https://evennia.blogspot.se/2016/05/evennia-in-pictures.html).
|
||||
|
||||
… And of course, if you need further help you can always drop into the [Evennia
|
||||
chatroom](http://webchat.freenode.net/?channels=evennia&uio=MT1mYWxzZSY5PXRydWUmMTE9MTk1JjEyPXRydWUbb)
|
||||
or post a question in our [forum/mailing list](https://groups.google.com/forum/#%21forum/evennia)!
|
||||
732
docs/source/Howto/Evennia-for-roleplaying-sessions.md
Normal file
732
docs/source/Howto/Evennia-for-roleplaying-sessions.md
Normal file
|
|
@ -0,0 +1,732 @@
|
|||
# Evennia for roleplaying sessions
|
||||
|
||||
This tutorial will explain how to set up a realtime or play-by-post tabletop style game using a
|
||||
fresh Evennia server.
|
||||
|
||||
The scenario is thus: You and a bunch of friends want to play a tabletop role playing game online.
|
||||
One of you will be the game master and you are all okay with playing using written text. You want
|
||||
both the ability to role play in real-time (when people happen to be online at the same time) as
|
||||
well as the ability for people to post when they can and catch up on what happened since they were
|
||||
last online.
|
||||
|
||||
This is the functionality we will be needing and using:
|
||||
|
||||
* The ability to make one of you the *GM* (game master), with special abilities.
|
||||
* A *Character sheet* that players can create, view and fill in. It can also be locked so only the
|
||||
GM can modify it.
|
||||
* A *dice roller* mechanism, for whatever type of dice the RPG rules require.
|
||||
* *Rooms*, to give a sense of location and to compartmentalize play going on- This means both
|
||||
Character movements from location to location and GM explicitly moving them around.
|
||||
* *Channels*, for easily sending text to all subscribing accounts, regardless of location.
|
||||
* Account-to-Account *messaging* capability, including sending to multiple recipients
|
||||
simultaneously, regardless of location.
|
||||
|
||||
We will find most of these things are already part of vanilla Evennia, but that we can expand on the
|
||||
defaults for our particular use-case. Below we will flesh out these components from start to finish.
|
||||
|
||||
## Starting out
|
||||
|
||||
We will assume you start from scratch. You need Evennia installed, as per the [Getting
|
||||
Started](Getting-Started) instructions. Initialize a new game directory with `evennia init
|
||||
<gamedirname>`. In this tutorial we assume your game dir is simply named `mygame`. You can use the
|
||||
default database and keep all other settings to default for now. Familiarize yourself with the
|
||||
`mygame` folder before continuing. You might want to browse the [First Steps Coding](First-Steps-
|
||||
Coding) tutorial, just to see roughly where things are modified.
|
||||
|
||||
## The Game Master role
|
||||
|
||||
In brief:
|
||||
|
||||
* Simplest way: Being an admin, just give one account `Admins` permission using the standard `@perm`
|
||||
command.
|
||||
* Better but more work: Make a custom command to set/unset the above, while tweaking the Character
|
||||
to show your renewed GM status to the other accounts.
|
||||
|
||||
### The permission hierarchy
|
||||
|
||||
Evennia has the following [permission hierarchy](Building-Permissions#assigning-permissions) out of
|
||||
the box: *Players, Helpers, Builders, Admins* and finally *Developers*. We could change these but
|
||||
then we'd need to update our Default commands to use the changes. We want to keep this simple, so
|
||||
instead we map our different roles on top of this permission ladder.
|
||||
|
||||
1. `Players` is the permission set on normal players. This is the default for anyone creating a new
|
||||
account on the server.
|
||||
2. `Helpers` are like `Players` except they also have the ability to create/edit new help entries.
|
||||
This could be granted to players who are willing to help with writing lore or custom logs for
|
||||
everyone.
|
||||
3. `Builders` is not used in our case since the GM should be the only world-builder.
|
||||
4. `Admins` is the permission level the GM should have. Admins can do everything builders can
|
||||
(create/describe rooms etc) but also kick accounts, rename them and things like that.
|
||||
5. `Developers`-level permission are the server administrators, the ones with the ability to
|
||||
restart/shutdown the server as well as changing the permission levels.
|
||||
|
||||
> The [superuser](Building-Permissions#the-super-user) is not part of the hierarchy and actually
|
||||
completely bypasses it. We'll assume server admin(s) will "just" be Developers.
|
||||
|
||||
### How to grant permissions
|
||||
|
||||
Only `Developers` can (by default) change permission level. Only they have access to the `@perm`
|
||||
command:
|
||||
|
||||
```
|
||||
> @perm Yvonne
|
||||
Permissions on Yvonne: accounts
|
||||
|
||||
> @perm Yvonne = Admins
|
||||
> @perm Yvonne
|
||||
Permissions on Yvonne: accounts, admins
|
||||
|
||||
> @perm/del Yvonne = Admins
|
||||
> @perm Yvonne
|
||||
Permissions on Yvonne: accounts
|
||||
```
|
||||
|
||||
There is no need to remove the basic `Players` permission when adding the higher permission: the
|
||||
highest will be used. Permission level names are *not* case sensitive. You can also use both plural
|
||||
and singular, so "Admins" gives the same powers as "Admin".
|
||||
|
||||
|
||||
### Optional: Making a GM-granting command
|
||||
|
||||
Use of `@perm` works out of the box, but it's really the bare minimum. Would it not be nice if other
|
||||
accounts could tell at a glance who the GM is? Also, we shouldn't really need to remember that the
|
||||
permission level is called "Admins". It would be easier if we could just do `@gm <account>` and
|
||||
`@notgm <account>` and at the same time change something make the new GM status apparent.
|
||||
|
||||
So let's make this possible. This is what we'll do:
|
||||
|
||||
1. We'll customize the default Character class. If an object of this class has a particular flag,
|
||||
its name will have the string`(GM)` added to the end.
|
||||
2. We'll add a new command, for the server admin to assign the GM-flag properly.
|
||||
|
||||
#### Character modification
|
||||
|
||||
Let's first start by customizing the Character. We recommend you browse the beginning of the
|
||||
[Account](Accounts) page to make sure you know how Evennia differentiates between the OOC "Account
|
||||
objects" (not to be confused with the `Accounts` permission, which is just a string specifying your
|
||||
access) and the IC "Character objects".
|
||||
|
||||
Open `mygame/typeclasses/characters.py` and modify the default `Character` class:
|
||||
|
||||
```python
|
||||
# in mygame/typeclasses/characters.py
|
||||
|
||||
# [...]
|
||||
|
||||
class Character(DefaultCharacter):
|
||||
# [...]
|
||||
def get_display_name(self, looker, **kwargs):
|
||||
"""
|
||||
This method customizes how character names are displayed. We assume
|
||||
only permissions of types "Developers" and "Admins" require
|
||||
special attention.
|
||||
"""
|
||||
name = self.key
|
||||
selfaccount = self.account # will be None if we are not puppeted
|
||||
lookaccount = looker.account # - " -
|
||||
|
||||
if selfaccount and selfaccount.db.is_gm:
|
||||
# A GM. Show name as name(GM)
|
||||
name = "%s(GM)" % name
|
||||
|
||||
if lookaccount and \
|
||||
(lookaccount.permissions.get("Developers") or lookaccount.db.is_gm):
|
||||
# Developers/GMs see name(#dbref) or name(GM)(#dbref)
|
||||
return "%s(#%s)" % (name, self.id)
|
||||
else:
|
||||
return name
|
||||
|
||||
```
|
||||
|
||||
Above, we change how the Character's name is displayed: If the account controlling this Character is
|
||||
a GM, we attach the string `(GM)` to the Character's name so everyone can tell who's the boss. If we
|
||||
ourselves are Developers or GM's we will see database ids attached to Characters names, which can
|
||||
help if doing database searches against Characters of exactly the same name. We base the "gm-
|
||||
ingness" on having an flag (an [Attribute](Attributes)) named `is_gm`. We'll make sure new GM's
|
||||
actually get this flag below.
|
||||
|
||||
> **Extra exercise:** This will only show the `(GM)` text on *Characters* puppeted by a GM account,
|
||||
that is, it will show only to those in the same location. If we wanted it to also pop up in, say,
|
||||
`who` listings and channels, we'd need to make a similar change to the `Account` typeclass in
|
||||
`mygame/typeclasses/accounts.py`. We leave this as an exercise to the reader.
|
||||
|
||||
#### New @gm/@ungm command
|
||||
|
||||
We will describe in some detail how to create and add an Evennia [command](Commands) here with the
|
||||
hope that we don't need to be as detailed when adding commands in the future. We will build on
|
||||
Evennia's default "mux-like" commands here.
|
||||
|
||||
Open `mygame/commands/command.py` and add a new Command class at the bottom:
|
||||
|
||||
```python
|
||||
# in mygame/commands/command.py
|
||||
|
||||
from evennia import default_cmds
|
||||
|
||||
# [...]
|
||||
|
||||
import evennia
|
||||
|
||||
class CmdMakeGM(default_cmds.MuxCommand):
|
||||
"""
|
||||
Change an account's GM status
|
||||
|
||||
Usage:
|
||||
@gm <account>
|
||||
@ungm <account>
|
||||
|
||||
"""
|
||||
# note using the key without @ means both @gm !gm etc will work
|
||||
key = "gm"
|
||||
aliases = "ungm"
|
||||
locks = "cmd:perm(Developers)"
|
||||
help_category = "RP"
|
||||
|
||||
def func(self):
|
||||
"Implement the command"
|
||||
caller = self.caller
|
||||
|
||||
if not self.args:
|
||||
caller.msg("Usage: @gm account or @ungm account")
|
||||
return
|
||||
|
||||
accountlist = evennia.search_account(self.args) # returns a list
|
||||
if not accountlist:
|
||||
caller.msg("Could not find account '%s'" % self.args)
|
||||
return
|
||||
elif len(accountlist) > 1:
|
||||
caller.msg("Multiple matches for '%s': %s" % (self.args, accountlist))
|
||||
return
|
||||
else:
|
||||
account = accountlist[0]
|
||||
|
||||
if self.cmdstring == "gm":
|
||||
# turn someone into a GM
|
||||
if account.permissions.get("Admins"):
|
||||
caller.msg("Account %s is already a GM." % account)
|
||||
else:
|
||||
account.permissions.add("Admins")
|
||||
caller.msg("Account %s is now a GM." % account)
|
||||
account.msg("You are now a GM (changed by %s)." % caller)
|
||||
account.character.db.is_gm = True
|
||||
else:
|
||||
# @ungm was entered - revoke GM status from someone
|
||||
if not account.permissions.get("Admins"):
|
||||
caller.msg("Account %s is not a GM." % account)
|
||||
else:
|
||||
account.permissions.remove("Admins")
|
||||
caller.msg("Account %s is no longer a GM." % account)
|
||||
account.msg("You are no longer a GM (changed by %s)." % caller)
|
||||
del account.character.db.is_gm
|
||||
|
||||
```
|
||||
|
||||
All the command does is to locate the account target and assign it the `Admins` permission if we
|
||||
used `@gm` or revoke it if using the `@ungm` alias. We also set/unset the `is_gm` Attribute that is
|
||||
expected by our new `Character.get_display_name` method from earlier.
|
||||
|
||||
> We could have made this into two separate commands or opted for a syntax like `@gm/revoke
|
||||
<accountname>`. Instead we examine how this command was called (stored in `self.cmdstring`) in order
|
||||
to act accordingly. Either way works, practicality and coding style decides which to go with.
|
||||
|
||||
To actually make this command available (only to Developers, due to the lock on it), we add it to
|
||||
the default Account command set. Open the file `mygame/commands/default_cmdsets.py` and find the
|
||||
`AccountCmdSet` class:
|
||||
|
||||
```python
|
||||
# mygame/commands/default_cmdsets.py
|
||||
|
||||
# [...]
|
||||
from commands.command import CmdMakeGM
|
||||
|
||||
class AccountCmdSet(default_cmds.AccountCmdSet):
|
||||
# [...]
|
||||
def at_cmdset_creation(self):
|
||||
# [...]
|
||||
self.add(CmdMakeGM())
|
||||
|
||||
```
|
||||
|
||||
Finally, issue the `@reload` command to update the server to your changes. Developer-level players
|
||||
(or the superuser) should now have the `@gm/@ungm` command available.
|
||||
|
||||
## Character sheet
|
||||
|
||||
In brief:
|
||||
|
||||
* Use Evennia's EvTable/EvForm to build a Character sheet
|
||||
* Tie individual sheets to a given Character.
|
||||
* Add new commands to modify the Character sheet, both by Accounts and GMs.
|
||||
* Make the Character sheet lockable by a GM, so the Player can no longer modify it.
|
||||
|
||||
### Building a Character sheet
|
||||
|
||||
There are many ways to build a Character sheet in text, from manually pasting strings together to
|
||||
more automated ways. Exactly what is the best/easiest way depends on the sheet one tries to create.
|
||||
We will here show two examples using the *EvTable* and *EvForm* utilities.Later we will create
|
||||
Commands to edit and display the output from those utilities.
|
||||
|
||||
> Note that due to the limitations of the wiki, no color is used in any of the examples. See [the
|
||||
text tag documentation](TextTags) for how to add color to the tables and forms.
|
||||
|
||||
#### Making a sheet with EvTable
|
||||
|
||||
[EvTable](github:evennia.utils.evtable) is a text-table generator. It helps with displaying text in
|
||||
ordered rows and columns. This is an example of using it in code:
|
||||
|
||||
````python
|
||||
# this can be tried out in a Python shell like iPython
|
||||
|
||||
from evennia.utils import evtable
|
||||
|
||||
# we hardcode these for now, we'll get them as input later
|
||||
STR, CON, DEX, INT, WIS, CHA = 12, 13, 8, 10, 9, 13
|
||||
|
||||
table = evtable.EvTable("Attr", "Value",
|
||||
table = [
|
||||
["STR", "CON", "DEX", "INT", "WIS", "CHA"],
|
||||
[STR, CON, DEX, INT, WIS, CHA]
|
||||
], align='r', border="incols")
|
||||
````
|
||||
|
||||
Above, we create a two-column table by supplying the two columns directly. We also tell the table to
|
||||
be right-aligned and to use the "incols" border type (borders drawns only in between columns). The
|
||||
`EvTable` class takes a lot of arguments for customizing its look, you can see [some of the possible
|
||||
keyword arguments here](github:evennia.utils.evtable#evtable__init__). Once you have the `table` you
|
||||
could also retroactively add new columns and rows to it with `table.add_row()` and
|
||||
`table.add_column()`: if necessary the table will expand with empty rows/columns to always remain
|
||||
rectangular.
|
||||
|
||||
The result from printing the above table will be
|
||||
|
||||
```python
|
||||
table_string = str(table)
|
||||
|
||||
print(table_string)
|
||||
|
||||
Attr | Value
|
||||
~~~~~~+~~~~~~~
|
||||
STR | 12
|
||||
CON | 13
|
||||
DEX | 8
|
||||
INT | 10
|
||||
WIS | 9
|
||||
CHA | 13
|
||||
```
|
||||
|
||||
This is a minimalistic but effective Character sheet. By combining the `table_string` with other
|
||||
strings one could build up a reasonably full graphical representation of a Character. For more
|
||||
advanced layouts we'll look into EvForm next.
|
||||
|
||||
#### Making a sheet with EvForm
|
||||
|
||||
[EvForm](github:evennia.utils.evform) allows the creation of a two-dimensional "graphic" made by
|
||||
text characters. On this surface, one marks and tags rectangular regions ("cells") to be filled with
|
||||
content. This content can be either normal strings or `EvTable` instances (see the previous section,
|
||||
one such instance would be the `table` variable in that example).
|
||||
|
||||
In the case of a Character sheet, these cells would be comparable to a line or box where you could
|
||||
enter the name of your character or their strength score. EvMenu also easily allows to update the
|
||||
content of those fields in code (it use EvTables so you rebuild the table first before re-sending it
|
||||
to EvForm).
|
||||
|
||||
The drawback of EvForm is that its shape is static; if you try to put more text in a region than it
|
||||
was sized for, the text will be cropped. Similarly, if you try to put an EvTable instance in a field
|
||||
too small for it, the EvTable will do its best to try to resize to fit, but will eventually resort
|
||||
to cropping its data or even give an error if too small to fit any data.
|
||||
|
||||
An EvForm is defined in a Python module. Create a new file `mygame/world/charsheetform.py` and
|
||||
modify it thus:
|
||||
|
||||
````python
|
||||
#coding=utf-8
|
||||
|
||||
# in mygame/world/charsheetform.py
|
||||
|
||||
FORMCHAR = "x"
|
||||
TABLECHAR = "c"
|
||||
|
||||
FORM = """
|
||||
.--------------------------------------.
|
||||
| |
|
||||
| Name: xxxxxxxxxxxxxx1xxxxxxxxxxxxxxx |
|
||||
| xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx |
|
||||
| |
|
||||
>------------------------------------<
|
||||
| |
|
||||
| ccccccccccc Advantages: |
|
||||
| ccccccccccc xxxxxxxxxxxxxxxxxxxxxx |
|
||||
| ccccccccccc xxxxxxxxxx3xxxxxxxxxxx |
|
||||
| ccccccccccc xxxxxxxxxxxxxxxxxxxxxx |
|
||||
| ccccc2ccccc Disadvantages: |
|
||||
| ccccccccccc xxxxxxxxxxxxxxxxxxxxxx |
|
||||
| ccccccccccc xxxxxxxxxx4xxxxxxxxxxx |
|
||||
| ccccccccccc xxxxxxxxxxxxxxxxxxxxxx |
|
||||
| |
|
||||
+--------------------------------------+
|
||||
"""
|
||||
````
|
||||
The `#coding` statement (which must be put on the very first line to work) tells Python to use the
|
||||
utf-8 encoding for the file. Using the `FORMCHAR` and `TABLECHAR` we define what single-character we
|
||||
want to use to "mark" the regions of the character sheet holding cells and tables respectively.
|
||||
Within each block (which must be separated from one another by at least one non-marking character)
|
||||
we embed identifiers 1-4 to identify each block. The identifier could be any single character except
|
||||
for the `FORMCHAR` and `TABLECHAR`
|
||||
|
||||
> You can still use `FORMCHAR` and `TABLECHAR` elsewhere in your sheet, but not in a way that it
|
||||
would identify a cell/table. The smallest identifiable cell/table area is 3 characters wide
|
||||
including the identifier (for example `x2x`).
|
||||
|
||||
Now we will map content to this form.
|
||||
|
||||
````python
|
||||
# again, this can be tested in a Python shell
|
||||
|
||||
# hard-code this info here, later we'll ask the
|
||||
# account for this info. We will re-use the 'table'
|
||||
# variable from the EvTable example.
|
||||
|
||||
NAME = "John, the wise old admin with a chip on his shoulder"
|
||||
ADVANTAGES = "Language-wiz, Intimidation, Firebreathing"
|
||||
DISADVANTAGES = "Bad body odor, Poor eyesight, Troubled history"
|
||||
|
||||
from evennia.utils import evform
|
||||
|
||||
# load the form from the module
|
||||
form = evform.EvForm("world/charsheetform.py")
|
||||
|
||||
# map the data to the form
|
||||
form.map(cells={"1":NAME, "3": ADVANTAGES, "4": DISADVANTAGES},
|
||||
tables={"2":table})
|
||||
````
|
||||
|
||||
We create some RP-sounding input and re-use the `table` variable from the previous `EvTable`
|
||||
example.
|
||||
|
||||
> Note, that if you didn't want to create the form in a separate module you *could* also load it
|
||||
directly into the `EvForm` call like this: `EvForm(form={"FORMCHAR":"x", "TABLECHAR":"c", "FORM":
|
||||
formstring})` where `FORM` specifies the form as a string in the same way as listed in the module
|
||||
above. Note however that the very first line of the `FORM` string is ignored, so start with a `\n`.
|
||||
|
||||
We then map those to the cells of the form:
|
||||
|
||||
````python
|
||||
print(form)
|
||||
````
|
||||
````
|
||||
.--------------------------------------.
|
||||
| |
|
||||
| Name: John, the wise old admin with |
|
||||
| a chip on his shoulder |
|
||||
| |
|
||||
>------------------------------------<
|
||||
| |
|
||||
| Attr|Value Advantages: |
|
||||
| ~~~~~+~~~~~ Language-wiz, |
|
||||
| STR| 12 Intimidation, |
|
||||
| CON| 13 Firebreathing |
|
||||
| DEX| 8 Disadvantages: |
|
||||
| INT| 10 Bad body odor, Poor |
|
||||
| WIS| 9 eyesight, Troubled |
|
||||
| CHA| 13 history |
|
||||
| |
|
||||
+--------------------------------------+
|
||||
````
|
||||
|
||||
As seen, the texts and tables have been slotted into the text areas and line breaks have been added
|
||||
where needed. We chose to just enter the Advantages/Disadvantages as plain strings here, meaning
|
||||
long names ended up split between rows. If we wanted more control over the display we could have
|
||||
inserted `\n` line breaks after each line or used a borderless `EvTable` to display those as well.
|
||||
|
||||
### Tie a Character sheet to a Character
|
||||
|
||||
We will assume we go with the `EvForm` example above. We now need to attach this to a Character so
|
||||
it can be modified. For this we will modify our `Character` class a little more:
|
||||
|
||||
```python
|
||||
# mygame/typeclasses/character.py
|
||||
|
||||
from evennia.utils import evform, evtable
|
||||
|
||||
[...]
|
||||
|
||||
class Character(DefaultCharacter):
|
||||
[...]
|
||||
def at_object_creation(self):
|
||||
"called only once, when object is first created"
|
||||
# we will use this to stop account from changing sheet
|
||||
self.db.sheet_locked = False
|
||||
# we store these so we can build these on demand
|
||||
self.db.chardata = {"str": 0,
|
||||
"con": 0,
|
||||
"dex": 0,
|
||||
"int": 0,
|
||||
"wis": 0,
|
||||
"cha": 0,
|
||||
"advantages": "",
|
||||
"disadvantages": ""}
|
||||
self.db.charsheet = evform.EvForm("world/charsheetform.py")
|
||||
self.update_charsheet()
|
||||
|
||||
def update_charsheet(self):
|
||||
"""
|
||||
Call this to update the sheet after any of the ingoing data
|
||||
has changed.
|
||||
"""
|
||||
data = self.db.chardata
|
||||
table = evtable.EvTable("Attr", "Value",
|
||||
table = [
|
||||
["STR", "CON", "DEX", "INT", "WIS", "CHA"],
|
||||
[data["str"], data["con"], data["dex"],
|
||||
data["int"], data["wis"], data["cha"]]],
|
||||
align='r', border="incols")
|
||||
self.db.charsheet.map(tables={"2": table},
|
||||
cells={"1":self.key,
|
||||
"3":data["advantages"],
|
||||
"4":data["disadvantages"]})
|
||||
|
||||
```
|
||||
|
||||
Use `@reload` to make this change available to all *newly created* Characters. *Already existing*
|
||||
Characters will *not* have the charsheet defined, since `at_object_creation` is only called once.
|
||||
The easiest to force an existing Character to re-fire its `at_object_creation` is to use the
|
||||
`@typeclass` command in-game:
|
||||
|
||||
```
|
||||
@typeclass/force <Character Name>
|
||||
```
|
||||
|
||||
### Command for Account to change Character sheet
|
||||
|
||||
We will add a command to edit the sections of our Character sheet. Open
|
||||
`mygame/commands/command.py`.
|
||||
|
||||
```python
|
||||
# at the end of mygame/commands/command.py
|
||||
|
||||
ALLOWED_ATTRS = ("str", "con", "dex", "int", "wis", "cha")
|
||||
ALLOWED_FIELDNAMES = ALLOWED_ATTRS + \
|
||||
("name", "advantages", "disadvantages")
|
||||
|
||||
def _validate_fieldname(caller, fieldname):
|
||||
"Helper function to validate field names."
|
||||
if fieldname not in ALLOWED_FIELDNAMES:
|
||||
err = "Allowed field names: %s" % (", ".join(ALLOWED_FIELDNAMES))
|
||||
caller.msg(err)
|
||||
return False
|
||||
if fieldname in ALLOWED_ATTRS and not value.isdigit():
|
||||
caller.msg("%s must receive a number." % fieldname)
|
||||
return False
|
||||
return True
|
||||
|
||||
class CmdSheet(MuxCommand):
|
||||
"""
|
||||
Edit a field on the character sheet
|
||||
|
||||
Usage:
|
||||
@sheet field value
|
||||
|
||||
Examples:
|
||||
@sheet name Ulrik the Warrior
|
||||
@sheet dex 12
|
||||
@sheet advantages Super strength, Night vision
|
||||
|
||||
If given without arguments, will view the current character sheet.
|
||||
|
||||
Allowed field names are:
|
||||
name,
|
||||
str, con, dex, int, wis, cha,
|
||||
advantages, disadvantages
|
||||
|
||||
"""
|
||||
|
||||
key = "sheet"
|
||||
aliases = "editsheet"
|
||||
locks = "cmd: perm(Players)"
|
||||
help_category = "RP"
|
||||
|
||||
def func(self):
|
||||
caller = self.caller
|
||||
if not self.args or len(self.args) < 2:
|
||||
# not enough arguments. Display the sheet
|
||||
if sheet:
|
||||
caller.msg(caller.db.charsheet)
|
||||
else:
|
||||
caller.msg("You have no character sheet.")
|
||||
return
|
||||
|
||||
# if caller.db.sheet_locked:
|
||||
caller.msg("Your character sheet is locked.")
|
||||
return
|
||||
|
||||
# split input by whitespace, once
|
||||
fieldname, value = self.args.split(None, 1)
|
||||
fieldname = fieldname.lower() # ignore case
|
||||
|
||||
if not _validate_fieldnames(caller, fieldname):
|
||||
return
|
||||
if fieldname == "name":
|
||||
self.key = value
|
||||
else:
|
||||
caller.chardata[fieldname] = value
|
||||
caller.update_charsheet()
|
||||
caller.msg("%s was set to %s." % (fieldname, value))
|
||||
|
||||
```
|
||||
|
||||
Most of this command is error-checking to make sure the right type of data was input. Note how the
|
||||
`sheet_locked` Attribute is checked and will return if not set.
|
||||
|
||||
This command you import into `mygame/commands/default_cmdsets.py` and add to the `CharacterCmdSet`,
|
||||
in the same way the `@gm` command was added to the `AccountCmdSet` earlier.
|
||||
|
||||
### Commands for GM to change Character sheet
|
||||
|
||||
Game masters use basically the same input as Players do to edit a character sheet, except they can
|
||||
do it on other players than themselves. They are also not stopped by any `sheet_locked` flags.
|
||||
|
||||
```python
|
||||
# continuing in mygame/commands/command.py
|
||||
|
||||
class CmdGMsheet(MuxCommand):
|
||||
"""
|
||||
GM-modification of char sheets
|
||||
|
||||
Usage:
|
||||
@gmsheet character [= fieldname value]
|
||||
|
||||
Switches:
|
||||
lock - lock the character sheet so the account
|
||||
can no longer edit it (GM's still can)
|
||||
unlock - unlock character sheet for Account
|
||||
editing.
|
||||
|
||||
Examples:
|
||||
@gmsheet Tom
|
||||
@gmsheet Anna = str 12
|
||||
@gmsheet/lock Tom
|
||||
|
||||
"""
|
||||
key = "gmsheet"
|
||||
locks = "cmd: perm(Admins)"
|
||||
help_category = "RP"
|
||||
|
||||
def func(self):
|
||||
caller = self.caller
|
||||
if not self.args:
|
||||
caller.msg("Usage: @gmsheet character [= fieldname value]")
|
||||
|
||||
if self.rhs:
|
||||
# rhs (right-hand-side) is set only if a '='
|
||||
# was given.
|
||||
if len(self.rhs) < 2:
|
||||
caller.msg("You must specify both a fieldname and value.")
|
||||
return
|
||||
fieldname, value = self.rhs.split(None, 1)
|
||||
fieldname = fieldname.lower()
|
||||
if not _validate_fieldname(caller, fieldname):
|
||||
return
|
||||
charname = self.lhs
|
||||
else:
|
||||
# no '=', so we must be aiming to look at a charsheet
|
||||
fieldname, value = None, None
|
||||
charname = self.args.strip()
|
||||
|
||||
character = caller.search(charname, global_search=True)
|
||||
if not character:
|
||||
return
|
||||
|
||||
if "lock" in self.switches:
|
||||
if character.db.sheet_locked:
|
||||
caller.msg("The character sheet is already locked.")
|
||||
else:
|
||||
character.db.sheet_locked = True
|
||||
caller.msg("%s can no longer edit their character sheet." % character.key)
|
||||
elif "unlock" in self.switches:
|
||||
if not character.db.sheet_locked:
|
||||
caller.msg("The character sheet is already unlocked.")
|
||||
else:
|
||||
character.db.sheet_locked = False
|
||||
caller.msg("%s can now edit their character sheet." % character.key)
|
||||
|
||||
if fieldname:
|
||||
if fieldname == "name":
|
||||
character.key = value
|
||||
else:
|
||||
character.db.chardata[fieldname] = value
|
||||
character.update_charsheet()
|
||||
caller.msg("You set %s's %s to %s." % (character.key, fieldname, value)
|
||||
else:
|
||||
# just display
|
||||
caller.msg(character.db.charsheet)
|
||||
```
|
||||
|
||||
The `@gmsheet` command takes an additional argument to specify which Character's character sheet to
|
||||
edit. It also takes `/lock` and `/unlock` switches to block the Player from tweaking their sheet.
|
||||
|
||||
Before this can be used, it should be added to the default `CharacterCmdSet` in the same way as the
|
||||
normal `@sheet`. Due to the lock set on it, this command will only be available to `Admins` (i.e.
|
||||
GMs) or higher permission levels.
|
||||
|
||||
## Dice roller
|
||||
|
||||
Evennia's *contrib* folder already comes with a full dice roller. To add it to the game, simply
|
||||
import `contrib.dice.CmdDice` into `mygame/commands/default_cmdsets.py` and add `CmdDice` to the
|
||||
`CharacterCmdset` as done with other commands in this tutorial. After a `@reload` you will be able
|
||||
to roll dice using normal RPG-style format:
|
||||
|
||||
```
|
||||
roll 2d6 + 3
|
||||
7
|
||||
```
|
||||
|
||||
Use `help dice` to see what syntax is supported or look at `evennia/contrib/dice.py` to see how it's
|
||||
implemented.
|
||||
|
||||
## Rooms
|
||||
|
||||
Evennia comes with rooms out of the box, so no extra work needed. A GM will automatically have all
|
||||
needed building commands available. A fuller go-through is found in the [Building
|
||||
tutorial](Building-Quickstart). Here are some useful highlights:
|
||||
|
||||
* `@dig roomname;alias = exit_there;alias, exit_back;alias` - this is the basic command for digging
|
||||
a new room. You can specify any exit-names and just enter the name of that exit to go there.
|
||||
* `@tunnel direction = roomname` - this is a specialized command that only accepts directions in the
|
||||
cardinal directions (n,ne,e,se,s,sw,w,nw) as well as in/out and up/down. It also automatically
|
||||
builds "matching" exits back in the opposite direction.
|
||||
* `@create/drop objectname` - this creates and drops a new simple object in the current location.
|
||||
* `@desc obj` - change the look-description of the object.
|
||||
* `@tel object = location` - teleport an object to a named location.
|
||||
* `@search objectname` - locate an object in the database.
|
||||
|
||||
> TODO: Describe how to add a logging room, that logs says and poses to a log file that people can
|
||||
access after the fact.
|
||||
|
||||
## Channels
|
||||
|
||||
Evennia comes with [Channels](Communications#Channels) in-built and they are described fully in the
|
||||
documentation. For brevity, here are the relevant commands for normal use:
|
||||
|
||||
* `@ccreate new_channel;alias;alias = short description` - Creates a new channel.
|
||||
* `addcom channel` - join an existing channel. Use `addcom alias = channel` to add a new alias you
|
||||
can use to talk to the channel, as many as desired.
|
||||
* `delcom alias or channel` - remove an alias from a channel or, if the real channel name is given,
|
||||
unsubscribe completely.
|
||||
* `@channels` lists all available channels, including your subscriptions and any aliases you have
|
||||
set up for them.
|
||||
|
||||
You can read channel history: if you for example are chatting on the `public` channel you can do
|
||||
`public/history` to see the 20 last posts to that channel or `public/history 32` to view twenty
|
||||
posts backwards, starting with the 32nd from the end.
|
||||
|
||||
## PMs
|
||||
|
||||
To send PMs to one another, players can use the `@page` (or `tell`) command:
|
||||
|
||||
```
|
||||
page recipient = message
|
||||
page recipient, recipient, ... = message
|
||||
```
|
||||
|
||||
Players can use `page` alone to see the latest messages. This also works if they were not online
|
||||
when the message was sent.
|
||||
218
docs/source/Howto/Game-Planning.md
Normal file
218
docs/source/Howto/Game-Planning.md
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
# Game Planning
|
||||
|
||||
|
||||
So you have Evennia up and running. You have a great game idea in mind. Now it's time to start
|
||||
cracking! But where to start? Here are some ideas for a workflow. Note that the suggestions on this
|
||||
page are just that - suggestions. Also, they are primarily aimed at a lone hobby designer or a small
|
||||
team developing a game in their free time. There is an article in the Imaginary Realities e-zine
|
||||
which was written by the Evennia lead dev. It focuses more on you finding out your motivations for
|
||||
making a game - you can [read the article here](http://journal.imaginary-
|
||||
realities.com/volume-07/issue-03/where-do-i-begin/index.html).
|
||||
|
||||
|
||||
Below are some minimal steps for getting the first version of a new game world going with players.
|
||||
It's worth to at least make the attempt to do these steps in order even if you are itching to jump
|
||||
ahead in the development cycle. On the other hand, you should also make sure to keep your work fun
|
||||
for you, or motivation will falter. Making a full game is a lot of work as it is, you'll need all
|
||||
your motivation to make it a reality.
|
||||
|
||||
Remember that *99.99999% of all great game ideas never lead to a game*. Especially not to an online
|
||||
game that people can actually play and enjoy. So our first all overshadowing goal is to beat those
|
||||
odds and get *something* out the door! Even if it's a scaled-down version of your dream game,
|
||||
lacking many "must-have" features! It's better to get it out there and expand on it later than to
|
||||
code in isolation forever until you burn out, lose interest or your hard drive crashes.
|
||||
|
||||
Like is common with online games, getting a game out the door does not mean you are going to be
|
||||
"finished" with the game - most MUDs add features gradually over the course of years - it's often
|
||||
part of the fun!
|
||||
|
||||
## Planning (step 1)
|
||||
|
||||
This is what you do before having coded a single line or built a single room. Many prospective game
|
||||
developers are very good at *parts* of this process, namely in defining what their world is "about":
|
||||
The theme, the world concept, cool monsters and so on. It is by all means very important to define
|
||||
what is the unique appeal of your game. But it's unfortunately not enough to make your game a
|
||||
reality. To do that you must also have an idea of how to actually map those great ideas onto
|
||||
Evennia.
|
||||
|
||||
A good start is to begin by planning out the basic primitives of the game and what they need to be
|
||||
able to do. Below are a far-from-complete list of examples (and for your first version you should
|
||||
definitely try for a much shorter list):
|
||||
|
||||
### Systems
|
||||
|
||||
These are the behind-the-scenes features that exist in your game, often without being represented by
|
||||
a specific in-game object.
|
||||
|
||||
- Should your game rules be enforced by coded systems or are you planning for human game masters to
|
||||
run and arbitrate rules?
|
||||
- What are the actual mechanical game rules? How do you decide if an action succeeds or fails? What
|
||||
"rolls" does the game need to be able to do? Do you base your game off an existing system or make up
|
||||
your own?
|
||||
- Does the flow of time matter in your game - does night and day change? What about seasons? Maybe
|
||||
your magic system is affected by the phase of the moon?
|
||||
- Do you want changing, global weather? This might need to operate in tandem over a large number of
|
||||
rooms.
|
||||
- Do you want a game-wide economy or just a simple barter system? Or no formal economy at all?
|
||||
- Should characters be able to send mail to each other in-game?
|
||||
- Should players be able to post on Bulletin boards?
|
||||
- What is the staff hierarchy in your game? What powers do you want your staff to have?
|
||||
- What should a Builder be able to build and what commands do they need in order to do that?
|
||||
- etc.
|
||||
|
||||
### Rooms
|
||||
|
||||
Consider the most basic room in your game.
|
||||
|
||||
- Is a simple description enough or should the description be able to change (such as with time, by
|
||||
light conditions, weather or season)?
|
||||
- Should the room have different statuses? Can it have smells, sounds? Can it be affected by
|
||||
dramatic weather, fire or magical effects? If so, how would this affect things in the room? Or are
|
||||
these things something admins/game masters should handle manually?
|
||||
- Can objects be hidden in the room? Can a person hide in the room? How does the room display this?
|
||||
- etc.
|
||||
|
||||
### Objects
|
||||
|
||||
Consider the most basic (non-player-controlled) object in your game.
|
||||
|
||||
- How numerous are your objects? Do you want large loot-lists or are objects just role playing props
|
||||
created on demand?
|
||||
- Does the game use money? If so, is each coin a separate object or do you just store a bank account
|
||||
value?
|
||||
- What about multiple identical objects? Do they form stacks and how are those stacks handled in
|
||||
that case?
|
||||
- Does an object have weight or volume (so you cannot carry an infinite amount of them)?
|
||||
- Can objects be broken? If so, does it have a health value? Is burning it causing the same damage
|
||||
as smashing it? Can it be repaired?
|
||||
- Is a weapon a specific type of object or are you supposed to be able to fight with a chair too?
|
||||
Can you fight with a flower or piece of paper as well?
|
||||
- NPCs/mobs are also objects. Should they just stand around or should they have some sort of AI?
|
||||
- Are NPCs/mobs differet entities? How is an Orc different from a Kobold, in code - are they the
|
||||
same object with different names or completely different types of objects, with custom code?
|
||||
- Should there be NPCs giving quests? If so, how would you track quest status and what happens when
|
||||
multiple players try to do the same quest? Do you use instances or some other mechanism?
|
||||
- etc.
|
||||
|
||||
### Characters
|
||||
|
||||
These are the objects controlled directly by Players.
|
||||
|
||||
- Can players have more than one Character active at a time or are they allowed to multi-play?
|
||||
- How does a Player create their Character? A Character-creation screen? Answering questions?
|
||||
Filling in a form?
|
||||
- Do you want to use classes (like "Thief", "Warrior" etc) or some other system, like Skill-based?
|
||||
- How do you implement different "classes" or "races"? Are they separate types of objects or do you
|
||||
simply load different stats on a basic object depending on what the Player wants?
|
||||
- If a Character can hide in a room, what skill will decide if they are detected?
|
||||
- What skill allows a Character to wield a weapon and hit? Do they need a special skill to wield a
|
||||
chair rather than a sword?
|
||||
- Does a Character need a Strength attribute to tell how much they can carry or which objects they
|
||||
can smash?
|
||||
- What does the skill tree look like? Can a Character gain experience to improve? By killing
|
||||
enemies? Solving quests? By roleplaying?
|
||||
- etc.
|
||||
|
||||
A MUD's a lot more involved than you would think and these things hang together in a complex web. It
|
||||
can easily become overwhelming and it's tempting to want *all* functionality right out of the door.
|
||||
Try to identify the basic things that "make" your game and focus *only* on them for your first
|
||||
release. Make a list. Keep future expansions in mind but limit yourself.
|
||||
|
||||
## Coding (step 2)
|
||||
|
||||
This is the actual work of creating the "game" part of your game. Many "game-designer" types tend to
|
||||
gloss over this bit and jump directly to **World Building**. Vice versa, many "game-coder" types
|
||||
tend to jump directly to this part without doing the **Planning** first. Neither way is good and
|
||||
*will* lead to you having to redo all your hard work at least once, probably more.
|
||||
|
||||
Evennia's [Developer Central](Developer-Central) tries to help you with this bit of development. We
|
||||
also have a slew of [Tutorials](Tutorials) with worked examples. Evennia tries hard to make this
|
||||
part easier for you, but there is no way around the fact that if you want anything but a very basic
|
||||
Talker-type game you *will* have to bite the bullet and code your game (or find a coder willing to
|
||||
do it for you).
|
||||
|
||||
Even if you won't code anything yourself, as a designer you need to at least understand the basic
|
||||
paradigms of Evennia, such as [Objects](Objects), [Commands](Commands) and [Scripts](Scripts) and
|
||||
how they hang together. We recommend you go through the [Tutorial World](Tutorial-World-
|
||||
Introduction) in detail (as well as glancing at its code) to get at least a feel for what is
|
||||
involved behind the scenes. You could also look through the tutorial for [building a game from
|
||||
scratch](Tutorial-for-basic-MUSH-like-game).
|
||||
|
||||
During Coding you look back at the things you wanted during the **Planning** phase and try to
|
||||
implement them. Don't be shy to update your plans if you find things easier/harder than you thought.
|
||||
The earlier you revise problems, the easier they will be to fix.
|
||||
|
||||
A good idea is to host your code online (publicly or privately) using version control. Not only will
|
||||
this make it easy for multiple coders to collaborate (and have a bug-tracker etc), it also means
|
||||
your work is backed up at all times. The [Version Control](Version-Control) tutorial has
|
||||
instructions for setting up a sane developer environment with proper version control.
|
||||
|
||||
### "Tech Demo" Building
|
||||
|
||||
This is an integral part of your Coding. It might seem obvious to experienced coders, but it cannot
|
||||
be emphasized enough that you should *test things on a small scale* before putting your untested
|
||||
code into a large game-world. The earlier you test, the easier and cheaper it will be to fix bugs
|
||||
and even rework things that didn't work out the way you thought they would. You might even have to
|
||||
go back to the **Planning** phase if your ideas can't handle their meet with reality.
|
||||
|
||||
This means building singular in-game examples. Make one room and one object of each important type
|
||||
and test so they work correctly in isolation. Then add more if they are supposed to interact with
|
||||
each other in some way. Build a small series of rooms to test how mobs move around ... and so on. In
|
||||
short, a test-bed for your growing code. It should be done gradually until you have a fully
|
||||
functioning (if not guaranteed bug-free) miniature tech demo that shows *all* the features you want
|
||||
in the first release of your game. There does not need to be any game play or even a theme to your
|
||||
tests, this is only for you and your co-coders to see. The more testing you do on this small scale,
|
||||
the less headaches you will have in the next phase.
|
||||
|
||||
## World Building (step 3)
|
||||
|
||||
Up until this point we've only had a few tech-demo objects in the database. This step is the act of
|
||||
populating the database with a larger, thematic world. Too many would-be developers jump to this
|
||||
stage too soon (skipping the **Coding** or even **Planning** stages). What if the rooms you build
|
||||
now doesn't include all the nice weather messages the code grows to support? Or the way you store
|
||||
data changes under the hood? Your building work would at best require some rework and at worst you
|
||||
would have to redo the whole thing. And whereas Evennia's typeclass system does allow you to edit
|
||||
the properties of existing objects, some hooks are only called at object creation ... Suffice to
|
||||
say you are in for a *lot* of unnecessary work if you build stuff en masse without having the
|
||||
underlying code systems in some reasonable shape first.
|
||||
|
||||
So before starting to build, the "game" bit (**Coding** + **Testing**) should be more or less
|
||||
**complete**, *at least to the level of your initial release*.
|
||||
|
||||
Before starting to build, you should also plan ahead again. Make sure it is clear to yourself and
|
||||
your eventual builders just which parts of the world you want for your initial release. Establish
|
||||
for everyone which style, quality and level of detail you expect. Your goal should *not* be to
|
||||
complete your entire world in one go. You want just enough to make the game's "feel" come across.
|
||||
You want a minimal but functioning world where the intended game play can be tested and roughly
|
||||
balanced. You can always add new areas later.
|
||||
|
||||
During building you get free and extensive testing of whatever custom build commands and systems you
|
||||
have made at this point. Since Building often involves different people than those Coding, you also
|
||||
get a chance to hear if some things are hard to understand or non-intuitive. Make sure to respond
|
||||
to this feedback.
|
||||
|
||||
|
||||
## Alpha Release
|
||||
|
||||
As mentioned, don't hold onto your world more than necessary. *Get it out there* with a huge *Alpha*
|
||||
flag and let people try it! Call upon your alpha-players to try everything - they *will* find ways
|
||||
to break your game in ways that you never could have imagined. In Alpha you might be best off to
|
||||
focus on inviting friends and maybe other MUD developers, people who you can pester to give proper
|
||||
feedback and bug reports (there *will* be bugs, there is no way around it). Follow the quick
|
||||
instructions for [Online Setup](Online-Setup) to make your game visible online. If you hadn't
|
||||
already, make sure to put up your game on the [Evennia game index](http://games.evennia.com/) so
|
||||
people know it's in the works (actually, even pre-alpha games are allowed in the index so don't be
|
||||
shy)!
|
||||
|
||||
## Beta Release/Perpetual Beta
|
||||
|
||||
Once things stabilize in Alpha you can move to *Beta* and let more people in. Many MUDs are in
|
||||
[perpetual beta](http://en.wikipedia.org/wiki/Perpetual_beta), meaning they are never considered
|
||||
"finished", but just repeat the cycle of Planning, Coding, Testing and Building over and over as new
|
||||
features get implemented or Players come with suggestions. As the game designer it is now up to you
|
||||
to gradually perfect your vision.
|
||||
|
||||
## Congratulate yourself!
|
||||
|
||||
You are worthy of a celebration since at this point you have joined the small, exclusive crowd who
|
||||
have made their dream game a reality!
|
||||
302
docs/source/Howto/Gametime-Tutorial.md
Normal file
302
docs/source/Howto/Gametime-Tutorial.md
Normal file
|
|
@ -0,0 +1,302 @@
|
|||
# Gametime Tutorial
|
||||
|
||||
|
||||
A lot of games use a separate time system we refer to as *game time*. This runs in parallel to what
|
||||
we usually think of as *real time*. The game time might run at a different speed, use different
|
||||
names for its time units or might even use a completely custom calendar. You don't need to rely on a
|
||||
game time system at all. But if you do, Evennia offers basic tools to handle these various
|
||||
situations. This tutorial will walk you through these features.
|
||||
|
||||
### A game time with a standard calendar
|
||||
|
||||
Many games let their in-game time run faster or slower than real time, but still use our normal
|
||||
real-world calendar. This is common both for games set in present day as well as for games in
|
||||
historical or futuristic settings. Using a standard calendar has some advantages:
|
||||
|
||||
- Handling repetitive actions is much easier, since converting from the real time experience to the
|
||||
in-game perceived one is easy.
|
||||
- The intricacies of the real world calendar, with leap years and months of different length etc are
|
||||
automatically handled by the system.
|
||||
|
||||
Evennia's game time features assume a standard calendar (see the relevant section below for a custom
|
||||
calendar).
|
||||
|
||||
#### Setting up game time for a standard calendar
|
||||
|
||||
All is done through the settings. Here are the settings you should use if you want a game time with
|
||||
a standard calendar:
|
||||
|
||||
```python
|
||||
# in a file settings.py in mygame/server/conf
|
||||
# The time factor dictates if the game world runs faster (timefactor>1)
|
||||
# or slower (timefactor<1) than the real world.
|
||||
TIME_FACTOR = 2.0
|
||||
|
||||
# The starting point of your game time (the epoch), in seconds.
|
||||
# In Python a value of 0 means Jan 1 1970 (use negatives for earlier
|
||||
# start date). This will affect the returns from the utils.gametime
|
||||
# module.
|
||||
TIME_GAME_EPOCH = None
|
||||
```
|
||||
|
||||
By default, the game time runs twice as fast as the real time. You can set the time factor to be 1
|
||||
(the game time would run exactly at the same speed than the real time) or lower (the game time will
|
||||
be slower than the real time). Most games choose to have the game time spinning faster (you will
|
||||
find some games that have a time factor of 60, meaning the game time runs sixty times as fast as the
|
||||
real time, a minute in real time would be an hour in game time).
|
||||
|
||||
The epoch is a slightly more complex setting. It should contain a number of seconds that would
|
||||
indicate the time your game started. As indicated, an epoch of 0 would mean January 1st, 1970. If
|
||||
you want to set your time in the future, you just need to find the starting point in seconds. There
|
||||
are several ways to do this in Python, this method will show you how to do it in local time:
|
||||
|
||||
```python
|
||||
# We're looking for the number of seconds representing
|
||||
# January 1st, 2020
|
||||
from datetime import datetime
|
||||
import time
|
||||
start = datetime(2020, 1, 1)
|
||||
time.mktime(start.timetuple())
|
||||
```
|
||||
|
||||
This should return a huge number - the number of seconds since Jan 1 1970. Copy that directly into
|
||||
your settings (editing `server/conf/settings.py`):
|
||||
|
||||
```python
|
||||
# in a file settings.py in mygame/server/conf
|
||||
TIME_GAME_EPOCH = 1577865600
|
||||
```
|
||||
|
||||
Reload the game with `@reload`, and then use the `@time` command. You should see something like
|
||||
this:
|
||||
|
||||
```
|
||||
+----------------------------+-------------------------------------+
|
||||
| Server time | |
|
||||
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
|
||||
| Current uptime | 20 seconds |
|
||||
| Total runtime | 1 day, 1 hour, 55 minutes |
|
||||
| First start | 2017-02-12 15:47:50.565000 |
|
||||
| Current time | 2017-02-13 17:43:10.760000 |
|
||||
+----------------------------+-------------------------------------+
|
||||
| In-Game time | Real time x 2 |
|
||||
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
|
||||
| Epoch (from settings) | 2020-01-01 00:00:00 |
|
||||
| Total time passed: | 1 day, 17 hours, 34 minutes |
|
||||
| Current time | 2020-01-02 17:34:55.430000 |
|
||||
+----------------------------+-------------------------------------+
|
||||
```
|
||||
|
||||
The line that is most relevant here is the game time epoch. You see it shown at 2020-01-01. From
|
||||
this point forward, the game time keeps increasing. If you keep typing `@time`, you'll see the game
|
||||
time updated correctly... and going (by default) twice as fast as the real time.
|
||||
|
||||
#### Time-related events
|
||||
|
||||
The `gametime` utility also has a way to schedule game-related events, taking into account your game
|
||||
time, and assuming a standard calendar (see below for the same feature with a custom calendar). For
|
||||
instance, it can be used to have a specific message every (in-game) day at 6:00 AM showing how the
|
||||
sun rises.
|
||||
|
||||
The function `schedule()` should be used here. It will create a [script](Scripts) with some
|
||||
additional features to make sure the script is always executed when the game time matches the given
|
||||
parameters.
|
||||
|
||||
The `schedule` function takes the following arguments:
|
||||
|
||||
- The *callback*, a function to be called when time is up.
|
||||
- The keyword `repeat` (`False` by default) to indicate whether this function should be called
|
||||
repeatedly.
|
||||
- Additional keyword arguments `sec`, `min`, `hour`, `day`, `month` and `year` to describe the time
|
||||
to schedule. If the parameter isn't given, it assumes the current time value of this specific unit.
|
||||
|
||||
Here is a short example for making the sun rise every day:
|
||||
|
||||
```python
|
||||
# in a file ingame_time.py in mygame/world/
|
||||
|
||||
from evennia.utils import gametime
|
||||
from typeclasses.rooms import Room
|
||||
|
||||
def at_sunrise():
|
||||
"""When the sun rises, display a message in every room."""
|
||||
# Browse all rooms
|
||||
for room in Room.objects.all():
|
||||
room.msg_contents("The sun rises from the eastern horizon.")
|
||||
|
||||
def start_sunrise_event():
|
||||
"""Schedule an sunrise event to happen every day at 6 AM."""
|
||||
script = gametime.schedule(at_sunrise, repeat=True, hour=6, min=0, sec=0)
|
||||
script.key = "at sunrise"
|
||||
```
|
||||
|
||||
If you want to test this function, you can easily do something like:
|
||||
|
||||
```
|
||||
@py from world import ingame_time; ingame_time.start_sunrise_event()
|
||||
```
|
||||
|
||||
The script will be created silently. The `at_sunrise` function will now be called every in-game day
|
||||
at 6 AM. You can use the `@scripts` command to see it. You could stop it using `@scripts/stop`. If
|
||||
we hadn't set `repeat` the sun would only have risen once and then never again.
|
||||
|
||||
We used the `@py` command here: nothing prevents you from adding the system into your game code.
|
||||
Remember to be careful not to add each event at startup, however, otherwise there will be a lot of
|
||||
overlapping events scheduled when the sun rises.
|
||||
|
||||
The `schedule` function when using `repeat` set to `True` works with the higher, non-specified unit.
|
||||
In our example, we have specified hour, minute and second. The higher unit we haven't specified is
|
||||
day: `schedule` assumes we mean "run the callback every day at the specified time". Therefore, you
|
||||
can have an event that runs every hour at HH:30, or every month on the 3rd day.
|
||||
|
||||
> A word of caution for repeated scripts on a monthly or yearly basis: due to the variations in the
|
||||
real-life calendar you need to be careful when scheduling events for the end of the month or year.
|
||||
For example, if you set a script to run every month on the 31st it will run in January but find no
|
||||
such day in February, April etc. Similarly, leap years may change the number of days in the year.
|
||||
|
||||
### A game time with a custom calendar
|
||||
|
||||
Using a custom calendar to handle game time is sometimes needed if you want to place your game in a
|
||||
fictional universe. For instance you may want to create the Shire calendar which Tolkien described
|
||||
having 12 months, each which 30 days. That would give only 360 days per year (presumably hobbits
|
||||
weren't really fond of the hassle of following the astronomical calendar). Another example would be
|
||||
creating a planet in a different solar system with, say, days 29 hours long and months of only 18
|
||||
days.
|
||||
|
||||
Evennia handles custom calendars through an optional *contrib* module, called `custom_gametime`.
|
||||
Contrary to the normal `gametime` module described above it is not active by default.
|
||||
|
||||
#### Setting up the custom calendar
|
||||
|
||||
In our first example of the Shire calendar, used by hobbits in books by Tolkien, we don't really
|
||||
need the notion of weeks... but we need the notion of months having 30 days, not 28.
|
||||
|
||||
The custom calendar is defined by adding the `TIME_UNITS` setting to your settings file. It's a
|
||||
dictionary containing as keys the name of the units, and as value the number of seconds (the
|
||||
smallest unit for us) in this unit. Its keys must be picked among the following: "sec", "min",
|
||||
"hour", "day", "week", "month" and "year" but you don't have to include them all. Here is the
|
||||
configuration for the Shire calendar:
|
||||
|
||||
```python
|
||||
# in a file settings.py in mygame/server/conf
|
||||
TIME_UNITS = {"sec": 1,
|
||||
"min": 60,
|
||||
"hour": 60 * 60,
|
||||
"day": 60 * 60 * 24,
|
||||
"month": 60 * 60 * 24 * 30,
|
||||
"year": 60 * 60 * 24 * 30 * 12 }
|
||||
```
|
||||
|
||||
We give each unit we want as keys. Values represent the number of seconds in that unit. Hour is
|
||||
set to 60 * 60 (that is, 3600 seconds per hour). Notice that we don't specify the week unit in this
|
||||
configuration: instead, we skip from days to months directly.
|
||||
|
||||
In order for this setting to work properly, remember all units have to be multiples of the previous
|
||||
units. If you create "day", it needs to be multiple of hours, for instance.
|
||||
|
||||
So for our example, our settings may look like this:
|
||||
|
||||
```python
|
||||
# in a file settings.py in mygame/server/conf
|
||||
# Time factor
|
||||
TIME_FACTOR = 4
|
||||
|
||||
# Game time epoch
|
||||
TIME_GAME_EPOCH = 0
|
||||
|
||||
# Units
|
||||
TIME_UNITS = {
|
||||
"sec": 1,
|
||||
"min": 60,
|
||||
"hour": 60 * 60,
|
||||
"day": 60 * 60 * 24,
|
||||
"month": 60 * 60 * 24 * 30,
|
||||
"year": 60 * 60 * 24 * 30 * 12,
|
||||
}
|
||||
```
|
||||
|
||||
Notice we have set a time epoch of 0. Using a custom calendar, we will come up with a nice display
|
||||
of time on our own. In our case the game time starts at year 0, month 0, day 0, and at midnight.
|
||||
|
||||
Note that while we use "month", "week" etc in the settings, your game may not use those terms in-
|
||||
game, instead referring to them as "cycles", "moons", "sand falls" etc. This is just a matter of you
|
||||
displaying them differently. See next section.
|
||||
|
||||
#### A command to display the current game time
|
||||
|
||||
As pointed out earlier, the `@time` command is meant to be used with a standard calendar, not a
|
||||
custom one. We can easily create a new command though. We'll call it `time`, as is often the case
|
||||
on other MU*. Here's an example of how we could write it (for the example, you can create a file
|
||||
`showtime.py` in your `commands` directory and paste this code in it):
|
||||
|
||||
```python
|
||||
# in a file mygame/commands/gametime.py
|
||||
|
||||
from evennia.contrib import custom_gametime
|
||||
|
||||
from commands.command import Command
|
||||
|
||||
class CmdTime(Command):
|
||||
|
||||
"""
|
||||
Display the time.
|
||||
|
||||
Syntax:
|
||||
time
|
||||
|
||||
"""
|
||||
|
||||
key = "time"
|
||||
locks = "cmd:all()"
|
||||
|
||||
def func(self):
|
||||
"""Execute the time command."""
|
||||
# Get the absolute game time
|
||||
year, month, day, hour, min, sec = custom_gametime.custom_gametime(absolute=True)
|
||||
string = "We are in year {year}, day {day}, month {month}."
|
||||
string += "\nIt's {hour:02}:{min:02}:{sec:02}."
|
||||
self.msg(string.format(year=year, month=month, day=day,
|
||||
hour=hour, min=min, sec=sec))
|
||||
```
|
||||
|
||||
Don't forget to add it in your CharacterCmdSet to see this command:
|
||||
|
||||
```python
|
||||
# in mygame/commands/default_cmdset.py
|
||||
|
||||
from commands.gametime import CmdTime # <-- Add
|
||||
|
||||
# ...
|
||||
|
||||
class CharacterCmdSet(default_cmds.CharacterCmdSet):
|
||||
"""
|
||||
The `CharacterCmdSet` contains general in-game commands like `look`,
|
||||
`get`, etc available on in-game Character objects. It is merged with
|
||||
the `AccountCmdSet` when an Account puppets a Character.
|
||||
"""
|
||||
key = "DefaultCharacter"
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
"""
|
||||
Populates the cmdset
|
||||
"""
|
||||
super().at_cmdset_creation()
|
||||
# ...
|
||||
self.add(CmdTime()) # <- Add
|
||||
```
|
||||
|
||||
Reload your game with the `@reload` command. You should now see the `time` command. If you enter
|
||||
it, you might see something like:
|
||||
|
||||
We are in year 0, day 0, month 0.
|
||||
It's 00:52:17.
|
||||
|
||||
You could display it a bit more prettily with names for months and perhaps even days, if you want.
|
||||
And if "months" are called "moons" in your game, this is where you'd add that.
|
||||
|
||||
#### Time-related events in custom gametime
|
||||
|
||||
The `custom_gametime` module also has a way to schedule game-related events, taking into account
|
||||
your game time (and your custom calendar). It can be used to have a specific message every day at
|
||||
6:00 AM, to show the sun rises, for instance. The `custom_gametime.schedule` function works in the
|
||||
same way as described for the default one above.
|
||||
467
docs/source/Howto/Help-System-Tutorial.md
Normal file
467
docs/source/Howto/Help-System-Tutorial.md
Normal file
|
|
@ -0,0 +1,467 @@
|
|||
# Help System Tutorial
|
||||
|
||||
|
||||
**Before doing this tutorial you will probably want to read the intro in [Basic Web tutorial](Web-
|
||||
Tutorial).** Reading the three first parts of the [Django
|
||||
tutorial](https://docs.djangoproject.com/en/1.9/intro/tutorial01/) might help as well.
|
||||
|
||||
This tutorial will show you how to access the help system through your website. Both help commands
|
||||
and regular help entries will be visible, depending on the logged-in user or an anonymous character.
|
||||
|
||||
This tutorial will show you how to:
|
||||
|
||||
- Create a new page to add to your website.
|
||||
- Take advantage of a basic view and basic templates.
|
||||
- Access the help system on your website.
|
||||
- Identify whether the viewer of this page is logged-in and, if so, to what account.
|
||||
|
||||
## Creating our app
|
||||
|
||||
The first step is to create our new Django *app*. An app in Django can contain pages and
|
||||
mechanisms: your website may contain different apps. Actually, the website provided out-of-the-box
|
||||
by Evennia has already three apps: a "webclient" app, to handle the entire webclient, a "website"
|
||||
app to contain your basic pages, and a third app provided by Django to create a simple admin
|
||||
interface. So we'll create another app in parallel, giving it a clear name to represent our help
|
||||
system.
|
||||
|
||||
From your game directory, use the following command:
|
||||
|
||||
evennia startapp help_system
|
||||
|
||||
> Note: calling the app "help" would have been more explicit, but this name is already used by
|
||||
Django.
|
||||
|
||||
This will create a directory named `help_system` at the root of your game directory. It's a good
|
||||
idea to keep things organized and move this directory in the "web" directory of your game. Your
|
||||
game directory should look like:
|
||||
|
||||
mygame/
|
||||
...
|
||||
web/
|
||||
help_system/
|
||||
...
|
||||
|
||||
The "web/help_system" directory contains files created by Django. We'll use some of them, but if
|
||||
you want to learn more about them all, you should read [the Django
|
||||
tutorial](https://docs.djangoproject.com/en/1.9/intro/tutorial01/).
|
||||
|
||||
There is a last thing to be done: your folder has been added, but Django doesn't know about it, it
|
||||
doesn't know it's a new app. We need to tell it, and we do so by editing a simple setting. Open
|
||||
your "server/conf/settings.py" file and add, or edit, these lines:
|
||||
|
||||
```python
|
||||
# Web configuration
|
||||
INSTALLED_APPS += (
|
||||
"web.help_system",
|
||||
)
|
||||
```
|
||||
|
||||
You can start Evennia if you want, and go to your website, probably at
|
||||
[http://localhost:4001](http://localhost:4001) . You won't see anything different though: we added
|
||||
the app but it's fairly empty.
|
||||
|
||||
## Our new page
|
||||
|
||||
At this point, our new *app* contains mostly empty files that you can explore. In order to create
|
||||
a page for our help system, we need to add:
|
||||
|
||||
- A *view*, dealing with the logic of our page.
|
||||
- A *template* to display our new page.
|
||||
- A new *URL* pointing to our page.
|
||||
|
||||
> We could get away by creating just a view and a new URL, but that's not a recommended way to work
|
||||
with your website. Building on templates is so much more convenient.
|
||||
|
||||
### Create a view
|
||||
|
||||
A *view* in Django is a simple Python function placed in the "views.py" file in your app. It will
|
||||
handle the behavior that is triggered when a user asks for this information by entering a *URL* (the
|
||||
connection between *views* and *URLs* will be discussed later).
|
||||
|
||||
So let's create our view. You can open the "web/help_system/view.py" file and paste the following
|
||||
lines:
|
||||
|
||||
```python
|
||||
from django.shortcuts import render
|
||||
|
||||
def index(request):
|
||||
"""The 'index' view."""
|
||||
return render(request, "help_system/index.html")
|
||||
```
|
||||
|
||||
Our view handles all code logic. This time, there's not much: when this function is called, it will
|
||||
render the template we will now create. But that's where we will do most of our work afterward.
|
||||
|
||||
### Create a template
|
||||
|
||||
The `render` function called into our *view* asks the *template* `help_system/index.html`. The
|
||||
*templates* of our apps are stored in the app directory, "templates" sub-directory. Django may have
|
||||
created the "templates" folder already. If not, create it yourself. In it, create another folder
|
||||
"help_system", and inside of this folder, create a file named "index.html". Wow, that's some
|
||||
hierarchy. Your directory structure (starting from `web`) should look like this:
|
||||
|
||||
web/
|
||||
help_system/
|
||||
...
|
||||
templates/
|
||||
help_system/
|
||||
index.html
|
||||
|
||||
Open the "index.html" file and paste in the following lines:
|
||||
|
||||
```
|
||||
{% extends "base.html" %}
|
||||
{% block titleblock %}Help index{% endblock %}
|
||||
{% block content %}
|
||||
<h2>Help index</h2>
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
Here's a little explanation line by line of what this template does:
|
||||
|
||||
1. It loads the "base.html" *template*. This describes the basic structure of all your pages, with
|
||||
a menu at the top and a footer, and perhaps other information like images and things to be present
|
||||
on each page. You can create templates that do not inherit from "base.html", but you should have a
|
||||
good reason for doing so.
|
||||
2. The "base.html" *template* defines all the structure of the page. What is left is to override
|
||||
some sections of our pages. These sections are called *blocks*. On line 2, we override the block
|
||||
named "blocktitle", which contains the title of our page.
|
||||
3. Same thing here, we override the *block* named "content", which contains the main content of our
|
||||
web page. This block is bigger, so we define it on several lines.
|
||||
4. This is perfectly normal HTML code to display a level-2 heading.
|
||||
5. And finally we close the *block* named "content".
|
||||
|
||||
### Create a new URL
|
||||
|
||||
Last step to add our page: we need to add a *URL* leading to it... otherwise users won't be able to
|
||||
access it. The URLs of our apps are stored in the app's directory "urls.py" file.
|
||||
|
||||
Open the "web/help_system/urls.py" file (you might have to create it) and write in it:
|
||||
|
||||
```python
|
||||
# URL patterns for the help_system app
|
||||
|
||||
from django.conf.urls import url
|
||||
from web.help_system.views import index
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^$', index, name="index")
|
||||
]
|
||||
```
|
||||
|
||||
We also need to add our app as a namespace holder for URLS. Edit the file "web/urls.py". In it you
|
||||
will find the `custom_patterns` variable. Replace it with:
|
||||
|
||||
```python
|
||||
custom_patterns = [
|
||||
url(r'^help/', include('web.help_system.urls',
|
||||
namespace='help_system', app_name='help_system')),
|
||||
]
|
||||
```
|
||||
|
||||
When a user will ask for a specific *URL* on your site, Django will:
|
||||
|
||||
1. Read the list of custom patterns defined in "web/urls.py". There's one pattern here, which
|
||||
describes to Django that all URLs beginning by 'help/' should be sent to the 'help_system' app. The
|
||||
'help/' part is removed.
|
||||
2. Then Django will check the "web.help_system/urls.py" file. It contains only one URL, which is
|
||||
empty (`^$`).
|
||||
|
||||
In other words, if the URL is '/help/', then Django will execute our defined view.
|
||||
|
||||
### Let's see it work
|
||||
|
||||
You can now reload or start Evennia. Open a tab in your browser and go to
|
||||
[http://localhost:4001/help/](http://localhost:4001/help/) . If everything goes well, you should
|
||||
see your new page... which isn't empty since Evennia uses our "base.html" *template*. In the
|
||||
content of our page, there's only a heading that reads "help index". Notice that the title of our
|
||||
page is "mygame - Help index" ("mygame" is replaced by the name of your game).
|
||||
|
||||
From now on, it will be easier to move forward and add features.
|
||||
|
||||
### A brief reminder
|
||||
|
||||
We'll be trying the following things:
|
||||
|
||||
- Have the help of commands and help entries accessed online.
|
||||
- Have various commands and help entries depending on whether the user is logged in or not.
|
||||
|
||||
In terms of pages, we'll have:
|
||||
|
||||
- One to display the list of help topics.
|
||||
- One to display the content of a help topic.
|
||||
|
||||
The first one would link to the second.
|
||||
|
||||
> Should we create two URLs?
|
||||
|
||||
The answer is... maybe. It depends on what you want to do. We have our help index accessible
|
||||
through the "/help/" URL. We could have the detail of a help entry accessible through "/help/desc"
|
||||
(to see the detail of the "desc" command). The problem is that our commands or help topics may
|
||||
contain special characters that aren't to be present in URLs. There are different ways around this
|
||||
problem. I have decided to use a *GET variable* here, which would create URLs like this:
|
||||
|
||||
/help?name=desc
|
||||
|
||||
If you use this system, you don't have to add a new URL: GET and POST variables are accessible
|
||||
through our requests and we'll see how soon enough.
|
||||
|
||||
## Handling logged-in users
|
||||
|
||||
One of our requirements is to have a help system tailored to our accounts. If an account with admin
|
||||
access logs in, the page should display a lot of commands that aren't accessible to common users.
|
||||
And perhaps even some additional help topics.
|
||||
|
||||
Fortunately, it's fairly easy to get the logged in account in our view (remember that we'll do most
|
||||
of our coding there). The *request* object, passed to our function, contains a `user` attribute.
|
||||
This attribute will always be there: we cannot test whether it's `None` or not, for instance. But
|
||||
when the request comes from a user that isn't logged in, the `user` attribute will contain an
|
||||
anonymous Django user. We then can use the `is_anonymous` method to see whether the user is logged-
|
||||
in or not. Last gift by Evennia, if the user is logged in, `request.user` contains a reference to
|
||||
an account object, which will help us a lot in coupling the game and online system.
|
||||
|
||||
So we might end up with something like:
|
||||
|
||||
```python
|
||||
def index(request):
|
||||
"""The 'index' view."""
|
||||
user = request.user
|
||||
if not user.is_anonymous() and user.character:
|
||||
character = user.character
|
||||
```
|
||||
|
||||
> Note: this code works when your MULTISESSION_MODE is set to 0 or 1. When it's above, you would
|
||||
have something like:
|
||||
|
||||
```python
|
||||
def index(request):
|
||||
"""The 'index' view."""
|
||||
user = request.user
|
||||
if not user.is_anonymous() and user.db._playable_characters:
|
||||
character = user.db._playable_characters[0]
|
||||
```
|
||||
|
||||
In this second case, it will select the first character of the account.
|
||||
|
||||
But what if the user's not logged in? Again, we have different solutions. One of the most simple
|
||||
is to create a character that will behave as our default character for the help system. You can
|
||||
create it through your game: connect to it and enter:
|
||||
|
||||
@charcreate anonymous
|
||||
|
||||
The system should answer:
|
||||
|
||||
Created new character anonymous. Use @ic anonymous to enter the game as this character.
|
||||
|
||||
So in our view, we could have something like this:
|
||||
|
||||
```python
|
||||
from typeclasses.characters import Character
|
||||
|
||||
def index(request):
|
||||
"""The 'index' view."""
|
||||
user = request.user
|
||||
if not user.is_anonymous() and user.character:
|
||||
character = user.character
|
||||
else:
|
||||
character = Character.objects.get(db_key="anonymous")
|
||||
```
|
||||
|
||||
This time, we have a valid character no matter what: remember to adapt this code if you're running
|
||||
in multisession mode above 1.
|
||||
|
||||
## The full system
|
||||
|
||||
What we're going to do is to browse through all commands and help entries, and list all the commands
|
||||
that can be seen by this character (either our 'anonymous' character, or our logged-in character).
|
||||
|
||||
The code is longer, but it presents the entire concept in our view. Edit the
|
||||
"web/help_system/views.py" file and paste into it:
|
||||
|
||||
```python
|
||||
from django.http import Http404
|
||||
from django.shortcuts import render
|
||||
from evennia.help.models import HelpEntry
|
||||
|
||||
from typeclasses.characters import Character
|
||||
|
||||
def index(request):
|
||||
"""The 'index' view."""
|
||||
user = request.user
|
||||
if not user.is_anonymous() and user.character:
|
||||
character = user.character
|
||||
else:
|
||||
character = Character.objects.get(db_key="anonymous")
|
||||
|
||||
# Get the categories and topics accessible to this character
|
||||
categories, topics = _get_topics(character)
|
||||
|
||||
# If we have the 'name' in our GET variable
|
||||
topic = request.GET.get("name")
|
||||
if topic:
|
||||
if topic not in topics:
|
||||
raise Http404("This help topic doesn't exist.")
|
||||
|
||||
topic = topics[topic]
|
||||
context = {
|
||||
"character": character,
|
||||
"topic": topic,
|
||||
}
|
||||
return render(request, "help_system/detail.html", context)
|
||||
else:
|
||||
context = {
|
||||
"character": character,
|
||||
"categories": categories,
|
||||
}
|
||||
return render(request, "help_system/index.html", context)
|
||||
|
||||
def _get_topics(character):
|
||||
"""Return the categories and topics for this character."""
|
||||
cmdset = character.cmdset.all()[0]
|
||||
commands = cmdset.commands
|
||||
entries = [entry for entry in HelpEntry.objects.all()]
|
||||
categories = {}
|
||||
topics = {}
|
||||
|
||||
# Browse commands
|
||||
for command in commands:
|
||||
if not command.auto_help or not command.access(character):
|
||||
continue
|
||||
|
||||
# Create the template for a command
|
||||
template = {
|
||||
"name": command.key,
|
||||
"category": command.help_category,
|
||||
"content": command.get_help(character, cmdset),
|
||||
}
|
||||
|
||||
category = command.help_category
|
||||
if category not in categories:
|
||||
categories[category] = []
|
||||
categories[category].append(template)
|
||||
topics[command.key] = template
|
||||
|
||||
# Browse through the help entries
|
||||
for entry in entries:
|
||||
if not entry.access(character, 'view', default=True):
|
||||
continue
|
||||
|
||||
# Create the template for an entry
|
||||
template = {
|
||||
"name": entry.key,
|
||||
"category": entry.help_category,
|
||||
"content": entry.entrytext,
|
||||
}
|
||||
|
||||
category = entry.help_category
|
||||
if category not in categories:
|
||||
categories[category] = []
|
||||
categories[category].append(template)
|
||||
topics[entry.key] = template
|
||||
|
||||
# Sort categories
|
||||
for entries in categories.values():
|
||||
entries.sort(key=lambda c: c["name"])
|
||||
|
||||
categories = list(sorted(categories.items()))
|
||||
return categories, topics
|
||||
```
|
||||
|
||||
That's a bit more complicated here, but all in all, it can be divided in small chunks:
|
||||
|
||||
- The `index` function is our view:
|
||||
- It begins by getting the character as we saw in the previous section.
|
||||
- It gets the help topics (commands and help entries) accessible to this character. It's another
|
||||
function that handles that part.
|
||||
- If there's a *GET variable* "name" in our URL (like "/help?name=drop"), it will retrieve it. If
|
||||
it's not a valid topic's name, it returns a *404*. Otherwise, it renders the template called
|
||||
"detail.html", to display the detail of our topic.
|
||||
- If there's no *GET variable* "name", render "index.html", to display the list of topics.
|
||||
- The `_get_topics` is a private function. Its sole mission is to retrieve the commands a character
|
||||
can execute, and the help entries this same character can see. This code is more Evennia-specific
|
||||
than Django-specific, it will not be detailed in this tutorial. Just notice that all help topics
|
||||
are stored in a dictionary. This is to simplify our job when displaying them in our templates.
|
||||
|
||||
Notice that, in both cases when we asked to render a *template*, we passed to `render` a third
|
||||
argument which is the dictionary of variables used in our templates. We can pass variables this
|
||||
way, and we will use them in our templates.
|
||||
|
||||
### The index template
|
||||
|
||||
Let's look at our full "index" *template*. You can open the
|
||||
"web/help_system/templates/help_sstem/index.html" file and paste the following into it:
|
||||
|
||||
```
|
||||
{% extends "base.html" %}
|
||||
{% block titleblock %}Help index{% endblock %}
|
||||
{% block content %}
|
||||
<h2>Help index</h2>
|
||||
{% if categories %}
|
||||
{% for category, topics in categories %}
|
||||
<h2>{{ category|capfirst }}</h2>
|
||||
<table>
|
||||
<tr>
|
||||
{% for topic in topics %}
|
||||
{% if forloop.counter|divisibleby:"5" %}
|
||||
</tr>
|
||||
<tr>
|
||||
{% endif %}
|
||||
<td><a href="{% url 'help_system:index' %}?name={{ topic.name|urlencode }}">
|
||||
{{ topic.name }}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</table>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
This template is definitely more detailed. What it does is:
|
||||
|
||||
1. Browse through all categories.
|
||||
2. For all categories, display a level-2 heading with the name of the category.
|
||||
3. All topics in a category (remember, they can be either commands or help entries) are displayed in
|
||||
a table. The trickier part may be that, when the loop is above 5, it will create a new line. The
|
||||
table will have 5 columns at the most per row.
|
||||
4. For every cell in the table, we create a link redirecting to the detail page (see below). The
|
||||
URL would look something like "help?name=say". We use `urlencode` to ensure special characters are
|
||||
properly escaped.
|
||||
|
||||
### The detail template
|
||||
|
||||
It's now time to show the detail of a topic (command or help entry). You can create the file
|
||||
"web/help_system/templates/help_system/detail.html". You can paste into it the following code:
|
||||
|
||||
```
|
||||
{% extends "base.html" %}
|
||||
{% block titleblock %}Help for {{ topic.name }}{% endblock %}
|
||||
{% block content %}
|
||||
<h2>{{ topic.name|capfirst }} help topic</h2>
|
||||
<p>Category: {{ topic.category|capfirst }}</p>
|
||||
{{ topic.content|linebreaks }}
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
This template is much easier to read. Some *filters* might be unknown to you, but they are just
|
||||
used to format here.
|
||||
|
||||
### Put it all together
|
||||
|
||||
Remember to reload or start Evennia, and then go to
|
||||
[http://localhost:4001/help](http://localhost:4001/help/). You should see the list of commands and
|
||||
topics accessible by all characters. Try to login (click the "login" link in the menu of your
|
||||
website) and go to the same page again. You should now see a more detailed list of commands and
|
||||
help entries. Click on one to see its detail.
|
||||
|
||||
## To improve this feature
|
||||
|
||||
As always, a tutorial is here to help you feel comfortable adding new features and code by yourself.
|
||||
Here are some ideas of things to improve this little feature:
|
||||
|
||||
- Links at the bottom of the detail template to go back to the index might be useful.
|
||||
- A link in the main menu to link to this page would be great... for the time being you have to
|
||||
enter the URL, users won't guess it's there.
|
||||
- Colors aren't handled at this point, which isn't exactly surprising. You could add it though.
|
||||
- Linking help entries between one another won't be simple, but it would be great. For instance, if
|
||||
you see a help entry about how to use several commands, it would be great if these commands were
|
||||
themselves links to display their details.
|
||||
265
docs/source/Howto/Implementing-a-game-rule-system.md
Normal file
265
docs/source/Howto/Implementing-a-game-rule-system.md
Normal file
|
|
@ -0,0 +1,265 @@
|
|||
# Implementing a game rule system
|
||||
|
||||
|
||||
The simplest way to create an online roleplaying game (at least from a code perspective) is to
|
||||
simply grab a paperback RPG rule book, get a staff of game masters together and start to run scenes
|
||||
with whomever logs in. Game masters can roll their dice in front of their computers and tell the
|
||||
players the results. This is only one step away from a traditional tabletop game and puts heavy
|
||||
demands on the staff - it is unlikely staff will be able to keep up around the clock even if they
|
||||
are very dedicated.
|
||||
|
||||
Many games, even the most roleplay-dedicated, thus tend to allow for players to mediate themselves
|
||||
to some extent. A common way to do this is to introduce *coded systems* - that is, to let the
|
||||
computer do some of the heavy lifting. A basic thing is to add an online dice-roller so everyone can
|
||||
make rolls and make sure noone is cheating. Somewhere at this level you find the most bare-bones
|
||||
roleplaying MUSHes.
|
||||
|
||||
The advantage of a coded system is that as long as the rules are fair the computer is too - it makes
|
||||
no judgement calls and holds no personal grudges (and cannot be accused of holding any). Also, the
|
||||
computer doesn't need to sleep and can always be online regardless of when a player logs on. The
|
||||
drawback is that a coded system is not flexible and won't adapt to the unprogrammed actions human
|
||||
players may come up with in role play. For this reason many roleplay-heavy MUDs do a hybrid
|
||||
variation - they use coded systems for things like combat and skill progression but leave role play
|
||||
to be mostly freeform, overseen by staff game masters.
|
||||
|
||||
Finally, on the other end of the scale are less- or no-roleplay games, where game mechanics (and
|
||||
thus player fairness) is the most important aspect. In such games the only events with in-game value
|
||||
are those resulting from code. Such games are very common and include everything from hack-and-slash
|
||||
MUDs to various tactical simulations.
|
||||
|
||||
So your first decision needs to be just what type of system you are aiming for. This page will try
|
||||
to give some ideas for how to organize the "coded" part of your system, however big that may be.
|
||||
|
||||
## Overall system infrastructure
|
||||
|
||||
We strongly recommend that you code your rule system as stand-alone as possible. That is, don't
|
||||
spread your skill check code, race bonus calculation, die modifiers or what have you all over your
|
||||
game.
|
||||
|
||||
- Put everything you would need to look up in a rule book into a module in `mygame/world`. Hide away
|
||||
as much as you can. Think of it as a black box (or maybe the code representation of an all-knowing
|
||||
game master). The rest of your game will ask this black box questions and get answers back. Exactly
|
||||
how it arrives at those results should not need to be known outside the box. Doing it this way
|
||||
makes it easier to change and update things in one place later.
|
||||
- Store only the minimum stuff you need with each game object. That is, if your Characters need
|
||||
values for Health, a list of skills etc, store those things on the Character - don't store how to
|
||||
roll or change them.
|
||||
- Next is to determine just how you want to store things on your Objects and Characters. You can
|
||||
choose to either store things as individual [Attributes](Attributes), like `character.db.STR=34` and
|
||||
`character.db.Hunting_skill=20`. But you could also use some custom storage method, like a
|
||||
dictionary `character.db.skills = {"Hunting":34, "Fishing":20, ...}`. A much more fancy solution is
|
||||
to look at the Ainneve [Trait
|
||||
handler](https://github.com/evennia/ainneve/blob/master/world/traits.py). Finally you could even go
|
||||
with a [custom django model](New-Models). Which is the better depends on your game and the
|
||||
complexity of your system.
|
||||
- Make a clear [API](http://en.wikipedia.org/wiki/Application_programming_interface) into your
|
||||
rules. That is, make methods/functions that you feed with, say, your Character and which skill you
|
||||
want to check. That is, you want something similar to this:
|
||||
|
||||
```python
|
||||
from world import rules
|
||||
result = rules.roll_skill(character, "hunting")
|
||||
result = rules.roll_challenge(character1, character2, "swords")
|
||||
```
|
||||
|
||||
You might need to make these functions more or less complex depending on your game. For example the
|
||||
properties of the room might matter to the outcome of a roll (if the room is dark, burning etc).
|
||||
Establishing just what you need to send into your game mechanic module is a great way to also get a
|
||||
feel for what you need to add to your engine.
|
||||
|
||||
## Coded systems
|
||||
|
||||
Inspired by tabletop role playing games, most game systems mimic some sort of die mechanic. To this
|
||||
end Evennia offers a full [dice
|
||||
roller](https://github.com/evennia/evennia/blob/master/evennia/contrib/dice.py) in its `contrib`
|
||||
folder. For custom implementations, Python offers many ways to randomize a result using its in-built
|
||||
`random` module. No matter how it's implemented, we will in this text refer to the action of
|
||||
determining an outcome as a "roll".
|
||||
|
||||
In a freeform system, the result of the roll is just compared with values and people (or the game
|
||||
master) just agree on what it means. In a coded system the result now needs to be processed somehow.
|
||||
There are many things that may happen as a result of rule enforcement:
|
||||
|
||||
- Health may be added or deducted. This can effect the character in various ways.
|
||||
- Experience may need to be added, and if a level-based system is used, the player might need to be
|
||||
informed they have increased a level.
|
||||
- Room-wide effects need to be reported to the room, possibly affecting everyone in the room.
|
||||
|
||||
There are also a slew of other things that fall under "Coded systems", including things like
|
||||
weather, NPC artificial intelligence and game economy. Basically everything about the world that a
|
||||
Game master would control in a tabletop role playing game can be mimicked to some level by coded
|
||||
systems.
|
||||
|
||||
|
||||
## Example of Rule module
|
||||
|
||||
Here is a simple example of a rule module. This is what we assume about our simple example game:
|
||||
- Characters have only four numerical values:
|
||||
- Their `level`, which starts at 1.
|
||||
- A skill `combat`, which determines how good they are at hitting things. Starts between 5 and
|
||||
10.
|
||||
- Their Strength, `STR`, which determine how much damage they do. Starts between 1 and 10.
|
||||
- Their Health points, `HP`, which starts at 100.
|
||||
- When a Character reaches `HP = 0`, they are presumed "defeated". Their HP is reset and they get a
|
||||
failure message (as a stand-in for death code).
|
||||
- Abilities are stored as simple Attributes on the Character.
|
||||
- "Rolls" are done by rolling a 100-sided die. If the result is below the `combat` value, it's a
|
||||
success and damage is rolled. Damage is rolled as a six-sided die + the value of `STR` (for this
|
||||
example we ignore weapons and assume `STR` is all that matters).
|
||||
- Every successful `attack` roll gives 1-3 experience points (`XP`). Every time the number of `XP`
|
||||
reaches `(level + 1) ** 2`, the Character levels up. When leveling up, the Character's `combat`
|
||||
value goes up by 2 points and `STR` by one (this is a stand-in for a real progression system).
|
||||
|
||||
### Character
|
||||
|
||||
The Character typeclass is simple. It goes in `mygame/typeclasses/characters.py`. There is already
|
||||
an empty `Character` class there that Evennia will look to and use.
|
||||
|
||||
```python
|
||||
from random import randint
|
||||
from evennia import DefaultCharacter
|
||||
|
||||
class Character(DefaultCharacter):
|
||||
"""
|
||||
Custom rule-restricted character. We randomize
|
||||
the initial skill and ability values bettween 1-10.
|
||||
"""
|
||||
def at_object_creation(self):
|
||||
"Called only when first created"
|
||||
self.db.level = 1
|
||||
self.db.HP = 100
|
||||
self.db.XP = 0
|
||||
self.db.STR = randint(1, 10)
|
||||
self.db.combat = randint(5, 10)
|
||||
```
|
||||
|
||||
`@reload` the server to load up the new code. Doing `examine self` will however *not* show the new
|
||||
Attributes on yourself. This is because the `at_object_creation` hook is only called on *new*
|
||||
Characters. Your Character was already created and will thus not have them. To force a reload, use
|
||||
the following command:
|
||||
|
||||
```
|
||||
@typeclass/force/reset self
|
||||
```
|
||||
|
||||
The `examine self` command will now show the new Attributes.
|
||||
|
||||
### Rule module
|
||||
|
||||
This is a module `mygame/world/rules.py`.
|
||||
|
||||
```python
|
||||
from random import randint
|
||||
|
||||
def roll_hit():
|
||||
"Roll 1d100"
|
||||
return randint(1, 100)
|
||||
|
||||
def roll_dmg():
|
||||
"Roll 1d6"
|
||||
return randint(1, 6)
|
||||
|
||||
def check_defeat(character):
|
||||
"Checks if a character is 'defeated'."
|
||||
if character.db.HP <= 0:
|
||||
character.msg("You fall down, defeated!")
|
||||
character.db.HP = 100 # reset
|
||||
|
||||
def add_XP(character, amount):
|
||||
"Add XP to character, tracking level increases."
|
||||
character.db.XP += amount
|
||||
if character.db.XP >= (character.db.level + 1) ** 2:
|
||||
character.db.level += 1
|
||||
character.db.STR += 1
|
||||
character.db.combat += 2
|
||||
character.msg("You are now level %i!" % character.db.level)
|
||||
|
||||
def skill_combat(*args):
|
||||
"""
|
||||
This determines outcome of combat. The one who
|
||||
rolls under their combat skill AND higher than
|
||||
their opponent's roll hits.
|
||||
"""
|
||||
char1, char2 = args
|
||||
roll1, roll2 = roll_hit(), roll_hit()
|
||||
failtext = "You are hit by %s for %i damage!"
|
||||
wintext = "You hit %s for %i damage!"
|
||||
xp_gain = randint(1, 3)
|
||||
if char1.db.combat >= roll1 > roll2:
|
||||
# char 1 hits
|
||||
dmg = roll_dmg() + char1.db.STR
|
||||
char1.msg(wintext % (char2, dmg))
|
||||
add_XP(char1, xp_gain)
|
||||
char2.msg(failtext % (char1, dmg))
|
||||
char2.db.HP -= dmg
|
||||
check_defeat(char2)
|
||||
elif char2.db.combat >= roll2 > roll1:
|
||||
# char 2 hits
|
||||
dmg = roll_dmg() + char2.db.STR
|
||||
char1.msg(failtext % (char2, dmg))
|
||||
char1.db.HP -= dmg
|
||||
check_defeat(char1)
|
||||
char2.msg(wintext % (char1, dmg))
|
||||
add_XP(char2, xp_gain)
|
||||
else:
|
||||
# a draw
|
||||
drawtext = "Neither of you can find an opening."
|
||||
char1.msg(drawtext)
|
||||
char2.msg(drawtext)
|
||||
|
||||
SKILLS = {"combat": skill_combat}
|
||||
|
||||
def roll_challenge(character1, character2, skillname):
|
||||
"""
|
||||
Determine the outcome of a skill challenge between
|
||||
two characters based on the skillname given.
|
||||
"""
|
||||
if skillname in SKILLS:
|
||||
SKILLS[skillname](character1, character2)
|
||||
else:
|
||||
raise RunTimeError("Skillname %s not found." % skillname)
|
||||
```
|
||||
|
||||
These few functions implement the entirety of our simple rule system. We have a function to check
|
||||
the "defeat" condition and reset the `HP` back to 100 again. We define a generic "skill" function.
|
||||
Multiple skills could all be added with the same signature; our `SKILLS` dictionary makes it easy to
|
||||
look up the skills regardless of what their actual functions are called. Finally, the access
|
||||
function `roll_challenge` just picks the skill and gets the result.
|
||||
|
||||
In this example, the skill function actually does a lot - it not only rolls results, it also informs
|
||||
everyone of their results via `character.msg()` calls.
|
||||
|
||||
Here is an example of usage in a game command:
|
||||
|
||||
```python
|
||||
from evennia import Command
|
||||
from world import rules
|
||||
|
||||
class CmdAttack(Command):
|
||||
"""
|
||||
attack an opponent
|
||||
|
||||
Usage:
|
||||
attack <target>
|
||||
|
||||
This will attack a target in the same room, dealing
|
||||
damage with your bare hands.
|
||||
"""
|
||||
def func(self):
|
||||
"Implementing combat"
|
||||
|
||||
caller = self.caller
|
||||
if not self.args:
|
||||
caller.msg("You need to pick a target to attack.")
|
||||
return
|
||||
|
||||
target = caller.search(self.args)
|
||||
if target:
|
||||
rules.roll_challenge(caller, target, "combat")
|
||||
```
|
||||
|
||||
Note how simple the command becomes and how generic you can make it. It becomes simple to offer any
|
||||
number of Combat commands by just extending this functionality - you can easily roll challenges and
|
||||
pick different skills to check. And if you ever decided to, say, change how to determine hit chance,
|
||||
you don't have to change every command, but need only change the single `roll_hit` function inside
|
||||
your `rules` module.
|
||||
70
docs/source/Howto/Learn-Python-for-Evennia-The-Hard-Way.md
Normal file
70
docs/source/Howto/Learn-Python-for-Evennia-The-Hard-Way.md
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
# Learn Python for Evennia The Hard Way
|
||||
|
||||
# WORK IN PROGRESS - DO NOT USE
|
||||
|
||||
Evennia provides a great foundation to build your very own MU* whether you have programming
|
||||
experience or none at all. Whilst Evennia has a number of in-game building commands and tutorials
|
||||
available to get you started, when approaching game systems of any complexity it is advisable to
|
||||
have the basics of Python under your belt before jumping into the code. There are many Python
|
||||
tutorials freely available online however this page focuses on Learn Python the Hard Way (LPTHW) by
|
||||
Zed Shaw. This tutorial takes you through the basics of Python and progresses you to creating your
|
||||
very own online text based game. Whilst completing the course feel free to install Evennia and try
|
||||
out some of our beginner tutorials. On completion you can return to this page, which will act as an
|
||||
overview to the concepts separating your online text based game and the inner-workings of Evennia.
|
||||
-The latter portion of the tutorial focuses working your engine into a webpage and is not strictly
|
||||
required for development in Evennia.
|
||||
|
||||
## Exercise 23
|
||||
You may have returned here when you were invited to read some code. If you haven’t already, you
|
||||
should now have the knowledge necessary to install Evennia. Head over to the Getting Started page
|
||||
for install instructions. You can also try some of our tutorials to get you started on working with
|
||||
Evennia.
|
||||
|
||||
## Bridging the gap.
|
||||
If you have successfully completed the Learn Python the Hard Way tutorial you should now have a
|
||||
simple browser based Interactive Fiction engine which looks similar to this.
|
||||
This engine is built using a single interactive object type, the Room class. The Room class holds a
|
||||
description of itself that is presented to the user and a list of hardcoded commands which if
|
||||
selected correctly will present you with the next rooms’ description and commands. Whilst your game
|
||||
only has one interactive object, MU* have many more: Swords and shields, potions and scrolls or even
|
||||
laser guns and robots. Even the player has an in-game representation in the form of your character.
|
||||
Each of these examples are represented by their own object with their own description that can be
|
||||
presented to the user.
|
||||
|
||||
A basic object in Evennia has a number of default functions but perhaps most important is the idea
|
||||
of location. In your text engine you receive a description of a room but you are not really in the
|
||||
room because you have no in-game representation. However, in Evennia when you enter a Dungeon you
|
||||
ARE in the dungeon. That is to say your character.location = Dungeon whilst the Dungeon.contents now
|
||||
has a spunky young adventurer added to it. In turn, your character.contents may have amongst it a
|
||||
number of swords and potions to help you on your adventure and their location would be you.
|
||||
|
||||
In reality each of these “objects” are just an entry in your Evennia projects database which keeps
|
||||
track of all these attributes, such as location and contents. Making changes to those attributes and
|
||||
the rules in which they are changed is the most fundamental perspective of how your game works. We
|
||||
define those rules in the objects Typeclass. The Typeclass is a Python class with a special
|
||||
connection to the games database which changes values for us through various class methods. Let’s
|
||||
look at your characters Typeclass rules for changing location.
|
||||
|
||||
1. `self.at_before_move(destination)` (if this returns False, move is aborted)
|
||||
2. `self.announce_move_from(destination)`
|
||||
3. (move happens here)
|
||||
4. `self.announce_move_to(source_location)`
|
||||
5. `self.at_after_move(source_location)`
|
||||
|
||||
First we check if it’s okay to leave our current location, then we tell everyone there that we’re
|
||||
leaving. We move locations and tell everyone at our new location that we’ve arrived before checking
|
||||
we’re okay to be there. By default stages 1 and 5 are empty ready for us to add some rules. We’ll
|
||||
leave an explanation as to how to make those changes for the tutorial section, but imagine if you
|
||||
were an astronaut. A smart astronaut might stop at step 1 to remember to put his helmet on whilst a
|
||||
slower astronaut might realise he’s forgotten in step 5 before shortly after ceasing to be an
|
||||
astronaut.
|
||||
|
||||
With all these objects and all this moving around it raises another problem. In your text engine the
|
||||
commands available to the player were hard-coded to the room. That means if we have commands we
|
||||
always want available to the player we’ll need to have those commands hard-coded on every single
|
||||
room. What about an armoury? When all the swords are gone the command to take a sword would still
|
||||
remain causing confusion. Evennia solves this problem by giving each object the ability to hold
|
||||
commands. Rooms can have commands attached to them specific to that location, like climbing a tree;
|
||||
Players can have commands which are always available to them, like ‘look’, ‘get’ and ‘say’; and
|
||||
objects can have commands attached to them which unlock when taking possession of it, like attack
|
||||
commands when obtaining a weapon.
|
||||
169
docs/source/Howto/Manually-Configuring-Color.md
Normal file
169
docs/source/Howto/Manually-Configuring-Color.md
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
# Manually Configuring Color
|
||||
|
||||
|
||||
This is a small tutorial for customizing your character objects, using the example of letting users
|
||||
turn on and off ANSI color parsing as an example. `@options NOCOLOR=True` will now do what this
|
||||
tutorial shows, but the tutorial subject can be applied to other toggles you may want, as well.
|
||||
|
||||
In the Building guide's [Colors](TextTags#coloured-text) page you can learn how to add color to your
|
||||
game by using special markup. Colors enhance the gaming experience, but not all users want color.
|
||||
Examples would be users working from clients that don't support color, or people with various seeing
|
||||
disabilities that rely on screen readers to play your game. Also, whereas Evennia normally
|
||||
automatically detects if a client supports color, it may get it wrong. Being able to turn it on
|
||||
manually if you know it **should** work could be a nice feature.
|
||||
|
||||
So here's how to allow those users to remove color. It basically means you implementing a simple
|
||||
configuration system for your characters. This is the basic sequence:
|
||||
|
||||
1. Define your own default character typeclass, inheriting from Evennia's default.
|
||||
1. Set an attribute on the character to control markup on/off.
|
||||
1. Set your custom character class to be the default for new accounts.
|
||||
1. Overload the `msg()` method on the typeclass and change how it uses markup.
|
||||
1. Create a custom command to allow users to change their setting.
|
||||
|
||||
## Setting up a custom Typeclass
|
||||
|
||||
Create a new module in `mygame/typeclasses` named, for example, `mycharacter.py`. Alternatively you
|
||||
can simply add a new class to 'mygamegame/typeclasses/characters.py'.
|
||||
|
||||
In your new module(or characters.py), create a new [Typeclass](Typeclasses) inheriting from
|
||||
`evennia.DefaultCharacter`. We will also import `evennia.utils.ansi`, which we will use later.
|
||||
|
||||
```python
|
||||
from evennia import Character
|
||||
from evennia.utils import ansi
|
||||
|
||||
class ColorableCharacter(Character):
|
||||
at_object_creation(self):
|
||||
# set a color config value
|
||||
self.db.config_color = True
|
||||
```
|
||||
|
||||
Above we set a simple config value as an [Attribute](Attributes).
|
||||
|
||||
Let's make sure that new characters are created of this type. Edit your
|
||||
`mygame/server/conf/settings.py` file and add/change `BASE_CHARACTER_TYPECLASS` to point to your new
|
||||
character class. Observe that this will only affect *new* characters, not those already created. You
|
||||
have to convert already created characters to the new typeclass by using the `@typeclass` command
|
||||
(try on a secondary character first though, to test that everything works - you don't want to render
|
||||
your root user unusable!).
|
||||
|
||||
@typeclass/reset/force Bob = mycharacter.ColorableCharacter
|
||||
|
||||
`@typeclass` changes Bob's typeclass and runs all its creation hooks all over again. The `/reset`
|
||||
switch clears all attributes and properties back to the default for the new typeclass - this is
|
||||
useful in this case to avoid ending up with an object having a "mixture" of properties from the old
|
||||
typeclass and the new one. `/force` might be needed if you edit the typeclass and want to update the
|
||||
object despite the actual typeclass name not having changed.
|
||||
|
||||
## Overload the `msg()` method
|
||||
|
||||
Next we need to overload the `msg()` method. What we want is to check the configuration value before
|
||||
calling the main function. The original `msg` method call is seen in `evennia/objects/objects.py`
|
||||
and is called like this:
|
||||
|
||||
```python
|
||||
msg(self, text=None, from_obj=None, session=None, options=None, **kwargs):
|
||||
```
|
||||
|
||||
As long as we define a method on our custom object with the same name and keep the same number of
|
||||
arguments/keywords we will overload the original. Here's how it could look:
|
||||
|
||||
```python
|
||||
class ColorableCharacter(Character):
|
||||
# [...]
|
||||
msg(self, text=None, from_obj=None, session=None, options=None,
|
||||
**kwargs):
|
||||
"our custom msg()"
|
||||
if self.db.config_color is not None: # this would mean it was not set
|
||||
if not self.db.config_color:
|
||||
# remove the ANSI from the text
|
||||
text = ansi.strip_ansi(text)
|
||||
super().msg(text=text, from_obj=from_obj,
|
||||
session=session, **kwargs)
|
||||
```
|
||||
|
||||
Above we create a custom version of the `msg()` method. If the configuration Attribute is set, it
|
||||
strips the ANSI from the text it is about to send, and then calls the parent `msg()` as usual. You
|
||||
need to `@reload` before your changes become visible.
|
||||
|
||||
There we go! Just flip the attribute `config_color` to False and your users will not see any color.
|
||||
As superuser (assuming you use the Typeclass `ColorableCharacter`) you can test this with the `@py`
|
||||
command:
|
||||
|
||||
@py self.db.config_color = False
|
||||
|
||||
## Custom color config command
|
||||
|
||||
For completeness, let's add a custom command so users can turn off their color display themselves if
|
||||
they want.
|
||||
|
||||
In `mygame/commands`, create a new file, call it for example `configcmds.py` (it's likely that
|
||||
you'll want to add other commands for configuration down the line). You can also copy/rename the
|
||||
command template.
|
||||
|
||||
```python
|
||||
from evennia import Command
|
||||
|
||||
class CmdConfigColor(Command):
|
||||
"""
|
||||
Configures your color
|
||||
|
||||
Usage:
|
||||
@togglecolor on|off
|
||||
|
||||
This turns ANSI-colors on/off.
|
||||
Default is on.
|
||||
"""
|
||||
|
||||
key = "@togglecolor"
|
||||
aliases = ["@setcolor"]
|
||||
|
||||
def func(self):
|
||||
"implements the command"
|
||||
# first we must remove whitespace from the argument
|
||||
self.args = self.args.strip()
|
||||
if not self.args or not self.args in ("on", "off"):
|
||||
self.caller.msg("Usage: @setcolor on|off")
|
||||
return
|
||||
if self.args == "on":
|
||||
self.caller.db.config_color = True
|
||||
# send a message with a tiny bit of formatting, just for fun
|
||||
self.caller.msg("Color was turned |won|W.")
|
||||
else:
|
||||
self.caller.db.config_color = False
|
||||
self.caller.msg("Color was turned off.")
|
||||
```
|
||||
|
||||
Lastly, we make this command available to the user by adding it to the default `CharacterCmdSet` in
|
||||
`mygame/commands/default_cmdsets.py` and reloading the server. Make sure you also import the
|
||||
command:
|
||||
|
||||
```python
|
||||
from mygame.commands import configcmds
|
||||
class CharacterCmdSet(default_cmds.CharacterCmdSet):
|
||||
# [...]
|
||||
def at_cmdset_creation(self):
|
||||
"""
|
||||
Populates the cmdset
|
||||
"""
|
||||
super().at_cmdset_creation()
|
||||
#
|
||||
# any commands you add below will overload the default ones.
|
||||
#
|
||||
|
||||
# here is the only line that we edit
|
||||
self.add(configcmds.CmdConfigColor())
|
||||
```
|
||||
|
||||
## More colors
|
||||
|
||||
Apart from ANSI colors, Evennia also supports **Xterm256** colors (See [Colors](TextTags#colored-
|
||||
text)). The `msg()` method supports the `xterm256` keyword for manually activating/deactiving
|
||||
xterm256. It should be easy to expand the above example to allow players to customize xterm256
|
||||
regardless of if Evennia thinks their client supports it or not.
|
||||
|
||||
To get a better understanding of how `msg()` works with keywords, you can try this as superuser:
|
||||
|
||||
@py self.msg("|123Dark blue with xterm256, bright blue with ANSI", xterm256=True)
|
||||
@py self.msg("|gThis should be uncolored", nomarkup=True)
|
||||
96
docs/source/Howto/Mass-and-weight-for-objects.md
Normal file
96
docs/source/Howto/Mass-and-weight-for-objects.md
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
# Mass and weight for objects
|
||||
|
||||
|
||||
An easy addition to add dynamic variety to your world objects is to give them some mass. Why mass
|
||||
and not weight? Weight varies in setting; for example things on the Moon weigh 1/6 as much. On
|
||||
Earth's surface and in most environments, no relative weight factor is needed.
|
||||
|
||||
In most settings, mass can be used as weight to spring a pressure plate trap or a floor giving way,
|
||||
determine a character's burden weight for travel speed... The total mass of an object can
|
||||
contribute to the force of a weapon swing, or a speeding meteor to give it a potential striking
|
||||
force.
|
||||
|
||||
#### Objects
|
||||
|
||||
Now that we have reasons for keeping track of object mass, let's look at the default object class
|
||||
inside your mygame/typeclasses/objects.py and see how easy it is to total up mass from an object and
|
||||
its contents.
|
||||
|
||||
```python
|
||||
# inside your mygame/typeclasses/objects.py
|
||||
|
||||
class Object(DefaultObject):
|
||||
# [...]
|
||||
def get_mass(self):
|
||||
mass = self.attributes.get('mass', 1) # Default objects have 1 unit mass.
|
||||
return mass + sum(obj.get_mass() for obj in self.contents)
|
||||
```
|
||||
|
||||
Adding the `get_mass` definition to the objects you want to sum up the masses for is done with
|
||||
Python's "sum" function which operates on all the contents, in this case by summing them to
|
||||
return a total mass value.
|
||||
|
||||
If you only wanted specific object types to have mass or have the new object type in a different
|
||||
module, see [[Adding-Object-Typeclass-Tutorial]] with its Heavy class object. You could set the
|
||||
default for Heavy types to something much larger than 1 gram or whatever unit you want to use. Any
|
||||
non-default mass would be stored on the `mass` [[Attributes]] of the objects.
|
||||
|
||||
|
||||
#### Characters and rooms
|
||||
|
||||
You can add a `get_mass` definition to characters and rooms, also.
|
||||
|
||||
If you were in a one metric-ton elevator with four other friends also wearing armor and carrying
|
||||
gold bricks, you might wonder if this elevator's going to move, and how fast.
|
||||
|
||||
Assuming the unit is grams and the elevator itself weights 1,000 kilograms, it would already be
|
||||
`@set elevator/mass=1000000`, we're `@set me/mass=85000` and our armor is `@set armor/mass=50000`.
|
||||
We're each carrying 20 gold bars each `@set gold bar/mass=12400` then step into the elevator and see
|
||||
the following message in the elevator's appearance: `Elevator weight and contents should not exceed
|
||||
3 metric tons.` Are we safe? Maybe not if you consider dynamic loading. But at rest:
|
||||
|
||||
```python
|
||||
# Elevator object knows when it checks itself:
|
||||
if self.get_mass() < 3000000:
|
||||
pass # Elevator functions as normal.
|
||||
else:
|
||||
pass # Danger! Alarm sounds, cable snaps, elevator stops...
|
||||
```
|
||||
|
||||
#### Inventory
|
||||
Example of listing mass of items in your inventory:
|
||||
|
||||
```python
|
||||
class CmdInventory(MuxCommand):
|
||||
"""
|
||||
view inventory
|
||||
Usage:
|
||||
inventory
|
||||
inv
|
||||
Switches:
|
||||
/weight to display all available channels.
|
||||
Shows your inventory: carrying, wielding, wearing, obscuring.
|
||||
"""
|
||||
|
||||
key = "inventory"
|
||||
aliases = ["inv", "i"]
|
||||
locks = "cmd:all()"
|
||||
|
||||
def func(self):
|
||||
"check inventory"
|
||||
items = self.caller.contents
|
||||
if not items:
|
||||
string = "You are not carrying anything."
|
||||
else:
|
||||
table = prettytable.PrettyTable(["name", "desc"])
|
||||
table.header = False
|
||||
table.border = False
|
||||
for item in items:
|
||||
second = item.get_mass() \
|
||||
if 'weight' in self.switches else item.db.desc
|
||||
table.add_row(["%s" % item.get_display_name(self.caller.sessions),
|
||||
second and second or ""])
|
||||
string = "|wYou are carrying:\n%s" % table
|
||||
self.caller.msg(string)
|
||||
|
||||
```
|
||||
334
docs/source/Howto/NPC-shop-Tutorial.md
Normal file
334
docs/source/Howto/NPC-shop-Tutorial.md
Normal file
|
|
@ -0,0 +1,334 @@
|
|||
# NPC shop Tutorial
|
||||
|
||||
This tutorial will describe how to make an NPC-run shop. We will make use of the [EvMenu](EvMenu)
|
||||
system to present shoppers with a menu where they can buy things from the store's stock.
|
||||
|
||||
Our shop extends over two rooms - a "front" room open to the shop's customers and a locked "store
|
||||
room" holding the wares the shop should be able to sell. We aim for the following features:
|
||||
|
||||
- The front room should have an Attribute `storeroom` that points to the store room.
|
||||
- Inside the front room, the customer should have a command `buy` or `browse`. This will open a
|
||||
menu listing all items available to buy from the store room.
|
||||
- A customer should be able to look at individual items before buying.
|
||||
- We use "gold" as an example currency. To determine cost, the system will look for an Attribute
|
||||
`gold_value` on the items in the store room. If not found, a fixed base value of 1 will be assumed.
|
||||
The wealth of the customer should be set as an Attribute `gold` on the Character. If not set, they
|
||||
have no gold and can't buy anything.
|
||||
- When the customer makes a purchase, the system will check the `gold_value` of the goods and
|
||||
compare it to the `gold` Attribute of the customer. If enough gold is available, this will be
|
||||
deducted and the goods transferred from the store room to the inventory of the customer.
|
||||
- We will lock the store room so that only people with the right key can get in there.
|
||||
|
||||
### The shop menu
|
||||
|
||||
We want to show a menu to the customer where they can list, examine and buy items in the store. This
|
||||
menu should change depending on what is currently for sale. Evennia's *EvMenu* utility will manage
|
||||
the menu for us. It's a good idea to [read up on EvMenu](EvMenu) if you are not familiar with it.
|
||||
|
||||
#### Designing the menu
|
||||
|
||||
The shopping menu's design is straightforward. First we want the main screen. You get this when you
|
||||
enter a shop and use the `browse` or `buy` command:
|
||||
|
||||
```
|
||||
*** Welcome to ye Old Sword shop! ***
|
||||
Things for sale (choose 1-3 to inspect, quit to exit):
|
||||
_________________________________________________________
|
||||
1. A rusty sword (5 gold)
|
||||
2. A sword with a leather handle (10 gold)
|
||||
3. Excalibur (100 gold)
|
||||
```
|
||||
|
||||
There are only three items to buy in this example but the menu should expand to however many items
|
||||
are needed. When you make a selection you will get a new screen showing the options for that
|
||||
particular item:
|
||||
|
||||
```
|
||||
You inspect A rusty sword:
|
||||
|
||||
This is an old weapon maybe once used by soldiers in some
|
||||
long forgotten army. It is rusty and in bad condition.
|
||||
__________________________________________________________
|
||||
1. Buy A rusty sword (5 gold)
|
||||
2. Look for something else.
|
||||
```
|
||||
|
||||
Finally, when you buy something, a brief message should pop up:
|
||||
|
||||
```
|
||||
You pay 5 gold and purchase A rusty sword!
|
||||
```
|
||||
or
|
||||
```
|
||||
You cannot afford 5 gold for A rusty sword!
|
||||
```
|
||||
After this you should be back to the top level of the shopping menu again and can continue browsing.
|
||||
|
||||
#### Coding the menu
|
||||
|
||||
EvMenu defines the *nodes* (each menu screen with options) as normal Python functions. Each node
|
||||
must be able to change on the fly depending on what items are currently for sale. EvMenu will
|
||||
automatically make the `quit` command available to us so we won't add that manually. For compactness
|
||||
we will put everything needed for our shop in one module, `mygame/typeclasses/npcshop.py`.
|
||||
|
||||
```python
|
||||
# mygame/typeclasses/npcshop.py
|
||||
|
||||
from evennia.utils import evmenu
|
||||
|
||||
def menunode_shopfront(caller):
|
||||
"This is the top-menu screen."
|
||||
|
||||
shopname = caller.location.key
|
||||
wares = caller.location.db.storeroom.contents
|
||||
|
||||
# Wares includes all items inside the storeroom, including the
|
||||
# door! Let's remove that from our for sale list.
|
||||
wares = [ware for ware in wares if ware.key.lower() != "door"]
|
||||
|
||||
text = "*** Welcome to %s! ***\n" % shopname
|
||||
if wares:
|
||||
text += " Things for sale (choose 1-%i to inspect);" \
|
||||
" quit to exit:" % len(wares)
|
||||
else:
|
||||
text += " There is nothing for sale; quit to exit."
|
||||
|
||||
options = []
|
||||
for ware in wares:
|
||||
# add an option for every ware in store
|
||||
options.append({"desc": "%s (%s gold)" %
|
||||
(ware.key, ware.db.gold_value or 1),
|
||||
"goto": "menunode_inspect_and_buy"})
|
||||
return text, options
|
||||
```
|
||||
|
||||
In this code we assume the caller to be *inside* the shop when accessing the menu. This means we can
|
||||
access the shop room via `caller.location` and get its `key` to display as the shop's name. We also
|
||||
assume the shop has an Attribute `storeroom` we can use to get to our stock. We loop over our goods
|
||||
to build up the menu's options.
|
||||
|
||||
Note that *all options point to the same menu node* called `menunode_inspect_and_buy`! We can't know
|
||||
which goods will be available to sale so we rely on this node to modify itself depending on the
|
||||
circumstances. Let's create it now.
|
||||
|
||||
```python
|
||||
# further down in mygame/typeclasses/npcshop.py
|
||||
|
||||
def menunode_inspect_and_buy(caller, raw_string):
|
||||
"Sets up the buy menu screen."
|
||||
|
||||
wares = caller.location.db.storeroom.contents
|
||||
# Don't forget, we will need to remove that pesky door again!
|
||||
wares = [ware for ware in wares if ware.key.lower() != "door"]
|
||||
iware = int(raw_string) - 1
|
||||
ware = wares[iware]
|
||||
value = ware.db.gold_value or 1
|
||||
wealth = caller.db.gold or 0
|
||||
text = "You inspect %s:\n\n%s" % (ware.key, ware.db.desc)
|
||||
|
||||
def buy_ware_result(caller):
|
||||
"This will be executed first when choosing to buy."
|
||||
if wealth >= value:
|
||||
rtext = "You pay %i gold and purchase %s!" % \
|
||||
(value, ware.key)
|
||||
caller.db.gold -= value
|
||||
ware.move_to(caller, quiet=True)
|
||||
else:
|
||||
rtext = "You cannot afford %i gold for %s!" % \
|
||||
(value, ware.key)
|
||||
caller.msg(rtext)
|
||||
|
||||
options = ({"desc": "Buy %s for %s gold" % \
|
||||
(ware.key, ware.db.gold_value or 1),
|
||||
"goto": "menunode_shopfront",
|
||||
"exec": buy_ware_result},
|
||||
{"desc": "Look for something else",
|
||||
"goto": "menunode_shopfront"})
|
||||
|
||||
return text, options
|
||||
```
|
||||
|
||||
In this menu node we make use of the `raw_string` argument to the node. This is the text the menu
|
||||
user entered on the *previous* node to get here. Since we only allow numbered options in our menu,
|
||||
`raw_input` must be an number for the player to get to this point. So we convert it to an integer
|
||||
index (menu lists start from 1, whereas Python indices always starts at 0, so we need to subtract
|
||||
1). We then use the index to get the corresponding item from storage.
|
||||
|
||||
We just show the customer the `desc` of the item. In a more elaborate setup you might want to show
|
||||
things like weapon damage and special stats here as well.
|
||||
|
||||
When the user choose the "buy" option, EvMenu will execute the `exec` instruction *before* we go
|
||||
back to the top node (the `goto` instruction). For this we make a little inline function
|
||||
`buy_ware_result`. EvMenu will call the function given to `exec` like any menu node but it does not
|
||||
need to return anything. In `buy_ware_result` we determine if the customer can afford the cost and
|
||||
give proper return messages. This is also where we actually move the bought item into the inventory
|
||||
of the customer.
|
||||
|
||||
#### The command to start the menu
|
||||
|
||||
We could *in principle* launch the shopping menu the moment a customer steps into our shop room, but
|
||||
this would probably be considered pretty annoying. It's better to create a [Command](Commands) for
|
||||
customers to explicitly wanting to shop around.
|
||||
|
||||
```python
|
||||
# mygame/typeclasses/npcshop.py
|
||||
|
||||
from evennia import Command
|
||||
|
||||
class CmdBuy(Command):
|
||||
"""
|
||||
Start to do some shopping
|
||||
|
||||
Usage:
|
||||
buy
|
||||
shop
|
||||
browse
|
||||
|
||||
This will allow you to browse the wares of the
|
||||
current shop and buy items you want.
|
||||
"""
|
||||
key = "buy"
|
||||
aliases = ("shop", "browse")
|
||||
|
||||
def func(self):
|
||||
"Starts the shop EvMenu instance"
|
||||
evmenu.EvMenu(self.caller,
|
||||
"typeclasses.npcshop",
|
||||
startnode="menunode_shopfront")
|
||||
```
|
||||
|
||||
This will launch the menu. The `EvMenu` instance is initialized with the path to this very module -
|
||||
since the only global functions available in this module are our menu nodes, this will work fine
|
||||
(you could also have put those in a separate module). We now just need to put this command in a
|
||||
[CmdSet](Command-Sets) so we can add it correctly to the game:
|
||||
|
||||
```python
|
||||
from evennia import CmdSet
|
||||
|
||||
class ShopCmdSet(CmdSet):
|
||||
def at_cmdset_creation(self):
|
||||
self.add(CmdBuy())
|
||||
```
|
||||
|
||||
### Building the shop
|
||||
|
||||
There are really only two things that separate our shop from any other Room:
|
||||
|
||||
- The shop has the `storeroom` Attribute set on it, pointing to a second (completely normal) room.
|
||||
- It has the `ShopCmdSet` stored on itself. This makes the `buy` command available to users entering
|
||||
the shop.
|
||||
|
||||
For testing we could easily add these features manually to a room using `@py` or other admin
|
||||
commands. Just to show how it can be done we'll instead make a custom [Typeclass](Typeclasses) for
|
||||
the shop room and make a small command that builders can use to build both the shop and the
|
||||
storeroom at once.
|
||||
|
||||
```python
|
||||
# bottom of mygame/typeclasses/npcshop.py
|
||||
|
||||
from evennia import DefaultRoom, DefaultExit, DefaultObject
|
||||
from evennia.utils.create import create_object
|
||||
|
||||
# class for our front shop room
|
||||
class NPCShop(DefaultRoom):
|
||||
def at_object_creation(self):
|
||||
# we could also use add(ShopCmdSet, permanent=True)
|
||||
self.cmdset.add_default(ShopCmdSet)
|
||||
self.db.storeroom = None
|
||||
|
||||
# command to build a complete shop (the Command base class
|
||||
# should already have been imported earlier in this file)
|
||||
class CmdBuildShop(Command):
|
||||
"""
|
||||
Build a new shop
|
||||
|
||||
Usage:
|
||||
@buildshop shopname
|
||||
|
||||
This will create a new NPCshop room
|
||||
as well as a linked store room (named
|
||||
simply <storename>-storage) for the
|
||||
wares on sale. The store room will be
|
||||
accessed through a locked door in
|
||||
the shop.
|
||||
"""
|
||||
key = "@buildshop"
|
||||
locks = "cmd:perm(Builders)"
|
||||
help_category = "Builders"
|
||||
|
||||
def func(self):
|
||||
"Create the shop rooms"
|
||||
if not self.args:
|
||||
self.msg("Usage: @buildshop <storename>")
|
||||
return
|
||||
# create the shop and storeroom
|
||||
shopname = self.args.strip()
|
||||
shop = create_object(NPCShop,
|
||||
key=shopname,
|
||||
location=None)
|
||||
storeroom = create_object(DefaultRoom,
|
||||
key="%s-storage" % shopname,
|
||||
location=None)
|
||||
shop.db.storeroom = storeroom
|
||||
# create a door between the two
|
||||
shop_exit = create_object(DefaultExit,
|
||||
key="back door",
|
||||
aliases=["storage", "store room"],
|
||||
location=shop,
|
||||
destination=storeroom)
|
||||
storeroom_exit = create_object(DefaultExit,
|
||||
key="door",
|
||||
location=storeroom,
|
||||
destination=shop)
|
||||
# make a key for accessing the store room
|
||||
storeroom_key_name = "%s-storekey" % shopname
|
||||
storeroom_key = create_object(DefaultObject,
|
||||
key=storeroom_key_name,
|
||||
location=shop)
|
||||
# only allow chars with this key to enter the store room
|
||||
shop_exit.locks.add("traverse:holds(%s)" % storeroom_key_name)
|
||||
|
||||
# inform the builder about progress
|
||||
self.caller.msg("The shop %s was created!" % shop)
|
||||
```
|
||||
|
||||
Our typeclass is simple and so is our `buildshop` command. The command (which is for Builders only)
|
||||
just takes the name of the shop and builds the front room and a store room to go with it (always
|
||||
named `"<shopname>-storage"`. It connects the rooms with a two-way exit. You need to add
|
||||
`CmdBuildShop` [to the default cmdset](Adding-Command-Tutorial#step-2-adding-the-command-to-a-
|
||||
default-cmdset) before you can use it. Once having created the shop you can now `@teleport` to it or
|
||||
`@open` a new exit to it. You could also easily expand the above command to automatically create
|
||||
exits to and from the new shop from your current location.
|
||||
|
||||
To avoid customers walking in and stealing everything, we create a [Lock](Locks) on the storage
|
||||
door. It's a simple lock that requires the one entering to carry an object named
|
||||
`<shopname>-storekey`. We even create such a key object and drop it in the shop for the new shop
|
||||
keeper to pick up.
|
||||
|
||||
> If players are given the right to name their own objects, this simple lock is not very secure and
|
||||
you need to come up with a more robust lock-key solution.
|
||||
|
||||
> We don't add any descriptions to all these objects so looking "at" them will not be too thrilling.
|
||||
You could add better default descriptions as part of the `@buildshop` command or leave descriptions
|
||||
this up to the Builder.
|
||||
|
||||
### The shop is open for business!
|
||||
|
||||
We now have a functioning shop and an easy way for Builders to create it. All you need now is to
|
||||
`@open` a new exit from the rest of the game into the shop and put some sell-able items in the store
|
||||
room. Our shop does have some shortcomings:
|
||||
|
||||
- For Characters to be able to buy stuff they need to also have the `gold` Attribute set on
|
||||
themselves.
|
||||
- We manually remove the "door" exit from our items for sale. But what if there are other unsellable
|
||||
items in the store room? What if the shop owner walks in there for example - anyone in the store
|
||||
could then buy them for 1 gold.
|
||||
- What if someone else were to buy the item we're looking at just before we decide to buy it? It
|
||||
would then be gone and the counter be wrong - the shop would pass us the next item in the list.
|
||||
|
||||
Fixing these issues are left as an exercise.
|
||||
|
||||
If you want to keep the shop fully NPC-run you could add a [Script](Scripts) to restock the shop's
|
||||
store room regularly. This shop example could also easily be owned by a human Player (run for them
|
||||
by a hired NPC) - the shop owner would get the key to the store room and be responsible for keeping
|
||||
it well stocked.
|
||||
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.
|
||||
127
docs/source/Howto/Tutorial-Aggressive-NPCs.md
Normal file
127
docs/source/Howto/Tutorial-Aggressive-NPCs.md
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
# Tutorial Aggressive NPCs
|
||||
|
||||
|
||||
This tutorial shows the implementation of an NPC object that responds to characters entering their
|
||||
location. In this example the NPC has the option to respond aggressively or not, but any actions
|
||||
could be triggered this way.
|
||||
|
||||
One could imagine using a [Script](Scripts) that is constantly checking for newcomers. This would be
|
||||
highly inefficient (most of the time its check would fail). Instead we handle this on-demand by
|
||||
using a couple of existing object hooks to inform the NPC that a Character has entered.
|
||||
|
||||
It is assumed that you already know how to create custom room and character typeclasses, please see
|
||||
the [Basic Game tutorial](Tutorial-for-basic-MUSH-like-game) if you haven't already done this.
|
||||
|
||||
What we will need is the following:
|
||||
|
||||
- An NPC typeclass that can react when someone enters.
|
||||
- A custom [Room](Objects#rooms) typeclass that can tell the NPC that someone entered.
|
||||
- We will also tweak our default `Character` typeclass a little.
|
||||
|
||||
To begin with, we need to create an NPC typeclass. Create a new file inside of your typeclasses
|
||||
folder and name it `npcs.py` and then add the following code:
|
||||
|
||||
```python
|
||||
from typeclasses.characters import Character
|
||||
|
||||
class NPC(Character):
|
||||
"""
|
||||
A NPC typeclass which extends the character class.
|
||||
"""
|
||||
def at_char_entered(self, character):
|
||||
"""
|
||||
A simple is_aggressive check.
|
||||
Can be expanded upon later.
|
||||
"""
|
||||
if self.db.is_aggressive:
|
||||
self.execute_cmd(f"say Graaah, die {character}!")
|
||||
else:
|
||||
self.execute_cmd(f"say Greetings, {character}!")
|
||||
```
|
||||
|
||||
We will define our custom `Character` typeclass below. As for the new `at_char_entered` method we've
|
||||
just defined, we'll ensure that it will be called by the room where the NPC is located, when a
|
||||
player enters that room. You'll notice that right now, the NPC merely speaks. You can expand this
|
||||
part as you like and trigger all sorts of effects here (like combat code, fleeing, bartering or
|
||||
quest-giving) as your game design dictates.
|
||||
|
||||
Now your `typeclasses.rooms` module needs to have the following added:
|
||||
|
||||
```python
|
||||
# Add this import to the top of your file.
|
||||
from evennia import utils
|
||||
|
||||
# Add this hook in any empty area within your Room class.
|
||||
def at_object_receive(self, obj, source_location):
|
||||
if utils.inherits_from(obj, 'typeclasses.npcs.NPC'): # An NPC has entered
|
||||
return
|
||||
elif utils.inherits_from(obj, 'typeclasses.characters.Character'):
|
||||
# A PC has entered.
|
||||
# Cause the player's character to look around.
|
||||
obj.execute_cmd('look')
|
||||
for item in self.contents:
|
||||
if utils.inherits_from(item, 'typeclasses.npcs.NPC'):
|
||||
# An NPC is in the room
|
||||
item.at_char_entered(obj)
|
||||
```
|
||||
|
||||
`inherits_from` must be given the full path of the class. If the object inherited a class from your
|
||||
`world.races` module, then you would check inheritance with `world.races.Human`, for example. There
|
||||
is no need to import these prior, as we are passing in the full path. As a matter of a fact,
|
||||
`inherits_from` does not properly work if you import the class and only pass in the name of the
|
||||
class.
|
||||
|
||||
> Note:
|
||||
[at_object_receive](https://github.com/evennia/evennia/blob/master/evennia/objects/objects.py#L1529)
|
||||
is a default hook of the `DefaultObject` typeclass (and its children). Here we are overriding this
|
||||
hook in our customized room typeclass to suit our needs.
|
||||
|
||||
This room checks the typeclass of objects entering it (using `utils.inherits_from` and responds to
|
||||
`Characters`, ignoring other NPCs or objects. When triggered the room will look through its
|
||||
contents and inform any `NPCs inside by calling their `at_char_entered` method.
|
||||
|
||||
You'll also see that we have added a 'look' into this code. This is because, by default, the
|
||||
`at_object_receive` is carried out *before* the character's `at_after_move` which, we will now
|
||||
overload. This means that a character entering would see the NPC perform its actions before the
|
||||
'look' command. Deactivate the look command in the default `Character` class within the
|
||||
`typeclasses.characters` module:
|
||||
|
||||
```python
|
||||
# Add this hook in any blank area within your Character class.
|
||||
def at_after_move(self, source_location):
|
||||
"""
|
||||
Default is to look around after a move
|
||||
Note: This has been moved to Room.at_object_receive
|
||||
"""
|
||||
#self.execute_cmd('look')
|
||||
pass
|
||||
```
|
||||
|
||||
Now let's create an NPC and make it aggressive. Type the following commands into your MUD client:
|
||||
```
|
||||
reload
|
||||
create/drop Orc:npcs.NPC
|
||||
```
|
||||
|
||||
> Note: You could also give the path as `typeclasses.npcs.NPC`, but Evennia will look into the
|
||||
`typeclasses` folder automatically, so this is a little shorter.
|
||||
|
||||
When you enter the aggressive NPC's location, it will default to using its peaceful action (say your
|
||||
name is Anna):
|
||||
|
||||
```
|
||||
Orc says, "Greetings, Anna!"
|
||||
```
|
||||
|
||||
Now we turn on the aggressive mode (we do it manually but it could also be triggered by some sort of
|
||||
AI code).
|
||||
|
||||
```
|
||||
set orc/is_aggressive = True
|
||||
```
|
||||
|
||||
Now it will perform its aggressive action whenever a character enters.
|
||||
|
||||
```
|
||||
Orc says, "Graaah, die, Anna!"
|
||||
```
|
||||
110
docs/source/Howto/Tutorial-NPCs-listening.md
Normal file
110
docs/source/Howto/Tutorial-NPCs-listening.md
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
# Tutorial NPCs listening
|
||||
|
||||
|
||||
This tutorial shows the implementation of an NPC object that responds to characters speaking in
|
||||
their location. In this example the NPC parrots what is said, but any actions could be triggered
|
||||
this way.
|
||||
|
||||
It is assumed that you already know how to create custom room and character typeclasses, please see
|
||||
the [Basic Game tutorial](Tutorial-for-basic-MUSH-like-game) if you haven't already done this.
|
||||
|
||||
What we will need is simply a new NPC typeclass that can react when someone speaks.
|
||||
|
||||
```python
|
||||
# mygame/typeclasses/npc.py
|
||||
|
||||
from characters import Character
|
||||
class Npc(Character):
|
||||
"""
|
||||
A NPC typeclass which extends the character class.
|
||||
"""
|
||||
def at_heard_say(self, message, from_obj):
|
||||
"""
|
||||
A simple listener and response. This makes it easy to change for
|
||||
subclasses of NPCs reacting differently to says.
|
||||
|
||||
"""
|
||||
# message will be on the form `<Person> says, "say_text"`
|
||||
# we want to get only say_text without the quotes and any spaces
|
||||
message = message.split('says, ')[1].strip(' "')
|
||||
|
||||
# we'll make use of this in .msg() below
|
||||
return "%s said: '%s'" % (from_obj, message)
|
||||
```
|
||||
|
||||
When someone in the room speaks to this NPC, its `msg` method will be called. We will modify the
|
||||
NPCs `.msg` method to catch says so the NPC can respond.
|
||||
|
||||
|
||||
```python
|
||||
# mygame/typeclasses/npc.py
|
||||
|
||||
from characters import Character
|
||||
class Npc(Character):
|
||||
|
||||
# [at_heard_say() goes here]
|
||||
|
||||
def msg(self, text=None, from_obj=None, **kwargs):
|
||||
"Custom msg() method reacting to say."
|
||||
|
||||
if from_obj != self:
|
||||
# make sure to not repeat what we ourselves said or we'll create a loop
|
||||
try:
|
||||
# if text comes from a say, `text` is `('say_text', {'type': 'say'})`
|
||||
say_text, is_say = text[0], text[1]['type'] == 'say'
|
||||
except Exception:
|
||||
is_say = False
|
||||
if is_say:
|
||||
# First get the response (if any)
|
||||
response = self.at_heard_say(say_text, from_obj)
|
||||
# If there is a response
|
||||
if response != None:
|
||||
# speak ourselves, using the return
|
||||
self.execute_cmd("say %s" % response)
|
||||
|
||||
# this is needed if anyone ever puppets this NPC - without it you would never
|
||||
# get any feedback from the server (not even the results of look)
|
||||
super().msg(text=text, from_obj=from_obj, **kwargs)
|
||||
```
|
||||
|
||||
So if the NPC gets a say and that say is not coming from the NPC itself, it will echo it using the
|
||||
`at_heard_say` hook. Some things of note in the above example:
|
||||
|
||||
- The `text` input can be on many different forms depending on where this `msg` is called from.
|
||||
Instead of trying to analyze `text` in detail with a range of `if` statements we just assume the
|
||||
form we want and catch the error if it does not match. This simplifies the code considerably. It's
|
||||
called 'leap before you look' and is a Python paradigm that may feel unfamiliar if you are used to
|
||||
other languages. Here we 'swallow' the error silently, which is fine when the code checked is
|
||||
simple. If not we may want to import `evennia.logger.log_trace` and add `log_trace()` in the
|
||||
`except` clause.
|
||||
- We use `execute_cmd` to fire the `say` command back. We could also have called
|
||||
`self.location.msg_contents` directly but using the Command makes sure all hooks are called (so
|
||||
those seeing the NPC's `say` can in turn react if they want).
|
||||
- Note the comments about `super` at the end. This will trigger the 'default' `msg` (in the parent
|
||||
class) as well. It's not really necessary as long as no one puppets the NPC (by `@ic <npcname>`) but
|
||||
it's wise to keep in there since the puppeting player will be totally blind if `msg()` is never
|
||||
returning anything to them!
|
||||
|
||||
Now that's done, let's create an NPC and see what it has to say for itself.
|
||||
|
||||
```
|
||||
@reload
|
||||
@create/drop Guild Master:npc.Npc
|
||||
```
|
||||
|
||||
(you could also give the path as `typeclasses.npc.Npc`, but Evennia will look into the `typeclasses`
|
||||
folder automatically so this is a little shorter).
|
||||
|
||||
> say hi
|
||||
You say, "hi"
|
||||
Guild Master says, "Anna said: 'hi'"
|
||||
|
||||
## Assorted notes
|
||||
|
||||
There are many ways to implement this kind of functionality. An alternative example to overriding
|
||||
`msg` would be to modify the `at_say` hook on the *Character* instead. It could detect that it's
|
||||
sending to an NPC and call the `at_heard_say` hook directly.
|
||||
|
||||
While the tutorial solution has the advantage of being contained only within the NPC class,
|
||||
combining this with using the Character class gives more direct control over how the NPC will react.
|
||||
Which way to go depends on the design requirements of your particular game.
|
||||
96
docs/source/Howto/Tutorial-Tweeting-Game-Stats.md
Normal file
96
docs/source/Howto/Tutorial-Tweeting-Game-Stats.md
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
# Tutorial Tweeting Game Stats
|
||||
|
||||
|
||||
This tutorial will create a simple script that will send a tweet to your already configured twitter
|
||||
account. Please see: [How to connect Evennia to Twitter](How-to-connect-Evennia-to-Twitter) if you
|
||||
haven't already done so.
|
||||
|
||||
The script could be expanded to cover a variety of statistics you might wish to tweet about
|
||||
regularly, from player deaths to how much currency is in the economy etc.
|
||||
|
||||
```python
|
||||
# evennia/typeclasses/tweet_stats.py
|
||||
|
||||
import twitter
|
||||
from random import randint
|
||||
from django.conf import settings
|
||||
from evennia import ObjectDB
|
||||
from evennia import spawner
|
||||
from evennia import logger
|
||||
from evennia import DefaultScript
|
||||
|
||||
class TweetStats(DefaultScript):
|
||||
"""
|
||||
This implements the tweeting of stats to a registered twitter account
|
||||
"""
|
||||
|
||||
# standard Script hooks
|
||||
|
||||
def at_script_creation(self):
|
||||
"Called when script is first created"
|
||||
|
||||
self.key = "tweet_stats"
|
||||
self.desc = "Tweets interesting stats about the game"
|
||||
self.interval = 86400 # 1 day timeout
|
||||
self.start_delay = False
|
||||
|
||||
def at_repeat(self):
|
||||
"""
|
||||
This is called every self.interval seconds to tweet interesting stats about the game.
|
||||
"""
|
||||
|
||||
api = twitter.Api(consumer_key='consumer_key',
|
||||
consumer_secret='consumer_secret',
|
||||
access_token_key='access_token_key',
|
||||
access_token_secret='access_token_secret')
|
||||
|
||||
number_tweet_outputs = 2
|
||||
|
||||
tweet_output = randint(1, number_tweet_outputs)
|
||||
|
||||
if tweet_output == 1:
|
||||
##Game Chars, Rooms, Objects taken from @stats command
|
||||
nobjs = ObjectDB.objects.count()
|
||||
base_char_typeclass = settings.BASE_CHARACTER_TYPECLASS
|
||||
nchars = ObjectDB.objects.filter(db_typeclass_path=base_char_typeclass).count()
|
||||
nrooms =
|
||||
ObjectDB.objects.filter(db_location__isnull=True).exclude(db_typeclass_path=base_char_typeclass).count()
|
||||
nexits = ObjectDB.objects.filter(db_location__isnull=False,
|
||||
db_destination__isnull=False).count()
|
||||
nother = nobjs - nchars - nrooms - nexits
|
||||
tweet = "Chars: %s, Rooms: %s, Objects: %s" %(nchars, nrooms, nother)
|
||||
else:
|
||||
if tweet_output == 2: ##Number of prototypes and 3 random keys - taken from @spawn
|
||||
command
|
||||
prototypes = spawner.spawn(return_prototypes=True)
|
||||
|
||||
keys = prototypes.keys()
|
||||
nprots = len(prototypes)
|
||||
tweet = "Prototype Count: %s Random Keys: " % nprots
|
||||
|
||||
tweet += " %s" % keys[randint(0,len(keys)-1)]
|
||||
for x in range(0,2): ##tweet 3
|
||||
tweet += ", %s" % keys[randint(0,len(keys)-1)]
|
||||
# post the tweet
|
||||
try:
|
||||
response = api.PostUpdate(tweet)
|
||||
except:
|
||||
logger.log_trace("Tweet Error: When attempting to tweet %s" % tweet)
|
||||
```
|
||||
|
||||
In the `at_script_creation` method, we configure the script to fire immediately (useful for testing)
|
||||
and setup the delay (1 day) as well as script information seen when you use `@scripts`
|
||||
|
||||
In the `at_repeat` method (which is called immediately and then at interval seconds later) we setup
|
||||
the Twitter API (just like in the initial configuration of twitter). numberTweetOutputs is used to
|
||||
show how many different types of outputs we have (in this case 2). We then build the tweet based on
|
||||
randomly choosing between these outputs.
|
||||
|
||||
1. Shows the number of Player Characters, Rooms and Other/Objects
|
||||
2. Shows the number of prototypes currently in the game and then selects 3 random keys to show
|
||||
|
||||
[Scripts Information](Scripts) will show you how to add it as a Global script, however, for testing
|
||||
it may be useful to start/stop it quickly from within the game. Assuming that you create the file
|
||||
as `mygame/typeclasses/tweet_stats.py` it can be started by using the following command
|
||||
|
||||
@script Here = tweet_stats.TweetStats
|
||||
422
docs/source/Howto/Tutorial-Vehicles.md
Normal file
422
docs/source/Howto/Tutorial-Vehicles.md
Normal file
|
|
@ -0,0 +1,422 @@
|
|||
# Tutorial Vehicles
|
||||
|
||||
|
||||
This tutorial explains how you can create vehicles that can move around in your world. The tutorial
|
||||
will explain how to create a train, but this can be equally applied to create other kind of vehicles
|
||||
(cars, planes, boats, spaceships, submarines, ...).
|
||||
|
||||
## How it works
|
||||
|
||||
Objects in Evennia have an interesting property: you can put any object inside another object. This
|
||||
is most obvious in rooms: a room in Evennia is just like any other game object (except rooms tend to
|
||||
not themselves be inside anything else).
|
||||
|
||||
Our train will be similar: it will be an object that other objects can get inside. We then simply
|
||||
move the Train, which brings along everyone inside it.
|
||||
|
||||
## Creating our train object
|
||||
|
||||
The first step we need to do is create our train object, including a new typeclass. To do this,
|
||||
create a new file, for instance in `mygame/typeclasses/train.py` with the following content:
|
||||
|
||||
```python
|
||||
# file mygame/typeclasses/train.py
|
||||
|
||||
from evennia import DefaultObject
|
||||
|
||||
class TrainObject(DefaultObject):
|
||||
|
||||
def at_object_creation(self):
|
||||
# We'll add in code here later.
|
||||
pass
|
||||
|
||||
```
|
||||
|
||||
Now we can create our train in our game:
|
||||
|
||||
```
|
||||
@create/drop train:train.TrainObject
|
||||
```
|
||||
|
||||
Now this is just an object that doesn't do much yet... but we can already force our way inside it
|
||||
and back (assuming we created it in limbo).
|
||||
|
||||
```
|
||||
@tel train
|
||||
@tel limbo
|
||||
```
|
||||
|
||||
## Entering and leaving the train
|
||||
|
||||
Using the `@tel`command like shown above is obviously not what we want. `@tel` is an admin command
|
||||
and normal players will thus never be able to enter the train! It is also not really a good idea to
|
||||
use [Exits](Objects#exits) to get in and out of the train - Exits are (at least by default) objects
|
||||
too. They point to a specific destination. If we put an Exit in this room leading inside the train
|
||||
it would stay here when the train moved away (still leading into the train like a magic portal!). In
|
||||
the same way, if we put an Exit object inside the train, it would always point back to this room,
|
||||
regardless of where the Train has moved. Now, one *could* define custom Exit types that move with
|
||||
the train or change their destination in the right way - but this seems to be a pretty cumbersome
|
||||
solution.
|
||||
|
||||
What we will do instead is to create some new [commands](Commands): one for entering the train and
|
||||
another for leaving it again. These will be stored *on the train object* and will thus be made
|
||||
available to whomever is either inside it or in the same room as the train.
|
||||
|
||||
Let's create a new command module as `mygame/commands/train.py`:
|
||||
|
||||
```python
|
||||
# mygame/commands/train.py
|
||||
|
||||
from evennia import Command, CmdSet
|
||||
|
||||
class CmdEnterTrain(Command):
|
||||
"""
|
||||
entering the train
|
||||
|
||||
Usage:
|
||||
enter train
|
||||
|
||||
This will be available to players in the same location
|
||||
as the train and allows them to embark.
|
||||
"""
|
||||
|
||||
key = "enter train"
|
||||
locks = "cmd:all()"
|
||||
|
||||
def func(self):
|
||||
train = self.obj
|
||||
self.caller.msg("You board the train.")
|
||||
self.caller.move_to(train)
|
||||
|
||||
|
||||
class CmdLeaveTrain(Command):
|
||||
"""
|
||||
leaving the train
|
||||
|
||||
Usage:
|
||||
leave train
|
||||
|
||||
This will be available to everyone inside the
|
||||
train. It allows them to exit to the train's
|
||||
current location.
|
||||
"""
|
||||
|
||||
key = "leave train"
|
||||
locks = "cmd:all()"
|
||||
|
||||
def func(self):
|
||||
train = self.obj
|
||||
parent = train.location
|
||||
self.caller.move_to(parent)
|
||||
|
||||
|
||||
class CmdSetTrain(CmdSet):
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
self.add(CmdEnterTrain())
|
||||
self.add(CmdLeaveTrain())
|
||||
```
|
||||
Note that while this seems like a lot of text, the majority of lines here are taken up by
|
||||
documentation.
|
||||
|
||||
These commands are work in a pretty straightforward way: `CmdEnterTrain` moves the location of the
|
||||
player to inside the train and `CmdLeaveTrain` does the opposite: it moves the player back to the
|
||||
current location of the train (back outside to its current location). We stacked them in a
|
||||
[cmdset](Command-Sets) `CmdSetTrain` so they can be used.
|
||||
|
||||
To make the commands work we need to add this cmdset to our train typeclass:
|
||||
|
||||
```python
|
||||
# file mygame/typeclasses/train.py
|
||||
|
||||
from evennia import DefaultObject
|
||||
from commands.train import CmdSetTrain
|
||||
|
||||
class TrainObject(DefaultObject):
|
||||
|
||||
def at_object_creation(self):
|
||||
self.cmdset.add_default(CmdSetTrain)
|
||||
|
||||
```
|
||||
|
||||
If we now `@reload` our game and reset our train, those commands should work and we can now enter
|
||||
and leave the train:
|
||||
|
||||
```
|
||||
@reload
|
||||
@typeclass/force/reset train = train.TrainObject
|
||||
enter train
|
||||
leave train
|
||||
```
|
||||
|
||||
Note the switches used with the `@typeclass` command: The `/force` switch is necessary to assign our
|
||||
object the same typeclass we already have. The `/reset` re-triggers the typeclass'
|
||||
`at_object_creation()` hook (which is otherwise only called the very first an instance is created).
|
||||
As seen above, when this hook is called on our train, our new cmdset will be loaded.
|
||||
|
||||
## Locking down the commands
|
||||
|
||||
If you have played around a bit, you've probably figured out that you can use `leave train` when
|
||||
outside the train and `enter train` when inside. This doesn't make any sense ... so let's go ahead
|
||||
and fix that. We need to tell Evennia that you can not enter the train when you're already inside
|
||||
or leave the train when you're outside. One solution to this is [locks](Locks): we will lock down
|
||||
the commands so that they can only be called if the player is at the correct location.
|
||||
|
||||
Right now commands defaults to the lock `cmd:all()`. The `cmd` lock type in combination with the
|
||||
`all()` lock function means that everyone can run those commands as long as they are in the same
|
||||
room as the train *or* inside the train. We're going to change this to check the location of the
|
||||
player and *only* allow access if they are inside the train.
|
||||
|
||||
First of all we need to create a new lock function. Evennia comes with many lock functions built-in
|
||||
already, but none that we can use for locking a command in this particular case. Create a new entry
|
||||
in `mygame/server/conf/lockfuncs.py`:
|
||||
|
||||
```python
|
||||
|
||||
# file mygame/server/conf/lockfuncs.py
|
||||
|
||||
def cmdinside(accessing_obj, accessed_obj, *args, **kwargs):
|
||||
"""
|
||||
Usage: cmdinside()
|
||||
Used to lock commands and only allows access if the command
|
||||
is defined on an object which accessing_obj is inside of.
|
||||
"""
|
||||
return accessed_obj.obj == accessing_obj.location
|
||||
|
||||
```
|
||||
If you didn't know, Evennia is by default set up to use all functions in this module as lock
|
||||
functions (there is a setting variable that points to it).
|
||||
|
||||
Our new lock function, `cmdinside`, is to be used by Commands. The `accessed_obj` is the Command
|
||||
object (in our case this will be `CmdEnterTrain` and `CmdLeaveTrain`) — Every command has an `obj`
|
||||
property: this is the the object on which the command "sits". Since we added those commands to our
|
||||
train object, the `.obj` property will be set to the train object. Conversely, `accessing_obj` is
|
||||
the object that called the command: in our case it's the Character trying to enter or leave the
|
||||
train.
|
||||
|
||||
What this function does is to check that the player's location is the same as the train object. If
|
||||
it is, it means the player is inside the train. Otherwise it means the player is somewhere else and
|
||||
the check will fail.
|
||||
|
||||
The next step is to actually use this new lock function to create a lock of type `cmd`:
|
||||
|
||||
```python
|
||||
# file commands/train.py
|
||||
...
|
||||
class CmdEnterTrain(Command):
|
||||
key = "enter train"
|
||||
locks = "cmd:not cmdinside()"
|
||||
# ...
|
||||
|
||||
class CmdLeaveTrain(Command):
|
||||
key = "leave train"
|
||||
locks = "cmd:cmdinside()"
|
||||
# ...
|
||||
```
|
||||
|
||||
Notice how we use the `not` here so that we can use the same `cmdinside` to check if we are inside
|
||||
and outside, without having to create two separate lock functions. After a `@reload` our commands
|
||||
should be locked down appropriately and you should only be able to use them at the right places.
|
||||
|
||||
> Note: If you're logged in as the super user (user `#1`) then this lock will not work: the super
|
||||
user ignores lock functions. In order to use this functionality you need to `@quell` first.
|
||||
|
||||
## Making our train move
|
||||
|
||||
Now that we can enter and leave the train correctly, it's time to make it move. There are different
|
||||
things we need to consider for this:
|
||||
|
||||
* Who can control your vehicle? The first player to enter it, only players that have a certain
|
||||
"drive" skill, automatically?
|
||||
* Where should it go? Can the player steer the vehicle to go somewhere else or will it always follow
|
||||
the same route?
|
||||
|
||||
For our example train we're going to go with automatic movement through a predefined route (its
|
||||
track). The train will stop for a bit at the start and end of the route to allow players to enter
|
||||
and leave it.
|
||||
|
||||
Go ahead and create some rooms for our train. Make a list of the room ids along the route (using the
|
||||
`@ex` command).
|
||||
|
||||
```
|
||||
@dig/tel South station
|
||||
@ex # note the id of the station
|
||||
@tunnel/tel n = Following a railroad
|
||||
@ex # note the id of the track
|
||||
@tunnel/tel n = Following a railroad
|
||||
...
|
||||
@tunnel/tel n = North Station
|
||||
```
|
||||
|
||||
Put the train onto the tracks:
|
||||
|
||||
```
|
||||
@tel south station
|
||||
@tel train = here
|
||||
```
|
||||
|
||||
Next we will tell the train how to move and which route to take.
|
||||
|
||||
```python
|
||||
# file typeclasses/train.py
|
||||
|
||||
from evennia import DefaultObject, search_object
|
||||
|
||||
from commands.train import CmdSetTrain
|
||||
|
||||
class TrainObject(DefaultObject):
|
||||
|
||||
def at_object_creation(self):
|
||||
self.cmdset.add_default(CmdSetTrain)
|
||||
self.db.driving = False
|
||||
# The direction our train is driving (1 for forward, -1 for backwards)
|
||||
self.db.direction = 1
|
||||
# The rooms our train will pass through (change to fit your game)
|
||||
self.db.rooms = ["#2", "#47", "#50", "#53", "#56", "#59"]
|
||||
|
||||
def start_driving(self):
|
||||
self.db.driving = True
|
||||
|
||||
def stop_driving(self):
|
||||
self.db.driving = False
|
||||
|
||||
def goto_next_room(self):
|
||||
currentroom = self.location.dbref
|
||||
idx = self.db.rooms.index(currentroom) + self.db.direction
|
||||
|
||||
if idx < 0 or idx >= len(self.db.rooms):
|
||||
# We reached the end of our path
|
||||
self.stop_driving()
|
||||
# Reverse the direction of the train
|
||||
self.db.direction *= -1
|
||||
else:
|
||||
roomref = self.db.rooms[idx]
|
||||
room = search_object(roomref)[0]
|
||||
self.move_to(room)
|
||||
self.msg_contents("The train is moving forward to %s." % (room.name, ))
|
||||
```
|
||||
|
||||
We added a lot of code here. Since we changed the `at_object_creation` to add in variables we will
|
||||
have to reset our train object like earlier (using the `@typeclass/force/reset` command).
|
||||
|
||||
We are keeping track of a few different things now: whether the train is moving or standing still,
|
||||
which direction the train is heading to and what rooms the train will pass through.
|
||||
|
||||
We also added some methods: one to start moving the train, another to stop and a third that actually
|
||||
moves the train to the next room in the list. Or makes it stop driving if it reaches the last stop.
|
||||
|
||||
Let's try it out, using `@py` to call the new train functionality:
|
||||
|
||||
```
|
||||
@reload
|
||||
@typeclass/force/reset train = train.TrainObject
|
||||
enter train
|
||||
@py here.goto_next_room()
|
||||
```
|
||||
|
||||
You should see the train moving forward one step along the rail road.
|
||||
|
||||
## Adding in scripts
|
||||
|
||||
If we wanted full control of the train we could now just add a command to step it along the track
|
||||
when desired. We want the train to move on its own though, without us having to force it by manually
|
||||
calling the `goto_next_room` method.
|
||||
|
||||
To do this we will create two [scripts](Scripts): one script that runs when the train has stopped at
|
||||
a station and is responsible for starting the train again after a while. The other script will take
|
||||
care of the driving.
|
||||
|
||||
Let's make a new file in `mygame/typeclasses/trainscript.py`
|
||||
|
||||
```python
|
||||
# file mygame/typeclasses/trainscript.py
|
||||
|
||||
from evennia import DefaultScript
|
||||
|
||||
class TrainStoppedScript(DefaultScript):
|
||||
|
||||
def at_script_creation(self):
|
||||
self.key = "trainstopped"
|
||||
self.interval = 30
|
||||
self.persistent = True
|
||||
self.repeats = 1
|
||||
self.start_delay = True
|
||||
|
||||
def at_repeat(self):
|
||||
self.obj.start_driving()
|
||||
|
||||
def at_stop(self):
|
||||
self.obj.scripts.add(TrainDrivingScript)
|
||||
|
||||
|
||||
class TrainDrivingScript(DefaultScript):
|
||||
|
||||
def at_script_creation(self):
|
||||
self.key = "traindriving"
|
||||
self.interval = 1
|
||||
self.persistent = True
|
||||
|
||||
def is_valid(self):
|
||||
return self.obj.db.driving
|
||||
|
||||
def at_repeat(self):
|
||||
if not self.obj.db.driving:
|
||||
self.stop()
|
||||
else:
|
||||
self.obj.goto_next_room()
|
||||
|
||||
def at_stop(self):
|
||||
self.obj.scripts.add(TrainStoppedScript)
|
||||
```
|
||||
|
||||
Those scripts work as a state system: when the train is stopped, it waits for 30 seconds and then
|
||||
starts again. When the train is driving, it moves to the next room every second. The train is always
|
||||
in one of those two states - both scripts take care of adding the other one once they are done.
|
||||
|
||||
As a last step we need to link the stopped-state script to our train, reload the game and reset our
|
||||
train again., and we're ready to ride it around!
|
||||
|
||||
```python
|
||||
# file typeclasses/train.py
|
||||
|
||||
from typeclasses.trainscript import TrainStoppedScript
|
||||
|
||||
class TrainObject(DefaultObject):
|
||||
|
||||
def at_object_creation(self):
|
||||
# ...
|
||||
self.scripts.add(TrainStoppedScript)
|
||||
```
|
||||
|
||||
```
|
||||
@reload
|
||||
@typeclass/force/reset train = train.TrainObject
|
||||
enter train
|
||||
|
||||
# output:
|
||||
< The train is moving forward to Following a railroad.
|
||||
< The train is moving forward to Following a railroad.
|
||||
< The train is moving forward to Following a railroad.
|
||||
...
|
||||
< The train is moving forward to Following a railroad.
|
||||
< The train is moving forward to North station.
|
||||
|
||||
leave train
|
||||
```
|
||||
|
||||
Our train will stop 30 seconds at each end station and then turn around to go back to the other end.
|
||||
|
||||
## Expanding
|
||||
|
||||
This train is very basic and still has some flaws. Some more things to do:
|
||||
|
||||
* Make it look like a train.
|
||||
* Make it impossible to exit and enter the train mid-ride. This could be made by having the
|
||||
enter/exit commands check so the train is not moving before allowing the caller to proceed.
|
||||
* Have train conductor commands that can override the automatic start/stop.
|
||||
* Allow for in-between stops between the start- and end station
|
||||
* Have a rail road track instead of hard-coding the rooms in the train object. This could for
|
||||
example be a custom [Exit](Objects#exits) only traversable by trains. The train will follow the
|
||||
track. Some track segments can split to lead to two different rooms and a player can switch the
|
||||
direction to which room it goes.
|
||||
* Create another kind of vehicle!
|
||||
198
docs/source/Howto/Understanding-Color-Tags.md
Normal file
198
docs/source/Howto/Understanding-Color-Tags.md
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
# Understanding Color Tags
|
||||
|
||||
This tutorial aims at dispelling confusions regarding the use of color tags within Evennia.
|
||||
|
||||
Correct understanding of this topic requires having read the [TextTags](TextTags) page and learned
|
||||
Evennia's color tags. Here we'll explain by examples the reasons behind the unexpected (or
|
||||
apparently incoherent) behaviors of some color tags, as mentioned _en passant_ in the
|
||||
[TextTags](TextTags) page.
|
||||
|
||||
|
||||
All you'll need for this tutorial is access to a running instance of Evennia via a color-enabled
|
||||
client. The examples provided are just commands that you can type in your client.
|
||||
|
||||
Evennia, ANSI and Xterm256
|
||||
==========================
|
||||
|
||||
All modern MUD clients support colors; nevertheless, the standards to which all clients abide dates
|
||||
back to old day of terminals, and when it comes to colors we are dealing with ANSI and Xterm256
|
||||
standards.
|
||||
|
||||
Evennia handles transparently, behind the scenes, all the code required to enforce these
|
||||
standards—so, if a user connects with a client which doesn't support colors, or supports only ANSI
|
||||
(16 colors), Evennia will take all due steps to ensure that the output will be adjusted to look
|
||||
right at the client side.
|
||||
|
||||
As for you, the developer, all you need to care about is knowing how to correctly use the color tags
|
||||
within your MUD. Most likely, you'll be adding colors to help pages, descriptions, automatically
|
||||
generated text, etc.
|
||||
|
||||
You are free to mix together ANSI and Xterm256 color tags, but you should be aware of a few
|
||||
pitfalls. ANSI and Xterm256 coexist without conflicts in Evennia, but in many ways they don't «see»
|
||||
each other: ANSI-specific color tags will have no effect on Xterm-defined colors, as we shall see
|
||||
here.
|
||||
|
||||
ANSI
|
||||
====
|
||||
|
||||
ANSI has a set of 16 colors, to be more precise: ANSI has 8 basic colors which come in _dark_ and
|
||||
_bright_ flavours—with _dark_ being _normal_. The colors are: red, green, yellow, blue, magenta,
|
||||
cyan, white and black. White in its dark version is usually referred to as gray, and black in its
|
||||
bright version as darkgray. Here, for sake of simplicity they'll be referred to as dark and bright:
|
||||
bright/dark black, bright/dark white.
|
||||
|
||||
The default colors of MUD clients is normal (dark) white on normal black (ie: gray on black).
|
||||
|
||||
It's important to grasp that in the ANSI standard bright colors apply only to text (foreground), not
|
||||
to background. Evennia allows to bypass this limitation via Xterm256, but doing so will impact the
|
||||
behavior of ANSI tags, as we shall see.
|
||||
|
||||
Also, it's important to remember that the 16 ANSI colors are a convention, and the final user can
|
||||
always customize their appearance—he might decide to have green show as red, and dark green as blue,
|
||||
etc.
|
||||
|
||||
Xterm256
|
||||
========
|
||||
|
||||
The 16 colors of ANSI should be more than enough to handle simple coloring of text. But when an
|
||||
author wants to be sure that a given color will show as he intended it, she might choose to rely on
|
||||
Xterm256 colors.
|
||||
|
||||
Xterm256 doesn't rely on a palette of named colors, it instead represent colors by their values. So,
|
||||
a red color could be `|[500` (bright and pure red), or `|[300` (darker red), and so on.
|
||||
|
||||
ANSI Color Tags in Evennia
|
||||
==========================
|
||||
|
||||
> NOTE: for ease of reading, the examples contain extra white spaces after the
|
||||
> color tags (eg: `|g green |b blue` ). This is done only so that it's easier
|
||||
> to see the tags separated from their context; it wouldn't be good practice
|
||||
> in real-life coding.
|
||||
|
||||
Let's proceed by examples. In your MUD client type:
|
||||
|
||||
|
||||
say Normal |* Negative
|
||||
|
||||
Evennia should output the word "Normal" normally (ie: gray on black) and "Negative" in reversed
|
||||
colors (ie: black on gray).
|
||||
|
||||
This is pretty straight forward, the `|*` ANSI *invert* tag switches between foreground and
|
||||
background—from now on, **FG** and **BG** shorthands will be used to refer to foreground and
|
||||
background.
|
||||
|
||||
But take mental note of this: `|*` has switched *dark white* and *dark black*.
|
||||
|
||||
Now try this:
|
||||
|
||||
say |w Bright white FG |* Negative
|
||||
|
||||
You'll notice that the word "Negative" is not black on white, it's darkgray on gray. Why is this?
|
||||
Shouldn't it be black text on a white BG? Two things are happening here.
|
||||
|
||||
As mentioned, ANSI has 8 base colors, the dark ones. The bright ones are achieved by means of
|
||||
*highlighting* the base/dark/normal colors, and they only apply to FG.
|
||||
|
||||
What happened here is that when we set the bright white FG with `|w`, Evennia translated this into
|
||||
the ANSI sequence of Highlight On + White FG. In terms of Evennia's color tags, it's as if we typed:
|
||||
|
||||
|
||||
say |h|!W Bright white FG |* Negative
|
||||
|
||||
Furthermore, the Highlight-On property (which only works for BG!) is preserved after the FG/BG
|
||||
switch, this being the reason why we see black as darkgray: highlighting makes it *bright black*
|
||||
(ie: darkgray).
|
||||
|
||||
As for the BG being also grey, that is normal—ie: you are seeing *normal white* (ie: dark white =
|
||||
gray). Remember that since there are no bright BG colors, the ANSI `|*` tag will transpose any FG
|
||||
color in its normal/dark version. So here the FG's bright white became dark white in the BG! In
|
||||
reality, it was always normal/dark white, except that in the FG is seen as bright because of the
|
||||
highlight tag behind the scenes.
|
||||
|
||||
Let's try the same thing with some color:
|
||||
|
||||
say |m |[G Bright Magenta on Dark Green |* Negative
|
||||
|
||||
Again, the BG stays dark because of ANSI rules, and the FG stays bright because of the implicit `|h`
|
||||
in `|m`.
|
||||
|
||||
Now, let's see what happens if we set a bright BG and then invert—yes, Evennia kindly allows us to
|
||||
do it, even if it's not within ANSI expectations.
|
||||
|
||||
say |[b Dark White on Bright Blue |* Negative
|
||||
|
||||
Before color inversion, the BG does show in bright blue, and after inversion (as expected) it's
|
||||
*dark white* (gray). The bright blue of the BG survived the inversion and gave us a bright blue FG.
|
||||
This behavior is tricky though, and not as simple as it might look.
|
||||
|
||||
If the inversion were to be pure ANSI, the bright blue would have been accounted just as normal
|
||||
blue, and should have converted to normal blue in the FG (after all, there was no highlighting on).
|
||||
The fact is that in reality this color is not bright blue at all, it just an Xterm version of it!
|
||||
|
||||
To demonstrate this, type:
|
||||
|
||||
say |[b Dark White on Bright Blue |* Negative |H un-bright
|
||||
|
||||
The `|H` Highlight-Off tag should have turned *dark blue* the last word; but it didn't because it
|
||||
couldn't: in order to enforce the non-ANSI bright BG Evennia turned to Xterm, and Xterm entities are
|
||||
not affected by ANSI tags!
|
||||
|
||||
So, we are getting at the heart of all confusions and possible odd-behaviors pertaining color tags
|
||||
in Evennia: apart from Evennia's translations from- and to- ANSI/Xterm, the two systems are
|
||||
independent and transparent to each other.
|
||||
|
||||
The bright blue of the previous example was just an Xterm representation of the ANSI standard blue.
|
||||
Try to change the default settings of your client, so that blue shows as some other color, you'll
|
||||
then realize the difference when Evennia is sending a true ANSI color (which will show up according
|
||||
to your settings) and when instead it's sending an Xterm representation of that color (which will
|
||||
show up always as defined by Evennia).
|
||||
|
||||
You'll have to keep in mind that the presence of an Xterm BG or FG color might affect the way your
|
||||
tags work on the text. For example:
|
||||
|
||||
say |[b Bright Blue BG |* Negative |!Y Dark Yellow |h not bright
|
||||
|
||||
Here the `|h` tag no longer affects the FG color. Even though it was changed via the `|!` tag, the
|
||||
ANSI system is out-of-tune because of the intrusion of an Xterm color (bright blue BG, then moved to
|
||||
FG with `|*`).
|
||||
|
||||
All unexpected ANSI behaviours are the result of mixing Xterm colors (either on purpose or either
|
||||
via bright BG colors). The `|n` tag will restore things in place and ANSI tags will respond properly
|
||||
again. So, at the end is just an issue of being mindful when using Xterm colors or bright BGs, and
|
||||
avoid wild mixing them with ANSI tags without normalizing (`|n`) things again.
|
||||
|
||||
Try this:
|
||||
|
||||
say |[b Bright Blue BG |* Negative |!R Red FG
|
||||
|
||||
And then:
|
||||
|
||||
say |[B Dark Blue BG |* Negative |!R Red BG??
|
||||
|
||||
In this second example the `|!` changes the BG color instead of the FG! In fact, the odd behavior is
|
||||
the one from the former example, non the latter. When you invert FG and BG with `|*` you actually
|
||||
inverting their references. This is why the last example (which has a normal/dark BG!) allows `|!`
|
||||
to change the BG color. In the first example, it's again the presence of an Xterm color (bright blue
|
||||
BG) which changes the default behavior.
|
||||
|
||||
Try this:
|
||||
|
||||
`say Normal |* Negative |!R Red BG`
|
||||
|
||||
This is the normal behavior, and as you can see it allows `|!` to change BG color after the
|
||||
inversion of FG and BG.
|
||||
|
||||
As long as you have an understanding of how ANSI works, it should be easy to handle color tags
|
||||
avoiding the pitfalls of Xterm-ANSI promisquity.
|
||||
|
||||
One last example:
|
||||
|
||||
`say Normal |* Negative |* still Negative`
|
||||
|
||||
Shows that `|*` only works once in a row and will not (and should not!) revert back if used again.
|
||||
Nor it will have any effect until the `|n` tag is called to "reset" ANSI back to normal. This is how
|
||||
it is meant to work.
|
||||
|
||||
ANSI operates according to a simple states-based mechanism, and it's important to understand the
|
||||
positive effect of resetting with the `|n` tag, and not try to
|
||||
push it over the limit, so to speak.
|
||||
54
docs/source/Howto/Weather-Tutorial.md
Normal file
54
docs/source/Howto/Weather-Tutorial.md
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
# Weather Tutorial
|
||||
|
||||
|
||||
This tutorial will have us create a simple weather system for our MUD. The way we want to use this
|
||||
is to have all outdoor rooms echo weather-related messages to the room at regular and semi-random
|
||||
intervals. Things like "Clouds gather above", "It starts to rain" and so on.
|
||||
|
||||
One could imagine every outdoor room in the game having a script running on themselves that fires
|
||||
regularly. For this particular example it is however more efficient to do it another way, namely by
|
||||
using a "ticker-subscription" model. The principle is simple: Instead of having each Object
|
||||
individually track the time, they instead subscribe to be called by a global ticker who handles time
|
||||
keeping. Not only does this centralize and organize much of the code in one place, it also has less
|
||||
computing overhead.
|
||||
|
||||
Evennia offers the [TickerHandler](TickerHandler) specifically for using the subscription model. We
|
||||
will use it for our weather system.
|
||||
|
||||
We will assume you know how to make your own Typeclasses. If not see one of the beginning tutorials.
|
||||
We will create a new WeatherRoom typeclass that is aware of the day-night cycle.
|
||||
|
||||
```python
|
||||
|
||||
import random
|
||||
from evennia import DefaultRoom, TICKER_HANDLER
|
||||
|
||||
ECHOES = ["The sky is clear.",
|
||||
"Clouds gather overhead.",
|
||||
"It's starting to drizzle.",
|
||||
"A breeze of wind is felt.",
|
||||
"The wind is picking up"] # etc
|
||||
|
||||
class WeatherRoom(DefaultRoom):
|
||||
"This room is ticked at regular intervals"
|
||||
|
||||
def at_object_creation(self):
|
||||
"called only when the object is first created"
|
||||
TICKER_HANDLER.add(60 * 60, self.at_weather_update)
|
||||
|
||||
def at_weather_update(self, *args, **kwargs):
|
||||
"ticked at regular intervals"
|
||||
echo = random.choice(ECHOES)
|
||||
self.msg_contents(echo)
|
||||
```
|
||||
|
||||
In the `at_object_creation` method, we simply added ourselves to the TickerHandler and tell it to
|
||||
call `at_weather_update` every hour (`60*60` seconds). During testing you might want to play with a
|
||||
shorter time duration.
|
||||
|
||||
For this to work we also create a custom hook `at_weather_update(*args, **kwargs)`, which is the
|
||||
call sign required by TickerHandler hooks.
|
||||
|
||||
Henceforth the room will inform everyone inside it when the weather changes. This particular example
|
||||
is of course very simplistic - the weather echoes are just randomly chosen and don't care what
|
||||
weather came before it. Expanding it to be more realistic is a useful exercise.
|
||||
639
docs/source/Howto/Web-Character-Generation.md
Normal file
639
docs/source/Howto/Web-Character-Generation.md
Normal file
|
|
@ -0,0 +1,639 @@
|
|||
# Web Character Generation
|
||||
|
||||
|
||||
## Introduction
|
||||
|
||||
This tutorial will create a simple web-based interface for generating a new in-game Character.
|
||||
Accounts will need to have first logged into the website (with their `AccountDB` account). Once
|
||||
finishing character generation the Character will be created immediately and the Accounts can then
|
||||
log into the game and play immediately (the Character will not require staff approval or anything
|
||||
like that). This guide does not go over how to create an AccountDB on the website with the right
|
||||
permissions to transfer to their web-created characters.
|
||||
|
||||
It is probably most useful to set `MULTISESSION_MODE = 2` or `3` (which gives you a character-
|
||||
selection screen when you log into the game later). Other modes can be used with some adaptation to
|
||||
auto-puppet the new Character.
|
||||
|
||||
You should have some familiarity with how Django sets up its Model Template View framework. You need
|
||||
to understand what is happening in the basic [Web Character View tutorial](Web-Character-View-
|
||||
Tutorial). If you don’t understand the listed tutorial or have a grasp of Django basics, please look
|
||||
at the [Django tutorial](https://docs.djangoproject.com/en/1.8/intro/) to get a taste of what Django
|
||||
does, before throwing Evennia into the mix (Evennia shares its API and attributes with the website
|
||||
interface). This guide will outline the format of the models, views, urls, and html templates
|
||||
needed.
|
||||
|
||||
## Pictures
|
||||
|
||||
Here are some screenshots of the simple app we will be making.
|
||||
|
||||
Index page, with no character application yet done:
|
||||
|
||||
***
|
||||

|
||||
***
|
||||
|
||||
Having clicked the "create" link you get to create your character (here we will only have name and
|
||||
background, you can add whatever is needed to fit your game):
|
||||
|
||||
***
|
||||

|
||||
***
|
||||
|
||||
Back to the index page. Having entered our character application (we called our character "TestApp")
|
||||
you see it listed:
|
||||
|
||||
***
|
||||

|
||||
***
|
||||
|
||||
We can also view an already written character application by clicking on it - this brings us to the
|
||||
*detail* page:
|
||||
|
||||
***
|
||||

|
||||
***
|
||||
|
||||
## Installing an App
|
||||
|
||||
Assuming your game is named "mygame", navigate to your `mygame/` directory, and type:
|
||||
|
||||
evennia startapp chargen
|
||||
|
||||
This will initialize a new Django app we choose to call "chargen". It is directory containing some
|
||||
basic starting things Django needs. You will need to move this directory: for the time being, it is
|
||||
in your `mygame` directory. Better to move it in your `mygame/web` directory, so you have
|
||||
`mygame/web/chargen` in the end.
|
||||
|
||||
Next, navigate to `mygame/server/conf/settings.py` and add or edit the following line to make
|
||||
Evennia (and Django) aware of our new app:
|
||||
|
||||
INSTALLED_APPS += ('web.chargen',)
|
||||
|
||||
After this, we will get into defining our *models* (the description of the database storage),
|
||||
*views* (the server-side website content generators), *urls* (how the web browser finds the pages)
|
||||
and *templates* (how the web page should be structured).
|
||||
|
||||
### Installing - Checkpoint:
|
||||
|
||||
* you should have a folder named `chargen` or whatever you chose in your mygame/web/ directory
|
||||
* you should have your application name added to your INSTALLED_APPS in settings.py
|
||||
|
||||
## Create Models
|
||||
|
||||
Models are created in `mygame/web/chargen/models.py`.
|
||||
|
||||
A [Django database model](New-Models) is a Python class that describes the database storage of the
|
||||
data you want to manage. Any data you choose to store is stored in the same database as the game and
|
||||
you have access to all the game's objects here.
|
||||
|
||||
We need to define what a character application actually is. This will differ from game to game so
|
||||
for this tutorial we will define a simple character sheet with the following database fields:
|
||||
|
||||
|
||||
* `app_id` (AutoField): Primary key for this character application sheet.
|
||||
* `char_name` (CharField): The new character's name.
|
||||
* `date_applied` (DateTimeField): Date that this application was received.
|
||||
* `background` (TextField): Character story background.
|
||||
* `account_id` (IntegerField): Which account ID does this application belong to? This is an
|
||||
AccountID from the AccountDB object.
|
||||
* `submitted` (BooleanField): `True`/`False` depending on if the application has been submitted yet.
|
||||
|
||||
> Note: In a full-fledged game, you’d likely want them to be able to select races, skills,
|
||||
attributes and so on.
|
||||
|
||||
Our `models.py` file should look something like this:
|
||||
|
||||
```python
|
||||
# in mygame/web/chargen/models.py
|
||||
|
||||
from django.db import models
|
||||
|
||||
class CharApp(models.Model):
|
||||
app_id = models.AutoField(primary_key=True)
|
||||
char_name = models.CharField(max_length=80, verbose_name='Character Name')
|
||||
date_applied = models.DateTimeField(verbose_name='Date Applied')
|
||||
background = models.TextField(verbose_name='Background')
|
||||
account_id = models.IntegerField(default=1, verbose_name='Account ID')
|
||||
submitted = models.BooleanField(default=False)
|
||||
```
|
||||
|
||||
You should consider how you are going to link your application to your account. For this tutorial,
|
||||
we are using the account_id attribute on our character application model in order to keep track of
|
||||
which characters are owned by which accounts. Since the account id is a primary key in Evennia, it
|
||||
is a good candidate, as you will never have two of the same IDs in Evennia. You can feel free to use
|
||||
anything else, but for the purposes of this guide, we are going to use account ID to join the
|
||||
character applications with the proper account.
|
||||
|
||||
### Model - Checkpoint:
|
||||
|
||||
* you should have filled out `mygame/web/chargen/models.py` with the model class shown above
|
||||
(eventually adding fields matching what you need for your game).
|
||||
|
||||
## Create Views
|
||||
|
||||
*Views* are server-side constructs that make dynamic data available to a web page. We are going to
|
||||
add them to `mygame/web/chargen.views.py`. Each view in our example represents the backbone of a
|
||||
specific web page. We will use three views and three pages here:
|
||||
|
||||
* The index (managing `index.html`). This is what you see when you navigate to
|
||||
`http://yoursite.com/chargen`.
|
||||
* The detail display sheet (manages `detail.html`). A page that passively displays the stats of a
|
||||
given Character.
|
||||
* Character creation sheet (manages `create.html`). This is the main form with fields to fill in.
|
||||
|
||||
### *Index* view
|
||||
|
||||
Let’s get started with the index first.
|
||||
|
||||
We’ll want characters to be able to see their created characters so let’s
|
||||
|
||||
```python
|
||||
# file mygame/web/chargen.views.py
|
||||
|
||||
from .models import CharApp
|
||||
|
||||
def index(request):
|
||||
current_user = request.user # current user logged in
|
||||
p_id = current_user.id # the account id
|
||||
# submitted Characters by this account
|
||||
sub_apps = CharApp.objects.filter(account_id=p_id, submitted=True)
|
||||
context = {'sub_apps': sub_apps}
|
||||
# make the variables in 'context' available to the web page template
|
||||
return render(request, 'chargen/index.html', context)
|
||||
```
|
||||
|
||||
### *Detail* view
|
||||
|
||||
Our detail page will have pertinent character application information our users can see. Since this
|
||||
is a basic demonstration, our detail page will only show two fields:
|
||||
|
||||
* Character name
|
||||
* Character background
|
||||
|
||||
We will use the account ID again just to double-check that whoever tries to check our character page
|
||||
is actually the account who owns the application.
|
||||
|
||||
```python
|
||||
# file mygame/web/chargen.views.py
|
||||
|
||||
def detail(request, app_id):
|
||||
app = CharApp.objects.get(app_id=app_id)
|
||||
name = app.char_name
|
||||
background = app.background
|
||||
submitted = app.submitted
|
||||
p_id = request.user.id
|
||||
context = {'name': name, 'background': background,
|
||||
'p_id': p_id, 'submitted': submitted}
|
||||
return render(request, 'chargen/detail.html', context)
|
||||
```
|
||||
|
||||
## *Creating* view
|
||||
|
||||
Predictably, our *create* function will be the most complicated of the views, as it needs to accept
|
||||
information from the user, validate the information, and send the information to the server. Once
|
||||
the form content is validated will actually create a playable Character.
|
||||
|
||||
The form itself we will define first. In our simple example we are just looking for the Character's
|
||||
name and background. This form we create in `mygame/web/chargen/forms.py`:
|
||||
|
||||
```python
|
||||
# file mygame/web/chargen/forms.py
|
||||
|
||||
from django import forms
|
||||
|
||||
class AppForm(forms.Form):
|
||||
name = forms.CharField(label='Character Name', max_length=80)
|
||||
background = forms.CharField(label='Background')
|
||||
```
|
||||
|
||||
Now we make use of this form in our view.
|
||||
|
||||
```python
|
||||
# file mygame/web/chargen/views.py
|
||||
|
||||
from web.chargen.models import CharApp
|
||||
from web.chargen.forms import AppForm
|
||||
from django.http import HttpResponseRedirect
|
||||
from datetime import datetime
|
||||
from evennia.objects.models import ObjectDB
|
||||
from django.conf import settings
|
||||
from evennia.utils import create
|
||||
|
||||
def creating(request):
|
||||
user = request.user
|
||||
if request.method == 'POST':
|
||||
form = AppForm(request.POST)
|
||||
if form.is_valid():
|
||||
name = form.cleaned_data['name']
|
||||
background = form.cleaned_data['background']
|
||||
applied_date = datetime.now()
|
||||
submitted = True
|
||||
if 'save' in request.POST:
|
||||
submitted = False
|
||||
app = CharApp(char_name=name, background=background,
|
||||
date_applied=applied_date, account_id=user.id,
|
||||
submitted=submitted)
|
||||
app.save()
|
||||
if submitted:
|
||||
# Create the actual character object
|
||||
typeclass = settings.BASE_CHARACTER_TYPECLASS
|
||||
home = ObjectDB.objects.get_id(settings.GUEST_HOME)
|
||||
# turn the permissionhandler to a string
|
||||
perms = str(user.permissions)
|
||||
# create the character
|
||||
char = create.create_object(typeclass=typeclass, key=name,
|
||||
home=home, permissions=perms)
|
||||
user.db._playable_characters.append(char)
|
||||
# add the right locks for the character so the account can
|
||||
# puppet it
|
||||
char.locks.add("puppet:id(%i) or pid(%i) or perm(Developers) "
|
||||
"or pperm(Developers)" % (char.id, user.id))
|
||||
char.db.background = background # set the character background
|
||||
return HttpResponseRedirect('/chargen')
|
||||
else:
|
||||
form = AppForm()
|
||||
return render(request, 'chargen/create.html', {'form': form})
|
||||
```
|
||||
|
||||
> Note also that we basically create the character using the Evennia API, and we grab the proper
|
||||
permissions from the `AccountDB` object and copy them to the character object. We take the user
|
||||
permissions attribute and turn that list of strings into a string object in order for the
|
||||
create_object function to properly process the permissions.
|
||||
|
||||
Most importantly, the following attributes must be set on the created character object:
|
||||
|
||||
* Evennia [permissions](Locks#permissions) (copied from the `AccountDB`).
|
||||
* The right `puppet` [locks](Locks) so the Account can actually play as this Character later.
|
||||
* The relevant Character [typeclass](Typeclasses)
|
||||
* Character name (key)
|
||||
* The Character's home room location (`#2` by default)
|
||||
|
||||
Other attributes are strictly speaking optional, such as the `background` attribute on our
|
||||
character. It may be a good idea to decompose this function and create a separate _create_character
|
||||
function in order to set up your character object the account owns. But with the Evennia API,
|
||||
setting custom attributes is as easy as doing it in the meat of your Evennia game directory.
|
||||
|
||||
After all of this, our `views.py` file should look like something like this:
|
||||
|
||||
```python
|
||||
# file mygame/web/chargen/views.py
|
||||
|
||||
from django.shortcuts import render
|
||||
from web.chargen.models import CharApp
|
||||
from web.chargen.forms import AppForm
|
||||
from django.http import HttpResponseRedirect
|
||||
from datetime import datetime
|
||||
from evennia.objects.models import ObjectDB
|
||||
from django.conf import settings
|
||||
from evennia.utils import create
|
||||
|
||||
def index(request):
|
||||
current_user = request.user # current user logged in
|
||||
p_id = current_user.id # the account id
|
||||
# submitted apps under this account
|
||||
sub_apps = CharApp.objects.filter(account_id=p_id, submitted=True)
|
||||
context = {'sub_apps': sub_apps}
|
||||
return render(request, 'chargen/index.html', context)
|
||||
|
||||
def detail(request, app_id):
|
||||
app = CharApp.objects.get(app_id=app_id)
|
||||
name = app.char_name
|
||||
background = app.background
|
||||
submitted = app.submitted
|
||||
p_id = request.user.id
|
||||
context = {'name': name, 'background': background,
|
||||
'p_id': p_id, 'submitted': submitted}
|
||||
return render(request, 'chargen/detail.html', context)
|
||||
|
||||
def creating(request):
|
||||
user = request.user
|
||||
if request.method == 'POST':
|
||||
form = AppForm(request.POST)
|
||||
if form.is_valid():
|
||||
name = form.cleaned_data['name']
|
||||
background = form.cleaned_data['background']
|
||||
applied_date = datetime.now()
|
||||
submitted = True
|
||||
if 'save' in request.POST:
|
||||
submitted = False
|
||||
app = CharApp(char_name=name, background=background,
|
||||
date_applied=applied_date, account_id=user.id,
|
||||
submitted=submitted)
|
||||
app.save()
|
||||
if submitted:
|
||||
# Create the actual character object
|
||||
typeclass = settings.BASE_CHARACTER_TYPECLASS
|
||||
home = ObjectDB.objects.get_id(settings.GUEST_HOME)
|
||||
# turn the permissionhandler to a string
|
||||
perms = str(user.permissions)
|
||||
# create the character
|
||||
char = create.create_object(typeclass=typeclass, key=name,
|
||||
home=home, permissions=perms)
|
||||
user.db._playable_characters.append(char)
|
||||
# add the right locks for the character so the account can
|
||||
# puppet it
|
||||
char.locks.add("puppet:id(%i) or pid(%i) or perm(Developers) "
|
||||
"or pperm(Developers)" % (char.id, user.id))
|
||||
char.db.background = background # set the character background
|
||||
return HttpResponseRedirect('/chargen')
|
||||
else:
|
||||
form = AppForm()
|
||||
return render(request, 'chargen/create.html', {'form': form})
|
||||
```
|
||||
|
||||
### Create Views - Checkpoint:
|
||||
|
||||
* you’ve defined a `views.py` that has an index, detail, and creating functions.
|
||||
* you’ve defined a forms.py with the `AppForm` class needed by the `creating` function of
|
||||
`views.py`.
|
||||
* your `mygame/web/chargen` directory should now have a `views.py` and `forms.py` file
|
||||
|
||||
## Create URLs
|
||||
|
||||
URL patterns helps redirect requests from the web browser to the right views. These patterns are
|
||||
created in `mygame/web/chargen/urls.py`.
|
||||
|
||||
```python
|
||||
# file mygame/web/chargen/urls.py
|
||||
|
||||
from django.conf.urls import url
|
||||
from web.chargen import views
|
||||
|
||||
urlpatterns = [
|
||||
# ex: /chargen/
|
||||
url(r'^$', views.index, name='index'),
|
||||
# ex: /chargen/5/
|
||||
url(r'^(?P<app_id>[0-9]+)/$', views.detail, name='detail'),
|
||||
# ex: /chargen/create
|
||||
url(r'^create/$', views.creating, name='creating'),
|
||||
]
|
||||
```
|
||||
|
||||
You could change the format as you desire. To make it more secure, you could remove app_id from the
|
||||
"detail" url, and instead just fetch the account’s applications using a unifying field like
|
||||
account_id to find all the character application objects to display.
|
||||
|
||||
We must also update the main `mygame/web/urls.py` file (that is, one level up from our chargen app),
|
||||
so the main website knows where our app's views are located. Find the `patterns` variable, and
|
||||
change it to include:
|
||||
|
||||
```python
|
||||
# in file mygame/web/urls.py
|
||||
|
||||
from django.conf.urls import url, include
|
||||
|
||||
# default evennia patterns
|
||||
from evennia.web.urls import urlpatterns
|
||||
|
||||
# eventual custom patterns
|
||||
custom_patterns = [
|
||||
# url(r'/desired/url/', view, name='example'),
|
||||
]
|
||||
|
||||
# this is required by Django.
|
||||
urlpatterns += [
|
||||
url(r'^chargen/', include('web.chargen.urls')),
|
||||
]
|
||||
|
||||
urlpatterns = custom_patterns + urlpatterns
|
||||
```
|
||||
|
||||
### URLs - Checkpoint:
|
||||
|
||||
* You’ve created a urls.py file in the `mygame/web/chargen` directory
|
||||
* You have edited the main `mygame/web/urls.py` file to include urls to the `chargen` directory.
|
||||
|
||||
## HTML Templates
|
||||
|
||||
So we have our url patterns, views, and models defined. Now we must define our HTML templates that
|
||||
the actual user will see and interact with. For this tutorial we us the basic *prosimii* template
|
||||
that comes with Evennia.
|
||||
|
||||
Take note that we use `user.is_authenticated` to make sure that the user cannot create a character
|
||||
without logging in.
|
||||
|
||||
These files will all go into the `/mygame/web/chargen/templates/chargen/` directory.
|
||||
|
||||
### index.html
|
||||
|
||||
This HTML template should hold a list of all the applications the account currently has active. For
|
||||
this demonstration, we will only list the applications that the account has submitted. You could
|
||||
easily adjust this to include saved applications, or other types of applications if you have
|
||||
different kinds.
|
||||
|
||||
Please refer back to `views.py` to see where we define the variables these templates make use of.
|
||||
|
||||
```html
|
||||
<!-- file mygame/web/chargen/templates/chargen/index.html-->
|
||||
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
{% if user.is_authenticated %}
|
||||
<h1>Character Generation</h1>
|
||||
{% if sub_apps %}
|
||||
<ul>
|
||||
{% for sub_app in sub_apps %}
|
||||
<li><a href="/chargen/{{ sub_app.app_id }}/">{{ sub_app.char_name }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p>You haven't submitted any character applications.</p>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p>Please <a href="{% url 'login'%}">login</a>first.<a/></p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
### detail.html
|
||||
|
||||
This page should show a detailed character sheet of their application. This will only show their
|
||||
name and character background. You will likely want to extend this to show many more fields for your
|
||||
game. In a full-fledged character generation, you may want to extend the boolean attribute of
|
||||
submitted to allow accounts to save character applications and submit them later.
|
||||
|
||||
```html
|
||||
<!-- file mygame/web/chargen/templates/chargen/detail.html-->
|
||||
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<h1>Character Information</h1>
|
||||
{% if user.is_authenticated %}
|
||||
{% if user.id == p_id %}
|
||||
<h2>{{name}}</h2>
|
||||
<h2>Background</h2>
|
||||
<p>{{background}}</p>
|
||||
<p>Submitted: {{submitted}}</p>
|
||||
{% else %}
|
||||
<p>You didn't submit this character.</p>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p>You aren't logged in.</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
### create.html
|
||||
|
||||
Our create HTML template will use the Django form we defined back in views.py/forms.py to drive the
|
||||
majority of the application process. There will be a form input for every field we defined in
|
||||
forms.py, which is handy. We have used POST as our method because we are sending information to the
|
||||
server that will update the database. As an alternative, GET would be much less secure. You can read
|
||||
up on documentation elsewhere on the web for GET vs. POST.
|
||||
|
||||
```html
|
||||
<!-- file mygame/web/chargen/templates/chargen/create.html-->
|
||||
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<h1>Character Creation</h1>
|
||||
{% if user.is_authenticated %}
|
||||
<form action="/chargen/create/" method="post">
|
||||
{% csrf_token %}
|
||||
{{ form }}
|
||||
<input type="submit" name="submit" value="Submit"/>
|
||||
</form>
|
||||
{% else %}
|
||||
<p>You aren't logged in.</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
### Templates - Checkpoint:
|
||||
|
||||
* Create a `index.html`, `detail.html` and `create.html` template in your
|
||||
`mygame/web/chargen/templates/chargen` directory
|
||||
|
||||
## Activating your new character generation
|
||||
|
||||
After finishing this tutorial you should have edited or created the following files:
|
||||
|
||||
```bash
|
||||
mygame/web/urls.py
|
||||
mygame/web/chargen/models.py
|
||||
mygame/web/chargen/views.py
|
||||
mygame/web/chargen/urls.py
|
||||
mygame/web/chargen/templates/chargen/index.html
|
||||
mygame/web/chargen/templates/chargen/create.html
|
||||
mygame/web/chargen/templates/chargen/detail.html
|
||||
```
|
||||
|
||||
Once you have all these files stand in your `mygame/`folder and run:
|
||||
|
||||
```bash
|
||||
evennia makemigrations
|
||||
evennia migrate
|
||||
```
|
||||
|
||||
This will create and update the models. If you see any errors at this stage, read the traceback
|
||||
carefully, it should be relatively easy to figure out where the error is.
|
||||
|
||||
Login to the website (you need to have previously registered an Player account with the game to do
|
||||
this). Next you navigate to `http://yourwebsite.com/chargen` (if you are running locally this will
|
||||
be something like `http://localhost:4001/chargen` and you will see your new app in action.
|
||||
|
||||
This should hopefully give you a good starting point in figuring out how you’d like to approach your
|
||||
own web generation. The main difficulties are in setting the appropriate settings on your newly
|
||||
created character object. Thankfully, the Evennia API makes this easy.
|
||||
|
||||
## Adding a no CAPCHA reCAPCHA on your character generation
|
||||
|
||||
As sad as it is, if your server is open to the web, bots might come to visit and take advantage of
|
||||
your open form to create hundreds, thousands, millions of characters if you give them the
|
||||
opportunity. This section shows you how to use the [No CAPCHA
|
||||
reCAPCHA](https://www.google.com/recaptcha/intro/invisible.html) designed by Google. Not only is it
|
||||
easy to use, it is user-friendly... for humans. A simple checkbox to check, except if Google has
|
||||
some suspicion, in which case you will have a more difficult test with an image and the usual text
|
||||
inside. It's worth pointing out that, as long as Google doesn't suspect you of being a robot, this
|
||||
is quite useful, not only for common users, but to screen-reader users, to which reading inside of
|
||||
an image is pretty difficult, if not impossible. And to top it all, it will be so easy to add in
|
||||
your website.
|
||||
|
||||
### Step 1: Obtain a SiteKey and secret from Google
|
||||
|
||||
The first thing is to ask Google for a way to safely authenticate your website to their service. To
|
||||
do it, we need to create a site key and a secret. Go to
|
||||
[https://www.google.com/recaptcha/admin](https://www.google.com/recaptcha/admin) to create such a
|
||||
site key. It's quite easy when you have a Google account.
|
||||
|
||||
When you have created your site key, save it safely. Also copy your secret key as well. You should
|
||||
find both information on the web page. Both would contain a lot of letters and figures.
|
||||
|
||||
### Step 2: installing and configuring the dedicated Django app
|
||||
|
||||
Since Evennia runs on Django, the easiest way to add our CAPCHA and perform the proper check is to
|
||||
install the dedicated Django app. Quite easy:
|
||||
|
||||
pip install django-nocaptcha-recaptcha
|
||||
|
||||
And add it to the installed apps in your settings. In your `mygame/server/conf/settings.py`, you
|
||||
might have something like this:
|
||||
|
||||
```python
|
||||
# ...
|
||||
INSTALLED_APPS += (
|
||||
'web.chargen',
|
||||
'nocaptcha_recaptcha',
|
||||
)
|
||||
```
|
||||
|
||||
Don't close the setting file just yet. We have to add in the site key and secret key. You can add
|
||||
them below:
|
||||
|
||||
```python
|
||||
# NoReCAPCHA site key
|
||||
NORECAPTCHA_SITE_KEY = "PASTE YOUR SITE KEY HERE"
|
||||
# NoReCAPCHA secret key
|
||||
NORECAPTCHA_SECRET_KEY = "PUT YOUR SECRET KEY HERE"
|
||||
```
|
||||
|
||||
### Step 3: Adding the CAPCHA to our form
|
||||
|
||||
Finally we have to add the CAPCHA to our form. It will be pretty easy too. First, open your
|
||||
`web/chargen/forms.py` file. We're going to add a new field, but hopefully, all the hard work has
|
||||
been done for us. Update at your convenience, You might end up with something like this:
|
||||
|
||||
```python
|
||||
from django import forms
|
||||
from nocaptcha_recaptcha.fields import NoReCaptchaField
|
||||
|
||||
class AppForm(forms.Form):
|
||||
name = forms.CharField(label='Character Name', max_length=80)
|
||||
background = forms.CharField(label='Background')
|
||||
captcha = NoReCaptchaField()
|
||||
```
|
||||
|
||||
As you see, we added a line of import (line 2) and a field in our form.
|
||||
|
||||
And lastly, we need to update our HTML file to add in the Google library. You can open
|
||||
`web/chargen/templates/chargen/create.html`. There's only one line to add:
|
||||
|
||||
```html
|
||||
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
|
||||
```
|
||||
|
||||
And you should put it at the bottom of the page. Just before the closing body would be good, but
|
||||
for the time being, the base page doesn't provide a footer block, so we'll put it in the content
|
||||
block. Note that it's not the best place, but it will work. In the end, your
|
||||
`web/chargen/templates/chargen/create.html` file should look like this:
|
||||
|
||||
```html
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<h1>Character Creation</h1>
|
||||
{% if user.is_authenticated %}
|
||||
<form action="/chargen/create/" method="post">
|
||||
{% csrf_token %}
|
||||
{{ form }}
|
||||
<input type="submit" name="submit" value="Submit"/>
|
||||
</form>
|
||||
{% else %}
|
||||
<p>You aren't logged in.</p>
|
||||
{% endif %}
|
||||
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
Reload and open [http://localhost:4001/chargen/create](http://localhost:4001/chargen/create/) and
|
||||
you should see your beautiful CAPCHA just before the "submit" button. Try not to check the checkbox
|
||||
to see what happens. And do the same while checking the checkbox!
|
||||
229
docs/source/Howto/Web-Character-View-Tutorial.md
Normal file
229
docs/source/Howto/Web-Character-View-Tutorial.md
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
# Web Character View Tutorial
|
||||
|
||||
|
||||
**Before doing this tutorial you will probably want to read the intro in [Basic Web tutorial](Web-
|
||||
Tutorial).**
|
||||
|
||||
In this tutorial we will create a web page that displays the stats of a game character. For this,
|
||||
and all other pages we want to make specific to our game, we'll need to create our own Django "app"
|
||||
|
||||
We'll call our app `character`, since it will be dealing with character information. From your game
|
||||
dir, run
|
||||
|
||||
evennia startapp character
|
||||
|
||||
This will create a directory named `character` in the root of your game dir. It contains all basic
|
||||
files that a Django app needs. To keep `mygame` well ordered, move it to your `mygame/web/`
|
||||
directory instead:
|
||||
|
||||
mv character web/
|
||||
|
||||
Note that we will not edit all files in this new directory, many of the generated files are outside
|
||||
the scope of this tutorial.
|
||||
|
||||
In order for Django to find our new web app, we'll need to add it to the `INSTALLED_APPS` setting.
|
||||
Evennia's default installed apps are already set, so in `server/conf/settings.py`, we'll just extend
|
||||
them:
|
||||
|
||||
```python
|
||||
INSTALLED_APPS += ('web.character',)
|
||||
```
|
||||
|
||||
> Note: That end comma is important. It makes sure that Python interprets the addition as a tuple
|
||||
instead of a string.
|
||||
|
||||
The first thing we need to do is to create a *view* and an *URL pattern* to point to it. A view is a
|
||||
function that generates the web page that a visitor wants to see, while the URL pattern lets Django
|
||||
know what URL should trigger the view. The pattern may also provide some information of its own as
|
||||
we shall see.
|
||||
|
||||
Here is our `character/urls.py` file (**Note**: you may have to create this file if a blank one
|
||||
wasn't generated for you):
|
||||
|
||||
```python
|
||||
# URL patterns for the character app
|
||||
|
||||
from django.conf.urls import url
|
||||
from web.character.views import sheet
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^sheet/(?P<object_id>\d+)/$', sheet, name="sheet")
|
||||
]
|
||||
```
|
||||
|
||||
This file contains all of the URL patterns for the application. The `url` function in the
|
||||
`urlpatterns` list are given three arguments. The first argument is a pattern-string used to
|
||||
identify which URLs are valid. Patterns are specified as *regular expressions*. Regular expressions
|
||||
are used to match strings and are written in a special, very compact, syntax. A detailed description
|
||||
of regular expressions is beyond this tutorial but you can learn more about them
|
||||
[here](https://docs.python.org/2/howto/regex.html). For now, just accept that this regular
|
||||
expression requires that the visitor's URL looks something like this:
|
||||
|
||||
````
|
||||
sheet/123/
|
||||
````
|
||||
|
||||
That is, `sheet/` followed by a number, rather than some other possible URL pattern. We will
|
||||
interpret this number as object ID. Thanks to how the regular expression is formulated, the pattern
|
||||
recognizer stores the number in a variable called `object_id`. This will be passed to the view (see
|
||||
below). We add the imported view function (`sheet`) in the second argument. We also add the `name`
|
||||
keyword to identify the URL pattern itself. You should always name your URL patterns, this makes
|
||||
them easy to refer to in html templates using the `{% url %}` tag (but we won't get more into that
|
||||
in this tutorial).
|
||||
|
||||
> Security Note: Normally, users do not have the ability to see object IDs within the game (it's
|
||||
restricted to superusers only). Exposing the game's object IDs to the public like this enables
|
||||
griefers to perform what is known as an [account enumeration
|
||||
attack](http://www.sans.edu/research/security-laboratory/article/attacks-browsing) in the efforts of
|
||||
hijacking your superuser account. Consider this: in every Evennia installation, there are two
|
||||
objects that we can *always* expect to exist and have the same object IDs-- Limbo (#2) and the
|
||||
superuser you create in the beginning (#1). Thus, the griefer can get 50% of the information they
|
||||
need to hijack the admin account (the admin's username) just by navigating to `sheet/1`!
|
||||
|
||||
Next we create `views.py`, the view file that `urls.py` refers to.
|
||||
|
||||
```python
|
||||
# Views for our character app
|
||||
|
||||
from django.http import Http404
|
||||
from django.shortcuts import render
|
||||
from django.conf import settings
|
||||
|
||||
from evennia.utils.search import object_search
|
||||
from evennia.utils.utils import inherits_from
|
||||
|
||||
def sheet(request, object_id):
|
||||
object_id = '#' + object_id
|
||||
try:
|
||||
character = object_search(object_id)[0]
|
||||
except IndexError:
|
||||
raise Http404("I couldn't find a character with that ID.")
|
||||
if not inherits_from(character, settings.BASE_CHARACTER_TYPECLASS):
|
||||
raise Http404("I couldn't find a character with that ID. "
|
||||
"Found something else instead.")
|
||||
return render(request, 'character/sheet.html', {'character': character})
|
||||
```
|
||||
|
||||
As explained earlier, the URL pattern parser in `urls.py` parses the URL and passes `object_id` to
|
||||
our view function `sheet`. We do a database search for the object using this number. We also make
|
||||
sure such an object exists and that it is actually a Character. The view function is also handed a
|
||||
`request` object. This gives us information about the request, such as if a logged-in user viewed it
|
||||
- we won't use that information here but it is good to keep in mind.
|
||||
|
||||
On the last line, we call the `render` function. Apart from the `request` object, the `render`
|
||||
function takes a path to an html template and a dictionary with extra data you want to pass into
|
||||
said template. As extra data we pass the Character object we just found. In the template it will be
|
||||
available as the variable "character".
|
||||
|
||||
The html template is created as `templates/character/sheet.html` under your `character` app folder.
|
||||
You may have to manually create both `template` and its subfolder `character`. Here's the template
|
||||
to create:
|
||||
|
||||
````html
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
|
||||
<h1>{{ character.name }}</h1>
|
||||
|
||||
<p>{{ character.db.desc }}</p>
|
||||
|
||||
<h2>Stats</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Stat</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Strength</td>
|
||||
<td>{{ character.db.str }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Intelligence</td>
|
||||
<td>{{ character.db.int }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Speed</td>
|
||||
<td>{{ character.db.spd }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Skills</h2>
|
||||
<ul>
|
||||
{% for skill in character.db.skills %}
|
||||
<li>{{ skill }}</li>
|
||||
{% empty %}
|
||||
<li>This character has no skills yet.</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
{% if character.db.approved %}
|
||||
<p class="success">This character has been approved!</p>
|
||||
{% else %}
|
||||
<p class="warning">This character has not yet been approved!</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
````
|
||||
|
||||
In Django templates, `{% ... %}` denotes special in-template "functions" that Django understands.
|
||||
The `{{ ... }}` blocks work as "slots". They are replaced with whatever value the code inside the
|
||||
block returns.
|
||||
|
||||
The first line, `{% extends "base.html" %}`, tells Django that this template extends the base
|
||||
template that Evennia is using. The base template is provided by the theme. Evennia comes with the
|
||||
open-source third-party theme `prosimii`. You can find it and its `base.html` in
|
||||
`evennia/web/templates/prosimii`. Like other templates, these can be overwritten.
|
||||
|
||||
The next line is `{% block content %}`. The `base.html` file has `block`s, which are placeholders
|
||||
that templates can extend. The main block, and the one we use, is named `content`.
|
||||
|
||||
We can access the `character` variable anywhere in the template because we passed it in the `render`
|
||||
call at the end of `view.py`. That means we also have access to the Character's `db` attributes,
|
||||
much like you would in normal Python code. You don't have the ability to call functions with
|
||||
arguments in the template-- in fact, if you need to do any complicated logic, you should do it in
|
||||
`view.py` and pass the results as more variables to the template. But you still have a great deal of
|
||||
flexibility in how you display the data.
|
||||
|
||||
We can do a little bit of logic here as well. We use the `{% for %} ... {% endfor %}` and `{% if %}
|
||||
... {% else %} ... {% endif %}` structures to change how the template renders depending on how many
|
||||
skills the user has, or if the user is approved (assuming your game has an approval system).
|
||||
|
||||
The last file we need to edit is the master URLs file. This is needed in order to smoothly integrate
|
||||
the URLs from your new `character` app with the URLs from Evennia's existing pages. Find the file
|
||||
`web/urls.py` and update its `patterns` list as follows:
|
||||
|
||||
```python
|
||||
# web/urls.py
|
||||
|
||||
custom_patterns = [
|
||||
url(r'^character/', include('web.character.urls'))
|
||||
]
|
||||
```
|
||||
|
||||
Now reload the server with `evennia reload` and visit the page in your browser. If you haven't
|
||||
changed your defaults, you should be able to find the sheet for character `#1` at
|
||||
`http://localhost:4001/character/sheet/1/`
|
||||
|
||||
Try updating the stats in-game and refresh the page in your browser. The results should show
|
||||
immediately.
|
||||
|
||||
As an optional final step, you can also change your character typeclass to have a method called
|
||||
'get_absolute_url'.
|
||||
```python
|
||||
# typeclasses/characters.py
|
||||
|
||||
# inside Character
|
||||
def get_absolute_url(self):
|
||||
from django.urls import reverse
|
||||
return reverse('character:sheet', kwargs={'object_id':self.id})
|
||||
```
|
||||
Doing so will give you a 'view on site' button in the top right of the Django Admin Objects
|
||||
changepage that links to your new character sheet, and allow you to get the link to a character's
|
||||
page by using {{ object.get_absolute_url }} in any template where you have a given object.
|
||||
|
||||
*Now that you've made a basic page and app with Django, you may want to read the full Django
|
||||
tutorial to get a better idea of what it can do. [You can find Django's tutorial
|
||||
here](https://docs.djangoproject.com/en/1.8/intro/tutorial01/).*
|
||||
Loading…
Add table
Add a link
Reference in a new issue