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: 0Adding 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):
| Option | Meaning |
|---|---|
policy | What to do when the key already has an instance. Required in that case. |
source | An optional owner, such as the entity that cast it. Disposing the source removes the effect. |
duration | Optional length, counted on the scheduler. A duration without a scheduler is an error. |
scheduler | Where 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:
| Policy | When 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.