mirror of
https://github.com/evennia/evennia.git
synced 2026-03-16 21:06:30 +01:00
Add TraitProperties as alternative way to define Traits from the traits contrib. Also clean up docs to resolve #2450.
This commit is contained in:
parent
6e975236eb
commit
78e063d9ca
4 changed files with 240 additions and 30 deletions
|
|
@ -134,6 +134,17 @@ Full director-style emoting system replacing names with sdescs/recogs. Supports
|
|||
|
||||
Dynamic obfuscation of emotes when speaking unfamiliar languages. Also obfuscates whispers.
|
||||
|
||||
|
||||
### Traits
|
||||
|
||||
*Whitenoise 2014, Griatch2021*
|
||||
|
||||
Powerful on-object properties (very extended Attributes) for representing
|
||||
health, mana, skill-levels etc, with automatic min/max value, base, modifiers
|
||||
and named tiers for different values. Also include timed rate increase/decrease
|
||||
to have values change over a period of time.
|
||||
|
||||
|
||||
### Turnbattle
|
||||
|
||||
*FlutterSprite 2017*
|
||||
|
|
|
|||
|
|
@ -10,8 +10,7 @@ from copy import copy
|
|||
from anything import Something
|
||||
from mock import MagicMock, patch
|
||||
from django.test import TestCase
|
||||
from django.test import override_settings
|
||||
from evennia.utils.test_resources import EvenniaTest
|
||||
from evennia.utils.utils import lazy_property
|
||||
from evennia.contrib import traits
|
||||
|
||||
|
||||
|
|
@ -903,3 +902,36 @@ class TestNumericTraitOperators(TestCase):
|
|||
self.assertGreaterEqual(8, self.st)
|
||||
self.assertGreaterEqual(self.st, 0)
|
||||
self.assertGreaterEqual(10, self.st)
|
||||
|
||||
|
||||
class DummyCharacter(_MockObj):
|
||||
@lazy_property
|
||||
def strength(self):
|
||||
return traits.TraitProperty(self, "str", trait_type="static", base=10, mod=2)
|
||||
|
||||
@lazy_property
|
||||
def hunting(self):
|
||||
return traits.TraitProperty(self, "hunting", trait_type="counter", base=10, mod=1, max=100)
|
||||
|
||||
@lazy_property
|
||||
def health(self):
|
||||
return traits.TraitProperty(self, "hp", trait_type="gauge", base=100)
|
||||
|
||||
|
||||
class TestTraitFields(TestCase):
|
||||
"""
|
||||
Test the TraitField class.
|
||||
|
||||
"""
|
||||
|
||||
@patch("evennia.contrib.traits._TRAIT_CLASS_PATHS", new=_TEST_TRAIT_CLASS_PATHS)
|
||||
def test_traitfields(self):
|
||||
obj = DummyCharacter()
|
||||
|
||||
# from evennia import set_trace;set_trace()
|
||||
self.assertEqual(12, obj.strength.value)
|
||||
self.assertEqual(11, obj.hunting.value)
|
||||
self.assertEqual(100, obj.health.value)
|
||||
|
||||
obj.strength.base += 5
|
||||
self.assertEqual(17, obj.strength.value)
|
||||
|
|
|
|||
|
|
@ -38,29 +38,78 @@ class Object(DefaultObject):
|
|||
# this adds the handler as .traits
|
||||
return TraitHandler(self)
|
||||
|
||||
|
||||
def at_object_creation(self):
|
||||
# (or wherever you want)
|
||||
self.traits.add("str", "Strength", trait_type="static", base=10, mod=2)
|
||||
self.traits.add("hp", "Health", trait_type="gauge", min=0, max=100)
|
||||
self.traits.add("hunting", "Hunting Skill", trait_type="counter",
|
||||
base=10, mod=1, min=0, max=100)
|
||||
|
||||
|
||||
```
|
||||
When adding the trait, you supply the name of the property (`hunting`) along
|
||||
with a more human-friendly name ("Hunting Skill"). The latter will show if you
|
||||
print the trait etc. The `trait_type` is important, this specifies which type
|
||||
of trait this is (see below).
|
||||
|
||||
There is an alternative way to define Traits, as individual `TraitProperty` entities. The
|
||||
advantage is that you can define the Traits directly in the class, much like Django model fields.
|
||||
You'll be able to access them as e.g. `self.strength` instead of `self.traits.strength`. The
|
||||
drawback is that you must make sure that the name of your TraitsProperties don't collide with any
|
||||
other properties/methods on your class. The `.traits` handler will also not automatically be
|
||||
available to you if you want to add traits on the fly later.
|
||||
|
||||
```python
|
||||
# mygame/typeclasses/objects.py
|
||||
|
||||
from evennia import DefaultObject
|
||||
from evennia.utils import lazy_property
|
||||
from evennia.contrib.traits import TraitProperty
|
||||
|
||||
# ...
|
||||
|
||||
class Object(DefaultObject):
|
||||
...
|
||||
@lazy_property
|
||||
def strength(self):
|
||||
# note that the trait's name must be set exactly the same as the name of the property!
|
||||
return TraitProperty(self, "strength", "Strength", trait_type="static", base=10, mod=2)
|
||||
|
||||
@lazy_property
|
||||
def hp(self):
|
||||
return TraitProperty(self, "hp", "Health", trait_type="gauge", min=0, base=100, mod=2)
|
||||
|
||||
@lazy_property
|
||||
def hunting(self):
|
||||
return TraitProperty(self, "hunting", "Hunting Skill", trait_type="counter",
|
||||
base=10, mod=1, min=0, max=100)
|
||||
|
||||
```
|
||||
> Note that the trait name ('str', 'hp' and 'hunting' above) must be set exactly the same as the
|
||||
> name of the property. Also, while the TraitHandler `.traits` is used under the hood, the
|
||||
> handler will only be spawned after the TraitProperty has loaded at least once. If having `.traits`
|
||||
> available matters to you, use `@property` instead of `@lazy_property` for one of the above
|
||||
> definitions to make sure the handler is always initialized.
|
||||
|
||||
|
||||
## Using traits
|
||||
|
||||
A trait is added to the traithandler, after which one can access it
|
||||
as a property on the handler (similarly to how you can do .db.attrname for Attributes
|
||||
in Evennia).
|
||||
A trait is added to the traithandler (if you use `TraitProperty` the handler is just created under
|
||||
the hood) after which one can access it as a property on the handler (similarly to how you can do
|
||||
.db.attrname for Attributes in Evennia).
|
||||
|
||||
```python
|
||||
# this is an example using the "static" trait, described below
|
||||
|
||||
>>> obj.traits.add("hunting", "Hunting Skill", trait_type="static", base=4)
|
||||
>>> obj.traits.hunting.value
|
||||
4
|
||||
>>> obj.traits.hunting.value += 5
|
||||
>>> obj.traits.hunting.value
|
||||
9
|
||||
>>> obj.traits.add("hp", "Health", trait_type="gauge", min=0, max=100)
|
||||
>>> obj.traits.strength.value
|
||||
12 # base + mod
|
||||
>>> obj.traits.strength.value += 5
|
||||
>>> obj.traits.strength.value
|
||||
17
|
||||
>>> obj.traits.hp.value
|
||||
100
|
||||
102 # base + mod
|
||||
>>> obj.traits.hp -= 200
|
||||
>>> obj.traits.hp.value
|
||||
0
|
||||
0 # min of 0
|
||||
>>> obj.traits.hp.reset()
|
||||
>>> obj.traits.hp.value
|
||||
100
|
||||
|
|
@ -72,12 +121,16 @@ in Evennia).
|
|||
>>> obj.traits.hp.effect
|
||||
"poisoned!"
|
||||
|
||||
# with TraitProperties, works the same:
|
||||
|
||||
>>> obj.hunting.value
|
||||
12
|
||||
>>> obj.strength.value += 5
|
||||
>>> obj.strength.value
|
||||
17
|
||||
|
||||
```
|
||||
|
||||
When adding the trait, you supply the name of the property (`hunting`) along
|
||||
with a more human-friendly name ("Hunting Skill"). The latter will show if you
|
||||
print the trait etc. The `trait_type` is important, this specifies which type
|
||||
of trait this is.
|
||||
|
||||
|
||||
## Trait types
|
||||
|
|
@ -259,7 +312,7 @@ This emulates a [fuel-] gauge that empties from a base+mod value.
|
|||
The `.current` value will start from a full gauge. The .max property is
|
||||
read-only and is set by `.base` + `.mod`. So contrary to a `Counter`, the
|
||||
`.mod` modifier only applies to the max value of the gauge and not the current
|
||||
value. The minimum bound defaults to 0 if not set explicitly.
|
||||
value. The minimum bound defaults to 0 if not set explicitly.
|
||||
|
||||
This trait is useful for showing commonly depletable resources like health,
|
||||
stamina and the like.
|
||||
|
|
@ -280,7 +333,7 @@ stamina and the like.
|
|||
The Gauge trait is subclass of the Counter, so you have access to the same
|
||||
methods and properties where they make sense. So gauges can also have a
|
||||
`.descs` dict to describe the intervals in text, and can use `.percent()` to
|
||||
get how filled it is as a percentage etc.
|
||||
get how filled it is as a percentage etc.
|
||||
|
||||
The `.rate` is particularly relevant for gauges - useful for everything
|
||||
from poison slowly draining your health, to resting gradually increasing it.
|
||||
|
|
@ -329,7 +382,7 @@ class RageTrait(StaticTrait):
|
|||
|
||||
def sedate(self):
|
||||
self.mod = 0
|
||||
|
||||
|
||||
|
||||
```
|
||||
|
||||
|
|
@ -435,12 +488,22 @@ class MandatoryTraitKey:
|
|||
This represents a required key that must be
|
||||
supplied when a Trait is initialized. It's used
|
||||
by Trait classes when defining their required keys.
|
||||
"""
|
||||
|
||||
"""
|
||||
|
||||
class TraitHandler:
|
||||
"""
|
||||
Factory class that instantiates Trait objects.
|
||||
Factory class that instantiates Trait objects. Must be assigned as a property
|
||||
on the class, usually with `lazy_property`.
|
||||
|
||||
Example:
|
||||
::
|
||||
class Object(DefaultObject):
|
||||
...
|
||||
@lazy_property
|
||||
def traits(self):
|
||||
# this adds the handler as .traits
|
||||
return TraitHandler(self)
|
||||
|
||||
"""
|
||||
|
||||
|
|
@ -450,12 +513,16 @@ class TraitHandler:
|
|||
|
||||
Args:
|
||||
obj (Object): Parent Object typeclass for this TraitHandler
|
||||
db_attribute_key (str): Name of the DB attribute for trait data storage
|
||||
db_attribute_key (str): Name of the DB attribute for trait data storage.
|
||||
db_attribute_category (str): Name of DB attribute's category to trait data storage.
|
||||
|
||||
"""
|
||||
# load the available classes, if necessary
|
||||
_delayed_import_trait_classes()
|
||||
|
||||
# initialize any
|
||||
|
||||
|
||||
# Note that .trait_data retains the connection to the database, meaning every
|
||||
# update we do to .trait_data automatically syncs with database.
|
||||
self.trait_data = obj.attributes.get(db_attribute_key, category=db_attribute_category)
|
||||
|
|
@ -615,6 +682,96 @@ class TraitHandler:
|
|||
self.remove(trait_key)
|
||||
|
||||
|
||||
class TraitProperty:
|
||||
"""
|
||||
Optional extra: Allows for applying traits as individual properties directly on the parent class
|
||||
instead for properties on the `.traits` handler. So with this you could access data e.g. as
|
||||
`character.hp.value` instead of `character.traits.hp.value`. This still uses the traitshandler
|
||||
under the hood.
|
||||
|
||||
Example:
|
||||
::
|
||||
from evennia.utils import lazy_property
|
||||
from evennia.contrib.traits import TraitProperty
|
||||
|
||||
class Character(DefaultCharacter):
|
||||
|
||||
@lazy_property
|
||||
def strength(self):
|
||||
return TraitProperty(self, "str", "Strength", trait_type="static", base=10, mod=2)
|
||||
|
||||
@lazy_property
|
||||
def hunting(self):
|
||||
return TraitProperty(self, "hunting", "Hunting Skill", trait_type="counter",
|
||||
base=10, mod=1, max=100)
|
||||
@lazy_property
|
||||
def health(self):
|
||||
return TraitProperty(self, "hp", "Health", trait_type="gauge", min=0, base=100)
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
obj,
|
||||
trait_key,
|
||||
**kwargs):
|
||||
"""
|
||||
Initialize a TraitField.
|
||||
|
||||
Args:
|
||||
obj (Object): The object the TraitProperty is defined on.
|
||||
trait_key (str): Name of Trait.
|
||||
Kwargs:
|
||||
traithandler_name (str): If given, this is used as the name of the TraitHandler created
|
||||
behind the scenes. If not set, this will be a property `traits` on the class.
|
||||
any: All other properties are the same as for adding a new trait of the given type using
|
||||
the normal TraitHandler.
|
||||
|
||||
"""
|
||||
_SA(self, "obj", obj)
|
||||
_SA(self, "trait_key", trait_key)
|
||||
traithandler_name = kwargs.pop("traithandler_name", "traits")
|
||||
_SA(self, 'traithandler_name', traithandler_name)
|
||||
_SA(self, 'trait_properties', kwargs)
|
||||
|
||||
@property
|
||||
def traithandler(self):
|
||||
"""
|
||||
Get/create the underlying traithandler.
|
||||
|
||||
"""
|
||||
try:
|
||||
return getattr(_GA(self, "obj"), _GA(self, "traithandler_name"))
|
||||
except AttributeError:
|
||||
# traithandler not found; create a new on-demand
|
||||
new_traithandler = TraitHandler(_GA(self, "obj"))
|
||||
setattr(_GA(self, "obj"), _GA(self, "traithandler_name"), new_traithandler)
|
||||
return new_traithandler
|
||||
|
||||
@property
|
||||
def trait(self):
|
||||
"""
|
||||
Get/create the underlying trait on the traithandler
|
||||
|
||||
"""
|
||||
trait_key = _GA(self, "trait_key")
|
||||
traithandler = _GA(self, "traithandler")
|
||||
trait = traithandler.get(trait_key)
|
||||
if trait is None:
|
||||
traithandler.add(
|
||||
trait_key,
|
||||
**_GA(self, "trait_properties")
|
||||
)
|
||||
trait = traithandler.get(trait_key) # this caches it properly
|
||||
return trait
|
||||
|
||||
def __getattribute__(self, name):
|
||||
return _GA(_GA(self, "trait"), name)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
_SA(_GA(self, "trait"), name, value)
|
||||
|
||||
|
||||
|
||||
# Parent Trait class
|
||||
|
||||
|
||||
|
|
@ -949,7 +1106,7 @@ class Trait:
|
|||
class StaticTrait(Trait):
|
||||
"""
|
||||
Static Trait. This is a single value with a modifier,
|
||||
with no concept of a 'current' value.
|
||||
with no concept of a 'current' value or min/max etc.
|
||||
|
||||
value = base + mod
|
||||
|
||||
|
|
@ -964,6 +1121,16 @@ class StaticTrait(Trait):
|
|||
return "{name:12} {status} ({mod:+3})".format(name=self.name, status=status, mod=self.mod)
|
||||
|
||||
# Helpers
|
||||
@property
|
||||
def base(self):
|
||||
return self._data["base"]
|
||||
|
||||
@base.setter
|
||||
def base(self, value):
|
||||
if value is None:
|
||||
self._data["base"] = self.default_keys["base"]
|
||||
if type(value) in (int, float):
|
||||
self._data["base"] = value
|
||||
|
||||
@property
|
||||
def mod(self):
|
||||
|
|
@ -1374,13 +1541,13 @@ class GaugeTrait(CounterTrait):
|
|||
@max.setter
|
||||
def max(self, value):
|
||||
raise TraitException(
|
||||
"The .max property is not settable " "on GaugeTraits. Set .base instead."
|
||||
"The .max property is not settable on GaugeTraits. Set .mod and .base instead."
|
||||
)
|
||||
|
||||
@max.deleter
|
||||
def max(self):
|
||||
raise TraitException(
|
||||
"The .max property cannot be reset " "on GaugeTraits. Reset .mod and .base instead."
|
||||
"The .max property cannot be reset on GaugeTraits. Reset .mod and .base instead."
|
||||
)
|
||||
|
||||
@property
|
||||
|
|
|
|||
|
|
@ -2048,7 +2048,7 @@ def deepsize(obj, max_depth=4):
|
|||
_missing = object()
|
||||
|
||||
|
||||
class lazy_property(object):
|
||||
class lazy_property:
|
||||
"""
|
||||
Delays loading of property until first access. Credit goes to the
|
||||
Implementation in the werkzeug suite:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue