'use strict';
/**
* Self-managing list of effects for a target
* @property {Set} effects
* @property {number} size Number of currently active effects
* @property {Character} target
*/
class EffectList {
/**
* @param {Character} target
* @param {Array<Object|Effect>} effects array of serialized effects (Object) or actual Effect instances
*/
constructor(target, effects) {
this.effects = new Set(effects);
this.target = target;
}
/**
* @type {number}
*/
get size() {
this.validateEffects();
return this.effects.size;
}
/**
* Get current list of effects as an array
* @return {Array<Effect>}
*/
entries() {
this.validateEffects();
return [...this.effects];
}
/**
* @param {string} type
* @return {boolean}
*/
hasEffectType(type) {
return !!this.getByType(type);
}
/**
* @param {string} type
* @return {Effect}
*/
getByType(type) {
return [...this.effects].find(effect => {
return effect.config.type === type;
});
}
/**
* Proxy an event to all effects
* @param {string} event
* @param {...*} args
*/
emit(event, ...args) {
this.validateEffects();
if (event === 'effectAdded' || event === 'effectRemoved') {
// don't forward these events on from the player as it would cause confusion between Character#effectAdded
// and Effect#effectAdded. The former being when any effect gets added to a character, the later is fired on
// an effect when it is added to a character
return;
}
for (const effect of this.effects) {
if (effect.paused) {
continue;
}
if (event === 'updateTick' && effect.config.tickInterval) {
const sinceLastTick = Date.now() - effect.state.lastTick;
if (sinceLastTick < effect.config.tickInterval * 1000) {
continue;
}
effect.state.lastTick = Date.now();
effect.state.ticks++;
}
effect.emit(event, ...args);
}
}
/**
* @param {Effect} effect
* @fires Effect#effectAdded
* @fires Effect#effectStackAdded
* @fires Effect#effectRefreshed
* @fires Character#effectAdded
*/
add(effect) {
if (effect.target) {
throw new Error('Cannot add effect, already has a target.');
}
for (const activeEffect of this.effects) {
if (effect.config.type === activeEffect.config.type) {
if (activeEffect.config.maxStacks && activeEffect.state.stacks < activeEffect.config.maxStacks) {
activeEffect.state.stacks = Math.min(activeEffect.config.maxStacks, activeEffect.state.stacks + 1);
/**
* @event Effect#effectStackAdded
* @param {Effect} effect The new effect that is trying to be added
*/
activeEffect.emit('effectStackAdded', effect);
return true;
}
if (activeEffect.config.refreshes) {
/**
* @event Effect#effectRefreshed
* @param {Effect} effect The new effect that is trying to be added
*/
activeEffect.emit('effectRefreshed', effect);
return true;
}
if (activeEffect.config.unique) {
return false;
}
}
}
this.effects.add(effect);
effect.target = this.target;
/**
* @event Effect#effectAdded
*/
effect.emit('effectAdded');
/**
* @event Character#effectAdded
*/
this.target.emit('effectAdded', effect);
effect.on('remove', () => this.remove(effect));
return true;
}
/**
* Deactivate and remove an effect
* @param {Effect} effect
* @throws ReferenceError
* @fires Character#effectRemoved
*/
remove(effect) {
if (!this.effects.has(effect)) {
throw new ReferenceError("Trying to remove effect that was never added");
}
effect.deactivate();
this.effects.delete(effect);
/**
* @event Character#effectRemoved
*/
this.target.emit('effectRemoved');
}
/**
* Remove all effects, bypassing all deactivate and remove events
*/
clear() {
this.effects = new Set();
}
/**
* Ensure effects are still current and if not remove them
*/
validateEffects() {
for (const effect of this.effects) {
if (!effect.isCurrent()) {
this.remove(effect);
}
}
}
/**
* Gets the effective "max" value of an attribute (before subtracting delta).
* Does the work of actaully applying attribute modification
* @param {Atrribute} attr
* @return {number}
*/
evaluateAttribute(attr) {
this.validateEffects();
let attrName = attr.name;
let attrValue = attr.base || 0;
for (const effect of this.effects) {
if (effect.paused) {
continue;
}
attrValue = effect.modifyAttribute(attrName, attrValue);
}
return attrValue;
}
/**
* @param {Damage} damage
* @param {number} currentAmount
* @return {number}
*/
evaluateIncomingDamage(damage, currentAmount) {
this.validateEffects();
for (const effect of this.effects) {
currentAmount = effect.modifyIncomingDamage(damage, currentAmount);
}
// Don't allow a modifier to make damage go negative, it would cause weird
// behavior where damage raises an attribute
return Math.max(currentAmount, 0) || 0;
}
/**
* @param {Damage} damage
* @param {number} currentAmount
* @return {number}
*/
evaluateOutgoingDamage(damage, currentAmount) {
this.validateEffects();
for (const effect of this.effects) {
currentAmount = effect.modifyOutgoingDamage(damage, currentAmount);
}
// Same thing, mutatis mutandis, for outgoing damage
return Math.max(currentAmount, 0) || 0;
}
serialize() {
this.validateEffects();
let serialized = [];
for (const effect of this.effects) {
if (!effect.config.persists) {
continue;
}
serialized.push(effect.serialize());
}
return serialized;
}
hydrate(state) {
const effects = this.effects;
this.effects = new Set();
for (const newEffect of effects) {
const effect = state.EffectFactory.create(newEffect.id);
effect.hydrate(state, newEffect);
this.add(effect);
}
}
}
module.exports = EffectList;