2022-11-15 20:46:50 +01:00
|
|
|
# Components
|
|
|
|
|
|
2023-04-29 08:52:29 +02:00
|
|
|
Contrib by ChrisLR, 2021
|
2022-11-15 20:46:50 +01:00
|
|
|
|
2023-04-29 08:52:29 +02:00
|
|
|
Expand typeclasses using a components/composition approach.
|
|
|
|
|
|
|
|
|
|
## The Components Contrib
|
2022-11-15 20:46:50 +01:00
|
|
|
|
|
|
|
|
This contrib introduces Components and Composition to Evennia.
|
|
|
|
|
Each 'Component' class represents a feature that will be 'enabled' on a typeclass instance.
|
|
|
|
|
You can register these components on an entire typeclass or a single object at runtime.
|
|
|
|
|
It supports both persisted attributes and in-memory attributes by using Evennia's AttributeHandler.
|
|
|
|
|
|
2023-04-29 08:52:29 +02:00
|
|
|
## Pros
|
2022-11-15 20:46:50 +01:00
|
|
|
- You can reuse a feature across multiple typeclasses without inheritance
|
|
|
|
|
- You can cleanly organize each feature into a self-contained class.
|
|
|
|
|
- You can check if your object supports a feature without checking its instance.
|
|
|
|
|
|
2023-04-29 08:52:29 +02:00
|
|
|
## Cons
|
2022-11-15 20:46:50 +01:00
|
|
|
- It introduces additional complexity.
|
|
|
|
|
- A host typeclass instance is required.
|
|
|
|
|
|
2023-04-29 08:52:29 +02:00
|
|
|
## How to install
|
2022-11-15 20:46:50 +01:00
|
|
|
|
|
|
|
|
To enable component support for a typeclass,
|
|
|
|
|
import and inherit the ComponentHolderMixin, similar to this
|
|
|
|
|
```python
|
|
|
|
|
from evennia.contrib.base_systems.components import ComponentHolderMixin
|
|
|
|
|
class Character(ComponentHolderMixin, DefaultCharacter):
|
|
|
|
|
# ...
|
|
|
|
|
```
|
|
|
|
|
|
2024-02-25 18:25:57 +01:00
|
|
|
Components need to inherit the Component class and require a unique name.
|
|
|
|
|
Components may inherit from other components but must specify another name.
|
|
|
|
|
You can assign the same 'slot' to both components to have alternative implementations.
|
2022-11-15 20:46:50 +01:00
|
|
|
```python
|
2023-01-06 18:13:41 +01:00
|
|
|
from evennia.contrib.base_systems.components import Component
|
2022-11-15 20:46:50 +01:00
|
|
|
|
2024-02-25 18:25:57 +01:00
|
|
|
|
2022-11-15 20:46:50 +01:00
|
|
|
class Health(Component):
|
|
|
|
|
name = "health"
|
2024-02-25 18:25:57 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class ItemHealth(Health):
|
|
|
|
|
name = "item_health"
|
|
|
|
|
slot = "health"
|
2022-11-15 20:46:50 +01:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Components may define DBFields or NDBFields at the class level.
|
|
|
|
|
DBField will store its values in the host's DB with a prefixed key.
|
|
|
|
|
NDBField will store its values in the host's NDB and will not persist.
|
|
|
|
|
The key used will be 'component_name::field_name'.
|
|
|
|
|
They use AttributeProperty under the hood.
|
|
|
|
|
|
|
|
|
|
Example:
|
|
|
|
|
```python
|
|
|
|
|
from evennia.contrib.base_systems.components import Component, DBField
|
|
|
|
|
|
|
|
|
|
class Health(Component):
|
|
|
|
|
health = DBField(default=1)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Note that default is optional and will default to None.
|
|
|
|
|
|
|
|
|
|
Adding a component to a host will also a similarly named tag with 'components' as category.
|
|
|
|
|
A Component named health will appear as key="health, category="components".
|
|
|
|
|
This allows you to retrieve objects with specific components by searching with the tag.
|
|
|
|
|
|
|
|
|
|
It is also possible to add Component Tags the same way, using TagField.
|
|
|
|
|
TagField accepts a default value and can be used to store a single or multiple tags.
|
|
|
|
|
Default values are automatically added when the component is added.
|
|
|
|
|
Component Tags are cleared from the host if the component is removed.
|
|
|
|
|
|
|
|
|
|
Example:
|
|
|
|
|
```python
|
|
|
|
|
from evennia.contrib.base_systems.components import Component, TagField
|
|
|
|
|
|
|
|
|
|
class Health(Component):
|
|
|
|
|
resistances = TagField()
|
|
|
|
|
vulnerability = TagField(default="fire", enforce_single=True)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The 'resistances' field in this example can be set to multiple times and it will keep the added tags.
|
|
|
|
|
The 'vulnerability' field in this example will override the previous tag with the new one.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Each typeclass using the ComponentHolderMixin can declare its components
|
|
|
|
|
in the class via the ComponentProperty.
|
|
|
|
|
These are components that will always be present in a typeclass.
|
|
|
|
|
You can also pass kwargs to override the default values
|
|
|
|
|
Example
|
|
|
|
|
```python
|
|
|
|
|
from evennia.contrib.base_systems.components import ComponentHolderMixin
|
|
|
|
|
class Character(ComponentHolderMixin, DefaultCharacter):
|
|
|
|
|
health = ComponentProperty("health", hp=10, max_hp=50)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
You can then use character.components.health to access it.
|
|
|
|
|
The shorter form character.cmp.health also exists.
|
|
|
|
|
character.health would also be accessible but only for typeclasses that have
|
|
|
|
|
this component defined on the class.
|
|
|
|
|
|
|
|
|
|
Alternatively you can add those components at runtime.
|
|
|
|
|
You will have to access those via the component handler.
|
|
|
|
|
Example
|
|
|
|
|
```python
|
|
|
|
|
character = self
|
|
|
|
|
vampirism = components.Vampirism.create(character)
|
|
|
|
|
character.components.add(vampirism)
|
|
|
|
|
|
|
|
|
|
...
|
|
|
|
|
|
2024-02-25 18:25:57 +01:00
|
|
|
vampirism = character.components.get("vampirism")
|
|
|
|
|
|
|
|
|
|
# Alternatively
|
|
|
|
|
vampirism = character.cmp.vampirism
|
2022-11-15 20:46:50 +01:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Keep in mind that all components must be imported to be visible in the listing.
|
|
|
|
|
As such, I recommend regrouping them in a package.
|
|
|
|
|
You can then import all your components in that package's __init__
|
|
|
|
|
|
|
|
|
|
Because of how Evennia import typeclasses and the behavior of python imports
|
|
|
|
|
I recommend placing the components package inside the typeclass package.
|
|
|
|
|
In other words, create a folder named components inside your typeclass folder.
|
|
|
|
|
Then, inside the 'typeclasses/__init__.py' file add the import to the folder, like
|
|
|
|
|
```python
|
|
|
|
|
from typeclasses import components
|
|
|
|
|
```
|
|
|
|
|
This ensures that the components package will be imported when the typeclasses are imported.
|
|
|
|
|
You will also need to import each components inside the package's own 'typeclasses/components/__init__.py' file.
|
|
|
|
|
You only need to import each module/file from there but importing the right class is a good practice.
|
|
|
|
|
```python
|
|
|
|
|
from typeclasses.components.health import Health
|
|
|
|
|
```
|
|
|
|
|
```python
|
|
|
|
|
from typeclasses.components import health
|
|
|
|
|
```
|
|
|
|
|
Both of the above examples will work.
|
|
|
|
|
|
2024-02-25 18:25:57 +01:00
|
|
|
## Known Issues
|
|
|
|
|
|
|
|
|
|
Assigning mutable default values such as a list to a DBField will share it across instances.
|
|
|
|
|
To avoid this, you must set autocreate=True on the field, like this.
|
|
|
|
|
```python
|
|
|
|
|
health = DBField(default=[], autocreate=True)
|
|
|
|
|
```
|
|
|
|
|
|
2023-04-29 08:52:29 +02:00
|
|
|
## Full Example
|
2022-11-15 20:46:50 +01:00
|
|
|
```python
|
|
|
|
|
from evennia.contrib.base_systems import components
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# This is the Component class
|
|
|
|
|
class Health(components.Component):
|
|
|
|
|
name = "health"
|
2023-04-29 08:52:29 +02:00
|
|
|
|
2022-11-15 20:46:50 +01:00
|
|
|
# Stores the current and max values as Attributes on the host, defaulting to 100
|
|
|
|
|
current = components.DBField(default=100)
|
|
|
|
|
max = components.DBField(default=100)
|
|
|
|
|
|
|
|
|
|
def damage(self, value):
|
|
|
|
|
if self.current <= 0:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
self.current -= value
|
|
|
|
|
if self.current > 0:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
self.current = 0
|
|
|
|
|
self.on_death()
|
|
|
|
|
|
|
|
|
|
def heal(self, value):
|
|
|
|
|
hp = self.current
|
|
|
|
|
hp += value
|
|
|
|
|
if hp >= self.max_hp:
|
|
|
|
|
hp = self.max_hp
|
|
|
|
|
|
|
|
|
|
self.current = hp
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def is_dead(self):
|
|
|
|
|
return self.current <= 0
|
|
|
|
|
|
|
|
|
|
def on_death(self):
|
|
|
|
|
# Behavior is defined on the typeclass
|
|
|
|
|
self.host.on_death()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# This is how the Character inherits the mixin and registers the component 'health'
|
|
|
|
|
class Character(ComponentHolderMixin, DefaultCharacter):
|
|
|
|
|
health = ComponentProperty("health")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# This is an example of a command that checks for the component
|
|
|
|
|
class Attack(Command):
|
|
|
|
|
key = "attack"
|
|
|
|
|
aliases = ('melee', 'hit')
|
|
|
|
|
|
|
|
|
|
def at_pre_cmd(self):
|
|
|
|
|
caller = self.caller
|
|
|
|
|
targets = self.caller.search(args, quiet=True)
|
|
|
|
|
valid_target = None
|
|
|
|
|
for target in targets:
|
|
|
|
|
# Attempt to retrieve the component, None is obtained if it does not exist.
|
|
|
|
|
if target.components.health:
|
|
|
|
|
valid_target = target
|
2023-04-29 08:52:29 +02:00
|
|
|
|
2022-11-15 20:46:50 +01:00
|
|
|
if not valid_target:
|
|
|
|
|
caller.msg("You can't attack that!")
|
|
|
|
|
return True
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
----
|
|
|
|
|
|
|
|
|
|
<small>This document page is generated from `evennia/contrib/base_systems/components/README.md`. Changes to this
|
|
|
|
|
file will be overwritten, so edit that file rather than this one.</small>
|