From e6090ef1ed616609d285b4b7957073befa28fc43 Mon Sep 17 00:00:00 2001 From: Griatch Date: Wed, 2 Nov 2022 00:30:43 +0100 Subject: [PATCH] Update cooldown tutorial. Resolve #2648. --- docs/source/Howtos/Command-Cooldown.md | 167 +++++++++++++++++-------- 1 file changed, 114 insertions(+), 53 deletions(-) diff --git a/docs/source/Howtos/Command-Cooldown.md b/docs/source/Howtos/Command-Cooldown.md index 6310acaa9a..0b04840cc8 100644 --- a/docs/source/Howtos/Command-Cooldown.md +++ b/docs/source/Howtos/Command-Cooldown.md @@ -1,16 +1,22 @@ # 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 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. Such effects are called *cooldowns*. This page exemplifies a very resource-efficient way to do cooldowns. A more 'active' way is to use asynchronous delays as in the [command duration 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 @@ -19,80 +25,135 @@ 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. +delay has passed. ```python -import time +# in, say, mygame/commands/spells.py + +import time from evennia import default_cmds - + class CmdSpellFirestorm(default_cmds.MuxCommand): """ Spell - Firestorm - Usage: + Usage: cast firestorm - - This will unleash a storm of flame. You can only release one - firestorm every five minutes (assuming you have the mana). + + This will unleash a storm of flame. You can only release one + 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: + + now = time.time() + 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 - - #[the spell effect is implemented] - + return + + # [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) - - now = time.time() - lastcast = self.caller.db.firestorm_lastcast - - if lastcast and now - lastcast < 5 * 60: - message = "You need to wait before casting this spell again." - self.caller.msg(message) - return - - #[the spell effect is implemented] - - # if the spell was successfully cast, store the casting time - self.caller.db.firestorm_lastcast = now +import time + +class CooldownCommandMixin: + + rate_of_fire = 60 + cooldown_storage_key = "last_used" + cooldown_storage_category = "cmd_cooldowns" + + 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 + + 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. \ No newline at end of file +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.