Start copying Traithandler from Ainneve to contribs
This commit is contained in:
parent
7d78eda3fd
commit
485ab5907c
1 changed files with 873 additions and 0 deletions
873
evennia/contrib/traits.py
Normal file
873
evennia/contrib/traits.py
Normal file
|
|
@ -0,0 +1,873 @@
|
||||||
|
"""
|
||||||
|
Traits
|
||||||
|
|
||||||
|
Whitenoise 2014, Ainneve contributors,
|
||||||
|
Griatch 2020
|
||||||
|
|
||||||
|
|
||||||
|
A `Trait` represents a modifiable property of (usually) a Character. They can
|
||||||
|
be used to represent everything from attributes (str, agi etc) to skills
|
||||||
|
(hunting, swords etc) or effects (poisoned, rested etc) and has extra
|
||||||
|
functionality beyond using plain Attributes for this.
|
||||||
|
|
||||||
|
Traits use Evennia Attributes under the hood, making them persistent (they survive
|
||||||
|
a server reload/reboot).
|
||||||
|
|
||||||
|
### Adding Traits to a typeclass
|
||||||
|
|
||||||
|
To access and manipulate tragts on an object, 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`).
|
||||||
|
|
||||||
|
Here's an example for adding the TraitHandler to the base Object class:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# mygame/typeclasses/objects.py
|
||||||
|
|
||||||
|
from evennia import DefaultObject
|
||||||
|
from evennia.utils import lazy_property
|
||||||
|
from evennia.contrib.traits import TraitHandler
|
||||||
|
|
||||||
|
# ...
|
||||||
|
|
||||||
|
class Object(DefaultObject):
|
||||||
|
...
|
||||||
|
@lazy_property
|
||||||
|
def traits(self):
|
||||||
|
# this adds the handler as .traits
|
||||||
|
return TraitHandler(self)
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### Trait Configuration
|
||||||
|
|
||||||
|
A single Trait can be one of three basic types:
|
||||||
|
|
||||||
|
- `Static` - this means a base value and an optional modifier. A typical example would be
|
||||||
|
something like a Strength stat or Skill value. That is, something that varies slowly or
|
||||||
|
not at all.
|
||||||
|
- `Counter` - a Trait of this type has a base value and a current value that
|
||||||
|
can vary inside a specified range. This could be used for skills that can only incrase
|
||||||
|
to a max value.
|
||||||
|
- `Gauge` - Modified counter type modeling a refillable "gauge" that varies between "empty"
|
||||||
|
and "full". The classic example is a Health stat.
|
||||||
|
|
||||||
|
|
||||||
|
```python
|
||||||
|
obj.traits.add("hp", name="Health", type="static",
|
||||||
|
base=0, mod=0, min=None, max=None, extra={})
|
||||||
|
```
|
||||||
|
|
||||||
|
All traits have a read-only `actual` property that will report the trait's
|
||||||
|
actual value.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```python
|
||||||
|
>>> hp = obj.traits.hp
|
||||||
|
>>> hp.actual
|
||||||
|
100
|
||||||
|
```
|
||||||
|
|
||||||
|
They also support storing arbitrary data via either dictionary key or
|
||||||
|
attribute syntax. Storage of arbitrary data in this way has the same
|
||||||
|
constraints as any nested collection type stored in a persistent Evennia
|
||||||
|
Attribute, so it is best to avoid attempting to store complex objects.
|
||||||
|
|
||||||
|
#### Static Trait Configuration
|
||||||
|
|
||||||
|
A static `Trait` stores a `base` value and a `mod` modifier value.
|
||||||
|
The trait's actual value is equal to `base`+`mod`.
|
||||||
|
|
||||||
|
Static traits can be used to model many different stats, such as
|
||||||
|
Strength, Character Level, or Defense Rating in many tabletop gaming
|
||||||
|
systems.
|
||||||
|
|
||||||
|
Constructor Args:
|
||||||
|
name (str): name of the trait
|
||||||
|
type (str): 'static' for static traits
|
||||||
|
base (int, float): base value of the trait
|
||||||
|
mod (int, optional): modifier value
|
||||||
|
extra (dict, optional): keys of this dict are accessible on the
|
||||||
|
`Trait` object as attributes or dict keys
|
||||||
|
|
||||||
|
Properties:
|
||||||
|
actual (int, float): returns the value of `mod`+`base` properties
|
||||||
|
extra (list[str]): list of keys stored in the extra data dict
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
reset_mod(): sets the value of the `mod` property to zero
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
'''python
|
||||||
|
>>> char.traits.add("str", "Strength", base=5)
|
||||||
|
>>> strength = char.traits.str
|
||||||
|
>>> strength.actual
|
||||||
|
5
|
||||||
|
>>> strength.mod = 2 # add a bonus to strength
|
||||||
|
>>> str(strength)
|
||||||
|
'Strength 7 (+2)'
|
||||||
|
>>> strength.reset_mod() # clear bonuses
|
||||||
|
>>> str(strength)
|
||||||
|
'Strength 5 (+0)'
|
||||||
|
>>> strength.newkey = 'newvalue'
|
||||||
|
>>> strength.extra
|
||||||
|
['newkey']
|
||||||
|
>>> strength
|
||||||
|
Trait({'name': 'Strength', 'type': 'trait', 'base': 5, 'mod': 0,
|
||||||
|
'min': None, 'max': None, 'extra': {'newkey': 'newvalue'}})
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Counter Trait Configuration
|
||||||
|
|
||||||
|
Counter type `Trait` objects have a `base` value similar to static
|
||||||
|
traits, but adds a `current` value and a range along which it may
|
||||||
|
vary. Modifier values are applied to this `current` value instead
|
||||||
|
of `base` when determining the `actual` value. The `current` can
|
||||||
|
also be reset to its `base` value by calling the `reset_counter()`
|
||||||
|
method.
|
||||||
|
|
||||||
|
Counter style traits are best used to represent game traits such as
|
||||||
|
carrying weight, alignment points, a money system, or bonus/penalty
|
||||||
|
counters.
|
||||||
|
|
||||||
|
Constructor Args:
|
||||||
|
(all keys listed above for 'static', plus:)
|
||||||
|
min Optional(int, float, None): default None
|
||||||
|
minimum allowable value for current; unbounded if None
|
||||||
|
max Optional(int, float, None): default None
|
||||||
|
maximum allowable value for current; unbounded if None
|
||||||
|
|
||||||
|
Properties:
|
||||||
|
actual (int, float): returns the value of `mod`+`current` properties
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
reset_counter(): resets `current` equal to the value of `base`
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
```python
|
||||||
|
>>> char.traits.add("carry", "Carry Weight", base=0, min=0, max=10000)
|
||||||
|
>>> carry = caller.traits.carry
|
||||||
|
>>> str(carry)
|
||||||
|
'Carry Weight 0 ( +0)'
|
||||||
|
>>> carry.current -= 3 # try to go negative
|
||||||
|
>>> carry # enforces zero minimum
|
||||||
|
'Carry Weight 0 ( +0)'
|
||||||
|
>>> carry.current += 15
|
||||||
|
>>> carry
|
||||||
|
'Carry Weight 15 ( +0)'
|
||||||
|
>>> carry.mod = -5 # apply a modifier to reduce
|
||||||
|
>>> carry # apparent weight
|
||||||
|
'Carry Weight: 10 ( -5)'
|
||||||
|
>>> carry.current = 10000 # set a semi-large value
|
||||||
|
>>> carry # still have the modifier
|
||||||
|
'Carry Weight 9995 ( -5)'
|
||||||
|
>>> carry.reset() # remove modifier
|
||||||
|
>>> carry
|
||||||
|
'Carry Weight 10000 ( +0)'
|
||||||
|
>>> carry.reset_counter()
|
||||||
|
>>> carry
|
||||||
|
0
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Gauge Trait Configuration
|
||||||
|
|
||||||
|
A "gauge" type `Trait` is a modified counter trait used to model a
|
||||||
|
gauge that can be emptied and refilled. The `base` property of a
|
||||||
|
gauge trait represents its "full" value. The `mod` property increases
|
||||||
|
or decreases that "full" value, rather than the `current`.
|
||||||
|
|
||||||
|
Gauge type traits are best used to represent traits such as health
|
||||||
|
points, stamina points, or magic points.
|
||||||
|
|
||||||
|
By default gauge type traits have a `min` of zero, and a `max` set
|
||||||
|
to the `base`+`mod` properties. A gauge will still work if its `max`
|
||||||
|
property is set to a value above its `base` or to None.
|
||||||
|
|
||||||
|
Constructor Args:
|
||||||
|
(all keys listed above for 'static', plus:)
|
||||||
|
min Optional(int, float, None): default 0
|
||||||
|
minimum allowable value for current; unbounded if None
|
||||||
|
max Optional(int, float, None, 'base'): default 'base'
|
||||||
|
maximum allowable value for current; unbounded if None;
|
||||||
|
if 'base', returns the value of `base`+`mod`.
|
||||||
|
|
||||||
|
Properties:
|
||||||
|
actual (int, float): returns the value of the `current` property
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
fill_gauge(): adds the value of `base`+`mod` to `current`
|
||||||
|
percent(): returns the ratio of actual value to max value as
|
||||||
|
a percentage. if `max` is unbound, return the ratio of
|
||||||
|
`current` to `base`+`mod` instead.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
```python
|
||||||
|
>>> caller.traits.add("hp", "Health", base=10)
|
||||||
|
>>> hp = caller.traits.hp
|
||||||
|
>>> repr(hp)
|
||||||
|
GaugeTrait({'name': 'HP', 'type': 'gauge', 'base': 10, 'mod': 0,
|
||||||
|
'min': 0, 'max': 'base', 'current': 10, 'extra': {}})
|
||||||
|
>>> str(hp)
|
||||||
|
'HP: 10 / 10 ( +0)'
|
||||||
|
>>> hp.current -= 6 # take damage
|
||||||
|
>>> str(hp)
|
||||||
|
'HP: 4 / 10 ( +0)'
|
||||||
|
>>> hp.current -= 6 # take damage to below min
|
||||||
|
>>> str(hp)
|
||||||
|
'HP: 0 / 10 ( +0)'
|
||||||
|
>>> hp.fill() # refill trait
|
||||||
|
>>> str(hp)
|
||||||
|
'HP: 10 / 10 ( +0)'
|
||||||
|
>>> hp.current = 15 # try to set above max
|
||||||
|
>>> str(hp) # disallowed because max=='actual'
|
||||||
|
'HP: 10 / 10 ( +0)'
|
||||||
|
>>> hp.mod += 3 # bonus on full trait
|
||||||
|
>>> str(hp) # buffs flow to current
|
||||||
|
'HP: 13 / 13 ( +3)'
|
||||||
|
>>> hp.current -= 5
|
||||||
|
>>> str(hp)
|
||||||
|
'HP: 8 / 13 ( +3)'
|
||||||
|
>>> hp.reset() # remove bonus on reduced trait
|
||||||
|
>>> str(hp) # debuffs do not affect current
|
||||||
|
'HP: 8 / 10 ( +0)'
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
from functools import total_ordering
|
||||||
|
from evennia.utils.dbserialize import _SaverDict
|
||||||
|
from evennia.utils import logger
|
||||||
|
from evennia.utils.utils import inherits_from
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
STATIC_TYPE = "static"
|
||||||
|
COUNTER_TYPE = "counter",
|
||||||
|
GAUGE_TYPE = "gauge"
|
||||||
|
|
||||||
|
|
||||||
|
TRAIT_TYPES = (STATIC_TYPE, COUNTER_TYPE, GAUGE_TYPE)
|
||||||
|
RANGE_TRAITS = (COUNTER_TYPE, GAUGE_TYPE)
|
||||||
|
|
||||||
|
|
||||||
|
class TraitException(Exception):
|
||||||
|
"""
|
||||||
|
Base exception class raised by `Trait` objects.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
msg (str): informative error message
|
||||||
|
|
||||||
|
"""
|
||||||
|
def __init__(self, msg):
|
||||||
|
self.msg = msg
|
||||||
|
|
||||||
|
|
||||||
|
class TraitHandler:
|
||||||
|
"""
|
||||||
|
Factory class that instantiates Trait objects.
|
||||||
|
|
||||||
|
"""
|
||||||
|
def __init__(self, obj, db_attribute_key='traits', db_attribute_category="traits"):
|
||||||
|
"""
|
||||||
|
Initialize the handler and set up its internal Attribute-based storage.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
obj (Object): Parent Object typeclass for this TraitHandler
|
||||||
|
db_attribute_key (str): Name of the DB attribute for trait data storage
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Note that this retains the connection to the database, meaning every
|
||||||
|
# update we do to .trait_data automatically syncs with database.
|
||||||
|
self.trait_data = obj.attributes.get(db_attribute_key, category=db_attribute_category)
|
||||||
|
if self.trait_data is None:
|
||||||
|
# no existing storage; initialize it
|
||||||
|
obj.attributes.add(db_attribute_key, {}, category=db_attribute_category)
|
||||||
|
self.trait_data = {}
|
||||||
|
self._cache = {}
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
"""Return number of Traits registered with the handler"""
|
||||||
|
return len(self.trait_data)
|
||||||
|
|
||||||
|
def __setattr__(self, key, value):
|
||||||
|
"""Returns error message if trait objects are assigned directly."""
|
||||||
|
if key in ('trait_data', '_cache'):
|
||||||
|
super().__setattr__(key, value)
|
||||||
|
else:
|
||||||
|
raise TraitException(
|
||||||
|
"Trait object not settable directly. Assign to one of "
|
||||||
|
f"`{key}.base`, `{key}.mod`, or `{key}.current` instead."
|
||||||
|
)
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
"""Returns error message if trait objects are assigned directly."""
|
||||||
|
return self.__setattr__(key, value)
|
||||||
|
|
||||||
|
def __getattr__(self, trait):
|
||||||
|
"""Returns Trait instances accessed as attributes."""
|
||||||
|
return self.get(trait)
|
||||||
|
|
||||||
|
def __getitem__(self, trait):
|
||||||
|
"""Returns `Trait` instances accessed as dict keys."""
|
||||||
|
return self.get(trait)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def all(self):
|
||||||
|
"""
|
||||||
|
Get all trait keys in this handler.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: All Trait keys.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return list(self.trait_data.keys())
|
||||||
|
|
||||||
|
def get(self, trait):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
trait (str): key from the traits dict containing config data
|
||||||
|
for the trait. "all" returns a list of all trait keys.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(`Trait` or `None`): named Trait class or None if trait key
|
||||||
|
is not found in traits collection.
|
||||||
|
|
||||||
|
"""
|
||||||
|
trait = self._cache.get(trait)
|
||||||
|
if trait is None and trait in self.trait_data:
|
||||||
|
trait = self.cache[trait] = Trait(self.trait_data[trait])
|
||||||
|
return trait
|
||||||
|
|
||||||
|
def add(self, key, name=None, trait_type=STATIC_TYPE,
|
||||||
|
base=0, modifier=0, min_value=0, max_value=0,
|
||||||
|
force=False, **extra_properties):
|
||||||
|
"""
|
||||||
|
Create a new Trait and add it to the handler.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key (str): This is the name of the property that will be made
|
||||||
|
available on this handler (example 'hp').
|
||||||
|
name (str, optional): This is a longer name used in Trait
|
||||||
|
string representation (example 'Health'). If not given, this
|
||||||
|
will be set the same as `key`, starting with a capital letter.
|
||||||
|
trait_type (str, optional): One of 'static', 'counter' or 'gauge'.
|
||||||
|
base (int or float, optional): The base value, or 'full' value in the case
|
||||||
|
of a gauge.
|
||||||
|
modifier (int, optional): A modifier affecting the current or base value.
|
||||||
|
min_value (int or float, optional): The minimum allowed value.
|
||||||
|
max_value (int or float, optional): The maximum allowed value.
|
||||||
|
force (bool, optional): Always add, replacing any existing trait.
|
||||||
|
**extra_properties (any): All other kwargs will be made available as key:value
|
||||||
|
properties on the handler. These must all be possible to store
|
||||||
|
in an Attribute.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
TraitException: If specifying invalid values or an existing trait
|
||||||
|
already exists (and `force` is unset).
|
||||||
|
|
||||||
|
"""
|
||||||
|
if key in self.trait_data:
|
||||||
|
if force:
|
||||||
|
self.remove(key)
|
||||||
|
else:
|
||||||
|
raise TraitException(f"Trait '{key}' already exists.")
|
||||||
|
|
||||||
|
if trait_type not in TRAIT_TYPES:
|
||||||
|
raise TraitException("Trait-type '{trait_type} is invalid.")
|
||||||
|
|
||||||
|
trait_kwargs = dict(
|
||||||
|
name=name if name is not None else key.title(),
|
||||||
|
trait_type=trait_type,
|
||||||
|
base=base,
|
||||||
|
modifier=modifier,
|
||||||
|
min_value=min_value,
|
||||||
|
max_value=max_value,
|
||||||
|
extra_properties=extra_properties
|
||||||
|
)
|
||||||
|
|
||||||
|
self.trait_data[key] = trait_kwargs
|
||||||
|
|
||||||
|
def remove(self, key):
|
||||||
|
"""
|
||||||
|
Remove a Trait from the handler's parent object.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key (str): The name of the trait to remove.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if key not in self.trait_data:
|
||||||
|
raise TraitException(f"Trait '{key}' not found.")
|
||||||
|
|
||||||
|
if key in self._cache:
|
||||||
|
del self.cache[key]
|
||||||
|
del self.trait_data[key]
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
"""
|
||||||
|
Remove all Traits from the handler's parent object.
|
||||||
|
"""
|
||||||
|
for key in self.all:
|
||||||
|
self.remove(key)
|
||||||
|
|
||||||
|
|
||||||
|
# Parent Trait class
|
||||||
|
|
||||||
|
@total_ordering
|
||||||
|
class Trait:
|
||||||
|
"""Represents an object or Character trait.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
See module docstring for configuration details.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_keys = set("name", "type", "base", "mod", "current",
|
||||||
|
"min", "max", "extra_properties")
|
||||||
|
|
||||||
|
def __init__(self, trait_data):
|
||||||
|
"""
|
||||||
|
Initialize a Trait with stored data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
trait_data (_SaverDict or dict): This will be a _SaverDict if
|
||||||
|
passed from the TraitHandler, which means this will automatically
|
||||||
|
save itself the database when updating
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not all(key in trait_data for key in self.valid_keys):
|
||||||
|
raise TraitException(
|
||||||
|
f"Required keys missing from trait_data "
|
||||||
|
f"(input was {list(trait_data.keys())}, "
|
||||||
|
f"required are {self.valid_keys}).")
|
||||||
|
|
||||||
|
self._type = trait_data['trait_type']
|
||||||
|
self._data = trait_data
|
||||||
|
self._locked = True
|
||||||
|
|
||||||
|
if not isinstance(trait_data, _SaverDict):
|
||||||
|
logger.log_warn(
|
||||||
|
f"Non-persistent Trait data (type(trait_data)) "
|
||||||
|
f"loaded for {type(self).__name__}.")
|
||||||
|
|
||||||
|
# Private helper members
|
||||||
|
|
||||||
|
def _enforce_bounds(self, value):
|
||||||
|
"""Ensures that incoming value falls within trait's range."""
|
||||||
|
if self._type in RANGE_TRAITS:
|
||||||
|
if self.min is not None and value <= self.min:
|
||||||
|
return self.min
|
||||||
|
if self._data['max'] == 'base' and value >= self.mod + self.base:
|
||||||
|
return self.mod + self.base
|
||||||
|
if self.max is not None and value >= self.max:
|
||||||
|
return self.max
|
||||||
|
return value
|
||||||
|
|
||||||
|
def _mod_base(self):
|
||||||
|
return self._enforce_bounds(self.mod + self.base)
|
||||||
|
|
||||||
|
def _mod_current(self):
|
||||||
|
return self._enforce_bounds(self.mod + self.current)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
"""Debug-friendly representation of this Trait."""
|
||||||
|
return "{}({{{}}})".format(
|
||||||
|
type(self).__name__,
|
||||||
|
', '.join(["'{}': {!r}".format(k, self._data[k])
|
||||||
|
for k in self._keys if k in self._data]))
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
status = "{actual:11}".format(actual=self.actual)
|
||||||
|
return "{name:12} {status} ({mod:+3})".format(
|
||||||
|
name=self.name,
|
||||||
|
status=status,
|
||||||
|
mod=self.mod)
|
||||||
|
|
||||||
|
# Extra Properties - allow access to properties on Trait
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
"""Access extra parameters as dict keys."""
|
||||||
|
try:
|
||||||
|
return self.__getattr__(key)
|
||||||
|
except AttributeError:
|
||||||
|
raise KeyError(key)
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
"""Set extra parameters as dict keys."""
|
||||||
|
self.__setattr__(key, value)
|
||||||
|
|
||||||
|
def __delitem__(self, key):
|
||||||
|
"""Delete extra parameters as dict keys."""
|
||||||
|
self.__delattr__(key)
|
||||||
|
|
||||||
|
def __getattr__(self, key):
|
||||||
|
"""Access extra parameters as attributes."""
|
||||||
|
try:
|
||||||
|
return self._data['extra_properties'][key]
|
||||||
|
except KeyError:
|
||||||
|
raise AttributeError(
|
||||||
|
"{} '{}' has no attribute {!r}".format(
|
||||||
|
type(self).__name__, self.name, key
|
||||||
|
))
|
||||||
|
|
||||||
|
def __setattr__(self, key, value):
|
||||||
|
"""Set extra parameters as attributes.
|
||||||
|
|
||||||
|
Arbitrary attributes set on a Trait object will be
|
||||||
|
stored in the 'extra' key of the `_data` attribute.
|
||||||
|
|
||||||
|
This behavior is enabled by setting the instance
|
||||||
|
variable `_locked` to True.
|
||||||
|
"""
|
||||||
|
propobj = getattr(self.__class__, key, None)
|
||||||
|
if isinstance(propobj, property):
|
||||||
|
if propobj.fset is None:
|
||||||
|
raise AttributeError(f"Can't set attribute {key}.")
|
||||||
|
propobj.fset(self, value)
|
||||||
|
else:
|
||||||
|
if (self.__dict__.get('_locked', False) and
|
||||||
|
key not in ('_keys',)):
|
||||||
|
self._data['extra_properties'][key] = value
|
||||||
|
else:
|
||||||
|
super().__setattr__(key, value)
|
||||||
|
|
||||||
|
def __delattr__(self, key):
|
||||||
|
"""Delete extra parameters as attributes."""
|
||||||
|
if key in self._data['extra_parameters']:
|
||||||
|
del self._data['extra_parameters'][key]
|
||||||
|
|
||||||
|
# Numeric operations
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
"""Support equality comparison between Traits or Trait and numeric.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
This class uses the @functools.total_ordering() decorator to
|
||||||
|
complete the rich comparison implementation, therefore only
|
||||||
|
`__eq__` and `__lt__` are implemented.
|
||||||
|
"""
|
||||||
|
if inherits_from(other, Trait):
|
||||||
|
return self.actual == other.actual
|
||||||
|
elif type(other) in (float, int):
|
||||||
|
return self.actual == 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
|
||||||
|
elif type(other) in (float, int):
|
||||||
|
return self.actual < other
|
||||||
|
else:
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
def __pos__(self):
|
||||||
|
"""Access `actual` property through unary `+` operator."""
|
||||||
|
return self.actual
|
||||||
|
|
||||||
|
def __add__(self, other):
|
||||||
|
"""Support addition between `Trait`s or `Trait` and numeric"""
|
||||||
|
if inherits_from(other, Trait):
|
||||||
|
return self.actual + other.actual
|
||||||
|
elif type(other) in (float, int):
|
||||||
|
return self.actual + 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
|
||||||
|
elif type(other) in (float, int):
|
||||||
|
return self.actual - 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
|
||||||
|
elif type(other) in (float, int):
|
||||||
|
return self.actual * 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
|
||||||
|
elif type(other) in (float, int):
|
||||||
|
return self.actual // other
|
||||||
|
else:
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
# commutative property
|
||||||
|
__radd__ = __add__
|
||||||
|
__rmul__ = __mul__
|
||||||
|
|
||||||
|
def __rsub__(self, other):
|
||||||
|
"""Support subtraction between `Trait`s or `Trait` and numeric"""
|
||||||
|
if inherits_from(other, Trait):
|
||||||
|
return other.actual - self.actual
|
||||||
|
elif type(other) in (float, int):
|
||||||
|
return other - self.actual
|
||||||
|
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
|
||||||
|
elif type(other) in (float, int):
|
||||||
|
return other // self.actual
|
||||||
|
else:
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
# Public members
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Display name for the trait."""
|
||||||
|
return self._data['name']
|
||||||
|
key = name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def actual(self):
|
||||||
|
"The actual value of the trait"
|
||||||
|
return self._mod_base()
|
||||||
|
|
||||||
|
@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, amount):
|
||||||
|
if self._data.get('max', None) == 'base':
|
||||||
|
self._data['base'] = amount
|
||||||
|
if type(amount) in (int, float):
|
||||||
|
self._data['base'] = self._enforce_bounds(amount)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mod(self):
|
||||||
|
"""The trait's modifier."""
|
||||||
|
return self._data['modifier']
|
||||||
|
|
||||||
|
@mod.setter
|
||||||
|
def mod(self, amount):
|
||||||
|
if type(amount) in (int, float):
|
||||||
|
self._data['modifier'] = amount
|
||||||
|
|
||||||
|
@property
|
||||||
|
def min(self):
|
||||||
|
return self._data["min_value"]
|
||||||
|
|
||||||
|
@min.setter
|
||||||
|
def min(self, value):
|
||||||
|
self._data["min_value"] = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max(self):
|
||||||
|
return self._data['max_value']
|
||||||
|
|
||||||
|
@max.setter
|
||||||
|
def max(self, value):
|
||||||
|
self._data["max_value"] = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current(self):
|
||||||
|
"""The `current` value of the `Trait`."""
|
||||||
|
return self._data.get('current', self.base)
|
||||||
|
|
||||||
|
@current.setter
|
||||||
|
def current(self, value):
|
||||||
|
self._data["current"] = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def extra(self):
|
||||||
|
"""Returns a list containing available extra data keys."""
|
||||||
|
return self._data['extra'].keys()
|
||||||
|
|
||||||
|
def reset_mod(self):
|
||||||
|
"""Clears any mod value to 0."""
|
||||||
|
self.mod = 0
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
"""Resets `current` property equal to `base` value."""
|
||||||
|
self.current = self.base
|
||||||
|
|
||||||
|
def percent(self):
|
||||||
|
"""Returns the value formatted as a percentage."""
|
||||||
|
return "100.0%"
|
||||||
|
|
||||||
|
|
||||||
|
# Implementation of the respective Trait types
|
||||||
|
|
||||||
|
class StaticTrait(Trait):
|
||||||
|
"""
|
||||||
|
Static Trait.
|
||||||
|
|
||||||
|
"""
|
||||||
|
@property
|
||||||
|
def min(self):
|
||||||
|
raise TraitException(f"Static Trait {self.key} has no minimum value.")
|
||||||
|
|
||||||
|
@min.setter
|
||||||
|
def min(self):
|
||||||
|
raise TraitException(f"Cannot set minimum value for static Trait {self.key}.")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max(self):
|
||||||
|
raise TraitException("Static Trait {self.key} has no maximum value.")
|
||||||
|
|
||||||
|
@max.setter
|
||||||
|
def max(self):
|
||||||
|
raise TraitException("Cannot set maximum value for static Trait {self.key}.")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current(self):
|
||||||
|
"""The `current` value of the `Trait`."""
|
||||||
|
return super().current
|
||||||
|
|
||||||
|
@current.setter
|
||||||
|
def current(self, value):
|
||||||
|
raise TraitException(
|
||||||
|
f"Cannot set 'current' property on static Trait {self.key}.")
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
raise TraitException(
|
||||||
|
f"Cannot reset static Trait {self.key}.")
|
||||||
|
|
||||||
|
|
||||||
|
class CounterTrait(Trait):
|
||||||
|
"""
|
||||||
|
Counter Trait.
|
||||||
|
|
||||||
|
"""
|
||||||
|
@property
|
||||||
|
def actual(self):
|
||||||
|
"The actual value of the Trait"
|
||||||
|
return self._mod_current()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def min(self):
|
||||||
|
"""The lower bound of the range."""
|
||||||
|
return super().min
|
||||||
|
|
||||||
|
@min.setter
|
||||||
|
def min(self, amount):
|
||||||
|
if amount is None:
|
||||||
|
self._data['min'] = amount
|
||||||
|
elif type(amount) in (int, float):
|
||||||
|
self._data['min'] = amount if amount < self.base else self.base
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max(self):
|
||||||
|
if self._data['max_value'] == 'base':
|
||||||
|
return self._mod_base()
|
||||||
|
return super().max
|
||||||
|
|
||||||
|
@max.setter
|
||||||
|
def max(self):
|
||||||
|
"""The maximum value of the `Trait`.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
This property may be set to the string literal 'base'.
|
||||||
|
When set this way, the property returns the value of the
|
||||||
|
`mod`+`base` properties.
|
||||||
|
"""
|
||||||
|
if self._data['max_value'] == 'base':
|
||||||
|
return self._mod_base()
|
||||||
|
return super().max
|
||||||
|
|
||||||
|
@max.setter
|
||||||
|
def max(self, value):
|
||||||
|
if value == 'base' or value is None:
|
||||||
|
self._data['max_value'] = value
|
||||||
|
elif type(value) in (int, float):
|
||||||
|
self._data['max_value'] = value if value > self.base else self.base
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current(self):
|
||||||
|
"""The `current` value of the `Trait`."""
|
||||||
|
return super().current
|
||||||
|
|
||||||
|
@current.setter
|
||||||
|
def current(self, value):
|
||||||
|
if type(value) in (int, float):
|
||||||
|
self._data['current'] = self._enforce_bounds(value)
|
||||||
|
else:
|
||||||
|
raise AttributeError(
|
||||||
|
"'current' property is read-only on static 'Trait'.")
|
||||||
|
|
||||||
|
def percent(self):
|
||||||
|
"""Returns the value formatted as a percentage."""
|
||||||
|
if self.max:
|
||||||
|
return "{:3.1f}%".format(self.current * 100.0 / self.max)
|
||||||
|
elif self.base != 0:
|
||||||
|
return "{:3.1f}%".format(self.current * 100.0 / self._mod_base())
|
||||||
|
# if we get to this point, it's may be a divide by zero situation
|
||||||
|
return "100.0%"
|
||||||
|
|
||||||
|
|
||||||
|
class GaugeTrait(CounterTrait):
|
||||||
|
"""
|
||||||
|
Gauge Trait.
|
||||||
|
|
||||||
|
"""
|
||||||
|
def __str__(self):
|
||||||
|
status = "{actual:4} / {base:4}".format(
|
||||||
|
actual=self.actual,
|
||||||
|
base=self.base)
|
||||||
|
return "{name:12} {status} ({mod:+3})".format(
|
||||||
|
name=self.name,
|
||||||
|
status=status,
|
||||||
|
mod=self.mod)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def actual(self):
|
||||||
|
"The actual value of the trait"
|
||||||
|
return self.current
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mod(self):
|
||||||
|
"""The trait's modifier."""
|
||||||
|
return super().mod
|
||||||
|
|
||||||
|
@mod.setter
|
||||||
|
def mod(self, amount):
|
||||||
|
if type(amount) in (int, float):
|
||||||
|
self._data['modifier'] = amount
|
||||||
|
delta = amount - self._data['modifier']
|
||||||
|
if delta >= 0:
|
||||||
|
# apply increases to current
|
||||||
|
self.current = self._enforce_bounds(self.current + delta)
|
||||||
|
else:
|
||||||
|
# but not decreases, unless current goes out of range
|
||||||
|
self.current = self._enforce_bounds(self.current)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current(self):
|
||||||
|
"""The `current` value of the `Trait`."""
|
||||||
|
return self._data.get('current', self._mod_base())
|
||||||
|
|
||||||
|
@current.setter
|
||||||
|
def current(self, value):
|
||||||
|
super().current = value
|
||||||
|
|
||||||
|
def fill_gauge(self):
|
||||||
|
"""Adds the `mod`+`base` to the `current` value.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
Will honor the upper bound if set.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.current = \
|
||||||
|
self._enforce_bounds(self.current + self._mod_base())
|
||||||
Loading…
Add table
Add a link
Reference in a new issue