'use strict';
const EventEmitter = require('events');
/** @typedef EffectModifiers {{attributes: !Object<string,function>}} */
var EffectModifiers;
/**
* See the {@link http://ranviermud.com/extending/effects/|Effect guide} for usage.
* @property {object} config Effect configuration (name/desc/duration/etc.)
* @property {boolean} config.autoActivate If this effect immediately activates itself when added to the target
* @property {boolean} config.hidden If this effect is shown in the character's effect list
* @property {boolean} config.refreshes If an effect with the same type is applied it will trigger an effectRefresh
* event instead of applying the additional effect.
* @property {boolean} config.unique If multiple effects with the same `config.type` can be applied at once
* @property {number} config.maxStacks When adding an effect of the same type it adds a stack to the current
* effect up to maxStacks instead of adding the effect. Implies `config.unique`
* @property {boolean} config.persists If false the effect will not save to the player
* @property {string} config.type The effect category, mainly used when disallowing stacking
* @property {boolean|number} config.tickInterval Number of seconds between calls to the `updateTick` listener
* @property {string} description
* @property {number} duration Total duration of effect in _milliseconds_
* @property {number} elapsed Get elapsed time in _milliseconds_
* @property {string} id filename minus .js
* @property {EffectModifiers} modifiers Attribute modifier functions
* @property {string} name
* @property {number} remaining Number of seconds remaining
* @property {number} startedAt Date.now() time this effect became active
* @property {object} state Configuration of this _type_ of effect (magnitude, element, stat, etc.)
* @property {Character} target Character this effect is... effecting
* @extends EventEmitter
*
* @listens Effect#effectAdded
*/
class Effect extends EventEmitter {
constructor(id, def) {
super();
this.id = id;
this.flags = def.flags || [];
this.config = Object.assign({
autoActivate: true,
description: '',
duration: Infinity,
hidden: false,
maxStacks: 0,
name: 'Unnamed Effect',
persists: true,
refreshes: false,
tickInterval: false,
type: 'undef',
unique: true,
}, def.config);
this.startedAt = 0;
this.paused = 0;
this.modifiers = Object.assign({
attributes: {},
incomingDamage: (damage, current) => current,
outgoingDamage: (damage, current) => current,
}, def.modifiers);
// internal state saved across player load e.g., stacks, amount of damage shield remaining, whatever
// Default state can be found in config.state
this.state = Object.assign({}, def.state);
if (this.config.maxStacks) {
this.state.stacks = 1;
}
// If an effect has a tickInterval it should always apply when first activated
if (this.config.tickInterval && !this.state.tickInterval) {
this.state.lastTick = -Infinity;
this.state.ticks = 0;
}
if (this.config.autoActivate) {
this.on('effectAdded', this.activate);
}
}
/**
* @type {string}
*/
get name() {
return this.config.name;
}
/**
* @type {string}
*/
get description() {
return this.config.description;
}
/**
* @type {number}
*/
get duration() {
return this.config.duration;
}
set duration(dur) {
this.config.duration = dur;
}
/**
* Elapsed time in milliseconds since event was activated
* @type {number}
*/
get elapsed () {
if (!this.startedAt) {
return null;
}
return this.paused || (Date.now() - this.startedAt);
}
/**
* Remaining time in seconds
* @type {number}
*/
get remaining() {
return this.config.duration - this.elapsed;
}
/**
* Whether this effect has lapsed
* @return {boolean}
*/
isCurrent() {
return this.elapsed < this.config.duration;
}
/**
* Set this effect active
* @fires Effect#effectActivated
*/
activate() {
if (!this.target) {
throw new Error('Cannot activate an effect without a target');
}
if (this.active) {
return;
}
this.startedAt = Date.now() - this.elapsed;
/**
* @event Effect#effectActivated
*/
this.emit('effectActivated');
this.active = true;
}
/**
* Set this effect active
* @fires Effect#effectDeactivated
*/
deactivate() {
if (!this.active) {
return;
}
/**
* @event Effect#effectDeactivated
*/
this.emit('effectDeactivated');
this.active = false;
}
/**
* Remove this effect from its target
* @fires Effect#remove
*/
remove() {
/**
* @event Effect#remove
*/
this.emit('remove');
}
/**
* Stop this effect from having any effect temporarily
*/
pause() {
this.paused = this.elapsed;
}
/**
* Resume a paused effect
*/
resume() {
this.startedAt = Date.now() - this.paused;
this.paused = null;
}
/**
* @param {string} attrName
* @param {number} currentValue
* @return {number} attribute modified by effect
*/
modifyAttribute(attrName, currentValue) {
let modifier = _ => _;
if (typeof this.modifiers.attributes === 'function') {
modifier = (current) => {
return this.modifiers.attributes.bind(this)(attrName, current);
};
} else if (attrName in this.modifiers.attributes) {
modifier = this.modifiers.attributes[attrName];
}
return modifier.bind(this)(currentValue);
}
/**
* @param {Damage} damage
* @param {number} currentAmount
* @return {Damage}
*/
modifyIncomingDamage(damage, currentAmount) {
const modifier = this.modifiers.incomingDamage.bind(this);
return modifier(damage, currentAmount);
}
/**
* @param {Damage} damage
* @param {number} currentAmount
* @return {Damage}
*/
modifyOutgoingDamage(damage, currentAmount) {
const modifier = this.modifiers.outgoingDamage.bind(this);
return modifier(damage, currentAmount);
}
/**
* Gather data to persist
* @return {Object}
*/
serialize() {
let config = Object.assign({}, this.config);
config.duration = config.duration === Infinity ? 'inf' : config.duration;
let state = Object.assign({}, this.state);
// store lastTick as a difference so we can make sure to start where we left off when we hydrate
if (state.lastTick && isFinite(state.lastTick)) {
state.lastTick = Date.now() - state.lastTick;
}
return {
config,
elapsed: this.elapsed,
id: this.id,
remaining: this.remaining,
skill: this.skill && this.skill.id,
state,
};
}
/**
* Reinitialize from persisted data
* @param {GameState}
* @param {Object} data
*/
hydrate(state, data) {
data.config.duration = data.config.duration === 'inf' ? Infinity : data.config.duration;
this.config = data.config;
if (!isNaN(data.elapsed)) {
this.startedAt = Date.now() - data.elapsed;
}
if (!isNaN(data.state.lastTick)) {
data.state.lastTick = Date.now() - data.state.lastTick;
}
this.state = data.state;
if (data.skill) {
this.skill = state.SkillManager.get(data.skill) || state.SpellManager.get(data.skill);
}
}
}
module.exports = Effect;