mirror of
https://github.com/evennia/evennia.git
synced 2026-03-20 23:06:31 +01:00
added missing div references to readme (+2 squashed commit)
Squashed commit: [b8a311b07] readme and test updates to support new features [70c12b74e] added "div" default modifier, altered how mods are calculated to enable view_modifiers, split modifier application into separate method
This commit is contained in:
parent
673c3e5f03
commit
adea728e25
3 changed files with 137 additions and 20 deletions
|
|
@ -142,6 +142,44 @@ buffs that are reactive to being checked; for example, removing themselves, alte
|
|||
|
||||
> **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`.
|
||||
|
|
@ -219,6 +257,15 @@ class ThornsBuff(BaseBuff):
|
|||
```
|
||||
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.
|
||||
|
|
@ -230,6 +277,7 @@ Regardless of any other functionality, all buffs have the following class attrib
|
|||
|
||||
- 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)
|
||||
|
|
@ -249,6 +297,13 @@ You can always access the raw cache dictionary through the `cache` attribute on
|
|||
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
|
||||
mutable information can be found in this cache, as well as any arbitrary information you pass through the handler `add` method (via `to_cache`).
|
||||
|
||||
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`)
|
||||
|
||||
### 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
|
||||
|
|
@ -257,7 +312,7 @@ mods of a specific stat string and apply their modifications to the value; howev
|
|||
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' and 'mult'. Modifiers are calculated additively, and in standard arithmetic order (see `_calculate_mods` for more)
|
||||
- `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)
|
||||
|
||||
|
|
@ -354,6 +409,7 @@ 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
|
||||
|
||||
|
|
|
|||
|
|
@ -98,7 +98,6 @@ You can see all the features of the `BaseBuff` class below, or browse `samplebuf
|
|||
many attributes and hook methods you can overload to create complex, interrelated buffs.
|
||||
|
||||
"""
|
||||
|
||||
from random import random
|
||||
import time
|
||||
from evennia import Command
|
||||
|
|
@ -235,8 +234,11 @@ class BaseBuff:
|
|||
self.start = time.time()
|
||||
self.handler.buffcache[self.buffkey]["start"] = time.time()
|
||||
|
||||
def alter_cache(self, to_cache: dict = None):
|
||||
"""Alters this buff's cache, both internally (this instance) and on the handler's buff cache."""
|
||||
def alter_cache(self, to_cache: dict):
|
||||
"""Alters this buff's cache, both internally (this instance) and on the handler's buff cache.
|
||||
|
||||
Args:
|
||||
to_cache: The dictionary of values you want to add to the cache"""
|
||||
if not isinstance(to_cache, dict):
|
||||
raise TypeError
|
||||
_cache = dict(self.handler.buffcache[self.buffkey])
|
||||
|
|
@ -331,6 +333,7 @@ class BuffHandler:
|
|||
self.dbkey = dbkey
|
||||
self.autopause = autopause
|
||||
if autopause:
|
||||
self._validate_state()
|
||||
signals.SIGNAL_OBJECT_POST_UNPUPPET.connect(self._pause_playtime)
|
||||
signals.SIGNAL_OBJECT_POST_PUPPET.connect(self._unpause_playtime)
|
||||
|
||||
|
|
@ -769,7 +772,9 @@ class BuffHandler:
|
|||
return True
|
||||
return False
|
||||
|
||||
def check(self, value: float, stat: str, loud=True, context=None, trigger=False):
|
||||
def check(
|
||||
self, value: float, stat: str, loud=True, context=None, trigger=False, strongest=False
|
||||
):
|
||||
"""Finds all buffs and perks related to a stat and applies their effects.
|
||||
|
||||
Args:
|
||||
|
|
@ -778,6 +783,7 @@ class BuffHandler:
|
|||
loud: (optional) Call the buff's at_post_check method after checking (default: True)
|
||||
context: (optional) A dictionary you wish to pass to the at_pre_check/at_post_check and conditional methods as kwargs
|
||||
trigger: (optional) Trigger buffs with the `stat` string as well. (default: False)
|
||||
strongest: (optional) Applies only the strongest mods of the corresponding stat value (default: False)
|
||||
|
||||
Returns the value modified by relevant buffs."""
|
||||
# Buff cleanup to make sure all buffs are valid before processing
|
||||
|
|
@ -789,15 +795,21 @@ class BuffHandler:
|
|||
applied = self.get_by_stat(stat)
|
||||
if not applied:
|
||||
return value
|
||||
|
||||
# Run pre-check hooks on related buffs
|
||||
for buff in applied.values():
|
||||
buff.at_pre_check(**context)
|
||||
|
||||
# Sift out buffs that won't be applying their mods (paused, conditional)
|
||||
applied = {
|
||||
k: buff for k, buff in applied.items() if buff.conditional(**context) if not buff.paused
|
||||
}
|
||||
|
||||
# The final result
|
||||
final = self._calculate_mods(value, stat, applied)
|
||||
# The mod totals
|
||||
calc = self._calculate_mods(stat, applied)
|
||||
|
||||
# The calculated final value
|
||||
final = self._apply_mods(value, calc, strongest=strongest)
|
||||
|
||||
# Run the "after check" functions on all relevant buffs
|
||||
for buff in applied.values():
|
||||
|
|
@ -999,29 +1011,58 @@ class BuffHandler:
|
|||
buff.unpause()
|
||||
pass
|
||||
|
||||
def _calculate_mods(self, value, stat: str, buffs: dict):
|
||||
"""Calculates a return value from a base value, a stat string, and a dictionary of instanced buffs with associated mods.
|
||||
def _calculate_mods(self, stat: str, buffs: dict):
|
||||
"""Calculates the total value of applicable mods.
|
||||
|
||||
Args:
|
||||
value: The base value to modify
|
||||
stat: The string identifier to search mods for
|
||||
buffs: The dictionary of buffs to apply"""
|
||||
buffs: The dictionary of buffs to calculate mods from
|
||||
|
||||
Returns a nested dictionary. The first layer's keys represent the type of modifier ('add' and 'mult'),
|
||||
and the second layer's keys represent the type of value ('total' and 'strongest')."""
|
||||
|
||||
# The base return dictionary. If you update how modifiers are calculated, make sure to update this too, or you will get key errors!
|
||||
calculated = {
|
||||
"add": {"total": 0, "strongest": 0},
|
||||
"mult": {"total": 0, "strongest": 0},
|
||||
"div": {"total": 0, "strongest": 0},
|
||||
}
|
||||
if not buffs:
|
||||
return value
|
||||
add = 0
|
||||
mult = 0
|
||||
return calculated
|
||||
|
||||
for buff in buffs.values():
|
||||
for mod in buff.mods:
|
||||
buff: BaseBuff
|
||||
mod: Mod
|
||||
if mod.stat == stat:
|
||||
if mod.modifier == "add":
|
||||
add += mod.value + ((buff.stacks) * mod.perstack)
|
||||
if mod.modifier == "mult":
|
||||
mult += mod.value + ((buff.stacks) * mod.perstack)
|
||||
_modval = mod.value + ((buff.stacks) * mod.perstack)
|
||||
calculated[mod.modifier]["total"] += _modval
|
||||
if _modval > calculated[mod.modifier]["strongest"]:
|
||||
calculated[mod.modifier]["strongest"] = _modval
|
||||
return calculated
|
||||
|
||||
final = (value + add) * max(0, 1.0 + mult)
|
||||
def _apply_mods(self, value, calc: dict, strongest=False):
|
||||
"""Applies modifiers to a value.
|
||||
|
||||
Args:
|
||||
value: The value to modify
|
||||
calc: The dictionary of calculated modifier values (see _calculate_mods)
|
||||
strongest: (optional) Applies only the strongest mods of the corresponding stat value (default: False)
|
||||
|
||||
Returns value modified by the relevant mods."""
|
||||
final = value
|
||||
if strongest:
|
||||
final = (
|
||||
(value + calc["add"]["strongest"])
|
||||
/ max(1, 1.0 + calc["div"]["strongest"])
|
||||
* max(0, 1.0 + calc["mult"]["strongest"])
|
||||
)
|
||||
else:
|
||||
final = (
|
||||
(value + calc["add"]["total"])
|
||||
/ max(1, 1.0 + calc["div"]["total"])
|
||||
* max(0, 1.0 + calc["mult"]["total"])
|
||||
)
|
||||
return final
|
||||
|
||||
def _remove_via_dict(self, buffs: dict, loud=True, dispel=False, expire=False, context=None):
|
||||
|
|
|
|||
|
|
@ -16,6 +16,13 @@ class _EmptyBuff(BaseBuff):
|
|||
pass
|
||||
|
||||
|
||||
class _TestDivBuff(BaseBuff):
|
||||
key = "tdb"
|
||||
name = "tdb"
|
||||
flavor = "divverbuff"
|
||||
mods = [Mod("stat1", "div", 1)]
|
||||
|
||||
|
||||
class _TestModBuff(BaseBuff):
|
||||
key = "tmb"
|
||||
name = "tmb"
|
||||
|
|
@ -190,12 +197,19 @@ class TestBuffsAndHandler(EvenniaTest):
|
|||
|
||||
@patch("evennia.contrib.rpg.buffs.buff.utils.delay", new=Mock())
|
||||
def test_details(self):
|
||||
"""tests that buff details like name and flavor are correct"""
|
||||
"""tests that buff details like name and flavor are correct; also test modifier viewing"""
|
||||
handler: BuffHandler = self.testobj.buffs
|
||||
handler.add(_TestModBuff)
|
||||
handler.add(_TestTrigBuff)
|
||||
self.assertEqual(handler.get("tmb").flavor, "modderbuff")
|
||||
self.assertEqual(handler.get("ttb").name, "ttb")
|
||||
mods = handler.view_modifiers("stat1")
|
||||
_testmods = {
|
||||
"add": {"total": 15, "strongest": 15},
|
||||
"mult": {"total": 0, "strongest": 0},
|
||||
"div": {"total": 0, "strongest": 0},
|
||||
}
|
||||
self.assertDictEqual(mods, _testmods)
|
||||
|
||||
@patch("evennia.contrib.rpg.buffs.buff.utils.delay", new=Mock())
|
||||
def test_modify(self):
|
||||
|
|
@ -228,9 +242,15 @@ class TestBuffsAndHandler(EvenniaTest):
|
|||
handler.add(_TestModBuff2)
|
||||
self.assertEqual(handler.check(_stat1, "stat1"), 100)
|
||||
self.assertEqual(handler.check(_stat2, "stat2"), 30)
|
||||
# apply only the strongest value
|
||||
self.assertEqual(handler.check(_stat1, "stat1", strongest=True), 80)
|
||||
# removing mod properly reduces value, doesn't affect other mods
|
||||
handler.remove_by_type(_TestModBuff)
|
||||
self.assertEqual(handler.check(_stat1, "stat1"), 30)
|
||||
self.assertEqual(handler.check(_stat2, "stat2"), 20)
|
||||
# divider mod test
|
||||
handler.add(_TestDivBuff)
|
||||
self.assertEqual(handler.check(_stat1, "stat1"), 15)
|
||||
|
||||
@patch("evennia.contrib.rpg.buffs.buff.utils.delay", new=Mock())
|
||||
def test_trigger(self):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue