mirror of
https://github.com/evennia/evennia.git
synced 2026-03-16 21:06:30 +01:00
Refactor 1.0 docs with new toctree structure and inheritance
This commit is contained in:
parent
62477eac50
commit
628afe9367
142 changed files with 3967 additions and 3024 deletions
232
docs/source/Howtos/Add-a-wiki-on-your-website.md
Normal file
232
docs/source/Howtos/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](Beginner-Tutorial/Part5/Web-Tutorial.md).** 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](https://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](https://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!
|
||||
262
docs/source/Howtos/Arxcode-Installation.md
Normal file
262
docs/source/Howtos/Arxcode-Installation.md
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
# Arxcode installing help
|
||||
|
||||
[Arx - After the Reckoning](https://play.arxmush.org/) is a big and very popular
|
||||
[Evennia](https://www.evennia.com)-based game. Arx is heavily roleplaying-centric, relying on game
|
||||
masters to drive the story. Technically it's maybe best described as "a MUSH, but with more coded
|
||||
systems". In August of 2018, the game's developer, Tehom, generously released the [source code of
|
||||
Arx on github](https://github.com/Arx-Game/arxcode). This is a treasure-trove for developers wanting
|
||||
to pick ideas or even get a starting game to build on.
|
||||
|
||||
> These instructions are based on the Arx-code released as of *Aug 12, 2018*. They will probably
|
||||
> not work 100% out of the box anymore. Report any differences and changes needed.
|
||||
|
||||
It's not too hard to run Arx from the sources (of course you'll start with an empty database) but
|
||||
since part of Arx has grown organically, it doesn't follow standard Evennia paradigms everywhere.
|
||||
This page covers one take on installing and setting things up while making your new Arx-based game
|
||||
better match with the vanilla Evennia install.
|
||||
|
||||
## Installing Evennia
|
||||
|
||||
Firstly, set aside a folder/directory on your drive for everything to follow.
|
||||
|
||||
You need to start by installing [Evennia](https://www.evennia.com) by following most of the
|
||||
[Git-installation instructions](../Setup/Installation-Git.md) for your OS. The difference is that you
|
||||
need to `git clone https://github.com/TehomCD/evennia.git` instead of Evennia's repo because Arx
|
||||
uses TehomCD's older Evennia 0.8 [fork](https://github.com/TehomCD/evennia), notably still using
|
||||
Python2. This detail is important if referring to newer Evennia documentation.
|
||||
|
||||
If you are new to Evennia it's *highly* recommended that you run through the normal install
|
||||
instructions in full - including initializing and starting a new empty game and connecting to it.
|
||||
That way you can be sure Evennia works correctly as a baseline.
|
||||
|
||||
After installing you should have a `virtualenv` running and you should have the following file
|
||||
structure in your set-aside folder:
|
||||
|
||||
```
|
||||
muddev/
|
||||
vienv/
|
||||
evennia/
|
||||
mygame/
|
||||
```
|
||||
|
||||
Here `mygame` is the empty game you created during the Evennia install, with `evennia --init`. Go to
|
||||
that and run `evennia stop` to make sure your empty game is not running. We'll instead let Evenna
|
||||
run Arx, so in principle you could erase `mygame` - but it could also be good to have a clean game
|
||||
to compare to.
|
||||
|
||||
## Installing Arxcode
|
||||
|
||||
`cd` to the root of your directory and clone the released source code from github:
|
||||
|
||||
git clone https://github.com/Arx-Game/arxcode.git myarx
|
||||
|
||||
A new folder `myarx` should appear next to the ones you already had. You could rename this to
|
||||
something else if you want.
|
||||
|
||||
`cd` into `myarx`. If you wonder about the structure of the game dir, you can
|
||||
[read more about it here](Beginner-Tutorial/Part1/Gamedir-Overview.md).
|
||||
|
||||
### Clean up settings
|
||||
|
||||
Arx has split evennia's normal settings into `base_settings.py` and `production_settings.py`. It
|
||||
also has its own solution for managing 'secret' parts of the settings file. We'll keep most of Arx
|
||||
way but we'll remove the secret-handling and replace it with the normal Evennia method.
|
||||
|
||||
`cd` into `myarx/server/conf/` and open the file `settings.py` in a text editor. The top part (within
|
||||
`"""..."""`) is just help text. Wipe everything underneath that and make it look like this instead
|
||||
(don't forget to save):
|
||||
|
||||
```
|
||||
from base_settings import *
|
||||
|
||||
TELNET_PORTS = [4000]
|
||||
SERVERNAME = "MyArx"
|
||||
GAME_SLOGAN = "The cool game"
|
||||
|
||||
try:
|
||||
from server.conf.secret_settings import *
|
||||
except ImportError:
|
||||
print("secret_settings.py file not found or failed to import.")
|
||||
```
|
||||
|
||||
> Note: Indents and capitalization matter in Python. Make indents 4 spaces (not tabs) for your own
|
||||
> sanity. If you want a starter on Python in Evennia, [you can look here](Python-basic-
|
||||
introduction).
|
||||
|
||||
This will import Arx' base settings and override them with the Evennia-default telnet port and give
|
||||
the game a name. The slogan changes the sub-text shown under the name of your game in the website
|
||||
header. You can tweak these to your own liking later.
|
||||
|
||||
Next, create a new, empty file `secret_settings.py` in the same location as the `settings.py` file.
|
||||
This can just contain the following:
|
||||
|
||||
```python
|
||||
SECRET_KEY = "sefsefiwwj3 jnwidufhjw4545_oifej whewiu hwejfpoiwjrpw09&4er43233fwefwfw"
|
||||
|
||||
```
|
||||
|
||||
Replace the long random string with random ASCII characters of your own. The secret key should not
|
||||
be shared.
|
||||
|
||||
Next, open `myarx/server/conf/base_settings.py` in your text editor. We want to remove/comment out
|
||||
all mentions of the `decouple` package, which Evennia doesn't use (we use `private_settings.py` to
|
||||
hide away settings that should not be shared).
|
||||
|
||||
Comment out `from decouple import config` by adding a `#` to the start of the line: `# from decouple
|
||||
import config`. Then search for `config(` in the file and comment out all lines where this is used.
|
||||
Many of these are specific to the server environment where the original Arx runs, so is not that
|
||||
relevant to us.
|
||||
|
||||
### Install Arx dependencies
|
||||
|
||||
Arx has some further dependencies beyond vanilla Evennia. Start by `cd`:ing to the root of your
|
||||
`myarx` folder.
|
||||
|
||||
> If you run *Linux* or *Mac*: Edit `myarx/requirements.txt` and comment out the line
|
||||
> `pypiwin32==219` - it's only needed on Windows and will give an error on other platforms.
|
||||
|
||||
Make sure your `virtualenv` is active, then run
|
||||
|
||||
pip install -r requirements.txt
|
||||
|
||||
The needed Python packages will be installed for you.
|
||||
|
||||
### Adding logs/ folder
|
||||
|
||||
The Arx repo does not contain the `myarx/server/logs/` folder Evennia expects for storing server
|
||||
logs. This is simple to add:
|
||||
|
||||
# linux/mac
|
||||
mkdir server/logs
|
||||
# windows
|
||||
mkdir server\logs
|
||||
|
||||
### Setting up the database and starting
|
||||
|
||||
From the `myarx` folder, run
|
||||
|
||||
evennia migrate
|
||||
|
||||
This creates the database and will step through all database migrations needed.
|
||||
|
||||
evennia start
|
||||
|
||||
If all goes well Evennia will now start up, running Arx! You can connect to it on `localhost` (or
|
||||
`127.0.0.1` if your platform doesn't alias `localhost`), port `4000` using a Telnet client.
|
||||
Alternatively, you can use your web browser to browse to `http://localhost:4001` to see the game's
|
||||
website and get to the web client.
|
||||
|
||||
When you log in you'll get the standard Evennia greeting (since the database is empty), but you can
|
||||
try `help` to see that it's indeed Arx that is running.
|
||||
|
||||
### Additional Setup Steps
|
||||
|
||||
The first time you start Evennia after creating the database with the `evennia migrate` step above,
|
||||
it should create a few starting objects for you - your superuser account, which it will prompt you
|
||||
to enter, a starting room (Limbo), and a character object for you. If for some reason this does not
|
||||
occur, you may have to follow the steps below. For the first time Superuser login you may have to
|
||||
run steps 7-8 and 10 to create and connect to your in-came Character.
|
||||
|
||||
1. Login to the game website with your Superuser account.
|
||||
2. Press the `Admin` button to get into the (Django-) Admin Interface.
|
||||
3. Navigate to the `Accounts` section.
|
||||
4. Add a new Account named for the new staffer. Use a place holder password and dummy e-mail
|
||||
address.
|
||||
5. Flag account as `Staff` and apply the `Admin` permission group (This assumes you have already set
|
||||
up an Admin Group in Django).
|
||||
6. Add Tags named `player` and `developer`.
|
||||
7. Log into the game using the web client (or a third-party telnet client) using your superuser
|
||||
account. Move to where you want the new staffer character to appear.
|
||||
8. In the game client, run `@create/drop <staffername>:typeclasses.characters.Character`, where
|
||||
`<staffername>` is usually the same name you used for the Staffer account you created in the
|
||||
Admin earlier (if you are creating a Character for your superuser, use your superuser account
|
||||
name).
|
||||
This creates a new in-game Character and places it in your current location.
|
||||
9. Have the new Admin player log into the game.
|
||||
10. Have the new Admin puppet the character with `@ic StafferName`.
|
||||
11. Have the new Admin change their password - `@password <old password> = <new password>`.
|
||||
|
||||
Now that you have a Character and an Account object, there's a few additional things you may need to
|
||||
do in order for some commands to function properly. You can either execute these as in-game commands
|
||||
while `ic` (controlling your character object).
|
||||
|
||||
1. `py from web.character.models import RosterEntry;RosterEntry.objects.create(player=self.player,
|
||||
character=self)`
|
||||
2. `py from world.dominion.models import PlayerOrNpc, AssetOwner;dompc =
|
||||
PlayerOrNpc.objects.create(player = self.player);AssetOwner.objects.create(player=dompc)`
|
||||
|
||||
Those steps will give you a 'RosterEntry', 'PlayerOrNpc', and 'AssetOwner' objects. RosterEntry
|
||||
explicitly connects a character and account object together, even while offline, and contains
|
||||
additional information about a character's current presence in game (such as which 'roster' they're
|
||||
in, if you choose to use an active roster of characters). PlayerOrNpc are more character extensions,
|
||||
as well as support for npcs with no in-game presence and just represented by a name which can be
|
||||
offscreen members of a character's family. It also allows for membership in Organizations.
|
||||
AssetOwner holds information about a character or organization's money and resources.
|
||||
|
||||
## Alternate Windows install guide
|
||||
|
||||
_Contributed by Pax_
|
||||
|
||||
If for some reason you cannot use the Windows Subsystem for Linux (which would use instructions
|
||||
identical to the ones above), it's possible to get Evennia/Arx running under Anaconda for Windows. The
|
||||
process is a little bit trickier.
|
||||
|
||||
Make sure you have:
|
||||
* Git for Windows https://git-scm.com/download/win
|
||||
* Anaconda for Windows https://www.anaconda.com/distribution/
|
||||
* VC++ Compiler for Python 2.7 https://aka.ms/vcpython27
|
||||
|
||||
conda update conda
|
||||
conda create -n arx python=2.7
|
||||
source activate arx
|
||||
|
||||
Set up a convenient repository place for things.
|
||||
|
||||
cd ~
|
||||
mkdir Source
|
||||
cd Source
|
||||
mkdir Arx
|
||||
cd Arx
|
||||
|
||||
Replace the SSH git clone links below with your own github forks.
|
||||
If you don't plan to change Evennia at all, you can use the
|
||||
evennia/evennia.git repo instead of a forked one.
|
||||
|
||||
git clone git@github.com:<youruser>/evennia.git
|
||||
git clone git@github.com:<youruser>/arxcode.git
|
||||
|
||||
Evennia is a package itself, so we want to install it and all of its
|
||||
prerequisites, after switching to the appropriately-tagged branch for
|
||||
Arxcode.
|
||||
|
||||
cd evennia
|
||||
git checkout tags/v0.7 -b arx-master
|
||||
pip install -e .
|
||||
|
||||
Arx has some dependencies of its own, so now we'll go install them
|
||||
As it is not a package, we'll use the normal requirements file.
|
||||
|
||||
cd ../arxcode
|
||||
pip install -r requirements.txt
|
||||
|
||||
The git repo doesn't include the empty log directory and Evennia is unhappy if you
|
||||
don't have it, so while still in the arxcode directory...
|
||||
|
||||
mkdir server/logs
|
||||
|
||||
Now hit https://github.com/evennia/evennia/wiki/Arxcode-installing-help and
|
||||
change the setup stuff as in the 'Clean up settings' section.
|
||||
|
||||
Then we will create our default database...
|
||||
|
||||
../evennia/bin/windows/evennia.bat migrate
|
||||
|
||||
...and do the first run. You need winpty because Windows does not have a TTY/PTY
|
||||
by default, and so the Python console input commands (used for prompts on first
|
||||
run) will fail and you will end up in an unhappy place. Future runs, you should
|
||||
not need winpty.
|
||||
|
||||
winpty ../evennia/bin/windows/evennia.bat start
|
||||
|
||||
Once this is done, you should have your Evennia server running Arxcode up
|
||||
on localhost at port 4000, and the webserver at http://localhost:4001/
|
||||
116
docs/source/Howtos/Beginner-Tutorial/Beginner-Tutorial-Intro.md
Normal file
116
docs/source/Howtos/Beginner-Tutorial/Beginner-Tutorial-Intro.md
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
# Beginner Tutorial
|
||||
|
||||
```{eval-rst}
|
||||
.. sidebar:: Tutorial Parts
|
||||
|
||||
**Introduction**
|
||||
Getting set up.
|
||||
Part 1: `What we have <Part1/Beginner-Tutorial-Part1-Intro.html>`_
|
||||
A tour of Evennia and how to use the tools, including an introduction to Python.
|
||||
Part 2: `What we want <Part2/Beginner-Tutorial-Part2-Intro.html>`_
|
||||
Planning our tutorial game and what to think about when planning your own in the future.
|
||||
Part 3: `How we get there <Part3/Beginner-Tutorial-Part3-Intro.html>`_
|
||||
Getting down to the meat of extending Evennia to make our game
|
||||
Part 4: `Using what we created <Part4/Beginner-Tutorial-Part4-Intro.html>`_
|
||||
Building a tech-demo and world content to go with our code
|
||||
Part 5: `Showing the world <Part5/Beginner-Tutorial-Part5-Intro.html>`_
|
||||
Taking our new game online and let players try it out
|
||||
|
||||
```
|
||||
Welcome to Evennia! This multi-part Beginner Tutorial will help you get off the ground. It consists
|
||||
of five parts, each with several lessons. You can pick what seems interesting, but if you
|
||||
follow through to the end you will have created a little online game of your own to play
|
||||
and share with others!
|
||||
|
||||
Use the menu on the right to get the index of each tutorial-part. Use the [next](Part1/Beginner-Tutorial-Part1-Intro.md)
|
||||
and [previous](../Howtos-Overview.md) links to step from lesson to lesson.
|
||||
|
||||
## Things you need
|
||||
|
||||
- A Command line
|
||||
- A MUD client (or web browser)
|
||||
- A text-editor/IDE
|
||||
- Evennia installed and a game-dir initialized
|
||||
|
||||
### A Command line
|
||||
|
||||
You need to know how to find your Terminal/Console in your OS. The Evennia server can be controlled
|
||||
from in-game, but you _will_ need to use the command-line to get anywhere. Here are some starters:
|
||||
|
||||
- [Django-girls' Intro to the Command line for different OS:es](https://tutorial.djangogirls.org/en/intro_to_command_line/)
|
||||
|
||||
Note that we usually only show forward-slashes `/` for file system paths. Windows users should mentally convert this to
|
||||
back-slashes `\` instead.
|
||||
|
||||
### A MUD client
|
||||
|
||||
You might already have a MUD-client you prefer. Check out the [grid of supported clients](../../Setup/Client-Support-Grid.md) for aid.
|
||||
If telnet's not your thing, you can also just use Evennia's web client in your browser.
|
||||
|
||||
> In this documentation we often use the terms 'MUD', 'MU' or 'MU*' interchangeably
|
||||
to represent all the historically different forms of text-based multiplayer game-styles,
|
||||
like MUD, MUX, MUSH, MUCK, MOO and others. Evennia can be used to create all those game-styles
|
||||
and more.
|
||||
|
||||
### An Editor
|
||||
You need a text-editor to edit Python source files. Most everything that can edit and output raw
|
||||
text works (so not Word).
|
||||
|
||||
- [Here's a blog post summing up some of the alternatives](https://www.elegantthemes.com/blog/resources/best-code-editors) - these
|
||||
things don't change much from year to year. Popular choices for Python are PyCharm, VSCode, Atom, Sublime Text and Notepad++.
|
||||
Evennia is to a very large degree coded in VIM, but that's not suitable for beginners.
|
||||
|
||||
> Hint: When setting up your editor, make sure that pressing TAB inserts _4 spaces_ rather than a Tab-character. Since
|
||||
> Python is whitespace-aware, this will make your life a lot easier.
|
||||
|
||||
|
||||
### Set up a game dir for the tutorial
|
||||
|
||||
Next you should make sure you have [installed Evennia](../../Setup/Installation.md). If you followed the instructions
|
||||
you will already have created a game-dir. You could use that for this tutorial or you may want to do the
|
||||
tutorial in its own, isolated game dir; it's up to you.
|
||||
|
||||
- If you want a new gamedir for the tutorial game and already have Evennia running with another gamedir,
|
||||
first enter that gamedir and run
|
||||
|
||||
evennia stop
|
||||
|
||||
> If you want to run two parallel servers, that'd be fine too, but one would have to use
|
||||
> different ports from the defaults, or there'd be a clash. We will go into changing settings later.
|
||||
- Now go to where you want to create your tutorial-game. We will always refer to it as `mygame` so
|
||||
it may be convenient if you do too:
|
||||
|
||||
evennia --init mygame
|
||||
cd mygame
|
||||
evennia migrate
|
||||
evennia start --log
|
||||
|
||||
Add your superuser name and password at the prompt (email is optional). Make sure you can
|
||||
go to `localhost:4000` in your MUD client or to [http://localhost:4001](http://localhost:4001)
|
||||
in your web browser (Mac users: Try `127.0.0.1` instead of `localhost` if you have trouble).
|
||||
|
||||
The above `--log` flag will have Evennia output all its logs to the terminal. This will block
|
||||
the terminal from other input. To leave the log-view, press `Ctrl-C` (`Cmd-C` on Mac). To see
|
||||
the log again just run
|
||||
|
||||
evennia --log
|
||||
|
||||
You should now be good to go on to [the first part of the tutorial](Part1/Beginner-Tutorial-Part1-Intro.md).
|
||||
Good luck!
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
Click here to expand a list of all Beginner-Tutorial sections (all parts).
|
||||
</summary>
|
||||
|
||||
```{toctree}
|
||||
|
||||
Part1/Beginner-Tutorial-Part1-Intro
|
||||
Part2/Beginner-Tutorial-Part2-Intro
|
||||
Part3/Beginner-Tutorial-Part3-Intro
|
||||
Part4/Beginner-Tutorial-Part4-Intro
|
||||
Part5/Beginner-Tutorial-Part5-Intro
|
||||
|
||||
```
|
||||
|
||||
</details>
|
||||
392
docs/source/Howtos/Beginner-Tutorial/Part1/Adding-Commands.md
Normal file
392
docs/source/Howtos/Beginner-Tutorial/Part1/Adding-Commands.md
Normal file
|
|
@ -0,0 +1,392 @@
|
|||
# Adding custom commands
|
||||
|
||||
In this lesson we'll learn how to create our own Evennia _Commands_. If you are new to Python you'll
|
||||
also learn some more basics about how to manipulate strings and get information out of Evennia.
|
||||
|
||||
A Command is something that handles the input from a user and causes a result to happen.
|
||||
An example is `look`, which examines your current location and tells how it looks like and
|
||||
what is in it.
|
||||
|
||||
```{sidebar} Commands are not typeclassed
|
||||
|
||||
If you just came from the previous lesson, you might want to know that Commands and
|
||||
CommandSets are not `typeclassed`. That is, instances of them are not saved to the
|
||||
database. They are "just" normal Python classes.
|
||||
```
|
||||
|
||||
In Evennia, a Command is a Python _class_. If you are unsure about what a class is, review the
|
||||
previous lessons! A Command inherits from `evennia.Command` or from one of the alternative command-
|
||||
classes, such as `MuxCommand` which is what most default commands use.
|
||||
|
||||
All Commands are in turn grouped in another class called a _Command Set_. Think of a Command Set
|
||||
as a bag holding many different commands. One CmdSet could for example hold all commands for
|
||||
combat, another for building etc. By default, Evennia groups all character-commands into one
|
||||
big cmdset.
|
||||
|
||||
Command-Sets are then associated with objects, for example with your Character. Doing so makes the
|
||||
commands in that cmdset available to the object. So, to summarize:
|
||||
|
||||
- Commands are classes
|
||||
- A group of Commands is stored in a CmdSet
|
||||
- CmdSets are stored on objects - this defines which commands are available to that object.
|
||||
|
||||
## Creating a custom command
|
||||
|
||||
Open `mygame/commands/command.py`:
|
||||
|
||||
```python
|
||||
"""
|
||||
(module docstring)
|
||||
"""
|
||||
|
||||
from evennia import Command as BaseCommand
|
||||
# from evennia import default_cmds
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""
|
||||
(class docstring)
|
||||
"""
|
||||
pass
|
||||
|
||||
# (lots of commented-out stuff)
|
||||
# ...
|
||||
```
|
||||
|
||||
Ignoring the docstrings (which you can read if you want), this is the only really active code in the module.
|
||||
|
||||
We can see that we import `Command` from `evennia` and use the `from ... import ... as ...` form to rename it
|
||||
to `BaseCommand`. This is so we can let our child class also be named `Command` for reference. The class
|
||||
itself doesn't do anything, it just has `pass`. So in the same way as `Object` in the previous lesson, this
|
||||
class is identical to its parent.
|
||||
|
||||
> The commented out `default_cmds` gives us access to Evennia's default commands for easy overriding. We'll try
|
||||
> that a little later.
|
||||
|
||||
We could modify this module directly, but to train imports we'll work in a separate module. Open a new file
|
||||
`mygame/commands/mycommands.py` and add the following code:
|
||||
|
||||
```python
|
||||
|
||||
from commands.command import Command
|
||||
|
||||
class CmdEcho(Command):
|
||||
key = "echo"
|
||||
|
||||
```
|
||||
|
||||
This is the simplest form of command you can imagine. It just gives itself a name, "echo". This is
|
||||
what you will use to call this command later.
|
||||
|
||||
Next we need to put this in a CmdSet. It will be a one-command CmdSet for now! Change your file as such:
|
||||
|
||||
|
||||
```python
|
||||
|
||||
from commands.command import Command
|
||||
from evennia import CmdSet
|
||||
|
||||
class CmdEcho(Command):
|
||||
key = "echo"
|
||||
|
||||
|
||||
class MyCmdSet(CmdSet):
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
self.add(CmdEcho)
|
||||
|
||||
```
|
||||
|
||||
Our `EchoCmdSet` class must have an `at_cmdset_creation` method, named exactly
|
||||
like this - this is what Evennia will be looking for when setting up the cmdset later, so
|
||||
if you didn't set it up, it will use the parent's version, which is empty. Inside we add the
|
||||
command class to the cmdset by `self.add()`. If you wanted to add more commands to this CmdSet you
|
||||
could just add more lines of `self.add` after this.
|
||||
|
||||
Finally, let's add this command to ourselves so we can try it out. In-game you can experiment with `py` again:
|
||||
|
||||
> py self.cmdset.add("commands.mycommands.MyCmdSet")
|
||||
|
||||
Now try
|
||||
|
||||
> echo
|
||||
Command echo has no defined `func()` - showing on-command variables:
|
||||
...
|
||||
...
|
||||
|
||||
You should be getting a long list of outputs. The reason for this is that your `echo` function is not really
|
||||
"doing" anything yet and the default function is then to show all useful resources available to you when you
|
||||
use your Command. Let's look at some of those listed:
|
||||
|
||||
Command echo has no defined `func()` - showing on-command variables:
|
||||
obj (<class 'typeclasses.characters.Character'>): YourName
|
||||
lockhandler (<class 'evennia.locks.lockhandler.LockHandler'>): cmd:all()
|
||||
caller (<class 'typeclasses.characters.Character'>): YourName
|
||||
cmdname (<class 'str'>): echo
|
||||
raw_cmdname (<class 'str'>): echo
|
||||
cmdstring (<class 'str'>): echo
|
||||
args (<class 'str'>):
|
||||
cmdset (<class 'evennia.commands.cmdset.CmdSet'>): @mail, about, access, accounts, addcom, alias, allcom, ban, batchcode, batchcommands, boot, cboot, ccreate,
|
||||
cdesc, cdestroy, cemit, channels, charcreate, chardelete, checklockstring, clientwidth, clock, cmdbare, cmdsets, color, copy, cpattr, create, cwho, delcom,
|
||||
desc, destroy, dig, dolphin, drop, echo, emit, examine, find, force, get, give, grapevine2chan, help, home, ic, inventory, irc2chan, ircstatus, link, lock,
|
||||
look, menutest, mudinfo, mvattr, name, nick, objects, ooc, open, option, page, password, perm, pose, public, py, quell, quit, reload, reset, rss2chan, say,
|
||||
script, scripts, server, service, sessions, set, setdesc, sethelp, sethome, shutdown, spawn, style, tag, tel, test2010, test2028, testrename, testtable,
|
||||
tickers, time, tunnel, typeclass, unban, unlink, up, up, userpassword, wall, whisper, who, wipe
|
||||
session (<class 'evennia.server.serversession.ServerSession'>): Griatch(#1)@1:2:7:.:0:.:0:.:1
|
||||
account (<class 'typeclasses.accounts.Account'>): Griatch(account 1)
|
||||
raw_string (<class 'str'>): echo
|
||||
|
||||
--------------------------------------------------
|
||||
echo - Command variables from evennia:
|
||||
--------------------------------------------------
|
||||
name of cmd (self.key): echo
|
||||
cmd aliases (self.aliases): []
|
||||
cmd locks (self.locks): cmd:all();
|
||||
help category (self.help_category): General
|
||||
object calling (self.caller): Griatch
|
||||
object storing cmdset (self.obj): Griatch
|
||||
command string given (self.cmdstring): echo
|
||||
current cmdset (self.cmdset): ChannelCmdSet
|
||||
|
||||
These are all properties you can access with `.` on the Command instance, such as `.key`, `.args` and so on.
|
||||
Evennia makes these available to you and they will be different every time a command is run. The most
|
||||
important ones we will make use of now are:
|
||||
|
||||
- `caller` - this is 'you', the person calling the command.
|
||||
- `args` - this is all arguments to the command. Now it's empty, but if you tried `echo foo bar` you'd find
|
||||
that this would be `" foo bar"`.
|
||||
- `obj` - this is object on which this Command (and CmdSet) "sits". So you, in this case.
|
||||
|
||||
The reason our command doesn't do anything yet is because it's missing a `func` method. This is what Evennia
|
||||
looks for to figure out what a Command actually does. Modify your `CmdEcho` class:
|
||||
|
||||
```python
|
||||
# ...
|
||||
|
||||
class CmdEcho(Command):
|
||||
"""
|
||||
A simple echo command
|
||||
|
||||
Usage:
|
||||
echo <something>
|
||||
|
||||
"""
|
||||
key = "echo"
|
||||
|
||||
def func(self):
|
||||
self.caller.msg(f"Echo: '{self.args}'")
|
||||
|
||||
# ...
|
||||
```
|
||||
|
||||
First we added a docstring. This is always a good thing to do in general, but for a Command class, it will also
|
||||
automatically become the in-game help entry! Next we add the `func` method. It has one active line where it
|
||||
makes use of some of those variables we found the Command offers to us. If you did the
|
||||
[basic Python tutorial](./Python-basic-introduction.md), you will recognize `.msg` - this will send a message
|
||||
to the object it is attached to us - in this case `self.caller`, that is, us. We grab `self.args` and includes
|
||||
that in the message.
|
||||
|
||||
Since we haven't changed `MyCmdSet`, that will work as before. Reload and re-add this command to ourselves to
|
||||
try out the new version:
|
||||
|
||||
> reload
|
||||
> py self.cmdset.add("commands.mycommands.MyCmdSet")
|
||||
> echo
|
||||
Echo: ''
|
||||
|
||||
Try to pass an argument:
|
||||
|
||||
> echo Woo Tang!
|
||||
Echo: ' Woo Tang!'
|
||||
|
||||
Note that there is an extra space before `Woo!`. That is because self.args contains the _everything_ after
|
||||
the command name, including spaces. Evennia will happily understand if you skip that space too:
|
||||
|
||||
> echoWoo Tang!
|
||||
Echo: 'Woo Tang!'
|
||||
|
||||
There are ways to force Evennia to _require_ an initial space, but right now we want to just ignore it since
|
||||
it looks a bit weird for our echo example. Tweak the code:
|
||||
|
||||
```python
|
||||
# ...
|
||||
|
||||
class CmdEcho(Command):
|
||||
"""
|
||||
A simple echo command
|
||||
|
||||
Usage:
|
||||
echo <something>
|
||||
|
||||
"""
|
||||
key = "echo"
|
||||
|
||||
def func(self):
|
||||
self.caller.msg(f"Echo: '{self.args.strip()}'")
|
||||
|
||||
# ...
|
||||
```
|
||||
|
||||
The only difference is that we called `.strip()` on `self.args`. This is a helper method available on all
|
||||
strings - it strips out all whitespace before and after the string. Now the Command-argument will no longer
|
||||
have any space in front of it.
|
||||
|
||||
> reload
|
||||
> py self.cmdset.add("commands.mycommands.MyCmdSet")
|
||||
> echo Woo Tang!
|
||||
Echo: 'Woo Tang!'
|
||||
|
||||
Don't forget to look at the help for the echo command:
|
||||
|
||||
> help echo
|
||||
|
||||
You will get the docstring you put in your Command-class.
|
||||
|
||||
### Making our cmdset persistent
|
||||
|
||||
It's getting a little annoying to have to re-add our cmdset every time we reload, right? It's simple
|
||||
enough to make `echo` a _persistent_ change though:
|
||||
|
||||
> py self.cmdset.add("commands.mycommands.MyCmdSet", persistent=True)
|
||||
|
||||
Now you can `reload` as much as you want and your code changes will be available directly without
|
||||
needing to re-add the MyCmdSet again. To remove the cmdset again, do
|
||||
|
||||
> py self.cmdset.remove("commands.mycommands.MyCmdSet")
|
||||
|
||||
But for now, keep it around, we'll expand it with some more examples.
|
||||
|
||||
### Figuring out who to hit
|
||||
|
||||
Let's try something a little more exciting than just echo. Let's make a `hit` command, for punching
|
||||
someone in the face! This is how we want it to work:
|
||||
|
||||
> hit <target>
|
||||
You hit <target> with full force!
|
||||
|
||||
Not only that, we want the <target> to see
|
||||
|
||||
You got hit by <hitter> with full force!
|
||||
|
||||
Here, `<hitter>` would be the one using the `hit` command and `<target>` is the one doing the punching.
|
||||
|
||||
Still in `mygame/commands/mycommands.py`, add a new class, between `CmdEcho` and `MyCmdSet`.
|
||||
|
||||
```python
|
||||
# ...
|
||||
|
||||
class CmdHit(Command):
|
||||
"""
|
||||
Hit a target.
|
||||
|
||||
Usage:
|
||||
hit <target>
|
||||
|
||||
"""
|
||||
key = "hit"
|
||||
|
||||
def func(self):
|
||||
args = self.args.strip()
|
||||
if not args:
|
||||
self.caller.msg("Who do you want to hit?")
|
||||
return
|
||||
target = self.caller.search(args)
|
||||
if not target:
|
||||
return
|
||||
self.caller.msg(f"You hit {target.key} with full force!")
|
||||
target.msg(f"You got hit by {self.caller.key} with full force!")
|
||||
# ...
|
||||
|
||||
```
|
||||
|
||||
A lot of things to dissect here:
|
||||
- **Line 4**: The normal `class` header. We inherit from `Command` which we imported at the top of this file.
|
||||
- **Lines 5**-11: The docstring and help-entry for the command. You could expand on this as much as you wanted.
|
||||
- **Line 12**: We want to write `hit` to use this command.
|
||||
- **Line 15**: We strip the whitespace from the argument like before. Since we don't want to have to do
|
||||
`self.args.strip()` over and over, we store the stripped version
|
||||
in a _local variable_ `args`. Note that we don't modify `self.args` by doing this, `self.args` will still
|
||||
have the whitespace and is not the same as `args` in this example.
|
||||
```{sidebar} if-statements
|
||||
|
||||
The full form of the if statement is
|
||||
|
||||
if condition:
|
||||
...
|
||||
elif othercondition:
|
||||
...
|
||||
else:
|
||||
...
|
||||
|
||||
There can be any number of `elifs` to mark when different branches of the code should run. If
|
||||
the `else` condition is given, it will run if none of the other conditions was truthy. In Python
|
||||
the `if..elif..else` structure also serves the same function as `case` in some other languages.
|
||||
|
||||
```
|
||||
- **Line 16** has our first _conditional_, an `if` statement. This is written on the form `if <condition>:` and only
|
||||
if that condition is 'truthy' will the indented code block under the `if` statement run. To learn what is truthy in
|
||||
Python it's usually easier to learn what is "falsy":
|
||||
- `False` - this is a reserved boolean word in Python. The opposite is `True`.
|
||||
- `None` - another reserved word. This represents nothing, a null-result or value.
|
||||
- `0` or `0.0`
|
||||
- The empty string `""` or `''` or `""""""` or `''''''`
|
||||
- Empty _iterables_ we haven't seen yet, like empty lists `[]`, empty tuples `()` and empty dicts `{}`.
|
||||
- Everything else is "truthy".
|
||||
|
||||
Line 16's condition is `not args`. The `not` _inverses_ the result, so if `args` is the empty string (falsy), the
|
||||
whole conditional becomes truthy. Let's continue in the code:
|
||||
- **Lines 17-18**: This code will only run if the `if` statement is truthy, in this case if `args` is the empty string.
|
||||
- **Line 18**: `return` is a reserved Python word that exits `func` immediately.
|
||||
- **Line 19**: We use `self.caller.search` to look for the target in the current location.
|
||||
- **Lines 20-21**: A feature of `.search` is that it will already inform `self.caller` if it couldn't find the target.
|
||||
In that case, `target` will be `None` and we should just directly `return`.
|
||||
- **Lines 22-23**: At this point we have a suitable target and can send our punching strings to each.
|
||||
|
||||
Finally we must also add this to a CmdSet. Let's add it to `MyCmdSet` which we made persistent earlier.
|
||||
|
||||
```python
|
||||
# ...
|
||||
|
||||
class MyCmdSet(CmdSet):
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
self.add(CmdEcho)
|
||||
self.add(CmdHit)
|
||||
|
||||
```
|
||||
|
||||
```{sidebar} Errors in your code
|
||||
|
||||
With longer code snippets to try, it gets more and more likely you'll
|
||||
make an error and get a `traceback` when you reload. This will either appear
|
||||
directly in-game or in your log (view it with `evennia -l` in a terminal).
|
||||
Don't panic; tracebacks are your friends - they are to be read bottom-up and usually describe
|
||||
exactly where your problem is. Refer to `The Python intro <Python-basic-introduction.html>`_ for
|
||||
more hints. If you get stuck, reach out to the Evennia community for help.
|
||||
|
||||
```
|
||||
|
||||
Next we reload to let Evennia know of these code changes and try it out:
|
||||
|
||||
> reload
|
||||
hit
|
||||
Who do you want to hit?
|
||||
hit me
|
||||
You hit YourName with full force!
|
||||
You got hit by YourName with full force!
|
||||
|
||||
Lacking a target, we hit ourselves. If you have one of the dragons still around from the previous lesson
|
||||
you could try to hit it (if you dare):
|
||||
|
||||
hit smaug
|
||||
You hit Smaug with full force!
|
||||
|
||||
You won't see the second string. Only Smaug sees that (and is not amused).
|
||||
|
||||
|
||||
## Summary
|
||||
|
||||
In this lesson we learned how to create our own Command, add it to a CmdSet and then to ourselves.
|
||||
We also upset a dragon.
|
||||
|
||||
In the next lesson we'll learn how to hit Smaug with different weapons. We'll also
|
||||
get into how we replace and extend Evennia's default Commands.
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
# Part 1: What we have
|
||||
|
||||
```{eval-rst}
|
||||
.. sidebar:: Beginner Tutorial Parts
|
||||
|
||||
`Introduction <../Beginner-Tutorial-Intro.html>`_
|
||||
Getting set up.
|
||||
**Part 1: What we have**
|
||||
A tour of Evennia and how to use the tools, including an introduction to Python.
|
||||
Part 2: `What we want <../Part2/Beginner-Tutorial-Part2-Intro.html>`_
|
||||
Planning our tutorial game and what to think about when planning your own in the future.
|
||||
Part 3: `How we get there <../Part3/Beginner-Tutorial-Part3-Intro.html>`_
|
||||
Getting down to the meat of extending Evennia to make our game
|
||||
Part 4: `Using what we created <../Part4/Beginner-Tutorial-Part4-Intro.html>`_
|
||||
Building a tech-demo and world content to go with our code
|
||||
Part 5: `Showing the world <../Part5/Beginner-Tutorial-Part5-Intro.html>`_
|
||||
Taking our new game online and let players try it out
|
||||
|
||||
```
|
||||
|
||||
In this first part we'll focus on what we get out of the box in Evennia - we'll get used to the tools,
|
||||
and how to find things we are looking for. We will also dive into some of things you'll
|
||||
need to know to fully utilize the system, including giving you a brief rundown of Python concepts. If you are
|
||||
an experienced Python programmer, some sections may feel a bit basic, but you will at least not have seen
|
||||
these concepts in the context of Evennia before.
|
||||
|
||||
## Lessons
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 1
|
||||
:numbered:
|
||||
|
||||
Building-Quickstart
|
||||
Tutorial-World
|
||||
Python-basic-introduction
|
||||
Gamedir-Overview
|
||||
Python-classes-and-objects
|
||||
Evennia-Library-Overview
|
||||
Learning-Typeclasses
|
||||
Adding-Commands
|
||||
More-on-Commands
|
||||
Creating-Things
|
||||
Searching-Things
|
||||
Django-queries
|
||||
|
||||
```
|
||||
|
||||
## Table of Contents
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 2
|
||||
|
||||
Building-Quickstart
|
||||
Tutorial-World
|
||||
Python-basic-introduction
|
||||
Gamedir-Overview
|
||||
Python-classes-and-objects
|
||||
Evennia-Library-Overview
|
||||
Learning-Typeclasses
|
||||
Adding-Commands
|
||||
More-on-Commands
|
||||
Creating-Things
|
||||
Searching-Things
|
||||
Django-queries
|
||||
|
||||
```
|
||||
|
|
@ -0,0 +1,311 @@
|
|||
# Using commands and building stuff
|
||||
|
||||
In this lesson we will test out what we can do in-game out-of-the-box. Evennia ships with
|
||||
[around 90 default commands](../../../Components/Default-Commands.md), and while you can override those as you please,
|
||||
they can be quite useful.
|
||||
|
||||
Connect and log into your new game and you will end up in the "Limbo" location. This
|
||||
is the only room in the game at this point. Let's explore the commands a little.
|
||||
|
||||
The default commands has syntax [similar to MUX](../../../Concepts/Using-MUX-as-a-Standard.md):
|
||||
|
||||
command[/switch/switch...] [arguments ...]
|
||||
|
||||
An example would be
|
||||
|
||||
create/drop box
|
||||
|
||||
A _/switch_ is a special, optional flag to the command to make it behave differently. It is always
|
||||
put directly after the command name, and begins with a forward slash (`/`). The _arguments_ are one
|
||||
or more inputs to the commands. It's common to use an equal sign (`=`) when assigning something to
|
||||
an object.
|
||||
|
||||
> Are you used to commands starting with @, like @create? That will work too. Evennia simply ignores
|
||||
> the preceeding @.
|
||||
|
||||
## Getting help
|
||||
|
||||
help
|
||||
|
||||
Will give you a list of all commands available to you. Use
|
||||
|
||||
help <commandname>
|
||||
|
||||
to see the in-game help for that command.
|
||||
|
||||
## Looking around
|
||||
|
||||
The most common comman is
|
||||
|
||||
look
|
||||
|
||||
This will show you the description of the current location. `l` is an alias.
|
||||
|
||||
When targeting objects in commands you have two special labels you can use, `here` for the current
|
||||
room or `me`/`self` to point back to yourself. So
|
||||
|
||||
look me
|
||||
|
||||
will give you your own description. `look here` is, in this case, the same as plain `look`.
|
||||
|
||||
|
||||
## Stepping Down From Godhood
|
||||
|
||||
If you just installed Evennia, your very first player account is called user #1, also known as the
|
||||
_superuser_ or _god user_. This user is very powerful, so powerful that it will override many game
|
||||
restrictions such as locks. This can be useful, but it also hides some functionality that you might
|
||||
want to test.
|
||||
|
||||
To temporarily step down from your superuser position you can use the `quell` command in-game:
|
||||
|
||||
quell
|
||||
|
||||
This will make you start using the permission of your current character's level instead of your
|
||||
superuser level. If you didn't change any settings your game Character should have an _Developer_
|
||||
level permission - high as can be without bypassing locks like the superuser does. This will work
|
||||
fine for the examples on this page. Use
|
||||
|
||||
unquell
|
||||
|
||||
to get superuser status again when you are done.
|
||||
|
||||
## Creating an Object
|
||||
|
||||
Basic objects can be anything -- swords, flowers and non-player characters. They are created using
|
||||
the `create` command:
|
||||
|
||||
create box
|
||||
|
||||
This created a new 'box' (of the default object type) in your inventory. Use the command `inventory`
|
||||
(or `i`) to see it. Now, 'box' is a rather short name, let's rename it and tack on a few aliases.
|
||||
|
||||
name box = very large box;box;very;crate
|
||||
|
||||
```{warning} MUD clients and semi-colon
|
||||
Some traditional MUD clients use the semi-colon `;` to separate client inputs. If so,
|
||||
the above line will give an error. You need to change your client to use another command-separator
|
||||
or to put it in 'verbatim' mode. If you still have trouble, use the Evennia web client instead.
|
||||
|
||||
```
|
||||
|
||||
|
||||
We now renamed the box to _very large box_ (and this is what we will see when looking at it), but we
|
||||
will also recognize it by any of the other names we give - like _crate_ or simply _box_ as before.
|
||||
We could have given these aliases directly after the name in the `create` command, this is true for
|
||||
all creation commands - you can always tag on a list of `;`-separated aliases to the name of your
|
||||
new object. If you had wanted to not change the name itself, but to only add aliases, you could have
|
||||
used the `alias` command.
|
||||
|
||||
We are currently carrying the box. Let's drop it (there is also a short cut to create and drop in
|
||||
one go by using the `/drop` switch, for example `create/drop box`).
|
||||
|
||||
drop box
|
||||
|
||||
Hey presto - there it is on the ground, in all its normality.
|
||||
|
||||
examine box
|
||||
|
||||
This will show some technical details about the box object. For now we will ignore what this
|
||||
information means.
|
||||
|
||||
Try to `look` at the box to see the (default) description.
|
||||
|
||||
look box
|
||||
You see nothing special.
|
||||
|
||||
The description you get is not very exciting. Let's add some flavor.
|
||||
|
||||
describe box = This is a large and very heavy box.
|
||||
|
||||
If you try the `get` command we will pick up the box. So far so good, but if we really want this to
|
||||
be a large and heavy box, people should _not_ be able to run off with it that easily. To prevent
|
||||
this we need to lock it down. This is done by assigning a _Lock_ to it. Make sure the box was
|
||||
dropped in the room, then try this:
|
||||
|
||||
lock box = get:false()
|
||||
|
||||
Locks represent a rather [big topic](../../../Components/Locks.md), but for now that will do what we want. This will lock
|
||||
the box so noone can lift it. The exception is superusers, they override all locks and will pick it
|
||||
up anyway. Make sure you are quelling your superuser powers and try to get the box now:
|
||||
|
||||
> get box
|
||||
You can't get that.
|
||||
|
||||
Think thís default error message looks dull? The `get` command looks for an [Attribute](../../../Components/Attributes.md)
|
||||
named `get_err_msg` for returning a nicer error message (we just happen to know this, you would need
|
||||
to peek into the
|
||||
[code](https://github.com/evennia/evennia/blob/master/evennia/commands/default/general.py#L235) for
|
||||
the `get` command to find out.). You set attributes using the `set` command:
|
||||
|
||||
set box/get_err_msg = It's way too heavy for you to lift.
|
||||
|
||||
Try to get it now and you should see a nicer error message echoed back to you. To see what this
|
||||
message string is in the future, you can use 'examine.'
|
||||
|
||||
examine box/get_err_msg
|
||||
|
||||
Examine will return the value of attributes, including color codes. `examine here/desc` would return
|
||||
the raw description of your current room (including color codes), so that you can copy-and-paste to
|
||||
set its description to something else.
|
||||
|
||||
You create new Commands (or modify existing ones) in Python outside the game. We will get to that
|
||||
later, in the [Commands tutorial](./Adding-Commands.md).
|
||||
|
||||
## Get a Personality
|
||||
|
||||
[Scripts](../../../Components/Scripts.md) are powerful out-of-character objects useful for many "under the hood" things.
|
||||
One of their optional abilities is to do things on a timer. To try out a first script, let's put one
|
||||
on ourselves. There is an example script in `evennia/contrib/tutorial_examples/bodyfunctions.py`
|
||||
that is called `BodyFunctions`. To add this to us we will use the `script` command:
|
||||
|
||||
script self = tutorial_examples.bodyfunctions.BodyFunctions
|
||||
|
||||
This string will tell Evennia to dig up the Python code at the place we indicate. It already knows
|
||||
to look in the `contrib/` folder, so we don't have to give the full path.
|
||||
|
||||
> Note also how we use `.` instead of `/` (or `\` on Windows). This is a so-called "Python path". In a Python-path,
|
||||
> you separate the parts of the path with `.` and skip the `.py` file-ending. Importantly, it also allows you to point to
|
||||
Python code _inside_ files, like the `BodyFunctions` class inside `bodyfunctions.py` (we'll get to classes later).
|
||||
These "Python-paths" are used extensively throughout Evennia.
|
||||
|
||||
Wait a while and you will notice yourself starting making random observations ...
|
||||
|
||||
script self
|
||||
|
||||
This will show details about scripts on yourself (also `examine` works). You will see how long it is
|
||||
until it "fires" next. Don't be alarmed if nothing happens when the countdown reaches zero - this
|
||||
particular script has a randomizer to determine if it will say something or not. So you will not see
|
||||
output every time it fires.
|
||||
|
||||
When you are tired of your character's "insights", kill the script with
|
||||
|
||||
script/stop self = tutorial_examples.bodyfunctions.BodyFunctions
|
||||
|
||||
You create your own scripts in Python, outside the game; the path you give to `script` is literally
|
||||
the Python path to your script file. The [Scripts](../../../Components/Scripts.md) page explains more details.
|
||||
|
||||
## Pushing Your Buttons
|
||||
|
||||
If we get back to the box we made, there is only so much fun you can have with it at this point. It's
|
||||
just a dumb generic object. If you renamed it to `stone` and changed its description, noone would be
|
||||
the wiser. However, with the combined use of custom [Typeclasses](../../../Components/Typeclasses.md), [Scripts](../../../Components/Scripts.md)
|
||||
and object-based [Commands](../../../Components/Commands.md), you could expand it and other items to be as unique, complex
|
||||
and interactive as you want.
|
||||
|
||||
Let's take an example. So far we have only created objects that use the default object typeclass
|
||||
named simply `Object`. Let's create an object that is a little more interesting. Under
|
||||
`evennia/contrib/tutorial_examples` there is a module `red_button.py`. It contains the enigmatic
|
||||
`RedButton` class.
|
||||
|
||||
Let's make us one of _those_!
|
||||
|
||||
create/drop button:tutorial_examples.red_button.RedButton
|
||||
|
||||
The same way we did with the Script Earler, we specify a "Python-path" to the Python code we want Evennia
|
||||
to use for creating the object. There you go - one red button.
|
||||
|
||||
The RedButton is an example object intended to show off a few of Evennia's features. You will find
|
||||
that the [Typeclass](../../../Components/Typeclasses.md) and [Commands](../../../Components/Commands.md) controlling it are
|
||||
inside [evennia/contrib/tutorials/red_button](evennia.contrib.tutorials.red_button)
|
||||
|
||||
If you wait for a while (make sure you dropped it!) the button will blink invitingly.
|
||||
|
||||
Why don't you try to push it ...?
|
||||
|
||||
Surely a big red button is meant to be pushed.
|
||||
|
||||
You know you want to.
|
||||
|
||||
```{warning} Don't press the invitingly blinking red button.
|
||||
```
|
||||
|
||||
## Making Yourself a House
|
||||
|
||||
The main command for shaping the game world is `dig`. For example, if you are standing in Limbo you
|
||||
can dig a route to your new house location like this:
|
||||
|
||||
dig house = large red door;door;in,to the outside;out
|
||||
|
||||
This will create a new room named 'house'. Spaces at the start/end of names and aliases are ignored
|
||||
so you could put more air if you wanted. This call will directly create an exit from your current
|
||||
location named 'large red door' and a corresponding exit named 'to the outside' in the house room
|
||||
leading back to Limbo. We also define a few aliases to those exits, so people don't have to write
|
||||
the full thing all the time.
|
||||
|
||||
If you wanted to use normal compass directions (north, west, southwest etc), you could do that with
|
||||
`dig` too. But Evennia also has a limited version of `dig` that helps for compass directions (and
|
||||
also up/down and in/out). It's called `tunnel`:
|
||||
|
||||
tunnel sw = cliff
|
||||
|
||||
This will create a new room "cliff" with an exit "southwest" leading there and a path "northeast"
|
||||
leading back from the cliff to your current location.
|
||||
|
||||
You can create new exits from where you are, using the `open` command:
|
||||
|
||||
open north;n = house
|
||||
|
||||
This opens an exit `north` (with an alias `n`) to the previously created room `house`.
|
||||
|
||||
If you have many rooms named `house` you will get a list of matches and have to select which one you
|
||||
want to link to.
|
||||
|
||||
Follow the north exit to your 'house' or `teleport` to it:
|
||||
|
||||
north
|
||||
|
||||
or:
|
||||
|
||||
teleport house
|
||||
|
||||
To manually open an exit back to Limbo (if you didn't do so with the `dig` command):
|
||||
|
||||
open door = limbo
|
||||
|
||||
(You can also us the #dbref of limbo, which you can find by using `examine here` when in limbo).
|
||||
|
||||
## Reshuffling the World
|
||||
|
||||
You can find things using the `find` command. Assuming you are back at `Limbo`, let's teleport the
|
||||
_large box_ to our house.
|
||||
|
||||
teleport box = house
|
||||
very large box is leaving Limbo, heading for house.
|
||||
Teleported very large box -> house.
|
||||
|
||||
We can still find the box by using find:
|
||||
|
||||
find box
|
||||
One Match(#1-#8):
|
||||
very large box(#8) - src.objects.objects.Object
|
||||
|
||||
Knowing the `#dbref` of the box (#8 in this example), you can grab the box and get it back here
|
||||
without actually yourself going to `house` first:
|
||||
|
||||
teleport #8 = here
|
||||
|
||||
As mentioned, `here` is an alias for 'your current location'. The box should now be back in Limbo with you.
|
||||
|
||||
We are getting tired of the box. Let's destroy it.
|
||||
|
||||
destroy box
|
||||
|
||||
It will ask you for confirmation. Once you give it, the box will be gone.
|
||||
|
||||
You can destroy many objects in one go by giving a comma-separated list of objects (or a range
|
||||
of #dbrefs, if they are not in the same location) to the command.
|
||||
|
||||
## Adding a Help Entry
|
||||
|
||||
The Command-help is something you modify in Python code. We'll get to that when we get to how to
|
||||
add Commands. But you can also add regular help entries, for example to explain something about
|
||||
the history of your game world:
|
||||
|
||||
sethelp/add History = At the dawn of time ...
|
||||
|
||||
You will now find your new `History` entry in the `help` list and read your help-text with `help History`.
|
||||
|
||||
## Adding a World
|
||||
|
||||
After this brief introduction to building and using in-game commands you may be ready to see a more fleshed-out
|
||||
example. Evennia comes with a tutorial world for you to explore. We will try that out in the next lesson.
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
# Creating things
|
||||
|
||||
We have already created some things - dragons for example. There are many different things to create
|
||||
in Evennia though. In the last lesson we learned about typeclasses, the way to make objects persistent in the database.
|
||||
|
||||
Given the path to a Typeclass, there are three ways to create an instance of it:
|
||||
|
||||
- Firstly, you can call the class directly, and then `.save()` it:
|
||||
|
||||
obj = SomeTypeClass(db_key=...)
|
||||
obj.save()
|
||||
|
||||
This has the drawback of being two operations; you must also import the class and have to pass
|
||||
the actual database field names, such as `db_key` instead of `key` as keyword arguments.
|
||||
- Secondly you can use the Evennia creation helpers:
|
||||
|
||||
obj = evennia.create_object(SomeTypeClass, key=...)
|
||||
|
||||
This is the recommended way if you are trying to create things in Python. The first argument can either be
|
||||
the class _or_ the python-path to the typeclass, like `"path.to.SomeTypeClass"`. It can also be `None` in which
|
||||
case the Evennia default will be used. While all the creation methods
|
||||
are available on `evennia`, they are actually implemented in [evennia/utils/create.py](../../../api/evennia.utils.create.md).
|
||||
- Finally, you can create objects using an in-game command, such as
|
||||
|
||||
create/drop obj:path.to.SomeTypeClass
|
||||
|
||||
As a developer you are usually best off using the two other methods, but a command is usually the only way
|
||||
to let regular players or builders without Python-access help build the game world.
|
||||
|
||||
## Creating Objects
|
||||
|
||||
This is one of the most common creation-types. These are entities that inherits from `DefaultObject` at any distance.
|
||||
They have an existence in the game world and includes rooms, characters, exits, weapons, flower pots and castles.
|
||||
|
||||
> py
|
||||
> import evennia
|
||||
> rose = evennia.create_object(key="rose")
|
||||
|
||||
Since we didn't specify the `typeclass` as the first argument, the default given by `settings.BASE_OBJECT_TYPECLASS`
|
||||
(`typeclasses.objects.Object`) will be used.
|
||||
|
||||
## Creating Accounts
|
||||
|
||||
An _Account_ is an out-of-character (OOC) entity, with no existence in the game world.
|
||||
You can find the parent class for Accounts in `typeclasses/accounts.py`.
|
||||
|
||||
_TODO_
|
||||
|
||||
397
docs/source/Howtos/Beginner-Tutorial/Part1/Django-queries.md
Normal file
397
docs/source/Howtos/Beginner-Tutorial/Part1/Django-queries.md
Normal file
|
|
@ -0,0 +1,397 @@
|
|||
# Advanced searching - Django Database queries
|
||||
|
||||
```{important} More advanced lesson!
|
||||
|
||||
Learning about Django's queryset language is very useful once you start doing more advanced things
|
||||
in Evennia. But it's not strictly needed out the box and can be a little overwhelming for a first
|
||||
reading. So if you are new to Python and Evennia, feel free to just skim this lesson and refer
|
||||
back to it later when you've gained more experience.
|
||||
```
|
||||
|
||||
The search functions and methods we used in the previous lesson are enough for most cases.
|
||||
But sometimes you need to be more specific:
|
||||
|
||||
- You want to find all `Characters` ...
|
||||
- ... who are in Rooms tagged as `moonlit` ...
|
||||
- ... _and_ who has the Attribute `lycantrophy` with a level higher than 2 ...
|
||||
- ... because they'll should immediately transform to werewolves!
|
||||
|
||||
In principle you could achieve this with the existing search functions combined with a lot of loops
|
||||
and if statements. But for something non-standard like this, querying the database directly will be
|
||||
much more efficient.
|
||||
|
||||
Evennia uses [Django](https://www.djangoproject.com/) to handle its connection to the database.
|
||||
A [django queryset](https://docs.djangoproject.com/en/3.0/ref/models/querysets/) represents
|
||||
a database query. One can add querysets together to build ever-more complicated queries. Only when
|
||||
you are trying to use the results of the queryset will it actually call the database.
|
||||
|
||||
The normal way to build a queryset is to define what class of entity you want to search by getting its
|
||||
`.objects` resource, and then call various methods on that. We've seen this one before:
|
||||
|
||||
all_weapons = Weapon.objects.all()
|
||||
|
||||
This is now a queryset representing all instances of `Weapon`. If `Weapon` had a subclass `Cannon` and we
|
||||
only wanted the cannons, we would do
|
||||
|
||||
all_cannons = Cannon.objects.all()
|
||||
|
||||
Note that `Weapon` and `Cannon` are different typeclasses. You won't find any `Cannon` instances in
|
||||
the `all_weapon` result above, confusing as that may sound. To get instances of a Typeclass _and_ the
|
||||
instances of all its children classes you need to use `_family`:
|
||||
|
||||
```{sidebar} _family
|
||||
|
||||
The all_family, filter_family etc is an Evennia-specific
|
||||
thing. It's not part of regular Django.
|
||||
|
||||
```
|
||||
|
||||
really_all_weapons = Weapon.objects.all_family()
|
||||
|
||||
This result now contains both `Weapon` and `Cannon` instances.
|
||||
|
||||
To limit your search by other criteria than the Typeclass you need to use `.filter`
|
||||
(or `.filter_family`) instead:
|
||||
|
||||
roses = Flower.objects.filter(db_key="rose")
|
||||
|
||||
This is a queryset representing all objects having a `db_key` equal to `"rose"`.
|
||||
Since this is a queryset you can keep adding to it; this will act as an `AND` condition.
|
||||
|
||||
local_roses = roses.filter(db_location=myroom)
|
||||
|
||||
We could also have written this in one statement:
|
||||
|
||||
local_roses = Flower.objects.filter(db_key="rose", db_location=myroom)
|
||||
|
||||
We can also `.exclude` something from results
|
||||
|
||||
local_non_red_roses = local_roses.exclude(db_key="red_rose")
|
||||
|
||||
Only until we actually try to examine the result will the database be called. Here it's called when we
|
||||
try to loop over the queryset:
|
||||
|
||||
for rose in local_non_red_roses:
|
||||
print(rose)
|
||||
|
||||
From now on, the queryset is _evaluated_ and we can't keep adding more queries to it - we'd need to
|
||||
create a new queryset if we wanted to find some other result. Other ways to evaluate the queryset is to
|
||||
print it, convert it to a list with `list()` and otherwise try to access its results.
|
||||
|
||||
Note how we use `db_key` and `db_location`. This is the actual names of these database fields. By convention
|
||||
Evennia uses `db_` in front of every database field. When you use the normal Evennia search helpers and objects
|
||||
you can skip the `db_` but here we are calling the database directly and need to use the 'real' names.
|
||||
|
||||
Here are the most commonly used methods to use with the `objects` managers:
|
||||
|
||||
- `filter` - query for a listing of objects based on search criteria. Gives empty queryset if none
|
||||
were found.
|
||||
- `get` - query for a single match - raises exception if none were found, or more than one was
|
||||
found.
|
||||
- `all` - get all instances of the particular type.
|
||||
- `filter_family` - like `filter`, but search all sub classes as well.
|
||||
- `get_family` - like `get`, but search all sub classes as well.
|
||||
- `all_family` - like `all`, but return entities of all subclasses as well.
|
||||
|
||||
> All of Evennia search functions use querysets under the hood. The `evennia.search_*` functions actually
|
||||
> return querysets, which means you could in principle keep adding queries to their results as well.
|
||||
|
||||
|
||||
## Queryset field lookups
|
||||
|
||||
Above we found roses with exactly the `db_key` `"rose"`. This is an _exact_ match that is _case sensitive_,
|
||||
so it would not find `"Rose"`.
|
||||
|
||||
# this is case-sensitive and the same as =
|
||||
roses = Flower.objects.filter(db_key__exact="rose"
|
||||
|
||||
# the i means it's case-insensitive
|
||||
roses = Flower.objects.filter(db_key__iexact="rose")
|
||||
|
||||
The Django field query language uses `__` in the same way as Python uses `.` to access resources. This
|
||||
is because `.` is not allowed in a function keyword.
|
||||
|
||||
roses = Flower.objects.filter(db_key__icontains="rose")
|
||||
|
||||
This will find all flowers whose name contains the string `"rose"`, like `"roses"`, `"wild rose"` etc. The
|
||||
`i` in the beginning makes the search case-insensitive. Other useful variations to use
|
||||
are `__istartswith` and `__iendswith`. You can also use `__gt`, `__ge` for "greater-than"/"greater-or-equal-than"
|
||||
comparisons (same for `__lt` and `__le`). There is also `__in`:
|
||||
|
||||
swords = Weapons.objects.filter(db_key__in=("rapier", "two-hander", "shortsword"))
|
||||
|
||||
One also uses `__` to access foreign objects like Tags. Let's for example assume this is how we identify mages:
|
||||
|
||||
char.tags.add("mage", category="profession")
|
||||
|
||||
Now, in this case we have an Evennia helper to do this search:
|
||||
|
||||
mages = evennia.search_tags("mage", category="profession")
|
||||
|
||||
But this will find all Objects with this tag+category. Maybe you are only looking for Vampire mages:
|
||||
|
||||
sparkly_mages = Vampire.objects.filter(db_tags__db_key="mage", db_tags__db_category="profession")
|
||||
|
||||
This looks at the `db_tags` field on the `Vampire` and filters on the values of each tag's
|
||||
`db_key` and `db_category` together.
|
||||
|
||||
For more field lookups, see the
|
||||
[django docs](https://docs.djangoproject.com/en/3.0/ref/models/querysets/#field-lookups) on the subject.
|
||||
|
||||
## Get that werewolf ...
|
||||
|
||||
Let's see if we can make a query for the werewolves in the moonlight we mentioned at the beginning
|
||||
of this section.
|
||||
|
||||
Firstly, we make ourselves and our current location match the criteria, so we can test:
|
||||
|
||||
> py here.tags.add("moonlit")
|
||||
> py me.db.lycantrophy = 3
|
||||
|
||||
This is an example of a more complex query. We'll consider it an example of what is
|
||||
possible.
|
||||
|
||||
```{sidebar} Line breaks
|
||||
|
||||
Note the way of writing this code. It would have been very hard to read if we just wrote it in
|
||||
one long line. But since we wrapped it in `(...)` we can spread it out over multiple lines
|
||||
without worrying about line breaks!
|
||||
```
|
||||
|
||||
```python
|
||||
from typeclasses.characters import Character
|
||||
|
||||
will_transform = (
|
||||
Character.objects
|
||||
.filter(
|
||||
db_location__db_tags__db_key__iexact="moonlit",
|
||||
db_attributes__db_key="lycantrophy",
|
||||
db_attributes__db_value__gt=2)
|
||||
)
|
||||
```
|
||||
|
||||
- **Line 3** - We want to find `Character`s, so we access `.objects` on the `Character` typeclass.
|
||||
- **Line 4** - We start to filter ...
|
||||
- **Line 5**
|
||||
- ... by accessing the `db_location` field (usually this is a Room)
|
||||
- ... and on that location, we get the value of `db_tags` (this is a _many-to-many_ database field
|
||||
that we can treat like an object for this purpose; it references all Tags on the location)
|
||||
- ... and from those `Tags`, we looking for `Tags` whose `db_key` is "monlit" (non-case sensitive).
|
||||
- **Line 6** - ... We also want only Characters with `Attributes` whose `db_key` is exactly `"lycantrophy"`
|
||||
- **Line 7** - ... at the same time as the `Attribute`'s `db_value` is greater-than 2.
|
||||
|
||||
Running this query makes our newly lycantrrophic Character appear in `will_transform`. Success!
|
||||
|
||||
> Don't confuse database fields with [Attributes](../../../Components/Attributes.md) you set via `obj.db.attr = 'foo'` or
|
||||
`obj.attributes.add()`. Attributes are custom database entities *linked* to an object. They are not
|
||||
separate fields *on* that object like `db_key` or `db_location` are.
|
||||
|
||||
## Complex queries
|
||||
|
||||
All examples so far used `AND` relations. The arguments to `.filter` are added together with `AND`
|
||||
("we want tag room to be "monlit" _and_ lycantrhopy be > 2").
|
||||
|
||||
For queries using `OR` and `NOT` we need Django's
|
||||
[Q object](https://docs.djangoproject.com/en/1.11/topics/db/queries/#complex-lookups-with-q-objects). It is
|
||||
imported from Django directly:
|
||||
|
||||
from django.db.models import Q
|
||||
|
||||
The `Q` is an object that is created with the same arguments as `.filter`, for example
|
||||
|
||||
Q(db_key="foo")
|
||||
|
||||
You can then use this `Q` instance as argument in a `filter`:
|
||||
|
||||
q1 = Q(db_key="foo")
|
||||
Character.objects.filter(q1)
|
||||
|
||||
|
||||
The useful thing about `Q` is that these objects can be chained together with special symbols (bit operators):
|
||||
`|` for `OR` and `&` for `AND`. A tilde `~` in front negates the expression inside the `Q` and thus
|
||||
works like `NOT`.
|
||||
|
||||
q1 = Q(db_key="Dalton")
|
||||
q2 = Q(db_location=prison)
|
||||
Character.objects.filter(q1 | ~q2)
|
||||
|
||||
Would get all Characters that are either named "Dalton" _or_ which is _not_ in prison. The result is a mix
|
||||
of Daltons and non-prisoners.
|
||||
|
||||
Let us expand our original werewolf query. Not only do we want to find all Characters in a moonlit room
|
||||
with a certain level of `lycanthrophy`. Now we also want the full moon to immediately transform people who were
|
||||
recently bitten, even if their `lycantrophy` level is not yet high enough (more dramatic this way!). Let's say there is
|
||||
a Tag "recently_bitten" that controls this.
|
||||
|
||||
This is how we'd change our query:
|
||||
|
||||
```python
|
||||
from django.db.models import Q
|
||||
|
||||
will_transform = (
|
||||
Character.objects
|
||||
.filter(
|
||||
Q(db_location__db_tags__db_key__iexact="moonlit")
|
||||
& (
|
||||
Q(db_attributes__db_key="lycantrophy",
|
||||
db_attributes__db_value__gt=2)
|
||||
| Q(db_tags__db_key__iexact="recently_bitten")
|
||||
))
|
||||
.distinct()
|
||||
)
|
||||
```
|
||||
|
||||
That's quite compact. It may be easier to see what's going on if written this way:
|
||||
|
||||
```python
|
||||
from django.db.models import Q
|
||||
|
||||
q_moonlit = Q(db_location__db_tags__db_key__iexact="moonlit")
|
||||
q_lycantropic = Q(db_attributes__db_key="lycantrophy", db_attributes__db_value__gt=2)
|
||||
q_recently_bitten = Q(db_tags__db_key__iexact="recently_bitten")
|
||||
|
||||
will_transform = (
|
||||
Character.objects
|
||||
.filter(q_moonlit & (q_lycantropic | q_recently_bitten))
|
||||
.distinct()
|
||||
)
|
||||
```
|
||||
|
||||
```{sidebar} SQL
|
||||
|
||||
These Python structures are internally converted to SQL, the native language of the database.
|
||||
If you are familiar with SQL, these are many-to-many tables joined with `LEFT OUTER JOIN`,
|
||||
which may lead to multiple merged rows combining the same object with different relations.
|
||||
|
||||
```
|
||||
|
||||
This reads as "Find all Characters in a moonlit room that either has the Attribute `lycantrophy` higher
|
||||
than two _or_ which has the Tag `recently_bitten`". With an OR-query like this it's possible to find the
|
||||
same Character via different paths, so we add `.distinct()` at the end. This makes sure that there is only
|
||||
one instance of each Character in the result.
|
||||
|
||||
## Annotations
|
||||
|
||||
What if we wanted to filter on some condition that isn't represented easily by a field on the
|
||||
object? Maybe we want to find rooms only containing five or more objects?
|
||||
|
||||
We *could* do it like this (don't actually do it this way!):
|
||||
|
||||
```python
|
||||
from typeclasses.rooms import Room
|
||||
|
||||
all_rooms = Rooms.objects.all()
|
||||
|
||||
rooms_with_five_objects = []
|
||||
for room in all_rooms:
|
||||
if len(room.contents) >= 5:
|
||||
rooms_with_five_objects.append(room)
|
||||
```
|
||||
|
||||
Above we get all rooms and then use `list.append()` to keep adding the right rooms
|
||||
to an ever-growing list. This is _not_ a good idea, once your database grows this will
|
||||
be unnecessarily computing-intensive. The database is much more suitable for this.
|
||||
|
||||
_Annotations_ allow you to set a 'variable' inside the query that you can
|
||||
then access from other parts of the query. Let's do the same example as before directly in the database:
|
||||
|
||||
```python
|
||||
from typeclasses.rooms import Room
|
||||
from django.db.models import Count
|
||||
|
||||
rooms = (
|
||||
Room.objects
|
||||
.annotate(
|
||||
num_objects=Count('locations_set'))
|
||||
.filter(num_objects__gte=5)
|
||||
)
|
||||
```
|
||||
|
||||
`Count` is a Django class for counting the number of things in the database.
|
||||
|
||||
Here we first create an annotation `num_objects` of type `Count`. It creates an in-database function
|
||||
that will count the number of results inside the database.
|
||||
|
||||
> Note the use of `location_set` in that `Count`. The `*_set` is a back-reference automatically created by
|
||||
Django. In this case it allows you to find all objects that *has the current object as location*.
|
||||
|
||||
Next we filter on this annotation, using the name `num_objects` as something we can filter for. We
|
||||
use `num_objects__gte=5` which means that `num_objects` should be greater than 5. This is a little
|
||||
harder to get one's head around but much more efficient than lopping over all objects in Python.
|
||||
|
||||
## F-objects
|
||||
|
||||
What if we wanted to compare two dynamic parameters against one another in a query? For example, what if
|
||||
instead of having 5 or more objects, we only wanted objects that had a bigger inventory than they had
|
||||
tags (silly example, but ...)? This can be with Django's
|
||||
[F objects](https://docs.djangoproject.com/en/1.11/ref/models/expressions/#f-expressions).
|
||||
So-called F expressions allow you to do a query that looks at a value of each object in the database.
|
||||
|
||||
```python
|
||||
from django.db.models import Count, F
|
||||
from typeclasses.rooms import Room
|
||||
|
||||
result = (
|
||||
Room.objects
|
||||
.annotate(
|
||||
num_objects=Count('locations_set'),
|
||||
num_tags=Count('db_tags'))
|
||||
.filter(num_objects__gt=F('num_tags'))
|
||||
)
|
||||
```
|
||||
|
||||
Here we used `.annotate` to create two in-query 'variables' `num_objects` and `num_tags`. We then
|
||||
directly use these results in the filter. Using `F()` allows for also the right-hand-side of the filter
|
||||
condition to be calculated on the fly, completely within the database.
|
||||
|
||||
## Grouping and returning only certain properties
|
||||
|
||||
Suppose you used tags to mark someone belonging to an organization. Now you want to make a list and
|
||||
need to get the membership count of every organization all at once.
|
||||
|
||||
The `.annotate`, `.values_list`, and `.order_by` queryset methods are useful for this. Normally when
|
||||
you run a `.filter`, what you get back is a bunch of full typeclass instances, like roses or swords.
|
||||
Using `.values_list` you can instead choose to only get back certain properties on objects.
|
||||
The `.order_by` method finally allows for sorting the results according to some criterion:
|
||||
|
||||
|
||||
```python
|
||||
from django.db.models import Count
|
||||
from typeclasses.rooms import Room
|
||||
|
||||
result = (
|
||||
Character.objects
|
||||
.filter(db_tags__db_category="organization")
|
||||
.annotate(tagcount=Count('id'))
|
||||
.order_by('-tagcount'))
|
||||
.values_list('db_tags__db_key', "tagcount")
|
||||
```
|
||||
|
||||
Here we fetch all Characters who ...
|
||||
- ... has a tag of category "organization" on them
|
||||
- ... along the way we count how many different Characters (each `id` is unique) we find for each organization
|
||||
and store it in a 'variable' `tagcount` using `.annotate` and `Count`
|
||||
- ... we use this count to sort the result in descending order of `tagcount` (descending because there is a minus sign,
|
||||
default is increasing order but we want the most popular organization to be first).
|
||||
- ... and finally we make sure to only return exactly the properties we want, namely the name of the organization tag
|
||||
and how many matches we found for that organization.
|
||||
|
||||
The result queryset will be a list of tuples ordered in descending order by the number of matches,
|
||||
in a format like the following:
|
||||
```
|
||||
[
|
||||
('Griatch's poets society', 3872),
|
||||
("Chainsol's Ainneve Testers", 2076),
|
||||
("Blaufeuer's Whitespace Fixers", 1903),
|
||||
("Volund's Bikeshed Design Crew", 1764),
|
||||
("Tehom's Glorious Misanthropes", 1763)
|
||||
]
|
||||
```
|
||||
|
||||
## Conclusions
|
||||
|
||||
We have covered a lot of ground in this lesson and covered several more complex topics. Knowing how to
|
||||
query using Django is a powerful skill to have.
|
||||
|
||||
This concludes the first part of the Evennia starting tutorial - "What we have". Now we have a good foundation
|
||||
to understand how to plan what our tutorial game will be about.
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
# Overview of the Evennia library
|
||||
|
||||
```{sidebar} API
|
||||
|
||||
API stands for `Application Programming Interface`, a description for how to access
|
||||
the resources of a program or library.
|
||||
```
|
||||
A good place to start exploring Evennia is the [Evenia-API frontpage](../../../Evennia-API.md).
|
||||
This page sums up the main components of Evennia with a short description of each. Try clicking through
|
||||
to a few entries - once you get deep enough you'll see full descriptions
|
||||
of each component along with their documentation. You can also click `[source]` to see the full Python source
|
||||
for each thing.
|
||||
|
||||
You can also browse [the evennia repository on github](https://github.com/evennia/evennia). This is exactly
|
||||
what you can download from us. The github repo is also searchable.
|
||||
|
||||
Finally, you can clone the evennia repo to your own computer and read the sources locally. This is necessary
|
||||
if you want to help with Evennia's development itself. See the
|
||||
[extended install instructions](../../../Setup/Installation-Git.md) if you want to do this.
|
||||
|
||||
## Where is it?
|
||||
|
||||
If Evennia is installed, you can import from it simply with
|
||||
|
||||
import evennia
|
||||
from evennia import some_module
|
||||
from evennia.some_module.other_module import SomeClass
|
||||
|
||||
and so on.
|
||||
|
||||
If you installed Evennia with `pip install`, the library folder will be installed deep inside your Python
|
||||
installation. If you cloned the repo there will be a folder `evennia` on your hard drive there.
|
||||
|
||||
If you cloned the repo or read the code on `github` you'll find this being the outermost structure:
|
||||
|
||||
evennia/
|
||||
bin/
|
||||
CHANGELOG.md
|
||||
...
|
||||
...
|
||||
docs/
|
||||
evennia/
|
||||
|
||||
This outer layer is for Evennia's installation and package distribution. That internal folder `evennia/evennia/` is
|
||||
the _actual_ library, the thing covered by the API auto-docs and what you get when you do `import evennia`.
|
||||
|
||||
> The `evennia/docs/` folder contains the sources for this documentation. See
|
||||
> [contributing to the docs](../../../Contributing-Docs.md) if you want to learn more about how this works.
|
||||
|
||||
This the the structure of the Evennia library:
|
||||
|
||||
- evennia
|
||||
- [`__init__.py`](../../../Evennia-API.md#shortcuts) - The "flat API" of Evennia resides here.
|
||||
- [`settings_default.py`](../../../Setup/Settings.md#settings-file) - Root settings of Evennia. Copy settings
|
||||
from here to `mygame/server/settings.py` file.
|
||||
- [`commands/`](../../../Components/Commands.md) - The command parser and handler.
|
||||
- `default/` - The [default commands](../../../Components/Default-Commands.md) and cmdsets.
|
||||
- [`comms/`](../../../Components/Channels.md) - Systems for communicating in-game.
|
||||
- `contrib/` - Optional plugins too game-specific for core Evennia.
|
||||
- `game_template/` - Copied to become the "game directory" when using `evennia --init`.
|
||||
- [`help/`](../../../Components/Help-System.md) - Handles the storage and creation of help entries.
|
||||
- `locale/` - Language files ([i18n](../../../Concepts/Internationalization.md)).
|
||||
- [`locks/`](../../../Components/Locks.md) - Lock system for restricting access to in-game entities.
|
||||
- [`objects/`](../../../Components/Objects.md) - In-game entities (all types of items and Characters).
|
||||
- [`prototypes/`](../../../Components/Prototypes.md) - Object Prototype/spawning system and OLC menu
|
||||
- [`accounts/`](../../../Components/Accounts.md) - Out-of-game Session-controlled entities (accounts, bots etc)
|
||||
- [`scripts/`](../../../Components/Scripts.md) - Out-of-game entities equivalence to Objects, also with timer support.
|
||||
- [`server/`](../../../Components/Portal-And-Server.md) - Core server code and Session handling.
|
||||
- `portal/` - Portal proxy and connection protocols.
|
||||
- [`typeclasses/`](../../../Components/Typeclasses.md) - Abstract classes for the typeclass storage and database system.
|
||||
- [`utils/`](../../../Components/Coding-Utils.md) - Various miscellaneous useful coding resources.
|
||||
- [`web/`](../../../Concepts/Web-Features.md) - Web resources and webserver. Partly copied into game directory on initialization.
|
||||
|
||||
```{sidebar} __init__.py
|
||||
|
||||
The `__init__.py` file is a special Python filename used to represent a Python 'package'.
|
||||
When you import `evennia` on its own, you import this file. When you do `evennia.foo` Python will
|
||||
first look for a property `.foo` in `__init__.py` and then for a module or folder of that name
|
||||
in the same location.
|
||||
|
||||
```
|
||||
|
||||
While all the actual Evennia code is found in the various folders, the `__init__.py` represents the entire
|
||||
package `evennia`. It contains "shortcuts" to code that is actually located elsewhere. Most of these shortcuts
|
||||
are listed if you [scroll down a bit](../../../Evennia-API.md) on the Evennia-API page.
|
||||
|
||||
## An example of exploring the library
|
||||
|
||||
In the previous lesson we took a brief look at `mygame/typeclasses/objects` as an example of a Python module. Let's
|
||||
open it again. Inside is the `Object` class, which inherits from `DefaultObject`.
|
||||
Near the top of the module is this line:
|
||||
|
||||
from evennia import DefaultObject
|
||||
|
||||
We want to figure out just what this DefaultObject offers. Since this is imported directly from `evennia`, we
|
||||
are actually importing from `evennia/__init__.py`.
|
||||
|
||||
[Look at Line 159](github:evennia/__init__.py#159) of `evennia/__init__.py` and you'll find this line:
|
||||
|
||||
from .objects.objects import DefaultObject
|
||||
|
||||
```{sidebar} Relative and absolute imports
|
||||
|
||||
The first full-stop in `from .objects.objects ...` means that
|
||||
we are importing from the current location. This is called a `relative import`.
|
||||
By comparison, `from evennia.objects.objects` is an `absolute import`. In this particular
|
||||
case, the two would give the same result.
|
||||
```
|
||||
|
||||
> You can also look at [the right section of the API frontpage](../../../Evennia-API.md#typeclasses) and click through
|
||||
> to the code that way.
|
||||
|
||||
The fact that `DefaultObject` is imported into `__init__.py` here is what makes it possible to also import
|
||||
it as `from evennia import DefaultObject` even though the code for the class is not actually here.
|
||||
|
||||
So to find the code for `DefaultObject` we need to look in `evennia/objects/objects.py`. Here's how
|
||||
to look it up in the docs:
|
||||
|
||||
1. Open the [API frontpage](../../../Evennia-API.md)
|
||||
2. Locate the link to [evennia.objects.objects](evennia.objects.objects) and click on it.
|
||||
3 You are now in the python module. Scroll down (or search in your web browser) to find the `DefaultObject` class.
|
||||
4 You can now read what this does and what methods are on it. If you want to see the full source, click the
|
||||
\[source\] link next to it.
|
||||
208
docs/source/Howtos/Beginner-Tutorial/Part1/Gamedir-Overview.md
Normal file
208
docs/source/Howtos/Beginner-Tutorial/Part1/Gamedir-Overview.md
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
# Overview of your new Game Dir
|
||||
|
||||
Next we will take a little detour to look at the _Tutorial World_. This is a little solo adventure
|
||||
that comes with Evennia, a showcase for some of the things that are possible.
|
||||
|
||||
Now we have 'run the game' a bit and started with our forays into Python from inside Evennia.
|
||||
It is time to start to look at how things look 'outside of the game'. Let's do a tour of your game-dir
|
||||
Like everywhere in the docs we'll assume it's called `mygame`.
|
||||
|
||||
> When looking through files, ignore files ending with `.pyc` and the
|
||||
`__pycache__` folder if it exists. This is internal Python compilation files that you should never
|
||||
> need to touch. Files `__init__.py` is also often empty and can be ignored (they have to do with
|
||||
> Python package management).
|
||||
|
||||
You may have noticed when we were building things in-game that we would often refer to code through
|
||||
"python paths", such as
|
||||
|
||||
```{sidebar} Python-paths
|
||||
|
||||
A 'python path' uses '.' instead of '/' or '`\\`' and
|
||||
skips the `.py` ending of files. It can also point to
|
||||
the code contents of python files. Since Evennia is already
|
||||
looking for code in your game dir, your python paths can start
|
||||
from there.
|
||||
|
||||
So a path `/home/foo/devel/mygame/commands/command.py`
|
||||
would translate to a Python-path `commands.command`.
|
||||
```
|
||||
|
||||
create/drop button:tutorial_examples.red_button.RedButton
|
||||
|
||||
This is a fundamental aspect of coding Evennia - _you create code and then you tell Evennia where that
|
||||
code is and when it should be used_. Above we told it to create a red button by pulling from specific code
|
||||
in the `contribs/` folder but the same principle is true everywhere. So it's important to know where code is
|
||||
and how you point to it correctly.
|
||||
|
||||
- `mygame/`
|
||||
- `commands/` - This holds all your custom commands (user-input handlers). You both add your own
|
||||
and override Evennia's defaults from here.
|
||||
- `server`/ - The structure of this folder should not change since Evennia expects it.
|
||||
- `conf/` - All server configuration files sits here. The most important file is `settings.py`.
|
||||
- `logs/` - Server log files are stored here. When you use `evennia --log` you are actually
|
||||
tailing the files in this directory.
|
||||
- `typeclasses/` - this holds empty templates describing all database-bound entities in the
|
||||
game, like Characters, Scripts, Accounts etc. Adding code here allows to customize and extend
|
||||
the defaults.
|
||||
- `web/` - This is where you override and extend the default templates, views and static files used
|
||||
for Evennia's web-presence, like the website and the HTML5 webclient.
|
||||
- `world/` - this is a "miscellaneous" folder holding everything related to the world you are
|
||||
building, such as build scripts and rules modules that don't fit with one of the other folders.
|
||||
|
||||
> The `server/` subfolder should remain the way it is - Evennia expects this. But you could in
|
||||
> principle change the structure of the rest of your game dir as best fits your preference.
|
||||
> Maybe you don't need a world/ folder but prefer many folders with different aspects of your world?
|
||||
> Or a new folder 'rules' for your RPG rules? This is fine. If you move things around you just need
|
||||
> to update Evennia's default settings to point to the right places in the new structure.
|
||||
|
||||
## commands/
|
||||
|
||||
The `commands/` folder holds Python modules related to creating and extending the [Commands](../../../Components/Commands.md)
|
||||
of Evennia. These manifest in game like the server understanding input like `look` or `dig`.
|
||||
|
||||
```{sidebar} Classes
|
||||
|
||||
A `class` is template for creating object-instances of a particular type
|
||||
in Python. We will explain classes in more detail in the next
|
||||
`python overview <Python-basic-tutorial-part-two>`_.
|
||||
|
||||
```
|
||||
- [command.py](github:evennia/game_template/commands/command.py) (Python-path: `commands.command`) - this contain the
|
||||
base _classes_ for designing new input commands, or override the defaults.
|
||||
- [default_cmdsets.py](github:evennia/game_template/commands/default_cmdsets.py) (Python path: `commands.default_commands`) -
|
||||
a cmdset (Command-Set) groups Commands together. Command-sets can be added and removed from objects on the fly,
|
||||
meaning a user could have a different set of commands (or versions of commands) available depending on their circumstance
|
||||
in the game. In order to add a new command to the game, it's common to import the new command-class
|
||||
from `command.py` and add it to one of the default cmdsets in this module.
|
||||
|
||||
## server/
|
||||
|
||||
This folder contains resource necessary for running Evennia. Contrary to the other folders, the structure
|
||||
of this should be kept the way it is.
|
||||
|
||||
- `evennia.db3` - you will only have this file if you are using the default SQLite3 database. This file
|
||||
contains the entire database. Just copy it to make a backup. For development you could also just
|
||||
make a copy once you have set up everything you need and just copy that back to 'reset' the state.
|
||||
If you delete this file you can easily recreate it by running `evennia migrate`.
|
||||
|
||||
### server/logs/
|
||||
|
||||
This holds the server logs. When you do `evennia --log`, the evennia program is in fact tailing and concatenating
|
||||
the `server.log` and `portal.log` files in this directory. The logs are rotated every week. Depending on your settings,
|
||||
other logs, like the webserver HTTP request log can also be found here.
|
||||
|
||||
### server/conf/
|
||||
|
||||
This contains all configuration files of the Evennia server. These are regular Python modules which
|
||||
means that they must be extended with valid Python. You can also add logic to them if you wanted to.
|
||||
|
||||
Common for the settings is that you generally will never them directly via their python-path; instead Evennia
|
||||
knows where they are and will read them to configure itself at startup.
|
||||
|
||||
- `settings.py` - this is by far the most important file. It's nearly empty by default, rather you
|
||||
are expected to copy&paste the changes you need from [evennia/default_settings.py](github:evennia/default_settings.py).
|
||||
The default settings file is extensively documented. Importing/accessing the values in the settings
|
||||
file is done in a special way, like this:
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
To get to the setting `TELNET_PORT` in the settings file you'd then do
|
||||
|
||||
telnet_port = settings.TELNET_PORT
|
||||
|
||||
You cannot assign to the settings file dynamically; you must change the `settings.py` file directly to
|
||||
change a setting.
|
||||
- `secret_settings.py` - If you are making your code effort public, you may not want to share all settings online.
|
||||
There may be server-specific secrets or just fine-tuning for your game systems that you prefer be kept secret
|
||||
from the players. Put such settings in here, it will override values in `settings.py` and not be included in
|
||||
version control.
|
||||
- `at_initial_setup.py` - When Evennia starts up for the very first time, it does some basic tasks, like creating the
|
||||
superuser and Limbo room. Adding to this file allows to add more actions for it to for first-startup.
|
||||
- `at_search.py` - When searching for objects and either finding no match or more than one match, it will
|
||||
respond by giving a warning or offering the user to differentiate between the multiple matches. Modifying
|
||||
the code here will change this behavior to your liking.
|
||||
- `at_server_startstop.py` - This allows to inject code to execute every time the server starts, stops or reloads
|
||||
in different ways.
|
||||
- `connection_screens.py` - This allows for changing the connection screen you see when you first connect to your
|
||||
game.
|
||||
- `inlinefuncs.py` - _Inlinefuncs_ are optional and limited 'functions' that can be embedded in any strings being
|
||||
sent to a player. They are written as `$funcname(args)` and are used to customize the output
|
||||
depending on the user receiving it. For example sending people the text `"Let's meet at $realtime(13:00, GMT)!`
|
||||
would show every player seeing that string the time given in their own time zone. The functions added to this
|
||||
module will become new inlinefuncs in the game.
|
||||
- `inputfucs.py` - When a command like `look` is received by the server, it is handled by an _inputfunc_
|
||||
that redirects it to the cmdhandler system. But there could be other inputs coming from the clients, like
|
||||
button-presses or the request to update a health-bar. While most common cases are already covered, this is
|
||||
where one adds new functions to process new types of input.
|
||||
- `lockfuncs.py` - _Locks_ restrict access to things in-game. Lock funcs are used in a mini-language
|
||||
to defined more complex locks. For example you could have a lockfunc that checks if the user is carrying
|
||||
a given item, is bleeding or has a certain skill value. New functions added in this modules will
|
||||
become available for use in lock definitions.
|
||||
- `mssp.py` - Mud Server Status Protocol is a way for online MUD archives/listings (which you usually have
|
||||
to sign up for) to track which MUDs are currently online, how many players they have etc. While Evennia handles
|
||||
the dynamic information automatically, this is were you set up the meta-info about your game, such as its
|
||||
theme, if player-killing is allowed and so on. This is a more generic form of the Evennia Game directory.
|
||||
- `portal_services_plugins.py` - If you want to add new external connection protocols to Evennia, this is the place
|
||||
to add them.
|
||||
- `server_services_plugins.py` - This allows to override internal server connection protocols.
|
||||
- `web_plugins.py` - This allows to add plugins to the Evennia webserver as it starts.
|
||||
|
||||
### typeclasses/
|
||||
|
||||
The [Typeclasses](../../../Components/Typeclasses.md) of Evennia are Evennia-specific Python classes whose instances save themselves
|
||||
to the database. This allows a Character to remain in the same place and your updated strength stat to still
|
||||
be the same after a server reboot.
|
||||
|
||||
- [accounts.py](github:evennia/game_template/typeclasses/accounts.py) (Python-path: `typeclasses.accounts`) - An
|
||||
[Account](../../../Components/Accounts.md) represents the player connecting to the game. It holds information like email,
|
||||
password and other out-of-character details.
|
||||
- [channels.py](github:evennia/game_template/typeclasses/channels.py) (Python-path: `typeclasses.channels`) -
|
||||
[Channels](../../../Components/Channels.md) are used to manage in-game communication between players.
|
||||
- [objects.py](github:evennia/game_template/typeclasses/objects.py) (Python-path: `typeclasses.objects`) -
|
||||
[Objects](../../../Components/Objects.md) represent all things having a location within the game world.
|
||||
- [characters.py](github:evennia/game_template/typeclasses/characters.py) (Python-path: `typeclasses.characters`) -
|
||||
The [Character](../../../Components/Objects.md#characters) is a subclass of Objects, controlled by Accounts - they are the player's
|
||||
avatars in the game world.
|
||||
- [rooms.py](github:evennia/game_template/typeclasses/rooms.py) (Python-path: `typeclasses.rooms`) - A
|
||||
[Room](../../../Components/Objects.md#rooms) is also a subclass of Object; describing discrete locations. While the traditional
|
||||
term is 'room', such a location can be anything and on any scale that fits your game, from a forest glade,
|
||||
an entire planet or an actual dungeon room.
|
||||
- [exits.py](github:evennia/game_template/typeclasses/exits.py) (Python-path: `typeclasses.exits`) -
|
||||
[Exits](../../../Components/Objects.md#exits) is another subclass of Object. Exits link one Room to another.
|
||||
- [scripts.py](github:evennia/game_template/typeclasses/scripts.py) (Python-path: `typeclasses.scripts`) -
|
||||
[Scripts](../../../Components/Scripts.md) are 'out-of-character' objects. They have no location in-game and can serve as basis for
|
||||
anything that needs database persistence, such as combat, weather, or economic systems. They also
|
||||
have the ability to execute code repeatedly, on a timer.
|
||||
|
||||
### web/
|
||||
|
||||
This folder contains folders for overriding the default web-presence of Evennia with your own designs.
|
||||
Most of these folders are empty except for a README file or a subset of other empty folders.
|
||||
|
||||
- `media/` - this empty folder is where you can place your own images or other media files you want the
|
||||
web server to serve. If you are releasing your game with a lot of media (especially if you want videos) you
|
||||
should consider re-pointing Evennia to use some external service to serve your media instead.
|
||||
- `static_overrides/` - 'static' files include fonts, CSS and JS. Within this folder you'll find sub-folders for
|
||||
overriding the static files for the `admin` (this is the Django web-admin), the `webclient` (this is thet
|
||||
HTML5 webclient) and the `website`. Adding files to this folder will replace same-named files in the
|
||||
default web presence.
|
||||
- `template_overrides/` - these are HTML files, for the `webclient` and the `website`. HTML files are written
|
||||
using [Jinja](https://jinja.palletsprojects.com/en/2.11.x/) templating, which means that one can override
|
||||
only particular parts of a default template without touching others.
|
||||
- `static/` - this is a work-directory for the web system and should _not_ be manually modified. Basically,
|
||||
Evennia will copy static data from `static_overrides` here when the server starts.
|
||||
- `urls.py` - this module links up the Python code to the URLs you go to in the browser.
|
||||
|
||||
### world/
|
||||
|
||||
This folder only contains some example files. It's meant to hold 'the rest' of your game implementation. Many
|
||||
people change and re-structure this in various ways to better fit their ideas.
|
||||
|
||||
- [batch_cmds.ev](github:evennia/game_template/world/batch_cmds.ev) - This is an `.ev` file, which is essentially
|
||||
just a list of Evennia commands to execute in sequence. This one is empty and ready to expand on. The
|
||||
[Tutorial World](./Tutorial-World.md) was built with such a batch-file.
|
||||
- [prototypes.py](github:evennia/game_template/world/prototypes.py) - A [prototype](../../../Components/Prototypes.md) is a way
|
||||
to easily vary objects without changing their base typeclass. For example, one could use prototypes to
|
||||
tell that Two goblins, while both of the class 'Goblin' (so they follow the same code logic), should have different
|
||||
equipment, stats and looks.
|
||||
|
||||
|
|
@ -0,0 +1,621 @@
|
|||
# Making objects persistent
|
||||
|
||||
Now that we have learned a little about how to find things in the Evennia library, let's use it.
|
||||
|
||||
In the [Python classes and objects](./Python-classes-and-objects.md) lesson we created the dragons Fluffy, Cuddly
|
||||
and Smaug and made them fly and breathe fire. So far our dragons are short-lived - whenever we `restart`
|
||||
the server or `quit()` out of python mode they are gone.
|
||||
|
||||
This is what you should have in `mygame/typeclasses/monsters.py` so far:
|
||||
|
||||
|
||||
```python
|
||||
|
||||
class Monster:
|
||||
"""
|
||||
This is a base class for Monsters.
|
||||
"""
|
||||
|
||||
def __init__(self, key):
|
||||
self.key = key
|
||||
|
||||
def move_around(self):
|
||||
print(f"{self.key} is moving!")
|
||||
|
||||
|
||||
class Dragon(Monster):
|
||||
"""
|
||||
This is a dragon-specific monster.
|
||||
"""
|
||||
|
||||
def move_around(self):
|
||||
super().move_around()
|
||||
print("The world trembles.")
|
||||
|
||||
def firebreath(self):
|
||||
"""
|
||||
Let our dragon breathe fire.
|
||||
"""
|
||||
print(f"{self.key} breathes fire!")
|
||||
|
||||
```
|
||||
|
||||
## Our first persistent object
|
||||
|
||||
At this point we should know enough to understand what is happening in `mygame/typeclasses/objects.py`. Let's
|
||||
open it:
|
||||
|
||||
```python
|
||||
"""
|
||||
module docstring
|
||||
"""
|
||||
from evennia import DefaultObject
|
||||
|
||||
class Object(DefaultObject):
|
||||
"""
|
||||
class docstring
|
||||
"""
|
||||
pass
|
||||
```
|
||||
|
||||
So we have a class `Object` that _inherits_ from `DefaultObject`, which we have imported from Evennia.
|
||||
The class itself doesn't do anything (it just `pass`es) but that doesn't mean it's useless. As we've seen,
|
||||
it inherits all the functionality of its parent. It's in fact an _exact replica_ of `DefaultObject` right now.
|
||||
If we knew what kind of methods and resources were available on `DefaultObject` we could add our own and
|
||||
change the way it works!
|
||||
|
||||
> Hint: We will get back to this, but to learn what resources an Evennia parent like `DefaultObject` offers,
|
||||
> easiest is to peek at its [API documentation](evennia.objects.objects.DefaultObject). The docstring for
|
||||
> the `Object` class can also help.
|
||||
|
||||
One thing that Evennia classes offers and which you don't get with vanilla Python classes is _persistence_. As
|
||||
you've found, Fluffy, Cuddly and Smaug are gone once we reload the server. Let's see if we can fix this.
|
||||
|
||||
Go back to `mygame/typeclasses/monsters.py`. Change it as follows:
|
||||
|
||||
```python
|
||||
|
||||
from typeclasses.objects import Object
|
||||
|
||||
class Monster(Object):
|
||||
"""
|
||||
This is a base class for Monsters.
|
||||
"""
|
||||
def move_around(self):
|
||||
print(f"{self.key} is moving!")
|
||||
|
||||
|
||||
class Dragon(Monster):
|
||||
"""
|
||||
This is a dragon-specific Monster.
|
||||
"""
|
||||
|
||||
def move_around(self):
|
||||
super().move_around()
|
||||
print("The world trembles.")
|
||||
|
||||
def firebreath(self):
|
||||
"""
|
||||
Let our dragon breathe fire.
|
||||
"""
|
||||
print(f"{self.key} breathes fire!")
|
||||
|
||||
```
|
||||
|
||||
Don't forget to save. We removed `Monster.__init__` and made `Monster` inherit from Evennia's `Object` (which in turn
|
||||
inherits from Evennia's `DefaultObject`, as we saw). By extension, this means that `Dragon` also inherits
|
||||
from `DefaultObject`, just from further away!
|
||||
|
||||
### Making a new object by calling the class
|
||||
|
||||
First reload the server as usual. We will need to create the dragon a little differently this time:
|
||||
|
||||
```{sidebar} Keyword arguments
|
||||
|
||||
Keyword arguments (like `db_key="Smaug"`) is a way to
|
||||
name the input arguments to a function or method. They make
|
||||
things easier to read but also allows for conveniently setting
|
||||
defaults for values not given explicitly.
|
||||
|
||||
```
|
||||
> py
|
||||
> from typeclasses.monsters import Dragon
|
||||
> smaug = Dragon(db_key="Smaug", db_location=here)
|
||||
> smaug.save()
|
||||
> smaug.move_around()
|
||||
Smaug is moving!
|
||||
The world trembles.
|
||||
|
||||
Smaug works the same as before, but we created him differently: first we used
|
||||
`Dragon(db_key="Smaug", db_location=here)` to create the object, and then we used `smaug.save()` afterwards.
|
||||
|
||||
> quit()
|
||||
Python Console is closing.
|
||||
> look
|
||||
|
||||
You should now see that Smaug _is in the room with you_. Woah!
|
||||
|
||||
> reload
|
||||
> look
|
||||
|
||||
_He's still there_... What we just did was to create a new entry in the database for Smaug. We gave the object
|
||||
its name (key) and set its location to our current location (remember that `here` is just something available
|
||||
in the `py` command, you can't use it elsewhere).
|
||||
|
||||
To make use of Smaug in code we must first find him in the database. For an object in the current
|
||||
location we can easily do this in `py` by using `me.search()`:
|
||||
|
||||
> py smaug = me.search("Smaug") ; smaug.firebreath()
|
||||
Smaug breathes fire!
|
||||
|
||||
### Creating using create_object
|
||||
|
||||
Creating Smaug like we did above is nice because it's similar to how we created non-database
|
||||
bound Python instances before. But you need to use `db_key` instead of `key` and you also have to
|
||||
remember to call `.save()` afterwards. Evennia has a helper function that is more common to use,
|
||||
called `create_object`:
|
||||
|
||||
> py fluffy = evennia.create_object('typeclases.monster.Monster', key="Fluffy", location=here)
|
||||
> look
|
||||
|
||||
Boom, Fluffy should now be in the room with you, a little less scary than Smaug. You specify the
|
||||
python-path to the code you want and then set the key and location. Evennia sets things up and saves for you.
|
||||
|
||||
If you want to find Fluffy from anywhere, you can use Evennia's `search_object` helper:
|
||||
|
||||
> fluffy = evennia.search_object("Fluffy")[0] ; fluffy.move_around()
|
||||
Fluffy is moving!
|
||||
|
||||
> The `[0]` is because `search_object` always returns a _list_ of zero, one or more found objects. The `[0]`
|
||||
means that we want the first element of this list (counting in Python always starts from 0). If there were
|
||||
multiple Fluffies we could get the second one with `[1]`.
|
||||
|
||||
### Creating using create-command
|
||||
|
||||
Finally, you can also create a new Dragon using the familiar builder-commands we explored a few lessons ago:
|
||||
|
||||
> create/drop Cuddly:typeclasses.monsters.Monster
|
||||
|
||||
Cuddly is now in the room. After learning about how objects are created you'll realize that all this command really
|
||||
does is to parse your input, figure out that `/drop` means to "give the object the same location as the caller",
|
||||
and then do a call akin to
|
||||
|
||||
evennia.create_object("typeclasses.monsters.Monster", key="Cuddly", location=here)
|
||||
|
||||
That's pretty much all there is to the mighty `create` command! The rest is just parsing for the command
|
||||
to understand just what the user wants to create.
|
||||
|
||||
## Typeclasses
|
||||
|
||||
The `Object` (and `DefafultObject` class we inherited from above is what we refer to as a _Typeclass_. This
|
||||
is an Evennia thing. The instance of a typeclass saves itself to the database when it is created, and after
|
||||
that you can just search for it to get it back. We use the term _typeclass_ or _typeclassed_ to differentiate
|
||||
these types of classes and objects from the normal Python classes, whose instances go away on a reload.
|
||||
|
||||
The number of typeclasses in Evennia are so few they can be learned by heart:
|
||||
|
||||
- `evennia.DefaultObject`: This is the parent of all in-game entities - everything with a location. Evennia makes
|
||||
a few very useful child classes of this class:
|
||||
- `evennia.DefaultCharacter`: The default entity represening a player avatar in-game.
|
||||
- `evennia.DefaultRoom`: A location in the game world.
|
||||
- `evennia.DefaultExit`: A link between locations.
|
||||
- `evennia.DefaultAccount`: The OOC representation of a player, holds password and account info.
|
||||
- `evennia.DefaultChannel`: In-game channels. These could be used for all sorts of in-game communication.
|
||||
- `evennia.DefaultScript`: Out-of-game objects, with no presence in the game world. Anything you want to create that
|
||||
needs to be persistent can be stored with these entities, such as combat state, economic systems or what have you.
|
||||
|
||||
If you take a look in `mygame/typeclasses/` you'll find modules for each of these. Each contains an empty child
|
||||
class ready that already inherits from the right parent, ready for you to modify or build from:
|
||||
|
||||
- `mygame/typeclasses/objects.py` has `class Object(DefaultObject)`, a class directly inheriting the basic in-game entity, this
|
||||
works as a base for any object.
|
||||
- `mygame/typeclasses/characters.py` has `class Character(DefaultCharacter)`
|
||||
- `mygame/typeclasses/rooms.py` has `class Room(DefaultRoom)`
|
||||
- `mygame/typeclasses/exits.py` has `class Exit(DefaultExit)`
|
||||
- `mygame/typeclasses/accounts.py` has `class Account(DefaultAccount)`
|
||||
- `mygame/typeclasses/channels.py` has `class Channel(DefaultChannel)`
|
||||
- `mygame/typeclasses/scripts.py` has `class Script(DefaultScript)`
|
||||
|
||||
> Notice that the classes in `mygame/typeclasses/` are _not inheriting from each other_. For example,
|
||||
> `Character` is inheriting from `evennia.DefaultCharacter` and not from `typeclasses.objects.Object`.
|
||||
> So if you change `Object` you will not cause any change in the `Character` class. If you want that you
|
||||
> can easily just change the child classes to inherit in that way instead; Evennia doesn't care.
|
||||
|
||||
As seen with our `Dragon` example, you don't _have_ to modify these modules directly. You can just make your
|
||||
own modules and import the base class.
|
||||
|
||||
### Examining and defaults
|
||||
|
||||
When you do
|
||||
|
||||
> create/drop giantess:typeclasses.monsters.Monster
|
||||
You create a new Monster: giantess.
|
||||
|
||||
or
|
||||
|
||||
> py evennia.create_object("typeclasses.monsters.Monster", key="Giantess", location=here)
|
||||
|
||||
You are specifying exactly which typeclass you want to use to build the Giantess. Let's examine the result:
|
||||
|
||||
> examine giantess
|
||||
-------------------------------------------------------------------------------
|
||||
Name/key: Giantess (#14)
|
||||
Typeclass: Monster (typeclasses.monsters.Monster)
|
||||
Location: Limbo (#2)
|
||||
Home: Limbo (#2)
|
||||
Permissions: <None>
|
||||
Locks: call:true(); control:id(1) or perm(Admin); delete:id(1) or perm(Admin);
|
||||
drop:holds(); edit:perm(Admin); examine:perm(Builder); get:all();
|
||||
puppet:pperm(Developer); tell:perm(Admin); view:all()
|
||||
Persistent attributes:
|
||||
desc = You see nothing special.
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
We used the `examine` command briefly in the [lesson about building in-game](./Building-Quickstart.md). Now these lines
|
||||
may be more useful to us:
|
||||
- **Name/key** - The name of this thing. The value `(#14)` is probably different for you. This is the
|
||||
unique 'primary key' or _dbref_ for this entity in the database.
|
||||
- **Typeclass**: This show the typeclass we specified, and the path to it.
|
||||
- **Location**: We are in Limbo. If you moved elsewhere you'll see that instead. Also the `#dbref` is shown.
|
||||
- **Permissions**: _Permissions_ are like the inverse to _Locks_ - they are like keys to unlock access to other things.
|
||||
The giantess have no such keys (maybe fortunately).
|
||||
- **Locks**: Locks are the inverse of _Permissions_ - specify what criterion _other_ objects must fulfill in order to
|
||||
access the `giantess` object. This uses a very flexible mini-language. For examine, the line `examine:perm(Builders)`
|
||||
is read as "Only those with permission _Builder_ or higher can _examine_ this object". Since we are the superuser
|
||||
we pass (even bypass) such locks with ease.
|
||||
- **Persistent attributes**: This allows for storing arbitrary, persistent data on the typeclassed entity. We'll get
|
||||
to those in the next section.
|
||||
|
||||
Note how the **Typeclass** line describes exactly where to find the code of this object? This is very useful for
|
||||
understanding how any object in Evennia works.
|
||||
|
||||
What happens if we _don't_ specify the typeclass though?
|
||||
|
||||
> create/drop box
|
||||
You create a new Object: box.
|
||||
|
||||
or
|
||||
|
||||
> py create.create_object(None, key="box", location=here)
|
||||
|
||||
Now check it out:
|
||||
|
||||
> examine box
|
||||
|
||||
You will find that the **Typeclass** line now reads
|
||||
|
||||
Typeclass: Object (typeclasses.objects.Object)
|
||||
|
||||
So when you didn't specify a typeclass, Evennia used a default, more specifically the (so far) empty `Object` class in
|
||||
`mygame/typeclasses/objects.py`. This is usually what you want, especially since you can tweak that class as much
|
||||
as you like.
|
||||
|
||||
But the reason Evennia knows to fall back to this class is not hard-coded - it's a setting. The default is
|
||||
in [evennia/settings_default.py](https://github.com/evennia/evennia/blob/master/evennia/settings_default.py#L465),
|
||||
with the name `BASE_OBJECT_TYPECLASS`, which is set to `typeclasses.objects.Object`.
|
||||
|
||||
```{sidebar} Changing things
|
||||
|
||||
While it's tempting to change folders around to your liking, this can
|
||||
make it harder to follow tutorials and may confuse if
|
||||
you are asking others for help. So don't overdo it unless you really
|
||||
know what you are doing.
|
||||
```
|
||||
|
||||
So if you wanted the creation commands and methods to default to some other class you could
|
||||
add your own `BASE_OBJECT_TYPECLASS` line to `mygame/server/conf/settings.py`. The same is true for all the other
|
||||
typeclasseses, like characters, rooms and accounts. This way you can change the
|
||||
layout of your game dir considerably if you wanted. You just need to tell Evennia where everything is.
|
||||
|
||||
## Modifying ourselves
|
||||
|
||||
Let's try to modify ourselves a little. Open up `mygame/typeclasses/characters.py`.
|
||||
|
||||
```python
|
||||
"""
|
||||
(module docstring)
|
||||
"""
|
||||
from evennia import DefaultCharacter
|
||||
|
||||
class Character(DefaultCharacter):
|
||||
"""
|
||||
(class docstring)
|
||||
"""
|
||||
pass
|
||||
```
|
||||
|
||||
This looks quite familiar now - an empty class inheriting from the Evennia base typeclass. As you would expect,
|
||||
this is also the default typeclass used for creating Characters if you don't specify it. You can verify it:
|
||||
|
||||
> examine me
|
||||
------------------------------------------------------------------------------
|
||||
Name/key: YourName (#1)
|
||||
Session id(s): #1
|
||||
Account: YourName
|
||||
Account Perms: <Superuser> (quelled)
|
||||
Typeclass: Character (typeclasses.characters.Character)
|
||||
Location: Limbo (#2)
|
||||
Home: Limbo (#2)
|
||||
Permissions: developer, player
|
||||
Locks: boot:false(); call:false(); control:perm(Developer); delete:false();
|
||||
drop:holds(); edit:false(); examine:perm(Developer); get:false();
|
||||
msg:all(); puppet:false(); tell:perm(Admin); view:all()
|
||||
Stored Cmdset(s):
|
||||
commands.default_cmdsets.CharacterCmdSet [DefaultCharacter] (Union, prio 0)
|
||||
Merged Cmdset(s):
|
||||
...
|
||||
Commands available to YourName (result of Merged CmdSets):
|
||||
...
|
||||
Persistent attributes:
|
||||
desc = This is User #1.
|
||||
prelogout_location = Limbo
|
||||
Non-Persistent attributes:
|
||||
last_cmd = None
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
You got a lot longer output this time. You have a lot more going on than a simple Object. Here are some new fields of note:
|
||||
- **Session id(s)**: This identifies the _Session_ (that is, the individual connection to a player's game client).
|
||||
- **Account** shows, well the `Account` object associated with this Character and Session.
|
||||
- **Stored/Merged Cmdsets** and **Commands available** is related to which _Commands_ are stored on you. We will
|
||||
get to them in the [next lesson](./Adding-Commands.md). For now it's enough to know these consitute all the
|
||||
commands available to you at a given moment.
|
||||
- **Non-Persistent attributes** are Attributes that are only stored temporarily and will go away on next reload.
|
||||
|
||||
Look at the **Typeclass** field and you'll find that it points to `typeclasses.character.Character` as expected.
|
||||
So if we modify this class we'll also modify ourselves.
|
||||
|
||||
### A method on ourselves
|
||||
|
||||
Let's try something simple first. Back in `mygame/typeclasses/characters.py`:
|
||||
|
||||
```python
|
||||
|
||||
class Character(DefaultCharacter):
|
||||
"""
|
||||
(class docstring)
|
||||
"""
|
||||
|
||||
str = 10
|
||||
dex = 12
|
||||
int = 15
|
||||
|
||||
def get_stats(self):
|
||||
"""
|
||||
Get the main stats of this character
|
||||
"""
|
||||
return self.str, self.dex, self.int
|
||||
|
||||
```
|
||||
|
||||
> reload
|
||||
> py self.get_stats()
|
||||
(10, 12, 15)
|
||||
|
||||
```{sidebar} Tuples and lists
|
||||
|
||||
- A `list` is written `[a, b, c, d, ...]`. It can be modified after creation.
|
||||
- A `tuple` is written `(a, b, c, ...)`. It cannot be modified once created.
|
||||
```
|
||||
We made a new method, gave it a docstring and had it `return` the RP-esque values we set. It comes back as a
|
||||
_tuple_ `(10, 12, 15)`. To get a specific value you could specify the _index_ of the value you want,
|
||||
starting from zero:
|
||||
|
||||
> py stats = self.get_stats() ; print(f"Strength is {stats[0]}.")
|
||||
Strength is 10.
|
||||
|
||||
### Attributes
|
||||
|
||||
So what happens when we increase our strength? This would be one way:
|
||||
|
||||
> py self.str = self.str + 1
|
||||
> py self.str
|
||||
11
|
||||
|
||||
Here we set the strength equal to its previous value + 1. A shorter way to write this is to use Python's `+=`
|
||||
operator:
|
||||
|
||||
> py self.str += 1
|
||||
> py self.str
|
||||
12
|
||||
> py self.get_stats()
|
||||
(12, 12, 15)
|
||||
|
||||
This looks correct! Try to change the values for dex and int too; it works fine. However:
|
||||
|
||||
> reload
|
||||
> py self.get_stats()
|
||||
(10, 12, 15)
|
||||
|
||||
After a reload all our changes were forgotten. When we change properties like this, it only changes in memory,
|
||||
not in the database (nor do we modify the python module's code). So when we reloaded, the 'fresh' `Character`
|
||||
class was loaded, and it still has the original stats we wrote to it.
|
||||
|
||||
In principle we could change the python code. But we don't want to do that manually every time. And more importantly
|
||||
since we have the stats hardcoded in the class, _every_ character instance in the game will have exactly the
|
||||
same `str`, `dex` and `int` now! This is clearly not what we want.
|
||||
|
||||
Evennia offers a special, persistent type of property for this, called an `Attribute`. Rework your
|
||||
`mygame/typeclasses/characters.py` like this:
|
||||
|
||||
```python
|
||||
|
||||
class Character(DefaultCharacter):
|
||||
"""
|
||||
(class docstring)
|
||||
"""
|
||||
|
||||
def get_stats(self):
|
||||
"""
|
||||
Get the main stats of this character
|
||||
"""
|
||||
return self.db.str, self.db.dex, self.db.int
|
||||
```
|
||||
|
||||
```{sidebar} Spaces in Attribute name?
|
||||
|
||||
What if you want spaces in your Attribute name? Or you want to assign the
|
||||
name of the Attribute on-the fly? Then you can use `.attributes.add(name, value)` instead,
|
||||
for example `self.attributes.add("str", 10)`.
|
||||
|
||||
```
|
||||
|
||||
We removed the hard-coded stats and added added `.db` for every stat. The `.db` handler makes the stat
|
||||
into an an Evennia `Attribute`.
|
||||
|
||||
> reload
|
||||
> py self.get_stats()
|
||||
(None, None, None)
|
||||
|
||||
Since we removed the hard-coded values, Evennia don't know what they should be (yet). So all we get back
|
||||
is `None`, which is a Python reserved word to represent nothing, a no-value. This is different from a normal python
|
||||
property:
|
||||
|
||||
> py self.str
|
||||
AttributeError: 'Character' object has no attribute 'str'
|
||||
> py self.db.str
|
||||
(nothing will be displayed, because it's None)
|
||||
|
||||
Trying to get an unknown normal Python property will give an error. Getting an unknown Evennia `Attribute` will
|
||||
never give an error, but only result in `None` being returned. This is often very practical.
|
||||
|
||||
> py self.db.str, self.db.dex, self.db.int = 10, 12, 15
|
||||
> py self.get_stats()
|
||||
(10, 12, 15)
|
||||
> reload
|
||||
> py self.get_stats()
|
||||
(10, 12, 15)
|
||||
|
||||
Now we set the Attributes to the right values. We can see that things work the same as before, also after a
|
||||
server reload. Let's modify the strength:
|
||||
|
||||
> py self.db.str += 2
|
||||
> py self.get_stats()
|
||||
(12, 12, 15)
|
||||
> reload
|
||||
> py self.get_stats()
|
||||
(12, 12, 15)
|
||||
|
||||
Our change now survives a reload since Evennia automatically saves the Attribute to the database for us.
|
||||
|
||||
### Setting things on new Characters
|
||||
|
||||
Things a looking better, but one thing remains strange - the stats start out with a value `None` and we
|
||||
have to manually set them to something reasonable. In a later lesson we will investigate character-creation
|
||||
in more detail. For now, let's give every new character some random stats to start with.
|
||||
|
||||
We want those stats to be set only once, when the object is first created. For the Character, this method
|
||||
is called `at_object_creation`.
|
||||
|
||||
```{sidebar} __init__ vs at_object_creation
|
||||
|
||||
For the `Monster` class we used `__init__` to set up the class. We can't use this
|
||||
for a typeclass because it will be called more than once, at the very least after
|
||||
every reload and maybe more depending on caching. Even if you are familiar with Python,
|
||||
avoid touching `__init__` for typeclasses, the results will not be what you expect.
|
||||
|
||||
```
|
||||
|
||||
```python
|
||||
# up by the other imports
|
||||
import random
|
||||
|
||||
class Character(DefaultCharacter):
|
||||
"""
|
||||
(class docstring)
|
||||
"""
|
||||
|
||||
def at_object_creation(self):
|
||||
self.db.str = random.randint(3, 18)
|
||||
self.db.dex = random.randint(3, 18)
|
||||
self.db.int = random.randint(3, 18)
|
||||
|
||||
def get_stats(self):
|
||||
"""
|
||||
Get the main stats of this character
|
||||
"""
|
||||
return self.db.str, self.db.dex, self.db.int
|
||||
```
|
||||
|
||||
We imported a new module, `random`. This is part of Python's standard library. We used `random.randint` to
|
||||
set a random value from 3 to 18 to each stat. Simple, but for some classical RPGs this is all you need!
|
||||
|
||||
> reload
|
||||
> py self.get_stats()
|
||||
(12, 12, 15)
|
||||
|
||||
Hm, this is the same values we set before. They are not random. The reason for this is of course that, as said,
|
||||
`at_object_creation` only runs _once_, the very first time a character is created. Our character object was already
|
||||
created long before, so it will not be called again.
|
||||
|
||||
It's simple enough to run it manually though:
|
||||
|
||||
> self.at_object_creation()
|
||||
> py self.get_stats()
|
||||
(5, 4, 8)
|
||||
|
||||
Lady luck didn't smile on us for this example; maybe you'll fare better. Evennia has a helper command
|
||||
`update` that re-runs the creation hook and also cleans up any other Attributes not re-created by `at_object_creation`:
|
||||
|
||||
> update self
|
||||
> py self.get_stats()
|
||||
(8, 16, 14)
|
||||
|
||||
### Updating all Characters in a loop
|
||||
|
||||
Needless to say, for your game you are wise to have a feel for what you want to go into the `at_object_creation` hook
|
||||
before you create a lot of objects (characters in this case). But should it come to that you don't want to have to
|
||||
go around and re-run the method on everyone manually. For the Python beginner, doing this will also give a chance to
|
||||
try out Python _loops_. We try them out in multi-line Python mode:
|
||||
|
||||
> py
|
||||
> for a in [1, 2, "foo"]: > print(a)
|
||||
1
|
||||
2
|
||||
foo
|
||||
|
||||
A python _for-loop_ allows us to loop over something. Above, we made a _list_ of two numbers and a string. In
|
||||
every iteration of the loop, the variable `a` becomes one element in turn, and we print that.
|
||||
|
||||
For our list, we want to loop over all Characters, and want to call `.at_object_creation` on each. This is how
|
||||
this is done (still in python multi-line mode):
|
||||
|
||||
> from typeclasses.characters import Character
|
||||
> for char in Character.objects.all()
|
||||
> char.at_object_creation()
|
||||
|
||||
```{sidebar} Database queries
|
||||
|
||||
`Character.objects.all()` is an example of a database query expressed in Python. This will be converted
|
||||
into a database query under the hood. This syntax is part of
|
||||
`Django's query language <https://docs.djangoproject.com/en/3.0/topics/db/queries/>`_. You don't need to
|
||||
know Django to use Evennia, but if you ever need more specific database queries, this is always available
|
||||
when you need it.
|
||||
|
||||
```
|
||||
We import the `Character` class and then we use `.objects.all()` to get all `Character` instances. Simplified,
|
||||
`.objects` is a resource from which one can _query_ for all `Characters`. Using `.all()` gets us a listing
|
||||
of all of them that we then immediately loop over. Boom, we just updated all Characters, including ourselves:
|
||||
|
||||
> quit()
|
||||
Closing the Python console.
|
||||
> self.get_stats()
|
||||
(3, 18, 10)
|
||||
|
||||
## Extra Credits
|
||||
|
||||
This principle is the same for other typeclasses. So using the tools explored in this lesson, try to expand
|
||||
the default room with an `is_dark` flag. It can be either `True` or `False`.
|
||||
Have all new rooms start with `is_dark = False` and make it so that once you change it, it survives a reload.
|
||||
Oh, and if you created any other rooms before, make sure they get the new flag too!
|
||||
|
||||
## Conclusions
|
||||
|
||||
In this lesson we created database-persistent dragons by having their classes inherit from one `Object`, one
|
||||
of Evennia's _typeclasses_. We explored where Evennia looks for typeclasses if we don't specify the path
|
||||
explicitly. We then modified ourselves - via the `Character` class - to give us some simple RPG stats. This
|
||||
led to the need to use Evennia's _Attributes_, settable via `.db` and to use a for-loop to update ourselves.
|
||||
|
||||
Typeclasses are a fundamental part of Evennia and we will see a lot of more uses of them in the course of
|
||||
this tutorial. But that's enough of them for now. It's time to take some action. Let's learn about _Commands_.
|
||||
|
||||
|
||||
497
docs/source/Howtos/Beginner-Tutorial/Part1/More-on-Commands.md
Normal file
497
docs/source/Howtos/Beginner-Tutorial/Part1/More-on-Commands.md
Normal file
|
|
@ -0,0 +1,497 @@
|
|||
# Parsing Command input
|
||||
|
||||
In this lesson we learn some basics about parsing the input of Commands. We will
|
||||
also learn how to add, modify and extend Evennia's default commands.
|
||||
|
||||
## More advanced parsing
|
||||
|
||||
In the last lesson we made a `hit` Command and hit a dragon with it. You should have the code
|
||||
from that still around.
|
||||
|
||||
Let's expand our simple `hit` command to accept a little more complex input:
|
||||
|
||||
hit <target> [[with] <weapon>]
|
||||
|
||||
That is, we want to support all of these forms
|
||||
|
||||
hit target
|
||||
hit target weapon
|
||||
hit target with weapon
|
||||
|
||||
If you don't specify a weapon you'll use your fists. It's also nice to be able to skip "with" if
|
||||
you are in a hurry. Time to modify `mygame/commands/mycommands.py` again. Let us break out the parsing
|
||||
a little, in a new method `parse`:
|
||||
|
||||
|
||||
```python
|
||||
#...
|
||||
|
||||
class CmdHit(Command):
|
||||
"""
|
||||
Hit a target.
|
||||
|
||||
Usage:
|
||||
hit <target>
|
||||
|
||||
"""
|
||||
key = "hit"
|
||||
|
||||
def parse(self):
|
||||
self.args = self.args.strip()
|
||||
target, *weapon = self.args.split(" with ", 1)
|
||||
if not weapon:
|
||||
target, *weapon = target.split(" ", 1)
|
||||
self.target = target.strip()
|
||||
if weapon:
|
||||
self.weapon = weapon.strip()
|
||||
else:
|
||||
self.weapon = ""
|
||||
|
||||
def func(self):
|
||||
if not self.args:
|
||||
self.caller.msg("Who do you want to hit?")
|
||||
return
|
||||
# get the target for the hit
|
||||
target = self.caller.search(self.target)
|
||||
if not target:
|
||||
return
|
||||
# get and handle the weapon
|
||||
weapon = None
|
||||
if self.weapon:
|
||||
weapon = self.caller.search(self.weapon)
|
||||
if weapon:
|
||||
weaponstr = f"{weapon.key}"
|
||||
else:
|
||||
weaponstr = "bare fists"
|
||||
|
||||
self.caller.msg(f"You hit {target.key} with {weaponstr}!")
|
||||
target.msg(f"You got hit by {self.caller.key} with {weaponstr}!")
|
||||
# ...
|
||||
|
||||
```
|
||||
|
||||
The `parse` method is called before `func` and has access to all the same on-command variables as in `func`. Using
|
||||
`parse` not only makes things a little easier to read, it also means you can easily let other Commands _inherit_
|
||||
your parsing - if you wanted some other Command to also understand input on the form `<arg> with <arg>` you'd inherit
|
||||
from this class and just implement the `func` needed for that command without implementing `parse` anew.
|
||||
|
||||
```{sidebar} Tuples and Lists
|
||||
|
||||
- A `list` is written as `[a, b, c, d, ...]`. You can add and grow/shrink a list after it was first created.
|
||||
- A `tuple` is written as `(a, b, c, d, ...)`. A tuple cannot be modified once it is created.
|
||||
|
||||
```
|
||||
- **Line 14** - We do the stripping of `self.args` once and for all here. We also store the stripped version back
|
||||
into `self.args`, overwriting it. So there is no way to get back the non-stripped version from here on, which is fine
|
||||
for this command.
|
||||
- **Line 15** - This makes use of the `.split` method of strings. `.split` will, well, split the string by some criterion.
|
||||
`.split(" with ", 1)` means "split the string once, around the substring `" with "` if it exists". The result
|
||||
of this split is a _list_. Just how that list looks depends on the string we are trying to split:
|
||||
1. If we entered just `hit smaug`, we'd be splitting just `"smaug"` which would give the result `["smaug"]`.
|
||||
2. `hit smaug sword` gives `["smaug sword"]`
|
||||
3. `hit smaug with sword` gives `["smaug", "sword"]`
|
||||
|
||||
So we get a list of 1 or 2 elements. We assign it to two variables like this, `target, *weapon = `. That
|
||||
asterisk in `*weapon` is a nifty trick - it will automatically become a list of _0 or more_ values. It sorts of
|
||||
"soaks" up everything left over.
|
||||
1. `target` becomes `"smaug"` and `weapon` becomes `[]`
|
||||
2. `target` becomes `"smaug sword"` and `weapon` becomes `[]`
|
||||
3. `target` becomes `"smaug"` and `weapon` becomes `sword`
|
||||
- **Lines 16-17** - In this `if` condition we check if `weapon` is falsy (that is, the empty list). This can happen
|
||||
under two conditions (from the example above):
|
||||
1. `target` is simply `smaug`
|
||||
2. `target` is `smaug sword`
|
||||
|
||||
To separate these cases we split `target` once again, this time by empty space `" "`. Again we store the
|
||||
result back with `target, *weapon =`. The result will be one of the following:
|
||||
1. `target` remains `smaug` and `weapon` remains `[]`
|
||||
2. `target` becomes `smaug` and `weapon` becomes `sword`
|
||||
- **Lines 18-22** - We now store `target` and `weapon` into `self.target` and `self.weapon`. We must do this in order
|
||||
for these local variables to made available in `func` later. Note how we need to check so `weapon` is not falsy
|
||||
before running `strip()` on it. This is because we know that if it's falsy, it's an empty list `[]` and lists
|
||||
don't have the `.strip()` method on them (so if we tried to use it, we'd get an error).
|
||||
|
||||
Now onto the `func` method. The main difference is we now have `self.target` and `self.weapon` available for
|
||||
convenient use.
|
||||
- **Lines 29 and 35** - We make use of the previously parsed search terms for the target and weapon to find the
|
||||
respective resource.
|
||||
- **Lines 34-39** - Since the weapon is optional, we need to supply a default (use our fists!) if it's not set. We
|
||||
use this to create a `weaponstr` that is different depending on if we have a weapon or not.
|
||||
- **Lines 41-42** - We merge the `weaponstr` with our attack text.
|
||||
|
||||
Let's try it out!
|
||||
|
||||
> reload
|
||||
> hit smaug with sword
|
||||
Could not find 'sword'.
|
||||
You hit smaug with bare fists!
|
||||
|
||||
Oops, our `self.caller.search(self.weapon)` is telling us that it found no sword. Since we are not `return`ing
|
||||
in this situation (like we do if failing to find `target`) we still continue fighting with our bare hands.
|
||||
This won't do. Let's make ourselves a sword.
|
||||
|
||||
> create sword
|
||||
|
||||
Since we didn't specify `/drop`, the sword will end up in our inventory and can seen with the `i` or
|
||||
`inventory` command. The `.search` helper will still find it there. There is no need to reload to see this
|
||||
change (no code changed, only stuff in the database).
|
||||
|
||||
> hit smaug with sword
|
||||
You hit smaug with sword!
|
||||
|
||||
|
||||
## Adding a Command to an object
|
||||
|
||||
The commands of a cmdset attached to an object with `obj.cmdset.add()` will by default be made available to that object
|
||||
but _also to those in the same location as that object_. If you did the [Building introduction](./Building-Quickstart.md)
|
||||
you've seen an example of this with the "Red Button" object. The [Tutorial world](./Tutorial-World.md)
|
||||
also has many examples of objects with commands on them.
|
||||
|
||||
To show how this could work, let's put our 'hit' Command on our simple `sword` object from the previous section.
|
||||
|
||||
> self.search("sword").cmdset.add("commands.mycommands.MyCmdSet", persistent=True)
|
||||
|
||||
We find the sword (it's still in our inventory so `self.search` should be able to find it), then
|
||||
add `MyCmdSet` to it. This actually adds both `hit` and `echo` to the sword, which is fine.
|
||||
|
||||
Let's try to swing it!
|
||||
|
||||
> hit
|
||||
More than one match for 'hit' (please narrow target):
|
||||
hit-1 (sword #11)
|
||||
hit-2
|
||||
|
||||
```{sidebar} Multi-matches
|
||||
|
||||
Some game engines will just pick the first hit when finding more than one.
|
||||
Evennia will always give you a choice. The reason for this is that Evennia
|
||||
cannot know if `hit` and `hit` are different or the same - maybe it behaves
|
||||
differently depending on the object it sits on? Besides, imagine if you had
|
||||
a red and a blue button both with the command `push` on it. Now you just write
|
||||
`push`. Wouldn't you prefer to be asked `which` button you really wanted to push?
|
||||
```
|
||||
Woah, that didn't go as planned. Evennia actually found _two_ `hit` commands to didn't know which one to use
|
||||
(_we_ know they are the same, but Evennia can't be sure of that). As we can see, `hit-1` is the one found on
|
||||
the sword. The other one is from adding `MyCmdSet` to ourself earlier. It's easy enough to tell Evennia which
|
||||
one you meant:
|
||||
|
||||
> hit-1
|
||||
Who do you want to hit?
|
||||
> hit-2
|
||||
Who do you want to hit?
|
||||
|
||||
In this case we don't need both command-sets, so let's just keep the one on the sword:
|
||||
|
||||
> self.cmdset.remove("commands.mycommands.MyCmdSet")
|
||||
> hit
|
||||
Who do you want to hit?
|
||||
|
||||
Now try this:
|
||||
|
||||
> tunnel n = kitchen
|
||||
> n
|
||||
> drop sword
|
||||
> s
|
||||
> hit
|
||||
Command 'hit' is not available. Maybe you meant ...
|
||||
> n
|
||||
> hit
|
||||
Who do you want to hit?
|
||||
|
||||
The `hit` command is now only available if you hold or are in the same room as the sword.
|
||||
|
||||
### You need to hold the sword!
|
||||
|
||||
Let's get a little ahead of ourselves and make it so you have to _hold_ the sword for the `hit` command to
|
||||
be available. This involves a _Lock_. We've cover locks in more detail later, just know that they are useful
|
||||
for limiting the kind of things you can do with an object, including limiting just when you can call commands on
|
||||
it.
|
||||
```{sidebar} Locks
|
||||
|
||||
Evennia Locks are defined as a mini-language defined in `lockstrings`. The lockstring
|
||||
is on a form `<situation>:<lockfuncs>`, where `situation` determines when this
|
||||
lock applies and the `lockfuncs` (there can be more than one) are run to determine
|
||||
if the lock-check passes or not depending on circumstance.
|
||||
```
|
||||
|
||||
> py self.search("sword").locks.add("call:holds()")
|
||||
|
||||
We added a new lock to the sword. The _lockstring_ `"call:holds()"` means that you can only _call_ commands on
|
||||
this object if you are _holding_ the object (that is, it's in your inventory).
|
||||
|
||||
For locks to work, you cannot be _superuser_, since the superuser passes all locks. You need to `quell` yourself
|
||||
first:
|
||||
```{sidebar} quell/unquell
|
||||
|
||||
Quelling allows you as a developer to take on the role of players with less
|
||||
priveleges. This is useful for testing and debugging, in particular since a
|
||||
superuser has a little `too` much power sometimes.
|
||||
Use `unquell` to get back to your normal self.
|
||||
```
|
||||
|
||||
> quell
|
||||
|
||||
If the sword lies on the ground, try
|
||||
|
||||
> hit
|
||||
Command 'hit' is not available. ..
|
||||
> get sword
|
||||
> hit
|
||||
> Who do you want to hit?
|
||||
|
||||
|
||||
Finally, we get rid of ours sword so we have a clean slate with no more `hit` commands floating around.
|
||||
We can do that in two ways:
|
||||
|
||||
delete sword
|
||||
|
||||
or
|
||||
|
||||
py self.search("sword").delete()
|
||||
|
||||
|
||||
## Adding the Command to a default Cmdset
|
||||
|
||||
|
||||
As we have seen we can use `obj.cmdset.add()` to add a new cmdset to objects, whether that object
|
||||
is ourself (`self`) or other objects like the `sword`.
|
||||
|
||||
This is how all commands in Evennia work, including default commands like `look`, `dig`, `inventory` and so on.
|
||||
All these commands are in just loaded on the default objects that Evennia provides out of the box.
|
||||
|
||||
- Characters (that is 'you' in the gameworld) has the `CharacterCmdSet`.
|
||||
- Accounts (the thing that represents your out-of-character existence on the server) has the `AccountCmdSet`
|
||||
- Sessions (representing one single client connection) has the `SessionCmdSet`
|
||||
- Before you log in (at the connection screen) you'll have access to the `UnloggedinCmdSet`.
|
||||
|
||||
The thing must commonly modified is the `CharacterCmdSet`.
|
||||
|
||||
The default cmdset are defined in `mygame/commands/default_cmdsets.py`. Open that file now:
|
||||
|
||||
```python
|
||||
"""
|
||||
(module docstring)
|
||||
"""
|
||||
|
||||
from evennia import default_cmds
|
||||
|
||||
class CharacterCmdSet(default_cmds.CharacterCmdSet):
|
||||
|
||||
key = "DefaultCharacter"
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
|
||||
super().at_cmdset_creation()
|
||||
#
|
||||
# any commands you add below will overload the default ones
|
||||
#
|
||||
|
||||
class AccountCmdSet(default_cmds.AccountCmdSet):
|
||||
|
||||
key = "DefaultAccount"
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
|
||||
super().at_cmdset_creation()
|
||||
#
|
||||
# any commands you add below will overload the default ones
|
||||
#
|
||||
|
||||
class UnloggedinCmdSet(default_cmds.UnloggedinCmdSet):
|
||||
|
||||
key = "DefaultUnloggedin"
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
|
||||
super().at_cmdset_creation()
|
||||
#
|
||||
# any commands you add below will overload the default ones
|
||||
#
|
||||
|
||||
class SessionCmdSet(default_cmds.SessionCmdSet):
|
||||
|
||||
key = "DefaultSession"
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
|
||||
super().at_cmdset_creation()
|
||||
#
|
||||
# any commands you add below will overload the default ones
|
||||
#
|
||||
```
|
||||
|
||||
```{sidebar} super()
|
||||
|
||||
The `super()` function refers to the parent of the current class and is commonly
|
||||
used to call same-named methods on the parent.
|
||||
```
|
||||
`evennia.default_cmds` is a container that holds all of Evennia's default commands and cmdsets. In this module
|
||||
we can see that this was imported and then a new child class was made for each cmdset. Each class looks familiar
|
||||
(except the `key`, that's mainly used to easily identify the cmdset in listings). In each `at_cmdset_creation` all
|
||||
we do is call `super().at_cmdset_creation` which means that we call `at_cmdset_creation() on the _parent_ CmdSet.
|
||||
This is what adds all the default commands to each CmdSet.
|
||||
|
||||
To add even more Commands to a default cmdset, we can just add them below the `super()` line. Usefully, if we were to
|
||||
add a Command with the same `.key` as a default command, it would completely replace that original. So if you were
|
||||
to add a command with a key `look`, the original `look` command would be replaced by your own version.
|
||||
|
||||
For now, let's add our own `hit` and `echo` commands to the `CharacterCmdSet`:
|
||||
|
||||
|
||||
```python
|
||||
# ...
|
||||
|
||||
from commands import mycommands
|
||||
|
||||
class CharacterCmdSet(default_cmds.CharacterCmdSet):
|
||||
|
||||
key = "DefaultCharacter"
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
|
||||
super().at_cmdset_creation()
|
||||
#
|
||||
# any commands you add below will overload the default ones
|
||||
#
|
||||
self.add(mycommands.CmdEcho)
|
||||
self.add(mycommands.CmdHit)
|
||||
|
||||
```
|
||||
|
||||
> reload
|
||||
> hit
|
||||
Who do you want to hit?
|
||||
|
||||
Your new commands are now available for all player characters in the game. There is another way to add a bunch
|
||||
of commands at once, and that is to add a _CmdSet_ to the other cmdset. All commands in that cmdset will then be added:
|
||||
|
||||
```python
|
||||
from commands import mycommands
|
||||
|
||||
class CharacterCmdSet(default_cmds.CharacterCmdSet):
|
||||
|
||||
key = "DefaultCharacter"
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
|
||||
super().at_cmdset_creation()
|
||||
#
|
||||
# any commands you add below will overload the default ones
|
||||
#
|
||||
self.add(mycommands.MyCmdSet)
|
||||
```
|
||||
|
||||
Which way you use depends on how much control you want, but if you already have a CmdSet,
|
||||
this is practical. A Command can be a part of any number of different CmdSets.
|
||||
|
||||
### Removing Commands
|
||||
|
||||
To remove your custom commands again, you of course just delete the change you did to
|
||||
`mygame/commands/default_cmdsets.py`. But what if you want to remove a default command?
|
||||
|
||||
We already know that we use `cmdset.remove()` to remove a cmdset. It turns out you can
|
||||
do the same in `at_cmdset_creation`. For example, let's remove the default `get` Command
|
||||
from Evennia. We happen to know this can be found as `default_cmds.CmdGet`.
|
||||
|
||||
|
||||
```python
|
||||
# ...
|
||||
from commands import mycommands
|
||||
|
||||
class CharacterCmdSet(default_cmds.CharacterCmdSet):
|
||||
|
||||
key = "DefaultCharacter"
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
|
||||
super().at_cmdset_creation()
|
||||
#
|
||||
# any commands you add below will overload the default ones
|
||||
#
|
||||
self.add(mycommands.MyCmdSet)
|
||||
self.remove(default_cmds.CmdGet)
|
||||
# ...
|
||||
```
|
||||
|
||||
> reload
|
||||
> get
|
||||
Command 'get' is not available ...
|
||||
|
||||
## Replace a default command
|
||||
|
||||
At this point you already have all the pieces for how to do this! We just need to add a new
|
||||
command with the same `key` in the `CharacterCmdSet` to replace the default one.
|
||||
|
||||
Let's combine this with what we know about classes and
|
||||
how to _override_ a parent class. Open `mygame/commands/mycommands.py` and lets override
|
||||
that `CmdGet` command.
|
||||
|
||||
```python
|
||||
# up top, by the other imports
|
||||
from evennia import default_cmds
|
||||
|
||||
# somewhere below
|
||||
class MyCmdGet(default_cmds.CmdGet):
|
||||
|
||||
def func(self):
|
||||
super().func()
|
||||
self.caller.msg(str(self.caller.location.contents))
|
||||
|
||||
```
|
||||
|
||||
- **Line2**: We import `default_cmds` so we can get the parent class.
|
||||
We made a new class and we make it _inherit_ `default_cmds.CmdGet`. We don't
|
||||
need to set `.key` or `.parse`, that's already handled by the parent.
|
||||
In `func` we call `super().func()` to let the parent do its normal thing,
|
||||
- **Line 7**: By adding our own `func` we replace the one in the parent.
|
||||
- **Line 8**: For this simple change we still want the command to work the
|
||||
same as before, so we use `super()` to call `func` on the parent.
|
||||
- **Line 9**: `.location` is the place an object is at. `.contents` contains, well, the
|
||||
contents of an object. If you tried `py self.contents` you'd get a list that equals
|
||||
your inventory. For a room, the contents is everything in it.
|
||||
So `self.caller.location.contents` gets the contents of our current location. This is
|
||||
a _list_. In order send this to us with `.msg` we turn the list into a string. Python
|
||||
has a special function `str()` to do this.
|
||||
|
||||
We now just have to add this so it replaces the default `get` command. Open
|
||||
`mygame/commands/default_cmdsets.py` again:
|
||||
|
||||
```python
|
||||
# ...
|
||||
from commands import mycommands
|
||||
|
||||
class CharacterCmdSet(default_cmds.CharacterCmdSet):
|
||||
|
||||
key = "DefaultCharacter"
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
|
||||
super().at_cmdset_creation()
|
||||
#
|
||||
# any commands you add below will overload the default ones
|
||||
#
|
||||
self.add(mycommands.MyCmdSet)
|
||||
self.add(mycommands.MyCmdGet)
|
||||
# ...
|
||||
```
|
||||
```{sidebar} Another way
|
||||
|
||||
Instead of adding `MyCmdGet` explicitly in default_cmdset.py,
|
||||
you could also add it to `mycommands.MyCmdSet` and let it be
|
||||
added automatically for you.
|
||||
```
|
||||
|
||||
> reload
|
||||
> get
|
||||
Get What?
|
||||
[smaug, fluffy, YourName, ...]
|
||||
|
||||
We just made a new `get`-command that tells us everything we could pick up (well, we can't pick up ourselves, so
|
||||
there's some room for improvement there).
|
||||
|
||||
## Summary
|
||||
|
||||
In this lesson we got into some more advanced string formatting - many of those tricks will help you a lot in
|
||||
the future! We also made a functional sword. Finally we got into how to add to, extend and replace a default
|
||||
command on ourselves.
|
||||
|
||||
|
|
@ -0,0 +1,653 @@
|
|||
# Intro to using Python with Evennia
|
||||
|
||||
Time to dip our toe into some coding! Evennia is written and extended in [Python](https://python.org),
|
||||
which is a mature and professional programming language that is very fast to work with.
|
||||
|
||||
That said, even though Python is widely considered easy to learn, we can only cover the most immediately
|
||||
important aspects of Python in this series of starting tutorials. Hopefully we can get you started
|
||||
but then you'll need to continue learning from there. See our [link section](../../../Links.md) for finding
|
||||
more reference material and dedicated Python tutorials.
|
||||
|
||||
> While this will be quite basic if you are an experienced developer, you may want to at least
|
||||
> stay around for the first few sections where we cover how to run Python from inside Evennia.
|
||||
|
||||
First, if you were quelling yourself to play the tutorial world, make sure to get your
|
||||
superuser powers back:
|
||||
|
||||
unquell
|
||||
|
||||
## Evennia Hello world
|
||||
|
||||
The `py` Command (or `!`, which is an alias) allows you as a superuser to execute raw Python from in-
|
||||
game. This is useful for quick testing. From the game's input line, enter the following:
|
||||
|
||||
> py print("Hello World!")
|
||||
|
||||
|
||||
```{sidebar} Command input
|
||||
|
||||
The line with `>` indicates input to enter in-game, while the lines below are the
|
||||
expected return from that input.
|
||||
```
|
||||
|
||||
You will see
|
||||
|
||||
> print("Hello world!")
|
||||
Hello World!
|
||||
|
||||
To understand what is going on: some extra info: The `print(...)` *function* is the basic, in-built
|
||||
way to output text in Python. We are sending "Hello World" as an _argument_ to this function. The quotes `"..."`
|
||||
mean that you are inputting a *string* (i.e. text). You could also have used single-quotes `'...'`,
|
||||
Python accepts both. A third variant is triple-quotes (`"""..."""` or `'''...'''`, which work across multiple
|
||||
lines and are common for larger text-blocks. The way we use the `py` command right now only supports
|
||||
single-line input however.
|
||||
|
||||
## Making some text 'graphics'
|
||||
|
||||
When making a text-game you will, unsurprisingly, be working a lot with text. Even if you have the occational
|
||||
button or even graphical element, the normal process is for the user to input commands as
|
||||
text and get text back. As we saw above, a piece of text is called a _string_ in Python and is enclosed in
|
||||
either single- or double-quotes.
|
||||
|
||||
Strings can be added together:
|
||||
|
||||
> py print("This is a " + "breaking change.")
|
||||
This is a breaking change.
|
||||
|
||||
A string multiplied with a number will repeat that string as many times:
|
||||
|
||||
> py print("|" + "-" * 40 + "|")
|
||||
|----------------------------------------|
|
||||
|
||||
or
|
||||
|
||||
> py print("A" + "a" * 5 + "rgh!")
|
||||
Aaaaaargh!
|
||||
|
||||
### .format()
|
||||
|
||||
While combining different strings is useful, even more powerful is the ability to modify the contents
|
||||
of the string in-place. There are several ways to do this in Python and we'll show two of them here. The first
|
||||
is to use the `.format` _method_ of the string:
|
||||
|
||||
> py print("This is a {} idea!".format("good"))
|
||||
This is a good idea!
|
||||
|
||||
```{eval-rst}
|
||||
.. sidebar:: Functions and Methods
|
||||
|
||||
Function:
|
||||
Something that performs and action when you `call` it with zero or more `arguments`. A function
|
||||
is stand-alone in a python module, like `print()`
|
||||
Method:
|
||||
A function that sits "on" an object, like `<string>.format()`.
|
||||
```
|
||||
|
||||
A method can be thought of as a resource "on" another object. The method knows on which object it
|
||||
sits and can thus affect it in various ways. You access it with the period `.`. In this case, the
|
||||
string has a resource `format(...)` that modifies it. More specifically, it replaced the `{}` marker
|
||||
inside the string with the value passed to the format. You can do so many times:
|
||||
|
||||
> py print("This is a {} idea!".format("bad"))
|
||||
This is a bad idea!
|
||||
|
||||
or
|
||||
|
||||
> py print("This is the {} and {} {} idea!".format("first", "second", "great"))
|
||||
This is the first and second great idea!
|
||||
|
||||
> Note the double-parenthesis at the end - the first closes the `format(...` method and the outermost
|
||||
closes the `print(...`. Not closing them will give you a scary `SyntaxError`. We will talk a
|
||||
little more about errors in the next section, for now just fix until it prints as expected.
|
||||
|
||||
Here we passed three comma-separated strings as _arguments_ to the string's `format` method. These
|
||||
replaced the `{}` markers in the same order as they were given.
|
||||
|
||||
The input does not have to be strings either:
|
||||
|
||||
> py print("STR: {}, DEX: {}, INT: {}".format(12, 14, 8))
|
||||
STR: 12, DEX: 14, INT: 8
|
||||
|
||||
To separate two Python instructions on the same line, you use the semi-colon, `;`. Try this:
|
||||
|
||||
> py a = "awesome sauce" ; print("This is {}!".format(a))
|
||||
This is awesome sauce!
|
||||
|
||||
```{warning} MUD clients and semi-colon
|
||||
|
||||
Some MUD clients use the semi-colon `;` to split client-inputs
|
||||
into separate sends. If so, the above will give an error. Most clients allow you to
|
||||
run in 'verbatim' mode or to remap to use some other separator than `;`. If you still have
|
||||
trouble, use the Evennia web client.
|
||||
```
|
||||
|
||||
What happened here was that we _assigned_ the string `"awesome sauce"` to a _variable_ we chose
|
||||
to name `a`. In the next statement, Python remembered what `a` was and we passed that into `format()`
|
||||
to get the output. If you replaced the value of `a` with something else in between, _that_ would be printed
|
||||
instead.
|
||||
|
||||
Here's the stat-example again, moving the stats to variables (here we just set them, but in a real
|
||||
game they may be changed over time, or modified by circumstance):
|
||||
|
||||
> py stren, dext, intel = 13, 14, 8 ; print("STR: {}, DEX: {}, INT: {}".format(stren, dext, intel))
|
||||
STR: 13, DEX: 14, INT: 8
|
||||
|
||||
The point is that even if the values of the stats change, the print() statement would not change - it just keeps
|
||||
pretty-printing whatever is given to it.
|
||||
|
||||
### f-strings
|
||||
|
||||
Using `.format()` is convenient (and there is a [lot more](https://www.w3schools.com/python/ref_string_format.asp)
|
||||
you can do with it). But the _f-string_ can be even more convenient. An
|
||||
f-string looks like a normal string ... except there is an `f` front of it, like this:
|
||||
|
||||
f"this is now an f-string."
|
||||
|
||||
An f-string on its own is just like any other string. But let's redo the example we did before, using an f-string:
|
||||
|
||||
> py a = "awesome sauce" ; print(f"This is {a}!")
|
||||
This is awesome sauce!
|
||||
|
||||
We could just insert that `a` variable directly into the f-string using `{a}`. Fewer parentheses to
|
||||
remember and arguable easier to read as well.
|
||||
|
||||
> py stren, dext, intel = 13, 14, 8 ; print(f"STR: {stren}, DEX: {dext}, INT: {intel}")
|
||||
STR: 13, DEX: 14, INT: 8
|
||||
|
||||
We will be exploring more complex string concepts when we get to creating Commands and need to
|
||||
parse and understand player input.
|
||||
|
||||
### Colored text
|
||||
|
||||
Python itself knows nothing about colored text, this is an Evennia thing. Evennia supports the
|
||||
standard color schemes of traditional MUDs.
|
||||
|
||||
> py print("|rThis is red text!|n This is normal color.")
|
||||
|
||||
Adding that `|r` at the start will turn our output bright red. `|R` will make it dark red. `|n`
|
||||
gives the normal text color. You can also use RGB (Red-Green-Blue) values from 0-5 (Xterm256 colors):
|
||||
|
||||
> py print("|043This is a blue-green color.|[530|003 Now dark blue text on orange background.")
|
||||
|
||||
> If you don't see the expected color, your client or terminal may not support Xterm256 (or
|
||||
color at all). Use the Evennia webclient.
|
||||
|
||||
Use the commands `color ansi` or `color xterm` to see which colors are available. Experiment!
|
||||
|
||||
## Importing code from other modules
|
||||
|
||||
As we saw in the previous sections, we used `.format` to format strings and `me.msg` to access
|
||||
the `msg` method on `me`. This use of the full-stop character is used to access all sorts of resources,
|
||||
including that in other Python modules.
|
||||
|
||||
Keep your game running, then open a text editor of your choice. If your game folder is called
|
||||
`mygame`, create a new text file `test.py` in the subfolder `mygame/world`. This is how the file
|
||||
structure should look:
|
||||
|
||||
```
|
||||
mygame/
|
||||
world/
|
||||
test.py
|
||||
```
|
||||
|
||||
For now, only add one line to `test.py`:
|
||||
|
||||
```python
|
||||
print("Hello World!")
|
||||
```
|
||||
|
||||
```{sidebar} Python module
|
||||
|
||||
This is a text file with the `.py` file ending. A module
|
||||
contains Python source code and from within Python one can
|
||||
access its contents by importing it via its python-path.
|
||||
```
|
||||
|
||||
Don't forget to _save_ the file. We just created our first Python _module_!
|
||||
To use this in-game we have to *import* it. Try this:
|
||||
|
||||
> py import world.test
|
||||
Hello World
|
||||
|
||||
If you make some error (we'll cover how to handle errors below), fix the error in the module and
|
||||
run the `reload` command in-game for your changes to take effect.
|
||||
|
||||
So importing `world.test` actually means importing `world/test.py`. Think of the period `.` as
|
||||
replacing `/` (or `\` for Windows) in your path. The `.py` ending of `test.py` is also never
|
||||
included in this "Python-path", but _only_ files with that ending can be imported this way.
|
||||
Where is `mygame` in that Python-path? The answer is that Evennia has already told Python that
|
||||
your `mygame` folder is a good place to look for imports. So we don't include `mygame` in the
|
||||
path - Evennia handles this for us.
|
||||
|
||||
When you import the module, the top "level" of it will execute. In this case, it will immediately
|
||||
print "Hello World".
|
||||
|
||||
Now try to run this a second time:
|
||||
|
||||
> py import world.test
|
||||
|
||||
You will *not* see any output this second time or any subsequent times! This is not a bug. Rather
|
||||
it is because of how Python importing works - it stores all imported modules and will
|
||||
avoid importing them more than once. So your `print` will only run the first time, when the module
|
||||
is first imported.
|
||||
|
||||
Try this:
|
||||
|
||||
> reload
|
||||
|
||||
And then
|
||||
|
||||
> py import world.test
|
||||
Hello World!
|
||||
|
||||
Now we see it again. The `reload` wiped the server's memory of what was imported, so it had to
|
||||
import it anew. You'd have to do this every time you wanted the print to show though, which is
|
||||
not very useful.
|
||||
|
||||
> We'll get back to more advanced ways to import code in later tutorial sections - this is an
|
||||
> important topic. But for now, let's press on and resolve this particular problem.
|
||||
|
||||
|
||||
### Our first own function
|
||||
|
||||
We want to be able to print our hello-world message at any time, not just once after a server
|
||||
reload. Change your `mygame/world/test.py` file to look like this:
|
||||
|
||||
```python
|
||||
def hello_world():
|
||||
print("Hello World!")
|
||||
```
|
||||
|
||||
As we are moving to multi-line Python code, there are some important things to remember:
|
||||
|
||||
- Capitalization matters in Python. It must be `def` and not `DEF`, `who` is not the same as `Who`.
|
||||
- Indentation matters in Python. The second line must be indented or it's not valid code. You should
|
||||
also use a consistent indentation length. We *strongly* recommend that you, for your own sanity's sake,
|
||||
set up your editor to always indent *4 spaces* (**not** a single tab-character) when you press the TAB key.
|
||||
|
||||
So about that function. Line 1:
|
||||
|
||||
- `def` is short for "define" and defines a *function* (or a *method*, if sitting on an object).
|
||||
This is a [reserved Python keyword](https://docs.python.org/2.5/ref/keywords.html); try not to use
|
||||
these words anywhere else.
|
||||
- A function name can not have spaces but otherwise we could have called it almost anything. We call
|
||||
it `hello_world`. Evennia follows [Python's standard naming style](https://github.com/evennia/evennia/blob/master/CODING_STYLE.md#a-quick-list-of-code-style-points)
|
||||
with lowercase letters and underscores. We recommend you do the same.
|
||||
- The colon (`:`) at the end of line 1 indicates that the header of the function is complete.
|
||||
|
||||
Line 2:
|
||||
|
||||
- The indentation marks the beginning of the actual operating code of the function (the function's
|
||||
*body*). If we wanted more lines to belong to this function those lines would all have to
|
||||
start at least at this indentation level.
|
||||
|
||||
Now let's try this out. First `reload` your game to have it pick up
|
||||
our updated Python module, then import it.
|
||||
|
||||
> reload
|
||||
> py import world.test
|
||||
|
||||
Nothing happened! That is because the function in our module won't do anything just by importing it (this
|
||||
is what we wanted). It will only act when we *call* it. So we need to first import the module and then access the
|
||||
function within:
|
||||
|
||||
> py import world.test ; world.test.hello_world()
|
||||
Hello world!
|
||||
|
||||
There is our "Hello World"! As mentioned earlier, use use semi-colon to put multiple
|
||||
Python-statements on one line. Note also the previous warning about mud-clients using the `;` to their
|
||||
own ends.
|
||||
|
||||
So what happened there? First we imported `world.test` as usual. But this time we continued and
|
||||
accessed the `hello_world` function _inside_ the newly imported module.
|
||||
|
||||
By adding `()` to the `hello_world` function we _call_ it, that is we run the body of the function and
|
||||
print our text. We can now redo this as many times as we want without having to `reload` in between:
|
||||
|
||||
|
||||
> py import world.test ; world.test.hello_world()
|
||||
Hello world!
|
||||
> py import world.test ; world.test.hello_world()
|
||||
Hello world!
|
||||
|
||||
## Sending text to others
|
||||
|
||||
The `print` command is a standard Python structure. We can use that here in the `py` command since
|
||||
we can se the output. It's great for debugging and quick testing. But if you need to send a text
|
||||
to an actual player, `print` won't do, because it doesn't know _who_ to send to. Try this:
|
||||
|
||||
> py me.msg("Hello world!")
|
||||
Hello world!
|
||||
|
||||
This looks the same as the `print` result, but we are now actually messaging a specific *object*,
|
||||
`me`. The `me` is a shortcut to 'us', the one running the `py` command. It is not some special
|
||||
Python thing, but something Evennia just makes available in the `py` command for convenience
|
||||
(`self` is an alias).
|
||||
|
||||
The `me` is an example of an *Object instance*. Objects are fundamental in Python and Evennia.
|
||||
The `me` object also contains a lot of useful resources for doing
|
||||
things with that object. We access those resources with '`.`'.
|
||||
|
||||
One such resource is `msg`, which works like `print` except it sends the text to the object it
|
||||
is attached to. So if we, for example, had an object `you`, doing `you.msg(...)` would send a message
|
||||
to the object `you`.
|
||||
|
||||
For now, `print` and `me.msg` behaves the same, just remember that `print` is mainly used for
|
||||
debugging and `.msg()` will be more useful for you in the future.
|
||||
|
||||
|
||||
## Parsing Python errors
|
||||
|
||||
Let's try this new text-sending in the function we just created. Go back to
|
||||
your `test.py` file and Replace the function with this instead:
|
||||
|
||||
```python
|
||||
def hello_world():
|
||||
me.msg("Hello World!")
|
||||
```
|
||||
|
||||
Save your file and `reload` your server to tell Evennia to re-import new code,
|
||||
then run it like before:
|
||||
|
||||
> py import world.test ; world.test.hello_world()
|
||||
|
||||
No go - this time you get an error!
|
||||
|
||||
```python
|
||||
File "./world/test.py", line 2, in hello_world
|
||||
me.msg("Hello World!")
|
||||
NameError: name 'me' is not defined
|
||||
```
|
||||
|
||||
```{sidebar} Errors in the logs
|
||||
|
||||
In regular use, tracebacks will often appear in the log rather than
|
||||
in the game. Use `evennia --log` to view the log in the terminal. Make
|
||||
sure to scroll back if you expect an error and don't see it. Use
|
||||
`Ctrl-C` (or `Cmd-C` on Mac) to exit the log-view.
|
||||
|
||||
```
|
||||
|
||||
This is called a *traceback*. Python's errors are very friendly and will most of the time tell you
|
||||
exactly what and where things go wrong. It's important that you learn to parse tracebacks so you
|
||||
know how to fix your code.
|
||||
|
||||
A traceback is to be read from the _bottom up_:
|
||||
|
||||
- (line 3) An error of type `NameError` is the problem ...
|
||||
- (line 3) ... more specifically it is due to the variable `me` not being defined.
|
||||
- (line 2) This happened on the line `me.msg("Hello world!")` ...
|
||||
- (line 1) ... which is on line `2` of the file `./world/test.py`.
|
||||
|
||||
In our case the traceback is short. There may be many more lines above it, tracking just how
|
||||
different modules called each other until the program got to the faulty line. That can
|
||||
sometimes be useful information, but reading from the bottom is always a good start.
|
||||
|
||||
The `NameError` we see here is due to a module being its own isolated thing. It knows nothing about
|
||||
the environment into which it is imported. It knew what `print` is because that is a special
|
||||
[reserved Python keyword](https://docs.python.org/2.5/ref/keywords.html). But `me` is *not* such a
|
||||
reserved word (as mentioned, it's just something Evennia came up with for convenience in the `py`
|
||||
command). As far as the module is concerned `me` is an unfamiliar name, appearing out of nowhere.
|
||||
Hence the `NameError`.
|
||||
|
||||
## Passing arguments to functions
|
||||
|
||||
We know that `me` exists at the point when we run the `py` command, because we can do `py me.msg("Hello World!")`
|
||||
with no problem. So let's _pass_ that me along to the function so it knows what it should be.
|
||||
Go back to your `test.py` and change it to this:
|
||||
|
||||
```python
|
||||
def hello_world(who):
|
||||
who.msg("Hello World!")
|
||||
```
|
||||
We now added an _argument_ to the function. We could have named it anything. Whatever `who` is,
|
||||
we will call a method `.msg()` on it.
|
||||
|
||||
As usual, `reload` the server to make sure the new code is available.
|
||||
|
||||
> py import world.test ; world.test.hello_world(me)
|
||||
Hello World!
|
||||
|
||||
Now it worked. We _passed_ `me` to our function. It will appear inside the function renamed as `who` and
|
||||
now the function works and prints as expected. Note how the `hello_world` function doesn't care _what_ you
|
||||
pass into it as long as it has a `.msg()` method on it. So you could reuse this function over and over for other
|
||||
suitable targets.
|
||||
|
||||
> **Extra Credit:** As an exercise, try to pass something else into `hello_world`. Try for example
|
||||
>to pass the number `5` or the string `"foo"`. You'll get errors telling you that they don't have
|
||||
>the attribute `msg`. They don't care about `me` itself not being a string or a number. If you are
|
||||
>familiar with other programming languages (especially C/Java) you may be tempted to start *validating*
|
||||
>`who` to make sure it's of the right type before you send it. This is usually not recommended in Python.
|
||||
>Python philosophy is to [handle](https://docs.python.org/2/tutorial/errors.html) the error if it happens
|
||||
>rather than to add a lot of code to prevent it from happening. See [duck typing](https://en.wikipedia.org/wiki/Duck_typing)
|
||||
>and the concept of _Leap before you Look_.
|
||||
|
||||
|
||||
## Finding others to send to
|
||||
|
||||
Let's wrap up this first Python `py` crash-course by finding someone else to send to.
|
||||
|
||||
In Evennia's `contrib/` folder (`evennia/contrib/tutorial_examples/mirror.py`) is a handy little
|
||||
object called the `TutorialMirror`. The mirror will echo whatever is being sent to it to
|
||||
the room it is in.
|
||||
|
||||
On the game command-line, let's create a mirror:
|
||||
|
||||
> create/drop mirror:contrib.tutorial_examples.mirror.TutorialMirror
|
||||
|
||||
```{sidebar} Creating objects
|
||||
|
||||
The `create` command was first used to create boxes in the
|
||||
`Building Stuff <Building-Quickstart>`_ tutorial. Note how it
|
||||
uses a "python-path" to describe where to load the mirror's code from.
|
||||
```
|
||||
|
||||
A mirror should appear in your location.
|
||||
|
||||
> look mirror
|
||||
mirror shows your reflection:
|
||||
This is User #1
|
||||
|
||||
What you are seeing is actually your own avatar in the game, the same thing that is available as `me` in the `py`
|
||||
command.
|
||||
|
||||
What we are aiming for now is the equivalent of `mirror.msg("Mirror Mirror on the wall")`. But the first thing that
|
||||
comes to mind will not work:
|
||||
|
||||
> py mirror.msg("Mirror, Mirror on the wall ...")
|
||||
NameError: name 'mirror' is not defined.
|
||||
|
||||
This is not surprising: Python knows nothing about "mirrors" or locations or anything. The `me` we've been using
|
||||
is, as mentioned, just a convenient thing the Evennia devs makes available to the `py` command. They couldn't possibly
|
||||
predict that you wanted to talk to mirrors.
|
||||
|
||||
Instead we will need to _search_ for that `mirror` object before we can send to it.
|
||||
Make sure you are in the same location as the mirror and try:
|
||||
|
||||
> py me.search("mirror")
|
||||
mirror
|
||||
|
||||
`me.search("name")` will, by default, search and _return_ an object with the given name found in _the same location_
|
||||
as the `me` object is. If it can't find anything you'll see an error.
|
||||
|
||||
```{sidebar} Function returns
|
||||
|
||||
Whereas a function like `print` only prints its arguments, it's very common
|
||||
for functions/methods to `return` a result of some kind. Think of the function
|
||||
as a machine - you put something in and out comes a result you can use. In the case
|
||||
of `me.search`, it will perform a database search and spit out the object it finds.
|
||||
```
|
||||
|
||||
> py me.search("dummy")
|
||||
Could not find 'dummy'.
|
||||
|
||||
Wanting to find things in the same location is very common, but as we continue we'll
|
||||
find that Evennia provides ample tools for tagging, searching and finding things from all over your game.
|
||||
|
||||
Now that we know how to find the 'mirror' object, we just need to use that instead of `me`!
|
||||
|
||||
> py mirror = self.search("mirror") ; mirror.msg("Mirror, Mirror on the wall ...")
|
||||
mirror echoes back to you:
|
||||
"Mirror, Mirror on the wall ..."
|
||||
|
||||
The mirror is useful for testing because its `.msg` method just echoes whatever is sent to it back to the room. More common
|
||||
would be to talk to a player character, in which case the text you sent would have appeared in their game client.
|
||||
|
||||
|
||||
## Multi-line py
|
||||
|
||||
So far we have use `py` in single-line mode, using `;` to separate multiple inputs. This is very convenient
|
||||
when you want to do some quick testing. But you can also start a full multi-line Python interactive interpreter
|
||||
inside Evennia.
|
||||
|
||||
> py
|
||||
Evennia Interactive Python mode
|
||||
Python 3.7.1 (default, Oct 22 2018, 11:21:55)
|
||||
[GCC 8.2.0] on Linux
|
||||
[py mode - quit() to exit]
|
||||
|
||||
(the details of the output will vary with your Python version and OS). You are now in python interpreter mode. It means
|
||||
that _everything_ you insert from now on will become a line of Python (you can no longer look around or do other
|
||||
commands).
|
||||
|
||||
> print("Hello World")
|
||||
|
||||
>>> print("Hello World")
|
||||
Hello World
|
||||
[py mode - quit() to exit]
|
||||
|
||||
Note that we didn't need to put `py` in front now. The system will also echo your input (that's the bit after
|
||||
the `>>>`). For brevity in this tutorual we'll turn the echo off. First exit `py` and then start again with the
|
||||
`/noecho` flag.
|
||||
|
||||
> quit()
|
||||
Closing the Python console.
|
||||
> py/noecho
|
||||
Evennia Interactive Python mode (no echoing of prompts)
|
||||
Python 3.7.1 (default, Oct 22 2018, 11:21:55)
|
||||
[GCC 8.2.0] on Linux
|
||||
[py mode - quit() to exit]
|
||||
|
||||
```{sidebar} interactive py
|
||||
|
||||
- Start with `py`.
|
||||
- Use `py/noecho` if you don't want your input to be echoed for every line.
|
||||
- All your inputs will now be interpreted as Python code.
|
||||
- Exit with `quit()`.
|
||||
```
|
||||
|
||||
We can now enter multi-line Python code:
|
||||
|
||||
> a = "Test"
|
||||
> print(f"This is a {a}."}
|
||||
This is a Test.
|
||||
|
||||
Let's try to define a function:
|
||||
|
||||
> def hello_world(who, txt):
|
||||
...
|
||||
> who.msg(txt)
|
||||
...
|
||||
>
|
||||
[py mode - quit() to exit]
|
||||
|
||||
Some important things above:
|
||||
|
||||
- Definining a function with `def` means we are starting a new code block. Python works so that you mark the content
|
||||
of the block with indention. So the next line must be manually indented (4 spaces is a good standard) in order
|
||||
for Python to know it's part of the function body.
|
||||
- We expand the `hello_world` function with another argument `txt`. This allows us to send any text, not just
|
||||
"Hello World" over and over.
|
||||
- To tell `py` that no more lines will be added to the function body, we end with an empty input. When
|
||||
the normal prompt on how to exit returns, we know we are done.
|
||||
|
||||
Now we have defined a new function. Let's try it out:
|
||||
|
||||
> hello_world(me, "Hello world to me!")
|
||||
Hello world to me!
|
||||
|
||||
The `me` is still available to us, so we pass that as the `who` argument, along with a little longer
|
||||
string. Let's combine this with searching for the mirror.
|
||||
|
||||
> mirror = me.search("mirror")
|
||||
> hello_world(mirror, "Mirror, Mirror on the wall ...")
|
||||
mirror echoes back to you:
|
||||
"Mirror, Mirror on the wall ..."
|
||||
|
||||
Exit the `py` mode with
|
||||
|
||||
> quit()
|
||||
Closing the Python console.
|
||||
|
||||
## Other ways to test Python code
|
||||
|
||||
The `py` command is very powerful for experimenting with Python in-game. It's great for quick testing.
|
||||
But you are still limited to working over telnet or the webclient, interfaces that doesn't know anything
|
||||
about Python per-se.
|
||||
|
||||
Outside the game, go to the terminal where you ran Evennia (or any terminal where the `evennia` command
|
||||
is available).
|
||||
|
||||
- `cd` to your game dir.
|
||||
- `evennia shell`
|
||||
|
||||
A Python shell opens. This works like `py` did inside the game, with the exception that you don't have
|
||||
`me` available out of the box. If you want `me`, you need to first find yourself:
|
||||
|
||||
> import evennia
|
||||
> me = evennia.search_object("YourChar")[0]
|
||||
|
||||
Here we make use of one of evennia's search functions, available by importing `evennia` directly.
|
||||
We will cover more advanced searching later, but suffice to say, you put your own character name instead of
|
||||
"YourChar" above.
|
||||
|
||||
> The `[0]` at the end is because `.search_object` returns a list of objects and we want to
|
||||
get at the first of them (counting starts from 0).
|
||||
|
||||
Use `Ctrl-D` (`Cmd-D` on Mac) or `quit()` to exit the Python console.
|
||||
|
||||
## ipython
|
||||
|
||||
The default Python shell is quite limited and ugly. It's *highly* recommended to install `ipython` instead. This
|
||||
is a much nicer, third-party Python interpreter with colors and many usability improvements.
|
||||
|
||||
pip install ipython
|
||||
|
||||
If `ipython` is installed, `evennia shell` will use it automatically.
|
||||
|
||||
evennia shell
|
||||
...
|
||||
IPython 7.4.0 -- An enhanced Interactive Python. Type '?' for help
|
||||
In [1]: You now have Tab-completion:
|
||||
|
||||
> import evennia
|
||||
> evennia.<TAB>
|
||||
|
||||
That is, enter `evennia.` and then press the TAB key - you will be given a list of all the resources
|
||||
available on the `evennia` object. This is great for exploring what Evennia has to offer. For example,
|
||||
use your arrow keys to scroll to `search_object()` to fill it in.
|
||||
|
||||
> evennia.search_object?
|
||||
|
||||
Adding a `?` and pressing return will give you the full documentation for `.search_object`. Use `??` if you
|
||||
want to see the entire source code.
|
||||
|
||||
As for the normal python interpreter, use `Ctrl-D`/`Cmd-D` or `quit()` to exit ipython.
|
||||
|
||||
```{important} Persistent code
|
||||
|
||||
Common for both `py` and `python`/`ipython` is that the code you write is not persistent - it will
|
||||
be gone after you shut down the interpreter (but ipython will remember your input history). For making long-lasting
|
||||
Python code, we need to save it in a Python module, like we did for `world/test.py`.
|
||||
```
|
||||
|
||||
|
||||
## Conclusions
|
||||
|
||||
This covers quite a lot of basic Python usage. We printed and formatted strings, defined our own
|
||||
first function, fixed an error and even searched and talked to a mirror! Being able to access
|
||||
python inside and outside of the game is an important skill for testing and debugging, but in
|
||||
practice you will be writing most your code in Python modules.
|
||||
|
||||
To that end we also created a first new Python module in the `mygame/` game dir, then imported and used it.
|
||||
Now let's look at the rest of the stuff you've got going on inside that `mygame/` folder ...
|
||||
|
|
@ -0,0 +1,415 @@
|
|||
# Introduction to Python classes and objects
|
||||
|
||||
We have now learned how to run some simple Python code from inside (and outside) your game server.
|
||||
We have also taken a look at what our game dir looks and what is where. Now we'll start to use it.
|
||||
|
||||
## Importing things
|
||||
|
||||
No one writes something as big as an online game in one single huge file. Instead one breaks up the
|
||||
code into separate files (modules). Each module is dedicated to different purposes. Not only does
|
||||
it make things cleaner, organized and easier to understand. It also makes it easier to re-use code -
|
||||
you just import the resources you need and know you only get just what you requested. This makes
|
||||
it much easier to find errors and to know what code is good and which has issues.
|
||||
|
||||
> Evennia itself uses your code in the same way - you just tell it where a particular type of code is,
|
||||
and it will import and use it (often instead of its defaults).
|
||||
|
||||
We have already successfully imported things, for example:
|
||||
|
||||
> py import world.test ; world.test.hello_world(me)
|
||||
Hello World!
|
||||
|
||||
In this example, on your hard drive, the files looks like this:
|
||||
|
||||
```
|
||||
mygame/
|
||||
world/
|
||||
test.py <- inside this file is a function hello_world
|
||||
|
||||
```
|
||||
If you followed earlier tutorial lessons, the `mygame/world/test.py` file should look like this (if
|
||||
not, make it so):
|
||||
|
||||
```python
|
||||
def hello_world(who):
|
||||
who.msg("Hello World!")
|
||||
```
|
||||
|
||||
```{eval-rst}
|
||||
|
||||
.. sidebar:: Remember:
|
||||
|
||||
- Indentation matters in Python
|
||||
- So does capitalization
|
||||
- Use 4 `spaces` to indent, not tabs
|
||||
- Empty lines are fine
|
||||
- Anything on a line after a `#` is a `comment`, ignored by Python
|
||||
```
|
||||
|
||||
The _python_path_ describes the relation between Python resources, both between and inside
|
||||
Python _modules_ (that is, files ending with .py). A python-path separates each part of the
|
||||
path `.` and always skips the `.py` file endings. Also, Evennia already knows to start looking
|
||||
for python resources inside `mygame/` so this should never be specified. Hence
|
||||
|
||||
import world.test
|
||||
|
||||
The `import` Python instruction loads `world.test` so you have it available. You can now go "into"
|
||||
this module to get to the function you want:
|
||||
|
||||
world.test.hello_world(me)
|
||||
|
||||
Using `import` like this means that you have to specify the full `world.test` every time you want
|
||||
to get to your function. Here's a more powerful form of import:
|
||||
|
||||
from world.test import hello_world
|
||||
|
||||
The `from ... import ...` is very, very common as long as you want to get something with a longer
|
||||
python path. It imports `hello_world` directly, so you can use it right away!
|
||||
|
||||
> py from world.test import hello_world ; hello_world(me)
|
||||
Hello World!
|
||||
|
||||
Let's say your `test.py` module had a bunch of interesting functions. You could then import them
|
||||
all one by one:
|
||||
|
||||
from world.test import hello_world, my_func, awesome_func
|
||||
|
||||
If there were _a lot_ of functions, you could instead just import `test` and get the function
|
||||
from there when you need (without having to give the full `world.test` every time):
|
||||
|
||||
> from world import test ; test.hello_world(me
|
||||
Hello World!
|
||||
|
||||
You can also _rename_ stuff you import. Say for example that the module you import to already
|
||||
has a function `hello_world` but we also want to use the one from `world/test.py`:
|
||||
|
||||
from world.test import hello_world as test_hello_world
|
||||
|
||||
The form `from ... import ... as ...` renames the import.
|
||||
|
||||
> from world.test import hello_world as hw ; hw(me)
|
||||
Hello World!
|
||||
|
||||
> Avoid renaming unless it's to avoid a name-collistion like above - you want to make things as
|
||||
> easy to read as possible, and renaming adds another layer of potential confusion.
|
||||
|
||||
In [the basic intro to Python](./Python-basic-introduction.md) we learned how to open the in-game
|
||||
multi-line interpreter.
|
||||
|
||||
> py
|
||||
Evennia Interactive Python mode
|
||||
Python 3.7.1 (default, Oct 22 2018, 11:21:55)
|
||||
[GCC 8.2.0] on Linux
|
||||
[py mode - quit() to exit]
|
||||
|
||||
You now only need to import once to use the imported function over and over.
|
||||
|
||||
> from world.test import hello_world
|
||||
> hello_world()
|
||||
Hello World!
|
||||
> hello_world()
|
||||
Hello World!
|
||||
> hello_world()
|
||||
Hello World!
|
||||
> quit()
|
||||
Closing the Python console.
|
||||
|
||||
The same goes when writing code in a module - in most Python modules you will see a bunch of
|
||||
imports at the top, resources that are then used by all code in that module.
|
||||
|
||||
## On classes and objects
|
||||
|
||||
Now that we know about imports, let look at a real Evennia module and try to understand it.
|
||||
|
||||
Open `mygame/typeclasses/objects.py` in your text editor of choice.
|
||||
|
||||
```python
|
||||
"""
|
||||
module docstring
|
||||
"""
|
||||
from evennia import DefaultObject
|
||||
|
||||
class Object(DefaultObject):
|
||||
"""
|
||||
class docstring
|
||||
"""
|
||||
pass
|
||||
```
|
||||
|
||||
```{sidebar} Docstrings vs Comments
|
||||
|
||||
A docstring is not the same as a comment (created by `#`). A
|
||||
docstring is not ignored by Python but is an integral part of the thing
|
||||
it is documenting (the module and the class in this case).
|
||||
```
|
||||
The real file is much longer but we can ignore the multi-line strings (`""" ... """`). These serve
|
||||
as documentation-strings, or _docstrings_ for the module (at the top) and the `class` below.
|
||||
|
||||
Below the module doc string we have the import. In this case we are importing a resource
|
||||
from the core `evennia` library itself. We will dive into this later, for now we just treat this
|
||||
as a black box.
|
||||
|
||||
Next we have a `class` named `Object`, which _inherits_ from `DefaultObject`. This class doesn't
|
||||
actually do anything on its own, its only code (except the docstring) is `pass` which means,
|
||||
well, to pass and don't do anything.
|
||||
|
||||
We will get back to this module in the [next lesson](./Learning-Typeclasses.md). First we need to do a
|
||||
little detour to understand what a 'class', an 'object' or 'instance' is. These are fundamental
|
||||
things to understand before you can use Evennia efficiently.
|
||||
```{sidebar} OOP
|
||||
|
||||
Classes, objects, instances and inheritance are fundamental to Python. This and some
|
||||
other concepts are often clumped together under the term Object-Oriented-Programming (OOP).
|
||||
```
|
||||
|
||||
### Classes and instances
|
||||
|
||||
A 'class' can be seen as a 'template' for a 'type' of object. The class describes the basic functionality
|
||||
of everyone of that class. For example, we could have a class `Monster` which has resources for moving itself
|
||||
from room to room.
|
||||
|
||||
Open a new file `mygame/typeclasses/monsters.py`. Add the following simple class:
|
||||
|
||||
```python
|
||||
|
||||
class Monster:
|
||||
|
||||
key = "Monster"
|
||||
|
||||
def move_around(self):
|
||||
print(f"{self.key} is moving!")
|
||||
|
||||
```
|
||||
|
||||
Above we have defined a `Monster` class with one variable `key` (that is, the name) and one
|
||||
_method_ on it. A method is like a function except it sits "on" the class. It also always has
|
||||
at least one argument (almost always written as `self` although you could in principle use
|
||||
another name), which is a reference back to itself. So when we print `self.key` we are referring
|
||||
back to the `key` on the class.
|
||||
|
||||
```{eval-rst}
|
||||
.. sidebar:: Terms
|
||||
|
||||
- A `class` is a code template describing a 'type' of something
|
||||
- An `object` is an `instance` of a `class`. Like using a mold to cast tin soldiers, one class can be `instantiated` into any number of object-instances.
|
||||
|
||||
```
|
||||
A class is just a template. Before it can be used, we must create an _instance_ of the class. If
|
||||
`Monster` is a class, then an instance is Fluffy, the individual red dragon. You instantiate
|
||||
by _calling_ the class, much like you would a function:
|
||||
|
||||
fluffy = Monster()
|
||||
|
||||
Let's try it in-game (we use multi-line mode, it's easier)
|
||||
|
||||
> py
|
||||
> from typeclasses.monsters import Monster
|
||||
> fluffy = Monster()
|
||||
> fluffy.move_around()
|
||||
Monster is moving!
|
||||
|
||||
We created an _instance_ of `Monster`, which we stored in the variable `fluffy`. We then
|
||||
called the `move_around` method on fluffy to get the printout.
|
||||
|
||||
> Note how we _didn't_ call the method as `fluffy.move_around(self)`. While the `self` has to be
|
||||
> there when defining the method, we _never_ add it explicitly when we call the method (Python
|
||||
> will add the correct `self` for us automatically behind the scenes).
|
||||
|
||||
Let's create the sibling of Fluffy, Cuddly:
|
||||
|
||||
> cuddly = Monster()
|
||||
> cuddly.move_around()
|
||||
Monster is moving!
|
||||
|
||||
We now have two dragons and they'll hang around until with call `quit()` to exit this Python
|
||||
instance. We can have them move as many times as we want. But no matter how many dragons we
|
||||
create, they will all show the same printout since `key` is always fixed as "Monster".
|
||||
|
||||
Let's make the class a little more flexible:
|
||||
|
||||
```python
|
||||
|
||||
class Monster:
|
||||
|
||||
def __init__(self, key):
|
||||
self.key = key
|
||||
|
||||
def move_around(self):
|
||||
print(f"{self.key} is moving!")
|
||||
|
||||
```
|
||||
|
||||
The `__init__` is a special method that Python recognizes. If given, this handles extra arguments
|
||||
when you instantiate a new Monster. We have it add an argument `key` that we store on `self`.
|
||||
|
||||
Now, for Evennia to see this code change, we need to reload the server. You can either do it this
|
||||
way:
|
||||
|
||||
> quit()
|
||||
Python Console is closing.
|
||||
> reload
|
||||
|
||||
Or you can use a separate terminal and restart from outside the game:
|
||||
```{sidebar} On reloading
|
||||
|
||||
Reloading with the python mode gets a little annoying since you need to redo everything
|
||||
after every reload. Just keep in mind that during regular development you will not be
|
||||
working this way. The in-game python mode is practical for quick fixes and experiments like
|
||||
this, but actual code is normally written externally, in python modules.
|
||||
```
|
||||
|
||||
$ evennia reload (or restart)
|
||||
|
||||
Either way you'll need to go into `py` again:
|
||||
|
||||
> py
|
||||
> from typeclasses.monsters import Monster
|
||||
fluffy = Monster("Fluffy")
|
||||
fluffy.move_around()
|
||||
Fluffy is moving!
|
||||
|
||||
Now we passed `"Fluffy"` as an argument to the class. This went into `__init__` and set `self.key`, which we
|
||||
later used to print with the right name! Again, note that we didn't include `self` when calling.
|
||||
|
||||
### What's so good about objects?
|
||||
|
||||
So far all we've seen a class do is to behave our first `hello_world` function but more complex. We
|
||||
could just have made a function:
|
||||
|
||||
```python
|
||||
def monster_move_around(key):
|
||||
print(f"{key} is moving!")
|
||||
```
|
||||
|
||||
The difference between the function and an instance of a class (the object), is that the
|
||||
object retains _state_. Once you called the function it forgets everything about what you called
|
||||
it with last time. The object, on the other hand, remembers changes:
|
||||
|
||||
> fluffy.key = "Cuddly"
|
||||
> fluffy.move_around()
|
||||
Cuddly is moving!
|
||||
|
||||
The `fluffy` object's `key` was changed to "Cuddly" for as long as it's around. This makes objects
|
||||
extremely useful for representing and remembering collections of data - some of which can be other
|
||||
objects in turn:
|
||||
|
||||
- A player character with all its stats
|
||||
- A monster with HP
|
||||
- A chest with a number of gold coins in it
|
||||
- A room with other objects inside it
|
||||
- The current policy positions of a political party
|
||||
- A rule with methods for resolving challenges or roll dice
|
||||
- A multi-dimenstional data-point for a complex economic simulation
|
||||
- And so much more!
|
||||
|
||||
### Classes can have children
|
||||
|
||||
Classes can _inherit_ from each other. A "child" class will inherit everything from its "parent" class. But if
|
||||
the child adds something with the same name as its parent, it will _override_ whatever it got from its parent.
|
||||
|
||||
Let's expand `mygame/typeclasses/monsters.py` with another class:
|
||||
|
||||
```python
|
||||
|
||||
class Monster:
|
||||
"""
|
||||
This is a base class for Monster.
|
||||
"""
|
||||
|
||||
def __init__(self, key):
|
||||
self.key = key
|
||||
|
||||
def move_around(self):
|
||||
print(f"{self.key} is moving!")
|
||||
|
||||
|
||||
class Dragon(Monster):
|
||||
"""
|
||||
This is a dragon-specific monster.
|
||||
"""
|
||||
|
||||
def move_around(self):
|
||||
print(f"{self.key} flies through the air high above!")
|
||||
|
||||
def firebreath(self):
|
||||
"""
|
||||
Let our dragon breathe fire.
|
||||
"""
|
||||
print(f"{self.key} breathes fire!")
|
||||
|
||||
```
|
||||
|
||||
We added some docstrings for clarity. It's always a good idea to add doc strings; you can do so also for methods,
|
||||
as exemplified for the new `firebreath` method.
|
||||
|
||||
We created the new class `Dragon` but we also specified that `Monster` is the _parent_ of `Dragon` but adding
|
||||
the parent in parenthesis. `class Classname(Parent)` is the way to do this.
|
||||
|
||||
```{sidebar} Multi-inheritance
|
||||
|
||||
It's possible to add more comma-separated parents to a class. You should usually avoid
|
||||
this until you `really` know what you are doing. A single parent will be enough for almost
|
||||
every case you'll need.
|
||||
|
||||
```
|
||||
|
||||
Let's try out our new class. First `reload` the server and the do
|
||||
|
||||
> py
|
||||
> from typeclasses.monsters import Dragon
|
||||
> smaug = Dragon("Smaug")
|
||||
> smaug.move_around()
|
||||
Smaug flies through the air high above!
|
||||
> smaug.firebreath()
|
||||
Smaug breathes fire!
|
||||
|
||||
Because we didn't implement `__init__` in `Dragon`, we got the one from `Monster` instead. But since we
|
||||
implemented our own `move_around` in `Dragon`, it _overrides_ the one in `Monster`. And `firebreath` is only
|
||||
available for `Dragon`s of course. Having that on `Monster` would not have made much sense, since not every monster
|
||||
can breathe fire.
|
||||
|
||||
One can also force a class to use resources from the parent even if you are overriding some of it. This is done
|
||||
with the `super()` method. Modify your `Dragon` class as follows:
|
||||
|
||||
|
||||
```python
|
||||
# ...
|
||||
|
||||
class Dragon(Monster):
|
||||
|
||||
def move_around(self):
|
||||
super().move_around()
|
||||
print("The world trembles.")
|
||||
|
||||
# ...
|
||||
```
|
||||
> Keep `Monster` and the `firebreath` method, `# ...` indicates the rest of the code is untouched.
|
||||
>
|
||||
|
||||
The `super().move_around()` line means that we are calling `move_around()` on the parent of the class. So in this
|
||||
case, we will call `Monster.move_around` first, before doing our own thing.
|
||||
|
||||
Now `reload` the server and then:
|
||||
|
||||
> py
|
||||
> from typeclasses.monsters import Dragon
|
||||
> smaug = Dragon("Smaug")
|
||||
> smaug.move_around()
|
||||
Smaug is moving!
|
||||
The world trembles.
|
||||
|
||||
We can see that `Monster.move_around()` is calls first and prints "Smaug is moving!", followed by the extra bit
|
||||
about the trembling world we added in the `Dragon` class.
|
||||
|
||||
Inheritance is very powerful because it allows you to organize and re-use code while only adding the special things
|
||||
you want to change. Evennia uses this concept a lot.
|
||||
|
||||
## Summary
|
||||
|
||||
We have created our first dragons from classes. We have learned a little about how you _instantiate_ a class
|
||||
into an _object_. We have seen some examples of _inheritance_ and we tested to _override_ a method in the parent
|
||||
with one in the child class. We also used `super()` to good effect.
|
||||
|
||||
We have used pretty much raw Python so far. In the coming lessons we'll start to look at the extra bits that Evennia
|
||||
provides. But first we need to learn just where to find everything.
|
||||
|
||||
261
docs/source/Howtos/Beginner-Tutorial/Part1/Searching-Things.md
Normal file
261
docs/source/Howtos/Beginner-Tutorial/Part1/Searching-Things.md
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
# Searching for things
|
||||
|
||||
We have gone through how to create the various entities in Evennia. But creating something is of little use
|
||||
if we cannot find and use it afterwards.
|
||||
|
||||
## Main search functions
|
||||
|
||||
The base tools are the `evennia.search_*` functions, such as `evennia.search_object`.
|
||||
|
||||
rose = evennia.search_object(key="rose")
|
||||
acct = evennia.search_account(key="MyAccountName", email="foo@bar.com")
|
||||
|
||||
```{sidebar} Querysets
|
||||
|
||||
What is returned from the main search functions is actually a `queryset`. They can be
|
||||
treated like lists except that they can't modified in-place. We'll discuss querysets in
|
||||
the `next lesson` <Django-queries>`_.
|
||||
```
|
||||
|
||||
Strings are always case-insensitive, so searching for `"rose"`, `"Rose"` or `"rOsE"` give the same results.
|
||||
It's important to remember that what is returned from these search methods is a _listing_ of 0, one or more
|
||||
elements - all the matches to your search. To get the first match:
|
||||
|
||||
rose = rose[0]
|
||||
|
||||
Often you really want all matches to the search parameters you specify. In other situations, having zero or
|
||||
more than one match is a sign of a problem and you need to handle this case yourself.
|
||||
|
||||
the_one_ring = evennia.search_object(key="The one Ring")
|
||||
if not the_one_ring:
|
||||
# handle not finding the ring at all
|
||||
elif len(the_one_ring) > 1:
|
||||
# handle finding more than one ring
|
||||
else:
|
||||
# ok - exactly one ring found
|
||||
the_one_ring = the_one_ring[0]
|
||||
|
||||
There are equivalent search functions for all the main resources. You can find a listing of them
|
||||
[in the Search functions section](../../../Evennia-API.md) of the API frontpage.
|
||||
|
||||
## Searching using Object.search
|
||||
|
||||
On the `DefaultObject` is a `.search` method which we have already tried out when we made Commands. For
|
||||
this to be used you must already have an object available:
|
||||
|
||||
rose = obj.search("rose")
|
||||
|
||||
The `.search` method wraps `evennia.search_object` and handles its output in various ways.
|
||||
|
||||
- By default it will always search for objects among those in `obj.location.contents` and `obj.contents` (that is,
|
||||
things in obj's inventory or in the same room).
|
||||
- It will always return exactly one match. If it found zero or more than one match, the return is `None`.
|
||||
- On a no-match or multimatch, `.search` will automatically send an error message to `obj`.
|
||||
|
||||
So this method handles error messaging for you. A very common way to use it is in commands:
|
||||
|
||||
```python
|
||||
from evennia import Command
|
||||
|
||||
class MyCommand(Command):
|
||||
|
||||
key = "findfoo"
|
||||
|
||||
def func(self):
|
||||
|
||||
foo = self.caller.search("foo")
|
||||
if not foo:
|
||||
return
|
||||
```
|
||||
|
||||
Remember, `self.caller` is the one calling the command. This is usually a Character, which
|
||||
inherits from `DefaultObject`! This (rather stupid) Command searches for an object named "foo" in
|
||||
the same location. If it can't find it, `foo` will be `None`. The error has already been reported
|
||||
to `self.caller` so we just abort with `return`.
|
||||
|
||||
You can use `.search` to find anything, not just stuff in the same room:
|
||||
|
||||
volcano = self.caller.search("Volcano", global=True)
|
||||
|
||||
If you only want to search for a specific list of things, you can do so too:
|
||||
|
||||
stone = self.caller.search("MyStone", candidates=[obj1, obj2, obj3, obj4])
|
||||
|
||||
This will only return a match if MyStone is one of the four provided candidate objects. This is quite powerful,
|
||||
here's how you'd find something only in your inventory:
|
||||
|
||||
potion = self.caller.search("Healing potion", candidates=self.caller.contents)
|
||||
|
||||
You can also turn off the automatic error handling:
|
||||
|
||||
swords = self.caller.search("Sword", quiet=True)
|
||||
|
||||
With `quiet=True` the user will not be notified on zero or multi-match errors. Instead you are expected to handle this
|
||||
yourself and what you get back is now a list of zero, one or more matches!
|
||||
|
||||
## What can be searched for
|
||||
|
||||
These are the main database entities one can search for:
|
||||
|
||||
- [Objects](../../../Components/Objects.md)
|
||||
- [Accounts](../../../Components/Accounts.md)
|
||||
- [Scripts](../../../Components/Scripts.md),
|
||||
- [Channels](../../../Components/Channels.md),
|
||||
- [Messages](../../../Components/Msg.md)
|
||||
- [Help Entries](../../../Components/Help-System.md).
|
||||
|
||||
Most of the time you'll likely spend your time searching for Objects and the occasional Accounts.
|
||||
|
||||
So to find an entity, what can be searched for?
|
||||
|
||||
### Search by key
|
||||
|
||||
The `key` is the name of the entity. Searching for this is always case-insensitive.
|
||||
|
||||
### Search by aliases
|
||||
|
||||
Objects and Accounts can have any number of aliases. When searching for `key` these will searched too,
|
||||
you can't easily search only for aliases.
|
||||
|
||||
rose.aliases.add("flower")
|
||||
|
||||
If the above `rose` has a `key` `"Rose"`, it can now also be found by searching for `flower`. In-game
|
||||
you can assign new aliases to things with the `alias` command.
|
||||
|
||||
### Search by location
|
||||
|
||||
Only Objects (things inheriting from `evennia.DefaultObject`) has a location. This is usually a room.
|
||||
The `Object.search` method will automatically limit it search by location, but it also works for the
|
||||
general search function. If we assume `room` is a particular Room instance,
|
||||
|
||||
chest = evennia.search_object("Treasure chest", location=room)
|
||||
|
||||
### Search by Tags
|
||||
|
||||
Think of a [Tag](../../../Components/Tags.md) as the label the airport puts on your luggage when flying.
|
||||
Everyone going on the same plane gets a tag grouping them together so the airport can know what should
|
||||
go to which plane. Entities in Evennia can be grouped in the same way. Any number of tags can be attached
|
||||
to each object.
|
||||
|
||||
rose.tags.add("flowers")
|
||||
daffodil.tags.add("flowers")
|
||||
tulip.tags.add("flowers")
|
||||
|
||||
You can now find all flowers using the `search_tag` function:
|
||||
|
||||
all_flowers = evennia.search_tag("flowers")
|
||||
|
||||
Tags can also have categories. By default this category is `None` which is also considered a category.
|
||||
|
||||
silmarillion.tags.add("fantasy", category="books")
|
||||
ice_and_fire.tags.add("fantasy", category="books")
|
||||
mona_lisa_overdrive.tags.add("cyberpunk", category="books")
|
||||
|
||||
Note that if you specify the tag you _must_ also include its category, otherwise that category
|
||||
will be `None` and find no matches.
|
||||
|
||||
all_fantasy_books = evennia.search_tag("fantasy") # no matches!
|
||||
all_fantasy_books = evennia.search_tag("fantasy", category="books")
|
||||
|
||||
Only the second line above returns the two fantasy books. If we specify a category however,
|
||||
we can get all tagged entities within that category:
|
||||
|
||||
all_books = evennia.search_tag(category="books")
|
||||
|
||||
This gets all three books.
|
||||
|
||||
### Search by Attribute
|
||||
|
||||
We can also search by the [Attributes](../../../Components/Attributes.md) associated with entities.
|
||||
|
||||
For example, let's give our rose thorns:
|
||||
|
||||
rose.db.has_thorns = True
|
||||
wines.db.has_thorns = True
|
||||
daffodil.db.has_thorns = False
|
||||
|
||||
Now we can find things attribute and the value we want it to have:
|
||||
|
||||
is_ouch = evennia.search_object_attribute("has_thorns", True)
|
||||
|
||||
This returns the rose and the wines.
|
||||
|
||||
> Searching by Attribute can be very practical. But if you plan to do a search very often, searching
|
||||
> by-tag is generally faster.
|
||||
|
||||
|
||||
### Search by Typeclass
|
||||
|
||||
Sometimes it's useful to find all objects of a specific Typeclass. All of Evennia's search tools support this.
|
||||
|
||||
all_roses = evennia.search_object(typeclass="typeclasses.flowers.Rose")
|
||||
|
||||
If you have the `Rose` class already imported you can also pass it directly:
|
||||
|
||||
all_roses = evennia.search_object(typeclass=Rose)
|
||||
|
||||
You can also search using the typeclass itself:
|
||||
|
||||
all_roses = Rose.objects.all()
|
||||
|
||||
This last way of searching is a simple form of a Django _query_. This is a way to express SQL queries using
|
||||
Python.
|
||||
|
||||
### Search by dbref
|
||||
|
||||
The database id or `#dbref` is unique and never-reused within each database table. In search methods you can
|
||||
replace the search for `key` with the dbref to search for. This must be written as a string `#dbref`:
|
||||
|
||||
the_answer = self.caller.search("#42")
|
||||
eightball = evennia.search_object("#8")
|
||||
|
||||
Since `#dbref` is always unique, this search is always global.
|
||||
|
||||
```{warning} Relying on #dbrefs
|
||||
|
||||
You may be used to using #dbrefs a lot from other codebases. It is however considered
|
||||
`bad practice` in Evennia to rely on hard-coded #dbrefs. It makes your code hard to maintain
|
||||
and tied to the exact layout of the database. In 99% of cases you should pass the actual objects
|
||||
around and search by key/tags/attribute instead.
|
||||
```
|
||||
|
||||
## Finding objects relative each other
|
||||
|
||||
Let's consider a `chest` with a `coin` inside it. The chests stand in a room `dungeon`. In the dungeon is also
|
||||
a `door`. This is an exit leading outside.
|
||||
|
||||
- `coin.location` is `chest`.
|
||||
- `chest.location` is `dungeon`.
|
||||
- `door.location` is `dungeon`.
|
||||
- `room.location` is `None` since it's not inside something else.
|
||||
|
||||
One can use this to find what is inside what. For example, `coin.location.location` is the `room`.
|
||||
We can also find what is inside each object. This is a list of things.
|
||||
|
||||
- `room.contents` is `[chest, door]`
|
||||
- `chest.contents` is `[coin]`
|
||||
- `coin.contents` is `[]`, the empty list since there's nothing 'inside' the coin.
|
||||
- `door.contents` is `[]` too.
|
||||
|
||||
A convenient helper is `.contents_get` - this allows to restrict what is returned:
|
||||
|
||||
- `room.contents_get(exclude=chest)` - this returns everything in the room except the chest (maybe it's hidden?)
|
||||
|
||||
There is a special property for finding exits:
|
||||
|
||||
- `room.exits` is `[door]`
|
||||
- `coin.exits` is `[]` (same for all the other objects)
|
||||
|
||||
There is a property `.destination` which is only used by exits:
|
||||
|
||||
- `door.destination` is `outside` (or wherever the door leads)
|
||||
- `room.destination` is `None` (same for all the other non-exit objects)
|
||||
|
||||
## Summary
|
||||
|
||||
Knowing how to find things is important and the tools from this section will serve you well. For most of your needs
|
||||
these tools will be all you need ...
|
||||
|
||||
... but not always. In the next lesson we will dive further into more complex searching when we look at
|
||||
Django queries and querysets in earnest.
|
||||
|
||||
123
docs/source/Howtos/Beginner-Tutorial/Part1/Tutorial-World.md
Normal file
123
docs/source/Howtos/Beginner-Tutorial/Part1/Tutorial-World.md
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
# The Tutorial World
|
||||
|
||||
The *Tutorial World* is a small and functioning MUD-style game world shipped with Evennia.
|
||||
It's a small showcase of what is possible. It can also be useful for those who have an easier
|
||||
time learning by deconstructing existing code.
|
||||
|
||||
Stand in the Limbo room and install it with
|
||||
|
||||
batchcommand tutorial_world.build
|
||||
|
||||
What this does is to run the build script
|
||||
[evennia/contrib/tutorial_world/build.ev](github:evennia/contrib/tutorial_world/build.ev).
|
||||
This is pretty much just a list of build-commands executed in sequence by the `batchcommand` command.
|
||||
Wait for the building to complete and don't run it twice.
|
||||
|
||||
> After having run the batchcommand, the `intro` command also becomes available in Limbo. Try it out to
|
||||
> for in-game help and to get an example of [EvMenu](../../../Components/EvMenu.md), Evennia's in-built
|
||||
> menu generation system!
|
||||
|
||||
The game consists of a single-player quest and has some 20 rooms that you can explore as you seek
|
||||
to discover the whereabouts of a mythical weapon.
|
||||
|
||||
A new exit should have appeared named _Tutorial_. Enter by writing `tutorial`.
|
||||
|
||||
You will automatically `quell` when you enter (and `unquell` when you leave), so you can play the way it was intended.
|
||||
Both if you are triumphant or if you use the `give up` command you will eventually end up back in Limbo.
|
||||
|
||||
```{important}
|
||||
Only LOSERS and QUITTERS use the `give up` command.
|
||||
```
|
||||
|
||||
## Gameplay
|
||||
|
||||

|
||||
|
||||
*To get into the mood of this miniature quest, imagine you are an adventurer out to find fame and
|
||||
fortune. You have heard rumours of an old castle ruin by the coast. In its depth a warrior princess
|
||||
was buried together with her powerful magical weapon - a valuable prize, if it's true. Of course
|
||||
this is a chance to adventure that you cannot turn down!*
|
||||
|
||||
*You reach the ocean in the midst of a raging thunderstorm. With wind and rain screaming in your
|
||||
face you stand where the moor meets the sea along a high, rocky coast ...*
|
||||
|
||||
---
|
||||
|
||||
### Gameplay hints
|
||||
|
||||
- Use the command `tutorial` to get code insight behind the scenes of every room.
|
||||
- Look at everything. While a demo, the Tutorial World is not necessarily trivial to solve - it depends
|
||||
on your experience with text-based adventure games. Just remember that everything can be solved or bypassed.
|
||||
- Some objects are interactive in more than one way. Use the normal `help` command to get a feel for
|
||||
which commands are available at any given time.
|
||||
- In order to fight, you need to first find some type of weapon.
|
||||
- *slash* is a normal attack
|
||||
- *stab* launches an attack that makes more damage but has a lower chance to hit.
|
||||
- *defend* will lower the chance to taking damage on your enemy's next attack.
|
||||
- Some things _cannot_ be hurt by mundane weapons. In that case it's OK to run away. Expect
|
||||
to be chased though.
|
||||
- Being defeated is a part of the experience. You can't actually die, but getting knocked out
|
||||
means being left in the dark ...
|
||||
|
||||
## Once you are done (or had enough)
|
||||
|
||||
Afterwards you'll either have conquered the old ruin and returned in glory and triumph ... or
|
||||
you returned limping and whimpering from the challenge by using the `give up` command.
|
||||
Either way you should now be back in Limbo, able to reflect on the experience.
|
||||
|
||||
Some features exemplified by the tutorial world:
|
||||
|
||||
- Rooms with custom ability to show details (like looking at the wall in the dark room)
|
||||
- Hidden or impassable exits until you fulfilled some criterion
|
||||
- Objects with multiple custom interactions (like swords, the well, the obelisk ...)
|
||||
- Large-area rooms (that bridge is actually only one room!)
|
||||
- Outdoor weather rooms with weather (the rain pummeling you)
|
||||
- Dark room, needing light source to reveal itself (the burning splinter even burns out after a while)
|
||||
- Puzzle object (the wines in the dark cell; hope you didn't get stuck!)
|
||||
- Multi-room puzzle (the obelisk and the crypt)
|
||||
- Aggressive mobile with roam, pursue and battle state-engine AI (quite deadly until you find the right weapon)
|
||||
- Weapons, also used by mobs (most are admittedly not that useful against the big baddie)
|
||||
- Simple combat system with attack/defend commands (teleporting on-defeat)
|
||||
- Object spawning (the weapons in the barrel and the final weapoon is actually randomized)
|
||||
- Teleporter trap rooms (if you fail the obelisk puzzle)
|
||||
|
||||
```{sidebar} Extra Credit
|
||||
|
||||
If you have previous programming experience (or after you have gone
|
||||
through this Starter tutorial) it may be instructive to dig a little deeper into the Tutorial-world
|
||||
code to learn how it achieves what it does. The code is heavily documented.
|
||||
You can find all the code in [evennia/contrib/tutorials/tutorial_world](evennia.contrib.tutorials.tutorial_world).
|
||||
The build-script is [here](github:evennia/contrib/tutorials/tutorial_world/build.ev).
|
||||
|
||||
|
||||
When reading the code, remember that the Tutorial World was designed to install easily and to not permanently modify
|
||||
the rest of the game. It therefore makes sure to only use temporary solutions and to clean up after itself. This is
|
||||
not something you will often need to worry about when making your own game.
|
||||
```
|
||||
|
||||
Quite a lot of stuff crammed in such a small area!
|
||||
|
||||
## Uninstall the tutorial world
|
||||
|
||||
Once are done playing with the tutorial world, let's uninstall it.
|
||||
Uninstalling the tutorial world basically means deleting all the rooms and objects it consists of.
|
||||
Make sure you are back in Limbo, then
|
||||
|
||||
find tut#01
|
||||
find tut#16
|
||||
|
||||
This should locate the first and last rooms created by `build.ev` - *Intro* and *Outro*. If you
|
||||
installed normally, everything created between these two numbers should be part of the tutorial.
|
||||
Note their #dbref numbers, for example 5 and 80. Next we just delete all objects in that range:
|
||||
|
||||
del 5-80
|
||||
|
||||
You will see some errors since some objects are auto-deleted and so cannot be found when the delete
|
||||
mechanism gets to them. That's fine. You should have removed the tutorial completely once the
|
||||
command finishes.
|
||||
|
||||
Even if the game-style of the Tutorial-world was not similar to the one you are interested in, it
|
||||
should hopefully have given you a little taste of some of the possibilities of Evennia. Now we'll
|
||||
move on with how to access this power through code.
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
# Part 2: What we want
|
||||
|
||||
```{eval-rst}
|
||||
.. sidebar:: Beginner Tutorial Parts
|
||||
|
||||
`Introduction <../Beginner-Tutorial-Intro.html>`_
|
||||
Getting set up.
|
||||
Part 1: `What we have <../Part1/Beginner-Tutorial-Part1-Intro.html>`_
|
||||
A tour of Evennia and how to use the tools, including an introduction to Python.
|
||||
**Part 2: What we want**
|
||||
Planning our tutorial game and what to think about when planning your own in the future.
|
||||
Part 3: `How we get there <../Part3/Beginner-Tutorial-Part3-Intro.html>`_
|
||||
Getting down to the meat of extending Evennia to make our game
|
||||
Part 4: `Using what we created <../Part4/Beginner-Tutorial-Part4-Intro.html>`_
|
||||
Building a tech-demo and world content to go with our code
|
||||
Part 5: `Showing the world <../Part5/Beginner-Tutorial-Part5-Intro.html>`_
|
||||
Taking our new game online and let players try it out
|
||||
```
|
||||
|
||||
In Part two of the Beginner Tutorial we'll take a step back and plan out the kind of tutorial
|
||||
game we want to make. This is a more 'theoretical' part where we won't do any hands-on
|
||||
programming.
|
||||
|
||||
In the process we'll go through the common questions of "where to start"
|
||||
and "what to think about" when creating a multiplayer online text game.
|
||||
|
||||
## Lessons
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 1
|
||||
|
||||
Planning-Where-Do-I-Begin.md
|
||||
Game-Planning.md
|
||||
Planning-Some-Useful-Contribs.md
|
||||
Planning-The-Tutorial-Game.md
|
||||
```
|
||||
|
||||
## Table of Contents
|
||||
|
||||
```{toctree}
|
||||
|
||||
Planning-Where-Do-I-Begin.md
|
||||
Game-Planning.md
|
||||
Planning-Some-Useful-Contribs.md
|
||||
Planning-The-Tutorial-Game.md
|
||||
```
|
||||
209
docs/source/Howtos/Beginner-Tutorial/Part2/Game-Planning.md
Normal file
209
docs/source/Howtos/Beginner-Tutorial/Part2/Game-Planning.md
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
# On Planning a Game
|
||||
|
||||
Last lesson we asked ourselves some questions about our motivation. In this one we'll present
|
||||
some more technical questions to consider. In the next lesson we'll answer them for the sake of
|
||||
our tutorial game.
|
||||
|
||||
Note that the suggestions on this page are just that - suggestions. Also, they are primarily aimed at a lone
|
||||
hobby designer or a small team developing a game in their free time.
|
||||
|
||||
```{important}
|
||||
|
||||
Your first all overshadowing goal is to beat the odds and get **something** out the door!
|
||||
Even if it's a scaled-down version of your dream game, lacking many "must-have" features!
|
||||
|
||||
```
|
||||
|
||||
Remember: *99.99999% of all great game ideas never lead to a game*. Especially not to an online
|
||||
game that people can actually play and enjoy. It's better to get your game out there and expand on it
|
||||
later than to code in isolation until you burn out, lose interest or your hard drive crashes.
|
||||
|
||||
- Keep the scope of your initial release down. Way down.
|
||||
- Start small, with an eye towards expansions later, after first release.
|
||||
- If the suggestions here seems boring or a chore to you, do it your way instead. Everyone's different.
|
||||
- Keep having _fun_. You must keep your motivation up, whichever way works for _you_.
|
||||
|
||||
|
||||
## The steps
|
||||
|
||||
Here are the rough steps towards your goal.
|
||||
|
||||
1. Planning
|
||||
2. Coding + Gradually building a tech-demo
|
||||
3. Building the actual game world
|
||||
4. Release
|
||||
5. Celebrate
|
||||
|
||||
## Planning
|
||||
|
||||
You need to have at least a rough idea about what you want to create. Some like a lot of planning, others
|
||||
do it more seat-of-the-pants style. Regardless, while _some_ planning is always good to do, it's common
|
||||
to have your plans change on you as you create your code prototypes. So don't get _too_ bogged down in
|
||||
the details out of the gate.
|
||||
|
||||
Many prospective game developers are very good at *parts* of this process, namely in defining what their
|
||||
world is "about": The theme, the world concept, cool monsters and so on. Such things are very important. But
|
||||
unfortunately, they are not enough to make your game. You need to figure out how to accomplish your ideas in
|
||||
Evennia.
|
||||
|
||||
Below are some questions to get you going. In the next lesson we will try to answer them for our particular
|
||||
tutorial game. There are of course many more questions you could be asking yourself.
|
||||
|
||||
### Administration
|
||||
|
||||
- Should your game rules be enforced by coded systems or by human game masters?
|
||||
- What is the staff hierarchy in your game? Is vanilla Evennia roles enough or do you need something else?
|
||||
- Should players be able to post out-of-characters on channels and via other means like bulletin-boards?
|
||||
|
||||
### Building
|
||||
|
||||
- How will the world be built? Traditionally (from in-game with build-commands) or externally (by batchcmds/code
|
||||
or directly with custom code)?
|
||||
- Can only privileged Builders create things or should regular players also have limited build-capability?
|
||||
|
||||
### Systems
|
||||
|
||||
- Do you base your game off an existing RPG system or make up your own?
|
||||
- What are the game mechanics? How do you decide if an action succeeds or fails?
|
||||
- Does the flow of time matter in your game - does night and day change? What about seasons?
|
||||
- Do you want changing, global weather or should weather just be set manually in roleplay?
|
||||
- Do you want a coded world-economy or just a simple barter system? Or no formal economy at all?
|
||||
- Do you have concepts like reputation and influence?
|
||||
- Will your characters be known by their name or only by their physical appearance?
|
||||
|
||||
### Rooms
|
||||
|
||||
- Is a simple room description enough or should the description be able to change (such as with time, by
|
||||
light conditions, weather or season)?
|
||||
- Should the room have different statuses? Can it have smells, sounds? Can it be affected by
|
||||
dramatic weather, fire or magical effects? If so, how would this affect things in the room? Or are
|
||||
these things something admins/game masters should handle manually?
|
||||
- Can objects be hidden in the room? Can a person hide in the room? How does the room display this?
|
||||
|
||||
### Objects / items
|
||||
|
||||
- How numerous are your objects? Do you want large loot-lists or are objects just role playing props
|
||||
created on demand?
|
||||
- If you use money, is each coin a separate object or do you just store a bank account value?
|
||||
- Do multiple similar objects form stacks and how are those stacks handled in that case?
|
||||
- Does an object have weight or volume (so you cannot carry an infinite amount of them)?
|
||||
- Can objects be broken? Can they be repaired?
|
||||
- Can you fight with a chair or a flower or must you use a specific 'weapon' kind of thing?
|
||||
- Will characters be able to craft new objects?
|
||||
- Should mobs/NPCs have some sort of AI?
|
||||
- Are NPCs and mobs different entities? How do they differ?
|
||||
- Should there be NPCs giving quests? If so, how do you track Quest status?
|
||||
|
||||
### Characters
|
||||
|
||||
- Can players have more than one Character active at a time or are they allowed to multi-play?
|
||||
- How does the character-generation work? Walk from room-to-room? A menu?
|
||||
- How do you implement different "classes" or "races"? Are they separate types of objects or do you
|
||||
simply load different stats on a basic object depending on what the Player wants?
|
||||
- If a Character can hide in a room, what skill will decide if they are detected?
|
||||
- What does the skill tree look like? Can a Character gain experience to improve? By killing
|
||||
enemies? Solving quests? By roleplaying?
|
||||
- May player-characters attack each other (PvP)?
|
||||
- What are the penalties of defeat? Permanent death? Quick respawn? Time in prison?
|
||||
|
||||
A MUD's a lot more involved than you would think and these things hang together in a complex web. It
|
||||
can easily become overwhelming and it's tempting to want *all* functionality right out of the door.
|
||||
Try to identify the basic things that "make" your game and focus *only* on them for your first
|
||||
release. Make a list. Keep future expansions in mind but limit yourself.
|
||||
|
||||
## Coding and Tech demo
|
||||
|
||||
This is the actual work of creating the "game" part of your game. As you code and test systems you should
|
||||
build a little "tech demo" along the way.
|
||||
|
||||
```{sidebar} Tech demo
|
||||
|
||||
With "tech demo" we mean a small example of your code in-action: A room with a mob,
|
||||
a way to jump into and test character-creation etc. The tech demo need not be pretty, it's
|
||||
there to test functionality. It's not the beginning of your game world (unless you find that
|
||||
to be more fun).
|
||||
|
||||
```
|
||||
|
||||
Try to avoid going wild with building a huge game world before you have a tech-demo showing off all parts
|
||||
you expect to have in the first version of your game. Otherwise you run the risk of having to redo it all
|
||||
again.
|
||||
|
||||
Evennia tries hard to make the coding easier for you, but there is no way around the fact that if you want
|
||||
anything but a basic chat room you *will* have to bite the bullet and code your game (or find a coder willing
|
||||
to do it for you).
|
||||
|
||||
> Even if you won't code anything yourself, as a designer you need to at least understand the basic
|
||||
paradigms and components of Evennia. It's recommended you look over the rest of this Beginner Tutorial to learn
|
||||
what tools you have available.
|
||||
|
||||
During Coding you look back at the things you wanted during the **Planning** phase and try to
|
||||
implement them. Don't be shy to update your plans if you find things easier/harder than you thought.
|
||||
The earlier you revise problems, the easier they will be to fix.
|
||||
|
||||
A good idea is to host your code online using _version control_. Github.com offers free Private repos
|
||||
these days if you don't want the world to learn your secrets. Not only version control
|
||||
make it easy for your team to collaborate, it also means
|
||||
your work is backed up at all times. The page on [Version Control](../../../Coding/Version-Control.md)
|
||||
will help you to setting up a sane developer environment with proper version control.
|
||||
|
||||
## World Building
|
||||
|
||||
Up until this point we've only had a few tech-demo objects in the database. This step is the act of
|
||||
populating the database with a larger, thematic world. Too many would-be developers jump to this
|
||||
stage too soon (skipping the **Coding** or even **Planning** stages). What if the rooms you build
|
||||
now doesn't include all the nice weather messages the code grows to support? Or the way you store
|
||||
data changes under the hood? Your building work would at best require some rework and at worst you
|
||||
would have to redo the whole thing. You could be in for a *lot* of unnecessary work if you build stuff
|
||||
en masse without having the underlying code systems in some reasonable shape first.
|
||||
|
||||
So before starting to build, the "game" bit (**Coding** + **Testing**) should be more or less
|
||||
**complete**, *at least to the level of your initial release*.
|
||||
|
||||
Make sure it is clear to yourself and your eventual builders just which parts of the world you want
|
||||
for your initial release. Establish for everyone which style, quality and level of detail you expect.
|
||||
|
||||
Your goal should *not* be to complete your entire world in one go. You want just enough to make the
|
||||
game's "feel" come across. You want a minimal but functioning world where the intended game play can
|
||||
be tested and roughly balanced. You can always add new areas later.
|
||||
|
||||
During building you get free and extensive testing of whatever custom build commands and systems you
|
||||
have made at this point. If Builders and coders are different people you also
|
||||
get a chance to hear if some things are hard to understand or non-intuitive. Make sure to respond
|
||||
to this feedback.
|
||||
|
||||
|
||||
## Alpha Release
|
||||
|
||||
As mentioned, don't hold onto your world more than necessary. *Get it out there* with a huge *Alpha*
|
||||
flag and let people try it!
|
||||
|
||||
Call upon your alpha-players to try everything - they *will* find ways to break your game in ways that
|
||||
you never could have imagined. In Alpha you might be best off to
|
||||
focus on inviting friends and maybe other MUD developers, people who you can pester to give proper
|
||||
feedback and bug reports (there *will* be bugs, there is no way around it).
|
||||
|
||||
Follow the quick instructions for [Online Setup](../../../Setup/Online-Setup.md) to make your
|
||||
game visible online.
|
||||
|
||||
If you hadn't already, make sure to put up your game on the
|
||||
[Evennia game index](http://games.evennia.com/) so people know it's in the works (actually, even
|
||||
pre-alpha games are allowed in the index so don't be shy)!
|
||||
|
||||
## Beta Release/Perpetual Beta
|
||||
|
||||
Once things stabilize in Alpha you can move to *Beta* and let more people in. Many MUDs are in
|
||||
[perpetual beta](https://en.wikipedia.org/wiki/Perpetual_beta), meaning they are never considered
|
||||
"finished", but just repeat the cycle of Planning, Coding, Testing and Building over and over as new
|
||||
features get implemented or Players come with suggestions. As the game designer it is now up to you
|
||||
to gradually perfect your vision.
|
||||
|
||||
## Congratulate yourself!
|
||||
|
||||
You are worthy of a celebration since at this point you have joined the small, exclusive crowd who
|
||||
have made their dream game a reality!
|
||||
|
||||
## Planning our tutorial game
|
||||
|
||||
In the next lesson we'll make use of these general points and try to plan out our tutorial game.
|
||||
|
||||
|
|
@ -0,0 +1,244 @@
|
|||
# Planning the use of some useful contribs
|
||||
|
||||
Evennia is deliberately bare-bones out of the box. The idea is that you should be as unrestricted as possible
|
||||
in designing your game. This is why you can easily replace the few defaults we have and why we don't try to
|
||||
prescribe any major game systems on you.
|
||||
|
||||
That said, Evennia _does_ offer some more game-opinionated _optional_ stuff. These are referred to as _Contribs_
|
||||
and is an ever-growing treasure trove of code snippets, concepts and even full systems you can pick and choose
|
||||
from to use, tweak or take inspiration from when you make your game.
|
||||
|
||||
The [Contrib overview](../../../Contribs/Contribs-Overview.md) page gives the full list of the current roster of contributions. On
|
||||
this page we will review a few contribs we will make use of for our game. We will do the actual installation
|
||||
of them when we start coding in the next part of this tutorial series. While we will introduce them here, you
|
||||
are wise to read their doc-strings yourself for the details.
|
||||
|
||||
This is the things we know we need:
|
||||
|
||||
- A barter system
|
||||
- Character generation
|
||||
- Some concept of wearing armor
|
||||
- The ability to roll dice
|
||||
- Rooms with awareness of day, night and season
|
||||
- Roleplaying with short-descs, poses and emotes
|
||||
- Quests
|
||||
- Combat (with players and against monsters)
|
||||
|
||||
## Barter contrib
|
||||
|
||||
[source](../../../api/evennia.contrib.game_systems.barter.md)
|
||||
|
||||
Reviewing this contrib suggests that it allows for safe trading between two parties. The basic principle
|
||||
is that the parties puts up the stuff they want to sell and the system will guarantee that these systems are
|
||||
exactly what is being offered. Both sides can modify their offers (bartering) until both mark themselves happy
|
||||
with the deal. Only then the deal is sealed and the objects are exchanged automatically. Interestingly, this
|
||||
works just fine for money too - just put coin objects on one side of the transaction.
|
||||
|
||||
Sue > trade Tom: Hi, I have a necklace to sell; wanna trade for a healing potion?
|
||||
Tom > trade Sue: Hm, I could use a necklace ...
|
||||
<both accepted trade. Start trade>
|
||||
Sue > offer necklace: This necklace is really worth it.
|
||||
Tom > evaluate necklace:
|
||||
<Tom sees necklace stats>
|
||||
Tom > offer ration: I don't have a healing potion, but I'll trade you an iron ration!
|
||||
Sue > Hey, this is a nice necklace, I need more than a ration for it...
|
||||
Tom > offer ration, 10gold: Ok, a ration and 10 gold as well.
|
||||
Sue > accept: Ok, that sounds fair!
|
||||
Tom > accept: Good! Nice doing business with you.
|
||||
<goods change hands automatically. Trade ends>
|
||||
|
||||
Arguably, in a small game you are just fine to just talk to people and use `give` to do the exchange. The
|
||||
barter system guarantees trading safety if you don't trust your counterpart to try to give you the wrong thing or
|
||||
to run away with your money.
|
||||
|
||||
We will use the barter contrib as an optional feature for player-player bartering. More importantly we can
|
||||
add it for NPC shopkeepers and expand it with a little AI, which allows them to potentially trade in other
|
||||
things than boring gold coin.
|
||||
|
||||
## Clothing contrib
|
||||
|
||||
[source](../../../api/evennia.contrib.game_systems.clothing.md)
|
||||
|
||||
This contrib provides a full system primarily aimed at wearing clothes, but it could also work for armor. You wear
|
||||
an object in a particular location and this will then be reflected in your character's description. You can
|
||||
also add roleplaying flavor:
|
||||
|
||||
> wear helmet slightly askew on her head
|
||||
look self
|
||||
Username is wearing a helmet slightly askew on her head.
|
||||
|
||||
By default there are no 'body locations' in this contrib, we will need to expand on it a little to make it useful
|
||||
for things like armor. It's a good contrib to build from though, so that's what we'll do.
|
||||
|
||||
## Dice contrib
|
||||
|
||||
[source](../../../api/evennia.contrib.rpg.dice.md)
|
||||
|
||||
The dice contrib presents a general dice roller to use in game.
|
||||
|
||||
> roll 2d6
|
||||
Roll(s): 2 and 5. Total result is 7.
|
||||
> roll 1d100 + 2
|
||||
Roll(s): 43. Total result is 47
|
||||
> roll 1d20 > 12
|
||||
Roll(s): 7. Total result is 7. This is a failure (by 5)
|
||||
> roll/hidden 1d20 > 12
|
||||
Roll(s): 18. Total result is 17. This is a success (by 6). (not echoed)
|
||||
|
||||
The contrib also has a python function for producing these results in-code. However, while
|
||||
we will emulate rolls for our rule system, we'll do this as simply as possible with Python's `random`
|
||||
module.
|
||||
|
||||
So while this contrib is fun to have around for GMs or for players who want to get a random result
|
||||
or play a game, we will not need it for the core of our game.
|
||||
|
||||
## Extended room contrib
|
||||
|
||||
[source](../../../api/evennia.contrib.grid.extended_room.md)
|
||||
|
||||
This is a custom Room typeclass that changes its description based on time of day and season.
|
||||
|
||||
For example, at night, in wintertime you could show the room as being dark and frost-covered while in daylight
|
||||
at summer it could describe a flowering meadow. The description can also contain special markers, so
|
||||
`<morning> ... </morning>` would include text only visible at morning.
|
||||
|
||||
The extended room also supports _details_, which are things to "look at" in the room without there having
|
||||
to be a separate database object created for it. For example, a player in a church may do `look window` and
|
||||
get a description of the windows without there needing to be an actual `window` object in the room.
|
||||
|
||||
Adding all those extra descriptions can be a lot of work, so they are optional; if not given the room works
|
||||
like a normal room.
|
||||
|
||||
The contrib is simple to add and provides a lot of optional flexibility, so we'll add it to our
|
||||
game, why not!
|
||||
|
||||
## RP-System contrib
|
||||
|
||||
[source](../../../api/evennia.contrib.rpg.rpsystem.md)
|
||||
|
||||
This contrib adds a full roleplaying subsystem to your game. It gives every character a "short-description"
|
||||
(sdesc) that is what people will see when first meeting them. Let's say Tom has an sdesc "A tall man" and
|
||||
Sue has the sdesc "A muscular, blonde woman"
|
||||
|
||||
Tom > look
|
||||
Tom: <room desc> ... You see: A muscular, blonde woman
|
||||
Tom > emote /me smiles to /muscular.
|
||||
Tom: Tom smiles to A muscular, blonde woman.
|
||||
Sue: A tall man smiles to Sue.
|
||||
Tom > emote Leaning forward, /me says, "Well hello, what's yer name?"
|
||||
Tom: Leaning forward, Tom says, "Well hello..."
|
||||
Sue: Leaning forward, A tall man says, "Well hello, what's yer name?"
|
||||
Sue > emote /me grins. "I'm Angelica", she says.
|
||||
Sue: Sue grins. "I'm Angelica", she says.
|
||||
Tom: A muscular, blonde woman grins. "I'm Angelica", she says.
|
||||
Tom > recog muscular Angelica
|
||||
Tom > emote /me nods to /angelica: "I have a message for you ..."
|
||||
Tom: Tom nods to Angelica: "I have a message for you ..."
|
||||
Sue: A tall man nods to Sue: "I have a message for you ..."
|
||||
|
||||
Above, Sue introduces herself as "Angelica" and Tom uses this info to `recoc` her as "Angelica" hereafter. He
|
||||
could have `recoc`-ed her with whatever name he liked - it's only for his own benefit. There is no separate
|
||||
`say`, the spoken words are embedded in the emotes in quotes `"..."`.
|
||||
|
||||
The RPSystem module also includes options for `poses`, which help to establish your position in the room
|
||||
when others look at you.
|
||||
|
||||
Tom > pose stands by the bar, looking bored.
|
||||
Sue > look
|
||||
Sue: <room desc> ... A tall man stands by the bar, looking bored.
|
||||
|
||||
You can also wear a mask to hide your identity; your sdesc will then be changed to the sdesc of the mask,
|
||||
like `a person with a mask`.
|
||||
|
||||
The RPSystem gives a lot of roleplaying power out of the box, so we will add it. There is also a separate
|
||||
[rplanguage](../../../api/evennia.contrib.rpg.rpsystem.md) module that integrates with the spoken words in your emotes and garbles them if you don't understand
|
||||
the language spoken. In order to restrict the scope we will not include languages for the tutorial game.
|
||||
|
||||
|
||||
## Talking NPC contrib
|
||||
|
||||
[source](../../../api/evennia.contrib.tutorials.talking_npc.md)
|
||||
|
||||
This exemplifies an NPC with a menu-driven dialogue tree. We will not use this contrib explicitly, but it's
|
||||
good as inspiration for how we'll do quest-givers later.
|
||||
|
||||
## Traits contrib
|
||||
|
||||
[source](../../../api/evennia.contrib.rpg.traits.md)
|
||||
|
||||
An issue with dealing with roleplaying attributes like strength, dexterity, or skills like hunting, sword etc
|
||||
is how to keep track of the values in the moment. Your strength may temporarily be buffed by a strength-potion.
|
||||
Your swordmanship may be worse because you are encumbered. And when you drink your health potion you must make
|
||||
sure that those +20 health does not bring your health higher than its maximum. All this adds complexity.
|
||||
|
||||
The _Traits_ contrib consists of several types of objects to help track and manage values like this. When
|
||||
installed, the traits are accessed on a new handler `.traits`, for example
|
||||
|
||||
> py self.traits.hp.value
|
||||
100
|
||||
> py self.traits.hp -= 20 # getting hurt
|
||||
> py self.traits.hp.value
|
||||
80
|
||||
> py self.traits.hp.reset() # drink a potion
|
||||
> py self.traits.hp.value
|
||||
100
|
||||
|
||||
A Trait is persistent (it uses an Attribute under the hood) and tracks changes, min/max and other things
|
||||
automatically. They can also be added together in various mathematical operations.
|
||||
|
||||
The contrib introduces three main Trait-classes
|
||||
|
||||
- _Static_ traits for single values like str, dex, things that at most gets a modifier.
|
||||
- _Counters_ is a value that never moves outside a given range, even with modifiers. For example a skill
|
||||
that can at most get a maximum amount of buff. Counters can also easily be _timed_ so that they decrease
|
||||
or increase with a certain rate per second. This could be good for a time-limited curse for example.
|
||||
- _Gauge_ is like a fuel-gauge; it starts at a max value and then empties gradually. This is perfect for
|
||||
things like health, stamina and the like. Gauges can also change with a rate, which works well for the
|
||||
effects of slow poisons and healing both.
|
||||
|
||||
```
|
||||
> py self.traits.hp.value
|
||||
100
|
||||
> py self.traits.hp.rate = -1 # poisoned!
|
||||
> py self.traits.hp.ratetarget = 50 # stop at 50 hp
|
||||
# Wait 30s
|
||||
> py self.traits.hp.value
|
||||
70
|
||||
# Wait another 30s
|
||||
> py self.traits.hp.value
|
||||
50 # stopped at 50
|
||||
> py self.traits.hp.rate = 0 # no more poison
|
||||
> py self.traits.hp.rate = 5 # healing magic!
|
||||
# wait 5s
|
||||
> pyself.traits.hp.value
|
||||
75
|
||||
```
|
||||
|
||||
Traits will be very practical to use for our character sheets.
|
||||
|
||||
## Turnbattle contrib
|
||||
|
||||
[source](../../../api/evennia.contrib.game_systems.turnbattle.md)
|
||||
|
||||
This contrib consists of several implementations of a turn-based combat system, divivided into complexity:
|
||||
|
||||
- basic - initiative and turn order, attacks against defense values, damage.
|
||||
- equip - considers weapons and armor, wielding and weapon accuracy.
|
||||
- items - adds usable items with conditions and status effects
|
||||
- magic - adds spellcasting system using MP.
|
||||
- range - adds abstract positioning and 1D movement to differentiate between melee and ranged attacks.
|
||||
|
||||
The turnbattle system is comprehensive, but it's meant as a base to start from rather than offer
|
||||
a complete system. It's also not built with _Traits_ in mind, so we will need to adjust it for that.
|
||||
|
||||
## Conclusions
|
||||
|
||||
With some contribs selected, we have pieces to build from and don't have to write everything from scratch.
|
||||
We will need Quests and will likely need to do a bunch of work on Combat to adapt the combat contrib
|
||||
to our needs.
|
||||
|
||||
We will now move into actually starting to implement our tutorial game
|
||||
in the next part of this tutorial series. When doing this for yourself, remember to refer
|
||||
back to your planning and adjust it as you learn what works and what does not.
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,425 @@
|
|||
# Planning our tutorial game
|
||||
|
||||
Using the general plan from last lesson we'll now establish what kind of game we want to create for this tutorial.
|
||||
Remembering that we need to keep the scope down, let's establish some parameters.
|
||||
Note that for your own
|
||||
game you don't _need_ to agree/adopt any of these. Many game-types need more or much less than this.
|
||||
But this makes for good, instructive examples.
|
||||
|
||||
- To have something to refer to rather than just saying "our tutorial game" over and over, we'll
|
||||
name it ... _EvAdventure_.
|
||||
- We want EvAdventure be a small game we can play ourselves for fun, but which could in principle be expanded
|
||||
to something more later.
|
||||
- Let's go with a fantasy theme, it's well understood.
|
||||
- We'll use some existing, simple RPG system.
|
||||
- We want to be able to create and customize a character of our own.
|
||||
- We want the tools to roleplay with other players.
|
||||
- We don't want to have to rely on a Game master to resolve things, but will rely on code for skill resolution
|
||||
and combat.
|
||||
- We want monsters to fight and NPCs we can talk to. So some sort of AI.
|
||||
- We want to be able to buy and sell stuff, both with NPCs and other players.
|
||||
- We want some sort of crafting system.
|
||||
- We want some sort of quest system.
|
||||
|
||||
Let's answer the questions from the previous lesson and discuss some of the possibilities.
|
||||
|
||||
## Administration
|
||||
|
||||
### Should your game rules be enforced by coded systems by human game masters?
|
||||
|
||||
Generally, the more work you expect human staffers/GMs to do, the less your code needs to work. To
|
||||
support GMs you'd need to design commands to support GM-specific actions and the type of game-mastering
|
||||
you want them to do. You may need to expand communication channels so you can easily
|
||||
talk to groups people in private and split off gaming groups from each other. RPG rules could be as simple
|
||||
as the GM sitting with the rule books and using a dice-roller for visibility.
|
||||
|
||||
GM:ing is work-intensive however, and even the most skilled and enthusiastic GM can't be awake all hours
|
||||
of the day to serve an international player base. The computer never needs sleep, so having the ability for
|
||||
players to "self-serve" their RP itch when no GMs are around is a good idea even for the most GM-heavy games.
|
||||
|
||||
On the other side of the spectrum are games with no GMs at all; all gameplay are driven either by the computer
|
||||
or by the interactions between players. Such games still need an active staff, but nowhere as much active
|
||||
involvement. Allowing for solo-play with the computer also allows players to have fun when the number of active
|
||||
players is low.
|
||||
|
||||
We want EvAdventure to work entirely without depending on human GMs. That said, there'd be nothing
|
||||
stopping a GM from stepping in and run an adventure for some players should they want to.
|
||||
|
||||
### What is the staff hierarchy in your game? Is vanilla Evennia roles enough or do you need something else?
|
||||
|
||||
The default hierarchy is
|
||||
|
||||
- `Player` - regular players
|
||||
- `Player Helper` - can create/edit help entries
|
||||
- `Builder` - can use build commands
|
||||
- `Admin` - can kick and ban accounts
|
||||
- `Developer` - full access, usually also trusted with server access
|
||||
|
||||
There is also the _superuser_, the "owner" of the game you create when you first set up your database. This user
|
||||
goes outside the regular hierarchy and should usually only.
|
||||
|
||||
We are okay with keeping this structure for our game.
|
||||
|
||||
### Should players be able to post out-of-characters on channels and via other means like bulletin-boards?
|
||||
|
||||
Evennia's _Channels_ are by default only available between _Accounts_. That is, for players to communicate with each
|
||||
other. By default, the `public` channel is created for general discourse.
|
||||
Channels are logged to a file and when you are coming back to the game you can view the history of a channel
|
||||
in case you missed something.
|
||||
|
||||
> public Hello world!
|
||||
[Public] MyName: Hello world!
|
||||
|
||||
But Channels can also be set up to work between Characters instead of Accounts. This would mean the channels
|
||||
would have an in-game meaning:
|
||||
|
||||
- Members of a guild could be linked telepathically.
|
||||
- Survivors of the apocalypse can communicate over walkie-talkies.
|
||||
- Radio stations you can tune into or have to discover.
|
||||
|
||||
_Bulletin boards_ are a sort of in-game forum where posts are made publicly or privately. Contrary to a channel,
|
||||
the messages are usually stored and are grouped into topics with replies. Evennia has no default bulletin-board
|
||||
system.
|
||||
|
||||
In EvAdventure we will just use the default inter-account channels. We will also not be implementing any
|
||||
bulletin boards.
|
||||
|
||||
## Building
|
||||
|
||||
### How will the world be built?
|
||||
|
||||
There are two main ways to handle this:
|
||||
- Traditionally, from in-game with build-commands: This means builders creating content in their game
|
||||
client. This has the advantage of not requiring Python skills nor server access. This can often be a quite
|
||||
intuitive way to build since you are sort-of walking around in your creation as you build it. However, the
|
||||
developer (you) must make sure to provide build-commands that are flexible enough for builders to be able to
|
||||
create the content you want for your game.
|
||||
- Externally (by batchcmds): Evennia's `batchcmd` takes a text file with Evennia Commands and executes them
|
||||
in sequence. This allows the build process to be repeated and applied quickly to a new database during development.
|
||||
It also allows builders to use proper text-editing tools rather than writing things line-by-line in their clients.
|
||||
The drawback is that for their changes to go live they either need server access or they need to send their
|
||||
batchcode to the game administrator so they can apply the changes. Or use version control.
|
||||
- Externally (with batchcode or custom code): This is the "professional game development" approach. This gives the
|
||||
builders maximum power by creating the content in Python using Evennia primitives. The `batchcode` processor
|
||||
allows Evennia to apply and re-apply build-scripts that are raw Python modules. Again, this would require the
|
||||
builder to have server access or to use version control to share their work with the rest of the development team.
|
||||
|
||||
In this tutorial, we will show examples of all these ways, but since we don't have a team of builders we'll
|
||||
build the brunt of things using Evennia's Batchcode system.
|
||||
|
||||
### Can only privileged Builders create things or should regular players also have limited build-capability?
|
||||
|
||||
In some game styles, players have the ability to create objects and even script them. While giving regular users
|
||||
the ability to create objects with in-built commands is easy and safe, actual code-creation (aka _softcode_ ) is
|
||||
not something Evennia supports natively. Regular, untrusted users should never be allowed to execute raw Python
|
||||
code (such as what you can do with the `py` command). You can
|
||||
[read more about Evennia's stance on softcode here](../../../Concepts/Soft-Code.md). If you want users to do limited scripting,
|
||||
it's suggested that this is accomplished by adding more powerful build-commands for them to use.
|
||||
|
||||
For our tutorial-game, we will only allow privileged builders to modify the world. The exception is crafting,
|
||||
which we will limit to repairing broken items by combining them with other repair-related items.
|
||||
|
||||
## Systems
|
||||
|
||||
### Do you base your game off an existing RPG system or make up your own?
|
||||
|
||||
We will make use of [Open Adventure](http://www.geekguild.com/openadventure/), a simple 'old school' RPG-system
|
||||
that is available for free under the Creative Commons license. We'll only use a subset of the rules from
|
||||
the blue "basic" book. For the sake of keeping down the length of this tutorial we will limit what features
|
||||
we will include:
|
||||
|
||||
- Only two 'archetypes' (classes) - Arcanist (wizard) and Warrior, these are examples of two different play
|
||||
styles.
|
||||
- Two races only (dwarves and elves), to show off how to implement races and race bonuses.
|
||||
- No extra features of the races/archetypes such as foci and special feats. While these are good for fleshing
|
||||
out a character, these will work the same as other bonuses and are thus not that instructive.
|
||||
- We will add only a small number of items/weapons from the Open Adventure rulebook to show how it's done.
|
||||
|
||||
### What are the game mechanics? How do you decide if an action succeeds or fails?
|
||||
|
||||
Open Adventure's conflict resolution is based on adding a trait (such as Strength) with a random number in
|
||||
order to beat a target. We will emulate this in code.
|
||||
|
||||
Having a "skill" means getting a bonus to that roll for a more narrow action.
|
||||
Since the computer will need to know exactly what those skills are, we will add them more explicitly than
|
||||
in the rules, but we will only add the minimum to show off the functionality we need.
|
||||
|
||||
### Does the flow of time matter in your game - does night and day change? What about seasons?
|
||||
|
||||
Most commonly, game-time runs faster than real-world time. There are
|
||||
a few advantages with this:
|
||||
|
||||
- Unlike in a single-player game, you can't fast-forward time in a multiplayer game if you are waiting for
|
||||
something, like NPC shops opening.
|
||||
- Healing and other things that we know takes time will go faster while still being reasonably 'realistic'.
|
||||
|
||||
The main drawback is for games with slower roleplay pace. While you are having a thoughtful roleplaying scene
|
||||
over dinner, the game world reports that two days have passed. Having a slower game time than real-time is
|
||||
a less common, but possible solution for such games.
|
||||
|
||||
It is however _not_ recommended to let game-time exactly equal the speed of real time. The reason for this
|
||||
is that people will join your game from all around the world, and they will often only be able to play at
|
||||
particular times of their day. With a game-time drifting relative real-time, everyone will eventually be
|
||||
able to experience both day and night in the game.
|
||||
|
||||
For this tutorial-game we will go with Evennia's default, which is that the game-time runs two times faster
|
||||
than real time.
|
||||
|
||||
### Do you want changing, global weather or should weather just be set manually in roleplay?
|
||||
|
||||
A weather system is a good example of a game-global system that affects a subset of game entities
|
||||
(outdoor rooms). We will not be doing any advanced weather simulation, but we'll show how to do
|
||||
random weather changes happening across the game world.
|
||||
|
||||
### Do you want a coded world-economy or just a simple barter system? Or no formal economy at all?
|
||||
|
||||
We will allow for money and barter/trade between NPCs/Players and Player/Player, but will not care about
|
||||
inflation. A real economic simulation could do things like modify shop prices based on supply and demand.
|
||||
We will not go down that rabbit hole.
|
||||
|
||||
### Do you have concepts like reputation and influence?
|
||||
|
||||
These are useful things for a more social-interaction heavy game. We will not include them for this
|
||||
tutorial however.
|
||||
|
||||
### Will your characters be known by their name or only by their physical appearance?
|
||||
|
||||
This is a common thing in RP-heavy games. Others will only see you as "The tall woman" until you
|
||||
introduce yourself and they 'recognize' you with a name. Linked to this is the concept of more complex
|
||||
emoting and posing.
|
||||
|
||||
Adding such a system from scratch is complex and way beyond the scope of this tutorial. However,
|
||||
there is an existing Evennia contrib that adds all of this functionality and more, so we will
|
||||
include that and explain briefly how it works.
|
||||
|
||||
## Rooms
|
||||
|
||||
### Is a simple room description enough or should the description be able to change?
|
||||
|
||||
Changing room descriptions for day and night, winder and summer is actually quite easy to do, but looks
|
||||
very impressive. We happen to know there is also a contrib that helps with this, so we'll show how to
|
||||
include that.
|
||||
|
||||
### Should the room have different statuses?
|
||||
|
||||
We will have different weather in outdoor rooms, but this will not have any gameplay effect - bow strings
|
||||
will not get wet and fireballs will not fizzle if it rains.
|
||||
|
||||
### Can objects be hidden in the room? Can a person hide in the room?
|
||||
|
||||
We will not model hiding and stealth. This will be a game of honorable face-to-face conflict.
|
||||
|
||||
## Objects
|
||||
|
||||
### How numerous are your objects? Do you want large loot-lists or are objects just role playing props?
|
||||
|
||||
Since we are not going for a pure freeform RPG here, we will want objects with properties, like weapons
|
||||
and potions and such. Monsters should drop loot even though our list of objects will not be huge.
|
||||
|
||||
### Is each coin a separate object or do you just store a bank account value?
|
||||
|
||||
Since we will use bartering, placing coin objects on one side of the barter makes for a simple way to
|
||||
handle payments. So we will use coins as-objects.
|
||||
|
||||
### Do multiple similar objects form stacks and how are those stacks handled in that case?
|
||||
|
||||
Since we'll use coins, it's practical to have them and other items stack together. While Evennia does not
|
||||
do this natively, we will make use of a contrib for this.
|
||||
|
||||
### Does an object have weight or volume (so you cannot carry an infinite amount of them)?
|
||||
|
||||
Limiting carrying weight is one way to stop players from hoarding. It also makes it more important
|
||||
for players to pick only the equipment they need. Carrying limits can easily come across as
|
||||
annoying to players though, so one needs to be careful with it.
|
||||
|
||||
Open Adventure rules include weight limits, so we will include them.
|
||||
|
||||
### Can objects be broken? Can they be repaired?
|
||||
|
||||
Item breakage is very useful for a game economy; breaking weapons adds tactical considerations (if it's not
|
||||
too common, then it becomes annoying) and repairing things gives work for crafting players.
|
||||
|
||||
We wanted a crafting system, so this is what we will limit it to - repairing items using some sort
|
||||
of raw materials.
|
||||
|
||||
### Can you fight with a chair or a flower or must you use a special 'weapon' kind of thing?
|
||||
|
||||
Traditionally, only 'weapons' could be used to fight with. In the past this was a useful
|
||||
simplification, but with Python classes and inheritance, it's not actually more work to just
|
||||
let all items in game work as a weapon in a pinch.
|
||||
|
||||
So for our game we will let a character use any item they want as a weapon. The difference will
|
||||
be that non-weapon items will do less damage and also break and become unusable much quicker.
|
||||
|
||||
### Will characters be able to craft new objects?
|
||||
|
||||
Crafting is a common feature in multiplayer games. In code it usually means using a skill-check
|
||||
to combine base ingredients from a fixed recipe in order to create a new item. The classic
|
||||
example is to combine _leather straps_, a _hilt_, a _pommel_ and a _blade_ to make a new _sword_.
|
||||
A full-fledged crafting system could require multiple levels of crafting, including having to mine
|
||||
for ore or cut down trees for wood.
|
||||
|
||||
In our case we will limit our crafting to repairing broken items. To show how it's done, we will require
|
||||
extra items (a recipe) in order to facilitate the repairs.
|
||||
|
||||
### Should mobs/NPCs have some sort of AI?
|
||||
|
||||
A rule of adding Artificial Intelligence is that with today's technology you should not hope to fool
|
||||
anyone with it anytime soon. Unless you have a side-gig as an AI researcher, users will likely
|
||||
not notice any practical difference between a simple state-machine and you spending a lot of time learning
|
||||
how to train a neural net.
|
||||
|
||||
For this tutorial, we will show how to add a simple state-machine for monsters. NPCs will only be
|
||||
shop-keepers and quest-gives so they won't need any real AI to speak of.
|
||||
|
||||
### Are NPCs and mobs different entities? How do they differ?
|
||||
|
||||
"Mobs" or "mobiles" are things that move around. This is traditionally monsters you can fight with, but could
|
||||
also be city guards or the baker going to chat with the neighbor. Back in the day, they were often fundamentally
|
||||
different these days it's often easier to just make NPCs and mobs essentially the same thing.
|
||||
|
||||
In EvAdventure, both Monsters and NPCs will be the same type of thing; A monster could give you a quest
|
||||
and an NPC might fight you as a mob as well as trade with you.
|
||||
|
||||
### _Should there be NPCs giving quests? If so, how do you track Quest status?
|
||||
|
||||
We will design a simple quest system to track the status of ongoing quests.
|
||||
|
||||
## Characters
|
||||
|
||||
### Can players have more than one Character active at a time or are they allowed to multi-play?
|
||||
|
||||
Since Evennia differentiates between `Sessions` (the client-connection to the game), `Accounts`
|
||||
and `Character`s, it natively supports multi-play. This is controlled by the `MULTISESSION_MODE`
|
||||
setting, which has a value from `0` (default) to `3`.
|
||||
|
||||
- `0`- One Character per Account and one Session per Account. This means that if you login to the same
|
||||
account from another client you'll be disconnected from the first. When creating a new account, a Character
|
||||
will be auto-created with the same name as your Account. This is default mode and mimics legacy code bases
|
||||
which had no separation between Account and Character.
|
||||
- `1` - One Character per Account, multiple Sessions per Account. So you can connect simultaneously from
|
||||
multiple clients and see the same output in all of them.
|
||||
- `2` - Multiple Characters per Account, one Session per Character. This will not auto-create a same-named
|
||||
Character for you, instead you get to create/choose between a number of Characters up to a max limit given by
|
||||
the `MAX_NR_CHARACTERS` setting (default 1). You can play them all simultaneously if you have multiple clients
|
||||
open, but only one client per Character.
|
||||
- `3` - Multiple Characters per Account, Multiple Sessions per Character. This is like mode 2, except players
|
||||
can control each Character from multiple clients, seeing the same output from each Character.
|
||||
|
||||
We will go with a multi-role game, so we will use `MULTISESSION_MODE=3` for this tutorial.
|
||||
|
||||
### How does the character-generation work?
|
||||
|
||||
There are a few common ways to do character generation:
|
||||
|
||||
- Rooms. This is the traditional way. Each room's description tells you what command to use to modify
|
||||
your character. When you are done you move to the next room. Only use this if you have another reason for
|
||||
using a room, like having a training dummy to test skills on, for example.
|
||||
- A Menu. The Evennia _EvMenu_ system allows you to code very flexible in-game menus without needing to walk
|
||||
between rooms. You can both have a step-by-step menu (a 'wizard') or allow the user to jump between the
|
||||
steps as they please. This tends to be a lot easier for newcomers to understand since it doesn't require
|
||||
using custom commands they will likely never use again after this.
|
||||
- Questions. A fun way to build a character is to answer a series of questions. This is usually implemented
|
||||
with a sequential menu.
|
||||
|
||||
For the tutorial we will use a menu to let the user modify each section of their character sheet in any order
|
||||
until they are happy.
|
||||
|
||||
### How do you implement different "classes" or "races"?
|
||||
|
||||
The way classes and races work in most RPGs (as well as in OpenAdventure) is that they act as static 'templates'
|
||||
that inform which bonuses and special abilities you have. This means that all we need to store on the
|
||||
Character is _which_ class and _which_ race they have; the actual logic can sit in Python code and just
|
||||
be looked up when we need it.
|
||||
|
||||
### If a Character can hide in a room, what skill will decide if they are detected?
|
||||
|
||||
Hiding means a few things.
|
||||
- The Character should not appear in the room's description / character list
|
||||
- Others hould not be able to interact with a hidden character. It'd be weird if you could do `attack <name>`
|
||||
or `look <name>` if the named character is in hiding.
|
||||
- There must be a way for the person to come out of hiding, and probably for others to search or accidentally
|
||||
find the person (probably based on skill checks).
|
||||
- The room will also need to be involved, maybe with some modifier as to how easy it is to hide in the room.
|
||||
|
||||
We will _not_ be including a hide-mechanic in EvAdventure though.
|
||||
|
||||
### What does the skill tree look like? Can a Character gain experience to improve? By killing enemies? Solving quests? By roleplaying?
|
||||
|
||||
Gaining experience points (XP) and improving one's character is a staple of roleplaying games. There are many
|
||||
ways to implement this:
|
||||
- Gaining XP from kills is very common; it's easy to let a monster be 'worth' a certain number of XP and it's
|
||||
easy to tell when you should gain it.
|
||||
- Gaining XP from quests is the same - each quest is 'worth' XP and you get them when completing the test.
|
||||
- Gaining XP from roleplay is harder to define. Different games have tried a lot of different ways to do this:
|
||||
- XP from being online - just being online gains you XP. This inflates player numbers but many players may
|
||||
just be lurking and not be actually playing the game at any given time.
|
||||
- XP from roleplaying scenes - you gain XP according to some algorithm analyzing your emotes for 'quality',
|
||||
how often you post, how long your emotes are etc.
|
||||
- XP from actions - you gain XP when doing things, anything. Maybe your XP is even specific to each action, so
|
||||
you gain XP only for running when you run, XP for your axe skill when you fight with an axe etc.
|
||||
- XP from fails - you only gain XP when failing rolls.
|
||||
- XP from other players - other players can award you XP for good RP.
|
||||
|
||||
For EvAdventure we will use Open Adventure's rules for XP, which will be driven by kills and quest successes.
|
||||
|
||||
### May player-characters attack each other (PvP)?
|
||||
|
||||
Deciding this affects the style of your entire game. PvP makes for exciting gameplay but it opens a whole new
|
||||
can of worms when it comes to "fairness". Players will usually accept dying to an overpowered NPC dragon. They
|
||||
will not be as accepting if they perceive another player is perceived as being overpowered. PvP means that you
|
||||
have to be very careful to balance the game - all characters does not have to be exactly equal but they should
|
||||
all be viable to play a fun game with. PvP does not only mean combat though. Players can compete in all sorts of ways, including gaining influence in
|
||||
a political game or gaining market share when selling their crafted merchandise.
|
||||
|
||||
For the EvAdventure we will support both Player-vs-environment combat and turn-based PvP. We will allow players
|
||||
to barter with each other (so potentially scam others?) but that's the extent of it. We will focus on showing
|
||||
off techniques and will not focus on making a balanced game.
|
||||
|
||||
### What are the penalties of defeat? Permanent death? Quick respawn? Time in prison?
|
||||
|
||||
This is another big decision that strongly affects the mood and style of your game.
|
||||
|
||||
Perma-death means that once your character dies, it's gone and you have to make a new one.
|
||||
|
||||
- It allows for true heroism. If you genuinely risk losing your character of two years to fight the dragon,
|
||||
your triumph is an actual feat.
|
||||
- It limits the old-timer dominance problem. If long-time players dies occationally, it will open things
|
||||
up for newcomers.
|
||||
- It lowers inflation, since the hoarded resources of a dead character can be removed.
|
||||
- It gives capital punishment genuine discouraging power.
|
||||
- It's realistic.
|
||||
|
||||
Perma-death comes with some severe disadvantages however.
|
||||
|
||||
- It's impopular. Many players will just not play a game where they risk losing their beloved character
|
||||
just like that.
|
||||
- Many players say they like the _idea_ of permadeath except when it could happen to them.
|
||||
- It can limit roleplaying freedom and make people refuse to take any risks.
|
||||
- It may make players even more reluctant to play conflict-driving 'bad guys'.
|
||||
- Game balance is much, much more important when results are "final". This escalates the severity of 'unfairness'
|
||||
a hundred-fold. Things like bugs or exploits can also lead to much more server effects.
|
||||
|
||||
For these reasons, it's very common to do hybrid systems. Some tried variations:
|
||||
|
||||
- NPCs cannot kill you, only other players can.
|
||||
- Death is permanent, but it's difficult to actually die - you are much more likely to end up being severely
|
||||
hurt/incapacitated.
|
||||
- You can pre-pay 'insurance' to magically/technologically avoid actually dying. Only if don't have insurance will
|
||||
you die permanently.
|
||||
- Death just means harsh penalties, not actual death.
|
||||
- When you die you can fight your way back to life from some sort of afterlife.
|
||||
- You'll only die permanently if you as a player explicitly allows it.
|
||||
|
||||
For our tutorial-game we will not be messing with perma-death; instead your defeat will mean you will re-spawn
|
||||
back at your home location with a fraction of your health.
|
||||
|
||||
## Conclusions
|
||||
|
||||
Going through the questions has helped us get a little bit more of a feel for the game we want to do. There are
|
||||
many other things we could ask ourselves, but if we can cover these points we will be a good way towards a complete,
|
||||
playable game!
|
||||
|
||||
Before starting to code in earnest a good coder should always do an inventory of all the stuff they _don't_ need
|
||||
to code themselves. So in the next lesson we will check out what help we have from Evennia's _contribs_.
|
||||
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
# Where do I begin?
|
||||
|
||||
The good news is that following this Starting tutorial is a great way to begin making an Evennia game.
|
||||
|
||||
The bad news is that everyone's different and when it comes to starting your own game there is no
|
||||
one-size-fits-all answer. Instead we will ask a series of questions
|
||||
to help you figure this out for yourself. It will also help you evaluate your own skills and maybe
|
||||
put some more realistic limits on how fast you can achieve your goals.
|
||||
|
||||
> The questions in this lesson do not really apply to our tutorial game since we know we are doing it
|
||||
> to learn Evennia. If you just want to follow along with the technical bits you can skip this lesson and
|
||||
> come back later when you feel ready to take on making your own game.
|
||||
|
||||
## What is your motivation for doing this?
|
||||
|
||||
So you want to make a game. First you need to make a few things clear to yourself.
|
||||
|
||||
Making a multiplayer online game is a _big_ undertaking. You will (if you are like most of us) be
|
||||
doing it as a hobby, without getting paid. And you’ll be doing it for a long time.
|
||||
|
||||
So the very first thing you should ask yourself (and your team, if you have any) is
|
||||
_why am I doing this_? Do some soul-searching here. Here are some possible answers:
|
||||
|
||||
- I want to earn recognition and fame from my online community and/or among my friends.
|
||||
- I want to build the game so I can play and enjoy it myself.
|
||||
- I want to build the same game I already play but without the bad people.
|
||||
- I want to create a game so that I can control it and be the head honcho.
|
||||
- A friend or online acquaintance talked me into working on it.
|
||||
- I work on this because I’m paid to (wow!)
|
||||
- I only build this for my own benefit or to see if I can pull it off.
|
||||
- I want to create something to give back to the community I love.
|
||||
- I want to use this project as a stepping-stone towards other projects (like a career in game design
|
||||
or programming).
|
||||
- I am interested in coding or server and network architectures, making a MUD just seems to be a good
|
||||
way to teach myself.
|
||||
- I want to build a commercial game and earn money.
|
||||
- I want to fulfill a life-long dream of game making.
|
||||
|
||||
There are many other possibilities. How “solid” your answer is for a long-term development project
|
||||
is up to you. The important point is that you ask yourself the question.
|
||||
|
||||
**Help someone else instead** - Maybe you should _not_ start a new project - maybe you're better off
|
||||
helping someone else or improve on something that already exists. Or maybe you find you are more of a
|
||||
game engine developer than a game designer.
|
||||
|
||||
**Driven by emotion** - Some answers may suggest that you are driven by emotions of revenge or disconcert. Be careful with that and
|
||||
check so that's not your _only_ driving force. Those emotions may have abated later when the project
|
||||
most needs your enthusiasm and motivation.
|
||||
|
||||
**Going commercial** - If your aim is to earn money, your design goals will likely be very different from
|
||||
those of a person who only creates as a hobby or for their own benefit. You may also have a much stricter
|
||||
timeline for release.
|
||||
|
||||
Whichever your motivation, you should at least have it clear in your own mind. It’s worth to make
|
||||
sure your eventual team is on the same page too.
|
||||
|
||||
## What are your skills?
|
||||
|
||||
Once you have your motivations straight you need to take a stock of your own skills and the skills
|
||||
available in your team, if you have any.
|
||||
|
||||
Your game will have two principal components and you will need skills to cater for both:
|
||||
|
||||
- The game engine / code base - Evennia in this case.
|
||||
- The assets created for using the game engine (“the game world”)
|
||||
|
||||
### The game engine
|
||||
|
||||
The game engine is maintained and modified by programmers (coders). It represents the infrastructure
|
||||
that runs the game - the network code, the protocol support, the handling of commands, scripting and
|
||||
data storage.
|
||||
|
||||
If you are just evaluating Evennia, it's worth to do the following:
|
||||
|
||||
- Hang out in the community/forums/chat. Expect to need to ask a lot of “stupid” questions as you start
|
||||
developing (hint: no question is stupid). Is this a community in which you would feel comfortable doing so?
|
||||
- Keep tabs on the manual (you're already here).
|
||||
- How's your Python skills? What are the skills in your team? Do you or your team already know it or are
|
||||
you willing to learn? Learning the language as you go is not too unusual with Evennia devs, but expect it
|
||||
to add development time. You will also be worse at predicting how 'hard' something is to do.
|
||||
- If you don’t know Python, you should have gotten a few tastes from the first part of this tutorial. But
|
||||
expect to have to refer to external online tutorials - there are many details of Python that will not be
|
||||
covered.
|
||||
|
||||
### Asset creation
|
||||
|
||||
Compared to the level of work needed to produce professional graphics for an MMORPG, detailed text
|
||||
assets for a mud are cheap to create. This is one of the many reasons muds are so well suited for a
|
||||
small team.
|
||||
|
||||
This is not to say that making “professional” text content is easy though. Knowing how to write
|
||||
imaginative and grammatically correct prose is only the minimal starting requirement. A good asset-
|
||||
creator (traditionally called a “builder”) must also be able to utilize the tools of the game engine
|
||||
to its fullest in order to script events, make quests, triggers and interactive, interesting
|
||||
environments.
|
||||
|
||||
Assuming you are not coding all alone, your team’s in-house builders will be the first ones to actually
|
||||
“use” your game framework and build tools. They will stumble on all the bugs. This means that you
|
||||
need people who are just not “artsy” or “good with words”. Assuming coders and builders are not the
|
||||
same people (common for early testing), builders need to be able to collaborate well and give clear
|
||||
and concise feedback.
|
||||
|
||||
If you know your builders are not tech-savvy, you may need to spend more time making easier
|
||||
build-tools and commands for them.
|
||||
|
||||
## So, where do I begin, then?
|
||||
|
||||
Right, after all this soul-searching and skill-inventory-checking, let’s go back to the original
|
||||
question. And maybe you’ll find that you have a better feeling for the answer yourself already:
|
||||
|
||||
- Keep following this tutorial and spend the time
|
||||
to really understand what is happening in the examples. Not only will this give you a better idea
|
||||
of how parts hang together, it may also give you ideas for what is possible. Maybe something
|
||||
is easier than you expected!
|
||||
- Introduce yourself in the IRC/Discord chat and don't be shy to ask questions as you go through
|
||||
the tutorial. Don't get hung up on trying to resolve something that a seasoned Evennia dev may
|
||||
clear up for you in five minutes. Also, not all errors are your faults - it's possible the
|
||||
tutorial is unclear or has bugs, asking will quickly bring those problems to light, if so.
|
||||
- If Python is new to you, you should complement the tutorial with third-party Python references
|
||||
so you can read, understand and replicate example code without being completely in the dark.
|
||||
|
||||
Once you are out of the starting tutorial, you'll be off to do your own thing.
|
||||
|
||||
- The starting tutorial cannot cover everything. Skim through the [Evennia docs](../../../index.md).
|
||||
Even if you don't read everything, it gives you a feeling for what's available should you need
|
||||
to look for something later. Make sure to use the search function.
|
||||
- You can now start by expanding on the tutorial-game we will have created. In the last part there
|
||||
there will be a list of possible future projects you could take on. Working on your own, without help
|
||||
from a tutorial is the next step.
|
||||
|
||||
As for your builders, they can start getting familiar with Evennia's default build commands ... but
|
||||
keep in mind that your game is not yet built! Don't set your builders off on creating large zone projects.
|
||||
If they build anything at all, it should be small test areas to agree on a homogenous form, mood
|
||||
and literary style.
|
||||
|
||||
## Conclusions
|
||||
|
||||
Remember that what kills a hobby game project will usually be your own lack of
|
||||
motivation. So do whatever you can to keep that motivation burning strong! Even if it means
|
||||
deviating from what you read in a tutorial like this one. Just get that game out there, whichever way
|
||||
works best for you.
|
||||
|
||||
In the next lesson we'll go through some of the technical questions you need to consider. This should
|
||||
hopefully help you figure out more about the game you want to make. In the lesson following that we'll
|
||||
then try to answer those questions for the sake of creating our little tutorial game.
|
||||
802
docs/source/Howtos/Beginner-Tutorial/Part3/A-Sittable-Object.md
Normal file
802
docs/source/Howtos/Beginner-Tutorial/Part3/A-Sittable-Object.md
Normal file
|
|
@ -0,0 +1,802 @@
|
|||
[prev lesson](../../../Unimplemented.md) | [next lesson](../../../Unimplemented.md)
|
||||
|
||||
# Making a sittable object
|
||||
|
||||
In this lesson we will go through how to make a chair you can sit on. Sounds easy, right?
|
||||
Well it is. But in the process of making the chair we will need to consider the various ways
|
||||
to do it depending on how we want our game to work.
|
||||
|
||||
The goals of this lesson are as follows:
|
||||
|
||||
- We want a new 'sittable' object, a Chair in particular".
|
||||
- We want to be able to use a command to sit in the chair.
|
||||
- Once we are sitting in the chair it should affect us somehow. To demonstrate this we'll
|
||||
set a flag "Resting" on the Character sitting in the Chair.
|
||||
- When you sit down you should not be able to walk to another room without first standing up.
|
||||
- A character should be able to stand up and move away from the chair.
|
||||
|
||||
There are two main ways to design the commands for sitting and standing up.
|
||||
- You can store the commands on the chair so they are only available when a chair is in the room
|
||||
- You can store the commands on the Character so they are always available and you must always specify
|
||||
which chair to sit on.
|
||||
|
||||
Both of these are very useful to know about, so in this lesson we'll try both. But first
|
||||
we need to handle some basics.
|
||||
|
||||
|
||||
## Don't move us when resting
|
||||
|
||||
When you are sitting in a chair you can't just walk off without first standing up.
|
||||
This requires a change to our Character typeclass. Open `mygame/typeclasses/characters.py`:
|
||||
|
||||
```python
|
||||
|
||||
# ...
|
||||
|
||||
class Character(DefaultCharacter):
|
||||
# ...
|
||||
|
||||
def at_pre_move(self, destination):
|
||||
"""
|
||||
Called by self.move_to when trying to move somewhere. If this returns
|
||||
False, the move is immediately cancelled.
|
||||
"""
|
||||
if self.db.is_resting:
|
||||
self.msg("You can't go anywhere while resting.")
|
||||
return False
|
||||
return True
|
||||
|
||||
```
|
||||
|
||||
When moving somewhere, [character.move_to](evennia.objects.objects.DefaultObject.move_to) is called. This in turn
|
||||
will call `character.at_pre_move`. Here we look for an Attribute `is_resting` (which we will assign below)
|
||||
to determine if we are stuck on the chair or not.
|
||||
|
||||
## Making the Chair itself
|
||||
|
||||
Next we need the Chair itself, or rather a whole family of "things you can sit on" that we will call
|
||||
_sittables_. We can't just use a default Object since we want a sittable to contain some custom code. We need
|
||||
a new, custom Typeclass. Create a new module `mygame/typeclasses/sittables.py` with the following content:
|
||||
|
||||
```python
|
||||
|
||||
from evennia import DefaultObject
|
||||
|
||||
class Sittable(DefaultObject):
|
||||
|
||||
def at_object_creation(self):
|
||||
self.db.sitter = None
|
||||
|
||||
def do_sit(self, sitter):
|
||||
"""
|
||||
Called when trying to sit on/in this object.
|
||||
|
||||
Args:
|
||||
sitter (Object): The one trying to sit down.
|
||||
|
||||
"""
|
||||
current = self.db.sitter
|
||||
if current:
|
||||
if current == sitter:
|
||||
sitter.msg("You are already sitting on {self.key}.")
|
||||
else:
|
||||
sitter.msg(f"You can't sit on {self.key} "
|
||||
f"- {current.key} is already sitting there!")
|
||||
return
|
||||
self.db.sitting = sitter
|
||||
sitter.db.is_resting = True
|
||||
sitter.msg(f"You sit on {self.key}")
|
||||
|
||||
def do_stand(self, stander):
|
||||
"""
|
||||
Called when trying to stand from this object.
|
||||
|
||||
Args:
|
||||
stander (Object): The one trying to stand up.
|
||||
|
||||
"""
|
||||
current = self.db.sitter
|
||||
if not stander == current:
|
||||
stander.msg(f"You are not sitting on {self.key}.")
|
||||
else:
|
||||
self.db.sitting = None
|
||||
stander.db.is_resting = False
|
||||
stander.msg(f"You stand up from {self.key}")
|
||||
```
|
||||
|
||||
Here we have a small Typeclass that handles someone trying to sit on it. It has two methods that we can simply
|
||||
call from a Command later. We set the `is_resting` Attribute on the one sitting down.
|
||||
|
||||
One could imagine that one could have the future `sit` command check if someone is already sitting in the
|
||||
chair instead. This would work too, but letting the `Sittable` class handle the logic around who can sit on it makes
|
||||
logical sense.
|
||||
|
||||
We let the typeclass handle the logic, and also let it do all the return messaging. This makes it easy to churn out
|
||||
a bunch of chairs for people to sit on. But it's not perfect. The `Sittable` class is general. What if you want to
|
||||
make an armchair. You sit "in" an armchair rather than "on" it. We _could_ make a child class of `Sittable` named
|
||||
`SittableIn` that makes this change, but that feels excessive. Instead we will make it so that Sittables can
|
||||
modify this per-instance:
|
||||
|
||||
|
||||
```python
|
||||
|
||||
from evennia import DefaultObject
|
||||
|
||||
class Sittable(DefaultObject):
|
||||
|
||||
def at_object_creation(self):
|
||||
self.db.sitter = None
|
||||
# do you sit "on" or "in" this object?
|
||||
self.db.adjective = "on"
|
||||
|
||||
def do_sit(self, sitter):
|
||||
"""
|
||||
Called when trying to sit on/in this object.
|
||||
|
||||
Args:
|
||||
sitter (Object): The one trying to sit down.
|
||||
|
||||
"""
|
||||
adjective = self.db.adjective
|
||||
current = self.db.sitter
|
||||
if current:
|
||||
if current == sitter:
|
||||
sitter.msg(f"You are already sitting {adjective} {self.key}.")
|
||||
else:
|
||||
sitter.msg(
|
||||
f"You can't sit {adjective} {self.key} "
|
||||
f"- {current.key} is already sitting there!")
|
||||
return
|
||||
self.db.sitting = sitter
|
||||
sitter.db.is_resting = True
|
||||
sitter.msg(f"You sit {adjective} {self.key}")
|
||||
|
||||
def do_stand(self, stander):
|
||||
"""
|
||||
Called when trying to stand from this object.
|
||||
|
||||
Args:
|
||||
stander (Object): The one trying to stand up.
|
||||
|
||||
"""
|
||||
current = self.db.sitter
|
||||
if not stander == current:
|
||||
stander.msg(f"You are not sitting {self.db.adjective} {self.key}.")
|
||||
else:
|
||||
self.db.sitting = None
|
||||
stander.db.is_resting = False
|
||||
stander.msg(f"You stand up from {self.key}")
|
||||
```
|
||||
|
||||
We added a new Attribute `adjective` which will probably usually be `in` or `on` but could also be `at` if you
|
||||
want to be able to sit _at a desk_ for example. A regular builder would use it like this:
|
||||
|
||||
> create/drop armchair : sittables.Sittable
|
||||
> set armchair/adjective = in
|
||||
|
||||
This is probably enough. But all those strings are hard-coded. What if we want some more dramatic flair when you
|
||||
sit down?
|
||||
|
||||
You sit down and a whoopie cushion makes a loud fart noise!
|
||||
|
||||
For this we need to allow some further customization. Let's let the current strings be defaults that
|
||||
we can replace.
|
||||
|
||||
```python
|
||||
|
||||
from evennia import DefaultObject
|
||||
|
||||
class Sittable(DefaultObject):
|
||||
"""
|
||||
An object one can sit on
|
||||
|
||||
Customizable Attributes:
|
||||
adjective: How to sit (on, in, at etc)
|
||||
Return messages (set as Attributes):
|
||||
msg_already_sitting: Already sitting here
|
||||
format tokens {adjective} and {key}
|
||||
msg_other_sitting: Someone else is sitting here.
|
||||
format tokens {adjective}, {key} and {other}
|
||||
msg_sitting_down: Successfully sit down
|
||||
format tokens {adjective}, {key}
|
||||
msg_standing_fail: Fail to stand because not sitting.
|
||||
format tokens {adjective}, {key}
|
||||
msg_standing_up: Successfully stand up
|
||||
format tokens {adjective}, {key}
|
||||
|
||||
"""
|
||||
def at_object_creation(self):
|
||||
self.db.sitter = None
|
||||
# do you sit "on" or "in" this object?
|
||||
self.db.adjective = "on"
|
||||
|
||||
def do_sit(self, sitter):
|
||||
"""
|
||||
Called when trying to sit on/in this object.
|
||||
|
||||
Args:
|
||||
sitter (Object): The one trying to sit down.
|
||||
|
||||
"""
|
||||
adjective = self.db.adjective
|
||||
current = self.db.sitter
|
||||
if current:
|
||||
if current == sitter:
|
||||
if self.db.msg_already_sitting:
|
||||
sitter.msg(
|
||||
self.db.msg_already_sitting.format(
|
||||
adjective=self.db.adjective, key=self.key))
|
||||
else:
|
||||
sitter.msg(f"You are already sitting {adjective} {self.key}.")
|
||||
else:
|
||||
if self.db.msg_other_sitting:
|
||||
sitter.msg(self.db.msg_already_sitting.format(
|
||||
other=current.key, adjective=self.db.adjective, key=self.key))
|
||||
else:
|
||||
sitter.msg(f"You can't sit {adjective} {self.key} "
|
||||
f"- {current.key} is already sitting there!")
|
||||
return
|
||||
self.db.sitting = sitter
|
||||
sitter.db.is_resting = True
|
||||
if self.db.msg_sitting_down:
|
||||
sitter.msg(self.db.msg_sitting_down.format(adjective=adjective, key=self.key))
|
||||
else:
|
||||
sitter.msg(f"You sit {adjective} {self.key}")
|
||||
|
||||
def do_stand(self, stander):
|
||||
"""
|
||||
Called when trying to stand from this object.
|
||||
|
||||
Args:
|
||||
stander (Object): The one trying to stand up.
|
||||
|
||||
"""
|
||||
current = self.db.sitter
|
||||
if not stander == current:
|
||||
if self.db.msg_standing_fail:
|
||||
stander.msg(self.db.msg_standing_fail.format(
|
||||
adjective=self.db.adjective, key=self.key))
|
||||
else:
|
||||
stander.msg(f"You are not sitting {self.db.adjective} {self.key}")
|
||||
else:
|
||||
self.db.sitting = None
|
||||
stander.db.is_resting = False
|
||||
if self.db.msg_standing_up:
|
||||
stander.msg(self.db.msg_standing_up.format(
|
||||
adjective=self.db.adjective, key=self.key))
|
||||
else:
|
||||
stander.msg(f"You stand up from {self.key}")
|
||||
```
|
||||
|
||||
Here we really went all out with flexibility. If you need this much is up to you.
|
||||
We added a bunch of optional Attributes to hold alternative versions of all the messages.
|
||||
There are some things to note:
|
||||
|
||||
- We don't actually initiate those Attributes in `at_object_creation`. This is a simple
|
||||
optimization. The assumption is that _most_ chairs will probably not be this customized.
|
||||
So initiating a bunch of Attributes to, say, empty strings would be a lot of useless database calls.
|
||||
The drawback is that the available Attributes become less visible when reading the code. So we add a long
|
||||
describing docstring to the end to explain all you can use.
|
||||
- We use `.format` to inject formatting-tokens in the text. The good thing about such formatting
|
||||
markers is that they are _optional_. They are there if you want them, but Python will not complain
|
||||
if you don't include some or any of them. Let's see an example:
|
||||
|
||||
> reload # if you have new code
|
||||
> create/drop armchair : sittables.Sittable
|
||||
> set armchair/adjective = in
|
||||
> set armchair/msg_sitting_down = As you sit down {adjective} {key}, life feels easier.
|
||||
> set armchair/msg_standing_up = You stand up from {key}. Life resumes.
|
||||
|
||||
The `{key}` and `{adjective}` are examples of optional formatting markers. Whenever the message is
|
||||
returned, the format-tokens within will be replaced with `armchair` and `in` respectively. Should we
|
||||
rename the chair later, this will show in the messages automatically (since `{key}` will change).
|
||||
|
||||
We have no Command to use this chair yet. But we can try it out with `py`:
|
||||
|
||||
> py self.search("armchair").do_sit(self)
|
||||
As you sit down in armchair, life feels easier.
|
||||
> self.db.resting
|
||||
True
|
||||
> py self.search("armchair").do_stand(self)
|
||||
You stand up from armchair. Life resumes
|
||||
> self.db.resting
|
||||
False
|
||||
|
||||
If you follow along and get a result like this, all seems to be working well!
|
||||
|
||||
## Command variant 1: Commands on the chair
|
||||
|
||||
This way to implement `sit` and `stand` puts new cmdsets on the Sittable itself.
|
||||
As we've learned before, commands on objects are made available to others in the room.
|
||||
This makes the command easy but instead adds some complexity in the management of the CmdSet.
|
||||
|
||||
This is how it will look if `armchair` is in the room:
|
||||
|
||||
> sit
|
||||
As you sit down in armchair, life feels easier.
|
||||
|
||||
What happens if there are sittables `sofa` and `barstool` also in the room? Evennia will automatically
|
||||
handle this for us and allow us to specify which one we want:
|
||||
|
||||
> sit
|
||||
More than one match for 'sit' (please narrow target):
|
||||
sit-1 (armchair)
|
||||
sit-2 (sofa)
|
||||
sit-3 (barstool)
|
||||
> sit-1
|
||||
As you sit down in armchair, life feels easier.
|
||||
|
||||
To keep things separate we'll make a new module `mygame/commands/sittables.py`:
|
||||
|
||||
```{sidebar} Separate Commands and Typeclasses?
|
||||
|
||||
You can organize these things as you like. If you wanted you could put the sit-command + cmdset
|
||||
together with the `Sittable` typeclass in `mygame/typeclasses/sittables.py`. That has the advantage of
|
||||
keeping everything related to sitting in one place. But there is also some organizational merit to
|
||||
keeping all Commands in one place as we do here.
|
||||
|
||||
```
|
||||
|
||||
```python
|
||||
from evennia import Command, CmdSet
|
||||
|
||||
class CmdSit(Command):
|
||||
"""
|
||||
Sit down.
|
||||
"""
|
||||
key = "sit"
|
||||
|
||||
def func(self):
|
||||
self.obj.do_sit(self.caller)
|
||||
|
||||
class CmdStand(Command):
|
||||
"""
|
||||
Stand up.
|
||||
"""
|
||||
key = "stand"
|
||||
def func(self):
|
||||
self.obj.do_stand(self.caller)
|
||||
|
||||
|
||||
class CmdSetSit(CmdSet):
|
||||
priority = 1
|
||||
def at_cmdset_creation(self):
|
||||
self.add(CmdSit)
|
||||
self.add(CmdStand)
|
||||
|
||||
```
|
||||
|
||||
As seen, the commands are nearly trivial. `self.obj` is the object to which we added the cmdset with this
|
||||
Command (so for example a chair). We just call the `do_sit/stand` on that object and the `Sittable` will
|
||||
do the rest.
|
||||
|
||||
Why that `priority = 1` on `CmdSetSit`? This makes same-named Commands from this cmdset merge with a bit higher
|
||||
priority than Commands from the Character-cmdset. Why this is a good idea will become clear shortly.
|
||||
|
||||
We also need to make a change to our `Sittable` typeclass. Open `mygame/typeclasses/sittables.py`:
|
||||
|
||||
```python
|
||||
from evennia import DefaultObject
|
||||
from commands.sittables import CmdSetSit # <- new
|
||||
|
||||
class Sittable(DefaultObject):
|
||||
"""
|
||||
(docstring)
|
||||
"""
|
||||
def at_object_creation(self):
|
||||
|
||||
self.db.sitter = None
|
||||
# do you sit "on" or "in" this object?
|
||||
self.db.adjective = "on"
|
||||
self.cmdset.add_default(CmdSetSit) # <- new
|
||||
```
|
||||
|
||||
Any _new_ Sittables will now have your `sit` Command. Your existing `armchair` will not,
|
||||
since `at_object_creation` will not re-run for already existing objects. We can update it manually:
|
||||
|
||||
> reload
|
||||
> update armchair
|
||||
|
||||
We could also update all existing sittables (all on one line):
|
||||
|
||||
> py from typeclasses.sittables import Sittable ;
|
||||
[sittable.at_object_creation() for sittable in Sittable.objects.all()]
|
||||
|
||||
> The above shows an example of a _list comprehension_. Think of it as an efficient way to construct a new list
|
||||
all in one line. You can read more about list comprehensions
|
||||
[here in the Python docs](https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions).
|
||||
|
||||
We should now be able to use `sit` while in the room with the armchair.
|
||||
|
||||
> sit
|
||||
As you sit down in armchair, life feels easier.
|
||||
> stand
|
||||
You stand up from armchair.
|
||||
|
||||
One issue with placing the `sit` (or `stand`) Command "on" the chair is that it will not be available when in a
|
||||
room without a Sittable object:
|
||||
|
||||
> sit
|
||||
Command 'sit' is not available. ...
|
||||
|
||||
This is practical but not so good-looking; it makes it harder for the user to know a `sit` action is at all
|
||||
possible. Here is a trick for fixing this. Let's add _another_ Command to the bottom
|
||||
of `mygame/commands/sittables.py`:
|
||||
|
||||
```python
|
||||
# ...
|
||||
|
||||
class CmdNoSitStand(Command):
|
||||
"""
|
||||
Sit down or Stand up
|
||||
"""
|
||||
key = "sit"
|
||||
aliases = ["stand"]
|
||||
|
||||
def func(self):
|
||||
if self.cmdname == "sit":
|
||||
self.msg("You have nothing to sit on.")
|
||||
else:
|
||||
self.msg("You are not sitting down.")
|
||||
|
||||
```
|
||||
|
||||
Here we have a Command that is actually two - it will answer to both `sit` and `stand` since we
|
||||
added `stand` to its `aliases`. In the command we look at `self.cmdname`, which is the string
|
||||
_actually used_ to call this command. We use this to return different messages.
|
||||
|
||||
We don't need a separate CmdSet for this, instead we will add this
|
||||
to the default Character cmdset. Open `mygame/commands/default_cmdsets.py`:
|
||||
|
||||
```python
|
||||
# ...
|
||||
from commands import sittables
|
||||
|
||||
class CharacterCmdSet(CmdSet):
|
||||
"""
|
||||
(docstring)
|
||||
"""
|
||||
def at_cmdset_creation(self):
|
||||
# ...
|
||||
self.add(sittables.CmdNoSitStand)
|
||||
|
||||
```
|
||||
|
||||
To test we'll build a new location without any comfy armchairs and go there:
|
||||
|
||||
> reload
|
||||
> tunnel n = kitchen
|
||||
north
|
||||
> sit
|
||||
You have nothing to sit on.
|
||||
> south
|
||||
sit
|
||||
As you sit down in armchair, life feels easier.
|
||||
|
||||
We now have a fully functioning `sit` action that is contained with the chair itself. When no chair is around, a
|
||||
default error message is shown.
|
||||
|
||||
How does this work? There are two cmdsets at play, both of which have a `sit` Command. As you may remember we
|
||||
set the chair's cmdset to `priority = 1`. This is where that matters. The default Character cmdset has a
|
||||
priority of 0. This means that whenever we enter a room with a Sittable thing, the `sit` command
|
||||
from _its_ cmdset will take _precedence_ over the Character cmdset's version. So we are actually picking
|
||||
_different_ `sit` commands depending on circumstance! The user will never be the wiser.
|
||||
|
||||
So this handles `sit`. What about `stand`? That will work just fine:
|
||||
|
||||
> stand
|
||||
You stand up from armchair.
|
||||
> north
|
||||
> stand
|
||||
You are not sitting down.
|
||||
|
||||
We have one remaining problem with `stand` though - what happens when you are sitting down and try to
|
||||
`stand` in a room with more than one chair:
|
||||
|
||||
> stand
|
||||
More than one match for 'stand' (please narrow target):
|
||||
stand-1 (armchair)
|
||||
stand-2 (sofa)
|
||||
stand-3 (barstool)
|
||||
|
||||
Since all the sittables have the `stand` Command on them, you'll get a multi-match error. This _works_ ... but
|
||||
you could pick _any_ of those sittables to "stand up from". That's really weird and non-intuitive. With `sit` it
|
||||
was okay to get a choice - Evennia can't know which chair we intended to sit on. But we know which chair we
|
||||
sit on so we should only get _its_ `stand` command.
|
||||
|
||||
We will fix this with a `lock` and a custom `lock function`. We want a lock on the `stand` Command that only
|
||||
makes it available when the caller is actually sitting on the chair the `stand` command is on.
|
||||
|
||||
First let's add the lock so we see what we want. Open `mygame/commands/sittables.py`:
|
||||
|
||||
```python
|
||||
# ...
|
||||
|
||||
class CmdStand(Command):
|
||||
"""
|
||||
Stand up.
|
||||
"""
|
||||
key = "stand"
|
||||
lock = "cmd:sitsonthis()" # < this is new
|
||||
|
||||
def func(self):
|
||||
self.obj.do_stand(self.caller)
|
||||
# ...
|
||||
```
|
||||
|
||||
We define a [Lock](../../../Components/Locks.md) on the command. The `cmd:` is in what situation Evennia will check
|
||||
the lock. The `cmd` means that it will check the lock when determining if a user has access to this command or not.
|
||||
What will be checked is the `sitsonthis` _lock function_ which doesn't exist yet.
|
||||
|
||||
Open `mygame/server/conf/lockfuncs.py` to add it!
|
||||
|
||||
```python
|
||||
"""
|
||||
(module lockstring)
|
||||
"""
|
||||
# ...
|
||||
|
||||
def sitsonthis(accessing_obj, accessed_obj, *args, **kwargs):
|
||||
"""
|
||||
True if accessing_obj is sitting on/in the accessed_obj.
|
||||
"""
|
||||
return accessed_obj.db.sitting == accessing_obj
|
||||
|
||||
# ...
|
||||
```
|
||||
|
||||
Evennia knows that all functions in `mygame/server/conf/lockfuncs` should be possible to use in a lock definition.
|
||||
The arguments are required and Evennia will pass all relevant objects to them:
|
||||
|
||||
```{sidebar} Lockfuncs
|
||||
|
||||
Evennia provides a large number of default lockfuncs, such as checking permission-levels,
|
||||
if you are carrying or are inside the accessed object etc. There is no concept of 'sitting'
|
||||
in default Evennia however, so this we need to specify ourselves.
|
||||
|
||||
```
|
||||
|
||||
- `accessing_obj` is the one trying to access the lock. So us, in this case.
|
||||
- `accessed_obj` is the entity we are trying to gain a particular type of access to. So the chair.
|
||||
- `args` is a tuple holding any arguments passed to the lockfunc. Since we use `sitsondthis()` this will
|
||||
be empty (and if we add anything, it will be ignored).
|
||||
- `kwargs` is a tuple of keyword arguments passed to the lockfuncs. This will be empty as well in our example.
|
||||
|
||||
If you are superuser, it's important that you `quell` yourself before trying this out. This is because the superuser
|
||||
bypasses all locks - it can never get locked out, but it means it will also not see the effects of a lock like this.
|
||||
|
||||
> reload
|
||||
> quell
|
||||
> stand
|
||||
You stand up from armchair
|
||||
|
||||
None of the other sittables' `stand` commands passed the lock and only the one we are actually sitting on did.
|
||||
|
||||
Adding a Command to the chair object like this is powerful and a good technique to know. It does come with some
|
||||
caveats though that one needs to keep in mind.
|
||||
|
||||
We'll now try another way to add the `sit/stand` commands.
|
||||
|
||||
## Command variant 2: Command on Character
|
||||
|
||||
Before we start with this, delete the chairs you've created (`del armchair` etc) and then do the following
|
||||
changes:
|
||||
|
||||
- In `mygame/typeclasses/sittables.py`, comment out the line `self.cmdset.add_default(CmdSetSit)`.
|
||||
- In `mygame/commands/default_cmdsets.py`, comment out the line `self.add(sittables.CmdNoSitStand)`.
|
||||
|
||||
This disables the on-object command solution so we can try an alternative. Make sure to `reload` so the
|
||||
changes are known to Evennia.
|
||||
|
||||
In this variation we will put the `sit` and `stand` commands on the `Character` instead of on the chair. This
|
||||
makes some things easier, but makes the Commands themselves more complex because they will not know which
|
||||
chair to sit on. We can't just do `sit` anymore. This is how it will work.
|
||||
|
||||
> sit <chair>
|
||||
You sit on chair.
|
||||
> stand
|
||||
You stand up from chair.
|
||||
|
||||
Open `mygame/commands.sittables.py` again. We'll add a new sit-command. We name the class `CmdSit2` since
|
||||
we already have `CmdSit` from the previous example. We put everything at the end of the module to
|
||||
keep it separate.
|
||||
|
||||
```python
|
||||
from evennia import Command, CmdSet
|
||||
from evennia import InterruptCommand # <- this is new
|
||||
|
||||
class CmdSit(Command):
|
||||
# ...
|
||||
|
||||
# ...
|
||||
|
||||
# new from here
|
||||
|
||||
class CmdSit2(Command):
|
||||
"""
|
||||
Sit down.
|
||||
|
||||
Usage:
|
||||
sit <sittable>
|
||||
|
||||
"""
|
||||
key = "sit"
|
||||
|
||||
def parse(self):
|
||||
self.args = self.args.strip()
|
||||
if not self.args:
|
||||
self.caller.msg("Sit on what?")
|
||||
raise InterruptCommand
|
||||
|
||||
def func(self):
|
||||
|
||||
# self.search handles all error messages etc.
|
||||
sittable = self.caller.search(self.args)
|
||||
if not sittable:
|
||||
return
|
||||
try:
|
||||
sittable.do_sit(self.caller)
|
||||
except AttributeError:
|
||||
self.caller.msg("You can't sit on that!")
|
||||
|
||||
```
|
||||
|
||||
With this Command-variation we need to search for the sittable. A series of methods on the Command
|
||||
are run in sequence:
|
||||
|
||||
1. `Command.at_pre_command` - this is not used by default
|
||||
2. `Command.parse` - this should parse the input
|
||||
3. `Command.func` - this should implement the actual Command functionality
|
||||
4. `Command.at_post_func` - this is not used by default
|
||||
|
||||
So if we just `return` in `.parse`, `.func` will still run, which is not what we want. To immediately
|
||||
abort this sequence we need to `raise InterruptCommand`.
|
||||
|
||||
```{sidebar} Raising exceptions
|
||||
|
||||
Raising an exception allows for immediately interrupting the current program flow. Python
|
||||
automatically raises error-exceptions when detecting problems with the code. It will be
|
||||
raised up through the sequence of called code (the 'stack') until it's either `caught` with
|
||||
a `try ... except` or reaches the outermost scope where it'll be logged or displayed.
|
||||
|
||||
```
|
||||
|
||||
`InterruptCommand` is an _exception_ that the Command-system catches with the understanding that we want
|
||||
to do a clean abort. In the `.parse` method we strip any whitespaces from the argument and
|
||||
sure there actuall _is_ an argument. We abort immediately if there isn't.
|
||||
|
||||
We we get to `.func` at all, we know that we have an argument. We search for this and abort if we there was
|
||||
a problem finding the target.
|
||||
|
||||
> We could have done `raise InterruptCommand` in `.func` as well, but `return` is a little shorter to write
|
||||
> and there is no harm done if `at_post_func` runs since it's empty.
|
||||
|
||||
Next we call the found sittable's `do_sit` method. Note that we wrap this call like this:
|
||||
|
||||
```python
|
||||
|
||||
try:
|
||||
# code
|
||||
except AttributeError:
|
||||
# stuff to do if AttributeError exception was raised
|
||||
```
|
||||
|
||||
The reason is that `caller.search` has no idea we are looking for a Sittable. The user could have tried
|
||||
`sit wall` or `sit sword`. These don't have a `do_sit` method _but we call it anyway and handle the error_.
|
||||
This is a very "Pythonic" thing to do. The concept is often called "leap before you look" or "it's easier to
|
||||
ask for forgiveness than for permission". If `sittable.do_sit` does not exist, Python will raise an `AttributeError`.
|
||||
We catch this with `try ... except AttributeError` and convert it to a proper error message.
|
||||
|
||||
While it's useful to learn about `try ... except`, there is also a way to leverage Evennia to do this without
|
||||
`try ... except`:
|
||||
|
||||
```python
|
||||
|
||||
# ...
|
||||
|
||||
def func(self):
|
||||
|
||||
# self.search handles all error messages etc.
|
||||
sittable = self.caller.search(
|
||||
self.args,
|
||||
typeclass="typeclasses.sittables.Sittable")
|
||||
if not sittable:
|
||||
return
|
||||
sittable.do_sit(self.caller)
|
||||
```
|
||||
|
||||
```{sidebar} Continuing across multiple lines
|
||||
|
||||
Note how the `.search()` method's arguments are spread out over multiple
|
||||
lines. This works for all lists, tuples and other listings and is
|
||||
a good way to avoid very long and hard-to-read lines.
|
||||
|
||||
```
|
||||
|
||||
The `caller.search` method has an keyword argument `typeclass` that can take either a python-path to a
|
||||
typeclass, the typeclass itself, or a list of either to widen the allowed options. In this case we know
|
||||
for sure that the `sittable` we get is actually a `Sittable` class and we can call `sittable.do_sit` without
|
||||
needing to worry about catching errors.
|
||||
|
||||
Let's do the `stand` command while we are at it. Again, since the Command is external to the chair we don't
|
||||
know which object we are sitting in and have to search for it.
|
||||
|
||||
```python
|
||||
|
||||
class CmdStand2(Command):
|
||||
"""
|
||||
Stand up.
|
||||
|
||||
Usage:
|
||||
stand
|
||||
|
||||
"""
|
||||
key = "stand"
|
||||
|
||||
def func(self):
|
||||
|
||||
caller = self.caller
|
||||
# find the thing we are sitting on/in, by finding the object
|
||||
# in the current location that as an Attribute "sitter" set
|
||||
# to the caller
|
||||
sittable = caller.search(
|
||||
caller,
|
||||
candidates=caller.location.contents,
|
||||
attribute_name="sitter",
|
||||
typeclass="typeclasses.sittables.Sittable")
|
||||
# if this is None, the error was already reported to user
|
||||
if not sittable:
|
||||
return
|
||||
|
||||
sittable.do_stand(caller)
|
||||
|
||||
```
|
||||
|
||||
This forced us to to use the full power of the `caller.search` method. If we wanted to search for something
|
||||
more complex we would likely need to break out a [Django query](../Part1/Django-queries.md) to do it. The key here is that
|
||||
we know that the object we are looking for is a `Sittable` and that it must have an Attribute named `sitter`
|
||||
which should be set to us, the one sitting on/in the thing. Once we have that we just call `.do_stand` on it
|
||||
and let the Typeclass handle the rest.
|
||||
|
||||
All that is left now is to make this available to us. This type of Command should be available to us all the time
|
||||
so we can put it in the default Cmdset` on the Character. Open `mygame/default_cmdsets.py`
|
||||
|
||||
|
||||
```python
|
||||
# ...
|
||||
from commands import sittables
|
||||
|
||||
class CharacterCmdSet(CmdSet):
|
||||
"""
|
||||
(docstring)
|
||||
"""
|
||||
def at_cmdset_creation(self):
|
||||
# ...
|
||||
self.add(sittables.CmdSit2)
|
||||
self.add(sittables.CmdStand2)
|
||||
|
||||
```
|
||||
|
||||
Now let's try it out:
|
||||
|
||||
> reload
|
||||
> create/drop sofa : sittables.Sittable
|
||||
> sit sofa
|
||||
You sit down on sofa.
|
||||
> stand
|
||||
You stand up from sofa.
|
||||
|
||||
|
||||
## Conclusions
|
||||
|
||||
In this lesson we accomplished quite a bit:
|
||||
|
||||
- We modified our `Character` class to avoid moving when sitting down.
|
||||
- We made a new `Sittable` typeclass
|
||||
- We tried two ways to allow a user to interact with sittables using `sit` and `stand` commands.
|
||||
|
||||
Eagle-eyed readers will notice that the `stand` command sitting "on" the chair (variant 1) would work just fine
|
||||
together with the `sit` command sitting "on" the Character (variant 2). There is nothing stopping you from
|
||||
mixing them, or even try a third solution that better fits what you have in mind.
|
||||
|
||||
[prev lesson](../../../Unimplemented.md) | [next lesson](../../../Unimplemented.md)
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
# Part 3: How we get there
|
||||
|
||||
```{eval-rst}
|
||||
.. sidebar:: Beginner Tutorial Parts
|
||||
|
||||
`Introduction <../Beginner-Tutorial-Intro.html>`_
|
||||
Getting set up.
|
||||
Part 1: `What we have <../Part1/Beginner-Tutorial-Part1-Intro.html>`_
|
||||
A tour of Evennia and how to use the tools, including an introduction to Python.
|
||||
Part 2: `What we want <../Part2/Beginner-Tutorial-Part2-Intro.html>`_
|
||||
Planning our tutorial game and what to think about when planning your own in the future.
|
||||
**Part 3: How we get there**
|
||||
Getting down to the meat of extending Evennia to make our game
|
||||
Part 4: `Using what we created <../Part4/Beginner-Tutorial-Part4-Intro.html>`_
|
||||
Building a tech-demo and world content to go with our code
|
||||
Part 5: `Showing the world <../Part5/Beginner-Tutorial-Part5-Intro.html>`_
|
||||
Taking our new game online and let players try it out
|
||||
```
|
||||
|
||||
In part three of the Evennia Beginner tutorial we will go through the creation of several key parts of our tutorial
|
||||
game _EvAdventure_. This is a pretty big part with plenty of examples.
|
||||
|
||||
If you followed the previous parts of this tutorial you will have some notions about Python and where to find
|
||||
and make use of things in Evennia. We also have a good idea of the type of game we want.
|
||||
Even if this is not the game-style you are interested in, following along will give you a lot of experience
|
||||
with using Evennia. This be of much use when doing your own thing later.
|
||||
|
||||
|
||||
## Lessons
|
||||
|
||||
_TODO_
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 1
|
||||
|
||||
Implementing-a-game-rule-system
|
||||
Turn-based-Combat-System
|
||||
A-Sittable-Object
|
||||
|
||||
```
|
||||
1. [Changing settings](../../../Unimplemented.md)
|
||||
1. [Applying contribs](../../../Unimplemented.md)
|
||||
1. [Creating a rule module](../../../Unimplemented.md)
|
||||
1. [Tweaking the base Typeclasses](../../../Unimplemented.md)
|
||||
1. [Character creation menu](../../../Unimplemented.md)
|
||||
1. [Wearing armor and wielding weapons](../../../Unimplemented.md)
|
||||
1. [Two types of combat](../../../Unimplemented.md)
|
||||
1. [Monsters and AI](../../../Unimplemented.md)
|
||||
1. [Questing and rewards](../../../Unimplemented.md)
|
||||
1. [Overview of Tech demo](../../../Unimplemented.md)
|
||||
|
||||
|
||||
## Table of Contents
|
||||
|
||||
_TODO_
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 1
|
||||
|
||||
Implementing-a-game-rule-system
|
||||
Turn-Based-Combat-System
|
||||
A-Sittable-Object
|
||||
```
|
||||
|
|
@ -0,0 +1,265 @@
|
|||
# Implementing a game rule system
|
||||
|
||||
|
||||
The simplest way to create an online roleplaying game (at least from a code perspective) is to
|
||||
simply grab a paperback RPG rule book, get a staff of game masters together and start to run scenes
|
||||
with whomever logs in. Game masters can roll their dice in front of their computers and tell the
|
||||
players the results. This is only one step away from a traditional tabletop game and puts heavy
|
||||
demands on the staff - it is unlikely staff will be able to keep up around the clock even if they
|
||||
are very dedicated.
|
||||
|
||||
Many games, even the most roleplay-dedicated, thus tend to allow for players to mediate themselves
|
||||
to some extent. A common way to do this is to introduce *coded systems* - that is, to let the
|
||||
computer do some of the heavy lifting. A basic thing is to add an online dice-roller so everyone can
|
||||
make rolls and make sure noone is cheating. Somewhere at this level you find the most bare-bones
|
||||
roleplaying MUSHes.
|
||||
|
||||
The advantage of a coded system is that as long as the rules are fair the computer is too - it makes
|
||||
no judgement calls and holds no personal grudges (and cannot be accused of holding any). Also, the
|
||||
computer doesn't need to sleep and can always be online regardless of when a player logs on. The
|
||||
drawback is that a coded system is not flexible and won't adapt to the unprogrammed actions human
|
||||
players may come up with in role play. For this reason many roleplay-heavy MUDs do a hybrid
|
||||
variation - they use coded systems for things like combat and skill progression but leave role play
|
||||
to be mostly freeform, overseen by staff game masters.
|
||||
|
||||
Finally, on the other end of the scale are less- or no-roleplay games, where game mechanics (and
|
||||
thus player fairness) is the most important aspect. In such games the only events with in-game value
|
||||
are those resulting from code. Such games are very common and include everything from hack-and-slash
|
||||
MUDs to various tactical simulations.
|
||||
|
||||
So your first decision needs to be just what type of system you are aiming for. This page will try
|
||||
to give some ideas for how to organize the "coded" part of your system, however big that may be.
|
||||
|
||||
## Overall system infrastructure
|
||||
|
||||
We strongly recommend that you code your rule system as stand-alone as possible. That is, don't
|
||||
spread your skill check code, race bonus calculation, die modifiers or what have you all over your
|
||||
game.
|
||||
|
||||
- Put everything you would need to look up in a rule book into a module in `mygame/world`. Hide away
|
||||
as much as you can. Think of it as a black box (or maybe the code representation of an all-knowing
|
||||
game master). The rest of your game will ask this black box questions and get answers back. Exactly
|
||||
how it arrives at those results should not need to be known outside the box. Doing it this way
|
||||
makes it easier to change and update things in one place later.
|
||||
- Store only the minimum stuff you need with each game object. That is, if your Characters need
|
||||
values for Health, a list of skills etc, store those things on the Character - don't store how to
|
||||
roll or change them.
|
||||
- Next is to determine just how you want to store things on your Objects and Characters. You can
|
||||
choose to either store things as individual [Attributes](../../../Components/Attributes.md), like `character.db.STR=34` and
|
||||
`character.db.Hunting_skill=20`. But you could also use some custom storage method, like a
|
||||
dictionary `character.db.skills = {"Hunting":34, "Fishing":20, ...}`. A much more fancy solution is
|
||||
to look at the Ainneve [Trait
|
||||
handler](https://github.com/evennia/ainneve/blob/master/world/traits.py). Finally you could even go
|
||||
with a [custom django model](../../../Concepts/New-Models.md). Which is the better depends on your game and the
|
||||
complexity of your system.
|
||||
- Make a clear [API](https://en.wikipedia.org/wiki/Application_programming_interface) into your
|
||||
rules. That is, make methods/functions that you feed with, say, your Character and which skill you
|
||||
want to check. That is, you want something similar to this:
|
||||
|
||||
```python
|
||||
from world import rules
|
||||
result = rules.roll_skill(character, "hunting")
|
||||
result = rules.roll_challenge(character1, character2, "swords")
|
||||
```
|
||||
|
||||
You might need to make these functions more or less complex depending on your game. For example the
|
||||
properties of the room might matter to the outcome of a roll (if the room is dark, burning etc).
|
||||
Establishing just what you need to send into your game mechanic module is a great way to also get a
|
||||
feel for what you need to add to your engine.
|
||||
|
||||
## Coded systems
|
||||
|
||||
Inspired by tabletop role playing games, most game systems mimic some sort of die mechanic. To this
|
||||
end Evennia offers a full [dice
|
||||
roller](https://github.com/evennia/evennia/blob/master/evennia/contrib/dice.py) in its `contrib`
|
||||
folder. For custom implementations, Python offers many ways to randomize a result using its in-built
|
||||
`random` module. No matter how it's implemented, we will in this text refer to the action of
|
||||
determining an outcome as a "roll".
|
||||
|
||||
In a freeform system, the result of the roll is just compared with values and people (or the game
|
||||
master) just agree on what it means. In a coded system the result now needs to be processed somehow.
|
||||
There are many things that may happen as a result of rule enforcement:
|
||||
|
||||
- Health may be added or deducted. This can effect the character in various ways.
|
||||
- Experience may need to be added, and if a level-based system is used, the player might need to be
|
||||
informed they have increased a level.
|
||||
- Room-wide effects need to be reported to the room, possibly affecting everyone in the room.
|
||||
|
||||
There are also a slew of other things that fall under "Coded systems", including things like
|
||||
weather, NPC artificial intelligence and game economy. Basically everything about the world that a
|
||||
Game master would control in a tabletop role playing game can be mimicked to some level by coded
|
||||
systems.
|
||||
|
||||
|
||||
## Example of Rule module
|
||||
|
||||
Here is a simple example of a rule module. This is what we assume about our simple example game:
|
||||
- Characters have only four numerical values:
|
||||
- Their `level`, which starts at 1.
|
||||
- A skill `combat`, which determines how good they are at hitting things. Starts between 5 and
|
||||
10.
|
||||
- Their Strength, `STR`, which determine how much damage they do. Starts between 1 and 10.
|
||||
- Their Health points, `HP`, which starts at 100.
|
||||
- When a Character reaches `HP = 0`, they are presumed "defeated". Their HP is reset and they get a
|
||||
failure message (as a stand-in for death code).
|
||||
- Abilities are stored as simple Attributes on the Character.
|
||||
- "Rolls" are done by rolling a 100-sided die. If the result is below the `combat` value, it's a
|
||||
success and damage is rolled. Damage is rolled as a six-sided die + the value of `STR` (for this
|
||||
example we ignore weapons and assume `STR` is all that matters).
|
||||
- Every successful `attack` roll gives 1-3 experience points (`XP`). Every time the number of `XP`
|
||||
reaches `(level + 1) ** 2`, the Character levels up. When leveling up, the Character's `combat`
|
||||
value goes up by 2 points and `STR` by one (this is a stand-in for a real progression system).
|
||||
|
||||
### Character
|
||||
|
||||
The Character typeclass is simple. It goes in `mygame/typeclasses/characters.py`. There is already
|
||||
an empty `Character` class there that Evennia will look to and use.
|
||||
|
||||
```python
|
||||
from random import randint
|
||||
from evennia import DefaultCharacter
|
||||
|
||||
class Character(DefaultCharacter):
|
||||
"""
|
||||
Custom rule-restricted character. We randomize
|
||||
the initial skill and ability values bettween 1-10.
|
||||
"""
|
||||
def at_object_creation(self):
|
||||
"Called only when first created"
|
||||
self.db.level = 1
|
||||
self.db.HP = 100
|
||||
self.db.XP = 0
|
||||
self.db.STR = randint(1, 10)
|
||||
self.db.combat = randint(5, 10)
|
||||
```
|
||||
|
||||
`@reload` the server to load up the new code. Doing `examine self` will however *not* show the new
|
||||
Attributes on yourself. This is because the `at_object_creation` hook is only called on *new*
|
||||
Characters. Your Character was already created and will thus not have them. To force a reload, use
|
||||
the following command:
|
||||
|
||||
```
|
||||
@typeclass/force/reset self
|
||||
```
|
||||
|
||||
The `examine self` command will now show the new Attributes.
|
||||
|
||||
### Rule module
|
||||
|
||||
This is a module `mygame/world/rules.py`.
|
||||
|
||||
```python
|
||||
from random import randint
|
||||
|
||||
def roll_hit():
|
||||
"Roll 1d100"
|
||||
return randint(1, 100)
|
||||
|
||||
def roll_dmg():
|
||||
"Roll 1d6"
|
||||
return randint(1, 6)
|
||||
|
||||
def check_defeat(character):
|
||||
"Checks if a character is 'defeated'."
|
||||
if character.db.HP <= 0:
|
||||
character.msg("You fall down, defeated!")
|
||||
character.db.HP = 100 # reset
|
||||
|
||||
def add_XP(character, amount):
|
||||
"Add XP to character, tracking level increases."
|
||||
character.db.XP += amount
|
||||
if character.db.XP >= (character.db.level + 1) ** 2:
|
||||
character.db.level += 1
|
||||
character.db.STR += 1
|
||||
character.db.combat += 2
|
||||
character.msg(f"You are now level {character.db.level}!")
|
||||
|
||||
def skill_combat(*args):
|
||||
"""
|
||||
This determines outcome of combat. The one who
|
||||
rolls under their combat skill AND higher than
|
||||
their opponent's roll hits.
|
||||
"""
|
||||
char1, char2 = args
|
||||
roll1, roll2 = roll_hit(), roll_hit()
|
||||
failtext_template = "You are hit by {attacker} for {dmg} damage!"
|
||||
wintext_template = "You hit {target} for {dmg} damage!"
|
||||
xp_gain = randint(1, 3)
|
||||
if char1.db.combat >= roll1 > roll2:
|
||||
# char 1 hits
|
||||
dmg = roll_dmg() + char1.db.STR
|
||||
char1.msg(wintext_template.format(target=char2, dmg=dmg))
|
||||
add_XP(char1, xp_gain)
|
||||
char2.msg(failtext_template.format(attacker=char1, dmg=dmg))
|
||||
char2.db.HP -= dmg
|
||||
check_defeat(char2)
|
||||
elif char2.db.combat >= roll2 > roll1:
|
||||
# char 2 hits
|
||||
dmg = roll_dmg() + char2.db.STR
|
||||
char1.msg(failtext_template.format(attacker=char2, dmg=dmg))
|
||||
char1.db.HP -= dmg
|
||||
check_defeat(char1)
|
||||
char2.msg(wintext_template.format(target=char1, dmg=dmg))
|
||||
add_XP(char2, xp_gain)
|
||||
else:
|
||||
# a draw
|
||||
drawtext = "Neither of you can find an opening."
|
||||
char1.msg(drawtext)
|
||||
char2.msg(drawtext)
|
||||
|
||||
SKILLS = {"combat": skill_combat}
|
||||
|
||||
def roll_challenge(character1, character2, skillname):
|
||||
"""
|
||||
Determine the outcome of a skill challenge between
|
||||
two characters based on the skillname given.
|
||||
"""
|
||||
if skillname in SKILLS:
|
||||
SKILLS[skillname](character1, character2)
|
||||
else:
|
||||
raise RunTimeError(f"Skillname {skillname} not found.")
|
||||
```
|
||||
|
||||
These few functions implement the entirety of our simple rule system. We have a function to check
|
||||
the "defeat" condition and reset the `HP` back to 100 again. We define a generic "skill" function.
|
||||
Multiple skills could all be added with the same signature; our `SKILLS` dictionary makes it easy to
|
||||
look up the skills regardless of what their actual functions are called. Finally, the access
|
||||
function `roll_challenge` just picks the skill and gets the result.
|
||||
|
||||
In this example, the skill function actually does a lot - it not only rolls results, it also informs
|
||||
everyone of their results via `character.msg()` calls.
|
||||
|
||||
Here is an example of usage in a game command:
|
||||
|
||||
```python
|
||||
from evennia import Command
|
||||
from world import rules
|
||||
|
||||
class CmdAttack(Command):
|
||||
"""
|
||||
attack an opponent
|
||||
|
||||
Usage:
|
||||
attack <target>
|
||||
|
||||
This will attack a target in the same room, dealing
|
||||
damage with your bare hands.
|
||||
"""
|
||||
def func(self):
|
||||
"Implementing combat"
|
||||
|
||||
caller = self.caller
|
||||
if not self.args:
|
||||
caller.msg("You need to pick a target to attack.")
|
||||
return
|
||||
|
||||
target = caller.search(self.args)
|
||||
if target:
|
||||
rules.roll_challenge(caller, target, "combat")
|
||||
```
|
||||
|
||||
Note how simple the command becomes and how generic you can make it. It becomes simple to offer any
|
||||
number of Combat commands by just extending this functionality - you can easily roll challenges and
|
||||
pick different skills to check. And if you ever decided to, say, change how to determine hit chance,
|
||||
you don't have to change every command, but need only change the single `roll_hit` function inside
|
||||
your `rules` module.
|
||||
|
|
@ -0,0 +1,521 @@
|
|||
# Turn based Combat System
|
||||
|
||||
|
||||
This tutorial gives an example of a full, if simplified, combat system for Evennia. It was inspired
|
||||
by the discussions held on the [mailing
|
||||
list](https://groups.google.com/forum/#!msg/evennia/wnJNM2sXSfs/-dbLRrgWnYMJ).
|
||||
|
||||
## Overview of combat system concepts
|
||||
|
||||
Most MUDs will use some sort of combat system. There are several main variations:
|
||||
|
||||
- _Freeform_ - the simplest form of combat to implement, common to MUSH-style roleplaying games.
|
||||
This means the system only supplies dice rollers or maybe commands to compare skills and spit out
|
||||
the result. Dice rolls are done to resolve combat according to the rules of the game and to direct
|
||||
the scene. A game master may be required to resolve rule disputes.
|
||||
- _Twitch_ - This is the traditional MUD hack&slash style combat. In a twitch system there is often
|
||||
no difference between your normal "move-around-and-explore mode" and the "combat mode". You enter an
|
||||
attack command and the system will calculate if the attack hits and how much damage was caused.
|
||||
Normally attack commands have some sort of timeout or notion of recovery/balance to reduce the
|
||||
advantage of spamming or client scripting. Whereas the simplest systems just means entering `kill
|
||||
<target>` over and over, more sophisticated twitch systems include anything from defensive stances
|
||||
to tactical positioning.
|
||||
- _Turn-based_ - a turn based system means that the system pauses to make sure all combatants can
|
||||
choose their actions before continuing. In some systems, such entered actions happen immediately
|
||||
(like twitch-based) whereas in others the resolution happens simultaneously at the end of the turn.
|
||||
The disadvantage of a turn-based system is that the game must switch to a "combat mode" and one also
|
||||
needs to take special care of how to handle new combatants and the passage of time. The advantage is
|
||||
that success is not dependent on typing speed or of setting up quick client macros. This potentially
|
||||
allows for emoting as part of combat which is an advantage for roleplay-heavy games.
|
||||
|
||||
To implement a freeform combat system all you need is a dice roller and a roleplaying rulebook. See
|
||||
[contrib/dice.py](https://github.com/evennia/evennia/blob/master/evennia/contrib/dice.py) for an
|
||||
example dice roller. To implement at twitch-based system you basically need a few combat
|
||||
[commands](../../../Components/Commands.md), possibly ones with a [cooldown](../../Command-Cooldown.md). You also need a [game rule
|
||||
module](./Implementing-a-game-rule-system.md) that makes use of it. We will focus on the turn-based
|
||||
variety here.
|
||||
|
||||
## Tutorial overview
|
||||
|
||||
This tutorial will implement the slightly more complex turn-based combat system. Our example has the
|
||||
following properties:
|
||||
|
||||
- Combat is initiated with `attack <target>`, this initiates the combat mode.
|
||||
- Characters may join an ongoing battle using `attack <target>` against a character already in
|
||||
combat.
|
||||
- Each turn every combating character will get to enter two commands, their internal order matters
|
||||
and they are compared one-to-one in the order given by each combatant. Use of `say` and `pose` is
|
||||
free.
|
||||
- The commands are (in our example) simple; they can either `hit <target>`, `feint <target>` or
|
||||
`parry <target>`. They can also `defend`, a generic passive defense. Finally they may choose to
|
||||
`disengage/flee`.
|
||||
- When attacking we use a classic [rock-paper-scissors](https://en.wikipedia.org/wiki/Rock-paper-
|
||||
scissors) mechanic to determine success: `hit` defeats `feint`, which defeats `parry` which defeats
|
||||
`hit`. `defend` is a general passive action that has a percentage chance to win against `hit`
|
||||
(only).
|
||||
- `disengage/flee` must be entered two times in a row and will only succeed if there is no `hit`
|
||||
against them in that time. If so they will leave combat mode.
|
||||
- Once every player has entered two commands, all commands are resolved in order and the result is
|
||||
reported. A new turn then begins.
|
||||
- If players are too slow the turn will time out and any unset commands will be set to `defend`.
|
||||
|
||||
For creating the combat system we will need the following components:
|
||||
|
||||
- A combat handler. This is the main mechanic of the system. This is a [Script](../../../Components/Scripts.md) object
|
||||
created for each combat. It is not assigned to a specific object but is shared by the combating
|
||||
characters and handles all the combat information. Since Scripts are database entities it also means
|
||||
that the combat will not be affected by a server reload.
|
||||
- A combat [command set](../../../Components/Command-Sets.md) with the relevant commands needed for combat, such as the
|
||||
various attack/defend options and the `flee/disengage` command to leave the combat mode.
|
||||
- A rule resolution system. The basics of making such a module is described in the [rule system
|
||||
tutorial](./Implementing-a-game-rule-system.md). We will only sketch such a module here for our end-turn
|
||||
combat resolution.
|
||||
- An `attack` [command](../../../Components/Commands.md) for initiating the combat mode. This is added to the default
|
||||
command set. It will create the combat handler and add the character(s) to it. It will also assign
|
||||
the combat command set to the characters.
|
||||
|
||||
## The combat handler
|
||||
|
||||
The _combat handler_ is implemented as a stand-alone [Script](../../../Components/Scripts.md). This Script is created when
|
||||
the first Character decides to attack another and is deleted when no one is fighting any more. Each
|
||||
handler represents one instance of combat and one combat only. Each instance of combat can hold any
|
||||
number of characters but each character can only be part of one combat at a time (a player would
|
||||
need to disengage from the first combat before they could join another).
|
||||
|
||||
The reason we don't store this Script "on" any specific character is because any character may leave
|
||||
the combat at any time. Instead the script holds references to all characters involved in the
|
||||
combat. Vice-versa, all characters holds a back-reference to the current combat handler. While we
|
||||
don't use this very much here this might allow the combat commands on the characters to access and
|
||||
update the combat handler state directly.
|
||||
|
||||
_Note: Another way to implement a combat handler would be to use a normal Python object and handle
|
||||
time-keeping with the [TickerHandler](../../../Components/TickerHandler.md). This would require either adding custom hook
|
||||
methods on the character or to implement a custom child of the TickerHandler class to track turns.
|
||||
Whereas the TickerHandler is easy to use, a Script offers more power in this case._
|
||||
|
||||
Here is a basic combat handler. Assuming our game folder is named `mygame`, we store it in
|
||||
`mygame/typeclasses/combat_handler.py`:
|
||||
|
||||
```python
|
||||
# mygame/typeclasses/combat_handler.py
|
||||
|
||||
import random
|
||||
from evennia import DefaultScript
|
||||
from world.rules import resolve_combat
|
||||
|
||||
class CombatHandler(DefaultScript):
|
||||
"""
|
||||
This implements the combat handler.
|
||||
"""
|
||||
|
||||
# standard Script hooks
|
||||
|
||||
def at_script_creation(self):
|
||||
"Called when script is first created"
|
||||
|
||||
self.key = f"combat_handler_{random.randint(1, 1000)}"
|
||||
self.desc = "handles combat"
|
||||
self.interval = 60 * 2 # two minute timeout
|
||||
self.start_delay = True
|
||||
self.persistent = True
|
||||
|
||||
# store all combatants
|
||||
self.db.characters = {}
|
||||
# store all actions for each turn
|
||||
self.db.turn_actions = {}
|
||||
# number of actions entered per combatant
|
||||
self.db.action_count = {}
|
||||
|
||||
def _init_character(self, character):
|
||||
"""
|
||||
This initializes handler back-reference
|
||||
and combat cmdset on a character
|
||||
"""
|
||||
character.ndb.combat_handler = self
|
||||
character.cmdset.add("commands.combat.CombatCmdSet")
|
||||
|
||||
def _cleanup_character(self, character):
|
||||
"""
|
||||
Remove character from handler and clean
|
||||
it of the back-reference and cmdset
|
||||
"""
|
||||
dbref = character.id
|
||||
del self.db.characters[dbref]
|
||||
del self.db.turn_actions[dbref]
|
||||
del self.db.action_count[dbref]
|
||||
del character.ndb.combat_handler
|
||||
character.cmdset.delete("commands.combat.CombatCmdSet")
|
||||
|
||||
def at_start(self):
|
||||
"""
|
||||
This is called on first start but also when the script is restarted
|
||||
after a server reboot. We need to re-assign this combat handler to
|
||||
all characters as well as re-assign the cmdset.
|
||||
"""
|
||||
for character in self.db.characters.values():
|
||||
self._init_character(character)
|
||||
|
||||
def at_stop(self):
|
||||
"Called just before the script is stopped/destroyed."
|
||||
for character in list(self.db.characters.values()):
|
||||
# note: the list() call above disconnects list from database
|
||||
self._cleanup_character(character)
|
||||
|
||||
def at_repeat(self):
|
||||
"""
|
||||
This is called every self.interval seconds (turn timeout) or
|
||||
when force_repeat is called (because everyone has entered their
|
||||
commands). We know this by checking the existence of the
|
||||
`normal_turn_end` NAttribute, set just before calling
|
||||
force_repeat.
|
||||
|
||||
"""
|
||||
if self.ndb.normal_turn_end:
|
||||
# we get here because the turn ended normally
|
||||
# (force_repeat was called) - no msg output
|
||||
del self.ndb.normal_turn_end
|
||||
else:
|
||||
# turn timeout
|
||||
self.msg_all("Turn timer timed out. Continuing.")
|
||||
self.end_turn()
|
||||
|
||||
# Combat-handler methods
|
||||
|
||||
def add_character(self, character):
|
||||
"Add combatant to handler"
|
||||
dbref = character.id
|
||||
self.db.characters[dbref] = character
|
||||
self.db.action_count[dbref] = 0
|
||||
self.db.turn_actions[dbref] = [("defend", character, None),
|
||||
("defend", character, None)]
|
||||
# set up back-reference
|
||||
self._init_character(character)
|
||||
|
||||
def remove_character(self, character):
|
||||
"Remove combatant from handler"
|
||||
if character.id in self.db.characters:
|
||||
self._cleanup_character(character)
|
||||
if not self.db.characters:
|
||||
# if no more characters in battle, kill this handler
|
||||
self.stop()
|
||||
|
||||
def msg_all(self, message):
|
||||
"Send message to all combatants"
|
||||
for character in self.db.characters.values():
|
||||
character.msg(message)
|
||||
|
||||
def add_action(self, action, character, target):
|
||||
"""
|
||||
Called by combat commands to register an action with the handler.
|
||||
|
||||
action - string identifying the action, like "hit" or "parry"
|
||||
character - the character performing the action
|
||||
target - the target character or None
|
||||
|
||||
actions are stored in a dictionary keyed to each character, each
|
||||
of which holds a list of max 2 actions. An action is stored as
|
||||
a tuple (character, action, target).
|
||||
"""
|
||||
dbref = character.id
|
||||
count = self.db.action_count[dbref]
|
||||
if 0 <= count <= 1: # only allow 2 actions
|
||||
self.db.turn_actions[dbref][count] = (action, character, target)
|
||||
else:
|
||||
# report if we already used too many actions
|
||||
return False
|
||||
self.db.action_count[dbref] += 1
|
||||
return True
|
||||
|
||||
def check_end_turn(self):
|
||||
"""
|
||||
Called by the command to eventually trigger
|
||||
the resolution of the turn. We check if everyone
|
||||
has added all their actions; if so we call force the
|
||||
script to repeat immediately (which will call
|
||||
`self.at_repeat()` while resetting all timers).
|
||||
"""
|
||||
if all(count > 1 for count in self.db.action_count.values()):
|
||||
self.ndb.normal_turn_end = True
|
||||
self.force_repeat()
|
||||
|
||||
def end_turn(self):
|
||||
"""
|
||||
This resolves all actions by calling the rules module.
|
||||
It then resets everything and starts the next turn. It
|
||||
is called by at_repeat().
|
||||
"""
|
||||
resolve_combat(self, self.db.turn_actions)
|
||||
|
||||
if len(self.db.characters) < 2:
|
||||
# less than 2 characters in battle, kill this handler
|
||||
self.msg_all("Combat has ended")
|
||||
self.stop()
|
||||
else:
|
||||
# reset counters before next turn
|
||||
for character in self.db.characters.values():
|
||||
self.db.characters[character.id] = character
|
||||
self.db.action_count[character.id] = 0
|
||||
self.db.turn_actions[character.id] = [("defend", character, None),
|
||||
("defend", character, None)]
|
||||
self.msg_all("Next turn begins ...")
|
||||
```
|
||||
|
||||
This implements all the useful properties of our combat handler. This Script will survive a reboot
|
||||
and will automatically re-assert itself when it comes back online. Even the current state of the
|
||||
combat should be unaffected since it is saved in Attributes at every turn. An important part to note
|
||||
is the use of the Script's standard `at_repeat` hook and the `force_repeat` method to end each turn.
|
||||
This allows for everything to go through the same mechanisms with minimal repetition of code.
|
||||
|
||||
What is not present in this handler is a way for players to view the actions they set or to change
|
||||
their actions once they have been added (but before the last one has added theirs). We leave this as
|
||||
an exercise.
|
||||
|
||||
## Combat commands
|
||||
|
||||
Our combat commands - the commands that are to be available to us during the combat - are (in our
|
||||
example) very simple. In a full implementation the commands available might be determined by the
|
||||
weapon(s) held by the player or by which skills they know.
|
||||
|
||||
We create them in `mygame/commands/combat.py`.
|
||||
|
||||
```python
|
||||
# mygame/commands/combat.py
|
||||
|
||||
from evennia import Command
|
||||
|
||||
class CmdHit(Command):
|
||||
"""
|
||||
hit an enemy
|
||||
|
||||
Usage:
|
||||
hit <target>
|
||||
|
||||
Strikes the given enemy with your current weapon.
|
||||
"""
|
||||
key = "hit"
|
||||
aliases = ["strike", "slash"]
|
||||
help_category = "combat"
|
||||
|
||||
def func(self):
|
||||
"Implements the command"
|
||||
if not self.args:
|
||||
self.caller.msg("Usage: hit <target>")
|
||||
return
|
||||
target = self.caller.search(self.args)
|
||||
if not target:
|
||||
return
|
||||
ok = self.caller.ndb.combat_handler.add_action("hit",
|
||||
self.caller,
|
||||
target)
|
||||
if ok:
|
||||
self.caller.msg("You add 'hit' to the combat queue")
|
||||
else:
|
||||
self.caller.msg("You can only queue two actions per turn!")
|
||||
|
||||
# tell the handler to check if turn is over
|
||||
self.caller.ndb.combat_handler.check_end_turn()
|
||||
```
|
||||
|
||||
The other commands `CmdParry`, `CmdFeint`, `CmdDefend` and `CmdDisengage` look basically the same.
|
||||
We should also add a custom `help` command to list all the available combat commands and what they
|
||||
do.
|
||||
|
||||
We just need to put them all in a cmdset. We do this at the end of the same module:
|
||||
|
||||
```python
|
||||
# mygame/commands/combat.py
|
||||
|
||||
from evennia import CmdSet
|
||||
from evennia import default_cmds
|
||||
|
||||
class CombatCmdSet(CmdSet):
|
||||
key = "combat_cmdset"
|
||||
mergetype = "Replace"
|
||||
priority = 10
|
||||
no_exits = True
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
self.add(CmdHit())
|
||||
self.add(CmdParry())
|
||||
self.add(CmdFeint())
|
||||
self.add(CmdDefend())
|
||||
self.add(CmdDisengage())
|
||||
self.add(CmdHelp())
|
||||
self.add(default_cmds.CmdPose())
|
||||
self.add(default_cmds.CmdSay())
|
||||
```
|
||||
|
||||
## Rules module
|
||||
|
||||
A general way to implement a rule module is found in the [rule system tutorial](Implementing-a-game-
|
||||
rule-system). Proper resolution would likely require us to change our Characters to store things
|
||||
like strength, weapon skills and so on. So for this example we will settle for a very simplistic
|
||||
rock-paper-scissors kind of setup with some randomness thrown in. We will not deal with damage here
|
||||
but just announce the results of each turn. In a real system the Character objects would hold stats
|
||||
to affect their skills, their chosen weapon affect the choices, they would be able to lose health
|
||||
etc.
|
||||
|
||||
Within each turn, there are "sub-turns", each consisting of one action per character. The actions
|
||||
within each sub-turn happens simultaneously and only once they have all been resolved we move on to
|
||||
the next sub-turn (or end the full turn).
|
||||
|
||||
*Note: In our simple example the sub-turns don't affect each other (except for `disengage/flee`),
|
||||
nor do any effects carry over between turns. The real power of a turn-based system would be to add
|
||||
real tactical possibilities here though; For example if your hit got parried you could be out of
|
||||
balance and your next action would be at a disadvantage. A successful feint would open up for a
|
||||
subsequent attack and so on ...*
|
||||
|
||||
Our rock-paper-scissor setup works like this:
|
||||
|
||||
- `hit` beats `feint` and `flee/disengage`. It has a random chance to fail against `defend`.
|
||||
- `parry` beats `hit`.
|
||||
- `feint` beats `parry` and is then counted as a `hit`.
|
||||
- `defend` does nothing but has a chance to beat `hit`.
|
||||
- `flee/disengage` must succeed two times in a row (i.e. not beaten by a `hit` once during the
|
||||
turn). If so the character leaves combat.
|
||||
|
||||
|
||||
```python
|
||||
# mygame/world/rules.py
|
||||
|
||||
import random
|
||||
|
||||
# messages
|
||||
|
||||
def resolve_combat(combat_handler, actiondict):
|
||||
"""
|
||||
This is called by the combat handler
|
||||
actiondict is a dictionary with a list of two actions
|
||||
for each character:
|
||||
{char.id:[(action1, char, target), (action2, char, target)], ...}
|
||||
"""
|
||||
flee = {} # track number of flee commands per character
|
||||
for isub in range(2):
|
||||
# loop over sub-turns
|
||||
messages = []
|
||||
for subturn in (sub[isub] for sub in actiondict.values()):
|
||||
# for each character, resolve the sub-turn
|
||||
action, char, target = subturn
|
||||
if target:
|
||||
taction, tchar, ttarget = actiondict[target.id][isub]
|
||||
if action == "hit":
|
||||
if taction == "parry" and ttarget == char:
|
||||
messages.append(
|
||||
f"{char} tries to hit {tchar}, but {tchar} parries the attack!"
|
||||
)
|
||||
elif taction == "defend" and random.random() < 0.5:
|
||||
messages.append(
|
||||
f"{tchar} defends against the attack by {char}."
|
||||
)
|
||||
elif taction == "flee":
|
||||
flee[tchar] = -2
|
||||
messages.append(
|
||||
f"{char} stops {tchar} from disengaging, with a hit!"
|
||||
)
|
||||
else:
|
||||
messages.append(
|
||||
f"{char} hits {tchar}, bypassing their {taction}!"
|
||||
)
|
||||
elif action == "parry":
|
||||
if taction == "hit":
|
||||
messages.append(f"{char} parries the attack by {tchar}.")
|
||||
elif taction == "feint":
|
||||
messages.append(
|
||||
f"{char} tries to parry, but {tchar} feints and hits!"
|
||||
)
|
||||
else:
|
||||
messages.append(f"{char} parries to no avail.")
|
||||
elif action == "feint":
|
||||
if taction == "parry":
|
||||
messages.append(
|
||||
f"{char} feints past {tchar}'s parry, landing a hit!"
|
||||
)
|
||||
elif taction == "hit":
|
||||
messages.append(f"{char} feints but is defeated by {tchar}'s hit!")
|
||||
else:
|
||||
messages.append(f"{char} feints to no avail.")
|
||||
elif action == "defend":
|
||||
messages.append(f"{char} defends.")
|
||||
elif action == "flee":
|
||||
if char in flee:
|
||||
flee[char] += 1
|
||||
else:
|
||||
flee[char] = 1
|
||||
messages.append(
|
||||
f"{char} tries to disengage (two subsequent turns needed)"
|
||||
)
|
||||
|
||||
# echo results of each subturn
|
||||
combat_handler.msg_all("\n".join(messages))
|
||||
|
||||
# at the end of both sub-turns, test if anyone fled
|
||||
for (char, fleevalue) in flee.items():
|
||||
if fleevalue == 2:
|
||||
combat_handler.msg_all(f"{char} withdraws from combat.")
|
||||
combat_handler.remove_character(char)
|
||||
```
|
||||
|
||||
To make it simple (and to save space), this example rule module actually resolves each interchange
|
||||
twice - first when it gets to each character and then again when handling the target. Also, since we
|
||||
use the combat handler's `msg_all` method here, the system will get pretty spammy. To clean it up,
|
||||
one could imagine tracking all the possible interactions to make sure each pair is only handled and
|
||||
reported once.
|
||||
|
||||
## Combat initiator command
|
||||
|
||||
This is the last component we need, a command to initiate combat. This will tie everything together.
|
||||
We store this with the other combat commands.
|
||||
|
||||
```python
|
||||
# mygame/commands/combat.py
|
||||
|
||||
from evennia import create_script
|
||||
|
||||
class CmdAttack(Command):
|
||||
"""
|
||||
initiates combat
|
||||
|
||||
Usage:
|
||||
attack <target>
|
||||
|
||||
This will initiate combat with <target>. If <target is
|
||||
already in combat, you will join the combat.
|
||||
"""
|
||||
key = "attack"
|
||||
help_category = "General"
|
||||
|
||||
def func(self):
|
||||
"Handle command"
|
||||
if not self.args:
|
||||
self.caller.msg("Usage: attack <target>")
|
||||
return
|
||||
target = self.caller.search(self.args)
|
||||
if not target:
|
||||
return
|
||||
# set up combat
|
||||
if target.ndb.combat_handler:
|
||||
# target is already in combat - join it
|
||||
target.ndb.combat_handler.add_character(self.caller)
|
||||
target.ndb.combat_handler.msg_all(f"{self.caller} joins combat!")
|
||||
else:
|
||||
# create a new combat handler
|
||||
chandler = create_script("combat_handler.CombatHandler")
|
||||
chandler.add_character(self.caller)
|
||||
chandler.add_character(target)
|
||||
self.caller.msg(f"You attack {target}! You are in combat.")
|
||||
target.msg(f"{self.caller} attacks you! You are in combat.")
|
||||
```
|
||||
|
||||
The `attack` command will not go into the combat cmdset but rather into the default cmdset. See e.g.
|
||||
the [Adding Command Tutorial](../Part1/Adding-Commands.md) if you are unsure about how to do this.
|
||||
|
||||
## Expanding the example
|
||||
|
||||
At this point you should have a simple but flexible turn-based combat system. We have taken several
|
||||
shortcuts and simplifications in this example. The output to the players is likely too verbose
|
||||
during combat and too limited when it comes to informing about things surrounding it. Methods for
|
||||
changing your commands or list them, view who is in combat etc is likely needed - this will require
|
||||
play testing for each game and style. There is also currently no information displayed for other
|
||||
people happening to be in the same room as the combat - some less detailed information should
|
||||
probably be echoed to the room to
|
||||
show others what's going on.
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
# Part 4: Using what we created
|
||||
|
||||
```{eval-rst}
|
||||
..sidebar:: Beginner Tutorial Parts
|
||||
|
||||
`Introduction <../Beginner-Tutorial-Intro.html>`_
|
||||
Getting set up.
|
||||
Part 1: `What we have <../Part1/Beginner-Tutorial-Part1-Intro.html>`_
|
||||
A tour of Evennia and how to use the tools, including an introduction to Python.
|
||||
Part 2: `What we want <../Part2/Beginner-Tutorial-Part2-Intro.html>`_
|
||||
Planning our tutorial game and what to think about when planning your own in the future.
|
||||
Part 3: `How we get there <../Part3/Beginner-Tutorial-Part3-Intro.html>`_
|
||||
Getting down to the meat of extending Evennia to make our game to make a tech-demo
|
||||
**Part 4: Using what we created**
|
||||
Using the tech-demo and world content to go with our code
|
||||
Part 5: `Showing the world <../Part5/Beginner-Tutorial-Part5-Intro.html>`_
|
||||
Taking our new game online and let players try it out
|
||||
```
|
||||
|
||||
We now have the code underpinnings of everything we need. We have also tested the various components
|
||||
and has a simple tech-demo to show it all works together. But there is no real coherence to it at this
|
||||
point - we need to actually make a world.
|
||||
In part four we will expand our tech demo into a more full-fledged (if small) game by use of batchcommand
|
||||
and batchcode processors.
|
||||
|
||||
## Lessons
|
||||
|
||||
_TODO_
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 1
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Table of Contents
|
||||
|
||||
_TODO_
|
||||
|
||||
```{toctree}
|
||||
|
||||
```
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
# Add a simple new web page
|
||||
|
||||
|
||||
Evennia leverages [Django](https://docs.djangoproject.com) which is a web development framework.
|
||||
Huge professional websites are made in Django and there is extensive documentation (and books) on it
|
||||
. You are encouraged to at least look at the Django basic tutorials. Here we will just give a brief
|
||||
introduction for how things hang together, to get you started.
|
||||
|
||||
We assume you have installed and set up Evennia to run. A webserver and website comes out of the
|
||||
box. You can get to that by entering `http://localhost:4001` in your web browser - you should see a
|
||||
welcome page with some game statistics and a link to the web client. Let us add a new page that you
|
||||
can get to by going to `http://localhost:4001/story`.
|
||||
|
||||
## Create the view
|
||||
|
||||
A django "view" is a normal Python function that django calls to render the HTML page you will see
|
||||
in the web browser. Here we will just have it spit back the raw html, but Django can do all sorts of
|
||||
cool stuff with the page in the view, like adding dynamic content or change it on the fly. Open
|
||||
`mygame/web` folder and add a new module there named `story.py` (you could also put it in its own
|
||||
folder if you wanted to be neat. Don't forget to add an empty `__init__.py` file if you do, to tell
|
||||
Python you can import from the new folder). Here's how it looks:
|
||||
|
||||
```python
|
||||
# in mygame/web/story.py
|
||||
|
||||
from django.shortcuts import render
|
||||
|
||||
def storypage(request):
|
||||
return render(request, "story.html")
|
||||
```
|
||||
|
||||
This view takes advantage of a shortcut provided to use by Django, _render_. This shortcut gives the
|
||||
template some information from the request, for instance, the game name, and then renders it.
|
||||
|
||||
## The HTML page
|
||||
|
||||
We need to find a place where Evennia (and Django) looks for html files (called *templates* in
|
||||
Django parlance). You can specify such places in your settings (see the `TEMPLATES` variable in
|
||||
`default_settings.py` for more info), but here we'll use an existing one. Go to
|
||||
`mygame/template/overrides/website/` and create a page `story.html` there.
|
||||
|
||||
This is not a HTML tutorial, so we'll go simple:
|
||||
|
||||
```html
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1>A story about a tree</h1>
|
||||
<p>
|
||||
This is a story about a tree, a classic tale ...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
Since we've used the _render_ shortcut, Django will allow us to extend our base styles easily.
|
||||
|
||||
If you'd rather not take advantage of Evennia's base styles, you can do something like this instead:
|
||||
|
||||
```html
|
||||
<html>
|
||||
<body>
|
||||
<h1>A story about a tree</h1>
|
||||
<p>
|
||||
This is a story about a tree, a classic tale ...
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
|
||||
## The URL
|
||||
|
||||
When you enter the address `http://localhost:4001/story` in your web browser, Django will parse that
|
||||
field to figure out which page you want to go to. You tell it which patterns are relevant in the
|
||||
file
|
||||
[mygame/web/urls.py](https://github.com/evennia/evennia/blob/master/evennia/game_template/web/urls.py).
|
||||
Open it now.
|
||||
|
||||
Django looks for the variable `urlpatterns` in this file. You want to add your new pattern to the
|
||||
`custom_patterns` list we have prepared - that is then merged with the default `urlpatterns`. Here's
|
||||
how it could look:
|
||||
|
||||
```python
|
||||
from web import story
|
||||
|
||||
# ...
|
||||
|
||||
custom_patterns = [
|
||||
url(r'story', story.storypage, name='Story'),
|
||||
]
|
||||
```
|
||||
|
||||
That is, we import our story view module from where we created it earlier and then create an `url`
|
||||
instance. The first argument to `url` is the pattern of the url we want to find (`"story"`) (this is
|
||||
a regular expression if you are familiar with those) and then our view function we want to direct
|
||||
to.
|
||||
|
||||
That should be it. Reload Evennia and you should be able to browse to your new story page!
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
# Part 5: Showing the world
|
||||
|
||||
```{eval-rst}
|
||||
.. sidebar:: Beginner Tutorial Parts
|
||||
|
||||
`Introduction <../Beginner-Tutorial-Intro.html>`_
|
||||
Getting set up.
|
||||
Part 1: `What we have <../Part1/Beginner-Tutorial-Part1-Intro.html>`_
|
||||
A tour of Evennia and how to use the tools, including an introduction to Python.
|
||||
Part 2: `What we want <../Part2/Beginner-Tutorial-Part2-Intro.html>`_
|
||||
Planning our tutorial game and what to think about when planning your own in the future.
|
||||
Part 3: `How we get there <../Part3/Beginner-Tutorial-Part3-Intro.html>`_
|
||||
Getting down to the meat of extending Evennia to make our game
|
||||
Part 4: `Using what we created <../Part4/Beginner-Tutorial-Part4-Intro.html>`_
|
||||
Building a tech-demo and world content to go with our code
|
||||
**Part 5: Showing the world**
|
||||
Taking our new game online and let players try it out
|
||||
```
|
||||
|
||||
You have a working game! In part five we will look at the web-components of Evennia and how to modify them
|
||||
to fit your game. We will also look at hosting your game and if you feel up to it we'll also go through how
|
||||
to bring your game online so you can invite your first players.
|
||||
|
||||
## Lessons
|
||||
|
||||
_TODO_
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 1
|
||||
|
||||
Add-a-simple-new-web-page.md
|
||||
Web-Tutorial.md
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Table of Contents
|
||||
|
||||
_TODO_
|
||||
|
||||
```{toctree}
|
||||
|
||||
Add-a-simple-new-web-page.md
|
||||
Web-Tutorial.md
|
||||
|
||||
```
|
||||
126
docs/source/Howtos/Beginner-Tutorial/Part5/Web-Tutorial.md
Normal file
126
docs/source/Howtos/Beginner-Tutorial/Part5/Web-Tutorial.md
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
# Web Tutorial
|
||||
|
||||
|
||||
Evennia uses the [Django](https://www.djangoproject.com/) web framework as the basis of both its
|
||||
database configuration and the website it provides. While a full understanding of Django requires
|
||||
reading the Django documentation, we have provided this tutorial to get you running with the basics
|
||||
and how they pertain to Evennia. This text details getting everything set up. The
|
||||
[Web-based Character view Tutorial](../../Web-Character-View-Tutorial.md) gives a more explicit example of making a
|
||||
custom web page connected to your game, and you may want to read that after finishing this guide.
|
||||
|
||||
## A Basic Overview
|
||||
|
||||
Django is a web framework. It gives you a set of development tools for building a website quickly
|
||||
and easily.
|
||||
|
||||
Django projects are split up into *apps* and these apps all contribute to one project. For instance,
|
||||
you might have an app for conducting polls, or an app for showing news posts or, like us, one for
|
||||
creating a web client.
|
||||
|
||||
Each of these applications has a `urls.py` file, which specifies what
|
||||
[URL](https://en.wikipedia.org/wiki/Uniform_resource_locator)s are used by the app, a `views.py` file
|
||||
for the code that the URLs activate, a `templates` directory for displaying the results of that code
|
||||
in [HTML](https://en.wikipedia.org/wiki/Html) for the user, and a `static` folder that holds assets
|
||||
like [CSS](https://en.wikipedia.org/wiki/CSS), [Javascript](https://en.wikipedia.org/wiki/Javascript),
|
||||
and Image files (You may note your mygame/web folder does not have a `static` or `template` folder.
|
||||
This is intended and explained further below). Django applications may also have a `models.py` file
|
||||
for storing information in the database. We will not change any models here, take a look at the
|
||||
[New Models](../../../Concepts/New-Models.md) page (as well as the [Django docs](https://docs.djangoproject.com/en/1.7/topics/db/models/) on models) if you are interested.
|
||||
|
||||
There is also a root `urls.py` that determines the URL structure for the entire project. A starter
|
||||
`urls.py` is included in the default game template, and automatically imports all of Evennia's
|
||||
default URLs for you. This is located in `web/urls.py`.
|
||||
|
||||
## Changing the logo on the front page
|
||||
|
||||
Evennia's default logo is a fun little googly-eyed snake wrapped around a gear globe. As cute as it
|
||||
is, it probably doesn't represent your game. So one of the first things you may wish to do is
|
||||
replace it with a logo of your own.
|
||||
|
||||
Django web apps all have _static assets_: CSS files, Javascript files, and Image files. In order to
|
||||
make sure the final project has all the static files it needs, the system collects the files from
|
||||
every app's `static` folder and places it in the `STATIC_ROOT` defined in `settings.py`. By default,
|
||||
the Evennia `STATIC_ROOT` is in `web/static`.
|
||||
|
||||
Because Django pulls files from all of those separate places and puts them in one folder, it's
|
||||
possible for one file to overwrite another. We will use this to plug in our own files without having
|
||||
to change anything in the Evennia itself.
|
||||
|
||||
By default, Evennia is configured to pull files you put in the `web/static_overrides` *after* all
|
||||
other static files. That means that files in `static_overrides` folder will overwrite any previously
|
||||
loaded files *having the same path under its static folder*. This last part is important to repeat:
|
||||
To overload the static resource from a standard `static` folder you need to replicate the path of
|
||||
folders and file names from that `static` folder in exactly the same way inside `static_overrides`.
|
||||
|
||||
Let's see how this works for our logo. The default web application is in the Evennia library itself,
|
||||
in `evennia/web/`. We can see that there is a `static` folder here. If we browse down, we'll
|
||||
eventually find the full path to the Evennia logo file:
|
||||
`evennia/web/static/evennia_general/images/evennia_logo.png`.
|
||||
|
||||
Inside our `static_overrides` we must replicate the part of the path inside the `static` folder, in
|
||||
other words, we must replicate `evennia_general/images/evennia_logo.png`.
|
||||
|
||||
So, to change the logo, we need to create the folder path `evennia_general/images/` in
|
||||
`static_overrides`. We then rename our own logo file to `evennia_logo.png` and copy it there. The
|
||||
final path for this file would thus be:
|
||||
`web/static_overrides/evennia_general/images/evennia_logo.png` in your local game folder.
|
||||
|
||||
To get this file pulled in, just change to your own game directory and reload the server:
|
||||
|
||||
```
|
||||
evennia reload
|
||||
```
|
||||
|
||||
This will reload the configuration and bring in the new static file(s). If you didn't want to reload
|
||||
the server you could instead use
|
||||
|
||||
```
|
||||
evennia collectstatic
|
||||
```
|
||||
|
||||
to only update the static files without any other changes.
|
||||
|
||||
> **Note**: Evennia will collect static files automatically during startup. So if `evennia
|
||||
collectstatic` reports finding 0 files to collect, make sure you didn't start the engine at some
|
||||
point - if so the collector has already done its work! To make sure, connect to the website and
|
||||
check so the logo has actually changed to your own version.
|
||||
|
||||
> **Note**: Sometimes the static asset collector can get confused. If no matter what you do, your
|
||||
overridden files aren't getting copied over the defaults, try removing the target file (or
|
||||
everything) in the `web/static` directory, and re-running `collectstatic` to gather everything from
|
||||
scratch.
|
||||
|
||||
## Changing the Front Page's Text
|
||||
|
||||
The default front page for Evennia contains information about the Evennia project. You'll probably
|
||||
want to replace this information with information about your own project. Changing the page template
|
||||
is done in a similar way to changing static resources.
|
||||
|
||||
Like static files, Django looks through a series of template folders to find the file it wants. The
|
||||
difference is that Django does not copy all of the template files into one place, it just searches
|
||||
through the template folders until it finds a template that matches what it's looking for. This
|
||||
means that when you edit a template, the changes are instant. You don't have to reload the server or
|
||||
run any extra commands to see these changes - reloading the web page in your browser is enough.
|
||||
|
||||
To replace the index page's text, we'll need to find the template for it. We'll go into more detail
|
||||
about how to determine which template is used for rendering a page in the
|
||||
[Web-based Character view Tutorial](../../Web-Character-View-Tutorial.md). For now, you should know that the template we want to change
|
||||
is stored in `evennia/web/website/templates/website/index.html`.
|
||||
|
||||
To replace this template file, you will put your changed template inside the
|
||||
`web/template_overrides/website` directory in your game folder. In the same way as with static
|
||||
resources you must replicate the path inside the default `template` directory exactly. So we must
|
||||
copy our replacement template named `index.html` there (or create the `website` directory in
|
||||
web/template_overrides` if it does not exist, first). The final path to the file should thus be:
|
||||
`web/template_overrides/website/index.html` within your game directory.
|
||||
|
||||
Note that it is usually easier to just copy the original template over and edit it in place. The
|
||||
original file already has all the markup and tags, ready for editing.
|
||||
|
||||
## Further reading
|
||||
|
||||
For further hints on working with the web presence, you could now continue to the
|
||||
[Web-based Character view Tutorial](../../Web-Character-View-Tutorial.md) where you learn to make a web page that
|
||||
displays in-game character stats. You can also look at [Django's own
|
||||
tutorial](https://docs.djangoproject.com/en/1.7/intro/tutorial01/) to get more insight in how Django
|
||||
works and what possibilities exist.
|
||||
237
docs/source/Howtos/Building-a-mech-tutorial.md
Normal file
237
docs/source/Howtos/Building-a-mech-tutorial.md
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
# 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](../Components/Accounts.md) represents the real person logging in and has no game-world existence.
|
||||
- Any [Object](../Components/Objects.md) can be puppeted by an Account (with proper permissions).
|
||||
- [Characters](../Components/Objects.md#characters), [Rooms](../Components/Objects.md#rooms), and [Exits](../Components/Objects.md#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:
|
||||
location.msg_contents(
|
||||
f"BOOM! The mech fires its gun at {target.key}"
|
||||
)
|
||||
|
||||
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](../Components/Command-Sets.md) (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](../Components/Typeclasses.md) 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, persistent=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.
|
||||
377
docs/source/Howtos/Coding-FAQ.md
Normal file
377
docs/source/Howtos/Coding-FAQ.md
Normal file
|
|
@ -0,0 +1,377 @@
|
|||
# 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.md#removing-default-commands)
|
||||
- [Preventing character from moving based on a condition](./Coding-FAQ.md#preventing-character-from-
|
||||
moving-based-on-a-condition)
|
||||
- [Reference initiating object in an EvMenu command](./Coding-FAQ.md#reference-initiating-object-in-an-
|
||||
evmenu-command)
|
||||
- [Adding color to default Evennia Channels](./Coding-FAQ.md#adding-color-to-default-evennia-channels)
|
||||
- [Selectively turn off commands in a room](./Coding-FAQ.md#selectively-turn-off-commands-in-a-room)
|
||||
- [Select Command based on a condition](./Coding-FAQ.md#select-command-based-on-a-condition)
|
||||
- [Automatically updating code when reloading](./Coding-FAQ.md#automatically-updating-code-when-
|
||||
reloading)
|
||||
- [Changing all exit messages](./Coding-FAQ.md#changing-all-exit-messages)
|
||||
- [Add parsing with the "to" delimiter](./Coding-FAQ.md#add-parsing-with-the-to-delimiter)
|
||||
- [Store last used session IP address](./Coding-FAQ.md#store-last-used-session-ip-address)
|
||||
- [Use wide characters with EvTable](./Coding-FAQ.md#non-latin-characters-in-evtable)
|
||||
|
||||
## Removing default commands
|
||||
**Q:** How does one *remove* (not replace) e.g. the default `get` [Command](../Components/Commands.md) from the
|
||||
Character [Command Set](../Components/Command-Sets.md)?
|
||||
|
||||
**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](Beginner-Tutorial/Part1/Adding-Commands.md)
|
||||
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_pre_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](../Components/Attributes.md) `cantmove`.
|
||||
Add the following code to the `Character` class:
|
||||
|
||||
```python
|
||||
def at_pre_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](../Components/EvMenu.md) 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):
|
||||
if self.key in COLORS:
|
||||
p_str = CHANNEL_COLORS.get(self.key.lower())
|
||||
else:
|
||||
p_str = self.key.capitalize()
|
||||
return f"[{p_str}] "
|
||||
```
|
||||
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](../Components/Locks.md). 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, persistent=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](../Components/Locks.md) 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](Beginner-Tutorial/Part1/Adding-Commands.md). The `is_full_moon` [lock
|
||||
function](../Components/Locks.md#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/Howtos/Command-Cooldown.md
Normal file
98
docs/source/Howtos/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.md#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](../Components/Attributes.md), 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/Howtos/Command-Duration.md
Normal file
403
docs/source/Howtos/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](../Components/Commands.md) 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](../Concepts/Async-Process.md#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(f"You shout '{self.args}' and wait for an echo ...")
|
||||
# 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
|
||||
self.caller.msg(
|
||||
f"You hear an echo: {shout.upper()} ... {shout.capitalize()} ... {shout.lower()}"
|
||||
)
|
||||
```
|
||||
|
||||
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](https://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(f"You shout '{self.args}', waiting for an echo ...")
|
||||
|
||||
# wait 2 seconds before calling self.echo1
|
||||
utils.delay(2, self.echo1)
|
||||
|
||||
# callback chain, started above
|
||||
def echo1(self):
|
||||
"First echo"
|
||||
self.caller.msg(f"... {self.args.upper()}")
|
||||
# wait 2 seconds for the next one
|
||||
utils.delay(2, self.echo2)
|
||||
|
||||
def echo2(self):
|
||||
"Second echo"
|
||||
self.caller.msg(f"... {self.args.capitalize()}")
|
||||
# wait another 2 seconds
|
||||
utils.delay(2, callback=self.echo3)
|
||||
|
||||
def echo3(self):
|
||||
"Last echo"
|
||||
self.caller.msg(f"... {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.md) 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
|
||||
caller.msg(
|
||||
f"You hear an echo: {shout.upper()} ... {shout.capitalize()} ... {shout.lower()}"
|
||||
)
|
||||
|
||||
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(f"You shout '{self.args}' and wait for an echo ...")
|
||||
# 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).
|
||||
125
docs/source/Howtos/Command-Prompt.md
Normal file
125
docs/source/Howtos/Command-Prompt.md
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
# 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](../Concepts/OOB.md)-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 = f"You diagnose {target} as having {hp} health, {mp} mana and {sp} stamina."
|
||||
prompt = f"{hp} HP, {mp} MP, {sp} 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 = f"{caller.db.hp} HP, {caller.db.mp} MP, {caller.db.sp} 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/Howtos/Coordinates.md
Normal file
349
docs/source/Howtos/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](../Components/Tags.md).
|
||||
|
||||
> 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.
|
||||
130
docs/source/Howtos/Default-Exit-Errors.md
Normal file
130
docs/source/Howtos/Default-Exit-Errors.md
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
# 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](../Components/Objects.md) and
|
||||
an [Exit Command](../Components/Commands.md) stored on said exit object. The command has the same key and aliases as the
|
||||
exit-object, which is why you can see the exit in the room and just write its name to traverse it.
|
||||
|
||||
So if you try to enter the name of a non-existing exit, Evennia treats is the same way as if you were trying to
|
||||
use a non-existing command:
|
||||
|
||||
> 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.
|
||||
|
||||
> west
|
||||
You cannot move west.
|
||||
|
||||
|
||||
## Adding default error commands
|
||||
|
||||
The way to do this is to give Evennia an _alternative_ Command to use when no Exit-Command is found
|
||||
in the room. See [Adding Commands](Beginner-Tutorial/Part1/Adding-Commands.md) for more info about the
|
||||
process of adding new Commands to Evennia.
|
||||
|
||||
In this example all we'll do is echo an error message.
|
||||
|
||||
```python
|
||||
# for example in a file mygame/commands/movecommands.py
|
||||
|
||||
from evennia import default_cmds, CmdSet
|
||||
|
||||
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 error based on key"""
|
||||
self.caller.msg(f"You cannot move {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"]
|
||||
|
||||
# you could add each command on its own to the default cmdset,
|
||||
# but putting them all in a cmdset here allows you to
|
||||
# just add this and makes it easier to expand with more
|
||||
# exit-errors in the future
|
||||
|
||||
class MovementFailCmdSet(CmdSet):
|
||||
def at_cmdset_creation(self):
|
||||
self.add(CmdExitErrorNorth())
|
||||
self.add(CmdExitErrorEast())
|
||||
self.add(CmdExitErrorWest())
|
||||
self.add(CmdExitErrorSouth())
|
||||
```
|
||||
|
||||
We pack our commands in a new little cmdset; if we add this to our
|
||||
`CharacterCmdSet`, we can just add more errors to `MovementFailCmdSet`
|
||||
later without having to change code in two places.
|
||||
|
||||
```python
|
||||
# in mygame/commands/default_cmdsets.py
|
||||
|
||||
from commands import movecommands
|
||||
|
||||
# [...]
|
||||
class CharacterCmdSet(default_cmds.CharacterCmdSet):
|
||||
# [...]
|
||||
def at_cmdset_creation(self):
|
||||
# [...]
|
||||
# this adds all the commands at once
|
||||
self.add(movecommands.MovementFailCmdSet)
|
||||
```
|
||||
|
||||
`reload` the server. 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 a direction without
|
||||
having a matching exit for it, you will fall back 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](../Components/Typeclasses.md) directly.
|
||||
|
||||
## Why not a single command?
|
||||
|
||||
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 reason is that this would *not* work. Understanding why is important.
|
||||
|
||||
Evennia's [command system](../Components/Commands.md) compares commands by key and/or aliases. If _any_ key or alias
|
||||
match, the two commands are considered _identical_. When the cmdsets merge, priority will then decide which of these
|
||||
'identical' commandss replace which.
|
||||
|
||||
So the above example would work fine as long as there were _no Exits at all_ in the room. But when we enter
|
||||
a room with an exit "north", its Exit-command (which has a higher priority) will override the single `CmdExitError`
|
||||
with its alias 'north'. So the `CmdExitError` will be gone and while "north" will work, we'll again get the normal
|
||||
"Command not recognized" error for the other directions.
|
||||
494
docs/source/Howtos/Dynamic-In-Game-Map.md
Normal file
494
docs/source/Howtos/Dynamic-In-Game-Map.md
Normal file
|
|
@ -0,0 +1,494 @@
|
|||
# Dynamic In Game Map
|
||||
|
||||
## Introduction
|
||||
|
||||
An often desired feature in a MUD is to show an in-game map to help navigation. The [Static in-game
|
||||
map](../Contribs/Contrib-Mapbuilder.md) tutorial solves this by creating a *static* map, meaning the map is pre-
|
||||
drawn once and for all - the rooms are then created to match that map. When walking around, parts of
|
||||
the static map is then cut out and displayed next to the room description.
|
||||
|
||||
In this tutorial we'll instead do it the other way around; We will dynamically draw the map based on
|
||||
the relationships we find between already existing rooms.
|
||||
|
||||
## The Grid of Rooms
|
||||
|
||||
There are at least two requirements needed for this tutorial to work.
|
||||
|
||||
1. The structure of your mud has to follow a logical layout. Evennia supports the layout of your
|
||||
world to be 'logically' impossible with rooms looping to themselves or exits leading to the other
|
||||
side of the map. Exits can also be named anything, from "jumping out the window" to "into the fifth
|
||||
dimension". This tutorial assumes you can only move in the cardinal directions (N, E, S and W).
|
||||
2. Rooms must be connected and linked together for the map to be generated correctly. Vanilla
|
||||
Evennia comes with a admin command [tunnel](evennia.commands.default.building.CmdTunnel) that allows a
|
||||
user to create rooms in the cardinal directions, but additional work is needed to assure that rooms
|
||||
are connected. For example, if you `tunnel east` and then immediately do `tunnel west` you'll find
|
||||
that you have created two completely stand-alone rooms. So care is needed if you want to create a
|
||||
"logical" layout. In this tutorial we assume you have such a grid of rooms that we can generate the
|
||||
map from.
|
||||
|
||||
## Concept
|
||||
|
||||
Before getting into the code, it is beneficial to understand and conceptualize how this is going to
|
||||
work. The idea is analogous to a worm that starts at your current position. It chooses a direction
|
||||
and 'walks' outward from it, mapping its route as it goes. Once it has traveled a pre-set distance
|
||||
it stops and starts over in another direction. An important note is that we want a system which is
|
||||
easily callable and not too complicated. Therefore we will wrap this entire code into a custom
|
||||
Python class (not a typeclass as this doesn't use any core objects from evennia itself).
|
||||
|
||||
We are going to create something that displays like this when you type 'look':
|
||||
|
||||
```
|
||||
Hallway
|
||||
|
||||
[.] [.]
|
||||
[@][.][.][.][.]
|
||||
[.] [.] [.]
|
||||
|
||||
The distant echoes of the forgotten
|
||||
wail throughout the empty halls.
|
||||
|
||||
Exits: North, East, South
|
||||
```
|
||||
|
||||
Your current location is defined by `[@]` while the `[.]`s are other rooms that the "worm" has seen
|
||||
since departing from your location.
|
||||
|
||||
## Setting up the Map Display
|
||||
|
||||
First we must define the components for displaying the map. For the "worm" to know what symbol to
|
||||
draw on the map we will have it check an Attribute on the room it visits called `sector_type`. For
|
||||
this tutorial we understand two symbols - a normal room and the room with us in it. We also define a
|
||||
fallback symbol for rooms without said Attribute - that way the map will still work even if we
|
||||
didn't prepare the room correctly. Assuming your game folder is named `mygame`, we create this code
|
||||
in `mygame/world/map.py.`
|
||||
|
||||
```python
|
||||
# in mygame/world/map.py
|
||||
|
||||
# the symbol is identified with a key "sector_type" on the
|
||||
# Room. Keys None and "you" must always exist.
|
||||
SYMBOLS = { None : ' . ', # for rooms without sector_type Attribute
|
||||
'you' : '[@]',
|
||||
'SECT_INSIDE': '[.]' }
|
||||
```
|
||||
|
||||
Since trying to access an unset Attribute returns `None`, this means rooms without the `sector_type`
|
||||
Atttribute will show as ` . `. Next we start building the custom class `Map`. It will hold all
|
||||
methods we need.
|
||||
|
||||
```python
|
||||
# in mygame/world/map.py
|
||||
|
||||
class Map(object):
|
||||
|
||||
def __init__(self, caller, max_width=9, max_length=9):
|
||||
self.caller = caller
|
||||
self.max_width = max_width
|
||||
self.max_length = max_length
|
||||
self.worm_has_mapped = {}
|
||||
self.curX = None
|
||||
self.curY = None
|
||||
```
|
||||
|
||||
- `self.caller` is normally your Character object, the one using the map.
|
||||
- `self.max_width/length` determine the max width and length of the map that will be generated. Note
|
||||
that it's important that these variables are set to *odd* numbers to make sure the display area has
|
||||
a center point.
|
||||
- ` self.worm_has_mapped` is building off the worm analogy above. This dictionary will store all
|
||||
rooms the "worm" has mapped as well as its relative position within the grid. This is the most
|
||||
important variable as it acts as a 'checker' and 'address book' that is able to tell us where the
|
||||
worm has been and what it has mapped so far.
|
||||
- `self.curX/Y` are coordinates representing the worm's current location on the grid.
|
||||
|
||||
|
||||
Before any sort of mapping can actually be done we need to create an empty display area and do some
|
||||
sanity checks on it by using the following methods.
|
||||
|
||||
```python
|
||||
# in mygame/world/map.py
|
||||
|
||||
class Map(object):
|
||||
# [... continued]
|
||||
|
||||
def create_grid(self):
|
||||
# This method simply creates an empty grid/display area
|
||||
# with the specified variables from __init__(self):
|
||||
board = []
|
||||
for row in range(self.max_width):
|
||||
board.append([])
|
||||
for column in range(self.max_length):
|
||||
board[row].append(' ')
|
||||
return board
|
||||
|
||||
def check_grid(self):
|
||||
# this method simply checks the grid to make sure
|
||||
# that both max_l and max_w are odd numbers.
|
||||
return True if self.max_length % 2 != 0 or self.max_width % 2 != 0\
|
||||
else False
|
||||
```
|
||||
|
||||
Before we can set our worm on its way, we need to know some of the computer science behind all this
|
||||
called 'Graph Traversing'. In Pseudo code what we are trying to accomplish is this:
|
||||
|
||||
```python
|
||||
# pseudo code
|
||||
|
||||
def draw_room_on_map(room, max_distance):
|
||||
self.draw(room)
|
||||
|
||||
if max_distance == 0:
|
||||
return
|
||||
|
||||
for exit in room.exits:
|
||||
if self.has_drawn(exit.destination):
|
||||
# skip drawing if we already visited the destination
|
||||
continue
|
||||
else:
|
||||
# first time here!
|
||||
self.draw_room_on_map(exit.destination, max_distance - 1)
|
||||
```
|
||||
|
||||
The beauty of Python is that our actual code of doing this doesn't differ much if at all from this
|
||||
Pseudo code example.
|
||||
|
||||
- `max_distance` is a variable indicating to our Worm how many rooms AWAY from your current location
|
||||
will it map. Obviously the larger the number the more time it will take if your current location has
|
||||
many many rooms around you.
|
||||
|
||||
The first hurdle here is what value to use for 'max_distance'. There is no reason for the worm to
|
||||
travel further than what is actually displayed to you. For example, if your current location is
|
||||
placed in the center of a display area of size `max_length = max_width = 9`, then the worm need only
|
||||
go `4` spaces in either direction:
|
||||
|
||||
```
|
||||
[.][.][.][.][@][.][.][.][.]
|
||||
4 3 2 1 0 1 2 3 4
|
||||
```
|
||||
|
||||
The `max_distance` can be set dynamically based on the size of the display area. As your width/length
|
||||
changes it becomes a simple algebraic linear relationship which is simply `max_distance =
|
||||
(min(max_width, max_length) -1) / 2`.
|
||||
|
||||
## Building the Mapper
|
||||
|
||||
Now we can start to fill our Map object with some methods. We are still missing a few methods that
|
||||
are very important:
|
||||
|
||||
* `self.draw(self, room)` - responsible for actually drawing room to grid.
|
||||
* `self.has_drawn(self, room)` - checks to see if the room has been mapped and worm has already been
|
||||
here.
|
||||
* `self.median(self, number)` - a simple utility method that finds the median (middle point) from 0,
|
||||
n
|
||||
* `self.update_pos(self, room, exit_name)` - updates the worm's physical position by reassigning
|
||||
self.curX/Y. .accordingly
|
||||
* `self.start_loc_on_grid(self)` - the very first initial draw on the grid representing your
|
||||
location in the middle of the grid
|
||||
* `self.show_map` - after everything is done convert the map into a readable string
|
||||
* `self.draw_room_on_map(self, room, max_distance)` - the main method that ties it all together.
|
||||
|
||||
|
||||
Now that we know which methods we need, let's refine our initial `__init__(self)` to pass some
|
||||
conditional statements and set it up to start building the display.
|
||||
|
||||
|
||||
```python
|
||||
#mygame/world/map.py
|
||||
|
||||
class Map(object):
|
||||
|
||||
def __init__(self, caller, max_width=9, max_length=9):
|
||||
self.caller = caller
|
||||
self.max_width = max_width
|
||||
self.max_length = max_length
|
||||
self.worm_has_mapped = {}
|
||||
self.curX = None
|
||||
self.curY = None
|
||||
|
||||
if self.check_grid():
|
||||
# we have to store the grid into a variable
|
||||
self.grid = self.create_grid()
|
||||
# we use the algebraic relationship
|
||||
self.draw_room_on_map(caller.location,
|
||||
((min(max_width, max_length) -1 ) / 2)
|
||||
|
||||
```
|
||||
|
||||
Here we check to see if the parameters for the grid are okay, then we create an empty canvas and map
|
||||
our initial location as the first room!
|
||||
|
||||
As mentioned above, the code for the `self.draw_room_on_map()` is not much different than the Pseudo
|
||||
code. The method is shown below:
|
||||
|
||||
```python
|
||||
# in mygame/world/map.py, in the Map class
|
||||
|
||||
def draw_room_on_map(self, room, max_distance):
|
||||
self.draw(room)
|
||||
|
||||
if max_distance == 0:
|
||||
return
|
||||
|
||||
for exit in room.exits:
|
||||
if exit.name not in ("north", "east", "west", "south"):
|
||||
# we only map in the cardinal directions. Mapping up/down would be
|
||||
# an interesting learning project for someone who wanted to try it.
|
||||
continue
|
||||
if self.has_drawn(exit.destination):
|
||||
# we've been to the destination already, skip ahead.
|
||||
continue
|
||||
|
||||
self.update_pos(room, exit.name.lower())
|
||||
self.draw_room_on_map(exit.destination, max_distance - 1)
|
||||
```
|
||||
|
||||
The first thing the "worm" does is to draw your current location in `self.draw`. Lets define that...
|
||||
|
||||
```python
|
||||
#in mygame/word/map.py, in the Map class
|
||||
|
||||
def draw(self, room):
|
||||
# draw initial ch location on map first!
|
||||
if room == self.caller.location:
|
||||
self.start_loc_on_grid()
|
||||
self.worm_has_mapped[room] = [self.curX, self.curY]
|
||||
else:
|
||||
# map all other rooms
|
||||
self.worm_has_mapped[room] = [self.curX, self.curY]
|
||||
# this will use the sector_type Attribute or None if not set.
|
||||
self.grid[self.curX][self.curY] = SYMBOLS[room.db.sector_type]
|
||||
```
|
||||
|
||||
In `self.start_loc_on_grid()`:
|
||||
|
||||
```python
|
||||
def median(self, num):
|
||||
lst = sorted(range(0, num))
|
||||
n = len(lst)
|
||||
m = n -1
|
||||
return (lst[n//2] + lst[m//2]) / 2.0
|
||||
|
||||
def start_loc_on_grid(self):
|
||||
x = self.median(self.max_width)
|
||||
y = self.median(self.max_length)
|
||||
# x and y are floats by default, can't index lists with float types
|
||||
x, y = int(x), int(y)
|
||||
|
||||
self.grid[x][y] = SYMBOLS['you']
|
||||
self.curX, self.curY = x, y # updating worms current location
|
||||
```
|
||||
|
||||
After the system has drawn the current map it checks to see if the `max_distance` is `0` (since this
|
||||
is the inital start phase it is not). Now we handle the iteration once we have each individual exit
|
||||
in the room. The first thing it does is check if the room the Worm is in has been mapped already..
|
||||
lets define that...
|
||||
|
||||
|
||||
```python
|
||||
def has_drawn(self, room):
|
||||
return True if room in self.worm_has_mapped.keys() else False
|
||||
```
|
||||
|
||||
If `has_drawn` returns `False` that means the worm has found a room that hasn't been mapped yet. It
|
||||
will then 'move' there. The self.curX/Y sort of lags behind, so we have to make sure to track the
|
||||
position of the worm; we do this in `self.update_pos()` below.
|
||||
|
||||
```python
|
||||
def update_pos(self, room, exit_name):
|
||||
# this ensures the coordinates stays up to date
|
||||
# to where the worm is currently at.
|
||||
self.curX, self.curY = \
|
||||
self.worm_has_mapped[room][0], self.worm_has_mapped[room][1]
|
||||
|
||||
# now we have to actually move the pointer
|
||||
# variables depending on which 'exit' it found
|
||||
if exit_name == 'east':
|
||||
self.curY += 1
|
||||
elif exit_name == 'west':
|
||||
self.curY -= 1
|
||||
elif exit_name == 'north':
|
||||
self.curX -= 1
|
||||
elif exit_name == 'south':
|
||||
self.curX += 1
|
||||
```
|
||||
|
||||
Once the system updates the position of the worm it feeds the new room back into the original
|
||||
`draw_room_on_map()` and starts the process all over again..
|
||||
|
||||
That is essentially the entire thing. The final method is to bring it all together and make a nice
|
||||
presentational string out of it using the `self.show_map()` method.
|
||||
|
||||
```python
|
||||
def show_map(self):
|
||||
map_string = ""
|
||||
for row in self.grid:
|
||||
map_string += " ".join(row)
|
||||
map_string += "\n"
|
||||
|
||||
return map_string
|
||||
```
|
||||
|
||||
## Using the Map
|
||||
|
||||
In order for the map to get triggered we store it on the Room typeclass. If we put it in
|
||||
`return_appearance` we will get the map back every time we look at the room.
|
||||
|
||||
> `return_appearance` is a default Evennia hook available on all objects; it is called e.g. by the
|
||||
`look` command to get the description of something (the room in this case).
|
||||
|
||||
```python
|
||||
# in mygame/typeclasses/rooms.py
|
||||
|
||||
from evennia import DefaultRoom
|
||||
from world.map import Map
|
||||
|
||||
class Room(DefaultRoom):
|
||||
|
||||
def return_appearance(self, looker):
|
||||
# [...]
|
||||
string = f"{Map(looker).show_map()}\n"
|
||||
# Add all the normal stuff like room description,
|
||||
# contents, exits etc.
|
||||
string += "\n" + super().return_appearance(looker)
|
||||
return string
|
||||
```
|
||||
|
||||
Obviously this method of generating maps doesn't take into account of any doors or exits that are
|
||||
hidden.. etc.. but hopefully it serves as a good base to start with. Like previously mentioned, it
|
||||
is very important to have a solid foundation on rooms before implementing this. You can try this on
|
||||
vanilla evennia by using @tunnel and essentially you can just create a long straight/edgy non-
|
||||
looping rooms that will show on your in-game map.
|
||||
|
||||
The above example will display the map above the room description. You could also use an
|
||||
[EvTable](github:evennia.utils.evtable) to place description and map next to each other. Some other
|
||||
things you can do is to have a [Command](../Components/Commands.md) that displays with a larger radius, maybe with a
|
||||
legend and other features.
|
||||
|
||||
Below is the whole `map.py` for your reference. You need to update your `Room` typeclass (see above)
|
||||
to actually call it. Remember that to see different symbols for a location you also need to set the
|
||||
`sector_type` Attribute on the room to one of the keys in the `SYMBOLS` dictionary. So in this
|
||||
example, to make a room be mapped as `[.]` you would set the room's `sector_type` to
|
||||
`"SECT_INSIDE"`. Try it out with `@set here/sector_type = "SECT_INSIDE"`. If you wanted all new
|
||||
rooms to have a given sector symbol, you could change the default in the `SYMBOLS` dictionary below,
|
||||
or you could add the Attribute in the Room's `at_object_creation` method.
|
||||
|
||||
```python
|
||||
# mygame/world/map.py
|
||||
|
||||
# These are keys set with the Attribute sector_type on the room.
|
||||
# The keys None and "you" must always exist.
|
||||
SYMBOLS = { None : ' . ', # for rooms without a sector_type attr
|
||||
'you' : '[@]',
|
||||
'SECT_INSIDE': '[.]' }
|
||||
|
||||
class Map(object):
|
||||
|
||||
def __init__(self, caller, max_width=9, max_length=9):
|
||||
self.caller = caller
|
||||
self.max_width = max_width
|
||||
self.max_length = max_length
|
||||
self.worm_has_mapped = {}
|
||||
self.curX = None
|
||||
self.curY = None
|
||||
|
||||
if self.check_grid():
|
||||
# we actually have to store the grid into a variable
|
||||
self.grid = self.create_grid()
|
||||
self.draw_room_on_map(caller.location,
|
||||
((min(max_width, max_length) -1 ) / 2))
|
||||
|
||||
def update_pos(self, room, exit_name):
|
||||
# this ensures the pointer variables always
|
||||
# stays up to date to where the worm is currently at.
|
||||
self.curX, self.curY = \
|
||||
self.worm_has_mapped[room][0], self.worm_has_mapped[room][1]
|
||||
|
||||
# now we have to actually move the pointer
|
||||
# variables depending on which 'exit' it found
|
||||
if exit_name == 'east':
|
||||
self.curY += 1
|
||||
elif exit_name == 'west':
|
||||
self.curY -= 1
|
||||
elif exit_name == 'north':
|
||||
self.curX -= 1
|
||||
elif exit_name == 'south':
|
||||
self.curX += 1
|
||||
|
||||
def draw_room_on_map(self, room, max_distance):
|
||||
self.draw(room)
|
||||
|
||||
if max_distance == 0:
|
||||
return
|
||||
|
||||
for exit in room.exits:
|
||||
if exit.name not in ("north", "east", "west", "south"):
|
||||
# we only map in the cardinal directions. Mapping up/down would be
|
||||
# an interesting learning project for someone who wanted to try it.
|
||||
continue
|
||||
if self.has_drawn(exit.destination):
|
||||
# we've been to the destination already, skip ahead.
|
||||
continue
|
||||
|
||||
self.update_pos(room, exit.name.lower())
|
||||
self.draw_room_on_map(exit.destination, max_distance - 1)
|
||||
|
||||
def draw(self, room):
|
||||
# draw initial caller location on map first!
|
||||
if room == self.caller.location:
|
||||
self.start_loc_on_grid()
|
||||
self.worm_has_mapped[room] = [self.curX, self.curY]
|
||||
else:
|
||||
# map all other rooms
|
||||
self.worm_has_mapped[room] = [self.curX, self.curY]
|
||||
# this will use the sector_type Attribute or None if not set.
|
||||
self.grid[self.curX][self.curY] = SYMBOLS[room.db.sector_type]
|
||||
|
||||
def median(self, num):
|
||||
lst = sorted(range(0, num))
|
||||
n = len(lst)
|
||||
m = n -1
|
||||
return (lst[n//2] + lst[m//2]) / 2.0
|
||||
|
||||
def start_loc_on_grid(self):
|
||||
x = self.median(self.max_width)
|
||||
y = self.median(self.max_length)
|
||||
# x and y are floats by default, can't index lists with float types
|
||||
x, y = int(x), int(y)
|
||||
|
||||
self.grid[x][y] = SYMBOLS['you']
|
||||
self.curX, self.curY = x, y # updating worms current location
|
||||
|
||||
|
||||
def has_drawn(self, room):
|
||||
return True if room in self.worm_has_mapped.keys() else False
|
||||
|
||||
|
||||
def create_grid(self):
|
||||
# This method simply creates an empty grid
|
||||
# with the specified variables from __init__(self):
|
||||
board = []
|
||||
for row in range(self.max_width):
|
||||
board.append([])
|
||||
for column in range(self.max_length):
|
||||
board[row].append(' ')
|
||||
return board
|
||||
|
||||
def check_grid(self):
|
||||
# this method simply checks the grid to make sure
|
||||
# both max_l and max_w are odd numbers
|
||||
return True if self.max_length % 2 != 0 or \
|
||||
self.max_width % 2 != 0 else False
|
||||
|
||||
def show_map(self):
|
||||
map_string = ""
|
||||
for row in self.grid:
|
||||
map_string += " ".join(row)
|
||||
map_string += "\n"
|
||||
|
||||
return map_string
|
||||
```
|
||||
|
||||
## Final Comments
|
||||
|
||||
The Dynamic map could be expanded with further capabilities. For example, it could mark exits or
|
||||
allow NE, SE etc directions as well. It could have colors for different terrain types. One could
|
||||
also look into up/down directions and figure out how to display that in a good way.
|
||||
196
docs/source/Howtos/Evennia-for-Diku-Users.md
Normal file
196
docs/source/Howtos/Evennia-for-Diku-Users.md
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
# 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 advantages of Python are an extremely fast
|
||||
development cycle and easy ways to create game systems. Doing the same with C can take many times
|
||||
more code and be harder to make stable and maintainable.
|
||||
|
||||
## 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 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. 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.
|
||||
221
docs/source/Howtos/Evennia-for-MUSH-Users.md
Normal file
221
docs/source/Howtos/Evennia-for-MUSH-Users.md
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
# Evennia for MUSH Users
|
||||
|
||||
*This page is adopted from an article originally posted for the MUSH community [here on
|
||||
musoapbox.net](https://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](../Components/FuncParser.md) 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](../Setup/Installation.md).
|
||||
|
||||
## 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](https://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.md).
|
||||
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](Beginner-Tutorial/Part1/Tutorial-World.md) 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](./Howtos-Overview.md)
|
||||
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](https://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/Howtos/Evennia-for-roleplaying-sessions.md
Normal file
732
docs/source/Howtos/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 [Setup Quickstart](../Setup/Installation.md)
|
||||
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](Beginner-Tutorial/Part1/Beginner-Tutorial-Part1-Intro.md) 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](../Concepts/Building-Permissions.md#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](../Concepts/Building-Permissions.md#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](../Components/Accounts.md) 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 = f"{name}(GM)"
|
||||
|
||||
if lookaccount and \
|
||||
(lookaccount.permissions.get("Developers") or lookaccount.db.is_gm):
|
||||
# Developers/GMs see name(#dbref) or name(GM)(#dbref)
|
||||
name = f"{name}(#{self.id})"
|
||||
|
||||
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](../Components/Attributes.md)) 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](../Components/Commands.md) 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(f"Could not find account '{self.args}'")
|
||||
return
|
||||
elif len(accountlist) > 1:
|
||||
caller.msg(f"Multiple matches for '{self.args}': {accountlist}")
|
||||
return
|
||||
else:
|
||||
account = accountlist[0]
|
||||
|
||||
if self.cmdstring == "gm":
|
||||
# turn someone into a GM
|
||||
if account.permissions.get("Admins"):
|
||||
caller.msg(f"Account {account} is already a GM.")
|
||||
else:
|
||||
account.permissions.add("Admins")
|
||||
caller.msg(f"Account {account} is now a GM.")
|
||||
account.msg(f"You are now a GM (changed by {caller}).")
|
||||
account.character.db.is_gm = True
|
||||
else:
|
||||
# @ungm was entered - revoke GM status from someone
|
||||
if not account.permissions.get("Admins"):
|
||||
caller.msg(f"Account {account} is not a GM.")
|
||||
else:
|
||||
account.permissions.remove("Admins")
|
||||
caller.msg(f"Account {account} is no longer a GM.")
|
||||
account.msg(f"You are no longer a GM (changed by {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](../Concepts/TextTags.md) 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:
|
||||
list_of_fieldnames = ", ".join(ALLOWED_FIELDNAMES)
|
||||
err = f"Allowed field names: {list_of_fieldnames}"
|
||||
caller.msg(err)
|
||||
return False
|
||||
if fieldname in ALLOWED_ATTRS and not value.isdigit():
|
||||
caller.msg(f"{fieldname} must receive a number.")
|
||||
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(f"{fieldname} was set to {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(f"{character.key} can no longer edit their character sheet.")
|
||||
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(f"{character.key} can now edit their character sheet.")
|
||||
|
||||
if fieldname:
|
||||
if fieldname == "name":
|
||||
character.key = value
|
||||
else:
|
||||
character.db.chardata[fieldname] = value
|
||||
character.update_charsheet()
|
||||
caller.msg(f"You set {character.key}'s {fieldname} to {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](Beginner-Tutorial/Part1/Building-Quickstart.md).
|
||||
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](../Components/Channels.md) 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.
|
||||
304
docs/source/Howtos/Gametime-Tutorial.md
Normal file
304
docs/source/Howtos/Gametime-Tutorial.md
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
# 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](../Components/Scripts.md) 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 1, day 1, and at midnight.
|
||||
|
||||
> Year, hour, minute and sec starts from 0, month, week and day starts from 1, this makes them
|
||||
> behave consistently with the standard time.
|
||||
|
||||
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, mins, secs = custom_gametime.custom_gametime(absolute=True)
|
||||
time_string = f"We are in year {year}, day {day}, month {month}."
|
||||
time_string += f"\nIt's {hour:02}:{mins:02}:{secs:02}."
|
||||
self.msg(time_string)
|
||||
```
|
||||
|
||||
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.
|
||||
485
docs/source/Howtos/Help-System-Tutorial.md
Normal file
485
docs/source/Howtos/Help-System-Tutorial.md
Normal file
|
|
@ -0,0 +1,485 @@
|
|||
# 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/4.0/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/views.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 make it look like this.
|
||||
|
||||
```python
|
||||
# URL patterns for the help_system app
|
||||
|
||||
from django.urls import path
|
||||
from .views import index
|
||||
|
||||
urlpatterns = [
|
||||
path('', index)
|
||||
]
|
||||
```
|
||||
|
||||
The `urlpatterns` variable is what Django/Evennia looks for to figure out how to
|
||||
direct a user entering an URL in their browser to the view-code you have
|
||||
written.
|
||||
|
||||
Last we need to tie this into the main namespace for your game. Edit the file
|
||||
`mygame/web/urls.py`. In it you will find the `urlpatterns` list again.
|
||||
Add a new `path` to the end of the list.
|
||||
|
||||
```python
|
||||
# mygame/web/urls.py
|
||||
# [...]
|
||||
|
||||
# add patterns
|
||||
urlpatterns = [
|
||||
# website
|
||||
path("", include("web.website.urls")),
|
||||
# webclient
|
||||
path("webclient/", include("web.webclient.urls")),
|
||||
# web admin
|
||||
path("admin/", include("web.admin.urls")),
|
||||
|
||||
# my help system
|
||||
path('help/', include('web.help_system.urls')) # <--- NEW
|
||||
]
|
||||
|
||||
# [...]
|
||||
```
|
||||
|
||||
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.
|
||||
91
docs/source/Howtos/Howtos-Overview.md
Normal file
91
docs/source/Howtos/Howtos-Overview.md
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
# Tutorials and Howto's
|
||||
|
||||
The documents in this section aims to teach how to use Evennia in a tutorial or
|
||||
a step-by-step way. They often give hints on about solving a problem or implementing
|
||||
a particular feature or concept. They will often refer to the
|
||||
[components](../Components/Components-Overview.md) or [concepts](../Concepts/Concepts-Overview.md)
|
||||
docs for those that want to dive deeper.
|
||||
|
||||
## Beginner Tutorial
|
||||
|
||||
Recommended starting point! This will take you from absolute beginner to making
|
||||
a small, but full, game with Evennia. Even if you have a very different game style
|
||||
in mind for your own game, this will give you a good start.
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 1
|
||||
|
||||
./Beginner-Tutorial/Beginner-Tutorial-Intro
|
||||
|
||||
```
|
||||
|
||||
## Howto's
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 1
|
||||
|
||||
Coding-FAQ.md
|
||||
Command-Prompt.md
|
||||
Command-Cooldown.md
|
||||
Command-Duration.md
|
||||
Default-Exit-Errors.md
|
||||
Manually-Configuring-Color.md
|
||||
Tutorial-Tweeting-Game-Stats.md
|
||||
|
||||
```
|
||||
## Mobs and NPCs
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 1
|
||||
|
||||
Tutorial-NPCs-listening.md
|
||||
Tutorial-Aggressive-NPCs.md
|
||||
NPC-shop-Tutorial.md
|
||||
```
|
||||
|
||||
## Vehicles
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 1
|
||||
|
||||
Building-a-mech-tutorial.md
|
||||
Tutorial-Vehicles.md
|
||||
```
|
||||
|
||||
## Systems
|
||||
```{toctree}
|
||||
:maxdepth: 1
|
||||
|
||||
Gametime-Tutorial.md
|
||||
Help-System-Tutorial.md
|
||||
Mass-and-weight-for-objects.md
|
||||
Weather-Tutorial.md
|
||||
Coordinates.md
|
||||
Dynamic-In-Game-Map.md
|
||||
Static-In-Game-Map.md
|
||||
Arxcode-Installation.md
|
||||
|
||||
```
|
||||
|
||||
## Web-related tutorials
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 1
|
||||
|
||||
Add-a-wiki-on-your-website.md
|
||||
Web-Character-Generation.md
|
||||
Web-Character-View-Tutorial.md
|
||||
|
||||
```
|
||||
## Deep-dives
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 1
|
||||
|
||||
Parsing-commands-tutorial.md
|
||||
Understanding-Color-Tags.md
|
||||
Evennia-for-roleplaying-sessions.md
|
||||
Evennia-for-Diku-Users.md
|
||||
Evennia-for-MUSH-Users.md
|
||||
Tutorial-for-basic-MUSH-like-game.md
|
||||
```
|
||||
169
docs/source/Howtos/Manually-Configuring-Color.md
Normal file
169
docs/source/Howtos/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](../Concepts/Colors.md) 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](../Components/Typeclasses.md) 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](../Components/Attributes.md).
|
||||
|
||||
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](../Concepts/TextTags.md#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)
|
||||
98
docs/source/Howtos/Mass-and-weight-for-objects.md
Normal file
98
docs/source/Howtos/Mass-and-weight-for-objects.md
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
# 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([
|
||||
str(item.get_display_name(self.caller.sessions)),
|
||||
second and second or "",
|
||||
])
|
||||
string = f"|wYou are carrying:\n{table}"
|
||||
self.caller.msg(string)
|
||||
|
||||
```
|
||||
334
docs/source/Howtos/NPC-shop-Tutorial.md
Normal file
334
docs/source/Howtos/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](../Components/EvMenu.md)
|
||||
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](../Components/EvMenu.md) 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 = f"*** Welcome to {shopname}! ***\n"
|
||||
if wares:
|
||||
text += f" Things for sale (choose 1-{len(wares)} to inspect); quit to exit:"
|
||||
else:
|
||||
text += " There is nothing for sale; quit to exit."
|
||||
|
||||
options = []
|
||||
for ware in wares:
|
||||
# add an option for every ware in store
|
||||
gold_val = ware.db.gold_value or 1
|
||||
options.append({"desc": f"{ware.key} ({gold_val} gold)",
|
||||
"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 = f"You inspect {ware.key}:\n\n{ware.db.desc}"
|
||||
|
||||
def buy_ware_result(caller):
|
||||
"This will be executed first when choosing to buy."
|
||||
if wealth >= value:
|
||||
rtext = f"You pay {value} gold and purchase {ware.key}!"
|
||||
caller.db.gold -= value
|
||||
ware.move_to(caller, quiet=True)
|
||||
else:
|
||||
rtext = f"You cannot afford {value} gold for {ware.key}!"
|
||||
caller.msg(rtext)
|
||||
|
||||
gold_val = ware.db.gold_value or 1
|
||||
options = ({
|
||||
"desc": f"Buy {ware.key} for {gold_val} gold",
|
||||
"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](../Components/Commands.md) 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](../Components/Command-Sets.md) 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](../Components/Typeclasses.md) 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, persistent=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=f"{shopname}-storage",
|
||||
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 = f"{shopname}-storekey"
|
||||
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(f"traverse:holds({storeroom_key_name})")
|
||||
|
||||
# inform the builder about progress
|
||||
self.caller.msg(f"The shop {shop} was created!")
|
||||
```
|
||||
|
||||
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](Starting/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](../Components/Locks.md) 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](../Components/Scripts.md) 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.
|
||||
748
docs/source/Howtos/Parsing-commands-tutorial.md
Normal file
748
docs/source/Howtos/Parsing-commands-tutorial.md
Normal file
|
|
@ -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](Beginner-Tutorial/Part1/Adding-Commands.md) 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](Beginner-Tutorial/Part1/Searching-Things.md).
|
||||
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.
|
||||
416
docs/source/Howtos/Static-In-Game-Map.md
Normal file
416
docs/source/Howtos/Static-In-Game-Map.md
Normal file
|
|
@ -0,0 +1,416 @@
|
|||
# Static In Game Map
|
||||
|
||||
|
||||
## Introduction
|
||||
|
||||
This tutorial describes the creation of an in-game map display based on a pre-drawn map. It also
|
||||
details how to use the [Batch code processor](../Components/Batch-Code-Processor.md) for advanced building. There is
|
||||
also the [Dynamic in-game map tutorial](./Dynamic-In-Game-Map.md) that works in the opposite direction,
|
||||
by generating a map from an existing grid of rooms.
|
||||
|
||||
Evennia does not require its rooms to be positioned in a "logical" way. Your exits could be named
|
||||
anything. You could make an exit "west" that leads to a room described to be in the far north. You
|
||||
could have rooms inside one another, exits leading back to the same room or describing spatial
|
||||
geometries impossible in the real world.
|
||||
|
||||
That said, most games *do* organize their rooms in a logical fashion, if nothing else to retain the
|
||||
sanity of their players. And when they do, the game becomes possible to map. This tutorial will give
|
||||
an example of a simple but flexible in-game map system to further help player's to navigate. We will
|
||||
|
||||
To simplify development and error-checking we'll break down the work into bite-size chunks, each
|
||||
building on what came before. For this we'll make extensive use of the [Batch code processor](Batch-
|
||||
Code-Processor), so you may want to familiarize yourself with that.
|
||||
|
||||
1. **Planning the map** - Here we'll come up with a small example map to use for the rest of the
|
||||
tutorial.
|
||||
2. **Making a map object** - This will showcase how to make a static in-game "map" object a
|
||||
Character could pick up and look at.
|
||||
3. **Building the map areas** - Here we'll actually create the small example area according to the
|
||||
map we designed before.
|
||||
4. **Map code** - This will link the map to the location so our output looks something like this:
|
||||
|
||||
```
|
||||
crossroads(#3)
|
||||
↑╚∞╝↑
|
||||
≈↑│↑∩ The merger of two roads. To the north looms a mighty castle.
|
||||
O─O─O To the south, the glow of a campfire can be seen. To the east lie
|
||||
≈↑│↑∩ the vast mountains and to the west is heard the waves of the sea.
|
||||
↑▲O▲↑
|
||||
|
||||
Exits: north(#8), east(#9), south(#10), west(#11)
|
||||
```
|
||||
|
||||
We will henceforth assume your game folder is name named `mygame` and that you haven't modified the
|
||||
default commands. We will also not be using [Colors](../Concepts/Colors.md) for our map since they
|
||||
don't show in the documentation wiki.
|
||||
|
||||
## Planning the Map
|
||||
|
||||
Let's begin with the fun part! Maps in MUDs come in many different [shapes and
|
||||
sizes](http://journal.imaginary-realities.com/volume-05/issue-01/modern-interface-modern-
|
||||
mud/index.html). Some appear as just boxes connected by lines. Others have complex graphics that are
|
||||
external to the game itself.
|
||||
|
||||
Our map will be in-game text but that doesn't mean we're restricted to the normal alphabet! If
|
||||
you've ever selected the [Wingdings font](https://en.wikipedia.org/wiki/Wingdings) in Microsoft Word
|
||||
you will know there are a multitude of other characters around to use. When creating your game with
|
||||
Evennia you have access to the [UTF-8 character encoding](https://en.wikipedia.org/wiki/UTF-8) which
|
||||
put at your disposal [thousands of letters, number and geometric shapes](https://mcdlr.com/utf-8/#1).
|
||||
|
||||
For this exercise, we've copy-and-pasted from the pallet of special characters used over at
|
||||
[Dwarf Fortress](https://dwarffortresswiki.org/index.php/Character_table) to create what is hopefully
|
||||
a pleasing and easy to understood landscape:
|
||||
|
||||
```
|
||||
≈≈↑↑↑↑↑∩∩
|
||||
≈≈↑╔═╗↑∩∩ Places the account can visit are indicated by "O".
|
||||
≈≈↑║O║↑∩∩ Up the top is a castle visitable by the account.
|
||||
≈≈↑╚∞╝↑∩∩ To the right is a cottage and to the left the beach.
|
||||
≈≈≈↑│↑∩∩∩ And down the bottom is a camp site with tents.
|
||||
≈≈O─O─O⌂∩ In the center is the starting location, a crossroads
|
||||
≈≈≈↑│↑∩∩∩ which connect the four other areas.
|
||||
≈≈↑▲O▲↑∩∩
|
||||
≈≈↑↑▲↑↑∩∩
|
||||
≈≈↑↑↑↑↑∩∩
|
||||
```
|
||||
There are many considerations when making a game map depending on the play style and requirements
|
||||
you intend to implement. Here we will display a 5x5 character map of the area surrounding the
|
||||
account. This means making sure to account for 2 characters around every visitable location. Good
|
||||
planning at this stage can solve many problems before they happen.
|
||||
|
||||
## Creating a Map Object
|
||||
|
||||
In this section we will try to create an actual "map" object that an account can pick up and look
|
||||
at.
|
||||
|
||||
Evennia offers a range of [default commands](../Components/Default-Commands.md) for
|
||||
[creating objects and rooms in-game](Beginner-Tutorial/Part1/Building-Quickstart.md). While readily accessible, these commands are made to do very
|
||||
specific, restricted things and will thus not offer as much flexibility to experiment (for an
|
||||
advanced exception see [the FuncParser](../Components/FuncParser.md)). Additionally, entering long
|
||||
descriptions and properties over and over in the game client can become tedious; especially when
|
||||
testing and you may want to delete and recreate things over and over.
|
||||
|
||||
To overcome this, Evennia offers [batch processors](../Components/Batch-Processors.md) that work as input-files
|
||||
created out-of-game. In this tutorial we'll be using the more powerful of the two available batch
|
||||
processors, the [Batch Code Processor ](../Components/Batch-Code-Processor.md), called with the `@batchcode` command.
|
||||
This is a very powerful tool. It allows you to craft Python files to act as blueprints of your
|
||||
entire game world. These files have access to use Evennia's Python API directly. Batchcode allows
|
||||
for easy editing and creation in whatever text editor you prefer, avoiding having to manually build
|
||||
the world line-by-line inside the game.
|
||||
|
||||
> Important warning: `@batchcode`'s power is only rivaled by the `@py` command. Batchcode is so
|
||||
powerful it should be reserved only for the [superuser](../Concepts/Building-Permissions.md). Think carefully
|
||||
before you let others (such as `Developer`- level staff) run `@batchcode` on their own - make sure
|
||||
you are okay with them running *arbitrary Python code* on your server.
|
||||
|
||||
While a simple example, the map object it serves as good way to try out `@batchcode`. Go to
|
||||
`mygame/world` and create a new file there named `batchcode_map.py`:
|
||||
|
||||
```Python
|
||||
# mygame/world/batchcode_map.py
|
||||
|
||||
from evennia import create_object
|
||||
from evennia import DefaultObject
|
||||
|
||||
# We use the create_object function to call into existence a
|
||||
# DefaultObject named "Map" wherever you are standing.
|
||||
|
||||
map = create_object(DefaultObject, key="Map", location=caller.location)
|
||||
|
||||
# We then access its description directly to make it our map.
|
||||
|
||||
map.db.desc = """
|
||||
≈≈↑↑↑↑↑∩∩
|
||||
≈≈↑╔═╗↑∩∩
|
||||
≈≈↑║O║↑∩∩
|
||||
≈≈↑╚∞╝↑∩∩
|
||||
≈≈≈↑│↑∩∩∩
|
||||
≈≈O─O─O⌂∩
|
||||
≈≈≈↑│↑∩∩∩
|
||||
≈≈↑▲O▲↑∩∩
|
||||
≈≈↑↑▲↑↑∩∩
|
||||
≈≈↑↑↑↑↑∩∩
|
||||
"""
|
||||
|
||||
# This message lets us know our map was created successfully.
|
||||
caller.msg("A map appears out of thin air and falls to the ground.")
|
||||
```
|
||||
|
||||
Log into your game project as the superuser and run the command
|
||||
|
||||
```
|
||||
@batchcode batchcode_map
|
||||
```
|
||||
|
||||
This will load your `batchcode_map.py` file and execute the code (Evennia will look in your `world/`
|
||||
folder automatically so you don't need to specify it).
|
||||
|
||||
A new map object should have appeared on the ground. You can view the map by using `look map`. Let's
|
||||
take it with the `get map` command. We'll need it in case we get lost!
|
||||
|
||||
## Building the map areas
|
||||
|
||||
We've just used batchcode to create an object useful for our adventures. But the locations on that
|
||||
map does not actually exist yet - we're all mapped up with nowhere to go! Let's use batchcode to
|
||||
build a game area based on our map. We have five areas outlined: a castle, a cottage, a campsite, a
|
||||
coastal beach and the crossroads which connects them. Create a new batchcode file for this in
|
||||
`mygame/world`, named `batchcode_world.py`.
|
||||
|
||||
```Python
|
||||
# mygame/world/batchcode_world.py
|
||||
|
||||
from evennia import create_object, search_object
|
||||
from typeclasses import rooms, exits
|
||||
|
||||
# We begin by creating our rooms so we can detail them later.
|
||||
|
||||
centre = create_object(rooms.Room, key="crossroads")
|
||||
north = create_object(rooms.Room, key="castle")
|
||||
east = create_object(rooms.Room, key="cottage")
|
||||
south = create_object(rooms.Room, key="camp")
|
||||
west = create_object(rooms.Room, key="coast")
|
||||
|
||||
# This is where we set up the cross roads.
|
||||
# The rooms description is what we see with the 'look' command.
|
||||
|
||||
centre.db.desc = """
|
||||
The merger of two roads. A single lamp post dimly illuminates the lonely crossroads.
|
||||
To the north looms a mighty castle. To the south the glow of a campfire can be seen.
|
||||
To the east lie a wall of mountains and to the west the dull roar of the open sea.
|
||||
"""
|
||||
|
||||
# Here we are creating exits from the centre "crossroads" location to
|
||||
# destinations to the north, east, south, and west. We will be able
|
||||
# to use the exit by typing it's key e.g. "north" or an alias e.g. "n".
|
||||
|
||||
centre_north = create_object(exits.Exit, key="north",
|
||||
aliases=["n"], location=centre, destination=north)
|
||||
centre_east = create_object(exits.Exit, key="east",
|
||||
aliases=["e"], location=centre, destination=east)
|
||||
centre_south = create_object(exits.Exit, key="south",
|
||||
aliases=["s"], location=centre, destination=south)
|
||||
centre_west = create_object(exits.Exit, key="west",
|
||||
aliases=["w"], location=centre, destination=west)
|
||||
|
||||
# Now we repeat this for the other rooms we'll be implementing.
|
||||
# This is where we set up the northern castle.
|
||||
|
||||
north.db.desc = "An impressive castle surrounds you. " \
|
||||
"There might be a princess in one of these towers."
|
||||
north_south = create_object(exits.Exit, key="south",
|
||||
aliases=["s"], location=north, destination=centre)
|
||||
|
||||
# This is where we set up the eastern cottage.
|
||||
|
||||
east.db.desc = "A cosy cottage nestled among mountains " \
|
||||
"stretching east as far as the eye can see."
|
||||
east_west = create_object(exits.Exit, key="west",
|
||||
aliases=["w"], location=east, destination=centre)
|
||||
|
||||
# This is where we set up the southern camp.
|
||||
|
||||
south.db.desc = "Surrounding a clearing are a number of " \
|
||||
"tribal tents and at their centre a roaring fire."
|
||||
south_north = create_object(exits.Exit, key="north",
|
||||
aliases=["n"], location=south, destination=centre)
|
||||
|
||||
# This is where we set up the western coast.
|
||||
|
||||
west.db.desc = "The dark forest halts to a sandy beach. " \
|
||||
"The sound of crashing waves calms the soul."
|
||||
west_east = create_object(exits.Exit, key="east",
|
||||
aliases=["e"], location=west, destination=centre)
|
||||
|
||||
# Lastly, lets make an entrance to our world from the default Limbo room.
|
||||
|
||||
limbo = search_object('Limbo')[0]
|
||||
limbo_exit = create_object(exits.Exit, key="enter world",
|
||||
aliases=["enter"], location=limbo, destination=centre)
|
||||
|
||||
```
|
||||
|
||||
Apply this new batch code with `@batchcode batchcode_world`. If there are no errors in the code we
|
||||
now have a nice mini-world to explore. Remember that if you get lost you can look at the map we
|
||||
created!
|
||||
|
||||
## In-game minimap
|
||||
|
||||
Now we have a landscape and matching map, but what we really want is a mini-map that displays
|
||||
whenever we move to a room or use the `look` command.
|
||||
|
||||
We *could* manually enter a part of the map into the description of every room like we did our map
|
||||
object description. But some MUDs have tens of thousands of rooms! Besides, if we ever changed our
|
||||
map we would have to potentially alter a lot of those room descriptions manually to match the
|
||||
change. So instead we will make one central module to hold our map. Rooms will reference this
|
||||
central location on creation and the map changes will thus come into effect when next running our
|
||||
batchcode.
|
||||
|
||||
To make our mini-map we need to be able to cut our full map into parts. To do this we need to put it
|
||||
in a format which allows us to do that easily. Luckily, python allows us to treat strings as lists
|
||||
of characters allowing us to pick out the characters we need.
|
||||
|
||||
`mygame/world/map_module.py`
|
||||
```Python
|
||||
# We place our map into a sting here.
|
||||
world_map = """\
|
||||
≈≈↑↑↑↑↑∩∩
|
||||
≈≈↑╔═╗↑∩∩
|
||||
≈≈↑║O║↑∩∩
|
||||
≈≈↑╚∞╝↑∩∩
|
||||
≈≈≈↑│↑∩∩∩
|
||||
≈≈O─O─O⌂∩
|
||||
≈≈≈↑│↑∩∩∩
|
||||
≈≈↑▲O▲↑∩∩
|
||||
≈≈↑↑▲↑↑∩∩
|
||||
≈≈↑↑↑↑↑∩∩
|
||||
"""
|
||||
|
||||
# This turns our map string into a list of rows. Because python
|
||||
# allows us to treat strings as a list of characters, we can access
|
||||
# those characters with world_map[5][5] where world_map[row][column].
|
||||
world_map = world_map.split('\n')
|
||||
|
||||
def return_map():
|
||||
"""
|
||||
This function returns the whole map
|
||||
"""
|
||||
map = ""
|
||||
|
||||
#For each row in our map, add it to map
|
||||
for valuey in world_map:
|
||||
map += valuey
|
||||
map += "\n"
|
||||
|
||||
return map
|
||||
|
||||
def return_minimap(x, y, radius = 2):
|
||||
"""
|
||||
This function returns only part of the map.
|
||||
Returning all chars in a 2 char radius from (x,y)
|
||||
"""
|
||||
map = ""
|
||||
|
||||
#For each row we need, add the characters we need.
|
||||
for valuey in world_map[y-radius:y+radius+1]: for valuex in valuey[x-radius:x+radius+1]:
|
||||
map += valuex
|
||||
map += "\n"
|
||||
|
||||
return map
|
||||
```
|
||||
|
||||
With our map_module set up, let's replace our hardcoded map in `mygame/world/batchcode_map.py` with
|
||||
a reference to our map module. Make sure to import our map_module!
|
||||
|
||||
```python
|
||||
# mygame/world/batchcode_map.py
|
||||
|
||||
from evennia import create_object
|
||||
from evennia import DefaultObject
|
||||
from world import map_module
|
||||
|
||||
map = create_object(DefaultObject, key="Map", location=caller.location)
|
||||
|
||||
map.db.desc = map_module.return_map()
|
||||
|
||||
caller.msg("A map appears out of thin air and falls to the ground.")
|
||||
```
|
||||
|
||||
Log into Evennia as the superuser and run this batchcode. If everything worked our new map should
|
||||
look exactly the same as the old map - you can use `@delete` to delete the old one (use a number to
|
||||
pick which to delete).
|
||||
|
||||
Now, lets turn our attention towards our game's rooms. Let's use the `return_minimap` method we
|
||||
created above in order to include a minimap in our room descriptions. This is a little more
|
||||
complicated.
|
||||
|
||||
By itself we would have to settle for either the map being *above* the description with
|
||||
`room.db.desc = map_string + description_string`, or the map going *below* by reversing their order.
|
||||
Both options are rather unsatisfactory - we would like to have the map next to the text! For this
|
||||
solution we'll explore the utilities that ship with Evennia. Tucked away in `evennia\evennia\utils`
|
||||
is a little module called [EvTable](github:evennia.utils.evtable) . This is an advanced ASCII table
|
||||
creator for you to utilize in your game. We'll use it by creating a basic table with 1 row and two
|
||||
columns (one for our map and one for our text) whilst also hiding the borders. Open the batchfile
|
||||
again
|
||||
|
||||
```python
|
||||
# mygame\world\batchcode_world.py
|
||||
|
||||
# Add to imports
|
||||
from evennia.utils import evtable
|
||||
from world import map_module
|
||||
|
||||
# [...]
|
||||
|
||||
# Replace the descriptions with the below code.
|
||||
|
||||
# The cross roads.
|
||||
# We pass what we want in our table and EvTable does the rest.
|
||||
# Passing two arguments will create two columns but we could add more.
|
||||
# We also specify no border.
|
||||
centre.db.desc = evtable.EvTable(map_module.return_minimap(4,5),
|
||||
"The merger of two roads. A single lamp post dimly " \
|
||||
"illuminates the lonely crossroads. To the north " \
|
||||
"looms a mighty castle. To the south the glow of " \
|
||||
"a campfire can be seen. To the east lie a wall of " \
|
||||
"mountains and to the west the dull roar of the open sea.",
|
||||
border=None)
|
||||
# EvTable allows formatting individual columns and cells. We use that here
|
||||
# to set a maximum width for our description, but letting the map fill
|
||||
# whatever space it needs.
|
||||
centre.db.desc.reformat_column(1, width=70)
|
||||
|
||||
# [...]
|
||||
|
||||
# The northern castle.
|
||||
north.db.desc = evtable.EvTable(map_module.return_minimap(4,2),
|
||||
"An impressive castle surrounds you. There might be " \
|
||||
"a princess in one of these towers.",
|
||||
border=None)
|
||||
north.db.desc.reformat_column(1, width=70)
|
||||
|
||||
# [...]
|
||||
|
||||
# The eastern cottage.
|
||||
east.db.desc = evtable.EvTable(map_module.return_minimap(6,5),
|
||||
"A cosy cottage nestled among mountains stretching " \
|
||||
"east as far as the eye can see.",
|
||||
border=None)
|
||||
east.db.desc.reformat_column(1, width=70)
|
||||
|
||||
# [...]
|
||||
|
||||
# The southern camp.
|
||||
south.db.desc = evtable.EvTable(map_module.return_minimap(4,7),
|
||||
"Surrounding a clearing are a number of tribal tents " \
|
||||
"and at their centre a roaring fire.",
|
||||
border=None)
|
||||
south.db.desc.reformat_column(1, width=70)
|
||||
|
||||
# [...]
|
||||
|
||||
# The western coast.
|
||||
west.db.desc = evtable.EvTable(map_module.return_minimap(2,5),
|
||||
"The dark forest halts to a sandy beach. The sound of " \
|
||||
"crashing waves calms the soul.",
|
||||
border=None)
|
||||
west.db.desc.reformat_column(1, width=70)
|
||||
```
|
||||
|
||||
Before we run our new batchcode, if you are anything like me you would have something like 100 maps
|
||||
lying around and 3-4 different versions of our rooms extending from limbo. Let's wipe it all and
|
||||
start with a clean slate. In Command Prompt you can run `evennia flush` to clear the database and
|
||||
start anew. It won't reset dbref values however, so if you are at #100 it will start from there.
|
||||
Alternatively you can navigate to `mygame/server` and delete the `evennia.db3` file. Now in Command
|
||||
Prompt use `evennia migrate` to have a completely freshly made database.
|
||||
|
||||
Log in to evennia and run `@batchcode batchcode_world` and you'll have a little world to explore.
|
||||
|
||||
## Conclusions
|
||||
|
||||
You should now have a mapped little world and a basic understanding of batchcode, EvTable and how
|
||||
easily new game defining features can be added to Evennia.
|
||||
|
||||
You can easily build from this tutorial by expanding the map and creating more rooms to explore. Why
|
||||
not add more features to your game by trying other tutorials: [Add weather to your world](Weather-
|
||||
Tutorial), [fill your world with NPC's](./Tutorial-Aggressive-NPCs.md) or
|
||||
[implement a combat system](Beginner-Tutorial/Part3/Turn-based-Combat-System.md).
|
||||
127
docs/source/Howtos/Tutorial-Aggressive-NPCs.md
Normal file
127
docs/source/Howtos/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](../Components/Scripts.md) 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.md) 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](../Components/Objects.md#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_post_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_post_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/Howtos/Tutorial-NPCs-listening.md
Normal file
110
docs/source/Howtos/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.md) 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 f"{from_obj} said: '{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(f"say {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/Howtos/Tutorial-Tweeting-Game-Stats.md
Normal file
96
docs/source/Howtos/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](../Setup/How-to-connect-Evennia-to-Twitter.md) 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 = f"Prototype Count: {nprots} Random Keys: "
|
||||
|
||||
tweet += f" {keys[randint(0,len(keys)-1)]}"
|
||||
for x in range(0,2): ##tweet 3
|
||||
tweet += f", {keys[randint(0,len(keys)-1)]}"
|
||||
# post the tweet
|
||||
try:
|
||||
response = api.PostUpdate(tweet)
|
||||
except:
|
||||
logger.log_trace(f"Tweet Error: When attempting to tweet {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](../Components/Scripts.md) 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/Howtos/Tutorial-Vehicles.md
Normal file
422
docs/source/Howtos/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](../Components/Objects.md#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](../Components/Commands.md): 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](../Components/Command-Sets.md) `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](../Components/Locks.md): 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(f"The train is moving forward to {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](../Components/Scripts.md): 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](../Components/Objects.md#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!
|
||||
665
docs/source/Howtos/Tutorial-for-basic-MUSH-like-game.md
Normal file
665
docs/source/Howtos/Tutorial-for-basic-MUSH-like-game.md
Normal file
|
|
@ -0,0 +1,665 @@
|
|||
# Tutorial for basic MUSH like game
|
||||
|
||||
|
||||
This tutorial lets you code a small but complete and functioning MUSH-like game in Evennia. A
|
||||
[MUSH](https://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](Beginner-Tutorial/Part1/Beginner-Tutorial-Part1-Intro.md) 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](../Components/Attributes.md) `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](../Components/Commands.md) to set the "Power" on the `Character`.
|
||||
- A chargen [CmdSet](../Components/Command-Sets.md) 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](../Components/Commands.md) 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(f"Your Power was set to {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](../Components/Command-Sets.md) 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, persistent=True)
|
||||
```
|
||||
Note how new rooms created with this typeclass will always start with `ChargenCmdset` on themselves.
|
||||
Don't forget the `persistent=True` keyword or you will lose the cmdset after a server reload. For
|
||||
more information about [Command Sets](../Components/Command-Sets.md) and [Commands](../Components/Commands.md), 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](../Components/Attributes.md) 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_template = "{attacker} +attack{s} with a combat score of {c_score}!"
|
||||
caller.msg(message_template.format(
|
||||
attacker="You",
|
||||
s="",
|
||||
c_score=combat_score,
|
||||
))
|
||||
caller.location.msg_contents(message_template.format(
|
||||
attacker=caller.key,
|
||||
s="s",
|
||||
c_score=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 = f" (combat score: {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](../Components/Permissions.md).
|
||||
|
||||
### 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=f"edit:id({caller.id}) and perm(Builders);call:false()")
|
||||
# announce
|
||||
message_template = "{creator} created the NPC '{npc}'."
|
||||
caller.msg(message_template.format(
|
||||
creator="You",
|
||||
npc=name,
|
||||
))
|
||||
caller.location.msg_contents(message_template.format(
|
||||
creator=caller.key,
|
||||
npc=name,
|
||||
), exclude=caller)
|
||||
```
|
||||
Here we define a `+createnpc` (`+createNPC` works too) that is callable by everyone *not* having the
|
||||
`nonpcs` "[permission](../Components/Permissions.md)" (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](../Components/Locks.md) 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](../Components/Attributes.md) 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 = f"Properties of {npc.key}:"
|
||||
for propname in allowed_propnames:
|
||||
propvalue = npc.attributes.get(propname, default="N/A")
|
||||
output += f"\n {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(f"You told {npc.key} to do '{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](Beginner-Tutorial/Part1/Tutorial-World.md). For
|
||||
more specific ideas, see the [other tutorials and hints](./Howtos-Overview.md) as well
|
||||
as the [Evennia Component overview](../Components/Components-Overview.md).
|
||||
198
docs/source/Howtos/Understanding-Color-Tags.md
Normal file
198
docs/source/Howtos/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](../Concepts/TextTags.md) 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](../Concepts/TextTags.md) 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/Howtos/Weather-Tutorial.md
Normal file
54
docs/source/Howtos/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](../Components/TickerHandler.md) 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.
|
||||
647
docs/source/Howtos/Web-Character-Generation.md
Normal file
647
docs/source/Howtos/Web-Character-Generation.md
Normal file
|
|
@ -0,0 +1,647 @@
|
|||
# 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](../Concepts/New-Models.md) 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(" or ".join([
|
||||
f"puppet:id({char.id})",
|
||||
f"pid({user.id})",
|
||||
"perm(Developers)",
|
||||
"pperm(Developers)",
|
||||
]))
|
||||
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](../Components/Permissions.md) (copied from the `AccountDB`).
|
||||
* The right `puppet` [locks](../Components/Locks.md) so the Account can actually play as this Character later.
|
||||
* The relevant Character [typeclass](../Components/Typeclasses.md)
|
||||
* 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(" or ".join([
|
||||
f"puppet:id({char.id})",
|
||||
f"pid({user.id})",
|
||||
"perm(Developers)",
|
||||
"pperm(Developers)",
|
||||
]))
|
||||
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/Howtos/Web-Character-View-Tutorial.md
Normal file
229
docs/source/Howtos/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