EffectLib

by Ter13
Handle generic temporary combat effects efficiently and cleanly
ID:2286256
 
This thread is all of the relevant documentation for 13.EffectLib.

Please refer all discussion regarding this thread to the general forums, bug reports, or feature requests for this library.

Listing:
effects var (mob)

See also:
  effect
  effect_registry var (mob)
  AddEffect() (mob)
  RemoveEffect() (mob)
  Add() (effect)
  Remove() (effect)

Default value:
  An empty list

The effects var is a list that stores all active effects on this mob. You should not add or remove elements from this list manually. Rearranging the list is not a problem, so long as the effects aren't removed or added manually.

Effects are added by calling mob.AddEffect(), or effect.Add(). Effects are removed by calling mob.RemoveEffect(), or effect.Remove(). Timed and Ticker effects will remove themselves automatically on demand. When effects override one another, they will also remove themselves on demand as well.

This variable exists not only to keep track of the internals of the effect library, but also as a tool for working with effects individually. For most use-cases, you will want to default to using effect_registry to find effects, as effect_registry uses an associative list to keep track of effects by their id/sub_id combination. However, effects can be added to the mob's effects variable without an id/sub_id combination, in which case they will not be stored in effect_registry at all.




effect_registry var (mob)

See also:
  effect
  AddEffect() (mob)
  RemoveEffect() (mob)
  Add() (effect)
  Remove() (effect)

Default value:
  An empty list

The effect_registry is a list that stores effects in an associative format by their id/subid. effect.Add() takes care of building and maintaining this list. As such, you should never add, assign or remove information from this list manually.

Structure:

{
    //id storage is either a list, or a single effect datum, associated to effect.id
    "id1" = [effect 1],
    "id2" = list([effect 2],[effect 3]),
    //unique id is "[effect.id].[effect.sub_id]"
    "id1.subid" = [effect1],
    "id2.subid" = [effect2],
    "id3.subid" = [effect3],
    //if id is non-null, but sub_id is null, the id storage is still used, but there is no unique id for them.
    "id4" = list([effect 4],[effect 5])
}


The effect datum will attempt to make up to two entries per effect when it is added. The first entry stores the effect in a list with all other effects already in the registry matching the id variable of the effect. If the effects list has no matching ids, the object itself is used associated to the id instead of the list of effects.

The second entry generates a unique id from "[effect.id].[effect.sub_id]". This only happens if sub_id is not null. Effects with the same unique id will attempt to override the older one when added, so the unique id storage in the registry will only ever reference a single effect datum.

Examples:

Getting a specific debuff by unique id.
var/damage = 10
var/effect/e = target.effect_registry["FireDebuff.global"] //pull from unique id
if(e)
damage *= 2


Getting any debuff by id
var/damage = 10
var/list/l = target.effect_registry["FireDebuff"] //pull from id only
if(e)
damage *= 2


Looping over a series of debuffs by id
var/list/l = target.effect_registry["FireDebuff"]
if(l) //if l is a list
if(!istype(l)) //if l is not a list, it's a single effect. Just plug it into a list anyway.
l = list(l)
for(var/effect/e in l)
e.Cancel(target) //cancel the effect





save_worldtime var (mob)

See also:
  save_realtime var (mob)
  Write() (mob)
  Read() (mob)

Default value:
  null

save_worldtime is actually part of StdLib. This is used during loading of a mob from a savefile to ensure that time offsets are regenerated relative to world.time when the savefile is loaded next.




save_realtime var (mob)

See also:
  save_worldtime var (mob)
  preserve var (effect)
  Write() (mob)
  Read() (mob)

Default value:
  null

save_realtime is actually part of StdLib. This is used during loading to ensure that time offsets are regenerated relative to the actual time passed while the mob was not loaded.




AddEffect proc (mob)

See also:
  effect
  RemoveEffect() (mob)
  Add() (effect)
  Remove() (effect)

Format:
  AddEffect(effect datum)
  AddEffect(effect datum, world timestamp)

Returns:
  0 if unsuccessful
  1 is successful


This is a convenience hook for effect.Add(). This proc simply calls Add() on the effect passed as the argument. This proc exists so that you don't have to define a variable to store the effect when you initialize it:

var/effect/e = new/effect/herpyderp()
e.Add(target)


target.AddEffect(new/effect/herpyderp())


