readme update to support new cache interface (+3 squashed commit)

Squashed commit:

[872cb2621] added instance-cache link (thanks aogier!)
added tests for cache link
fix poison samplebuff
fix min tickrate

[6bd6ce3e6] fixed infinite-duration ticking buffs

[2dbc0594d] tickrate improvements, init hook
This commit is contained in:
Tegiminis 2022-08-07 18:07:28 -07:00
parent d93f4762a0
commit e77b6d4d74
4 changed files with 63 additions and 36 deletions

View file

@ -283,20 +283,6 @@ Regardless of any other functionality, all buffs have the following class attrib
- 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:
- `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
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
@ -304,6 +290,29 @@ Buffs also have a few useful properties:
- `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 mutable 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.
> **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.
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`).
> **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
@ -418,4 +427,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.

View file

@ -116,9 +116,8 @@ class BaseBuff:
handler = None
start = 0
# Default buff duration; -1 or lower for permanent, 0 for "instant" (removed immediately)
duration = -1
duration = -1 # Default buff duration; -1 for permanent, 0 for "instant", >0 normal
playtime = False # Does this buff autopause when owning object is unpuppeted?
refresh = True # Does the buff refresh its timer on application?
@ -133,7 +132,7 @@ class BaseBuff:
@property
def ticknum(self):
"""Returns how many ticks this buff has gone through as an integer."""
x = (time.time() - self.start) / self.tickrate
x = (time.time() - self.start) / max(1, self.tickrate)
return int(x)
@property
@ -145,12 +144,12 @@ class BaseBuff:
@property
def timeleft(self):
"""Returns how much time this buff has left"""
"""Returns how much time this buff has left. If -1, it is permanent."""
_tl = 0
if not self.start:
_tl = self.duration
else:
_tl = self.duration - (time.time() - self.start)
_tl = max(-1, self.duration - (time.time() - self.start))
return _tl
@property
@ -169,17 +168,18 @@ class BaseBuff:
handler: The handler this buff is attached to
buffkey: The key this buff uses on the cache
cache: The cache dictionary (what you get if you use `handler.buffcache.get(key)`)"""
self.handler: BuffHandler = handler
self.buffkey = buffkey
# Cache assignment
self.cache = cache
# Default system cache values
self.start = self.cache.get("start")
self.duration = self.cache.get("duration")
self.prevtick = self.cache.get("prevtick")
self.paused = self.cache.get("paused")
self.stacks = self.cache.get("stacks")
self.source = self.cache.get("source")
required = {"handler": handler, "buffkey": buffkey, "cache": cache}
self.__dict__.update(cache)
self.__dict__.update(required)
# Init hook
self.at_init()
def __setattr__(self, attr, value):
if attr in self.cache:
if attr == "tickrate":
value = max(0, value)
self.handler.buffcache[self.buffkey][attr] = value
super().__setattr__(attr, value)
def conditional(self, *args, **kwargs):
"""Hook function for conditional evaluation.
@ -249,6 +249,10 @@ class BaseBuff:
# endregion
# region hook methods
def at_init(self, *args, **kwargs):
"""Hook function called when this buff object is initialized."""
pass
def at_apply(self, *args, **kwargs):
"""Hook function to run when this buff is applied to an object."""
pass
@ -461,6 +465,7 @@ class BuffHandler:
"ref": buff,
"start": time.time(),
"duration": buff.duration,
"tickrate": buff.tickrate,
"prevtick": time.time(),
"paused": False,
"stacks": stacks,
@ -1159,7 +1164,7 @@ def tick_buff(handler: BuffHandler, buffkey: str, context=None, initial=True):
# Instantiate the buff and tickrate
buff: BaseBuff = handler.get(buffkey)
tr = buff.tickrate
tr = max(1, buff.tickrate)
# This stops the old ticking process if you refresh/stack the buff
if tr > time.time() - buff.prevtick and initial != True:
@ -1172,7 +1177,7 @@ def tick_buff(handler: BuffHandler, buffkey: str, context=None, initial=True):
buff.at_tick(initial, **context)
# Tick this buff one last time, then remove
if buff.duration <= time.time() - buff.start:
if buff.duration > -1 and buff.duration <= time.time() - buff.start:
if tr < time.time() - buff.prevtick:
buff.at_tick(initial, **context)
buff.remove(expire=True)
@ -1183,6 +1188,7 @@ def tick_buff(handler: BuffHandler, buffkey: str, context=None, initial=True):
buff.at_tick(initial, **context)
handler.buffcache[buffkey]["prevtick"] = time.time()
tr = max(1, buff.tickrate)
# Recur this function at the tickrate interval, if it didn't stop/fail
utils.delay(

View file

@ -84,13 +84,13 @@ class Poison(BaseBuff):
def at_pause(self, *args, **kwargs):
self.owner.db.prelogout_location.msg_contents(
"{actor} stops twitching, their flesh a deathly pallor.".format(actor=self.owner.named)
"{actor} stops twitching, their flesh a deathly pallor.".format(actor=self.owner)
)
def at_unpause(self, *args, **kwargs):
self.owner.location.msg_contents(
"{actor} begins to twitch again, their cheeks flushing red with blood.".format(
actor=self.owner.named
actor=self.owner
)
)
@ -99,7 +99,7 @@ class Poison(BaseBuff):
if not initial:
self.owner.location.msg_contents(
"Poison courses through {actor}'s body, dealing {damage} damage.".format(
actor=self.owner.named, damage=_dmg
actor=self.owner, damage=_dmg
)
)

View file

@ -13,6 +13,7 @@ from evennia.contrib.rpg.buffs import buff
class _EmptyBuff(BaseBuff):
key = "empty"
pass
@ -372,6 +373,17 @@ class TestBuffsAndHandler(EvenniaTest):
handler.cleanup()
self.assertFalse(handler.get("ttib"), None)
@patch("evennia.contrib.rpg.buffs.buff.utils.delay", new=Mock())
def test_cacheattrlink(self):
"""tests the link between the instance attribute and the cache attribute"""
# setup
handler: BuffHandler = self.testobj.buffs
handler.add(_EmptyBuff)
self.assertEqual(handler.buffcache["empty"]["duration"], -1)
empty: _EmptyBuff = handler.get("empty")
empty.duration = 30
self.assertEqual(handler.buffcache["empty"]["duration"], 30)
@patch("evennia.contrib.rpg.buffs.buff.utils.delay", new=Mock())
def test_buffableproperty(self):
"""tests buffable properties"""