Merge branch 'develop' into contrib/evadventure

This commit is contained in:
Griatch 2022-08-16 19:33:29 +02:00
commit f716fc4380
7 changed files with 310 additions and 90 deletions

View file

@ -1,13 +1,13 @@
# Upgrading an existing installation
## Evennia v0.9.5 to 1.0
## Evennia v0.9.5 to 1.0
Prior to 1.0, all Evennia installs were [Git-installs](./Installation-Git.md). These instructions
assume that you have a cloned `evennia` repo and use a virtualenv (best practices).
- Make sure to stop Evennia 0.9.5 entirely with `evennia stop`.
- `deactivate` to leave your active virtualenv.
- Make a _backup_ of your entire `mygame` folder, just to be sure!
- Make a _backup_ of your entire `mygame` folder, just to be sure!
- Delete the old `evenv` folder, or rename it (in case you want to keep using 0.9.5 for a while).
- Install Python 3.10 (recommended) or 3.9. Follow the [Git-installation](./Installation-Git.md) for your OS if needed.
- If using virtualenv, make a _new_ one with `python3.10 -m venv evenv`, then activate with `source evenv/bin/activate`
@ -15,28 +15,31 @@ assume that you have a cloned `evennia` repo and use a virtualenv (best practice
- `cd` into your `evennia/` folder (you want to see the `docs/`, `bin/` directories as well as a nested `evennia/` folder)
- **Prior to 1.0 release only** - do `git checkout develop` to switch to the develop branch. After release, this will
be found on the default master branch.
- `git pull`
- `git pull`
- `pip install -e .`
- If you want the optional extra libs, do `pip install -r requirements_extra.txt`.
- Test that you can run the `evennia` command.
If you don't have anything you want to keep in your existing game dir, you can just start a new onew
using the normal [install instructions](./Installation.md). If you want to keep/convert your existing
If you don't have anything you want to keep in your existing game dir, you can just start a new onew
using the normal [install instructions](./Installation.md). If you want to keep/convert your existing
game dir, continue below.
- First, make a backup of your exising game dir! If you use version control, make sure to commit your current state.
- `cd` to your existing 0.9.5-based game folder (like `mygame`.)
- If you have changed `mygame/web`, _rename_ the folder to `web_0.9.5`. If you didn't change anything (or don't have
- If you have changed `mygame/web`, _rename_ the folder to `web_0.9.5`. If you didn't change anything (or don't have
anything you want to keep), you can _delete_ it entirely.
- Copy `evennia/evennia/game_template/web` to `mygame/` (e.g. using `cp -Rf` or a file manager). This new `web` folder
replaces the old one and has a very different structure.
- `evennia migrate`
- `evennia start`
- It's possible you need to replace/comment out import and calls to the deprecated
[`django.conf.urls`](https://docs.djangoproject.com/en/3.2/ref/urls/#url). The new way to call it is
[available here](https://docs.djangoproject.com/en/4.0/ref/urls/#django.urls.re_path).
- Run `evennia migrate`
- Run `evennia start`
If you made extensive work in your game dir, you may well find that you need to do some (hopefully minor)
changes to your code before it will start with Evennia 1.0. Some important points:
If you made extensive work in your game dir, you may well find that you need to do some (hopefully minor)
changes to your code before it will start with Evennia 1.0. Some important points:
- The `evennia/contrib/` folder changed structure - there are now categorized sub-folders, so you have to update
- The `evennia/contrib/` folder changed structure - there are now categorized sub-folders, so you have to update
your imports.
- Any `web` changes need to be moved back from your backup into the new structure of `web/` manually.
- See the [Evennia 1.0 Changelog](../Coding/Changelog.md) for all changes.
- See the [Evennia 1.0 Changelog](../Coding/Changelog.md) for all changes.

View file

@ -81,6 +81,8 @@ 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.
> **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.
@ -142,6 +144,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 +259,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,24 +279,45 @@ 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)
- 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:
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.
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`).
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
@ -257,9 +327,9 @@ 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)
- `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:
@ -281,8 +351,7 @@ An advanced way to do mods is to generate them when the buff is initialized. Thi
```python
class GeneratedStatBuff(BaseBuff):
...
def __init__(self, handler, buffkey, cache={}) -> None:
super().__init__(handler, buffkey, cache)
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:
@ -339,7 +408,7 @@ example, if you want a buff that makes the player take more damage when they are
class FireSick(BaseBuff):
...
def conditional(self, *args, **kwargs):
if self.owner.buffs.get_by_type(FireBuff):
if self.owner.buffs.has(FireBuff):
return True
return False
```
@ -354,6 +423,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
@ -362,4 +432,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

@ -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
@ -117,15 +116,14 @@ 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?
unique = True # Does the buff overwrite existing buffs with the same key on the same target?
maxstacks = 1 # The maximum number of stacks the buff can have. If >1, this buff will stack.
stacks = 1 # If >1, used as the default when applying this buff
stacks = 1 # Used as the default when applying this buff if no or negative stacks were specified (min: 1)
tickrate = 0 # How frequent does this buff tick, in seconds (cannot be lower than 1)
mods = [] # List of mod objects. See Mod class below for more detail
@ -134,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
@ -144,6 +142,16 @@ class BaseBuff:
return None
return self.handler.owner
@property
def timeleft(self):
"""Returns how much time this buff has left. If -1, it is permanent."""
_tl = 0
if not self.start:
_tl = self.duration
else:
_tl = max(-1, self.duration - (time.time() - self.start))
return _tl
@property
def ticking(self) -> bool:
"""Returns if this buff ticks or not (tickrate => 1)"""
@ -160,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.
@ -222,11 +231,28 @@ class BaseBuff:
def reset(self):
"""Resets the buff start time as though it were just applied; functionally identical to a refresh"""
self.start = time.time()
self.handler.buffcache[self.buffkey]["start"] = time.time()
def update_cache(self, to_cache: dict):
"""Updates this buff's cache using the given values, both internally (this instance) and on the handler.
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])
_cache.update(to_cache)
self.cache = _cache
self.handler.buffcache[self.buffkey] = _cache
# 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
@ -311,6 +337,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)
@ -425,10 +452,14 @@ class BuffHandler:
context = {}
b = {}
_context = dict(context)
# Initial cache updating, starting with the class cache attribute and/or to_cache
if buff.cache:
b = dict(buff.cache)
if to_cache:
b.update(dict(to_cache))
# Guarantees we stack either at least 1 stack or whatever the class stacks attribute is
if stacks < 1:
stacks = min(1, buff.stacks)
@ -438,6 +469,7 @@ class BuffHandler:
"ref": buff,
"start": time.time(),
"duration": buff.duration,
"tickrate": buff.tickrate,
"prevtick": time.time(),
"paused": False,
"stacks": stacks,
@ -749,7 +781,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:
@ -758,6 +792,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
@ -769,15 +804,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():
@ -840,23 +881,25 @@ class BuffHandler:
start = buff["start"] # Start
duration = buff["duration"] # Duration
prevtick = buff["prevtick"] # Previous tick timestamp
tickrate = buff["ref"].tickrate # Buff's tick rate
# Original buff ending, and new duration
tickrate = buff["tickrate"] # Buff's tick rate
end = start + duration # End
newduration = end - current # New duration
# Apply the new duration
if newduration > 0:
buff["duration"] = newduration
if buff["ref"].ticking:
buff["tickleft"] = max(1, tickrate - (current - prevtick))
self.buffcache[key] = buff
instance: BaseBuff = buff["ref"](self, key, buff)
instance.at_pause(**context)
else:
self.remove(key)
return
# Setting "tickleft"
if buff["ref"].ticking:
buff["tickleft"] = max(1, tickrate - (current - prevtick))
# Setting the new duration (if applicable)
if duration > -1:
newduration = end - current # New duration
if newduration > 0:
buff["duration"] = newduration
else:
self.remove(key)
# Apply new cache info, call pause hook
self.buffcache[key] = buff
instance: BaseBuff = buff["ref"](self, key, buff)
instance.at_pause(**context)
def unpause(self, key: str, context=None):
"""Unpauses a buff. This makes it visible to the various buff systems again.
@ -883,33 +926,60 @@ class BuffHandler:
buff["start"] = current
if buff["ref"].ticking:
buff["prevtick"] = current - (tickrate - tickleft)
# Apply new cache info, call hook
self.buffcache[key] = buff
instance: BaseBuff = buff["ref"](self, key, buff)
instance.at_unpause(**context)
utils.delay(buff["duration"], cleanup_buffs, self, persistent=True)
# Set up typical delays (cleanup/ticking)
if instance.duration > -1:
utils.delay(buff["duration"], cleanup_buffs, self, persistent=True)
if instance.ticking:
utils.delay(
tickrate, tick_buff, handler=self, buffkey=key, initial=False, persistent=True
)
return
def set_duration(self, key, value):
"""Sets the duration of the specified buff.
def view(self, to_filter=None) -> dict:
"""Returns a buff flavor text as a dictionary of tuples in the format {key: (name, flavor)}. Common use for this is a buff readout of some kind.
Args:
key: The key of the buff whose duration you want to set
value: The value you want the new duration to be"""
if key in self.buffcache.keys():
self.buffcache[key]["duration"] = value
return
def view(self) -> dict:
"""Returns a buff flavor text as a dictionary of tuples in the format {key: (name, flavor)}. Common use for this is a buff readout of some kind."""
to_filter: (optional) The dictionary of buffs to iterate over. If none is provided, returns all buffs (default: None)"""
if not isinstance(to_filter, dict):
raise TypeError
self.cleanup()
_cache = self.visible
_cache = self.visible if not to_filter else to_filter
_flavor = {k: (buff.name, buff.flavor) for k, buff in _cache.items()}
return _flavor
def view_modifiers(self, stat: str, context=None):
"""Checks all modifiers of the specified stat without actually applying them. Hits the conditional hook for relevant buffs.
Args:
stat: The mod identifier string to search for
context: (optional) A dictionary you wish to pass to the conditional hooks as kwargs
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')."""
# Buff cleanup to make sure all buffs are valid before processing
self.cleanup()
# Find all buffs and traits related to the specified stat.
if not context:
context = {}
applied = self.get_by_stat(stat)
if not applied:
return None
# 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
}
# Calculate and return our values dictionary
calc = self._calculate_mods(stat, applied)
return calc
def cleanup(self):
"""Removes expired buffs, ensures pause state is respected."""
self._validate_state()
@ -946,29 +1016,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):
@ -1065,10 +1164,10 @@ 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:
if (tr > time.time() - buff.prevtick and initial != True) or buff.paused:
return
# Only fire the at_tick methods if the conditional is truthy
@ -1078,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)
@ -1089,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,9 +13,17 @@ from evennia.contrib.rpg.buffs import buff
class _EmptyBuff(BaseBuff):
key = "empty"
pass
class _TestDivBuff(BaseBuff):
key = "tdb"
name = "tdb"
flavor = "divverbuff"
mods = [Mod("stat1", "div", 1)]
class _TestModBuff(BaseBuff):
key = "tmb"
name = "tmb"
@ -190,12 +198,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 +243,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):
@ -347,11 +368,22 @@ class TestBuffsAndHandler(EvenniaTest):
_instance.at_tick()
self.assertTrue(self.testobj.db.ticktest)
# test duration modification and cleanup
handler.set_duration("ttib", 0)
_instance.duration = 0
self.assertEqual(handler.get("ttib").duration, 0)
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"""

View file

@ -1885,7 +1885,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
now in.
Args:
source_location (Object): Wwhere we came from. This may be `None`.
source_location (Object): Where we came from. This may be `None`.
move_type (str): The type of move. "give", "traverse", etc.
This is an arbitrary string provided to obj.move_to().
Useful for altering messages or altering logic depending

View file

@ -202,6 +202,21 @@ class AttributeProperty:
self._lockstring = lockstring
self._autocreate = autocreate
self._key = ""
@property
def _default(self):
"""
Tries returning a new instance of default if callable.
"""
if callable(self.__default):
return self.__default()
return self.__default
@_default.setter
def _default(self, value):
self.__default = value
def __set_name__(self, cls, name):
"""