// Copyright 2019 Campbell Crowley. All rights reserved.
// Author: Campbell Crowley (dev@campbellcrowley.com)
const HungryGames = require('./HungryGames.js');
/**
* Event that can happen in a game.
*
* @memberof HungryGames
* @inner
* @augments HungryGames~Event
*/
class NormalEvent extends HungryGames.Event {
/**
* @description Creates a HungryGames Normal Event.
*
* @param {string} message The message to show.
* @param {number} [numVictim=0] The number of victims in this event.
* @param {number} [numAttacker=0] The number of attackers in this event.
* @param {string} [victimOutcome=nothing] The outcome of the victims from
* this event.
* @param {string} [attackerOutcome=nothing] The outcome of the attackers from
* this event.
* @param {boolean} [victimKiller=false] Do the victims kill anyone in this
* event. Used for calculating kill count.
* @param {boolean} [attackerKiller=false] Do the attackers kill anyone in
* this event. Used for calculating kill count.
* @param {boolean} [battle] Is this event a battle?
* @param {number} [state=0] State of event if there are multiple attacks
* before the event.
* @param {HungryGames~NormalEvent[]} [attacks=[]] Array of attacks that take
* place before the event.
*/
constructor(
message, numVictim = 0, numAttacker = 0, victimOutcome = 'nothing',
attackerOutcome = 'nothing', victimKiller = false, attackerKiller = false,
battle = false, state = 0, attacks = []) {
super(message);
/**
* The action to format into a message if this is a weapon event.
*
* @public
* @type {?string}
* @default
*/
this.action = null;
/**
* Information about the victims in this event.
*
* @public
* @type {object}
* @property {number} count Number of victims. Negative means "at least" the
* magnitude.
* @property {string} outcome The outcome of the victims.
* @property {boolean} killer Do the victims kill the attackers.
* @property {?{id: string, count: number}} weapon The weapon information to
* give to the player.
*/
this.victim = {
count: numVictim,
outcome: victimOutcome,
killer: victimKiller,
weapon: null,
};
/**
* Information about the attackers in this event.
*
* @public
* @type {object}
* @property {number} count Number of attackers. Negative means "at least"
* the magnitude.
* @property {string} outcome The outcome of the attackers.
* @property {boolean} killer Do the attackers kill the victims.
* @property {?{id: string, count: number}} weapon The weapon information to
* give to the player.
*/
this.attacker = {
count: numAttacker,
outcome: attackerOutcome,
killer: attackerKiller,
weapon: null,
};
/**
* Is this event a battle event.
*
* @public
* @type {boolean}
* @default false
*/
this.battle = battle;
/**
* The current state of printing the battle messages.
*
* @public
* @type {number}
* @default 0
*/
this.state = state;
/**
* The attacks in a battle to show before the message.
*
* @public
* @type {HungryGames~Event[]}
* @default []
*/
this.attacks = attacks;
/**
* Amount of consumables used if this is a weapon event.
*
* @public
* @type {?number|string}
* @default
*/
this.consumes = null;
}
/**
* @description Compare this Event to another to check if they are equivalent.
* @public
* @param {HungryGames~NormalEvent} two Other NormalEvent to compare against.
* @returns {boolean} If they are equivalent.
*/
equal(two) {
return NormalEvent.equal(this, two);
}
/**
* @description Finalize this instance.
* @public
* @param {HungryGames~GuildGame} game Game context.
* @param {HungryGames~Player[]} affected An array of all players affected by
* this event.
* @returns {HungryGames~FinalEvent} The finalized event.
*/
finalize(game, affected) {
return new HungryGames.FinalEvent(this, game, affected);
}
/**
* @description Format an event string based on specified users.
* @public
* @static
* @param {string} message The message to show.
* @param {HungryGames~Player[]} affectedUsers An array of all users affected
* by this event.
* @param {number} numVictim Number of victims in this event.
* @param {number} numAttacker Number of attackers in this event.
* @param {string} victimOutcome The outcome of the victims from this event.
* @param {string} attackerOutcome The outcome of the attackers from this
* event.
* @param {HungryGames~GuildGame} game The GuildGame to make this event for.
* Used for settings and fetching other players not affected by this event if
* necessary.
* @returns {HungryGames~FinalEvent} The final event that was created and
* formatted ready for display.
*/
static finalize(
message, affectedUsers, numVictim, numAttacker, victimOutcome,
attackerOutcome, game) {
return new HungryGames.FinalEvent(
new NormalEvent(
message, numVictim, numAttacker, victimOutcome, attackerOutcome),
game, affectedUsers);
}
/**
* @description Compare two events to check if they are equivalent.
* @public
* @static
* @param {HungryGames~NormalEvent} e1 First event.
* @param {HungryGames~NormalEvent} e2 Second event to compare.
* @returns {boolean} If the two given events are equivalent.
*/
static equal(e1, e2) {
if (!e1 || !e2) return false;
if (e1.message != e2.message) return false;
if (e1.action != e2.action) return false;
if (e1.consumes != e2.consumes) return false;
if (e1.type != e2.type) return false;
if (!e1.battle != !e2.battle) return false;
const v1 = e1.victim;
const v2 = e2.victim;
if (v1 && v2) {
if (v1.count != v2.count) return false;
if (v1.outcome != v2.outcome) return false;
if (!v1.killer != !v2.killer) return false;
if (v1.weapon && v2.weapon) {
if (v1.weapon.id != v2.weapon.id) return false;
if (v1.weapon.count != v2.weapon.count) return false;
} else if (!(!v1.weapon && !v2.weapon)) {
return false;
}
} else if (!(!v1 && !v2)) {
return false;
}
const a1 = e1.attacker;
const a2 = e2.attacker;
if (a1 && a2) {
if (a1.count != a2.count) return false;
if (a1.outcome != a2.outcome) return false;
if (!a1.killer != !a2.killer) return false;
if (a1.weapon && a2.weapon) {
if (a1.weapon.id != a2.weapon.id) return false;
if (a1.weapon.count != a2.weapon.count) return false;
} else if (!(!a1.weapon && !a2.weapon)) {
return false;
}
} else if (!(!a1 && !a2)) {
return false;
}
return true;
}
/**
* @description Validate that the given data is properly typed and structured
* to be converted to a NormalEvent. Also coerces values to correct types if
* possible.
* @public
* @static
* @param {HungryGames~NormalEvent} evt The event data to verify.
* @returns {?string} Error string, or null if no error.
*/
static validate(evt) {
const err = HungryGames.Event.validate(evt);
if (err) return err;
if (evt.action && (typeof evt.action !== 'string' ||
evt.action.length === 0 || evt.action.length > 1000)) {
return 'BAD_ACTION';
}
if (evt.battle != null && typeof evt.battle !== 'boolean' &&
evt.battle !== 'true' && evt.battle !== 'false') {
return 'BAD_BATTLE';
} else if (evt.battle != null) {
evt.battle = evt.battle === 'true';
}
if (evt.state != null && !Number.isSafeInteger(evt.state *= 1)) {
return 'BAD_STATE';
}
if (evt.attacks && !Array.isArray(evt.attacks)) {
return 'BAD_ATTACKS';
} else if (evt.attacks) {
let outerr;
let index;
evt.attacks.find((el, i) => {
outerr = NormalEvent.validate(el);
index = i;
return outerr;
});
if (outerr) {
return `BAD_ATTACK_${index}_${outerr}`;
}
}
if (evt.consumes &&
(typeof evt.consumes !== 'string' || evt.consumes.length > 100 ||
evt.consumes.length === 0) &&
!Number.isSafeInteger(evt.consumes * 1)) {
return 'BAD_CONSUMES';
}
if (evt.victim) {
switch (evt.victim.outcome) {
case 'nothing':
case 'dies':
case 'wounded':
case 'revived':
case 'thrives':
break;
default:
return 'BAD_VICTIM_OUTCOME';
}
if (!Number.isSafeInteger(evt.victim.count *= 1)) {
return 'BAD_VICTIM_COUNT';
}
if (typeof evt.victim.killer !== 'boolean') {
if (evt.victim.killer && evt.victim.killer !== 'true' &&
evt.victim.killer !== 'false') {
return 'BAD_VICTIM_KILLER';
} else {
evt.victim.killer = evt.victim.killer === 'true';
}
}
if (evt.victim.weapon &&
(typeof evt.victim.weapon.id !== 'string' ||
!Number.isSafeInteger(evt.victim.weapon.count *= 1))) {
return 'BAD_VICTIM_WEAPON';
}
}
if (evt.attacker) {
switch (evt.attacker.outcome) {
case 'nothing':
case 'dies':
case 'wounded':
case 'revived':
case 'thrives':
break;
default:
return 'BAD_ATTACKER_OUTCOME';
}
if (!Number.isSafeInteger(evt.attacker.count *= 1)) {
return 'BAD_ATTACKER_COUNT';
}
if (typeof evt.attacker.killer !== 'boolean') {
if (evt.attacker.killer && evt.attacker.killer !== 'true' &&
evt.attacker.killer !== 'false') {
return 'BAD_ATTACKER_KILLER';
} else {
evt.attacker.killer = evt.attacker.killer === 'true';
}
}
if (evt.attacker.weapon &&
(typeof evt.attacker.weapon.id !== 'string' ||
!Number.isSafeInteger(evt.attacker.weapon.count *= 1))) {
return 'BAD_ATTACKER_WEAPON';
}
}
return null;
}
/**
* @description Create a new NormalEvent object from a NormalEvent-like
* object. Similar to copy-constructor.
*
* @public
* @static
* @param {object} obj Event-like object to copy.
* @returns {HungryGames~NormalEvent} Copy of event.
*/
static from(obj) {
const out = new NormalEvent(obj.message);
out.fill(obj);
if (obj.victim) {
out.victim.count = obj.victim.count || 0;
out.victim.outcome = obj.victim.outcome || 'nothing';
out.victim.killer = obj.victim.killer || false;
if (obj.victim.weapon) {
out.victim.weapon = {
id: obj.victim.weapon.id,
count: obj.victim.weapon.count,
};
}
}
if (obj.attacker) {
out.attacker.count = obj.attacker.count || 0;
out.attacker.outcome = obj.attacker.outcome || 'nothing';
out.attacker.killer = obj.attacker.killer || false;
if (obj.attacker.weapon) {
out.attacker.weapon = {
id: obj.attacker.weapon.id,
count: obj.attacker.weapon.count,
};
}
}
if (Array.isArray(obj.attacks)) {
out.attacks = obj.attacks.map((el) => NormalEvent.from(el));
}
if (typeof obj.action === 'string') out.action = obj.action;
if (typeof obj.battle === 'boolean') out.battle = obj.battle;
if (typeof obj.state === 'number') out.state = obj.state;
if (obj.consumes) out.consumes = obj.consumes;
return out;
}
}
module.exports = NormalEvent;