'use strict';
const SkillFlag = require('./SkillFlag');
const SkillType = require('./SkillType');
const SkillErrors = require('./SkillErrors');
const Damage = require('./Damage');
const Broadcast = require('./Broadcast');
/**
* @property {function (Effect)} configureEffect modify the skill's effect before adding to player
* @property {null|number} cooldownLength When a number > 0 apply a cooldown effect to disallow usage
* until the cooldown has ended
* @property {string} effect Id of the passive effect for this skill
* @property {Array<SkillFlag>} flags
* @property {function ()} info Function to run to display extra info about this skill
* @property {function ()} run Function to run when skill is executed/activated
* @property {GameState} state
* @property {SkillType} type
*/
class Skill {
/**
* @param {string} id
* @param {object} config
* @param {GameState} state
*/
constructor(id, config, state) {
const {
configureEffect = _ => _,
cooldown = null,
effect = null,
flags = [],
info = _ => {},
initiatesCombat = false,
name,
requiresTarget = true,
resource = null, /* format [{ attribute: 'someattribute', cost: 10}] */
run = _ => {},
targetSelf = false,
type = SkillType.SKILL,
options = {}
} = config;
this.configureEffect = configureEffect;
this.cooldownGroup = null;
if (cooldown && typeof cooldown === 'object') {
this.cooldownGroup = cooldown.group;
this.cooldownLength = cooldown.length;
} else {
this.cooldownLength = cooldown;
}
this.effect = effect;
this.flags = flags;
this.id = id;
this.info = info.bind(this);
this.initiatesCombat = initiatesCombat;
this.name = name;
this.options = options;
this.requiresTarget = requiresTarget;
this.resource = resource;
this.run = run.bind(this);
this.state = state;
this.targetSelf = targetSelf;
this.type = type;
}
/**
* perform an active skill
* @param {string} args
* @param {Player} player
* @param {Character} target
*/
execute(args, player, target) {
if (this.flags.includes(SkillFlag.PASSIVE)) {
throw new SkillErrors.PassiveError();
}
const cdEffect = this.onCooldown(player);
if (this.cooldownLength && cdEffect) {
throw new SkillErrors.CooldownError(cdEffect);
}
if (this.resource) {
if (!this.hasEnoughResources(player)) {
throw new SkillErrors.NotEnoughResourcesError();
}
}
if (target !== player && this.initiatesCombat) {
player.initiateCombat(target);
}
// allow skills to not incur the cooldown if they return false in run
if (this.run(args, player, target) !== false) {
this.cooldown(player);
if (this.resource) {
this.payResourceCosts(player);
}
}
return true;
}
/**
* @param {Player} player
* @return {boolean} If the player has paid the resource cost(s).
*/
payResourceCosts(player) {
const hasMultipleResourceCosts = Array.isArray(this.resource);
if (hasMultipleResourceCosts) {
for (const resourceCost of this.resource) {
this.payResourceCost(player, resourceCost);
}
return true;
}
return this.payResourceCost(player, this.resource);
}
// Helper to pay a single resource cost.
payResourceCost(player, resource) {
// Resource cost is calculated as the player damaging themself so effects
// could potentially reduce resource costs
const damage = new Damage(resource.attribute, resource.cost, player, this, {
hidden: true,
});
damage.commit(player);
}
activate(player) {
if (!this.flags.includes(SkillFlag.PASSIVE)) {
return;
}
if (!this.effect) {
throw new Error('Passive skill has no attached effect');
}
let effect = this.state.EffectFactory.create(this.effect, { description: this.info(player) });
effect = this.configureEffect(effect);
effect.skill = this;
player.addEffect(effect);
this.run(player);
}
/**
* @param {Character} character
* @return {boolean|Effect} If on cooldown returns the cooldown effect
*/
onCooldown(character) {
for (const effect of character.effects.entries()) {
if (effect.id === 'cooldown' && effect.state.cooldownId === this.getCooldownId()) {
return effect;
}
}
return false;
}
/**
* Put this skill on cooldown
* @param {number} duration Cooldown duration
* @param {Character} character
*/
cooldown(character) {
if (!this.cooldownLength) {
return;
}
character.addEffect(this.createCooldownEffect());
}
getCooldownId() {
return this.cooldownGroup ? "skillgroup:" + this.cooldownGroup : "skill:" + this.id;
}
/**
* Create an instance of the cooldown effect for use by cooldown()
*
* @private
* @return {Effect}
*/
createCooldownEffect() {
if (!this.state.EffectFactory.has('cooldown')) {
this.state.EffectFactory.add('cooldown', this.getDefaultCooldownConfig());
}
const effect = this.state.EffectFactory.create(
'cooldown',
{ name: "Cooldown: " + this.name, duration: this.cooldownLength * 1000 },
{ cooldownId: this.getCooldownId() }
);
effect.skill = this;
return effect;
}
getDefaultCooldownConfig() {
return {
config: {
name: 'Cooldown',
description: 'Cannot use ability while on cooldown.',
unique: false,
type: 'cooldown',
},
state: {
cooldownId: null
},
listeners: {
effectDeactivated: function () {
Broadcast.sayAt(this.target, `You may now use <bold>${this.skill.name}</bold> again.`);
}
}
};
}
/**
* @param {Character} character
* @return {boolean}
*/
hasEnoughResources(character) {
if (Array.isArray(this.resource)) {
return this.resource.every((resource) => this.hasEnoughResource(character, resource));
}
return this.hasEnoughResource(character, this.resource);
}
/**
* @param {Character} character
* @param {{ attribute: string, cost: number}} resource
* @return {boolean}
*/
hasEnoughResource(character, resource) {
return !resource.cost || (
character.hasAttribute(resource.attribute) &&
character.getAttribute(resource.attribute) >= resource.cost
);
}
}
module.exports = Skill;