mirror of
https://github.com/evennia/evennia.git
synced 2026-03-25 09:16:32 +01:00
Cleanup and simplifications of Trait props
Remove NumericTrait type, instead incorporating it into the Trait base. Change .actual to .value to homogenize the interface for all Trait subclasses. Rename data_keys to default_keys. Improve and correct documentation in several places.
This commit is contained in:
parent
39c7889336
commit
06cb85ea6c
2 changed files with 187 additions and 257 deletions
|
|
@ -34,7 +34,6 @@ class _MockObj:
|
|||
# we want to test the base traits too
|
||||
_TEST_TRAIT_CLASS_PATHS = [
|
||||
"evennia.contrib.traits.Trait",
|
||||
"evennia.contrib.traits.NumericTrait",
|
||||
"evennia.contrib.traits.StaticTrait",
|
||||
"evennia.contrib.traits.CounterTrait",
|
||||
"evennia.contrib.traits.GaugeTrait",
|
||||
|
|
@ -169,7 +168,7 @@ class TraitHandlerTest(_TraitHandlerBase):
|
|||
)
|
||||
|
||||
|
||||
class TraitTest(_TraitHandlerBase):
|
||||
class TestTrait(_TraitHandlerBase):
|
||||
"""
|
||||
Test the base Trait class
|
||||
"""
|
||||
|
|
@ -217,7 +216,7 @@ class TraitTest(_TraitHandlerBase):
|
|||
"extra_val": 1000
|
||||
}
|
||||
expected = copy(dat)
|
||||
expected["value"] = traits.Trait.data_keys['value']
|
||||
expected["value"] = traits.Trait.default_keys['value']
|
||||
self.assertEqual(expected, traits.Trait.validate_input(traits.Trait, dat))
|
||||
|
||||
# make sure extra values are cleaned if trait accepts no extras
|
||||
|
|
@ -246,10 +245,10 @@ class TraitTest(_TraitHandlerBase):
|
|||
traits.Trait.validate_input(traits.Trait, dat)
|
||||
|
||||
# make value a required key
|
||||
mock_data_keys = {
|
||||
mock_default_keys = {
|
||||
"value": traits.MandatoryTraitKey
|
||||
}
|
||||
with patch.object(traits.Trait, "data_keys", mock_data_keys):
|
||||
with patch.object(traits.Trait, "default_keys", mock_default_keys):
|
||||
dat = {
|
||||
"name": "Trait",
|
||||
"trait_type": "trait",
|
||||
|
|
@ -288,54 +287,13 @@ class TraitTest(_TraitHandlerBase):
|
|||
self.trait.extra_val1
|
||||
del self.trait.value
|
||||
# fall back to default
|
||||
self.assertTrue(self.trait.value == traits.Trait.data_keys["value"])
|
||||
self.assertTrue(self.trait.value == traits.Trait.default_keys["value"])
|
||||
|
||||
def test_repr(self):
|
||||
self.assertEqual(repr(self.trait), Something)
|
||||
self.assertEqual(str(self.trait), Something)
|
||||
|
||||
|
||||
class TestTraitNumeric(_TraitHandlerBase):
|
||||
"""
|
||||
Test the numeric base class
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.traithandler.add(
|
||||
"test1",
|
||||
name="Test1",
|
||||
trait_type='numeric',
|
||||
base=1,
|
||||
extra_val1="xvalue1",
|
||||
extra_val2="xvalue2"
|
||||
)
|
||||
self.trait = self.traithandler.get("test1")
|
||||
|
||||
def _get_actuals(self):
|
||||
"""Get trait actuals for comparisons"""
|
||||
return self.trait.actual, self.trait2.actual
|
||||
|
||||
def test_init(self):
|
||||
self.assertEqual(
|
||||
self.trait._data,
|
||||
{"name": "Test1",
|
||||
"trait_type": "numeric",
|
||||
"base": 1,
|
||||
"extra_val1": "xvalue1",
|
||||
"extra_val2": "xvalue2"
|
||||
}
|
||||
)
|
||||
|
||||
def test_set_wrong_type(self):
|
||||
self.trait.base = "foo"
|
||||
self.assertEqual(self.trait.base, 1)
|
||||
|
||||
def test_actual(self):
|
||||
self.trait.base = 10
|
||||
self.assertEqual(self.trait.actual, 10)
|
||||
|
||||
|
||||
class TestTraitStatic(_TraitHandlerBase):
|
||||
"""
|
||||
Test for static Traits
|
||||
|
|
@ -354,7 +312,7 @@ class TestTraitStatic(_TraitHandlerBase):
|
|||
self.trait = self.traithandler.get("test1")
|
||||
|
||||
def _get_values(self):
|
||||
return self.trait.base, self.trait.mod, self.trait.actual
|
||||
return self.trait.base, self.trait.mod, self.trait.value
|
||||
|
||||
def test_init(self):
|
||||
self.assertEqual(
|
||||
|
|
@ -368,8 +326,8 @@ class TestTraitStatic(_TraitHandlerBase):
|
|||
}
|
||||
)
|
||||
|
||||
def test_actual(self):
|
||||
"""Actual is base + mod"""
|
||||
def test_value(self):
|
||||
"""value is base + mod"""
|
||||
self.assertEqual(self._get_values(), (1, 2, 3))
|
||||
self.trait.base += 4
|
||||
self.assertEqual(self._get_values(), (5, 2, 7))
|
||||
|
|
@ -410,9 +368,9 @@ class TestTraitCounter(_TraitHandlerBase):
|
|||
self.trait = self.traithandler.get("test1")
|
||||
|
||||
def _get_values(self):
|
||||
"""Get (base, mod, actual, min, max)."""
|
||||
"""Get (base, mod, value, min, max)."""
|
||||
return (self.trait.base, self.trait.mod,
|
||||
self.trait.actual, self.trait.min, self.trait.max)
|
||||
self.trait.value, self.trait.min, self.trait.max)
|
||||
|
||||
def test_init(self):
|
||||
self.assertEqual(
|
||||
|
|
@ -437,8 +395,8 @@ class TestTraitCounter(_TraitHandlerBase):
|
|||
}
|
||||
)
|
||||
|
||||
def test_actual(self):
|
||||
"""Actual is current + mod, where current defaults to base"""
|
||||
def test_value(self):
|
||||
"""value is current + mod, where current defaults to base"""
|
||||
self.assertEqual(self._get_values(), (1, 2, 3, 0, 10))
|
||||
self.trait.base += 4
|
||||
self.assertEqual(self._get_values(), (5, 2, 7, 0, 10))
|
||||
|
|
@ -600,7 +558,7 @@ class TestTraitCounterTimed(_TraitHandlerBase):
|
|||
self.trait = self.traithandler.get("test1")
|
||||
|
||||
def _get_timer_data(self):
|
||||
return (self.trait.actual, self.trait.current, self.trait.rate,
|
||||
return (self.trait.value, self.trait.current, self.trait.rate,
|
||||
self.trait._data["last_update"], self.trait.ratetarget)
|
||||
|
||||
@patch("evennia.contrib.traits.time")
|
||||
|
|
@ -671,8 +629,8 @@ class TestTraitGauge(_TraitHandlerBase):
|
|||
self.trait = self.traithandler.get("test1")
|
||||
|
||||
def _get_values(self):
|
||||
"""Get (base, mod, actual, min, max)."""
|
||||
return (self.trait.base, self.trait.mod, self.trait.actual,
|
||||
"""Get (base, mod, value, min, max)."""
|
||||
return (self.trait.base, self.trait.mod, self.trait.value,
|
||||
self.trait.min, self.trait.max)
|
||||
|
||||
def test_init(self):
|
||||
|
|
@ -696,8 +654,8 @@ class TestTraitGauge(_TraitHandlerBase):
|
|||
"last_update": None,
|
||||
}
|
||||
)
|
||||
def test_actual(self):
|
||||
"""Actual is current, where current defaults to base + mod"""
|
||||
def test_value(self):
|
||||
"""value is current, where current defaults to base + mod"""
|
||||
# current unset - follows base + mod
|
||||
self.assertEqual(self._get_values(), (8, 2, 10, 0, 10))
|
||||
self.trait.base += 4
|
||||
|
|
@ -864,7 +822,7 @@ class TestTraitGaugeTimed(_TraitHandlerBase):
|
|||
self.trait = self.traithandler.get("test1")
|
||||
|
||||
def _get_timer_data(self):
|
||||
return (self.trait.actual, self.trait.current, self.trait.rate,
|
||||
return (self.trait.value, self.trait.current, self.trait.rate,
|
||||
self.trait._data["last_update"], self.trait.ratetarget)
|
||||
|
||||
@patch("evennia.contrib.traits.time")
|
||||
|
|
@ -919,24 +877,24 @@ class TestNumericTraitOperators(TestCase):
|
|||
"""Test case for numeric magic method implementations."""
|
||||
def setUp(self):
|
||||
# direct instantiation for testing only; use TraitHandler in production
|
||||
self.st = traits.NumericTrait({
|
||||
self.st = traits.Trait({
|
||||
'name': 'Strength',
|
||||
'trait_type': 'numeric',
|
||||
'base': 8,
|
||||
'trait_type': 'trait',
|
||||
'value': 8,
|
||||
})
|
||||
self.at = traits.NumericTrait({
|
||||
self.at = traits.Trait({
|
||||
'name': 'Attack',
|
||||
'trait_type': 'numeric',
|
||||
'base': 4,
|
||||
'trait_type': 'trait',
|
||||
'value': 4,
|
||||
})
|
||||
|
||||
def tearDown(self):
|
||||
self.st, self.at = None, None
|
||||
|
||||
def test_pos_shortcut(self):
|
||||
"""overridden unary + operator returns `actual` property"""
|
||||
"""overridden unary + operator returns `value` property"""
|
||||
self.assertIn(type(+self.st), (float, int))
|
||||
self.assertEqual(+self.st, self.st.actual)
|
||||
self.assertEqual(+self.st, self.st.value)
|
||||
self.assertEqual(+self.st, 8)
|
||||
|
||||
def test_add_traits(self):
|
||||
|
|
|
|||
|
|
@ -46,8 +46,8 @@ 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).
|
||||
|
||||
|
||||
```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
|
||||
|
|
@ -55,21 +55,25 @@ in Evennia).
|
|||
>>> obj.traits.hunting.value
|
||||
9
|
||||
>>> obj.traits.add("hp", "Health", trait_type="gauge", min=0, max=100)
|
||||
>>> obj.traits.hp.actual
|
||||
>>> obj.traits.hp.value
|
||||
100
|
||||
>>> obj.traits.hp -= 200
|
||||
>>> obj.traits.hp.actual
|
||||
>>> obj.traits.hp.value
|
||||
0
|
||||
>>> obj.traits.hp.reset()
|
||||
>>> obj.traits.hp.actual
|
||||
>>> obj.traits.hp.value
|
||||
100
|
||||
# you can also access property with getitem
|
||||
>>> obj.traits.hp["actual"]
|
||||
>>> obj.traits.hp["value"]
|
||||
100
|
||||
# you can store arbitrary data persistently as well
|
||||
>>> obj.traits.hp.effect = "poisoned!"
|
||||
>>> obj.traits.hp.effect
|
||||
"poisoned!"
|
||||
|
||||
```
|
||||
|
||||
When creating the trait, you supply the name of the property (`hunting`) along
|
||||
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.
|
||||
|
|
@ -77,11 +81,23 @@ of trait this is.
|
|||
|
||||
## Trait types
|
||||
|
||||
All the default-available traits are number-based. They all have a read-only
|
||||
`actual` property that shows the relevant value. Exactly what this means depends
|
||||
on the type of trait.
|
||||
All default traits have a read-only `.value` property that shows the relevant or
|
||||
'current' value of the trait. Exactly what this means depends on the type of trait.
|
||||
|
||||
Traits can also be combined to do arithmetic with their .value, if both have a
|
||||
compatible type.
|
||||
|
||||
```python
|
||||
>>> trait1 + trait2
|
||||
54
|
||||
>>> trait1.value
|
||||
3
|
||||
>>> trait1 + 2
|
||||
>>> trait1.value
|
||||
5
|
||||
|
||||
```
|
||||
|
||||
Numerical traits can also be combined to do arithmetic with their .actual values.
|
||||
Two numerical traits can also be compared (bigger-than etc), which is useful in
|
||||
all sorts of rule-resolution.
|
||||
|
||||
|
|
@ -91,55 +107,52 @@ if trait1 > trait2:
|
|||
# do stuff
|
||||
|
||||
```
|
||||
## Static trait
|
||||
|
||||
## Static traits
|
||||
`value = base + mod`
|
||||
|
||||
|
||||
actual = base + mod
|
||||
|
||||
|
||||
The static trait has
|
||||
a `base` value and an optional `mod`-ifier. A typical use of a static trait
|
||||
would be a Strength stat or Skill value. That is, something that varies slowly or
|
||||
not at all, and which can have modifier.
|
||||
The static trait has a `base` value and an optional `mod`-ifier. A typical use
|
||||
of a static trait would be a Strength stat or Skill value. That is, something
|
||||
that varies slowly or not at all, and which may be modified in-place.
|
||||
|
||||
```python
|
||||
>>> obj.traits.add("str", "Strength", trait_type="static", base=10, mod=2)
|
||||
>>> obj.traits.mytrait.actual
|
||||
>>> obj.traits.mytrait.value
|
||||
12 # base + mod
|
||||
>>> obj.traits.mytrait.base += 2
|
||||
>>> obj.traits.mytrait.mod += 1
|
||||
>>> obj.traits.mytrait.actual
|
||||
>>> obj.traits.mytrait.value
|
||||
15
|
||||
>>> obj.traits.mytrait.mod = 0
|
||||
>>> obj.traits.mytrait.actual
|
||||
>>> obj.traits.mytrait.value
|
||||
12
|
||||
|
||||
```
|
||||
|
||||
### Counter
|
||||
|
||||
min/unset base base+mod max/unset
|
||||
|--------------|--------|---------X--------X------------|
|
||||
current actual
|
||||
current value
|
||||
= current
|
||||
+ mod
|
||||
|
||||
A counter describes a value that varies from a base value. The `current` property
|
||||
starts at the `base` and tracks the current value. One can also add a modifier,
|
||||
which will both be added to the base and to current (forming actual).
|
||||
A counter describes a value that can move from a base. The `current` property
|
||||
is the thing usually modified. It starts at the `base`. One can also add a modifier,
|
||||
which will both be added to the base and to current (forming .value).
|
||||
The min/max of the range are optional, a boundary set to None will remove it.
|
||||
|
||||
```python
|
||||
>>> obj.traits.add("hunting", "Hunting Skill", trait_type="counter",
|
||||
base=10, mod=1, min=0, max=100)
|
||||
>>> obj.traits.hunting.actual
|
||||
>>> obj.traits.hunting.value
|
||||
11 # current starts at base + mod
|
||||
>>> obj.traits.hunting.current += 10
|
||||
>>> obj.traits.hunting.actual
|
||||
>>> obj.traits.hunting.value
|
||||
21
|
||||
# reset back to base+mod by deleting
|
||||
# reset back to base+mod by deleting current
|
||||
>>> del obj.traits.hunting.current
|
||||
>>> obj.traits.hunting.actual
|
||||
>>> obj.traits.hunting.value
|
||||
11
|
||||
>>> obj.traits.hunting.max = None # removing upper bound
|
||||
|
||||
|
|
@ -147,60 +160,71 @@ The min/max of the range are optional, a boundary set to None will remove it.
|
|||
|
||||
Counters have some extra properties:
|
||||
|
||||
`descs` is a dict of upper-bounds to a text description. This allows for easily
|
||||
storing getting a more human-friendly description of the current value in the
|
||||
`descs` is a dict {upper_bound:text_description}. This allows for easily
|
||||
storing a more human-friendly description of the current value in the
|
||||
interval. Here is an example for skill values between 0 and 10:
|
||||
{0: "unskilled", 1: "neophyte", 5: "trained", 7: "expert", 9: "master"}
|
||||
The list must go from smallest to largest. Any values below the lowest and above the
|
||||
The keys must be supplied from smallest to largest. Any values below the lowest and above the
|
||||
highest description will be considered to be included in the closest description slot.
|
||||
By calling `.desc()` on the Counter, will you get the text matching the current `actual`
|
||||
By calling `.desc()` on the Counter, will you get the text matching the current `value`
|
||||
value.
|
||||
|
||||
```python
|
||||
# (could also have passed descs= to traits.add())
|
||||
>>> obj.traits.hunting.descs = {
|
||||
0: "unskilled", 10: "neophyte", 50: "trained", 70: "expert", 90: "master"}
|
||||
>>> obj.traits.hunting.actual
|
||||
>>> obj.traits.hunting.value
|
||||
11
|
||||
>>> obj.traits.hunting.desc()
|
||||
"neophyte"
|
||||
>>> obj.traits.hunting.current += 60
|
||||
>>> obj.traits.hunting.actual
|
||||
>>> obj.traits.hunting.value
|
||||
71
|
||||
>>> obj.traits.hunting.desc()
|
||||
"expert"
|
||||
|
||||
```
|
||||
|
||||
`rate` defaults to 0, but allows the trait to change value dynamically. This could be
|
||||
used for example for an attribute that was temporarily lowered but will gradually
|
||||
(or abruptly) recover after a certain time. The rate is given per-second, and the value
|
||||
will still be restrained by min/max boundaries, if given.
|
||||
#### .rate
|
||||
|
||||
It is also possible to set a "ratetarget", for the auto-change to stop at (rather
|
||||
than at the min/max boundaries). This allows for returning to some previous value.
|
||||
The `rate` property defaults to 0. If set to a value different from 0, it
|
||||
allows the trait to change value dynamically. This could be used for example
|
||||
for an attribute that was temporarily lowered but will gradually (or abruptly)
|
||||
recover after a certain time. The rate is given as change of the `current`
|
||||
per-second, and the .value will still be restrained by min/max boundaries, if
|
||||
those are set.
|
||||
|
||||
It is also possible to set a ".ratetarget", for the auto-change to stop at
|
||||
(rather than at the min/max boundaries). This allows the value to return to
|
||||
a previous value.
|
||||
|
||||
```python
|
||||
|
||||
>>> obj.traits.hunting.actual
|
||||
>>> obj.traits.hunting.value
|
||||
71
|
||||
>>> obj.traits.hunting.ratetarget = 71
|
||||
# debuff hunting for some reason
|
||||
>>> obj.traits.hunting.current -= 30
|
||||
>>> obj.traits.hunting.actual
|
||||
>>> obj.traits.hunting.value
|
||||
41
|
||||
>>> obj.traits.hunting.rate = 1 # 1/s increase
|
||||
# Waiting 5s
|
||||
>>> obj.traits.hunting.actual
|
||||
>>> obj.traits.hunting.value
|
||||
46
|
||||
# Waiting 8s
|
||||
>>> obj.traits.hunting.actual
|
||||
>>> obj.traits.hunting.value
|
||||
54
|
||||
# Waiting 100s
|
||||
>>> obj.traits.hunting.actual
|
||||
>>> obj.traits.hunting.value
|
||||
71 # we have stopped at the ratetarget
|
||||
>>> obj.traits.hunting.rate = 0 # disable auto-change
|
||||
|
||||
```
|
||||
Note that if rate is a non-integer, the resulting .value (at least until it
|
||||
reaches the boundary) will likely also come out a float. If you expect an
|
||||
integer, you must run run int() on the result yourself.
|
||||
|
||||
#### .percentage()
|
||||
|
||||
If both min and max are defined, the `.percentage()` method of the trait will
|
||||
return the value as a percentage.
|
||||
|
|
@ -211,31 +235,30 @@ return the value as a percentage.
|
|||
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
### Gauge
|
||||
|
||||
This emulates a [fuel-] gauge, that empties from a base+mod value.
|
||||
This emulates a [fuel-] gauge that empties from a base+mod value.
|
||||
|
||||
min/0 max=base+mod
|
||||
|-----------------------X---------------------------|
|
||||
actual
|
||||
value
|
||||
= current
|
||||
|
||||
The 'current' value will be with a full gauge. Modifiers only add to the maximum,
|
||||
which is set by base + mod. The minimum bound defaults to 0. This trait is useful
|
||||
for showing resources that can deplete, like health or stamina etc.
|
||||
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 modifier
|
||||
only applies to the max value of the gauge and not the current value. The
|
||||
minimum bound defaults to 0. This trait is useful for showing resources that
|
||||
can deplete, like health, stamina and the like.
|
||||
|
||||
```python
|
||||
>>> obj.traits.add("hp", "Health", trait_type="gauge", base=100)
|
||||
>>> obj.traits.hp.actual # (or .current)
|
||||
>>> obj.traits.hp.value # (or .current)
|
||||
100
|
||||
>>> obj.traits.hp.mod = 10
|
||||
>>> obj.traits.hp.actual
|
||||
>>> obj.traits.hp.value
|
||||
110
|
||||
>>> obj.traits.hp.current -= 30
|
||||
>>> obj.traits.hp.actual
|
||||
>>> obj.traits.hp.value
|
||||
80
|
||||
|
||||
```
|
||||
|
|
@ -246,14 +269,15 @@ for gauges, for everything from poison slowly draining your health, to resting g
|
|||
increasing it. You can also use the `.percentage()` function to show the current value
|
||||
as a percentage.
|
||||
|
||||
|
||||
### Trait
|
||||
|
||||
A single value of any type.
|
||||
|
||||
This is not a numerical trait and does not have an .actual property. This is
|
||||
the 'base' Trait, meant to inherit from if you want to make your own
|
||||
trait-types (see below), but you can also use it directly:
|
||||
This is the 'base' Trait, meant to inherit from if you want to make your own
|
||||
trait-types (see below). Its .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.
|
||||
|
||||
|
||||
```python
|
||||
>>> obj.traits.add("mytrait", "My Trait", trait_type="trait", value=30)
|
||||
|
|
@ -264,17 +288,6 @@ trait-types (see below), but you can also use it directly:
|
|||
"stringvalue"
|
||||
|
||||
```
|
||||
The "trait" trait-type is little more than a glorified Attribute. It has a .value
|
||||
that can be anything, and nothing more fancy. It's meant to expand on.
|
||||
|
||||
### NumericTrait
|
||||
|
||||
A single value, actual = base
|
||||
|
||||
This is a base class for the numeric traits. Basically the Static Trait but
|
||||
without the modifier. Is useful to inherit from. It adds arithmetic so that
|
||||
you can add two traits together, do comparisons between them etc.
|
||||
|
||||
|
||||
## Expanding with your own Traits
|
||||
|
||||
|
|
@ -289,7 +302,7 @@ from evennia.contrib.traits import Trait
|
|||
class RageTrait(Trait):
|
||||
|
||||
trait_type = "rage"
|
||||
data_keys = {
|
||||
default_keys = {
|
||||
"rage": 0
|
||||
}
|
||||
|
||||
|
|
@ -334,6 +347,7 @@ from evennia.utils.utils import (
|
|||
# "counter" and "gauge".
|
||||
|
||||
_TRAIT_CLASS_PATHS = [
|
||||
"evennia.contrib.traits.Trait",
|
||||
"evennia.contrib.traits.StaticTrait",
|
||||
"evennia.contrib.traits.CounterTrait",
|
||||
"evennia.contrib.traits.GaugeTrait",
|
||||
|
|
@ -443,7 +457,7 @@ class TraitHandler:
|
|||
_SA(self, trait_key, value)
|
||||
else:
|
||||
trait_cls = self._get_trait_class(trait_key=trait_key)
|
||||
valid_keys = list_to_string(list(trait_cls.data_keys.keys()), endsep="or")
|
||||
valid_keys = list_to_string(list(trait_cls.default_keys.keys()), endsep="or")
|
||||
raise TraitException(
|
||||
"Trait object not settable directly. "
|
||||
f"Assign to {trait_key}.{valid_keys}."
|
||||
|
|
@ -578,7 +592,7 @@ class TraitHandler:
|
|||
|
||||
# Parent Trait class
|
||||
|
||||
|
||||
@total_ordering
|
||||
class Trait:
|
||||
"""Represents an object or Character trait. This simple base is just
|
||||
storing anything in it's 'value' property, so it's pretty much just a
|
||||
|
|
@ -600,7 +614,7 @@ class Trait:
|
|||
# not given, set the default value to the `traits.MandatoryTraitKey` class.
|
||||
# Apart from the keys given here, "name" and "trait_type" will also always
|
||||
# have to be a apart of the data.
|
||||
data_keys = {"value": None}
|
||||
default_keys = {"value": None}
|
||||
|
||||
# enable to set/retrieve other arbitrary properties on the Trait
|
||||
# and have them treated like data to store.
|
||||
|
|
@ -615,7 +629,7 @@ class Trait:
|
|||
|
||||
Args:
|
||||
trait_data (any): Any pickle-able values to store with this trait.
|
||||
This must contain any cls.data_keys that do not have a default
|
||||
This must contain any cls.default_keys that do not have a default
|
||||
value in cls.data_default_values. Any extra kwargs will be made
|
||||
available as extra properties on the Trait, assuming the class
|
||||
variable `allow_extra_properties` is set.
|
||||
|
|
@ -642,7 +656,7 @@ class Trait:
|
|||
initialization of this trait.
|
||||
Returns:
|
||||
dict: Validated data, possibly complemented with default
|
||||
values from data_keys.
|
||||
values from default_keys.
|
||||
Raises:
|
||||
TraitException: If finding unset keys without a default.
|
||||
|
||||
|
|
@ -663,9 +677,9 @@ class Trait:
|
|||
_raise_err(unsets)
|
||||
|
||||
# check other keys, these likely have defaults to fall back to
|
||||
req = set(list(cls.data_keys.keys()))
|
||||
req = set(list(cls.default_keys.keys()))
|
||||
unsets = req.difference(inp.intersection(req))
|
||||
unset_defaults = {key: cls.data_keys[key] for key in unsets}
|
||||
unset_defaults = {key: cls.default_keys[key] for key in unsets}
|
||||
|
||||
if MandatoryTraitKey in unset_defaults.values():
|
||||
# we have one or more unset keys that was mandatory
|
||||
|
|
@ -701,7 +715,7 @@ class Trait:
|
|||
|
||||
def __getattr__(self, key):
|
||||
"""Access extra parameters as attributes."""
|
||||
if key in ("data_keys", "data_default", "trait_type", "allow_extra_properties"):
|
||||
if key in ("default_keys", "data_default", "trait_type", "allow_extra_properties"):
|
||||
return _GA(self, key)
|
||||
try:
|
||||
return self._data[key]
|
||||
|
|
@ -752,18 +766,18 @@ class Trait:
|
|||
without a default value to reset to.
|
||||
Notes:
|
||||
This will outright delete extra keys (if allow_extra_properties is
|
||||
set). Keys in self.data_keys with a default value will be
|
||||
set). Keys in self.default_keys with a default value will be
|
||||
reset to default. A data_key with a default of MandatoryDefaultKey
|
||||
will raise a TraitException. Unfound matches will be silently ignored.
|
||||
|
||||
"""
|
||||
if key in self.data_keys:
|
||||
if self.data_keys[key] == MandatoryTraitKey:
|
||||
if key in self.default_keys:
|
||||
if self.default_keys[key] == MandatoryTraitKey:
|
||||
raise TraitException(
|
||||
"Trait-Key {key} cannot be deleted: It's a mandatory property "
|
||||
"with no default value to fall back to.")
|
||||
# set to default
|
||||
self._data[key] = self.data_keys[key]
|
||||
self._data[key] = self.default_keys[key]
|
||||
elif key in self._data:
|
||||
try:
|
||||
# check if we have a custom deleter
|
||||
|
|
@ -783,7 +797,7 @@ class Trait:
|
|||
return "{}({{{}}})".format(
|
||||
type(self).__name__,
|
||||
", ".join(
|
||||
["'{}': {!r}".format(k, self._data[k]) for k in self.data_keys if k in self._data]
|
||||
["'{}': {!r}".format(k, self._data[k]) for k in self.default_keys if k in self._data]
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -799,37 +813,6 @@ class Trait:
|
|||
|
||||
key = name
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
"""Store a value"""
|
||||
return self._data["value"]
|
||||
|
||||
@value.setter
|
||||
def value(self, value):
|
||||
"""Get value"""
|
||||
self._data["value"] = value
|
||||
|
||||
|
||||
@total_ordering
|
||||
class NumericTrait(Trait):
|
||||
"""
|
||||
Base trait for all Traits based on numbers. This implements
|
||||
number-comparisons, limits etc. It works on the 'base' property since this
|
||||
makes more sense for child classes. For this base class, the .actual
|
||||
property is just an alias of .base.
|
||||
|
||||
actual = base
|
||||
|
||||
"""
|
||||
|
||||
trait_type = "numeric"
|
||||
|
||||
data_keys = {
|
||||
"base": 0
|
||||
}
|
||||
def __str__(self):
|
||||
return f"<Trait {self.name}: {self._data['base']}>"
|
||||
|
||||
# Numeric operations
|
||||
|
||||
def __eq__(self, other):
|
||||
|
|
@ -841,58 +824,58 @@ class NumericTrait(Trait):
|
|||
`__eq__` and `__lt__` are implemented.
|
||||
"""
|
||||
if inherits_from(other, Trait):
|
||||
return self.actual == other.actual
|
||||
return self.value == other.value
|
||||
elif type(other) in (float, int):
|
||||
return self.actual == other
|
||||
return self.value == other
|
||||
else:
|
||||
return NotImplemented
|
||||
|
||||
def __lt__(self, other):
|
||||
"""Support less than comparison between `Trait`s or `Trait` and numeric."""
|
||||
if inherits_from(other, Trait):
|
||||
return self.actual < other.actual
|
||||
return self.value < other.value
|
||||
elif type(other) in (float, int):
|
||||
return self.actual < other
|
||||
return self.value < other
|
||||
else:
|
||||
return NotImplemented
|
||||
|
||||
def __pos__(self):
|
||||
"""Access `actual` property through unary `+` operator."""
|
||||
return self.actual
|
||||
"""Access `value` property through unary `+` operator."""
|
||||
return self.value
|
||||
|
||||
def __add__(self, other):
|
||||
"""Support addition between `Trait`s or `Trait` and numeric"""
|
||||
if inherits_from(other, Trait):
|
||||
return self.actual + other.actual
|
||||
return self.value + other.value
|
||||
elif type(other) in (float, int):
|
||||
return self.actual + other
|
||||
return self.value + other
|
||||
else:
|
||||
return NotImplemented
|
||||
|
||||
def __sub__(self, other):
|
||||
"""Support subtraction between `Trait`s or `Trait` and numeric"""
|
||||
if inherits_from(other, Trait):
|
||||
return self.actual - other.actual
|
||||
return self.value - other.value
|
||||
elif type(other) in (float, int):
|
||||
return self.actual - other
|
||||
return self.value - other
|
||||
else:
|
||||
return NotImplemented
|
||||
|
||||
def __mul__(self, other):
|
||||
"""Support multiplication between `Trait`s or `Trait` and numeric"""
|
||||
if inherits_from(other, Trait):
|
||||
return self.actual * other.actual
|
||||
return self.value * other.value
|
||||
elif type(other) in (float, int):
|
||||
return self.actual * other
|
||||
return self.value * other
|
||||
else:
|
||||
return NotImplemented
|
||||
|
||||
def __floordiv__(self, other):
|
||||
"""Support floor division between `Trait`s or `Trait` and numeric"""
|
||||
if inherits_from(other, Trait):
|
||||
return self.actual // other.actual
|
||||
return self.value // other.value
|
||||
elif type(other) in (float, int):
|
||||
return self.actual // other
|
||||
return self.value // other
|
||||
else:
|
||||
return NotImplemented
|
||||
|
||||
|
|
@ -903,64 +886,53 @@ class NumericTrait(Trait):
|
|||
def __rsub__(self, other):
|
||||
"""Support subtraction between `Trait`s or `Trait` and numeric"""
|
||||
if inherits_from(other, Trait):
|
||||
return other.actual - self.actual
|
||||
return other.value - self.value
|
||||
elif type(other) in (float, int):
|
||||
return other - self.actual
|
||||
return other - self.value
|
||||
else:
|
||||
return NotImplemented
|
||||
|
||||
def __rfloordiv__(self, other):
|
||||
"""Support floor division between `Trait`s or `Trait` and numeric"""
|
||||
if inherits_from(other, Trait):
|
||||
return other.actual // self.actual
|
||||
return other.value // self.value
|
||||
elif type(other) in (float, int):
|
||||
return other // self.actual
|
||||
return other // self.value
|
||||
else:
|
||||
return NotImplemented
|
||||
|
||||
# Public members
|
||||
|
||||
@property
|
||||
def actual(self):
|
||||
"The actual value of the trait"
|
||||
return self.base
|
||||
def value(self):
|
||||
"""Store a value"""
|
||||
return self._data["value"]
|
||||
|
||||
@property
|
||||
def base(self):
|
||||
"""The trait's base value.
|
||||
|
||||
Note:
|
||||
The setter for this property will enforce any range bounds set
|
||||
on this `Trait`.
|
||||
"""
|
||||
return self._data["base"]
|
||||
|
||||
@base.setter
|
||||
def base(self, value):
|
||||
"""Base must be a numerical value."""
|
||||
if type(value) in (int, float):
|
||||
self._data["base"] = value
|
||||
@value.setter
|
||||
def value(self, value):
|
||||
"""Get value"""
|
||||
self._data["value"] = value
|
||||
|
||||
|
||||
# Implementation of the respective Trait types
|
||||
|
||||
class StaticTrait(NumericTrait):
|
||||
class StaticTrait(Trait):
|
||||
"""
|
||||
Static Trait. This is a single value with a modifier,
|
||||
with no concept of a 'current' value.
|
||||
|
||||
actual = base + mod
|
||||
value = base + mod
|
||||
|
||||
"""
|
||||
trait_type = "static"
|
||||
|
||||
data_keys = {
|
||||
default_keys = {
|
||||
"base": 0,
|
||||
"mod": 0
|
||||
}
|
||||
|
||||
def __str__(self):
|
||||
status = "{actual:11}".format(actual=self.actual)
|
||||
status = "{value:11}".format(value=self.value)
|
||||
return "{name:12} {status} ({mod:+3})".format(name=self.name, status=status, mod=self.mod)
|
||||
|
||||
# Helpers
|
||||
|
|
@ -976,12 +948,12 @@ class StaticTrait(NumericTrait):
|
|||
self._data["mod"] = amount
|
||||
|
||||
@property
|
||||
def actual(self):
|
||||
"The actual value of the Trait"
|
||||
def value(self):
|
||||
"The value of the Trait"
|
||||
return self.base + self.mod
|
||||
|
||||
|
||||
class CounterTrait(NumericTrait):
|
||||
class CounterTrait(Trait):
|
||||
"""
|
||||
Counter Trait.
|
||||
|
||||
|
|
@ -990,15 +962,15 @@ class CounterTrait(NumericTrait):
|
|||
|
||||
min/unset base base+mod max/unset
|
||||
|--------------|--------|---------X--------X------------|
|
||||
current actual
|
||||
current value
|
||||
= current
|
||||
+ mod
|
||||
|
||||
- actual = current + mod, starts at base + mod
|
||||
- value = current + mod, starts at base + mod
|
||||
- if min or max is None, there is no upper/lower bound (default)
|
||||
- if max is set to "base", max will be equal ot base+mod
|
||||
- descs are used to optionally describe each value interval.
|
||||
The desc of the current `actual` value can then be retrieved
|
||||
The desc of the current `value` value can then be retrieved
|
||||
with .desc(). The property is set as {lower_bound_inclusive:desc}
|
||||
and should be given smallest-to-biggest. For example, for
|
||||
a skill rating between 0 and 10:
|
||||
|
|
@ -1018,7 +990,7 @@ class CounterTrait(NumericTrait):
|
|||
trait_type = "counter"
|
||||
|
||||
# current starts equal to base.
|
||||
data_keys = {
|
||||
default_keys = {
|
||||
"base": 0,
|
||||
"mod": 0,
|
||||
"min": None,
|
||||
|
|
@ -1095,16 +1067,16 @@ class CounterTrait(NumericTrait):
|
|||
now = time()
|
||||
tdiff = now - self._data['last_update']
|
||||
current += rate * tdiff
|
||||
actual = current + self.mod
|
||||
value = current + self.mod
|
||||
|
||||
# we must make sure so we don't overstep our bounds
|
||||
# even if .mod is included
|
||||
|
||||
if self._passed_ratetarget(actual):
|
||||
if self._passed_ratetarget(value):
|
||||
current = self._data['ratetarget'] - self.mod
|
||||
self._stop_timer()
|
||||
elif not self._within_boundaries(actual):
|
||||
current = self._enforce_boundaries(actual) - self.mod
|
||||
elif not self._within_boundaries(value):
|
||||
current = self._enforce_boundaries(value) - self.mod
|
||||
self._stop_timer()
|
||||
else:
|
||||
self._data['last_update'] = now
|
||||
|
|
@ -1122,7 +1094,7 @@ class CounterTrait(NumericTrait):
|
|||
@base.setter
|
||||
def base(self, value):
|
||||
if value is None:
|
||||
self._data["base"] = self.data_keys['base']
|
||||
self._data["base"] = self.default_keys['base']
|
||||
if type(value) in (int, float):
|
||||
if self.min is not None and value + self.mod < self.min:
|
||||
value = self.min - self.mod
|
||||
|
|
@ -1138,7 +1110,7 @@ class CounterTrait(NumericTrait):
|
|||
def mod(self, value):
|
||||
if value is None:
|
||||
# unsetting the boundary to default
|
||||
self._data["mod"] = self.data_keys['mod']
|
||||
self._data["mod"] = self.default_keys['mod']
|
||||
elif type(value) in (int, float):
|
||||
if self.min is not None and value + self.base < self.min:
|
||||
value = self.min - self.base
|
||||
|
|
@ -1190,8 +1162,8 @@ class CounterTrait(NumericTrait):
|
|||
self._data["current"] = self.base
|
||||
|
||||
@property
|
||||
def actual(self):
|
||||
"The actual value of the Trait (current + mod)"
|
||||
def value(self):
|
||||
"The value of the Trait (current + mod)"
|
||||
return self._enforce_boundaries(self.current + self.mod)
|
||||
|
||||
@property
|
||||
|
|
@ -1201,7 +1173,7 @@ class CounterTrait(NumericTrait):
|
|||
@ratetarget.setter
|
||||
def ratetarget(self, value):
|
||||
self._data['ratetarget'] = self._enforce_boundaries(value)
|
||||
self._check_and_start_timer(self.actual)
|
||||
self._check_and_start_timer(self.value)
|
||||
|
||||
def percent(self, formatting="{:3.1f}%"):
|
||||
"""
|
||||
|
|
@ -1216,7 +1188,7 @@ class CounterTrait(NumericTrait):
|
|||
float or str: Depending of if a `formatting` string
|
||||
is supplied or not.
|
||||
"""
|
||||
return percent(self.actual, self.min, self.max, formatting=formatting)
|
||||
return percent(self.value, self.min, self.max, formatting=formatting)
|
||||
|
||||
def reset(self):
|
||||
"""Resets `current` property equal to `base` value."""
|
||||
|
|
@ -1233,13 +1205,13 @@ class CounterTrait(NumericTrait):
|
|||
describe the interval.
|
||||
|
||||
Returns:
|
||||
str: The description describing the `actual` value.
|
||||
str: The description describing the `value` value.
|
||||
If not found, returns the empty string.
|
||||
"""
|
||||
descs = self._data["descs"]
|
||||
if descs is None:
|
||||
return ""
|
||||
value = self.actual
|
||||
value = self.value
|
||||
# we rely on Python3.7+ dicts retaining ordering
|
||||
highest = ""
|
||||
for bound, txt in descs.items():
|
||||
|
|
@ -1259,13 +1231,13 @@ class GaugeTrait(CounterTrait):
|
|||
|
||||
min/0 max=base+mod
|
||||
|-----------------------X---------------------------|
|
||||
actual
|
||||
value
|
||||
= current
|
||||
|
||||
- min defaults to 0
|
||||
- max value is always base + mad
|
||||
- .max is an alias of .base
|
||||
- actual = current and varies from min to max.
|
||||
- value = current and varies from min to max.
|
||||
- descs is a mapping {upper_bound_inclusive: desc}. These
|
||||
are checked with .desc() and can be retrieve a text
|
||||
description for a given current value.
|
||||
|
|
@ -1284,7 +1256,7 @@ class GaugeTrait(CounterTrait):
|
|||
|
||||
# same as Counter, here for easy reference
|
||||
# current starts out equal to base
|
||||
data_keys = {
|
||||
default_keys = {
|
||||
"base": 0,
|
||||
"mod": 0,
|
||||
"min": 0,
|
||||
|
|
@ -1300,15 +1272,15 @@ class GaugeTrait(CounterTrait):
|
|||
now = time()
|
||||
tdiff = now - self._data['last_update']
|
||||
current += rate * tdiff
|
||||
actual = current
|
||||
value = current
|
||||
|
||||
# we don't worry about .mod for gauges
|
||||
|
||||
if self._passed_ratetarget(actual):
|
||||
if self._passed_ratetarget(value):
|
||||
current = self._data['ratetarget']
|
||||
self._stop_timer()
|
||||
elif not self._within_boundaries(actual):
|
||||
current = self._enforce_boundaries(actual)
|
||||
elif not self._within_boundaries(value):
|
||||
current = self._enforce_boundaries(value)
|
||||
self._stop_timer()
|
||||
else:
|
||||
self._data['last_update'] = now
|
||||
|
|
@ -1324,7 +1296,7 @@ class GaugeTrait(CounterTrait):
|
|||
return min(self.mod + self.base, value)
|
||||
|
||||
def __str__(self):
|
||||
status = "{actual:4} / {base:4}".format(actual=self.actual, base=self.base)
|
||||
status = "{value:4} / {base:4}".format(value=self.value, base=self.base)
|
||||
return "{name:12} {status} ({mod:+3})".format(name=self.name, status=status, mod=self.mod)
|
||||
|
||||
@property
|
||||
|
|
@ -1354,13 +1326,13 @@ class GaugeTrait(CounterTrait):
|
|||
@property
|
||||
def min(self):
|
||||
val = self._data["min"]
|
||||
return self.data_keys["min"] if val is None else val
|
||||
return self.default_keys["min"] if val is None else val
|
||||
|
||||
@min.setter
|
||||
def min(self, value):
|
||||
"""Limit so min can never be greater than base+mod."""
|
||||
if value is None:
|
||||
self._data["min"] = self.data_keys['min']
|
||||
self._data["min"] = self.default_keys['min']
|
||||
elif type(value) in (int, float):
|
||||
self._data["min"] = min(value, self.base + self.mod)
|
||||
|
||||
|
|
@ -1395,8 +1367,8 @@ class GaugeTrait(CounterTrait):
|
|||
self._data["current"] = self.base + self.mod
|
||||
|
||||
@property
|
||||
def actual(self):
|
||||
"The actual value of the trait"
|
||||
def value(self):
|
||||
"The value of the trait"
|
||||
return self.current
|
||||
|
||||
def percent(self, formatting="{:3.1f}%"):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue