Skip to content

Effects ​

An effect is a temporary, keyed value on an entity: poisoned, hasted, shielded, on fire. gamekit tracks its lifetime, its duration and what happens when a second one with the same key arrives. What the effect does is up to you; gamekit is not a rule engine.

ts
import { EffectHandle, EffectKey, EffectOptions, EffectStacking, Entity } from "gamekit";
import { ClockScheduler } from "gamekit/src/testing";

class Poison {
    damage: int;
    constructor(damage: int) { this.damage = damage; }
    onActivate(owner: Entity, effect: any): void { console.log("poisoned for " + this.damage); }
    onExpire(owner: Entity, effect: any): void { console.log("poison wore off"); }
    onRemove(owner: Entity, effect: any, reason: string): void { console.log("removed: " + reason); }
}

const POISON: EffectKey<Poison> = new EffectKey<Poison>("poison");

// In a game the scheduler is a Ticker (App.ticker or a node's ticker).
// A ClockScheduler from testing advances only when you tell it to.
const clock: ClockScheduler = new ClockScheduler();
const hero: Entity = new Entity();
hero.activate();

const strongest: Function = EffectStacking.strongest((a: Poison, b: Poison): int => a.damage - b.damage);
hero.addEffect(POISON, new Poison(3), new EffectOptions<Poison>(strongest, null, 2.0, clock));
const weaker: EffectHandle<Poison> | null = hero.addEffect(POISON, new Poison(1), new EffectOptions<Poison>(strongest, null, 2.0, clock));
console.log("weaker accepted: " + (weaker != null));

clock.advance(2.5);
console.log("active poison: " + hero.getEffects(POISON).length);
text
poisoned for 3
removed: rejected
weaker accepted: false
poison wore off
removed: expired
active poison: 0

Adding effects ​

entity.addEffect(key, value, options) returns an EffectHandle, or null when the stacking policy rejects it. entity.getEffects(key) returns every current instance for a key.

EffectOptions(policy, source, duration, scheduler):

OptionMeaning
policyWhat to do when the key already has an instance. Required in that case.
sourceAn optional owner, such as the entity that cast it. Disposing the source removes the effect.
durationOptional length, counted on the scheduler. A duration without a scheduler is an error.
schedulerWhere time comes from. In a game, a core Ticker (App.ticker or a scene node's ticker), so App.timeScale stretches it and a paused node stops it. In tests, a ClockScheduler or a PresentationClock's scheduler.

Stacking policies ​

EffectStacking has four ready policies:

PolicyWhen a new instance arrives
accumulate()Keep both.
replace()Remove the old ones, keep the new one.
strongest(compare)Keep whichever compare says is stronger.
refreshDuration()Keep the existing one and restart its duration.

A custom policy is a function that receives an EffectStackingContext and returns context.accept(), reject(), replace() or refresh(effect).

Hooks ​

An effect value may define onAttach, onActivate, onSuspend, onExpire and onRemove. Each receives the owner entity and the handle; onRemove also gets the reason (EffectRemovalReason). An effect can subscribe to owner.events in onActivate and release the subscription in onRemove.

Effects share one attachment order with components: activation goes forward, suspension goes in reverse. Every way an effect can end (removed, expired, its source disposed, its owner disposed) calls onRemove exactly once.

The effect pipeline ​

Before the policy runs, entity.effects.pipeline (a MutablePipeline) sees each EffectApplication. A stage can change it, for example halving a duration for a resistant entity, or cancel it.

Reference: EffectKey, EffectOptions, EffectStacking, EffectHandle, EffectCollection.