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

|
||||
|
||||
*To get into the mood of this miniature quest, imagine you are an adventurer out to find fame and
|
||||
fortune. You have heard rumours of an old castle ruin by the coast. In its depth a warrior princess
|
||||
was buried together with her powerful magical weapon - a valuable prize, if it's true. Of course
|
||||
this is a chance to adventure that you cannot turn down!*
|
||||
|
||||
*You reach the ocean in the midst of a raging thunderstorm. With wind and rain screaming in your
|
||||
face you stand where the moor meets the sea along a high, rocky coast ...*
|
||||
|
||||
---
|
||||
|
||||
### Gameplay hints
|
||||
|
||||
- Use the command `tutorial` to get code insight behind the scenes of every room.
|
||||
- Look at everything. While a demo, the Tutorial World is not necessarily trivial to solve - it depends
|
||||
on your experience with text-based adventure games. Just remember that everything can be solved or bypassed.
|
||||
- Some objects are interactive in more than one way. Use the normal `help` command to get a feel for
|
||||
which commands are available at any given time.
|
||||
- In order to fight, you need to first find some type of weapon.
|
||||
- *slash* is a normal attack
|
||||
- *stab* launches an attack that makes more damage but has a lower chance to hit.
|
||||
- *defend* will lower the chance to taking damage on your enemy's next attack.
|
||||
- Some things _cannot_ be hurt by mundane weapons. In that case it's OK to run away. Expect
|
||||
to be chased though.
|
||||
- Being defeated is a part of the experience. You can't actually die, but getting knocked out
|
||||
means being left in the dark ...
|
||||
|
||||
## Once you are done (or had enough)
|
||||
|
||||
Afterwards you'll either have conquered the old ruin and returned in glory and triumph ... or
|
||||
you returned limping and whimpering from the challenge by using the `give up` command.
|
||||
Either way you should now be back in Limbo, able to reflect on the experience.
|
||||
|
||||
Some features exemplified by the tutorial world:
|
||||
|
||||
- Rooms with custom ability to show details (like looking at the wall in the dark room)
|
||||
- Hidden or impassable exits until you fulfilled some criterion
|
||||
- Objects with multiple custom interactions (like swords, the well, the obelisk ...)
|
||||
- Large-area rooms (that bridge is actually only one room!)
|
||||
- Outdoor weather rooms with weather (the rain pummeling you)
|
||||
- Dark room, needing light source to reveal itself (the burning splinter even burns out after a while)
|
||||
- Puzzle object (the wines in the dark cell; hope you didn't get stuck!)
|
||||
- Multi-room puzzle (the obelisk and the crypt)
|
||||
- Aggressive mobile with roam, pursue and battle state-engine AI (quite deadly until you find the right weapon)
|
||||
- Weapons, also used by mobs (most are admittedly not that useful against the big baddie)
|
||||
- Simple combat system with attack/defend commands (teleporting on-defeat)
|
||||
- Object spawning (the weapons in the barrel and the final weapoon is actually randomized)
|
||||
- Teleporter trap rooms (if you fail the obelisk puzzle)
|
||||
|
||||
```{sidebar} Extra Credit
|
||||
|
||||
If you have previous programming experience (or after you have gone
|
||||
through this Starter tutorial) it may be instructive to dig a little deeper into the Tutorial-world
|
||||
code to learn how it achieves what it does. The code is heavily documented.
|
||||
You can find all the code in [evennia/contrib/tutorials/tutorial_world](../../../api/evennia.contrib.tutorials.tutorial_world.md).
|
||||
The build-script is [here](github:evennia/contrib/tutorials/tutorial_world/build.ev).
|
||||
|
||||
|
||||
When reading the code, remember that the Tutorial World was designed to install easily and to not permanently modify
|
||||
the rest of the game. It therefore makes sure to only use temporary solutions and to clean up after itself. This is
|
||||
not something you will often need to worry about when making your own game.
|
||||
```
|
||||
|
||||
Quite a lot of stuff crammed in such a small area!
|
||||
|
||||
## Uninstall the tutorial world
|
||||
|
||||
Once are done playing with the tutorial world, let's uninstall it.
|
||||
Uninstalling the tutorial world basically means deleting all the rooms and objects it consists of.
|
||||
Make sure you are back in Limbo, then
|
||||
|
||||
find tut#01
|
||||
find tut#16
|
||||
|
||||
This should locate the first and last rooms created by `build.ev` - *Intro* and *Outro*. If you
|
||||
installed normally, everything created between these two numbers should be part of the tutorial.
|
||||
Note their #dbref numbers, for example 5 and 80. Next we just delete all objects in that range:
|
||||
|
||||
del 5-80
|
||||
|
||||
You will see some errors since some objects are auto-deleted and so cannot be found when the delete
|
||||
mechanism gets to them. That's fine. You should have removed the tutorial completely once the
|
||||
command finishes.
|
||||
|
||||
Even if the game-style of the Tutorial-world was not similar to the one you are interested in, it
|
||||
should hopefully have given you a little taste of some of the possibilities of Evennia. Now we'll
|
||||
move on with how to access this power through code.
|
||||
|
||||
|
||||
|
|
@ -1,206 +0,0 @@
|
|||
# On Planning a Game
|
||||
|
||||
Last lesson we asked ourselves some questions about our motivation. In this one we'll present
|
||||
some more technical questions to consider. In the next lesson we'll answer them for the sake of
|
||||
our tutorial game.
|
||||
|
||||
Note that the suggestions on this page are just that - suggestions. Also, they are primarily aimed at a lone
|
||||
hobby designer or a small team developing a game in their free time.
|
||||
|
||||
```{important}
|
||||
|
||||
Your first all overshadowing goal is to beat the odds and get **something** out the door!
|
||||
Even if it's a scaled-down version of your dream game, lacking many "must-have" features!
|
||||
|
||||
```
|
||||
|
||||
Remember: *99.99999% of all great game ideas never lead to a game*. Especially not to an online
|
||||
game that people can actually play and enjoy. It's better to get your game out there and expand on it
|
||||
later than to code in isolation until you burn out, lose interest or your hard drive crashes.
|
||||
|
||||
- Keep the scope of your initial release down. Way down.
|
||||
- Start small, with an eye towards expansions later, after first release.
|
||||
- If the suggestions here seems boring or a chore to you, do it your way instead. Everyone's different.
|
||||
- Keep having _fun_. You must keep your motivation up, whichever way works for _you_.
|
||||
|
||||
|
||||
## The steps
|
||||
|
||||
Here are the rough steps towards your goal.
|
||||
|
||||
1. Planning
|
||||
2. Coding + Gradually building a tech-demo
|
||||
3. Building the actual game world
|
||||
4. Release
|
||||
5. Celebrate
|
||||
|
||||
## Planning
|
||||
|
||||
You need to have at least a rough idea about what you want to create. Some like a lot of planning, others
|
||||
do it more seat-of-the-pants style. Regardless, while _some_ planning is always good to do, it's common
|
||||
to have your plans change on you as you create your code prototypes. So don't get _too_ bogged down in
|
||||
the details out of the gate.
|
||||
|
||||
Many prospective game developers are very good at *parts* of this process, namely in defining what their
|
||||
world is "about": The theme, the world concept, cool monsters and so on. Such things are very important. But
|
||||
unfortunately, they are not enough to make your game. You need to figure out how to accomplish your ideas in
|
||||
Evennia.
|
||||
|
||||
Below are some questions to get you going. In the next lesson we will try to answer them for our particular
|
||||
tutorial game. There are of course many more questions you could be asking yourself.
|
||||
|
||||
### Administration
|
||||
|
||||
- Should your game rules be enforced by coded systems or by human game masters?
|
||||
- What is the staff hierarchy in your game? Is vanilla Evennia roles enough or do you need something else?
|
||||
- Should players be able to post out-of-characters on channels and via other means like bulletin-boards?
|
||||
|
||||
### Building
|
||||
|
||||
- How will the world be built? Traditionally (from in-game with build-commands) or externally (by batchcmds/code
|
||||
or directly with custom code)?
|
||||
- Can only privileged Builders create things or should regular players also have limited build-capability?
|
||||
|
||||
### Systems
|
||||
|
||||
- Do you base your game off an existing RPG system or make up your own?
|
||||
- What are the game mechanics? How do you decide if an action succeeds or fails?
|
||||
- Does the flow of time matter in your game - does night and day change? What about seasons?
|
||||
- Do you want changing, global weather or should weather just be set manually in roleplay?
|
||||
- Do you want a coded world-economy or just a simple barter system? Or no formal economy at all?
|
||||
- Do you have concepts like reputation and influence?
|
||||
- Will your characters be known by their name or only by their physical appearance?
|
||||
|
||||
### Rooms
|
||||
|
||||
- Is a simple room description enough or should the description be able to change (such as with time, by
|
||||
light conditions, weather or season)?
|
||||
- Should the room have different statuses? Can it have smells, sounds? Can it be affected by
|
||||
dramatic weather, fire or magical effects? If so, how would this affect things in the room? Or are
|
||||
these things something admins/game masters should handle manually?
|
||||
- Can objects be hidden in the room? Can a person hide in the room? How does the room display this?
|
||||
|
||||
### Objects / items
|
||||
|
||||
- How numerous are your objects? Do you want large loot-lists or are objects just role playing props
|
||||
created on demand?
|
||||
- If you use money, is each coin a separate object or do you just store a bank account value?
|
||||
- Do multiple similar objects form stacks and how are those stacks handled in that case?
|
||||
- Does an object have weight or volume (so you cannot carry an infinite amount of them)?
|
||||
- Can objects be broken? Can they be repaired?
|
||||
- Can you fight with a chair or a flower or must you use a specific 'weapon' kind of thing?
|
||||
- Will characters be able to craft new objects?
|
||||
- Should mobs/NPCs have some sort of AI?
|
||||
- Are NPCs and mobs different entities? How do they differ?
|
||||
- Should there be NPCs giving quests? If so, how do you track Quest status?
|
||||
|
||||
### Characters
|
||||
|
||||
- Can players have more than one Character active at a time or are they allowed to multi-play?
|
||||
- How does the character-generation work? Walk from room-to-room? A menu?
|
||||
- How do you implement different "classes" or "races"? Are they separate types of objects or do you
|
||||
simply load different stats on a basic object depending on what the Player wants?
|
||||
- If a Character can hide in a room, what skill will decide if they are detected?
|
||||
- What does the skill tree look like? Can a Character gain experience to improve? By killing
|
||||
enemies? Solving quests? By roleplaying?
|
||||
- May player-characters attack each other (PvP)?
|
||||
- What are the penalties of defeat? Permanent death? Quick respawn? Time in prison?
|
||||
|
||||
A MUD's a lot more involved than you would think and these things hang together in a complex web. It
|
||||
can easily become overwhelming and it's tempting to want *all* functionality right out of the door.
|
||||
Try to identify the basic things that "make" your game and focus *only* on them for your first
|
||||
release. Make a list. Keep future expansions in mind but limit yourself.
|
||||
|
||||
## Coding and Tech demo
|
||||
|
||||
This is the actual work of creating the "game" part of your game. As you code and test systems you should
|
||||
build a little "tech demo" along the way.
|
||||
|
||||
```{sidebar} Tech demo
|
||||
|
||||
With "tech demo" we mean a small example of your code in-action: A room with a mob, a way to jump into and test character-creation etc. The tech demo need not be pretty, it's there to test functionality. It's not the beginning of your game world (unless you find that to be more fun).
|
||||
|
||||
```
|
||||
|
||||
Try to avoid going wild with building a huge game world before you have a tech-demo showing off all parts
|
||||
you expect to have in the first version of your game. Otherwise you run the risk of having to redo it all
|
||||
again.
|
||||
|
||||
Evennia tries hard to make the coding easier for you, but there is no way around the fact that if you want
|
||||
anything but a basic chat room you *will* have to bite the bullet and code your game (or find a coder willing
|
||||
to do it for you).
|
||||
|
||||
> Even if you won't code anything yourself, as a designer you need to at least understand the basic
|
||||
paradigms and components of Evennia. It's recommended you look over the rest of this Beginner Tutorial to learn
|
||||
what tools you have available.
|
||||
|
||||
During Coding you look back at the things you wanted during the **Planning** phase and try to
|
||||
implement them. Don't be shy to update your plans if you find things easier/harder than you thought.
|
||||
The earlier you revise problems, the easier they will be to fix.
|
||||
|
||||
A good idea is to host your code online using _version control_. Github.com offers free Private repos
|
||||
these days if you don't want the world to learn your secrets. Not only version control
|
||||
make it easy for your team to collaborate, it also means
|
||||
your work is backed up at all times. The page on [Version Control](../../../Coding/Version-Control.md)
|
||||
will help you to setting up a sane developer environment with proper version control.
|
||||
|
||||
## World Building
|
||||
|
||||
Up until this point we've only had a few tech-demo objects in the database. This step is the act of
|
||||
populating the database with a larger, thematic world. Too many would-be developers jump to this
|
||||
stage too soon (skipping the **Coding** or even **Planning** stages). What if the rooms you build
|
||||
now doesn't include all the nice weather messages the code grows to support? Or the way you store
|
||||
data changes under the hood? Your building work would at best require some rework and at worst you
|
||||
would have to redo the whole thing. You could be in for a *lot* of unnecessary work if you build stuff
|
||||
en masse without having the underlying code systems in some reasonable shape first.
|
||||
|
||||
So before starting to build, the "game" bit (**Coding** + **Testing**) should be more or less
|
||||
**complete**, *at least to the level of your initial release*.
|
||||
|
||||
Make sure it is clear to yourself and your eventual builders just which parts of the world you want
|
||||
for your initial release. Establish for everyone which style, quality and level of detail you expect.
|
||||
|
||||
Your goal should *not* be to complete your entire world in one go. You want just enough to make the
|
||||
game's "feel" come across. You want a minimal but functioning world where the intended game play can
|
||||
be tested and roughly balanced. You can always add new areas later.
|
||||
|
||||
During building you get free and extensive testing of whatever custom build commands and systems you
|
||||
have made at this point. If Builders and coders are different people you also
|
||||
get a chance to hear if some things are hard to understand or non-intuitive. Make sure to respond
|
||||
to this feedback.
|
||||
|
||||
|
||||
## Alpha Release
|
||||
|
||||
As mentioned, don't hold onto your world more than necessary. *Get it out there* with a huge *Alpha*
|
||||
flag and let people try it!
|
||||
|
||||
Call upon your alpha-players to try everything - they *will* find ways to break your game in ways that
|
||||
you never could have imagined. In Alpha you might be best off to
|
||||
focus on inviting friends and maybe other MUD developers, people who you can pester to give proper
|
||||
feedback and bug reports (there *will* be bugs, there is no way around it).
|
||||
|
||||
Follow the quick instructions for [Online Setup](../../../Setup/Online-Setup.md) to make your
|
||||
game visible online.
|
||||
|
||||
If you hadn't already, make sure to put up your game on the
|
||||
[Evennia game index](http://games.evennia.com/) so people know it's in the works (actually, even
|
||||
pre-alpha games are allowed in the index so don't be shy)!
|
||||
|
||||
## Beta Release/Perpetual Beta
|
||||
|
||||
Once things stabilize in Alpha you can move to *Beta* and let more people in. Many MUDs are in
|
||||
[perpetual beta](https://en.wikipedia.org/wiki/Perpetual_beta), meaning they are never considered
|
||||
"finished", but just repeat the cycle of Planning, Coding, Testing and Building over and over as new
|
||||
features get implemented or Players come with suggestions. As the game designer it is now up to you
|
||||
to gradually perfect your vision.
|
||||
|
||||
## Congratulate yourself!
|
||||
|
||||
You are worthy of a celebration since at this point you have joined the small, exclusive crowd who
|
||||
have made their dream game a reality!
|
||||
|
||||
## Planning our tutorial game
|
||||
|
||||
In the next lesson we'll make use of these general points and try to plan out our tutorial game.
|
||||
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
# Part 2: What we want
|
||||
|
||||
```{sidebar} Beginner Tutorial Parts
|
||||
- [Introduction](../Beginner-Tutorial-Intro.md)
|
||||
<br>Getting set up.
|
||||
- Part 1: [What we have](../Part1/Beginner-Tutorial-Part1-Intro.md)
|
||||
<br>A tour of Evennia and how to use the tools, including an introduction to Python.
|
||||
- **Part 2: [What we want](./Beginner-Tutorial-Part2-Intro.md)**
|
||||
<br>Planning our tutorial game and what to think about when planning your own in the future.
|
||||
- Part 3: [How we get there](../Part3/Beginner-Tutorial-Part3-Intro.md)
|
||||
<br>Getting down to the meat of extending Evennia to make our game
|
||||
- Part 4: [Using what we created](../Part4/Beginner-Tutorial-Part4-Intro.md)
|
||||
<br>Building a tech-demo and world content to go with our code
|
||||
- Part 5: [Showing the world](../Part5/Beginner-Tutorial-Part5-Intro.md)
|
||||
<br>Taking our new game online and let players try it out
|
||||
```
|
||||
|
||||
In Part two of the Evennia Beginner Tutorial we'll take a step back and plan out the kind of tutorial game we want to make. This is a more 'theoretical' part where we won't do any hands-on
|
||||
programming.
|
||||
|
||||
In the process we'll go through the common questions of "where to start"
|
||||
and "what to think about" when creating a multiplayer online text game.
|
||||
|
||||
## Lessons
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 1
|
||||
|
||||
Beginner-Tutorial-Planning-Where-Do-I-Begin.md
|
||||
Beginner-Tutorial-Game-Planning.md
|
||||
Beginner-Tutorial-Planning-The-Tutorial-Game.md
|
||||
```
|
||||
|
||||
## Table of Contents
|
||||
|
||||
```{toctree}
|
||||
|
||||
Beginner-Tutorial-Planning-Where-Do-I-Begin.md
|
||||
Beginner-Tutorial-Game-Planning.md
|
||||
Beginner-Tutorial-Planning-The-Tutorial-Game.md
|
||||
```
|
||||
|
|
@ -1,462 +0,0 @@
|
|||
# Planning our tutorial game
|
||||
|
||||
Using the general plan from last lesson we'll now establish what kind of game we want to create for this tutorial. We'll call it ... _EvAdventure_.
|
||||
Remembering that we need to keep the scope down, let's establish some parameters.
|
||||
|
||||
- We want EvAdventure be a small game we can play ourselves for fun, but which could in principle be expanded to something more later.
|
||||
- We want to have a clear game-loop, with clear goals.
|
||||
- Let's go with a fantasy theme, it's well understood.
|
||||
- We will use a small, existing tabletop RPG rule set ([Knave](https://www.drivethrurpg.com/product/250888/Knave), more info later)
|
||||
- We want to be able to create and customize a character of our own.
|
||||
- While not roleplay-focused, it should still be possible to socialize and to collaborate.
|
||||
- We don't want to have to rely on a Game master to resolve things, but will rely on code for skill resolution and combat.
|
||||
- We want monsters to fight and NPCs we can talk to. So some sort of AI.
|
||||
- We want some sort of quest system and merchants to buy stuff from.
|
||||
|
||||
|
||||
## Game concept
|
||||
|
||||
With these points in mind, here's a quick blurb for our game:
|
||||
|
||||
_Recently, the nearby village discovered that the old abandoned well contained a dark secret. The bottom of the well led to a previously undiscovered dungeon of ever shifting passages. No one knew why it was there or what its purpose was, but local rumors abound. The first adventurer that went down didn't come back. The second ... brought back a handful of glittering riches._
|
||||
|
||||
_Now the rush is on - there's a dungeon to explore and coin to earn. Knaves, cutthroats, adventurers and maybe even a hero or two are coming from all over the realm to challenge whatever lurks at the bottom of that well._
|
||||
|
||||
_Local merchants and opportunists have seen a chance for profit. A camp of tents has sprung up around the old well, providing food and drink, equipment, entertainment and rumors for a price. It's a festival to enjoy before paying the entrance fee for dropping down the well to find your fate among the shadows below ..._
|
||||
|
||||
Our game will consist of two main game modes - above ground and below. The player starts above ground and is expected to do 'expeditions' into the dark. The design goal is for them to be forced back up again when their health, equipment and luck is about to run out.
|
||||
- Above, in the "dungeon festival", the player can restock and heal up, buy things and do a small set of quests. It's the only place where the characters can sleep and fully heal. They also need to spend coin here to gain XP and levels. This is a place for players to socialize and RP. There is no combat above ground except for an optional spot for non-lethal PvP.
|
||||
- Below is the mysterious dungeon. This is a procedurally generated set of rooms. Players can collaborate if they go down the well together, they will not be able to run into each other otherwise (so this works as an instance). Each room generally presents some challenge (normally a battle). Pushing deeper is more dangerous but can grant greater rewards. While the rooms could in theory go on forever, there should be a boss encounter once a player reaches deep enough.
|
||||
|
||||
Here's an overview of the topside camp for inspiration (quickly thrown together in the free version of [Inkarnate](https://inkarnate.com/)). We'll explore how to break this up into "rooms" (locations) when we get to creating the game world later.
|
||||
|
||||

|
||||
|
||||
For the rest of this lesson we'll answer and reason around the specific questions posed in the previous [Game Planning](./Beginner-Tutorial-Game-Planning.md) lesson.
|
||||
|
||||
## Administration
|
||||
|
||||
### Should your game rules be enforced by coded systems by human game masters?
|
||||
|
||||
Generally, the more work you expect human staffers/GMs to do, the less your code needs to work. To support GMs you'd need to design commands to support GM-specific actions and the type of game-mastering you want them to do. You may need to expand communication channels so you can easily talk to groups people in private and split off gaming groups from each other. RPG rules could be as simple
|
||||
as the GM sitting with the rule books and using a dice-roller for visibility.
|
||||
|
||||
GM:ing is work-intensive however, and even the most skilled and enthusiastic GM can't be awake all hours of the day to serve an international player base. The computer never needs sleep, so having the ability for players to "self-serve" their RP itch when no GMs are around is a good idea even for the most GM-heavy games.
|
||||
|
||||
On the other side of the spectrum are games with no GMs at all; all gameplay are driven either by the computer or by the interactions between players. Such games still need an active staff, but nowhere as much active involvement. Allowing for solo-play with the computer also allows players to have fun when the number of active
|
||||
players is low.
|
||||
|
||||
**EvAdventure Answer:**
|
||||
|
||||
We want EvAdventure to work entirely without depending on human GMs. That said, there'd be nothing stopping a GM from stepping in and run an adventure for some players should they want to.
|
||||
|
||||
### What is the staff hierarchy in your game? Is vanilla Evennia roles enough or do you need something else?
|
||||
|
||||
The default hierarchy is
|
||||
|
||||
- `Player` - regular players
|
||||
- `Player Helper` - can create/edit help entries
|
||||
- `Builder` - can use build commands
|
||||
- `Admin` - can kick and ban accounts
|
||||
- `Developer` - full access, usually also trusted with server access
|
||||
|
||||
There is also the _superuser_, the "owner" of the game you create when you first set up your database. This user
|
||||
goes outside the regular hierarchy and should usually only.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
We are okay with keeping the default permission structure for our game.
|
||||
|
||||
### Should players be able to post out-of-characters on channels and via other means like bulletin-boards?
|
||||
|
||||
Evennia's _Channels_ are by default only available between _Accounts_. That is, for players to communicate with each
|
||||
other. By default, the `public` channel is created for general discourse.
|
||||
Channels are logged to a file and when you are coming back to the game you can view the history of a channel in case you missed something.
|
||||
|
||||
> public Hello world!
|
||||
[Public] MyName: Hello world!
|
||||
|
||||
But Channels can also be set up to work between Characters instead of Accounts. This would mean the channels would have an in-game meaning:
|
||||
|
||||
- Members of a guild could be linked telepathically.
|
||||
- Survivors of the apocalypse can communicate over walkie-talkies.
|
||||
- Radio stations you can tune into or have to discover.
|
||||
|
||||
_Bulletin boards_ are a sort of in-game forum where posts are made publicly or privately. Contrary to a channel, the messages are usually stored and are grouped into topics with replies. Evennia has no default bulletin-board system.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
In EvAdventure we will just use the default inter-account channels. We will also not be implementing any bulletin boards; instead the merchant NPCs will act as quest givers.
|
||||
|
||||
## Building
|
||||
|
||||
### How will the world be built?
|
||||
|
||||
There are two main ways to handle this:
|
||||
- Traditionally, from in-game with build-commands: This means builders creating content in their game client. This has the advantage of not requiring Python skills nor server access. This can often be a quite intuitive way to build since you are sort-of walking around in your creation as you build it. However, the developer (you) must make sure to provide build-commands that are flexible enough for builders to be able to create the content you want for your game.
|
||||
- Externally (by batchcmds): Evennia's `batchcmd` takes a text file with Evennia Commands and executes them in sequence. This allows the build process to be repeated and applied quickly to a new database during development.
|
||||
It also allows builders to use proper text-editing tools rather than writing things line-by-line in their clients. The drawback is that for their changes to go live they either need server access or they need to send their batchcode to the game administrator so they can apply the changes. Or use version control.
|
||||
- Externally (with batchcode or custom code): This is the "professional game development" approach. This gives the builders maximum power by creating the content in Python using Evennia primitives. The `batchcode` processor
|
||||
allows Evennia to apply and re-apply build-scripts that are raw Python modules. Again, this would require the builder to have server access or to use version control to share their work with the rest of the development team.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
For EvAdventure, we will build the above-ground part of the game world using batch-scripts. The world below-ground we will build procedurally, using raw code.
|
||||
|
||||
### Can only privileged Builders create things or should regular players also have limited build-capability?
|
||||
|
||||
In some game styles, players have the ability to create objects and even script them. While giving regular users the ability to create objects with in-built commands is easy and safe, actual code-creation (aka _softcode_ ) is not something Evennia supports natively.
|
||||
|
||||
Regular, untrusted users should never be allowed to execute raw Python
|
||||
code (such as what you can do with the `py` command). You can
|
||||
[read more about Evennia's stance on softcode here](../../../Concepts/Soft-Code.md). If you want users to do limited scripting, it's suggested that this is accomplished by adding more powerful build-commands for them to use.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
For our tutorial-game, we will only allow privileged builders and admins to modify the world.
|
||||
|
||||
## Systems
|
||||
|
||||
### Do you base your game off an existing RPG system or make up your own?
|
||||
|
||||
There is a plethora of options out there, and what you choose depends on the game you want. It can be tempting to grab a short free-form ruleset, but remember that the computer does not have any intuitiion or common sense to interpret the rules like a human GM could. Conversely, if you pick a very 'crunchy' game system, with detailed simulation of the real world, remember that you'll need to actually _code_ all those exceptions and tables yourself.
|
||||
|
||||
For speediest development, what you want is a game with a _consolidated_ resolution mechanic - one you can code once and then use in a lot of situations. But you still want enough rules to help telling the computer how various situations should be resolved (combat is the most common system that needs such structure).
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
For this tutorial, we will make use of [Knave](https://www.drivethrurpg.com/product/250888/Knave), a very light [OSR](https://en.wikipedia.org/wiki/Old_School_Renaissance) ruleset by Ben Milton. It's only a few pages long but highly compatible with old-school D&D games. It's consolidates all rules around a few opposed d20 rolls and includes clear rules for combat, inventory, equipment and so on. Since _Knave_ is a tabletop RPG, we will have to do some minor changes here and there to fit it to the computer medium.
|
||||
|
||||
_Knave_ is available under a Creative Commons Attributions 4.0 License, meaning it can be used for derivative work (even commercially). The above link allows you to purchase the PDF and supporting the author. Alternatively you can find unofficial fan releases of the rules [on this page](https://dungeonsandpossums.com/2020/04/some-great-knave-rpg-resources/).
|
||||
|
||||
|
||||
### What are the game mechanics? How do you decide if an action succeeds or fails?
|
||||
|
||||
This follows from the RPG system decided upon in the previous question.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
_Knave_ gives every character a set of six traditional stats: Strength, Intelligence, Dexterity, Constitution, Intelligence, Wisdom and Charisma. Each has a value from +1 to +10. To find its "Defense" value, you add 10.
|
||||
|
||||
You have Strength +1. Your Strength-Defense is 10 + 1 = 11
|
||||
|
||||
To make a check, say an arm-wrestling challenge you roll a twenty-sided die (d20) and add your stat. You have to roll higher than the opponents defense for that stat.
|
||||
|
||||
I have Strength +1, my opponent has a Strength of +2. To beat them in arm wrestling I must roll d20 + 1 and hope to get higher than 12, which is their Strength defense (10 + 2).
|
||||
|
||||
If you attack someone you do the same, except you roll against their `Armor` defense. If you rolled higher, you roll for how much damage you do (depends on your weapon).
|
||||
You can have _advantage_ or _disadvantage_ on a roll. This means rolling 2d20 and picking highest or lowest value.
|
||||
|
||||
In Knave, combat is turn-based. In our implementation we'll also play turn-based, but we'll resolve everything _simultaneously_. This changes _Knave_'s feel quite a bit, but is a case where the computer can do things not practical to do when playing around a table.
|
||||
|
||||
There are also a few tables we'll need to implement. For example, if you lose all health, there's a one-in-six chance you'll die outright. We'll keep this perma-death aspect, but make it very easy to start a new character and jump back in.
|
||||
|
||||
> In this tutorial we will not add opportunities to make use of all of the character stats, making some, like strength, intelligence and dexterity more useful than others. In a full game, one would want to expand so a user can utilize all of their character's strengths.
|
||||
|
||||
### Does the flow of time matter in your game - does night and day change? What about seasons?
|
||||
|
||||
Most commonly, game-time runs faster than real-world time. There are
|
||||
a few advantages with this:
|
||||
|
||||
- Unlike in a single-player game, you can't fast-forward time in a multiplayer game if you are waiting for something, like NPC shops opening.
|
||||
- Healing and other things that we know takes time will go faster while still being reasonably 'realistic'.
|
||||
|
||||
The main drawback is for games with slower roleplay pace. While you are having a thoughtful roleplaying scene over dinner, the game world reports that two days have passed. Having a slower game time than real-time is a less common, but possible solution for such games.
|
||||
|
||||
It is however _not_ recommended to let game-time exactly equal the speed of real time. The reason for this is that people will join your game from all around the world, and they will often only be able to play at particular times of their day. With a game-time drifting relative real-time, everyone will eventually be able to experience both day and night in the game.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
The passage of time will have no impact on our particular game example, so we'll go with Evennia's default, which is that the game-time runs two times faster than real time.
|
||||
|
||||
### Do you want changing, global weather or should weather just be set manually in roleplay?
|
||||
|
||||
A weather system is a good example of a game-global system that affects a subset of game entities (outdoor rooms).
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
We'll not change the weather, but will add some random messages to echo through
|
||||
the game world at random intervals just to show the principle.
|
||||
|
||||
### Do you want a coded world-economy or just a simple barter system? Or no formal economy at all?
|
||||
This is a big question and depends on how deep and interconnected the virtual transactions are that are happening in the game. Shop prices could rice and drop due to supply and demand, supply chains could involve crafting and production. One also could consider adding money sinks and manipulate the in-game market to combat inflation.
|
||||
|
||||
The [Barter](../../../Contribs/Contrib-Barter.md) contrib provides a full interface for trading with another player in a safe way.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
We will not deal with any of this complexity. We will allow for players to buy from npc sellers and players will be able to trade using the normal `give` command.
|
||||
|
||||
### Do you have concepts like reputation and influence?
|
||||
|
||||
These are useful things for a more social-interaction heavy game.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
We will not include them for this tutorial. Adding the Barter contrib is simple though.
|
||||
|
||||
### Will your characters be known by their name or only by their physical appearance?
|
||||
|
||||
This is a common thing in RP-heavy games. Others will only see you as "The tall woman" until you introduce yourself and they 'recognize' you with a name. Linked to this is the concept of more complex emoting and posing.
|
||||
|
||||
Implementing such a system is not trivial, but the [RPsystem](../../../Contribs/Contrib-RPSystem.md) Evennia contrib offers a ready system with everything needed for free emoting, recognizing people by their appearance and more.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
We will not use any special RP systems for this tutorial. Adding the RPSystem contrib is a good extra expansion though!
|
||||
|
||||
## Rooms
|
||||
|
||||
### Is a simple room description enough or should the description be able to change?
|
||||
|
||||
Changing room descriptions for day and night, winder and summer is actually quite easy to do, but looks very impressive. We happen to know there is also a contrib that helps with this, so we'll show how to include that.
|
||||
|
||||
There is an [Extended Room](../../../Contribs/Contrib-Extended-Room.md) contrib that adds a Room type that is aware of the time-of-day as well as seasonal variations.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
We will stick to a normal room in this tutorial and let the world be in a perpetual daylight. Making Rooms into ExtendedRooms is not hard though.
|
||||
|
||||
### Should the room have different statuses?
|
||||
|
||||
One could picture weather making outdoor rooms wet, cold or burnt. In rain, bow strings could get wet and fireballs fizz out. In a hot room, characters could require drinking more water, or even take damage if not finding shelter.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
For the above-ground we need to be able to disable combat all rooms except for the PvP location. We also need to consider how to auto-generate the rooms under ground. So we probably will need some statuses to control that.
|
||||
|
||||
Since each room under ground should present some sort of challenge, we may need a few different room types different from the above-ground Rooms.
|
||||
|
||||
### Can objects be hidden in the room? Can a person hide in the room?
|
||||
|
||||
This ties into if you have hide/stealth mechanics. Maybe you could evesdrop or attack out of hiding.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
We will not model hiding and stealth. This will be a game of honorable face-to-face conflict.
|
||||
|
||||
## Objects
|
||||
|
||||
### How numerous are your objects? Do you want large loot-lists or are objects just role playing props?
|
||||
|
||||
This also depends on the type of game. In a pure freeform RPG, most objects may be 'imaginary' and just appearing in fiction. If the game is more coded, you want objects with properties that the computer can measure, track and calculate. In many roleplaying-heavy games, you find a mixture of the two, with players imagining items for roleplaying scenes, but only using 'real' objects to resolve conflicts.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
We will want objects with properties, like weapons and potions and such. Monsters should drop loot even though our list of objects will not be huge in this example game.
|
||||
|
||||
### Is each coin a separate object or do you just store a bank account value?
|
||||
|
||||
The advantage of having multiple items is that it can be more immersive. The drawback is that it's also very fiddly to deal with individual coins, especially if you have to deal with different currencies.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
_Knave_ uses the "copper" as the base coin and so will we. Knave considers the weight of coin and one inventory "slot" can hold 100 coins. So we'll implement a "coin item" to represent many coins.
|
||||
|
||||
### Do multiple similar objects form stack and how are those stacks handled in that case?
|
||||
|
||||
If you drop two identical apples on the ground, Evennia will default to show this in the room as "two apples", but this is just a visual effect - there are still two apple-objects in the room. One could picture instead merging the two into a single object "X nr of apples" when you drop the apples.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
We will keep Evennia's default.
|
||||
|
||||
### Does an object have weight or volume (so you cannot carry an infinite amount of them)?
|
||||
|
||||
Limiting carrying weight is one way to stop players from hoarding. It also makes it more important for players to pick only the equipment they need. Carrying limits can easily come across as annoying to players though, so one needs to be careful with it.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
_Knave_ limits your inventory to `Constitution + 10` "slots", where most items take up one slot and some large things, like armor, uses two. Small items (like rings) can fit 2-10 per slot and you can fit 100 coins in a slot. This is an important game mechanic to limit players from hoarding. Especially since you need coin to level up.
|
||||
|
||||
### Can objects be broken? Can they be repaired?
|
||||
|
||||
Item breakage is very useful for a game economy; breaking weapons adds tactical considerations (if it's not too common, then it becomes annoying) and repairing things gives work for crafting players.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
In _Knave_, items will break if you make a critical failure on using them (rolls a native 1 on d20). This means they lose a level of `quality` and once at 0, it's unusable. We will not allow players to repair, but we could allow merchants to repair items for a fee.
|
||||
|
||||
### Can you fight with a chair or a flower or must you use a special 'weapon' kind of thing?
|
||||
|
||||
Traditionally, only 'weapons' could be used to fight with. In the past this was a useful
|
||||
simplification, but with Python classes and inheritance, it's not actually more work to just let all items in game work as a weapon in a pinch.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
Since _Knave_ deals with weapon lists and positions where items can be wielded, we will have a separate "Weapon" class for everything you can use for fighting. So, you won't be able to fight with a chair (unless we make it a weapon-inherited chair).
|
||||
|
||||
### Will characters be able to craft new objects?
|
||||
|
||||
Crafting is a common feature in multiplayer games. In code it usually means using a skill-check to combine base ingredients from a fixed recipe in order to create a new item. The classic example is to combine _leather straps_, a _hilt_, a _pommel_ and a _blade_ to make a new _sword_.
|
||||
|
||||
A full-fledged crafting system could require multiple levels of crafting, including having to mine for ore or cut down trees for wood.
|
||||
|
||||
Evennia's [Crafting](../../../Contribs/Contrib-Crafting.md) contrib adds a full crafting system to any game. It's based on [Tags](../../../Components/Tags.md), meaning that pretty much any object can be made usable for crafting, even used in an unexpected way.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
In our case we will not add any crafting in order to limit the scope of our game. Maybe NPCs will be able to repair items - for a cost?
|
||||
|
||||
### Should mobs/NPCs have some sort of AI?
|
||||
|
||||
As a rule, you should not hope to fool anyone into thinking your AI is actually intelligent. The best you will be able to do is to give interesting results and unless you have a side-gig as an AI researcher, users will likely not notice any practical difference between a simple state-machine and you spending a lot of time learning
|
||||
how to train a neural net.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
For this tutorial, we will show how to add a simple state-machine AI for monsters. NPCs will only be shop-keepers and quest-gives so they won't need any real AI to speak of.
|
||||
|
||||
### Are NPCs and mobs different entities? How do they differ?
|
||||
|
||||
"Mobs" or "mobiles" are things that move around. This is traditionally monsters you can fight with, but could also be city guards or the baker going to chat with the neighbor. Back in the day, they were often fundamentally different these days it's often easier to just make NPCs and mobs essentially the same thing.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
In EvAdventure, Monsters and NPCs do very different things, so they will be different classes, sharing some code where possible.
|
||||
|
||||
### _Should there be NPCs giving quests? If so, how do you track Quest status?
|
||||
|
||||
Quests are a staple of many classic RPGs.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
We will design a simple quest system with some simple conditions for success, like carrying the right item or items back to the quest giver.
|
||||
|
||||
## Characters
|
||||
|
||||
### Can players have more than one Character active at a time or are they allowed to multi-play?
|
||||
|
||||
Since Evennia differentiates between `Sessions` (the client-connection to the game), `Accounts` and `Character`s, it natively supports multi-play. This is controlled by the `MULTISESSION_MODE` setting, which has a value from `0` (default) to `3`.
|
||||
|
||||
- `0`- One Character per Account and one Session per Account. This means that if you login to the same
|
||||
account from another client you'll be disconnected from the first. When creating a new account, a Character
|
||||
will be auto-created with the same name as your Account. This is default mode and mimics legacy code bases
|
||||
which had no separation between Account and Character.
|
||||
- `1` - One Character per Account, multiple Sessions per Account. So you can connect simultaneously from
|
||||
multiple clients and see the same output in all of them.
|
||||
- `2` - Multiple Characters per Account, one Session per Character. This will not auto-create a same-named
|
||||
Character for you, instead you get to create/choose between a number of Characters up to a max limit given by
|
||||
the `MAX_NR_CHARACTERS` setting (default 1). You can play them all simultaneously if you have multiple clients
|
||||
open, but only one client per Character.
|
||||
- `3` - Multiple Characters per Account, Multiple Sessions per Character. This is like mode 2, except players
|
||||
can control each Character from multiple clients, seeing the same output from each Character.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
Due to the nature of _Knave_, characters are squishy and probably short-lived. So it makes little sense to keep a stable of them. We'll use use mode 0 or 1.
|
||||
|
||||
### How does the character-generation work?
|
||||
|
||||
There are a few common ways to do character generation:
|
||||
|
||||
- Rooms. This is the traditional way. Each room's description tells you what command to use to modify your character. When you are done you move to the next room. Only use this if you have another reason for using a room, like having a training dummy to test skills on, for example.
|
||||
- A Menu. The Evennia _EvMenu_ system allows you to code very flexible in-game menus without needing to walk between rooms. You can both have a step-by-step menu (a 'wizard') or allow the user to jump between the
|
||||
steps as they please. This tends to be a lot easier for newcomers to understand since it doesn't require
|
||||
using custom commands they will likely never use again after this.
|
||||
- Questions. A fun way to build a character is to answer a series of questions. This is usually implemented with a sequential menu.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
Knave randomizes almost aspects of the Character generation. We'll use a menu to let the player add their name and sex as well as do the minor re-assignment of stats allowed by the rules.
|
||||
|
||||
### How do you implement different "classes" or "races"?
|
||||
|
||||
The way classes and races work in most RPGs is that they act as static 'templates' that inform which bonuses and special abilities you have. Much of this only comes into play during character generation or when leveling up.
|
||||
|
||||
Often all we need to store on the Character is _which_ class and _which_ race they have; the actual logic can sit in Python code and just be looked up when we need it.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
There are no races and no classes in _Knave_. Every character is a human.
|
||||
|
||||
### If a Character can hide in a room, what skill will decide if they are detected?
|
||||
|
||||
Hiding means a few things.
|
||||
- The Character should not appear in the room's description / character list
|
||||
- Others hould not be able to interact with a hidden character. It'd be weird if you could do `attack <name>`
|
||||
or `look <name>` if the named character is in hiding.
|
||||
- There must be a way for the person to come out of hiding, and probably for others to search or accidentally
|
||||
find the person (probably based on skill checks).
|
||||
- The room will also need to be involved, maybe with some modifier as to how easy it is to hide in the room.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
We will not be including a hide-mechanic in EvAdventure.
|
||||
|
||||
### What does the skill tree look like? Can a Character gain experience to improve? By killing enemies? Solving quests? By roleplaying?
|
||||
|
||||
Gaining experience points (XP) and improving one's character is a staple of roleplaying games. There are many
|
||||
ways to implement this:
|
||||
- Gaining XP from kills is very common; it's easy to let a monster be 'worth' a certain number of XP and it's easy to tell when you should gain it.
|
||||
- Gaining XP from quests is the same - each quest is 'worth' XP and you get them when completing the test.
|
||||
- Gaining XP from roleplay is harder to define. Different games have tried a lot of different ways to do this:
|
||||
- XP from being online - just being online gains you XP. This inflates player numbers but many players may
|
||||
just be lurking and not be actually playing the game at any given time.
|
||||
- XP from roleplaying scenes - you gain XP according to some algorithm analyzing your emotes for 'quality',
|
||||
how often you post, how long your emotes are etc.
|
||||
- XP from actions - you gain XP when doing things, anything. Maybe your XP is even specific to each action, so
|
||||
you gain XP only for running when you run, XP for your axe skill when you fight with an axe etc.
|
||||
- XP from fails - you only gain XP when failing rolls.
|
||||
- XP from other players - other players can award you XP for good RP.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
We will use an alternative rule in _Knave_, where Characters gain XP by spending coins they carry back from their adventures. The above-ground merchants will allow you to spend your coins and exchange them for XP 1:1. Each level costs 1000 coins. Every level you have `1d8 * new level` (minimum what you had before + 1) HP, and can raise 3 different ability scores by 1 (max +10). There are no skills in _Knave_, but the principle of increasing them would be the same.
|
||||
|
||||
### May player-characters attack each other (PvP)?
|
||||
|
||||
Deciding this affects the style of your entire game. PvP makes for exciting gameplay but it opens a whole new can of worms when it comes to "fairness". Players will usually accept dying to an overpowered NPC dragon. They will not be as accepting if they perceive another player as being overpowered. PvP means that you
|
||||
have to be very careful to balance the game - all characters does not have to be exactly equal but they should all be viable to play a fun game with.
|
||||
|
||||
PvP does not only mean combat though. Players can compete in all sorts of ways, including gaining influence in a political game or gaining market share when selling their crafted merchandise.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
We will allow PvP only in one place - a special Dueling location where players can play-fight each other for training and prestige, but not actually get killed. Otherwise no PvP will be allowed. Note that without a full Barter system in place (just regular `give`, it makes it theoretically easier for players to scam one another.
|
||||
|
||||
### What are the penalties of defeat? Permanent death? Quick respawn? Time in prison?
|
||||
|
||||
This is another big decision that strongly affects the mood and style of your game.
|
||||
|
||||
Perma-death means that once your character dies, it's gone and you have to make a new one.
|
||||
|
||||
- It allows for true heroism. If you genuinely risk losing your character of two years to fight the dragon,
|
||||
your triumph is an actual feat.
|
||||
- It limits the old-timer dominance problem. If long-time players dies occationally, it will open things
|
||||
up for newcomers.
|
||||
- It lowers inflation, since the hoarded resources of a dead character can be removed.
|
||||
- It gives capital punishment genuine discouraging power.
|
||||
- It's realistic.
|
||||
|
||||
Perma-death comes with some severe disadvantages however.
|
||||
|
||||
- Many players say they like the _idea_ of permadeath except when it could happen to them.
|
||||
- Some players refuse to take any risks if death is permanent.
|
||||
- It may make players even more reluctant to play conflict-driving 'bad guys'.
|
||||
- Balancing PvP becomes very hard. Fairness and avoiding exploits becomes critical when the outcome
|
||||
is permanent.
|
||||
|
||||
For these reasons, it's very common to do hybrid systems. Some tried variations:
|
||||
|
||||
- NPCs cannot kill you, only other players can.
|
||||
- Death is permanent, but it's difficult to actually die - you are much more likely to end up being severely hurt/incapacitated.
|
||||
- You can pre-pay 'insurance' to magically/technologically avoid actually dying. Only if don't have insurance will
|
||||
you die permanently.
|
||||
- Death just means harsh penalties, not actual death.
|
||||
- When you die you can fight your way back to life from some sort of afterlife.
|
||||
- You'll only die permanently if you as a player explicitly allows it.
|
||||
|
||||
**EvAdventure Answer**
|
||||
|
||||
In _Knave_, when you hit 0 HP, you roll on a death table, with a 1/8 chance of immediate death (otherwise you lose
|
||||
points in a random stat). We will offer an "Insurance" that allows you to resurrect if you carry enough coin on you when
|
||||
you die. If not, you are perma-dead and have to create a new character (which is easy and quick since it's mostly
|
||||
randomized).
|
||||
|
||||
## Conclusions
|
||||
|
||||
Going through the questions has helped us get a little bit more of a feel for the game we want to do. There are many, many other things we could ask ourselves, but if we can cover these points we will be a good way towards a complete,
|
||||
playable game!
|
||||
|
||||
In the last of these planning lessons we'll sketch out how these ideas will map to Evennia.
|
||||
|
|
@ -1,145 +0,0 @@
|
|||
# Where do I begin?
|
||||
|
||||
The good news is that following this Starting tutorial is a great way to begin making an Evennia game.
|
||||
|
||||
The bad news is that everyone's different and when it comes to starting your own game there is no
|
||||
one-size-fits-all answer. Instead we will ask a series of questions
|
||||
to help you figure this out for yourself. It will also help you evaluate your own skills and maybe
|
||||
put some more realistic limits on how fast you can achieve your goals.
|
||||
|
||||
> The questions in this lesson do not really apply to our tutorial game since we know we are doing it
|
||||
> to learn Evennia. If you just want to follow along with the technical bits you can skip this lesson and
|
||||
> come back later when you feel ready to take on making your own game.
|
||||
|
||||
## What is your motivation for doing this?
|
||||
|
||||
So you want to make a game. First you need to make a few things clear to yourself.
|
||||
|
||||
Making a multiplayer online game is a _big_ undertaking. You will (if you are like most of us) be
|
||||
doing it as a hobby, without getting paid. And you’ll be doing it for a long time.
|
||||
|
||||
So the very first thing you should ask yourself (and your team, if you have any) is
|
||||
_why am I doing this_? Do some soul-searching here. Here are some possible answers:
|
||||
|
||||
- I want to earn recognition and fame from my online community and/or among my friends.
|
||||
- I want to build the game so I can play and enjoy it myself.
|
||||
- I want to build the same game I already play but without the bad people.
|
||||
- I want to create a game so that I can control it and be the head honcho.
|
||||
- A friend or online acquaintance talked me into working on it.
|
||||
- I work on this because I’m paid to (wow!)
|
||||
- I only build this for my own benefit or to see if I can pull it off.
|
||||
- I want to create something to give back to the community I love.
|
||||
- I want to use this project as a stepping-stone towards other projects (like a career in game design
|
||||
or programming).
|
||||
- I am interested in coding or server and network architectures, making a MUD just seems to be a good
|
||||
way to teach myself.
|
||||
- I want to build a commercial game and earn money.
|
||||
- I want to fulfill a life-long dream of game making.
|
||||
|
||||
There are many other possibilities. How “solid” your answer is for a long-term development project
|
||||
is up to you. The important point is that you ask yourself the question.
|
||||
|
||||
**Help someone else instead** - Maybe you should _not_ start a new project - maybe you're better off
|
||||
helping someone else or improve on something that already exists. Or maybe you find you are more of a
|
||||
game engine developer than a game designer.
|
||||
|
||||
**Driven by emotion** - Some answers may suggest that you are driven by emotions of revenge or disconcert. Be careful with that and
|
||||
check so that's not your _only_ driving force. Those emotions may have abated later when the project
|
||||
most needs your enthusiasm and motivation.
|
||||
|
||||
**Going commercial** - If your aim is to earn money, your design goals will likely be very different from
|
||||
those of a person who only creates as a hobby or for their own benefit. You may also have a much stricter
|
||||
timeline for release.
|
||||
|
||||
Whichever your motivation, you should at least have it clear in your own mind. It’s worth to make
|
||||
sure your eventual team is on the same page too.
|
||||
|
||||
## What are your skills?
|
||||
|
||||
Once you have your motivations straight you need to take a stock of your own skills and the skills
|
||||
available in your team, if you have any.
|
||||
|
||||
Your game will have two principal components and you will need skills to cater for both:
|
||||
|
||||
- The game engine / code base - Evennia in this case.
|
||||
- The assets created for using the game engine (“the game world”)
|
||||
|
||||
### The game engine
|
||||
|
||||
The game engine is maintained and modified by programmers (coders). It represents the infrastructure
|
||||
that runs the game - the network code, the protocol support, the handling of commands, scripting and
|
||||
data storage.
|
||||
|
||||
If you are just evaluating Evennia, it's worth to do the following:
|
||||
|
||||
- Hang out in the community/forums/chat. Expect to need to ask a lot of “stupid” questions as you start
|
||||
developing (hint: no question is stupid). Is this a community in which you would feel comfortable doing so?
|
||||
- Keep tabs on the manual (you're already here).
|
||||
- How's your Python skills? What are the skills in your team? Do you or your team already know it or are
|
||||
you willing to learn? Learning the language as you go is not too unusual with Evennia devs, but expect it
|
||||
to add development time. You will also be worse at predicting how 'hard' something is to do.
|
||||
- If you don’t know Python, you should have gotten a few tastes from the first part of this tutorial. But
|
||||
expect to have to refer to external online tutorials - there are many details of Python that will not be
|
||||
covered.
|
||||
|
||||
### Asset creation
|
||||
|
||||
Compared to the level of work needed to produce professional graphics for an MMORPG, detailed text
|
||||
assets for a mud are cheap to create. This is one of the many reasons muds are so well suited for a
|
||||
small team.
|
||||
|
||||
This is not to say that making “professional” text content is easy though. Knowing how to write
|
||||
imaginative and grammatically correct prose is only the minimal starting requirement. A good asset-
|
||||
creator (traditionally called a “builder”) must also be able to utilize the tools of the game engine
|
||||
to its fullest in order to script events, make quests, triggers and interactive, interesting
|
||||
environments.
|
||||
|
||||
Assuming you are not coding all alone, your team’s in-house builders will be the first ones to actually
|
||||
“use” your game framework and build tools. They will stumble on all the bugs. This means that you
|
||||
need people who are just not “artsy” or “good with words”. Assuming coders and builders are not the
|
||||
same people (common for early testing), builders need to be able to collaborate well and give clear
|
||||
and concise feedback.
|
||||
|
||||
If you know your builders are not tech-savvy, you may need to spend more time making easier
|
||||
build-tools and commands for them.
|
||||
|
||||
## So, where do I begin, then?
|
||||
|
||||
Right, after all this soul-searching and skill-inventory-checking, let’s go back to the original
|
||||
question. And maybe you’ll find that you have a better feeling for the answer yourself already:
|
||||
|
||||
- Keep following this tutorial and spend the time
|
||||
to really understand what is happening in the examples. Not only will this give you a better idea
|
||||
of how parts hang together, it may also give you ideas for what is possible. Maybe something
|
||||
is easier than you expected!
|
||||
- Introduce yourself in the IRC/Discord chat and don't be shy to ask questions as you go through
|
||||
the tutorial. Don't get hung up on trying to resolve something that a seasoned Evennia dev may
|
||||
clear up for you in five minutes. Also, not all errors are your faults - it's possible the
|
||||
tutorial is unclear or has bugs, asking will quickly bring those problems to light, if so.
|
||||
- If Python is new to you, you should complement the tutorial with third-party Python references
|
||||
so you can read, understand and replicate example code without being completely in the dark.
|
||||
|
||||
Once you are out of the starting tutorial, you'll be off to do your own thing.
|
||||
|
||||
- The starting tutorial cannot cover everything. Skim through the [Evennia docs](../../../index.md).
|
||||
Even if you don't read everything, it gives you a feeling for what's available should you need
|
||||
to look for something later. Make sure to use the search function.
|
||||
- You can now start by expanding on the tutorial-game we will have created. In the last part there
|
||||
there will be a list of possible future projects you could take on. Working on your own, without help
|
||||
from a tutorial is the next step.
|
||||
|
||||
As for your builders, they can start getting familiar with Evennia's default build commands ... but
|
||||
keep in mind that your game is not yet built! Don't set your builders off on creating large zone projects.
|
||||
If they build anything at all, it should be small test areas to agree on a homogenous form, mood
|
||||
and literary style.
|
||||
|
||||
## Conclusions
|
||||
|
||||
Remember that what kills a hobby game project will usually be your own lack of
|
||||
motivation. So do whatever you can to keep that motivation burning strong! Even if it means
|
||||
deviating from what you read in a tutorial like this one. Just get that game out there, whichever way
|
||||
works best for you.
|
||||
|
||||
In the next lesson we'll go through some of the technical questions you need to consider. This should
|
||||
hopefully help you figure out more about the game you want to make. In the lesson following that we'll
|
||||
then try to answer those questions for the sake of creating our little tutorial game.
|
||||
|
|
@ -1,414 +0,0 @@
|
|||
# Player Characters
|
||||
|
||||
In the [previous lesson about rules and dice rolling](./Beginner-Tutorial-Rules.md) we made some
|
||||
assumptions about the "Player Character" entity:
|
||||
|
||||
- It should store Abilities on itself as `character.strength`, `character.constitution` etc.
|
||||
- It should have a `.heal(amount)` method.
|
||||
|
||||
So we have some guidelines of how it should look! A Character is a database entity with values that
|
||||
should be able to be changed over time. It makes sense to base it off Evennia's
|
||||
[DefaultCharacter Typeclass](../../../Components/Typeclasses.md). The Character class is like a 'character sheet' in a tabletop
|
||||
RPG, it will hold everything relevant to that PC.
|
||||
|
||||
## Inheritance structure
|
||||
|
||||
Player Characters (PCs) are not the only "living" things in our world. We also have _NPCs_
|
||||
(like shopkeepers and other friendlies) as well as _monsters_ (mobs) that can attack us.
|
||||
|
||||
In code, there are a few ways we could structure this. If NPCs/monsters were just special cases of PCs,
|
||||
we could use a class inheritance like this:
|
||||
|
||||
```python
|
||||
from evennia import DefaultCharacter
|
||||
|
||||
class EvAdventureCharacter(DefaultCharacter):
|
||||
# stuff
|
||||
|
||||
class EvAdventureNPC(EvAdventureCharacter):
|
||||
# more stuff
|
||||
|
||||
class EvAdventureMob(EvAdventureNPC):
|
||||
# more stuff
|
||||
```
|
||||
|
||||
All code we put on the `Character` class would now be inherited to `NPC` and `Mob` automatically.
|
||||
|
||||
However, in _Knave_, NPCs and particularly monsters are _not_ using the same rules as PCs - they are
|
||||
simplified to use a Hit-Die (HD) concept. So while still character-like, NPCs should be separate from
|
||||
PCs like this:
|
||||
|
||||
```python
|
||||
from evennia import DefaultCharacter
|
||||
|
||||
class EvAdventureCharacter(DefaultCharacter):
|
||||
# stuff
|
||||
|
||||
class EvAdventureNPC(DefaultCharacter):
|
||||
# separate stuff
|
||||
|
||||
class EvAdventureMob(EvadventureNPC):
|
||||
# more separate stuff
|
||||
```
|
||||
|
||||
Nevertheless, there are some things that _should_ be common for all 'living things':
|
||||
|
||||
- All can take damage.
|
||||
- All can die.
|
||||
- All can heal
|
||||
- All can hold and lose coins
|
||||
- All can loot their fallen foes.
|
||||
- All can get looted when defeated.
|
||||
|
||||
We don't want to code this separately for every class but we no longer have a common parent
|
||||
class to put it on. So instead we'll use the concept of a _mixin_ class:
|
||||
|
||||
```python
|
||||
from evennia import DefaultCharacter
|
||||
|
||||
class LivingMixin:
|
||||
# stuff common for all living things
|
||||
|
||||
class EvAdventureCharacter(LivingMixin, DefaultCharacter):
|
||||
# stuff
|
||||
|
||||
class EvAdventureNPC(LivingMixin, DefaultCharacter):
|
||||
# stuff
|
||||
|
||||
class EvAdventureMob(LivingMixin, EvadventureNPC):
|
||||
# more stuff
|
||||
```
|
||||
|
||||
```{sidebar}
|
||||
In [evennia/contrib/tutorials/evadventure/characters.py](../../../api/evennia.contrib.tutorials.evadventure.characters.md)
|
||||
is an example of a character class structure.
|
||||
```
|
||||
Above, the `LivingMixin` class cannot work on its own - it just 'patches' the other classes with some
|
||||
extra functionality all living things should be able to do. This is an example of
|
||||
_multiple inheritance_. It's useful to know about, but one should not over-do multiple inheritance
|
||||
since it can also get confusing to follow the code.
|
||||
|
||||
## Living mixin class
|
||||
|
||||
> Create a new module `mygame/evadventure/characters.py`
|
||||
|
||||
Let's get some useful common methods all living things should have in our game.
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/characters.py
|
||||
|
||||
from .rules import dice
|
||||
|
||||
class LivingMixin:
|
||||
|
||||
# makes it easy for mobs to know to attack PCs
|
||||
is_pc = False
|
||||
|
||||
def heal(self, hp):
|
||||
"""
|
||||
Heal hp amount of health, not allowing to exceed our max hp
|
||||
|
||||
"""
|
||||
damage = self.hp_max - self.hp
|
||||
healed = min(damage, hp)
|
||||
self.hp += healed
|
||||
|
||||
self.msg("You heal for {healed} HP.")
|
||||
|
||||
def at_pay(self, amount):
|
||||
"""When paying coins, make sure to never detract more than we have"""
|
||||
amount = min(amount, self.coins)
|
||||
self.coins -= amount
|
||||
return amount
|
||||
|
||||
def at_damage(self, damage, attacker=None):
|
||||
"""Called when attacked and taking damage."""
|
||||
self.hp -= damage
|
||||
|
||||
def at_defeat(self):
|
||||
"""Called when defeated. By default this means death."""
|
||||
self.at_death()
|
||||
|
||||
def at_death(self):
|
||||
"""Called when this thing dies."""
|
||||
# this will mean different things for different living things
|
||||
pass
|
||||
|
||||
def at_do_loot(self, looted):
|
||||
"""Called when looting another entity"""
|
||||
looted.at_looted(self)
|
||||
|
||||
def at_looted(self, looter):
|
||||
"""Called when looted by another entity"""
|
||||
|
||||
# default to stealing some coins
|
||||
max_steal = dice.roll("1d10")
|
||||
stolen = self.at_pay(max_steal)
|
||||
looter.coins += stolen
|
||||
|
||||
```
|
||||
Most of these are empty since they will behave differently for characters and npcs. But having them
|
||||
in the mixin means we can expect these methods to be available for all living things.
|
||||
|
||||
|
||||
## Character class
|
||||
|
||||
We will now start making the basic Character class, based on what we need from _Knave_.
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/characters.py
|
||||
|
||||
from evennia import DefaultCharacter, AttributeProperty
|
||||
from .rules import dice
|
||||
|
||||
class LivingMixin:
|
||||
# ...
|
||||
|
||||
|
||||
class EvAdventureCharacter(LivingMixin, DefaultCharacter):
|
||||
"""
|
||||
A character to use for EvAdventure.
|
||||
"""
|
||||
is_pc = True
|
||||
|
||||
strength = AttributeProperty(1)
|
||||
dexterity = AttributeProperty(1)
|
||||
constitution = AttributeProperty(1)
|
||||
intelligence = AttributeProperty(1)
|
||||
wisdom = AttributeProperty(1)
|
||||
charisma = AttributeProperty(1)
|
||||
|
||||
hp = AttributeProperty(8)
|
||||
hp_max = AttributeProperty(8)
|
||||
|
||||
level = AttributeProperty(1)
|
||||
xp = AttributeProperty(0)
|
||||
coins = AttributeProperty(0)
|
||||
|
||||
def at_defeat(self):
|
||||
"""Characters roll on the death table"""
|
||||
if self.location.allow_death:
|
||||
# this allow rooms to have non-lethal battles
|
||||
dice.roll_death(self)
|
||||
else:
|
||||
self.location.msg_contents(
|
||||
"$You() $conj(collapse) in a heap, alive but beaten.",
|
||||
from_obj=self)
|
||||
self.heal(self.hp_max)
|
||||
|
||||
def at_death(self):
|
||||
"""We rolled 'dead' on the death table."""
|
||||
self.location.msg_contents(
|
||||
"$You() collapse in a heap, embraced by death.",
|
||||
from_obj=self)
|
||||
# TODO - go back into chargen to make a new character!
|
||||
```
|
||||
|
||||
We make an assumption about our rooms here - that they have a property `.allow_death`. We need
|
||||
to make a note to actually add such a property to rooms later!
|
||||
|
||||
In our `Character` class we implement all attributes we want to simulate from the _Knave_ ruleset.
|
||||
The `AttributeProperty` is one way to add an Attribute in a field-like way; these will be accessible
|
||||
on every character in several ways:
|
||||
|
||||
- As `character.strength`
|
||||
- As `character.db.strength`
|
||||
- As `character.attributes.get("strength")`
|
||||
|
||||
See [Attributes](../../../Components/Attributes.md) for seeing how Attributes work.
|
||||
|
||||
Unlike in base _Knave_, we store `coins` as a separate Attribute rather than as items in the inventory,
|
||||
this makes it easier to handle barter and trading later.
|
||||
|
||||
We implement the Player Character versions of `at_defeat` and `at_death`. We also make use of `.heal()`
|
||||
from the `LivingMixin` class.
|
||||
|
||||
### Funcparser inlines
|
||||
|
||||
This piece of code is worth some more explanation:
|
||||
|
||||
```python
|
||||
self.location.msg_contents(
|
||||
"$You() $conj(collapse) in a heap, alive but beaten.",
|
||||
from_obj=self)
|
||||
```
|
||||
|
||||
Remember that `self` is the Character instance here. So `self.location.msg_contents` means "send a
|
||||
message to everything inside my current location". In other words, send a message to everyone
|
||||
in the same place as the character.
|
||||
|
||||
The `$You() $conj(collapse)` are [FuncParser inlines](../../../Components/FuncParser.md). These are functions that
|
||||
execute
|
||||
in the string. The resulting string may look different for different audiences. The `$You()` inline
|
||||
function will use `from_obj` to figure out who 'you' are and either show your name or 'You'.
|
||||
The `$conj()` (verb conjugator) will tweak the (English) verb to match.
|
||||
|
||||
- You will see: `"You collapse in a heap, alive but beaten."`
|
||||
- Others in the room will see: `"Thomas collapses in a heap, alive but beaten."`
|
||||
|
||||
Note how `$conj()` chose `collapse/collapses` to make the sentences grammatically correct.
|
||||
|
||||
### Backtracking
|
||||
|
||||
We make our first use of the `rules.dice` roller to roll on the death table! As you may recall, in the
|
||||
previous lesson, we didn't know just what to do when rolling 'dead' on this table. Now we know - we
|
||||
should be calling `at_death` on the character. So let's add that where we had TODOs before:
|
||||
|
||||
```python
|
||||
# mygame/evadventure/rules.py
|
||||
|
||||
class EvAdventureRollEngine:
|
||||
|
||||
# ...
|
||||
|
||||
def roll_death(self, character):
|
||||
ability_name = self.roll_random_table("1d8", death_table)
|
||||
|
||||
if ability_name == "dead":
|
||||
# kill the character!
|
||||
character.at_death() # <------ TODO no more
|
||||
else:
|
||||
# ...
|
||||
|
||||
if current_ability < -10:
|
||||
# kill the character!
|
||||
character.at_death() # <------- TODO no more
|
||||
else:
|
||||
# ...
|
||||
```
|
||||
|
||||
## Connecting the Character with Evennia
|
||||
|
||||
You can easily make yourself an `EvAdventureCharacter` in-game by using the
|
||||
`type` command:
|
||||
|
||||
type self = evadventure.characters.EvAdventureCharacter
|
||||
|
||||
You can now do `examine self` to check your type updated.
|
||||
|
||||
If you want _all_ new Characters to be of this type you need to tell Evennia about it. Evennia
|
||||
uses a global setting `BASE_CHARACTER_TYPECLASS` to know which typeclass to use when creating
|
||||
Characters (when logging in, for example). This defaults to `typeclasses.characters.Character` (that is,
|
||||
the `Character` class in `mygame/typeclasses/characters.py`).
|
||||
|
||||
There are thus two ways to weave your new Character class into Evennia:
|
||||
|
||||
1. Change `mygame/server/conf/settings.py` and add `BASE_CHARACTER_CLASS = "evadventure.characters.EvAdventureCharacter"`.
|
||||
2. Or, change `typeclasses.characters.Character` to inherit from `EvAdventureCharacter`.
|
||||
|
||||
You must always reload the server for changes like this to take effect.
|
||||
|
||||
```{important}
|
||||
In this tutorial we are making all changes in a folder `mygame/evadventure/`. This means we can isolate
|
||||
our code but means we need to do some extra steps to tie the character (and other objects) into Evennia.
|
||||
For your own game it would be just fine to start editing `mygame/typeclasses/characters.py` directly
|
||||
instead.
|
||||
```
|
||||
|
||||
|
||||
## Unit Testing
|
||||
|
||||
> Create a new module `mygame/evadventure/tests/test_characters.py`
|
||||
|
||||
For testing, we just need to create a new EvAdventure character and check
|
||||
that calling the methods on it doesn't error out.
|
||||
|
||||
```python
|
||||
# mygame/evadventure/tests/test_characters.py
|
||||
|
||||
from evennia.utils import create
|
||||
from evennia.utils.test_resources import BaseEvenniaTest
|
||||
|
||||
from ..characters import EvAdventureCharacter
|
||||
|
||||
class TestCharacters(BaseEvenniaTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.character = create.create_object(EvAdventureCharacter, key="testchar")
|
||||
|
||||
def test_heal(self):
|
||||
self.character.hp = 0
|
||||
self.character.hp_max = 8
|
||||
|
||||
self.character.heal(1)
|
||||
self.assertEqual(self.character.hp, 1)
|
||||
# make sure we can't heal more than max
|
||||
self.character.heal(100)
|
||||
self.assertEqual(self.character.hp, 8)
|
||||
|
||||
def test_at_pay(self):
|
||||
self.character.coins = 100
|
||||
|
||||
result = self.character.at_pay(60)
|
||||
self.assertEqual(result, 60)
|
||||
self.assertEqual(self.character.coins, 40)
|
||||
|
||||
# can't get more coins than we have
|
||||
result = self.character.at_pay(100)
|
||||
self.assertEqual(result, 40)
|
||||
self.assertEqual(self.character.coins, 0)
|
||||
|
||||
# tests for other methods ...
|
||||
|
||||
```
|
||||
If you followed the previous lessons, these tests should look familiar. Consider adding
|
||||
tests for other methods as practice. Refer to previous lessons for details.
|
||||
|
||||
For running the tests you do:
|
||||
|
||||
evennia test --settings settings.py .evadventure.tests.test_character
|
||||
|
||||
|
||||
## About races and classes
|
||||
|
||||
_Knave_ doesn't have any D&D-style _classes_ (like Thief, Fighter etc). It also does not bother with
|
||||
_races_ (like dwarves, elves etc). This makes the tutorial shorter, but you may ask yourself how you'd
|
||||
add these functions.
|
||||
|
||||
In the framework we have sketched out for _Knave_, it would be simple - you'd add your race/class as
|
||||
an Attribute on your Character:
|
||||
|
||||
```python
|
||||
# mygame/evadventure/characters.py
|
||||
|
||||
from evennia import DefaultCharacter, AttributeProperty
|
||||
# ...
|
||||
|
||||
class EvAdventureCharacter(LivingMixin, DefaultCharacter):
|
||||
|
||||
# ...
|
||||
|
||||
charclass = AttributeProperty("Fighter")
|
||||
charrace = AttributeProperty("Human")
|
||||
|
||||
```
|
||||
We use `charclass` rather than `class` here, because `class` is a reserved Python keyword. Naming
|
||||
`race` as `charrace` thus matches in style.
|
||||
|
||||
We'd then need to expand our [rules module](./Beginner-Tutorial-Rules.md) (and later
|
||||
[character generation](./Beginner-Tutorial-Chargen.md) to check and include what these classes mean.
|
||||
|
||||
|
||||
## Summary
|
||||
|
||||
|
||||
With the `EvAdventureCharacter` class in place, we have a better understanding of how our PCs will look
|
||||
like under _Knave_.
|
||||
|
||||
For now, we only have bits and pieces and haven't been testing this code in-game. But if you want
|
||||
you can swap yourself into `EvAdventureCharacter` right now. Log into your game and run
|
||||
the command
|
||||
|
||||
type self = evadventure.characters.EvAdventureCharacter
|
||||
|
||||
If all went well, `ex self` will now show your typeclass as being `EvAdventureCharacter`.
|
||||
Check out your strength with
|
||||
|
||||
py self.strength = 3
|
||||
|
||||
```{important}
|
||||
When doing `ex self` you will _not_ see all your Abilities listed yet. That's because
|
||||
Attributes added with `AttributeProperty` are not available until they have been accessed at
|
||||
least once. So once you set (or look at) `.strength` above, `strength` will show in `examine` from
|
||||
then on.
|
||||
```
|
||||
|
|
@ -1,711 +0,0 @@
|
|||
# Character Generation
|
||||
|
||||
In previous lessons we have established how a character looks. Now we need to give the player a
|
||||
chance to create one.
|
||||
|
||||
## How it will work
|
||||
|
||||
A fresh Evennia install will automatically create a new Character with the same name as your
|
||||
Account when you log in. This is quick and simple and mimics older MUD styles. You could picture
|
||||
doing this, and then customizing the Character in-place.
|
||||
|
||||
We will be a little more sophisticated though. We want the user to be able to create a character
|
||||
using a menu when they log in.
|
||||
|
||||
We do this by editing `mygame/server/conf/settings.py` and adding the line
|
||||
|
||||
AUTO_CREATE_CHARACTER_WITH_ACCOUNT = False
|
||||
|
||||
When doing this, connecting with the game with a new account will land you in "OOC" mode. The
|
||||
ooc-version of `look` (sitting in the Account cmdset) will show a list of available characters
|
||||
if you have any. You can also enter `charcreate` to make a new character. The `charcreate` is a
|
||||
simple command coming with Evennia that just lets you make a new character with a given name and
|
||||
description. We will later modify that to kick off our chargen. For now we'll just keep in mind
|
||||
that's how we'll start off the menu.
|
||||
|
||||
In _Knave_, most of the character-generation is random. This means this tutorial can be pretty
|
||||
compact while still showing the basic idea. What we will create is a menu looking like this:
|
||||
|
||||
|
||||
```
|
||||
Silas
|
||||
|
||||
STR +1
|
||||
DEX +2
|
||||
CON +1
|
||||
INT +3
|
||||
WIS +1
|
||||
CHA +2
|
||||
|
||||
You are lanky with a sunken face and filthy hair, breathy speech, and foreign clothing.
|
||||
You were a herbalist, but you were pursued and ended up a knave. You are honest but also
|
||||
suspicious. You are of the neutral alignment.
|
||||
|
||||
Your belongings:
|
||||
Brigandine armor, ration, ration, sword, torch, torch, torch, torch, torch,
|
||||
tinderbox, chisel, whistle
|
||||
|
||||
----------------------------------------------------------------------------------------
|
||||
1. Change your name
|
||||
2. Swap two of your ability scores (once)
|
||||
3. Accept and create character
|
||||
```
|
||||
|
||||
If you select 1, you get a new menu node:
|
||||
|
||||
```
|
||||
Your current name is Silas. Enter a new name or leave empty to abort.
|
||||
-----------------------------------------------------------------------------------------
|
||||
```
|
||||
You can now enter a new name. When pressing return you'll get back to the first menu node
|
||||
showing your character, now with the new name.
|
||||
|
||||
If you select 2, you go to another menu node:
|
||||
|
||||
```
|
||||
Your current abilities:
|
||||
|
||||
STR +1
|
||||
DEX +2
|
||||
CON +1
|
||||
INT +3
|
||||
WIS +1
|
||||
CHA +2
|
||||
|
||||
You can swap the values of two abilities around.
|
||||
You can only do this once, so choose carefully!
|
||||
|
||||
To swap the values of e.g. STR and INT, write 'STR INT'. Empty to abort.
|
||||
------------------------------------------------------------------------------------------
|
||||
```
|
||||
If you enter `WIS CHA` here, WIS will become `+2` and `CHA` `+1`. You will then again go back
|
||||
to the main node to see your new character, but this time the option to swap will no longer be
|
||||
available (you can only do it once).
|
||||
|
||||
If you finally select the `Accept and create character` option, the character will be created
|
||||
and you'll leave the menu;
|
||||
|
||||
Character was created!
|
||||
|
||||
## Random tables
|
||||
|
||||
```{sidebar}
|
||||
Full Knave random tables are found in
|
||||
[evennia/contrib/tutorials/evadventure/random_tables.py](../../../api/evennia.contrib.tutorials.evadventure.random_tables.md).
|
||||
```
|
||||
|
||||
> Make a new module `mygame/evadventure/random_tables.py`.
|
||||
|
||||
Since most of _Knave_'s character generation is random we will need to roll on random tables
|
||||
from the _Knave_ rulebook. While we added the ability to roll on a random table back in the
|
||||
[Rules Tutorial](./Beginner-Tutorial-Rules.md), we haven't added the relevant tables yet.
|
||||
|
||||
```
|
||||
# in mygame/evadventure/random_tables.py
|
||||
|
||||
chargen_tables = {
|
||||
"physique": [
|
||||
"athletic", "brawny", "corpulent", "delicate", "gaunt", "hulking", "lanky",
|
||||
"ripped", "rugged", "scrawny", "short", "sinewy", "slender", "flabby",
|
||||
"statuesque", "stout", "tiny", "towering", "willowy", "wiry",
|
||||
],
|
||||
"face": [
|
||||
"bloated", "blunt", "bony", # ...
|
||||
], # ...
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
The tables are just copied from the _Knave_ rules. We group the aspects in a dict
|
||||
`character_generation` to separate chargen-only tables from other random tables we'll also
|
||||
keep in here.
|
||||
|
||||
## Storing state of the menu
|
||||
|
||||
```{sidebar}
|
||||
There is a full implementation of the chargen in
|
||||
[evennia/contrib/tutorials/evadventure/chargen.py](../../../api/evennia.contrib.tutorials.evadventure.chargen.md).
|
||||
```
|
||||
> create a new module `mygame/evadventure/chargen.py`.
|
||||
|
||||
During character generation we will need an entity to store/retain the changes, like a
|
||||
'temporary character sheet'.
|
||||
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/chargen.py
|
||||
|
||||
from .random_tables import chargen_tables
|
||||
from .rules import dice
|
||||
|
||||
class TemporaryCharacterSheet:
|
||||
|
||||
def _random_ability(self):
|
||||
return min(dice.roll("1d6"), dice.roll("1d6"), dice.roll("1d6"))
|
||||
|
||||
def __init__(self):
|
||||
self.ability_changes = 0 # how many times we tried swap abilities
|
||||
|
||||
# name will likely be modified later
|
||||
self.name = dice.roll_random_table("1d282", chargen_tables["name"])
|
||||
|
||||
# base attribute values
|
||||
self.strength = self._random_ability()
|
||||
self.dexterity = self._random_ability()
|
||||
self.constitution = self._random_ability()
|
||||
self.intelligence = self._random_ability()
|
||||
self.wisdom = self._random_ability()
|
||||
self.charisma = self._random_ability()
|
||||
|
||||
# physical attributes (only for rp purposes)
|
||||
physique = dice.roll_random_table("1d20", chargen_tables["physique"])
|
||||
face = dice.roll_random_table("1d20", chargen_tables["face"])
|
||||
skin = dice.roll_random_table("1d20", chargen_tables["skin"])
|
||||
hair = dice.roll_random_table("1d20", chargen_tables["hair"])
|
||||
clothing = dice.roll_random_table("1d20", chargen_tables["clothing"])
|
||||
speech = dice.roll_random_table("1d20", chargen_tables["speech"])
|
||||
virtue = dice.roll_random_table("1d20", chargen_tables["virtue"])
|
||||
vice = dice.roll_random_table("1d20", chargen_tables["vice"])
|
||||
background = dice.roll_random_table("1d20", chargen_tables["background"])
|
||||
misfortune = dice.roll_random_table("1d20", chargen_tables["misfortune"])
|
||||
alignment = dice.roll_random_table("1d20", chargen_tables["alignment"])
|
||||
|
||||
self.desc = (
|
||||
f"You are {physique} with a {face} face, {skin} skin, {hair} hair, {speech} speech,"
|
||||
f" and {clothing} clothing. You were a {background.title()}, but you were"
|
||||
f" {misfortune} and ended up a knave. You are {virtue} but also {vice}. You are of the"
|
||||
f" {alignment} alignment."
|
||||
)
|
||||
|
||||
#
|
||||
self.hp_max = max(5, dice.roll("1d8"))
|
||||
self.hp = self.hp_max
|
||||
self.xp = 0
|
||||
self.level = 1
|
||||
|
||||
# random equipment
|
||||
self.armor = dice.roll_random_table("1d20", chargen_tables["armor"])
|
||||
|
||||
_helmet_and_shield = dice.roll_random_table("1d20", chargen_tables["helmets and shields"])
|
||||
self.helmet = "helmet" if "helmet" in _helmet_and_shield else "none"
|
||||
self.shield = "shield" if "shield" in _helmet_and_shield else "none"
|
||||
|
||||
self.weapon = dice.roll_random_table("1d20", chargen_tables["starting weapon"])
|
||||
|
||||
self.backpack = [
|
||||
"ration",
|
||||
"ration",
|
||||
dice.roll_random_table("1d20", chargen_tables["dungeoning gear"]),
|
||||
dice.roll_random_table("1d20", chargen_tables["dungeoning gear"]),
|
||||
dice.roll_random_table("1d20", chargen_tables["general gear 1"]),
|
||||
dice.roll_random_table("1d20", chargen_tables["general gear 2"]),
|
||||
]
|
||||
```
|
||||
|
||||
Here we have followed the _Knave_ rulebook to randomize abilities, description and equipment.
|
||||
The `dice.roll()` and `dice.roll_random_table` methods now become very useful! Everything here
|
||||
should be easy to follow.
|
||||
|
||||
The main difference from baseline _Knave_ is that we make a table of "starting weapon" (in Knave
|
||||
you can pick whatever you like).
|
||||
|
||||
We also initialize `.ability_changes = 0`. Knave only allows us to swap the values of two
|
||||
Abilities _once_. We will use this to know if it has been done or not.
|
||||
|
||||
### Showing the sheet
|
||||
|
||||
Now that we have our temporary character sheet, we should make it easy to visualize it.
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/chargen.py
|
||||
|
||||
_TEMP_SHEET = """
|
||||
{name}
|
||||
|
||||
STR +{strength}
|
||||
DEX +{dexterity}
|
||||
CON +{constitution}
|
||||
INT +{intelligence}
|
||||
WIS +{wisdom}
|
||||
CHA +{charisma}
|
||||
|
||||
{description}
|
||||
|
||||
Your belongings:
|
||||
{equipment}
|
||||
"""
|
||||
|
||||
class TemporaryCharacterSheet:
|
||||
|
||||
# ...
|
||||
|
||||
def show_sheet(self):
|
||||
equipment = (
|
||||
str(item)
|
||||
for item in [self.armor, self.helmet, self.shield, self.weapon] + self.backpack
|
||||
if item
|
||||
)
|
||||
|
||||
return _TEMP_SHEET.format(
|
||||
name=self.name,
|
||||
strength=self.strength,
|
||||
dexterity=self.dexterity,
|
||||
constitution=self.constitution,
|
||||
intelligence=self.intelligence,
|
||||
wisdom=self.wisdom,
|
||||
charisma=self.charisma,
|
||||
description=self.desc,
|
||||
equipment=", ".join(equipment),
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
The new `show_sheet` method collect the data from the temporary sheet and return it in a pretty
|
||||
form. Making a 'template' string like `_TEMP_SHEET` makes it easier to change things later if you want
|
||||
to change how things look.
|
||||
|
||||
### Apply character
|
||||
|
||||
Once we are happy with our character, we need to actually create it with the stats we chose.
|
||||
This is a bit more involved.
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/chargen.py
|
||||
|
||||
# ...
|
||||
|
||||
from .characters import EvAdventureCharacter
|
||||
from evennia import create_object
|
||||
from evennia.prototypes.spawner import spawn
|
||||
|
||||
|
||||
class TemporaryCharacterSheet:
|
||||
|
||||
# ...
|
||||
|
||||
def apply(self):
|
||||
# create character object with given abilities
|
||||
new_character = create_object(
|
||||
EvAdventureCharacter,
|
||||
key=self.name,
|
||||
attrs=(
|
||||
("strength", self.strength),
|
||||
("dexterity", self.dexterity),
|
||||
("constitution", self.constitution),
|
||||
("intelligence", self.intelligence),
|
||||
("wisdom", self.wisdom),
|
||||
("charisma", self.wisdom),
|
||||
("hp", self.hp),
|
||||
("hp_max", self.hp_max),
|
||||
("desc", self.desc),
|
||||
),
|
||||
)
|
||||
# spawn equipment (will require prototypes created before it works)
|
||||
if self.weapon:
|
||||
weapon = spawn(self.weapon)
|
||||
new_character.equipment.move(weapon)
|
||||
if self.shield:
|
||||
shield = spawn(self.shield)
|
||||
new_character.equipment.move(shield)
|
||||
if self.armor:
|
||||
armor = spawn(self.armor)
|
||||
new_character.equipment.move(armor)
|
||||
if self.helmet:
|
||||
helmet = spawn(self.helmet)
|
||||
new_character.equipment.move(helmet)
|
||||
|
||||
for item in self.backpack:
|
||||
item = spawn(item)
|
||||
new_character.equipment.store(item)
|
||||
|
||||
return new_character
|
||||
```
|
||||
|
||||
We use `create_object` to create a new `EvAdventureCharacter`. We feed it with all relevant data
|
||||
from the temporary character sheet. This is when these become an actual character.
|
||||
|
||||
```{sidebar}
|
||||
A prototype is basically a `dict` describing how the object should be created. Since
|
||||
it's just a piece of code, it can stored in a Python module and used to quickly _spawn_ (create)
|
||||
things from those prototypes.
|
||||
```
|
||||
|
||||
Each piece of equipment is an object in in its own right. We will here assume that all game
|
||||
items are defined as [Prototypes](../../../Components/Prototypes.md) keyed to its name, such as "sword", "brigandine
|
||||
armor" etc.
|
||||
|
||||
We haven't actually created those prototypes yet, so for now we'll need to assume they are there.
|
||||
Once a piece of equipment has been spawned, we make sure to move it into the `EquipmentHandler` we
|
||||
created in the [Equipment lesson](./Beginner-Tutorial-Equipment.md).
|
||||
|
||||
|
||||
## Initializing EvMenu
|
||||
|
||||
Evennia comes with a full menu-generation system based on [Command sets](../../../Components/Command-Sets.md), called
|
||||
[EvMenu](../../../Components/EvMenu.md).
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/chargen.py
|
||||
|
||||
from evennia import EvMenu
|
||||
|
||||
# ...
|
||||
|
||||
# chargen menu
|
||||
|
||||
|
||||
# this goes to the bottom of the module
|
||||
|
||||
def start_chargen(caller, session=None):
|
||||
"""
|
||||
This is a start point for spinning up the chargen from a command later.
|
||||
|
||||
"""
|
||||
|
||||
menutree = {} # TODO!
|
||||
|
||||
# this generates all random components of the character
|
||||
tmp_character = TemporaryCharacterSheet()
|
||||
|
||||
EvMenu(caller, menutree, session=session, tmp_character=tmp_character)
|
||||
|
||||
```
|
||||
|
||||
This first function is what we will call from elsewhere (for example from a custom `charcreate`
|
||||
command) to kick the menu into gear.
|
||||
|
||||
It takes the `caller` (the one to want to start the menu) and a `session` argument. The latter will help
|
||||
track just which client-connection we are using (depending on Evennia settings, you could be
|
||||
connecting with multiple clients).
|
||||
|
||||
We create a `TemporaryCharacterSheet` and call `.generate()` to make a random character. We then
|
||||
feed all this into `EvMenu`.
|
||||
|
||||
The moment this happens, the user will be in the menu, there are no further steps needed.
|
||||
|
||||
The `menutree` is what we'll create next. It describes which menu 'nodes' are available to jump
|
||||
between.
|
||||
|
||||
## Main Node: Choosing what to do
|
||||
|
||||
This is the first menu node. It will act as a central hub, from which one can choose different
|
||||
actions.
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/chargen.py
|
||||
|
||||
# ...
|
||||
|
||||
# at the end of the module, but before the `start_chargen` function
|
||||
|
||||
def node_chargen(caller, raw_string, **kwargs):
|
||||
|
||||
tmp_character = kwargs["tmp_character"]
|
||||
|
||||
text = tmp_character.show_sheet()
|
||||
|
||||
options = [
|
||||
{
|
||||
"desc": "Change your name",
|
||||
"goto": ("node_change_name", kwargs)
|
||||
}
|
||||
]
|
||||
if tmp_character.ability_changes <= 0:
|
||||
options.append(
|
||||
{
|
||||
"desc": "Swap two of your ability scores (once)",
|
||||
"goto": ("node_swap_abilities", kwargs),
|
||||
}
|
||||
)
|
||||
options.append(
|
||||
{
|
||||
"desc": "Accept and create character",
|
||||
"goto": ("node_apply_character", kwargs)
|
||||
},
|
||||
)
|
||||
|
||||
return text, options
|
||||
|
||||
# ...
|
||||
```
|
||||
|
||||
A lot to unpack here! In Evennia, it's convention to name your node-functions `node_*`. While
|
||||
not required, it helps you track what is a node and not.
|
||||
|
||||
Every menu-node, should accept `caller, raw_string, **kwargs` as arguments. Here `caller` is the
|
||||
`caller` you passed into the `EvMenu` call. `raw_string` is the input given by the user in order
|
||||
to _get to this node_, so currently empty. The `**kwargs` are all extra keyword arguments passed
|
||||
into `EvMenu`. They can also be passed between nodes. In this case, we passed the
|
||||
keyword `tmp_character` to `EvMenu`. We now have the temporary character sheet available in the
|
||||
node!
|
||||
|
||||
An `EvMenu` node must always return two things - `text` and `options`. The `text` is what will
|
||||
show to the user when looking at this node. The `options` are, well, what options should be
|
||||
presented to move on from here to some other place.
|
||||
|
||||
For the text, we simply get a pretty-print of the temporary character sheet. A single option is
|
||||
defined as a `dict` like this:
|
||||
|
||||
```python
|
||||
{
|
||||
"key": ("name". "alias1", "alias2", ...), # if skipped, auto-show a number
|
||||
"desc": "text to describe what happens when selecting option",.
|
||||
"goto": ("name of node or a callable", kwargs_to_pass_into_next_node_or_callable)
|
||||
}
|
||||
```
|
||||
|
||||
Multiple option-dicts are returned in a list or tuple. The `goto` option-key is important to
|
||||
understand. The job of this is to either point directly to another node (by giving its name), or
|
||||
by pointing to a Python callable (like a function) _that then returns that name_. You can also
|
||||
pass kwargs (as a dict). This will be made available as `**kwargs` in the callable or next node.
|
||||
|
||||
While an option can have a `key`, you can also skip it to just get a running number.
|
||||
|
||||
In our `node_chargen` node, we point to three nodes by name: `node_change_name`,
|
||||
`node_swap_abilities`, and `node_apply_character`. We also make sure to pass along `kwargs`
|
||||
to each node, since that contains our temporary character sheet.
|
||||
|
||||
The middle of these options only appear if we haven't already switched two abilities around - to
|
||||
know this, we check the `.ability_changes` property to make sure it's still 0.
|
||||
|
||||
|
||||
## Node: Changing your name
|
||||
|
||||
This is where you end up if you opted to change your name in `node_chargen`.
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/chargen.py
|
||||
|
||||
# ...
|
||||
|
||||
# after previous node
|
||||
|
||||
def _update_name(caller, raw_string, **kwargs):
|
||||
"""
|
||||
Used by node_change_name below to check what user
|
||||
entered and update the name if appropriate.
|
||||
|
||||
"""
|
||||
if raw_string:
|
||||
tmp_character = kwargs["tmp_character"]
|
||||
tmp_character.name = raw_string.lower().capitalize()
|
||||
|
||||
return "node_chargen", kwargs
|
||||
|
||||
|
||||
def node_change_name(caller, raw_string, **kwargs):
|
||||
"""
|
||||
Change the random name of the character.
|
||||
|
||||
"""
|
||||
tmp_character = kwargs["tmp_character"]
|
||||
|
||||
text = (
|
||||
f"Your current name is |w{tmp_character.name}|n. "
|
||||
"Enter a new name or leave empty to abort."
|
||||
)
|
||||
|
||||
options = {
|
||||
"key": "_default",
|
||||
"goto": (_update_name, kwargs)
|
||||
}
|
||||
|
||||
return text, options
|
||||
```
|
||||
|
||||
There are two functions here - the menu node itself (`node_change_name`) and a
|
||||
helper _goto_function_ (`_update_name`) to handle the user's input.
|
||||
|
||||
For the (single) option, we use a special `key` named `_default`. This makes this option
|
||||
a catch-all: If the user enters something that does not match any other option, this is
|
||||
the option that will be used.
|
||||
Since we have no other options here, we will always use this option no matter what the user enters.
|
||||
|
||||
Also note that the `goto` part of the option points to the `_update_name` callable rather than to
|
||||
the name of a node. It's important we keep passing `kwargs` along to it!
|
||||
|
||||
When a user writes anything at this node, the `_update_name` callable will be called. This has
|
||||
the same arguments as a node, but it is _not_ a node - we will only use it to _figure out_ which
|
||||
node to go to next.
|
||||
|
||||
In `_update_name` we now have a use for the `raw_string` argument - this is what was written by
|
||||
the user on the previous node, remember? This is now either an empty string (meaning to ignore
|
||||
it) or the new name of the character.
|
||||
|
||||
A goto-function like `_update_name` must return the name of the next node to use. It can also
|
||||
optionally return the `kwargs` to pass into that node - we want to always do this, so we don't
|
||||
loose our temporary character sheet. Here we will always go back to the `node_chargen`.
|
||||
|
||||
> Hint: If returning `None` from a goto-callable, you will always return to the last node you
|
||||
> were at.
|
||||
|
||||
## Node: Swapping Abilities around
|
||||
|
||||
You get here by selecting the second option from the `node_chargen` node.
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/chargen.py
|
||||
|
||||
# ...
|
||||
|
||||
# after previous node
|
||||
|
||||
_ABILITIES = {
|
||||
"STR": "strength",
|
||||
"DEX": "dexterity",
|
||||
"CON": "constitution",
|
||||
"INT": "intelligence",
|
||||
"WIS": "wisdom",
|
||||
"CHA": "charisma",
|
||||
}
|
||||
|
||||
|
||||
def _swap_abilities(caller, raw_string, **kwargs):
|
||||
"""
|
||||
Used by node_swap_abilities to parse the user's input and swap ability
|
||||
values.
|
||||
|
||||
"""
|
||||
if raw_string:
|
||||
abi1, *abi2 = raw_string.split(" ", 1)
|
||||
if not abi2:
|
||||
caller.msg("That doesn't look right.")
|
||||
return None, kwargs
|
||||
abi2 = abi2[0]
|
||||
abi1, abi2 = abi1.upper().strip(), abi2.upper().strip()
|
||||
if abi1 not in _ABILITIES or abi2 not in _ABILITIES:
|
||||
caller.msg("Not a familiar set of abilites.")
|
||||
return None, kwargs
|
||||
|
||||
# looks okay = swap values. We need to convert STR to strength etc
|
||||
tmp_character = kwargs["tmp_character"]
|
||||
abi1 = _ABILITIES[abi1]
|
||||
abi2 = _ABILITIES[abi2]
|
||||
abival1 = getattr(tmp_character, abi1)
|
||||
abival2 = getattr(tmp_character, abi2)
|
||||
|
||||
setattr(tmp_character, abi1, abival2)
|
||||
setattr(tmp_character, abi2, abival1)
|
||||
|
||||
tmp_character.ability_changes += 1
|
||||
|
||||
return "node_chargen", kwargs
|
||||
|
||||
|
||||
def node_swap_abilities(caller, raw_string, **kwargs):
|
||||
"""
|
||||
One is allowed to swap the values of two abilities around, once.
|
||||
|
||||
"""
|
||||
tmp_character = kwargs["tmp_character"]
|
||||
|
||||
text = f"""
|
||||
Your current abilities:
|
||||
|
||||
STR +{tmp_character.strength}
|
||||
DEX +{tmp_character.dexterity}
|
||||
CON +{tmp_character.constitution}
|
||||
INT +{tmp_character.intelligence}
|
||||
WIS +{tmp_character.wisdom}
|
||||
CHA +{tmp_character.charisma}
|
||||
|
||||
You can swap the values of two abilities around.
|
||||
You can only do this once, so choose carefully!
|
||||
|
||||
To swap the values of e.g. STR and INT, write |wSTR INT|n. Empty to abort.
|
||||
"""
|
||||
|
||||
options = {"key": "_default", "goto": (_swap_abilities, kwargs)}
|
||||
|
||||
return text, options
|
||||
```
|
||||
|
||||
This is more code, but the logic is the same - we have a node (`node_swap_abilities`) and
|
||||
and a goto-callable helper (`_swap_abilities`). We catch everything the user writes on the
|
||||
node (such as `WIS CON`) and feed it into the helper.
|
||||
|
||||
In `_swap_abilities`, we need to analyze the `raw_string` from the user to see what they
|
||||
want to do.
|
||||
|
||||
Most code in the helper is validating the user didn't enter nonsense. If they did,
|
||||
we use `caller.msg()` to tell them and then return `None, kwargs`, which re-runs the same node (the
|
||||
name-selection) all over again.
|
||||
|
||||
Since we want users to be able to write "CON" instead of the longer "constitution", we need a
|
||||
mapping `_ABILITIES` to easily convert between the two (it's stored as `consitution` on the
|
||||
temporary character sheet). Once we know which abilities they want to swap, we do so and tick up
|
||||
the `.ability_changes` counter. This means this option will no longer be available from the main
|
||||
node.
|
||||
|
||||
Finally, we return to `node_chargen` again.
|
||||
|
||||
## Node: Creating the Character
|
||||
|
||||
We get here from the main node by opting to finish chargen.
|
||||
|
||||
```python
|
||||
node_apply_character(caller, raw_string, **kwargs):
|
||||
"""
|
||||
End chargen and create the character. We will also puppet it.
|
||||
|
||||
"""
|
||||
tmp_character = kwargs["tmp_character"]
|
||||
new_character = tmp_character.apply(caller)
|
||||
|
||||
caller.account.db._playable_characters = [new_character]
|
||||
|
||||
text = "Character created!"
|
||||
|
||||
return text, None
|
||||
```
|
||||
When entering the node, we will take the Temporary character sheet and use its `.appy` method to
|
||||
create a new Character with all equipment.
|
||||
|
||||
This is what is called an _end node_, because it returns `None` instead of options. After this,
|
||||
the menu will exit. We will be back to the default character selection screen. The characters
|
||||
found on that screen are the ones listed in the `_playable_characters` Attribute, so we need to
|
||||
also the new character to it.
|
||||
|
||||
|
||||
## Tying the nodes together
|
||||
|
||||
```python
|
||||
def start_chargen(caller, session=None):
|
||||
"""
|
||||
This is a start point for spinning up the chargen from a command later.
|
||||
|
||||
"""
|
||||
menutree = { # <----- can now add this!
|
||||
"node_chargen": node_chargen,
|
||||
"node_change_name": node_change_name,
|
||||
"node_swap_abilities": node_swap_abilities,
|
||||
"node_apply_character": node_apply_character
|
||||
}
|
||||
|
||||
# this generates all random components of the character
|
||||
tmp_character = TemporaryCharacterSheet()
|
||||
tmp_character.generate()
|
||||
|
||||
EvMenu(caller, menutree, session=session,
|
||||
startnode="node_chargen", # <-----
|
||||
tmp_character=tmp_character)
|
||||
|
||||
```
|
||||
|
||||
Now that we have all the nodes, we add them to the `menutree` we left empty before. We only add
|
||||
the nodes, _not_ the goto-helpers! The keys we set in the `menutree` dictionary are the names we
|
||||
should use to point to nodes from inside the menu (and we did).
|
||||
|
||||
We also add a keyword argument `startnode` pointing to the `node_chargen` node. This tells EvMenu
|
||||
to first jump into that node when the menu is starting up.
|
||||
|
||||
## Conclusions
|
||||
|
||||
This lesson taught us how to use `EvMenu` to make an interactive character generator. In an RPG
|
||||
more complex than _Knave_, the menu would be bigger and more intricate, but the same principles
|
||||
apply.
|
||||
|
||||
Together with the previous lessons we have now fished most of the basics around player
|
||||
characters - how they store their stats, handle their equipment and how to create them.
|
||||
|
||||
In the next lesson we'll address how EvAdventure _Rooms_ work.
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
# In-game Commands
|
||||
|
||||
```{warning}
|
||||
This part of the Beginner tutorial is still being developed.
|
||||
```
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
# Dynamically generated Dungeon
|
||||
|
||||
```{warning}
|
||||
This part of the Beginner tutorial is still being developed.
|
||||
```
|
||||
|
|
@ -1,599 +0,0 @@
|
|||
# Handling Equipment
|
||||
|
||||
In _Knave_, you have a certain number of inventory "slots". The amount of slots is given by `CON + 10`.
|
||||
All items (except coins) have a `size`, indicating how many slots it uses. You can't carry more items
|
||||
than you have slot-space for. Also items wielded or worn count towards the slots.
|
||||
|
||||
We still need to track what the character is using however: What weapon they have readied affects the damage
|
||||
they can do. The shield, helmet and armor they use affects their defense.
|
||||
|
||||
We have already set up the possible 'wear/wield locations' when we defined our Objects
|
||||
[in the previous lesson](./Beginner-Tutorial-Objects.md). This is what we have in `enums.py`:
|
||||
|
||||
```python
|
||||
# mygame/evadventure/enums.py
|
||||
|
||||
# ...
|
||||
|
||||
class WieldLocation(Enum):
|
||||
|
||||
BACKPACK = "backpack"
|
||||
WEAPON_HAND = "weapon_hand"
|
||||
SHIELD_HAND = "shield_hand"
|
||||
TWO_HANDS = "two_handed_weapons"
|
||||
BODY = "body" # armor
|
||||
HEAD = "head" # helmets
|
||||
```
|
||||
|
||||
Basically, all the weapon/armor locations are exclusive - you can only have one item in each (or none).
|
||||
The BACKPACK is special - it contains any number of items (up to the maximum slot usage).
|
||||
|
||||
## EquipmentHandler that saves
|
||||
|
||||
> Create a new module `mygame/evadventure/equipment.py`.
|
||||
|
||||
```{sidebar}
|
||||
If you want to understand more about behind how Evennia uses handlers, there is a
|
||||
[dedicated tutorial](../../Tutorial-Persistent-Handler.md) talking about the principle.
|
||||
```
|
||||
In default Evennia, everything you pick up will end up "inside" your character object (that is, have
|
||||
you as its `.location`). This is called your _inventory_ and has no limit. We will keep 'moving items into us'
|
||||
when we pick them up, but we will add more functionality using an _Equipment handler_.
|
||||
|
||||
A handler is (for our purposes) an object that sits "on" another entity, containing functionality
|
||||
for doing one specific thing (managing equipment, in our case).
|
||||
|
||||
This is the start of our handler:
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/equipment.py
|
||||
|
||||
from .enums import WieldLocation
|
||||
|
||||
class EquipmentHandler:
|
||||
save_attribute = "inventory_slots"
|
||||
|
||||
def __init__(self, obj):
|
||||
# here obj is the character we store the handler on
|
||||
self.obj = obj
|
||||
self._load()
|
||||
|
||||
def _load(self):
|
||||
"""Load our data from an Attribute on `self.obj`"""
|
||||
self.slots = self.obj.attributes.get(
|
||||
self.save_attribute,
|
||||
category="inventory",
|
||||
default={
|
||||
WieldLocation.WEAPON_HAND: None,
|
||||
WieldLocation.SHIELD_HAND: None,
|
||||
WieldLocation.TWO_HANDS: None,
|
||||
WieldLocation.BODY: None,
|
||||
WieldLocation.HEAD: None,
|
||||
WieldLocation.BACKPACK: []
|
||||
}
|
||||
)
|
||||
|
||||
def _save(self):
|
||||
"""Save our data back to the same Attribute"""
|
||||
self.obj.attributes.add(self.save_attribute, self.slots, category="inventory")
|
||||
```
|
||||
|
||||
This is a compact and functional little handler. Before analyzing how it works, this is how
|
||||
we will add it to the Character:
|
||||
|
||||
```python
|
||||
# mygame/evadventure/characters.py
|
||||
|
||||
# ...
|
||||
|
||||
from evennia.utils.utils import lazy_property
|
||||
from .equipment import EquipmentHandler
|
||||
|
||||
# ...
|
||||
|
||||
class EvAdventureCharacter(LivingMixin, DefaultCharacter):
|
||||
|
||||
# ...
|
||||
|
||||
@lazy_property
|
||||
def equipment(self):
|
||||
return EquipmentHandler(self)
|
||||
```
|
||||
|
||||
After reloading the server, the equipment-handler will now be accessible on character-instances as
|
||||
|
||||
character.equipment
|
||||
|
||||
The `@lazy_property` works such that it will not load the handler until someone actually tries to
|
||||
fetch it with `character.equipment`. When that
|
||||
happens, we start up the handler and feed it `self` (the `Character` instance itself). This is what
|
||||
enters `__init__` as `.obj` in the `EquipmentHandler` code above.
|
||||
|
||||
So we now have a handler on the character, and the handler has a back-reference to the character it sits
|
||||
on.
|
||||
|
||||
Since the handler itself is just a regular Python object, we need to use the `Character` to store
|
||||
our data - our _Knave_ "slots". We must save them to the database, because we want the server to remember
|
||||
them even after reloading.
|
||||
|
||||
Using `self.obj.attributes.add()` and `.get()` we save the data to the Character in a specially named
|
||||
[Attribute](../../../Components/Attributes.md). Since we use a `category`, we are unlikely to collide with
|
||||
other Attributes.
|
||||
|
||||
Our storage structure is a `dict` with keys after our available `WieldLocation` enums. Each can only
|
||||
have one item except `WieldLocation.BACKPACK`, which is a list.
|
||||
|
||||
## Connecting the EquipmentHandler
|
||||
|
||||
Whenever an object leaves from one location to the next, Evennia will call a set of _hooks_ (methods) on the
|
||||
object that moves, on the source-location and on its destination. This is the same for all moving things -
|
||||
whether it's a character moving between rooms or an item being dropping from your hand to the ground.
|
||||
|
||||
We need to tie our new `EquipmentHandler` into this system. By reading the doc page on [Objects](../../../Components/Objects.md),
|
||||
or looking at the [DefaultObject.move_to](evennia.objects.objects.DefaultObject.move_to) docstring, we'll
|
||||
find out what hooks Evennia will call. Here `self` is the object being moved from
|
||||
`source_location` to `destination`:
|
||||
|
||||
|
||||
1. `self.at_pre_move(destination)` (abort if return False)
|
||||
2. `source_location.at_pre_object_leave(self, destination)` (abort if return False)
|
||||
3. `destination.at_pre_object_receive(self, source_location)` (abort if return False)
|
||||
4. `source_location.at_object_leave(self, destination)`
|
||||
5. `self.announce_move_from(destination)`
|
||||
6. (move happens here)
|
||||
7. `self.announce_move_to(source_location)`
|
||||
8. `destination.at_object_receive(self, source_location)`
|
||||
9. `self.at_post_move(source_location)`
|
||||
|
||||
All of these hooks can be overridden to customize movement behavior. In this case we are interested in
|
||||
controlling how items 'enter' and 'leave' our character - being 'inside' the character is the same as
|
||||
them 'carrying' it. We have three good hook-candidates to use for this.
|
||||
|
||||
- `.at_pre_object_receive` - used to check if you can actually pick something up, or if your equipment-store is full.
|
||||
- `.at_object_receive` - used to add the item to the equipmenthandler
|
||||
- `.at_object_leave` - used to remove the item from the equipmenthandler
|
||||
|
||||
You could also picture using `.at_pre_object_leave` to restrict dropping (cursed?) items, but
|
||||
we will skip that for this tutorial.
|
||||
|
||||
```python
|
||||
# mygame/evadventure/character.py
|
||||
|
||||
# ...
|
||||
|
||||
class EvAdventureCharacter(LivingMixin, DefaultCharacter):
|
||||
|
||||
# ...
|
||||
|
||||
def at_pre_object_receive(self, moved_object, source_location, **kwargs):
|
||||
"""Called by Evennia before object arrives 'in' this character (that is,
|
||||
if they pick up something). If it returns False, move is aborted.
|
||||
|
||||
"""
|
||||
return self.equipment.validate_slot_usage(moved_object)
|
||||
|
||||
def at_object_receive(self, moved_object, source_location, **kwargs):
|
||||
"""
|
||||
Called by Evennia when an object arrives 'in' the character.
|
||||
|
||||
"""
|
||||
self.equipment.add(moved_object)
|
||||
|
||||
def at_object_leave(self, moved_object, destination, **kwargs):
|
||||
"""
|
||||
Called by Evennia when object leaves the Character.
|
||||
|
||||
"""
|
||||
self.equipment.remove(moved_object)
|
||||
```
|
||||
|
||||
Above we have assumed the `EquipmentHandler` (`.equipment`) has methods `.validate_slot_usage`,
|
||||
`.add` and `.remove`. But we haven't actually added them yet - we just put some reasonable names! Before
|
||||
we can use this, we need to go actually adding those methods.
|
||||
|
||||
## Expanding the Equipmenthandler
|
||||
|
||||
## `.validate_slot_usage`
|
||||
|
||||
Let's start with implementing the first method we came up with above, `validate_slot_usage`:
|
||||
|
||||
```python
|
||||
# mygame/evadventure/equipment.py
|
||||
|
||||
from .enums import WieldLocation, Ability
|
||||
|
||||
class EquipmentError(TypeError):
|
||||
"""All types of equipment-errors"""
|
||||
pass
|
||||
|
||||
class EquipmentHandler:
|
||||
|
||||
# ...
|
||||
|
||||
@property
|
||||
def max_slots(self):
|
||||
"""Max amount of slots, based on CON defense (CON + 10)"""
|
||||
return getattr(self.obj, Ability.CON.value, 1) + 10
|
||||
|
||||
def count_slots(self):
|
||||
"""Count current slot usage"""
|
||||
slots = self.slots
|
||||
wield_usage = sum(
|
||||
getattr(slotobj, "size", 0) or 0
|
||||
for slot, slotobj in slots.items()
|
||||
if slot is not WieldLocation.BACKPACK
|
||||
)
|
||||
backpack_usage = sum(
|
||||
getattr(slotobj, "size", 0) or 0 for slotobj in slots[WieldLocation.BACKPACK]
|
||||
)
|
||||
return wield_usage + backpack_usage
|
||||
|
||||
def validate_slot_usage(self, obj):
|
||||
"""
|
||||
Check if obj can fit in equipment, based on its size.
|
||||
|
||||
"""
|
||||
if not inherits_from(obj, EvAdventureObject):
|
||||
# in case we mix with non-evadventure objects
|
||||
raise EquipmentError(f"{obj.key} is not something that can be equipped.")
|
||||
|
||||
size = obj.size
|
||||
max_slots = self.max_slots
|
||||
current_slot_usage = self.count_slots()
|
||||
return current_slot_usage + size <= max_slots:
|
||||
|
||||
```
|
||||
|
||||
```{sidebar}
|
||||
The `@property` decorator turns a method into a property so you don't need to 'call' it.
|
||||
That is, you can access `.max_slots` instead of `.max_slots()`. In this case, it's just a
|
||||
little less to type.
|
||||
```
|
||||
We add two helpers - the `max_slots` _property_ and `count_slots`, a method that calculate the current
|
||||
slots being in use. Let's figure out how they work.
|
||||
|
||||
### `.max_slots`
|
||||
|
||||
For `max_slots`, remember that `.obj` on the handler is a back-reference to the `EvAdventureCharacter` we
|
||||
put this handler on. `getattr` is a Python method for retrieving a named property on an object.
|
||||
The `Enum` `Ability.CON.value` is the string `Constitution` (check out the
|
||||
[first Utility and Enums tutorial](./Beginner-Tutorial-Utilities.md) if you don't recall).
|
||||
|
||||
So to be clear,
|
||||
|
||||
```python
|
||||
getattr(self.obj, Ability.CON.value) + 10
|
||||
```
|
||||
is the same as writing
|
||||
|
||||
```python
|
||||
getattr(your_character, "Constitution") + 10
|
||||
```
|
||||
|
||||
which is the same as doing something like this:
|
||||
|
||||
```python
|
||||
your_character.Constitution + 10
|
||||
```
|
||||
|
||||
In our code we write `getattr(self.obj, Ability.CON.value, 1)` - that extra `1` means that if there
|
||||
should happen to _not_ be a property "Constitution" on `self.obj`, we should not error out but just
|
||||
return 1.
|
||||
|
||||
|
||||
### `.count_slots`
|
||||
|
||||
In this helper we use two Python tools - the `sum()` function and a
|
||||
[list comprehension](https://www.w3schools.com/python/python_lists_comprehension.asp). The former
|
||||
simply adds the values of any iterable together. The latter is a more efficient way to create a list:
|
||||
|
||||
new_list = [item for item in some_iterable if condition]
|
||||
all_above_5 = [num for num in range(10) if num > 5] # [6, 7, 8, 9]
|
||||
all_below_5 = [num for num in range(10) if num < 5] # [0, 1, 2, 3, 4]
|
||||
|
||||
To make it easier to understand, try reading the last line above as "for every number in the range 0-9,
|
||||
pick all with a value below 5 and make a list of them". You can also embed such comprehensions
|
||||
directly in a function call like `sum()` without using `[]` around it.
|
||||
|
||||
In `count_slots` we have this code:
|
||||
|
||||
```python
|
||||
wield_usage = sum(
|
||||
getattr(slotobj, "size", 0)
|
||||
for slot, slotobj in slots.items()
|
||||
if slot is not WieldLocation.BACKPACK
|
||||
)
|
||||
```
|
||||
|
||||
We should be able to follow all except `slots.items()`. Since `slots` is a `dict`, we can use `.items()`
|
||||
to get a sequence of `(key, value)` pairs. We store these in `slot` and `slotobj`. So the above can
|
||||
be understood as "for every `slot` and `slotobj`-pair in `slots`, check which slot location it is.
|
||||
If it is _not_ in the backpack, get its size and add it to the list. Sum over all these
|
||||
sizes".
|
||||
|
||||
A less compact but maybe more readonable way to write this would be:
|
||||
|
||||
```python
|
||||
backpack_item_sizes = []
|
||||
for slot, slotobj in slots.items():
|
||||
if slot is not WieldLocation.BACKPACK:
|
||||
size = getattr(slotobj, "size", 0)
|
||||
backpack_item_sizes.append(size)
|
||||
wield_usage = sum(backpack_item_sizes)
|
||||
```
|
||||
|
||||
The same is done for the items actually in the BACKPACK slot. The total sizes are added
|
||||
together.
|
||||
|
||||
### Validating slots
|
||||
|
||||
With these helpers in place, `validate_slot_usage` now becomes simple. We use `max_slots` to see how much we can carry.
|
||||
We then get how many slots we are already using (with `count_slots`) and see if our new `obj`'s size
|
||||
would be too much for us.
|
||||
|
||||
## `.add` and `.remove`
|
||||
|
||||
We will make it so `.add` puts something in the `BACKPACK` location and `remove` drops it, wherever
|
||||
it is (even if it was in your hands).
|
||||
|
||||
```python
|
||||
# mygame/evadventure/equipment.py
|
||||
|
||||
from .enums import WieldLocation, Ability
|
||||
|
||||
# ...
|
||||
|
||||
class EquipmentHandler:
|
||||
|
||||
# ...
|
||||
|
||||
def add(self, obj):
|
||||
"""
|
||||
Put something in the backpack.
|
||||
"""
|
||||
self.validate_slot_usage(obj)
|
||||
self.slots[WieldLocation.BACKPACK].append(obj)
|
||||
self._save()
|
||||
|
||||
def remove(self, slot):
|
||||
"""
|
||||
Remove contents of a particular slot, for
|
||||
example `equipment.remove(WieldLocation.SHIELD_HAND)`
|
||||
"""
|
||||
slots = self.slots
|
||||
ret = []
|
||||
if slot is WieldLocation.BACKPACK:
|
||||
# empty entire backpack!
|
||||
ret.extend(slots[slot])
|
||||
slots[slot] = []
|
||||
else:
|
||||
ret.append(slots[slot])
|
||||
slots[slot] = None
|
||||
if ret:
|
||||
self._save()
|
||||
return ret
|
||||
```
|
||||
|
||||
Both of these should be straight forward to follow. In `.add`, we make use of `validate_slot_usage` to
|
||||
double-check we can actually fit the thing, then we add the item to the backpack.
|
||||
|
||||
In `.delete`, we allow emptying by `WieldLocation` - we figure out what slot it is and return
|
||||
the item within (if any). If we gave `BACKPACK` as the slot, we empty the backpack and
|
||||
return all items.
|
||||
|
||||
Whenever we change the equipment loadout we must make sure to `._save()` the result, or it will
|
||||
be lost after a server reload.
|
||||
|
||||
## Moving things around
|
||||
|
||||
With the help of `.remove()` and `.add()` we can get things in and out of the `BACKPACK` equipment
|
||||
location. We also need to grab stuff from the backpack and wield or wear it. We add a `.move` method
|
||||
on the `EquipmentHandler` to do this:
|
||||
|
||||
```python
|
||||
# mygame/evadventure/equipment.py
|
||||
|
||||
from .enums import WieldLocation, Ability
|
||||
|
||||
# ...
|
||||
|
||||
class EquipmentHandler:
|
||||
|
||||
# ...
|
||||
|
||||
def move(self, obj):
|
||||
"""Move object from backpack to its intended `inventory_use_slot`."""
|
||||
|
||||
# make sure to remove from equipment/backpack first, to avoid double-adding
|
||||
self.remove(obj)
|
||||
|
||||
slots = self.slots
|
||||
use_slot = getattr(obj, "inventory_use_slot", WieldLocation.BACKPACK)
|
||||
|
||||
to_backpack = []
|
||||
if use_slot is WieldLocation.TWO_HANDS:
|
||||
# two-handed weapons can't co-exist with weapon/shield-hand used items
|
||||
to_backpack = [slots[WieldLocation.WEAPON_HAND], slots[WieldLocation.SHIELD_HAND]]
|
||||
slots[WieldLocation.WEAPON_HAND] = slots[WieldLocation.SHIELD_HAND] = None
|
||||
slots[use_slot] = obj
|
||||
elif use_slot in (WieldLocation.WEAPON_HAND, WieldLocation.SHIELD_HAND):
|
||||
# can't keep a two-handed weapon if adding a one-handed weapon or shield
|
||||
to_backpack = [slots[WieldLocation.TWO_HANDS]]
|
||||
slots[WieldLocation.TWO_HANDS] = None
|
||||
slots[use_slot] = obj
|
||||
elif use_slot is WieldLocation.BACKPACK:
|
||||
# it belongs in backpack, so goes back to it
|
||||
to_backpack = [obj]
|
||||
else:
|
||||
# for others (body, head), just replace whatever's there
|
||||
replaced = [obj]
|
||||
slots[use_slot] = obj
|
||||
|
||||
for to_backpack_obj in to_backpack:
|
||||
# put stuff in backpack
|
||||
slots[use_slot].append(to_backpack_obj)
|
||||
|
||||
# store new state
|
||||
self._save()
|
||||
```
|
||||
|
||||
Here we remember that every `EvAdventureObject` has an `inventory_use_slot` property that tells us where
|
||||
it goes. So we just need to move the object to that slot, replacing whatever is in that place
|
||||
from before. Anything we replace goes back to the backpack.
|
||||
|
||||
## Get everything
|
||||
|
||||
In order to visualize our inventory, we need some method to get everything we are carrying.
|
||||
|
||||
|
||||
```python
|
||||
# mygame/evadventure/equipment.py
|
||||
|
||||
from .enums import WieldLocation, Ability
|
||||
|
||||
# ...
|
||||
|
||||
class EquipmentHandler:
|
||||
|
||||
# ...
|
||||
|
||||
def all(self):
|
||||
"""
|
||||
Get all objects in inventory, regardless of location.
|
||||
"""
|
||||
slots = self.slots
|
||||
lst = [
|
||||
(slots[WieldLocation.WEAPON_HAND], WieldLocation.WEAPON_HAND),
|
||||
(slots[WieldLocation.SHIELD_HAND], WieldLocation.SHIELD_HAND),
|
||||
(slots[WieldLocation.TWO_HANDS], WieldLocation.TWO_HANDS),
|
||||
(slots[WieldLocation.BODY], WieldLocation.BODY),
|
||||
(slots[WieldLocation.HEAD], WieldLocation.HEAD),
|
||||
] + [(item, WieldLocation.BACKPACK) for item in slots[WieldLocation.BACKPACK]]
|
||||
return lst
|
||||
```
|
||||
|
||||
Here we get all the equipment locations and add their contents together into a list of tuples
|
||||
`[(item, WieldLocation), ...]`. This is convenient for display.
|
||||
|
||||
## Weapon and armor
|
||||
|
||||
It's convenient to have the `EquipmentHandler` easily tell you what weapon is currently wielded
|
||||
and what _armor_ level all worn equipment provides. Otherwise you'd need to figure out what item is
|
||||
in which wield-slot and to add up armor slots manually every time you need to know.
|
||||
|
||||
|
||||
```python
|
||||
# mygame/evadventure/equipment.py
|
||||
|
||||
from .objects import WeaponEmptyHand
|
||||
from .enums import WieldLocation, Ability
|
||||
|
||||
# ...
|
||||
|
||||
class EquipmentHandler:
|
||||
|
||||
# ...
|
||||
|
||||
@property
|
||||
def armor(self):
|
||||
slots = self.slots
|
||||
return sum(
|
||||
(
|
||||
# armor is listed using its defense, so we remove 10 from it
|
||||
# (11 is base no-armor value in Knave)
|
||||
getattr(slots[WieldLocation.BODY], "armor", 1),
|
||||
# shields and helmets are listed by their bonus to armor
|
||||
getattr(slots[WieldLocation.SHIELD_HAND], "armor", 0),
|
||||
getattr(slots[WieldLocation.HEAD], "armor", 0),
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def weapon(self):
|
||||
# first checks two-handed wield, then one-handed; the two
|
||||
# should never appear simultaneously anyhow (checked in `move` method).
|
||||
slots = self.slots
|
||||
weapon = slots[WieldLocation.TWO_HANDS]
|
||||
if not weapon:
|
||||
weapon = slots[WieldLocation.WEAPON_HAND]
|
||||
if not weapon:
|
||||
weapon = WeaponEmptyHand()
|
||||
return weapon
|
||||
|
||||
```
|
||||
|
||||
In the `.armor()` method we get the item (if any) out of each relevant wield-slot (body, shield, head),
|
||||
and grab their `armor` Attribute. We then `sum()` them all up.
|
||||
|
||||
In `.weapon()`, we simply check which of the possible weapon slots (weapon-hand or two-hands) have
|
||||
something in them. If not we fall back to the 'fake' weapon `WeaponEmptyHand` which is just a 'dummy'
|
||||
object that represents your bare hands with damage and all.
|
||||
(created in [The Object tutorial](./Beginner-Tutorial-Objects.md#your-bare-hands) earlier).
|
||||
|
||||
|
||||
## Extra credits
|
||||
|
||||
This covers the basic functionality of the equipment handler. There are other useful methods that
|
||||
can be added:
|
||||
|
||||
- Given an item, figure out which equipment slot it is currently in
|
||||
- Make a string representing the current loadout
|
||||
- Get everything in the backpack (only)
|
||||
- Get all wieldable items (weapons, shields) from backpack
|
||||
- Get all usable items (items with a use-location of `BACKPACK`) from the backpack
|
||||
|
||||
Experiment with adding those. A full example is found in
|
||||
[evennia/contrib/tutorials/evadventure/equipment.py](../../../api/evennia.contrib.tutorials.evadventure.equipment.md).
|
||||
|
||||
## Unit Testing
|
||||
|
||||
> Create a new module `mygame/evadventure/tests/test_equipment.py`.
|
||||
|
||||
```{sidebar}
|
||||
See [evennia/contrib/tutorials/evadventure/tests/test_equipment.py](../../../api/evennia.contrib.tutorials.evadventure.tests.test_equipment.md)
|
||||
for a finished testing example.
|
||||
```
|
||||
|
||||
To test the `EquipmentHandler`, easiest is create an `EvAdventureCharacter` (this should by now
|
||||
have `EquipmentHandler` available on itself as `.equipment`) and a few test objects; then test
|
||||
passing these into the handler's methods.
|
||||
|
||||
|
||||
```python
|
||||
# mygame/evadventure/tests/test_equipment.py
|
||||
|
||||
from evennia.utils import create
|
||||
from evennia.utils.test_resources import BaseEvenniaTest
|
||||
|
||||
from ..objects import EvAdventureRoom
|
||||
from ..enums import WieldLocation
|
||||
|
||||
class TestEquipment(BaseEvenniaTest):
|
||||
|
||||
def setUp(self):
|
||||
self.character = create.create_object(EvAdventureCharacter, key='testchar')
|
||||
self.helmet = create.create_object(EvAdventureHelmet, key="helmet")
|
||||
self.weapon = create.create_object(EvAdventureWeapon, key="weapon")
|
||||
|
||||
def test_add_remove):
|
||||
self.character.equipment.add(self.helmet)
|
||||
self.assertEqual(
|
||||
self.character.equipment.slots[WieldLocation.BACKPACK],
|
||||
[self.helmet]
|
||||
)
|
||||
self.character.equipment.remove(self.helmet)
|
||||
self.assertEqual(self.character.equipment.slots[WieldLocation.BACKPACK], [])
|
||||
|
||||
# ...
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
_Handlers_ are useful for grouping functionality together. Now that we spent our time making the
|
||||
`EquipmentHandler`, we shouldn't need to worry about item-slots anymore - the handler 'handles' all
|
||||
the details for us. As long as we call its methods, the details can be forgotten about.
|
||||
|
||||
We also learned to use _hooks_ to tie _Knave_'s custom equipment handling into Evennia.
|
||||
|
||||
With `Characters`, `Objects` and now `Equipment` in place, we should be able to move on to character
|
||||
generation - where players get to make their own character!
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
# Non-Player-Characters (NPCs)
|
||||
|
||||
```{warning}
|
||||
This part of the Beginner tutorial is still being developed.
|
||||
```
|
||||
|
|
@ -1,360 +0,0 @@
|
|||
# In-game Objects and items
|
||||
|
||||
In the previous lesson we established what a 'Character' is in our game. Before we continue
|
||||
we also need to have a notion what an 'item' or 'object' is.
|
||||
|
||||
Looking at _Knave_'s item lists, we can get some ideas of what we need to track:
|
||||
|
||||
- `size` - this is how many 'slots' the item uses in the character's inventory.
|
||||
- `value` - a base value if we want to sell or buy the item.
|
||||
- `inventory_use_slot` - some items can be worn or wielded. For example, a helmet needs to be
|
||||
worn on the head and a shield in the shield hand. Some items can't be used this way at all, but
|
||||
only belong in the backpack.
|
||||
- `obj_type` - Which 'type' of item this is.
|
||||
|
||||
|
||||
## New Enums
|
||||
|
||||
We added a few enumberations for Abilities back in the [Utilities tutorial](./Beginner-Tutorial-Utilities.md).
|
||||
Before we continue, let's expand with enums for use-slots and object types.
|
||||
|
||||
```python
|
||||
# mygame/evadventure/enums.py
|
||||
|
||||
# ...
|
||||
|
||||
class WieldLocation(Enum):
|
||||
|
||||
BACKPACK = "backpack"
|
||||
WEAPON_HAND = "weapon_hand"
|
||||
SHIELD_HAND = "shield_hand"
|
||||
TWO_HANDS = "two_handed_weapons"
|
||||
BODY = "body" # armor
|
||||
HEAD = "head" # helmets
|
||||
|
||||
class ObjType(Enum):
|
||||
|
||||
WEAPON = "weapon"
|
||||
ARMOR = "armor"
|
||||
SHIELD = "shield"
|
||||
HELMET = "helmet"
|
||||
CONSUMABLE = "consumable"
|
||||
GEAR = "gear"
|
||||
MAGIC = "magic"
|
||||
QUEST = "quest"
|
||||
TREASURE = "treasure"
|
||||
```
|
||||
|
||||
Once we have these enums, we will use them for referencing things.
|
||||
|
||||
## The base object
|
||||
|
||||
> Create a new module `mygame/evadventure/objects.py`
|
||||
|
||||
```{sidebar}
|
||||
[evennia/contrib/tutorials/evadventure/objects.py](../../../api/evennia.contrib.tutorials.evadventure.objects.md) has
|
||||
a full set of objects implemented.
|
||||
```
|
||||
<div style="clear: right;"></div>
|
||||
|
||||
We will make a base `EvAdventureObject` class off Evennia's standard `DefaultObject`. We will then add
|
||||
child classes to represent the relevant types:
|
||||
|
||||
```python
|
||||
# mygame/evadventure/objects.py
|
||||
|
||||
from evennia import AttributeProperty, DefaultObject
|
||||
from evennia.utils.utils import make_iter
|
||||
from .utils import get_obj_stats
|
||||
from .enums import WieldLocation, ObjType
|
||||
|
||||
|
||||
class EvAdventureObject(DefaultObject):
|
||||
"""
|
||||
Base for all evadventure objects.
|
||||
|
||||
"""
|
||||
inventory_use_slot = WieldLocation.BACKPACK
|
||||
size = AttributeProperty(1, autocreate=False)
|
||||
value = AttributeProperty(0, autocreate=False)
|
||||
|
||||
# this can be either a single type or a list of types (for objects able to be
|
||||
# act as multiple). This is used to tag this object during creation.
|
||||
obj_type = ObjType.GEAR
|
||||
|
||||
def at_object_creation(self):
|
||||
"""Called when this object is first created. We convert the .obj_type
|
||||
property to a database tag."""
|
||||
|
||||
for obj_type in make_iter(self.obj_type):
|
||||
self.tags.add(self.obj_type.value, category="obj_type")
|
||||
|
||||
def get_help(self):
|
||||
"""Get any help text for this item"""
|
||||
return "No help for this item"
|
||||
```
|
||||
|
||||
### Using Attributes or not
|
||||
|
||||
In theory, `size` and `value` does not change and _could_ also be just set as a regular Python
|
||||
property on the class:
|
||||
|
||||
```python
|
||||
class EvAdventureObject(DefaultObject):
|
||||
inventory_use_slot = WieldLocation.BACKPACK
|
||||
size = 1
|
||||
value = 0
|
||||
```
|
||||
|
||||
The problem with this is that if we want to make a new object of `size 3` and `value 20`, we have to
|
||||
make a new class for it. We can't change it on the fly because the change would only be in memory and
|
||||
be lost on next server reload.
|
||||
|
||||
Because we use `AttributeProperties`, we can set `size` and `value` to whatever we like when we
|
||||
create the object (or later), and the Attributes will remember our changes to that object indefinitely.
|
||||
|
||||
To make this a little more efficient, we use `autocreate=False`. Normally when you create a
|
||||
new object with defined `AttributeProperties`, a matching `Attribute` is immediately created at
|
||||
the same time. So normally, the object would be created along with two Attributes `size` and `value`.
|
||||
With `autocreate=False`, no Attribute will be created _unless the default is changed_. That is, as
|
||||
long as your object has `size=1` no database `Attribute` will be created at all. This saves time and
|
||||
resources when creating large number of objects.
|
||||
|
||||
The drawback is that since no Attribute is created you can't refer to it
|
||||
with `obj.db.size` or `obj.attributes.get("size")` _unless you change its default_. You also can't query
|
||||
the database for all objects with `size=1`, since most objects would not yet have an in-database
|
||||
`size` Attribute to search for.
|
||||
|
||||
In our case, we'll only refer to these properties as `obj.size` etc, and have no need to find
|
||||
all objects of a particular size. So we should be safe.
|
||||
|
||||
### Creating tags in `at_object_creation`
|
||||
|
||||
The `at_object_creation` is a method Evennia calls on every child of `DefaultObject` whenever it is
|
||||
first created.
|
||||
|
||||
We do a tricky thing here, converting our `.obj_type` to one or more [Tags](../../../Components/Tags.md). Tagging the
|
||||
object like this means you can later efficiently find all objects of a given type (or combination of
|
||||
types) with Evennia's search functions:
|
||||
|
||||
```python
|
||||
from .enums import ObjType
|
||||
from evennia.utils import search
|
||||
|
||||
# get all shields in the game
|
||||
all_shields = search.search_object_by_tag(ObjType.SHIELD.value, category="obj_type")
|
||||
```
|
||||
|
||||
We allow `.obj_type` to be given as a single value or a list of values. We use `make_iter` from the
|
||||
evennia utility library to make sure we don't balk at either. This means you could have a Shield that
|
||||
is also Magical, for example.
|
||||
|
||||
## Other object types
|
||||
|
||||
Some of the other object types are very simple so far.
|
||||
|
||||
```python
|
||||
# mygame/evadventure/objects.py
|
||||
|
||||
from evennia import AttributeProperty, DefaultObject
|
||||
from .enums import ObjType
|
||||
|
||||
class EvAdventureObject(DefaultObject):
|
||||
# ...
|
||||
|
||||
|
||||
class EvAdventureQuestObject(EvAdventureObject):
|
||||
"""Quest objects should usually not be possible to sell or trade."""
|
||||
obj_type = ObjType.QUEST
|
||||
|
||||
class EvAdventureTreasure(EvAdventureObject):
|
||||
"""Treasure is usually just for selling for coin"""
|
||||
obj_type = ObjType.TREASURE
|
||||
value = AttributeProperty(100, autocreate=False)
|
||||
|
||||
```
|
||||
|
||||
## Consumables
|
||||
|
||||
A 'consumable' is an item that has a certain number of 'uses'. Once fully consumed, it can't be used
|
||||
anymore. An example would be a health potion.
|
||||
|
||||
|
||||
```python
|
||||
# mygame/evadventure/objects.py
|
||||
|
||||
# ...
|
||||
|
||||
class EvAdventureConsumable(EvAdventureObject):
|
||||
"""An item that can be used up"""
|
||||
|
||||
obj_type = ObjType.CONSUMABLE
|
||||
value = AttributeProperty(0.25, autocreate=False)
|
||||
uses = AttributeProperty(1, autocreate=False)
|
||||
|
||||
def at_pre_use(self, user, *args, **kwargs):
|
||||
"""Called before using. If returning False, abort use."""
|
||||
return uses > 0
|
||||
|
||||
def at_use(self, user, *args, **kwargs):
|
||||
"""Called when using the item"""
|
||||
pass
|
||||
|
||||
def at_post_use(self. user, *args, **kwargs):
|
||||
"""Called after using the item"""
|
||||
# detract a usage, deleting the item if used up.
|
||||
self.uses -= 1
|
||||
if self.uses <= 0:
|
||||
user.msg(f"{self.key} was used up.")
|
||||
self.delete()
|
||||
```
|
||||
|
||||
What exactly each consumable does will vary - we will need to implement children of this class
|
||||
later, overriding `at_use` with different effects.
|
||||
|
||||
## Weapons
|
||||
|
||||
All weapons need properties that describe how efficient they are in battle.
|
||||
|
||||
```python
|
||||
# mygame/evadventure/objects.py
|
||||
|
||||
from .enums import WieldLocation, ObjType, Ability
|
||||
|
||||
# ...
|
||||
|
||||
class EvAdventureWeapon(EvAdventureObject):
|
||||
"""Base class for all weapons"""
|
||||
|
||||
obj_type = ObjType.WEAPON
|
||||
inventory_use_slot = AttributeProperty(WieldLocation.WEAPON_HAND, autocreate=False)
|
||||
quality = AttributeProperty(3, autocreate=False)
|
||||
|
||||
attack_type = AttibuteProperty(Ability.STR, autocreate=False)
|
||||
defend_type = AttibuteProperty(Ability.ARMOR, autocreate=False)
|
||||
|
||||
damage_roll = AttibuteProperty("1d6", autocreate=False)
|
||||
```
|
||||
|
||||
The `quality` is something we need to track in _Knave_. When getting critical failures on attacks,
|
||||
a weapon's quality will go down. When it reaches 0, it will break.
|
||||
|
||||
The attack/defend type tracks how we resolve attacks with the weapon, like `roll + STR vs ARMOR + 10`.
|
||||
|
||||
## Magic
|
||||
|
||||
In _Knave_, anyone can use magic if they are wielding a rune stone (our name for spell books) in both
|
||||
hands. You can only use a rune stone once per rest. So a rune stone is an example of a 'magical weapon'
|
||||
that is also a 'consumable' of sorts.
|
||||
|
||||
|
||||
```python
|
||||
# mygame/evadventure/objects.py
|
||||
|
||||
# ...
|
||||
class EvAdventureConsumable(EvAdventureObject):
|
||||
# ...
|
||||
|
||||
class EvAdventureWeapon(EvAdventureObject):
|
||||
# ...
|
||||
|
||||
class EvAdventureRuneStone(EvAdventureWeapon, EvAdventureConsumable):
|
||||
"""Base for all magical rune stones"""
|
||||
|
||||
obj_type = (ObjType.WEAPON, ObjType.MAGIC)
|
||||
inventory_use_slot = WieldLocation.TWO_HANDS # always two hands for magic
|
||||
quality = AttributeProperty(3, autocreate=False)
|
||||
|
||||
attack_type = AttibuteProperty(Ability.INT, autocreate=False)
|
||||
defend_type = AttibuteProperty(Ability.DEX, autocreate=False)
|
||||
|
||||
damage_roll = AttibuteProperty("1d8", autocreate=False)
|
||||
|
||||
def at_post_use(self, user, *args, **kwargs):
|
||||
"""Called after usage/spell was cast"""
|
||||
self.uses -= 1
|
||||
# we don't delete the rune stone here, but
|
||||
# it must be reset on next rest.
|
||||
|
||||
def refresh(self):
|
||||
"""Refresh the rune stone (normally after rest)"""
|
||||
self.uses = 1
|
||||
```
|
||||
|
||||
We make the rune stone a mix of weapon and consumable. Note that we don't have to add `.uses`
|
||||
again, it's inherited from `EvAdventureConsumable` parent. The `at_pre_use` and `at_use` methods
|
||||
are also inherited; we only override `at_post_use` since we don't want the runestone to be deleted
|
||||
when it runs out of uses.
|
||||
|
||||
We add a little convenience method `refresh` - we should call this when the character rests, to
|
||||
make the runestone active again.
|
||||
|
||||
Exactly what rune stones _do_ will be implemented in the `at_use` methods of subclasses to this
|
||||
base class. Since magic in _Knave_ tends to be pretty custom, it makes sense that it will lead to a lot
|
||||
of custom code.
|
||||
|
||||
|
||||
## Armor
|
||||
|
||||
Armor, shields and helmets increase the `ARMOR` stat of the character. In _Knave_, what is stored is the
|
||||
defense value of the armor (values 11-20). We will instead store the 'armor bonus' (1-10). As we know,
|
||||
defending is always `bonus + 10`, so the result will be the same - this means
|
||||
we can use `Ability.ARMOR` as any other defensive ability without worrying about a special case.
|
||||
|
||||
``
|
||||
```python
|
||||
# mygame/evadventure/objects.py
|
||||
|
||||
# ...
|
||||
|
||||
class EvAdventureAmor(EvAdventureObject):
|
||||
obj_type = ObjType.ARMOR
|
||||
inventory_use_slot = WieldLocation.BODY
|
||||
|
||||
armor = AttributeProperty(1, autocreate=False)
|
||||
quality = AttributeProperty(3, autocreate=False)
|
||||
|
||||
|
||||
class EvAdventureShield(EvAdventureArmor):
|
||||
obj_type = ObjType.SHIELD
|
||||
inventory_use_slot = WieldLocation.SHIELD_HAND
|
||||
|
||||
|
||||
class EvAdventureHelmet(EvAdventureArmor):
|
||||
obj_type = ObjType.HELMET
|
||||
inventory_use_slot = WieldLocation.HEAD
|
||||
```
|
||||
|
||||
## Your Bare hands
|
||||
|
||||
This is a 'dummy' object that is not stored in the database. We will use this in the upcoming
|
||||
[Equipment tutorial lesson](./Beginner-Tutorial-Equipment.md) to represent when you have 'nothing'
|
||||
in your hands. This way we don't need to add any special case for this.
|
||||
|
||||
```python
|
||||
class WeaponEmptyHand:
|
||||
obj_type = ObjType.WEAPON
|
||||
key = "Empty Fists"
|
||||
inventory_use_slot = WieldLocation.WEAPON_HAND
|
||||
attack_type = Ability.STR
|
||||
defense_type = Ability.ARMOR
|
||||
damage_roll = "1d4"
|
||||
quality = 100000 # let's assume fists are always available ...
|
||||
|
||||
def __repr__(self):
|
||||
return "<WeaponEmptyHand>"
|
||||
```
|
||||
|
||||
## Testing and Extra credits
|
||||
|
||||
Remember the `get_obj_stats` function from the [Utility Tutorial](./Beginner-Tutorial-Utilities.md) earlier?
|
||||
We had to use dummy-values since we didn't yet know how we would store properties on Objects in the game.
|
||||
|
||||
Well, we just figured out all we need! You can go back and update `get_obj_stats` to properly read the data
|
||||
from the object it receives.
|
||||
|
||||
When you change this function you must also update the related unit test - so your existing test becomes a
|
||||
nice way to test your new Objects as well! Add more tests showing the output of feeding different object-types
|
||||
to `get_obj_stats`.
|
||||
|
||||
Try it out yourself. If you need help, a finished utility example is found in [evennia/contrib/tutorials/evadventure/utils.py](get_obj_stats).
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
# Part 3: How we get there
|
||||
|
||||
```{warning}
|
||||
The tutorial game is under development and is not yet complete, nor tested. Use the existing
|
||||
lessons as inspiration and to help get you going, but don't expect out-of-the-box perfection
|
||||
from it at this time.
|
||||
```
|
||||
|
||||
```{sidebar} Beginner Tutorial Parts
|
||||
- [Introduction](../Beginner-Tutorial-Intro.md)
|
||||
<br>Getting set up.
|
||||
- Part 1: [What we have](../Part1/Beginner-Tutorial-Part1-Intro.md)
|
||||
<br>A tour of Evennia and how to use the tools, including an introduction to Python.
|
||||
- Part 2: [What we want](../Part2/Beginner-Tutorial-Part2-Intro.md)
|
||||
<br>Planning our tutorial game and what to think about when planning your own in the future.
|
||||
- **Part 3: [How we get there](./Beginner-Tutorial-Part3-Intro.md)**
|
||||
<br>Getting down to the meat of extending Evennia to make our game
|
||||
- Part 4: [Using what we created](../Part4/Beginner-Tutorial-Part4-Intro.md)
|
||||
<br>Building a tech-demo and world content to go with our code
|
||||
- Part 5: [Showing the world](../Part5/Beginner-Tutorial-Part5-Intro.md)
|
||||
<br>Taking our new game online and let players try it out
|
||||
```
|
||||
|
||||
In part three of the Evennia Beginner tutorial we will go through the actual creation of
|
||||
our tutorial game _EvAdventure_, based on the [Knave](https://www.drivethrurpg.com/product/250888/Knave)
|
||||
RPG ruleset.
|
||||
|
||||
This is a big part. You'll be seeing a lot of code and there are plenty of lessons to go through.
|
||||
Take your time!
|
||||
|
||||
If you followed the previous parts of this tutorial you will have some notions about Python and where to
|
||||
find and make use of things in Evennia. We also have a good idea of the type of game we will
|
||||
create.
|
||||
|
||||
Even if this is not the game-style you are interested in, following along will give you a lot
|
||||
of experience using Evennia and be really helpful for doing your own thing later!
|
||||
|
||||
Fully coded examples of all code we make in this part can be found in the
|
||||
[evennia/contrib/tutorials/evadventure](../../../api/evennia.contrib.tutorials.evadventure.md) package.
|
||||
|
||||
## Lessons
|
||||
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 1
|
||||
|
||||
Beginner-Tutorial-Utilities
|
||||
Beginner-Tutorial-Rules
|
||||
Beginner-Tutorial-Characters
|
||||
Beginner-Tutorial-Objects
|
||||
Beginner-Tutorial-Equipment
|
||||
Beginner-Tutorial-Chargen
|
||||
Beginner-Tutorial-Rooms
|
||||
Beginner-Tutorial-NPCs
|
||||
Beginner-Tutorial-Turnbased-Combat
|
||||
Beginner-Tutorial-Quests
|
||||
Beginner-Tutorial-Shops
|
||||
Beginner-Tutorial-Dungeon
|
||||
Beginner-Tutorial-Commands
|
||||
```
|
||||
|
||||
## Table of Contents
|
||||
|
||||
```{toctree}
|
||||
|
||||
Beginner-Tutorial-Utilities
|
||||
Beginner-Tutorial-Rules
|
||||
Beginner-Tutorial-Characters
|
||||
Beginner-Tutorial-Objects
|
||||
Beginner-Tutorial-Equipment
|
||||
Beginner-Tutorial-Chargen
|
||||
Beginner-Tutorial-Rooms
|
||||
Beginner-Tutorial-NPCs
|
||||
Beginner-Tutorial-Turnbased-Combat
|
||||
Beginner-Tutorial-Quests
|
||||
Beginner-Tutorial-Shops
|
||||
Beginner-Tutorial-Dungeon
|
||||
Beginner-Tutorial-Commands
|
||||
```
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
# Game Quests
|
||||
|
||||
```{warning}
|
||||
This part of the Beginner tutorial is still being developed.
|
||||
```
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
# In-game Rooms
|
||||
|
||||
```{warning}
|
||||
This part of the Beginner tutorial is still being developed.
|
||||
```
|
||||
|
|
@ -1,633 +0,0 @@
|
|||
# Rules and dice rolling
|
||||
|
||||
In _EvAdventure_ we have decided to use the [Knave](https://www.drivethrurpg.com/product/250888/Knave)
|
||||
RPG ruleset. This is commercial, but released under Creative Commons 4.0, meaning it's okay to share and
|
||||
adapt _Knave_ for any purpose, even commercially. If you don't want to buy it but still follow
|
||||
along, you can find a [free fan-version here](http://abominablefancy.blogspot.com/2018/10/knaves-fancypants.html).
|
||||
|
||||
## Summary of _Knave_ rules
|
||||
|
||||
Knave, being inspired by early Dungeons & Dragons, is very simple.
|
||||
|
||||
- It uses six Ability bonuses
|
||||
_Strength_ (STR), _Dexterity_ (DEX), _Constitution_ (CON), _Intelligence_ (INT), _Wisdom_ (WIS)
|
||||
and _Charisma_ (CHA). These are rated from `+1` to `+10`.
|
||||
- Rolls are made with a twenty-sided die (`1d20`), usually adding a suitable Ability bonus to the roll.
|
||||
- If you roll _with advantage_, you roll `2d20` and pick the
|
||||
_highest_ value, If you roll _with disadvantage_, you roll `2d20` and pick the _lowest_.
|
||||
- Rolling a natural `1` is a _critical failure_. A natural `20` is a _critical success_. Rolling such
|
||||
in combat means your weapon or armor loses quality, which will eventually destroy it.
|
||||
- A _saving throw_ (trying to succeed against the environment) means making a roll to beat `15` (always).
|
||||
So if you are lifting a heavy stone and have `STR +2`, you'd roll `1d20 + 2` and hope the result
|
||||
is higher than `15`.
|
||||
- An _opposed saving throw_ means beating the enemy's suitable Ability 'defense', which is always their
|
||||
`Ability bonus + 10`. So if you have `STR +1` and are arm wrestling someone with `STR +2`, you roll
|
||||
`1d20 + 1` and hope to roll higher than `2 + 10 = 12`.
|
||||
- A special bonus is `Armor`, `+1` is unarmored, additional armor is given by equipment. Melee attacks
|
||||
test `STR` versus the `Armor` defense value while ranged attacks uses `WIS` vs `Armor`.
|
||||
- _Knave_ has no skills or classes. Everyone can use all items and using magic means having a special
|
||||
'rune stone' in your hands; one spell per stone and day.
|
||||
- A character has `CON + 10` carry 'slots'. Most normal items uses one slot, armor and large weapons uses
|
||||
two or three.
|
||||
- Healing is random, `1d8 + CON` health healed after food and sleep.
|
||||
- Monster difficulty is listed by hy many 1d8 HP they have; this is called their "hit die" or HD. If
|
||||
needing to test Abilities, monsters have HD bonus in every Ability.
|
||||
- Monsters have a _morale rating_. When things go bad, they have a chance to panic and flee if
|
||||
rolling `2d6` over their morale rating.
|
||||
- All Characters in _Knave_ are mostly randomly generated. HP is `<level>d8` but we give every
|
||||
new character max HP to start.
|
||||
- _Knave_ also have random tables, such as for starting equipment and to see if dying when
|
||||
hitting 0. Death, if it happens, is permanent.
|
||||
|
||||
|
||||
## Making a rule module
|
||||
|
||||
> Create a new module mygame/evadventure/rules.py
|
||||
|
||||
```{sidebar}
|
||||
A complete version of the rule module is found in
|
||||
[evennia/contrib/tutorials/evadventure/rules.py](../../../api/evennia.contrib.tutorials.evadventure.rules.md).
|
||||
```
|
||||
There are three broad sets of rules for most RPGS:
|
||||
|
||||
- Character generation rules, often only used during character creation
|
||||
- Regular gameplay rules - rolling dice and resolving game situations
|
||||
- Character improvement - getting and spending experience to improve the character
|
||||
|
||||
We want our `rules` module to cover as many aspeects of what we'd otherwise would have to look up
|
||||
in a rulebook.
|
||||
|
||||
|
||||
## Rolling dice
|
||||
|
||||
We will start by making a dice roller. Let's group all of our dice rolling into a structure like this
|
||||
(not functional code yet):
|
||||
|
||||
```python
|
||||
class EvAdventureRollEngine:
|
||||
|
||||
def roll(...):
|
||||
# get result of one generic roll, for any type and number of dice
|
||||
|
||||
def roll_with_advantage_or_disadvantage(...)
|
||||
# get result of normal d20 roll, with advantage/disadvantage (or not)
|
||||
|
||||
def saving_throw(...):
|
||||
# do a saving throw against a specific target number
|
||||
|
||||
def opposed_saving_throw(...):
|
||||
# do an opposed saving throw against a target's defense
|
||||
|
||||
def roll_random_table(...):
|
||||
# make a roll against a random table (loaded elsewere)
|
||||
|
||||
def morale_check(...):
|
||||
# roll a 2d6 morale check for a target
|
||||
|
||||
def heal_from_rest(...):
|
||||
# heal 1d8 when resting+eating, but not more than max value.
|
||||
|
||||
def roll_death(...):
|
||||
# roll to determine penalty when hitting 0 HP.
|
||||
|
||||
|
||||
dice = EvAdventureRollEngine()
|
||||
|
||||
```
|
||||
```{sidebar}
|
||||
This groups all dice-related code into one 'container' that is easy to import. But it's mostly a matter
|
||||
of taste. You _could_ also break up the class' methods into normal functions at the top-level of the
|
||||
module if you wanted.
|
||||
```
|
||||
|
||||
This structure (called a _singleton_) means we group all dice rolls into one class that we then initiate
|
||||
into a variable `dice` at the end of the module. This means that we can do the following from other
|
||||
modules:
|
||||
|
||||
```python
|
||||
from .rules import dice
|
||||
|
||||
dice.roll("1d8")
|
||||
```
|
||||
|
||||
### Generic dice roller
|
||||
|
||||
We want to be able to do `roll("1d20")` and get a random result back from the roll.
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/rules.py
|
||||
|
||||
from random import randint
|
||||
|
||||
class EvAdventureRollEngine:
|
||||
|
||||
def roll(self, roll_string):
|
||||
"""
|
||||
Roll XdY dice, where X is the number of dice
|
||||
and Y the number of sides per die.
|
||||
|
||||
Args:
|
||||
roll_string (str): A dice string on the form XdY.
|
||||
Returns:
|
||||
int: The result of the roll.
|
||||
|
||||
"""
|
||||
|
||||
# split the XdY input on the 'd' one time
|
||||
number, diesize = roll_string.split("d", 1)
|
||||
|
||||
# convert from string to integers
|
||||
number = int(number)
|
||||
diesize = int(diesize)
|
||||
|
||||
# make the roll
|
||||
return sum(randint(1, diesize) for _ in range(number))
|
||||
```
|
||||
|
||||
```{sidebar}
|
||||
For this tutorial we have opted to not use any contribs, so we create
|
||||
our own dice roller. But normally you could instead use the [dice](../../../Contribs/Contrib-Dice.md) contrib for this.
|
||||
We'll point out possible helpful contribs in sidebars as we proceed.
|
||||
```
|
||||
|
||||
The `randint` standard Python library module produces a random integer
|
||||
in a specific range. The line
|
||||
|
||||
```python
|
||||
sum(randint(1, diesize) for _ in range(number))
|
||||
```
|
||||
works like this:
|
||||
|
||||
- For a certain `number` of times ...
|
||||
- ... create a random integer between `1` and `diesize` ...
|
||||
- ... and `sum` all those integers together.
|
||||
|
||||
You could write the same thing less compactly like this:
|
||||
|
||||
```python
|
||||
rolls = []
|
||||
for _ in range(number):
|
||||
random_result = randint(1, diesize)
|
||||
rolls.append(random_result)
|
||||
return sum(rolls)
|
||||
```
|
||||
|
||||
```{sidebar}
|
||||
Note that `range` generates a value `0...number-1`. We use `_` in the `for` loop to
|
||||
indicate we don't really care what this value is - we just want to repeat the loop
|
||||
a certain amount of times.
|
||||
```
|
||||
|
||||
We don't ever expect end users to call this method; if we did, we would have to validate the inputs
|
||||
much more - We would have to make sure that `number` or `diesize` are valid inputs and not
|
||||
crazy big so the loop takes forever!
|
||||
|
||||
### Rolling with advantage
|
||||
|
||||
Now that we have the generic roller, we can start using it to do a more complex roll.
|
||||
|
||||
```
|
||||
# in mygame/evadventure/rules.py
|
||||
|
||||
# ...
|
||||
|
||||
class EvAdventureRollEngine:
|
||||
|
||||
def roll(roll_string):
|
||||
# ...
|
||||
|
||||
def roll_with_advantage_or_disadvantage(self, advantage=False, disadvantage=False):
|
||||
|
||||
if not (advantage or disadvantage) or (advantage and disadvantage):
|
||||
# normal roll - advantage/disadvantage not set or they cancel
|
||||
# each other out
|
||||
return self.roll("1d20")
|
||||
elif advantage:
|
||||
# highest of two d20 rolls
|
||||
return max(self.roll("1d20"), self.roll("1d20"))
|
||||
else:
|
||||
# disadvantage - lowest of two d20 rolls
|
||||
return min(self.roll("1d20"), self.roll("1d20"))
|
||||
```
|
||||
|
||||
The `min()` and `max()` functions are standard Python fare for getting the biggest/smallest
|
||||
of two arguments.
|
||||
|
||||
### Saving throws
|
||||
|
||||
We want the saving throw to itself figure out if it succeeded or not. This means it needs to know
|
||||
the Ability bonus (like STR `+1`). It would be convenient if we could just pass the entity
|
||||
doing the saving throw to this method, tell it what type of save was needed, and then
|
||||
have it figure things out:
|
||||
|
||||
```python
|
||||
result, quality = dice.saving_throw(character, Ability.STR)
|
||||
```
|
||||
The return will be a boolean `True/False` if they pass, as well as a `quality` that tells us if
|
||||
a perfect fail/success was rolled or not.
|
||||
|
||||
To make the saving throw method this clever, we need to think some more about how we want to store our
|
||||
data on the character.
|
||||
|
||||
For our purposes it sounds reasonable that we will be using [Attributes](../../../Components/Attributes.md) for storing
|
||||
the Ability scores. To make it easy, we will name them the same as the
|
||||
[Enum values](./Beginner-Tutorial-Utilities.md#enums) we set up in the previous lesson. So if we have
|
||||
an enum `STR = "strength"`, we want to store the Ability on the character as an Attribute `strength`.
|
||||
|
||||
From the Attribute documentation, we can see that we can use `AttributeProperty` to make it so the
|
||||
Attribute is available as `character.strength`, and this is what we will do.
|
||||
|
||||
So, in short, we'll create the saving throws method with the assumption that we will be able to do
|
||||
`character.strength`, `character.constitution`, `character.charisma` etc to get the relevant Abilities.
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/rules.py
|
||||
# ...
|
||||
from .enums import Ability
|
||||
|
||||
class EvAdventureRollEngine:
|
||||
|
||||
def roll(...)
|
||||
# ...
|
||||
|
||||
def roll_with_advantage_or_disadvantage(...)
|
||||
# ...
|
||||
|
||||
def saving_throw(self, character, bonus_type=Ability.STR, target=15,
|
||||
advantage=False, disadvantage=False):
|
||||
"""
|
||||
Do a saving throw, trying to beat a target.
|
||||
|
||||
Args:
|
||||
character (Character): A character (assumed to have Ability bonuses
|
||||
stored on itself as Attributes).
|
||||
bonus_type (Ability): A valid Ability bonus enum.
|
||||
target (int): The target number to beat. Always 15 in Knave.
|
||||
advantage (bool): If character has advantage on this roll.
|
||||
disadvantage (bool): If character has disadvantage on this roll.
|
||||
|
||||
Returns:
|
||||
tuple: A tuple (bool, Ability), showing if the throw succeeded and
|
||||
the quality is one of None or Ability.CRITICAL_FAILURE/SUCCESS
|
||||
|
||||
"""
|
||||
|
||||
# make a roll
|
||||
dice_roll = self.roll_with_advantage_or_disadvantage(advantage, disadvantage)
|
||||
|
||||
# figure out if we had critical failure/success
|
||||
quality = None
|
||||
if dice_roll == 1:
|
||||
quality = Ability.CRITICAL_FAILURE
|
||||
elif dice_roll == 20:
|
||||
quality = Ability.CRITICAL_SUCCESS
|
||||
|
||||
# figure out bonus
|
||||
bonus = getattr(character, bonus_type.value, 1)
|
||||
|
||||
# return a tuple (bool, quality)
|
||||
return (dice_roll + bonus) > target, quality
|
||||
```
|
||||
|
||||
The `getattr(obj, attrname, default)` function is a very useful Python tool for getting an attribute
|
||||
off an object and getting a default value if the attribute is not defined.
|
||||
|
||||
### Opposed saving throw
|
||||
|
||||
With the building pieces we already created, this method is simple. Remember that the defense you have
|
||||
to beat is always the relevant bonus + 10 in _Knave_. So if the enemy defends with `STR +3`, you must
|
||||
roll higher than `13`.
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/rules.py
|
||||
|
||||
from .enums import Ability
|
||||
|
||||
class EvAdventureRollEngine:
|
||||
|
||||
def roll(...):
|
||||
# ...
|
||||
|
||||
def roll_with_advantage_or_disadvantage(...):
|
||||
# ...
|
||||
|
||||
def saving_throw(...):
|
||||
# ...
|
||||
|
||||
def opposed_saving_throw(self, attacker, defender,
|
||||
attack_type=Ability.STR, defense_type=Ability.ARMOR,
|
||||
advantage=False, disadvantage=False):
|
||||
defender_defense = getattr(defender, defense_type.value, 1) + 10
|
||||
result, quality = self.saving_throw(attacker, bonus_type=attack_type,
|
||||
target=defender_defense,
|
||||
advantage=advantave, disadvantage=disadvantage)
|
||||
|
||||
return result, quality
|
||||
```
|
||||
|
||||
### Morale check
|
||||
|
||||
We will make the assumption that the `morale` value is available from the creature simply as
|
||||
`monster.morale` - we need to remember to make this so later!
|
||||
|
||||
In _Knave_, a creature have roll with `2d6` equal or under its morale to not flee or surrender
|
||||
when things go south. The standard morale value is 9.
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/rules.py
|
||||
|
||||
class EvAdventureRollEngine:
|
||||
|
||||
# ...
|
||||
|
||||
def morale_check(self, defender):
|
||||
return self.roll("2d6") <= getattr(defender, "morale", 9)
|
||||
|
||||
```
|
||||
|
||||
### Roll for Healing
|
||||
|
||||
To be able to handle healing, we need to make some more assumptions about how we store
|
||||
health on game entities. We will need `hp_max` (the total amount of available HP) and `hp`
|
||||
(the current health value). We again assume these will be available as `obj.hp` and `obj.hp_max`.
|
||||
|
||||
According to the rules, after consuming a ration and having a full night's sleep, a character regains
|
||||
`1d8 + CON` HP.
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/rules.py
|
||||
|
||||
from .enums import Ability
|
||||
|
||||
class EvAdventureRollEngine:
|
||||
|
||||
# ...
|
||||
|
||||
def heal_from_rest(self, character):
|
||||
"""
|
||||
A night's rest retains 1d8 + CON HP
|
||||
|
||||
"""
|
||||
con_bonus = getattr(character, Ability.CON.value, 1)
|
||||
character.heal(self.roll("1d8") + con_bonus)
|
||||
```
|
||||
|
||||
We make another assumption here - that `character.heal()` is a thing. We tell this function how
|
||||
much the character should heal, and it will do so, making sure to not heal more than its max
|
||||
number of HPs
|
||||
|
||||
> Knowing what is available on the character and what rule rolls we need is a bit of a chicken-and-egg
|
||||
> problem. We will make sure to implement the matching _Character_ class next lesson.
|
||||
|
||||
|
||||
### Rolling on a table
|
||||
|
||||
We occasionally need to roll on a 'table' - a selection of choices. There are two main table-types
|
||||
we need to support:
|
||||
|
||||
Simply one element per row of the table (same odds to get each result).
|
||||
|
||||
| Result |
|
||||
|:------:|
|
||||
| item1 |
|
||||
| item2 |
|
||||
| item3 |
|
||||
| item4 |
|
||||
|
||||
This we will simply represent as a plain list
|
||||
|
||||
```python
|
||||
["item1", "item2", "item3", "item4"]
|
||||
```
|
||||
|
||||
Ranges per item (varying odds per result):
|
||||
|
||||
| Range | Result |
|
||||
|:-----:|:------:|
|
||||
| 1-5 | item1 |
|
||||
| 6-15 | item2 |
|
||||
| 16-19 | item3 |
|
||||
| 20 | item4 |
|
||||
|
||||
This we will represent as a list of tuples:
|
||||
|
||||
```python
|
||||
[("1-5", "item1"), ("6-15", "item2"), ("16-19", "item4"), ("20", "item5")]
|
||||
```
|
||||
|
||||
We also need to know what die to roll to get a result on the table (it may not always
|
||||
be obvious, and in some games you could be asked to roll a lower dice to only get
|
||||
early table results, for example).
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/rules.py
|
||||
|
||||
from random import randint, choice
|
||||
|
||||
class EvAdventureRollEngine:
|
||||
|
||||
# ...
|
||||
|
||||
def roll_random_table(self, dieroll, table_choices):
|
||||
"""
|
||||
Args:
|
||||
dieroll (str): A die roll string, like "1d20".
|
||||
table_choices (iterable): A list of either single elements or
|
||||
of tuples.
|
||||
Returns:
|
||||
Any: A random result from the given list of choices.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If rolling dice giving results outside the table.
|
||||
|
||||
"""
|
||||
roll_result = self.roll(dieroll)
|
||||
|
||||
if isinstance(table_choices[0], (tuple, list)):
|
||||
# the first element is a tuple/list; treat as on the form [("1-5", "item"),...]
|
||||
for (valrange, choice) in table_choices:
|
||||
minval, *maxval = valrange.split("-", 1)
|
||||
minval = abs(int(minval))
|
||||
maxval = abs(int(maxval[0]) if maxval else minval)
|
||||
|
||||
if minval <= roll_result <= maxval:
|
||||
return choice
|
||||
|
||||
# if we get here we must have set a dieroll producing a value
|
||||
# outside of the table boundaries - raise error
|
||||
raise RuntimeError("roll_random_table: Invalid die roll")
|
||||
else:
|
||||
# a simple regular list
|
||||
roll_result = max(1, min(len(table_choices), roll_result))
|
||||
return table_choices[roll_result - 1]
|
||||
```
|
||||
Check that you understand what this does.
|
||||
|
||||
This may be confusing:
|
||||
```python
|
||||
minval, *maxval = valrange.split("-", 1)
|
||||
minval = abs(int(minval))
|
||||
maxval = abs(int(maxval[0]) if maxval else minval)
|
||||
```
|
||||
|
||||
If `valrange` is the string `1-5`, then `valrange.split("-", 1)` would result in a tuple `("1", "5")`.
|
||||
But if the string was in fact just `"20"` (possible for a single entry in an RPG table), this would
|
||||
lead to an error since it would only split out a single element - and we expected two.
|
||||
|
||||
By using `*maxval` (with the `*`), `maxval` is told to expect _0 or more_ elements in a tuple.
|
||||
So the result for `1-5` will be `("1", ("5",))` and for `20` it will become `("20", ())`. In the line
|
||||
|
||||
```python
|
||||
maxval = abs(int(maxval[0]) if maxval else minval)
|
||||
```
|
||||
|
||||
we check if `maxval` actually has a value `("5",)` or if its empty `()`. The result is either
|
||||
`"5"` or the value of `minval`.
|
||||
|
||||
|
||||
### Roll for death
|
||||
|
||||
While original Knave suggests hitting 0 HP means insta-death, we will grab the optional "death table"
|
||||
from the "prettified" Knave's optional rules to make it a little less punishing. We also changed the
|
||||
result of `2` to 'dead' since we don't simulate 'dismemberment' in this tutorial:
|
||||
|
||||
| Roll | Result | -1d4 Loss of Ability |
|
||||
|:---: |:--------:|:--------------------:|
|
||||
| 1-2 | dead | -
|
||||
| 3 | weakened | STR |
|
||||
|4 | unsteady | DEX |
|
||||
| 5 | sickly | CON |
|
||||
| 6 | addled | INT |
|
||||
| 7 | rattled | WIS |
|
||||
| 8 | disfigured | CHA |
|
||||
|
||||
All the non-dead values map to a loss of 1d4 in one of the six Abilities (but you get HP back).
|
||||
We need to map back to this from the above table. One also cannot have less than -10 Ability bonus,
|
||||
if you do, you die too.
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/rules.py
|
||||
|
||||
death_table = (
|
||||
("1-2", "dead"),
|
||||
("3": "strength",
|
||||
("4": "dexterity"),
|
||||
("5": "constitution"),
|
||||
("6": "intelligence"),
|
||||
("7": "wisdom"),
|
||||
("8": "charisma"),
|
||||
)
|
||||
|
||||
|
||||
class EvAdventureRollEngine:
|
||||
|
||||
# ...
|
||||
|
||||
def roll_random_table(...)
|
||||
# ...
|
||||
|
||||
def roll_death(self, character):
|
||||
ability_name = self.roll_random_table("1d8", death_table)
|
||||
|
||||
if ability_name == "dead":
|
||||
# TODO - kill the character!
|
||||
pass
|
||||
else:
|
||||
loss = self.roll("1d4")
|
||||
|
||||
current_ability = getattr(character, ability_name)
|
||||
current_ability -= loss
|
||||
|
||||
if current_ability < -10:
|
||||
# TODO - kill the character!
|
||||
pass
|
||||
else:
|
||||
# refresh 1d4 health, but suffer 1d4 ability loss
|
||||
self.heal(character, self.roll("1d4")
|
||||
setattr(character, ability_name, current_ability)
|
||||
|
||||
character.msg(
|
||||
"You survive your brush with death, and while you recover "
|
||||
f"some health, you permanently lose {loss} {ability_name} instead."
|
||||
)
|
||||
|
||||
dice = EvAdventureRollEngine()
|
||||
```
|
||||
|
||||
Here we roll on the 'death table' from the rules to see what happens. We give the character
|
||||
a message if they survive, to let them know what happened.
|
||||
|
||||
We don't yet know what 'killing the character' technically means, so we mark this as `TODO` and
|
||||
return to it in a later lesson. We just know that we need to do _something_ here to kill off the
|
||||
character!
|
||||
|
||||
## Testing
|
||||
|
||||
> Make a new module `mygame/evadventure/tests/test_rules.py`
|
||||
|
||||
Testing the `rules` module will also showcase some very useful tools when testing.
|
||||
|
||||
```python
|
||||
# mygame/evadventure/tests/test_rules.py
|
||||
|
||||
from unittest.mock import patch
|
||||
from evennia.utils.test_resources import BaseEvenniaTest
|
||||
from .. import rules
|
||||
|
||||
class TestEvAdventureRuleEngine(BaseEvenniaTest):
|
||||
|
||||
def setUp(self):
|
||||
"""Called before every test method"""
|
||||
super().setUp()
|
||||
self.roll_engine = rules.EvAdventureRollEngine()
|
||||
|
||||
@patch("evadventure.rules.randint")
|
||||
def test_roll(self, mock_randint):
|
||||
mock_randint.return_value = 4
|
||||
self.assertEqual(self.roll_engine.roll("1d6", 4)
|
||||
self.assertEqual(self.roll_engine.roll("2d6", 2 * 4)
|
||||
|
||||
# test of the other rule methods below ...
|
||||
```
|
||||
|
||||
As before, run the specific test with
|
||||
|
||||
evennia test --settings settings.py .evadventure.tests.test_rules
|
||||
|
||||
### Mocking and patching
|
||||
|
||||
```{sidebar}
|
||||
In [evennia/contrib/tutorials/evadventure/tests/test_rules.py](../../../api/evennia.contrib.tutorials.evadventure.tests.test_rules.md)
|
||||
has a complete example of rule testing.
|
||||
```
|
||||
The `setUp` method is a special method of the testing class. It will be run before every
|
||||
test method. We use `super().setUp()` to make sure the parent class' version of this method
|
||||
always fire. Then we create a fresh `EvAdventureRollEngine` we can test with.
|
||||
|
||||
In our test, we import `patch` from the `unittest.mock` library. This is a very useful tool for testing.
|
||||
Normally the `randint` function we imported in `rules` will return a random value. That's very hard to
|
||||
test for, since the value will be different every test.
|
||||
|
||||
With `@patch` (this is called a _decorator_), we temporarily replace `rules.randint` with a 'mock' - a
|
||||
dummy entity. This mock is passed into the testing method. We then take this `mock_randint` and set
|
||||
`.return_value = 4` on it.
|
||||
|
||||
Adding `return_value` to the mock means that every time this mock is called, it will return 4. For the
|
||||
duration of the test we can now check with `self.assertEqual` that our `roll` method always returns a
|
||||
result as-if the random result was 4.
|
||||
|
||||
There are [many resources for understanding mock](https://realpython.com/python-mock-library/), refer to
|
||||
them for further help.
|
||||
|
||||
> The `EvAdventureRollEngine` have many methods to test. We leave this as an extra exercise!
|
||||
|
||||
## Summary
|
||||
|
||||
This concludes all the core rule mechanics of _Knave_ - the rules used during play. We noticed here
|
||||
that we are going to soon need to establish how our _Character_ actually stores data. So we will
|
||||
address that next.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
# In-game Shops
|
||||
|
||||
```{warning}
|
||||
This part of the Beginner tutorial is still being developed.
|
||||
```
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
# Turn-based combat
|
||||
|
||||
```{warning}
|
||||
This part of the Beginner tutorial is still being developed.
|
||||
```
|
||||
|
|
@ -1,322 +0,0 @@
|
|||
# Code structure and Utilities
|
||||
|
||||
In this lesson we will set up the file structure for _EvAdventure_. We will make some
|
||||
utilities that will be useful later. We will also learn how to write _tests_.
|
||||
|
||||
## Folder structure
|
||||
|
||||
Create a new folder under your `mygame` folder, named `evadventure`. Inside it, create
|
||||
another folder `tests/` and make sure to put empty `__init__.py` files in both. This turns both
|
||||
folders into packages Python understands to import from.
|
||||
|
||||
```
|
||||
mygame/
|
||||
commands/
|
||||
evadventure/ <---
|
||||
__init__.py <---
|
||||
tests/ <---
|
||||
__init__.py <---
|
||||
__init__.py
|
||||
README.md
|
||||
server/
|
||||
typeclasses/
|
||||
web/
|
||||
world/
|
||||
|
||||
```
|
||||
|
||||
Importing anything from inside this folder from anywhere else under `mygame` will be done by
|
||||
|
||||
```python
|
||||
# from anywhere in mygame/
|
||||
from evadventure.yourmodulename import whatever
|
||||
```
|
||||
|
||||
This is the 'absolute path` type of import.
|
||||
|
||||
Between two modules both in `evadventure/`, you can use a 'relative' import with `.`:
|
||||
|
||||
```python
|
||||
# from a module inside mygame/evadventure
|
||||
from .yourmodulename import whatever
|
||||
```
|
||||
|
||||
From e.g. inside `mygame/evadventure/tests/` you can import from one level above using `..`:
|
||||
|
||||
```python
|
||||
# from mygame/evadventure/tests/
|
||||
from ..yourmodulename import whatever
|
||||
```
|
||||
|
||||
|
||||
## Enums
|
||||
|
||||
```{sidebar}
|
||||
A full example of the enum module is found in
|
||||
[evennia/contrib/tutorials/evadventure/enums.py](../../../api/evennia.contrib.tutorials.evadventure.enums.md).
|
||||
```
|
||||
Create a new file `mygame/evadventure/enums.py`.
|
||||
|
||||
An [enum](https://docs.python.org/3/library/enum.html) (enumeration) is a way to establish constants
|
||||
in Python. Best is to show an example:
|
||||
|
||||
```python
|
||||
# in a file mygame/evadventure/enums.py
|
||||
|
||||
from enum import Enum
|
||||
|
||||
class Ability(Enum):
|
||||
|
||||
STR = "strength"
|
||||
|
||||
```
|
||||
|
||||
You access an enum like this:
|
||||
|
||||
```
|
||||
# from another module in mygame/evadventure
|
||||
|
||||
from .enums import Ability
|
||||
|
||||
Ability.STR # the enum itself
|
||||
Ability.STR.value # this is the string "strength"
|
||||
|
||||
```
|
||||
|
||||
Having enums is recommended practice. With them set up, it means we can make sure to refer to the
|
||||
same thing every time. Having all enums in one place also means you have a good overview of the
|
||||
constants you are dealing with.
|
||||
|
||||
The alternative would be to for example pass around a string `"constitution"`. If you mis-spell
|
||||
this (`"consitution"`), you would not necessarily know it right away - the error would happen later
|
||||
when the string is not recognized. If you make a typo getting `Ability.COM` instead of `Ability.CON`,
|
||||
Python will immediately raise an error since this enum is not recognized.
|
||||
|
||||
With enums you can also do nice direct comparisons like `if ability is Ability.WIS: <do stuff>`.
|
||||
|
||||
Note that the `Ability.STR` enum does not have the actual _value_ of e.g. your Strength.
|
||||
It's just a fixed label for the Strength ability.
|
||||
|
||||
Here is the `enum.py` module needed for _Knave_. It covers the basic aspects of
|
||||
rule systems we need to track (check out the _Knave_ rules. If you use another rule system you'll
|
||||
likely gradually expand on your enums as you figure out what you'll need).
|
||||
|
||||
```python
|
||||
# mygame/evadventure/enums.py
|
||||
|
||||
class Ability(Enum):
|
||||
"""
|
||||
The six base ability-bonuses and other
|
||||
abilities
|
||||
|
||||
"""
|
||||
|
||||
STR = "strength"
|
||||
DEX = "dexterity"
|
||||
CON = "constitution"
|
||||
INT = "intelligence"
|
||||
WIS = "wisdom"
|
||||
CHA = "charisma"
|
||||
|
||||
ARMOR = "armor"
|
||||
|
||||
CRITICAL_FAILURE = "critical_failure"
|
||||
CRITICAL_SUCCESS = "critical_success"
|
||||
|
||||
ALLEGIANCE_HOSTILE = "hostile"
|
||||
ALLEGIANCE_NEUTRAL = "neutral"
|
||||
ALLEGIANCE_FRIENDLY = "friendly"
|
||||
|
||||
|
||||
```
|
||||
|
||||
Here the `Ability` class holds basic properties of a character sheet.
|
||||
|
||||
|
||||
## Utility module
|
||||
|
||||
> Create a new module `mygame/evadventure/utils.py`
|
||||
|
||||
```{sidebar}
|
||||
An example of the utility module is found in
|
||||
[evennia/contrib/tutorials/evadventure/utils.py](../../../api/evennia.contrib.tutorials.evadventure.utils.md)
|
||||
```
|
||||
|
||||
This is for general functions we may need from all over. In this case we only picture one utility,
|
||||
a function that produces a pretty display of any object we pass to it.
|
||||
|
||||
This is an example of the string we want to see:
|
||||
|
||||
```
|
||||
Chipped Sword
|
||||
Value: ~10 coins [wielded in Weapon hand]
|
||||
|
||||
A simple sword used by mercenaries all over
|
||||
the world.
|
||||
|
||||
Slots: 1, Used from: weapon hand
|
||||
Quality: 3, Uses: None
|
||||
Attacks using strength against armor.
|
||||
Damage roll: 1d6
|
||||
```
|
||||
|
||||
Here's the start of how the function could look:
|
||||
|
||||
```python
|
||||
# in mygame/evadventure/utils.py
|
||||
|
||||
_OBJ_STATS = """
|
||||
|c{key}|n
|
||||
Value: ~|y{value}|n coins{carried}
|
||||
|
||||
{desc}
|
||||
|
||||
Slots: |w{size}|n, Used from: |w{use_slot_name}|n
|
||||
Quality: |w{quality}|n, Uses: |wuses|n
|
||||
Attacks using |w{attack_type_name}|n against |w{defense_type_name}|n
|
||||
Damage roll: |w{damage_roll}|n
|
||||
""".strip()
|
||||
|
||||
|
||||
def get_obj_stats(obj, owner=None):
|
||||
"""
|
||||
Get a string of stats about the object.
|
||||
|
||||
Args:
|
||||
obj (Object): The object to get stats for.
|
||||
owner (Object): The one currently owning/carrying `obj`, if any. Can be
|
||||
used to show e.g. where they are wielding it.
|
||||
Returns:
|
||||
str: A nice info string to display about the object.
|
||||
|
||||
"""
|
||||
return _OBJ_STATS.format(
|
||||
key=obj.key,
|
||||
value=10,
|
||||
carried="[Not carried]",
|
||||
desc=obj.db.desc,
|
||||
size=1,
|
||||
quality=3,
|
||||
uses="infinite"
|
||||
use_slot_name="backpack",
|
||||
attack_type_name="strength"
|
||||
defense_type_name="armor"
|
||||
damage_roll="1d6"
|
||||
)
|
||||
```
|
||||
Here we set up the string template with place holders for where every piece of info should go.
|
||||
Study this string so you understand what it does. The `|c`, `|y`, `|w` and `|n` markers are
|
||||
[Evennia color markup](../../../Concepts/Colors.md) for making the text cyan, yellow, white and neutral-color respectively.
|
||||
|
||||
We can guess some things, such that `obj.key` is the name of the object, and that `obj.db.desc` will
|
||||
hold its description (this is how it is in default Evennia).
|
||||
|
||||
But so far we have not established how to get any of the other properties like `size` or `attack_type`.
|
||||
So we just set them to dummy values. We'll need to get back to this when we have more code in place!
|
||||
|
||||
## Testing
|
||||
|
||||
```{important}
|
||||
It's useful for any game dev to know how to effectively test their code. So we'll try to include a
|
||||
*Testing* section at the end of each of the implementation lessons to follow. Writing tests for your code
|
||||
is optional but highly recommended; it can feel a little cumbersome at first, but you'll thank yourself later.
|
||||
```
|
||||
|
||||
> create a new module `mygame/evadventure/tests/test_utils.py`
|
||||
|
||||
How do you know if you made a typo in the code above? You could _manually_ test it by reloading your
|
||||
Evennia server and do the following from in-game:
|
||||
|
||||
py from evadventure.utils import get_obj_stats;print(get_obj_stats(self))
|
||||
|
||||
You should get back a nice string about yourself! If that works, great! But you'll need to remember
|
||||
doing that test when you change this code later.
|
||||
|
||||
```{sidebar}
|
||||
In [evennia/contrib/tutorials/evadventure/tests/test_utils.py](evennia.contrib.tutorials.
|
||||
evadventure.tests.test_utils)
|
||||
is an example of the testing module. To dive deeper into unit testing in Evennia, see the
|
||||
[Unit testing](../../../Coding/Unit-Testing.md) documentation.
|
||||
```
|
||||
|
||||
A _unit test_ allows you to set up automated testing of code. Once you've written your test you
|
||||
can run it over and over and make sure later changes to your code didn't break things.
|
||||
|
||||
In this particular case, we _expect_ to later have to update the test when `get_obj_stats` becomes more
|
||||
complete and returns more reasonable data.
|
||||
|
||||
Evennia comes with extensive functionality to help you test your code. Here's a module for
|
||||
testing `get_obj_stats`.
|
||||
|
||||
```python
|
||||
# mygame/evadventure/tests/test_utils.py
|
||||
|
||||
from evennia.utils import create
|
||||
from evennia.utils.test_resources import BaseEvenniaTest
|
||||
|
||||
from ..import utils
|
||||
|
||||
class TestUtils(BaseEvenniaTest):
|
||||
def test_get_obj_stats(self):
|
||||
# make a simple object to test with
|
||||
obj = create.create_object(
|
||||
key="testobj",
|
||||
attributes=(("desc", "A test object"),)
|
||||
)
|
||||
# run it through the function
|
||||
result = utils.get_obj_stats(obj)
|
||||
# check that the result is what we expected
|
||||
self.assertEqual(
|
||||
result,
|
||||
"""
|
||||
|ctestobj|n
|
||||
Value: ~|y10|n coins
|
||||
|
||||
A test object
|
||||
|
||||
Slots: |w1|n, Used from: |wbackpack|n
|
||||
Quality: |w3|n, Uses: |winfinite|n
|
||||
Attacks using |wstrength|n against |warmor|n
|
||||
Damage roll: |w1d6|n
|
||||
""".strip()
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
What happens here is that we create a new test-class `TestUtils` that inherits from `BaseEvenniaTest`.
|
||||
This inheritance is what makes this a testing class.
|
||||
|
||||
We can have any number of methods on this class. To have a method recognized as one containing
|
||||
code to test, its name _must_ start with `test_`. We have one - `test_get_obj_stats`.
|
||||
|
||||
In this method we create a dummy `obj` and gives it a `key` "testobj". Note how we add the
|
||||
`desc` [Attribute](../../../Components/Attributes.md) directly in the `create_object` call by specifying the attribute as a
|
||||
tuple `(name, value)`!
|
||||
|
||||
We then get the result of passing this dummy-object through `get_obj_stats` we imported earlier.
|
||||
|
||||
The `assertEqual` method is available on all testing classes and checks that the `result` is equal
|
||||
to the string we specify. If they are the same, the test _passes_, otherwise it _fails_ and we
|
||||
need to investigate what went wrong.
|
||||
|
||||
### Running your test
|
||||
|
||||
To run your test you need to stand inside your `mygame` folder and execute the following command:
|
||||
|
||||
evennia test --settings settings.py .evadventure.tests
|
||||
|
||||
This will run all your `evadventure` tests (if you had more of them). To only run your utility tests
|
||||
you could do
|
||||
|
||||
evennia test --settings settings.py .evadventure.tests.test_utils
|
||||
|
||||
If all goes well, you should get an `OK` back. Otherwise you need to check the failure, maybe
|
||||
your return string doesn't quite match what you expected.
|
||||
|
||||
## Summary
|
||||
|
||||
It's very important to understand how you import code between modules in Python, so if this is still
|
||||
confusing to you, it's worth to read up on this more.
|
||||
|
||||
That said, many newcomers are confused with how to begin, so by creating the folder structure, some
|
||||
small modules and even making your first unit test, you are off to a great start!
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
# Part 4: Using what we created
|
||||
|
||||
```{sidebar} Beginner Tutorial Parts
|
||||
- [Introduction](../Beginner-Tutorial-Intro.md)
|
||||
<br>Getting set up.
|
||||
- Part 1: [What we have](../Part1/Beginner-Tutorial-Part1-Intro.md)
|
||||
<br>A tour of Evennia and how to use the tools, including an introduction to Python.
|
||||
- Part 2: [What we want](../Part2/Beginner-Tutorial-Part2-Intro.md)
|
||||
<br>Planning our tutorial game and what to think about when planning your own in the future.
|
||||
- Part 3: [How we get there](../Part3/Beginner-Tutorial-Part3-Intro.md)
|
||||
<br>Getting down to the meat of extending Evennia to make our game
|
||||
- **Part 4: [Using what we created](./Beginner-Tutorial-Part4-Intro.md)**
|
||||
<br>Building a tech-demo and world content to go with our code
|
||||
- Part 5: [Showing the world](../Part5/Beginner-Tutorial-Part5-Intro.md)
|
||||
<br>Taking our new game online and let players try it out
|
||||
```
|
||||
|
||||
We now have the code underpinnings of everything we need. We have also tested the various components
|
||||
and has a simple tech-demo to show it all works together. But there is no real coherence to it at this
|
||||
point - we need to actually make a world.
|
||||
In part four we will expand our tech demo into a more full-fledged (if small) game by use of batchcommand
|
||||
and batchcode processors.
|
||||
|
||||
## Lessons
|
||||
|
||||
_TODO_
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 1
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Table of Contents
|
||||
|
||||
_TODO_
|
||||
|
||||
```{toctree}
|
||||
|
||||
```
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
# Add a simple new web page
|
||||
|
||||
|
||||
Evennia leverages [Django](https://docs.djangoproject.com) which is a web development framework.
|
||||
Huge professional websites are made in Django and there is extensive documentation (and books) on it
|
||||
. You are encouraged to at least look at the Django basic tutorials. Here we will just give a brief
|
||||
introduction for how things hang together, to get you started.
|
||||
|
||||
We assume you have installed and set up Evennia to run. A webserver and website comes out of the
|
||||
box. You can get to that by entering `http://localhost:4001` in your web browser - you should see a
|
||||
welcome page with some game statistics and a link to the web client. Let us add a new page that you
|
||||
can get to by going to `http://localhost:4001/story`.
|
||||
|
||||
## Create the view
|
||||
|
||||
A django "view" is a normal Python function that django calls to render the HTML page you will see
|
||||
in the web browser. Here we will just have it spit back the raw html, but Django can do all sorts of
|
||||
cool stuff with the page in the view, like adding dynamic content or change it on the fly. Open
|
||||
`mygame/web` folder and add a new module there named `story.py` (you could also put it in its own
|
||||
folder if you wanted to be neat. Don't forget to add an empty `__init__.py` file if you do, to tell
|
||||
Python you can import from the new folder). Here's how it looks:
|
||||
|
||||
```python
|
||||
# in mygame/web/story.py
|
||||
|
||||
from django.shortcuts import render
|
||||
|
||||
def storypage(request):
|
||||
return render(request, "story.html")
|
||||
```
|
||||
|
||||
This view takes advantage of a shortcut provided to use by Django, _render_. This shortcut gives the
|
||||
template some information from the request, for instance, the game name, and then renders it.
|
||||
|
||||
## The HTML page
|
||||
|
||||
We need to find a place where Evennia (and Django) looks for html files (called *templates* in
|
||||
Django parlance). You can specify such places in your settings (see the `TEMPLATES` variable in
|
||||
`default_settings.py` for more info), but here we'll use an existing one. Go to
|
||||
`mygame/template/overrides/website/` and create a page `story.html` there.
|
||||
|
||||
This is not a HTML tutorial, so we'll go simple:
|
||||
|
||||
```html
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1>A story about a tree</h1>
|
||||
<p>
|
||||
This is a story about a tree, a classic tale ...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
Since we've used the _render_ shortcut, Django will allow us to extend our base styles easily.
|
||||
|
||||
If you'd rather not take advantage of Evennia's base styles, you can do something like this instead:
|
||||
|
||||
```html
|
||||
<html>
|
||||
<body>
|
||||
<h1>A story about a tree</h1>
|
||||
<p>
|
||||
This is a story about a tree, a classic tale ...
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
|
||||
## The URL
|
||||
|
||||
When you enter the address `http://localhost:4001/story` in your web browser, Django will parse that
|
||||
field to figure out which page you want to go to. You tell it which patterns are relevant in the
|
||||
file
|
||||
[mygame/web/urls.py](https://github.com/evennia/evennia/blob/master/evennia/game_template/web/urls.py).
|
||||
Open it now.
|
||||
|
||||
Django looks for the variable `urlpatterns` in this file. You want to add your new pattern to the
|
||||
`custom_patterns` list we have prepared - that is then merged with the default `urlpatterns`. Here's
|
||||
how it could look:
|
||||
|
||||
```python
|
||||
from web import story
|
||||
|
||||
# ...
|
||||
|
||||
custom_patterns = [
|
||||
url(r'story', story.storypage, name='Story'),
|
||||
]
|
||||
```
|
||||
|
||||
That is, we import our story view module from where we created it earlier and then create an `url`
|
||||
instance. The first argument to `url` is the pattern of the url we want to find (`"story"`) (this is
|
||||
a regular expression if you are familiar with those) and then our view function we want to direct
|
||||
to.
|
||||
|
||||
That should be it. Reload Evennia and you should be able to browse to your new story page!
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
# Part 5: Showing the world
|
||||
|
||||
|
||||
```{sidebar} Beginner Tutorial Parts
|
||||
- [Introduction](../Beginner-Tutorial-Intro.md)
|
||||
<br>Getting set up.
|
||||
- Part 1: [What we have](../Part1/Beginner-Tutorial-Part1-Intro.md)
|
||||
<br>A tour of Evennia and how to use the tools, including an introduction to Python.
|
||||
- Part 2: [What we want](../Part2/Beginner-Tutorial-Part2-Intro.md)
|
||||
<br>Planning our tutorial game and what to think about when planning your own in the future.
|
||||
- Part 3: [How we get there](../Part3/Beginner-Tutorial-Part3-Intro.md)
|
||||
<br>Getting down to the meat of extending Evennia to make our game
|
||||
- Part 4: [Using what we created](../Part4/Beginner-Tutorial-Part4-Intro.md)
|
||||
<br>Building a tech-demo and world content to go with our code
|
||||
- **Part 5: [Showing the world](./Beginner-Tutorial-Part5-Intro.md)**
|
||||
<br>Taking our new game online and let players try it out
|
||||
```
|
||||
You have a working game! In part five we will look at the web-components of Evennia and how to modify them
|
||||
to fit your game. We will also look at hosting your game and if you feel up to it we'll also go through how
|
||||
to bring your game online so you can invite your first players.
|
||||
|
||||
## Lessons
|
||||
|
||||
_TODO_
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 1
|
||||
|
||||
Add-a-simple-new-web-page.md
|
||||
Web-Tutorial.md
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Table of Contents
|
||||
|
||||
_TODO_
|
||||
|
||||
```{toctree}
|
||||
|
||||
Add-a-simple-new-web-page.md
|
||||
Web-Tutorial.md
|
||||
|
||||
```
|
||||
|
|
@ -1,126 +0,0 @@
|
|||
# Web Tutorial
|
||||
|
||||
|
||||
Evennia uses the [Django](https://www.djangoproject.com/) web framework as the basis of both its
|
||||
database configuration and the website it provides. While a full understanding of Django requires
|
||||
reading the Django documentation, we have provided this tutorial to get you running with the basics
|
||||
and how they pertain to Evennia. This text details getting everything set up. The
|
||||
[Web-based Character view Tutorial](../../Web-Character-View-Tutorial.md) gives a more explicit example of making a
|
||||
custom web page connected to your game, and you may want to read that after finishing this guide.
|
||||
|
||||
## A Basic Overview
|
||||
|
||||
Django is a web framework. It gives you a set of development tools for building a website quickly
|
||||
and easily.
|
||||
|
||||
Django projects are split up into *apps* and these apps all contribute to one project. For instance,
|
||||
you might have an app for conducting polls, or an app for showing news posts or, like us, one for
|
||||
creating a web client.
|
||||
|
||||
Each of these applications has a `urls.py` file, which specifies what
|
||||
[URL](https://en.wikipedia.org/wiki/Uniform_resource_locator)s are used by the app, a `views.py` file
|
||||
for the code that the URLs activate, a `templates` directory for displaying the results of that code
|
||||
in [HTML](https://en.wikipedia.org/wiki/Html) for the user, and a `static` folder that holds assets
|
||||
like [CSS](https://en.wikipedia.org/wiki/CSS), [Javascript](https://en.wikipedia.org/wiki/Javascript),
|
||||
and Image files (You may note your mygame/web folder does not have a `static` or `template` folder.
|
||||
This is intended and explained further below). Django applications may also have a `models.py` file
|
||||
for storing information in the database. We will not change any models here, take a look at the
|
||||
[New Models](../../../Concepts/New-Models.md) page (as well as the [Django docs](https://docs.djangoproject.com/en/1.7/topics/db/models/) on models) if you are interested.
|
||||
|
||||
There is also a root `urls.py` that determines the URL structure for the entire project. A starter
|
||||
`urls.py` is included in the default game template, and automatically imports all of Evennia's
|
||||
default URLs for you. This is located in `web/urls.py`.
|
||||
|
||||
## Changing the logo on the front page
|
||||
|
||||
Evennia's default logo is a fun little googly-eyed snake wrapped around a gear globe. As cute as it
|
||||
is, it probably doesn't represent your game. So one of the first things you may wish to do is
|
||||
replace it with a logo of your own.
|
||||
|
||||
Django web apps all have _static assets_: CSS files, Javascript files, and Image files. In order to
|
||||
make sure the final project has all the static files it needs, the system collects the files from
|
||||
every app's `static` folder and places it in the `STATIC_ROOT` defined in `settings.py`. By default,
|
||||
the Evennia `STATIC_ROOT` is in `web/static`.
|
||||
|
||||
Because Django pulls files from all of those separate places and puts them in one folder, it's
|
||||
possible for one file to overwrite another. We will use this to plug in our own files without having
|
||||
to change anything in the Evennia itself.
|
||||
|
||||
By default, Evennia is configured to pull files you put in the `web/static_overrides` *after* all
|
||||
other static files. That means that files in `static_overrides` folder will overwrite any previously
|
||||
loaded files *having the same path under its static folder*. This last part is important to repeat:
|
||||
To overload the static resource from a standard `static` folder you need to replicate the path of
|
||||
folders and file names from that `static` folder in exactly the same way inside `static_overrides`.
|
||||
|
||||
Let's see how this works for our logo. The default web application is in the Evennia library itself,
|
||||
in `evennia/web/`. We can see that there is a `static` folder here. If we browse down, we'll
|
||||
eventually find the full path to the Evennia logo file:
|
||||
`evennia/web/static/evennia_general/images/evennia_logo.png`.
|
||||
|
||||
Inside our `static_overrides` we must replicate the part of the path inside the `static` folder, in
|
||||
other words, we must replicate `evennia_general/images/evennia_logo.png`.
|
||||
|
||||
So, to change the logo, we need to create the folder path `evennia_general/images/` in
|
||||
`static_overrides`. We then rename our own logo file to `evennia_logo.png` and copy it there. The
|
||||
final path for this file would thus be:
|
||||
`web/static_overrides/evennia_general/images/evennia_logo.png` in your local game folder.
|
||||
|
||||
To get this file pulled in, just change to your own game directory and reload the server:
|
||||
|
||||
```
|
||||
evennia reload
|
||||
```
|
||||
|
||||
This will reload the configuration and bring in the new static file(s). If you didn't want to reload
|
||||
the server you could instead use
|
||||
|
||||
```
|
||||
evennia collectstatic
|
||||
```
|
||||
|
||||
to only update the static files without any other changes.
|
||||
|
||||
> **Note**: Evennia will collect static files automatically during startup. So if `evennia
|
||||
collectstatic` reports finding 0 files to collect, make sure you didn't start the engine at some
|
||||
point - if so the collector has already done its work! To make sure, connect to the website and
|
||||
check so the logo has actually changed to your own version.
|
||||
|
||||
> **Note**: Sometimes the static asset collector can get confused. If no matter what you do, your
|
||||
overridden files aren't getting copied over the defaults, try removing the target file (or
|
||||
everything) in the `web/static` directory, and re-running `collectstatic` to gather everything from
|
||||
scratch.
|
||||
|
||||
## Changing the Front Page's Text
|
||||
|
||||
The default front page for Evennia contains information about the Evennia project. You'll probably
|
||||
want to replace this information with information about your own project. Changing the page template
|
||||
is done in a similar way to changing static resources.
|
||||
|
||||
Like static files, Django looks through a series of template folders to find the file it wants. The
|
||||
difference is that Django does not copy all of the template files into one place, it just searches
|
||||
through the template folders until it finds a template that matches what it's looking for. This
|
||||
means that when you edit a template, the changes are instant. You don't have to reload the server or
|
||||
run any extra commands to see these changes - reloading the web page in your browser is enough.
|
||||
|
||||
To replace the index page's text, we'll need to find the template for it. We'll go into more detail
|
||||
about how to determine which template is used for rendering a page in the
|
||||
[Web-based Character view Tutorial](../../Web-Character-View-Tutorial.md). For now, you should know that the template we want to change
|
||||
is stored in `evennia/web/website/templates/website/index.html`.
|
||||
|
||||
To replace this template file, you will put your changed template inside the
|
||||
`web/template_overrides/website` directory in your game folder. In the same way as with static
|
||||
resources you must replicate the path inside the default `template` directory exactly. So we must
|
||||
copy our replacement template named `index.html` there (or create the `website` directory in
|
||||
web/template_overrides` if it does not exist, first). The final path to the file should thus be:
|
||||
`web/template_overrides/website/index.html` within your game directory.
|
||||
|
||||
Note that it is usually easier to just copy the original template over and edit it in place. The
|
||||
original file already has all the markup and tags, ready for editing.
|
||||
|
||||
## Further reading
|
||||
|
||||
For further hints on working with the web presence, you could now continue to the
|
||||
[Web-based Character view Tutorial](../../Web-Character-View-Tutorial.md) where you learn to make a web page that
|
||||
displays in-game character stats. You can also look at [Django's own
|
||||
tutorial](https://docs.djangoproject.com/en/1.7/intro/tutorial01/) to get more insight in how Django
|
||||
works and what possibilities exist.
|
||||
|
|
@ -1,237 +0,0 @@
|
|||
# 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.
|
||||
|
|
@ -1,377 +0,0 @@
|
|||
# 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/Beginner-Tutorial-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._evmenu`.
|
||||
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._evmenu.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/Beginner-Tutorial-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.
|
||||
|
|
@ -1,159 +0,0 @@
|
|||
# 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.
|
||||
|
||||
## The Cooldown Contrib
|
||||
|
||||
The [Cooldown contrib](../Contribs/Contrib-Cooldowns.md) is a ready-made solution for
|
||||
command cooldowns you can use. It implements a _handler_ on the object to
|
||||
conveniently manage and store the cooldowns in a similar manner exemplified in
|
||||
this tutorial.
|
||||
|
||||
## 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
|
||||
# in, say, mygame/commands/spells.py
|
||||
|
||||
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"
|
||||
rate_of_fire = 60 * 2 # 2 minutes
|
||||
|
||||
def func(self):
|
||||
"Implement the spell"
|
||||
|
||||
now = time.time()
|
||||
last_cast = caller.ndb.firestorm_last_cast # could be None
|
||||
if last_cast and (now - last_cast < self.rate_of_fire):
|
||||
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.caller.ndb.firestorm_last_cast = now
|
||||
```
|
||||
|
||||
We specify `rate_of_fire` and then just check for a NAtrribute
|
||||
`firestorm_last_cast` 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.
|
||||
|
||||
## Persistent cooldown
|
||||
|
||||
To make a cooldown _persistent_ (so it survives a server reload), just
|
||||
use the same technique, but use [Attributes](../Components/Attributes.md) (that is, `.db` instead
|
||||
of `.ndb` storage to save the last-cast time.
|
||||
|
||||
## Make a cooldown-aware command parent
|
||||
|
||||
If you have many different spells or other commands with cooldowns, you don't
|
||||
want to have to add this code every time. Instead you can make a "cooldown
|
||||
command mixin" class. A _mixin_ is a class that you can 'add' to another class
|
||||
(via multiple inheritance) to give it some special ability. Here's an example
|
||||
with persistent storage:
|
||||
|
||||
```python
|
||||
# in, for example, mygame/commands/mixins.py
|
||||
|
||||
import time
|
||||
|
||||
class CooldownCommandMixin:
|
||||
|
||||
rate_of_fire = 60
|
||||
cooldown_storage_key = "last_used"
|
||||
cooldown_storage_category = "cmd_cooldowns"
|
||||
|
||||
def check_cooldown(self):
|
||||
last_time = self.caller.attributes.get(
|
||||
key=self.cooldown_storage_key,
|
||||
category=self.cooldown_storage_category)
|
||||
)
|
||||
return (time.time() - last_time) < self.rate_of_fire
|
||||
|
||||
def update_cooldown(self):
|
||||
self.caller.attribute.add(
|
||||
key=self.cooldown_storage_key,
|
||||
value=time.time(),
|
||||
category=self.cooldown_storage_category
|
||||
|
||||
)
|
||||
```
|
||||
|
||||
This is meant to be mixed into a Command, so we assume `self.caller` exists.
|
||||
We allow for setting what Attribute key/category to use to store the cooldown.
|
||||
|
||||
It also uses an Attribute-category to make sure what it stores is not mixed up
|
||||
with other Attributes on the caller.
|
||||
|
||||
Here's how it's used:
|
||||
|
||||
```python
|
||||
# in, say, mygame/commands/spells.py
|
||||
|
||||
from evennia import default_cmds
|
||||
from .mixins import CooldownCommandMixin
|
||||
|
||||
|
||||
class CmdSpellFirestorm(
|
||||
CooldownCommandMixin, default_cmds.MuxCommand):
|
||||
key = "cast firestorm"
|
||||
|
||||
cooldown_storage_key = "firestorm_last_cast"
|
||||
rate_of_fire = 60 * 2
|
||||
|
||||
def func(self):
|
||||
|
||||
if not self.check_cooldown():
|
||||
self.caller.msg("You cannot cast this spell again yet.")
|
||||
return
|
||||
|
||||
# [the spell effect happens]
|
||||
|
||||
self.update_cooldown()
|
||||
|
||||
```
|
||||
|
||||
So the same as before, we have just hidden away the cooldown checks and you can
|
||||
reuse this mixin for all your cooldowns.
|
||||
|
||||
### Command crossover
|
||||
|
||||
This example of cooldown-checking also works *between* commands. For example,
|
||||
you can have all fire-related spells store the cooldown with the same
|
||||
`cooldown_storage_key` (like `fire_spell_last_used`). That would mean casting
|
||||
of *Firestorm* would block all other fire-related spells for a while.
|
||||
|
||||
Similarly, when you take that that big sword swing, other types of attacks could
|
||||
be blocked before you can recover your balance.
|
||||
|
|
@ -1,403 +0,0 @@
|
|||
# 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).
|
||||
|
|
@ -1,125 +0,0 @@
|
|||
# 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.
|
||||
|
|
@ -1,349 +0,0 @@
|
|||
# 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.
|
||||
|
|
@ -1,130 +0,0 @@
|
|||
# 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/Beginner-Tutorial-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.
|
||||
|
|
@ -1,494 +0,0 @@
|
|||
# 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.
|
||||
|
|
@ -1,196 +0,0 @@
|
|||
# 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.
|
||||
|
|
@ -1,221 +0,0 @@
|
|||
# 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/Beginner-Tutorial-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)!
|
||||
|
|
@ -1,732 +0,0 @@
|
|||
# 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/Beginner-Tutorial-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.
|
||||
|
|
@ -1,304 +0,0 @@
|
|||
# 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
|
||||
`gametime.py` in your `commands` directory and paste this code in it):
|
||||
|
||||
```python
|
||||
# in a file mygame/commands/gametime.py
|
||||
|
||||
from evennia.contrib.base_systems 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.
|
||||
|
|
@ -1,485 +0,0 @@
|
|||
# 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.
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
# Tutorials and Howto's
|
||||
|
||||
All Evennia tutorials. They will often refer to the [components](../Components/Components-Overview.md) or [concepts](../Concepts/Concepts-Overview.md) if you 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
|
||||
|
||||
Tutorial-Persistent-Handler.md
|
||||
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
|
||||
```
|
||||
|
||||
## Old tutorials
|
||||
|
||||
These will be replaced by the Beginner Tutorial, but remain here until that is complete.
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 1
|
||||
|
||||
Implementing-a-game-rule-system.md
|
||||
Turn-based-Combat-System.md
|
||||
A-Sittable-Object.md
|
||||
|
||||
|
||||
```
|
||||
|
|
@ -1,265 +0,0 @@
|
|||
# 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.
|
||||
|
|
@ -1,169 +0,0 @@
|
|||
# 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)
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
# 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)
|
||||
|
||||
```
|
||||
|
|
@ -1,334 +0,0 @@
|
|||
# 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, move_type="buy")
|
||||
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.
|
||||
|
|
@ -1,748 +0,0 @@
|
|||
# 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/Beginner-Tutorial-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/Beginner-Tutorial-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.
|
||||
|
|
@ -1,416 +0,0 @@
|
|||
# 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/Beginner-Tutorial-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](./Turn-based-Combat-System.md).
|
||||
|
|
@ -1,521 +0,0 @@
|
|||
# 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](Beginner-Tutorial/Part1/Beginner-Tutorial-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.
|
||||
|
|
@ -1,126 +0,0 @@
|
|||
# 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')
|
||||
```
|
||||
|
||||
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!"
|
||||
```
|
||||
|
|
@ -1,110 +0,0 @@
|
|||
# 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.
|
||||
|
|
@ -1,235 +0,0 @@
|
|||
# Making a Persistent object Handler
|
||||
|
||||
A _handler_ is a convenient way to group functionality on an object. This allows you to logically
|
||||
group all actions related to that thing in one place. This tutorial expemplifies how to make your
|
||||
own handlers and make sure data you store in them survives a reload.
|
||||
|
||||
For example, when you do `obj.attributes.get("key")` or `obj.tags.add('tagname')` you are evoking
|
||||
handlers stored as `.attributes` and `tags` on the `obj`. On these handlers are methods (`get()`
|
||||
and `add()` in this example).
|
||||
|
||||
## Base Handler example
|
||||
|
||||
Here is a base way to set up an on-object handler:
|
||||
|
||||
```python
|
||||
|
||||
from evennia import DefaultObject, create_object
|
||||
from evennia.utils.utils import lazy_property
|
||||
|
||||
class NameChanger:
|
||||
def __init__(self, obj):
|
||||
self.obj = obj
|
||||
|
||||
def add_to_key(self, suffix):
|
||||
self.obj.key = f"self.obj.key_{suffix}"
|
||||
|
||||
# make a test object
|
||||
class MyObject(DefaultObject):
|
||||
@lazy_property:
|
||||
def namechange(self):
|
||||
return NameChanger(self)
|
||||
|
||||
|
||||
obj = create_object(MyObject, key="test")
|
||||
print(obj.key)
|
||||
>>> "test"
|
||||
obj.namechange.add_to_key("extra")
|
||||
print(obj.key)
|
||||
>>> "test_extra"
|
||||
```
|
||||
|
||||
What happens here is that we make a new class `NameChanger`. We use the
|
||||
`@lazy_property` decorator to set it up - this means the handler will not be
|
||||
actually created until someone really wants to use it, by accessing
|
||||
`obj.namechange` later. The decorated `namechange` method returns the handler
|
||||
and makes sure to initialize it with `self` - this becomes the `obj` inside the
|
||||
handler!
|
||||
|
||||
We then make a silly method `add_to_key` that uses the handler to manipulate the
|
||||
key of the object. In this example, the handler is pretty pointless, but
|
||||
grouping functionality this way can both make for an easy-to-remember API and
|
||||
can also allow you cache data for easy access - this is how the
|
||||
`AttributeHandler` (`.attributes`) and `TagHandler` (`.tags`) works.
|
||||
|
||||
## Persistent storage of data in handler
|
||||
|
||||
Let's say we want to track 'quests' in our handler. A 'quest' is a regular class
|
||||
that represents the quest. Let's make it simple as an example:
|
||||
|
||||
```python
|
||||
# for example in mygame/world/quests.py
|
||||
|
||||
|
||||
class Quest:
|
||||
|
||||
key = "The quest for the red key"
|
||||
|
||||
def __init__(self):
|
||||
self.current_step = "start"
|
||||
|
||||
def check_progress(self):
|
||||
# uses self.current_step to check
|
||||
# progress of this quest
|
||||
getattr(self, f"step_{self.current_step}")()
|
||||
|
||||
def step_start(self):
|
||||
# check here if quest-step is complete
|
||||
self.current_step = "find_the_red_key"
|
||||
def step_find_the_red_key(self):
|
||||
# check if step is complete
|
||||
self.current_step = "hand_in_quest"
|
||||
def step_hand_in_quest(self):
|
||||
# check if handed in quest to quest giver
|
||||
self.current_step = None # finished
|
||||
|
||||
```
|
||||
|
||||
We expect the dev to make subclasses of this to implement different quests. Exactly how this works
|
||||
doesn't matter, the key is that we want to track `self.current_step` - a property that _should
|
||||
survive a server reload_. But so far there is no way for `Quest` to accomplish this, it's just a
|
||||
normal Python class with no connection to the database.
|
||||
|
||||
### Handler with save/load capability
|
||||
|
||||
Let's make a `QuestHandler` that manages a character's quests.
|
||||
|
||||
```python
|
||||
# for example in the same mygame/world/quests.py
|
||||
|
||||
|
||||
class QuestHandler:
|
||||
def __init__(self, obj):
|
||||
self.obj = obj
|
||||
self.do_save = False
|
||||
self._load()
|
||||
|
||||
def _load(self):
|
||||
self.storage = self.obj.attributes.get(
|
||||
"quest_storage", default={}, category="quests")
|
||||
|
||||
def _save(self):
|
||||
self.obj.attributes.add(
|
||||
"quest_storage", self.storage, category="quests")
|
||||
self._load() # important
|
||||
self.do_save = False
|
||||
|
||||
def add(self, questclass):
|
||||
self.storage[questclass.key] = questclass(self.obj)
|
||||
self._save()
|
||||
|
||||
def check_progress(self):
|
||||
quest.check_progress()
|
||||
if self.do_save:
|
||||
# .do_save is set on handler by Quest if it wants to save progress
|
||||
self._save()
|
||||
|
||||
```
|
||||
|
||||
The handler is just a normal Python class and has no database-storage on its own. But it has a link
|
||||
to `.obj`, which is assumed to be a full typeclased entity, on which we can create
|
||||
persistent [Attributes](../Components/Attributes.md) to store things however we like!
|
||||
|
||||
We make two helper methods `_load` and
|
||||
`_save` that handles local fetches and saves `storage` to an Attribute on the object. To avoid
|
||||
saving more than necessary, we have a property `do_save`. This we will set in `Quest` below.
|
||||
|
||||
> Note that once we `_save` the data, we need to call `_load` again. This is to make sure the version we store on the handler is properly de-serialized. If you get an error about data being `bytes`, you probably missed this step.
|
||||
|
||||
|
||||
### Make quests storable
|
||||
|
||||
The handler will save all `Quest` objects as a `dict` in an Attribute on `obj`. We are not done yet
|
||||
though, the `Quest` object needs access to the `obj` too - not only will this is important to figure
|
||||
out if the quest is complete (the `Quest` must be able to check the quester's inventory to see if
|
||||
they have the red key, for example), it also allows the `Quest` to tell the handler when its state
|
||||
changed and it should be saved.
|
||||
|
||||
We change the `Quest` such:
|
||||
|
||||
```python
|
||||
from evennia.utils import dbserialize
|
||||
|
||||
|
||||
class Quest:
|
||||
|
||||
def __init__(self, obj):
|
||||
self.obj = obj
|
||||
self._current_step = "start"
|
||||
|
||||
def __serialize_dbobjs__(self):
|
||||
self.obj = dbserialize.dbserialize(self.obj)
|
||||
|
||||
def __deserialize_dbobjs__(self):
|
||||
if isinstance(self.obj, bytes):
|
||||
self.obj = dbserialize.dbunserialize(self.obj)
|
||||
|
||||
@property
|
||||
def questhandler(self):
|
||||
return self.obj.quests
|
||||
|
||||
@property
|
||||
def current_step(self):
|
||||
return self._current_step
|
||||
|
||||
@current_step.setter
|
||||
def current_step(self, value):
|
||||
self._current_step = value
|
||||
self.questhandler.do_save = True # this triggers save in handler!
|
||||
|
||||
# [same as before]
|
||||
|
||||
```
|
||||
|
||||
The `Quest.__init__` now takes `obj` as argument, to match what we pass to it in
|
||||
`QuestHandler.add`. We want to monitor the changing of `current_step`, so we
|
||||
make it into a `property`. When we edit that value, we set the `do_save` flag on
|
||||
the handler, which means it will save the status to database once it has checked
|
||||
progress on all its quests. The `Quest.questhandler` property allows to easily
|
||||
get back to the handler (and the object on which it sits).
|
||||
|
||||
The `__serialize__dbobjs__` and `__deserialize_dbobjs__` methods are needed
|
||||
because `Attributes` can't store 'hidden' database objects (the `Quest.obj`
|
||||
property. The methods help Evennia serialize/deserialize `Quest` propertly when
|
||||
the handler saves it. For more information, see [Storing Single
|
||||
objects](../Components/Attributes.md#storing-single-objects) in the Attributes
|
||||
|
||||
### Tying it all together
|
||||
|
||||
The final thing we need to do is to add the quest-handler to the character:
|
||||
|
||||
```python
|
||||
# in mygame/typeclasses/characters.py
|
||||
|
||||
from evennia import DefaultCharacter
|
||||
from evennia.utils.utils import lazy_property
|
||||
from .world.quests import QuestHandler # as an example
|
||||
|
||||
|
||||
class Character(DefaultCharacter):
|
||||
# ...
|
||||
@lazy_property
|
||||
def quests(self):
|
||||
return QuestHandler(self)
|
||||
|
||||
```
|
||||
|
||||
|
||||
You can now make your Quest classes to describe your quests and add them to
|
||||
characters with
|
||||
|
||||
```python
|
||||
character.quests.add(FindTheRedKey)
|
||||
```
|
||||
|
||||
and can later do
|
||||
|
||||
```python
|
||||
character.quests.check_progress()
|
||||
```
|
||||
|
||||
and be sure that quest data is not lost between reloads.
|
||||
|
||||
You can find a full-fledged quest-handler example as [EvAdventure
|
||||
quests](evennia.contrib.tutorials.evadventure.quests) contrib in the Evennia
|
||||
repository.
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
# 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
|
||||
|
|
@ -1,422 +0,0 @@
|
|||
# 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, move_type="board")
|
||||
|
||||
|
||||
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, move_type="disembark")
|
||||
|
||||
|
||||
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!
|
||||
|
|
@ -1,665 +0,0 @@
|
|||
# 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/Beginner-Tutorial-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).
|
||||
|
|
@ -1,198 +0,0 @@
|
|||
# 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.
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
# 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.
|
||||
|
|
@ -1,638 +0,0 @@
|
|||
# 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.urls import path
|
||||
from web.chargen import views
|
||||
|
||||
urlpatterns = [
|
||||
# url: /chargen/
|
||||
path("", views.index, name='chargen-index'),
|
||||
# url: /chargen/5/
|
||||
path("<int:app_id>/", views.detail, name="chargen-detail"),
|
||||
# url: /chargen/create
|
||||
path("create/", views.creating, name='chargen-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.
|
||||
|
||||
To add this to our website, we must also update the main `mygame/website/urls.py` file; this
|
||||
will help tying our new chargen app in with the rest of the website. `urlpatterns` variable, and
|
||||
change it to include:
|
||||
|
||||
```python
|
||||
# in file mygame/website/urls.py
|
||||
|
||||
from django.urls import path, include
|
||||
|
||||
urlpatterns = [
|
||||
# make all chargen endpoints available under /chargen url
|
||||
path("chargen/", include("web.chargen.urls")
|
||||
]
|
||||
|
||||
```
|
||||
|
||||
### 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/website/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!
|
||||
|
|
@ -1,230 +0,0 @@
|
|||
# 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.urls import path
|
||||
from web.character.views import sheet
|
||||
|
||||
urlpatterns = [
|
||||
path("sheet/<int:object_id>", 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/website/urls.py` and update its `patterns` list as follows:
|
||||
|
||||
```python
|
||||
# web/website/urls.py
|
||||
|
||||
urlpatterns = [
|
||||
# ...
|
||||
path("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