+ +
+

Components

+

Contrib by ChrisLR 2021

+
+
+

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 directly and require a name.

+
from evennia.contrib.components import Component
+
+class Health(Component):
+    name = "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

+

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_from_elsewhere = character.components.get("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.

+
+
+

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.

+
+ + +
+