gibbon.js
Version:
Actor/Component system for use with pixi.js.
438 lines (355 loc) • 10.7 kB
text/typescript
import type { ComponentKey } from '../core/actor';
import { Component } from '../core/component';
import { Actor } from '../core/actor';
import { addUnique } from '../utils/array-utils';
type EffectTarget = ComponentKey | Actor;
export type StateEffectDef = {
add?: Array<ComponentKey>;
remove?: Array<ComponentKey>;
disable?: Array<EffectTarget>;
enable?: Array<EffectTarget>;
}
export class StateEffect {
/**
* Components or component types to add when using this transition.
*/
protected add?: Array<ComponentKey>;
/**
* Components or component types to remove when using this transition.
*/
protected remove?: Array<ComponentKey>;
/**
* Components to disable.
*/
protected disable?: Array<EffectTarget>;
protected enable?: Array<EffectTarget>;
constructor(
changes: StateEffectDef) {
this.add = changes.add;
this.remove = changes.remove;
this.disable = changes.disable;
this.enable = changes.enable;
}
addEffects(effects: StateEffectDef) {
if (effects.add) {
if (this.add) {
addUnique(this.add, effects.add);
} else {
this.add = effects.add;
}
}
if (effects.remove) {
if (this.remove) {
addUnique(this.remove, effects.remove);
} else {
this.remove = effects.remove;
}
}
if (effects.disable) {
if (this.disable) {
addUnique(this.disable, effects.disable);
} else {
this.disable = effects.disable;
}
}
if (effects.enable) {
if (this.enable) {
addUnique(this.enable, effects.enable);
} else {
this.enable = effects.enable;
}
}
}
/**
* Get list of components/actors to be enabled on transition.
*/
getEnables() { return this.enable; }
/**
* Get list of components/actors to be disabled on transition.
*/
getDisables() { return this.disable; }
/**
* Get list of components to be removed on transition.
*/
getRemoves() { return this.remove; }
/**
* Get list of components to be added on transition.
*/
getAdds() { return this.add; }
/**
* Add component or component type to be added.
*/
addAdd(effect: ComponentKey) {
if (this.add === undefined) {
this.add = [effect];
} else if (!this.add.includes(effect)) {
this.add.push(effect);
}
}
/**
* Add component or component type to be removed.
*/
addRemove(effect: ComponentKey) {
if (this.remove === undefined) {
this.remove = [effect];
} else if (!this.remove.includes(effect)) {
this.remove.push(effect);
}
}
addEnable(effect: EffectTarget) {
if (this.enable === undefined) {
this.enable = [effect];
} else if (!this.enable.includes(effect)) {
this.enable.push(effect);
}
}
addDisable(effect: EffectTarget) {
if (this.disable === undefined) {
this.disable = [effect];
} else if (!this.disable.includes(effect)) {
this.disable.push(effect);
}
}
/**
* Set components to disable on this transition.
* @param disable
*/
setDisables(disable: EffectTarget[]) {
this.disable = disable;
}
/**
* Set components to disable on this transition.
* @param enable
*/
setEnable(enable: EffectTarget[]) {
this.enable = enable;
}
/**
* Set components to disable on this transition.
* @param disable
*/
setAdds(add: ComponentKey[]) {
this.add = add;
}
/**
* Set components to disable on this transition.
* @param disable
*/
setRemoves(removes: ComponentKey[]) {
this.remove = removes;
}
/**
* Apply transition to actor.
* @param actor
*/
apply(actor: Actor) {
if (this.add) {
const add = this.add;
for (let i = 0; i < add.length; i++) {
const comp = add[i];
if ('_isComponent' in comp) {
actor!.add(comp);
} else {
actor!.require(comp);
}
}
}
if (this.enable) {
const enable = this.enable;
for (let i = 0; i < enable.length; i++) {
let comp = enable[i];
if ('_isComponent' in comp) {
comp.enabled = true;
}
else if ('active' in comp) {
comp.active = true;
} else {
const val = actor.get(comp);
if (val) {
val.enabled = true;
}
}
}
}
if (this.remove) {
const remove = this.remove;
for (let i = 0; i < remove.length; i++) {
actor!.remove(remove[i]);
}
}
if (this.disable) {
const disable = this.disable;
for (let i = 0; i < disable.length; i++) {
let comp = disable[i];
if ('_isComponent' in comp) {
comp.enabled = false;
} else if ('active' in comp) {
comp.active = false;
} else {
const val = actor.get(comp);
if (val) {
val.enabled = false;
}
}
}
}
}
}
export class Transition<TKey = string | number | symbol> {
dest: TKey;
/**
* Time before transition enters new state.
*/
enterTime: number;
constructor(destState: TKey, enterTime: number = 0) {
this.dest = destState;
this.enterTime = enterTime;
}
}
export class State<TKey = string | number | Symbol, TTrigger = string | Symbol> {
readonly name: TKey;
onEnter?: StateEffect;
onExit?: StateEffect;
autoNext?: Transition<TKey>;
/**
* If two state transitions overlap, a state with lower priority ( lower numeric value )
* will be ignored.
*/
priority: number = 0;
/**
* Maps triggers to next state key.
*/
private readonly edges: Map<TTrigger, TKey> = new Map();
constructor(name: TKey, opts?: { onEnter?: StateEffect | StateEffectDef, onExit?: StateEffect | StateEffectDef, autoNext?: Transition<TKey> }) {
this.name = name;
if (opts) {
if (opts.onEnter) {
this.onEnter = opts.onEnter instanceof StateEffect ? opts.onEnter : new StateEffect(opts.onEnter!);
}
if (opts.onExit) {
this.onExit = opts.onExit instanceof StateEffect ? opts.onExit : new StateEffect(opts.onExit!);
}
this.autoNext = opts.autoNext;
}
}
/**
*
* @param state
* @returns true if an edge exists that leads
* from this state to the named state. This is not
* an efficient search.
*/
hasEdge(state: TKey) {
for (const name of this.edges.values()) {
if (name === state) return true;
}
return false;
}
/**
* @param trigger
* @returns True if the state contains an edge
* to another state with this trigger.
*/
canTrigger(trigger: TTrigger) {
return this.edges.has(trigger);
}
/**
* Add trigger to next state.
* @param trigger
* @param state
*/
addTrigger(trigger: TTrigger, state: TKey) {
this.edges.set(trigger, state);
}
removeTrigger(trigger: TTrigger) {
this.edges.delete(trigger);
}
/**
* Get name of state resulting from trigger.
* @param trigger
* @returns
*/
getNextState(trigger: TTrigger) {
return this.edges.get(trigger);
}
addEnterEffect(effect: StateEffectDef) {
if (this.onEnter) {
this.onEnter.addEffects(effect);
} else {
this.onEnter = new StateEffect(effect);
}
}
/**
* Add Component to enable when entering state.
* @param key
*/
addEnterEnable(key: ComponentKey) {
if (this.onEnter) {
this.onEnter.addEnable(key);
} else {
this.onEnter = new StateEffect({
enable: [key]
})
}
}
/**
* Add Component to disable when entering state.
* @param key
*/
addEnterDisable(key: ComponentKey) {
if (this.onEnter) {
this.onEnter.addDisable(key);
} else {
this.onEnter = new StateEffect({
disable: [key]
})
}
}
/**
* Add Component to enable when entering state.
* @param key
*/
addExitEnable(key: ComponentKey) {
if (this.onExit) {
this.onExit.addEnable(key);
} else {
this.onExit = new StateEffect({
enable: [key]
})
}
}
/**
* Add Component to disable when entering state.
* @param key
*/
addExitDisable(key: ComponentKey) {
if (this.onExit) {
this.onExit.addDisable(key);
} else {
this.onExit = new StateEffect({
disable: [key]
})
}
}
addExitEffect(effect: StateEffectDef) {
if (this.onExit) {
this.onExit.addEffects(effect);
} else {
this.onExit = new StateEffect(effect);
}
}
/**
* Set Enter-State Transition.
* @param t
*/
setEnter(t: StateEffect | StateEffectDef) {
this.onEnter = t instanceof StateEffect ? t : new StateEffect(t);
}
/**
* Set leave-state transition.
* @param t
*/
setExit(t: StateEffect | StateEffectDef) {
this.onExit = t instanceof StateEffect ? t : new StateEffect(t);
}
}