Further updates
This commit is contained in:
parent
830f793aa4
commit
a515ececff
11 changed files with 856 additions and 540 deletions
|
|
@ -10,7 +10,7 @@ Here are some pointers to get you going.
|
||||||
|
|
||||||
Evennia is developed using Python. Even if you are more of a designer than a coder, it is wise to
|
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,
|
learn how to read and understand basic Python code. If you are new to Python, or need a refresher,
|
||||||
take a look at our two-part [Python introduction](Part1/Python-basic-introduction).
|
take a look at our two-part [Python introduction](../Howto/Starting/Part1/Python-basic-introduction).
|
||||||
|
|
||||||
### Explore Evennia interactively
|
### Explore Evennia interactively
|
||||||
|
|
||||||
|
|
@ -31,7 +31,7 @@ This will open an Evennia-aware python shell (using ipython). From within this s
|
||||||
evennia.<TAB>
|
evennia.<TAB>
|
||||||
|
|
||||||
That is, enter `evennia.` and press the `<TAB>` key. This will show you all the resources made
|
That is, enter `evennia.` and press the `<TAB>` key. This will show you all the resources made
|
||||||
available at the top level of Evennia's "flat API". See the [flat API](../../Evennia-API) page for more
|
available at the top level of Evennia's "flat API". See the [flat API](../Evennia-API) page for more
|
||||||
info on how to explore it efficiently.
|
info on how to explore it efficiently.
|
||||||
|
|
||||||
You can complement your exploration by peeking at the sections of the much more detailed [Developer
|
You can complement your exploration by peeking at the sections of the much more detailed [Developer
|
||||||
|
|
@ -52,7 +52,7 @@ using such a checker can be a good start to weed out the simple problems.
|
||||||
|
|
||||||
### Plan before you code
|
### Plan before you code
|
||||||
|
|
||||||
Before you start coding away at your dream game, take a look at our [Game Planning](Game-Planning)
|
Before you start coding away at your dream game, take a look at our [Game Planning](../Howto/Starting/Game-Planning)
|
||||||
page. It might hopefully help you avoid some common pitfalls and time sinks.
|
page. It might hopefully help you avoid some common pitfalls and time sinks.
|
||||||
|
|
||||||
### Code in your game folder, not in the evennia/ repository
|
### Code in your game folder, not in the evennia/ repository
|
||||||
|
|
@ -64,7 +64,7 @@ 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
|
If you find that Evennia doesn't support some functionality you need, make a [Feature
|
||||||
Request](feature-request) about it. Same goes for [bugs][bug]. If you add features or fix bugs
|
Request](feature-request) about it. Same goes for [bugs][bug]. If you add features or fix bugs
|
||||||
yourself, please consider [Contributing](../../Contributing) your changes upstream!
|
yourself, please consider [Contributing](../Contributing) your changes upstream!
|
||||||
|
|
||||||
### Learn to read tracebacks
|
### Learn to read tracebacks
|
||||||
|
|
||||||
|
|
@ -1,109 +0,0 @@
|
||||||
# Adding Object Typeclass Tutorial
|
|
||||||
|
|
||||||
Evennia comes with a few very basic classes of in-game entities:
|
|
||||||
|
|
||||||
DefaultObject
|
|
||||||
|
|
|
||||||
DefaultCharacter
|
|
||||||
DefaultRoom
|
|
||||||
DefaultExit
|
|
||||||
DefaultChannel
|
|
||||||
|
|
||||||
When you create a new Evennia game (with for example `evennia --init mygame`) Evennia will
|
|
||||||
automatically create empty child classes `Object`, `Character`, `Room` and `Exit` respectively. They
|
|
||||||
are found `mygame/typeclasses/objects.py`, `mygame/typeclasses/rooms.py` etc.
|
|
||||||
|
|
||||||
> Technically these are all [Typeclassed](../../Component/Typeclasses), which can be ignored for now. In
|
|
||||||
> `mygame/typeclasses` are also base typeclasses for out-of-character things, notably
|
|
||||||
> [Channels](../../Component/Communications), [Accounts](../../Component/Accounts) and [Scripts](../../Component/Scripts). We don't cover those in
|
|
||||||
> this tutorial.
|
|
||||||
|
|
||||||
For your own game you will most likely want to expand on these very simple beginnings. It's normal
|
|
||||||
to want your Characters to have various attributes, for example. Maybe Rooms should hold extra
|
|
||||||
information or even *all* Objects in your game should have properties not included in basic Evennia.
|
|
||||||
|
|
||||||
## Change Default Rooms, Exits, Character Typeclass
|
|
||||||
|
|
||||||
This is the simplest case.
|
|
||||||
|
|
||||||
The default build commands of a new Evennia game is set up to use the `Room`, `Exit` and `Character`
|
|
||||||
classes found in the same-named modules under `mygame/typeclasses/`. By default these are empty and
|
|
||||||
just implements the default parents from the Evennia library (`DefaultRoom`etc). Just add the
|
|
||||||
changes you want to these classes and run `@reload` to add your new functionality.
|
|
||||||
|
|
||||||
## Create a new type of object
|
|
||||||
|
|
||||||
Say you want to create a new "Heavy" object-type that characters should not have the ability to pick
|
|
||||||
up.
|
|
||||||
|
|
||||||
1. Edit `mygame/typeclasses/objects.py` (you could also create a new module there, named something
|
|
||||||
like `heavy.py`, that's up to how you want to organize things).
|
|
||||||
1. Create a new class inheriting at any distance from `DefaultObject`. It could look something like
|
|
||||||
this:
|
|
||||||
```python
|
|
||||||
# end of file mygame/typeclasses/objects.py
|
|
||||||
from evennia import DefaultObject
|
|
||||||
|
|
||||||
class Heavy(DefaultObject):
|
|
||||||
"Heavy object"
|
|
||||||
def at_object_creation(self):
|
|
||||||
"Called whenever a new object is created"
|
|
||||||
# lock the object down by default
|
|
||||||
self.locks.add("get:false()")
|
|
||||||
# the default "get" command looks for this Attribute in order
|
|
||||||
# to return a customized error message (we just happen to know
|
|
||||||
# this, you'd have to look at the code of the 'get' command to
|
|
||||||
# find out).
|
|
||||||
self.db.get_err_msg = "This is too heavy to pick up."
|
|
||||||
```
|
|
||||||
1. Once you are done, log into the game with a build-capable account and do `@create/drop
|
|
||||||
rock:objects.Heavy` to drop a new heavy "rock" object in your location. Next try to pick it up
|
|
||||||
(`@quell` yourself first if you are a superuser). If you get errors, look at your log files where
|
|
||||||
you will find the traceback. The most common error is that you have some sort of syntax error in
|
|
||||||
your class.
|
|
||||||
|
|
||||||
Note that the [Locks](../../Component/Locks) and [Attribute](../../Component/Attributes) which are set in the typeclass could just
|
|
||||||
as well have been set using commands in-game, so this is a *very* simple example.
|
|
||||||
|
|
||||||
## Storing data on initialization
|
|
||||||
|
|
||||||
The `at_object_creation` is only called once, when the object is first created. This makes it ideal
|
|
||||||
for database-bound things like [Attributes](../../Component/Attributes). But sometimes you want to create temporary
|
|
||||||
properties (things that are not to be stored in the database but still always exist every time the
|
|
||||||
object is created). Such properties can be initialized in the `at_init` method on the object.
|
|
||||||
`at_init` is called every time the object is loaded into memory.
|
|
||||||
|
|
||||||
> Note: It's usually pointless and wasteful to assign database data in `at_init`, since this will
|
|
||||||
> hit the database with the same value over and over. Put those in `at_object_creation` instead.
|
|
||||||
|
|
||||||
You are wise to use `ndb` (non-database Attributes) to store these non-persistent properties, since
|
|
||||||
ndb-properties are protected against being cached out in various ways and also allows you to list
|
|
||||||
them using various in-game tools:
|
|
||||||
|
|
||||||
```python
|
|
||||||
def at_init(self):
|
|
||||||
self.ndb.counter = 0
|
|
||||||
self.ndb.mylist = []
|
|
||||||
```
|
|
||||||
|
|
||||||
> Note: As mentioned in the [Typeclasses](../../Component/Typeclasses) documentation, `at_init` replaces the use of
|
|
||||||
> the standard `__init__` method of typeclasses due to how the latter may be called in situations
|
|
||||||
> other than you'd expect. So use `at_init` where you would normally use `__init__`.
|
|
||||||
|
|
||||||
|
|
||||||
## Updating existing objects
|
|
||||||
|
|
||||||
If you already have some `Heavy` objects created and you add a new `Attribute` in
|
|
||||||
`at_object_creation`, you will find that those existing objects will not have this Attribute. This
|
|
||||||
is not so strange, since `at_object_creation` is only called once, it will not be called again just
|
|
||||||
because you update it. You need to update existing objects manually.
|
|
||||||
|
|
||||||
If the number of objects is limited, you can use `@typeclass/force/reload objectname` to force a
|
|
||||||
re-load of the `at_object_creation` method (only) on the object. This case is common enough that
|
|
||||||
there is an alias `@update objectname` you can use to get the same effect. If there are multiple
|
|
||||||
objects you can use `@py` to loop over the objects you need:
|
|
||||||
|
|
||||||
```
|
|
||||||
@py from typeclasses.objects import Heavy; [obj.at_object_creation() for obj in Heavy.objects.all()]
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
@ -10,7 +10,7 @@ Started](Getting-Started) instructions. You should have initialized a new game f
|
||||||
`evennia --init foldername` command. We will in the following assume this folder is called
|
`evennia --init foldername` command. We will in the following assume this folder is called
|
||||||
"mygame".
|
"mygame".
|
||||||
|
|
||||||
It might be a good idea to eye through the brief [Coding Introduction](Coding-Introduction) too
|
It might be a good idea to eye through the brief [Coding Introduction](../../Coding/Coding-Introduction) too
|
||||||
(especially the recommendations in the section about the evennia "flat" API and about using `evennia
|
(especially the recommendations in the section about the evennia "flat" API and about using `evennia
|
||||||
shell` will help you here and in the future).
|
shell` will help you here and in the future).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
# Adding Command Tutorial
|
# Our own commands
|
||||||
|
|
||||||
[prev lesson](Python-classes-and-objects) | [next lesson]()
|
[prev lesson](Python-classes-and-objects) | [next lesson](More-on-Commands)
|
||||||
|
|
||||||
|
In this lesson we'll learn how to create our own Evennia _Commands_. If you are new to Python you'll
|
||||||
|
also learn some more basics about how to manipulate strings and get information out of Evennia.
|
||||||
|
|
||||||
A Command is something that handles the input from a user and causes a result to happen.
|
A Command is something that handles the input from a user and causes a result to happen.
|
||||||
An example is `look`, which examines your current location and tells how it looks like and
|
An example is `look`, which examines your current location and tells how it looks like and
|
||||||
|
|
@ -10,16 +13,17 @@ In Evennia, a Command is a Python _class_. If you are unsure about what a class
|
||||||
previous lesson. A Command inherits from `evennia.Command` or from one of the alternative command-
|
previous lesson. A Command inherits from `evennia.Command` or from one of the alternative command-
|
||||||
classes, such as `MuxCommand` which is what most default commands use.
|
classes, such as `MuxCommand` which is what most default commands use.
|
||||||
|
|
||||||
All Commands are in turn grouped in another class called a _Command Set_. Think of a Command set
|
All Commands are in turn grouped in another class called a _Command Set_. Think of a Command Set
|
||||||
as a bag holding many different commands. One CmdSet could for example hold all commands for
|
as a bag holding many different commands. One CmdSet could for example hold all commands for
|
||||||
combat, another for building etc.
|
combat, another for building etc. By default, Evennia groups all character-commands into one
|
||||||
|
big cmdset.
|
||||||
|
|
||||||
Command-Sets are then associated with objects. Doing so makes the commands in that cmdset available
|
Command-Sets are then associated with objects. Doing so makes the commands in that cmdset available
|
||||||
to the object. So, to summarize:
|
to the object. So, to summarize:
|
||||||
|
|
||||||
- Commands are classes
|
- Commands are classes
|
||||||
- A group of Commands is stored in a CmdSet
|
- A group of Commands is stored in a CmdSet
|
||||||
- Putting a CmdSet on an object makes all commands in it available to the object
|
- CmdSets are stored on objects - this defines which commands are available to that object.
|
||||||
|
|
||||||
## Creating a custom command
|
## Creating a custom command
|
||||||
|
|
||||||
|
|
@ -373,271 +377,14 @@ you could try to hit it (if you dare):
|
||||||
|
|
||||||
You won't see the second string. Only Smaug sees that (and is not amused).
|
You won't see the second string. Only Smaug sees that (and is not amused).
|
||||||
|
|
||||||
## More advanced parsing
|
|
||||||
|
|
||||||
Let's expand our simple `hit` command to accept a little more complex input:
|
## Summary
|
||||||
|
|
||||||
hit <target> [[with] <weapon>]
|
In this lesson we learned how to create our own Command, add it to a CmdSet and then ourselves.
|
||||||
|
We also upset a dragon.
|
||||||
That is, we want to support all of these forms
|
|
||||||
|
|
||||||
hit target
|
In the next lesson we'll learn how to hit Smaug with different weapons. We'll also
|
||||||
hit target weapon
|
get into how we replace and extend Evennia's default Commands.
|
||||||
hit target with weapon
|
|
||||||
|
|
||||||
If you don't specify a weapon you'll use your fists. It's also nice to be able to skip "with" if
|
|
||||||
you are in a hurry. Time to modify `mygame/commands/mycommands.py` again. Let us break out the parsing
|
|
||||||
a little, in a new method `parse`:
|
|
||||||
|
|
||||||
|
|
||||||
```python
|
[prev lesson](Python-classes-and-objects) | [next lesson](More-on-Commands)
|
||||||
#...
|
|
||||||
|
|
||||||
class CmdHit(Command):
|
|
||||||
"""
|
|
||||||
Hit a target.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
hit <target>
|
|
||||||
|
|
||||||
"""
|
|
||||||
key = "hit"
|
|
||||||
|
|
||||||
def parse(self):
|
|
||||||
self.args = self.args.strip()
|
|
||||||
target, *weapon = self.args.split(" with ", 1)
|
|
||||||
if not weapon:
|
|
||||||
target, *weapon = target.split(" ", 1)
|
|
||||||
self.target = target.strip()
|
|
||||||
if weapon:
|
|
||||||
self.weapon = weapon.strip()
|
|
||||||
else:
|
|
||||||
self.weapon = ""
|
|
||||||
|
|
||||||
def func(self):
|
|
||||||
if not self.args:
|
|
||||||
self.caller.msg("Who do you want to hit?")
|
|
||||||
return
|
|
||||||
# get the target for the hit
|
|
||||||
target = self.caller.search(self.target)
|
|
||||||
if not target:
|
|
||||||
return
|
|
||||||
# get and handle the weapon
|
|
||||||
weapon = None
|
|
||||||
if self.weapon:
|
|
||||||
weapon = self.caller.search(self.weapon)
|
|
||||||
if weapon:
|
|
||||||
weaponstr = f"{weapon.key}"
|
|
||||||
else:
|
|
||||||
weaponstr = "bare fists"
|
|
||||||
|
|
||||||
self.caller.msg(f"You hit {target.key} with {weaponstr}!")
|
|
||||||
target.msg(f"You got hit by {self.caller.key} with {weaponstr}!")
|
|
||||||
# ...
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
The `parse` method is called before `func` and has access to all the same on-command variables as in `func`. Using
|
|
||||||
`parse` not only makes things a little easier to read, it also means you can easily let other Commands _inherit_
|
|
||||||
your parsing - if you wanted some other Command to also understand input on the form `<arg> with <arg>` you'd inherit
|
|
||||||
from this class and just implement the `func` needed for that command without implementing `parse` anew.
|
|
||||||
|
|
||||||
```sidebar:: Tuples and Lists
|
|
||||||
|
|
||||||
- A `list` is written as `[a, b, c, d, ...]`. You can add and grow/shrink a list after it was first created.
|
|
||||||
- A `tuple` is written as `(a, b, c, d, ...)`. A tuple cannot be modified once it is created.
|
|
||||||
|
|
||||||
```
|
|
||||||
- **Line 14** - We do the stripping of `self.args` once and for all here. We also store the stripped version back
|
|
||||||
into `self.args`, overwriting it. So there is no way to get back the non-stripped version from here on, which is fine
|
|
||||||
for this command.
|
|
||||||
- **Line 15** - This makes use of the `.split` method of strings. `.split` will, well, split the string by some criterion.
|
|
||||||
`.split(" with ", 1)` means "split the string once, around the substring `" with "` if it exists". The result
|
|
||||||
of this split is a _list_. Just how that list looks depends on the string we are trying to split:
|
|
||||||
1. If we entered just `hit smaug`, we'd be splitting just `"smaug"` which would give the result `["smaug"]`.
|
|
||||||
2. `hit smaug sword` gives `["smaug sword"]`
|
|
||||||
3. `hit smaug with sword` gives `["smaug", "sword"]`
|
|
||||||
|
|
||||||
So we get a list of 1 or 2 elements. We assign it to two variables like this, `target, *weapon = `. That
|
|
||||||
asterisk in `*weapon` is a nifty trick - it will automatically become a list of _0 or more_ values. It sorts of
|
|
||||||
"soaks" up everything left over.
|
|
||||||
1. `target` becomes `"smaug"` and `weapon` becomes `[]`
|
|
||||||
2. `target` becomes `"smaug sword"` and `weapon` becomes `[]`
|
|
||||||
3. `target` becomes `"smaug"` and `weapon` becomes `sword`
|
|
||||||
- **Lines 16-17** - In this `if` condition we check if `weapon` is falsy (that is, the empty list). This can happen
|
|
||||||
under two conditions (from the example above):
|
|
||||||
1. `target` is simply `smaug`
|
|
||||||
2. `target` is `smaug sword`
|
|
||||||
|
|
||||||
To separate these cases we split `target` once again, this time by empty space `" "`. Again we store the
|
|
||||||
result back with `target, *weapon =`. The result will be one of the following:
|
|
||||||
1. `target` remains `smaug` and `weapon` remains `[]`
|
|
||||||
2. `target` becomes `smaug` and `weapon` becomes `sword`
|
|
||||||
- **Lines 18-22** - We now store `target` and `weapon` into `self.target` and `self.weapon`. We must do this in order
|
|
||||||
for these local variables to made available in `func` later. Note how we need to check so `weapon` is not falsy
|
|
||||||
before running `strip()` on it. This is because we know that if it's falsy, it's an empty list `[]` and lists
|
|
||||||
don't have the `.strip()` method on them (so if we tried to use it, we'd get an error).
|
|
||||||
|
|
||||||
Now onto the `func` method. The main difference is we now have `self.target` and `self.weapon` available for
|
|
||||||
convenient use.
|
|
||||||
- **Lines 29 and 35** - We make use of the previously parsed search terms for the target and weapon to find the
|
|
||||||
respective resource.
|
|
||||||
- **Lines 34-39** - Since the weapon is optional, we need to supply a default (use our fists!) if it's not set. We
|
|
||||||
use this to create a `weaponstr` that is different depending on if we have a weapon or not.
|
|
||||||
- **Lines 41-42** - We merge the `weaponstr` with our attack text.
|
|
||||||
|
|
||||||
Let's try it out!
|
|
||||||
|
|
||||||
> reload
|
|
||||||
> hit smaug with sword
|
|
||||||
Could not find 'sword'.
|
|
||||||
You hit smaug with bare fists!
|
|
||||||
|
|
||||||
Oops, our `self.caller.search(self.weapon)` is telling us that it found no sword. Since we are not `return`ing
|
|
||||||
in this situation (like we do if failing to find `target`) we still continue fighting with our bare hands.
|
|
||||||
This won't do. Let's make ourselves a sword.
|
|
||||||
|
|
||||||
> create sword
|
|
||||||
|
|
||||||
Since we didn't specify `/drop`, the sword will end up in our inventory and can seen with the `i` or
|
|
||||||
`inventory` command. The `.search` helper will still find it there. There is no need to reload to see this
|
|
||||||
change (no code changed, only stuff in the database).
|
|
||||||
|
|
||||||
> hit smaug with sword
|
|
||||||
You hit smaug with sword!
|
|
||||||
|
|
||||||
|
|
||||||
## Adding the Command to a default Cmdset
|
|
||||||
|
|
||||||
|
|
||||||
For now, let's drop MyCmdSet:
|
|
||||||
|
|
||||||
> py self.cmdset.remove("commands.mycommands.MyCmdSet")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
The command is not available to use until it is part of a [Command Set](../../../Component/Command-Sets). In this
|
|
||||||
example we will go the easiest route and add it to the default Character commandset that already
|
|
||||||
exists.
|
|
||||||
|
|
||||||
1. Edit `mygame/commands/default_cmdsets.py`
|
|
||||||
1. Import your new command with `from commands.command import CmdEcho`.
|
|
||||||
1. Add a line `self.add(CmdEcho())` to `CharacterCmdSet`, in the `at_cmdset_creation` method (the
|
|
||||||
template tells you where).
|
|
||||||
|
|
||||||
This is approximately how it should look at this point:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# file mygame/commands/default_cmdsets.py
|
|
||||||
#[...]
|
|
||||||
from commands.command import CmdEcho
|
|
||||||
#[...]
|
|
||||||
class CharacterCmdSet(default_cmds.CharacterCmdSet):
|
|
||||||
|
|
||||||
key = "DefaultCharacter"
|
|
||||||
|
|
||||||
def at_cmdset_creation(self):
|
|
||||||
|
|
||||||
# this first adds all default commands
|
|
||||||
super(DefaultSet, self).at_cmdset_creation()
|
|
||||||
|
|
||||||
# all commands added after this point will extend or
|
|
||||||
# overwrite the default commands.
|
|
||||||
self.add(CmdEcho())
|
|
||||||
```
|
|
||||||
|
|
||||||
Next, run the `@reload` command. You should now be able to use your new `echo` command from inside
|
|
||||||
the game. Use `help echo` to see the documentation for the command.
|
|
||||||
|
|
||||||
If you have trouble, make sure to check the log for error messages (probably due to syntax errors in
|
|
||||||
your command definition).
|
|
||||||
|
|
||||||
> Note: Typing `echotest` will also work. It will be handled as the command `echo` directly followed
|
|
||||||
by
|
|
||||||
its argument `test` (which will end up in `self.args). To change this behavior, you can add the
|
|
||||||
`arg_regex` property alongside `key`, `help_category` etc. [See the arg_regex
|
|
||||||
documentation](Commands#on-arg_regex) for more info.
|
|
||||||
|
|
||||||
If you want to overload existing default commands (such as `look` or `get`), just add your new
|
|
||||||
command with the same key as the old one - it will then replace it. Just remember that you must use
|
|
||||||
`@reload` to see any changes.
|
|
||||||
|
|
||||||
See [Commands](../../../Component/Commands) for many more details and possibilities when defining Commands and using
|
|
||||||
Cmdsets in various ways.
|
|
||||||
|
|
||||||
|
|
||||||
## Adding the command to specific object types
|
|
||||||
|
|
||||||
Adding your Command to the `CharacterCmdSet` is just one easy exapmple. The cmdset system is very
|
|
||||||
generic. You can create your own cmdsets (let's say in a module `mycmdsets.py`) and add them to
|
|
||||||
objects as you please (how to control their merging is described in detail in the [Command Set
|
|
||||||
documentation](Command-Sets)).
|
|
||||||
|
|
||||||
```python
|
|
||||||
# file mygame/commands/mycmdsets.py
|
|
||||||
#[...]
|
|
||||||
from commands.command import CmdEcho
|
|
||||||
from evennia import CmdSet
|
|
||||||
#[...]
|
|
||||||
class MyCmdSet(CmdSet):
|
|
||||||
|
|
||||||
key = "MyCmdSet"
|
|
||||||
|
|
||||||
def at_cmdset_creation(self):
|
|
||||||
self.add(CmdEcho())
|
|
||||||
```
|
|
||||||
Now you just need to add this to an object. To test things (as superuser) you can do
|
|
||||||
|
|
||||||
@py self.cmdset.add("mycmdsets.MyCmdSet")
|
|
||||||
|
|
||||||
This will add this cmdset (along with its echo command) to yourself so you can test it. Note that
|
|
||||||
you cannot add a single Command to an object on its own, it must be part of a CommandSet in order to
|
|
||||||
do so.
|
|
||||||
|
|
||||||
The Command you added is not there permanently at this point. If you do a `@reload` the merger will
|
|
||||||
be gone. You *could* add the `permanent=True` keyword to the `cmdset.add` call. This will however
|
|
||||||
only make the new merged cmdset permanent on that *single* object. Often you want *all* objects of
|
|
||||||
this particular class to have this cmdset.
|
|
||||||
|
|
||||||
To make sure all new created objects get your new merged set, put the `cmdset.add` call in your
|
|
||||||
custom [Typeclasses](../../../Component/Typeclasses)' `at_object_creation` method:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# e.g. in mygame/typeclasses/objects.py
|
|
||||||
|
|
||||||
from evennia import DefaultObject
|
|
||||||
class MyObject(DefaultObject):
|
|
||||||
|
|
||||||
def at_object_creation(self):
|
|
||||||
"called when the object is first created"
|
|
||||||
self.cmdset.add("mycmdset.MyCmdSet", permanent=True)
|
|
||||||
```
|
|
||||||
|
|
||||||
All new objects of this typeclass will now start with this cmdset and it will survive a `@reload`.
|
|
||||||
|
|
||||||
*Note:* An important caveat with this is that `at_object_creation` is only called *once*, when the
|
|
||||||
object is first created. This means that if you already have existing objects in your databases
|
|
||||||
using that typeclass, they will not have been initiated the same way. There are many ways to update
|
|
||||||
them; since it's a one-time update you can usually just simply loop through them. As superuser, try
|
|
||||||
the following:
|
|
||||||
|
|
||||||
@py from typeclasses.objects import MyObject; [o.cmdset.add("mycmdset.MyCmdSet") for o in
|
|
||||||
MyObject.objects.all()]
|
|
||||||
|
|
||||||
This goes through all objects in your database having the right typeclass, adding the new cmdset to
|
|
||||||
each. The good news is that you only have to do this if you want to post-add *cmdsets*. If you just
|
|
||||||
want to add a new *command*, you can simply add that command to the cmdset's `at_cmdset_creation`
|
|
||||||
and `@reload` to make the Command immediately available.
|
|
||||||
|
|
||||||
## Change where Evennia looks for command sets
|
|
||||||
|
|
||||||
Evennia uses settings variables to know where to look for its default command sets. These are
|
|
||||||
normally not changed unless you want to re-organize your game folder in some way. For example, the
|
|
||||||
default character cmdset defaults to being defined as
|
|
||||||
|
|
||||||
CMDSET_CHARACTER="commands.default_cmdset.CharacterCmdSet"
|
|
||||||
|
|
||||||
|
|
||||||
[prev lesson](Python-classes-and-objects) | [next lesson]()
|
|
||||||
|
|
|
||||||
303
docs/source/Howto/Starting/Part1/Learning-Typeclasses.md
Normal file
303
docs/source/Howto/Starting/Part1/Learning-Typeclasses.md
Normal file
|
|
@ -0,0 +1,303 @@
|
||||||
|
# Persistent objects and typeclasses
|
||||||
|
|
||||||
|
[prev lesson](Python-classes-and-objects) | [next lesson](Adding-Commands)
|
||||||
|
|
||||||
|
In the last lesson we created the dragons Fluffy, Cuddly and Smaug and made the fly and breathe fire. We
|
||||||
|
learned a bit about _classes_ in the process. But 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/mobile.py` so far:
|
||||||
|
|
||||||
|
|
||||||
|
```python
|
||||||
|
|
||||||
|
class Mobile:
|
||||||
|
"""
|
||||||
|
This is a base class for Mobiles.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, key):
|
||||||
|
self.key = key
|
||||||
|
|
||||||
|
def move_around(self):
|
||||||
|
print(f"{self.key} is moving!")
|
||||||
|
|
||||||
|
|
||||||
|
class Dragon(Mobile):
|
||||||
|
"""
|
||||||
|
This is a dragon-specific mobile.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def move_around(self):
|
||||||
|
super().move_around()
|
||||||
|
print("The world trembles.")
|
||||||
|
|
||||||
|
def firebreath(self):
|
||||||
|
"""
|
||||||
|
Let our dragon breathe fire.
|
||||||
|
"""
|
||||||
|
print(f"{self.key} breathes fire!")
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Our first persistent object
|
||||||
|
|
||||||
|
Now we should know enough to understand what is happening in `mygame/typeclasses/objects.py`.
|
||||||
|
Open it again:
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""
|
||||||
|
module docstring
|
||||||
|
"""
|
||||||
|
from evennia import DefaultObject
|
||||||
|
|
||||||
|
class Object(DefaultObject):
|
||||||
|
"""
|
||||||
|
class docstring
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
So we have a class `Object` that _inherits_ from `DefaultObject`, which we have imported from Evennia.
|
||||||
|
The class itself doesn't do anything (it just `pass`es) but that doesn't mean it's useless. As we've seen,
|
||||||
|
it inherits all the functionality of its parent. It's in fact an _exact replica_ of `DefaultObject` right now.
|
||||||
|
If we knew what kind of methods and resources were available on `DefaultObject` we could add our own and
|
||||||
|
change the way it works!
|
||||||
|
|
||||||
|
> Hint: We will get back to this, but to learn what resources an Evennia parent like `DefaultObject` offers,
|
||||||
|
> easiest is to peek at its [API documentation](api:evennia.objects.objects#DefaultObject). The docstring for
|
||||||
|
> the `Object` class can also help.
|
||||||
|
|
||||||
|
One thing that Evennia offers and which you don't get with vanilla Python classes is _persistence_. As you've
|
||||||
|
found, Fluffy, Cuddly and Smaug are gone once we reload the server. Let's see if we can fix this.
|
||||||
|
|
||||||
|
Go back to `mygame/typeclasses/mobile.py`. Change it as follows:
|
||||||
|
|
||||||
|
```python
|
||||||
|
|
||||||
|
from typeclasses.objects import Object
|
||||||
|
|
||||||
|
class Mobile(Object):
|
||||||
|
"""
|
||||||
|
This is a base class for Mobiles.
|
||||||
|
"""
|
||||||
|
def move_around(self):
|
||||||
|
print(f"{self.key} is moving!")
|
||||||
|
|
||||||
|
|
||||||
|
class Dragon(Mobile):
|
||||||
|
"""
|
||||||
|
This is a dragon-specific mobile.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def move_around(self):
|
||||||
|
super().move_around()
|
||||||
|
print("The world trembles.")
|
||||||
|
|
||||||
|
def firebreath(self):
|
||||||
|
"""
|
||||||
|
Let our dragon breathe fire.
|
||||||
|
"""
|
||||||
|
print(f"{self.key} breathes fire!")
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Don't forget to save. We removed `Monster.__init__` and made `Mobile` inherit from Evennia's `Object` (which in turn
|
||||||
|
inherits from Evennia's `DefaultObject`, as we saw). By extension, this means that `Dragon` also inherits
|
||||||
|
from `DefaultObject`, just from further away!
|
||||||
|
|
||||||
|
### Creating by calling the class (less common way)
|
||||||
|
|
||||||
|
First reload the server as usual. We will need to create the dragon a little differently this time:
|
||||||
|
|
||||||
|
```sidebar:: Keyword arguments
|
||||||
|
|
||||||
|
Keyword arguments (like `db_key="Smaug"`) is a way to
|
||||||
|
name the input arguments to a function or method. They make
|
||||||
|
things easier to read but also allows for conveniently setting
|
||||||
|
defaults for values not given explicitly.
|
||||||
|
|
||||||
|
```
|
||||||
|
> py
|
||||||
|
> from typeclasses.mymobile import Dragon
|
||||||
|
> smaug = Dragon(db_key="Smaug", db_location=here)
|
||||||
|
> smaug.save()
|
||||||
|
> smaug.move_around()
|
||||||
|
Smaug is moving!
|
||||||
|
The world trembles.
|
||||||
|
|
||||||
|
Smaug works the same as before, but we created him differently: first we used
|
||||||
|
`Dragon(db_key="Smaug", db_location=here)` to create the object, and then we used `smaug.save()` afterwards.
|
||||||
|
|
||||||
|
> quit()
|
||||||
|
Python Console is closing.
|
||||||
|
> look
|
||||||
|
|
||||||
|
You should now see that Smaug _is in the room with you_. Woah!
|
||||||
|
|
||||||
|
> reload
|
||||||
|
> look
|
||||||
|
|
||||||
|
_He's still there_... What we just did is to create a new entry in the database for Smaug. We gave the object
|
||||||
|
its name (key) and set its location to our current location (remember that `here` is just something available
|
||||||
|
in the `py` command, you can't use it elsewhere).
|
||||||
|
|
||||||
|
To make use of Smaug in code we must first find him in the database. For an object in the current
|
||||||
|
location we can easily do this in `py` by using `me.search()`:
|
||||||
|
|
||||||
|
> py smaug = me.search("Smaug") ; smaug.firebreath()
|
||||||
|
Smaug breathes fire!
|
||||||
|
|
||||||
|
### Creating using create_object
|
||||||
|
|
||||||
|
Creating Smaug like we did above is nice because it's similar to how we created non-database
|
||||||
|
bound Python instances before. But you need to use `db_key` instead of `key` and you also have to
|
||||||
|
remember to call `.save()` afterwards. Evennia has a helper function that is more common to use,
|
||||||
|
called `create_object`:
|
||||||
|
|
||||||
|
> py fluffy = evennia.create_object('typeclases.mymobile.Mobile', key="Fluffy", location=here)
|
||||||
|
> look
|
||||||
|
|
||||||
|
Boom, Fluffy should now be in the room with you, a little less scary than Smaug. You specify the
|
||||||
|
python-path to the code you want and then set the key and location. Evennia sets things up and saves for you.
|
||||||
|
|
||||||
|
If you want to find Fluffy from anywhere, you can use Evennia's `search_object` helper:
|
||||||
|
|
||||||
|
> fluffy = evennia.search_object("Fluffy")[0] ; fluffy.move_around()
|
||||||
|
Fluffy is moving!
|
||||||
|
|
||||||
|
> The `[0]` is because `search_object` always returns a _list_ of zero, one or more found objects. The `[0]`
|
||||||
|
means that we want the first element of this list (counting in Python always starts from 0). If there were
|
||||||
|
multiple Fluffies we could get the second one with `[1]`.
|
||||||
|
|
||||||
|
### Creating using create-command
|
||||||
|
|
||||||
|
Finally, you can also create a new Dragon using the familiar builder-commands we explored a few lessons ago:
|
||||||
|
|
||||||
|
> create/drop Cuddly:typeclasses.mymobile.Mobile
|
||||||
|
|
||||||
|
Cuddly is now in the room. After learning about how objects are created you'll realize that all this command really
|
||||||
|
does is to parse your input, figure out that `/drop` means to "give the object the same location as the caller",
|
||||||
|
and then do a call akin to
|
||||||
|
|
||||||
|
evennia.create_object("typeclasses.mymobile.Mobile", key="Cuddly", location=here)
|
||||||
|
|
||||||
|
That's pretty much all there is to the mighty `create` command.
|
||||||
|
|
||||||
|
... And speaking of Commands, we should try to add one of our own next.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Adding Object Typeclass Tutorial
|
||||||
|
|
||||||
|
Evennia comes with a few very basic classes of in-game entities:
|
||||||
|
|
||||||
|
DefaultObject
|
||||||
|
|
|
||||||
|
DefaultCharacter
|
||||||
|
DefaultRoom
|
||||||
|
DefaultExit
|
||||||
|
DefaultChannel
|
||||||
|
|
||||||
|
When you create a new Evennia game (with for example `evennia --init mygame`) Evennia will
|
||||||
|
automatically create empty child classes `Object`, `Character`, `Room` and `Exit` respectively. They
|
||||||
|
are found `mygame/typeclasses/objects.py`, `mygame/typeclasses/rooms.py` etc.
|
||||||
|
|
||||||
|
> Technically these are all [Typeclassed](../../../Component/Typeclasses), which can be ignored for now. In
|
||||||
|
> `mygame/typeclasses` are also base typeclasses for out-of-character things, notably
|
||||||
|
> [Channels](../../../Component/Communications), [Accounts](../../../Component/Accounts) and [Scripts](../../../Component/Scripts). We don't cover those in
|
||||||
|
> this tutorial.
|
||||||
|
|
||||||
|
For your own game you will most likely want to expand on these very simple beginnings. It's normal
|
||||||
|
to want your Characters to have various attributes, for example. Maybe Rooms should hold extra
|
||||||
|
information or even *all* Objects in your game should have properties not included in basic Evennia.
|
||||||
|
|
||||||
|
## Change Default Rooms, Exits, Character Typeclass
|
||||||
|
|
||||||
|
This is the simplest case.
|
||||||
|
|
||||||
|
The default build commands of a new Evennia game is set up to use the `Room`, `Exit` and `Character`
|
||||||
|
classes found in the same-named modules under `mygame/typeclasses/`. By default these are empty and
|
||||||
|
just implements the default parents from the Evennia library (`DefaultRoom`etc). Just add the
|
||||||
|
changes you want to these classes and run `@reload` to add your new functionality.
|
||||||
|
|
||||||
|
## Create a new type of object
|
||||||
|
|
||||||
|
Say you want to create a new "Heavy" object-type that characters should not have the ability to pick
|
||||||
|
up.
|
||||||
|
|
||||||
|
1. Edit `mygame/typeclasses/objects.py` (you could also create a new module there, named something
|
||||||
|
like `heavy.py`, that's up to how you want to organize things).
|
||||||
|
1. Create a new class inheriting at any distance from `DefaultObject`. It could look something like
|
||||||
|
this:
|
||||||
|
```python
|
||||||
|
# end of file mygame/typeclasses/objects.py
|
||||||
|
from evennia import DefaultObject
|
||||||
|
|
||||||
|
class Heavy(DefaultObject):
|
||||||
|
"Heavy object"
|
||||||
|
def at_object_creation(self):
|
||||||
|
"Called whenever a new object is created"
|
||||||
|
# lock the object down by default
|
||||||
|
self.locks.add("get:false()")
|
||||||
|
# the default "get" command looks for this Attribute in order
|
||||||
|
# to return a customized error message (we just happen to know
|
||||||
|
# this, you'd have to look at the code of the 'get' command to
|
||||||
|
# find out).
|
||||||
|
self.db.get_err_msg = "This is too heavy to pick up."
|
||||||
|
```
|
||||||
|
1. Once you are done, log into the game with a build-capable account and do `@create/drop
|
||||||
|
rock:objects.Heavy` to drop a new heavy "rock" object in your location. Next try to pick it up
|
||||||
|
(`@quell` yourself first if you are a superuser). If you get errors, look at your log files where
|
||||||
|
you will find the traceback. The most common error is that you have some sort of syntax error in
|
||||||
|
your class.
|
||||||
|
|
||||||
|
Note that the [Locks](../../../Component/Locks) and [Attribute](../../../Component/Attributes) which are set in the typeclass could just
|
||||||
|
as well have been set using commands in-game, so this is a *very* simple example.
|
||||||
|
|
||||||
|
## Storing data on initialization
|
||||||
|
|
||||||
|
The `at_object_creation` is only called once, when the object is first created. This makes it ideal
|
||||||
|
for database-bound things like [Attributes](../../../Component/Attributes). But sometimes you want to create temporary
|
||||||
|
properties (things that are not to be stored in the database but still always exist every time the
|
||||||
|
object is created). Such properties can be initialized in the `at_init` method on the object.
|
||||||
|
`at_init` is called every time the object is loaded into memory.
|
||||||
|
|
||||||
|
> Note: It's usually pointless and wasteful to assign database data in `at_init`, since this will
|
||||||
|
> hit the database with the same value over and over. Put those in `at_object_creation` instead.
|
||||||
|
|
||||||
|
You are wise to use `ndb` (non-database Attributes) to store these non-persistent properties, since
|
||||||
|
ndb-properties are protected against being cached out in various ways and also allows you to list
|
||||||
|
them using various in-game tools:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def at_init(self):
|
||||||
|
self.ndb.counter = 0
|
||||||
|
self.ndb.mylist = []
|
||||||
|
```
|
||||||
|
|
||||||
|
> Note: As mentioned in the [Typeclasses](../../../Component/Typeclasses) documentation, `at_init` replaces the use of
|
||||||
|
> the standard `__init__` method of typeclasses due to how the latter may be called in situations
|
||||||
|
> other than you'd expect. So use `at_init` where you would normally use `__init__`.
|
||||||
|
|
||||||
|
|
||||||
|
## Updating existing objects
|
||||||
|
|
||||||
|
If you already have some `Heavy` objects created and you add a new `Attribute` in
|
||||||
|
`at_object_creation`, you will find that those existing objects will not have this Attribute. This
|
||||||
|
is not so strange, since `at_object_creation` is only called once, it will not be called again just
|
||||||
|
because you update it. You need to update existing objects manually.
|
||||||
|
|
||||||
|
If the number of objects is limited, you can use `@typeclass/force/reload objectname` to force a
|
||||||
|
re-load of the `at_object_creation` method (only) on the object. This case is common enough that
|
||||||
|
there is an alias `@update objectname` you can use to get the same effect. If there are multiple
|
||||||
|
objects you can use `@py` to loop over the objects you need:
|
||||||
|
|
||||||
|
```
|
||||||
|
@py from typeclasses.objects import Heavy; [obj.at_object_creation() for obj in Heavy.objects.all()]
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
[prev lesson](Python-classes-and-objects) | [next lesson](Adding-Commands)
|
||||||
502
docs/source/Howto/Starting/Part1/More-on-Commands.md
Normal file
502
docs/source/Howto/Starting/Part1/More-on-Commands.md
Normal file
|
|
@ -0,0 +1,502 @@
|
||||||
|
# More about Commands
|
||||||
|
|
||||||
|
[prev lesson](Adding-Commands) | [next lesson](Learning-Typeclasses)
|
||||||
|
|
||||||
|
In this lesson we learn some basics about parsing the input of Commands. We will
|
||||||
|
also learn how to add, modify and extend Evennia's default commands.
|
||||||
|
|
||||||
|
## More advanced parsing
|
||||||
|
|
||||||
|
In the last lesson we made a `hit` Command and hit a dragon with it. You should have the code
|
||||||
|
from that still around.
|
||||||
|
|
||||||
|
Let's expand our simple `hit` command to accept a little more complex input:
|
||||||
|
|
||||||
|
hit <target> [[with] <weapon>]
|
||||||
|
|
||||||
|
That is, we want to support all of these forms
|
||||||
|
|
||||||
|
hit target
|
||||||
|
hit target weapon
|
||||||
|
hit target with weapon
|
||||||
|
|
||||||
|
If you don't specify a weapon you'll use your fists. It's also nice to be able to skip "with" if
|
||||||
|
you are in a hurry. Time to modify `mygame/commands/mycommands.py` again. Let us break out the parsing
|
||||||
|
a little, in a new method `parse`:
|
||||||
|
|
||||||
|
|
||||||
|
```python
|
||||||
|
#...
|
||||||
|
|
||||||
|
class CmdHit(Command):
|
||||||
|
"""
|
||||||
|
Hit a target.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
hit <target>
|
||||||
|
|
||||||
|
"""
|
||||||
|
key = "hit"
|
||||||
|
|
||||||
|
def parse(self):
|
||||||
|
self.args = self.args.strip()
|
||||||
|
target, *weapon = self.args.split(" with ", 1)
|
||||||
|
if not weapon:
|
||||||
|
target, *weapon = target.split(" ", 1)
|
||||||
|
self.target = target.strip()
|
||||||
|
if weapon:
|
||||||
|
self.weapon = weapon.strip()
|
||||||
|
else:
|
||||||
|
self.weapon = ""
|
||||||
|
|
||||||
|
def func(self):
|
||||||
|
if not self.args:
|
||||||
|
self.caller.msg("Who do you want to hit?")
|
||||||
|
return
|
||||||
|
# get the target for the hit
|
||||||
|
target = self.caller.search(self.target)
|
||||||
|
if not target:
|
||||||
|
return
|
||||||
|
# get and handle the weapon
|
||||||
|
weapon = None
|
||||||
|
if self.weapon:
|
||||||
|
weapon = self.caller.search(self.weapon)
|
||||||
|
if weapon:
|
||||||
|
weaponstr = f"{weapon.key}"
|
||||||
|
else:
|
||||||
|
weaponstr = "bare fists"
|
||||||
|
|
||||||
|
self.caller.msg(f"You hit {target.key} with {weaponstr}!")
|
||||||
|
target.msg(f"You got hit by {self.caller.key} with {weaponstr}!")
|
||||||
|
# ...
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
The `parse` method is called before `func` and has access to all the same on-command variables as in `func`. Using
|
||||||
|
`parse` not only makes things a little easier to read, it also means you can easily let other Commands _inherit_
|
||||||
|
your parsing - if you wanted some other Command to also understand input on the form `<arg> with <arg>` you'd inherit
|
||||||
|
from this class and just implement the `func` needed for that command without implementing `parse` anew.
|
||||||
|
|
||||||
|
```sidebar:: Tuples and Lists
|
||||||
|
|
||||||
|
- A `list` is written as `[a, b, c, d, ...]`. You can add and grow/shrink a list after it was first created.
|
||||||
|
- A `tuple` is written as `(a, b, c, d, ...)`. A tuple cannot be modified once it is created.
|
||||||
|
|
||||||
|
```
|
||||||
|
- **Line 14** - We do the stripping of `self.args` once and for all here. We also store the stripped version back
|
||||||
|
into `self.args`, overwriting it. So there is no way to get back the non-stripped version from here on, which is fine
|
||||||
|
for this command.
|
||||||
|
- **Line 15** - This makes use of the `.split` method of strings. `.split` will, well, split the string by some criterion.
|
||||||
|
`.split(" with ", 1)` means "split the string once, around the substring `" with "` if it exists". The result
|
||||||
|
of this split is a _list_. Just how that list looks depends on the string we are trying to split:
|
||||||
|
1. If we entered just `hit smaug`, we'd be splitting just `"smaug"` which would give the result `["smaug"]`.
|
||||||
|
2. `hit smaug sword` gives `["smaug sword"]`
|
||||||
|
3. `hit smaug with sword` gives `["smaug", "sword"]`
|
||||||
|
|
||||||
|
So we get a list of 1 or 2 elements. We assign it to two variables like this, `target, *weapon = `. That
|
||||||
|
asterisk in `*weapon` is a nifty trick - it will automatically become a list of _0 or more_ values. It sorts of
|
||||||
|
"soaks" up everything left over.
|
||||||
|
1. `target` becomes `"smaug"` and `weapon` becomes `[]`
|
||||||
|
2. `target` becomes `"smaug sword"` and `weapon` becomes `[]`
|
||||||
|
3. `target` becomes `"smaug"` and `weapon` becomes `sword`
|
||||||
|
- **Lines 16-17** - In this `if` condition we check if `weapon` is falsy (that is, the empty list). This can happen
|
||||||
|
under two conditions (from the example above):
|
||||||
|
1. `target` is simply `smaug`
|
||||||
|
2. `target` is `smaug sword`
|
||||||
|
|
||||||
|
To separate these cases we split `target` once again, this time by empty space `" "`. Again we store the
|
||||||
|
result back with `target, *weapon =`. The result will be one of the following:
|
||||||
|
1. `target` remains `smaug` and `weapon` remains `[]`
|
||||||
|
2. `target` becomes `smaug` and `weapon` becomes `sword`
|
||||||
|
- **Lines 18-22** - We now store `target` and `weapon` into `self.target` and `self.weapon`. We must do this in order
|
||||||
|
for these local variables to made available in `func` later. Note how we need to check so `weapon` is not falsy
|
||||||
|
before running `strip()` on it. This is because we know that if it's falsy, it's an empty list `[]` and lists
|
||||||
|
don't have the `.strip()` method on them (so if we tried to use it, we'd get an error).
|
||||||
|
|
||||||
|
Now onto the `func` method. The main difference is we now have `self.target` and `self.weapon` available for
|
||||||
|
convenient use.
|
||||||
|
- **Lines 29 and 35** - We make use of the previously parsed search terms for the target and weapon to find the
|
||||||
|
respective resource.
|
||||||
|
- **Lines 34-39** - Since the weapon is optional, we need to supply a default (use our fists!) if it's not set. We
|
||||||
|
use this to create a `weaponstr` that is different depending on if we have a weapon or not.
|
||||||
|
- **Lines 41-42** - We merge the `weaponstr` with our attack text.
|
||||||
|
|
||||||
|
Let's try it out!
|
||||||
|
|
||||||
|
> reload
|
||||||
|
> hit smaug with sword
|
||||||
|
Could not find 'sword'.
|
||||||
|
You hit smaug with bare fists!
|
||||||
|
|
||||||
|
Oops, our `self.caller.search(self.weapon)` is telling us that it found no sword. Since we are not `return`ing
|
||||||
|
in this situation (like we do if failing to find `target`) we still continue fighting with our bare hands.
|
||||||
|
This won't do. Let's make ourselves a sword.
|
||||||
|
|
||||||
|
> create sword
|
||||||
|
|
||||||
|
Since we didn't specify `/drop`, the sword will end up in our inventory and can seen with the `i` or
|
||||||
|
`inventory` command. The `.search` helper will still find it there. There is no need to reload to see this
|
||||||
|
change (no code changed, only stuff in the database).
|
||||||
|
|
||||||
|
> hit smaug with sword
|
||||||
|
You hit smaug with sword!
|
||||||
|
|
||||||
|
|
||||||
|
## Adding a Command to an object
|
||||||
|
|
||||||
|
The commands of a cmdset attached to an object with `obj.cmdset.add()` will by default be made available to that object
|
||||||
|
but _also to those in the same location as that object_. If you did the [Building introduction](Building-Quickstart)
|
||||||
|
you've seen an example of this with the "Red Button" object. The [Tutorial world](Tutorial-World-Introduction)
|
||||||
|
also has many examples of objects with commands on them.
|
||||||
|
|
||||||
|
To show how this could work, let's put our 'hit' Command on our simple `sword` object from the previous section.
|
||||||
|
|
||||||
|
> self.search("sword").cmdset.add("commands.mycommands.MyCmdSet", permanent=True)
|
||||||
|
|
||||||
|
We find the sword (it's still in our inventory so `self.search` should be able to find it), then
|
||||||
|
add `MyCmdSet` to it. This actually adds both `hit` and `echo` to the sword, which is fine.
|
||||||
|
|
||||||
|
Let's try to swing it!
|
||||||
|
|
||||||
|
> hit
|
||||||
|
More than one match for 'hit' (please narrow target):
|
||||||
|
hit-1 (sword #11)
|
||||||
|
hit-2
|
||||||
|
|
||||||
|
```sidebar:: Multi-matches
|
||||||
|
|
||||||
|
Some game engines will just pick the first hit when finding more than one.
|
||||||
|
Evennia will always give you a choice. The reason for this is that Evennia
|
||||||
|
cannot know if `hit` and `hit` are different or the same - maybe it behaves
|
||||||
|
differently depending on the object it sits on? Besides, imagine if you had
|
||||||
|
a red and a blue button both with the command `push` on it. Now you just write
|
||||||
|
`push`. Wouldn't you prefer to be asked `which` button you really wanted to push?
|
||||||
|
```
|
||||||
|
Woah, that didn't go as planned. Evennia actually found _two_ `hit` commands to didn't know which one to use
|
||||||
|
(_we_ know they are the same, but Evennia can't be sure of that). As we can see, `hit-1` is the one found on
|
||||||
|
the sword. The other one is from adding `MyCmdSet` to ourself earlier. It's easy enough to tell Evennia which
|
||||||
|
one you meant:
|
||||||
|
|
||||||
|
> hit-1
|
||||||
|
Who do you want to hit?
|
||||||
|
> hit-2
|
||||||
|
Who do you want to hit?
|
||||||
|
|
||||||
|
In this case we don't need both command-sets, so let's just keep the one on the sword:
|
||||||
|
|
||||||
|
> self.cmdset.remove("commands.mycommands.MyCmdSet")
|
||||||
|
> hit
|
||||||
|
Who do you want to hit?
|
||||||
|
|
||||||
|
Now try this:
|
||||||
|
|
||||||
|
> tunnel n = kitchen
|
||||||
|
> n
|
||||||
|
> drop sword
|
||||||
|
> s
|
||||||
|
> hit
|
||||||
|
Command 'hit' is not available. Maybe you meant ...
|
||||||
|
> n
|
||||||
|
> hit
|
||||||
|
Who do you want to hit?
|
||||||
|
|
||||||
|
The `hit` command is now only available if you hold or are in the same room as the sword.
|
||||||
|
|
||||||
|
### You need to hold the sword!
|
||||||
|
|
||||||
|
Let's get a little ahead of ourselves and make it so you have to _hold_ the sword for the `hit` command to
|
||||||
|
be available. This involves a _Lock_. We've cover locks in more detail later, just know that they are useful
|
||||||
|
for limiting the kind of things you can do with an object, including limiting just when you can call commands on
|
||||||
|
it.
|
||||||
|
```sidebar:: Locks
|
||||||
|
|
||||||
|
Evennia Locks are defined as a mini-language defined in `lockstrings`. The lockstring
|
||||||
|
is on a form `<situation>:<lockfuncs>`, where `situation` determines when this
|
||||||
|
lock applies and the `lockfuncs` (there can be more than one) are run to determine
|
||||||
|
if the lock-check passes or not depending on circumstance.
|
||||||
|
```
|
||||||
|
|
||||||
|
> py self.search("sword").locks.add("call:holds()")
|
||||||
|
|
||||||
|
We added a new lock to the sword. The _lockstring_ `"call:holds()"` means that you can only _call_ commands on
|
||||||
|
this object if you are _holding_ the object (that is, it's in your inventory).
|
||||||
|
|
||||||
|
For locks to work, you cannot be _superuser_, since the superuser passes all locks. You need to `quell` yourself
|
||||||
|
first:
|
||||||
|
```sidebar:: quell/unquell
|
||||||
|
|
||||||
|
Quelling allows you as a developer to take on the role of players with less
|
||||||
|
priveleges. This is useful for testing and debugging, in particular since a
|
||||||
|
superuser has a little `too` much power sometimes.
|
||||||
|
Use `unquell` to get back to your normal self.
|
||||||
|
```
|
||||||
|
|
||||||
|
> quell
|
||||||
|
|
||||||
|
If the sword lies on the ground, try
|
||||||
|
|
||||||
|
> hit
|
||||||
|
Command 'hit' is not available. ..
|
||||||
|
> get sword
|
||||||
|
> hit
|
||||||
|
> Who do you want to hit?
|
||||||
|
|
||||||
|
|
||||||
|
Finally, we get rid of ours sword so we have a clean slate with no more `hit` commands floating around.
|
||||||
|
We can do that in two ways:
|
||||||
|
|
||||||
|
delete sword
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
py self.search("sword").delete()
|
||||||
|
|
||||||
|
|
||||||
|
## Adding the Command to a default Cmdset
|
||||||
|
|
||||||
|
|
||||||
|
As we have seen we can use `obj.cmdset.add()` to add a new cmdset to objects, whether that object
|
||||||
|
is ourself (`self`) or other objects like the `sword`.
|
||||||
|
|
||||||
|
This is how all commands in Evennia work, including default commands like `look`, `dig`, `inventory` and so on.
|
||||||
|
All these commands are in just loaded on the default objects that Evennia provides out of the box.
|
||||||
|
|
||||||
|
- Characters (that is 'you' in the gameworld) has the `CharacterCmdSet`.
|
||||||
|
- Accounts (the thing that represents your out-of-character existence on the server) has the `AccountCmdSet`
|
||||||
|
- Sessions (representing one single client connection) has the `SessionCmdSet`
|
||||||
|
- Before you log in (at the connection screen) you'll have access to the `UnloggedinCmdSet`.
|
||||||
|
|
||||||
|
The thing must commonly modified is the `CharacterCmdSet`.
|
||||||
|
|
||||||
|
The default cmdset are defined in `mygame/commands/default_cmdsets.py`. Open that file now:
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""
|
||||||
|
(module docstring)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from evennia import default_cmds
|
||||||
|
|
||||||
|
class CharacterCmdSet(default_cmds.CharacterCmdSet):
|
||||||
|
|
||||||
|
key = "DefaultCharacter"
|
||||||
|
|
||||||
|
def at_cmdset_creation(self):
|
||||||
|
|
||||||
|
super().at_cmdset_creation()
|
||||||
|
#
|
||||||
|
# any commands you add below will overload the default ones
|
||||||
|
#
|
||||||
|
|
||||||
|
class AccountCmdSet(default_cmds.AccountCmdSet):
|
||||||
|
|
||||||
|
key = "DefaultAccount"
|
||||||
|
|
||||||
|
def at_cmdset_creation(self):
|
||||||
|
|
||||||
|
super().at_cmdset_creation()
|
||||||
|
#
|
||||||
|
# any commands you add below will overload the default ones
|
||||||
|
#
|
||||||
|
|
||||||
|
class UnloggedinCmdSet(default_cmds.UnloggedinCmdSet):
|
||||||
|
|
||||||
|
key = "DefaultUnloggedin"
|
||||||
|
|
||||||
|
def at_cmdset_creation(self):
|
||||||
|
|
||||||
|
super().at_cmdset_creation()
|
||||||
|
#
|
||||||
|
# any commands you add below will overload the default ones
|
||||||
|
#
|
||||||
|
|
||||||
|
class SessionCmdSet(default_cmds.SessionCmdSet):
|
||||||
|
|
||||||
|
key = "DefaultSession"
|
||||||
|
|
||||||
|
def at_cmdset_creation(self):
|
||||||
|
|
||||||
|
super().at_cmdset_creation()
|
||||||
|
#
|
||||||
|
# any commands you add below will overload the default ones
|
||||||
|
#
|
||||||
|
```
|
||||||
|
|
||||||
|
```sidebar:: super()
|
||||||
|
|
||||||
|
The `super()` function refers to the parent of the current class and is commonly
|
||||||
|
used to call same-named methods on the parent.
|
||||||
|
```
|
||||||
|
`evennia.default_cmds` is a container that holds all of Evennia's default commands and cmdsets. In this module
|
||||||
|
we can see that this was imported and then a new child class was made for each cmdset. Each class looks familiar
|
||||||
|
(except the `key`, that's mainly used to easily identify the cmdset in listings). In each `at_cmdset_creation` all
|
||||||
|
we do is call `super().at_cmdset_creation` which means that we call `at_cmdset_creation() on the _parent_ CmdSet.
|
||||||
|
This is what adds all the default commands to each CmdSet.
|
||||||
|
|
||||||
|
To add even more Commands to a default cmdset, we can just add them below the `super()` line. Usefully, if we were to
|
||||||
|
add a Command with the same `.key` as a default command, it would completely replace that original. So if you were
|
||||||
|
to add a command with a key `look`, the original `look` command would be replaced by your own version.
|
||||||
|
|
||||||
|
For now, let's add our own `hit` and `echo` commands to the `CharacterCmdSet`:
|
||||||
|
|
||||||
|
|
||||||
|
```python
|
||||||
|
# ...
|
||||||
|
|
||||||
|
from commands import mycommands
|
||||||
|
|
||||||
|
class CharacterCmdSet(default_cmds.CharacterCmdSet):
|
||||||
|
|
||||||
|
key = "DefaultCharacter"
|
||||||
|
|
||||||
|
def at_cmdset_creation(self):
|
||||||
|
|
||||||
|
super().at_cmdset_creation()
|
||||||
|
#
|
||||||
|
# any commands you add below will overload the default ones
|
||||||
|
#
|
||||||
|
self.add(mycommands.CmdEcho)
|
||||||
|
self.add(mycommands.CmdHit)
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
> reload
|
||||||
|
> hit
|
||||||
|
Who do you want to hit?
|
||||||
|
|
||||||
|
Your new commands are now available for all player characters in the game. There is another way to add a bunch
|
||||||
|
of commands at once, and that is to add a _CmdSet_ to the other cmdset. All commands in that cmdset will then be added:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from commands import mycommands
|
||||||
|
|
||||||
|
class CharacterCmdSet(default_cmds.CharacterCmdSet):
|
||||||
|
|
||||||
|
key = "DefaultCharacter"
|
||||||
|
|
||||||
|
def at_cmdset_creation(self):
|
||||||
|
|
||||||
|
super().at_cmdset_creation()
|
||||||
|
#
|
||||||
|
# any commands you add below will overload the default ones
|
||||||
|
#
|
||||||
|
self.add(mycommands.MyCmdSet)
|
||||||
|
```
|
||||||
|
|
||||||
|
Which way you use depends on how much control you want, but if you already have a CmdSet,
|
||||||
|
this is practical. A Command can be a part of any number of different CmdSets.
|
||||||
|
|
||||||
|
### Removing Commands
|
||||||
|
|
||||||
|
To remove your custom commands again, you of course just delete the change you did to
|
||||||
|
`mygame/commands/default_cmdsets.py`. But what if you want to remove a default command?
|
||||||
|
|
||||||
|
We already know that we use `cmdset.remove()` to remove a cmdset. It turns out you can
|
||||||
|
do the same in `at_cmdset_creation`. For example, let's remove the default `get` Command
|
||||||
|
from Evennia. We happen to know this can be found as `default_cmds.CmdGet`.
|
||||||
|
|
||||||
|
|
||||||
|
```python
|
||||||
|
# ...
|
||||||
|
from commands import mycommands
|
||||||
|
|
||||||
|
class CharacterCmdSet(default_cmds.CharacterCmdSet):
|
||||||
|
|
||||||
|
key = "DefaultCharacter"
|
||||||
|
|
||||||
|
def at_cmdset_creation(self):
|
||||||
|
|
||||||
|
super().at_cmdset_creation()
|
||||||
|
#
|
||||||
|
# any commands you add below will overload the default ones
|
||||||
|
#
|
||||||
|
self.add(mycommands.MyCmdSet)
|
||||||
|
self.remove(default_cmds.CmdGet)
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|
> reload
|
||||||
|
> get
|
||||||
|
Command 'get' is not available ...
|
||||||
|
|
||||||
|
## Replace a default command
|
||||||
|
|
||||||
|
At this point you already have all the pieces for how to do this! We just need to add a new
|
||||||
|
command with the same `key` in the `CharacterCmdSet` to replace the default one.
|
||||||
|
|
||||||
|
Let's combine this with what we know about classes and
|
||||||
|
how to _override_ a parent class. Open `mygame/commands/mycommands.py` and lets override
|
||||||
|
that `CmdGet` command.
|
||||||
|
|
||||||
|
```python
|
||||||
|
# up top, by the other imports
|
||||||
|
from evennia import default_cmds
|
||||||
|
|
||||||
|
# somewhere below
|
||||||
|
class MyCmdGet(default_cmds.CmdGet):
|
||||||
|
|
||||||
|
def func(self):
|
||||||
|
super().func()
|
||||||
|
self.caller.msg(str(self.caller.location.contents))
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Line2**: We import `default_cmds` so we can get the parent class.
|
||||||
|
We made a new class and we make it _inherit_ `default_cmds.CmdGet`. We don't
|
||||||
|
need to set `.key` or `.parse`, that's already handled by the parent.
|
||||||
|
In `func` we call `super().func()` to let the parent do its normal thing,
|
||||||
|
- **Line 7**: By adding our own `func` we replace the one in the parent.
|
||||||
|
- **Line 8**: For this simple change we still want the command to work the
|
||||||
|
same as before, so we use `super()` to call `func` on the parent.
|
||||||
|
- **Line 9**: `.location` is the place an object is at. `.contents` contains, well, the
|
||||||
|
contents of an object. If you tried `py self.contents` you'd get a list that equals
|
||||||
|
your inventory. For a room, the contents is everything in it.
|
||||||
|
So `self.caller.location.contents` gets the contents of our current location. This is
|
||||||
|
a _list_. In order send this to us with `.msg` we turn the list into a string. Python
|
||||||
|
has a special function `str()` to do this.
|
||||||
|
|
||||||
|
We now just have to add this so it replaces the default `get` command. Open
|
||||||
|
`mygame/commands/default_cmdsets.py` again:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# ...
|
||||||
|
from commands import mycommands
|
||||||
|
|
||||||
|
class CharacterCmdSet(default_cmds.CharacterCmdSet):
|
||||||
|
|
||||||
|
key = "DefaultCharacter"
|
||||||
|
|
||||||
|
def at_cmdset_creation(self):
|
||||||
|
|
||||||
|
super().at_cmdset_creation()
|
||||||
|
#
|
||||||
|
# any commands you add below will overload the default ones
|
||||||
|
#
|
||||||
|
self.add(mycommands.MyCmdSet)
|
||||||
|
self.add(mycommands.MyCmdGet)
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
```sidebar:: Another way
|
||||||
|
|
||||||
|
Instead of adding `MyCmdGet` explicitly in default_cmdset.py,
|
||||||
|
you could also add it to `mycommands.MyCmdSet` and let it be
|
||||||
|
added automatically for you.
|
||||||
|
```
|
||||||
|
|
||||||
|
> reload
|
||||||
|
> get
|
||||||
|
Get What?
|
||||||
|
[smaug, fluffy, YourName, ...]
|
||||||
|
|
||||||
|
We just made a new `get`-command that tells us everything we could pick up (well, we can't pick up ourselves, so
|
||||||
|
there's some room for improvement there).
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
In this lesson we got into some more advanced string formatting - many of those tricks will help you a lot in
|
||||||
|
the future! We also made a functional sword. Finally we got into how to add to, extend and replace a default
|
||||||
|
command on ourselves.
|
||||||
|
|
||||||
|
Let's explore 'ourselves' and other 'things' in the game next.
|
||||||
|
|
||||||
|
[prev lesson](Adding-Commands) | [next lesson](Learning-Typeclasses)
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
# Python Classes and Evennia Typeclasses
|
# Python Classes and objects
|
||||||
|
|
||||||
[prev lesson](Gamedir-Overview) | [next lesson](Adding-Commands)
|
[prev lesson](Gamedir-Overview) | [next lesson](Learning-Typeclasses)
|
||||||
|
|
||||||
We have now learned how to run some simple Python code from inside (and outside) your game server.
|
We have now learned how to run some simple Python code from inside (and outside) your game server.
|
||||||
We have also taken a look at what our game dir looks and what is where. Now we'll start to use it.
|
We have also taken a look at what our game dir looks and what is where. Now we'll start to use it.
|
||||||
|
|
||||||
## Importing
|
## Importing things
|
||||||
|
|
||||||
No one writes something as big as an online game in one single huge file. Instead one breaks up the
|
No one writes something as big as an online game in one single huge file. Instead one breaks up the
|
||||||
code into separate files (modules). Each module is dedicated to different purposes. Not only does
|
code into separate files (modules). Each module is dedicated to different purposes. Not only does
|
||||||
|
|
@ -153,7 +153,9 @@ Next we have a `class` named `Object`, which _inherits_ from `DefaultObject`. Th
|
||||||
actually do anything on its own, its only code (except the docstring) is `pass` which means,
|
actually do anything on its own, its only code (except the docstring) is `pass` which means,
|
||||||
well, to pass and don't do anything.
|
well, to pass and don't do anything.
|
||||||
|
|
||||||
To understand what we are looking at, we need to explain what a 'class', an 'object' and an 'instance' is.
|
We will get back to this module in the [next lesson](Learning-Typeclasses). First we need to do a
|
||||||
|
little detour to understand what a 'class', an 'object' or 'instance' is. These are fundamental
|
||||||
|
things to understand before you can use Evennia efficiently.
|
||||||
```sidebar:: OOP
|
```sidebar:: OOP
|
||||||
|
|
||||||
Classes, objects, instances and inheritance are fundamental to Python. This and some
|
Classes, objects, instances and inheritance are fundamental to Python. This and some
|
||||||
|
|
@ -185,6 +187,12 @@ at least one argument (almost always written as `self` although you could in pri
|
||||||
another name), which is a reference back to itself. So when we print `self.key` we are referring
|
another name), which is a reference back to itself. So when we print `self.key` we are referring
|
||||||
back to the `key` on the class.
|
back to the `key` on the class.
|
||||||
|
|
||||||
|
```sidebar:: Terms
|
||||||
|
|
||||||
|
- A `class` is a code template describing a 'type' of something
|
||||||
|
- An `object` is an `instance` of a `class`. Like using a mold to cast tin soldiers, one class can be `instantiated` into any number of object-instances.
|
||||||
|
|
||||||
|
```
|
||||||
A class is just a template. Before it can be used, we must create an _instance_ of the class. If
|
A class is just a template. Before it can be used, we must create an _instance_ of the class. If
|
||||||
`Mobile` is a class, then an instance is Fluffy, the individual red dragon. You instantiate
|
`Mobile` is a class, then an instance is Fluffy, the individual red dragon. You instantiate
|
||||||
by _calling_ the class, much like you would a function:
|
by _calling_ the class, much like you would a function:
|
||||||
|
|
@ -395,152 +403,14 @@ about the trembling world we added in the `Dragon` class.
|
||||||
Inheritance is very powerful because it allows you to organize and re-use code while only adding the special things
|
Inheritance is very powerful because it allows you to organize and re-use code while only adding the special things
|
||||||
you want to change. Evennia uses this concept a lot.
|
you want to change. Evennia uses this concept a lot.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
## Our first persistent object
|
We have created our first dragons from classes. We have learned a little about how you _instantiate_ a class
|
||||||
|
into an _object_. We have seen some examples of _inheritance_ and we tested to _override_ a method in the parent
|
||||||
|
with one in the child class. We also used `super()` to good effect.
|
||||||
|
|
||||||
Now we should know enough to understand what is happening in `mygame/typeclasses/objects.py`.
|
But so far our dragons are gone as soon as we `restart` the server or `quit()` the Python interpreter. In the
|
||||||
Open it again:
|
next lesson we'll get up close and personal with Smaug.
|
||||||
|
|
||||||
```python
|
|
||||||
"""
|
|
||||||
module docstring
|
|
||||||
"""
|
|
||||||
from evennia import DefaultObject
|
|
||||||
|
|
||||||
class Object(DefaultObject):
|
|
||||||
"""
|
|
||||||
class docstring
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
```
|
|
||||||
|
|
||||||
So we have a class `Object` that _inherits_ from `DefaultObject`, which we have imported from Evennia.
|
|
||||||
The class itself doesn't do anything (it just `pass`es) but that doesn't mean it's useless. As we've seen,
|
|
||||||
it inherits all the functionality of its parent. It's in fact an _exact replica_ of `DefaultObject` right now.
|
|
||||||
If we knew what kind of methods and resources were available on `DefaultObject` we could add our own and
|
|
||||||
change the way it works!
|
|
||||||
|
|
||||||
> Hint: We will get back to this, but to learn what resources an Evennia parent like `DefaultObject` offers,
|
|
||||||
> easiest is to peek at its [API documentation](api:evennia.objects.objects#DefaultObject). The docstring for
|
|
||||||
> the `Object` class can also help.
|
|
||||||
|
|
||||||
One thing that Evennia offers and which you don't get with vanilla Python classes is _persistence_. As you've
|
|
||||||
found, Fluffy, Cuddly and Smaug are gone once we reload the server. Let's see if we can fix this.
|
|
||||||
|
|
||||||
Go back to `mygame/typeclasses/mobile.py`. Change it as follows:
|
|
||||||
|
|
||||||
```python
|
|
||||||
|
|
||||||
from typeclasses.objects import Object
|
|
||||||
|
|
||||||
class Mobile(Object):
|
|
||||||
"""
|
|
||||||
This is a base class for Mobiles.
|
|
||||||
"""
|
|
||||||
def move_around(self):
|
|
||||||
print(f"{self.key} is moving!")
|
|
||||||
|
|
||||||
|
|
||||||
class Dragon(Mobile):
|
[prev lesson](Gamedir-Overview) | [next lesson](Learning-Typeclasses)
|
||||||
"""
|
|
||||||
This is a dragon-specific mobile.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def move_around(self):
|
|
||||||
super().move_around()
|
|
||||||
print("The world trembles.")
|
|
||||||
|
|
||||||
def firebreath(self):
|
|
||||||
"""
|
|
||||||
Let our dragon breathe fire.
|
|
||||||
"""
|
|
||||||
print(f"{self.key} breathes fire!")
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Don't forget to save. We removed `Monster.__init__` and made `Mobile` inherit from Evennia's `Object` (which in turn
|
|
||||||
inherits from Evennia's `DefaultObject`, as we saw). By extension, this means that `Dragon` also inherits
|
|
||||||
from `DefaultObject`, just from further away!
|
|
||||||
|
|
||||||
### Creating by calling the class (less common way)
|
|
||||||
|
|
||||||
First reload the server as usual. We will need to create the dragon a little differently this time:
|
|
||||||
|
|
||||||
```sidebar:: Keyword arguments
|
|
||||||
|
|
||||||
Keyword arguments (like `db_key="Smaug"`) is a way to
|
|
||||||
name the input arguments to a function or method. They make
|
|
||||||
things easier to read but also allows for conveniently setting
|
|
||||||
defaults for values not given explicitly.
|
|
||||||
|
|
||||||
```
|
|
||||||
> py
|
|
||||||
> from typeclasses.mymobile import Dragon
|
|
||||||
> smaug = Dragon(db_key="Smaug", db_location=here)
|
|
||||||
> smaug.save()
|
|
||||||
> smaug.move_around()
|
|
||||||
Smaug is moving!
|
|
||||||
The world trembles.
|
|
||||||
|
|
||||||
Smaug works the same as before, but we created him differently: first we used
|
|
||||||
`Dragon(db_key="Smaug", db_location=here)` to create the object, and then we used `smaug.save()` afterwards.
|
|
||||||
|
|
||||||
> quit()
|
|
||||||
Python Console is closing.
|
|
||||||
> look
|
|
||||||
|
|
||||||
You should now see that Smaug _is in the room with you_. Woah!
|
|
||||||
|
|
||||||
> reload
|
|
||||||
> look
|
|
||||||
|
|
||||||
_He's still there_... What we just did is to create a new entry in the database for Smaug. We gave the object
|
|
||||||
its name (key) and set its location to our current location (remember that `here` is just something available
|
|
||||||
in the `py` command, you can't use it elsewhere).
|
|
||||||
|
|
||||||
To make use of Smaug in code we must first find him in the database. For an object in the current
|
|
||||||
location we can easily do this in `py` by using `me.search()`:
|
|
||||||
|
|
||||||
> py smaug = me.search("Smaug") ; smaug.firebreath()
|
|
||||||
Smaug breathes fire!
|
|
||||||
|
|
||||||
### Creating using create_object
|
|
||||||
|
|
||||||
Creating Smaug like we did above is nice because it's similar to how we created non-database
|
|
||||||
bound Python instances before. But you need to use `db_key` instead of `key` and you also have to
|
|
||||||
remember to call `.save()` afterwards. Evennia has a helper function that is more common to use,
|
|
||||||
called `create_object`:
|
|
||||||
|
|
||||||
> py fluffy = evennia.create_object('typeclases.mymobile.Mobile', key="Fluffy", location=here)
|
|
||||||
> look
|
|
||||||
|
|
||||||
Boom, Fluffy should now be in the room with you, a little less scary than Smaug. You specify the
|
|
||||||
python-path to the code you want and then set the key and location. Evennia sets things up and saves for you.
|
|
||||||
|
|
||||||
If you want to find Fluffy from anywhere, you can use Evennia's `search_object` helper:
|
|
||||||
|
|
||||||
> fluffy = evennia.search_object("Fluffy")[0] ; fluffy.move_around()
|
|
||||||
Fluffy is moving!
|
|
||||||
|
|
||||||
> The `[0]` is because `search_object` always returns a _list_ of zero, one or more found objects. The `[0]`
|
|
||||||
means that we want the first element of this list (counting in Python always starts from 0). If there were
|
|
||||||
multiple Fluffies we could get the second one with `[1]`.
|
|
||||||
|
|
||||||
### Creating using create-command
|
|
||||||
|
|
||||||
Finally, you can also create a new Dragon using the familiar builder-commands we explored a few lessons ago:
|
|
||||||
|
|
||||||
> create/drop Cuddly:typeclasses.mymobile.Mobile
|
|
||||||
|
|
||||||
Cuddly is now in the room. After learning about how objects are created you'll realize that all this command really
|
|
||||||
does is to parse your input, figure out that `/drop` means to "give the object the same location as the caller",
|
|
||||||
and then do a call akin to
|
|
||||||
|
|
||||||
evennia.create_object("typeclasses.mymobile.Mobile", key="Cuddly", location=here)
|
|
||||||
|
|
||||||
That's pretty much all there is to the mighty `create` command.
|
|
||||||
|
|
||||||
... And speaking of Commands, we should try to add one of our own next.
|
|
||||||
|
|
||||||
|
|
||||||
[prev lesson](Gamedir-Overview) | [next lesson](Adding-Commands)
|
|
||||||
|
|
|
||||||
|
|
@ -26,10 +26,11 @@ own first little game in Evennia. Let's get started!
|
||||||
1. [The Tutorial World](Part1/Tutorial-World-Introduction)
|
1. [The Tutorial World](Part1/Tutorial-World-Introduction)
|
||||||
1. [Python basics](Part1/Python-basic-introduction)
|
1. [Python basics](Part1/Python-basic-introduction)
|
||||||
1. [Game dir overview](Part1/Gamedir-Overview)
|
1. [Game dir overview](Part1/Gamedir-Overview)
|
||||||
1. [Python classes](Python-basic-tutorial-part-two)
|
1. [Python classes and objects](Part1/Python-classes-and-objects)
|
||||||
1. [Running Python in- and outside the game](../../Coding/Execute-Python-Code)
|
1. [Persistent objects](Part1/Learning-Typeclasses)
|
||||||
1. [Understanding errors](Understanding-Errors)
|
1. [Making our first own commands](Part1/Adding-Commands)
|
||||||
1. [Searching for things](Tutorial-Searching-For-Objects)
|
1. [Parsing and replacing default Commands](Part1/More-on-Commands)
|
||||||
|
1. [Searching and creating things](Tutorial-Searching-For-Objects)
|
||||||
1. [A walkthrough of the API](Walkthrough-of-API)
|
1. [A walkthrough of the API](Walkthrough-of-API)
|
||||||
|
|
||||||
In this first part we'll focus on what we get out of the box in Evennia - we'll get used to the tools,
|
In this first part we'll focus on what we get out of the box in Evennia - we'll get used to the tools,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
# Toc
|
# Toc
|
||||||
|
|
||||||
|
- [Coding/Coding Introduction](Coding/Coding-Introduction)
|
||||||
- [Coding/Coding Overview](Coding/Coding-Overview)
|
- [Coding/Coding Overview](Coding/Coding-Overview)
|
||||||
- [Coding/Continuous Integration](Coding/Continuous-Integration)
|
- [Coding/Continuous Integration](Coding/Continuous-Integration)
|
||||||
- [Coding/Debugging](Coding/Debugging)
|
- [Coding/Debugging](Coding/Debugging)
|
||||||
|
|
@ -93,8 +94,6 @@
|
||||||
- [Howto/NPC shop Tutorial](Howto/NPC-shop-Tutorial)
|
- [Howto/NPC shop Tutorial](Howto/NPC-shop-Tutorial)
|
||||||
- [Howto/Starting/API Overview](Howto/Starting/API-Overview)
|
- [Howto/Starting/API Overview](Howto/Starting/API-Overview)
|
||||||
- [Howto/Starting/Add a simple new web page](Howto/Starting/Add-a-simple-new-web-page)
|
- [Howto/Starting/Add a simple new web page](Howto/Starting/Add-a-simple-new-web-page)
|
||||||
- [Howto/Starting/Adding Object Typeclass Tutorial](Howto/Starting/Adding-Object-Typeclass-Tutorial)
|
|
||||||
- [Howto/Starting/Coding Introduction](Howto/Starting/Coding-Introduction)
|
|
||||||
- [Howto/Starting/Coordinates](Howto/Starting/Coordinates)
|
- [Howto/Starting/Coordinates](Howto/Starting/Coordinates)
|
||||||
- [Howto/Starting/First Steps Coding](Howto/Starting/First-Steps-Coding)
|
- [Howto/Starting/First Steps Coding](Howto/Starting/First-Steps-Coding)
|
||||||
- [Howto/Starting/Game Planning](Howto/Starting/Game-Planning)
|
- [Howto/Starting/Game Planning](Howto/Starting/Game-Planning)
|
||||||
|
|
@ -103,6 +102,8 @@
|
||||||
- [Howto/Starting/Part1/Adding Commands](Howto/Starting/Part1/Adding-Commands)
|
- [Howto/Starting/Part1/Adding Commands](Howto/Starting/Part1/Adding-Commands)
|
||||||
- [Howto/Starting/Part1/Building Quickstart](Howto/Starting/Part1/Building-Quickstart)
|
- [Howto/Starting/Part1/Building Quickstart](Howto/Starting/Part1/Building-Quickstart)
|
||||||
- [Howto/Starting/Part1/Gamedir Overview](Howto/Starting/Part1/Gamedir-Overview)
|
- [Howto/Starting/Part1/Gamedir Overview](Howto/Starting/Part1/Gamedir-Overview)
|
||||||
|
- [Howto/Starting/Part1/Learning Typeclasses](Howto/Starting/Part1/Learning-Typeclasses)
|
||||||
|
- [Howto/Starting/Part1/More on Commands](Howto/Starting/Part1/More-on-Commands)
|
||||||
- [Howto/Starting/Part1/Python basic introduction](Howto/Starting/Part1/Python-basic-introduction)
|
- [Howto/Starting/Part1/Python basic introduction](Howto/Starting/Part1/Python-basic-introduction)
|
||||||
- [Howto/Starting/Part1/Python classes and objects](Howto/Starting/Part1/Python-classes-and-objects)
|
- [Howto/Starting/Part1/Python classes and objects](Howto/Starting/Part1/Python-classes-and-objects)
|
||||||
- [Howto/Starting/Part1/Tutorial World Introduction](Howto/Starting/Part1/Tutorial-World-Introduction)
|
- [Howto/Starting/Part1/Tutorial World Introduction](Howto/Starting/Part1/Tutorial-World-Introduction)
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ A simple mirror object to experiment with.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from evennia import DefaultObject
|
from evennia import DefaultObject
|
||||||
from evennia.utils import make_iter
|
from evennia.utils import make_iter, is_iter
|
||||||
from evennia import logger
|
from evennia import logger
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -51,6 +51,7 @@ class TutorialMirror(DefaultObject):
|
||||||
"""
|
"""
|
||||||
if not text:
|
if not text:
|
||||||
text = "<silence>"
|
text = "<silence>"
|
||||||
|
text = text[0] if is_iter(text) else text
|
||||||
if from_obj:
|
if from_obj:
|
||||||
for obj in make_iter(from_obj):
|
for obj in make_iter(from_obj):
|
||||||
obj.msg(f"{self.key} echoes back to you:\n\"{text}\".")
|
obj.msg(f"{self.key} echoes back to you:\n\"{text}\".")
|
||||||
|
|
|
||||||
|
|
@ -299,7 +299,7 @@ COMMAND_PARSER = "evennia.commands.cmdparser.cmdparser"
|
||||||
# parser expects this. It should also involve a number starting from 1.
|
# parser expects this. It should also involve a number starting from 1.
|
||||||
# When changing this you must also update SEARCH_MULTIMATCH_TEMPLATE
|
# When changing this you must also update SEARCH_MULTIMATCH_TEMPLATE
|
||||||
# to properly describe the syntax.
|
# to properly describe the syntax.
|
||||||
SEARCH_MULTIMATCH_REGEX = r"(?P<number>[0-9]+)-(?P<name>.*)"
|
SEARCH_MULTIMATCH_REGEX = r"(?P<name>.*)-(?P<number>[0-9]+)"
|
||||||
# To display multimatch errors in various listings we must display
|
# To display multimatch errors in various listings we must display
|
||||||
# the syntax in a way that matches what SEARCH_MULTIMATCH_REGEX understand.
|
# the syntax in a way that matches what SEARCH_MULTIMATCH_REGEX understand.
|
||||||
# The template will be populated with data and expects the following markup:
|
# The template will be populated with data and expects the following markup:
|
||||||
|
|
@ -307,7 +307,7 @@ SEARCH_MULTIMATCH_REGEX = r"(?P<number>[0-9]+)-(?P<name>.*)"
|
||||||
# name (key) of the multimatched entity; {aliases} - eventual
|
# name (key) of the multimatched entity; {aliases} - eventual
|
||||||
# aliases for the entity; {info} - extra info like #dbrefs for staff. Don't
|
# aliases for the entity; {info} - extra info like #dbrefs for staff. Don't
|
||||||
# forget a line break if you want one match per line.
|
# forget a line break if you want one match per line.
|
||||||
SEARCH_MULTIMATCH_TEMPLATE = " {number}-{name}{aliases}{info}\n"
|
SEARCH_MULTIMATCH_TEMPLATE = " {name}-{number}{aliases}{info}\n"
|
||||||
# The handler that outputs errors when using any API-level search
|
# The handler that outputs errors when using any API-level search
|
||||||
# (not manager methods). This function should correctly report errors
|
# (not manager methods). This function should correctly report errors
|
||||||
# both for command- and object-searches. This allows full control
|
# both for command- and object-searches. This allows full control
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue