diff --git a/docs/1.0-dev/.buildinfo b/docs/1.0-dev/.buildinfo
index a808e3f59e..4e7ae5a460 100644
--- a/docs/1.0-dev/.buildinfo
+++ b/docs/1.0-dev/.buildinfo
@@ -1,4 +1,4 @@
# Sphinx build info version 1
# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done.
-config: c0024f80050e9e2b9dfd653b5cfcd067
+config: 7378769b481fe0c2d225b058b431f2ac
tags: 645f666f9bcd5a90fca523b33c5a78b7
diff --git a/docs/1.0-dev/Howtos/Coding-FAQ.html b/docs/1.0-dev/Coding/Coding-FAQ.html
similarity index 96%
rename from docs/1.0-dev/Howtos/Coding-FAQ.html
rename to docs/1.0-dev/Coding/Coding-FAQ.html
index 76c47b8a07..1d8b43cd27 100644
--- a/docs/1.0-dev/Howtos/Coding-FAQ.html
+++ b/docs/1.0-dev/Coding/Coding-FAQ.html
@@ -17,8 +17,8 @@
-
-
+
+
@@ -145,7 +145,7 @@ reloading)
Character Command Set?
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
+self.remove(default_cmds.CmdGet()). See the Adding Commands Tutorial
for more info.
It’s highly recommended that you jump in on the Starting Tutorial. Even if
-you only the beginning or some part of it, it covers much of the things needed to get started.
Evennia is developed using Python. Even if you are more of a designer than a coder, it is wise to
-learn how to read and understand basic Python code. If you are new to Python, or need a refresher,
-take a look at our Python introduction.
+you only the beginning or some part of it, it covers much of the things needed to get started, including giving you are first introduction to Python.
When new to Evennia it can be hard to find things or figure out what is available. Evennia offers a
-special interactive python shell that allows you to experiment and try out things. It’s recommended
-to use ipython for this since the vanilla python prompt is very limited. Here
-are some simple commands to get started:
+
As mentioned in the Starting tutorial, it’s a good idea to use ipython to explore things using a python shell:
# [open a new console/terminal]
-# [activate your evennia virtualenv in this console/terminal]
-pip install -r requirements_extra.txt # install ipython etc
cd mygame
evennia shell
@@ -150,9 +138,6 @@ evennia shell
evennia.<TAB>
-
That is, enter evennia. and press the <TAB> key. This will show you all the resources made
-available at the top level of Evennia’s “flat API”. See the flat API page for more
-info on how to explore it efficiently.
You can also explore evennia interactively in a Jupyter notebook. This offers
@@ -212,9 +197,7 @@ page. It might hopefully help you avoid some common pitfalls and time sinks.
home. You should never need to modify anything in the evennia library (anything you download
from us, really). You import useful functionality from here and if you see code you like, copy&paste
it out into your game folder and edit it there.
-
If you find that Evennia doesn’t support some functionality you need, make a Feature
-Request about it. Same goes for [bugs][bug]. If you add features or fix bugs
-yourself, please consider Contributing your changes upstream!
+
If you find that Evennia doesn’t support some functionality you need, make a Feature Request about it. Same goes for bugs. If you add features or fix bugs yourself, please consider Contributing your changes upstream!
This documentation aims to help you set up a sane development environment to
-make your game, also if you never coded before. If you are an experienced coder, much of this will be familiar
-to you, but some things may still be useful.
+make your game, also if you never coded before. If you are an experienced coder, much of this will be familiar to you, but some things may still be useful.
@@ -423,6 +424,13 @@ Due to typeclasses staying so long in memory, stale caches of such relationships
visible than common in Django. See the closed issue #1098 and its
comments for examples and solutions.
+
+
Evennia does not re-use its #dbrefs. This means new objects get an ever-increasing #dbref, also if you delete older objects. There are technical and safety reasons for this. But you may wonder if this means you have to worry about a big game ‘running out’ of dbref integers eventually.
+
The answer is simply no.
+
For example, the max dbref value for the default sqlite3 database is 2**64. If you created 10 000 new objects every second of every minute of every day of the year it would take about 60 million years for you to run out of dbref numbers. That’s a database of 140 TeraBytes, just to store the dbrefs, no other data.
+
If you are still using Evennia at that point and has this concern, get back to us and we can discuss adding dbref reuse then.
diff --git a/docs/1.0-dev/Concepts/Colors.html b/docs/1.0-dev/Concepts/Colors.html
index 62c034317f..24d638195e 100644
--- a/docs/1.0-dev/Concepts/Colors.html
+++ b/docs/1.0-dev/Concepts/Colors.html
@@ -122,7 +122,7 @@ desired. Some clients don’t even support color - text games are also played wi
equipment by people who are blind or have otherwise diminished eyesight.
So a good rule of thumb is to use colour to enhance your game but don’t rely on it to display
critical information. If you are coding the game, you can add functionality to let users disable
-colours as they please, as described here.
There is an Understanding Color Tags tutorial which expands on the use of ANSI color tags and the pitfalls of mixing ANSI and Xterms256 color tags in the same context.
+
There is an Understanding Color Tags tutorial which expands on the use of ANSI color tags and the pitfalls of mixing ANSI and Xterms256 color tags in the same context.
@@ -105,9 +105,7 @@ become ANSI/XTerm256 color tags for Telnet connections and CSS information for t
Clickable links - This allows you to provide a text the user can click to execute an
in-game command. This is on the form |lccommand|lttext|le.
FuncParser callables - These are full-fledged function calls on the form $funcname(args,kwargs)
-that lead to calls to Python functions. The parser can be run with different available callables in different
-circumstances. The parser is run on all outgoing messages if settings.FUNCPARSER_PARSE_OUTGOING_MESSAGES_ENABLED=True
-(disabled by default).
+that lead to calls to Python functions. The parser can be run with different available callables in different circumstances. The parser is run on all outgoing messages if settings.FUNCPARSER_PARSE_OUTGOING_MESSAGES_ENABLED=True (disabled by default).
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.
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:
-
-# ...
-
-classCharacter(DefaultCharacter):
- # ...
-
- defat_pre_move(self,destination):
- """
- Called by self.move_to when trying to move somewhere. If this returns
- False, the move is immediately cancelled.
- """
- ifself.db.is_resting:
- self.msg("You can't go anywhere while resting.")
- returnFalse
- returnTrue
-
-
-
-
When moving somewhere, character.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.
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:
-
-fromevenniaimportDefaultObject
-
-classSittable(DefaultObject):
-
- defat_object_creation(self):
- self.db.sitter=None
-
- defdo_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
- ifcurrent:
- ifcurrent==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}")
-
- defdo_stand(self,stander):
- """
- Called when trying to stand from this object.
-
- Args:
- stander (Object): The one trying to stand up.
-
- """
- current=self.db.sitter
- ifnotstander==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:
-
-fromevenniaimportDefaultObject
-
-classSittable(DefaultObject):
-
- defat_object_creation(self):
- self.db.sitter=None
- # do you sit "on" or "in" this object?
- self.db.adjective="on"
-
- defdo_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
- ifcurrent:
- ifcurrent==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}")
-
- defdo_stand(self,stander):
- """
- Called when trying to stand from this object.
-
- Args:
- stander (Object): The one trying to stand up.
-
- """
- current=self.db.sitter
- ifnotstander==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.
-
-fromevenniaimportDefaultObject
-
-classSittable(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}
-
- """
- defat_object_creation(self):
- self.db.sitter=None
- # do you sit "on" or "in" this object?
- self.db.adjective="on"
-
- defdo_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
- ifcurrent:
- ifcurrent==sitter:
- ifself.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:
- ifself.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
- ifself.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}")
-
- defdo_stand(self,stander):
- """
- Called when trying to stand from this object.
-
- Args:
- stander (Object): The one trying to stand up.
-
- """
- current=self.db.sitter
- ifnotstander==current:
- ifself.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
- ifself.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!
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:
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:
-
fromevenniaimportDefaultObject
-fromcommands.sittablesimportCmdSetSit# <- new
-
-classSittable(DefaultObject):
- """
- (docstring)
- """
- defat_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.
-
-
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:
-
# ...
-
-classCmdNoSitStand(Command):
- """
- Sit down or Stand up
- """
- key="sit"
- aliases=["stand"]
-
- deffunc(self):
- ifself.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:
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
-differentsit 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 itsstand command.
-
We will fix this with a lock and a custom lockfunction. 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:
-
# ...
-
-classCmdStand(Command):
- """
- Stand up.
- """
- key="stand"
- lock="cmd:sitsonthis()"# < this is new
-
- deffunc(self):
- self.obj.do_stand(self.caller)
-# ...
-
-
-
We define a Lock 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 sitsonthislock function which doesn’t exist yet.
-
Open mygame/server/conf/lockfuncs.py to add it!
-
"""
-(module lockstring)
-"""
-# ...
-
-defsitsonthis(accessing_obj,accessed_obj,*args,**kwargs):
- """
- True if accessing_obj is sitting on/in the accessed_obj.
- """
- returnaccessed_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:
-
-
-
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.
Before we start with this, delete the chairs you’ve created (delarmchair 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.
-
fromevenniaimportCommand,CmdSet
-fromevenniaimportInterruptCommand# <- this is new
-
-classCmdSit(Command):
- # ...
-
-# ...
-
-# new from here
-
-classCmdSit2(Command):
- """
- Sit down.
-
- Usage:
- sit <sittable>
-
- """
- key="sit"
-
- defparse(self):
- self.args=self.args.strip()
- ifnotself.args:
- self.caller.msg("Sit on what?")
- raiseInterruptCommand
-
- deffunc(self):
-
- # self.search handles all error messages etc.
- sittable=self.caller.search(self.args)
- ifnotsittable:
- return
- try:
- sittable.do_sit(self.caller)
- exceptAttributeError:
- 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:
-
-
Command.at_pre_command - this is not used by default
-
Command.parse - this should parse the input
-
Command.func - this should implement the actual Command functionality
-
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 raiseInterruptCommand.
-
-
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 raiseInterruptCommand 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:
-
-try:
- # code
-exceptAttributeError:
- # 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
-sitwall or sitsword. 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...exceptAttributeError 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:
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.
-
-classCmdStand2(Command):
- """
- Stand up.
-
- Usage:
- stand
-
- """
- key="stand"
-
- deffunc(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
- ifnotsittable:
- 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 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 CmdsetontheCharacter.Openmygame/default_cmdsets.py`
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.
diff --git a/docs/1.0-dev/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Adding-Commands.html b/docs/1.0-dev/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Adding-Commands.html
index bb56051bbf..e00bbd03d0 100644
--- a/docs/1.0-dev/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Adding-Commands.html
+++ b/docs/1.0-dev/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Adding-Commands.html
@@ -356,26 +356,26 @@ You hit <target> with full force!
2223
# ...
-classCmdHit(Command):
- """
- Hit a target.
+classCmdHit(Command):
+"""
+ Hit a target. Usage: hit <target> """
- key="hit"
-
+key="hit"
+deffunc(self):
- args=self.args.strip()
- ifnotargs:
- self.caller.msg("Who do you want to hit?")
- return
- target=self.caller.search(args)
- ifnottarget:
- 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!")
+args=self.args.strip()
+ifnotargs:
+self.caller.msg("Who do you want to hit?")
+return
+target=self.caller.search(args)
+ifnottarget:
+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!")# ...
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:
+
We have already created some things - dragons for example. There are many different things to create in Evennia though. In the Typeclasses tutorial, we noted that there are 7 default Typeclasses coming with Evennia out of the box:
+
+
+
Evennia base typeclass
+
mygame.typeclasses child
+
description
+
+
+
+
evennia.DefaultObject
+
typeclasses.objects.Object
+
Everything with a location
+
+
evennia.DefaultCharacter (child of DefaultObject)
+
typeclasses.characters.Character
+
Player avatars
+
+
evennia.DefaultRoom (child of DefaultObject)
+
typeclasses.rooms.Room
+
In-game locations
+
+
evennia.DefaultExit (chld of DefaultObject)
+
typeclasses.exits.Exit
+
Links between rooms
+
+
evennia.DefaultAccount
+
typeclasses.accounts.Account
+
A player account
+
+
evennia.DefaultChannel
+
typeclasses.channels.Channel
+
In-game comms
+
+
evennia.DefaultScript
+
typeclasses.scripts.Script
+
Entities with no location
+
+
+
+
Given you have an imported Typeclass, there are four ways to create an instance of it:
Firstly, you can call the class directly, and then .save() it:
obj = SomeTypeClass(db_key=...)
@@ -119,42 +163,142 @@ in Evennia though. In the last lesson we learned about typeclasses, the way to m
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.
+the actual database field names, such as db_key instead of key as keyword arguments. This is closest to how a ‘normal’ Python class works, but is not recommended.
Secondly you can use the Evennia creation helpers:
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.
+
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. Each of the different base classes have their own creation function, like create_account and create_script etc.
-
Finally, you can create objects using an in-game command, such as
-
create/drop obj:path.to.SomeTypeClass
+
Thirdly, you can use the .create method on the Typeclass itself:
+
obj,err=SomeTypeClass.create(key=...)
-
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.
+
Since .create is a method on the typeclass, this form is useful if you want to customize how the creation process works for your custom typeclasses. Note that it returns two values - the obj is either the new object or None, in which case err should be a list of error-strings detailing what went wrong.
+
+
Finally, you can create objects using an in-game command, such as
+
create obj:path.to.SomeTypeClass
+
+
+
As a developer you are usually best off using the other methods, but a command is usually the only way to let regular players or builders without Python-access help build the game world.
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.
+
An Object 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.
+
Since we didn’t specify the typeclass as the first argument, the default given by settings.BASE_OBJECT_TYPECLASS (typeclasses.objects.Object out of the box) will be used.
+
The create_object has a lot of options. A more detailed example in code:
Characters, Rooms and Exits are all subclasses of DefaultObject. So there is for example no separate create_character, you just create characters with create_object pointing to the Character typeclass.
An Exit is a one-way link between rooms. For example, east could be an Exit between the Forest room and the Meadow room.
+
Meadow -> east -> Forest
+
+
+
The east exit has a key of east, a location of Meadow and a destination of Forest. If you wanted to be able to go back from Forest to Meadow, you’d need to create a new Exit, say, west, where location is Forest and destination is Meadow.
+
Meadow -> east -> Forest
+Forest -> west -> Meadow
+
+
+
In-game you do this with tunnel and dig commands, bit if you want to ever set up these links in code, you can do it like this:
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
+
Normally, you want to create the Account when a user authenticates. By default, this happens in the createaccount and login default commands in the UnloggedInCmdSet. This means that customizing this just means replacing those commands!
+
So normally you’d modify those commands rather than make something from scratch. But here’s the principle:
The inputs are usually taken from the player via the command. The email must be given, but can be None if you are not using it. The accountname must be globally unique on the server. The password is stored encrypted in the database. If typeclass is not given, the settings.BASE_ACCOUNT_TYPECLASS will be used (typeclasses.accounts.Account).
A Script is an entity that has no in-game location. It can be used to store arbitrary data and is often used for game systems that need persistent storage but which you can’t ‘look’ at in-game. Examples are economic systems, weather and combat handlers.
+
Scripts are multi-use and depending on what they do, a given script can either be ‘global’ or be attached “to” another object (like a Room or Character).
A convenient way to create global scripts is define them in the GLOBAL_SCRIPTS setting; Evennia will then make sure to initialize them. Scripts also have an optional ‘timer’ component. See the dedicated Script documentation for more info.
Any game will need peristent storage of data. This was a quick run-down of how to create each default type of typeclassed entity. If you make your own typeclasses (as children of the default ones), you create them in the same way.
+
Next we’ll learn how to find them again by searching for them in the database.
@@ -135,30 +135,20 @@ But sometimes you need to be more specific:
and if statements. But for something non-standard like this, querying the database directly will be
much more efficient.
Evennia uses Django to handle its connection to the database.
-A django queryset 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:
+A django queryset 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 variants of this 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
+
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 Weaponas well as all the
-subclasses of Weapon, such as Cannon, you need to use the _family type of
-query:
+
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 Weaponas well as all the subclasses of Weapon, such as Cannon, you need to use the _family type of query:
really_all_weapons = Weapon.objects.all_family()
@@ -192,23 +182,13 @@ get results out of it to be able to loop):
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.
+
From now on, the queryset is evaluated and we can’t keep adding more queries to it - we’d need to create a new queryset if we wanted to find some other result. Other ways to evaluate the queryset is to print it, convert it to a list with list() and otherwise try to access its results.
+
Note how we use db_key and db_location. This is the actual names of these database fields. By convention Evennia uses db_ in front of every database field. When you use the normal Evennia search helpers and objects you can skip the db_ but here we are calling the database directly and need to use the ‘real’ names.
Here are the most commonly used methods to use with the objects managers:
filter - query for a listing of objects based on search criteria. Gives empty queryset if none
@@ -216,57 +196,61 @@ 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.
+
filter_family - like filter, but search all subclasses as well.
+
get_family - like get, but search all subclasses 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.
+
All of Evennia search functions use querysets under the hood. The evennia.search_* functions actually return querysets (we have just been treating them as lists so far). This means you could in principle add a .filter query to the result of evennia.search_object to further refine the search.
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"
+
# 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 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.
This will find all flowers whose name contains the string "rose", like "roses", "wildrose" 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:
This will find all flowers whose name contains the string "rose", like "roses", "wildrose" 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:
We want to find Characters, so we access .objects on the Character typeclass.
-
We start to filter …
-
-
… by accessing the db_location field (usually this is a Room)
+
Line 4 We want to find Characters, so we access .objects on the Character typeclass.
+
We start to filter …
+
+
Line 6: … 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.
+
Line 7: … We also want only Characters with Attributes whose db_key is exactly "lycantrophy"
+
Line 8 :… 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 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.
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. It is
-imported from Django directly:
+
For queries using OR and NOT we need Django’s Q object. It is imported from Django directly:
from django.db.models import Q
@@ -332,10 +319,11 @@ imported from Django directly:
You can then use this Q instance as argument in a filter:
q1 = Q(db_key="foo")
Character.objects.filter(q1)
+# this is the same as
+Character.objects.filter(db_key="foo")
-
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
+
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.
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.
+
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 - we decide that if they have been newly bitten, they should also turn, regardless of their lycantrophy level (more dramatic that way!).
+
Let’s say that getting bitten means that you’ll get assigned a Tag recently_bitten.
This is how we’d change our query:
fromdjango.db.modelsimportQ
@@ -396,57 +380,67 @@ sure that there is only one instance of each Character in the result.
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?
+field on the object? An example would wanting to find rooms only containing five or more objects.
We could do it like this (don’t actually do it this way!):
Above we get all rooms and then use list.append() to keep adding the right
+
+
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.
+grows this will be unnecessarily compute-intensive. It’s much better to query the
+database directly
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:
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
+
+
Line 6-7: 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. The fact annotation means that now num_objects is avaiable to be used in other parts of the query.
+
Line 8 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.
+should be greater than or equal to 5.
+
+
Annotations can be a little harder to get one’s head around but much more efficient than lopping over all objects in Python.
What if we wanted to compare two 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.
-So-called F expressions allow you to do a query that looks at a value of each
-object in the database.
+objects that had a bigger inventory than they had tags (silly example, but …)?
+
This can be with Django’s F objects. So-called F expressions allow you to do a query that looks at a value of each object in the database.
fromdjango.db.modelsimportCount,Ffromtypeclasses.roomsimportRoom
@@ -465,34 +459,35 @@ condition to be calculated on the fly, completely within the database.
12.6. 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:
-
fromdjango.db.modelsimportCount
+
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:
… 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.
+
Line 6: … has a tag of category “organization” on them
+
Line 7:… 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
+
Line 8: … 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).
+
Line 9: … 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. For this we use the values_list method on the queryset. This will evaluate the queryset immediately.
-
The result queryset will be a list of tuples ordered in descending order by the number of matches,
+
The result will be a list of tuples ordered in descending order by the number of matches,
in a format like the following:
[('Griatch'spoetssociety', 3872),
@@ -506,11 +501,7 @@ in a format like the following:
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.
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 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.
+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:
classMonster:
diff --git a/docs/1.0-dev/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Making-A-Sittable-Object.html b/docs/1.0-dev/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Making-A-Sittable-Object.html
new file mode 100644
index 0000000000..38a8313e2a
--- /dev/null
+++ b/docs/1.0-dev/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Making-A-Sittable-Object.html
@@ -0,0 +1,943 @@
+
+
+
+
+
+
+
+
+ 13. Building a chair you can sit on — Evennia 1.0-dev documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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.
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:
+
# in mygame/typeclasses/characters.py
+
+# ...
+
+classCharacter(DefaultCharacter):
+ # ...
+
+ defat_pre_move(self,destination):
+ """
+ Called by self.move_to when trying to move somewhere. If this returns
+ False, the move is immediately cancelled.
+ """
+ ifself.db.is_resting:
+ self.msg("You need to stand up first.")
+ returnFalse
+ returnTrue
+
+
+
+
When moving somewhere, character.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.
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:
# in mygame/typeclasses/sittables.py
+
+fromtypeclasses.objectsimportObject
+
+classSittable(Object):
+
+defdo_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
+ifcurrent:
+ifcurrent==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 Attributesitter 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.
# add this right after the `do_sit method` in the same class
+
+ defdo_stand(self,stander):
+ """
+ Called when trying to stand from this object.
+
+ Args:
+ stander (Object): The one trying to stand up.
+
+ """
+ current=self.db.sitter
+ifnotstander==current:
+stander.msg(f"You are not sitting on {self.key}.")
+ else:
+self.db.sitter=None
+delstander.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.
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:
# in mygame/typeclasses/sittables.py
+
+fromevenniaimportDefaultObject
+
+classSittable(DefaultObject):
+
+ defdo_sit(self,sitter):
+ """
+ Called when trying to sit on/in this object.
+
+ Args:
+ sitter (Object): The one trying to sit down.
+
+ """
+adjective=self.db.adjectiveor"on"
+current=self.db.sitter
+ ifcurrent:
+ ifcurrent==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}")
+
+ defdo_stand(self,stander):
+ """
+ Called when trying to stand from this object.
+
+ Args:
+ stander (Object): The one trying to stand up.
+
+ """
+ current=self.db.sitter
+ ifnotstander==current:
+ stander.msg(f"You are not sitting {self.db.adjective}{self.key}.")
+ else:
+ self.db.sitting=None
+ delstander.db.is_sitting
+stander.msg(f"You stand up from {self.key}")
+
+
+
+
Line 15: We grab the adjective Attribute. Using seld.db.adjectiveor"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):
+
>setarmchair/adjective=in
+
+
+
Since we haven’t added the sit command yet, we must still use py to test:
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.
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:
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:
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 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):
+
+
> 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:
# after the other commands in mygame/commands/sittables.py
+# ...
+
+classCmdNoSitStand(Command):
+ """
+ Sit down or Stand up
+ """
+ key="sit"
+aliases=["stand"]
+
+ deffunc(self):
+ifself.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:
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 and a custom lockfunction. 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:
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 sitsonthislock function which doesn’t exist yet.
+
+
Open mygame/server/conf/lockfuncs.py to add it!
+
# mygame/server/conf/lockfuncs.py
+
+"""
+(module lockstring)
+"""
+# ...
+
+defsitsonthis(accessing_obj,accessed_obj,*args,**kwargs):
+ """
+ True if accessing_obj is sitting on/in the accessed_obj.
+ """
+ returnaccessed_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.
+
+
+
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.
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.
# in mygame/commands/sittables.py
+
+fromevenniaimportCommand,CmdSet
+fromevenniaimportInterruptCommand
+
+classCmdSit(Command):
+ # ...
+
+# ...
+
+# new from here
+
+classCmdSit2(Command):
+ """
+ Sit down.
+
+ Usage:
+ sit <sittable>
+
+ """
+ key="sit"
+
+ defparse(self):
+ self.args=self.args.strip()
+ ifnotself.args:
+ self.caller.msg("Sit on what?")
+raiseInterruptCommand
+
+ deffunc(self):
+
+ # self.search handles all error messages etc.
+sittable=self.caller.search(self.args)
+ifnotsittable:
+ return
+try:
+sittable.do_sit(self.caller)
+ exceptAttributeError:
+ self.caller.msg("You can't sit on that!")
+
+
+
+
+
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 raiseInterruptCommand. 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, 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.
# end of mygame/commands/sittables.py
+
+classCmdStand2(Command):
+ """
+ Stand up.
+
+ Usage:
+ stand
+
+ """
+ key="stand"
+
+ deffunc(self):
+
+ caller=self.caller
+ # if we are sitting, this should be set on us
+sittable=caller.db.is_sitting
+ifnotsittable:
+ 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.
> 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.
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!
+
+
+
\ No newline at end of file
diff --git a/docs/1.0-dev/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-More-on-Commands.html b/docs/1.0-dev/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-More-on-Commands.html
index 2a1dfc12ac..741f9cf751 100644
--- a/docs/1.0-dev/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-More-on-Commands.html
+++ b/docs/1.0-dev/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-More-on-Commands.html
@@ -122,7 +122,7 @@
also learn how to add, modify and extend Evennia’s default commands.
In the last lesson we made a hit Command and hit a dragon with it. You should have the code
+
In the last lesson we made a hit Command and struck 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>]
@@ -135,9 +135,50 @@ 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:
-
#...
+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:
+
#...classCmdHit(Command):"""
@@ -150,12 +191,12 @@ a little, in a new method key="hit"defparse(self):
- self.args=self.args.strip()
- target,*weapon=self.args.split(" with ",1)
- ifnotweapon:
- target,*weapon=target.split(" ",1)
- self.target=target.strip()
- ifweapon:
+self.args=self.args.strip()
+target,*weapon=self.args.split(" with ",1)
+ifnotweapon:
+target,*weapon=target.split(" ",1)
+self.target=target.strip()
+ifweapon:self.weapon=weapon.strip()else:self.weapon=""
@@ -165,28 +206,24 @@ a little, in a new method self.caller.msg("Who do you want to hit?")return# get the target for the hit
- target=self.caller.search(self.target)
- ifnottarget:
+target=self.caller.search(self.target)
+ifnottarget:return# get and handle the weaponweapon=Noneifself.weapon:
- weapon=self.caller.search(self.weapon)
- ifweapon:
+weapon=self.caller.search(self.weapon)
+ifweapon: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}!")
+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.
+
The parse method is a special one Evennia knows to call beforefunc. At this time it has access to all the same on-command variables as func does. 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.
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
-you’ve seen an example of this with the “Red Button” object. The Tutorial world
-also has many examples of objects with commands on them.
+
+
As we learned in the lesson about Adding commands, Commands are are grouped in Command Sets. Such Command Sets are attached to an object with obj.cmdset.add() and will then be available for that object to use.
+
What we didn’t mention before is that by default those commands are also available to those in the same location as that object. If you did the Building quickstart lesson you’ve seen an example of this with the “Red Button” object. The Tutorial world 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.
Woah, that didn’t go as planned. Evennia actually found twohit 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:
+
Woah, that didn’t go as planned. Evennia actually found twohit commands and 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
@@ -302,7 +341,7 @@ Who do you want to hit?
Who do you want to hit?
-
Now try this:
+
Now try making a new location and then drop the sword in it.
> tunnel n = kitchen
> n
> drop sword
@@ -314,24 +353,19 @@ Command 'hit' is not available. Maybe you meant ...
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.
+
The hit command is only available if you hold or are in the same room as 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.
+
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.
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:
+
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:
-
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:
+
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:
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.
+
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. Doing this this way is a little cumbersome though. It would be better to add this to all characters.
The default cmdset are defined in mygame/commands/default_cmdsets.py. Open that file now:
"""(module docstring)
@@ -427,14 +450,15 @@ All these commands are in just loaded on the default objects that Evennia provid
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.
+
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.
+
When the DefaultCharacter (or a child of it) is created, you’ll find that the equivalence of self.cmdset.add("default_cmdsets.CharacterCmdSet,persistent=True") gets called. This means that all new Characters get this cmdset. After adding more commands to it, you just need to reload to have all characters see it.
+
+
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) your Session have access to the UnloggedinCmdSet.
+
For now, let’s add our own hit and echo commands to the CharacterCmdSet:
# ...
@@ -460,8 +484,7 @@ to add a command with a key
-
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:
+
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 your own CmdSet to the other cmdset.
fromcommandsimportmycommandsclassCharacterCmdSet(default_cmds.CharacterCmdSet):
@@ -485,7 +508,7 @@ this is practical. A Command can be a part of any number of different CmdSets.
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.
+from Evennia. If you investigate the default_cmds.CharacterCmdSet parent, you’ll find that its class is default_cmds.CmdGet (the ‘real’ location is evennia.commands.default.general.CmdGet).
# ...fromcommandsimportmycommands
@@ -515,23 +538,28 @@ Command 'get' is not available ...
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.
-
# up top, by the other imports
-fromevenniaimportdefault_cmds
-
+
Let’s combine this with what we know about classes and how to override a parent class. Open mygame/commands/mycommands.py and make a new get command:
+
1
+2
+3
+4
+5
+6
+7
+8
+9
# up top, by the other imports
+fromevenniaimportdefault_cmds
+# somewhere belowclassMyCmdGet(default_cmds.CmdGet):
- deffunc(self):
- super().func()
- self.caller.msg(str(self.caller.location.contents))
-
-
Line2: We import default_cmds so we can get the parent class.
+
Line 2: We import default_cmds so we can get the parent class.
We made a new class and we make it inheritdefault_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,
@@ -565,6 +593,7 @@ has a special function # ...
+
We don’t need to use self.remove() first; just adding a command with the same key (get) will replace the default get we had from before.
-
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).
+
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 …).
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.
+
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. Knowing to add commands is a big part of making a game!
+
We have been beating on poor Smaug for too long. Next we’ll create more things to play around with.
diff --git a/docs/1.0-dev/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Searching-Things.html b/docs/1.0-dev/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Searching-Things.html
index 107f192465..54d292a86e 100644
--- a/docs/1.0-dev/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Searching-Things.html
+++ b/docs/1.0-dev/Howtos/Beginner-Tutorial/Part1/Beginner-Tutorial-Searching-Things.html
@@ -126,30 +126,31 @@ if we cannot find and use it afterwards.
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]
+
+```{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>`_.
-
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]
+
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 zero, one or more elements - all the matches to your search. To get the first match:
+
rose = roses[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")
+ ifnotthe_one_ring:
+ # handle not finding the ring at all
+ eliflen(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
@@ -157,46 +158,54 @@ else:
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:
+
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.
+
This searches for objects based on key or aliases. 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).
+
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:
fromevenniaimportCommand
-classMyCommand(Command):
+classCmdQuickFind(Command):
+ """
+ Find an item in your current location.
- key="findfoo"
+ Usage:
+ quickfind <query>
+
+ """
+
+ key="quickfind"deffunc(self):
-
- foo=self.caller.search("foo")
- ifnotfoo:
+ query=self.args
+ result=self.caller.search(query)
+ ifnotresultreturn
+ self.caller.msg(f"Found match for {query}: {foo}")
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.
+inherits from DefaultObject!
+
This simple little Command takes its arguments and searches for a match. If it can’t find it, result 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:
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:
+
This will only return a match if “MyStone” is in the room (or in your inventory) and is one of the four provided candidate objects. This is quite powerful, here’s how you’d find something only in your inventory:
@@ -204,8 +213,7 @@ here’s how you’d find something only in your inventory:
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!
+
With quiet=True the user will not be notified on zero or multi-match errors. Instead you are expected to handle this yourself. Furthermore, what is returned is now a list of zero, one or more matches!
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,
+
Only Objects (things inheriting from evennia.DefaultObject) has a location. The location 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,
Think of a Tag 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
+
Think of a Tag 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.
Tags can also have categories. By default this category is None which is also considered a category.
@@ -310,13 +317,15 @@ by-tag is generally faster.
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.
+
This last way of searching is a simple form of a Django query. This is a way to express SQL queries using Python. See the next lesson, where we’ll explore this way to searching in more detail.
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 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:
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.
-
-
+
In legacy code bases you may be used to relying a lot on #dbrefs to find and track things. Looking something up by #dbref can be practical - if used occationally. It is however considered bad practice to rely on hard-coded #dbrefs in Evennia. Especially to expect end users to know them. It makes your code fragile and hard to maintain, while tying your code to the exact layout of the database. In 99% of use cases you should organize your code such that you pass the actual objects around and search by key/tags/attribute instead.
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.
+
It’s important to understand how objects relate to one another when searching.
+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.
@@ -366,13 +383,22 @@ We can also find what is inside each object. This is a list of things.
door.destination is outside (or wherever the door leads)
room.destination is None (same for all the other non-exit objects)
+
You can also include this information in searches:
+
fromevenniaimportsearch_object
+
+# we assume only one match of each
+dungeons=search_object("dungeon",typeclass="typeclasses.rooms.Room")
+chests=search_object("chest",location=dungeons[0])
+# find if there are any skulls in the chest
+skulls=search_object("Skull",candidates=chests[0].contents)
+
+
+
More advanced, nested queries like this can however often be made more efficient by using the hints in the next lesson.
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.
+
Knowing how to find things is important and the tools from this section will serve you well. These tools will cover most of your needs …
+
… 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.
diff --git a/docs/1.0-dev/Howtos/Beginner-Tutorial/Part2/Beginner-Tutorial-Game-Planning.html b/docs/1.0-dev/Howtos/Beginner-Tutorial/Part2/Beginner-Tutorial-Game-Planning.html
index 8a7a04dce2..13faac8169 100644
--- a/docs/1.0-dev/Howtos/Beginner-Tutorial/Part2/Beginner-Tutorial-Game-Planning.html
+++ b/docs/1.0-dev/Howtos/Beginner-Tutorial/Part2/Beginner-Tutorial-Game-Planning.html
@@ -6,7 +6,7 @@
- On Planning a Game — Evennia 1.0-dev documentation
+ 2. On Planning a Game — Evennia 1.0-dev documentation
@@ -17,8 +17,8 @@
-
-
+
+
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.
@@ -143,7 +143,7 @@ later than to code in isolation until you burn out, lose interest or your hard d
Keep having fun. You must keep your motivation up, whichever way works for you.
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
@@ -166,7 +166,7 @@ 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.
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
@@ -289,7 +289,7 @@ get a chance to hear if some things are hard to understand or non-intuitive. Ma
to this feedback.
Once things stabilize in Alpha you can move to Beta and let more people in. Many MUDs are in
perpetual beta, meaning they are never considered
“finished”, but just repeat the cycle of Planning, Coding, Testing and Building over and over as new
@@ -311,12 +311,12 @@ features get implemented or Players come with suggestions. As the game designer
to gradually perfect your vision.
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.
@@ -173,7 +173,7 @@ Remembering that we need to keep the scope down, let’s establish some paramete
We want some sort of quest system and merchants to buy stuff from.
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.
@@ -188,9 +188,9 @@ Remembering that we need to keep the scope down, let’s establish some paramete
For the rest of this lesson we’ll answer and reason around the specific questions posed in the previous Game Planning lesson.
Should your game rules be enforced by coded systems by human game masters?¶
+
3.2.1. 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.
@@ -200,7 +200,7 @@ players is low.
We want EvAdventure to work entirely without depending on human GMs. That said, there’d be nothing stopping a GM from stepping in and run an adventure for some players should they want to.
-
What is the staff hierarchy in your game? Is vanilla Evennia roles enough or do you need something else?¶
+
3.2.2. 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
@@ -215,7 +215,7 @@ goes outside the regular hierarchy and should usually only.
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?¶
+
3.2.3. 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.
@@ -235,9 +235,9 @@ Channels are logged to a file and when you are coming back to the game you can v
-
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.
@@ -250,7 +250,7 @@ allows Evennia to apply and re-apply build-scripts that are raw Python modules.
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?¶
+
3.3.2. 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
@@ -260,9 +260,9 @@ code (such as what you can do with the
-
Do you base your game off an existing RPG system or make up your own?¶
+
3.4.1. 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
@@ -270,7 +270,7 @@ code (such as what you can do with the on this page.
-
What are the game mechanics? How do you decide if an action succeeds or fails?¶
+
3.4.2. 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.
@@ -290,7 +290,7 @@ You can have advantage or disadvantage on a roll. This means r
-
Does the flow of time matter in your game - does night and day change? What about seasons?¶
+
3.4.3. 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:
@@ -303,27 +303,27 @@ a few advantages with this:
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?¶
+
3.4.4. 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?¶
+
3.4.5. 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 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?¶
+
3.4.6. 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?¶
+
3.4.7. 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 Evennia contrib offers a ready system with everything needed for free emoting, recognizing people by their appearance and more.
EvAdventure Answer
@@ -331,69 +331,69 @@ the game world at random intervals just to show the principle.
-
Is a simple room description enough or should the description be able to change?¶
+
3.5.1. 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 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.
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?¶
+
3.5.3. 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.
How numerous are your objects? Do you want large loot-lists or are objects just role playing props?¶
+
3.6.1. 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?¶
+
3.6.2. 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?¶
+
3.6.3. 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)?¶
+
3.6.4. 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.
3.6.5. 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?¶
+
3.6.6. 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).
3.6.7. 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 contrib adds a full crafting system to any game. It’s based on Tags, meaning that pretty much any object can be made usable for crafting, even used in an unexpected way.
@@ -401,29 +401,29 @@ simplification, but with Python classes and inheritance, it’s not actually mor
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?
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?¶
+
3.6.9. 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?¶
+
3.6.10. _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.
Can players have more than one Character active at a time or are they allowed to multi-play?¶
+
3.7.1. 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 Characters, 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
@@ -443,7 +443,7 @@ can control each Character from multiple clients, seeing the same output from ea
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.
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.
@@ -456,14 +456,14 @@ using custom commands they will likely never use again after this.
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”?¶
+
3.7.3. 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?¶
+
3.7.4. 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
@@ -477,7 +477,7 @@ find the person (probably based on skill checks).
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?¶
+
3.7.5. 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:
@@ -500,7 +500,7 @@ you gain XP only for running when you run, XP for your axe skill when you fight
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*newlevel (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.
3.7.6. 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.
@@ -508,7 +508,7 @@ have to be very careful to balance the game - all characters does not have to be
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?¶
+
3.7.7. 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.
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.
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
@@ -126,7 +126,7 @@ to learn Evennia. If you just want to follow along with the technical bits you c
come back later when you feel ready to take on making your own game.
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.
@@ -163,7 +163,7 @@ timeline for release.
sure your eventual team is on the same page too.
-
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.
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.
@@ -209,7 +209,7 @@ build-tools and commands for them.
-
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:
@@ -239,7 +239,7 @@ If they build anything at all, it should be small test areas to agree on a homog
and literary style.
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
@@ -266,7 +266,7 @@ then try to answer those questions for the sake of creating our little tutorial
modules |
@@ -129,7 +129,7 @@ should be able to be changed over time. It makes sense to base it off Evennia’
DefaultCharacter Typeclass. The Character class is like a ‘character sheet’ in a tabletop
RPG, it will hold everything relevant to that PC.
-
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,
@@ -198,7 +198,7 @@ extra functionality all living things should be able to do. This is an example o
since it can also get confusing to follow the code.
Create a new module mygame/evadventure/characters.py
@@ -260,7 +260,7 @@ since it can also get confusing to follow the code.
in the mixin means we can expect these methods to be available for all living things.
-
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:
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.
@@ -481,7 +481,7 @@ an Attribute on your Character:
character generation to check and include what these classes mean.
-
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.