Add TraitProperties as alternative way to define Traits from the traits contrib. Also clean up docs to resolve #2450.

This commit is contained in:
Griatch 2021-08-30 22:33:21 +02:00
parent 6e975236eb
commit 78e063d9ca
4 changed files with 240 additions and 30 deletions

View file

@ -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. 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 ### Turnbattle
*FlutterSprite 2017* *FlutterSprite 2017*

View file

@ -10,8 +10,7 @@ from copy import copy
from anything import Something from anything import Something
from mock import MagicMock, patch from mock import MagicMock, patch
from django.test import TestCase from django.test import TestCase
from django.test import override_settings from evennia.utils.utils import lazy_property
from evennia.utils.test_resources import EvenniaTest
from evennia.contrib import traits from evennia.contrib import traits
@ -903,3 +902,36 @@ class TestNumericTraitOperators(TestCase):
self.assertGreaterEqual(8, self.st) self.assertGreaterEqual(8, self.st)
self.assertGreaterEqual(self.st, 0) self.assertGreaterEqual(self.st, 0)
self.assertGreaterEqual(10, self.st) 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)

View file

@ -38,29 +38,78 @@ class Object(DefaultObject):
# this adds the handler as .traits # this adds the handler as .traits
return TraitHandler(self) 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 ## Using traits
A trait is added to the traithandler, after which one can access it A trait is added to the traithandler (if you use `TraitProperty` the handler is just created under
as a property on the handler (similarly to how you can do .db.attrname for Attributes the hood) after which one can access it as a property on the handler (similarly to how you can do
in Evennia). .db.attrname for Attributes in Evennia).
```python ```python
# this is an example using the "static" trait, described below >>> obj.traits.strength.value
12 # base + mod
>>> obj.traits.add("hunting", "Hunting Skill", trait_type="static", base=4) >>> obj.traits.strength.value += 5
>>> obj.traits.hunting.value >>> obj.traits.strength.value
4 17
>>> obj.traits.hunting.value += 5
>>> obj.traits.hunting.value
9
>>> obj.traits.add("hp", "Health", trait_type="gauge", min=0, max=100)
>>> obj.traits.hp.value >>> obj.traits.hp.value
100 102 # base + mod
>>> obj.traits.hp -= 200 >>> obj.traits.hp -= 200
>>> obj.traits.hp.value >>> obj.traits.hp.value
0 0 # min of 0
>>> obj.traits.hp.reset() >>> obj.traits.hp.reset()
>>> obj.traits.hp.value >>> obj.traits.hp.value
100 100
@ -72,12 +121,16 @@ in Evennia).
>>> obj.traits.hp.effect >>> obj.traits.hp.effect
"poisoned!" "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 ## Trait types
@ -435,12 +488,22 @@ class MandatoryTraitKey:
This represents a required key that must be This represents a required key that must be
supplied when a Trait is initialized. It's used supplied when a Trait is initialized. It's used
by Trait classes when defining their required keys. by Trait classes when defining their required keys.
"""
"""
class TraitHandler: 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: Args:
obj (Object): Parent Object typeclass for this TraitHandler 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 # load the available classes, if necessary
_delayed_import_trait_classes() _delayed_import_trait_classes()
# initialize any
# Note that .trait_data retains the connection to the database, meaning every # Note that .trait_data retains the connection to the database, meaning every
# update we do to .trait_data automatically syncs with database. # update we do to .trait_data automatically syncs with database.
self.trait_data = obj.attributes.get(db_attribute_key, category=db_attribute_category) self.trait_data = obj.attributes.get(db_attribute_key, category=db_attribute_category)
@ -615,6 +682,96 @@ class TraitHandler:
self.remove(trait_key) 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 # Parent Trait class
@ -949,7 +1106,7 @@ class Trait:
class StaticTrait(Trait): class StaticTrait(Trait):
""" """
Static Trait. This is a single value with a modifier, 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 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) return "{name:12} {status} ({mod:+3})".format(name=self.name, status=status, mod=self.mod)
# Helpers # 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 @property
def mod(self): def mod(self):
@ -1374,13 +1541,13 @@ class GaugeTrait(CounterTrait):
@max.setter @max.setter
def max(self, value): def max(self, value):
raise TraitException( 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 @max.deleter
def max(self): def max(self):
raise TraitException( 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 @property

View file

@ -2048,7 +2048,7 @@ def deepsize(obj, max_depth=4):
_missing = object() _missing = object()
class lazy_property(object): class lazy_property:
""" """
Delays loading of property until first access. Credit goes to the Delays loading of property until first access. Credit goes to the
Implementation in the werkzeug suite: Implementation in the werkzeug suite: