Skip to content

Custom Relic Effects

Brandon edited this page Jun 23, 2026 · 1 revision

Writing a RelicEffect is similiar to writing a RoomModifier. All RelicEffects are a subclass of RelicEffectBase, but like RoomModifiers, subclassing isn't enough. In addition, your class must implement a subinterface of IRelicEffect, which determines when your RelicEffect is called.

Overview

For the example RelicEffect we will be creating an effect that removes status effects of a specific type from units at the end of combat.

The RelicEffect accepts three parameters:

param_int - The type of status effects to remove.

param_excluded_subtypes - The character subtypes to ignore.

source_team - The target team the RelicEffect operates on.

Code

    public class RelicEffectClearStatusEffectsEndOfTurn : RelicEffectBase, ITurnPhaseEndOfTurnRelicEffect
    {
        private StatusEffectData.DisplayCategory displayCategory;
        private Team.Type sourceTeam;
        private VfxAtLoc? appliedVfx;
        private SubtypeData[]? excludeCharacterSubtypes;

        public override PropDescriptions CreateEditorInspectorDescriptions()
        {
            return new PropDescriptions
            {
                [RelicEffectFieldNames.ParamInt.GetFieldName()] = new PropDescription("StatusEffectData.DisplayCategory Enum"),
                [RelicEffectFieldNames.ParamSourceTeam.GetFieldName()] = new PropDescription("Target Team"),
                [RelicEffectFieldNames.ParamExcludeCharacterSubtypes.GetFieldName()] = new PropDescription("Excluded Character Subtypes")
            };
        }

        public override void Initialize(RelicState relicState, RelicData relicData, RelicEffectData relicEffectData)
        {
            base.Initialize(relicState, relicData, relicEffectData);
            displayCategory = (StatusEffectData.DisplayCategory) relicEffectData.GetParamInt();
            sourceTeam = relicEffectData.GetParamSourceTeam();
            appliedVfx = relicEffectData.GetAppliedVfx();
            excludeCharacterSubtypes = relicEffectData.GetParamExcludeCharacterSubtypes() ?? [];
        }

        public bool TestEffectEndOfTurn(EndOfTurnRelicEffectParams relicEffectParams, ICoreGameManagers coreGameManagers)
        {
            return true;
        }

        public IEnumerator ApplyEffectEndOfTurn(EndOfTurnRelicEffectParams relicEffectParams, ICoreGameManagers coreGameManagers)
        {
            using (GenericPools.Get<List<CharacterState>>(out var targets))
            {
                relicEffectParams.roomState.AddCharactersToList(targets, sourceTeam);
                using (GenericPools.Get<CharacterState.RemoveStatusEffectParams>(out var removeStatusEffectParams))
                {
                    removeStatusEffectParams.sourceRelicState = SourceRelicState;
                    removeStatusEffectParams.sourceCardState = null;
                    removeStatusEffectParams.showNotification = true;
                    removeStatusEffectParams.fromEffectType = this.GetType();
                    foreach (var characterState in targets)
                    {
                        if (!CharacterIsExcluded(characterState))
                        {
                            RemoveStatusEffectsFromCharacter(characterState, removeStatusEffectParams);
                        }
                    }
                }
            }
            yield break;
        }

        private void RemoveStatusEffectsFromCharacter(CharacterState characterState, RemoveStatusEffectParams removeStatusEffectParams)
        {
            using (GenericPools.Get<List<CharacterState.StatusEffectStack>>(out var statusEffectStacks))
            {
                characterState.GetStatusEffects(ref statusEffectStacks);
                foreach (var statusEffect in statusEffectStacks)
                {
                    if (statusEffect.State.GetDisplayCategory() == displayCategory)
                    {
                        int num = statusEffect.Count;
                        characterState.RemoveStatusEffect(statusEffect.State.GetStatusId(), num, removeStatusEffectParams, allowModification: false);
                        characterState.GetCharacterUI().ShowEffectVFX(characterState, appliedVfx);
                    }
                }
            }
        }
    }

Declaration

    public class RelicEffectClearStatusEffectsEndOfTurn : RelicEffectBase, ITurnPhaseEndOfTurnRelicEffect

We inherit from RelicEffectBase and implement the interface ITurnPhaseEndOfTurnRelicEffect. ITurnPhaseEndOfTurnRelicEffect is a IRelicEffect interface that is used to apply an effect after combat ends.

Initialize

Like with RoomModifiers, Initialize allows you to setup state. Remember to always call the base classes Initialize if you choose to override this function as it does necessary internal initializations.

TestEffectEndOfTurn

This function like TestEffect for Custom CardEffects determines whether or not to actually apply the effect. It is an interface function of ITurnPhaseEndOfTurnRelicEffect.

ApplyEffectEndOfTurn

In this function you apply the effect of the RelicEffect, as mentioned this function is called after combat ends on a floor. This function is where we actually implement the effects of the RelicEffect.

        public IEnumerator ApplyEffectEndOfTurn(EndOfTurnRelicEffectParams relicEffectParams, ICoreGameManagers coreGameManagers)
        {
            using (GenericPools.Get<List<CharacterState>>(out var targets))
            {
                relicEffectParams.roomState.AddCharactersToList(targets, sourceTeam);
                using (GenericPools.Get<CharacterState.RemoveStatusEffectParams>(out var removeStatusEffectParams))
                {
                    removeStatusEffectParams.sourceRelicState = SourceRelicState;
                    removeStatusEffectParams.sourceCardState = null;
                    removeStatusEffectParams.showNotification = true;
                    removeStatusEffectParams.fromEffectType = this.GetType();
                    foreach (var characterState in targets)
                    {
                        if (!CharacterIsExcluded(characterState))
                        {
                            RemoveStatusEffectsFromCharacter(characterState, removeStatusEffectParams);
                        }
                    }
                }
            }
            yield break;
        }
  1. We get the current characters in the room. The GenericPools call, gives us a preallocated type without using new, saving memory. We then get all of the characters in the Room through the EndOfTurnRelicEffectParams

  2. We get a preallocated RemoveStatusEffectParams and initialize its values appropriately to indicate the removal comes from a Relic.

  3. We then iterate over the characters in the room. If the character is not excluded by any of the subtypes provided then we will remove the status efects from the character

  4. Since the function is a Coroutine and we didn't call another coroutine yield return, so to exit from it we yield break to exit from it.

CharacterIsExcluded

This function checks if the character has an excluded subtype.

characterState.GetHasSubtype given a SubtypeData will return true if the character has the specified subtype.

RemoveStatusEffectsFromCharacter

Here we remove the status effects of a given type from the character.

  1. We get all of the status effects the character has.

  2. We then iterate over the status effects. If the DisplayCategory matches what we the RelicEffectData specfies then we remove the status effect. We then play a VFX (Visual effect) if that information was specified in the data as well.

DisplayCategory

Status Effects have a DisplayCategory for each specified type. This determines the color of the status effect in game and whether it is considered a buff (Positive: 0), a debuff (Negative: 1), a general status effect (Persistent: 2), or ability status effect (Ability: 3)

Note that DisplayCategory.Ability is a special value used only by the unit_ability status effect, which is a special status effect.

Testing

You can create a Relic using the RelicEffect to test this. An example usage of the RelicEffect that causes enemy units (Except Collectors and Bosses) to lose their debuffs at the end of the turn.

	"relic_effects": [
		{
			"id": "PurifyingDebuffs",
			"name": "@RelicEffectClearStatusEndOfTurn",
			"source_team": "heroes",
			"param_int": 1,
			"param_excluded_subtypes": [
				"SubtypesData_TreasureCollector",
				"SubtypesData_Boss"
			]
		}
	],

Clone this wiki locally