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 # Command Cooldown
Some types of games want to limit how often a command can be run. If a 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 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. 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 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. 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 ## Non-persistent cooldown
This little recipe will limit how often a particular command can be run. Since 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. delay has passed.
```python ```python
# in, say, mygame/commands/spells.py
import time import time
from evennia import default_cmds from evennia import default_cmds
@ -36,63 +44,116 @@ class CmdSpellFirestorm(default_cmds.MuxCommand):
firestorm every five minutes (assuming you have the mana). firestorm every five minutes (assuming you have the mana).
""" """
key = "cast firestorm" key = "cast firestorm"
locks = "cmd:isFireMage()" rate_of_fire = 60 * 2 # 2 minutes
def func(self): def func(self):
"Implement the spell" "Implement the spell"
# check cooldown (5 minute cooldown)
now = time.time() now = time.time()
if hasattr(self, "lastcast") and \ last_cast = caller.ndb.firestorm_last_cast # could be None
now - self.lastcast < 5 * 60: if last_cast and (now - last_cast < self.rate_of_fire):
message = "You cannot cast this spell again yet." message = "You cannot cast this spell again yet."
self.caller.msg(message) self.caller.msg(message)
return return
#[the spell effect is implemented] # [the spell effect is implemented]
# if the spell was successfully cast, store the casting time # 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 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 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 `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 forgotten.
(easily) check for this value.
## Persistent cooldown ## Persistent cooldown
This is essentially the same mechanism as the simple one above, except we use To make a cooldown _persistent_ (so it survives a server reload), just
the database to store the information which means the cooldown will survive a use the same technique, but use [Attributes](Attributes) (that is, `.db` instead
server reload/reboot. Since commands themselves have no representation in the of `.ndb` storage to save the last-cast time.
database, you need to use the caster for the storage.
## 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 ```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() class CooldownCommandMixin:
lastcast = self.caller.db.firestorm_lastcast
if lastcast and now - lastcast < 5 * 60: rate_of_fire = 60
message = "You need to wait before casting this spell again." cooldown_storage_key = "last_used"
self.caller.msg(message) cooldown_storage_category = "cmd_cooldowns"
return
#[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 def update_cooldown(self):
self.caller.db.firestorm_lastcast = now 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 This is meant to be mixed into a Command, so we assume `self.caller` exists.
variable as `firestorm_lastcast` so we are sure we get the right one (we'll We allow for setting what Attribute key/category to use to store the cooldown.
likely have other skills with cooldowns after all). But this method of
using cooldowns also has the advantage of working *between* commands - you can It also uses an Attribute-category to make sure what it stores is not mixed up
for example let all fire-related spells check the same cooldown to make sure with other Attributes on the caller.
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 Here's how it's used:
other types of attacks for a while before the warrior can recover.
```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.