The time argument is added so that effects can be backdated. This is useful when loading effects from a savefile, but the time argument is optional. It defaults to the current world.time




RemoveEffect proc (mob)

See also:
  effect
  AddEffect() (mob)
  Remove() (effect)
  Add() (effect)

Format:
  RemoveEffect(effect datum)
  RemoveEffect(effect datum, world timestamp)

Returns:
  0 if unsuccessful
  1 is successful


This is a convenience hook for effect.Remove(). This proc simply calls Remove() on the effect passed as the argument. It exists so that you don't have to store references to effects in the local proc:

var/effect/e = target.effect_registry["id.sub_id"]
if(e)
e.Remove(target)


target.RemoveEffect(effect_registry["id.sub_id"])


The time argument is only included for the sake of reciprocity with effect.Add(). It is optional and will default to the current world.time.




Read proc (mob)

See also:
  save_realtime var (mob)
  save_worldtime var (mob)
  preserve var (effect)
  Write() (mob)

DO NOT CALL THIS PROC MANUALLY, YOU DONKEY! It is part of the default savefile handling system of BYOND. You should never, ever call it manually.

This override handles restoring effects on mobs. It copies the list of effects on the loaded mob, then loops through them, calling Add() on each effect with a backdated time argument. Depending on the effect's preserve variable, this will either account for the time spent offline, or not. effect_registry is temporary by default, so this rebuilds the whole registry and gets timer and ticker effects back on track where you left them.
effect datum

See also:
  effects var (mob)
  effect_registry var (mob)

effect datums are pattern placeholders for common patterns in action/RPG combat systems. This datum exists to allow you to define new temporary, reversible effect trackers on mobs. They provide a series of variables that you can configure to easily encompass a wide range of behaviors all highly relevant to just about any combat system in a videogame.

The standard patterns this datum allows:

Toggle buffs/debuffs
Timed buffs/debuffs
Ticked buffs/debuffs (DoT/HoT effects)
Stacking buffs/debuffs (accumulating effects)

Unique effects
Unique per-player effects
non-unique effects

Effect overriding
Differentiation between expiration, cancellation, and removal.

Defining new effects is very simple, and only requires you to override the correct hooks and variables in your effect subtypes.




id var (effect)

See also:
  sub_id var (effect)
  effect_registry var (mob)
  Add proc (effect)

Default value:
  null

Effect ids are text strings that are used to register effects in the mob's effect_registry for later retrieval. You can query mob.effect_registry to get a list of effects by their id string. If id is null, the effect is not stored in the effect_registry at all. It is, however, stored in mob.effects.




sub_id var (effect)

See also:
  id var (effect)
  effect_registry var (mob)
  Add proc (effect)
  Override proc (effect)
  Overridden proc (effect)

Default value:
  "global"

Effect sub ids are text strings that are used to help make effects more unique. When an effect specifies both an id and a sub_id, the effect will be considered "unique". This means that you can pull it out of the mob's effect_registry reliably by using effect_registry["id.sub_id"]. If the mob has the effect, it will return that effect instance. If the mob doesn't, though, it will be null. Unlike querying by id alone (which will return an effect, a list, or null), you can always be assured that this will always be an effect datum or null. Unique effects also have the added bonus of only existing one at a time. effect.Override() is called when two effects sharing the same unique_id attempt to enter the mob's effect list. If the override is allowed, the Overridden hook will be called on the object that was replaced.

If an effect's id is null, the value of sub_id doesn't matter at all. One cool consequence of this, is that if you have an attack that a mob can perform that leaves a temporary debuff on the opponent, you can actually limit the system to a single one of those debuffs per player using "\ref[attacker]" (npcs or players) or attacker.ckey (players) as a sub_id. This will create a reliable unique_id for the effect for later querying while still allowing the ids to be identical.




active var (effect)

See also:
  id var (effect)
  effect_registry var (mob)
  Add proc (effect)
  Override proc (effect)
  Overridden proc (effect)

Default value:
  0

The active var is a way to determine whether an effect is still active on the target without having to check the effect_registry of that target. It's a boolean value that is set to 1 when added to a target, and set to 0 when removed from a target. This is set to 1 as part of the default action of the Add() proc, and 0 as a part of the Remove() and Expire() procs.

The active variable is used to help in determining the outcome of several of the built in hooks.

If active is set to 0 during Added(), the effect won't actually stay added. It will instantly remove itself, and Add() will return 0.

If active is set to 0 during Canceled(), the effect will be removed. active is left set to 1 when Cancel() is called, and the default action of Canceled() is to set active to 0.

active is auto-set to 0 during the Expire() procs. Remove() will only be called if it stays 0 after Expired() is called.

This allows a good amount of flexibility in how effect datums will expire, as well as offers you a good number of options to test what state an effect is in to get more information about its lifecycle.




start_time var (effect)

See also:
  duration var (effect)
  Add proc (effect)
  Timer proc (effect)
  Ticker proc (effect)

Default value:
  null

The start_time var keeps track of when the effect was last restarted/readded. This is used to track time in timed/ticker effects. This is measured in world ticks. It is set before Added() is called within Add(). This is not set to the current world.time, but rather the time passed as an optional argument to effect.Add(). Always remember that effects can be backdated, and therefore are liars.




start_time var (effect)

See also:
  start_time var (effect)
  Add proc (effect)
  Timer proc (effect)
  Ticker proc (effect)

Default value:
  1#INF (infinity)

An effect's duration determines whether it is a timed/ticker effect, or a toggle effect. Toggle effects always have a duration of infinite. If the duration is less than infinite, Add() will attempt to call Timer() or Ticker() immediately after Added() (provided active is still 1).




tick_delay var (effect)

See also:
  duration var (effect)
  ticks var (effect)
  Add proc (effect)
  Ticker proc (effect)
  Ticked proc (effect)

Default value:
  0

The tick delay variable stores the separation between effect ticks. A timed or toggle effect can be a ticker simply by setting this variable to any non-zero (positive) value in world ticks. effect.Ticker() is called as a part of the Add() proc.

Every (tick_delay) deciseconds while the effect is active, effect.Ticked() will be called. Ticked() can be overridden to change what the ticker effect does every time it ticks. The Ticker() loop will also maintain the number of ticks that have passed in the effect.ticks variable.




stacks var (effect)

See also:
  max_stacks var (effect)
  Override proc (effect)
  Overridden proc (effect)

This variable stores the number of stacks currently tracked on the effect. Stacking effects allow you to have variable strength or accumulating effects. Effects will only stack with effects that have a matching uniqueid. If id is not set, you cannot stack multiple of those effects together at all.

When a stacking effect is Overridden by an effect with a matching unique id, the old effect is removed, and the new one is added with the stacks of the two added together within the Override() proc.




max_stacks var (effect)

See also:
  stacks var (effect)
  Override proc (effect)
  Overridden proc (effect)

This variable determines the maximum number of stacks that are allowed on a stacking effect. See the stacks var for details on the rules for stacking effects.




preserve var (effect)

See also:
  Read() proc (mob)

Default value:
  PRESERVE_WORLDTIME (1)

Possible values:
  PRESERVE_NONE (0)
  PRESERVE_WORLDTIME (1)
  PRESERVE_REALTIME (2)

This variable uses bitflag definitions found in StdLib. The time calculations are also dependent on the mob/Write() override in StdLib, and the save_realtime/save_worldtime variables defined in StdLib too.


PRESERVE_NONE will save the effect, but when the effect is loaded from a savefile, it will be orphaned, causing it to be garbage collected rather than adopted.

PRESERVE_WORLDTIME simply backdates the effect as though it were added back in time during the current session.

PRESERVE_REALTIME backdates the effect back to when it was actually added to the mob during the last session the mob played. Any effects backdated beyond their expiration will simply refuse to be added, and therefore will join the orphans from above in the dumpster.


The effect saving system packaged with effect_lib is capable of accounting for the time between when a player was saved and when they were loaded. Effects that are marked to PRESERVE_REALTIME use that time difference calculated via the mob's save_realtime variable to force time to pass on effects. Backdated ticks on ticker effects will not tick all at once, instead any ticker datums designed to be reloaded in context will need to be designed to calculate their backdated effects over time on Added().

When an effect is loaded from a savefile, Read() passes a backdated timestamp to Add(). Effects will refuse to be added whatsoever should the backdated time make the current time past the expiration time of a timed or ticker effect. Anything that an effect modifies on a mob should be marked as temporary. The effects should NOT modify non-tmp variables on the mob, especially if you are using a saving system. Anything that an effect datum touches should be considered garbage meant to be thrown away the minute the mob is destroyed.
Add proc (effect)

See also:
  effects var (mob)
  effect_registry var (mob)
  duration var (effect)
  id var (effect)
  sub_id var (effect)
  active var (effect)
  start_time var (effect)
  tick_delay var (effect)
  Remove() (effect)
  Override() (effect)
  Timer() (effect)
  Ticker() (effect)
  Added() (effect)
  AddEffect() (mob)
  RemoveEffect() (mob)

Format:
  Add(target mob)
  Add(target mob,world timestamp)

Returns:
  1 if successful
  0 if unsuccessful

Default action:

This is called to add an effect to a mob. The time argument is optional, but can be a value in the past, but defaults to the current world.time. If the current time minus the time argument is greater than the effect's duration, we return unsuccessful.

Second, we attempt to generate a unique id if both id and subid are set. Since two effects can't share the same unique id, we check the target's effect registry for an existing unique id match. If found, we call src.Override() with the found effect on the target.

Third, we check if the id of the effect has been set. If so, we check the registry for existing effects with that id. If it's a single effect, we reset the id storage to a list containing the old effect and the current effect. If it's a list of effects, we just add this one to that list. If it is null, we set that id storage value in the registry to this effect. If it's any other kind of value, we return unsuccessful.

Fourth, we actually add the effect to the target's effects list and the uniqueid registry if relevant. Then we set active to 1, the start time to the time passed into the Add() proc, and we call the Added() hook on this object.

If active is still true after Added() finishes, we check to see if this is a timed or a ticker effect, and call the relevant Ticker()/Timer() procs on this object. If active was not true after Added(), we just go ahead and remove this effect from the registry and return unsuccessful.

Last, we return success provided Add() hasn't already ejected early.

Notes:

There is little reason to ever override Add() for child types of an effect. You may need to in the future, but the odds are good that you can achieve whatever you want with this system without modifications. If you find yourself modifying Add() in a child type, it would probably be best to think of a way to generalize the behavior you are adding and apply it to all effects by fully overriding Add() on the root effect type.




Override proc (effect)

See also:
  max_stacks var (effect)
  Add proc (effect)
  Stack proc (effect)
  Cancel proc (effect)
  Overridden proc (effect)

Format:
  Override(target mob,effect datum,world timestamp)

Returns:
  1 if successful
  0 if unsuccessful

Default action:

This proc is called as a part of Add(). You should not call this proc yourself.

If the effect is a stacking effect (max_stacks is >1), and the two effects have the same max_stack value, call Stack() on this effect with a reference to the stack this one is trying to override. Call Overridden() on the old effect with a reference to this effect, then Cancel() the old effect.

Finally, return 1 by default. By default overriding is always successful.

Notes:

You may wish to override this function to change how overriding works for your own game. You might compare buffs/debuffs and prefer the strong one, or something else. You might even want an effect to outright refuse to yield to any newer one altogether.




Stack proc (effect)

See also:
  stacks var (effect)
  max_stacks var (effect)
  Override() (effect)

Default action:

This proc is called as a part of Override(). You should not call this proc yourself.

This simply adds the stack variable of another effect to this one by default. You can override this function to perform a different default action.




Timer proc (effect)

See also:
  active var (effect)
  start_time var (effect)
  duration var (effect)
  Ticker proc (effect)
  Expire proc (effect)
  Add proc (effect)
  Reset proc (effect)

Default action:

This proc is called as a part of Add().

This proc runs a timer, which will periodically (every second, or the time until the proc ends, whichever is smaller) check if the effect is active, and the time is <= start_time + duration. This is only called for timed effects that do not tick.

When the loop stops, Expire() will be called on this effect.

There is no reason to override this proc.




Ticker proc (effect)

See also:
  active var (effect)
  start_time var (effect)
  duration var (effect)
  Timer proc (effect)
  Expire proc (effect)
  Add proc (effect)
  Reset proc (effect)

Default action:

This proc is called as a part of Add().

This proc runs a timer, which will periodically (tick_delay, or the time until the proc ends, whichever is smaller) check if the effect is active, and the time is <= start_time + duration. The loop that checks whether the timer is active will call effect.Ticked() and add one to the effect's ticks variable whenever it iterates.

When the loop stops, Expire() will be called on this effect.

There is no reason to override this proc.




Reset proc (effect)

See also:
  start_time var (effect)
  Timer proc (effect)
  Ticker proc (effect)

Format:
  Reset(target mob)
  Reset(target mob,world timestamp)

Default action:

When you want to restart a timer or ticker, you will want to call this proc on the desired effect datum. The optional timestamp allows you to specify the new start_time. This will not restart a timer that is already expired, as it is assumed that an expired timer will never be allowed to sit in the target's effects list.




Cancel proc (effect)

See also:
  active var (effect)
  Canceled proc (effect)
  Remove proc (effect)

Format:
  Cancel(target mob)
  Cancel(target mob,world timestamp)

Default action:

When you want to end an effect early, you can call Cancel() on it. It will call the Canceled() hook if the effect is currently active. Provided the effect's active var is still 0 after it has been canceled, it will call Remove() on the target. Time is provided for potential expansion, and is not actually used by this proc.




Remove proc (effect)

See also:
  effects var (mob)
  effect_registry var (mob)
  id var (effect)
  sub_id var (effect)
  active var (effect)
  Removed proc (effect)
  Expire proc (effect)
  Cancel proc (effect)

Format:
  Remove(target mob)
  Remove(target mob,world timestamp)

Default action:

This proc is called by default by Expire() and Cancel().

The Remove() proc does basically the opposite of the Add() proc. It attempts to remove any id or uniqueid references to the effect in the target's registry, then removes the effect from the effects list, calls the Removed() hook, then forcibly sets active to 0. By the time that Remove() is called, you cannot stop the effect from being removed. Therefore, it is unsafe to re-add the effect within Removed().




Expire proc (effect)

See also:
  active var (effect)
  Expired proc (effect)
  Remove proc (effect)
  Timer proc (effect)
  Ticker proc (effect)

Format:
  Expire(target mob)
  Expire(target mob,world timestamp)

Default action:

This proc is called by default by Timer() and Ticker().

The Expire() proc cleans up the effect after the Timer()/Ticker() procs have noted that the effect has reached the end of its lifetime. The proc checks if the effect is still marked active, marks it non-active, then calls to Expired(). If the effect is still marked non-active, effect.Removed() is called.




Added proc (effect)

See also:
  Add proc (effect)

Arguments:
  target (the mob this effect is active on)
  time (the time that this effect was added)

Default action:

This proc is called as a part of Add(). This hook is meant to be overridden. It has no default action. This is merely a place for you to define what your child effect types do when first taking effect on the target, or when reloaded after a savefile was loaded with active effects on the restored mob.




Ticked proc (effect)

See also:
  Ticker proc (effect)

Arguments:
  target (the mob this effect is active on)
  tick (The current tick number being processed)
  time (the time that this effect ticked)

Default action:

This proc is called by Ticker() every tick_delay ticks. It has no default action. This is merely a place for you to define what your child effect types do when ticking.




Removed proc (effect)

See also:
  Remove proc (effect)

Arguments:
  target (the mob this effect is active on)
  time (the time that this effect was removed)

Default action:

This proc is called by Remove() after the effect has been successfully removed from the target's effects list and registry. It has no default action. This is merely a place for you to undo anything that was done when the effect was added.




Overridden proc (effect)

See also:
  Override proc (effect)

Arguments:
  target (the mob this effect is active on)
  override (the effect that overrode this one)
  time (the time that the overriding effect was added)

Default action:

This proc is called by Override() as a part of Add() before the current effect has been Canceled. It has no default action. This is merely a place for you to define new behavior for child subtypes of the effect datum.




Expired proc (effect)

See also:
  Timer proc (effect)
  Ticker proc (effect)
  Expire proc (effect)

Arguments:
  target (the mob this effect is active on)
  time (the time that the overriding effect expired)

Default action:

This proc is called by Expire() as a part of Timer()/Ticker() before the current effect has been Removed. It has no default action. This is merely a place for you to define new behavior for child subtypes of the effect datum.




Canceled proc (effect)

See also:
  Cancel proc (effect)

Arguments:
  target (the mob this effect is active on)
  time (the time that the overriding effect was canceled)

Default action:

This proc is called by Cancel(). It has no default action. This is merely a place for you to define new behavior for child subtypes of the effect datum.