mirror of
https://github.com/evennia/evennia.git
synced 2026-03-16 21:06:30 +01:00
440 lines
22 KiB
Markdown
440 lines
22 KiB
Markdown
# Buffs
|
|
|
|
Contribution by Tegiminis 2022
|
|
|
|
A buff is a timed object, attached to a game entity. It is capable of modifying values, triggering code, or both.
|
|
It is a common design pattern in RPGs, particularly action games.
|
|
|
|
Features:
|
|
|
|
- `BuffHandler`: A buff handler to apply to your objects.
|
|
- `BaseBuff`: A buff class to extend from to create your own buffs.
|
|
- `BuffableProperty`: A sample property class to show how to automatically check modifiers.
|
|
- `CmdBuff`: A command which applies buffs.
|
|
- `samplebuffs.py`: Some sample buffs to learn from.
|
|
|
|
## Quick Start
|
|
Assign the handler to a property on the object, like so.
|
|
|
|
```python
|
|
@lazy_property
|
|
def buffs(self) -> BuffHandler:
|
|
return BuffHandler(self)
|
|
```
|
|
|
|
You may then call the handler to add or manipulate buffs like so: `object.buffs`. See **Using the Handler**.
|
|
|
|
### Customization
|
|
|
|
If you want to customize the handler, you can feed the constructor two arguments:
|
|
- `dbkey`: The string you wish to use as the attribute key for the buff database. Defaults to "buffs". This allows you to keep separate buff pools - for example, "buffs" and "perks".
|
|
- `autopause`: If you want this handler to automatically pause playtime buffs when its owning object is unpuppeted.
|
|
|
|
> **Note**: If you enable autopausing, you MUST initialize the property in your owning object's
|
|
> `at_init` hook. Otherwise, a hot reload can cause playtime buffs to not update properly
|
|
> on puppet/unpuppet. You have been warned!
|
|
|
|
Let's say you want another handler for an object, `perks`, which has a separate database and
|
|
respects playtime buffs. You'd assign this new property as so:
|
|
|
|
```python
|
|
class BuffableObject(Object):
|
|
@lazy_property
|
|
def perks(self) -> BuffHandler:
|
|
return BuffHandler(self, dbkey='perks', autopause=True)
|
|
|
|
def at_init(self):
|
|
self.perks
|
|
```
|
|
|
|
## Using the Handler
|
|
|
|
Here's how to make use of your new handler.
|
|
|
|
### Apply a Buff
|
|
|
|
Call the handler's `add` method. This requires a class reference, and also contains a number of
|
|
optional arguments to customize the buff's duration, stacks, and so on. You can also store any arbitrary value
|
|
in the buff's cache by passing a dictionary through the `to_cache` optional argument. This will not overwrite the normal
|
|
values on the cache.
|
|
|
|
```python
|
|
self.buffs.add(StrengthBuff) # A single stack of StrengthBuff with normal duration
|
|
self.buffs.add(DexBuff, stacks=3, duration=60) # Three stacks of DexBuff, with a duration of 60 seconds
|
|
self.buffs.add(ReflectBuff, to_cache={'reflect': 0.5}) # A single stack of ReflectBuff, with an extra cache value
|
|
```
|
|
|
|
Two important attributes on the buff are checked when the buff is applied: `refresh` and `unique`.
|
|
- `refresh` (default: True) determines if a buff's timer is refreshed when it is reapplied.
|
|
- `unique` (default: True) determines if this buff is unique; that is, only one of it exists on the object.
|
|
|
|
The combination of these two booleans creates one of three kinds of keys:
|
|
- `Unique is True, Refresh is True/False`: The buff's default key.
|
|
- `Unique is False, Refresh is True`: The default key mixed with the applier's dbref. This makes the buff "unique-per-player", so you can refresh through reapplication.
|
|
- `Unique is False, Refresh is False`: The default key mixed with a randomized number.
|
|
|
|
### Get Buffs
|
|
|
|
The handler has several getter methods which return instanced buffs. You won't need to use these for basic functionality, but if you want to manipulate
|
|
buffs after application, they are very useful. The handler's `check`/`trigger` methods utilize some of these getters, while others are just for developer convenience.
|
|
|
|
`get(key)` is the most basic getter. It returns a single buff instance, or `None` if the buff doesn't exist on the handler. It is also the only getter
|
|
that returns a single buff instance, rather than a dictionary.
|
|
|
|
> **Note**: The handler method `has(buff)` allows you to check if a matching key (if a string) or buff class (if a class) is present on the handler cache, without actually instantiating the buff. You should use this method for basic "is this buff present?" checks.
|
|
|
|
Group getters, listed below, return a dictionary of values in the format `{buffkey: instance}`. If you want to iterate over all of these buffs,
|
|
you should do so via the `dict.values()` method.
|
|
|
|
- `get_all()` returns all buffs on this handler. You can also use the `handler.all` property.
|
|
- `get_by_type(BuffClass)` returns buffs of the specified type.
|
|
- `get_by_stat(stat)` returns buffs with a `Mod` object of the specified `stat` string in their `mods` list.
|
|
- `get_by_trigger(string)` returns buffs with the specified string in their `triggers` list.
|
|
- `get_by_source(Object)` returns buffs applied by the specified `source` object.
|
|
- `get_by_cachevalue(key, value)` returns buffs with the matching `key: value` pair in their cache. `value` is optional.
|
|
|
|
All group getters besides `get_all()` can "slice" an existing dictionary through the optional `to_filter` argument.
|
|
|
|
```python
|
|
dict1 = handler.get_by_type(Burned) # This finds all "Burned" buffs on the handler
|
|
dict2 = handler.get_by_source(self, to_filter=dict1) # This filters dict1 to find buffs with the matching source
|
|
```
|
|
|
|
> **Note**: Most of these getters also have an associated handler property. For example, `handler.effects` returns all buffs that can be triggered, which
|
|
> is then iterated over by the `get_by_trigger` method.
|
|
|
|
### Remove Buffs
|
|
|
|
There are also a number of remover methods. Generally speaking, these follow the same format as the getters.
|
|
|
|
- `remove(key)` removes the buff with the specified key.
|
|
- `clear()` removes all buffs.
|
|
- `remove_by_type(BuffClass)` removes buffs of the specified type.
|
|
- `remove_by_stat(stat)` removes buffs with a `Mod` object of the specified `stat` string in their `mods` list.
|
|
- `remove_by_trigger(string)` removes buffs with the specified string in their `triggers` list.
|
|
- `remove_by_source(Object)` removes buffs applied by the specified source
|
|
- `remove_by_cachevalue(key, value)` removes buffs with the matching `key: value` pair in their cache. `value` is optional.
|
|
|
|
You can also remove a buff by calling the instance's `remove` helper method. You can do this on the dictionaries returned by the
|
|
getters listed above.
|
|
|
|
```python
|
|
to_remove = handler.get_by_trigger(trigger) # Finds all buffs with the specified trigger
|
|
for buff in to_remove.values(): # Removes all buffs in the to_remove dictionary via helper methods
|
|
buff.remove()
|
|
```
|
|
|
|
### Check Modifiers
|
|
|
|
Call the handler `check(value, stat)` method when you want to see the modified value.
|
|
This will return the `value`, modified by any relevant buffs on the handler's owner (identified by
|
|
the `stat` string).
|
|
|
|
For example, let's say you want to modify how much damage you take. That might look something like this:
|
|
|
|
```python
|
|
# The method we call to damage ourselves
|
|
def take_damage(self, source, damage):
|
|
_damage = self.buffs.check(damage, 'taken_damage')
|
|
self.db.health -= _damage
|
|
```
|
|
|
|
This method calls the `at_pre_check` and `at_post_check` methods at the relevant points in the process. You can use to this make
|
|
buffs that are reactive to being checked; for example, removing themselves, altering their values, or interacting with the game state.
|
|
|
|
> **Note**: You can also trigger relevant buffs at the same time as you check them by ensuring the optional argument `trigger` is True in the `check` method.
|
|
|
|
Modifiers are calculated additively - that is, all modifiers of the same type are added together before being applied. They are then
|
|
applied through the following formula.
|
|
|
|
```python
|
|
(base + total_add) / max(1, 1.0 + total_div) * max(0, 1.0 + total_mult)
|
|
```
|
|
|
|
#### Multiplicative Buffs (Advanced)
|
|
|
|
Multiply/divide modifiers in this buff system are additive by default. This means that two +50% modifiers will equal a +100% modifier. But what if you want to apply mods multiplicatively?
|
|
|
|
First, you should carefully consider if you truly want multiplicative modifiers. Here's some things to consider.
|
|
|
|
- They are unintuitive to the average user, as two +50% damage buffs equal +125% instead of +100%.
|
|
- They lead to "power explosion", where stacking buffs in the right way can turn characters into unstoppable forces
|
|
|
|
Doing purely-additive multipliers allows you to better control the balance of your game. Conversely, doing multiplicative multipliers enables very fun build-crafting where smart usage of buffs and skills can turn you into a one-shot powerhouse. Each has its place.
|
|
|
|
The best design practice for multiplicative buffs is to divide your multipliers into "tiers", where each tier is applied separately. You can easily do this with multiple `check` calls.
|
|
|
|
```python
|
|
damage = damage
|
|
damage = handler.check(damage, 'damage')
|
|
damage = handler.check(damage, 'empower')
|
|
damage = handler.check(damage, 'radiant')
|
|
damage = handler.check(damage, 'overpower')
|
|
```
|
|
|
|
#### Buff Strength Priority (Advanced)
|
|
|
|
Sometimes you only want to apply the strongest modifier to a stat. This is supported by the optional `strongest` bool arg in the handler's check method
|
|
|
|
```python
|
|
def take_damage(self, source, damage):
|
|
_damage = self.buffs.check(damage, 'taken_damage', strongest=True)
|
|
self.db.health -= _damage
|
|
```
|
|
|
|
### Trigger Buffs
|
|
|
|
Call the handler's `trigger(string)` method when you want an event call. This will call the `at_trigger` hook method on all buffs with the relevant trigger `string`.
|
|
|
|
For example, let's say you want to trigger a buff to "detonate" when you hit your target with an attack.
|
|
You'd write a buff that might look like this:
|
|
|
|
```python
|
|
class Detonate(BaseBuff):
|
|
...
|
|
triggers = ['take_damage']
|
|
def at_trigger(self, trigger, *args, **kwargs)
|
|
self.owner.take_damage(100)
|
|
self.remove()
|
|
```
|
|
|
|
And then call `handler.trigger('take_damage')` in the method you use to take damage.
|
|
|
|
> **Note** You could also do this through mods and `at_post_check` if you like, depending on how to want to add the damage.
|
|
|
|
### Ticking
|
|
|
|
Ticking buffs are slightly special. They are similar to trigger buffs in that they run code, but instead of
|
|
doing so on an event trigger, they do so on a periodic tick. A common use case for a buff like this is a poison,
|
|
or a heal over time.
|
|
|
|
```python
|
|
class Poison(BaseBuff):
|
|
...
|
|
tickrate = 5
|
|
def at_tick(self, initial=True, *args, **kwargs):
|
|
_dmg = self.dmg * self.stacks
|
|
if not initial:
|
|
self.owner.location.msg_contents(
|
|
"Poison courses through {actor}'s body, dealing {damage} damage.".format(
|
|
actor=self.owner.named, damage=_dmg
|
|
)
|
|
)
|
|
```
|
|
|
|
To make a buff ticking, ensure the `tickrate` is 1 or higher, and it has code in its `at_tick`
|
|
method. Once you add it to the handler, it starts ticking!
|
|
|
|
> **Note**: Ticking buffs always tick on initial application, when `initial` is `True`. If you don't want your hook to fire at that time,
|
|
> make sure to check the value of `initial` in your `at_tick` method.
|
|
|
|
### Context
|
|
|
|
Every important handler method optionally accepts a `context` dictionary.
|
|
|
|
Context is an important concept for this handler. Every method which checks, triggers, or ticks a buff passes this
|
|
dictionary (default: empty) to the buff hook methods as keyword arguments (`**kwargs`). It is used for nothing else. This allows you to make those
|
|
methods "event-aware" by storing relevant data in the dictionary you feed to the method.
|
|
|
|
For example, let's say you want a "thorns" buff which damages enemies that attack you. Let's take our `take_damage` method
|
|
and add a context to the mix.
|
|
|
|
```python
|
|
def take_damage(attacker, damage):
|
|
context = {'attacker': attacker, 'damage': damage}
|
|
_damage = self.buffs.check(damage, 'taken_damage', context=context)
|
|
self.buffs.trigger('taken_damage', context=context)
|
|
self.db.health -= _damage
|
|
```
|
|
Now we use the values that context passes to the buff kwargs to customize our logic.
|
|
```python
|
|
class ThornsBuff(BaseBuff):
|
|
...
|
|
triggers = ['taken_damage']
|
|
# This is the hook method on our thorns buff
|
|
def at_trigger(self, trigger, attacker=None, damage=0, **kwargs):
|
|
if not attacker:
|
|
return
|
|
attacker.db.health -= damage * 0.2
|
|
```
|
|
Apply the buff, take damage, and watch the thorns buff do its work!
|
|
|
|
### Viewing
|
|
|
|
There are two helper methods on the handler that allow you to get useful buff information back.
|
|
|
|
- `view`: Returns a dictionary of tuples in the format `{buffkey: (buff.name, buff.flavor)}`. Finds all buffs by default, but optionally accepts a dictionary of buffs to filter as well. Useful for basic buff readouts.
|
|
- `view_modifiers(stat)`: Returns a nested dictionary of information on modifiers that affect the specified stat. The first layer is the modifier type (`add/mult/div`) and the second layer is the value type (`total/strongest`). Does not return the buffs that cause these modifiers, just the modifiers themselves (akin to using `handler.check` but without actually modifying a value). Useful for stat sheets.
|
|
|
|
You can also create your own custom viewing methods through the various handler getters, which will always return the entire buff object.
|
|
|
|
## Creating New Buffs
|
|
|
|
Creating a new buff is very easy: extend `BaseBuff` into a new class, and fill in all the relevant buff details.
|
|
However, there are a lot of individual moving parts to a buff. Here's a step-through of the important stuff.
|
|
|
|
### Basics
|
|
|
|
Regardless of any other functionality, all buffs have the following class attributes:
|
|
|
|
- They have customizable `key`, `name`, and `flavor` strings.
|
|
- They have a `duration` (float), and automatically clean-up at the end. Use -1 for infinite duration, and 0 to clean-up immediately. (default: -1)
|
|
- They have a `tickrate` (float), and automatically tick if it is greater than 1 (default: 0)
|
|
- They can stack, if `maxstacks` (int) is not equal to 1. If it's 0, the buff stacks forever. (default: 1)
|
|
- They can be `unique` (bool), which determines if they have a unique namespace or not. (default: True)
|
|
- They can `refresh` (bool), which resets the duration when stacked or reapplied. (default: True)
|
|
- They can be `playtime` (bool) buffs, where duration only counts down during active play. (default: False)
|
|
|
|
Buffs also have a few useful properties:
|
|
|
|
- `owner`: The object this buff is attached to
|
|
- `ticknum`: How many ticks the buff has gone through
|
|
- `timeleft`: How much time is remaining on the buff
|
|
- `ticking`/`stacking`: If this buff ticks/stacks (checks `tickrate` and `maxstacks`)
|
|
|
|
#### Buff Cache (Advanced)
|
|
|
|
Buffs always store some useful mutable information about themselves in the cache (what is stored on the owning object's database attribute). A buff's cache corresponds to `{buffkey: buffcache}`, where `buffcache` is a dictionary containing __at least__ the information below:
|
|
|
|
- `ref` (class): The buff class path we use to construct the buff.
|
|
- `start` (float): The timestamp of when the buff was applied.
|
|
- `source` (Object): If specified; this allows you to track who or what applied the buff.
|
|
- `prevtick` (float): The timestamp of the previous tick.
|
|
- `duration` (float): The cached duration. This can vary from the class duration, depending on if the duration has been modified (paused, extended, shortened, etc).
|
|
- `tickrate` (float): The buff's tick rate. Cannot go below 0. Altering the tickrate on an applied buff will not cause it to start ticking if it wasn't ticking before. (`pause` and `unpause` to start/stop ticking on existing buffs)
|
|
- `stacks` (int): How many stacks they have.
|
|
- `paused` (bool): Paused buffs do not clean up, modify values, tick, or fire any hook methods.
|
|
|
|
Sometimes you will want to dynamically update a buff's cache at runtime, such as changing a tickrate in a hook method, or altering a buff's duration.
|
|
You can do so by using the interface `buff.cachekey`. As long as the attribute name matches a key in the cache dictionary, it will update the stored
|
|
cache with the new value.
|
|
|
|
If there is no matching key, it will do nothing. If you wish to add a new key to the cache, you must use the `buff.update_cache(dict)` method,
|
|
which will properly update the cache (including adding new keys) using the dictionary provided.
|
|
|
|
> **Example**: You want to increase a buff's duration by 30 seconds. You use `buff.duration += 30`. This new duration is now reflected on both the instance and the cache.
|
|
|
|
The buff cache can also store arbitrary information. To do so, pass a dictionary through the handler `add` method (`handler.add(BuffClass, to_cache=dict)`),
|
|
set the `cache` dictionary attribute on your buff class, or use the aforementioned `buff.update_cache(dict)` method.
|
|
|
|
> **Example**: You store `damage` as a value in the buff cache and use it for your poison buff. You want to increase it over time, so you use `buff.damage += 1` in the tick method.
|
|
|
|
### Modifiers
|
|
|
|
Mods are stored in the `mods` list attribute. Buffs which have one or more Mod objects in them can modify stats. You can use the handler method to check all
|
|
mods of a specific stat string and apply their modifications to the value; however, you are encouraged to use `check` in a getter/setter, for easy access.
|
|
|
|
Mod objects consist of only four values, assigned by the constructor in this order:
|
|
|
|
- `stat`: The stat you want to modify. When `check` is called, this string is used to find all the mods that are to be collected.
|
|
- `mod`: The modifier. Defaults are `add` (addition/subtraction), `mult` (multiply), and `div` (divide). Modifiers are calculated additively (see `_calculate_mods` for more)
|
|
- `value`: How much value the modifier gives regardless of stacks
|
|
- `perstack`: How much value the modifier grants per stack, **INCLUDING** the first. (default: 0)
|
|
|
|
The most basic way to add a Mod to a buff is to do so in the buff class definition, like this:
|
|
|
|
```python
|
|
class DamageBuff(BaseBuff):
|
|
mods = [Mod('damage', 'add', 10)]
|
|
```
|
|
|
|
No mods applied to the value are permanent in any way. All calculations are done at runtime, and the mod values are never stored
|
|
anywhere except on the buff in question. In other words: you don't need to track the origin of particular stat mods, and you will
|
|
never permanently change a stat modified by a buff. To remove the modification, simply remove the buff from the object.
|
|
|
|
> **Note**: You can add your own modifier types by overloading the `_calculate_mods` method, which contains the basic modifier application logic.
|
|
|
|
#### Generating Mods (Advanced)
|
|
|
|
An advanced way to do mods is to generate them when the buff is initialized. This lets you create mods on the fly that are reactive to the game state.
|
|
|
|
```python
|
|
class GeneratedStatBuff(BaseBuff):
|
|
...
|
|
def at_init(self, *args, **kwargs) -> None:
|
|
# Finds our "modgen" cache value, and generates a mod from it
|
|
modgen = list(self.cache.get("modgen"))
|
|
if modgen:
|
|
self.mods = [Mod(*modgen)]
|
|
```
|
|
|
|
### Triggers
|
|
|
|
Buffs which have one or more strings in the `triggers` attribute can be triggered by events.
|
|
|
|
When the handler's `trigger` method is called, it searches all buffs on the handler for any with a matchingtrigger,
|
|
then calls their `at_trigger` hooks. Buffs can have multiple triggers, and you can tell which trigger was used by
|
|
the `trigger` argument in the hook.
|
|
|
|
```python
|
|
class AmplifyBuff(BaseBuff):
|
|
triggers = ['damage', 'heal']
|
|
|
|
def at_trigger(self, trigger, **kwargs):
|
|
if trigger == 'damage': print('Damage trigger called!')
|
|
if trigger == 'heal': print('Heal trigger called!')
|
|
```
|
|
|
|
### Ticking
|
|
|
|
A buff which ticks isn't much different than one which triggers. You're still executing arbitrary hooks on
|
|
the buff class. To tick, the buff must have a `tickrate` of 1 or higher.
|
|
|
|
```python
|
|
class Poison(BaseBuff):
|
|
...
|
|
# this buff will tick 6 times between application and cleanup.
|
|
duration = 30
|
|
tickrate = 5
|
|
def at_tick(self, initial, **kwargs):
|
|
self.owner.take_damage(10)
|
|
```
|
|
> **Note**: The buff always ticks once when applied. For this **first tick only**, `initial` will be True in the `at_tick` hook method. `initial` will be False on subsequent ticks.
|
|
|
|
Ticks utilize a persistent delay, so they should be pickleable. As long as you are not adding new properties to your buff class, this shouldn't be a concern.
|
|
If you **are** adding new properties, try to ensure they do not end up with a circular code path to their object or handler, as this will cause pickling errors.
|
|
|
|
### Extras
|
|
|
|
Buffs have a grab-bag of extra functionality to let you add complexity to your designs.
|
|
|
|
#### Conditionals
|
|
|
|
You can restrict whether or not the buff will `check`, `trigger`, or `tick` through defining the `conditional` hook. As long
|
|
as it returns a "truthy" value, the buff will apply itself. This is useful for making buffs dependent on game state - for
|
|
example, if you want a buff that makes the player take more damage when they are on fire:
|
|
|
|
```python
|
|
class FireSick(BaseBuff):
|
|
...
|
|
def conditional(self, *args, **kwargs):
|
|
if self.owner.buffs.has(FireBuff):
|
|
return True
|
|
return False
|
|
```
|
|
|
|
Conditionals for `check`/`trigger` are checked when the buffs are gathered by the handler methods for the respective operations. `Tick`
|
|
conditionals are checked each tick.
|
|
|
|
#### Helper Methods
|
|
|
|
Buff instances have a number of helper methods.
|
|
|
|
- `remove`/`dispel`: Allows you to remove or dispel the buff. Calls `at_remove`/`at_dispel`, depending on optional arguments.
|
|
- `pause`/`unpause`: Pauses and unpauses the buff. Calls `at_pause`/`at_unpause`.
|
|
- `reset`: Resets the buff's start to the current time; same as "refreshing" it.
|
|
- `alter_cache`: Updates the buff's cache with the `{key:value}` pairs in the provided dictionary. Can overwrite default values, so be careful!
|
|
|
|
#### Playtime Duration
|
|
|
|
If your handler has `autopause` enabled, any buffs with truthy `playtime` value will automatically pause
|
|
and unpause when the object the handler is attached to is puppetted or unpuppetted. This even works with ticking buffs,
|
|
although if you have less than 1 second of tick duration remaining, it will round up to 1s.
|
|
|
|
> **Note**: If you want more control over this process, you can comment out the signal subscriptions on the handler and move the autopause logic
|
|
> to your object's `at_pre/post_puppet/unpuppet` hooks.
|
|
|
|
----
|
|
|
|
<small>This document page is generated from `evennia/contrib/rpg/buffs/README.md`. Changes to this
|
|
file will be overwritten, so edit that file rather than this one.</small>
|