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 `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 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: Buffs also have a few useful properties:
- `owner`: The object this buff is attached to - `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 - `timeleft`: How much time is remaining on the buff
- `ticking`/`stacking`: If this buff ticks/stacks (checks `tickrate` and `maxstacks`) - `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 ### 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 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

View file

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

View file

@ -84,13 +84,13 @@ class Poison(BaseBuff):
def at_pause(self, *args, **kwargs): def at_pause(self, *args, **kwargs):
self.owner.db.prelogout_location.msg_contents( 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): def at_unpause(self, *args, **kwargs):
self.owner.location.msg_contents( self.owner.location.msg_contents(
"{actor} begins to twitch again, their cheeks flushing red with blood.".format( "{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: if not initial:
self.owner.location.msg_contents( self.owner.location.msg_contents(
"Poison courses through {actor}'s body, dealing {damage} damage.".format( "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): class _EmptyBuff(BaseBuff):
key = "empty"
pass pass
@ -372,6 +373,17 @@ class TestBuffsAndHandler(EvenniaTest):
handler.cleanup() handler.cleanup()
self.assertFalse(handler.get("ttib"), None) 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()) @patch("evennia.contrib.rpg.buffs.buff.utils.delay", new=Mock())
def test_buffableproperty(self): def test_buffableproperty(self):
"""tests buffable properties""" """tests buffable properties"""