mirror of
https://github.com/evennia/evennia.git
synced 2026-03-16 21:06:30 +01:00
Continue reworking/refactoring the tutorial docs
This commit is contained in:
parent
b97d3581eb
commit
77e78573ca
18 changed files with 741 additions and 926 deletions
|
|
@ -1115,7 +1115,7 @@ function - for example you can't use other Python keywords like `if` inside the
|
|||
|
||||
Unless you are dealing with a relatively simple dynamic menu, defining menus with lambda's is
|
||||
probably more work than it's worth: You can create dynamic menus by instead making each node
|
||||
function more clever. See the [NPC shop tutorial](../Howtos/NPC-shop-Tutorial.md) for an example of this.
|
||||
function more clever. See the [NPC shop tutorial](../Howtos/Tutorial-NPC-Merchants.md) for an example of this.
|
||||
|
||||
|
||||
## Ask for simple input
|
||||
|
|
|
|||
|
|
@ -13,8 +13,7 @@ state. They do not fire callbacks, so are not a good fit for use cases
|
|||
where something needs to happen on a specific schedule (use delay or
|
||||
a TickerHandler for that instead).
|
||||
|
||||
See also the evennia documentation for command cooldowns
|
||||
(https://github.com/evennia/evennia/wiki/Command-Cooldown) for more information
|
||||
See also the evennia [howto](../Howtos/Howto-Command-Cooldown.md) for more information
|
||||
about the concept.
|
||||
|
||||
## Installation
|
||||
|
|
|
|||
|
|
@ -1,36 +1,32 @@
|
|||
# Arxcode installing help
|
||||
|
||||
[Arx - After the Reckoning](https://play.arxmush.org/) is a big and very popular
|
||||
[Evennia](https://www.evennia.com)-based game. Arx is heavily roleplaying-centric, relying on game
|
||||
masters to drive the story. Technically it's maybe best described as "a MUSH, but with more coded
|
||||
systems". In August of 2018, the game's developer, Tehom, generously released the [source code of
|
||||
Arx on github](https://github.com/Arx-Game/arxcode). This is a treasure-trove for developers wanting
|
||||
to pick ideas or even get a starting game to build on.
|
||||
```{warning} Arxcode is separately maintained.
|
||||
|
||||
> These instructions are based on the Arx-code released as of *Aug 12, 2018*. They will probably
|
||||
> not work 100% out of the box anymore. Report any differences and changes needed.
|
||||
While Arxcode uses Evennia, it is _not_ part of Evennia itself; we include this documentation only as a service to users. Also, while Arxcode is still actively maintained (2022), these instructions are based on the Arx-code released as of *Aug 12, 2018*. They will probably not work 100% out of the box anymore.
|
||||
|
||||
Arxcode bugs should be directed to [the Arxcode github issue tracker](https://github.com/Arx-Game/arxcode/issues).
|
||||
```
|
||||
|
||||
[Arx - After the Reckoning](https://play.arxmush.org/) is a big and very popular [Evennia](https://www.evennia.com)-based game. Arx is heavily roleplaying-centric, relying on game masters to drive the story. Technically it's maybe best described as "a MUSH, but with more coded systems". In August of 2018, the game's developer, Tehom, generously released the [source code of Arx on github](https://github.com/Arx-Game/arxcode). This is a treasure-trove for developers wanting to pick ideas or even get a starting game to build on.
|
||||
|
||||
It's not too hard to run Arx from the sources (of course you'll start with an empty database) but
|
||||
since part of Arx has grown organically, it doesn't follow standard Evennia paradigms everywhere.
|
||||
This page covers one take on installing and setting things up while making your new Arx-based game
|
||||
better match with the vanilla Evennia install.
|
||||
This page covers one take on installing and setting things up while making your new Arx-based game better match with the vanilla Evennia install.
|
||||
|
||||
## Installing Evennia
|
||||
|
||||
Firstly, set aside a folder/directory on your drive for everything to follow.
|
||||
|
||||
You need to start by installing [Evennia](https://www.evennia.com) by following most of the
|
||||
[Git-installation instructions](../Setup/Installation-Git.md) for your OS. The difference is that you
|
||||
need to `git clone https://github.com/TehomCD/evennia.git` instead of Evennia's repo because Arx
|
||||
uses TehomCD's older Evennia 0.8 [fork](https://github.com/TehomCD/evennia), notably still using
|
||||
Python2. This detail is important if referring to newer Evennia documentation.
|
||||
You need to start by installing [Evennia](https://www.evennia.com) by following most of the [Git-installation instructions](../Setup/Installation-Git.md) for your OS. The difference is that instead of cloning from upstream Evennia, you should do
|
||||
|
||||
If you are new to Evennia it's *highly* recommended that you run through the normal install
|
||||
instructions in full - including initializing and starting a new empty game and connecting to it.
|
||||
git clone https://github.com/TehomCD/evennia.git
|
||||
|
||||
This is because Arx uses TehomCD's older Evennia 0.8 [fork](https://github.com/TehomCD/evennia), notably still using Python2. This detail is important if referring to newer Evennia documentation.
|
||||
|
||||
If you are new to Evennia it's *highly* recommended that you run through the normal install instructions in full - including initializing and starting a new empty game and connecting to it.
|
||||
That way you can be sure Evennia works correctly as a baseline.
|
||||
|
||||
After installing you should have a `virtualenv` running and you should have the following file
|
||||
structure in your set-aside folder:
|
||||
After installing you should have a `virtualenv` running and you should have the following file structure in your set-aside folder:
|
||||
|
||||
```
|
||||
muddev/
|
||||
|
|
@ -53,8 +49,7 @@ to compare to.
|
|||
A new folder `myarx` should appear next to the ones you already had. You could rename this to
|
||||
something else if you want.
|
||||
|
||||
`cd` into `myarx`. If you wonder about the structure of the game dir, you can
|
||||
[read more about it here](Beginner-Tutorial/Part1/Beginner-Tutorial-Gamedir-Overview.md).
|
||||
`cd` into `myarx`. If you wonder about the structure of the game dir, you can [read more about it here](Beginner-Tutorial/Part1/Beginner-Tutorial-Gamedir-Overview.md).
|
||||
|
||||
### Clean up settings
|
||||
|
||||
|
|
@ -79,13 +74,9 @@ except ImportError:
|
|||
print("secret_settings.py file not found or failed to import.")
|
||||
```
|
||||
|
||||
> Note: Indents and capitalization matter in Python. Make indents 4 spaces (not tabs) for your own
|
||||
> sanity. If you want a starter on Python in Evennia, [you can look here](Python-basic-
|
||||
introduction).
|
||||
> Note: Indents and capitalization matter in Python. Make indents 4 spaces (not tabs) for your own sanity. If you want a starter on Python in Evennia, [you can look here](Beginner-Tutorial-Python-basic- introduction).
|
||||
|
||||
This will import Arx' base settings and override them with the Evennia-default telnet port and give
|
||||
the game a name. The slogan changes the sub-text shown under the name of your game in the website
|
||||
header. You can tweak these to your own liking later.
|
||||
This will import Arx' base settings and override them with the Evennia-default telnet port and give the game a name. The slogan changes the sub-text shown under the name of your game in the website header. You can tweak these to your own liking later.
|
||||
|
||||
Next, create a new, empty file `secret_settings.py` in the same location as the `settings.py` file.
|
||||
This can just contain the following:
|
||||
|
|
@ -95,17 +86,11 @@ SECRET_KEY = "sefsefiwwj3 jnwidufhjw4545_oifej whewiu hwejfpoiwjrpw09&4er43233fw
|
|||
|
||||
```
|
||||
|
||||
Replace the long random string with random ASCII characters of your own. The secret key should not
|
||||
be shared.
|
||||
Replace the long random string with random ASCII characters of your own. The secret key should not be shared.
|
||||
|
||||
Next, open `myarx/server/conf/base_settings.py` in your text editor. We want to remove/comment out
|
||||
all mentions of the `decouple` package, which Evennia doesn't use (we use `private_settings.py` to
|
||||
hide away settings that should not be shared).
|
||||
Next, open `myarx/server/conf/base_settings.py` in your text editor. We want to remove/comment out all mentions of the `decouple` package, which Evennia doesn't use (we use `private_settings.py` to hide away settings that should not be shared).
|
||||
|
||||
Comment out `from decouple import config` by adding a `#` to the start of the line: `# from decouple
|
||||
import config`. Then search for `config(` in the file and comment out all lines where this is used.
|
||||
Many of these are specific to the server environment where the original Arx runs, so is not that
|
||||
relevant to us.
|
||||
Comment out `from decouple import config` by adding a `#` to the start of the line: `# from decouple import config`. Then search for `config(` in the file and comment out all lines where this is used. Many of these are specific to the server environment where the original Arx runs, so is not that relevant to us.
|
||||
|
||||
### Install Arx dependencies
|
||||
|
||||
|
|
@ -141,10 +126,7 @@ This creates the database and will step through all database migrations needed.
|
|||
|
||||
evennia start
|
||||
|
||||
If all goes well Evennia will now start up, running Arx! You can connect to it on `localhost` (or
|
||||
`127.0.0.1` if your platform doesn't alias `localhost`), port `4000` using a Telnet client.
|
||||
Alternatively, you can use your web browser to browse to `http://localhost:4001` to see the game's
|
||||
website and get to the web client.
|
||||
If all goes well Evennia will now start up, running Arx! You can connect to it on `localhost` (or `127.0.0.1` if your platform doesn't alias `localhost`), port `4000` using a Telnet client. Alternatively, you can use your web browser to browse to `http://localhost:4001` to see the game's website and get to the web client.
|
||||
|
||||
When you log in you'll get the standard Evennia greeting (since the database is empty), but you can
|
||||
try `help` to see that it's indeed Arx that is running.
|
||||
|
|
@ -162,101 +144,87 @@ run steps 7-8 and 10 to create and connect to your in-came Character.
|
|||
3. Navigate to the `Accounts` section.
|
||||
4. Add a new Account named for the new staffer. Use a place holder password and dummy e-mail
|
||||
address.
|
||||
5. Flag account as `Staff` and apply the `Admin` permission group (This assumes you have already set
|
||||
up an Admin Group in Django).
|
||||
5. Flag account as `Staff` and apply the `Admin` permission group (This assumes you have already set up an Admin Group in Django).
|
||||
6. Add Tags named `player` and `developer`.
|
||||
7. Log into the game using the web client (or a third-party telnet client) using your superuser
|
||||
account. Move to where you want the new staffer character to appear.
|
||||
8. In the game client, run `@create/drop <staffername>:typeclasses.characters.Character`, where
|
||||
`<staffername>` is usually the same name you used for the Staffer account you created in the
|
||||
Admin earlier (if you are creating a Character for your superuser, use your superuser account
|
||||
name).
|
||||
This creates a new in-game Character and places it in your current location.
|
||||
7. Log into the game using the web client (or a third-party telnet client) using your superuser account. Move to where you want the new staffer character to appear.
|
||||
8. In the game client, run `@create/drop <staffername>:typeclasses.characters.Character`, where `<staffername>` is usually the same name you used for the Staffer account you created in the Admin earlier (if you are creating a Character for your superuser, use your superuser account name). This creates a new in-game Character and places it in your current location.
|
||||
9. Have the new Admin player log into the game.
|
||||
10. Have the new Admin puppet the character with `@ic StafferName`.
|
||||
11. Have the new Admin change their password - `@password <old password> = <new password>`.
|
||||
|
||||
Now that you have a Character and an Account object, there's a few additional things you may need to
|
||||
do in order for some commands to function properly. You can either execute these as in-game commands
|
||||
while `ic` (controlling your character object).
|
||||
Now that you have a Character and an Account object, there's a few additional things you may need to do in order for some commands to function properly. You can either execute these as in-game commands while `ic` (controlling your character object).
|
||||
|
||||
1. `py from web.character.models import RosterEntry;RosterEntry.objects.create(player=self.player,
|
||||
character=self)`
|
||||
2. `py from world.dominion.models import PlayerOrNpc, AssetOwner;dompc =
|
||||
PlayerOrNpc.objects.create(player = self.player);AssetOwner.objects.create(player=dompc)`
|
||||
py from web.character.models import RosterEntry;RosterEntry.objects.create(player=self.player, character=self)
|
||||
|
||||
Those steps will give you a 'RosterEntry', 'PlayerOrNpc', and 'AssetOwner' objects. RosterEntry
|
||||
py from world.dominion.models import PlayerOrNpc, AssetOwner;dompc = PlayerOrNpc.objects.create(player=self.player);AssetOwner.objects.create(player=dompc)
|
||||
|
||||
Those steps will give you 'RosterEntry', 'PlayerOrNpc', and 'AssetOwner' objects. RosterEntry
|
||||
explicitly connects a character and account object together, even while offline, and contains
|
||||
additional information about a character's current presence in game (such as which 'roster' they're
|
||||
in, if you choose to use an active roster of characters). PlayerOrNpc are more character extensions,
|
||||
as well as support for npcs with no in-game presence and just represented by a name which can be
|
||||
offscreen members of a character's family. It also allows for membership in Organizations.
|
||||
AssetOwner holds information about a character or organization's money and resources.
|
||||
in, if you choose to use an active roster of characters). PlayerOrNpc are more character extensions, as well as support for npcs with no in-game presence and just represented by a name which can be offscreen members of a character's family. It also allows for membership in Organizations. AssetOwner holds information about a character or organization's money and resources.
|
||||
|
||||
## Alternate Windows install guide
|
||||
|
||||
_Contributed by Pax_
|
||||
|
||||
If for some reason you cannot use the Windows Subsystem for Linux (which would use instructions
|
||||
identical to the ones above), it's possible to get Evennia/Arx running under Anaconda for Windows. The
|
||||
process is a little bit trickier.
|
||||
If for some reason you cannot use the Windows Subsystem for Linux (which would use instructions identical to the ones above), it's possible to get Evennia/Arx running under Anaconda for Windows. The process is a little bit trickier.
|
||||
|
||||
Make sure you have:
|
||||
* Git for Windows https://git-scm.com/download/win
|
||||
* Anaconda for Windows https://www.anaconda.com/distribution/
|
||||
* VC++ Compiler for Python 2.7 https://aka.ms/vcpython27
|
||||
|
||||
conda update conda
|
||||
conda create -n arx python=2.7
|
||||
source activate arx
|
||||
conda update conda
|
||||
conda create -n arx python=2.7
|
||||
source activate arx
|
||||
|
||||
Set up a convenient repository place for things.
|
||||
Set up a convenient repository place for things.
|
||||
|
||||
cd ~
|
||||
mkdir Source
|
||||
cd Source
|
||||
mkdir Arx
|
||||
cd Arx
|
||||
cd ~
|
||||
mkdir Source
|
||||
cd Source
|
||||
mkdir Arx
|
||||
cd Arx
|
||||
|
||||
Replace the SSH git clone links below with your own github forks.
|
||||
If you don't plan to change Evennia at all, you can use the
|
||||
evennia/evennia.git repo instead of a forked one.
|
||||
Replace the SSH git clone links below with your own github forks.
|
||||
If you don't plan to change Evennia at all, you can use the
|
||||
evennia/evennia.git repo instead of a forked one.
|
||||
|
||||
git clone git@github.com:<youruser>/evennia.git
|
||||
git clone git@github.com:<youruser>/arxcode.git
|
||||
git clone git@github.com:<youruser>/evennia.git
|
||||
git clone git@github.com:<youruser>/arxcode.git
|
||||
|
||||
Evennia is a package itself, so we want to install it and all of its
|
||||
prerequisites, after switching to the appropriately-tagged branch for
|
||||
Arxcode.
|
||||
Evennia is a package itself, so we want to install it and all of its
|
||||
prerequisites, after switching to the appropriately-tagged branch for
|
||||
Arxcode.
|
||||
|
||||
cd evennia
|
||||
git checkout tags/v0.7 -b arx-master
|
||||
pip install -e .
|
||||
cd evennia
|
||||
git checkout tags/v0.7 -b arx-master
|
||||
pip install -e .
|
||||
|
||||
Arx has some dependencies of its own, so now we'll go install them
|
||||
As it is not a package, we'll use the normal requirements file.
|
||||
Arx has some dependencies of its own, so now we'll go install them
|
||||
As it is not a package, we'll use the normal requirements file.
|
||||
|
||||
cd ../arxcode
|
||||
pip install -r requirements.txt
|
||||
cd ../arxcode
|
||||
pip install -r requirements.txt
|
||||
|
||||
The git repo doesn't include the empty log directory and Evennia is unhappy if you
|
||||
don't have it, so while still in the arxcode directory...
|
||||
The git repo doesn't include the empty log directory and Evennia is unhappy if you
|
||||
don't have it, so while still in the arxcode directory...
|
||||
|
||||
mkdir server/logs
|
||||
mkdir server/logs
|
||||
|
||||
Now hit https://github.com/evennia/evennia/wiki/Arxcode-installing-help and
|
||||
change the setup stuff as in the 'Clean up settings' section.
|
||||
Now hit https://github.com/evennia/evennia/wiki/Arxcode-installing-help and
|
||||
change the setup stuff as in the 'Clean up settings' section.
|
||||
|
||||
Then we will create our default database...
|
||||
Then we will create our default database...
|
||||
|
||||
../evennia/bin/windows/evennia.bat migrate
|
||||
../evennia/bin/windows/evennia.bat migrate
|
||||
|
||||
...and do the first run. You need winpty because Windows does not have a TTY/PTY
|
||||
by default, and so the Python console input commands (used for prompts on first
|
||||
run) will fail and you will end up in an unhappy place. Future runs, you should
|
||||
not need winpty.
|
||||
|
||||
winpty ../evennia/bin/windows/evennia.bat start
|
||||
winpty ../evennia/bin/windows/evennia.bat start
|
||||
|
||||
Once this is done, you should have your Evennia server running Arxcode up
|
||||
on localhost at port 4000, and the webserver at http://localhost:4001/
|
||||
on localhost at port 4000, and the webserver at http://localhost:4001/.
|
||||
113
docs/source/Howtos/Howto-Add-Object-Weight.md
Normal file
113
docs/source/Howtos/Howto-Add-Object-Weight.md
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
# Give objects weight
|
||||
|
||||
All in-game objets you can touch usually has some weight. What weight does varies from game to game. Commonly it limits how much you can carry. A heavy stone may also hurt you more than a ballon, if it falls on you. If you want to get fancy, a pressure plate may only trigger if the one stepping on it is heavy enough.
|
||||
|
||||
```{code-block} python
|
||||
:linenos:
|
||||
:emphasize-lines: 6,8,10,12
|
||||
|
||||
# inside your mygame/typeclasses/objects.py
|
||||
|
||||
from evennia import DefaultObject
|
||||
from evennia import AttributeProperty
|
||||
|
||||
class ObjectParent:
|
||||
|
||||
weight = AttributeProperty(default=1, autocreate=False)
|
||||
|
||||
@property
|
||||
def total_weight(self):
|
||||
return self.weight + sum(obj.total_weight for obj in self.contents)
|
||||
|
||||
|
||||
class Object(ObjectParent, DefaultObject):
|
||||
# ...
|
||||
```
|
||||
|
||||
```{sidebar} Why not mass?
|
||||
Yes, we know weight varies with gravity. 'Mass' is more scientifically correct. But 'mass' is less commonly used in RPGs, so we stick to 'weight' here. Just know if if your sci-fi characters can vacation on the Moon (1/6 gravity of Earth) you should consider using `mass` everywhere and calculate the current weight on the fly.
|
||||
```
|
||||
|
||||
- **Line 6**: We use the `ObjectParent` mixin. Since this mixin is used for `Characters`, `Exits` and `Rooms` as well as for `Object`, it means all of those will automatically _also_ have weight!
|
||||
- **Line 8**: We use an [AttributeProperty](../Components/Attributes.md#using-attributeproperty) to set up the 'default' weight of 1 (whatever that is). Setting `autocreate=False` means no actual `Attribute` will be created until the weight is actually changed from the default of 1. See the `AttributeProperty` documentation for caveats with this.
|
||||
- **Line 10 and 11**: Using the `@property` decorator on `total_weight` means that we will be able to call `obj.total_weight` instead of `obj.total_weight()` later.
|
||||
- **Line 12**: We sum up all weights from everything "in" this object, by looping over `self.contents`. Since _all_ objects will have weight now, this should always work!
|
||||
|
||||
Let's check out the weight of some trusty boxes
|
||||
```
|
||||
> create/drop box1
|
||||
> py self.search("box1").weight
|
||||
1
|
||||
> py self.search("box1").total_weight
|
||||
1
|
||||
```
|
||||
|
||||
Let's put another box into the first one.
|
||||
|
||||
```
|
||||
> create/drop box2
|
||||
> py self.search("box2").total_weight
|
||||
1
|
||||
> py self.search("box2").location = self.search("box1")
|
||||
> py self.search(box1).total_weight
|
||||
2
|
||||
```
|
||||
|
||||
|
||||
## Limit inventory by weight carried
|
||||
|
||||
To limit how much you can carry, you first need to know your own strength
|
||||
|
||||
```python
|
||||
# in mygame/typeclasses/characters.py
|
||||
|
||||
from evennia import AttributeProperty
|
||||
|
||||
# ...
|
||||
|
||||
class Character(ObjectParent, DefaultCharacter):
|
||||
|
||||
carrying_capacity = AttributeProperty(10, autocreate=False)
|
||||
|
||||
@property
|
||||
def carried_weight(self):
|
||||
return self.total_weight - self.weight
|
||||
|
||||
```
|
||||
|
||||
Here we make sure to add another `AttributeProperty` telling us how much to carry. In a real game, this may be based on how strong the Character is. When we consider how much weight we already carry, we should not include _our own_ weight, so we subtract that.
|
||||
|
||||
To honor this limit, we'll need to override the default `get` command.
|
||||
|
||||
|
||||
```{sidebar} Overriding default commands
|
||||
|
||||
In this example, we implement the beginning of the `CmdGet` and then call the full `CmdGet()` at the end. This is not very efficient, because the parent `CmdGet` will again have to do the `caller.search()` again. To be more efficient, you will likely want to copy the entirety of the `CmdGet` code into your own version and modify it.
|
||||
```
|
||||
|
||||
```python
|
||||
# in mygame/commands/command.py
|
||||
|
||||
# ...
|
||||
from evennia import default_cmds
|
||||
|
||||
# ...
|
||||
|
||||
class WeightAwareCmdGet(default_cmds.CmdGet):
|
||||
|
||||
def func(self):
|
||||
caller = self.caller
|
||||
if not self.args:
|
||||
caller.msg("Get what?")
|
||||
return
|
||||
|
||||
obj = caller.search(self.args)
|
||||
|
||||
if (obj.weight + caller.carried_weight
|
||||
> caller.carrying_capacity):
|
||||
caller.msg("You can't carry that much!")
|
||||
return
|
||||
super().func()
|
||||
```
|
||||
|
||||
Here we add an extra check for the weight of the thing we are trying to pick up, then we call the normal `CmdGet` with `super().func()`.
|
||||
|
|
@ -1,30 +1,27 @@
|
|||
# Command Cooldown
|
||||
# Adding Command Cooldowns
|
||||
|
||||
> hit goblin with sword
|
||||
You strike goblin with the sword. It dodges!
|
||||
> hit goblin with sword
|
||||
You are off-balance and can't attack again yet.
|
||||
|
||||
Some types of games want to limit how often a command can be run. If a
|
||||
character casts the spell *Firestorm*, you might not want them to spam that
|
||||
command over and over. Or in an advanced combat system, a massive swing may
|
||||
command over and over. In an advanced combat system, a massive swing may
|
||||
offer a chance of lots of damage at the cost of not being able to re-do it for
|
||||
a while. Such effects are called *cooldowns*.
|
||||
a while.
|
||||
|
||||
This page exemplifies a very resource-efficient way to do cooldowns. A more
|
||||
'active' way is to use asynchronous delays as in the [](Howto-Command-Duration.md#blocking-commands), the two might be useful to
|
||||
combine if you want to echo some message to the user after the cooldown ends.
|
||||
Such effects are called *command cooldowns*.
|
||||
|
||||
## The Cooldown Contrib
|
||||
```{sidebar}
|
||||
The [Cooldown contrib](../Contribs/Contrib-Cooldowns.md) is a ready-made solution for command cooldowns. It is based on this howto and implements a [handler](Tutorial-Peristent-Handler) on the object to conveniently manage and store the cooldowns.
|
||||
```
|
||||
This howto exemplifies a very resource-efficient way to do cooldowns. A more
|
||||
'active' way is to use asynchronous delays as in the [Command-Duration howto](./Howto-Command-Duration.md#blocking-commands) suggests. The two howto's might be useful to combine if you want to echo some message to the user after the cooldown ends.
|
||||
|
||||
The [Cooldown contrib](../Contribs/Contrib-Cooldowns.md) is a ready-made solution for
|
||||
command cooldowns you can use. It implements a _handler_ on the object to
|
||||
conveniently manage and store the cooldowns in a similar manner exemplified in
|
||||
this tutorial.
|
||||
## An efficient cooldown
|
||||
|
||||
## Non-persistent cooldown
|
||||
|
||||
This little recipe will limit how often a particular command can be run. Since
|
||||
Commands are class instances, and those are cached in memory, a command
|
||||
instance will remember things you store on it. So just store the current time
|
||||
of execution! Next time the command is run, it just needs to check if it has
|
||||
that time stored, and compare it with the current time to see if a desired
|
||||
delay has passed.
|
||||
The idea is that when a [Command](../Components/Commands.md) runs, we store the time it runs. When it next runs, we check again the current time. The command is only allowed to run if enough time passed since now and the previous run. This is a _very_ efficient implementation that only checks on-demand.
|
||||
|
||||
```python
|
||||
# in, say, mygame/commands/spells.py
|
||||
|
|
@ -49,7 +46,7 @@ class CmdSpellFirestorm(default_cmds.MuxCommand):
|
|||
"Implement the spell"
|
||||
|
||||
now = time.time()
|
||||
last_cast = caller.ndb.firestorm_last_cast # could be None
|
||||
last_cast = caller.db.firestorm_last_cast # could be None
|
||||
if last_cast and (now - last_cast < self.rate_of_fire):
|
||||
message = "You cannot cast this spell again yet."
|
||||
self.caller.msg(message)
|
||||
|
|
@ -58,22 +55,21 @@ class CmdSpellFirestorm(default_cmds.MuxCommand):
|
|||
# [the spell effect is implemented]
|
||||
|
||||
# if the spell was successfully cast, store the casting time
|
||||
self.caller.ndb.firestorm_last_cast = now
|
||||
self.caller.db.firestorm_last_cast = now
|
||||
```
|
||||
|
||||
We specify `rate_of_fire` and then just check for a NAtrribute
|
||||
`firestorm_last_cast` and update it if everything works out.
|
||||
We specify `rate_of_fire` and then just check for an [Attribute](../Components/Attributes.md) `firestorm_last_cast` on the `caller.` It is either `None` (because the spell was never cast before) or an timestamp representing the last time the spell was cast.
|
||||
|
||||
Simple and very effective since everything is just stored in memory. The
|
||||
drawback of this simple scheme is that it's non-persistent. If you do
|
||||
`reload`, the cache is cleaned and all such ongoing cooldowns will be
|
||||
forgotten.
|
||||
### Non-Persistent cooldown
|
||||
|
||||
## Persistent cooldown
|
||||
The above implementation will survive a reload. If you don't want that, you can just switch to let `firestorm_last_cast` be a [NAtrribute](../Components/Attributes.md#in-memory-attributes-nattributes) instead. For example:
|
||||
|
||||
To make a cooldown _persistent_ (so it survives a server reload), just
|
||||
use the same technique, but use [Attributes](../Components/Attributes.md) (that is, `.db` instead
|
||||
of `.ndb` storage to save the last-cast time.
|
||||
```python
|
||||
last_cast = caller.ndb.firestorm_last_cast
|
||||
# ...
|
||||
self.caller.ndb.firestorm_last_cast = now
|
||||
```
|
||||
That is, use `.ndb` instead of `.db`. Since a `NAttribute`s are purely in-memory, they can be faster to read and write to than an `Attribute`. So this can be more optimal if your intervals are short and need to change often. The drawback is that they'll reset if the server reloads.
|
||||
|
||||
## Make a cooldown-aware command parent
|
||||
|
||||
|
|
@ -154,5 +150,5 @@ you can have all fire-related spells store the cooldown with the same
|
|||
`cooldown_storage_key` (like `fire_spell_last_used`). That would mean casting
|
||||
of *Firestorm* would block all other fire-related spells for a while.
|
||||
|
||||
Similarly, when you take that that big sword swing, other types of attacks could
|
||||
Similarly, when you take that big sword swing, other types of attacks could
|
||||
be blocked before you can recover your balance.
|
||||
|
|
|
|||
|
|
@ -1,24 +1,41 @@
|
|||
# Command Duration
|
||||
# Commands that take time to finish
|
||||
|
||||
|
||||
Before reading this tutorial, if you haven't done so already, you might want to
|
||||
read [the documentation on commands](../Components/Commands.md) to get a basic understanding of
|
||||
how commands work in Evennia.
|
||||
> craft fine sword
|
||||
You start crafting a fine sword.
|
||||
> north
|
||||
You are too focused on your crafting, and can't move!
|
||||
You create the blade of the sword.
|
||||
You create the pommel of the sword.
|
||||
You finish crafting a Fine Sword.
|
||||
|
||||
In some types of games a command should not start and finish immediately.
|
||||
|
||||
Loading a crossbow might take a bit of time to do - time you don't have when
|
||||
the enemy comes rushing at you. Crafting that armour will not be immediate
|
||||
either. For some types of games the very act of moving or changing pose all
|
||||
comes with a certain time associated with it.
|
||||
|
||||
## The simple way to pause commands with yield
|
||||
There are two main suitable ways to introduce a 'delay' in a [Command](../Components/Commands.md)'s execution:
|
||||
|
||||
Evennia allows a shortcut in syntax to create simple pauses in commands. This
|
||||
syntax uses the `yield` keyword. The `yield` keyword is used in Python to
|
||||
create generators, although you don't need to know what generators are to use
|
||||
this syntax. A short example will probably make it clear:
|
||||
- Using `yield` in the Command's `func` method.
|
||||
- Using the `evennia.utils.delay` utility function.
|
||||
|
||||
We'll simplify both below.
|
||||
|
||||
## Pause commands with `yield`
|
||||
|
||||
The `yield` keyword is a reserved word in Python. It's used to create [generators](https://realpython.com/introduction-to-python-generators/), which are interesting in their own right. For the purpose of this howto though, we just need to know that Evennia will use it to 'pause' the execution of the command for a certain time.
|
||||
|
||||
```{sidebar} This only works in Command.func!
|
||||
|
||||
This `yield` functionality will *only* work in the `func` method of
|
||||
Commands. It works because Evennia has especially catered for it as a convenient shortcut. Trying to use it elsewhere will not work. If you want the same functionality elsewhere you should look up the [interactive decorator](../Concepts/Async-Process.md#the-interactive-decorator).
|
||||
```
|
||||
|
||||
```{code-block} python
|
||||
:linenos:
|
||||
:emphasize-lines: 15
|
||||
|
||||
```python
|
||||
class CmdTest(Command):
|
||||
|
||||
"""
|
||||
|
|
@ -30,53 +47,55 @@ class CmdTest(Command):
|
|||
"""
|
||||
|
||||
key = "test"
|
||||
locks = "cmd:all()"
|
||||
|
||||
def func(self):
|
||||
self.msg("Before ten seconds...")
|
||||
yield 10
|
||||
self.msg("Afterwards.")
|
||||
```
|
||||
> Important: The `yield` functionality will *only* work in the `func` method of
|
||||
> Commands. It only works because Evennia has especially
|
||||
> catered for it in Commands. If you want the same functionality elsewhere you
|
||||
> must use the [interactive decorator](../Concepts/Async-Process.md#the-interactive-decorator).
|
||||
|
||||
The important line is the `yield 10`. It tells Evennia to "pause" the command
|
||||
- **Line 15** : This is the important line. The `yield 10` tells Evennia to "pause" the command
|
||||
and to wait for 10 seconds to execute the rest. If you add this command and
|
||||
run it, you'll see the first message, then, after a pause of ten seconds, the
|
||||
next message. You can use `yield` several times in your command.
|
||||
|
||||
This syntax will not "freeze" all commands. While the command is "pausing",
|
||||
you can execute other commands (or even call the same command again). And
|
||||
other players aren't frozen either.
|
||||
This syntax will not "freeze" all commands. While the command is "pausing", you can execute other commands (or even call the same command again). And other players aren't frozen either.
|
||||
|
||||
> Note: this will not save anything in the database. If you reload the game
|
||||
> while a command is "paused", it will not resume after the server has
|
||||
> reloaded.
|
||||
> Using `yield` is non-persistent. If you `reload` the game while a command is "paused", that pause state is lost and it will _not_ resume after the server has reloaded.
|
||||
|
||||
## Pause commands with `utils.delay`
|
||||
|
||||
## The more advanced way with utils.delay
|
||||
The `yield` syntax is easy to read, easy to understand, easy to use. But it's non-persistent and not that flexible if you want more advanced options.
|
||||
|
||||
The `yield` syntax is easy to read, easy to understand, easy to use. But it's not that flexible if
|
||||
you want more advanced options. Learning to use alternatives might be much worth it in the end.
|
||||
The `evennia.utils.delay` represents is a more powerful way to introduce delays. Unlike `yield`, it
|
||||
can be made persistent and also works outside of `Command.func`. It's however a little more cumbersome to write since unlike `yield` it will not actually stop at the line it's called.
|
||||
|
||||
Below is a simple command example for adding a duration for a command to finish.
|
||||
```{code-block} python
|
||||
:linenos:
|
||||
:emphasize-lines: 14,30
|
||||
|
||||
```python
|
||||
from evennia import default_cmds, utils
|
||||
|
||||
class CmdEcho(default_cmds.MuxCommand):
|
||||
"""
|
||||
wait for an echo
|
||||
Wait for an echo
|
||||
|
||||
Usage:
|
||||
echo <string>
|
||||
|
||||
Calls and waits for an echo
|
||||
Calls and waits for an echo.
|
||||
"""
|
||||
key = "echo"
|
||||
locks = "cmd:all()"
|
||||
|
||||
def echo(self):
|
||||
"Called after 10 seconds."
|
||||
shout = self.args
|
||||
self.caller.msg(
|
||||
"You hear an echo: "
|
||||
f"{shout.upper()} ... "
|
||||
f"{shout.capitalize()} ... "
|
||||
f"{shout.lower()}"
|
||||
)
|
||||
|
||||
def func(self):
|
||||
"""
|
||||
|
|
@ -86,50 +105,39 @@ class CmdEcho(default_cmds.MuxCommand):
|
|||
# this waits non-blocking for 10 seconds, then calls self.echo
|
||||
utils.delay(10, self.echo) # call echo after 10 seconds
|
||||
|
||||
def echo(self):
|
||||
"Called after 10 seconds."
|
||||
shout = self.args
|
||||
self.caller.msg(
|
||||
f"You hear an echo: {shout.upper()} ... {shout.capitalize()} ... {shout.lower()}"
|
||||
)
|
||||
```
|
||||
|
||||
Import this new echo command into the default command set and reload the server. You will find that
|
||||
it will take 10 seconds before you see your shout coming back. You will also find that this is a
|
||||
*non-blocking* effect; you can issue other commands in the interim and the game will go on as usual.
|
||||
The echo will come back to you in its own time.
|
||||
Import this new echo command into the default command set and reload the server. You will find that it will take 10 seconds before you see your shout coming back.
|
||||
|
||||
### About utils.delay()
|
||||
- **Line 14**: We add a new method `echo`. This is a _callback_ - a method/function we will call after a certain time.
|
||||
- **Line 30**: Here we use `utils.delay` to tell Evennia "Please wait for 10 seconds, then call "`self.echo`". Note how we pass `self.echo` and _not_ `self.echo()`! If we did the latter, `echo` would fire _immediately_. Instead we let Evennia do this call for us ten seconds later.
|
||||
|
||||
`utils.delay(timedelay, callback, persistent=False, *args, **kwargs)` is a useful function. It will
|
||||
wait `timedelay` seconds, then call the `callback` function, optionally passing to it the arguments
|
||||
provided to utils.delay by way of *args and/or **kwargs`.
|
||||
You will also find that this is a *non-blocking* effect; you can issue other commands in the interim and the game will go on as usual. The echo will come back to you in its own time.
|
||||
|
||||
> Note: The callback argument should be provided with a python path to the desired function, for
|
||||
instance `my_object.my_function` instead of `my_object.my_function()`. Otherwise my_function would
|
||||
get called and run immediately upon attempting to pass it to the delay function.
|
||||
If you want to provide arguments for utils.delay to use, when calling your callback function, you
|
||||
have to do it separatly, for instance using the utils.delay *args and/or **kwargs, as mentioned
|
||||
above.
|
||||
The call signature for `utils.delay` is:
|
||||
|
||||
> If you are not familiar with the syntax `*args` and `**kwargs`, [see the Python documentation
|
||||
here](https://docs.python.org/2/tutorial/controlflow.html#arbitrary-argument-lists).
|
||||
```python
|
||||
utils.delay(timedelay, callback, persistent=False, *args, **kwargs)
|
||||
```
|
||||
|
||||
Looking at it you might think that `utils.delay(10, callback)` in the code above is just an
|
||||
alternative to some more familiar thing like `time.sleep(10)`. This is *not* the case. If you do
|
||||
`time.sleep(10)` you will in fact freeze the *entire server* for ten seconds! The `utils.delay()`is
|
||||
a thin wrapper around a Twisted
|
||||
[Deferred](https://twistedmatrix.com/documents/11.0.0/core/howto/defer.html) that will delay
|
||||
execution until 10 seconds have passed, but will do so asynchronously, without bothering anyone else
|
||||
(not even you - you can continue to do stuff normally while it waits to continue).
|
||||
```{sidebar} *args and **kwargs
|
||||
|
||||
The point to remember here is that the `delay()` call will not "pause" at that point when it is
|
||||
These are used to indicate any number of arguments or keyword-arguments should be picked up here. In code they are treated as a `tuple` and a `dict` respectively.
|
||||
|
||||
`*args` and `**kwargs` are used in many places in Evennia. [See an online tutorial here](https://realpython.com/python-kwargs-and-args).
|
||||
```
|
||||
If you set `persistent=True`, this delay will survive a `reload`. If you pass `*args` and/or `**kwargs`, they will be passed on into the `callback`. So this way you can pass more complex arguments to the delayed function.
|
||||
|
||||
It's important to remember that the `delay()` call will not "pause" at that point when it is
|
||||
called (the way `yield` does in the previous section). The lines after the `delay()` call will
|
||||
actually execute *right away*. What you must do is to tell it which function to call *after the time
|
||||
has passed* (its "callback"). This may sound strange at first, but it is normal practice in
|
||||
asynchronous systems. You can also link such calls together as seen below:
|
||||
asynchronous systems. You can also link such calls together:
|
||||
|
||||
```{code-block}
|
||||
:linenos:
|
||||
:emphasize-lines: 19,22,28,34
|
||||
|
||||
```python
|
||||
from evennia import default_cmds, utils
|
||||
|
||||
class CmdEcho(default_cmds.MuxCommand):
|
||||
|
|
@ -142,7 +150,6 @@ class CmdEcho(default_cmds.MuxCommand):
|
|||
Calls and waits for an echo
|
||||
"""
|
||||
key = "echo"
|
||||
locks = "cmd:all()"
|
||||
|
||||
def func(self):
|
||||
"This sets off a chain of delayed calls"
|
||||
|
|
@ -172,25 +179,36 @@ class CmdEcho(default_cmds.MuxCommand):
|
|||
The above version will have the echoes arrive one after another, each separated by a two second
|
||||
delay.
|
||||
|
||||
> echo Hello!
|
||||
... HELLO!
|
||||
... Hello!
|
||||
... hello! ...
|
||||
- **Line 19**: This sets off the chain, telling Evennia to wait 2 seconds before calling `self.echo1`.
|
||||
- **Line 22**: This is called after 2 seconds. It tells Evennia to wait another 2 seconds before calling `self.echo2`.
|
||||
- **Line 28**: This is called after yet another 2 seonds (4s total). It tells Evennia to wait another 2 seconds before calling, `self.echo3`.
|
||||
- **Line34** Called after another 2 seconds (6s total). This ends the delay-chain.
|
||||
|
||||
## Blocking commands
|
||||
```
|
||||
> echo Hello!
|
||||
... HELLO!
|
||||
... Hello!
|
||||
... hello! ...
|
||||
```
|
||||
|
||||
As mentioned, a great thing about the delay introduced by `yield` or `utils.delay()` is that it does
|
||||
not block. It just goes on in the background and you are free to play normally in the interim. In
|
||||
some cases this is not what you want however. Some commands should simply "block" other commands
|
||||
while they are running. If you are in the process of crafting a helmet you shouldn't be able to also
|
||||
start crafting a shield at the same time, or if you just did a huge power-swing with your weapon you
|
||||
should not be able to do it again immediately.
|
||||
```{warning} What about time.sleep?
|
||||
|
||||
The simplest way of implementing blocking is to use the technique covered in the [](Howto-Command-Cooldown.md) tutorial. In that tutorial we implemented cooldowns by having the
|
||||
Command store the current time. Next time the Command was called, we compared the current time to
|
||||
the stored time to determine if enough time had passed for a renewed use. This is a *very*
|
||||
efficient, reliable and passive solution. The drawback is that there is nothing to tell the Player
|
||||
when enough time has passed unless they keep trying.
|
||||
You may be aware of the `time.sleep` function coming with Python. Doing `time.sleep(10) pauses Python for 10 seconds. **Do not use this**, it will not work with Evennia. If you use it, you will block the _entire server_ (everyone!) for ten seconds!
|
||||
|
||||
If you want specifics, `utils.delay` is a thin wrapper around a [Twisted Deferred](https://docs.twisted.org/en/twisted-22.1.0/core/howto/defer.html). This is an [asynchronous concept](../Concepts/Async-Process.md).
|
||||
```
|
||||
|
||||
## Making a blocking command
|
||||
|
||||
Both `yield` or `utils.delay()` pauses the command but allows the user to use other commands while the first one waits to finish.
|
||||
|
||||
In some cases you want to instead have that command 'block' other commands from running. An example is crafting a helmet: most likely you should not be able to start crafting a shield at the same time. Or even walk out of the smithy.
|
||||
|
||||
The simplest way of implementing blocking is to use the technique covered in the [How to implement a Command Cooldown](./Howto-Command-Cooldown.md) tutorial. In that tutorial we cooldowns are implemented by comparing the current time with the last time the command was used. This is the best approach if you can get away with it. It could work well for our crafting example ... _if_ you don't want to automatically update the player on their progress.
|
||||
|
||||
In short:
|
||||
- If you are fine with the player making an active input to check their status, compare timestamps as done in the Command-cooldown tutorial. On-demand is by far the most efficent.
|
||||
- If you want Evennia to tell the user their status without them taking a further action, you need to use `yield` , `delay` (or some other active time-keeping method).
|
||||
|
||||
Here is an example where we will use `utils.delay` to tell the player when the cooldown has passed:
|
||||
|
||||
|
|
@ -238,11 +256,9 @@ Note how, after the cooldown, the user will get a message telling them they are
|
|||
another swing.
|
||||
|
||||
By storing the `off_balance` flag on the character (rather than on, say, the Command instance
|
||||
itself) it can be accessed by other Commands too. Other attacks may also not work when you are off
|
||||
balance. You could also have an enemy Command check your `off_balance` status to gain bonuses, to
|
||||
take another example.
|
||||
itself) it can be accessed by other Commands too. Other attacks may also not work when you are off balance. You could also have an enemy Command check your `off_balance` status to gain bonuses, to take another example.
|
||||
|
||||
## Abortable commands
|
||||
## Make a Command possible to Abort
|
||||
|
||||
One can imagine that you will want to abort a long-running command before it has a time to finish.
|
||||
If you are in the middle of crafting your armor you will probably want to stop doing that when a
|
||||
|
|
@ -344,59 +360,4 @@ class CmdAttack(default_cmds.MuxCommand):
|
|||
|
||||
The above code creates a delayed crafting command that will gradually create the armour. If the
|
||||
`attack` command is issued during this process it will set a flag that causes the crafting to be
|
||||
quietly canceled next time it tries to update.
|
||||
|
||||
## Persistent delays
|
||||
|
||||
In the latter examples above we used `.ndb` storage. This is fast and easy but it will reset all
|
||||
cooldowns/blocks/crafting etc if you reload the server. If you don't want that you can replace
|
||||
`.ndb` with `.db`. But even this won't help because the `yield` keyword is not persisent and nor is
|
||||
the use of `delay` shown above. To resolve this you can use `delay` with the `persistent=True`
|
||||
keyword. But wait! Making something persistent will add some extra complications, because now you
|
||||
must make sure Evennia can properly store things to the database.
|
||||
|
||||
Here is the original echo-command reworked to function with persistence:
|
||||
```python
|
||||
from evennia import default_cmds, utils
|
||||
|
||||
# this is now in the outermost scope and takes two args!
|
||||
def echo(caller, args):
|
||||
"Called after 10 seconds."
|
||||
shout = args
|
||||
caller.msg(
|
||||
f"You hear an echo: {shout.upper()} ... {shout.capitalize()} ... {shout.lower()}"
|
||||
)
|
||||
|
||||
class CmdEcho(default_cmds.MuxCommand):
|
||||
"""
|
||||
wait for an echo
|
||||
|
||||
Usage:
|
||||
echo <string>
|
||||
|
||||
Calls and waits for an echo
|
||||
"""
|
||||
key = "echo"
|
||||
locks = "cmd:all()"
|
||||
|
||||
def func(self):
|
||||
"""
|
||||
This is called at the initial shout.
|
||||
"""
|
||||
self.caller.msg(f"You shout '{self.args}' and wait for an echo ...")
|
||||
# this waits non-blocking for 10 seconds, then calls echo(self.caller, self.args)
|
||||
utils.delay(10, echo, self.caller, self.args, persistent=True) # changes!
|
||||
|
||||
```
|
||||
|
||||
Above you notice two changes:
|
||||
- The callback (`echo`) was moved out of the class and became its own stand-alone function in the
|
||||
outermost scope of the module. It also now takes `caller` and `args` as arguments (it doesn't have
|
||||
access to them directly since this is now a stand-alone function).
|
||||
- `utils.delay` specifies the `echo` function (not `self.echo` - it's no longer a method!) and sends
|
||||
`self.caller` and `self.args` as arguments for it to use. We also set `persistent=True`.
|
||||
|
||||
The reason for this change is because Evennia needs to `pickle` the callback into storage and it
|
||||
cannot do this correctly when the method sits on the command class. Now this behave the same as the
|
||||
first version except if you reload (or even shut down) the server mid-delay it will still fire the
|
||||
callback when the server comes back up (it will resume the countdown and ignore the downtime).
|
||||
quietly canceled next time it tries to update.
|
||||
|
|
@ -1,24 +1,26 @@
|
|||
# Command Prompt
|
||||
# Adding a Command Prompt
|
||||
|
||||
A *prompt* is quite common in MUDs:
|
||||
|
||||
A *prompt* is quite common in MUDs. The prompt display useful details about your character that you
|
||||
are likely to want to keep tabs on at all times, such as health, magical power etc. It might also
|
||||
show things like in-game time, weather and so on. Many modern MUD clients (including Evennia's own
|
||||
webclient) allows for identifying the prompt and have it appear in a correct location (usually just
|
||||
above the input line). Usually it will remain like that until it is explicitly updated.
|
||||
HP: 5, MP: 2, SP: 8
|
||||
>
|
||||
|
||||
## Sending a prompt
|
||||
The prompt display useful details about your character that you are likely to want to keep tabs on at all times. It could be health, magical power, gold and current location. It might also show things like in-game time, weather and so on.
|
||||
|
||||
Traditionally, the prompt (changed or not) was returned with every reply from the server and just displayed on its own line. Many modern MUD clients (including Evennia's own webclient) allows for identifying the prompt and have it appear in a fixed location that gets updated in-place (usually just above the input line).
|
||||
|
||||
## A fixed-location prompt
|
||||
|
||||
A prompt is sent using the `prompt` keyword to the `msg()` method on objects. The prompt will be
|
||||
sent without any line breaks.
|
||||
|
||||
```python
|
||||
self.msg(prompt="HP: 5, MP: 2, SP: 8")
|
||||
self.msg(prompt="HP: 5, MP: 2, SP: 8")
|
||||
```
|
||||
You can combine the sending of normal text with the sending (updating of the prompt):
|
||||
|
||||
```python
|
||||
self.msg("This is a text", prompt="This is a prompt")
|
||||
self.msg("This is a text", prompt="This is a prompt")
|
||||
```
|
||||
|
||||
You can update the prompt on demand, this is normally done using [OOB](../Concepts/OOB.md)-tracking of the relevant
|
||||
|
|
@ -63,18 +65,11 @@ Here is a simple example of the prompt sent/updated from a command class:
|
|||
prompt = f"{hp} HP, {mp} MP, {sp} SP"
|
||||
self.caller.msg(text, prompt=prompt)
|
||||
```
|
||||
## A prompt sent with every command
|
||||
## A prompt with every command
|
||||
|
||||
The prompt sent as described above uses a standard telnet instruction (the Evennia web client gets a
|
||||
special flag). Most MUD telnet clients will understand and allow users to catch this and keep the
|
||||
prompt in place until it updates. So *in principle* you'd not need to update the prompt every
|
||||
command.
|
||||
The prompt sent as described above uses a standard telnet instruction (the Evennia web client gets a special flag). Most MUD telnet clients will understand and allow users to catch this and keep the prompt in place until it updates. So *in principle* you'd not need to update the prompt every command.
|
||||
|
||||
However, with a varying user base it can be unclear which clients are used and which skill level the
|
||||
users have. So sending a prompt with every command is a safe catch-all. You don't need to manually
|
||||
go in and edit every command you have though. Instead you edit the base command class for your
|
||||
custom commands (like `MuxCommand` in your `mygame/commands/command.py` folder) and overload the
|
||||
`at_post_cmd()` hook. This hook is always called *after* the main `func()` method of the Command.
|
||||
However, with a varying user base it can be unclear which clients are used and which skill level the users have. So sending a prompt with every command is a safe catch-all. You don't need to manually go in and edit every command you have though. Instead you edit the base command class for your custom commands (like `MuxCommand` in your `mygame/commands/command.py` folder) and overload the `at_post_cmd()` hook. This hook is always called *after* the main `func()` method of the Command.
|
||||
|
||||
```python
|
||||
from evennia import default_cmds
|
||||
|
|
@ -91,8 +86,7 @@ class MuxCommand(default_cmds.MuxCommand):
|
|||
|
||||
### Modifying default commands
|
||||
|
||||
If you want to add something small like this to Evennia's default commands without modifying them
|
||||
directly the easiest way is to just wrap those with a multiple inheritance to your own base class:
|
||||
If you want to add something small like this to Evennia's default commands without modifying them directly the easiest way is to just wrap those with a multiple inheritance to your own base class:
|
||||
|
||||
```python
|
||||
# in (for example) mygame/commands/mycommands.py
|
||||
|
|
|
|||
|
|
@ -1,36 +1,33 @@
|
|||
# Default Exit Errors
|
||||
# Return custom errors on missing Exits
|
||||
|
||||
Evennia allows for exits to have any name. The command "kitchen" is a valid exit name as well as "jump out the window"
|
||||
or "north". An exit actually consists of two parts: an [Exit Object](../Components/Objects.md) and
|
||||
|
||||
> north
|
||||
Ouch! You bump into a wall!
|
||||
> out
|
||||
But you are already outside ...?
|
||||
|
||||
Evennia allows for exits to have any name. The command "kitchen" is a valid exit name as well as "jump out the window" or "north". An exit actually consists of two parts: an [Exit Object](../Components/Objects.md) and
|
||||
an [Exit Command](../Components/Commands.md) stored on said exit object. The command has the same key and aliases as the
|
||||
exit-object, which is why you can see the exit in the room and just write its name to traverse it.
|
||||
|
||||
So if you try to enter the name of a non-existing exit, Evennia treats is the same way as if you were trying to
|
||||
use a non-existing command:
|
||||
So if you try to enter the name of a non-existing exit, Evennia treats is the same way as if you were trying to use a non-existing command:
|
||||
|
||||
> jump out the window
|
||||
Command 'jump out the window' is not available. Type "help" for help.
|
||||
|
||||
Many games don't need this type of freedom however. They define only the cardinal directions as valid exit names (
|
||||
Evennia's `tunnel` command also offers this functionality). In this case, the error starts to look less logical:
|
||||
Many games don't need this type of freedom. They define only the cardinal directions as valid exit names ( Evennia's `tunnel` command also offers this functionality). In this case, the error starts to look less logical:
|
||||
|
||||
> west
|
||||
Command 'west' is not available. Maybe you meant "set" or "reset"?
|
||||
|
||||
Since we for our particular game *know* that west is an exit direction, it would be better if the error message just
|
||||
told us that we couldn't go there.
|
||||
Since we for our particular game *know* that west is an exit direction, it would be better if the error message just told us that we couldn't go there.
|
||||
|
||||
> west
|
||||
You cannot move west.
|
||||
|
||||
The way to do this is to give Evennia an _alternative_ Command to use when no Exit-Command is found in the room. See [Adding Commands](Beginner-Tutorial/Part1/Beginner-Tutorial-Adding-Commands.md) for more info about the process of adding new Commands to Evennia.
|
||||
|
||||
## Adding default error commands
|
||||
|
||||
The way to do this is to give Evennia an _alternative_ Command to use when no Exit-Command is found
|
||||
in the room. See [Adding Commands](Beginner-Tutorial/Part1/Beginner-Tutorial-Adding-Commands.md) for more info about the
|
||||
process of adding new Commands to Evennia.
|
||||
|
||||
In this example all we'll do is echo an error message.
|
||||
In this example we will just echo an error message, but you could do everything (maybe you lose health if you bump into a wall?)
|
||||
|
||||
```python
|
||||
# for example in a file mygame/commands/movecommands.py
|
||||
|
|
@ -75,9 +72,7 @@ class MovementFailCmdSet(CmdSet):
|
|||
self.add(CmdExitErrorSouth())
|
||||
```
|
||||
|
||||
We pack our commands in a new little cmdset; if we add this to our
|
||||
`CharacterCmdSet`, we can just add more errors to `MovementFailCmdSet`
|
||||
later without having to change code in two places.
|
||||
We pack our commands in a new little cmdset; if we add this to our `CharacterCmdSet`, we can just add more errors to `MovementFailCmdSet` later without having to change code in two places.
|
||||
|
||||
```python
|
||||
# in mygame/commands/default_cmdsets.py
|
||||
|
|
@ -93,15 +88,12 @@ class CharacterCmdSet(default_cmds.CharacterCmdSet):
|
|||
self.add(movecommands.MovementFailCmdSet)
|
||||
```
|
||||
|
||||
`reload` the server. What happens henceforth is that if you are in a room with an Exitobject (let's say it's "north"),
|
||||
the proper Exit-command will _overload_ your error command (also named "north"). But if you enter a direction without
|
||||
having a matching exit for it, you will fall back to your default error commands:
|
||||
`reload` the server. What happens henceforth is that if you are in a room with an Exitobject (let's say it's "north"), the proper Exit-command will _overload_ your error command (also named "north"). But if you enter a direction without having a matching exit for it, you will fall back to your default error commands:
|
||||
|
||||
> east
|
||||
You cannot move east.
|
||||
|
||||
Further expansions by the exit system (including manipulating the way the Exit command itself is created) can be done by
|
||||
modifying the [Exit typeclass](../Components/Typeclasses.md) directly.
|
||||
Further expansions by the exit system (including manipulating the way the Exit command itself is created) can be done by modifying the [Exit typeclass](../Components/Typeclasses.md) directly.
|
||||
|
||||
## Why not a single command?
|
||||
|
||||
|
|
@ -118,13 +110,8 @@ class CmdExitError(default_cmds.MuxCommand):
|
|||
#[...]
|
||||
```
|
||||
|
||||
The reason is that this would *not* work. Understanding why is important.
|
||||
This would *not* work the way we want. Understanding why is important.
|
||||
|
||||
Evennia's [command system](../Components/Commands.md) compares commands by key and/or aliases. If _any_ key or alias
|
||||
match, the two commands are considered _identical_. When the cmdsets merge, priority will then decide which of these
|
||||
'identical' commandss replace which.
|
||||
Evennia's [command system](../Components/Commands.md) compares commands by key and/or aliases. If _any_ key or alias match, the two commands are considered _identical_. When the cmdsets merge, priority will then decide which of these 'identical' commandss replace which.
|
||||
|
||||
So the above example would work fine as long as there were _no Exits at all_ in the room. But when we enter
|
||||
a room with an exit "north", its Exit-command (which has a higher priority) will override the single `CmdExitError`
|
||||
with its alias 'north'. So the `CmdExitError` will be gone and while "north" will work, we'll again get the normal
|
||||
"Command not recognized" error for the other directions.
|
||||
So the above example would work fine as long as there were _no Exits at all_ in the room. But when we enter a room with an exit "north", its Exit-command (which has a higher priority) will override the single `CmdExitError` with its alias 'north'. So the `CmdExitError` will be gone and while "north" will work, we'll again get the normal "Command not recognized" error for the other directions.
|
||||
|
|
@ -5,8 +5,7 @@ All Evennia tutorials. They will often refer to the [components](../Components/C
|
|||
## Beginner Tutorial
|
||||
|
||||
Recommended starting point! This will take you from absolute beginner to making
|
||||
a small, but full, game with Evennia. Even if you have a very different game style
|
||||
in mind for your own game, this will give you a good start.
|
||||
a small but full game with Evennia. Other tutorials and howto's tend to assume you are already familiar with the concepts explained in the Beginning tutorial.
|
||||
|
||||
> The latter parts of the beginner tutorial are still being worked on.
|
||||
|
||||
|
|
@ -20,25 +19,26 @@ in mind for your own game, this will give you a good start.
|
|||
./Beginner-Tutorial/Part5/Beginner-Tutorial-Part5-Intro
|
||||
```
|
||||
|
||||
|
||||
## Howto's
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 1
|
||||
:maxdepth: 2
|
||||
|
||||
Howto-Command-Prompt.md
|
||||
Howto-Command-Cooldown.md
|
||||
Howto-Command-Duration.md
|
||||
Howto-Default-Exit-Errors.md
|
||||
|
||||
Howto-Add-Object-Weight.md
|
||||
```
|
||||
## Mobs and NPCs
|
||||
|
||||
```{toctree}
|
||||
:maxdepth: 1
|
||||
|
||||
Tutorial-NPCs-listening.md
|
||||
Tutorial-Aggressive-NPCs.md
|
||||
NPC-shop-Tutorial.md
|
||||
Tutorial-NPC-Listening.md
|
||||
Tutorial-NPC-Reacting.md
|
||||
Tutorial-NPC-Merchants.md
|
||||
```
|
||||
|
||||
## Vehicles
|
||||
|
|
@ -56,14 +56,11 @@ Tutorial-Vehicles.md
|
|||
|
||||
Tutorial-Persistent-Handler.md
|
||||
Gametime-Tutorial.md
|
||||
Mass-and-weight-for-objects.md
|
||||
Weather-Tutorial.md
|
||||
Tutorial-Coordinates.md
|
||||
Dynamic-In-Game-Map.md
|
||||
Static-In-Game-Map.md
|
||||
Arxcode-Installation.md
|
||||
Tutorial-Tweeting-Game-Stats.md
|
||||
|
||||
```
|
||||
|
||||
## Web-related tutorials
|
||||
|
|
@ -87,6 +84,7 @@ Tutorial-Understanding-Color-Tags.md
|
|||
Evennia-for-roleplaying-sessions.md
|
||||
Evennia-for-Diku-Users.md
|
||||
Evennia-for-MUSH-Users.md
|
||||
Arxcode-Installation.md
|
||||
```
|
||||
|
||||
## Old tutorials
|
||||
|
|
|
|||
|
|
@ -1,98 +0,0 @@
|
|||
# Mass and weight for objects
|
||||
|
||||
|
||||
An easy addition to add dynamic variety to your world objects is to give them some mass. Why mass
|
||||
and not weight? Weight varies in setting; for example things on the Moon weigh 1/6 as much. On
|
||||
Earth's surface and in most environments, no relative weight factor is needed.
|
||||
|
||||
In most settings, mass can be used as weight to spring a pressure plate trap or a floor giving way,
|
||||
determine a character's burden weight for travel speed... The total mass of an object can
|
||||
contribute to the force of a weapon swing, or a speeding meteor to give it a potential striking
|
||||
force.
|
||||
|
||||
## Objects
|
||||
|
||||
Now that we have reasons for keeping track of object mass, let's look at the default object class
|
||||
inside your mygame/typeclasses/objects.py and see how easy it is to total up mass from an object and
|
||||
its contents.
|
||||
|
||||
```python
|
||||
# inside your mygame/typeclasses/objects.py
|
||||
|
||||
class Object(DefaultObject):
|
||||
# [...]
|
||||
def get_mass(self):
|
||||
mass = self.attributes.get('mass', 1) # Default objects have 1 unit mass.
|
||||
return mass + sum(obj.get_mass() for obj in self.contents)
|
||||
```
|
||||
|
||||
Adding the `get_mass` definition to the objects you want to sum up the masses for is done with
|
||||
Python's "sum" function which operates on all the contents, in this case by summing them to
|
||||
return a total mass value.
|
||||
|
||||
If you only wanted specific object types to have mass or have the new object type in a different
|
||||
module, see [[Adding-Object-Typeclass-Tutorial]] with its Heavy class object. You could set the
|
||||
default for Heavy types to something much larger than 1 gram or whatever unit you want to use. Any
|
||||
non-default mass would be stored on the `mass` [[Attributes]] of the objects.
|
||||
|
||||
|
||||
## Characters and rooms
|
||||
|
||||
You can add a `get_mass` definition to characters and rooms, also.
|
||||
|
||||
If you were in a one metric-ton elevator with four other friends also wearing armor and carrying
|
||||
gold bricks, you might wonder if this elevator's going to move, and how fast.
|
||||
|
||||
Assuming the unit is grams and the elevator itself weights 1,000 kilograms, it would already be
|
||||
`@set elevator/mass=1000000`, we're `@set me/mass=85000` and our armor is `@set armor/mass=50000`.
|
||||
We're each carrying 20 gold bars each `@set gold bar/mass=12400` then step into the elevator and see
|
||||
the following message in the elevator's appearance: `Elevator weight and contents should not exceed
|
||||
3 metric tons.` Are we safe? Maybe not if you consider dynamic loading. But at rest:
|
||||
|
||||
```python
|
||||
# Elevator object knows when it checks itself:
|
||||
if self.get_mass() < 3000000:
|
||||
pass # Elevator functions as normal.
|
||||
else:
|
||||
pass # Danger! Alarm sounds, cable snaps, elevator stops...
|
||||
```
|
||||
|
||||
## Inventory
|
||||
Example of listing mass of items in your inventory:
|
||||
|
||||
```python
|
||||
class CmdInventory(MuxCommand):
|
||||
"""
|
||||
view inventory
|
||||
Usage:
|
||||
inventory
|
||||
inv
|
||||
Switches:
|
||||
/weight to display all available channels.
|
||||
Shows your inventory: carrying, wielding, wearing, obscuring.
|
||||
"""
|
||||
|
||||
key = "inventory"
|
||||
aliases = ["inv", "i"]
|
||||
locks = "cmd:all()"
|
||||
|
||||
def func(self):
|
||||
"check inventory"
|
||||
items = self.caller.contents
|
||||
if not items:
|
||||
string = "You are not carrying anything."
|
||||
else:
|
||||
table = prettytable.PrettyTable(["name", "desc"])
|
||||
table.header = False
|
||||
table.border = False
|
||||
for item in items:
|
||||
second = item.get_mass() \
|
||||
if "weight" in self.switches else item.db.desc
|
||||
table.add_row([
|
||||
str(item.get_display_name(self.caller.sessions)),
|
||||
second and second or "",
|
||||
])
|
||||
string = f"|wYou are carrying:\n{table}"
|
||||
self.caller.msg(string)
|
||||
|
||||
```
|
||||
|
|
@ -1,334 +0,0 @@
|
|||
# NPC shop Tutorial
|
||||
|
||||
This tutorial will describe how to make an NPC-run shop. We will make use of the [EvMenu](../Components/EvMenu.md)
|
||||
system to present shoppers with a menu where they can buy things from the store's stock.
|
||||
|
||||
Our shop extends over two rooms - a "front" room open to the shop's customers and a locked "store
|
||||
room" holding the wares the shop should be able to sell. We aim for the following features:
|
||||
|
||||
- The front room should have an Attribute `storeroom` that points to the store room.
|
||||
- Inside the front room, the customer should have a command `buy` or `browse`. This will open a
|
||||
menu listing all items available to buy from the store room.
|
||||
- A customer should be able to look at individual items before buying.
|
||||
- We use "gold" as an example currency. To determine cost, the system will look for an Attribute
|
||||
`gold_value` on the items in the store room. If not found, a fixed base value of 1 will be assumed.
|
||||
The wealth of the customer should be set as an Attribute `gold` on the Character. If not set, they
|
||||
have no gold and can't buy anything.
|
||||
- When the customer makes a purchase, the system will check the `gold_value` of the goods and
|
||||
compare it to the `gold` Attribute of the customer. If enough gold is available, this will be
|
||||
deducted and the goods transferred from the store room to the inventory of the customer.
|
||||
- We will lock the store room so that only people with the right key can get in there.
|
||||
|
||||
## The shop menu
|
||||
|
||||
We want to show a menu to the customer where they can list, examine and buy items in the store. This
|
||||
menu should change depending on what is currently for sale. Evennia's *EvMenu* utility will manage
|
||||
the menu for us. It's a good idea to [read up on EvMenu](../Components/EvMenu.md) if you are not familiar with it.
|
||||
|
||||
### Designing the menu
|
||||
|
||||
The shopping menu's design is straightforward. First we want the main screen. You get this when you
|
||||
enter a shop and use the `browse` or `buy` command:
|
||||
|
||||
```
|
||||
*** Welcome to ye Old Sword shop! ***
|
||||
Things for sale (choose 1-3 to inspect, quit to exit):
|
||||
_________________________________________________________
|
||||
1. A rusty sword (5 gold)
|
||||
2. A sword with a leather handle (10 gold)
|
||||
3. Excalibur (100 gold)
|
||||
```
|
||||
|
||||
There are only three items to buy in this example but the menu should expand to however many items
|
||||
are needed. When you make a selection you will get a new screen showing the options for that
|
||||
particular item:
|
||||
|
||||
```
|
||||
You inspect A rusty sword:
|
||||
|
||||
This is an old weapon maybe once used by soldiers in some
|
||||
long forgotten army. It is rusty and in bad condition.
|
||||
__________________________________________________________
|
||||
1. Buy A rusty sword (5 gold)
|
||||
2. Look for something else.
|
||||
```
|
||||
|
||||
Finally, when you buy something, a brief message should pop up:
|
||||
|
||||
```
|
||||
You pay 5 gold and purchase A rusty sword!
|
||||
```
|
||||
or
|
||||
```
|
||||
You cannot afford 5 gold for A rusty sword!
|
||||
```
|
||||
After this you should be back to the top level of the shopping menu again and can continue browsing.
|
||||
|
||||
### Coding the menu
|
||||
|
||||
EvMenu defines the *nodes* (each menu screen with options) as normal Python functions. Each node
|
||||
must be able to change on the fly depending on what items are currently for sale. EvMenu will
|
||||
automatically make the `quit` command available to us so we won't add that manually. For compactness
|
||||
we will put everything needed for our shop in one module, `mygame/typeclasses/npcshop.py`.
|
||||
|
||||
```python
|
||||
# mygame/typeclasses/npcshop.py
|
||||
|
||||
from evennia.utils import evmenu
|
||||
|
||||
def menunode_shopfront(caller):
|
||||
"This is the top-menu screen."
|
||||
|
||||
shopname = caller.location.key
|
||||
wares = caller.location.db.storeroom.contents
|
||||
|
||||
# Wares includes all items inside the storeroom, including the
|
||||
# door! Let's remove that from our for sale list.
|
||||
wares = [ware for ware in wares if ware.key.lower() != "door"]
|
||||
|
||||
text = f"*** Welcome to {shopname}! ***\n"
|
||||
if wares:
|
||||
text += f" Things for sale (choose 1-{len(wares)} to inspect); quit to exit:"
|
||||
else:
|
||||
text += " There is nothing for sale; quit to exit."
|
||||
|
||||
options = []
|
||||
for ware in wares:
|
||||
# add an option for every ware in store
|
||||
gold_val = ware.db.gold_value or 1
|
||||
options.append({"desc": f"{ware.key} ({gold_val} gold)",
|
||||
"goto": "menunode_inspect_and_buy"})
|
||||
return text, options
|
||||
```
|
||||
|
||||
In this code we assume the caller to be *inside* the shop when accessing the menu. This means we can
|
||||
access the shop room via `caller.location` and get its `key` to display as the shop's name. We also
|
||||
assume the shop has an Attribute `storeroom` we can use to get to our stock. We loop over our goods
|
||||
to build up the menu's options.
|
||||
|
||||
Note that *all options point to the same menu node* called `menunode_inspect_and_buy`! We can't know
|
||||
which goods will be available to sale so we rely on this node to modify itself depending on the
|
||||
circumstances. Let's create it now.
|
||||
|
||||
```python
|
||||
# further down in mygame/typeclasses/npcshop.py
|
||||
|
||||
def menunode_inspect_and_buy(caller, raw_string):
|
||||
"Sets up the buy menu screen."
|
||||
|
||||
wares = caller.location.db.storeroom.contents
|
||||
# Don't forget, we will need to remove that pesky door again!
|
||||
wares = [ware for ware in wares if ware.key.lower() != "door"]
|
||||
iware = int(raw_string) - 1
|
||||
ware = wares[iware]
|
||||
value = ware.db.gold_value or 1
|
||||
wealth = caller.db.gold or 0
|
||||
text = f"You inspect {ware.key}:\n\n{ware.db.desc}"
|
||||
|
||||
def buy_ware_result(caller):
|
||||
"This will be executed first when choosing to buy."
|
||||
if wealth >= value:
|
||||
rtext = f"You pay {value} gold and purchase {ware.key}!"
|
||||
caller.db.gold -= value
|
||||
ware.move_to(caller, quiet=True, move_type="buy")
|
||||
else:
|
||||
rtext = f"You cannot afford {value} gold for {ware.key}!"
|
||||
caller.msg(rtext)
|
||||
|
||||
gold_val = ware.db.gold_value or 1
|
||||
options = ({
|
||||
"desc": f"Buy {ware.key} for {gold_val} gold",
|
||||
"goto": "menunode_shopfront",
|
||||
"exec": buy_ware_result,
|
||||
}, {
|
||||
"desc": "Look for something else",
|
||||
"goto": "menunode_shopfront",
|
||||
})
|
||||
|
||||
return text, options
|
||||
```
|
||||
|
||||
In this menu node we make use of the `raw_string` argument to the node. This is the text the menu
|
||||
user entered on the *previous* node to get here. Since we only allow numbered options in our menu,
|
||||
`raw_input` must be an number for the player to get to this point. So we convert it to an integer
|
||||
index (menu lists start from 1, whereas Python indices always starts at 0, so we need to subtract
|
||||
1). We then use the index to get the corresponding item from storage.
|
||||
|
||||
We just show the customer the `desc` of the item. In a more elaborate setup you might want to show
|
||||
things like weapon damage and special stats here as well.
|
||||
|
||||
When the user choose the "buy" option, EvMenu will execute the `exec` instruction *before* we go
|
||||
back to the top node (the `goto` instruction). For this we make a little inline function
|
||||
`buy_ware_result`. EvMenu will call the function given to `exec` like any menu node but it does not
|
||||
need to return anything. In `buy_ware_result` we determine if the customer can afford the cost and
|
||||
give proper return messages. This is also where we actually move the bought item into the inventory
|
||||
of the customer.
|
||||
|
||||
### The command to start the menu
|
||||
|
||||
We could *in principle* launch the shopping menu the moment a customer steps into our shop room, but
|
||||
this would probably be considered pretty annoying. It's better to create a [Command](../Components/Commands.md) for
|
||||
customers to explicitly wanting to shop around.
|
||||
|
||||
```python
|
||||
# mygame/typeclasses/npcshop.py
|
||||
|
||||
from evennia import Command
|
||||
|
||||
class CmdBuy(Command):
|
||||
"""
|
||||
Start to do some shopping
|
||||
|
||||
Usage:
|
||||
buy
|
||||
shop
|
||||
browse
|
||||
|
||||
This will allow you to browse the wares of the
|
||||
current shop and buy items you want.
|
||||
"""
|
||||
key = "buy"
|
||||
aliases = ("shop", "browse")
|
||||
|
||||
def func(self):
|
||||
"Starts the shop EvMenu instance"
|
||||
evmenu.EvMenu(self.caller,
|
||||
"typeclasses.npcshop",
|
||||
startnode="menunode_shopfront")
|
||||
```
|
||||
|
||||
This will launch the menu. The `EvMenu` instance is initialized with the path to this very module -
|
||||
since the only global functions available in this module are our menu nodes, this will work fine
|
||||
(you could also have put those in a separate module). We now just need to put this command in a
|
||||
[CmdSet](../Components/Command-Sets.md) so we can add it correctly to the game:
|
||||
|
||||
```python
|
||||
from evennia import CmdSet
|
||||
|
||||
class ShopCmdSet(CmdSet):
|
||||
def at_cmdset_creation(self):
|
||||
self.add(CmdBuy())
|
||||
```
|
||||
|
||||
## Building the shop
|
||||
|
||||
There are really only two things that separate our shop from any other Room:
|
||||
|
||||
- The shop has the `storeroom` Attribute set on it, pointing to a second (completely normal) room.
|
||||
- It has the `ShopCmdSet` stored on itself. This makes the `buy` command available to users entering
|
||||
the shop.
|
||||
|
||||
For testing we could easily add these features manually to a room using `@py` or other admin
|
||||
commands. Just to show how it can be done we'll instead make a custom [Typeclass](../Components/Typeclasses.md) for
|
||||
the shop room and make a small command that builders can use to build both the shop and the
|
||||
storeroom at once.
|
||||
|
||||
```python
|
||||
# bottom of mygame/typeclasses/npcshop.py
|
||||
|
||||
from evennia import DefaultRoom, DefaultExit, DefaultObject
|
||||
from evennia.utils.create import create_object
|
||||
|
||||
# class for our front shop room
|
||||
class NPCShop(DefaultRoom):
|
||||
def at_object_creation(self):
|
||||
# we could also use add(ShopCmdSet, persistent=True)
|
||||
self.cmdset.add_default(ShopCmdSet)
|
||||
self.db.storeroom = None
|
||||
|
||||
# command to build a complete shop (the Command base class
|
||||
# should already have been imported earlier in this file)
|
||||
class CmdBuildShop(Command):
|
||||
"""
|
||||
Build a new shop
|
||||
|
||||
Usage:
|
||||
@buildshop shopname
|
||||
|
||||
This will create a new NPCshop room
|
||||
as well as a linked store room (named
|
||||
simply <storename>-storage) for the
|
||||
wares on sale. The store room will be
|
||||
accessed through a locked door in
|
||||
the shop.
|
||||
"""
|
||||
key = "@buildshop"
|
||||
locks = "cmd:perm(Builders)"
|
||||
help_category = "Builders"
|
||||
|
||||
def func(self):
|
||||
"Create the shop rooms"
|
||||
if not self.args:
|
||||
self.msg("Usage: @buildshop <storename>")
|
||||
return
|
||||
# create the shop and storeroom
|
||||
shopname = self.args.strip()
|
||||
shop = create_object(NPCShop,
|
||||
key=shopname,
|
||||
location=None)
|
||||
storeroom = create_object(DefaultRoom,
|
||||
key=f"{shopname}-storage",
|
||||
location=None)
|
||||
shop.db.storeroom = storeroom
|
||||
# create a door between the two
|
||||
shop_exit = create_object(DefaultExit,
|
||||
key="back door",
|
||||
aliases=["storage", "store room"],
|
||||
location=shop,
|
||||
destination=storeroom)
|
||||
storeroom_exit = create_object(DefaultExit,
|
||||
key="door",
|
||||
location=storeroom,
|
||||
destination=shop)
|
||||
# make a key for accessing the store room
|
||||
storeroom_key_name = f"{shopname}-storekey"
|
||||
storeroom_key = create_object(DefaultObject,
|
||||
key=storeroom_key_name,
|
||||
location=shop)
|
||||
# only allow chars with this key to enter the store room
|
||||
shop_exit.locks.add(f"traverse:holds({storeroom_key_name})")
|
||||
|
||||
# inform the builder about progress
|
||||
self.caller.msg(f"The shop {shop} was created!")
|
||||
```
|
||||
|
||||
Our typeclass is simple and so is our `buildshop` command. The command (which is for Builders only)
|
||||
just takes the name of the shop and builds the front room and a store room to go with it (always
|
||||
named `"<shopname>-storage"`. It connects the rooms with a two-way exit. You need to add
|
||||
`CmdBuildShop` [to the default cmdset](Starting/Adding-Command-Tutorial#step-2-adding-the-command-to-a-
|
||||
default-cmdset) before you can use it. Once having created the shop you can now `@teleport` to it or
|
||||
`@open` a new exit to it. You could also easily expand the above command to automatically create
|
||||
exits to and from the new shop from your current location.
|
||||
|
||||
To avoid customers walking in and stealing everything, we create a [Lock](../Components/Locks.md) on the storage
|
||||
door. It's a simple lock that requires the one entering to carry an object named
|
||||
`<shopname>-storekey`. We even create such a key object and drop it in the shop for the new shop
|
||||
keeper to pick up.
|
||||
|
||||
> If players are given the right to name their own objects, this simple lock is not very secure and
|
||||
you need to come up with a more robust lock-key solution.
|
||||
|
||||
> We don't add any descriptions to all these objects so looking "at" them will not be too thrilling.
|
||||
You could add better default descriptions as part of the `@buildshop` command or leave descriptions
|
||||
this up to the Builder.
|
||||
|
||||
## The shop is open for business!
|
||||
|
||||
We now have a functioning shop and an easy way for Builders to create it. All you need now is to
|
||||
`@open` a new exit from the rest of the game into the shop and put some sell-able items in the store
|
||||
room. Our shop does have some shortcomings:
|
||||
|
||||
- For Characters to be able to buy stuff they need to also have the `gold` Attribute set on
|
||||
themselves.
|
||||
- We manually remove the "door" exit from our items for sale. But what if there are other unsellable
|
||||
items in the store room? What if the shop owner walks in there for example - anyone in the store
|
||||
could then buy them for 1 gold.
|
||||
- What if someone else were to buy the item we're looking at just before we decide to buy it? It
|
||||
would then be gone and the counter be wrong - the shop would pass us the next item in the list.
|
||||
|
||||
Fixing these issues are left as an exercise.
|
||||
|
||||
If you want to keep the shop fully NPC-run you could add a [Script](../Components/Scripts.md) to restock the shop's
|
||||
store room regularly. This shop example could also easily be owned by a human Player (run for them
|
||||
by a hired NPC) - the shop owner would get the key to the store room and be responsible for keeping
|
||||
it well stocked.
|
||||
|
|
@ -412,5 +412,5 @@ easily new game defining features can be added to Evennia.
|
|||
|
||||
You can easily build from this tutorial by expanding the map and creating more rooms to explore. Why
|
||||
not add more features to your game by trying other tutorials: [Add weather to your world](Weather-
|
||||
Tutorial), [fill your world with NPC's](./Tutorial-Aggressive-NPCs.md) or
|
||||
Tutorial), [fill your world with NPC's](./Tutorial-NPC-Reacting.md) or
|
||||
[implement a combat system](./Turn-based-Combat-System.md).
|
||||
|
|
|
|||
|
|
@ -1,126 +0,0 @@
|
|||
# Tutorial Aggressive NPCs
|
||||
|
||||
|
||||
This tutorial shows the implementation of an NPC object that responds to characters entering their
|
||||
location. In this example the NPC has the option to respond aggressively or not, but any actions
|
||||
could be triggered this way.
|
||||
|
||||
One could imagine using a [Script](../Components/Scripts.md) that is constantly checking for newcomers. This would be
|
||||
highly inefficient (most of the time its check would fail). Instead we handle this on-demand by
|
||||
using a couple of existing object hooks to inform the NPC that a Character has entered.
|
||||
|
||||
It is assumed that you already know how to create custom room and character typeclasses, please see
|
||||
the [Basic Game tutorial](./Tutorial-for-basic-MUSH-like-game.md) if you haven't already done this.
|
||||
|
||||
What we will need is the following:
|
||||
|
||||
- An NPC typeclass that can react when someone enters.
|
||||
- A custom [Room](../Components/Objects.md#rooms) typeclass that can tell the NPC that someone entered.
|
||||
- We will also tweak our default `Character` typeclass a little.
|
||||
|
||||
To begin with, we need to create an NPC typeclass. Create a new file inside of your typeclasses
|
||||
folder and name it `npcs.py` and then add the following code:
|
||||
|
||||
```python
|
||||
from typeclasses.characters import Character
|
||||
|
||||
class NPC(Character):
|
||||
"""
|
||||
A NPC typeclass which extends the character class.
|
||||
"""
|
||||
def at_char_entered(self, character):
|
||||
"""
|
||||
A simple is_aggressive check.
|
||||
Can be expanded upon later.
|
||||
"""
|
||||
if self.db.is_aggressive:
|
||||
self.execute_cmd(f"say Graaah, die {character}!")
|
||||
else:
|
||||
self.execute_cmd(f"say Greetings, {character}!")
|
||||
```
|
||||
|
||||
We will define our custom `Character` typeclass below. As for the new `at_char_entered` method we've
|
||||
just defined, we'll ensure that it will be called by the room where the NPC is located, when a
|
||||
player enters that room. You'll notice that right now, the NPC merely speaks. You can expand this
|
||||
part as you like and trigger all sorts of effects here (like combat code, fleeing, bartering or
|
||||
quest-giving) as your game design dictates.
|
||||
|
||||
Now your `typeclasses.rooms` module needs to have the following added:
|
||||
|
||||
```python
|
||||
# Add this import to the top of your file.
|
||||
from evennia import utils
|
||||
|
||||
# Add this hook in any empty area within your Room class.
|
||||
def at_object_receive(self, obj, source_location):
|
||||
if utils.inherits_from(obj, 'typeclasses.npcs.NPC'): # An NPC has entered
|
||||
return
|
||||
elif utils.inherits_from(obj, 'typeclasses.characters.Character'):
|
||||
# A PC has entered.
|
||||
# Cause the player's character to look around.
|
||||
obj.execute_cmd('look')
|
||||
for item in self.contents:
|
||||
if utils.inherits_from(item, 'typeclasses.npcs.NPC'):
|
||||
# An NPC is in the room
|
||||
item.at_char_entered(obj)
|
||||
```
|
||||
|
||||
`inherits_from` must be given the full path of the class. If the object inherited a class from your
|
||||
`world.races` module, then you would check inheritance with `world.races.Human`, for example. There
|
||||
is no need to import these prior, as we are passing in the full path. As a matter of a fact,
|
||||
`inherits_from` does not properly work if you import the class and only pass in the name of the
|
||||
class.
|
||||
|
||||
> Note:
|
||||
[at_object_receive](https://github.com/evennia/evennia/blob/master/evennia/objects/objects.py#L1529)
|
||||
is a default hook of the `DefaultObject` typeclass (and its children). Here we are overriding this
|
||||
hook in our customized room typeclass to suit our needs.
|
||||
|
||||
This room checks the typeclass of objects entering it (using `utils.inherits_from` and responds to
|
||||
`Characters`, ignoring other NPCs or objects. When triggered the room will look through its
|
||||
contents and inform any `NPCs inside by calling their `at_char_entered` method.
|
||||
|
||||
You'll also see that we have added a 'look' into this code. This is because, by default, the
|
||||
`at_object_receive` is carried out *before* the character's `at_post_move` which, we will now
|
||||
overload. This means that a character entering would see the NPC perform its actions before the
|
||||
'look' command. Deactivate the look command in the default `Character` class within the
|
||||
`typeclasses.characters` module:
|
||||
|
||||
```python
|
||||
# Add this hook in any blank area within your Character class.
|
||||
def at_post_move(self, source_location):
|
||||
"""
|
||||
Default is to look around after a move
|
||||
Note: This has been moved to Room.at_object_receive
|
||||
"""
|
||||
# self.execute_cmd('look')
|
||||
```
|
||||
|
||||
Now let's create an NPC and make it aggressive. Type the following commands into your MUD client:
|
||||
```
|
||||
reload
|
||||
create/drop Orc:npcs.NPC
|
||||
```
|
||||
|
||||
> Note: You could also give the path as `typeclasses.npcs.NPC`, but Evennia will look into the
|
||||
`typeclasses` folder automatically, so this is a little shorter.
|
||||
|
||||
When you enter the aggressive NPC's location, it will default to using its peaceful action (say your
|
||||
name is Anna):
|
||||
|
||||
```
|
||||
Orc says, "Greetings, Anna!"
|
||||
```
|
||||
|
||||
Now we turn on the aggressive mode (we do it manually but it could also be triggered by some sort of
|
||||
AI code).
|
||||
|
||||
```
|
||||
set orc/is_aggressive = True
|
||||
```
|
||||
|
||||
Now it will perform its aggressive action whenever a character enters.
|
||||
|
||||
```
|
||||
Orc says, "Graaah, die, Anna!"
|
||||
```
|
||||
|
|
@ -1,19 +1,16 @@
|
|||
# Tutorial NPCs listening
|
||||
# NPCs that listen to what is said
|
||||
|
||||
> say hi
|
||||
You say, "hi"
|
||||
The troll under the bridge answers, "well, well. Hello."
|
||||
|
||||
This tutorial shows the implementation of an NPC object that responds to characters speaking in
|
||||
their location. In this example the NPC parrots what is said, but any actions could be triggered
|
||||
this way.
|
||||
|
||||
It is assumed that you already know how to create custom room and character typeclasses, please see
|
||||
the [Basic Game tutorial](./Tutorial-for-basic-MUSH-like-game.md) if you haven't already done this.
|
||||
|
||||
What we will need is simply a new NPC typeclass that can react when someone speaks.
|
||||
This howto explains how to make an NPC that reacts to characters speaking in their current location. The principle applies to other situations, such as enemies joining a fight or reacting to a character drawing a weapon.
|
||||
|
||||
```python
|
||||
# mygame/typeclasses/npc.py
|
||||
|
||||
from characters import Character
|
||||
|
||||
class Npc(Character):
|
||||
"""
|
||||
A NPC typeclass which extends the character class.
|
||||
|
|
@ -32,11 +29,18 @@ class Npc(Character):
|
|||
return f"{from_obj} said: '{message}'"
|
||||
```
|
||||
|
||||
We add a simple method `at_heard_say` that formats what it hears. We assume that the message that enters it is on the form `Someone says, "Hello"`, and we make sure to only get `Hello` in that example.
|
||||
|
||||
We are not actually calling `at_heard_say` yet. We'll handle that next.
|
||||
|
||||
When someone in the room speaks to this NPC, its `msg` method will be called. We will modify the
|
||||
NPCs `.msg` method to catch says so the NPC can respond.
|
||||
|
||||
|
||||
```python
|
||||
```{code-block} python
|
||||
:linenos:
|
||||
:emphasize-lines:
|
||||
|
||||
# mygame/typeclasses/npc.py
|
||||
|
||||
from characters import Character
|
||||
|
|
@ -70,26 +74,15 @@ class Npc(Character):
|
|||
So if the NPC gets a say and that say is not coming from the NPC itself, it will echo it using the
|
||||
`at_heard_say` hook. Some things of note in the above example:
|
||||
|
||||
- The `text` input can be on many different forms depending on where this `msg` is called from.
|
||||
Instead of trying to analyze `text` in detail with a range of `if` statements we just assume the
|
||||
form we want and catch the error if it does not match. This simplifies the code considerably. It's
|
||||
called 'leap before you look' and is a Python paradigm that may feel unfamiliar if you are used to
|
||||
other languages. Here we 'swallow' the error silently, which is fine when the code checked is
|
||||
simple. If not we may want to import `evennia.logger.log_trace` and add `log_trace()` in the
|
||||
`except` clause.
|
||||
- We use `execute_cmd` to fire the `say` command back. We could also have called
|
||||
`self.location.msg_contents` directly but using the Command makes sure all hooks are called (so
|
||||
those seeing the NPC's `say` can in turn react if they want).
|
||||
- Note the comments about `super` at the end. This will trigger the 'default' `msg` (in the parent
|
||||
class) as well. It's not really necessary as long as no one puppets the NPC (by `@ic <npcname>`) but
|
||||
it's wise to keep in there since the puppeting player will be totally blind if `msg()` is never
|
||||
returning anything to them!
|
||||
- **Line 15** The `text` input can be on many different forms depending on where this `msg` is called from. If you look at the [code of the 'say' command](evennia.commands.default.general.CmdSay) you'd find that it will call `.msg` with `("Hello", {"type": "say"})`. We use this knowledge to figure out if this comes from a `say` or not.
|
||||
- **Line 24**: We use `execute_cmd` to fire the NPCs own `say` command back. This works because the NPC is actually a child of `DefaultCharacter` - so it has the `CharacterCmdSet` on it! Normally you should use `execute_cmd` only sparingly; it's usually more efficient to call the actual code used by the Command directly. For this tutorial, invoking the command is shorter to write while making sure all hooks are called
|
||||
- **Line26**: Note the comments about `super` at the end. This will trigger the 'default' `msg` (in the parent class) as well. It's not really necessary as long as no one puppets the NPC (by `@ic <npcname>`) but it's wise to keep in there since the puppeting player will be totally blind if `msg()` is never returning anything to them!
|
||||
|
||||
Now that's done, let's create an NPC and see what it has to say for itself.
|
||||
|
||||
```
|
||||
@reload
|
||||
@create/drop Guild Master:npc.Npc
|
||||
reload
|
||||
create/drop Guild Master:npc.Npc
|
||||
```
|
||||
|
||||
(you could also give the path as `typeclasses.npc.Npc`, but Evennia will look into the `typeclasses`
|
||||
|
|
@ -106,5 +99,4 @@ There are many ways to implement this kind of functionality. An alternative exam
|
|||
sending to an NPC and call the `at_heard_say` hook directly.
|
||||
|
||||
While the tutorial solution has the advantage of being contained only within the NPC class,
|
||||
combining this with using the Character class gives more direct control over how the NPC will react.
|
||||
Which way to go depends on the design requirements of your particular game.
|
||||
combining this with using the Character class gives more direct control over how the NPC will react. Which way to go depends on the design requirements of your particular game.
|
||||
276
docs/source/Howtos/Tutorial-NPC-Merchants.md
Normal file
276
docs/source/Howtos/Tutorial-NPC-Merchants.md
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
# NPC merchants
|
||||
|
||||
```
|
||||
*** Welcome to ye Old Sword shop! ***
|
||||
Things for sale (choose 1-3 to inspect, quit to exit):
|
||||
_________________________________________________________
|
||||
1. A rusty sword (5 gold)
|
||||
2. A sword with a leather handle (10 gold)
|
||||
3. Excalibur (100 gold)
|
||||
```
|
||||
|
||||
This will introduce an NPC able to sell things. In practice this means that when you interact with them you'll get shown a _menu_ of choices. Evennia provides the [EvMenu](../Components/EvMenu.md) utility to easily create in-game menus.
|
||||
|
||||
We will store all the merchant's wares in their inventory. This means that they may stand in an actual shop room, at a market or wander the road. We will also use 'gold' as an example currency.
|
||||
To enter the shop, you'll just need to stand in the same room and use the `buy/shop` command.
|
||||
|
||||
## Making the merchant class
|
||||
|
||||
The merchant will respond to you giving the `shop` or `buy` command in their presence.
|
||||
|
||||
```python
|
||||
# in for example mygame/typeclasses/merchants.py
|
||||
|
||||
from typeclasses.objects import Object
|
||||
from evennia import Command, CmdSet, EvMenu
|
||||
|
||||
class CmdOpenShop(Command):
|
||||
"""
|
||||
Open the shop!
|
||||
|
||||
Usage:
|
||||
shop/buy
|
||||
|
||||
"""
|
||||
key = "shop"
|
||||
aliases = ["buy"]
|
||||
|
||||
def func(self):
|
||||
# this will sit on the Merchant, which is self.obj.
|
||||
# the self.caller is the player wanting to buy stuff.
|
||||
self.obj.open_shop(self.caller)
|
||||
|
||||
|
||||
class MerchantCmdSet(CmdSet):
|
||||
def at_cmdset_creation(self):
|
||||
self.add(CmdOpenShop())
|
||||
|
||||
|
||||
class NPCMerchant(Object):
|
||||
|
||||
def at_object_creation(self):
|
||||
self.cmdset.add_default(MerchantCmdSet)
|
||||
|
||||
def open_shop(self, shopper):
|
||||
menunodes = {} # TODO!
|
||||
shopname = self.db.shopname or "The shop"
|
||||
EvMenu(shopper, menunodes, startnode="shop_start",
|
||||
shopname=shopname, shopkeeper=self, wares=self.contents)
|
||||
|
||||
```
|
||||
|
||||
We could also have put the commands in a separate module, but for compactness, we put it all with the merchant typeclass.
|
||||
|
||||
Note that we make the merchant an `Object`! Since we don't give them any other commands, it makes little sense to let them be a `Character`.
|
||||
|
||||
We make a very simple `shop`/`buy` Command and make sure to add it on the merchant in its own cmdset.
|
||||
|
||||
We initialize `EvMenu` on the `shopper` but we haven't created any `menunodes` yet, so this will not actually do much at this point. It's important that we we pass `shopname`, `shopkeeper` and `wares` into the menu, it means they will be made available as properties on the EvMenu instance - we will be able to access them from inside the menu.
|
||||
|
||||
## Coding the shopping menu
|
||||
|
||||
[EvMenu](../Components/EvMenu.md) splits the menu into _nodes_ represented by Python functions. Each node represents a stop in the menu where the user has to make a choice.
|
||||
|
||||
For simplicity, we'll code the shop interface above the `NPCMerchant` class in the same module.
|
||||
|
||||
The start node of the shop named "ye Old Sword shop!" will look like this if there are only 3 wares to sell:
|
||||
|
||||
```
|
||||
*** Welcome to ye Old Sword shop! ***
|
||||
Things for sale (choose 1-3 to inspect, quit to exit):
|
||||
_________________________________________________________
|
||||
1. A rusty sword (5 gold)
|
||||
2. A sword with a leather handle (10 gold)
|
||||
3. Excalibur (100 gold)
|
||||
```
|
||||
|
||||
|
||||
```python
|
||||
# in mygame/typeclasses/merchants.py
|
||||
|
||||
# top of module, above NPCMerchant class.
|
||||
|
||||
def node_shopfront(caller, raw_string, **kwargs):
|
||||
"This is the top-menu screen."
|
||||
|
||||
# made available since we passed them to EvMenu on start
|
||||
menu = caller.ndb._evmenu
|
||||
shopname = menu.shopname
|
||||
shopkeeper = menu.shopkeeper
|
||||
wares = menu.wares
|
||||
|
||||
text = f"*** Welcome to {shopname}! ***\n"
|
||||
if wares:
|
||||
text += f" Things for sale (choose 1-{len(wares)} to inspect); quit to exit:"
|
||||
else:
|
||||
text += " There is nothing for sale; quit to exit."
|
||||
|
||||
options = []
|
||||
for ware in wares:
|
||||
# add an option for every ware in store
|
||||
gold_val = ware.db.gold_value or 1
|
||||
options.append({"desc": f"{ware.key} ({gold_val} gold)",
|
||||
"goto": ("inspect_and_buy",
|
||||
{"selected_ware": ware})
|
||||
})
|
||||
|
||||
return text, options
|
||||
```
|
||||
|
||||
Inside the node we can access the menu on the caller as `caller.ndb._evmenu`. The extra keywords we passed into `EvMenu` are available on this menu instance. Armed with this we can easily present a shop interface. Each option will become a numbered choice on this screen.
|
||||
|
||||
Note how we pass the `ware` with each option and label it `selected_ware`. This will be accessible in the next node's `**kwargs` argument
|
||||
|
||||
If a player choose one of the wares, they should be able to inspect it. Here's how it should look if they selected `1` in ye Old Sword shop:
|
||||
|
||||
```
|
||||
You inspect A rusty sword:
|
||||
|
||||
This is an old weapon maybe once used by soldiers in some
|
||||
long forgotten army. It is rusty and in bad condition.
|
||||
__________________________________________________________
|
||||
1. Buy A rusty sword (5 gold)
|
||||
2. Look for something else.
|
||||
```
|
||||
|
||||
If you buy, you'll see
|
||||
|
||||
```
|
||||
You pay 5 gold and purchase A rusty sword!
|
||||
```
|
||||
or
|
||||
```
|
||||
You cannot afford 5 gold for A rusty sword!
|
||||
```
|
||||
|
||||
Either way you should end up back at the top level of the shopping menu again and can continue browsing or quit the menu with `quit`.
|
||||
|
||||
Here's how it looks in code:
|
||||
|
||||
```python
|
||||
# in mygame/typeclasses/merchants.py
|
||||
|
||||
# right after the other node
|
||||
|
||||
def _buy_item(caller, raw_string, **kwargs):
|
||||
"Called if buyer chooses to buy"
|
||||
selected_ware = kwargs["selected_ware"]
|
||||
value = selected_ware.db.gold_value or 1
|
||||
wealth = caller.db.gold or 0
|
||||
|
||||
if wealth >= value:
|
||||
rtext = f"You pay {value} gold and purchase {ware.key}!"
|
||||
caller.db.gold -= value
|
||||
move_to(caller, quiet=True, move_type="buy")
|
||||
else:
|
||||
rtext = f"You cannot afford {value} gold for {ware.key}!"
|
||||
caller.msg(rtext)
|
||||
# no matter what, we return to the top level of the shop
|
||||
return "shopfront"
|
||||
|
||||
def node_inspect_and_buy(caller, raw_string, **kwargs):
|
||||
"Sets up the buy menu screen."
|
||||
|
||||
# passed from the option we chose
|
||||
selected_ware = kwargs["selected_ware"]
|
||||
|
||||
value = selected_ware.db.gold_value or 1
|
||||
text = f"You inspect {ware.key}:\n\n{ware.db.desc}"
|
||||
gold_val = ware.db.gold_value or 1
|
||||
|
||||
options = ({
|
||||
"desc": f"Buy {ware.key} for {gold_val} gold",
|
||||
"goto": (_buy_item, kwargs)
|
||||
}, {
|
||||
"desc": "Look for something else",
|
||||
"goto": "shopfront",
|
||||
})
|
||||
return text, options
|
||||
```
|
||||
|
||||
In this node we grab the `selected_ware` from `kwargs` - this we pased along from the option on the previous node. We display its description and value. If the user buys, we reroute through the `_buy_item` helper function (this is not a node, it's just a callable that must return the name of the next node to go to.). In `_buy_item` we check if the buyer can affort the ware, and if it can we move it to their inventory. Either way, this method returns `shop_front` as the next node.
|
||||
|
||||
We have been referring to two nodes here: `"shopfront"` and `"inspect_and_buy"` , we should map them to the code in the menu. Scroll down to the `NPCMerchant` class in the same module and find that unfinished `open_shop` method again:
|
||||
|
||||
|
||||
```python
|
||||
# in /mygame/typeclasses/merchants.py
|
||||
|
||||
def node_shopfront(caller, raw_string, **kwargs):
|
||||
# ...
|
||||
|
||||
def _buy_item(caller, raw_string, **kwargs):
|
||||
# ...
|
||||
|
||||
def node_inspect_and_buy(caller, raw_string, **kwargs):
|
||||
# ...
|
||||
|
||||
class NPCMerchant(Object):
|
||||
|
||||
# ...
|
||||
|
||||
def open_shop(self, shopper):
|
||||
menunodes = {
|
||||
"shopfront": node_shopfront,
|
||||
"inspect_and_buy": node_inspect_and_buy
|
||||
}
|
||||
shopname = self.db.shopname or "The shop"
|
||||
EvMenu(shopper, menunodes, startnode="shop_start",
|
||||
shopname=shopname, shopkeeper=self, wares=self.contents)
|
||||
|
||||
```
|
||||
|
||||
|
||||
We now added the nodes to the Evmenu under their right labels. The merchant is now ready!
|
||||
|
||||
|
||||
## The shop is open for business!
|
||||
|
||||
Make sure to `reload`.
|
||||
|
||||
Let's try it out by creating the merchant and a few wares in-game. Remember that we also must create some gold get this economy going.
|
||||
|
||||
```
|
||||
> set self/gold = 8
|
||||
|
||||
> create/drop Stan S. Stanman;stan:typeclasses.merchants.NPCMerchant
|
||||
> set stan/shopname = Stan's previously owned vessles
|
||||
|
||||
> create/drop A proud vessel;ship
|
||||
> set ship/desc = The thing has holes in it.
|
||||
> set ship/gold_value = 5
|
||||
|
||||
> create/drop A classic speedster;rowboat
|
||||
> set rowboat/gold_value = 2
|
||||
> set rowboat/desc = It's not going anywhere fast.
|
||||
```
|
||||
|
||||
Note that a builder without any access to Python code can now set up a personalized merchant with just in-game commands. With the shop all set up, we just need to be in the same room to start consuming!
|
||||
|
||||
```
|
||||
> buy
|
||||
*** Welcome to Stan's previously owned vessels! ***
|
||||
Things for sale (choose 1-3 to inspect, quit to exit):
|
||||
_________________________________________________________
|
||||
1. A proud vessel (5 gold)
|
||||
2. A classic speedster (2 gold)
|
||||
|
||||
> 1
|
||||
|
||||
You inspect A proud vessel:
|
||||
|
||||
The thing has holes in it.
|
||||
__________________________________________________________
|
||||
1. Buy A proud vessel (5 gold)
|
||||
2. Look for something else.
|
||||
|
||||
> 1
|
||||
You pay 5 gold and purchase A proud vessel!
|
||||
|
||||
*** Welcome to Stan's previously owned vessels! ***
|
||||
Things for sale (choose 1-3 to inspect, quit to exit):
|
||||
_________________________________________________________
|
||||
1. A classic speedster (2 gold)
|
||||
|
||||
```
|
||||
|
||||
88
docs/source/Howtos/Tutorial-NPC-Reacting.md
Normal file
88
docs/source/Howtos/Tutorial-NPC-Reacting.md
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
# NPCs reacting to your presence
|
||||
|
||||
|
||||
> north
|
||||
------------------------------------
|
||||
Meadow
|
||||
You are standing in a green meadow.
|
||||
A bandit is here.
|
||||
------------------------------------
|
||||
Bandit gives you a menacing look!
|
||||
|
||||
This tutorial shows the implementation of an NPC object that responds to characters entering their
|
||||
location.
|
||||
|
||||
What we will need is the following:
|
||||
|
||||
- An NPC typeclass that can react when someone enters.
|
||||
- A custom [Room](../Components/Objects.md#rooms) typeclass that can tell the NPC that someone entered.
|
||||
- We will also tweak our default `Character` typeclass a little.
|
||||
|
||||
```python
|
||||
# in mygame/typeclasses/npcs.py (for example)
|
||||
|
||||
from typeclasses.characters import Character
|
||||
|
||||
class NPC(Character):
|
||||
"""
|
||||
A NPC typeclass which extends the character class.
|
||||
"""
|
||||
def at_char_entered(self, character):
|
||||
"""
|
||||
A simple is_aggressive check.
|
||||
Can be expanded upon later.
|
||||
"""
|
||||
if self.db.is_aggressive:
|
||||
self.execute_cmd(f"say Graaah! Die, {character}!")
|
||||
else:
|
||||
self.execute_cmd(f"say Greetings, {character}!")
|
||||
```
|
||||
|
||||
Here we make a simple method on the `NPC`˙. We expect it to be called when a (player-)character enters the room. We don't actually set the `is_aggressive` [Attribute](../Components/Attributes.md) beforehand; if it's not set, the NPC is simply non-hostile.
|
||||
|
||||
Whenever _something_ enters the `Room`, its [at_object_receive](DefaultObject.at_object_receive) hook will be called. So we should override it.
|
||||
|
||||
|
||||
```python
|
||||
# in mygame/typeclasses/rooms.py
|
||||
|
||||
from evennia import utils
|
||||
|
||||
# ...
|
||||
|
||||
class Room(ObjectParent, DefaultRoom):
|
||||
|
||||
# ...
|
||||
|
||||
def at_object_receive(self, arriving_obj, source_location):
|
||||
if arriving_obj.account:
|
||||
# this has an active acccount - a player character
|
||||
for item in self.contents:
|
||||
# get all npcs in the room and inform them
|
||||
if utils.inherits_from(item, "typeclasses.npcs.NPC"):
|
||||
self.at_char_entered(arriving_obj)
|
||||
|
||||
```
|
||||
|
||||
```{sidebar} Universal Object methods
|
||||
Remember that Rooms are `Objects`. So the same `at_object_receive` hook will fire for you when you pick something up (making you 'receive' it). Or for a box when putting something inside it.
|
||||
```
|
||||
A currently puppeted Character will have an `.account` attached to it. We use that to know that the thing arriving is a Character. We then use Evennia's [utils.inherits_from](evennia.utils.utils.inherits_from) helper utility to get every NPC in the room can each of their newly created `at_char_entered` method.
|
||||
|
||||
Make sure to `reload`.
|
||||
|
||||
Let's create an NPC and make it aggressive. For the sake of this example, let's assume your name is "Anna" and that there is a room to the north of your current location.
|
||||
|
||||
> create/drop Orc:typeclasses.npcs.NPC
|
||||
> north
|
||||
> south
|
||||
Orc says, Greetings, Anna!
|
||||
|
||||
Now let's turn the orc aggressive.
|
||||
|
||||
> set orc/is_aggressive = True
|
||||
> north
|
||||
> south
|
||||
Orc says, Graah! Die, Anna!
|
||||
|
||||
That's one easily aggravated Orc!
|
||||
|
|
@ -4,7 +4,6 @@ General Character commands usually available to all characters
|
|||
import re
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from evennia.typeclasses.attributes import NickTemplateInvalid
|
||||
from evennia.utils import utils
|
||||
|
||||
|
|
@ -324,7 +323,10 @@ class CmdNick(COMMAND_DEFAULT_CLASS):
|
|||
if replstring == old_replstring:
|
||||
string += f"\nIdentical {nicktypestr.lower()} already set."
|
||||
else:
|
||||
string += f"\n{nicktypestr} '|w{old_nickstring}|n' updated to map to '|w{replstring}|n'."
|
||||
string += (
|
||||
f"\n{nicktypestr} '|w{old_nickstring}|n' updated to map to"
|
||||
f" '|w{replstring}|n'."
|
||||
)
|
||||
else:
|
||||
string += f"\n{nicktypestr} '|w{nickstring}|n' mapped to '|w{replstring}|n'."
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -13,8 +13,7 @@ state. They do not fire callbacks, so are not a good fit for use cases
|
|||
where something needs to happen on a specific schedule (use delay or
|
||||
a TickerHandler for that instead).
|
||||
|
||||
See also the evennia documentation for command cooldowns
|
||||
(https://github.com/evennia/evennia/wiki/Command-Cooldown) for more information
|
||||
See also the evennia [howto](Howto-Command-Cooldown) for more information
|
||||
about the concept.
|
||||
|
||||
## Installation
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue