# 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 ```python 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. ```python 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: ```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) ... 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 ```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. ## 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) ``` ## Full Example ```python 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.