mirror of
https://github.com/evennia/evennia.git
synced 2026-03-16 21:06:30 +01:00
readme review, made non-unique/refresh namespace less prone to collision
This commit is contained in:
parent
6ba3cf12fc
commit
a95dea471f
2 changed files with 74 additions and 66 deletions
|
|
@ -5,12 +5,13 @@ 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.
|
||||
|
||||
This contrib offers you:
|
||||
- A buff handler to apply to your objects (`BuffHandler`).
|
||||
- A buff class to extend from to create your own buffs (`BaseBuff`).
|
||||
- A sample property class to show how to automatically check modifiers (`BuffableProperty`).
|
||||
- A command which applies buffs (`CmdBuff`).
|
||||
- Some sample buffs to learn from (`samplebuffs.py`).
|
||||
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.
|
||||
|
|
@ -21,28 +22,30 @@ def buffs(self) -> BuffHandler:
|
|||
return BuffHandler(self)
|
||||
```
|
||||
|
||||
You may then call the handler to add or manipulate buffs.
|
||||
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 a 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 certain buffs when its owning object is unpuppeted.
|
||||
- `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 object's
|
||||
> `at_init` hook to cache. Otherwise, a hot reload can cause playtime buffs to not update properly
|
||||
> **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
|
||||
@lazy_property
|
||||
def perks(self) -> BuffHandler:
|
||||
return BuffHandler(self, dbkey='perks', autopause=True)
|
||||
```
|
||||
class BuffableObject(Object):
|
||||
@lazy_property
|
||||
def perks(self) -> BuffHandler:
|
||||
return BuffHandler(self, dbkey='perks', autopause=True)
|
||||
|
||||
And initialize it by adding `self.perks` to the object's `at_init`.
|
||||
def at_init(self):
|
||||
self.perks
|
||||
```
|
||||
|
||||
## Using the Handler
|
||||
|
||||
|
|
@ -50,9 +53,9 @@ Here's how to make use of your new handler.
|
|||
|
||||
### Apply a Buff
|
||||
|
||||
Call the handler `add` method. This requires a class reference, and also contains a number of
|
||||
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` argument. This will not overwrite the normal
|
||||
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
|
||||
|
|
@ -63,9 +66,12 @@ self.buffs.add(ReflectBuff, to_cache={'reflect': 0.5}) # A single stack of Refl
|
|||
|
||||
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 the buff uses the buff's normal key (True) or one created with the key and the applier's dbref (False)
|
||||
- `unique` (default: True) determines if this buff is unique; that is, only one of it exists on the object.
|
||||
|
||||
If both are `False`, the buff creates a random (rather than buff + source) key each time it is applied, and will never refresh.
|
||||
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
|
||||
|
||||
|
|
@ -75,24 +81,24 @@ buffs after application, they are very useful. The handler's `check`/`trigger` m
|
|||
`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.
|
||||
|
||||
Grouped getters, listed below, return a dictionary of values in the format `{buffkey: instance}`. If you want to iterate over all these buffs,
|
||||
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. Used by `handler.check(stat)`.
|
||||
- `get_by_trigger(trigger)` returns buffs with the specified trigger in their `triggers` list. Used by `handler.trigger(key)`.
|
||||
- `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 grouped getters besides `get_all()` can "slice" an existing dictionary through the optional `to_filter` argument.
|
||||
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 which can be triggered, which
|
||||
> **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
|
||||
|
|
@ -103,7 +109,7 @@ There are also a number of remover methods. Generally speaking, these follow the
|
|||
- `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(trigger)` removes buffs with the specified trigger in their `triggers` 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.
|
||||
|
||||
|
|
@ -112,12 +118,13 @@ getters listed above.
|
|||
|
||||
```python
|
||||
to_remove = handler.get_by_trigger(trigger) # Finds all buffs with the specified trigger
|
||||
for buff in to_remove.values(): buff.remove() # Removes all buffs in the to_remove dictionary via helper methods
|
||||
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 wherever you want to see the modified value.
|
||||
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).
|
||||
|
||||
|
|
@ -130,21 +137,20 @@ def take_damage(self, source, damage):
|
|||
self.db.health -= _damage
|
||||
```
|
||||
|
||||
This method call the `at_pre_check` and `at_post_check` methods at the relevant points in the process. You can use to this make
|
||||
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.
|
||||
|
||||
### Trigger Buffs
|
||||
|
||||
Call the handler `trigger(string)` method wherever you want an event call. This
|
||||
will call the `at_trigger` hook method on all buffs with the relevant trigger.
|
||||
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
|
||||
def Detonate(BaseBuff):
|
||||
class Detonate(BaseBuff):
|
||||
...
|
||||
triggers = ['take_damage']
|
||||
def at_trigger(self, trigger, *args, **kwargs)
|
||||
|
|
@ -163,7 +169,7 @@ doing so on an event trigger, they do so on a periodic tick. A common use case f
|
|||
or a heal over time.
|
||||
|
||||
```python
|
||||
def Poison(BaseBuff):
|
||||
class Poison(BaseBuff):
|
||||
...
|
||||
tickrate = 5
|
||||
def at_tick(self, initial=True, *args, **kwargs):
|
||||
|
|
@ -176,7 +182,7 @@ def Poison(BaseBuff):
|
|||
)
|
||||
```
|
||||
|
||||
All you need to do to make a buff tick is ensure the `tickrate` is 1 or higher, and it has code in its `at_tick`
|
||||
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,
|
||||
|
|
@ -186,7 +192,7 @@ method. Once you add it to the handler, it starts ticking!
|
|||
|
||||
Every important handler method optionally accepts a `context` dictionary.
|
||||
|
||||
Context is an important concept for this handler. Every method which modifies, triggers, or checks a buff passes this
|
||||
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.
|
||||
|
||||
|
|
@ -202,12 +208,13 @@ def take_damage(attacker, damage):
|
|||
```
|
||||
Now we use the values that context passes to the buff kwargs to customize our logic.
|
||||
```python
|
||||
def ThornsBuff(BaseBuff):
|
||||
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
|
||||
if not attacker:
|
||||
return
|
||||
attacker.db.health -= damage * 0.2
|
||||
```
|
||||
Apply the buff, take damage, and watch the thorns buff do its work!
|
||||
|
|
@ -222,21 +229,21 @@ However, there are a lot of individual moving parts to a buff. Here's a step-thr
|
|||
Regardless of any other functionality, all buffs have the following class attributes:
|
||||
|
||||
- They have customizable `key`, `name`, and `flavor` strings.
|
||||
- They have a `duration`, and automatically clean up at the end of it. Use -1 for infinite duration, and 0 to cleanup immediately. (default: -1)
|
||||
- They can stack, if `maxstacks` is not equal to 1. If it's 0, the buff stacks forever. (default: 1)
|
||||
- They can be `unique`, which determines if they have a unique namespace or not. (default: True)
|
||||
- They can `refresh`, which resets the duration when stacked or reapplied. (default: True)
|
||||
- They can be `playtime` buffs, where duration only counts down during active play. (default: False)
|
||||
- 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 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)
|
||||
|
||||
They also always store some useful mutable information about themselves in the cache:
|
||||
|
||||
- The `ref` class, which is the buff class path we use to construct the buff.
|
||||
- The `start` timestamp of when the buff was applied.
|
||||
- Their `source`, if specified; this allows you to track who or what applied the buff.
|
||||
- The `prevtick` timestamp of the previous time this buff ticked.
|
||||
- The current `duration`. This can vary from the class duration, as you might apply buffs with variable durations, or alter them.
|
||||
- The number of `stacks` they have.
|
||||
- Whether they are `paused` or not. Paused buffs do not clean up, modify values, tick, or fire any hook methods.
|
||||
- `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).
|
||||
- `stacks` (int): How many stacks they have.
|
||||
- `paused` (bool): Paused buffs do not clean up, modify values, tick, or fire any hook methods.
|
||||
|
||||
You can always access the raw cache dictionary through the `cache` attribute on an instanced buff. This is grabbed when you get the buff through
|
||||
a handler method, so it may not always reflect recent changes you've made, depending on how you structure your buff calls. All of the above
|
||||
|
|
@ -257,13 +264,13 @@ Mod objects consist of only four values, assigned by the constructor in this ord
|
|||
The most basic way to add a Mod to a buff is to do so in the buff class definition, like this:
|
||||
|
||||
```python
|
||||
def DamageBuff(BaseBuff):
|
||||
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 trait buff. To remove the modification, simply remove the buff off the object.
|
||||
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.
|
||||
|
||||
|
|
@ -272,7 +279,7 @@ never permanently change a stat modified by a trait buff. To remove the modifica
|
|||
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
|
||||
def GeneratedStatBuff(BaseBuff):
|
||||
class GeneratedStatBuff(BaseBuff):
|
||||
...
|
||||
def __init__(self, handler, buffkey, cache={}) -> None:
|
||||
super().__init__(handler, buffkey, cache)
|
||||
|
|
@ -286,12 +293,12 @@ def GeneratedStatBuff(BaseBuff):
|
|||
|
||||
Buffs which have one or more strings in the `triggers` attribute can be triggered by events.
|
||||
|
||||
When the handler `trigger` method is called, it searches all buffs on the handler for any with a matching
|
||||
trigger, then calls their `at_trigger` methods. Buffs can have multiple triggers, and you can tell which trigger was fired by the `trigger`
|
||||
argument in the method.
|
||||
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
|
||||
def AmplifyBuff(BaseBuff):
|
||||
class AmplifyBuff(BaseBuff):
|
||||
triggers = ['damage', 'heal']
|
||||
|
||||
def at_trigger(self, trigger, **kwargs):
|
||||
|
|
@ -301,11 +308,11 @@ def AmplifyBuff(BaseBuff):
|
|||
|
||||
### Ticking
|
||||
|
||||
A buff with ticking isn't much different than one which triggers. You're still executing arbitrary code off
|
||||
the buff class. The main thing is you need to have a `tickrate` higher than 1.
|
||||
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
|
||||
def Poison(BaseBuff):
|
||||
class Poison(BaseBuff):
|
||||
...
|
||||
# this buff will tick 6 times between application and cleanup.
|
||||
duration = 30
|
||||
|
|
@ -313,7 +320,7 @@ def Poison(BaseBuff):
|
|||
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.
|
||||
> **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.
|
||||
|
|
@ -329,10 +336,11 @@ as it returns a "truthy" value, the buff will apply itself. This is useful for m
|
|||
example, if you want a buff that makes the player take more damage when they are on fire:
|
||||
|
||||
```python
|
||||
def FireSick(BaseBuff):
|
||||
class FireSick(BaseBuff):
|
||||
...
|
||||
def conditional(self, *args, **kwargs):
|
||||
if self.owner.buffs.get_by_type(FireBuff): return True
|
||||
if self.owner.buffs.get_by_type(FireBuff):
|
||||
return True
|
||||
return False
|
||||
```
|
||||
|
||||
|
|
@ -341,8 +349,7 @@ conditionals are checked each tick.
|
|||
|
||||
#### Helper Methods
|
||||
|
||||
Buff instances have a number of helper methods you can access either on the buff itself or wherever the buff is instances (typically through
|
||||
handler getters)
|
||||
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`.
|
||||
|
|
@ -355,4 +362,4 @@ and unpause when the object the handler is attached to is puppetted or unpuppett
|
|||
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.
|
||||
> to your object's `at_pre/post_puppet/unpuppet` hooks.
|
||||
|
|
@ -447,7 +447,8 @@ class BuffHandler(object):
|
|||
if source:
|
||||
mix = str(source.dbref).replace("#", "")
|
||||
elif not (buff.unique or buff.refresh) or not source:
|
||||
mix = str(random() * 10000)
|
||||
mix = "_ufrf" + str(int((random() * 999999) * 100000))
|
||||
|
||||
buffkey = buff.key if buff.unique is True else buff.key + mix
|
||||
|
||||
# Rules for applying over an existing buff
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue