// Copyright 2019 Campbell Crowley. All rights reserved.
// Author: Campbell Crowley (dev@campbellcrowley.com)
const HungryGames = require('./HungryGames.js');
/**
* A single instance of a game in a guild.
*
* @memberof HungryGames
* @inner
*/
class GuildGame {
/**
* @description Create a game instance for a single guild.
* @param {string} bot User id of the current bot instance.
* @param {string} id Guild id of the Guild that this object is for.
* @param {object<number|boolean|string|object>} options The game options.
* @param {string} [name] Name of this game to be passed to the Game object.
* @param {string[]|HungryGames~Player[]} [includedUsers] Array of user IDs
* that will be included in the next game, or array of Players to include.
* @param {string[]} [excludedUsers] Array of user IDs that have been
* excluded from the games.
* @param {HungryGames~NPC[]} [includedNPCs] Array of NPC objects to include
* in the game.
* @param {HungryGames~NPC[]} [excludedNPCs] Array of NPC objects to exclude
* from the game.
* @param {{
* bloodbath: string[],
* player: string[],
* weapon: string[],
* arena: string[],
* battle: {
* starts: string[],
* attacks: string[],
* outcomes: string[]
* }
* }} customEventIds Array of IDs of custom events to load.
* @param {string[]} disabledEventIds Array of IDs of events to be disabled
* from game.
*/
constructor(
bot, id, options, name, includedUsers, excludedUsers, includedNPCs,
excludedNPCs, customEventIds, disabledEventIds) {
/**
* The ID of the current bot account.
*
* @public
* @type {string}
* @constant
*/
this.bot = bot;
/**
* The ID of the Guild this is for.
*
* @public
* @type {string}
* @constant
*/
this.id = id;
/**
* Array of user IDs that will be included in the next game.
*
* @public
* @type {string[]}
* @default []
*/
this.includedUsers = [];
if (Array.isArray(includedUsers)) {
for (let i = 0; i < includedUsers.length; i++) {
if (typeof includedUsers[i] === 'string') {
this.includedUsers.push(includedUsers.splice(i, 1)[0]);
i--;
} else {
this.includedUsers.push(includedUsers[i].id);
}
}
}
/**
* Array of user IDs that will be excluded from the next game.
*
* @public
* @type {string[]}
* @default []
*/
this.excludedUsers = excludedUsers || [];
/**
* Array of NPCs that will be included in the game.
*
* @public
* @type {HungryGames~NPC[]}
* @default []
*/
this.includedNPCs = includedNPCs || [];
/**
* Array of NPCs that will be excluded from the game.
*
* @public
* @type {HungryGames~NPC[]}
* @default []
*/
this.excludedNPCs = excludedNPCs || [];
/**
* Game options.
*
* @public
* @type {object}
*/
this.options = options;
/**
* Is this game autoplaying?
*
* @public
* @type {boolean}
* @default false
*/
this.autoPlay = false;
/**
* Does this game currently have any long running operations being
* performed.
*
* @public
* @type {boolean}
* @default false
*/
this.loading = false;
/**
* Is this game automatically stepping, or are steps controlled manually.
*
* @private
* @type {boolean}
* @default false
*/
this._autoStep = false;
/**
* @description Storage manager for all custom events.
* @todo Currently each guild will cache all events on its own, this will be
* find for now, but would be more efficient if a global cache was used
* instead.
* @public
* @type {HungryGames~EventContainer}
* @constant
*/
this.customEventStore = new HungryGames.EventContainer(customEventIds);
/**
* Current game information.
*
* @public
* @type {HungryGames~Game}
* @default
*/
this.currentGame = new HungryGames.Game(name, includedUsers);
/**
* @description List of IDs of events to disable per-category.
* @public
* @type {{
* bloodbath: string[],
* player: string[],
* arena: string[],
* weapon: string[],
* battle: {
* starts: string[],
* attacks: string[],
* outcomes: string[]
* }
* }}
*/
this.disabledEventIds = disabledEventIds || {
bloodbath: [],
player: [],
arena: [],
weapon: [],
battle: {
starts: [],
attacks: [],
outcomes: [],
},
};
/**
* The channel id a command was last sent from that affected this guild
* game.
*
* @public
* @type {?string}
* @default
*/
this.channel = null;
/**
* The id of the user that last sent a command which interacted with this
* guild game.
*
* @public
* @type {?string}
* @default
*/
this.author = null;
/**
* The channel id where the game messages are currently being sent in.
*
* @public
* @type {?string}
* @default
*/
this.outputChannel = null;
/**
* Message ID of the message to fetch reactions from for join via react.
*
* @public
* @type {?{id: string, channel: string}}
* @default
*/
this.reactMessage = null;
/**
* The ID of the currently active {@link HungryGames~StatGroup} tracking
* stats.
*
* @public
* @type {?string}
* @default
*/
this.statGroup = null;
/**
* The actions to perform when certain events occur.
*
* @public
* @type {HungryGames~ActionStore}
* @default
*/
this.actions = new HungryGames.ActionStore();
/**
* Interval for day events.
*
* @private
* @type {?Timeout}
* @default
*/
this._dayEventInterval = null;
/**
* The timeout to continue autoplaying after the day ends. Used for
* cancelling if user ends the game between days.
*
* @private
* @type {?Timeout}
* @default
*/
this._autoPlayTimeout = null;
/**
* Function to call when state is modified.
*
* @private
* @type {?HungryGames~GuildGame~StateUpdateCB}
* @default
*/
this._stateUpdateCallback = null;
/**
* Manages all stats for all players.
*
* @private
* @type {HungryGames~StatManager}
* @constant
*/
this._stats = new HungryGames.StatManager(this);
this.step = this.step.bind(this);
this.modifyPlayerWeapon = this.modifyPlayerWeapon.bind(this);
}
/**
* @description Get a serializable version of this class instance. Strips all
* private variables, and all functions. Assumes all public variables are
* serializable if they aren't a function.
* @public
* @returns {object} Serializable version of this instance.
*/
get serializable() {
const all = Object.entries(Object.getOwnPropertyDescriptors(this));
const output = {};
for (const one of all) {
if (typeof one[1].value === 'function' || one[0].startsWith('_')) {
continue;
} else if (one[1].value && one[1].value.serializable) {
output[one[0]] = one[1].value.serializable;
} else {
output[one[0]] = one[1].value;
}
}
return output;
}
/**
* @description Callback to fire when game state is about to be modified.
* @callback HungryGames~GuildGame~StateUpdateCB
* @param {boolean} dayComplete True if this update is after a day has ended,
* false if the state is still during a day.
* @param {boolean} doSim True if the next day should be simulated and
* started.
*/
/**
* @description Add users to teams, and remove excluded users from teams.
* Deletes empty teams, and adds teams once all teams have teamSize of
* players.
*
* @public
* @returns {?string} Null if success, string if error.
*/
formTeams() {
if (this.options.teamSize < 0) this.options.teamSize = 0;
if (this.options.teamSize == 0) {
this.currentGame.teams = [];
return;
}
let corruptTeam = false;
const teamSize = this.options.teamSize;
const numTeams =
Math.ceil(this.currentGame.includedUsers.length / teamSize);
// If teams already exist, update them. Otherwise, create new teams.
if (this.currentGame.teams && this.currentGame.teams.length > 0) {
this.currentGame.teams.forEach((obj) => {
obj.players.forEach((p) => {
if (typeof p !== 'string' && typeof p !== 'number') {
corruptTeam = true;
console.error(
'(PreTeamForm) Player in team is invalid: ' + typeof p +
' in team ' + obj.id + ' guild: ' + this.id + ' players: ' +
JSON.stringify(obj.players));
}
});
});
this.currentGame.teams.sort((a, b) => a.id - b.id);
const notIncluded = this.currentGame.includedUsers.slice(0);
// Remove players from teams if they are no longer included in game.
for (let i = 0; i < this.currentGame.teams.length; i++) {
const team = this.currentGame.teams[i];
team.id = i;
for (let j = 0; j < team.players.length; j++) {
if (!this.currentGame.includedUsers.find(
(obj) => obj.id === team.players[j])) {
team.players.splice(j, 1);
j--;
} else {
notIncluded.splice(
notIncluded.findIndex((obj) => obj.id === team.players[j]), 1);
}
}
if (team.players.length == 0) {
this.currentGame.teams.splice(i, 1);
i--;
}
}
// Add players who are not on a team, to a team.
for (let i = 0; i < notIncluded.length; i++) {
let found = false;
for (let j = 0; j < this.currentGame.teams.length; j++) {
const team = this.currentGame.teams[j];
if (team.players.length < teamSize) {
team.players.push(notIncluded[i].id);
found = true;
break;
}
}
if (found) continue;
// Add a team if all existing teams are full.
this.currentGame.teams[this.currentGame.teams.length] =
new HungryGames.Team(
this.currentGame.teams.length,
'Team ' + (this.currentGame.teams.length + 1),
[notIncluded[i].id]);
}
} else {
// Create all teams for players.
this.currentGame.teams = [];
for (let i = 0; i < numTeams; i++) {
this.currentGame.teams[i] = new HungryGames.Team(
i, `Team ${i + 1}`,
this.currentGame.includedUsers
.slice(i * teamSize, i * teamSize + teamSize)
.map((obj) => obj.id));
}
}
// Reset team data.
this.currentGame.teams.forEach((obj) => {
obj.numAlive = obj.players.length;
obj.rank = 1;
obj.players.forEach((p) => {
if (typeof p !== 'string' && typeof p !== 'number') {
corruptTeam = true;
console.error(
'(PostTeamForm) Player in team is invalid: ' + typeof p +
' in team ' + obj.id + ' guild: ' + this.id + ' players: ' +
JSON.stringify(obj.players));
}
});
});
if (corruptTeam) {
return 'Teams appeared to be corrupted, teams may have been ' +
'rearranged.\nIf you have more information, please report this bug.';
}
return null;
}
/**
* @description Force this current game to end immediately.
* @public
*/
end() {
this.currentGame.inProgress = false;
this.currentGame.isPaused = true;
this.currentGame.ended = true;
this.autoPlay = false;
this.clearIntervals();
if (this.currentGame.day.state === 1) this.currentGame.day.state = 0;
}
/**
* @description Clear all timeouts and intervals.
* @public
*/
clearIntervals() {
if (this._dayEventInterval) {
clearInterval(this._dayEventInterval);
this._dayEventInterval = null;
}
if (this._autoPlayTimeout) {
clearInterval(this._autoPlayTimeout);
this._autoPlayTimeout = null;
}
this._autoStep = false;
this.currentGame.isPaused = true;
}
/**
* @description Create an interval for this guild. Calls the callback every
* time the game state is about to be modified. State is updated immediately
* after the callback completes. This also sets `_autoStep` to true.
* @public
* @param {HungryGames~GuildGame~StateUpdateCB} [cb] Callback to fire on the
* interval. Optional only if set via {@link setStateUpdateCallback} prior to
* call to this function.
*/
createInterval(cb) {
if (cb && typeof cb !== 'function') {
throw new Error('Callback must be a function');
} else if (
typeof cb !== 'function' &&
typeof this._stateUpdateCallback !== 'function') {
throw new Error('Callback must be a function');
}
if (this._dayEventInterval) {
throw new Error(
'Attempted to register second listener for existing interval.');
}
this.currentGame.isPaused = false;
this._autoStep = true;
if (cb) this._stateUpdateCallback = cb;
const delay = this.options.disableOutput ? 1 : this.options.delayEvents;
this.step();
this._dayEventInterval = setInterval(this.step, delay);
}
/**
* @description Set the state update callback function.
* @public
* @param {HungryGames~GuildGame~StateUpdateCB} cb Callback to fire when
* stepped.
*/
setStateUpdateCallback(cb) {
if (typeof cb !== 'function') {
throw new Error('Callback must be a function');
}
this._stateUpdateCallback = cb;
}
/**
* @description Progress to the next game state. Calls `_stateUpdateCallback`
* prior to any action, if it's set.
* @public
*/
step() {
const day = this.currentGame.day;
const index = day.state - 2;
const dayOver = index >= day.events.length;
if (typeof this._stateUpdateCallback === 'function') {
this._stateUpdateCallback(dayOver, index < 0 && !this.currentGame.ended);
}
if (this._autoPlayTimeout) {
clearTimeout(this._autoPlayTimeout);
this._autoPlayTimeout = null;
}
if (dayOver) {
if (this._dayEventInterval) {
clearInterval(this._dayEventInterval);
this._dayEventInterval = null;
}
if (this.autoPlay && this._autoStep && !this.currentGame.isPaused) {
const delay = this.options.disableOutput ? 1 : this.options.delayDays;
this._autoPlayTimeout = setTimeout(this.step, delay);
}
day.state = 0;
this._stats.parseDay();
} else if (index < 0) {
return;
} else if (
day.events[index].battle &&
day.events[index].state < day.events[index].attacks.length) {
day.events[index].state++;
} else {
day.state++;
}
}
/**
* @description Create a GuildGame from data parsed from file. Similar to copy
* constructor.
*
* @public
* @static
* @param {object} data GuildGame like object.
* @param {Discord~Client} client Discord client reference for
* creating {@link HungryGames~ActionStore}.
* @returns {HungryGames~GuildGame} Created GuildGame.
*/
static from(data, client) {
const game = new GuildGame(
data.bot, data.id, data.options, data.name, data.includedUsers,
data.excludedUsers, data.includedNPCs, data.excludedNPCs);
game.autoPlay = data.autoPlay || false;
game.reactMessage = data.reactMessage || null;
// Legacy custom event storage.
if (data.customEvents) {
if (!game.legacyEvents) game.legacyEvents = {};
game.legacyEvents.bloodbath = data.customEvents.bloodbath || [];
game.legacyEvents.player = data.customEvents.player || [];
game.legacyEvents.arena = data.customEvents.arena || [];
game.legacyEvents.weapon = data.customEvents.weapon || {};
if (data.customEvents.battle) {
if (!game.legacyEvents.battle) game.legacyEvents.battle = {};
game.legacyEvents.battle.starts = data.customEvents.battle.starts || [];
game.legacyEvents.battle.attacks =
data.customEvents.battle.attacks || [];
game.legacyEvents.battle.outcomes =
data.customEvents.battle.outcomes || [];
}
}
if (data.legacyEvents) {
game.legacyEvents = data.legacyEvents;
}
// End legacy custom events.
if (data.customEventStore) {
game.customEventStore.updateAndFetchAll(data.customEventStore);
}
if (data.disabledEventIds) {
game.disabledEventIds.bloodbath = data.disabledEventIds.bloodbath || [];
game.disabledEventIds.player = data.disabledEventIds.player || [];
game.disabledEventIds.arena = data.disabledEventIds.arena || [];
game.disabledEventIds.weapon = data.disabledEventIds.weapon || [];
if (data.disabledEventIds.battle) {
game.disabledEventIds.battle.starts =
data.disabledEventIds.battle.starts || [];
game.disabledEventIds.battle.attacks =
data.disabledEventIds.battle.attacks || [];
game.disabledEventIds.battle.outcomes =
data.disabledEventIds.battle.outcomes || [];
}
}
game.channel = data.channel || null;
game.author = data.author || null;
game.outputChannel = data.outputChannel || null;
game.statGroup = data.statGroup || null;
if (data.currentGame) {
game.currentGame = HungryGames.Game.from(data.currentGame);
}
if (data.actions) {
game.actions =
HungryGames.ActionStore.from(client, game.id, data.actions);
}
return game;
}
/**
* @description Force a player to have a certain outcome in the current day
* being simulated, or the next day that will be simulated. This is acheived
* by adding a custom event in which the player will be affected after their
* normal event for the day.
*
* @public
* @static
* @param {HungryGames~GuildGame} game The game context.
* @param {string[]} list The array of player IDs of which to affect.
* @param {string} state The outcome to force the players to have been
* victims of by the end of the simulated day. ("living", "dead", "wounded",
* or "thriving").
* @param {HungryGames~Messages} messages Reference to current Messages
* instance.
* @param {string|HungryGames~NormalEvent[]} text Message to show when the
* user is affected, or array of default events if not specifying a specific
* message.
* @param {?string} locale Language locale to use for string lookup.
* @param {Function} cb Callback once complete. Single parameter is the
* output message to tell the user of the outcome of the operation.
*/
static forcePlayerState(game, list, state, messages, text, locale, cb) {
if (!Array.isArray(list)) {
messages = state;
text = list.text;
state = list.state;
list = list.list;
}
if (!Array.isArray(list) || list.length == 0) {
cb('effectPlayerNoPlayer');
return;
}
if (typeof state !== 'string') {
cb('effectPlayerNoOutcome');
return;
}
const players = [];
const outcomes = {};
list.forEach((p) => {
const player = game.currentGame.includedUsers.find((el) => el.id == p);
if (!player) return;
players.push(player.name);
let living = player.living;
let currentState = player.state;
if (game.currentGame.day.state <= 1) {
game.currentGame.nextDay.events.forEach((evt) => {
const found = evt.icons.find((icon) => icon.id === player.id);
if (!found) return;
const group = found.settings.victim ? evt.victim : evt.attacker;
switch (group.outcome) {
case 'dies':
living = false;
currentState = 'dead';
break;
case 'revived':
living = true;
currentState = 'zombie';
break;
case 'thrives':
living = true;
currentState = 'living';
break;
case 'wounded':
living = true;
currentState = 'wounded';
break;
}
});
}
let outcome;
if (living && state === 'dead') {
outcome = 'dies';
} else if (
!living && (state === 'living' || state === 'thriving')) {
outcome = 'revived';
} else if (currentState === 'wounded' && state === 'thriving') {
outcome = 'thrives';
} else if (living && currentState !== 'wounded' && state === 'wounded') {
outcome = 'wounded';
} else {
return;
}
if (!outcomes[outcome]) outcomes[outcome] = [];
outcomes[outcome].push(player);
});
/**
* @description Find and apply an event to players.
* @private
* @param {HungryGames~Player[]} affected Array of players to affect.
* @param {string} outcome The outcome to apply.
*/
const findEvent = function(affected, outcome) {
let evt;
if (typeof text !== 'string' && Array.isArray(text) &&
game.options.anonForceOutcome) {
let eventPool = text.concat(game.customEventStore.getArray('player'));
eventPool = eventPool.filter((el) => {
const checkVictim = el.victim.outcome === outcome &&
(el.victim.count === affected.length ||
(el.victim.count < 0 &&
el.victim.count * -1 <= affected.length));
const checkAttacker = el.attacker.outcome === outcome &&
(el.attacker.count === affected.length ||
(el.attacker.count < 0 &&
el.attacker.count * -1 <= affected.length));
const checkCount = el.attacker.count === 0 || el.victim.count === 0;
return (checkVictim || checkAttacker) && checkCount &&
!game.disabledEventIds.player.includes(el.id);
});
if (eventPool.length > 0) {
const pick = eventPool[Math.floor(eventPool.length * Math.random())];
text = pick.message;
const vC = pick.victim.count == 0 ? 0 : affected.length;
const aC = pick.attacker.count == 0 ? 0 : affected.length;
evt = HungryGames.NormalEvent.finalize(
text, affected, vC, aC, outcome, outcome, game,
pick.victim.killer, pick.attacker.killer, pick.victim.weapon,
pick.attacker.weapon);
}
}
if (typeof text !== 'string') {
switch (state) {
case 'dead':
text = messages.get('forcedDeath', locale);
break;
case 'thriving':
text = messages.get('forcedHeal', locale);
break;
case 'wounded':
text = messages.get('forcedWound', locale);
break;
}
}
if (!evt) {
evt = HungryGames.NormalEvent.finalize(
text, affected, affected.length, 0, outcome, 'nothing', game);
}
if (game.currentGame.day.state > 1) {
for (const player of affected) {
if (!HungryGames.Simulator._applyOutcome(
game, player, 0, null, outcome)) {
break;
}
}
game.currentGame.day.events.push(evt);
} else {
game.currentGame.nextDay.events.push(evt);
}
};
game.customEventStore.waitForReady(() => {
for (const outcome in outcomes) {
if (!outcomes[outcome] || outcomes[outcome].length == 0) continue;
const affected = outcomes[outcome];
if (affected.length < 7) {
findEvent(affected, outcome);
} else {
do {
findEvent(affected.splice(0, 7), outcome);
} while (affected.length > 0);
}
}
if (players.length == 0) {
cb('effectPlayerNoPlayerFound');
} else if (players.length < 5) {
const names = players.map((el) => `\`${el}\``).join(', ');
cb(messages.get('forceStateSuccessFew', locale, names, state));
} else {
cb(messages.get(
'forceStateSuccessMany', locale, players.length, state));
}
});
}
/**
* @description Give or take a weapon from a player.
* @public
* @param {string} player The ID of the player to modify.
* @param {string} weapon The weapon ID to give/take.
* @param {?string|HungryGames} [text=null] The message text to show, or
* reference to object storing default events. If no value is given, a random
* message is chosen from `./save/hgMessages.json`.
* @param {number} [count=1] The amount to give to the player. Negative to
* take away.
* @param {boolean} [set=false] Set the amount to `count` instead of
* incrementing.
* @param {Function} [cb] Callback once complete. First parameter is string
* key, following are optional values to fill template.
*/
modifyPlayerWeapon(player, weapon, text = null, count = 1, set = false, cb) {
if (typeof cb !== 'function') cb = function() {};
const game = this;
if (!game.currentGame || !game.currentGame.includedUsers) {
cb('noGameInProgress');
return;
}
player = game.currentGame.includedUsers.find((el) => el.id == player);
if (!player) {
cb('unableToFindPlayer');
return;
}
let current = player.weapons[weapon] || 0;
if (game.currentGame.day.state <= 1) {
for (const evt of game.currentGame.nextDay.events) {
if (evt.consumer === player.id) {
for (const w of evt.consumes) {
if (w.id !== weapon) continue;
current -= w.count;
}
}
const icon = evt.icons.find((icon) => icon.id === player.id);
if (icon) {
const list =
icon.settings.victim ? evt.victim.weapons : evt.attacker.weapons;
for (const w of list) {
if (w.id !== weapon) continue;
current += w.count * 1;
}
}
}
}
const diff = (set ? count - current : count) || 0;
if (!diff) {
cb('modifyPlayerCountNonZero');
return;
}
count = Math.max(0, current + diff);
const defaultEvents =
text.getDefaultEvents && text.getDefaultEvents().get('weapon');
game.customEventStore.waitForReady(() => {
const customWeapons = game.customEventStore.get('weapon');
const weapons = {};
if (player.weapons) {
for (const w in player.weapons) {
if (!player.weapons[w]) continue;
const existing = customWeapons[w] || defaultEvents[w];
if (!existing) {
console.error('Unable to find weapon:', w, 'in guild', game.id);
continue;
}
weapons[w] = new HungryGames.WeaponEvent(
[], existing.consumable, existing.name);
}
}
if (!weapons[weapon]) {
const existing = customWeapons[weapon] || defaultEvents[weapon];
weapons[weapon] = new HungryGames.WeaponEvent(
[], existing && existing.consumable,
existing && existing.name || weapon);
}
const name = weapons[weapon].name;
let evt;
if (text && typeof text === 'object' && game.options.anonForceOutcome) {
const defaultWeapon = defaultEvents[weapon];
const custom = customWeapons[weapon];
weapons.action = HungryGames.WeaponEvent.action;
weapons[weapon] = new HungryGames.WeaponEvent(
[], (custom && custom.consumable) ||
(defaultWeapon && defaultWeapon.consumable),
(custom && custom.name) || (defaultWeapon && defaultWeapon.name));
let eventPool;
if (diff < 0) {
if (defaultWeapon && custom) {
eventPool = defaultWeapon.outcomes.concat(custom.outcomes);
} else if (defaultWeapon) {
eventPool = defaultWeapon.outcomes;
} else if (custom) {
eventPool = custom.outcomes;
} else {
cb('modifyPlayerUnableToFindWeapon');
return;
}
weapons[weapon].outcomes = eventPool.slice(0);
const disabled = game.disabledEventIds.weapon;
eventPool = eventPool.filter((el) => {
return Math.abs(el.victim.count) + Math.abs(el.attacker.count) ===
1 &&
!disabled.includes(el.id);
});
} else {
eventPool = text.getDefaultEvents().getArray('player').concat(
game.customEventStore.getArray('player'));
const disabled = game.disabledEventIds.player;
eventPool = eventPool.filter((el) => {
const aW = el.attacker.weapon;
const aCheck = aW && aW.name === weapon && aW.count > 0;
const vW = el.victim.weapon;
const vCheck = vW && vW.name === weapon && vW.count > 0;
return (aCheck || vCheck) &&
(Math.abs(el.attacker.count) + Math.abs(el.victim.count) ===
1) &&
!disabled.includes(el.id);
});
weapons[weapon].outcomes = eventPool.slice(0);
}
if (eventPool.length > 0) {
const pick = HungryGames.NormalEvent.from(
eventPool[Math.floor(eventPool.length * Math.random())]);
text = pick.message = pick.message.replace(/\{owner\}/g, 'their');
evt = pick.finalize(game, [player]);
}
}
if (text && typeof text === 'object') {
if (diff < 0) {
text = text.messages.get('takeWeapon');
} else {
text = text.messages.get('giveWeapon');
}
}
if (!evt) {
const name = weapons[weapon].name;
text = text.replace(
/\{weapon\}/g, Math.abs(diff) === 1 ? `their ${name}` : `${name}s`);
text = text.replace(
/\[W([^|]*)\|([^\]]*)\]/g, (Math.abs(diff) == 1 ? '$1' : '$2'));
evt = HungryGames.NormalEvent.finalize(
text, [player], 0, 1, 'nothing', 'nothing', game);
}
const nameFormat = game.options.useNicknames ? 'nickname' : 'username';
if (diff < 0) {
const ownerName =
HungryGames.Grammar.formatMultiNames([player], nameFormat);
const firstAttacker = true;
evt.consumer = player.id;
evt.consumes = [{id: weapon, count: -diff}];
evt.subMessage = HungryGames.Simulator.formatWeaponEvent(
evt, player, ownerName, firstAttacker, weapon, weapons, count);
} else {
const aW = evt.attacker.weapons.find((w) => w.id === weapon);
if (!aW) {
evt.attacker.weapons.push({id: weapon, count: diff});
} else {
aW.count = diff;
}
const vW = evt.victim.weapons.find((w) => w.id === weapon);
if (!vW) {
evt.victim.weapons.push({id: weapon, count: diff});
} else {
vW.count = diff;
}
}
if (game.currentGame.day.state > 1) {
if (count <= 0) {
count = 0;
delete player.weapons[weapon];
} else {
player.weapons[weapon] = count;
}
evt.subMessage += HungryGames.Simulator.formatWeaponCounts(
evt, [player], weapons, nameFormat);
// State - 2 = the event index, + 1 is the next index to get shown.
/* let lastIndex = game.currentGame.day.state - 1;
for (let i = game.currentGame.day.events.length - 1; i > lastIndex; i--)
{
if (game.currentGame.day.events[i].icons.find(
(el) => el.id == player.id)) {
lastIndex = i + 1;
break;
}
}
if (lastIndex < game.currentGame.day.events.length) {
game.currentGame.day.events.splice(lastIndex, 0, evt);
} else { */
game.currentGame.day.events.push(evt);
// }
cb('modifyPlayerNowHas', player.name, count, name);
return;
} else {
game.currentGame.nextDay.events.push(evt);
cb('modifyPlayerWillHave', player.name, count, name);
return;
}
});
}
}
module.exports = GuildGame;