2022-01-08 00:58:56 +01:00
|
|
|
# Traits
|
|
|
|
|
|
2022-01-08 14:40:58 +01:00
|
|
|
Contribution by Griatch 2020, based on code by Whitenoise and Ainneve contribs, 2014
|
2022-01-08 00:58:56 +01:00
|
|
|
|
|
|
|
|
A `Trait` represents a modifiable property on (usually) a Character. They can
|
|
|
|
|
be used to represent everything from attributes (str, agi etc) to skills
|
2022-01-08 14:40:58 +01:00
|
|
|
(hunting 10, swords 14 etc) and dynamically changing things like HP, XP etc.
|
2022-01-08 00:58:56 +01:00
|
|
|
Traits differ from normal Attributes in that they track their changes and limit
|
|
|
|
|
themselves to particular value-ranges. One can add/subtract from them easily and
|
|
|
|
|
they can even change dynamically at a particular rate (like you being poisoned or
|
|
|
|
|
healed).
|
|
|
|
|
|
|
|
|
|
Traits use Evennia Attributes under the hood, making them persistent (they survive
|
|
|
|
|
a server reload/reboot).
|
|
|
|
|
|
|
|
|
|
## Installation
|
|
|
|
|
|
|
|
|
|
Traits are always added to a typeclass, such as the Character class.
|
|
|
|
|
|
|
|
|
|
There are two ways to set up Traits on a typeclass. The first sets up the `TraitHandler`
|
|
|
|
|
as a property `.traits` on your class and you then access traits as e.g. `.traits.strength`.
|
|
|
|
|
The other alternative uses a `TraitProperty`, which makes the trait available directly
|
|
|
|
|
as e.g. `.strength`. This solution also uses the `TraitHandler`, but you don't need to
|
|
|
|
|
define it explicitly. You can combine both styles if you like.
|
|
|
|
|
|
|
|
|
|
### Traits with TraitHandler
|
|
|
|
|
|
|
|
|
|
Here's an example for adding the TraitHandler to the Character class:
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
# mygame/typeclasses/objects.py
|
|
|
|
|
|
|
|
|
|
from evennia import DefaultCharacter
|
|
|
|
|
from evennia.utils import lazy_property
|
|
|
|
|
from evennia.contrib.rpg.traits import TraitHandler
|
|
|
|
|
|
|
|
|
|
# ...
|
|
|
|
|
|
|
|
|
|
class Character(DefaultCharacter):
|
|
|
|
|
...
|
|
|
|
|
@lazy_property
|
|
|
|
|
def traits(self):
|
|
|
|
|
# this adds the handler as .traits
|
|
|
|
|
return TraitHandler(self)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def at_object_creation(self):
|
|
|
|
|
# (or wherever you want)
|
|
|
|
|
self.traits.add("str", "Strength", trait_type="static", base=10, mod=2)
|
|
|
|
|
self.traits.add("hp", "Health", trait_type="gauge", min=0, max=100)
|
|
|
|
|
self.traits.add("hunting", "Hunting Skill", trait_type="counter",
|
|
|
|
|
base=10, mod=1, min=0, max=100)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
When adding the trait, you supply the name of the property (`hunting`) along
|
|
|
|
|
with a more human-friendly name ("Hunting Skill"). The latter will show if you
|
|
|
|
|
print the trait etc. The `trait_type` is important, this specifies which type
|
|
|
|
|
of trait this is (see below).
|
|
|
|
|
|
|
|
|
|
### TraitProperties
|
|
|
|
|
|
|
|
|
|
Using `TraitProperties` makes the trait available directly on the class, much like Django model
|
|
|
|
|
fields. The drawback is that you must make sure that the name of your Traits don't collide with any
|
|
|
|
|
other properties/methods on your class.
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
# mygame/typeclasses/objects.py
|
|
|
|
|
|
|
|
|
|
from evennia import DefaultObject
|
|
|
|
|
from evennia.utils import lazy_property
|
|
|
|
|
from evennia.contrib.rpg.traits import TraitProperty
|
|
|
|
|
|
|
|
|
|
# ...
|
|
|
|
|
|
|
|
|
|
class Object(DefaultObject):
|
|
|
|
|
...
|
|
|
|
|
strength = TraitProperty("Strength", trait_type="static", base=10, mod=2)
|
|
|
|
|
health = TraitProperty("Health", trait_type="gauge", min=0, base=100, mod=2)
|
|
|
|
|
hunting = TraitProperty("Hunting Skill", trait_type="counter", base=10, mod=1, min=0, max=100)
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
> Note that the property-name will become the name of the trait and you don't supply `trait_key`
|
|
|
|
|
> separately.
|
|
|
|
|
|
|
|
|
|
> The `.traits` TraitHandler will still be created (it's used under the
|
|
|
|
|
> hood. But it will only be created when the TraitProperty has been accessed at least once,
|
|
|
|
|
> so be careful if mixing the two styles. If you want to make sure `.traits` is always available,
|
|
|
|
|
> add the `TraitHandler` manually like shown earlier - the `TraitProperty` will by default use
|
|
|
|
|
> the same handler (`.traits`).
|
|
|
|
|
|
|
|
|
|
## Using traits
|
|
|
|
|
|
|
|
|
|
A trait is added to the traithandler (if you use `TraitProperty` the handler is just created under
|
|
|
|
|
the hood) after which one can access it as a property on the handler (similarly to how you can do
|
|
|
|
|
.db.attrname for Attributes in Evennia).
|
|
|
|
|
|
|
|
|
|
All traits have a _read-only_ field `.value`. This is only used to read out results, you never
|
|
|
|
|
manipulate it directly (if you try, it will just remain unchanged). The `.value` is calculated based
|
|
|
|
|
on combining fields, like `.base` and `.mod` - which fields are available and how they relate to
|
|
|
|
|
each other depends on the trait type.
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
> obj.traits.strength.value
|
|
|
|
|
12 # base + mod
|
|
|
|
|
|
|
|
|
|
> obj.traits.strength.base += 5
|
|
|
|
|
obj.traits.strength.value
|
|
|
|
|
17
|
|
|
|
|
|
|
|
|
|
> obj.traits.hp.value
|
|
|
|
|
102 # base + mod
|
|
|
|
|
|
|
|
|
|
> obj.traits.hp.base -= 200
|
|
|
|
|
> obj.traits.hp.value
|
|
|
|
|
0 # min of 0
|
|
|
|
|
|
|
|
|
|
> obj.traits.hp.reset()
|
|
|
|
|
> obj.traits.hp.value
|
|
|
|
|
100
|
|
|
|
|
|
|
|
|
|
# you can also access properties like a dict
|
|
|
|
|
> obj.traits.hp["value"]
|
|
|
|
|
100
|
|
|
|
|
|
|
|
|
|
# you can store arbitrary data persistently for easy reference
|
|
|
|
|
> obj.traits.hp.effect = "poisoned!"
|
|
|
|
|
> obj.traits.hp.effect
|
|
|
|
|
"poisoned!"
|
|
|
|
|
|
|
|
|
|
# with TraitProperties:
|
|
|
|
|
|
|
|
|
|
> obj.hunting.value
|
|
|
|
|
12
|
|
|
|
|
|
|
|
|
|
> obj.strength.value += 5
|
|
|
|
|
> obj.strength.value
|
|
|
|
|
17
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Trait types
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Two numerical traits can also be compared (bigger-than etc), which is useful in
|
|
|
|
|
all sorts of rule-resolution.
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
|
|
|
|
|
if trait1 > trait2:
|
|
|
|
|
# do stuff
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Static trait
|
|
|
|
|
|
|
|
|
|
`value = 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 may be modified in-place.
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
> obj.traits.add("str", "Strength", trait_type="static", base=10, mod=2)
|
|
|
|
|
> obj.traits.mytrait.value
|
|
|
|
|
|
|
|
|
|
12 # base + mod
|
|
|
|
|
> obj.traits.mytrait.base += 2
|
|
|
|
|
> obj.traits.mytrait.mod += 1
|
|
|
|
|
> obj.traits.mytrait.value
|
|
|
|
|
15
|
|
|
|
|
|
|
|
|
|
> obj.traits.mytrait.mod = 0
|
|
|
|
|
> obj.traits.mytrait.value
|
|
|
|
|
12
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Counter
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
min/unset base base+mod max/unset
|
|
|
|
|
|--------------|--------|---------X--------X------------|
|
|
|
|
|
current value
|
|
|
|
|
= current
|
|
|
|
|
+ mod
|
|
|
|
|
|
|
|
|
|
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. A suggested use for a Counter Trait would be to track skill values.
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
> obj.traits.add("hunting", "Hunting Skill", trait_type="counter",
|
|
|
|
|
base=10, mod=1, min=0, max=100)
|
|
|
|
|
> obj.traits.hunting.value
|
|
|
|
|
11 # current starts at base + mod
|
|
|
|
|
|
|
|
|
|
> obj.traits.hunting.current += 10
|
|
|
|
|
> obj.traits.hunting.value
|
|
|
|
|
21
|
|
|
|
|
|
|
|
|
|
# reset back to base+mod by deleting current
|
|
|
|
|
> del obj.traits.hunting.current
|
|
|
|
|
> obj.traits.hunting.value
|
|
|
|
|
11
|
|
|
|
|
> obj.traits.hunting.max = None # removing upper bound
|
|
|
|
|
|
|
|
|
|
# for TraitProperties, pass the args/kwargs of traits.add() to the
|
|
|
|
|
# TraitProperty constructor instead.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Counters have some extra properties:
|
|
|
|
|
|
|
|
|
|
#### .descs
|
|
|
|
|
|
|
|
|
|
The `descs` property 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 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, you will get the text matching the current `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.value
|
|
|
|
|
11
|
|
|
|
|
|
|
|
|
|
> obj.traits.hunting.desc()
|
|
|
|
|
"neophyte"
|
|
|
|
|
> obj.traits.hunting.current += 60
|
|
|
|
|
> obj.traits.hunting.value
|
|
|
|
|
71
|
|
|
|
|
|
|
|
|
|
> obj.traits.hunting.desc()
|
|
|
|
|
"expert"
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
#### .rate
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
`.value` per-second, and this 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.value
|
|
|
|
|
71
|
|
|
|
|
|
|
|
|
|
> obj.traits.hunting.ratetarget = 71
|
|
|
|
|
# debuff hunting for some reason
|
|
|
|
|
> obj.traits.hunting.current -= 30
|
|
|
|
|
> obj.traits.hunting.value
|
|
|
|
|
41
|
|
|
|
|
|
|
|
|
|
> obj.traits.hunting.rate = 1 # 1/s increase
|
|
|
|
|
# Waiting 5s
|
|
|
|
|
> obj.traits.hunting.value
|
|
|
|
|
46
|
|
|
|
|
|
|
|
|
|
# Waiting 8s
|
|
|
|
|
> obj.traits.hunting.value
|
|
|
|
|
54
|
|
|
|
|
|
|
|
|
|
# Waiting 100s
|
|
|
|
|
> obj.traits.hunting.value
|
|
|
|
|
71 # we have stopped at the ratetarget
|
|
|
|
|
|
|
|
|
|
> obj.traits.hunting.rate = 0 # disable auto-change
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
Note that when retrieving the `current`, the result will always be of the same
|
|
|
|
|
type as the `.base` even `rate` is a non-integer value. So if `base` is an `int`
|
|
|
|
|
(default)`, the `current` value will also be rounded the closest full integer.
|
|
|
|
|
If you want to see the exact `current` value, set `base` to a float - you
|
|
|
|
|
will then need to use `round()` yourself on the result if you want integers.
|
|
|
|
|
|
|
|
|
|
#### .percent()
|
|
|
|
|
|
|
|
|
|
If both min and max are defined, the `.percent()` method of the trait will
|
|
|
|
|
return the value as a percentage.
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
> obj.traits.hunting.percent()
|
|
|
|
|
"71.0%"
|
|
|
|
|
|
|
|
|
|
> obj.traits.hunting.percent(formatting=None)
|
|
|
|
|
71.0
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Gauge
|
|
|
|
|
|
|
|
|
|
This emulates a [fuel-] gauge that empties from a base+mod value.
|
|
|
|
|
|
|
|
|
|
min/0 max=base+mod
|
|
|
|
|
|-----------------------X---------------------------|
|
|
|
|
|
value
|
|
|
|
|
= current
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
`.mod` modifier only applies to the max value of the gauge and not the current
|
|
|
|
|
value. The minimum bound defaults to 0 if not set explicitly.
|
|
|
|
|
|
|
|
|
|
This trait is useful for showing commonly depletable resources like health,
|
|
|
|
|
stamina and the like.
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
> obj.traits.add("hp", "Health", trait_type="gauge", base=100)
|
|
|
|
|
> obj.traits.hp.value # (or .current)
|
|
|
|
|
100
|
|
|
|
|
|
|
|
|
|
> obj.traits.hp.mod = 10
|
|
|
|
|
> obj.traits.hp.value
|
|
|
|
|
110
|
|
|
|
|
|
|
|
|
|
> obj.traits.hp.current -= 30
|
|
|
|
|
> obj.traits.hp.value
|
|
|
|
|
80
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The Gauge trait is subclass of the Counter, so you have access to the same
|
|
|
|
|
methods and properties where they make sense. So gauges can also have a
|
|
|
|
|
`.descs` dict to describe the intervals in text, and can use `.percent()` to
|
|
|
|
|
get how filled it is as a percentage etc.
|
|
|
|
|
|
|
|
|
|
The `.rate` is particularly relevant for gauges - useful for everything
|
|
|
|
|
from poison slowly draining your health, to resting gradually increasing it.
|
|
|
|
|
|
|
|
|
|
### Trait
|
|
|
|
|
|
|
|
|
|
A single value of any type.
|
|
|
|
|
|
|
|
|
|
This is the 'base' Trait, meant to inherit from if you want to invent
|
|
|
|
|
trait-types from scratch (most of the time you'll probably inherit from some of
|
|
|
|
|
the more advanced trait-type classes though).
|
|
|
|
|
|
|
|
|
|
Unlike other Trait-types, the single `.value` property of the base `Trait` can
|
|
|
|
|
be editied. The value can hold any data that can be stored in an Attribute. If
|
|
|
|
|
it's an integer/float you can do arithmetic with it, but otherwise this acts just
|
|
|
|
|
like a glorified Attribute.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
> obj.traits.add("mytrait", "My Trait", trait_type="trait", value=30)
|
|
|
|
|
> obj.traits.mytrait.value
|
|
|
|
|
30
|
|
|
|
|
|
|
|
|
|
> obj.traits.mytrait.value = "stringvalue"
|
|
|
|
|
> obj.traits.mytrait.value
|
|
|
|
|
"stringvalue"
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Expanding with your own Traits
|
|
|
|
|
|
|
|
|
|
A Trait is a class inhering from `evennia.contrib.rpg.traits.Trait` (or from one of
|
|
|
|
|
the existing Trait classes).
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
# in a file, say, 'mygame/world/traits.py'
|
|
|
|
|
|
|
|
|
|
from evennia.contrib.rpg.traits import StaticTrait
|
|
|
|
|
|
|
|
|
|
class RageTrait(StaticTrait):
|
|
|
|
|
|
|
|
|
|
trait_type = "rage"
|
|
|
|
|
default_keys = {
|
|
|
|
|
"rage": 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def berserk(self):
|
|
|
|
|
self.mod = 100
|
|
|
|
|
|
|
|
|
|
def sedate(self):
|
|
|
|
|
self.mod = 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Above is an example custom-trait-class "rage" that stores a property "rage" on
|
|
|
|
|
itself, with a default value of 0. This has all the functionality of a Trait -
|
|
|
|
|
for example, if you do del on the `rage` property, it will be set back to its
|
|
|
|
|
default (0). Above we also added some helper methods.
|
|
|
|
|
|
|
|
|
|
To add your custom RageTrait to Evennia, add the following to your settings file
|
|
|
|
|
(assuming your class is in mygame/world/traits.py):
|
|
|
|
|
|
|
|
|
|
TRAIT_CLASS_PATHS = ["world.traits.RageTrait"]
|
|
|
|
|
|
|
|
|
|
Reload the server and you should now be able to use your trait:
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
> obj.traits.add("mood", "A dark mood", rage=30, trait_type='rage')
|
|
|
|
|
> obj.traits.mood.rage
|
|
|
|
|
30
|
|
|
|
|
|
|
|
|
|
# as TraitProperty
|
|
|
|
|
|
|
|
|
|
class Character(DefaultCharacter):
|
|
|
|
|
rage = TraitProperty("A dark mood", rage=30, trait_type='rage')
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
----
|
|
|
|
|
|
|
|
|
|
<small>This document page is generated from `evennia/contrib/rpg/traits/README.md`. Changes to this
|
|
|
|
|
file will be overwritten, so edit that file rather than this one.</small>
|