mirror of
https://github.com/evennia/evennia.git
synced 2026-03-16 21:06:30 +01:00
Merge branch 'develop' into contrib/evadventure
This commit is contained in:
commit
f716fc4380
7 changed files with 310 additions and 90 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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"""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue