Update cooldown tutorial. Resolve #2648.

This commit is contained in:
Griatch 2022-11-02 00:30:43 +01:00
parent 6e4bb8047b
commit e6090ef1ed

View file

@ -1,6 +1,5 @@
# Command Cooldown
Some types of games want to limit how often a command can be run. If a
character casts the spell *Firestorm*, you might not want them to spam that
command over and over. Or in an advanced combat system, a massive swing may
@ -12,6 +11,13 @@ This page exemplifies a very resource-efficient way to do cooldowns. A more
tutorial](./Command-Duration.md#blocking-commands), the two might be useful to
combine if you want to echo some message to the user after the cooldown ends.
## The Cooldown Contrib
The [Cooldown contrib](Contribs/Contrib-Cooldowns) is a ready-made solution for
command cooldowns you can use. It implements a _handler_ on the object to
conveniently manage and store the cooldowns in a similar manner exemplified in
this tutorial.
## Non-persistent cooldown
This little recipe will limit how often a particular command can be run. Since
@ -22,6 +28,8 @@ that time stored, and compare it with the current time to see if a desired
delay has passed.
```python
# in, say, mygame/commands/spells.py
import time
from evennia import default_cmds
@ -36,15 +44,14 @@ class CmdSpellFirestorm(default_cmds.MuxCommand):
firestorm every five minutes (assuming you have the mana).
"""
key = "cast firestorm"
locks = "cmd:isFireMage()"
rate_of_fire = 60 * 2 # 2 minutes
def func(self):
"Implement the spell"
# check cooldown (5 minute cooldown)
now = time.time()
if hasattr(self, "lastcast") and \
now - self.lastcast < 5 * 60:
last_cast = caller.ndb.firestorm_last_cast # could be None
if last_cast and (now - last_cast < self.rate_of_fire):
message = "You cannot cast this spell again yet."
self.caller.msg(message)
return
@ -52,47 +59,101 @@ class CmdSpellFirestorm(default_cmds.MuxCommand):
# [the spell effect is implemented]
# if the spell was successfully cast, store the casting time
self.lastcast = now
self.caller.ndb.firestorm_last_cast = now
```
We just check the `lastcast` flag, and update it if everything works out.
We specify `rate_of_fire` and then just check for a NAtrribute
`firestorm_last_cast` and update it if everything works out.
Simple and very effective since everything is just stored in memory. The
drawback of this simple scheme is that it's non-persistent. If you do
`@reload`, the cache is cleaned and all such ongoing cooldowns will be
forgotten. It is also limited only to this one command, other commands cannot
(easily) check for this value.
`reload`, the cache is cleaned and all such ongoing cooldowns will be
forgotten.
## Persistent cooldown
This is essentially the same mechanism as the simple one above, except we use
the database to store the information which means the cooldown will survive a
server reload/reboot. Since commands themselves have no representation in the
database, you need to use the caster for the storage.
To make a cooldown _persistent_ (so it survives a server reload), just
use the same technique, but use [Attributes](Attributes) (that is, `.db` instead
of `.ndb` storage to save the last-cast time.
## Make a cooldown-aware command parent
If you have many different spells or other commands with cooldowns, you don't
want to have to add this code every time. Instead you can make a "cooldown
command mixin" class. A _mixin_ is a class that you can 'add' to another class
(via multiple inheritance) to give it some special ability. Here's an example
with persistent storage:
```python
# inside the func() of CmdSpellFirestorm as above
# in, for example, mygame/commands/mixins.py
# check cooldown (5 minute cooldown)
import time
now = time.time()
lastcast = self.caller.db.firestorm_lastcast
class CooldownCommandMixin:
if lastcast and now - lastcast < 5 * 60:
message = "You need to wait before casting this spell again."
self.caller.msg(message)
return
rate_of_fire = 60
cooldown_storage_key = "last_used"
cooldown_storage_category = "cmd_cooldowns"
#[the spell effect is implemented]
def check_cooldown(self):
last_time = self.caller.attributes.get(
key=self.cooldown_storage_key,
category=self.cooldown_storage_category)
)
return (time.time() - last_time) < self.rate_of_fire
# if the spell was successfully cast, store the casting time
self.caller.db.firestorm_lastcast = now
def update_cooldown(self):
self.caller.attribute.add(
key=self.cooldown_storage_key,
value=time.time(),
category=self.cooldown_storage_category
)
```
Since we are storing as an [Attribute](../Components/Attributes.md), we need to identify the
variable as `firestorm_lastcast` so we are sure we get the right one (we'll
likely have other skills with cooldowns after all). But this method of
using cooldowns also has the advantage of working *between* commands - you can
for example let all fire-related spells check the same cooldown to make sure
the casting of *Firestorm* blocks all fire-related spells for a while. Or, in
the case of taking that big swing with the sword, this could now block all
other types of attacks for a while before the warrior can recover.
This is meant to be mixed into a Command, so we assume `self.caller` exists.
We allow for setting what Attribute key/category to use to store the cooldown.
It also uses an Attribute-category to make sure what it stores is not mixed up
with other Attributes on the caller.
Here's how it's used:
```python
# in, say, mygame/commands/spells.py
from evennia import default_cmds
from .mixins import CooldownCommandMixin
class CmdSpellFirestorm(
CooldownCommandMixin, default_cmds.MuxCommand):
key = "cast firestorm"
cooldown_storage_key = "firestorm_last_cast"
rate_of_fire = 60 * 2
def func(self):
if not self.check_cooldown():
self.caller.msg("You cannot cast this spell again yet.")
return
# [the spell effect happens]
self.update_cooldown()
```
So the same as before, we have just hidden away the cooldown checks and you can
reuse this mixin for all your cooldowns.
### Command crossover
This example of cooldown-checking also works *between* commands. For example,
you can have all fire-related spells store the cooldown with the same
`cooldown_storage_key` (like `fire_spell_last_used`). That would mean casting
of *Firestorm* would block all other fire-related spells for a while.
Similarly, when you take that that big sword swing, other types of attacks could
be blocked before you can recover your balance.