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:
Griatch 2020-04-20 09:17:06 +02:00
parent 39c7889336
commit 06cb85ea6c
2 changed files with 187 additions and 257 deletions

View file

@ -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):

View file

@ -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}%"):