-
Notifications
You must be signed in to change notification settings - Fork 30
Effects System
The Effects system exists to provide a class structure suitable for representing any "effect" in-game. These could include dealing damage, healing a target, area-based effects, over-time effects, or even permanent modifiers. The system provides the capability for effects to have duration in arbitrary units, from instantaneous (immediate), to infinite (activates whenever a certain event happens, forever).
At its core, the Effect
class is an abstract class that should be subclassed in order to define what a given effect does. It defines an abstract onTrigger
method that, when called, should take all needed actions to cause a particular effect. The (non abstract) public Trigger()
function should be called to trigger the effect, as it calls the onTrigger
function, as well as decrements the remaining duration on an effect (if it is not instantaneous or infinite).
Different effects may need vastly different types and numbers of parameters passed to the Trigger()
function in order to work properly. For this reason, Effect
is a template class. The template parameter represents the type of the (single) argument that will be passed to the Trigger
function. In order to enable some advanced functionality with EffectTriggers, all template types for Effect
must inherit from the class EffectArgs
. Taking this template parameter makes it possible to pass multiple parameters to the Trigger
function -- simply create a class/struct that wraps all the values you need to pass into one class, and use that as the template parameter when subclassing. This will be demonstrated in a later code example.
Each effect takes a string parameter representing its name (for display purposes), and an integer variable representing its duration. Duration (including infinite and instant duration effects), are covered in more depth [TODO: BELOW].
For the sake of a concise code example, we will create a small code example which takes a Monster class with an HP field, and creates an effect to apply basic damage.
using GoRogue;
using GoRogue.DiceNotation;
class Monster
{
private int _hp;
public int HP
{
get => _hp;
set
{
_hp = value;
System.Console.WriteLine("Monster took damage, now has " + _hp + " HP.");
}
}
public Monster(int startingHP)
{
HP = startingHP;
}
}
// Our Damage effect will need two parameters to function -- who is taking
// the damage, eg. the target, and a damage bonus to apply to the roll.
// Thus, we wrap these in one class so an instance may be passed to Trigger.
class DamageArgs : EffectArgs
{
public Monster Target { get; private set; }
public int DamageBonus {get; private set; }
public DamageArgs(Monster target, int damageBonus)
{
Target = target;
DamageBonus = damageBonus
}
}
// We inherit from Effect<T>, where T is the type of the
// argument we want to pass to the Trigger function.
class Damage : Effect<DamageArgs>
{
// Since our damage effect can be instantaneous or
// span a duration (details on durations later),
// we take a duration and pass it along to the base
// class constructor.
public Damage(int duration)
: base("Damage", duration)
{ }
// Our damage is 1d6, plus the damage bonus.
protected override void onTrigger(DamageArgs args)
{
// Rolls 1d6 -- see Dice Rolling documentation for details
int damageRoll = Dice.Roll("1d6");
int totalDamage = damageRoll + args.DamageBonus;
args.Target.HP -= totalDamage;
}
}
class Program
{
static void Main(string[] args)
{
Monster myMonster = new Monster(10);
// Effect that triggers instantaneously -- details later
Damage myDamage = new Damage(Damage.INSTANT);
// Instant effect, so it happens whenever we call Trigger
myDamage.Trigger(new DamageArgs(myMonster, 2));
}
}
The code example above may appear to be excessively large for such a simple task. The advantage of using Effect
for this type of functionality lies in Effect's capability for durations. Effect
takes as a constructor parameter an integer duration. This duration can either be an integer constant in range [1, int.MaxValue]
, or one of two special (static) constants. These constants are either Effect<T>.INSTANT
, which represents effects that simply take place whenever their Trigger()
function is called and do not partake in the duration system, or Effect<T>.INFINITE
, which represents and effect that has an infinite duration.
The duration value is in no particular unit of measurement, other than "number of times Trigger()
is called". In fact, the duration value means very little by itself -- rather, any non-instant effect is explicitly meant to be used with an EffectTrigger
. EffectTrigger
is, in essence, a highly augmented list of Effect
instances that all take the same parameter to their Trigger()
function. It has a method that calls the Trigger()
functions of all Effects in its list (which modifies the duration value for the Effect
as appropriate), then removes any effect from the list whose durations have reached 0. It also allows any effect in the list to "cancel" the trigger, preventing the Trigger()
functions in subsequent effects from being called. In this way, EffectTrigger
provides a convenient way to manage duration-based effects.
When we create an EffectTrigger
, we must specify a template parameter. This is the same template parameter that we specified when dealing with effects -- it is the type of the argument passed to the Trigger()
function of effects it holds, and must subclass EffectArgs
. Only Effect
instances taking this specified type to their Trigger()
function may be added to that EffectTrigger
. For example, if I have an instance of type EffectTrigger<DamageArgs>
, only Effect<DamageArgs>
instances may be added to it -- eg. only Effect
instances that take an argument of type DamageArgs
to their Trigger()
function.
Effect
instances can be added to an EffectTrigger
by calling the Add()
function, and passing the Effect
to add. Such an effect will automatically have its Trigger()
method called next time the effect trigger's TriggerEffects function is called. If an effect with duration 0, (instant or expired duration) is added, an exception is thrown.
Once effects have been added, all the effects may be triggered with a single call to the TriggerEffects()
function. When this function is called, all effects that have been added to the EffectTrigger
have their Trigger()
function called. If any of the effects set the CancelTrigger
field of their argument to true, the trigger is "cancelled", and no subsequent effects will have their Trigger()
function called.
In this example, we will utilize the Damage
effect written in the previous code example to create and demonstrate instantaneous, damage-over-time, and infinite damage-over-time effects.
using GoRogue;
using GoRogue.DiceNotation;
class Monster
{
private int _hp;
public int HP
{
get => _hp;
set
{
_hp = value;
System.Console.WriteLine("An effect triggered; monster now has " + _hp + " HP.");
}
}
public Monster(int startingHP)
{
HP = startingHP;
}
}
// Our Damage effect will need two parameters to function -- who is taking
// the damage, eg. the target, and a damage bonus to apply to the roll.
// Thus, we wrap these in one class so an instance may be passed to Trigger.
class DamageArgs : EffectArgs
{
public Monster Target { get; private set; }
public int DamageBonus {get; private set; }
public DamageArgs(Monster target, int damageBonus)
{
Target = target;
DamageBonus = damageBonus
}
}
// We inherit from Effect<T>, where T is the type of the
// argument we want to pass to the Trigger function.
class Damage : Effect<DamageArgs>
{
// Since our damage effect can be instantaneous or
// span a duration (details on durations later),
// we take a duration and pass it along to the base
// class constructor.
public Damage(int duration)
: base("Damage", duration)
{ }
// Our damage is 1d6, plus the damage bonus.
protected override void onTrigger(DamageArgs args)
{
// Rolls 1d6 -- see Dice Rolling documentation for details
int damageRoll = Dice.Roll("1d6");
int totalDamage = damageRoll + args.DamageBonus;
args.Target.HP -= totalDamage;
}
}
class Program
{
static void Main(string[] args)
{
Monster myMonster = new Monster(40);
// Effect that triggers instantaneously, so it happens only when we call Trigger
// and cannot be added to any EffectTrigger
Damage myDamage = new Damage(Damage.INSTANT);
Console.WriteLine("Triggering instantaneous effect...");
myDamage.Trigger(new DamageArgs(myMonster, 2));
EffectTrigger<DamageArgs> trigger = new EffectTrigger<DamageArgs>();
// We add one 3-round damage over time effect, one infinite damage effect.
trigger.Add(new Damage(3));
trigger.Add(new Damage(Damage.INFINITE));
// TODO: Print effects, or potentially demo cancellation
System.Console.WriteLine("Enter a character to advance to the first turn: ");
System.Console.Read();
for (int round = 1; round <= 4; round++)
{
System.Console.WriteLine("Triggering round " + round + "....");
// TODO: To be continued; print effects, wait for input on turn,
// and call trigger.
}
}
}
- Home
- Getting Started
- GoRogue 1.0 to 2.0 Upgrade Guide
- Grid System
- Dice Rolling
- Effects System
- Field of View
- Map Generation (docs coming soon)
- Map View System
- Pathfinding (docs coming soon)
- Random Number Generation (docs coming soon)
- Sense Mapping (docs coming soon)
- Spatial Maps (docs coming soon)
- Utility/Miscellaneous (docs coming soon)