Extensive cleanup of docs

This commit is contained in:
Griatch 2022-11-21 00:33:36 +01:00
parent dd0c5f74e0
commit 0b9f6ac540
34 changed files with 779 additions and 1126 deletions

View file

@ -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)

View file

@ -408,8 +408,4 @@ in a format like the following:
## 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.
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.

View file

@ -0,0 +1,666 @@
# Building a chair you can sit on
In this lesson we will make use of what we have learned to create a new game object: a chair you can sit on.
Out goals are:
- 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 store
the current chair in an attribute `is_sitting`. Other systems could check this to affect us in different ways.
- A character should be able to stand up and move away from the chair.
- When you sit down you should not be able to walk to another room without first standing up.
## Make us not able to move while 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
# in mygame/typeclasses/characters.py
# ...
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 need to stand up first.")
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`. If this returns `False`, the move is aborted.
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:
```{code-block} python
:linenos:
:emphasize-lines: 3,7,15,16,23,24,25
# in mygame/typeclasses/sittables.py
from typeclasses.objects import Object
class Sittable(Object):
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(f"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_sitting = self.obj
sitter.msg(f"You sit on {self.key}")
```
This handles the logic of someone sitting down on the chair.
- **Line 3**: We inherit from the empty `Object` class in `mygame/typeclasses/objects.py`. This means we can theoretically modify that in the future and have those changes affect sittables too.
- **Line 7**: The `do_sit` method expects to be called with the argument `sitter`, which is to be an `Object` (most likely a `Character`). This is the one wanting to sit down.
- **Line 15**: Note that, if the [Attribute](../../../Components/Attributes.md) `sitter` is not defined on the chair (because this is the first time someone sits in it), this will simply return `None`, which is fine.
- **Lines 16-22** We check if someone is already sitting on the chair and returns appropriate error messages depending on if it's you or someone else. We use `return` to abort the sit-action.
- **Line 23**: If we get to this point, `sitter` gets to, well, sit down. We store them in the `sitter` Attribute on the chair.
- **Line 24**: `self.obj` is the chair this command is attachd to. We store that in the `is_sitting` Attribute on the `sitter` itself.
- **Line 25**: Finally we tell the sitter that they could sit down.
Let's continue:
```{code-block} python
:linenos:
:emphasize-lines: 12,15,16,17
# add this right after the `do_sit method` in the same class
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.sitter = None
del stander.db.is_sitting
stander.msg(f"You stand up from {self.key}")
```
This is the inverse of sitting down; we need to do some cleanup.
- **Line 12**: If we are not sitting on the chair, it makes no sense to stand up from it.
- **Line 15**: If we get here, we could stand up. We make sure to un-set the `sitter` Attribute so someone else could use the chair later.
- **Line 16**: The character is no longer sitting, so we delete their `is_sitting` Attribute. We could also have done `stander.db.is_sitting = None` here, but deleting the Attribute feels cleaner.
- **Line 17**: Finally, we inform them that they stood up successfully.
One could imagine that one could have the future `sit` command (which we haven't created yet) 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 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.
### Sitting on or in?
It's fine to sit 'on' a chair. But what if our Sittable is an armchair?
```
> armchair = evennia.create_object("typeclasses.sittables.Sittable", key="armchair", location=here)
> armchair.do_sit(me)
> You sit on armchair.
```
This is not grammatically correct, you actually sit "in" an armchair rather than "on" it. It's also possible to both sit 'in' or 'on' a chair depending on the type of chair (English is weird). We want to be able to control this.
We _could_ make a child class of `Sittable` named `SittableIn` that makes this change, but that feels excessive. Instead we will modify what we have:
```{code-block} python
:linenos:
:emphasize-lines: 15,22,43
# in mygame/typeclasses/sittables.py
from evennia import DefaultObject
class Sittable(DefaultObject):
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 or "on"
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_sitting = self.obj
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
del stander.db.is_sitting
stander.msg(f"You stand up from {self.key}")
```
- **Line 15**: We grab the `adjective` Attribute. Using `seld.db.adjective or "on"` here means that if the Attribute is not set (is `None`/falsy) the default "on" string will be assumed.
- **Lines 22 and 43**: We use this adjective to modify the return text we see.
`reload` the server. An advantage of using Attributes like this is that they can be modified on the fly, in-game. Let's look at a builder could use this by normal building commands (no need for `py`):
```
> set armchair/adjective = in
```
Since we haven't added the `sit` command yet, we must still use `py` to test:
```
> py armchair = evennia.search_object("armchair")[0];armchair.do_sit(me)
You sit in armchair.
```
### Extra credits
What if we want some more dramatic flair when you sit down in certain chairs?
You sit down and a whoopie cushion makes a loud fart noise!
You can make this happen by tweaking your `Sittable` class having the return messages be replaceable by `Attributes` that you can set on the object you create. You want something like this:
```
> chair = evennia.create_object("typeclasses.sittables.Sittable", key="pallet")
> chair.do_sit(me)
You sit down on pallet.
> chair.do_stand(me)
You stand up from pallet.
> chair.db.msg_sitting_down = "You sit down and a whoopie cushion makes a loud fart noise!"
> chair.do_sit(me)
You sit down and a whoopie cushion makes a loud fart noise!
```
That is, if you are not setting the Attribute, you should get a default value. We leave this implementation up to the reader.
## Adding commands
As we discussed in the [lesson about adding Commands](./Beginner-Tutorial-More-on-Commands.md), 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.
### 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 could look if `armchair` is in the room (if you overrode the sit message):
> 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.
```
```{code-block} python
:linenos:
:emphasize-lines: 11,19,23
# in mygame/commands/sittables.py
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.
- **Lines 11 and 19**: The `self.obj` is the object to which we added the cmdset with this Command (so the chair). We just call the `do_sit/stand` on that object and pass the `caller` (the person sitting down). The `Sittable` will do the rest.
- **Line 23**: The `priority = 1` on `CmdSetSit` means that same-named Commands from this cmdset merge with a bit higher priority than Commands from the on-Character-cmdset (which has `priority = 0`). This means that if you have a `sit` command on your Character and comes into a room with a chair, the `sit` command on the chair will take precedence.
We also need to make a change to our `Sittable` typeclass. Open `mygame/typeclasses/sittables.py`:
```{code-block} python
:linenos:
:emphasize-lines: 4,10,11
# in mygame/typeclasses/sittables.py
from evennia import DefaultObject
from commands.sittables import CmdSetSit
class Sittable(DefaultObject):
"""
(docstring)
"""
def at_object_creation(self):
self.cmdset.add_default(CmdSetSit)A
# ...
```
- **Line 4**: We must install the `CmdSetSit` .
- **Line 10**: The `at_object_creation` method will only be called once, when the object is first created.
- **Line 11**: We add the command-set as a 'default' cmdset with `add_default`. This makes it persistent also protects it from being deleted should another cmdset be added. See [Command Sets](../../../Components/Command-Sets.md) for more info.
Make sure to `reload` to make the code changes available.
All _new_ Sittables will now have your `sit` Command. Your existing `armchair` will not though. This is because `at_object_creation` will not re-run for already existing objects. We can update it manually:
> update armchair
We could also update all existing sittables (all on one line):
```{sidebar} List comprehensions
`[obj for obj in iterator]` is 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).
```
> py from typeclasses.sittables import Sittable ;
[sittable.at_object_creation() for sittable in Sittable.objects.all()]
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`:
```{code-block} python
:linenos:
:emphasize-lines: 9,12
# after the other commands in mygame/commands/sittables.py
# ...
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.")
```
- **Line 9**: This command responds both to `sit` and `stand` because we added `stand` to its `aliases` list. Command aliases have the same 'weight' as the `key` of the command, both equally identify the Command.
- **Line 12**: The `.cmdname` of a `Command` holds the name actually used to call it. This will be one of `"sit"` or `"stand"`. This leads to different return messages.
We don't need a new CmdSet for this, instead we will add this to the default Character cmdset. Open `mygame/commands/default_cmdsets.py`:
```python
# in mygame/commands/default_cmdsets.py
# ...
from commands import sittables
class CharacterCmdSet(CmdSet):
"""
(docstring)
"""
def at_cmdset_creation(self):
# ...
self.add(sittables.CmdNoSitStand)
```
As usual, make sure to `reload` the server to have the new code recognized.
To test we'll build a new location without any comfy armchairs and go there:
> 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/stand` Command - one on the `Sittable` (armchair) and the other on us (via the `CharacterCmdSet`). Since we set a `priority=1` on the chair's cmdset (and `CharacterCmdSet` has `priority=0`), there will be no command-collision: the chair's `sit` takes precedence over the `sit` defined on us ... until there is no chair around.
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 `Sittable`:
> 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.
With `sit` it was okay to get a choice - Evennia can't know which chair we intended to sit on. But once we sit we sure know from which chair we should stand up from! We must make sure that we only get the command from the chair we are actually sitting on.
We will fix this with a [Lock](../../../Components/Locks.md) 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 that particular `stand` command is attached to.
First let's add the lock so we see what we want. Open `mygame/commands/sittables.py`:
```{code-block} python
:linenos:
:emphasize-lines: 10
# in mygame/commands/sittables.py
# ...
class CmdStand(Command):
"""
Stand up.
"""
key = "stand"
lock = "cmd:sitsonthis()"
def func(self):
self.obj.do_stand(self.caller)
# ...
```
- **Line 10**: This is the lock definition. It's on the form `condition:lockfunc`. The `cmd:` type lock is checked by Evennia when determining if a user has access to a Command at all. We want the lock-function to only return `True` if this command is on a chair which the caller is sitting on.
What will be checked is the `sitsonthis` _lock function_ which doesn't exist yet.
Open `mygame/server/conf/lockfuncs.py` to add it!
```python
# mygame/server/conf/lockfuncs.py
"""
(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.
All lock functions must acccept the same arguments. The arguments are required and Evennia will pass all relevant objects as needed.
```{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.
Make sure you `reload`.
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.
> 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! This is a fully functional chair now!
Adding a Command to the chair object like this is powerful and is a good technique to know. It does come with some caveats though, as we've seen.
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
> del sofa
> (etc)
The make the following changes:
- In `mygame/typeclasses/sittables.py`, comment out the entire `at_object_creation` method.
- 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.
```{code-block} python
:linenos:
:emphasize-lines: 4,27,32,35
# in mygame/commands/sittables.py
from evennia import Command, CmdSet
from evennia import InterruptCommand
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!")
```
```{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. In this case Evennia knows to catch the `InterruptCommand` exception and stop the command execution early.
```
- **Line 4**: We need the `InterruptCommand` to be able to abort command parsing early (see below).
- **Line 27**: The `parse` method runs before the `func` method on a `Command`. If no argument is provided to the command, we want to fail early, already in `parse`, so `func` never fires. Just `return` is not enough to do that, we need to `raise InterruptCommand`. Evennia will see a raised `InterruptCommand` as a sign it should immediately abort the command execution.
- **Line 32**: We use the parsed command arguments as the target-chair to search for. As discussed in the [search tutorial](./Beginner-Tutorial-Searching-Things.md), `self.caller.search()` will handle error messages itself. So if it returns `None`, we can just `return`.
- **Line 35-38**: The `try...except` block 'catches' and exception and handles it. In this case we try to run `do_sit` on the object. If the object we found is _not_ a `Sittable`, it will likely not have a `do_sit` method and an `AttributeError` will be raised. We should handle that case gracefully.
Let's do the `stand` command while we are at it. Since the Command is external to the chair we don't know which object we are sitting on and have to search for it. In this case we really want to find _only_ things we are sitting on.
```{code-block} python
:linenos:
:emphasize-lines: 17,21
# end of mygame/commands/sittables.py
class CmdStand2(Command):
"""
Stand up.
Usage:
stand
"""
key = "stand"
def func(self):
caller = self.caller
# if we are sitting, this should be set on us
sittable = caller.db.is_sitting
if not sittable:
caller.msg("You are not sitting down.")
else:
sittable.do_stand(caller)
```
- **Line 17**: We didn't need the `is_sitting` Attribute for the first version of these Commands, but we do need it now. Since we have this, we don't need to search and know just which chair we sit on. If we don't have this set, we are not sitting anywhere.
- **Line 21**: We stand up using the sittable we found.
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/commands/default_cmdsets.py`.
```python
# in mygame/commands/default_cmdsets.py
# ...
from commands import sittables
class CharacterCmdSet(CmdSet):
"""
(docstring)
"""
def at_cmdset_creation(self):
# ...
self.add(sittables.CmdSit2)
self.add(sittables.CmdStand2)
```
Make sure to `reload`.
Now let's try it out:
> create/drop sofa : sittables.Sittable
> sit sofa
You sit down on sofa.
> stand
You stand up from sofa.
> north
> sit sofa
> You can't find 'sofa'.
Storing commands on the Character centralizes them, but you must instead search or store any external objects you want that command to interact on.
## Conclusions
In this lesson we built ourselves a chair and even a sofa!
- 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.
This concludes the first part of the Beginner tutorial!

View file

@ -24,27 +24,7 @@ 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
@ -59,5 +39,6 @@ Beginner-Tutorial-More-on-Commands
Beginner-Tutorial-Creating-Things
Beginner-Tutorial-Searching-Things
Beginner-Tutorial-Django-queries
Beginner-Tutorial-Making-A-Sittable-Object
```

View file

@ -85,6 +85,10 @@ You can use `.search` to find anything, not just stuff in the same room:
volcano = self.caller.search("Vesuvio", global=True)
You can limit your matches to particular typeclasses:
water_glass = self.caller.search("glass", typeclass="typeclasses.objects.WaterGlass")
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])

View file

@ -24,16 +24,8 @@ 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}
:numbered:
:maxdepth: 2
Beginner-Tutorial-Planning-Where-Do-I-Begin.md
Beginner-Tutorial-Game-Planning.md

View file

@ -1,4 +1,4 @@
# Part 3: How we get there
# Part 3: How we get there (example game)
```{warning}
The tutorial game is under development and is not yet complete, nor tested. Use the existing
@ -42,26 +42,8 @@ Fully coded examples of all code we make in this part can be found in the
```{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}
:numbered:
:maxdepth: 2
Beginner-Tutorial-Utilities
Beginner-Tutorial-Rules

View file

@ -26,15 +26,9 @@ and batchcode processors.
_TODO_
```{toctree}
:maxdepth: 1
:numbered:
:maxdepth: 2
../../../Unimplemented.md
```
## Table of Contents
_TODO_
```{toctree}
```

View file

@ -24,21 +24,8 @@ to bring your game online so you can invite your first players.
_TODO_
```{toctree}
:maxdepth: 1
:numbered:
:maxdepth: 2
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
```
```

View file

@ -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.

View file

@ -266,7 +266,7 @@ We will here show two examples using the *EvTable* and *EvForm* utilities.Later
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.
> [the text tag documentation](../Concepts/Inline-Tags-and-Functions.md) for how to add color to the tables and forms.
#### Making a sheet with EvTable

View file

@ -7,8 +7,7 @@ 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
'active' way is to use asynchronous delays as in the [](Howto-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

View file

@ -186,8 +186,7 @@ while they are running. If you are in the process of crafting a helmet you shoul
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
The simplest way of implementing blocking is to use the technique covered in the [](Howto-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

View file

@ -6,13 +6,18 @@ All Evennia tutorials. They will often refer to the [components](../Components/C
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.
in mind for your own game, this will give you a good start.
> The latter parts of the beginner tutorial are still being worked on.
```{toctree}
:maxdepth: 1
:maxdepth: 3
./Beginner-Tutorial/Beginner-Tutorial-Intro
./Beginner-Tutorial/Part1/Beginner-Tutorial-Part1-Intro
./Beginner-Tutorial/Part2/Beginner-Tutorial-Part2-Intro
./Beginner-Tutorial/Part3/Beginner-Tutorial-Part3-Intro
./Beginner-Tutorial/Part4/Beginner-Tutorial-Part4-Intro
./Beginner-Tutorial/Part5/Beginner-Tutorial-Part5-Intro
```
## Howto's
@ -20,13 +25,10 @@ in mind for your own game, this will give you a good start.
```{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
Howto-Command-Prompt.md
Howto-Command-Cooldown.md
Howto-Command-Duration.md
Howto-Default-Exit-Errors.md
```
## Mobs and NPCs
@ -54,13 +56,13 @@ Tutorial-Vehicles.md
Tutorial-Persistent-Handler.md
Gametime-Tutorial.md
Help-System-Tutorial.md
Mass-and-weight-for-objects.md
Weather-Tutorial.md
Coordinates.md
Tutorial-Coordinates.md
Dynamic-In-Game-Map.md
Static-In-Game-Map.md
Arxcode-Installation.md
Tutorial-Tweeting-Game-Stats.md
```
@ -69,9 +71,10 @@ Arxcode-Installation.md
```{toctree}
:maxdepth: 1
Add-a-wiki-on-your-website.md
Web-Add-a-wiki.md
Web-Character-Generation.md
Web-Character-View-Tutorial.md
Web-Help-System-Tutorial.md
```
## Deep-dives
@ -80,11 +83,10 @@ Web-Character-View-Tutorial.md
:maxdepth: 1
Parsing-commands-tutorial.md
Understanding-Color-Tags.md
Tutorial-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
@ -96,7 +98,5 @@ These will be replaced by the Beginner Tutorial, but remain here until that is c
Implementing-a-game-rule-system.md
Turn-based-Combat-System.md
A-Sittable-Object.md
Tutorial-for-basic-MUSH-like-game.md
```

View file

@ -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)

View file

@ -31,7 +31,7 @@ allows for emoting as part of combat which is an advantage for roleplay-heavy ga
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
[commands](../Components/Commands.md), possibly ones with a [cooldown](./Howto-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.

View file

@ -2,17 +2,12 @@
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.
Correct understanding of this topic requires having read the 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 [Colors](../Concepts/Colors.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
==========================
## 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
@ -32,8 +27,7 @@ pitfalls. ANSI and Xterm256 coexist without conflicts in Evennia, but in many wa
each other: ANSI-specific color tags will have no effect on Xterm-defined colors, as we shall see
here.
ANSI
====
## 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,
@ -51,8 +45,7 @@ Also, it's important to remember that the 16 ANSI colors are a convention, and t
always customize their appearance—he might decide to have green show as red, and dark green as blue,
etc.
Xterm256
========
## 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
@ -61,8 +54,7 @@ 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
==========================
## 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
@ -193,6 +185,5 @@ Shows that `|*` only works once in a row and will not (and should not!) revert b
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
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.

View file

@ -9,8 +9,7 @@ Fortunately, you don't have to create the features manually, since it has been d
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
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
@ -144,8 +143,7 @@ always have read/write permissions on that article. Additionally, the article wi
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.
Go to the Groups page of your game's Django admin panel 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.*
@ -204,7 +202,6 @@ WIKI_CAN_READ = is_player
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).
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).

View file

@ -1,8 +1,7 @@
# Web Character View Tutorial
**Before doing this tutorial you will probably want to read the intro in [Basic Web tutorial](Web-
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"

View file

@ -1,9 +1,7 @@
# 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.
**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.