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
e1439104e0
14 changed files with 2134 additions and 31 deletions
|
|
@ -30,9 +30,10 @@ jobs:
|
|||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: pip
|
||||
|
||||
- name: Install doc-building dependencies
|
||||
run: |
|
||||
|
|
|
|||
22
.github/workflows/github_action_test_suite.yml
vendored
22
.github/workflows/github_action_test_suite.yml
vendored
|
|
@ -92,9 +92,13 @@ jobs:
|
|||
run: docker ps -a
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: pip
|
||||
cache-dependency-path: |
|
||||
requirements.txt
|
||||
requirements_extra.txt
|
||||
|
||||
- name: Install package dependencies
|
||||
run: |
|
||||
|
|
@ -118,9 +122,10 @@ jobs:
|
|||
evennia migrate
|
||||
evennia collectstatic --noinput
|
||||
|
||||
- name: Run test suite
|
||||
- name: Run test suite with coverage
|
||||
if: matrix.TESTING_DB == 'sqlite3' && matrix.python-version == '3.10'
|
||||
working-directory: testing_mygame
|
||||
run: |
|
||||
cd testing_mygame
|
||||
coverage run \
|
||||
--source=../evennia \
|
||||
--omit=*/migrations/*,*/urls.py,*/test*.py,*.sh,*.txt,*.md,*.pyc,*.service \
|
||||
|
|
@ -132,6 +137,17 @@ jobs:
|
|||
evennia
|
||||
coverage xml
|
||||
|
||||
- name: Run test suite
|
||||
if: matrix.TESTING_DB != 'sqlite3' || matrix.python-version != '3.10'
|
||||
working-directory: testing_mygame
|
||||
run: |
|
||||
evennia test \
|
||||
--settings=settings \
|
||||
--keepdb \
|
||||
--parallel 4 \
|
||||
--timing \
|
||||
evennia
|
||||
|
||||
# we only want to run coverall/codacy once, so we only do it for one of the matrix combinations
|
||||
# it's also not critical if pushing to either service fails (happens for PRs since env is not
|
||||
# available outside of the evennia org)
|
||||
|
|
|
|||
|
|
@ -189,6 +189,9 @@ Up requirements to Django 4.0+, Twisted 22+, Python 3.9 or 3.10
|
|||
to better match how search works elsewhere (volund)
|
||||
- The `.at_traverse` hook now receives a `exit_obj` kwarg, linking back to the
|
||||
exit triggering the hook (volund)
|
||||
- Contrib `buffs` for managing temporary and permanent RPG status buffs effects (tegiminis)
|
||||
- New `at_server_init()` hook called before all other startup hooks for all
|
||||
startup modes. Used for more generic overriding (volund)
|
||||
|
||||
|
||||
## Evennia 0.9.5
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@ COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
|
|||
_FUNCPARSER = None
|
||||
_ATTRFUNCPARSER = None
|
||||
|
||||
_KEY_REGEX = re.compile(r"(?P<attr>.*?)(?P<key>(\[.*\]\ *)+)?$")
|
||||
|
||||
# limit symbol import for API
|
||||
__all__ = (
|
||||
"ObjManipCommand",
|
||||
|
|
@ -126,7 +128,28 @@ class ObjManipCommand(COMMAND_DEFAULT_CLASS):
|
|||
aliases = [alias.strip() for alias in aliases.split(";") if alias.strip()]
|
||||
if "/" in objdef:
|
||||
objdef, attrs = [part.strip() for part in objdef.split("/", 1)]
|
||||
attrs = [part.strip().lower() for part in attrs.split("/") if part.strip()]
|
||||
_attrs = []
|
||||
|
||||
# Should an attribute key is specified, ie. we're working
|
||||
# on a dict, what we want is to lowercase attribute name
|
||||
# as usual but to preserve dict key case as one would
|
||||
# expect:
|
||||
#
|
||||
# set box/MyAttr = {'FooBar': 1}
|
||||
# Created attribute box/myattr [category:None] = {'FooBar': 1}
|
||||
# set box/MyAttr['FooBar'] = 2
|
||||
# Modified attribute box/myattr [category:None] = {'FooBar': 2}
|
||||
for match in (
|
||||
match
|
||||
for part in map(str.strip, attrs.split("/"))
|
||||
if part and (match := _KEY_REGEX.match(part.strip()))
|
||||
):
|
||||
attr = match.group("attr").lower()
|
||||
# reappend untouched key, if present
|
||||
if match.group("key"):
|
||||
attr += match.group("key")
|
||||
_attrs.append(attr)
|
||||
attrs = _attrs
|
||||
# store data
|
||||
obj_defs[iside].append({"name": objdef, "option": option, "aliases": aliases})
|
||||
obj_attrs[iside].append({"name": objdef, "attrs": attrs})
|
||||
|
|
|
|||
|
|
@ -957,6 +957,39 @@ class TestBuilding(BaseEvenniaCommandTest):
|
|||
"{'one': 99, 'three': 3, '+': 42, '+1': 33}",
|
||||
)
|
||||
|
||||
# dict - case sensitive keys
|
||||
|
||||
self.call(
|
||||
building.CmdSetAttribute(),
|
||||
"Obj/test_case = {'FooBar': 1}",
|
||||
"Created attribute Obj/test_case [category:None] = {'FooBar': 1}",
|
||||
)
|
||||
self.call(
|
||||
building.CmdSetAttribute(),
|
||||
"Obj/test_case['FooBar'] = 2",
|
||||
"Modified attribute Obj/test_case [category:None] = {'FooBar': 2}",
|
||||
)
|
||||
self.call(
|
||||
building.CmdSetAttribute(),
|
||||
"Obj/test_case",
|
||||
"Attribute Obj/test_case [category:None] = {'FooBar': 2}",
|
||||
)
|
||||
self.call(
|
||||
building.CmdSetAttribute(),
|
||||
"Obj/test_case['FooBar'] = {'BarBaz': 1}",
|
||||
"Modified attribute Obj/test_case [category:None] = {'FooBar': {'BarBaz': 1}}",
|
||||
)
|
||||
self.call(
|
||||
building.CmdSetAttribute(),
|
||||
"Obj/test_case['FooBar']['BarBaz'] = 2",
|
||||
"Modified attribute Obj/test_case [category:None] = {'FooBar': {'BarBaz': 2}}",
|
||||
)
|
||||
self.call(
|
||||
building.CmdSetAttribute(),
|
||||
"Obj/test_case",
|
||||
"Attribute Obj/test_case [category:None] = {'FooBar': {'BarBaz': 2}}",
|
||||
)
|
||||
|
||||
# tuple
|
||||
self.call(
|
||||
building.CmdSetAttribute(),
|
||||
|
|
|
|||
|
|
@ -8,14 +8,14 @@ the commands with XYZ-aware equivalents.
|
|||
"""
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
from django.conf import settings
|
||||
from evennia import InterruptCommand
|
||||
from evennia import default_cmds, CmdSet
|
||||
from evennia import CmdSet, InterruptCommand, default_cmds
|
||||
from evennia.commands.default import building
|
||||
from evennia.contrib.grid.xyzgrid.xyzroom import XYZRoom
|
||||
from evennia.contrib.grid.xyzgrid.xyzgrid import get_xyzgrid
|
||||
from evennia.contrib.grid.xyzgrid.xyzroom import XYZRoom
|
||||
from evennia.utils import ansi
|
||||
from evennia.utils.utils import list_to_string, class_from_module, delay
|
||||
from evennia.utils.utils import class_from_module, delay, list_to_string
|
||||
|
||||
COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
|
||||
|
||||
|
|
@ -149,7 +149,8 @@ class CmdXYZOpen(building.CmdOpen):
|
|||
|
||||
if all(char in self.rhs for char in ("(", ")", ",")):
|
||||
# search by (X,Y) or (X,Y,Z)
|
||||
X, Y, *Z = self.rhs.split(",", 2)
|
||||
inp = self.rhs.strip("()")
|
||||
X, Y, *Z = inp.split(",", 2)
|
||||
if not Z:
|
||||
self.caller.msg("A full (X,Y,Z) coordinate must be given for the destination.")
|
||||
raise InterruptCommand
|
||||
|
|
@ -159,7 +160,7 @@ class CmdXYZOpen(building.CmdOpen):
|
|||
try:
|
||||
self.destination = XYZRoom.objects.get_xyz(xyz=(X, Y, Z))
|
||||
except XYZRoom.DoesNotExist:
|
||||
self.caller.msg("Found no target XYZRoom at ({X},{Y},{Y}).")
|
||||
self.caller.msg(f"Found no target XYZRoom at ({X},{Y},{Z}).")
|
||||
raise InterruptCommand
|
||||
else:
|
||||
# regular search query
|
||||
|
|
|
|||
365
evennia/contrib/rpg/buffs/README.md
Normal file
365
evennia/contrib/rpg/buffs/README.md
Normal file
|
|
@ -0,0 +1,365 @@
|
|||
# Buffs
|
||||
|
||||
Contribution by Tegiminis 2022
|
||||
|
||||
A buff is a timed object, attached to a game entity. It is capable of modifying values, triggering code, or both.
|
||||
It is a common design pattern in RPGs, particularly action games.
|
||||
|
||||
Features:
|
||||
|
||||
- `BuffHandler`: A buff handler to apply to your objects.
|
||||
- `BaseBuff`: A buff class to extend from to create your own buffs.
|
||||
- `BuffableProperty`: A sample property class to show how to automatically check modifiers.
|
||||
- `CmdBuff`: A command which applies buffs.
|
||||
- `samplebuffs.py`: Some sample buffs to learn from.
|
||||
|
||||
## Quick Start
|
||||
Assign the handler to a property on the object, like so.
|
||||
|
||||
```python
|
||||
@lazy_property
|
||||
def buffs(self) -> BuffHandler:
|
||||
return BuffHandler(self)
|
||||
```
|
||||
|
||||
You may then call the handler to add or manipulate buffs like so: `object.buffs`. See **Using the Handler**.
|
||||
|
||||
### Customization
|
||||
|
||||
If you want to customize the handler, you can feed the constructor two arguments:
|
||||
- `dbkey`: The string you wish to use as the attribute key for the buff database. Defaults to "buffs". This allows you to keep separate buff pools - for example, "buffs" and "perks".
|
||||
- `autopause`: If you want this handler to automatically pause playtime buffs when its owning object is unpuppeted.
|
||||
|
||||
> **Note**: If you enable autopausing, you MUST initialize the property in your owning object's
|
||||
> `at_init` hook. Otherwise, a hot reload can cause playtime buffs to not update properly
|
||||
> on puppet/unpuppet. You have been warned!
|
||||
|
||||
Let's say you want another handler for an object, `perks`, which has a separate database and
|
||||
respects playtime buffs. You'd assign this new property as so:
|
||||
|
||||
```python
|
||||
class BuffableObject(Object):
|
||||
@lazy_property
|
||||
def perks(self) -> BuffHandler:
|
||||
return BuffHandler(self, dbkey='perks', autopause=True)
|
||||
|
||||
def at_init(self):
|
||||
self.perks
|
||||
```
|
||||
|
||||
## Using the Handler
|
||||
|
||||
Here's how to make use of your new handler.
|
||||
|
||||
### Apply a Buff
|
||||
|
||||
Call the handler's `add` method. This requires a class reference, and also contains a number of
|
||||
optional arguments to customize the buff's duration, stacks, and so on. You can also store any arbitrary value
|
||||
in the buff's cache by passing a dictionary through the `to_cache` optional argument. This will not overwrite the normal
|
||||
values on the cache.
|
||||
|
||||
```python
|
||||
self.buffs.add(StrengthBuff) # A single stack of StrengthBuff with normal duration
|
||||
self.buffs.add(DexBuff, stacks=3, duration=60) # Three stacks of DexBuff, with a duration of 60 seconds
|
||||
self.buffs.add(ReflectBuff, to_cache={'reflect': 0.5}) # A single stack of ReflectBuff, with an extra cache value
|
||||
```
|
||||
|
||||
Two important attributes on the buff are checked when the buff is applied: `refresh` and `unique`.
|
||||
- `refresh` (default: True) determines if a buff's timer is refreshed when it is reapplied.
|
||||
- `unique` (default: True) determines if this buff is unique; that is, only one of it exists on the object.
|
||||
|
||||
The combination of these two booleans creates one of three kinds of keys:
|
||||
- `Unique is True, Refresh is True/False`: The buff's default key.
|
||||
- `Unique is False, Refresh is True`: The default key mixed with the applier's dbref. This makes the buff "unique-per-player", so you can refresh through reapplication.
|
||||
- `Unique is False, Refresh is False`: The default key mixed with a randomized number.
|
||||
|
||||
### Get Buffs
|
||||
|
||||
The handler has several getter methods which return instanced buffs. You won't need to use these for basic functionality, but if you want to manipulate
|
||||
buffs after application, they are very useful. The handler's `check`/`trigger` methods utilize some of these getters, while others are just for developer convenience.
|
||||
|
||||
`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.
|
||||
|
||||
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.
|
||||
|
||||
- `get_all()` returns all buffs on this handler. You can also use the `handler.all` property.
|
||||
- `get_by_type(BuffClass)` returns buffs of the specified type.
|
||||
- `get_by_stat(stat)` returns buffs with a `Mod` object of the specified `stat` string in their `mods` list.
|
||||
- `get_by_trigger(string)` returns buffs with the specified string in their `triggers` list.
|
||||
- `get_by_source(Object)` returns buffs applied by the specified `source` object.
|
||||
- `get_by_cachevalue(key, value)` returns buffs with the matching `key: value` pair in their cache. `value` is optional.
|
||||
|
||||
All group getters besides `get_all()` can "slice" an existing dictionary through the optional `to_filter` argument.
|
||||
|
||||
```python
|
||||
dict1 = handler.get_by_type(Burned) # This finds all "Burned" buffs on the handler
|
||||
dict2 = handler.get_by_source(self, to_filter=dict1) # This filters dict1 to find buffs with the matching source
|
||||
```
|
||||
|
||||
> **Note**: Most of these getters also have an associated handler property. For example, `handler.effects` returns all buffs that can be triggered, which
|
||||
> is then iterated over by the `get_by_trigger` method.
|
||||
|
||||
### Remove Buffs
|
||||
|
||||
There are also a number of remover methods. Generally speaking, these follow the same format as the getters.
|
||||
|
||||
- `remove(key)` removes the buff with the specified key.
|
||||
- `clear()` removes all buffs.
|
||||
- `remove_by_type(BuffClass)` removes buffs of the specified type.
|
||||
- `remove_by_stat(stat)` removes buffs with a `Mod` object of the specified `stat` string in their `mods` list.
|
||||
- `remove_by_trigger(string)` removes buffs with the specified string in their `triggers` list.
|
||||
- `remove_by_source(Object)` removes buffs applied by the specified source
|
||||
- `remove_by_cachevalue(key, value)` removes buffs with the matching `key: value` pair in their cache. `value` is optional.
|
||||
|
||||
You can also remove a buff by calling the instance's `remove` helper method. You can do this on the dictionaries returned by the
|
||||
getters listed above.
|
||||
|
||||
```python
|
||||
to_remove = handler.get_by_trigger(trigger) # Finds all buffs with the specified trigger
|
||||
for buff in to_remove.values(): # Removes all buffs in the to_remove dictionary via helper methods
|
||||
buff.remove()
|
||||
```
|
||||
|
||||
### Check Modifiers
|
||||
|
||||
Call the handler `check(value, stat)` method when you want to see the modified value.
|
||||
This will return the `value`, modified by any relevant buffs on the handler's owner (identified by
|
||||
the `stat` string).
|
||||
|
||||
For example, let's say you want to modify how much damage you take. That might look something like this:
|
||||
|
||||
```python
|
||||
# The method we call to damage ourselves
|
||||
def take_damage(self, source, damage):
|
||||
_damage = self.buffs.check(damage, 'taken_damage')
|
||||
self.db.health -= _damage
|
||||
```
|
||||
|
||||
This method calls the `at_pre_check` and `at_post_check` methods at the relevant points in the process. You can use to this make
|
||||
buffs that are reactive to being checked; for example, removing themselves, altering their values, or interacting with the game state.
|
||||
|
||||
> **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.
|
||||
|
||||
### 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`.
|
||||
|
||||
For example, let's say you want to trigger a buff to "detonate" when you hit your target with an attack.
|
||||
You'd write a buff that might look like this:
|
||||
|
||||
```python
|
||||
class Detonate(BaseBuff):
|
||||
...
|
||||
triggers = ['take_damage']
|
||||
def at_trigger(self, trigger, *args, **kwargs)
|
||||
self.owner.take_damage(100)
|
||||
self.remove()
|
||||
```
|
||||
|
||||
And then call `handler.trigger('take_damage')` in the method you use to take damage.
|
||||
|
||||
> **Note** You could also do this through mods and `at_post_check` if you like, depending on how to want to add the damage.
|
||||
|
||||
### Ticking
|
||||
|
||||
Ticking buffs are slightly special. They are similar to trigger buffs in that they run code, but instead of
|
||||
doing so on an event trigger, they do so on a periodic tick. A common use case for a buff like this is a poison,
|
||||
or a heal over time.
|
||||
|
||||
```python
|
||||
class Poison(BaseBuff):
|
||||
...
|
||||
tickrate = 5
|
||||
def at_tick(self, initial=True, *args, **kwargs):
|
||||
_dmg = self.dmg * self.stacks
|
||||
if not initial:
|
||||
self.owner.location.msg_contents(
|
||||
"Poison courses through {actor}'s body, dealing {damage} damage.".format(
|
||||
actor=self.owner.named, damage=_dmg
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
To make a buff ticking, ensure the `tickrate` is 1 or higher, and it has code in its `at_tick`
|
||||
method. Once you add it to the handler, it starts ticking!
|
||||
|
||||
> **Note**: Ticking buffs always tick on initial application, when `initial` is `True`. If you don't want your hook to fire at that time,
|
||||
> make sure to check the value of `initial` in your `at_tick` method.
|
||||
|
||||
### Context
|
||||
|
||||
Every important handler method optionally accepts a `context` dictionary.
|
||||
|
||||
Context is an important concept for this handler. Every method which checks, triggers, or ticks a buff passes this
|
||||
dictionary (default: empty) to the buff hook methods as keyword arguments (`**kwargs`). It is used for nothing else. This allows you to make those
|
||||
methods "event-aware" by storing relevant data in the dictionary you feed to the method.
|
||||
|
||||
For example, let's say you want a "thorns" buff which damages enemies that attack you. Let's take our `take_damage` method
|
||||
and add a context to the mix.
|
||||
|
||||
```python
|
||||
def take_damage(attacker, damage):
|
||||
context = {'attacker': attacker, 'damage': damage}
|
||||
_damage = self.buffs.check(damage, 'taken_damage', context=context)
|
||||
self.buffs.trigger('taken_damage', context=context)
|
||||
self.db.health -= _damage
|
||||
```
|
||||
Now we use the values that context passes to the buff kwargs to customize our logic.
|
||||
```python
|
||||
class ThornsBuff(BaseBuff):
|
||||
...
|
||||
triggers = ['taken_damage']
|
||||
# This is the hook method on our thorns buff
|
||||
def at_trigger(self, trigger, attacker=None, damage=0, **kwargs):
|
||||
if not attacker:
|
||||
return
|
||||
attacker.db.health -= damage * 0.2
|
||||
```
|
||||
Apply the buff, take damage, and watch the thorns buff do its work!
|
||||
|
||||
## Creating New Buffs
|
||||
|
||||
Creating a new buff is very easy: extend `BaseBuff` into a new class, and fill in all the relevant buff details.
|
||||
However, there are a lot of individual moving parts to a buff. Here's a step-through of the important stuff.
|
||||
|
||||
### Basics
|
||||
|
||||
Regardless of any other functionality, all buffs have the following class attributes:
|
||||
|
||||
- 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 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:
|
||||
|
||||
- `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`).
|
||||
|
||||
### 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 of a specific stat string and apply their modifications to the value; however, you are encouraged to use `check` in a getter/setter, for easy access.
|
||||
|
||||
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)
|
||||
- `value`: How much value the modifier gives regardless of stacks
|
||||
- `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:
|
||||
|
||||
```python
|
||||
class DamageBuff(BaseBuff):
|
||||
mods = [Mod('damage', 'add', 10)]
|
||||
```
|
||||
|
||||
No mods applied to the value are permanent in any way. All calculations are done at runtime, and the mod values are never stored
|
||||
anywhere except on the buff in question. In other words: you don't need to track the origin of particular stat mods, and you will
|
||||
never permanently change a stat modified by a buff. To remove the modification, simply remove the buff from the object.
|
||||
|
||||
> **Note**: You can add your own modifier types by overloading the `_calculate_mods` method, which contains the basic modifier application logic.
|
||||
|
||||
#### Generating Mods (Advanced)
|
||||
|
||||
An advanced way to do mods is to generate them when the buff is initialized. This lets you create mods on the fly that are reactive to the game state.
|
||||
|
||||
```python
|
||||
class GeneratedStatBuff(BaseBuff):
|
||||
...
|
||||
def __init__(self, handler, buffkey, cache={}) -> None:
|
||||
super().__init__(handler, buffkey, cache)
|
||||
# Finds our "modgen" cache value, and generates a mod from it
|
||||
modgen = list(self.cache.get("modgen"))
|
||||
if modgen:
|
||||
self.mods = [Mod(*modgen)]
|
||||
```
|
||||
|
||||
### Triggers
|
||||
|
||||
Buffs which have one or more strings in the `triggers` attribute can be triggered by events.
|
||||
|
||||
When the handler's `trigger` method is called, it searches all buffs on the handler for any with a matchingtrigger,
|
||||
then calls their `at_trigger` hooks. Buffs can have multiple triggers, and you can tell which trigger was used by
|
||||
the `trigger` argument in the hook.
|
||||
|
||||
```python
|
||||
class AmplifyBuff(BaseBuff):
|
||||
triggers = ['damage', 'heal']
|
||||
|
||||
def at_trigger(self, trigger, **kwargs):
|
||||
if trigger == 'damage': print('Damage trigger called!')
|
||||
if trigger == 'heal': print('Heal trigger called!')
|
||||
```
|
||||
|
||||
### Ticking
|
||||
|
||||
A buff which ticks isn't much different than one which triggers. You're still executing arbitrary hooks on
|
||||
the buff class. To tick, the buff must have a `tickrate` of 1 or higher.
|
||||
|
||||
```python
|
||||
class Poison(BaseBuff):
|
||||
...
|
||||
# this buff will tick 6 times between application and cleanup.
|
||||
duration = 30
|
||||
tickrate = 5
|
||||
def at_tick(self, initial, **kwargs):
|
||||
self.owner.take_damage(10)
|
||||
```
|
||||
> **Note**: The buff always ticks once when applied. For this **first tick only**, `initial` will be True in the `at_tick` hook method. `initial` will be False on subsequent ticks.
|
||||
|
||||
Ticks utilize a persistent delay, so they should be pickleable. As long as you are not adding new properties to your buff class, this shouldn't be a concern.
|
||||
If you **are** adding new properties, try to ensure they do not end up with a circular code path to their object or handler, as this will cause pickling errors.
|
||||
|
||||
### Extras
|
||||
|
||||
Buffs have a grab-bag of extra functionality to let you add complexity to your designs.
|
||||
|
||||
#### Conditionals
|
||||
|
||||
You can restrict whether or not the buff will `check`, `trigger`, or `tick` through defining the `conditional` hook. As long
|
||||
as it returns a "truthy" value, the buff will apply itself. This is useful for making buffs dependent on game state - for
|
||||
example, if you want a buff that makes the player take more damage when they are on fire:
|
||||
|
||||
```python
|
||||
class FireSick(BaseBuff):
|
||||
...
|
||||
def conditional(self, *args, **kwargs):
|
||||
if self.owner.buffs.get_by_type(FireBuff):
|
||||
return True
|
||||
return False
|
||||
```
|
||||
|
||||
Conditionals for `check`/`trigger` are checked when the buffs are gathered by the handler methods for the respective operations. `Tick`
|
||||
conditionals are checked each tick.
|
||||
|
||||
#### Helper Methods
|
||||
|
||||
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.
|
||||
|
||||
#### Playtime Duration
|
||||
|
||||
If your handler has `autopause` enabled, any buffs with truthy `playtime` value will automatically pause
|
||||
and unpause when the object the handler is attached to is puppetted or unpuppetted. This even works with ticking buffs,
|
||||
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.
|
||||
0
evennia/contrib/rpg/buffs/__init__.py
Normal file
0
evennia/contrib/rpg/buffs/__init__.py
Normal file
1102
evennia/contrib/rpg/buffs/buff.py
Normal file
1102
evennia/contrib/rpg/buffs/buff.py
Normal file
File diff suppressed because it is too large
Load diff
141
evennia/contrib/rpg/buffs/samplebuffs.py
Normal file
141
evennia/contrib/rpg/buffs/samplebuffs.py
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
import random
|
||||
from .buff import BaseBuff, Mod
|
||||
|
||||
|
||||
class Exploit(BaseBuff):
|
||||
key = "exploit"
|
||||
name = "Exploit"
|
||||
flavor = "You are learning your opponent's weaknesses."
|
||||
|
||||
duration = -1
|
||||
maxstacks = 20
|
||||
|
||||
triggers = ["hit"]
|
||||
|
||||
stack_msg = {
|
||||
1: "You begin to notice flaws in your opponent's defense.",
|
||||
10: "You've begun to match the battle's rhythm.",
|
||||
20: "You've found a gap in the guard!",
|
||||
}
|
||||
|
||||
def conditional(self, *args, **kwargs):
|
||||
if self.handler.get_by_type(Exploited):
|
||||
return False
|
||||
return True
|
||||
|
||||
def at_trigger(self, trigger: str, *args, **kwargs):
|
||||
chance = self.stacks / 20
|
||||
roll = random.random()
|
||||
|
||||
if chance > roll:
|
||||
self.handler.add(Exploited)
|
||||
self.owner.msg("An opportunity presents itself!")
|
||||
elif chance < roll:
|
||||
self.handler.add(Exploit)
|
||||
|
||||
if self.stacks in self.stack_msg:
|
||||
self.owner.msg(self.stack_msg[self.stacks])
|
||||
|
||||
|
||||
class Exploited(BaseBuff):
|
||||
key = "exploited"
|
||||
name = "Exploited"
|
||||
flavor = "You have sensed your target's vulnerability, and are poised to strike."
|
||||
|
||||
duration = 30
|
||||
|
||||
mods = [Mod("damage", "add", 100)]
|
||||
|
||||
def at_post_check(self, *args, **kwargs):
|
||||
self.owner.msg("You ruthlessly exploit your target's weakness!")
|
||||
self.remove(quiet=True)
|
||||
|
||||
def at_remove(self, *args, **kwargs):
|
||||
self.owner.msg("You have waited too long; the opportunity passes.")
|
||||
|
||||
|
||||
class Leeching(BaseBuff):
|
||||
key = "leeching"
|
||||
name = "Leeching"
|
||||
flavor = "Attacking this target fills you with vigor."
|
||||
|
||||
duration = 30
|
||||
|
||||
triggers = ["taken_damage"]
|
||||
|
||||
def at_trigger(self, trigger: str, attacker=None, damage=None, *args, **kwargs):
|
||||
if not attacker or not damage:
|
||||
return
|
||||
attacker.msg("You have been healed for {heal} life!".format(heal=damage * 0.1))
|
||||
|
||||
|
||||
class Poison(BaseBuff):
|
||||
key = "poison"
|
||||
name = "Poison"
|
||||
flavor = "A poison wracks this body with painful spasms."
|
||||
|
||||
duration = 120
|
||||
|
||||
maxstacks = 5
|
||||
tickrate = 5
|
||||
dmg = 5
|
||||
|
||||
playtime = True
|
||||
|
||||
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)
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
)
|
||||
|
||||
def at_tick(self, initial=True, *args, **kwargs):
|
||||
_dmg = self.dmg * self.stacks
|
||||
if not initial:
|
||||
self.owner.location.msg_contents(
|
||||
"Poison courses through {actor}'s body, dealing {damage} damage.".format(
|
||||
actor=self.owner.named, damage=_dmg
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class Sated(BaseBuff):
|
||||
key = "sated"
|
||||
name = "Sated"
|
||||
flavor = "You have eaten a great meal!"
|
||||
|
||||
duration = 180
|
||||
maxstacks = 3
|
||||
|
||||
mods = [Mod("mood", "add", 15)]
|
||||
|
||||
|
||||
class StatBuff(BaseBuff):
|
||||
"""Customize the stat this buff affects by feeding a list in the order [stat, mod, base, perstack] to the cache argument when added"""
|
||||
|
||||
key = "statbuff"
|
||||
name = "statbuff"
|
||||
flavor = "This buff affects the following stats: {stats}"
|
||||
|
||||
maxstacks = 0
|
||||
refresh = True
|
||||
unique = False
|
||||
|
||||
cache = {"modgen": ["foo", "add", 0, 0]}
|
||||
|
||||
def __init__(self, handler, buffkey, cache={}) -> None:
|
||||
super().__init__(handler, buffkey, cache)
|
||||
# Finds our "modgen" cache value, which we pass on application
|
||||
modgen = list(self.cache.get("modgen"))
|
||||
if modgen:
|
||||
self.mods = [Mod(*modgen)]
|
||||
msg = ""
|
||||
_msg = [mod.stat for mod in self.mods]
|
||||
for stat in _msg:
|
||||
msg += stat
|
||||
self.flavor = self.flavor.format(stats=msg)
|
||||
393
evennia/contrib/rpg/buffs/tests.py
Normal file
393
evennia/contrib/rpg/buffs/tests.py
Normal file
|
|
@ -0,0 +1,393 @@
|
|||
"""
|
||||
Tests for the buff system contrib
|
||||
"""
|
||||
from unittest.mock import Mock, call, patch
|
||||
from evennia import DefaultObject, create_object
|
||||
from evennia.utils import create
|
||||
from evennia.utils.utils import lazy_property
|
||||
from .samplebuffs import StatBuff
|
||||
from .buff import BaseBuff, Mod, BuffHandler, BuffableProperty
|
||||
from evennia.utils.test_resources import EvenniaTest
|
||||
|
||||
from evennia.contrib.rpg.buffs import buff
|
||||
|
||||
|
||||
class _EmptyBuff(BaseBuff):
|
||||
pass
|
||||
|
||||
|
||||
class _TestModBuff(BaseBuff):
|
||||
key = "tmb"
|
||||
name = "tmb"
|
||||
flavor = "modderbuff"
|
||||
maxstacks = 5
|
||||
mods = [Mod("stat1", "add", 10, 5), Mod("stat2", "mult", 0.5)]
|
||||
|
||||
|
||||
class _TestModBuff2(BaseBuff):
|
||||
key = "tmb2"
|
||||
name = "tmb2"
|
||||
flavor = "modderbuff2"
|
||||
maxstacks = 1
|
||||
mods = [Mod("stat1", "mult", 1.0), Mod("stat1", "add", 10)]
|
||||
|
||||
|
||||
class _TestTrigBuff(BaseBuff):
|
||||
key = "ttb"
|
||||
name = "ttb"
|
||||
flavor = "triggerbuff"
|
||||
triggers = ["test1", "test2"]
|
||||
|
||||
def at_trigger(self, trigger: str, *args, **kwargs):
|
||||
if trigger == "test1":
|
||||
self.owner.db.triggertest1 = True
|
||||
if trigger == "test2":
|
||||
self.owner.db.triggertest2 = True
|
||||
|
||||
|
||||
class _TestConBuff(BaseBuff):
|
||||
key = "tcb"
|
||||
name = "tcb"
|
||||
flavor = "condbuff"
|
||||
triggers = ["condtest"]
|
||||
|
||||
def conditional(self, *args, **kwargs):
|
||||
return self.owner.db.cond1
|
||||
|
||||
def at_trigger(self, trigger: str, attacker=None, defender=None, damage=0, *args, **kwargs):
|
||||
defender.db.att, defender.db.dmg = attacker, damage
|
||||
|
||||
|
||||
class _TestComplexBuff(BaseBuff):
|
||||
key = "tcomb"
|
||||
name = "complex"
|
||||
flavor = "combuff"
|
||||
triggers = ["comtest", "complextest"]
|
||||
|
||||
mods = [
|
||||
Mod("com1", "add", 0, 10),
|
||||
Mod("com1", "add", 15),
|
||||
Mod("com1", "mult", 2.0),
|
||||
Mod("com2", "add", 100),
|
||||
]
|
||||
|
||||
def conditional(self, cond=False, *args, **kwargs):
|
||||
return not cond
|
||||
|
||||
def at_trigger(self, trigger: str, *args, **kwargs):
|
||||
if trigger == "comtest":
|
||||
self.owner.db.comtext = {"cond": True}
|
||||
else:
|
||||
self.owner.db.comtext = {}
|
||||
|
||||
|
||||
class _TestTimeBuff(BaseBuff):
|
||||
key = "ttib"
|
||||
name = "ttib"
|
||||
flavor = "timerbuff"
|
||||
maxstacks = 1
|
||||
tickrate = 1
|
||||
duration = 5
|
||||
mods = [Mod("timetest", "add", 665)]
|
||||
|
||||
def at_tick(self, initial=True, *args, **kwargs):
|
||||
self.owner.db.ticktest = True
|
||||
|
||||
|
||||
class BuffableObject(DefaultObject):
|
||||
stat1 = BuffableProperty(10)
|
||||
|
||||
@lazy_property
|
||||
def buffs(self) -> BuffHandler:
|
||||
return BuffHandler(self)
|
||||
|
||||
def at_init(self):
|
||||
self.stat1, self.buffs
|
||||
return super().at_init()
|
||||
|
||||
|
||||
class TestBuffsAndHandler(EvenniaTest):
|
||||
"This tests a number of things about buffs."
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.testobj = create.create_object(BuffableObject, key="testobj")
|
||||
|
||||
def tearDown(self):
|
||||
"""done after every test_* method below"""
|
||||
self.testobj.buffs.clear()
|
||||
del self.testobj
|
||||
super().tearDown()
|
||||
|
||||
@patch("evennia.contrib.rpg.buffs.buff.utils.delay", new=Mock())
|
||||
def test_addremove(self):
|
||||
"""tests adding and removing buffs"""
|
||||
# setup
|
||||
handler: BuffHandler = self.testobj.buffs
|
||||
# add
|
||||
handler.add(_TestModBuff, to_cache={"cachetest": True})
|
||||
handler.add(_TestTrigBuff)
|
||||
self.assertEqual(self.testobj.db.buffs["tmb"]["ref"], _TestModBuff)
|
||||
self.assertTrue(self.testobj.db.buffs["tmb"].get("cachetest"))
|
||||
self.assertFalse(self.testobj.db.buffs["ttb"].get("cachetest"))
|
||||
# has
|
||||
self.assertTrue(handler.has(_TestModBuff))
|
||||
self.assertTrue(handler.has("tmb"))
|
||||
self.assertFalse(handler.has(_EmptyBuff))
|
||||
# remove
|
||||
handler.remove("tmb")
|
||||
self.assertFalse(self.testobj.db.buffs.get("tmb"))
|
||||
# remove stacks
|
||||
handler.add(_TestModBuff, stacks=3)
|
||||
handler.remove("tmb", stacks=3)
|
||||
self.assertFalse(self.testobj.db.buffs.get("tmb"))
|
||||
# remove by type
|
||||
handler.add(_TestModBuff)
|
||||
handler.remove_by_type(_TestModBuff)
|
||||
self.assertFalse(self.testobj.db.buffs.get("tmb"))
|
||||
# remove by buff instance
|
||||
handler.add(_TestModBuff)
|
||||
handler.all["tmb"].remove()
|
||||
self.assertFalse(self.testobj.db.buffs.get("tmb"))
|
||||
# remove by source
|
||||
handler.add(_TestModBuff)
|
||||
handler.remove_by_source(None)
|
||||
self.assertFalse(self.testobj.db.buffs.get("tmb"))
|
||||
# remove by cachevalue
|
||||
handler.add(_TestModBuff)
|
||||
handler.remove_by_cachevalue("failure", True)
|
||||
self.assertTrue(self.testobj.db.buffs.get("tmb"))
|
||||
# remove all
|
||||
handler.add(_TestModBuff)
|
||||
handler.clear()
|
||||
self.assertFalse(self.testobj.buffs.all)
|
||||
|
||||
@patch("evennia.contrib.rpg.buffs.buff.utils.delay", new=Mock())
|
||||
def test_getters(self):
|
||||
"""tests all built-in getters"""
|
||||
# setup
|
||||
handler: BuffHandler = self.testobj.buffs
|
||||
handler.add(_TestModBuff, source=self.obj2)
|
||||
handler.add(_TestTrigBuff, to_cache={"ttbcache": True})
|
||||
# normal getter
|
||||
self.assertTrue(isinstance(handler.get("tmb"), _TestModBuff))
|
||||
# stat getters
|
||||
self.assertTrue(isinstance(handler.get_by_stat("stat1")["tmb"], _TestModBuff))
|
||||
self.assertFalse(handler.get_by_stat("nullstat"))
|
||||
# trigger getters
|
||||
self.assertTrue("ttb" in handler.get_by_trigger("test1").keys())
|
||||
self.assertFalse("ttb" in handler.get_by_trigger("nulltrig").keys())
|
||||
# type getters
|
||||
self.assertTrue("tmb" in handler.get_by_type(_TestModBuff))
|
||||
self.assertFalse("tmb" in handler.get_by_type(_EmptyBuff))
|
||||
# source getter
|
||||
self.assertTrue("tmb" in handler.get_by_source(self.obj2))
|
||||
self.assertFalse("ttb" in handler.get_by_source(self.obj2))
|
||||
# cachevalue getter
|
||||
self.assertFalse("tmb" in handler.get_by_cachevalue("ttbcache"))
|
||||
self.assertTrue("ttb" in handler.get_by_cachevalue("ttbcache"))
|
||||
self.assertTrue("ttb" in handler.get_by_cachevalue("ttbcache", True))
|
||||
|
||||
@patch("evennia.contrib.rpg.buffs.buff.utils.delay", new=Mock())
|
||||
def test_details(self):
|
||||
"""tests that buff details like name and flavor are correct"""
|
||||
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")
|
||||
|
||||
@patch("evennia.contrib.rpg.buffs.buff.utils.delay", new=Mock())
|
||||
def test_modify(self):
|
||||
"""tests to ensure that values are modified correctly, and stack across mods"""
|
||||
# setup
|
||||
handler: BuffHandler = self.testobj.buffs
|
||||
_stat1, _stat2 = 0, 10
|
||||
handler.add(_TestModBuff)
|
||||
# stat1 and 2 basic mods
|
||||
self.assertEqual(handler.check(_stat1, "stat1"), 15)
|
||||
self.assertEqual(handler.check(_stat2, "stat2"), 15)
|
||||
# checks can take any base value
|
||||
self.assertEqual(handler.check(_stat1, "stat2"), 0)
|
||||
self.assertEqual(handler.check(_stat2, "stat1"), 25)
|
||||
# change to base stat reflected in check
|
||||
_stat1 += 5
|
||||
self.assertEqual(handler.check(_stat1, "stat1"), 20)
|
||||
_stat2 += 10
|
||||
self.assertEqual(handler.check(_stat2, "stat2"), 30)
|
||||
# test stacking; single stack, multiple stack, max stacks
|
||||
handler.add(_TestModBuff)
|
||||
self.assertEqual(handler.check(_stat1, "stat1"), 25)
|
||||
handler.add(_TestModBuff, stacks=3)
|
||||
self.assertEqual(handler.check(_stat1, "stat1"), 40)
|
||||
handler.add(_TestModBuff, stacks=5)
|
||||
self.assertEqual(handler.check(_stat1, "stat1"), 40)
|
||||
# stat2 mod doesn't stack
|
||||
self.assertEqual(handler.check(_stat2, "stat2"), 30)
|
||||
# layers with second mod
|
||||
handler.add(_TestModBuff2)
|
||||
self.assertEqual(handler.check(_stat1, "stat1"), 100)
|
||||
self.assertEqual(handler.check(_stat2, "stat2"), 30)
|
||||
handler.remove_by_type(_TestModBuff)
|
||||
self.assertEqual(handler.check(_stat1, "stat1"), 30)
|
||||
self.assertEqual(handler.check(_stat2, "stat2"), 20)
|
||||
|
||||
@patch("evennia.contrib.rpg.buffs.buff.utils.delay", new=Mock())
|
||||
def test_trigger(self):
|
||||
"""tests to ensure triggers correctly fire"""
|
||||
# setup
|
||||
handler: BuffHandler = self.testobj.buffs
|
||||
handler.add(_TestTrigBuff)
|
||||
# trigger buffs
|
||||
handler.trigger("nulltest")
|
||||
self.assertFalse(self.testobj.db.triggertest1)
|
||||
self.assertFalse(self.testobj.db.triggertest2)
|
||||
handler.trigger("test1")
|
||||
self.assertTrue(self.testobj.db.triggertest1)
|
||||
self.assertFalse(self.testobj.db.triggertest2)
|
||||
handler.trigger("test2")
|
||||
self.assertTrue(self.testobj.db.triggertest1)
|
||||
self.assertTrue(self.testobj.db.triggertest2)
|
||||
|
||||
@patch("evennia.contrib.rpg.buffs.buff.utils.delay", new=Mock())
|
||||
def test_context_conditional(self):
|
||||
"""tests to ensure context is passed to buffs, and also tests conditionals"""
|
||||
# setup
|
||||
handler: BuffHandler = self.testobj.buffs
|
||||
handler.add(_TestConBuff)
|
||||
self.testobj.db.cond1, self.testobj.db.att, self.testobj.db.dmg = False, None, 0
|
||||
# context to pass, containing basic event data and a little extra to be ignored
|
||||
_testcontext = {
|
||||
"attacker": self.obj2,
|
||||
"defender": self.testobj,
|
||||
"damage": 5,
|
||||
"overflow": 10,
|
||||
}
|
||||
# test negative conditional
|
||||
self.assertEqual(
|
||||
handler.get_by_type(_TestConBuff)["tcb"].conditional(**_testcontext), False
|
||||
)
|
||||
handler.trigger("condtest", _testcontext)
|
||||
self.assertEqual(self.testobj.db.att, None)
|
||||
self.assertEqual(self.testobj.db.dmg, 0)
|
||||
# test positive conditional + context passing
|
||||
self.testobj.db.cond1 = True
|
||||
self.assertEqual(handler.get_by_type(_TestConBuff)["tcb"].conditional(**_testcontext), True)
|
||||
handler.trigger("condtest", _testcontext)
|
||||
self.assertEqual(self.testobj.db.att, self.obj2)
|
||||
self.assertEqual(self.testobj.db.dmg, 5)
|
||||
|
||||
@patch("evennia.contrib.rpg.buffs.buff.utils.delay", new=Mock())
|
||||
def test_complex(self):
|
||||
"""tests a complex mod (conditionals, multiple triggers/mods)"""
|
||||
# setup
|
||||
handler: BuffHandler = self.testobj.buffs
|
||||
self.testobj.db.comone, self.testobj.db.comtwo, self.testobj.db.comtext = 10, 0, {}
|
||||
handler.add(_TestComplexBuff)
|
||||
# stat checks work correctly and separately
|
||||
self.assertEqual(self.testobj.db.comtext, {})
|
||||
self.assertEqual(handler.check(self.testobj.db.comone, "com1"), 105)
|
||||
self.assertEqual(handler.check(self.testobj.db.comtwo, "com2"), 100)
|
||||
# stat checks don't happen if the conditional is true
|
||||
handler.trigger("comtest", self.testobj.db.comtext)
|
||||
self.assertEqual(self.testobj.db.comtext, {"cond": True})
|
||||
self.assertEqual(
|
||||
handler.get_by_type(_TestComplexBuff)["tcomb"].conditional(**self.testobj.db.comtext),
|
||||
False,
|
||||
)
|
||||
self.assertEqual(
|
||||
handler.check(self.testobj.db.comone, "com1", context=self.testobj.db.comtext), 10
|
||||
)
|
||||
self.assertEqual(
|
||||
handler.check(self.testobj.db.comtwo, "com2", context=self.testobj.db.comtext), 0
|
||||
)
|
||||
handler.trigger("complextest", self.testobj.db.comtext)
|
||||
self.assertEqual(
|
||||
handler.check(self.testobj.db.comone, "com1", context=self.testobj.db.comtext), 10
|
||||
)
|
||||
self.assertEqual(
|
||||
handler.check(self.testobj.db.comtwo, "com2", context=self.testobj.db.comtext), 0
|
||||
)
|
||||
# separate trigger follows different codepath
|
||||
self.testobj.db.comtext = {"cond": False}
|
||||
handler.trigger("complextest", self.testobj.db.comtext)
|
||||
self.assertEqual(self.testobj.db.comtext, {})
|
||||
self.assertEqual(
|
||||
handler.check(self.testobj.db.comone, "com1", context=self.testobj.db.comtext), 105
|
||||
)
|
||||
self.assertEqual(
|
||||
handler.check(self.testobj.db.comtwo, "com2", context=self.testobj.db.comtext), 100
|
||||
)
|
||||
|
||||
@patch("evennia.contrib.rpg.buffs.buff.utils.delay")
|
||||
def test_timing(self, mock_delay: Mock):
|
||||
"""tests timing-related features, such as ticking and duration"""
|
||||
# setup
|
||||
handler: BuffHandler = self.testobj.buffs
|
||||
mock_delay.side_effect = [None, handler.cleanup]
|
||||
handler.add(_TestTimeBuff)
|
||||
calls = [
|
||||
call(
|
||||
1,
|
||||
buff.tick_buff,
|
||||
handler=handler,
|
||||
buffkey="ttib",
|
||||
context={},
|
||||
initial=False,
|
||||
persistent=True,
|
||||
),
|
||||
call(5, handler.cleanup, persistent=True),
|
||||
]
|
||||
mock_delay.assert_has_calls(calls)
|
||||
self.testobj.db.timetest, self.testobj.db.ticktest = 1, False
|
||||
# test duration and ticking
|
||||
_instance = handler.get("ttib")
|
||||
self.assertTrue(_instance.ticking)
|
||||
self.assertEqual(_instance.duration, 5)
|
||||
_instance.at_tick()
|
||||
self.assertTrue(self.testobj.db.ticktest)
|
||||
# test duration modification and cleanup
|
||||
handler.set_duration("ttib", 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_buffableproperty(self):
|
||||
"""tests buffable properties"""
|
||||
# setup
|
||||
self.testobj.buffs.add(_TestModBuff)
|
||||
self.assertEqual(self.testobj.stat1, 25)
|
||||
self.testobj.buffs.remove("tmb")
|
||||
self.assertEqual(self.testobj.stat1, 10)
|
||||
|
||||
@patch("evennia.contrib.rpg.buffs.buff.utils.delay", new=Mock())
|
||||
def test_stresstest(self):
|
||||
"""tests large amounts of buffs, and related removal methods"""
|
||||
# setup
|
||||
for x in range(1, 20):
|
||||
self.testobj.buffs.add(_TestModBuff, key="test" + str(x))
|
||||
self.testobj.buffs.add(_TestTrigBuff, key="trig" + str(x))
|
||||
self.assertEqual(self.testobj.stat1, 295)
|
||||
self.testobj.buffs.trigger("test1")
|
||||
self.testobj.buffs.remove_by_type(_TestModBuff)
|
||||
self.assertEqual(self.testobj.stat1, 10)
|
||||
self.testobj.buffs.clear()
|
||||
self.assertFalse(self.testobj.buffs.all)
|
||||
|
||||
@patch("evennia.contrib.rpg.buffs.buff.utils.delay", new=Mock())
|
||||
def test_modgen(self):
|
||||
"""test generating mods on the fly"""
|
||||
# setup
|
||||
handler: BuffHandler = self.testobj.buffs
|
||||
self.testobj.db.gentest = 5
|
||||
self.assertEqual(self.testobj.db.gentest, 5)
|
||||
tc = {"modgen": ["gentest", "add", 5, 0]}
|
||||
handler.add(StatBuff, key="gentest", to_cache=tc)
|
||||
self.assertEqual(handler.check(self.testobj.db.gentest, "gentest"), 10)
|
||||
tc = {"modgen": ["gentest", "add", 10, 0]}
|
||||
handler.add(StatBuff, key="gentest", to_cache=tc)
|
||||
self.assertEqual(handler.check(self.testobj.db.gentest, "gentest"), 15)
|
||||
self.assertEqual(
|
||||
handler.get("gentest").flavor, "This buff affects the following stats: gentest"
|
||||
)
|
||||
|
|
@ -7,6 +7,7 @@ allows for customizing the server operation as desired.
|
|||
|
||||
This module must contain at least these global functions:
|
||||
|
||||
at_server_init()
|
||||
at_server_start()
|
||||
at_server_stop()
|
||||
at_server_reload_start()
|
||||
|
|
@ -16,6 +17,11 @@ at_server_cold_stop()
|
|||
|
||||
"""
|
||||
|
||||
def at_server_init():
|
||||
"""
|
||||
This is called first as the server is starting up, regardless of how.
|
||||
"""
|
||||
pass
|
||||
|
||||
def at_server_start():
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -46,8 +46,9 @@ _SA = object.__setattr__
|
|||
# a file with a flag telling the server to restart after shutdown or not.
|
||||
SERVER_RESTART = os.path.join(settings.GAME_DIR, "server", "server.restart")
|
||||
|
||||
# module containing hook methods called during start_stop
|
||||
SERVER_STARTSTOP_MODULE = mod_import(settings.AT_SERVER_STARTSTOP_MODULE)
|
||||
# modules containing hook methods called during start_stop
|
||||
SERVER_STARTSTOP_MODULES = [mod_import(mod) for mod in make_iter(settings.AT_SERVER_STARTSTOP_MODULE)
|
||||
if isinstance(mod, str)]
|
||||
|
||||
# modules containing plugin services
|
||||
SERVER_SERVICES_PLUGIN_MODULES = make_iter(settings.SERVER_SERVICES_PLUGIN_MODULES)
|
||||
|
|
@ -413,6 +414,8 @@ class Evennia:
|
|||
for typeclass_db in TypedObject.__subclasses__()
|
||||
]
|
||||
|
||||
self.at_server_init()
|
||||
|
||||
# call correct server hook based on start file value
|
||||
if mode == "reload":
|
||||
logger.log_msg("Server successfully reloaded.")
|
||||
|
|
@ -525,14 +528,23 @@ class Evennia:
|
|||
|
||||
# server start/stop hooks
|
||||
|
||||
def at_server_init(self):
|
||||
"""
|
||||
This is called first when the server is starting, before any other hooks, regardless of how it's starting.
|
||||
"""
|
||||
for mod in SERVER_STARTSTOP_MODULES:
|
||||
if hasattr(mod, "at_server_init"):
|
||||
mod.at_server_init()
|
||||
|
||||
def at_server_start(self):
|
||||
"""
|
||||
This is called every time the server starts up, regardless of
|
||||
how it was shut down.
|
||||
|
||||
"""
|
||||
if SERVER_STARTSTOP_MODULE:
|
||||
SERVER_STARTSTOP_MODULE.at_server_start()
|
||||
for mod in SERVER_STARTSTOP_MODULES:
|
||||
if hasattr(mod, "at_server_start"):
|
||||
mod.at_server_start()
|
||||
|
||||
def at_server_stop(self):
|
||||
"""
|
||||
|
|
@ -540,16 +552,18 @@ class Evennia:
|
|||
of it is fore a reload, reset or shutdown.
|
||||
|
||||
"""
|
||||
if SERVER_STARTSTOP_MODULE:
|
||||
SERVER_STARTSTOP_MODULE.at_server_stop()
|
||||
for mod in SERVER_STARTSTOP_MODULES:
|
||||
if hasattr(mod, "at_server_stop"):
|
||||
mod.at_server_stop()
|
||||
|
||||
def at_server_reload_start(self):
|
||||
"""
|
||||
This is called only when server starts back up after a reload.
|
||||
|
||||
"""
|
||||
if SERVER_STARTSTOP_MODULE:
|
||||
SERVER_STARTSTOP_MODULE.at_server_reload_start()
|
||||
for mod in SERVER_STARTSTOP_MODULES:
|
||||
if hasattr(mod, "at_server_reload_start"):
|
||||
mod.at_server_reload_start()
|
||||
|
||||
def at_post_portal_sync(self, mode):
|
||||
"""
|
||||
|
|
@ -589,8 +603,9 @@ class Evennia:
|
|||
This is called only time the server stops before a reload.
|
||||
|
||||
"""
|
||||
if SERVER_STARTSTOP_MODULE:
|
||||
SERVER_STARTSTOP_MODULE.at_server_reload_stop()
|
||||
for mod in SERVER_STARTSTOP_MODULES:
|
||||
if hasattr(mod, "at_server_reload_stop"):
|
||||
mod.at_server_reload_stop()
|
||||
|
||||
def at_server_cold_start(self):
|
||||
"""
|
||||
|
|
@ -618,16 +633,18 @@ class Evennia:
|
|||
if character:
|
||||
character.delete()
|
||||
guest.delete()
|
||||
if SERVER_STARTSTOP_MODULE:
|
||||
SERVER_STARTSTOP_MODULE.at_server_cold_start()
|
||||
for mod in SERVER_STARTSTOP_MODULES:
|
||||
if hasattr(mod, "at_server_cold_start"):
|
||||
mod.at_server_cold_start()
|
||||
|
||||
def at_server_cold_stop(self):
|
||||
"""
|
||||
This is called only when the server goes down due to a shutdown or reset.
|
||||
|
||||
"""
|
||||
if SERVER_STARTSTOP_MODULE:
|
||||
SERVER_STARTSTOP_MODULE.at_server_cold_stop()
|
||||
for mod in SERVER_STARTSTOP_MODULES:
|
||||
if hasattr(mod, "at_server_cold_stop"):
|
||||
mod.at_server_cold_stop()
|
||||
|
||||
|
||||
# ------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -12,12 +12,12 @@ value - which may change as Evennia is developed. This way you can
|
|||
always be sure of what you have changed and what is default behaviour.
|
||||
|
||||
"""
|
||||
from django.contrib.messages import constants as messages
|
||||
from django.urls import reverse_lazy
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from django.contrib.messages import constants as messages
|
||||
from django.urls import reverse_lazy
|
||||
|
||||
######################################################################
|
||||
# Evennia base server config
|
||||
######################################################################
|
||||
|
|
@ -386,9 +386,11 @@ INITIAL_SETUP_MODULE = "evennia.server.initial_setup"
|
|||
# the server's initial setup sequence (the very first startup of the system).
|
||||
# The check will fail quietly if module doesn't exist or fails to load.
|
||||
AT_INITIAL_SETUP_HOOK_MODULE = "server.conf.at_initial_setup"
|
||||
# Module containing your custom at_server_start(), at_server_reload() and
|
||||
# at_server_stop() methods. These methods will be called every time
|
||||
# the server starts, reloads and resets/stops respectively.
|
||||
# Module(s) containing custom at_server_init(), at_server_start(),
|
||||
# at_server_reload() and at_server_stop() methods. These methods will be called
|
||||
# every time the server starts, reloads and resets/stops
|
||||
# respectively. Can be given as a single path or a list of paths. If a list,
|
||||
# each module's hooks will be called in list order.
|
||||
AT_SERVER_STARTSTOP_MODULE = "server.conf.at_server_startstop"
|
||||
# List of one or more module paths to modules containing a function start_
|
||||
# plugin_services(application). This module will be called with the main
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue