EffectLib

by Ter13
Handle generic temporary combat effects efficiently and cleanly
ID:2288783
 
EffectLib has been seeing extensive modification in the last several days thanks to my own private experiments behind the scenes.

Mind you, I'm attempting to keep as much of EffectLib's API consistent as possible between releases. This doesn't mean I'll be fully rewriting this library again and again like I've done over the last few years, but I am not attempting to make the whole operation backwards compatible.

EffectLib 2.0-2.2 included quite a few changes that actually fully change the build process for the library. Among them, I migrated some of EffectLib's behavior over into StdLib, and made EffectLib dependent on StdLib 2.2.

2.3 so far is a pure feature upgrade. At the moment, my private build of the library includes some new functionality that will make using the structures much easier without actually forcing the user to modify them.

active -> state

I've removed effect.active in favor of effect.state. Effect.active was a simple yes/no boolean value that would only tell you whether the effect was still in the target's effects list. I actually ran into a situation where I needed to know whether an effect was canceled or whether it expired naturally. This prompted a sweeping overhaul of how effects report their current state. Effects now provide information about how they were removed, and not just whether they are still running.

time

I also ran into a situation where I needed to work out when an effect had actually ended, so effects now store their end_time in a new variable upon removal. This makes working with effects as sort of dead-man switches possible, as well as using them for things like action progress and spell chargeup possible to boot.

pretty

I just finished working in a new preprocessor declaration that effectlib will look for in order to determine what /effect inherits from. If this flag is declared, the /effect type will potentially wind up being an /obj child type rather than a /datum child type. This allows developers to actually give effects visual appearances so that they can be displayed in statpanels, grids, or on screen if the developer so wishes.

ticking

I'm still mulling over a better way to handle ticker effects. Currently the first tick happens the instant that an object has been added to the target, and ticks have a minimum separation of a single tick. I'm going to be doing away with both of these limitations by doing some better math to work out when the next tick actually should happen. This will also have the added benefit of allowing ticker effects to save and restore in between ticks, and still have the number of ticks passed over the object's two lifetimes stay perfectly aligned to the time and separation specified by the developer.


This is an open call for feature requests or questions about EffectLib. I want to have more to push before 2.3 gets dropped, and I want to make it as user-friendly as possible, so I'm definitely open to any suggestions about my direction with this library.
As promised, I'll go ahead and unveil a few of the things I've been doing myself with EffectLib.


Often times in games, users have one primary state. We often track that state with a string of some sort. While the player's action is in a particular state, we assume that other actions can't be done, or we keep track of it to work out how long they've been doing a thing.

EffectLib actually makes working with these action states a lot more flexible, and allows us to actually use a player global effect to ensure that the user is never in more than one state at a time. It also allows us to handle what happens when the player starts doing a thing, stops doing a thing, or when we want to know whether we can do something else.

First, we need to declare a new type of /effect and call it /action:

effect/action
id = "action"
sub_id = "global"


By declaring the unique identifier to always be "action.global", we've ensured that the user will never have more than one of these active at a time.

Let's make our lives easier by keeping a spare reference around to the user's current action state:

mob
var/tmp
effect/action/action

effect/action
Add(mob/target,time=world.time)
. = ..()
if(.)
target.action = src

Remove(mob/target,time=world.time)
. = ..()
if(.)
target.action = null


This allows us to quickly get information about the user's current action without having to dig through the effect_registry every single time.

Next, we're going to add a little bit of configuration to effect objects that will determine whether the user can move during the action, whether moving (or being moved) during the action cancels the action, or whether another action can override this one and cause it to end early.

#define ACTION_ALLOW_MOVE 1
#define ACTION_MOVE_INTERRUPT 2
#define ACTION_ALLOW_OVERRIDE 4

#define ACTION_DEFAULT_FLAGS (ACTION_ALLOW_MOVE|ACTION_MOVE_INTERRUPT|ACTION_ALLOW_OVERRIDE)

effect
action
var
flags = ACTION_DEFAULT_FLAGS

mob
canMove() //this is a custom hook I use to determine the rules for self-controlled movement
if(action && !(action.flags&ACTION_ALLOW_MOVE))
return 0
return ..()

Moved() //this is a custom hook I used that's called any time the movable is relocated
..()
if(action&&action.flags&ACTION_MOVE_INTERRUPT)
action.Cancel(src)
client
Move(atom/NewLoc,Dir=0)
if(!mob.canMove(NewLoc,Dir))
return 0
return ..()


Now you can set the user to different actions. effect.Added() is when an action starts. effect.Removed() is when an action ends. So you can change aspects of the mob's stats, their icon state, etc. during an action, and restore the correct states after it ends. Actions can be temporary effects too!
I have also revamped how I was handling skill combos in my private ability library.

Before, I was doing a lot of messy stuff involving cooldowns and state trackers, but I decided that temporary effects were by far the best way to handle this.

In my system, combos are abilities that modify how other abilities work. For instance:

slash - does a small amount of damage, adds a "slash" combo. If "flurry" combo is active, attack is an instant critical.

slice - does a small amount of damage, if "slash" combo is active: applies a bleed effect and adds a "slice" combo.

flurry - hits 3 times. if "slice" combo is active: hits an additional 3 times, applies a "flurry" combo.

The above would be a mainline rotation for a character class that encourages players to repeat the same three attacks in sequence over and over by applying bonus damage effects if used in the correct order. However, these abilities may have their uses in other orders.

Let's take a look at how effectlib can help us implement this kind of logic:

ability
var
combo_id //stores the id of the combo this ability will grab from the user's effects on use.
combo_type = COMBO_NONE
combo_effect //stores the type of combo effect this ability adds when used successfully


combo_id will store a string that identifies a particular kind of combo.

combot_type determines how this skill interacts with the combo system. It stores one of the following values:

#define COMBO_IGNORE   0
#define COMBO_NONE 1
#define COMBO_REQUIRE 2
#define COMBO_OPTIONAL 3


COMBO_IGNORE means that this ability does not remove existing combos, and does not attempt to apply one either.

COMBO_NONE means that this ability will clear any existing combo, but does not apply one.

COMBO_REQUIRE means that this ability can only be used if the correct combo is found.

COMBO_OPTIONAL means that this ability can still be used if the correct combo is not found. The combo effects are optional.

effect
var
combo_id //used for checking combo effects in the ability code.

ability
canUse(mob/user,atom/target)
. = ..()
if(combo_type==COMBO_REQUIRE&&!isCombo(user))
return 0

Use(mob/user,atom/target,keybind/hotkey)
var/effect/combo
if(combo_type)
combo = user.effect_registry["combo.global"] //get the current combo active on the user
if(combo) //if it exists, cancel it. Only store it as a valid combo if the combo's id matches.
combo.Cancel(user)
if(combo.combo_id!=combo_id)
combo = null
//do the rest of the built in ability logic
Used(user,target,combo)
if(combo_effect)
user.AddEffect(new combo_effect())
return 1

Used(mob/user,atom/target,effect/combo) //you can check the combo when creating the code for new attacks to change the effect of the ability dynamically.


The fun part about all of this is we can quickly create combo chains for attacks by only modifying a few properties of the global ability singletons!

Neat, right?