Components

Contrib by ChrisLR, 2021

Expand typeclasses using a components/composition approach.

The Components Contrib

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.

Pros

  • 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.

Cons

  • It introduces additional complexity.

  • A host typeclass instance is required.

How to install

To enable component support for a typeclass, import and inherit the ComponentHolderMixin, similar to this

from evennia.contrib.base_systems.components import ComponentHolderMixin
class Character(ComponentHolderMixin, DefaultCharacter):
# ...

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.

from evennia.contrib.base_systems.components import Component


class Health(Component):
    name = "health"

    
class ItemHealth(Health):
    name = "item_health"
    slot = "health"

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:

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:

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

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

character = self
vampirism = components.Vampirism.create(character)
character.components.add(vampirism)

...

vampirism = character.components.get("vampirism")

# Alternatively
vampirism = character.cmp.vampirism

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

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.

from typeclasses.components.health import Health
from typeclasses.components import health

Both of the above examples will work.

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.

health = DBField(default=[], autocreate=True)

Full Example

from evennia.contrib.base_systems import components


# This is the Component class
class Health(components.Component):
    name = "health"

    # 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

        if not valid_target:
            caller.msg("You can't attack that!")
            return True

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.