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.
|
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*
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
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
|
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
|
`.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,
|
This trait is useful for showing commonly depletable resources like health,
|
||||||
stamina and the like.
|
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
|
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
|
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
|
`.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
|
The `.rate` is particularly relevant for gauges - useful for everything
|
||||||
from poison slowly draining your health, to resting gradually increasing it.
|
from poison slowly draining your health, to resting gradually increasing it.
|
||||||
|
|
@ -329,7 +382,7 @@ class RageTrait(StaticTrait):
|
||||||
|
|
||||||
def sedate(self):
|
def sedate(self):
|
||||||
self.mod = 0
|
self.mod = 0
|
||||||
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue