mirror of
https://github.com/evennia/evennia.git
synced 2026-03-25 09:16:32 +01:00
Fix TraitProperty to not need trait_key and fix __get__ issues pointed out in discussions.
This commit is contained in:
parent
de13ad932e
commit
4aa999cd4a
2 changed files with 104 additions and 81 deletions
|
|
@ -905,9 +905,9 @@ class TestNumericTraitOperators(TestCase):
|
|||
|
||||
|
||||
class DummyCharacter(_MockObj):
|
||||
strength = traits.TraitProperty("str", trait_type="static", base=10, mod=2)
|
||||
hunting = traits.TraitProperty("hunting", trait_type="counter", base=10, mod=1, max=100)
|
||||
health = traits.TraitProperty("hp", trait_type="gauge", base=100)
|
||||
strength = traits.TraitProperty("Strength", trait_type="static", base=10, mod=2)
|
||||
hunting = traits.TraitProperty("Hunting skill", trait_type="counter", base=10, mod=1, max=100)
|
||||
health = traits.TraitProperty("Health value", trait_type="gauge", base=100)
|
||||
|
||||
|
||||
class TestTraitFields(TestCase):
|
||||
|
|
@ -919,8 +919,8 @@ class TestTraitFields(TestCase):
|
|||
@patch("evennia.contrib.traits._TRAIT_CLASS_PATHS", new=_TEST_TRAIT_CLASS_PATHS)
|
||||
def test_traitfields(self):
|
||||
obj = DummyCharacter()
|
||||
obj2 = 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)
|
||||
|
|
@ -931,5 +931,19 @@ class TestTraitFields(TestCase):
|
|||
obj.strength.berserk = True
|
||||
self.assertEqual(obj.strength.berserk, True)
|
||||
|
||||
self.assertEqual(100, obj.traits.hp)
|
||||
self.assertEqual(None, obj.traits.health) # the property name doesn't matter
|
||||
self.assertEqual(100, obj.traits.health)
|
||||
self.assertEqual(None, obj.traits.hp)
|
||||
|
||||
# the traithandler still works
|
||||
obj.traits.health.current -= 1
|
||||
self.assertEqual(99, obj.health.value)
|
||||
|
||||
# making sure Descriptors are separate
|
||||
self.assertEqual(12, obj2.strength.value)
|
||||
self.assertEqual(17, obj.strength.value)
|
||||
|
||||
obj2.strength.base += 1
|
||||
obj.strength.base += 3
|
||||
|
||||
self.assertEqual(13, obj2.strength.value)
|
||||
self.assertEqual(20, obj.strength.value)
|
||||
|
|
|
|||
|
|
@ -14,24 +14,26 @@ a server reload/reboot).
|
|||
|
||||
## Adding Traits to a typeclass
|
||||
|
||||
To access and manipulate traits on an entity, its Typeclass needs to have a
|
||||
`TraitHandler` assigned it. Usually, the handler is made available as `.traits`
|
||||
(in the same way as `.tags` or `.attributes`). It's recommended to do this
|
||||
using Evennia's `lazy_property` (which basically just means it's not
|
||||
initialized until it's actually accessed).
|
||||
There are two ways to set up Traits on a typeclass. The first sets up the `TraitHandler`
|
||||
as a property `.traits` on your class and you then access traits as e.g. `.traits.strength`.
|
||||
The other alternative uses a `TraitProperty`, which makes the trait available directly
|
||||
as e.g. `.strength`. This solution also uses the `TraitHandler`, but you don't need to
|
||||
define it explicitly. You can combine both styles if you like.
|
||||
|
||||
Here's an example for adding the TraitHandler to the base Object class:
|
||||
### Traits with TraitHandler
|
||||
|
||||
Here's an example for adding the TraitHandler to the Character class:
|
||||
|
||||
```python
|
||||
# mygame/typeclasses/objects.py
|
||||
|
||||
from evennia import DefaultObject
|
||||
from evennia import DefaultCharacter
|
||||
from evennia.utils import lazy_property
|
||||
from evennia.contrib.traits import TraitHandler
|
||||
|
||||
# ...
|
||||
|
||||
class Object(DefaultObject):
|
||||
class Character(DefaultCharacter):
|
||||
...
|
||||
@lazy_property
|
||||
def traits(self):
|
||||
|
|
@ -53,12 +55,11 @@ 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.
|
||||
### TraitProperties
|
||||
|
||||
Using `TraitProperties` makes the trait available directly on the class, much like Django model
|
||||
fields. The drawback is that you must make sure that the name of your Traits don't collide with any
|
||||
other properties/methods on your class.
|
||||
|
||||
```python
|
||||
# mygame/typeclasses/objects.py
|
||||
|
|
@ -71,17 +72,20 @@ from evennia.contrib.traits import TraitProperty
|
|||
|
||||
class Object(DefaultObject):
|
||||
...
|
||||
strength = TraitProperty("strength", "Strength", trait_type="static", base=10, mod=2)
|
||||
health = TraitProperty("hp", "Health", trait_type="gauge", min=0, base=100, mod=2)
|
||||
strength = TraitProperty("Strength", trait_type="static", base=10, mod=2)
|
||||
health = TraitProperty("Health", trait_type="gauge", min=0, base=100, mod=2)
|
||||
hunting = TraitProperty("Hunting Skill", trait_type="counter", base=10, mod=1, min=0, max=100)
|
||||
|
||||
```
|
||||
> Note that the property trait name ('str', 'hp' and 'hunting' above) are what will be stored in the
|
||||
> database and what you can access via `obj.traits` should you want to - so `obj.traits.hp` will be
|
||||
> available in the above example, and _not_ `obj.traits.health`. For simplicity you may want to put
|
||||
> the property-name and trait-key the same.
|
||||
|
||||
Most examples below use the `TraitHandler`, but all works with `TraitProperty` syntax as well.
|
||||
> Note that the property-name will become the name of the trait and you don't supply `trait_key`
|
||||
> separately.
|
||||
|
||||
> The `.traits` TraitHandler will still be created (it's used under the
|
||||
> hood. But it will only be created when the TraitProperty has been accessed at least once,
|
||||
> so be careful if mixing the two styles. If you want to make sure `.traits` is always available,
|
||||
> add the `TraitHandler` manually like shown earlier - the `TraitProperty` will by default use
|
||||
> the same handler (`.traits`).
|
||||
|
||||
## Using traits
|
||||
|
||||
|
|
@ -89,24 +93,29 @@ A trait is added to the traithandler (if you use `TraitProperty` the handler is
|
|||
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).
|
||||
|
||||
All traits have a _read-only_ field `.value`. This is only used to read out results, you never
|
||||
manipulate it directly (if you try, it will just remain unchanged). The `.value` is calculated based
|
||||
on combining fields, like `.base` and `.mod` - which fields are available and how they relate to
|
||||
each other depends on the trait type.
|
||||
|
||||
```python
|
||||
>>> obj.traits.strength.value
|
||||
12 # base + mod
|
||||
>>> obj.traits.strength.value += 5
|
||||
>>> obj.traits.strength.base += 5
|
||||
>>> obj.traits.strength.value
|
||||
17
|
||||
>>> obj.traits.hp.value
|
||||
102 # base + mod
|
||||
>>> obj.traits.hp -= 200
|
||||
>>> obj.traits.hp.base -= 200
|
||||
>>> obj.traits.hp.value
|
||||
0 # min of 0
|
||||
>>> obj.traits.hp.reset()
|
||||
>>> obj.traits.hp.value
|
||||
100
|
||||
# you can also access property with getitem
|
||||
# you can also access properties like a dict
|
||||
>>> obj.traits.hp["value"]
|
||||
100
|
||||
# you can store arbitrary data persistently as well
|
||||
# you can store arbitrary data persistently for easy reference
|
||||
>>> obj.traits.hp.effect = "poisoned!"
|
||||
>>> obj.traits.hp.effect
|
||||
"poisoned!"
|
||||
|
|
@ -337,10 +346,12 @@ A single value of any type.
|
|||
|
||||
This is the 'base' Trait, meant to inherit from if you want to invent
|
||||
trait-types from scratch (most of the time you'll probably inherit from some of
|
||||
the more advanced trait-type classes though). A `Trait`s `.value` can be
|
||||
anything (that can be stored in an Attribute) and if it's a integer/float you
|
||||
can do arithmetic with it, but otherwise it acts just like a glorified
|
||||
Attribute.
|
||||
the more advanced trait-type classes though).
|
||||
|
||||
Unlike other Trait-types, the single `.value` property of the base `Trait` can
|
||||
be editied. The value can hold any data that can be stored in an Attribute. If
|
||||
it's an integer/float you can do arithmetic with it, but otherwise this acts just
|
||||
like a glorified Attribute.
|
||||
|
||||
|
||||
```python
|
||||
|
|
@ -618,7 +629,7 @@ class TraitHandler:
|
|||
name (str, optional): Name of the Trait, like "Health". If
|
||||
not given, will use `trait_key` starting with a capital letter.
|
||||
trait_type (str, optional): One of 'static', 'counter' or 'gauge'.
|
||||
force_add (bool): If set, create a new Trait even if a Trait with
|
||||
force (bool): If set, create a new Trait even if a Trait with
|
||||
the same `trait_key` already exists.
|
||||
trait_properties (dict): These will all be use to initialize
|
||||
the new trait. See the `properties` class variable on each
|
||||
|
|
@ -694,56 +705,36 @@ class TraitProperty:
|
|||
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
trait_key,
|
||||
**kwargs):
|
||||
def __init__(self, name=None, trait_type=DEFAULT_TRAIT_TYPE, force=True, **trait_properties):
|
||||
"""
|
||||
Initialize a TraitField.
|
||||
Initialize a TraitField. Mimics TraitHandler.add input except no `trait_key`.
|
||||
|
||||
Args:
|
||||
trait_key (str): Name of Trait.
|
||||
name (str, optional): Name of the Trait, like "Health". If
|
||||
not given, will use `trait_key` starting with a capital letter.
|
||||
trait_type (str, optional): One of 'static', 'counter' or 'gauge'.
|
||||
force (bool): If set, create a new Trait even if a Trait with
|
||||
the same `trait_key` already exists.
|
||||
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.
|
||||
any: All other trait_properties are the same as for adding a new trait of the given type
|
||||
using the normal TraitHandler.
|
||||
|
||||
"""
|
||||
_SA(self, "trait_key", trait_key)
|
||||
traithandler_name = kwargs.pop("traithandler_name", "traits")
|
||||
_SA(self, 'traithandler_name', traithandler_name)
|
||||
_SA(self, 'trait_properties', kwargs)
|
||||
self._traithandler_name = trait_properties.pop("traithandler_name", "traits")
|
||||
|
||||
@property
|
||||
def traithandler(self):
|
||||
trait_properties.update({"name": name, "trait_type": trait_type, "force": force})
|
||||
self._trait_properties = trait_properties
|
||||
self._cache = {}
|
||||
|
||||
def __set_name__(self, instance, name):
|
||||
"""
|
||||
Get/create the underlying traithandler.
|
||||
This is called the very first time the Descriptor is assigned to the
|
||||
class; we store it so we can create new instances with this later.
|
||||
|
||||
"""
|
||||
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
|
||||
self._trait_key = name
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
"""
|
||||
|
|
@ -754,17 +745,35 @@ class TraitProperty:
|
|||
Returns:
|
||||
Trait: The trait this property represents.
|
||||
|
||||
Notes:
|
||||
We have one descriptor on the class, but we don't want each instance to share the
|
||||
state (self) of that descriptor. So we must make sure to cache the trait per-instance
|
||||
or we would end up with cross-use between instances.
|
||||
|
||||
"""
|
||||
try:
|
||||
_GA(self, "obj")
|
||||
except AttributeError:
|
||||
if instance:
|
||||
_SA(self, "obj", instance)
|
||||
return self.trait
|
||||
if instance not in self._cache:
|
||||
try:
|
||||
traithandler = getattr(instance, self._traithandler_name)
|
||||
except AttributeError:
|
||||
# traithandler not found; create a new on-demand
|
||||
traithandler = TraitHandler(instance)
|
||||
setattr(instance, self._traithandler_name, traithandler)
|
||||
|
||||
# this will either get the trait from attribute or make a new one
|
||||
trait = traithandler.get(self._trait_key)
|
||||
if trait is None:
|
||||
# initialize the trait
|
||||
traithandler.add(
|
||||
self._trait_key,
|
||||
**self._trait_properties
|
||||
)
|
||||
trait = traithandler.get(self._trait_key) # caches it in the traithandler
|
||||
self._cache[instance] = trait
|
||||
return self._cache[instance]
|
||||
|
||||
def __set__(self, instance, value):
|
||||
"""
|
||||
We don't set data directly on this - it's all rerouted to the trait.
|
||||
We don't set data directly, it's all rerouted to the trait.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue