Effect Duration in Forge controls how long effects remain active on target entities. It provides three distinct duration types that determine an effect's lifecycle and behavior.
For a practical guide on using durations, see the Quick Start Guide.
The DurationType enum defines how an effect persists over time:
public enum DurationType : byte
{
Instant = 0,
Infinite = 1,
HasDuration = 2
}DurationData encapsulates the duration configuration for an effect. Durations use ModifierMagnitude, enabling scalable, attribute-driven, custom-calculated, or set-by-caller values.
public readonly record struct DurationData(DurationType durationType, ModifierMagnitude? durationMagnitude = null)
{
public DurationType DurationType { get; }
public ModifierMagnitude? DurationMagnitude { get; }
}Instant effects apply their changes immediately and then end. They:
- Execute once at the moment of application.
- Don't remain active in the effects manager.
- Modify base attribute values directly.
- Don't return an
ActiveEffectHandlewhen applied.
// Create an instant damage effect
var damageEffectData = new EffectData(
"Direct Damage",
new DurationData(DurationType.Instant),
new[] {
new Modifier(
"CombatAttributeSet.CurrentHealth",
ModifierOperation.FlatBonus,
new ModifierMagnitude(MagnitudeCalculationType.ScalableFloat, new ScalableFloat(-25)))
}
);Infinite effects have no built-in expiration time and remain active until manually removed. They:
- Apply their modifiers continuously.
- Remain active indefinitely.
- Must be explicitly removed using
EffectsManager.RemoveEffect. - Are useful for permanent buffs, persistent status effects, and equipment bonuses.
Equipment-based buffs are a perfect use case for Infinite effects:
// Create an equipment buff that lasts until the item is unequipped
var swordBuffEffectData = new EffectData(
"Magic Sword Bonus",
new DurationData(DurationType.Infinite),
new[] {
new Modifier("CombatAttributeSet.AttackPower", ModifierOperation.FlatBonus, new ModifierMagnitude(MagnitudeCalculationType.ScalableFloat, new ScalableFloat(10))),
new Modifier("CombatAttributeSet.CriticalChance", ModifierOperation.PercentBonus, new ModifierMagnitude(MagnitudeCalculationType.ScalableFloat, new ScalableFloat(0.05f)))
}
);
var swordBuffEffect = new Effect(swordBuffEffectData, new EffectOwnership(character, character));
// When equipping the item
ActiveEffectHandle? equipmentBuffHandle = character.EffectsManager.ApplyEffect(swordBuffEffect);
// When unequipping the item
if (equipmentBuffHandle is not null)
{
character.EffectsManager.RemoveEffect(equipmentBuffHandle);
}Duration-based effects automatically expire after a specific amount of time. They:
- Apply their modifiers for a limited period.
- Automatically remove themselves when their duration ends.
- Are often used for temporary buffs and debuffs.
- Can use any
ModifierMagnitude, so durations can scale with level, attributes, custom calculators, or set-by-caller values. - Re-evaluate while active if the duration depends on non-snapshot inputs (live attribute captures or non-snapshot set-by-caller values).
// Create a temporary buff that lasts 30 seconds
var temporaryBuffEffectData = new EffectData(
"Temporary Speed Boost",
new DurationData(
DurationType.HasDuration,
new ModifierMagnitude(
MagnitudeCalculationType.ScalableFloat,
scalableFloatMagnitude: new ScalableFloat(30.0f))),
new[] {
new Modifier("MovementAttributeSet.Speed", ModifierOperation.PercentBonus, new ModifierMagnitude(MagnitudeCalculationType.ScalableFloat, new ScalableFloat(0.3f)))
}
);
// Attribute-driven duration (live capture)
var attributeDurationEffectData = new EffectData(
"Attribute-Based Shield",
new DurationData(
DurationType.HasDuration,
new ModifierMagnitude(
MagnitudeCalculationType.AttributeBased,
attributeBasedFloat: new AttributeBasedFloat(
new AttributeCaptureDefinition("StatAttributeSet.Strength", AttributeCaptureSource.Source, snapshot: false),
AttributeCalculationType.CurrentValue,
coefficient: new ScalableFloat(0.2f),
preMultiplyAdditiveValue: new ScalableFloat(0),
postMultiplyAdditiveValue: new ScalableFloat(5)))),
new[] {
new Modifier("CombatAttributeSet.CurrentHealth", ModifierOperation.FlatBonus, new ModifierMagnitude(MagnitudeCalculationType.ScalableFloat, new ScalableFloat(25)))
}
);When working with durations, several constraints apply to ensure effects behave consistently:
-
No Periodic Data: Instant effects cannot be periodic.
// INVALID - Instant effects can't have periodic data new EffectData( "Invalid Effect", new DurationData(DurationType.Instant), [/*...*/], periodicData: new PeriodicData(new ScalableFloat(1.0f)) // Error );
-
No Stacking: Instant effects cannot have stacking data.
// INVALID - Instant effects can't have stacking data new EffectData( "Invalid Effect", new DurationData(DurationType.Instant), [/*...*/], stackingData: new StackingData(/*...*/) // Error );
-
Snapshot Requirements: Instant effects must use snapshot attributes and level.
// INVALID - Instant effects must snapshot their level new EffectData( "Invalid Effect", new DurationData(DurationType.Instant), [/*...*/], snapshotLevel: false // Error );
-
No ModifierTags: Instant effects cannot apply tags through
ModifierTagsEffectComponent.// INVALID - Instant effects can't apply modifier tags new EffectData( "Invalid Effect", new DurationData(DurationType.Instant), [/*...*/], effectComponents: new[] { new ModifierTagsEffectComponent(new TagContainer()) } // Error );
-
Duration Required:
HasDurationeffects must provide a validDurationMagnitude.// INVALID - HasDuration requires a duration value new EffectData( "Invalid Effect", new DurationData(DurationType.HasDuration), // Error: missing duration [/*...*/] ); // VALID - Providing required duration magnitude new EffectData( "Valid Effect", new DurationData( DurationType.HasDuration, new ModifierMagnitude( MagnitudeCalculationType.ScalableFloat, scalableFloatMagnitude: new ScalableFloat(10.0f))), [/*...*/] );
-
Stacking Requirement: If stacking is configured,
ApplicationRefreshPolicymust be defined.// VALID - HasDuration with stacking needs ApplicationRefreshPolicy new EffectData( "Valid Effect", new DurationData( DurationType.HasDuration, new ModifierMagnitude( MagnitudeCalculationType.ScalableFloat, scalableFloatMagnitude: new ScalableFloat(10.0f))), [/*...*/], stackingData: new StackingData( stackLimit: new ScalableInt(3), initialStack: new ScalableInt(1), // ... other stacking data applicationRefreshPolicy: StackApplicationRefreshPolicy.RefreshOnSuccessfulApplication ) );
-
Periodic + Stacking: When both periodic and stacking are present,
ExecuteOnSuccessfulApplicationandApplicationResetPeriodPolicymust be defined in stacking data.
-
DurationMagnitude Scope:
DurationMagnitudeis only valid whenDurationTypeisHasDuration.// INVALID - Can't provide duration magniteude for non-HasDuration effects new DurationData( DurationType.Infinite, new ModifierMagnitude(MagnitudeCalculationType.ScalableFloat, new ScalableFloat(10.0f)) // Invalid );
-
Dynamic Re-evaluation: Active effects re-evaluate duration when non-snapshot inputs used by
DurationMagnitudechange (live attribute captures or non-snapshot set-by-caller values). Remaining duration adjusts accordingly.
-
Choose Appropriate Types:
- Use
Instantfor one-time effects like damage or healing. - Use
HasDurationfor temporary buffs and debuffs. - Use
Infinitefor permanent effects or those requiring manual removal.
- Use
-
Choose Appropriate ModifierMagnitudes:
- Use
ScalableFloatwith curves for level-based duration scaling. - Use
AttributeBasedfor attribute-driven duration. - Use
CustomCalculatorClassfor custom logic duration. - Use
SetByCallerfor duration based on external inputs.
- Use
-
Snapshot Attributes:
- Capture only the attributes needed.
- Decide between snapshot and live captures based on whether the duration should update while active.
-
Handle Effect Removal:
- Always store
ActiveEffectHandleforInfiniteeffects. - Consider early removal conditions for
HasDurationeffects. - Use
EffectsManager.RemoveEffectappropriately.
- Always store
-
Consider Performance:
- Minimize the number of long-duration effects active simultaneously.
- Use
Instanteffects when appropriate to avoid tracking overhead. - Be cautious with infinitely stacking
HasDurationeffects.