This guide will help you quickly get started with the Forge framework, showing you how to set up a simple entity with attributes, tags, effects, stacking, and cues. It integrates examples from core systems such as modifiers, periodic effects, stacking effects, unique effects, cues, and custom components.
Install Forge via NuGet (recommended):
dotnet add package Gamesmiths.ForgeFor other installation methods, see the main README.
Let's create a simple player entity with three attributes: health, mana, strength and speed.
For that we need to first define an AttributeSet that will hold those attributes.
// First we need to create the player attribute set
public class PlayerAttributeSet : AttributeSet
{
public EntityAttribute Health { get; }
public EntityAttribute Mana { get; }
public EntityAttribute Strength { get; }
public EntityAttribute Speed { get; }
public PlayerAttributeSet()
{
// Initialize the attributes with the current, min and max values.
Health = InitializeAttribute(nameof(Health), 100, 0, 150);
Mana = InitializeAttribute(nameof(Mana), 100, 0, 100);
Strength = InitializeAttribute(nameof(Strength), 10, 0, 99);
Speed = InitializeAttribute(nameof(Speed), 5, 0, 10);
}
}
// Then create a new entity that implements IForgeEntity
public class Player : IForgeEntity
{
public EntityAttributes Attributes { get; }
public EntityTags Tags { get; }
public EffectsManager EffectsManager { get; }
public EntityAbilities Abilities { get; }
public EventManager Events { get; }
public Player(TagsManager tagsManager, CuesManager cuesManager)
{
// Initialize base tags during construction
var baseTags = new TagContainer(
tagsManager,
new HashSet<Tag>()
{
Tag.RequestTag(tagsManager, "character.player"),
Tag.RequestTag(tagsManager, "class.warrior")
});
Attributes = new EntityAttributes(new PlayerAttributeSet());
Tags = new EntityTags(baseTags);
EffectsManager = new EffectsManager(this, cuesManager);
Abilities = new(this);
Events = new();
}
}
// Initialize managers
var cuesManager = new CuesManager();
var tagsManager = new TagsManager(new string[]
{
"character.player",
"class.warrior",
"status.stunned",
"status.burning",
"status.enraged",
"status.immune.fire",
"cues.damage.fire",
"events.combat.damage",
"events.combat.hit",
"cooldown.fireball"
});
// Create player instance
var player = new Player(tagsManager, cuesManager);
// Access the player's attribute values
var health = player.Attributes["PlayerAttributeSet.Health"].CurrentValue; // 100
var mana = player.Attributes["PlayerAttributeSet.Mana"].CurrentValue; // 100
var strength = player.Attributes["PlayerAttributeSet.Strength"].CurrentValue; // 10
var speed = player.Attributes["PlayerAttributeSet.Speed"].CurrentValue; // 5Tags allow you to classify entities and define conditions for effects. Base tags are immutable and assignable only at creation time.
You should generally do checks against the CombinedTags property since it includes both the BaseTags and ModifierTags.
// Tags must be requested through the static method Tag.RequestTag
var playerTag = Tag.RequestTag(tagsManager, "character.player");
var warriorTag = Tag.RequestTag(tagsManager, "class.warrior");
// Check if the player has specific tags
bool isPlayer = player.Tags.CombinedTags.HasTag(playerTag);
bool isWarrior = player.Tags.CombinedTags.HasTag(warriorTag);Effects are the core way to modify entity attributes or apply status conditions.
Instant effects modify the BaseValue of an attribute directly.
The following is an effect that causes 10 damage directly to the player's base health:
// Create a damage effect data
var damageEffectData = new EffectData(
"Basic Attack",
new DurationData(DurationType.Instant),
new[] {
new Modifier(
"PlayerAttributeSet.Health",
ModifierOperation.FlatBonus,
new ModifierMagnitude(
MagnitudeCalculationType.ScalableFloat,
new ScalableFloat(-10) // -10 damage
)
)
});
// Create an effect instance and apply the effect
var damageEffect = new Effect(damageEffectData, new EffectOwnership(player, player));
player.EffectsManager.ApplyEffect(damageEffect);
// Check the current health value
int currentHealth = player.Attributes["PlayerAttributeSet.Health"].CurrentValue;
int baseHealth = player.Attributes["PlayerAttributeSet.Health"].BaseValue;
Console.WriteLine($"Player health after damage: {currentHealth}"); // 90 - Assuming initial health was 100
Console.WriteLine($"Player base health after damage: {baseHealth}"); // 90 - Same as CurrentValueYou can create temporary buffs through DurationType.HasDuration. Temporary effects modify the Modifier value of an attribute and last for the configured time.
The following effect increases the player's strength by +5 for 10 seconds:
// Create a strength buff effect that lasts for 10 seconds
var strengthBuffEffectData = new EffectData(
"Strength Potion",
new DurationData(
DurationType.HasDuration,
new ModifierMagnitude(
MagnitudeCalculationType.ScalableFloat,
new ScalableFloat(10.0f) // 10 seconds duration
)
),
new[] {
new Modifier(
"PlayerAttributeSet.Strength",
ModifierOperation.FlatBonus,
new ModifierMagnitude(
MagnitudeCalculationType.ScalableFloat,
new ScalableFloat(5) // +5 strength
)
)
}
);
// Create and apply the effect
var strengthBuffEffect = new Effect(strengthBuffEffectData, new EffectOwnership(player, player));
ActiveEffectHandle? buffHandle = player.EffectsManager.ApplyEffect(strengthBuffEffect);
// Check strength values
int buffedStrength = player.Attributes["PlayerAttributeSet.Strength"].CurrentValue;
int baseStrength = player.Attributes["PlayerAttributeSet.Strength"].BaseValue;
int strengthModifier = player.Attributes["PlayerAttributeSet.Strength"].ValidModifier;
Console.WriteLine($"Player strength with buff: {buffedStrength}"); // 15 - Assuming base strength was 10
Console.WriteLine($"Player base strength: {baseStrength}"); // 10 - Base value remains unchanged
Console.WriteLine($"Player strength modifier: {strengthModifier}"); // +5 from the buffRemember to update your EffectsManager periodically in your game loop:
// In your game's update loop
void Update(float deltaTime)
{
player.EffectsManager.UpdateEffects(deltaTime);
}
// Or in turn-based games
void EndTurn()
{
player.EffectsManager.UpdateEffects(1.0f);
}Use DurationType.Infinite for effects that remain active indefinitely until manually removed. Infinite effects affect attribute Modifiers in the same way as temporary effects.
The following effect increases the player's strength by +10 until actively removed:
// Create an infinite effect for a piece of equipment
var equipmentBuffEffectData = new EffectData(
"Magic Sword Bonus",
new DurationData(DurationType.Infinite),
new[] {
new Modifier(
"PlayerAttributeSet.Strength",
ModifierOperation.FlatBonus,
new ModifierMagnitude(
MagnitudeCalculationType.ScalableFloat,
new ScalableFloat(10) // +10 Strength
)
)
}
);
// Apply the equipment buff
var equipmentBuffEffect = new Effect(equipmentBuffEffectData, new EffectOwnership(player, player));
var activeEffectHandle = player.EffectsManager.ApplyEffect(equipmentBuffEffect);
// Remove the effect manually (e.g., when the item is unequipped)
if (activeEffectHandle != null)
{
player.EffectsManager.RemoveEffect(activeEffectHandle);
}You can create effects that "tick" at pre-defined intervals. Periodic effects, like instant effects, directly modify the BaseValue of an attribute.
This poison effect ticks every 2 seconds for 10 seconds, causing -5 damage per tick. It also ticks immediately when applied, resulting in 6 total ticks (1 initial + 5 periodic) for -30 total damage:
// Create a poison effect that ticks every 2 seconds for 10 seconds
var poisonEffectData = new EffectData(
"Poison",
new DurationData(
DurationType.HasDuration,
new ModifierMagnitude(
MagnitudeCalculationType.ScalableFloat,
new ScalableFloat(10.0f)
)
),
new[] {
new Modifier(
"PlayerAttributeSet.Health",
ModifierOperation.FlatBonus,
new ModifierMagnitude(
MagnitudeCalculationType.ScalableFloat,
new ScalableFloat(-5) // -5 damage per tick
)
)
},
periodicData: new PeriodicData(
period: new ScalableFloat(2.0f),
executeOnApplication: true,
periodInhibitionRemovedPolicy: PeriodInhibitionRemovedPolicy.ResetPeriod
)
);
// Apply the poison effect
var poisonEffect = new Effect(poisonEffectData, new EffectOwnership(player, player));
player.EffectsManager.ApplyEffect(poisonEffect);
// Simulate 10 seconds of game time
player.EffectsManager.UpdateEffects(10f);
// After 6 ticks total (including the initial execute), health should be 70 if it started at 100
int currentHealthAfterPoison = player.Attributes["PlayerAttributeSet.Health"].CurrentValue; // 70
int baseHealthAfterPoison = player.Attributes["PlayerAttributeSet.Health"].BaseValue; // 70This example shows a poison effect that ticks every 2 seconds for -3 damage per tick over 6 seconds. It can stack up to three times, with each stack adding -3 damage to each tick. With three applications, it will cause -27 total damage over 6 seconds:
// Create a poison effect that stacks up to 3 times
var stackingPoisonEffectData = new EffectData(
"Stacking Poison",
new DurationData(
DurationType.HasDuration,
new ModifierMagnitude(
MagnitudeCalculationType.ScalableFloat,
new ScalableFloat(6.0f) // Each stack lasts 6 seconds
)
),
new[] {
new Modifier(
"PlayerAttributeSet.Health",
ModifierOperation.FlatBonus,
new ModifierMagnitude(
MagnitudeCalculationType.ScalableFloat,
new ScalableFloat(-3) // -3 damage per stack
)
)
},
periodicData: new PeriodicData(
period: new ScalableFloat(2.0f),
executeOnApplication: false,
periodInhibitionRemovedPolicy: PeriodInhibitionRemovedPolicy.ResetPeriod
),
stackingData: new StackingData(
stackLimit: new ScalableInt(3), // Max 3 stacks
initialStack: new ScalableInt(1), // Starts with 1 stack
overflowPolicy: StackOverflowPolicy.DenyApplication, // Deny if max stacks reached
magnitudePolicy: StackMagnitudePolicy.Sum, // Total damage increases with stacks
expirationPolicy: StackExpirationPolicy.ClearEntireStack, // All stacks expire together
applicationRefreshPolicy: StackApplicationRefreshPolicy.RefreshOnSuccessfulApplication,
stackPolicy: StackPolicy.AggregateBySource, // Aggregate stacks from the same source
stackLevelPolicy: StackLevelPolicy.SegregateLevels, // Each stack can have its own level
// The next two values must be defined because this is a periodic effect with stacking
executeOnSuccessfulApplication: false, // Do not execute on successful application
applicationResetPeriodPolicy: StackApplicationResetPeriodPolicy.ResetOnSuccessfulApplication // Reset period on successful application
)
);
// Apply the stacking poison effect multiple times to demonstrate stacking
var stackingPoisonEffect = new Effect(stackingPoisonEffectData, new EffectOwnership(player, player));
// Apply the effect three times, each time adds a stack for a total of -9 damage per tick
player.EffectsManager.ApplyEffect(stackingPoisonEffect);
player.EffectsManager.ApplyEffect(stackingPoisonEffect);
player.EffectsManager.ApplyEffect(stackingPoisonEffect);
// Simulate 6 seconds of game time
player.EffectsManager.UpdateEffects(6f);
// After three ticks at -9 per tick, total damage is -27
var healthAfterStacks = player.Attributes["PlayerAttributeSet.Health"].CurrentValue; // 73 if starting at 100This example shows an effect that is unique to a target and can be overridden only by a higher-level version of the same effect:
// Define the unique effect data
var uniqueEffectData = new EffectData(
"Unique Buff",
new DurationData(
DurationType.HasDuration,
new ModifierMagnitude(
MagnitudeCalculationType.ScalableFloat,
new ScalableFloat(10.0f) // Lasts 10 seconds
)
),
new[] {
new Modifier(
"PlayerAttributeSet.Strength",
ModifierOperation.FlatBonus,
new ModifierMagnitude(
MagnitudeCalculationType.ScalableFloat,
new ScalableFloat(5.0f, new Curve(new[]
{
new CurveKey(1, 1.0f), // Level 1: base value × 1 = +5 Strength
new CurveKey(2, 2.0f) // Level 2: base value × 2 = +10 Strength
}))
)
)
},
stackingData: new StackingData(
stackLimit: new ScalableInt(1), // Only 1 instance allowed
initialStack: new ScalableInt(1), // Starts with 1 stack
overflowPolicy: StackOverflowPolicy.AllowApplication, // Allow application even if max stacks reached
magnitudePolicy: StackMagnitudePolicy.Sum, // Total damage increases with stacks
expirationPolicy: StackExpirationPolicy.ClearEntireStack, // All stacks expire together
applicationRefreshPolicy: StackApplicationRefreshPolicy.RefreshOnSuccessfulApplication,
stackPolicy: StackPolicy.AggregateByTarget, // Only one effect per target
ownerDenialPolicy: StackOwnerDenialPolicy.AlwaysAllow, // Always allow application regardless of owner
ownerOverridePolicy: StackOwnerOverridePolicy.Override, // Override existing effect if applied again
ownerOverrideStackCountPolicy: StackOwnerOverrideStackCountPolicy.ResetStacks, // Reset stack count on override
stackLevelPolicy: StackLevelPolicy.AggregateLevels, // Aggregate levels of the effect
levelOverridePolicy: LevelComparison.Equal | LevelComparison.Higher, // Allow equal or higher-level effects to override
levelDenialPolicy: LevelComparison.Lower, // Deny lower-level effects
levelOverrideStackCountPolicy: StackLevelOverrideStackCountPolicy.ResetStacks // Reset stack count on override
)
);
// Apply the unique effect at level 1
var uniqueEffectLevel1 = new Effect(uniqueEffectData, new EffectOwnership(player, player), 1);
player.EffectsManager.ApplyEffect(uniqueEffectLevel1);
Console.WriteLine("Level 1 Unique Buff applied: +5 Strength.");
// Apply the unique effect at level 2 (overrides level 1)
var uniqueEffectLevel2 = new Effect(uniqueEffectData, new EffectOwnership(player, player), 2);
player.EffectsManager.ApplyEffect(uniqueEffectLevel2);
Console.WriteLine("Level 2 Unique Buff applied, overriding Level 1: +10 Strength.");This example shows how to add a temporary "status.stunned" tag to the target while an effect is active. The effect also sets the target's speed to 0:
// Create a "Stunned" effect that adds a tag and reduces speed to 0
var stunEffectData = new EffectData(
"Stunned",
new DurationData(
DurationType.HasDuration,
new ModifierMagnitude(
MagnitudeCalculationType.ScalableFloat,
new ScalableFloat(3.0f) // 3 seconds duration
)
),
new[] {
new Modifier(
"PlayerAttributeSet.Speed",
ModifierOperation.Override,
new ModifierMagnitude(
MagnitudeCalculationType.ScalableFloat,
new ScalableFloat(0) // Set speed to 0
)
)
},
effectComponents: new[] {
new ModifierTagsEffectComponent(
tagsManager.RequestTagContainer(new[] { "status.stunned" })
)
}
);
// Apply the stun effect
var stunEffect = new Effect(stunEffectData, new EffectOwnership(player, player));
ActiveEffectHandle? stunHandle = player.EffectsManager.ApplyEffect(stunEffect);
// Check if player is stunned
bool isStunned = player.Tags.CombinedTags.HasTag(Tag.RequestTag(tagsManager, "status.stunned"));
int currentSpeed = player.Attributes["PlayerAttributeSet.Speed"].CurrentValue;
Console.WriteLine($"Player stunned: {isStunned}, Speed: {currentSpeed}");This example shows an effect that won't be applied if the target has the "status.immune.fire" tag:
// Create an effect that cannot be applied if the target has the "status.immune.fire" tag
var fireAttackData = new EffectData(
"Fire Attack",
new DurationData(DurationType.Instant),
new[] {
new Modifier(
"PlayerAttributeSet.Health",
ModifierOperation.FlatBonus,
new ModifierMagnitude(
MagnitudeCalculationType.ScalableFloat,
new ScalableFloat(-15) // -15 damage
)
)
},
effectComponents: new[] {
new TargetTagRequirementsEffectComponent(
applicationTagRequirements: new TagRequirements(
ignoreTags: tagsManager.RequestTagContainer(new[] { "status.immune.fire" }) // Prevent application if target has "status.immune.fire"
)
)
}
);
// Apply the fire attack effect
var fireAttack = new Effect(fireAttackData, new EffectOwnership(player, player));
player.EffectsManager.ApplyEffect(fireAttack);
// If the player had the "status.immune.fire" tag, no damage would be appliedYou can extend effects with custom logic using components.
This component applies an additional effect when the target's Health attribute falls below a specified threshold at the time of application:
// Custom component that applies extra effect when Health is below threshold
public class DamageThresholdComponent : IEffectComponent
{
private readonly float _threshold;
private readonly Effect _bonusEffect;
public DamageThresholdComponent(float threshold, Effect bonusEffect)
{
_threshold = threshold;
_bonusEffect = bonusEffect;
}
public void OnEffectApplied(IForgeEntity target, in EffectEvaluatedData effectEvaluatedData)
{
// Check if target health is below threshold and apply bonus effect if it is
var health = target.Attributes["PlayerAttributeSet.Health"];
if (health.CurrentValue <= _threshold)
{
target.EffectsManager.ApplyEffect(_bonusEffect);
}
}
}
// Create a damage effect with threshold component
var thresholdAttackData = new EffectData(
"Basic Attack",
new DurationData(DurationType.Instant),
new[] {
new Modifier(
"PlayerAttributeSet.Health",
ModifierOperation.FlatBonus,
new ModifierMagnitude(
MagnitudeCalculationType.ScalableFloat,
new ScalableFloat(-10) // -10 damage
)
)
},
effectComponents: [
new DamageThresholdComponent(90, stunEffect) // Apply stun if health drops below 90
]
);
// Apply the effect twice (second application should trigger the stun)
var thresholdAttack = new Effect(thresholdAttackData, new EffectOwnership(player, player));
player.EffectsManager.ApplyEffect(thresholdAttack);
player.EffectsManager.ApplyEffect(thresholdAttack);
// Check if the stun was applied (will be true if health was 90 or less after damage)
bool isStunned = player.Tags.CombinedTags.HasTag(Tag.RequestTag(tagsManager, "status.stunned"));Custom Calculators let you create modifiers that depend on multiple attributes.
This calculator calculates damage based on both strength and speed attributes: (speed * 2) + (strength * 0.5f):
// Create a custom calculator that scales damage based on strength and speed
public class StrengthDamageCalculator : CustomModifierMagnitudeCalculator
{
public AttributeCaptureDefinition StrengthAttribute { get; }
public AttributeCaptureDefinition SpeedAttribute { get; }
public StrengthDamageCalculator()
{
StrengthAttribute = new AttributeCaptureDefinition(
"PlayerAttributeSet.Strength",
AttributeCaptureSource.Source,
snapshot: true);
SpeedAttribute = new AttributeCaptureDefinition(
"PlayerAttributeSet.Speed",
AttributeCaptureSource.Source,
snapshot: true);
AttributesToCapture.Add(StrengthAttribute);
AttributesToCapture.Add(SpeedAttribute);
}
public override float CalculateBaseMagnitude(Effect effect, IForgeEntity target, EffectEvaluatedData? effectEvaluatedData)
{
int strength = CaptureAttributeMagnitude(StrengthAttribute, effect, target, effectEvaluatedData);
int speed = CaptureAttributeMagnitude(SpeedAttribute, effect, target, effectEvaluatedData);
// Calculate damage based on strength and speed
float damage = (speed * 2) + (strength * 0.5f);
return -damage; // Negative for damage
}
}
// Use the custom calculator in an effect
var calculatedDamageEffectData = new EffectData(
"Power Attack",
new DurationData(DurationType.Instant),
new[] {
new Modifier(
"PlayerAttributeSet.Health",
ModifierOperation.FlatBonus,
new ModifierMagnitude(
MagnitudeCalculationType.CustomCalculatorClass,
customCalculationBasedFloat: new CustomCalculationBasedFloat(
new StrengthDamageCalculator(),
new ScalableFloat(1.0f), // Coefficient
new ScalableFloat(0), // PreMultiply
new ScalableFloat(0) // PostMultiply
)
)
)
}
);
// Apply the effect
var calculatedDamageEffect = new Effect(calculatedDamageEffectData, new EffectOwnership(player, player));
player.EffectsManager.ApplyEffect(calculatedDamageEffect);Custom Executions allow you to modify multiple attributes with a single calculation.
This execution drains health from the target based on the source's strength, and returns 80% of that health to the source:
public class HealthDrainExecution : CustomExecution
{
// Define attributes to capture and modify
public AttributeCaptureDefinition TargetHealth { get; }
public AttributeCaptureDefinition SourceHealth { get; }
public AttributeCaptureDefinition SourceStrength { get; }
public HealthDrainExecution()
{
// Capture target health
TargetHealth = new AttributeCaptureDefinition(
"PlayerAttributeSet.Health",
AttributeCaptureSource.Target,
snapshot: false);
SourceHealth = new AttributeCaptureDefinition(
"PlayerAttributeSet.Health",
AttributeCaptureSource.Source,
snapshot: false);
SourceStrength = new AttributeCaptureDefinition(
"PlayerAttributeSet.Strength",
AttributeCaptureSource.Source,
snapshot: false);
// Register attributes for capture
AttributesToCapture.Add(TargetHealth);
AttributesToCapture.Add(SourceHealth);
AttributesToCapture.Add(SourceStrength);
}
public override ModifierEvaluatedData[] EvaluateExecution(Effect effect, IForgeEntity target, EffectEvaluatedData? effectEvaluatedData)
{
var results = new List<ModifierEvaluatedData>();
// Get attribute values
int targetHealth = CaptureAttributeMagnitude(TargetHealth, effect, target, effectEvaluatedData);
int sourceHealth = CaptureAttributeMagnitude(SourceHealth, effect, effect.Ownership.Source, effectEvaluatedData);
int sourceStrength = CaptureAttributeMagnitude(SourceStrength, effect, effect.Ownership.Source, effectEvaluatedData);
// Calculate health drain amount based on source strength
float drainAmount = sourceStrength * 0.5f;
// Cap the drain at the target's available health
drainAmount = Math.Min(drainAmount, targetHealth);
// Apply health reduction to target if attribute exists
if (TargetHealth.TryGetAttribute(target, out EntityAttribute? targetHealthAttribute))
{
results.Add(new ModifierEvaluatedData(
targetHealthAttribute,
ModifierOperation.FlatBonus,
-drainAmount, // Negative for drain
channel: 0
));
}
// Apply health gain to source if attribute exists
if (SourceHealth.TryGetAttribute(effect.Ownership.Source, out EntityAttribute? sourceHealthAttribute))
{
results.Add(new ModifierEvaluatedData(
sourceHealthAttribute,
ModifierOperation.FlatBonus,
drainAmount * 0.8f, // Transfer 80% of drained health
channel: 0
));
}
return results.ToArray();
}
}
// Use the custom execution in an effect
var drainEffectData = new EffectData(
"Life Drain",
new DurationData(DurationType.Instant),
customExecutions: new[] {
new HealthDrainExecution()
}
);
// Apply the effect (using player1 as source and player2 as target)
var drainEffect = new Effect(drainEffectData, new EffectOwnership(player1, player1));
player2.EffectsManager.ApplyEffect(drainEffect);Cues bridge gameplay mechanics with visual and audio feedback. Here's how to define, trigger, and implement them.
For a cue handler to work, you must register it with the CuesManager using the appropriate tag:
// Register the cue with the manager
cuesManager.RegisterCue(
Tag.RequestTag(tagsManager, "cues.damage.fire"),
new FireDamageCueHandler()
);This example shows how to trigger cues as part of an effect:
// Define a burning effect that includes the fire damage cue
var burningEffectData = new EffectData(
"Burning",
new DurationData(
DurationType.HasDuration,
new ModifierMagnitude(
MagnitudeCalculationType.ScalableFloat,
new ScalableFloat(5.0f)
)
),
new[] {
new Modifier(
"PlayerAttributeSet.Health",
ModifierOperation.FlatBonus,
new ModifierMagnitude(
MagnitudeCalculationType.ScalableFloat,
new ScalableFloat(-5) // Damage per tick
)
)
},
periodicData: new PeriodicData(
period: new ScalableFloat(1.0f), // Ticks every second
executeOnApplication: true,
periodInhibitionRemovedPolicy: PeriodInhibitionRemovedPolicy.ResetPeriod
),
cues: new[] {
new CueData(
cueTags: tagsManager.RequestTagContainer(new[] { "cues.damage.fire" }),
minValue: 0,
maxValue: 100,
magnitudeType: CueMagnitudeType.AttributeValueChange,
magnitudeAttribute: "PlayerAttributeSet.Health" // Tracks health changes
)
}
);
// Apply the burning effect
var burningEffect = new Effect(burningEffectData, new EffectOwnership(player, player));
player.EffectsManager.ApplyEffect(burningEffect);
player.EffectsManager.UpdateEffects(5f);You can trigger cues directly through the CuesManager:
// Manually trigger a fire damage cue with custom parameters
var cueParameters = new CueParameters(
magnitude: 25, // Raw damage value
normalizedMagnitude: 0.25f, // Normalized between 0-1
source: player,
customParameters: new Dictionary<StringKey, object>
{
{ "DamageType", "Fire" },
{ "IsCritical", true }
}
);
cuesManager.ExecuteCue(
cueTag: Tag.RequestTag(tagsManager, "cues.damage.fire"),
target: player,
parameters: cueParameters
);This example shows a simple cue handler that outputs messages to the console. In a real game, you would use this to trigger visual effects, sounds, or other feedback:
public class FireDamageCueHandler : ICueHandler
{
public void OnExecute(IForgeEntity? target, CueParameters? parameters)
{
if (parameters.HasValue)
{
Console.WriteLine($"Fire damage executed: {parameters.Value.Magnitude}");
}
}
public void OnApply(IForgeEntity? target, CueParameters? parameters)
{
// Logic for when a persistent cue starts (e.g., play fire animation)
if (target != null)
{
Console.WriteLine("Fire damage cue applied to target.");
}
}
public void OnRemove(IForgeEntity? target, bool interrupted)
{
// Logic for when a cue ends (e.g., stop fire animation)
Console.WriteLine("Fire damage cue removed.");
}
public void OnUpdate(IForgeEntity? target, CueParameters? parameters)
{
// Logic for updating persistent cues (e.g., adjust fire intensity)
if (parameters.HasValue)
{
Console.WriteLine($"Fire damage cue updated with magnitude: {parameters.Value.Magnitude}");
}
}
}The Events system allows entities to communicate and trigger reactions through tagged events.
// Subscribe to the damage event
var damageTag = Tag.RequestTag(tagsManager, "events.combat.damage");
player.Events.Subscribe(damageTag, eventData =>
{
Console.WriteLine($"Player took {eventData.EventMagnitude} damage!");
});
// Raise the event
player.Events.Raise(new EventData
{
EventTags = damageTag.GetSingleTagContainer(),
Source = null,
Target = player,
EventMagnitude = 50f
});You can also instantiate your own EventManager and use it in any part of your code, providing a way to handle global or system-specific events independently of entities.
You can optimize events to avoid boxing by using generic EventData.
// Define a strongly typed payload
public record struct DamageInfo(int Value, DamageType DamageType, bool IsCritical);
var damageTag = Tag.RequestTag(tagsManager, "events.combat.damage");
// Subscribe using the specific payload type
player.Events.Subscribe<CombatLogPayload>(damageTag, eventData =>
{
Console.WriteLine(
$"[Combat Log] Damage: {eventData.Payload.Value}, " +
$"Type: {eventData.Payload.DamageType}, " +
$"Critical: {eventData.Payload.IsCritical}"
);
});
// Raise the event with the typed payload
player.Events.Raise(new EventData<DamageInfo>
{
EventTags = damageTag.GetSingleTagContainer(),
Source = null,
Target = player,
Payload = new DamageInfo(120, DamageType.Physical, true)
});Abilities are discrete actions that can have costs, cooldowns, and custom behaviors. They can be triggered manually, by events, or in reaction to tag application.
When defining an ability, you typically configure effects for costs and cooldowns, implement a behavior, and then tie it all together in the AbilityData.
// Define cost: 20 Mana
var fireballCostEffect = new EffectData(
"Fireball Mana Cost",
new DurationData(DurationType.Instant),
new[] {
new Modifier(
"PlayerAttributeSet.Mana",
ModifierOperation.FlatBonus,
new ModifierMagnitude(
MagnitudeCalculationType.ScalableFloat,
new ScalableFloat(-20)
)
)
});
// Define cooldown: 10 seconds
var fireballCooldownEffect = new EffectData(
"Fireball Cooldown",
new DurationData(
DurationType.HasDuration,
new ModifierMagnitude(
MagnitudeCalculationType.ScalableFloat,
new ScalableFloat(10.0f))),
effectComponents: new[] {
new ModifierTagsEffectComponent(
tagsManager.RequestTagContainer(new[] { "cooldown.fireball" })
)
});
// Define behavior
public class FireballBehavior : IAbilityBehavior
{
public void OnStarted(AbilityBehaviorContext context)
{
// Apply costs and cooldowns
context.AbilityHandle.CommitAbility();
Console.WriteLine("Fireball cast!");
context.InstanceHandle.End();
}
public void OnEnded(AbilityBehaviorContext context)
{
// Do any necessary cleanups
}
}
// Define Ability Data
var fireballData = new AbilityData(
name: "Fireball",
costEffect: fireballCostEffect,
cooldownEffects: [fireballCooldownEffect],
instancingPolicy: AbilityInstancingPolicy.PerEntity,
behaviorFactory: () => new FireballBehavior());Abilities can be granted through effects and will be tied to the effect's lifetime. If the effect has a duration, the ability will be granted only while the effect is active.
// Grant an ability via a GrantAbilityEffectComponent
var grantConfig = new GrantAbilityConfig
{
AbilityData = fireballData,
ScalableLevel = new ScalableInt(1),
LevelOverridePolicy = LevelComparison.None,
RemovalPolicy = AbilityDeactivationPolicy.CancelImmediately,
InhibitionPolicy = AbilityDeactivationPolicy.CancelImmediately,
};
var grantAbilityComponent = new GrantAbilityEffectComponent([grantConfig]);
// Wrap the component in an infinite effect
var grantFireballEffect = new EffectData(
"Grant Fireball Effect",
new DurationData(DurationType.Infinite),
effectComponents: [grantAbilityComponent]
);
// Apply the effect to grant the ability (e.g., when Wand of Fireball is equipped)
var grantEffectHandle = player.EffectsManager.ApplyEffect(
new Effect(grantFireballEffect, new EffectOwnership(player, player)));
// You can access the granted ability handle directly from the component
// This list contains handles for all abilities granted by this specific effect component instance
AbilityHandle grantedHandle = grantAbilityComponent.GrantedAbilities[0];
// The ability is now granted. To remove it, simply remove the effect. (e.g., when the wand is unequipped)
player.EffectsManager.RemoveEffect(grantEffectHandle);Abilities can be activated directly through their handle. You can also use the handle to find out what's the required cost and cooldown of the ability, useful for updating the UI. The activation returns flags indicating failure reasons, which is useful for player feedback.
// Retrieve the handle from the granted ability component or via TryGetAbility
if (player.Abilities.TryGetAbility(fireballData, out AbilityHandle? handle))
{
// Check cooldown state before activation (useful for UI)
var cooldowns = handle.GetCooldownData();
foreach (var cd in cooldowns)
{
Console.WriteLine($"Cooldown remaining: {cd.RemainingTime}");
}
// Check cost state before activation (useful for UI)
var costs = handle.GetCostData();
foreach (var cost in costs)
{
// Assuming you want to check Mana costs
if (cost.AttributeName == "PlayerAttributeSet.Mana")
{
Console.WriteLine($"Mana Cost: {cost.Value}");
}
}
// Try to activate
if (handle.Activate(out AbilityActivationFailures failures))
{
Console.WriteLine("Activation successful");
}
else
{
Console.WriteLine($"Activation failed: {failures}");
if (failures.HasFlag(AbilityActivationFailures.InsufficientResources))
{
Console.WriteLine("Not enough mana!");
}
}
}One other way to grant an ability permanently is directly through the entity's EntityAbilities component.
// Grant permanently
AbilityHandle handle = player.Abilities.GrantAbilityPermanently(
fireballData,
abilityLevel: 1,
levelOverridePolicy: LevelComparison.None,
sourceEntity: player);In some cases you just want a quick way to activate an ability on a target without creating a persistent effect or granting it permanently.
The example below shows the use of a "Scroll of Fireball" that grants the fireball ability transiently, attempts to activate it immediately, and then removes the grant once the ability concludes or fails.
AbilityHandle? handle = player.Abilities.GrantAbilityAndActivateOnce(
abilityData: fireballData,
abilityLevel: 1,
levelOverridePolicy: LevelComparison.None,
out AbilityActivationFailures failureFlags,
targetEntity: enemy, // The target of the fireball
sourceEntity: scrollItem // The source (e.g., the scroll item)
);
if (handle is not null)
{
Console.WriteLine("Scroll used successfully! Fireball cast.");
}
else
{
Console.WriteLine($"Failed to use scroll: {failureFlags}");
}You can configure abilities to trigger automatically when specific events occur.
var hitTag = Tag.RequestTag(tagsManager, "events.combat.hit");
var autoShieldData = new AbilityData(
name: "Auto Shield",
// Configure the trigger
abilityTriggerData: new AbilityTriggerData(
TriggerTag: hitTag,
TriggerSource: AbilityTriggerSource.Event
),
instancingPolicy: AbilityInstancingPolicy.PerEntity,
behaviorFactory: () => new ShieldBehavior()); // Assumes ShieldBehavior exists
// Grant the ability
player.Abilities.GrantAbilityPermanently(autoShieldData, 1, LevelComparison.None, player);
// Raising the event will automatically trigger the ability
player.Events.Raise(new EventData
{
EventTags = hitTag.GetSingleTagContainer(),
Target = player
});In this example, a granted ability (like a passive aura) is activated automatically while the character has a specific tag (e.g., "status.enraged").
// Define an ability that triggers when the "status.enraged" tag is present
var rageAbilityData = new AbilityData(
"Rage Aura",
abilityTriggerData: new AbilityTriggerData(
TriggerTag: Tag.RequestTag(tagsManager, "status.enraged"),
TriggerSource: AbilityTriggerSource.TagPresent),
instancingPolicy: AbilityInstancingPolicy.PerEntity,
behaviorFactory: () => new RageBehavior());
// Grant the ability permanently so it monitors tags
player.Abilities.GrantAbilityPermanently(rageAbilityData, 1, LevelComparison.None, player);
// Apply an effect that grants the "status.enraged" tag
// The Rage Aura ability will automatically activate when this tag is added
var enrageEffect = new EffectData(
"Enrage",
new DurationData(
DurationType.HasDuration,
new ModifierMagnitude(
MagnitudeCalculationType.ScalableFloat,
new ScalableFloat(10f))),
effectComponents: [
new ModifierTagsEffectComponent(tagsManager.RequestTagContainer(["status.enraged"]))
]);
player.EffectsManager.ApplyEffect(new Effect(enrageEffect, new EffectOwnership(player, player)));Now that you've seen the basics of Forge, you can:
- Explore Attributes for more on attribute channels and dependencies.
- Check Modifiers for advanced attribute modifications.
- Apply Effect Durations to create infinite, timed, or instant effects.
- Implement Stacking for cumulative effects.
- Use Periodic Effects for recurring gameplay mechanics.
- Extend effects with Components for custom behaviors.
- Integrate Cues for visual and audio feedback.
- Orchestrate gameplay reactions with Events.
- Define discrete actions and skills using Abilities.
- For catching configuration errors during development, see Validation and Debugging.
For more detailed documentation, refer to the Forge Documentation Index.