`;
}
return buf;
}
/**
* @param {User} user
* @param {string} target
* @param {number} mod
*/
applyLynchModifier(user, target, mod) {
const targetPlayer = this.playerTable[target] || this.dead[target];
if (!targetPlayer) return this.sendUser(user, `|error|${target} is not in the game of mafia.`);
if (target in this.dead && !targetPlayer.restless) return this.sendUser(user, `|error|${target} is not alive or a restless spirit, and therefore cannot lynch.`);
const oldMod = this.lynchModifiers[target];
if (mod === oldMod || ((isNaN(mod) || mod === 1) && oldMod === undefined)) {
if (isNaN(mod) || mod === 1) return this.sendUser(user, `|error|${target} already has no lynch modifier.`);
return this.sendUser(user, `|error|${target} already has a lynch modifier of ${mod}`);
}
const newMod = isNaN(mod) ? 1 : mod;
if (targetPlayer.lynching) {
this.lynches[targetPlayer.lynching].trueCount += oldMod - newMod;
if (this.getHammerValue(targetPlayer.lynching) <= this.lynches[targetPlayer.lynching].trueCount) {
this.sendRoom(`${targetPlayer.lynching} has been lynched due to a modifier change! They have not been eliminated.`);
this.night(true);
}
}
if (newMod === 1) {
delete this.lynchModifiers[target];
return this.sendUser(user, `${targetPlayer.name} has had their lynch modifier removed.`);
} else {
this.lynchModifiers[target] = newMod;
return this.sendUser(user, `${targetPlayer.name} has been given a lynch modifier of ${newMod}`);
}
}
/**
* @param {User} user
* @param {string} target
* @param {number} mod
*/
applyHammerModifier(user, target, mod) {
if (!(target in this.playerTable || target === 'nolynch')) return this.sendUser(user, `|error|${target} is not in the game of mafia.`);
const oldMod = this.hammerModifiers[target];
if (mod === oldMod || ((isNaN(mod) || mod === 0) && oldMod === undefined)) {
if (isNaN(mod) || mod === 0) return this.sendUser(user, `|error|${target} already has no hammer modifier.`);
return this.sendUser(user, `|error|${target} already has a hammer modifier of ${mod}`);
}
const newMod = isNaN(mod) ? 0 : mod;
if (this.lynches[target]) {
if (this.hammerCount + newMod <= this.lynches[target].trueCount) { // do this manually since we havent actually changed the value yet
this.sendRoom(`${target} has been lynched due to a modifier change! They have not been eliminated.`); // make sure these strings are the same
this.night(true);
}
}
if (newMod === 0) {
delete this.hammerModifiers[target];
return this.sendUser(user, `${target} has had their hammer modifier removed.`);
} else {
this.hammerModifiers[target] = newMod;
return this.sendUser(user, `${target} has been given a hammer modifier of ${newMod}`);
}
}
/**
* @param {User} user
*/
clearLynchModifiers(user) {
for (const player of [...Object.keys(this.playerTable), ...Object.keys(this.dead)]) {
if (this.lynchModifiers[player]) this.applyLynchModifier(user, player, 1);
}
}
/**
* @param {User} user
*/
clearHammerModifiers(user) {
for (const player of ['nolynch', ...Object.keys(this.playerTable)]) {
if (this.hammerModifiers[player]) this.applyHammerModifier(user, player, 0);
}
}
/**
* @param {string} userid
*/
getLynchValue(userid) {
const mod = this.lynchModifiers[userid];
return (mod === undefined ? 1 : mod);
}
/**
* @param {string} userid
*/
getHammerValue(userid) {
const mod = this.hammerModifiers[userid];
return (mod === undefined ? this.hammerCount : this.hammerCount + mod);
}
/**
* @return {void}
*/
resetHammer() {
this.setHammer(Math.floor(Object.keys(this.playerTable).length / 2) + 1);
}
/**
* @param {number} count
* @return {void}
*/
setHammer(count) {
this.hammerCount = count;
if (isNaN(count)) {
this.sendRoom(`Hammering has been disabled, and lynches have been reset.`, {declare: true});
} else {
this.sendRoom(`The hammer count has been set at ${this.hammerCount}, and lynches have been reset.`, {declare: true});
}
this.clearLynches();
}
/**
* @param {number} count
* @return {void}
*/
shiftHammer(count) {
this.hammerCount = count;
if (isNaN(count)) {
this.sendRoom(`Hammering has been disabled. Lynches have not been reset.`, {declare: true});
} else {
this.sendRoom(`The hammer count has been shifted to ${this.hammerCount}. Lynches have not been reset.`, {declare: true});
}
let hammered = [];
for (const lynch in this.lynches) {
if (this.lynches[lynch].trueCount >= this.getHammerValue(lynch)) hammered.push(lynch === 'nolynch' ? 'Nobody' : lynch);
}
if (hammered.length) {
this.sendRoom(`${Chat.count(hammered, "players have")} been hammered: ${hammered.join(', ')}. They have not been removed from the game.`, {declare: true});
this.night(true);
}
}
/**
* @return {string?}
*/
getPlurality() {
if (this.hasPlurality) return this.hasPlurality;
if (!Object.keys(this.lynches).length) return null;
let max = 0;
let topLynches = /** @type {string[]} */ ([]);
for (let key in this.lynches) {
if (this.lynches[key].count > max) {
max = this.lynches[key].count;
topLynches = [key];
} else if (this.lynches[key].count === max) {
topLynches.push(key);
}
}
if (topLynches.length <= 1) {
this.hasPlurality = topLynches[0];
return this.hasPlurality;
}
topLynches = topLynches.sort((key1, key2) => {
const l1 = this.lynches[key1];
const l2 = this.lynches[key2];
if (l1.dir !== l2.dir) {
return (l1.dir === 'down' ? -1 : 1);
} else {
if (l1.dir === 'up') return (l1.lastLynch < l2.lastLynch ? -1 : 1);
return (l1.lastLynch > l2.lastLynch ? -1 : 1);
}
});
this.hasPlurality = topLynches[0];
return this.hasPlurality;
}
/**
* @param {MafiaPlayer} player
* @param {string} ability
* @return {void}
*/
eliminate(player, ability = 'kill') {
if (!(player.id in this.playerTable)) return;
if (!this.started) {
// Game has not started, simply kick the player
this.sendRoom(`${player.safeName} was kicked from the game!`, {declare: true});
if (this.hostRequestedSub.includes(player.id)) this.hostRequestedSub.splice(this.hostRequestedSub.indexOf(player.id), 1);
if (this.requestedSub.includes(player.id)) this.requestedSub.splice(this.requestedSub.indexOf(player.id), 1);
delete this.playerTable[player.id];
this.playerCount--;
player.updateHtmlRoom();
player.destroy();
return;
}
this.dead[player.id] = player;
let msg = `${player.safeName}`;
switch (ability) {
case 'treestump':
this.dead[player.id].treestump = true;
msg += ` has been treestumped`;
break;
case 'spirit':
this.dead[player.id].restless = true;
msg += ` became a restless spirit`;
break;
case 'spiritstump':
this.dead[player.id].treestump = true;
this.dead[player.id].restless = true;
msg += ` became a restless treestump`;
break;
case 'kick':
msg += ` was kicked from the game`;
break;
default:
msg += ` was eliminated`;
}
if (player.lynching) this.unlynch(player.id, true);
this.sendRoom(`${msg}! ${!this.noReveal && toID(ability) === 'kill' ? `${player.safeName}'s role was ${player.getRole()}.` : ''}`, {declare: true});
const targetRole = player.role;
if (targetRole) {
for (const [roleIndex, role] of this.roles.entries()) {
if (role.id === targetRole.id) {
this.roles.splice(roleIndex, 1);
break;
}
}
}
this.clearLynches(player.id);
delete this.playerTable[player.id];
let subIndex = this.requestedSub.indexOf(player.id);
if (subIndex !== -1) this.requestedSub.splice(subIndex, 1);
subIndex = this.hostRequestedSub.indexOf(player.id);
if (subIndex !== -1) this.hostRequestedSub.splice(subIndex, 1);
this.playerCount--;
this.updateRoleString();
this.updatePlayers();
player.updateHtmlRoom();
}
/**
* @param {User} user
* @param {string} toRevive
* @param {boolean} force
* @return {void}
*/
revive(user, toRevive, force = false) {
if (this.phase === 'IDEApicking') return user.sendTo(this.room, `|error|You cannot add or remove players while IDEA roles are being picked.`);
if (toRevive in this.playerTable) {
user.sendTo(this.room, `|error|The user ${toRevive} is already a living player.`);
return;
}
if (toRevive in this.dead) {
const deadPlayer = this.dead[toRevive];
if (deadPlayer.treestump) deadPlayer.treestump = false;
if (deadPlayer.restless) deadPlayer.restless = false;
this.sendRoom(`${deadPlayer.safeName} was revived!`, {declare: true});
this.playerTable[deadPlayer.id] = deadPlayer;
const targetRole = deadPlayer.role;
if (targetRole) {
this.roles.push(targetRole);
} else {
// Should never happen
deadPlayer.role = {
name: `Unknown`,
safeName: `Unknown`,
id: `unknown`,
alignment: 'solo',
image: '',
memo: [`You were revived, but had no role. Please let a Mafia Room Owner know this happened. To learn about your role, PM the host (${this.host}).`],
};
this.roles.push(deadPlayer.role);
}
this.roles.sort((a, b) => a.alignment.localeCompare(b.alignment) || a.name.localeCompare(b.name));
delete this.dead[deadPlayer.id];
} else {
const targetUser = Users.get(toRevive);
if (!targetUser) return;
const canJoin = this.canJoin(targetUser, false, force);
if (canJoin) {
user.sendTo(this.room, `|error|${canJoin}`);
return;
}
let player = this.makePlayer(targetUser);
if (this.started) {
player.role = {
name: `Unknown`,
safeName: `Unknown`,
id: `unknown`,
alignment: 'solo',
image: '',
memo: [`You were added to the game after it had started. To learn about your role, PM the host (${this.host}).`],
};
this.roles.push(player.role);
} else {
this.originalRoles = [];
this.originalRoleString = '';
this.roles = [];
this.roleString = '';
}
if (this.subs.includes(targetUser.id)) this.subs.splice(this.subs.indexOf(targetUser.id), 1);
this.played.push(targetUser.id);
this.playerTable[targetUser.id] = player;
this.sendRoom(`${Chat.escapeHTML(targetUser.name)} has been added to the game by ${Chat.escapeHTML(user.name)}!`, {declare: true});
}
this.playerCount++;
this.updateRoleString();
this.updatePlayers();
}
/**
* @param {number} minutes
* @param {boolean} silent
*/
setDeadline(minutes, silent = false) {
if (isNaN(minutes)) return;
if (!minutes) {
if (!this.timer) return;
clearTimeout(this.timer);
this.timer = null;
this.dlAt = 0;
if (!silent) this.sendRoom(`The deadline has been cleared.`, {strong: true});
return;
}
if (minutes < 1 || minutes > 20) return;
if (this.timer) clearTimeout(this.timer);
this.dlAt = Date.now() + (minutes * 60000);
if (minutes > 3) {
this.timer = setTimeout(() => {
this.sendRoom(`3 minutes left!`, {strong: true});
this.timer = setTimeout(() => {
this.sendRoom(`1 minute left!`, {strong: true});
this.timer = setTimeout(() => {
this.sendRoom(`Time is up!`, {strong: true});
this.night();
}, 60000);
}, 2 * 60000);
}, (minutes - 3) * 60000);
} else if (minutes > 1) {
this.timer = setTimeout(() => {
this.sendRoom(`1 minute left!`, {strong: true});
this.timer = setTimeout(() => {
this.sendRoom(`Time is up!`, {strong: true});
if (this.phase === 'day') this.night();
}, 60000);
}, (minutes - 1) * 60000);
} else {
this.timer = setTimeout(() => {
this.sendRoom(`Time is up!`, {strong: true});
if (this.phase === 'day') this.night();
}, minutes * 60000);
}
this.sendRoom(`The deadline has been set for ${minutes} minute${minutes === 1 ? '' : 's'}.`, {strong: true});
}
/**
* @param {string} player
* @param {string} replacement
* @return {void}
*/
sub(player, replacement) {
let oldPlayer = this.playerTable[player];
if (!oldPlayer) return; // should never happen
const newUser = Users.get(replacement);
if (!newUser) return; // should never happen
let newPlayer = this.makePlayer(newUser);
newPlayer.role = oldPlayer.role;
newPlayer.IDEA = oldPlayer.IDEA;
if (oldPlayer.lynching) {
// Dont change plurality
let lynch = this.lynches[oldPlayer.lynching];
lynch.lynchers.splice(lynch.lynchers.indexOf(oldPlayer.id), 1);
lynch.lynchers.push(newPlayer.id);
newPlayer.lynching = oldPlayer.lynching;
oldPlayer.lynching = '';
}
this.playerTable[newPlayer.id] = newPlayer;
// Transfer lynches on the old player to the new one
if (this.lynches[oldPlayer.id]) {
this.lynches[newPlayer.id] = this.lynches[oldPlayer.id];
delete this.lynches[oldPlayer.id];
for (let p in this.playerTable) {
if (this.playerTable[p].lynching === oldPlayer.id) this.playerTable[p].lynching = newPlayer.id;
}
for (let p in this.dead) {
if (this.dead[p].restless && this.dead[p].lynching === oldPlayer.id) this.dead[p].lynching = newPlayer.id;
}
}
if (newUser && newUser.connected) {
for (const conn of newUser.connections) {
Chat.resolvePage(`view-mafia-${this.room.roomid}`, newUser, conn);
}
newUser.send(`>${this.room.roomid}\n|notify|You have been substituted in the mafia game for ${oldPlayer.safeName}.`);
}
if (this.started) this.played.push(newPlayer.id);
this.sendRoom(`${oldPlayer.safeName} has been subbed out. ${newPlayer.safeName} has joined the game.`, {declare: true});
this.updatePlayers();
delete this.playerTable[oldPlayer.id];
oldPlayer.destroy();
if (this.room.roomid === 'mafia' && this.started) {
const month = new Date().toLocaleString("en-us", {month: "numeric", year: "numeric"});
if (!logs.leavers[month]) logs.leavers[month] = {};
if (!logs.leavers[month][player]) logs.leavers[month][player] = 0;
logs.leavers[month][player]++;
writeFile(LOGS_FILE, logs);
}
}
/**
* @param {string?} userid
* @return {void}
*/
nextSub(userid = null) {
if (!this.subs.length || (!this.hostRequestedSub.length && ((!this.requestedSub.length || !this.autoSub)) && !userid)) return;
const nextSub = this.subs.shift();
if (!nextSub) return;
const sub = Users.get(nextSub, true);
if (!sub || !sub.connected || !sub.named || !this.room.users[sub.id]) return; // should never happen, just to be safe
const toSubOut = userid || this.hostRequestedSub.shift() || this.requestedSub.shift();
if (!toSubOut) {
// Should never happen
this.subs.unshift(nextSub);
return;
}
if (this.hostRequestedSub.includes(toSubOut)) this.hostRequestedSub.splice(this.hostRequestedSub.indexOf(toSubOut), 1);
if (this.requestedSub.includes(toSubOut)) this.requestedSub.splice(this.requestedSub.indexOf(toSubOut), 1);
this.sub(toSubOut, sub.id);
}
/**
* @param {User} user
* @param {number} choices
* @param {string[]} picks
* @param {string} rolesString
*/
customIdeaInit(user, choices, picks, rolesString) {
this.originalRoles = [];
this.originalRoleString = '';
this.roles = [];
this.roleString = '';
const roles = Chat.stripHTML(rolesString);
let roleList = roles.split('\n');
if (roleList.length === 1) {
roleList = roles.split(',').map(r => r.trim());
}
this.IDEA.data = {
name: `${this.host}'s IDEA`, // already escaped
untrusted: true,
roles: roleList,
picks,
choices,
};
return this.ideaDistributeRoles(user);
}
/**
*
* @param {User} user
* @param {string} moduleName
*/
ideaInit(user, moduleName) {
this.originalRoles = [];
this.originalRoleString = '';
this.roles = [];
this.roleString = '';
this.IDEA.data = MafiaData.IDEAs[moduleName];
if (typeof this.IDEA.data === 'string') this.IDEA.data = MafiaData.IDEAs[this.IDEA.data];
if (!this.IDEA.data) return user.sendTo(this.room, `|error|${moduleName} is not a valid IDEA.`);
if (typeof this.IDEA.data !== 'object') return this.sendRoom(`Invalid alias for IDEA ${moduleName}. Please report this to a mod.`);
return this.ideaDistributeRoles(user);
}
/**
*
* @param {User} user
*/
ideaDistributeRoles(user) {
if (!this.IDEA.data) return user.sendTo(this.room, `|error|No IDEA module loaded`);
if (this.phase !== 'locked' && this.phase !== 'IDEAlocked') return user.sendTo(this.room, `|error|The game must be in a locked state to distribute IDEA roles.`);
const neededRoles = this.IDEA.data.choices * this.playerCount;
if (neededRoles > this.IDEA.data.roles.length) return user.sendTo(this.room, `|error|Not enough roles in the IDEA module.`);
let roles = [];
let selectedIndexes = [];
for (let i = 0; i < neededRoles; i++) {
let randomIndex;
do {
randomIndex = Math.floor(Math.random() * this.IDEA.data.roles.length);
} while (selectedIndexes.indexOf(randomIndex) !== -1);
roles.push(this.IDEA.data.roles[randomIndex]);
selectedIndexes.push(randomIndex);
}
Dex.shuffle(roles);
this.IDEA.waitingPick = [];
for (const p in this.playerTable) {
const player = this.playerTable[p];
player.role = null;
player.IDEA = {
choices: roles.splice(0, this.IDEA.data.choices),
originalChoices: [], // MAKE SURE TO SET THIS
picks: {},
};
player.IDEA.originalChoices = player.IDEA.choices.slice();
for (const pick of this.IDEA.data.picks) {
player.IDEA.picks[pick] = null;
this.IDEA.waitingPick.push(p);
}
const u = Users.get(p);
// @ts-ignore guaranteed at this point
if (u && u.connected) u.send(`>${this.room.roomid}\n|notify|Pick your role in the IDEA module.`);
}
this.phase = 'IDEApicking';
this.updatePlayers();
this.sendRoom(`${this.IDEA.data.name} roles have been distributed. You will have ${IDEA_TIMER / 1000} seconds to make your picks.`, {declare: true});
this.IDEA.timer = setTimeout(() => { this.ideaFinalizePicks(); }, IDEA_TIMER);
return ``;
}
/**
*
* @param {User} user
* @param {string[]} selection
*/
ideaPick(user, selection) {
let buf = '';
if (this.phase !== 'IDEApicking') return 'The game is not in the IDEA picking phase.';
if (!this.IDEA || !this.IDEA.data) return this.sendRoom(`Trying to pick an IDEA role with no module running, target: ${JSON.stringify(selection)}. Please report this to a mod.`);
const player = this.playerTable[user.id];
if (!player.IDEA) return this.sendRoom(`Trying to pick an IDEA role with no player IDEA object, user: ${user.id}. Please report this to a mod.`);
selection = selection.map(toID);
if (selection.length === 1 && this.IDEA.data.picks.length === 1) selection = [this.IDEA.data.picks[0], selection[0]];
if (selection.length !== 2) return user.sendTo(this.room, `|error|Invalid selection.`);
// input is formatted as ['selection', 'role']
// eg: ['role', 'bloodhound']
// ['alignment', 'alien']
// ['selection', ''] deselects
if (selection[1]) {
const roleIndex = player.IDEA.choices.map(toID).indexOf(/** @type {ID} */(selection[1]));
if (roleIndex === -1) return user.sendTo(this.room, `|error|${selection[1]} is not an available role, perhaps it is already selected?`);
selection[1] = player.IDEA.choices.splice(roleIndex, 1)[0];
} else {
selection[1] = '';
}
const selected = player.IDEA.picks[selection[0]];
if (selected) {
buf += `You have deselected ${selected}. `;
player.IDEA.choices.push(selected);
}
if (player.IDEA.picks[selection[0]] && !selection[1]) {
this.IDEA.waitingPick.push(player.id);
} else if (!player.IDEA.picks[selection[0]] && selection[1]) {
this.IDEA.waitingPick.splice(this.IDEA.waitingPick.indexOf(player.id), 1);
}
player.IDEA.picks[selection[0]] = selection[1];
if (selection[1]) buf += `You have selected ${selection[0]}: ${selection[1]}.`;
player.updateHtmlRoom();
if (!this.IDEA.waitingPick.length) {
if (this.IDEA.timer) clearTimeout(this.IDEA.timer);
this.ideaFinalizePicks();
return;
}
return user.sendTo(this.room, buf);
}
ideaFinalizePicks() {
if (!this.IDEA || !this.IDEA.data) return this.sendRoom(`Tried to finalize IDEA picks with no IDEA module running, please report this to a mod.`);
let randed = [];
for (const p in this.playerTable) {
const player = this.playerTable[p];
if (!player.IDEA) return this.sendRoom(`Trying to pick an IDEA role with no player IDEA object, user: ${player.id}. Please report this to a mod.`);
let randPicked = false;
let role = [];
for (const choice of this.IDEA.data.picks) {
if (!player.IDEA.picks[choice]) {
randPicked = true;
let randomChoice = player.IDEA.choices.shift();
if (randomChoice) {
player.IDEA.picks[choice] = randomChoice;
} else {
throw new Error(`No roles left to randomly assign from IDEA module choices.`);
}
this.sendUser(player.id, `You were randomly assigned ${choice}: ${randomChoice}`);
}
role.push(`${choice}: ${player.IDEA.picks[choice]}`);
}
if (randPicked) randed.push(p);
// if there's only one option, it's their role, parse it properly
let roleName = '';
if (this.IDEA.data.picks.length === 1) {
let pick = player.IDEA.picks[this.IDEA.data.picks[0]];
if (!pick) throw new Error('Pick not found when parsing role selected in IDEA module.');
const role = MafiaTracker.parseRole(pick);
player.role = role.role;
if (role.problems.length && !this.IDEA.data.untrusted) this.sendRoom(`Problems found when parsing IDEA role ${player.IDEA.picks[this.IDEA.data.picks[0]]}. Please report this to a mod.`);
} else {
roleName = role.join('; ');
player.role = {
name: roleName,
safeName: Chat.escapeHTML(roleName),
id: toID(roleName),
alignment: 'solo',
memo: [`(Your role was set from an IDEA.)`],
image: '',
};
// hardcoding this because it makes GestI so much nicer
if (!this.IDEA.data.untrusted) {
for (const pick of role) {
if (pick.substr(0, 10) === 'alignment:') {
const parsedRole = MafiaTracker.parseRole(pick.substr(9));
if (parsedRole.problems.length) this.sendRoom(`Problems found when parsing IDEA role ${pick}. Please report this to a mod.`);
player.role.alignment = parsedRole.role.alignment;
}
}
}
}
}
this.IDEA.discardsHtml = `Discards: `;
for (const p of Object.keys(this.playerTable).sort()) {
const IDEA = this.playerTable[p].IDEA;
if (!IDEA) return this.sendRoom(`No IDEA data for player ${p} when finalising IDEAs. Please report this to a mod.`);
this.IDEA.discardsHtml += `${this.playerTable[p].safeName}: ${IDEA.choices.join(', ')} `;
}
this.phase = 'IDEAlocked';
if (randed.length) this.sendRoom(`${randed.join(', ')} did not pick a role in time and were randomly assigned one.`, {declare: true});
this.sendRoom(`IDEA picks are locked!`, {declare: true});
this.sendRoom(`To start, use /mafia start, or to reroll use /mafia ideareroll`);
this.updatePlayers();
}
/**
* @return {void}
*/
sendPlayerList() {
this.room.add(`|c:|${(Math.floor(Date.now() / 1000))}|~|**Players (${this.playerCount})**: ${Object.keys(this.playerTable).map(p => this.playerTable[p].name).join(', ')}`).update();
}
/**
* @return {void}
*/
updatePlayers() {
for (const p in this.playerTable) {
this.playerTable[p].updateHtmlRoom();
}
for (const p in this.dead) {
if (this.dead[p].restless || this.dead[p].treestump) this.dead[p].updateHtmlRoom();
}
// Now do the host
this.updateHost();
}
updatePlayersLynches() {
for (const p in this.playerTable) {
this.playerTable[p].updateHtmlLynches();
}
for (const p in this.dead) {
if (this.dead[p].restless || this.dead[p].treestump) this.dead[p].updateHtmlLynches();
}
}
/**
* @return {void}
*/
updateHost() {
for (const hostid of [...this.cohosts, this.hostid]) {
const host = Users.get(hostid);
if (!host || !host.connected) return;
for (const conn of host.connections) {
Chat.resolvePage(`view-mafia-${this.room.roomid}`, host, conn);
}
}
}
/**
* @return {void}
*/
updateRoleString() {
this.roleString = this.roles.slice().map(r => `${r.safeName}`).join(', ');
}
/**
* @param {string} message
* @param {{uhtml?: boolean, declare?: boolean, strong?: boolean, timestamp?: boolean}} options
* @return {void}
*/
sendRoom(message, options = {}) {
if (options.uhtml) return this.room.add(`|uhtml|mafia|${message}`).update();
if (options.declare) return this.room.add(`|raw|
${message}
`).update();
if (options.strong) return this.room.add(`|raw|${message}`).update();
if (options.timestamp) return this.room.add(`|c:|${(Math.floor(Date.now() / 1000))}|~|${message}`).update();
return this.room.add(message).update();
}
/**
* @return {string}
*/
roomWindow() {
if (this.ended) return `
The game of ${this.title} has ended.
`;
let output = `
`;
if (this.phase === 'signups') {
output += `
A game of ${this.title} was created
`;
} else {
output += `
A game of ${this.title} is in progress.
`;
}
output += `
`;
return output;
}
/**
* @param {User} user
* @param {boolean} self
* @param {boolean} force
*/
canJoin(user, self = false, force = false) {
if (!user || !user.connected) return `User not found.`;
const targetString = self ? `You are` : `${user.id} is`;
if (!this.room.users[user.id]) return `${targetString} not in the room.`;
if (this.playerTable[user.id]) return `${targetString} already in the game.`;
if (this.hostid === user.id) return `${targetString} the host.`;
if (this.cohosts.includes(user.id)) return `${targetString} a cohost.`;
if (!force) {
for (const alt of user.getAltUsers(true)) {
if (this.playerTable[alt.id] || this.played.includes(alt.id)) return `${self ? `You already have` : `${user.id} already has`} an alt in the game.`;
if (this.hostid === alt.id || this.cohosts.includes(alt.id)) return `${self ? `You have` : `${user.id} has`} an alt as a game host.`;
}
}
return false;
}
/**
* @param {User | string | null} user
* @param {string} message
*/
sendUser(user, message) {
const userObject = (typeof user === 'string' ? Users.get(user) : user);
if (!userObject || !userObject.connected) return;
userObject.sendTo(this.room, message);
}
/**
* @param {User} user
* @param {boolean | 'hammer'} setting
*/
setSelfLynch(user, setting) {
const from = this.selfEnabled;
if (from === setting) return user.sendTo(this.room, `|error|Selflynching is already ${setting ? `set to Self${setting === 'hammer' ? 'hammering' : 'lynching'}` : 'disabled'}.`);
if (from) {
this.sendRoom(`Self${from === 'hammer' ? 'hammering' : 'lynching'} has been ${setting ? `changed to Self${setting === 'hammer' ? 'hammering' : 'lynching'}` : 'disabled'}.`, {declare: true});
} else {
this.sendRoom(`Self${setting === 'hammer' ? 'hammering' : 'lynching'} has been ${setting ? 'enabled' : 'disabled'}.`, {declare: true});
}
this.selfEnabled = setting;
if (!setting) {
for (const player of Object.values(this.playerTable)) {
if (player.lynching === player.id) this.unlynch(player.id, true);
}
}
this.updatePlayers();
}
/**
* @param {User} user
* @param {boolean} setting
*/
setNoLynch(user, setting) {
if (this.enableNL === setting) return user.sendTo(this.room, `|error|No Lynch is already ${setting ? 'enabled' : 'disabled'}.`);
this.enableNL = setting;
this.sendRoom(`No Lynch has been ${setting ? 'enabled' : 'disabled'}.`, {declare: true});
if (!setting) this.clearLynches('nolynch');
this.updatePlayers();
}
/**
* @param {string} target
*/
clearLynches(target = '') {
if (target) delete this.lynches[target];
if (!target) this.lynches = Object.create(null);
for (const player of Object.values(this.playerTable)) {
if (this.forcelynch) {
if (!target || (player.lynching === target)) {
player.lynching = player.id;
this.lynches[player.id] = {count: 1, trueCount: this.getLynchValue(player.id), lastLynch: Date.now(), dir: 'up', lynchers: [player.id]};
}
} else {
if (!target || (player.lynching === target)) player.lynching = '';
}
}
for (const player of Object.values(this.dead)) {
if (player.restless && (!target || player.lynching === target)) player.lynching = '';
}
this.hasPlurality = null;
}
/**
* @param {string} message
* @param {User} user
* @return {(string | false)}
*/
onChatMessage(message, user) {
const subIndex = this.hostRequestedSub.indexOf(user.id);
if (subIndex !== -1) {
this.hostRequestedSub.splice(subIndex, 1);
for (const hostid of [...this.cohosts, this.hostid]) {
this.sendUser(hostid, `${user.id} has spoken and been removed from the host sublist.`);
}
}
// Silenced check bypasses staff
const player = this.playerTable[user.id] || this.dead[user.id];
if (player && player.silenced) {
return `You are silenced and cannot speak.${user.can('mute', null, this.room) ? " You can remove this with /mafia unsilence." : ''}`;
}
if (user.isStaff || (this.room.auth && this.room.auth[user.id] && this.room.auth[user.id] !== '+') || this.hostid === user.id || this.cohosts.includes(user.id) || !this.started) return false;
if (!this.playerTable[user.id] && (!this.dead[user.id] || !this.dead[user.id].treestump)) return `You cannot talk while a game of ${this.title} is going on.`;
if (this.phase === 'night') return `You cannot talk at night.`;
return false;
}
/**
* @param {User} user
* @return {void}
*/
onConnect(user) {
user.sendTo(this.room, `|uhtml|mafia|${this.roomWindow()}`);
}
/**
* @param {User} user
* @return {void}
*/
onJoin(user) {
if (user.id in this.playerTable) {
return this.playerTable[user.id].updateHtmlRoom();
}
if (user.id === this.hostid) return this.updateHost();
}
/**
* @param {User} user
* @param {ID} oldUserid
*/
onLeave(user, oldUserid) {
const userid = oldUserid || user.id;
if (this.subs.includes(userid)) this.subs.splice(this.subs.indexOf(userid), 1);
}
/**
* @param {User} user
* @return {void}
*/
removeBannedUser(user) {
// Player was banned, attempt to sub now
// If we can't sub now, make subbing them out the top priority
if (!(user.id in this.playerTable)) return;
this.requestedSub.unshift(user.id);
this.nextSub();
}
/**
* @param {User} user
* @return {void}
*/
forfeit(user) {
// Add the player to the sub list.
if (!(user.id in this.playerTable)) return;
this.requestedSub.push(user.id);
this.nextSub();
}
/**
* @return {void}
*/
end() {
this.ended = true;
this.sendRoom(this.roomWindow(), {uhtml: true});
this.updatePlayers();
if (this.room.roomid === 'mafia' && this.started) {
// Intead of using this.played, which shows players who have subbed out as well
// We check who played through to the end when recording playlogs
const played = Object.keys(this.playerTable).concat(Object.keys(this.dead));
const month = new Date().toLocaleString("en-us", {month: "numeric", year: "numeric"});
if (!logs.plays[month]) logs.plays[month] = {};
for (const player of played) {
if (!logs.plays[month][player]) logs.plays[month][player] = 0;
logs.plays[month][player]++;
}
if (!logs.hosts[month]) logs.hosts[month] = {};
for (const hostid of [...this.cohosts, this.hostid]) {
if (!logs.hosts[month][hostid]) logs.hosts[month][hostid] = 0;
logs.hosts[month][hostid]++;
}
writeFile(LOGS_FILE, logs);
}
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
this.destroy();
}
destroy() {
// Slightly modified to handle dead players
if (this.timer) clearTimeout(this.timer);
if (this.IDEA.timer) clearTimeout(this.IDEA.timer);
this.room.game = null;
this.room = /** @type {any} */ (null);
for (let i in this.playerTable) {
this.playerTable[i].destroy();
}
for (let i in this.dead) {
this.dead[i].destroy();
}
}
}
/** @type {PageTable} */
const pages = {
mafia(query, user) {
if (!user.named) return Rooms.RETRY_AFTER_LOGIN;
if (!query.length) return this.close();
let roomid = query.shift();
if (roomid === 'groupchat') roomid += `-${query.shift()}-${query.shift()}`;
const room = /** @type {ChatRoom} */ (Rooms.get(roomid));
if (!room || !room.users[user.id] || !room.game || room.game.gameid !== 'mafia' || room.game.ended) return this.close();
const game = /** @type {MafiaTracker} */ (room.game);
const isPlayer = user.id in game.playerTable;
const isHost = user.id === game.hostid || game.cohosts.includes(user.id);
this.title = game.title;
let buf = `
Please note that anything inside (parentheses) is ignored by the role parser.
`;
buf += `
If you have roles that have conflicting alignments or base roles, you can use /mafia forcesetroles [comma seperated list of roles] to forcibly set the roles.
`;
buf += `
Please note that you will have to PM all the players their alignment, partners (if any), and other information about their role because the server will not provide it.
`;
buf += `
`;
buf += `
Players who will be subbed unless they talk: ${game.hostRequestedSub.join(', ')}
`;
buf += `
Players who are requesting a sub: ${game.requestedSub.join(', ')}
`;
}
buf += `
Sub List: ${game.subs.join(', ')}
`;
if (!isHost) {
if (game.phase === 'signups') {
if (isPlayer) {
buf += ``;
} else {
buf += ``;
}
} else if ((!isPlayer && game.subs.includes(user.id)) || (isPlayer && !game.requestedSub.includes(user.id))) {
buf += `
${isPlayer ? 'Request to be subbed out' : 'Cancel sub request'}`;
buf += `
`;
} else {
buf += `
${isPlayer ? 'Cancel sub request' : 'Join the game as a sub'}`;
buf += `
`;
const section = ladder.section;
if (!logs[section][month] || !Object.keys(logs[section][month]).length) {
buf += `${ladder.title} for ${date.toLocaleString("en-us", {month: 'long'})} ${date.getFullYear()} not found.
`;
return buf;
}
const keys = Object.keys(logs[section][month]).sort((keyA, keyB) => {
const a = logs[section][month][keyA];
const b = logs[section][month][keyB];
return b - a;
});
buf += `
Mafia ${ladder.title} for ${date.toLocaleString("en-us", {month: 'long'})} ${date.getFullYear()}
`;
buf += `
User
${ladder.type}
`;
for (const key of keys) {
buf += `
${key}
${logs[section][month][key]}
`;
}
return buf + `
`;
},
};
/** @type {ChatCommands} */
const commands = {
mafia: {
''(target, room, user) {
const game = /** @type {MafiaTracker?} */ (room.game);
if (game && game.gameid === 'mafia') {
if (!this.runBroadcast()) return;
return this.sendReply(`|html|${game.roomWindow()}`);
}
return this.parse('/help mafia');
},
forcehost: 'host',
nexthost: 'host',
host(target, room, user, connection, cmd) {
if (room.mafiaDisabled) return this.errorReply(`Mafia is disabled for this room.`);
if (!this.canTalk()) return;
if (!room || room.type !== 'chat') return this.errorReply(`This command is only meant to be used in chat rooms.`);
if (room.game) return this.errorReply(`There is already a game of ${room.game.title} in progress in this room.`);
if (!user.can('broadcast', null, room)) return this.errorReply(`/mafia ${cmd} - Access denied.`);
let nextHost = false;
if (room.roomid === 'mafia') {
if (cmd === 'nexthost') {
nextHost = true;
if (!hostQueue.length) return this.errorReply(`Nobody is on the host queue.`);
let skipped = [];
do {
// @ts-ignore guaranteed
this.splitTarget(hostQueue.shift(), true);
if (!this.targetUser || !this.targetUser.connected || !room.users[this.targetUser.id] || isHostBanned(this.targetUser.id)) {
skipped.push(this.targetUsername);
this.targetUser = null;
}
} while (!this.targetUser && hostQueue.length);
if (skipped.length) this.sendReply(`${skipped.join(', ')} ${Chat.plural(skipped.length, 'were', 'was')} not online, not in the room, or are host banned and were removed from the host queue.`);
if (!this.targetUser) return this.errorReply(`Nobody on the host queue could be hosted.`);
} else {
if (cmd !== 'forcehost' && hostQueue.length && toID(target) !== hostQueue[0]) return this.errorReply(`${target} is not next on the host queue. To host them now anyways, use /mafia forcehost ${target}`);
this.splitTarget(target, true);
}
} else {
this.splitTarget(target, true);
}
if (!this.targetUser || !this.targetUser.connected) return this.errorReply(`The user "${this.targetUsername}" was not found.`);
if (!nextHost && this.targetUser.id !== user.id && !this.can('mute', null, room)) return false;
if (!room.users[this.targetUser.id]) return this.errorReply(`${this.targetUser.name} is not in this room, and cannot be hosted.`);
if (room.roomid === 'mafia' && isHostBanned(this.targetUser.id)) return this.errorReply(`${this.targetUser.name} is banned from hosting games.`);
let targetUser = this.targetUser;
room.game = new MafiaTracker(room, targetUser);
for (const conn of targetUser.connections) {
Chat.resolvePage(`view-mafia-${room.roomid}`, targetUser, conn);
}
room.addByUser(user, `${targetUser.name} was appointed the mafia host by ${user.name}.`);
if (room.roomid === 'mafia') {
const queueIndex = hostQueue.indexOf(targetUser.id);
if (queueIndex > -1) hostQueue.splice(queueIndex, 1);
room.add(`|c:|${(Math.floor(Date.now() / 1000))}|~|**Mafiasignup!**`).update();
}
this.modlog('MAFIAHOST', targetUser, null, {noalts: true, noip: true});
},
hosthelp: [
`/mafia host [user] - Create a game of Mafia with [user] as the host. Requires + % @ # & ~, voices can only host themselves`,
],
q: 'queue',
queue(target, room, user) {
if (room.mafiaDisabled) return this.errorReply(`Mafia is disabled for this room.`);
if (room.roomid !== 'mafia') return this.errorReply(`This command can only be used in the Mafia room.`);
const args = target.split(',').map(toID);
if (['forceadd', 'add', 'remove', 'del', 'delete'].includes(args[0])) {
const permission = (user.id === args[1]) ? 'broadcast' : 'mute';
if (['forceadd', 'add'].includes(args[0]) && !this.can(permission, null, room)) return;
if (['remove', 'del', 'delete'].includes(args[0]) && user.id !== args[1] && !this.can('mute', null, room)) return;
} else {
if (!this.runBroadcast()) return false;
}
switch (args[0]) {
case 'forceadd':
case 'add':
if (!this.canTalk()) return;
let targetUser = Users.get(args[1]);
if ((!targetUser || !targetUser.connected) && args[0] !== 'forceadd') return this.errorReply(`User ${args[1]} not found. To forcefully add the user to the queue, use /mafia queue forceadd, ${args[1]}`);
if (hostQueue.includes(args[1])) return this.errorReply(`User ${args[1]} is already on the host queue.`);
if (isHostBanned(args[1])) return this.errorReply(`User ${args[1]} is banned from hosting games.`);
hostQueue.push(args[1]);
room.add(`User ${args[1]} has been added to the host queue by ${user.name}.`).update();
break;
case 'del':
case 'delete':
case 'remove':
let index = hostQueue.indexOf(args[1]);
if (index === -1) return this.errorReply(`User ${args[1]} is not on the host queue.`);
hostQueue.splice(index, 1);
room.add(`User ${args[1]} has been removed from the host queue by ${user.name}.`).update();
break;
case '':
case 'show':
case 'view':
this.sendReplyBox(`Host Queue: ${hostQueue.join(', ')}`);
break;
default:
this.parse('/help mafia queue');
}
},
queuehelp: [
`/mafia queue - Shows the upcoming users who are going to host.`,
`/mafia queue add, (user) - Adds the user to the hosting queue. Requires: + % @ # & ~`,
`/mafia queue remove, (user) - Removes the user from the hosting queue. Requires: + % @ # & ~`,
],
qadd: 'queueadd',
qforceadd: 'queueadd',
queueforceadd: 'queueadd',
queueadd(target, room, user, connection, cmd) {
this.parse(`/mafia queue ${cmd.includes('force') ? `forceadd` : `add`}, ${target}`);
},
qdel: 'queueremove',
qdelete: 'queueremove',
qremove: 'queueremove',
queueremove(target, room, user) {
this.parse(`/mafia queue remove, ${target}`);
},
'!mafjoin': true,
// Typescript doesn't like "join" as the command name for some reason, so this is a hack to get around that.
join: 'mafjoin',
mafjoin(target, room, user) {
let targetRoom = /** @type {ChatRoom} */ (Rooms.get(target));
if (!targetRoom || targetRoom.type !== 'chat' || !targetRoom.users[user.id]) {
if (!room || room.type !== 'chat') return this.errorReply(`This command is only meant to be used in chat rooms.`);
targetRoom = room;
}
if (!targetRoom.game || targetRoom.game.gameid !== 'mafia') return user.sendTo(targetRoom, `|error|There is no game of mafia running in this room.`);
const game = /** @type {MafiaTracker} */ (targetRoom.game);
if (!this.canTalk(null, targetRoom)) return;
game.join(user);
},
joinhelp: [`/mafia join - Join the game.`],
'!leave': true,
leave(target, room, user) {
let targetRoom /** @type {ChatRoom?} */ = (Rooms.get(target));
if (!targetRoom || targetRoom.type !== 'chat' || !targetRoom.users[user.id]) {
if (!room || room.type !== 'chat') return this.errorReply(`This command is only meant to be used in chat rooms.`);
targetRoom = room;
}
if (!targetRoom.game || targetRoom.game.gameid !== 'mafia') return user.sendTo(targetRoom, `|error|There is no game of mafia running in this room.`);
const game = /** @type {MafiaTracker} */ (targetRoom.game);
game.leave(user);
},
leavehelp: [`/mafia leave - Leave the game. Can only be done while signups are open.`],
playercap(target, room, user) {
if (!room || !room.game || room.game.gameid !== 'mafia') return this.errorReply(`There is no game of mafia running in this room.`);
const game = /** @type {MafiaTracker} */ (room.game);
if (game.hostid !== user.id && !game.cohosts.includes(user.id) && !this.can('mute', null, room)) return;
if (game.phase !== 'signups') return this.errorReply(`Signups are already closed.`);
if (toID(target) === 'none') target = '20';
const num = parseInt(target);
if (isNaN(num) || num > 20 || num < 2) return this.parse('/help mafia playercap');
if (num < game.playerCount) return this.errorReply(`Player cap has to be equal or more than the amount of players in game.`);
if (num === game.playerCap) return this.errorReply(`Player cap is already set at ${game.playerCap}.`);
game.playerCap = num;
game.sendRoom(`Player cap has been set to ${game.playerCap}`, {declare: true});
},
playercaphelp: [`/mafia playercap [cap|none]- Limit the number of players being able to join the game. Player cap cannot be more than 20 or less than 2. Requires: host % @ # & ~`],
'!close': true,
close(target, room, user) {
let targetRoom /** @type {ChatRoom?} */ = (Rooms.get(target));
if (!targetRoom || targetRoom.type !== 'chat' || !targetRoom.users[user.id]) {
if (!room || room.type !== 'chat') return this.errorReply(`This command is only meant to be used in chat rooms.`);
targetRoom = room;
}
if (!targetRoom.game || targetRoom.game.gameid !== 'mafia') return user.sendTo(targetRoom, `|error|There is no game of mafia running in this room.`);
const game = /** @type {MafiaTracker} */ (targetRoom.game);
if (game.hostid !== user.id && !game.cohosts.includes(user.id) && !this.can('mute', null, room)) return;
if (game.phase !== 'signups') return user.sendTo(targetRoom, `|error|Signups are already closed.`);
if (game.playerCount < 2) return user.sendTo(targetRoom, `|error|You need at least 2 players to start.`);
game.phase = 'locked';
game.sendRoom(game.roomWindow(), {uhtml: true});
game.updatePlayers();
},
closehelp: [`/mafia close - Closes signups for the current game. Requires: host % @ # & ~`],
'!closedsetup': true,
cs: 'closedsetup',
closedsetup(target, room, user) {
const args = target.split(',');
let targetRoom /** @type {ChatRoom?} */ = (Rooms.get(args[0]));
if (!targetRoom || targetRoom.type !== 'chat' || !targetRoom.users[user.id]) {
if (!room || room.type !== 'chat') return this.errorReply(`This command is only meant to be used in chat rooms.`);
targetRoom = room;
} else {
args.shift();
}
if (!targetRoom.game || targetRoom.game.gameid !== 'mafia') return user.sendTo(targetRoom, `|error|There is no game of mafia running in this room.`);
const game = /** @type {MafiaTracker} */ (targetRoom.game);
if (game.hostid !== user.id && !game.cohosts.includes(user.id) && !this.can('mute', null, room)) return;
const action = toID(args.join(''));
if (!['on', 'off'].includes(action)) return this.parse('/help mafia closedsetup');
if (game.started) return user.sendTo(targetRoom, `|error|You can't ${action === 'on' ? 'enable' : 'disable'} closed setup because the game has already started.`);
if ((action === 'on' && game.closedSetup) || (action === 'off' && !game.closedSetup)) return user.sendTo(targetRoom, `|error|Closed setup is already ${game.closedSetup ? 'enabled' : 'disabled'}.`);
game.closedSetup = action === 'on';
game.updateHost();
},
closedsetuphelp: [`/mafia closedsetup [on|off] - Sets if the game is a closed setup. Closed setups don't show the role list to players. Requires host % @ # & ~`],
'!reveal': true,
reveal(target, room, user) {
const args = target.split(',');
let targetRoom /** @type {ChatRoom?} */ = (Rooms.get(args[0]));
if (!targetRoom || targetRoom.type !== 'chat' || !targetRoom.users[user.id]) {
if (!room || room.type !== 'chat') return this.errorReply(`This command is only meant to be used in chat rooms.`);
targetRoom = room;
} else {
args.shift();
}
if (!targetRoom.game || targetRoom.game.gameid !== 'mafia') return user.sendTo(targetRoom, `|error|There is no game of mafia running in this room.`);
const game = /** @type {MafiaTracker} */ (targetRoom.game);
if (game.hostid !== user.id && !game.cohosts.includes(user.id) && !this.can('mute', null, room)) return;
const action = toID(args.join(''));
if (!['on', 'off'].includes(action)) return this.parse('/help mafia reveal');
if ((action === 'off' && game.noReveal) || (action === 'on' && !game.noReveal)) return user.sendTo(targetRoom, `|error|Revealing of roles is already ${game.noReveal ? 'disabled' : 'enabled'}.`);
game.noReveal = action === 'off';
game.sendRoom(`Revealing of roles has been ${action === 'off' ? 'disabled' : 'enabled'}.`, {declare: true});
game.updatePlayers();
},
revealhelp: [`/mafia reveal [on|off] - Sets if roles reveal on death or not. Requires host % @ # & ~`],
resetroles: 'setroles',
forceresetroles: 'setroles',
forcesetroles: 'setroles',
setroles(target, room, user, connection, cmd) {
if (!room || !room.game || room.game.gameid !== 'mafia') return this.errorReply(`There is no game of mafia running in this room.`);
const game = /** @type {MafiaTracker} */ (room.game);
if (game.hostid !== user.id && !game.cohosts.includes(user.id) && !this.can('mute', null, room)) return;
const reset = cmd.includes('reset');
if (reset) {
if (game.phase !== 'day' && game.phase !== 'night') return this.errorReply(`The game has not started yet.`);
} else {
if (game.phase !== 'locked' && game.phase !== 'IDEAlocked') return this.errorReply(game.phase === 'signups' ? `You need to close signups first.` : `The game has already started.`);
}
if (!target) return this.parse('/help mafia setroles');
game.setRoles(user, target, cmd.includes('force'), reset);
},
setroleshelp: [
`/mafia setroles [comma separated roles] - Set the roles for a game of mafia. You need to provide one role per player.`,
`/mafia forcesetroles [comma separated roles] - Forcibly set the roles for a game of mafia. No role PM information or alignment will be set.`,
`/mafia resetroles [comma separated roles] - Reset the roles in an ongoing game.`,
],
idea(target, room, user) {
if (!room.game || room.game.gameid !== 'mafia') return this.errorReply(`There is no game of mafia running in this room.`);
const game = /** @type {MafiaTracker} */ (room.game);
if (!user.can('broadcast', null, room) || (!user.can('mute', null, room) && game.hostid !== user.id && !game.cohosts.includes(user.id))) return this.errorReply(`/mafia idea - Access denied.`);
if (game.started) return this.errorReply(`You cannot start an IDEA after the game has started.`);
if (game.phase !== 'locked' && game.phase !== 'IDEAlocked') return this.errorReply(`You need to close the signups first.`);
game.ideaInit(user, toID(target));
},
ideahelp: [
`/mafia idea [idea] - starts the IDEA module [idea]. Requires + % @ # & ~, voices can only start for themselves`,
`/mafia ideareroll - rerolls the IDEA module. Requires host % @ # & ~`,
`/mafia ideapick [selection], [role] - selects a role`,
`/mafia ideadiscards - shows the discarded roles`,
],
customidea(target, room, user) {
if (!this.can('mute', null, room)) return;
if (!room.game || room.game.gameid !== 'mafia') return this.errorReply(`There is no game of mafia running in this room.`);
const game = /** @type {MafiaTracker} */ (room.game);
if (game.started) return this.errorReply(`You cannot start an IDEA after the game has started.`);
if (game.phase !== 'locked' && game.phase !== 'IDEAlocked') return this.errorReply(`You need to close the signups first.`);
const [options, roles] = Chat.splitFirst(target, '\n');
if (!options || !roles) return this.parse('/help mafia idea');
const [choicesStr, ...picks] = options.split(',').map(x => x.trim());
const choices = parseInt(choicesStr);
if (!choices || choices <= picks.length) return this.errorReply(`You need to have more choices than picks.`);
if (picks.some((value, index, arr) => arr.indexOf(value, index + 1) > 0)) return this.errorReply(`Your picks must be unique.`);
game.customIdeaInit(user, choices, picks, roles);
},
customideahelp: [
`/mafia customidea choices, picks (new line here, shift+enter)`,
`(comma or newline separated rolelist) - Starts an IDEA module with custom roles. Requires % @ # & ~`,
],
'!ideapick': true,
ideapick(target, room, user) {
const args = target.split(',');
let targetRoom /** @type {ChatRoom?} */ = (Rooms.get(args[0]));
if (!targetRoom || targetRoom.type !== 'chat' || !targetRoom.users[user.id]) {
if (!room || room.type !== 'chat') return this.errorReply(`This command is only meant to be used in chat rooms.`);
targetRoom = room;
} else {
args.shift();
}
if (!targetRoom.game || targetRoom.game.gameid !== 'mafia') return user.sendTo(targetRoom, `|error|There is no game of mafia running in this room.`);
const game = /** @type {MafiaTracker} */ (targetRoom.game);
if (!(user.id in game.playerTable)) return user.sendTo(targetRoom, '|error|You are not a player in the game.');
if (game.phase !== 'IDEApicking') return user.sendTo(targetRoom, `|error|The game is not in the IDEA picking phase.`);
game.ideaPick(user, args);
},
'!ideareroll': true,
ideareroll(target, room, user) {
let targetRoom /** @type {ChatRoom?} */ = (Rooms.get(target));
if (!targetRoom || targetRoom.type !== 'chat' || !targetRoom.users[user.id]) {
if (!room || room.type !== 'chat') return this.errorReply(`This command is only meant to be used in chat rooms.`);
targetRoom = room;
}
if (!targetRoom.game || targetRoom.game.gameid !== 'mafia') return user.sendTo(targetRoom, `|error|There is no game of mafia running in this room.`);
const game = /** @type {MafiaTracker} */ (targetRoom.game);
if (game.hostid !== user.id && !game.cohosts.includes(user.id) && !this.can('mute', null, room)) return;
game.ideaDistributeRoles(user);
},
idearerollhelp: [`/mafia ideareroll - rerolls the roles for the current IDEA module. Requires host % @ # & ~`],
discards: 'ideadiscards',
ideadiscards(target, room, user) {
if (!room.game || room.game.gameid !== 'mafia') return this.errorReply(`There is no game of mafia running in this room.`);
const game = /** @type {MafiaTracker} */ (room.game);
if (!game.IDEA.data) return this.errorReply(`There is no IDEA module in the mafia game.`);
if (target) {
if (game.hostid !== user.id && !game.cohosts.includes(user.id) && !this.can('mute', null, room)) return;
if (this.meansNo(target)) {
if (game.IDEA.discardsHidden) return this.errorReply(`IDEA discards are already hidden.`);
game.IDEA.discardsHidden = true;
} else if (this.meansYes(target)) {
if (!game.IDEA.discardsHidden) return this.errorReply(`IDEA discards are already visible.`);
game.IDEA.discardsHidden = false;
} else {
return this.parse('/help mafia ideadiscards');
}
return this.sendReply(`IDEA discards are now ${game.IDEA.discardsHidden ? 'hidden' : 'visible'}.`);
}
if (game.IDEA.discardsHidden) return this.errorReply(`Discards are not visible.`);
if (!game.IDEA.discardsHtml) return this.errorReply(`The IDEA module does not have finalised discards yet.`);
if (!this.runBroadcast()) return;
this.sendReplyBox(`IDEA discards:${game.IDEA.discardsHtml}`);
},
ideadiscardshelp: [
`/mafia ideadiscards - shows the discarded roles`,
`/mafia ideadiscards off - hides discards from the players. Requires host % @ # & ~`,
`/mafia ideadiscards on - shows discards to the players. Requires host % @ # & ~`,
],
'!start': true,
nightstart: 'start',
start(target, room, user, connection, cmd) {
let targetRoom /** @type {ChatRoom?} */ = (Rooms.get(target));
if (!targetRoom || targetRoom.type !== 'chat' || !targetRoom.users[user.id]) {
if (!room || room.type !== 'chat') return this.errorReply(`This command is only meant to be used in chat rooms.`);
targetRoom = room;
} else {
target = '';
}
if (!targetRoom.game || targetRoom.game.gameid !== 'mafia') return user.sendTo(targetRoom, `|error|There is no game of mafia running in this room.`);
const game = /** @type {MafiaTracker} */ (targetRoom.game);
if (game.hostid !== user.id && !game.cohosts.includes(user.id) && !this.can('mute', null, room)) return;
if (target) {
this.parse(`/mafia close`);
this.parse(`/mafia setroles ${target}`);
this.parse(`/mafia ${cmd}`);
return;
}
game.start(user, cmd === 'nightstart');
},
starthelp: [`/mafia start - Start the game of mafia. Signups must be closed. Requires host % @ # & ~`],
'!day': true,
extend: 'day',
night: 'day',
day(target, room, user, connection, cmd) {
const args = target.split(',');
let targetRoom /** @type {ChatRoom?} */ = (Rooms.get(args[0]));
if (!targetRoom || targetRoom.type !== 'chat' || !targetRoom.users[user.id]) {
if (!room || room.type !== 'chat') return this.errorReply(`This command is only meant to be used in chat rooms.`);
targetRoom = room;
} else {
args.shift();
}
if (!targetRoom.game || targetRoom.game.gameid !== 'mafia') return user.sendTo(targetRoom, `|error|There is no game of mafia running in this room.`);
const game = /** @type {MafiaTracker} */ (targetRoom.game);
if (game.hostid !== user.id && !game.cohosts.includes(user.id) && !this.can('mute', null, room)) return;
if (cmd === 'night') {
game.night();
} else {
let extension = parseInt(toID(args.join('')));
if (isNaN(extension)) {
extension = 0;
} else {
if (extension < 1) extension = 1;
if (extension > 10) extension = 10;
}
game.day((cmd === 'extend' ? extension : null));
}
},
dayhelp: [
`/mafia day - Move to the next game day. Requires host % @ # & ~`,
`/mafia night - Move to the next game night. Requires host % @ # & ~`,
`/mafia extend (minutes) - Return to the previous game day. If (minutes) is provided, set the deadline for (minutes) minutes. Requires host % @ # & ~`,
],
'!lynch': true,
l: 'lynch',
lynch(target, room, user) {
const args = target.split(',');
let targetRoom /** @type {ChatRoom?} */ = (Rooms.get(args[0]));
if (!targetRoom || targetRoom.type !== 'chat' || !targetRoom.users[user.id]) {
if (!room || room.type !== 'chat') return this.errorReply(`This command is only meant to be used in chat rooms.`);
targetRoom = room;
} else {
args.shift();
}
if (!targetRoom.game || targetRoom.game.gameid !== 'mafia') return user.sendTo(targetRoom, `|error|There is no game of mafia running in this room.`);
const game = /** @type {MafiaTracker} */ (targetRoom.game);
if (!this.canTalk(null, targetRoom)) return;
if (!(user.id in game.playerTable) && (!(user.id in game.dead) || !game.dead[user.id].restless)) return user.sendTo(targetRoom, `|error|You are not in the game of ${game.title}.`);
game.lynch(user.id, toID(args.join('')));
},
lynchhelp: [`/mafia lynch [player|nolynch] - Vote to lynch the specified player or to not lynch anyone.`],
'!unlynch': true,
ul: 'unlynch',
unl: 'unlynch',
unnolynch: 'unlynch',
unlynch(target, room, user) {
let targetRoom /** @type {ChatRoom?} */ = (Rooms.get(target));
if (!targetRoom || targetRoom.type !== 'chat' || !targetRoom.users[user.id]) {
if (!room || room.type !== 'chat') return this.errorReply(`This command is only meant to be used in chat rooms.`);
targetRoom = room;
}
if (!targetRoom.game || targetRoom.game.gameid !== 'mafia') return user.sendTo(targetRoom, `|error|There is no game of mafia running in this room.`);
const game = /** @type {MafiaTracker} */ (targetRoom.game);
if (!this.canTalk(null, targetRoom)) return;
if (!(user.id in game.playerTable) && (!(user.id in game.dead) || !game.dead[user.id].restless)) return user.sendTo(targetRoom, `|error|You are not in the game of ${targetRoom.game.title}.`);
game.unlynch(user.id);
},
unlynchhelp: [`/mafia unlynch - Withdraw your lynch vote. Fails if you're not voting to lynch anyone`],
nl: 'nolynch',
nolynch() {
this.parse('/mafia lynch nolynch');
},
'!selflynch': true,
enableself: 'selflynch',
selflynch(target, room, user, connection, cmd) {
const args = target.split(',');
let targetRoom /** @type {ChatRoom?} */ = (Rooms.get(args[0]));
if (!targetRoom || targetRoom.type !== 'chat' || !targetRoom.users[user.id]) {
if (!room || room.type !== 'chat') return this.errorReply(`This command is only meant to be used in chat rooms.`);
targetRoom = room;
} else {
args.shift();
}
if (!targetRoom.game || targetRoom.game.gameid !== 'mafia') return user.sendTo(targetRoom, `|error|There is no game of mafia running in this room.`);
const game = /** @type {MafiaTracker} */ (targetRoom.game);
if (game.hostid !== user.id && !game.cohosts.includes(user.id) && !this.can('mute', null, room)) return;
let action = toID(args.shift());
if (!action) return this.parse(`/help mafia selflynch`);
if (this.meansYes(action)) {
game.setSelfLynch(user, true);
} else if (this.meansNo(action)) {
game.setSelfLynch(user, false);
} else if (action === 'hammer') {
game.setSelfLynch(user, 'hammer');
} else {
return this.parse(`/help mafia selflynch`);
}
},
selflynchhelp: [`/mafia selflynch [on|hammer|off] - Allows players to self lynch themselves either at hammer or anytime. Requires host % @ # & ~`],
'!kill': true,
treestump: 'kill',
spirit: 'kill',
spiritstump: 'kill',
kick: 'kill',
kill(target, room, user, connection, cmd) {
const args = target.split(',');
let targetRoom /** @type {ChatRoom?} */ = (Rooms.get(args[0]));
if (!targetRoom || targetRoom.type !== 'chat' || !targetRoom.users[user.id]) {
if (!room || room.type !== 'chat') return this.errorReply(`This command is only meant to be used in chat rooms.`);
targetRoom = room;
} else {
args.shift();
}
if (!targetRoom.game || targetRoom.game.gameid !== 'mafia') return user.sendTo(targetRoom, `|error|There is no game of mafia running in this room.`);
const game = /** @type {MafiaTracker} */ (targetRoom.game);
if (game.hostid !== user.id && !game.cohosts.includes(user.id) && !this.can('mute', null, room)) return;
const player = game.playerTable[toID(args.join(''))];
if (!player) return user.sendTo(targetRoom, `|error|"${args.join(',')}" is not a living player.`);
if (game.phase === 'IDEApicking') return this.errorReply(`You cannot add or remove players while IDEA roles are being picked.`); // needs to be here since eliminate doesn't pass the user
game.eliminate(player, cmd);
},
killhelp: [
`/mafia kill [player] - Kill a player, eliminating them from the game. Requires host % @ # & ~`,
`/mafia treestump [player] - Kills a player, but allows them to talk during the day still.`,
`/mafia spirit [player] - Kills a player, but allows them to vote on the lynch still.`,
`/mafia spiritstump [player] Kills a player, but allows them to talk during the day, and vote on the lynch.`,
],
'!revive': true,
forceadd: 'revive',
add: 'revive',
revive(target, room, user, connection, cmd) {
const args = target.split(',');
let targetRoom /** @type {ChatRoom?} */ = (Rooms.get(args[0]));
if (!targetRoom || targetRoom.type !== 'chat' || !targetRoom.users[user.id]) {
if (!room || room.type !== 'chat') return this.errorReply(`This command is only meant to be used in chat rooms.`);
targetRoom = room;
} else {
args.shift();
}
if (!targetRoom.game || targetRoom.game.gameid !== 'mafia') return user.sendTo(targetRoom, `|error|There is no game of mafia running in this room.`);
const game = /** @type {MafiaTracker} */ (targetRoom.game);
if (game.hostid !== user.id && !game.cohosts.includes(user.id) && !this.can('mute', null, room)) return;
if (!toID(args.join(''))) return this.parse('/help mafia revive');
for (const targetUser of args) {
game.revive(user, toID(targetUser), cmd === 'forceadd');
}
},
revivehelp: [`/mafia revive [player] - Revive a player who died or add a new player to the game. Requires host % @ # & ~`],
dl: 'deadline',
deadline(target, room, user) {
if (!room || !room.game || room.game.gameid !== 'mafia') return this.errorReply(`There is no game of mafia running in this room.`);
const game = /** @type {MafiaTracker} */ (room.game);
target = toID(target);
if (target && game.hostid !== user.id && !game.cohosts.includes(user.id) && !this.can('mute', null, room)) return;
if (target === 'off') {
return game.setDeadline(0);
} else {
const num = parseInt(target);
if (isNaN(num)) {
if ((game.hostid === user.id || game.cohosts.includes(user.id)) && this.cmdToken === "!") {
const broadcastMessage = this.message.toLowerCase().replace(/[^a-z0-9\s!,]/g, '');
if (room.lastBroadcast === broadcastMessage &&
room.lastBroadcastTime >= Date.now() - 20 * 1000) {
return this.errorReply("You can't broadcast this because it was just broadcasted.");
}
this.broadcasting = true;
this.broadcastMessage = broadcastMessage;
this.sendReply('|c|' + this.user.getIdentity(this.room.roomid) + '|' + this.message);
room.lastBroadcastTime = Date.now();
room.lastBroadcast = broadcastMessage;
}
if (!this.runBroadcast()) return false;
if ((game.dlAt - Date.now()) > 0) {
return this.sendReply(`|raw|The deadline is in ${Chat.toDurationString(game.dlAt - Date.now()) || '0 seconds'}.`);
} else {
return this.parse(`/help mafia deadline`);
}
}
if (num < 1 || num > 20) return this.errorReply(`The deadline must be between 1 and 20 minutes.`);
return game.setDeadline(num);
}
},
deadlinehelp: [`/mafia deadline [minutes|off] - Sets or removes the deadline for the game. Cannot be more than 20 minutes.`],
applylynchmodifier: 'applyhammermodifier',
applyhammermodifier(target, room, user, connection, cmd) {
if (!room || !room.game || room.game.gameid !== 'mafia') return this.errorReply(`There is no game of mafia running in this room.`);
const game = /** @type {MafiaTracker} */ (room.game);
if (game.hostid !== user.id && !game.cohosts.includes(user.id) && !this.can('mute', null, room)) return;
if (!game.started) return this.errorReply(`The game has not started yet.`);
const [player, mod] = target.split(',');
if (cmd === 'applyhammermodifier') {
game.applyHammerModifier(user, toID(player), parseInt(mod));
} else {
game.applyLynchModifier(user, toID(player), parseInt(mod));
}
},
clearlynchmodifiers: 'clearhammermodifiers',
clearhammermodifiers(target, room, user, connection, cmd) {
if (!room || !room.game || room.game.gameid !== 'mafia') return this.errorReply(`There is no game of mafia running in this room.`);
const game = /** @type {MafiaTracker} */ (room.game);
if (game.hostid !== user.id && !game.cohosts.includes(user.id) && !this.can('mute', null, room)) return;
if (!game.started) return this.errorReply(`The game has not started yet.`);
if (cmd === 'clearhammermodifiers') {
game.clearHammerModifiers(user);
} else {
game.clearLynchModifiers(user);
}
},
hate: 'love',
unhate: 'love',
unlove: 'love',
removehammermodifier: 'love',
love(target, room, user, connection, cmd) {
let mod;
switch (cmd) {
case 'hate':
mod = -1;
break;
case 'love':
mod = 1;
break;
case 'unhate': case 'unlove': case 'removehammermodifier':
mod = 0;
break;
}
this.parse(`/mafia applyhammermodifier ${target}, ${mod}`);
},
doublevoter: 'mayor',
voteless: 'mayor',
unvoteless: 'mayor',
unmayor: 'mayor',
removelynchmodifier: 'mayor',
mayor(target, room, user, connection, cmd) {
let mod;
switch (cmd) {
case 'doublevoter': case 'mayor':
mod = 2;
break;
case 'voteless':
mod = 0;
break;
case 'unvoteless': case 'unmayor': case 'removelynchmodifier':
mod = 1;
break;
}
this.parse(`/mafia applylynchmodifier ${target}, ${mod}`);
},
unsilence: 'silence',
silence(target, room, user, connection, cmd) {
if (!room || !room.game || room.game.gameid !== 'mafia') return this.errorReply(`There is no game of mafia running in this room.`);
const game = /** @type {MafiaTracker} */ (room.game);
if (game.hostid !== user.id && !game.cohosts.includes(user.id) && !this.can('mute', null, room)) return;
if (!game.started) return this.errorReply(`The game has not started yet.`);
target = toID(target);
const targetPlayer = game.playerTable[target] || game.dead[target];
const silence = cmd === 'silence';
if (!targetPlayer) return this.errorReply(`${target} is not in the game of mafia.`);
if (silence === targetPlayer.silenced) return this.errorReply(`${targetPlayer.name} is already ${!silence ? 'not' : ''} silenced`);
targetPlayer.silenced = silence;
this.sendReply(`${targetPlayer.name} has been ${!silence ? 'un' : ''}silenced.`);
},
silencehelp: [
`/mafia silence [player] - Silences [player], preventing them from talking at all. Requires host % @ # & ~`,
`/mafia unsilence [player] - Removes a silence on [player], allowing them to talk again. Requires host % @ # & ~`,
],
shifthammer: 'hammer',
resethammer: 'hammer',
hammer(target, room, user, connection, cmd) {
if (!room || !room.game || room.game.gameid !== 'mafia') return this.errorReply(`There is no game of mafia running in this room.`);
const game = /** @type {MafiaTracker} */ (room.game);
if (game.hostid !== user.id && !game.cohosts.includes(user.id) && !this.can('mute', null, room)) return;
if (!game.started) return this.errorReply(`The game has not started yet.`);
const hammer = parseInt(target);
if (toID(cmd) !== `resethammer` && ((isNaN(hammer) && !this.meansNo(target)) || hammer < 1)) return this.errorReply(`${target} is not a valid hammer count.`);
switch (cmd.toLowerCase()) {
case 'shifthammer':
game.shiftHammer(hammer);
break;
case 'hammer':
game.setHammer(hammer);
break;
default:
game.resetHammer();
break;
}
},
hammerhelp: [
`/mafia hammer [hammer] - sets the hammer count to [hammer] and resets lynches`,
`/mafia hammer off - disables hammering`,
`/mafia shifthammer [hammer] - sets the hammer count to [hammer] without resetting lynches`,
`/mafia resethammer - sets the hammer to the default, resetting lynches`,
],
'!enablenl': true,
disablenl: 'enablenl',
enablenl(target, room, user, connection, cmd) {
let targetRoom /** @type {ChatRoom?} */ = (Rooms.get(target));
if (!targetRoom || targetRoom.type !== 'chat' || !targetRoom.users[user.id]) {
if (!room || room.type !== 'chat') return this.errorReply(`This command is only meant to be used in chat rooms.`);
targetRoom = room;
}
if (!targetRoom.game || targetRoom.game.gameid !== 'mafia') return user.sendTo(targetRoom, `|error|There is no game of mafia running in this room.`);
const game = /** @type {MafiaTracker} */ (targetRoom.game);
if (game.hostid !== user.id && !game.cohosts.includes(user.id) && !this.can('mute', null, room)) return;
if (cmd === 'enablenl') {
game.setNoLynch(user, true);
} else {
game.setNoLynch(user, false);
}
},
enablenlhelp: [`/mafia [enablenl|disablenl] - Allows or disallows players abstain from lynching. Requires host % @ # & ~`],
forcelynch(target, room, user) {
if (!room || !room.game || room.game.gameid !== 'mafia') return this.errorReply(`There is no game of mafia running in this room.`);
const game = /** @type {MafiaTracker} */ (room.game);
if (game.hostid !== user.id && !game.cohosts.includes(user.id) && !this.can('mute', null, room)) return;
target = toID(target);
if (this.meansYes(target)) {
if (game.forcelynch) return this.errorReply(`Forcelynching is already enabled.`);
game.forcelynch = true;
if (game.started) game.resetHammer();
game.sendRoom(`Forcelynching has been enabled. Your lynch will start on yourself, and you cannot unlynch!`, {declare: true});
} else if (this.meansNo(target)) {
if (!game.forcelynch) return this.errorReply(`Forcelynching is already disabled.`);
game.forcelynch = false;
game.sendRoom(`Forcelynching has been disabled. You can lynch normally now!`, {declare: true});
} else {
this.parse('/help mafia forcelynch');
}
},
forcelynchhelp: [`/mafia forcelynch [yes/no] - Forces player's lynches onto themselves, and prevents unlynching. Requires host % @ # & ~`],
lynches(target, room, user) {
if (!room || !room.game || room.game.gameid !== 'mafia') return this.errorReply(`There is no game of mafia running in this room.`);
const game = /** @type {MafiaTracker} */ (room.game);
if (!game.started) return this.errorReply(`The game of mafia has not started yet.`);
if ((game.hostid === user.id || game.cohosts.includes(user.id)) && this.cmdToken === "!") {
const broadcastMessage = this.message.toLowerCase().replace(/[^a-z0-9\s!,]/g, '');
if (room.lastBroadcast === broadcastMessage &&
room.lastBroadcastTime >= Date.now() - 20 * 1000) {
return this.errorReply("You can't broadcast this because it was just broadcasted.");
}
this.broadcasting = true;
this.broadcastMessage = broadcastMessage;
this.sendReply('|c|' + this.user.getIdentity(this.room.roomid) + '|' + this.message);
room.lastBroadcastTime = Date.now();
room.lastBroadcast = broadcastMessage;
}
if (!this.runBroadcast()) return false;
this.sendReplyBox(game.lynchBox());
},
pl: 'players',
players(target, room, user) {
if (!room || !room.game || room.game.gameid !== 'mafia') return this.errorReply(`There is no game of mafia running in this room.`);
const game = /** @type {MafiaTracker} */ (room.game);
if ((game.hostid === user.id || game.cohosts.includes(user.id)) && this.cmdToken === "!") {
const broadcastMessage = this.message.toLowerCase().replace(/[^a-z0-9\s!,]/g, '');
if (room.lastBroadcast === broadcastMessage &&
room.lastBroadcastTime >= Date.now() - 20 * 1000) {
return this.errorReply("You can't broadcast this because it was just broadcasted.");
}
this.broadcasting = true;
this.broadcastMessage = broadcastMessage;
this.sendReply('|c|' + this.user.getIdentity(this.room.roomid) + '|' + this.message);
room.lastBroadcastTime = Date.now();
room.lastBroadcast = broadcastMessage;
}
if (!this.runBroadcast()) return false;
if (this.broadcasting) {
game.sendPlayerList();
} else {
this.sendReplyBox(`Players (${game.playerCount}): ${Object.keys(game.playerTable).map(p => game.playerTable[p].safeName).join(', ')}`);
}
},
originalrolelist: 'rolelist',
orl: 'rolelist',
rl: 'rolelist',
rolelist(target, room, user, connection, cmd) {
if (!room || !room.game || room.game.gameid !== 'mafia') return this.errorReply(`There is no game of mafia running in this room.`);
const game = /** @type {MafiaTracker} */ (room.game);
if (game.closedSetup) return this.errorReply(`You cannot show roles in a closed setup.`);
if (!this.runBroadcast()) return false;
if (game.IDEA.data) {
let buf = `IDEA roles:${game.IDEA.data.roles.join(` `)}`;
return this.sendReplyBox(buf);
}
const showOrl = (['orl', 'originalrolelist'].includes(cmd) || game.noReveal);
const roleString = (showOrl ? game.originalRoles : game.roles).sort((a, b) => {
if (a.alignment < b.alignment) return -1;
if (b.alignment < a.alignment) return 1;
return 0;
}).map(role => role.safeName).join(', ');
this.sendReplyBox(`${showOrl ? `Original Rolelist: ` : `Rolelist: `}${roleString}`);
},
playerroles(target, room, user) {
if (!room || !room.game || room.game.gameid !== 'mafia') return this.errorReply(`There is no game of mafia running in this room.`);
const game = /** @type {MafiaTracker} */ (room.game);
if (game.hostid !== user.id && !game.cohosts.includes(user.id)) return this.errorReply(`Only the host can view roles.`);
if (!game.started) return this.errorReply(`The game has not started.`);
const players = [...Object.values(game.playerTable), ...Object.values(game.dead)];
this.sendReplyBox(players.map(p => `${p.safeName}: ${p.role ? (p.role.alignment === 'solo' ? 'Solo ' : '') + p.role.safeName : 'No role'}`).join(' '));
},
spectate: 'view',
view(target, room, user, connection) {
if (!room || !room.game || room.game.gameid !== 'mafia') return this.errorReply(`There is no game of mafia running in this room.`);
if (!this.runBroadcast()) return;
if (this.broadcasting) return this.sendReplyBox(``);
return this.parse(`/join view-mafia-${room.roomid}`);
},
'!refreshlynches': true,
refreshlynches(target, room, user, connection) {
let targetRoom /** @type {ChatRoom?} */ = (Rooms.get(target));
if (!targetRoom || targetRoom.type !== 'chat' || !targetRoom.users[user.id]) {
if (!room || room.type !== 'chat') return this.errorReply(`This command is only meant to be used in chat rooms.`);
targetRoom = room;
}
if (!targetRoom.game || targetRoom.game.gameid !== 'mafia') return user.sendTo(targetRoom, `|error|There is no game of mafia running in this room.`);
const game = /** @type {MafiaTracker} */ (targetRoom.game);
const lynches = game.lynchBoxFor(user.id);
user.send(`>view-mafia-${game.room.roomid}\n|selectorhtml|#mafia-lynches|` + lynches);
},
'!mafsub': true,
forcesub: 'mafsub',
sub: 'mafsub', // Typescript doesn't like "sub" as the command name for some reason, so this is a hack to get around that.
mafsub(target, room, user, connection, cmd) {
const args = target.split(',');
let targetRoom /** @type {ChatRoom?} */ = (Rooms.get(args[0]));
if (!targetRoom || targetRoom.type !== 'chat' || !targetRoom.users[user.id]) {
if (!room || room.type !== 'chat') return this.errorReply(`This command is only meant to be used in chat rooms.`);
targetRoom = room;
} else {
args.shift();
}
if (!targetRoom.game || targetRoom.game.gameid !== 'mafia') return user.sendTo(targetRoom, `|error|There is no game of mafia running in this room.`);
const game = /** @type {MafiaTracker} */ (targetRoom.game);
let action = toID(args.shift());
switch (action) {
case 'in':
if (user.id in game.playerTable) {
// Check if they have requested to be subbed out.
if (!game.requestedSub.includes(user.id)) return user.sendTo(targetRoom, `|error|You have not requested to be subbed out.`);
game.requestedSub.splice(game.requestedSub.indexOf(user.id), 1);
user.sendTo(room, `|error|You have cancelled your request to sub out.`);
game.playerTable[user.id].updateHtmlRoom();
} else {
if (!this.canTalk(null, targetRoom)) return;
if (game.subs.includes(user.id)) return user.sendTo(targetRoom, `|error|You are already on the sub list.`);
if (game.played.includes(user.id)) return user.sendTo(targetRoom, `|error|You cannot sub back into the game.`);
const canJoin = game.canJoin(user, true);
if (canJoin) return user.sendTo(targetRoom, `|error|${canJoin}`);
game.subs.push(user.id);
game.nextSub();
// Update spectator's view
this.parse(`/join view-mafia-${targetRoom.roomid}`);
}
break;
case 'out':
if (user.id in game.playerTable) {
if (game.requestedSub.includes(user.id)) return user.sendTo(targetRoom, `|error|You have already requested to be subbed out.`);
game.requestedSub.push(user.id);
game.playerTable[user.id].updateHtmlRoom();
game.nextSub();
} else {
if (game.hostid === user.id || game.cohosts.includes(user.id)) return user.sendTo(targetRoom, `|error|The host cannot sub out of the game.`);
if (!game.subs.includes(user.id)) return user.sendTo(targetRoom, `|error|You are not on the sub list.`);
game.subs.splice(game.subs.indexOf(user.id), 1);
// Update spectator's view
this.parse(`/join view-mafia-${targetRoom.roomid}`);
}
break;
case 'next':
if (game.hostid !== user.id && !game.cohosts.includes(user.id) && !this.can('mute', null, room)) return;
let toSub = args.shift();
if (!(toID(toSub) in game.playerTable)) return user.sendTo(targetRoom, `|error|${toSub} is not in the game.`);
if (!game.subs.length) {
if (game.hostRequestedSub.indexOf(toID(toSub)) !== -1) return user.sendTo(targetRoom, `|error|${toSub} is already on the list to be subbed out.`);
user.sendTo(targetRoom, `|error|There are no subs to replace ${toSub}, they will be subbed if a sub is available before they speak next.`);
game.hostRequestedSub.unshift(toID(toSub));
} else {
game.nextSub(toID(toSub));
}
break;
case 'remove':
if (game.hostid !== user.id && !game.cohosts.includes(user.id) && !this.can('mute', null, room)) return;
const toRemove = toID(args.shift());
const toRemoveIndex = game.subs.indexOf(toRemove);
if (toRemoveIndex === -1) return user.sendTo(room, `|error|${toRemove} is not on the sub list.`);
game.subs.splice(toRemoveIndex, 1);
user.sendTo(room, `${toRemove} has been removed from the sublist`);
break;
case 'unrequest':
if (game.hostid !== user.id && !game.cohosts.includes(user.id) && !this.can('mute', null, room)) return;
const toUnrequest = toID(args.shift());
const userIndex = game.requestedSub.indexOf(toUnrequest);
const hostIndex = game.hostRequestedSub.indexOf(toUnrequest);
if (userIndex < 0 && hostIndex < 0) return user.sendTo(room, `|error|${toUnrequest} is not requesting a sub.`);
if (userIndex > -1) {
game.requestedSub.splice(userIndex, 1);
user.sendTo(room, `${toUnrequest}'s sub request has been removed.`);
}
if (hostIndex > -1) {
game.hostRequestedSub.splice(userIndex, 1);
user.sendTo(room, `${toUnrequest} has been removed from the host sublist.`);
}
break;
default:
if (game.hostid !== user.id && !game.cohosts.includes(user.id) && !this.can('mute', null, room)) return;
const toSubOut = action;
const toSubIn = toID(args.shift());
if (!(toSubOut in game.playerTable)) return user.sendTo(targetRoom, `|error|${toSubOut} is not in the game.`);
const targetUser = Users.get(toSubIn);
if (!targetUser) return user.sendTo(targetRoom, `|error|The user "${toSubIn}" was not found.`);
const canJoin = game.canJoin(targetUser, false, cmd === 'forcesub');
if (canJoin) return user.sendTo(targetRoom, `|error|${canJoin}`);
if (game.subs.includes(targetUser.id)) game.subs.splice(game.subs.indexOf(targetUser.id), 1);
if (game.hostRequestedSub.includes(toSubOut)) game.hostRequestedSub.splice(game.hostRequestedSub.indexOf(toSubOut), 1);
if (game.requestedSub.includes(toSubOut)) game.requestedSub.splice(game.requestedSub.indexOf(toSubOut), 1);
game.sub(toSubOut, toSubIn);
}
},
subhelp: [
`/mafia sub in - Request to sub into the game, or cancel a request to sub out.`,
`/mafia sub out - Request to sub out of the game, or cancel a request to sub in.`,
`/mafia sub next, [player] - Forcibly sub [player] out of the game. Requires host % @ # & ~`,
`/mafia sub remove, [user] - Remove [user] from the sublist. Requres host % @ # & ~`,
`/mafia sub unrequest, [player] - Remove's a player's request to sub out of the game. Requires host % @ # & ~`,
`/mafia sub [player], [user] - Forcibly sub [player] for [user]. Requires host % @ # & ~`,
],
"!autosub": true,
autosub(target, room, user) {
const args = target.split(',');
let targetRoom /** @type {ChatRoom?} */ = (Rooms.get(args[0]));
if (!targetRoom || targetRoom.type !== 'chat' || !targetRoom.users[user.id]) {
if (!room || room.type !== 'chat') return this.errorReply(`This command is only meant to be used in chat rooms.`);
targetRoom = room;
} else {
args.shift();
}
if (!targetRoom.game || targetRoom.game.gameid !== 'mafia') return user.sendTo(targetRoom, `|error|There is no game of mafia running in this room.`);
const game = /** @type {MafiaTracker} */ (targetRoom.game);
if (game.hostid !== user.id && !game.cohosts.includes(user.id) && !this.can('mute', null, room)) return;
if (this.meansYes(toID(args.join('')))) {
if (game.autoSub) return user.sendTo(targetRoom, `|error|Automatic subbing of players is already enabled.`);
game.autoSub = true;
user.sendTo(targetRoom, `Automatic subbing of players has been enabled.`);
game.nextSub();
} else if (this.meansNo(toID(args.join('')))) {
if (!game.autoSub) return user.sendTo(targetRoom, `|error|Automatic subbing of players is already disabled.`);
game.autoSub = false;
user.sendTo(targetRoom, `Automatic subbing of players has been disabled.`);
} else {
return this.parse(`/help mafia autosub`);
}
},
autosubhelp: [`/mafia autosub [yes|no] - Sets if players will automatically sub out if a user is on the sublist. Requires host % @ # & ~`],
cohost: 'subhost',
forcecohost: 'subhost',
forcesubhost: 'subhost',
subhost(target, room, user, connection, cmd) {
if (!room || !room.game || room.game.gameid !== 'mafia') return this.errorReply(`There is no game of mafia running in this room.`);
const game = /** @type {MafiaTracker} */ (room.game);
if (!this.canTalk()) return;
if (!target) return this.parse(`/help mafia ${cmd}`);
if (!this.can('mute', null, room)) return false;
this.splitTarget(target, false);
let targetUser = this.targetUser;
if (!targetUser || !targetUser.connected) return this.errorReply(`The user "${this.targetUsername}" was not found.`);
if (!room.users[targetUser.id]) return this.errorReply(`${targetUser.name} is not in this room, and cannot be hosted.`);
if (game.hostid === targetUser.id) return this.errorReply(`${targetUser.name} is already the host.`);
if (game.cohosts.includes(targetUser.id)) return this.errorReply(`${targetUser.name} is already a cohost.`);
if (targetUser.id in game.playerTable) return this.errorReply(`The host cannot be ingame.`);
if (targetUser.id in game.dead) {
if (!cmd.includes('force')) return this.errorReply(`${targetUser.name} could potentially be revived. To continue anyway, use /mafia force${cmd} ${target}.`);
if (game.dead[targetUser.id].lynching) game.unlynch(targetUser.id);
game.dead[targetUser.id].destroy();
delete game.dead[targetUser.id];
}
if (cmd.includes('cohost')) {
game.cohosts.push(targetUser.id);
game.sendRoom(`${Chat.escapeHTML(targetUser.name)} has been added as a cohost by ${Chat.escapeHTML(user.name)}`, {declare: true});
for (const conn of targetUser.connections) {
Chat.resolvePage(`view-mafia-${room.roomid}`, targetUser, conn);
}
this.modlog('MAFIACOHOST', targetUser, null, {noalts: true, noip: true});
} else {
const oldHostid = game.hostid;
const oldHost = Users.get(game.hostid);
if (oldHost) oldHost.send(`>view-mafia-${room.roomid}\n|deinit`);
if (game.subs.includes(targetUser.id)) game.subs.splice(game.subs.indexOf(targetUser.id), 1);
const queueIndex = hostQueue.indexOf(targetUser.id);
if (queueIndex > -1) hostQueue.splice(queueIndex, 1);
game.host = Chat.escapeHTML(targetUser.name);
game.hostid = targetUser.id;
game.played.push(targetUser.id);
for (const conn of targetUser.connections) {
Chat.resolvePage(`view-mafia-${room.roomid}`, targetUser, conn);
}
game.sendRoom(`${Chat.escapeHTML(targetUser.name)} has been substituted as the new host, replacing ${oldHostid}.`, {declare: true});
this.modlog('MAFIASUBHOST', targetUser, `replacing ${oldHostid}`, {noalts: true, noip: true});
}
},
subhosthelp: [`/mafia subhost [user] - Substitues the user as the new game host.`],
cohosthelp: [`/mafia cohost [user] - Adds the user as a cohost. Cohosts can talk during the game, as well as perform host actions.`],
uncohost: 'removecohost',
removecohost(target, room, user) {
if (!room || !room.game || room.game.gameid !== 'mafia') return this.errorReply(`There is no game of mafia running in this room.`);
const game = /** @type {MafiaTracker} */ (room.game);
if (!this.canTalk()) return;
if (!target) return this.parse('/help mafia subhost');
if (!this.can('mute', null, room)) return false;
target = toID(target);
const cohostIndex = game.cohosts.indexOf(target);
if (cohostIndex < 0) {
if (game.hostid === target) return this.errorReply(`${target} is the host, not a cohost. Use /mafia subhost to replace them.`);
return this.errorReply(`${target} is not a cohost.`);
}
game.cohosts.splice(cohostIndex, 1);
game.sendRoom(`${target} was removed as a cohost by ${Chat.escapeHTML(user.name)}`, {declare: true});
this.modlog('MAFIAUNCOHOST', target, null, {noalts: true, noip: true});
},
'!end': true,
end(target, room, user) {
let targetRoom /** @type {ChatRoom?} */ = (Rooms.get(target));
if (!targetRoom || targetRoom.type !== 'chat' || !targetRoom.users[user.id]) {
if (!room || room.type !== 'chat') return this.errorReply(`This command is only meant to be used in chat rooms.`);
targetRoom = room;
}
if (!targetRoom.game || targetRoom.game.gameid !== 'mafia') return user.sendTo(targetRoom, `|error|There is no game of mafia running in this room.`);
const game = /** @type {MafiaTracker} */ (targetRoom.game);
if (game.hostid !== user.id && !game.cohosts.includes(user.id) && !this.can('broadcast', null, room)) return;
game.end();
this.room = targetRoom;
this.modlog('MAFIAEND', null);
},
endhelp: [`/mafia end - End the current game of mafia. Requires host + % @ # & ~`],
'!data': true,
role: 'data',
modifier: 'data',
alignment: 'data',
theme: 'data',
term: 'data',
dt: 'data',
data(target, room, user, connection, cmd) {
if (room && room.mafiaDisabled) return this.errorReply(`Mafia is disabled for this room.`);
if (cmd === 'role' && !target && room) {
// Support /mafia role showing your current role if you're in a game
const game = /** @type {MafiaTracker} */ (room.game);
if (!game || game.roomid !== 'mafia') return this.errorReply(`There is no game of mafia running in this room. If you meant to display information about a role, use /mafia role [role name]`);
if (!(user.id in game.playerTable)) return this.errorReply(`You are not in the game of ${game.title}.`);
const role = game.playerTable[user.id].role;
if (!role) return this.errorReply(`You do not have a role yet.`);
return this.sendReplyBox(`Your role is: ${role.safeName}`);
}
if (!this.runBroadcast()) return;
if (!target) return this.parse(`/help mafia data`);
/** @type {{[k: string]: string}} */
const types = {alignment: 'alignments', role: 'roles', modifier: 'modifiers', theme: 'themes', term: 'terms'};
let id = target.split(' ').map(toID).join('_');
let result = null;
let dataType = cmd;
if (cmd in types) {
let type = /** @type {'alignments' | 'roles' | 'modifiers' | 'themes' | 'terms'} */ (types[cmd]);
let data = MafiaData[type];
if (!data) return this.errorReply(`"${type}" is not a valid search area.`); // Should never happen
if (!data[id]) return this.errorReply(`"${target} is not a valid ${cmd}."`);
result = data[id];
if (typeof result === 'string') result = data[result];
} else {
// Search all
for (let i in types) {
let type = /** @type {'alignments' | 'roles' | 'modifiers' | 'themes' | 'terms'} */ (types[i]);
let data = MafiaData[type];
if (!data) continue; // Should never happen
if (!data[id]) continue;
result = data[id];
if (typeof result === 'string') result = data[result];
dataType = i;
break;
}
if (!result) return this.errorReply(`"${target}" is not a valid mafia alignment, role, modifier, or theme.`);
}
let buf = `
${result.name}
Type: ${dataType} `;
if (dataType === 'theme') {
buf += `Description: ${result.desc} Setups:`;
for (let i in result) {
if (isNaN(parseInt(i))) continue;
buf += `${i}: `;
/** @type {{[k: string]: number}} */
let count = {};
let roles = [];
for (const role of result[i].split(',').map((/** @type {string} */x) => x.trim())) {
count[role] = count[role] ? count[role] + 1 : 1;
}
for (const role in count) {
roles.push(count[role] > 1 ? `${count[role]}x ${role}` : role);
}
buf += `${roles.join(', ')} `;
}
} else {
buf += `${result.memo.join(' ')}`;
}
return this.sendReplyBox(buf);
},
datahelp: [`/mafia data [alignment|role|modifier|theme|term] - Get information on a mafia alignment, role, modifier, theme, or term.`],
winfaction: 'win',
win(target, room, user, connection, cmd) {
if (!room || room.mafiaDisabled) return this.errorReply(`Mafia is disabled for this room.`);
if (room.roomid !== 'mafia') return this.errorReply(`This command can only be used in the Mafia room.`);
if (cmd === 'winfaction' && (!room.game || room.game.gameid !== 'mafia')) return this.errorReply(`There is no game of mafia running in the room`);
if (!this.can('mute', null, room)) return;
const args = target.split(',');
let points = parseInt(args[0]);
if (isNaN(points)) {
points = 10;
} else {
if (points > 100 || points < -100) return this.errorReply(`You cannot give or take more than 100 points at a time.`);
// shift out the point count
args.shift();
}
if (!args.length) return this.parse('/help mafia win');
const month = new Date().toLocaleString("en-us", {month: "numeric", year: "numeric"});
if (!logs.leaderboard[month]) logs.leaderboard[month] = {};
let toGiveTo = [];
let buf = `${points} were awarded to: `;
if (cmd === 'winfaction') {
const game = /** @type {MafiaTracker} */ (room.game);
for (let faction of args) {
faction = toID(faction);
let inFaction = [];
for (const user of [...Object.values(game.playerTable), ...Object.values(game.dead)]) {
if (user.role && toID(user.role.alignment) === faction) {
toGiveTo.push(user.id);
inFaction.push(user.id);
}
}
if (inFaction.length) buf += ` the ${faction} faction: ${inFaction.join(', ')};`;
}
} else {
toGiveTo = args;
buf += toGiveTo.join(', ');
}
if (!toGiveTo.length) return this.parse('/help mafia win');
let gavePoints = false;
for (let u of toGiveTo) {
u = toID(u);
if (!u) continue;
if (!gavePoints) gavePoints = true;
if (!logs.leaderboard[month][u]) logs.leaderboard[month][u] = 0;
logs.leaderboard[month][u] += points;
if (logs.leaderboard[month][u] === 0) delete logs.leaderboard[month][u];
}
if (!gavePoints) return this.parse('/help mafia win');
writeFile(LOGS_FILE, logs);
this.modlog(`MAFIAPOINTS`, null, `${points} points were awarded to ${Chat.toListString(toGiveTo)}`);
room.add(buf).update();
},
winhelp: [
`/mafia win (points), [user1], [user2], [user3], ... - Award the specified users points to the mafia leaderboard for this month. The amount of points can be negative to take points. Defaults to 10 points.`,
`/mafia winfaction (points), [faction] - Award the specified points to all the players in the given faction.`,
],
unmvp: 'mvp',
mvp(target, room, user, connection, cmd) {
if (!room || room.mafiaDisabled) return this.errorReply(`Mafia is disabled for this room.`);
if (room.roomid !== 'mafia') return this.errorReply(`This command can only be used in the Mafia room.`);
if (!this.can('mute', null, room)) return;
const args = target.split(',');
if (!args.length) return this.parse('/help mafia mvp');
const month = new Date().toLocaleString("en-us", {month: "numeric", year: "numeric"});
if (!logs.mvps[month]) logs.mvps[month] = {};
if (!logs.leaderboard[month]) logs.leaderboard[month] = {};
let gavePoints = false;
for (let u of args) {
u = toID(u);
if (!u) continue;
if (!gavePoints) gavePoints = true;
if (!logs.leaderboard[month][u]) logs.leaderboard[month][u] = 0;
if (!logs.mvps[month][u]) logs.mvps[month][u] = 0;
if (cmd === 'unmvp') {
logs.mvps[month][u]--;
logs.leaderboard[month][u] -= 10;
if (logs.mvps[month][u] === 0) delete logs.mvps[month][u];
if (logs.leaderboard[month][u] === 0) delete logs.leaderboard[month][u];
} else {
logs.mvps[month][u]++;
logs.leaderboard[month][u] += 10;
}
}
if (!gavePoints) return this.parse('/help mafia mvp');
writeFile(LOGS_FILE, logs);
this.modlog(`MAFIA${cmd.toUpperCase()}`, null, `MVP and 10 points were ${cmd === 'unmvp' ? 'taken from' : 'awarded to'} ${Chat.toListString(args)}`);
room.add(`MVP and 10 points were ${cmd === 'unmvp' ? 'taken from' : 'awarded to'}: ${Chat.toListString(args)}`).update();
},
mvphelp: [
`/mafia mvp [user1], [user2], ... - Gives a MVP point and 10 leaderboard points to the users specified.`,
`/mafia unmvp [user1], [user2], ... - Takes away a MVP point and 10 leaderboard points from the users specified.`,
],
hostlogs: 'leaderboard',
playlogs: 'leaderboard',
leaverlogs: 'leaderboard',
mvpladder: 'leaderboard',
lb: 'leaderboard',
leaderboard(target, room, user, connection, cmd) {
if (!room || room.mafiaDisabled) return this.errorReply(`Mafia is disabled for this room.`);
if (room.roomid !== 'mafia') return this.errorReply(`This command can only be used in the Mafia room.`);
if (['hostlogs', 'playlogs', 'leaverlogs'].includes(cmd)) {
if (!this.can('mute', null, room)) return;
} else {
// Deny broadcasting host/playlogs
if (!this.runBroadcast()) return;
}
if (cmd === 'lb') cmd = 'leaderboard';
if (this.broadcasting) return this.sendReplyBox(``);
return this.parse(`/join view-mafialadder-${cmd}`);
},
leaderboardhelp: [
`/mafia [leaderboard|mvpladder] - View the leaderboard or MVP ladder for the current or last month.`,
`/mafia [hostlogs|playlogs|leaverlogs] - View the host, play, or leaver logs for the current or last month. Requires % @ # & ~`,
],
unhostban: 'hostban',
hostban(target, room, user, connection, cmd) {
if (!room || room.mafiaDisabled) return this.errorReply(`Mafia is disabled for this room.`);
if (room.roomid !== 'mafia') return this.errorReply(`This command can only be used in the Mafia room.`);
let [targetUser, durationString] = this.splitOne(target);
targetUser = toID(targetUser);
const duration = parseInt(durationString);
if (!targetUser) return this.errorReply(`User not found.`);
if (!this.can('mute', null, room)) return false;
const isUnban = (cmd.startsWith('un'));
if (isHostBanned(targetUser) === !isUnban) return this.errorReply(`${targetUser} is ${isUnban ? 'not' : 'already'} banned from hosting games.`);
if (isUnban) {
delete hostBans[targetUser];
this.modlog(`MAFIAUNHOSTBAN`, null, `${targetUser}`);
} else {
if (isNaN(duration) || duration < 1) return this.parse('/help mafia hostban');
if (duration > 7) return this.errorReply(`Bans cannot be longer than 7 days.`);
hostBans[targetUser] = Date.now() + 1000 * 60 * 60 * 24 * duration;
this.modlog(`MAFIAHOSTBAN`, null, `${targetUser}, for ${duration} days.`);
const queueIndex = hostQueue.indexOf(targetUser);
if (queueIndex > -1) hostQueue.splice(queueIndex, 1);
}
writeFile(BANS_FILE, hostBans);
room.add(`${targetUser} was ${isUnban ? 'un' : ''}banned from hosting games${!isUnban ? ` for ${duration} days` : ''} by ${user.name}.`).update();
},
hostbanhelp: [
`/mafia hostban [user], [duration] - Ban a user from hosting games for [duration] days. Requires % @ # & ~`,
`/mafia unhostban [user] - Unbans a user from hosting games. Requires % @ # & ~`,
`/mafia hostbans - Checks current hostbans. Requires % @ # & ~`,
],
hostbans(target, room) {
if (!room || room.roomid !== 'mafia') return this.errorReply(`This command can only be used in the Mafia room.`);
if (!this.can('mute', null, room)) return;
let buf = 'Hostbanned users:';
for (const [id, date] of Object.entries(hostBans)) {
buf += ` ${id}: for ${Chat.toDurationString(date - Date.now())}`;
}
return this.sendReplyBox(buf);
},
disable(target, room, user) {
if (!room || !this.can('gamemanagement', null, room)) return;
if (room.mafiaDisabled) {
return this.errorReply("Mafia is already disabled.");
}
room.mafiaDisabled = true;
if (room.chatRoomData) {
room.chatRoomData.mafiaDisabled = true;
Rooms.global.writeChatRoomData();
}
this.modlog('MAFIADISABLE', null);
return this.sendReply("Mafia has been disabled for this room.");
},
disablehelp: [`/mafia disable - Disables mafia in this room. Requires # & ~`],
enable(target, room, user) {
if (!room || !this.can('gamemanagement', null, room)) return;
if (!room.mafiaDisabled) {
return this.errorReply("Mafia is already enabled.");
}
room.mafiaDisabled = false;
if (room.chatRoomData) {
room.chatRoomData.mafiaDisabled = false;
Rooms.global.writeChatRoomData();
}
this.modlog('MAFIAENABLE', null);
return this.sendReply("Mafia has been enabled for this room.");
},
enablehelp: [`/mafia enable - Enables mafia in this room. Requires # & ~`],
},
mafiahelp(target, room, user) {
if (!this.runBroadcast()) return;
let buf = `Commands for the Mafia Plugin Most commands are used through buttons in the game screen.
`;
buf += `General Commands`;
buf += [
` General Commands for the Mafia Plugin: `,
`/mafia host [user] - Create a game of Mafia with [user] as the host. Roomvoices can only host themselves. Requires + % @ # & ~`,
`/mafia nexthost - Host the next user in the host queue. Only works in the Mafia Room. Requires + % @ # & ~`,
`/mafia forcehost [user] - Bypass the host queue and host [user]. Only works in the Mafia Room. Requires % @ # & ~`,
`/mafia sub in - Request to sub into the game, or cancel a request to sub out.`,
`/mafia sub out - Request to sub out of the game, or cancel a request to sub in.`,
`/mafia spectate - Spectate the game of mafia.`,
`/mafia lynches - Display the current lynch count, and whos lynching who.`,
`/mafia players - Display the current list of players, will highlight players.`,
`/mafia [rl|orl] - Display the role list or the original role list for the current game.`,
`/mafia data [alignment|role|modifier|theme|term] - Get information on a mafia alignment, role, modifier, theme, or term.`,
`/mafia subhost [user] - Substitues the user as the new game host. Requires % @ # & ~`,
`/mafia cohost [user] - Adds the user as a cohost. Cohosts can talk during the game, as well as perform host actions. Requires % @ # & ~`,
`/mafia uncohost [user] - Remove [user]'s cohost status. Requires % @ # & ~`,
`/mafia disable - Disables mafia in this room. Requires # & ~`,
`/mafia enable - Enables mafia in this room. Requires # & ~`,
].join(' ');
buf += `Player Commands`;
buf += [
` Commands that players can use: `,
`/mafia join - Join the game.`,
`/mafia leave - Leave the game. Can only be done while signups are open.`,
`/mafia lynch [player|nolynch] - Vote to lynch the specified player or to not lynch anyone.`,
`/mafia unlynch - Withdraw your lynch vote. Fails if you're not voting to lynch anyone`,
`/mafia deadline - View the deadline for the current game.`,
`/mafia sub in - Request to sub into the game, or cancel a request to sub out.`,
`/mafia sub out - Request to sub out of the game, or cancel a request to sub in.`,
].join(' ');
buf += `Host Commands`;
buf += [
` Commands for game hosts and Cohosts to use: `,
`/mafia playercap [cap|none]- Limit the number of players able to join the game. Player cap cannot be more than 20 or less than 2. Requires: host % @ # & ~`,
`/mafia close - Closes signups for the current game. Requires: host % @ # & ~`,
`/mafia closedsetup [on|off] - Sets if the game is a closed setup. Closed setups don't show the role list to players. Requires host % @ # & ~`,
`/mafia reveal [on|off] - Sets if roles reveal on death or not. Requires host % @ # & ~`,
`/mafia selflynch [on|hammer|off] - Allows players to self lynch themselves either at hammer or anytime. Requires host % @ # & ~`,
`/mafia [enablenl|disablenl] - Allows or disallows players abstain from lynching. Requires host % @ # & ~`,
`/mafia forcelynch [yes/no] - Forces player's lynches onto themselves, and prevents unlynching. Requires host % @ # & ~`,
`/mafia setroles [comma seperated roles] - Set the roles for a game of mafia. You need to provide one role per player. Requires host % @ # & ~`,
`/mafia forcesetroles [comma seperated roles] - Forcibly set the roles for a game of mafia. No role PM information or alignment will be set. Requires host % @ # & ~`,
`/mafia start - Start the game of mafia. Signups must be closed. Requires host % @ # & ~`,
`/mafia day - Move to the next game day. Requires host % @ # & ~`,
`/mafia night - Move to the next game night. Requires host % @ # & ~`,
`/mafia extend (minutes) - Return to the previous game day. If (minutes) is provided, set the deadline for (minutes) minutes. Requires host % @ # & ~`,
`/mafia kill [player] - Kill a player, eliminating them from the game. Requires host % @ # & ~`,
`/mafia treestump [player] - Kills a player, but allows them to talk during the day still. Requires host % @ # & ~`,
`/mafia spirit [player] - Kills a player, but allows them to vote on the lynch still. Requires host % @ # & ~`,
`/mafia spiritstump [player] - Kills a player, but allows them to talk during the day, and vote on the lynch. Requires host % @ # & ~`,
`/mafia kick [player] - Kicks a player from the game without revealing their role. Requires host % @ # & ~`,
`/mafia silence [player] - Silences [player], preventing them from talking at all. Requires host % @ # & ~`,
`/mafia unsilence [player] - Removes a silence on [player], allowing them to talk again. Requires host % @ # & ~`,
`/mafia revive [player] - Revive a player who died or add a new player to the game. Requires host % @ # & ~`,
`/mafia deadline [minutes|off] - Sets or removes the deadline for the game. Cannot be more than 20 minutes.`,
`/mafia sub next, [player] - Forcibly sub [player] out of the game. Requires host % @ # & ~`,
`/mafia sub remove, [user] - Forcibly remove [user] from the sublist. Requres host % @ # & ~`,
`/mafia sub unrequest, [player] - Remove's a player's request to sub out of the game. Requires host % @ # & ~`,
`/mafia sub [player], [user] - Forcibly sub [player] for [user]. Requires host % @ # & ~`,
`/mafia autosub [yes|no] - Sets if players will automatically sub out if a user is on the sublist. Defaults to yes. Requires host % @ # & ~`,
`/mafia [love|hate] [player] - Makes it take 1 more (love) or less (hate) lynch to hammer [player]. Requires host % @ # & ~`,
`/mafia [unlove|unhate] [player] - Removes loved or hated status from [player]. Requires host % @ # & ~`,
`/mafia [mayor|voteless] [player] - Makes [player]'s' lynch worth 2 votes (mayor) or makes [player]'s lynch worth 0 votes (voteless). Requires host % @ # & ~`,
`/mafia [unmayor|unvoteless] [player] - Removes mayor or voteless status from [player]. Requires host % @ # & ~`,
`/mafia hammer [hammer] - sets the hammer count to [hammer] and resets lynches`,
`/mafia hammer off - disables hammering`,
`/mafia shifthammer [hammer] - sets the hammer count to [hammer] without resetting lynches`,
`/mafia resethammer - sets the hammer to the default, resetting lynches`,
`/mafia playerroles - View all the player's roles in chat. Requires host`,
`/mafia end - End the current game of mafia. Requires host % @ # & ~`,
].join(' ');
buf += `IDEA Module Commands`;
buf += [
` Commands for using IDEA modules `,
`/mafia idea [idea] - starts the IDEA module [idea]. Requires + % @ # & ~, voices can only start for themselves`,
`/mafia ideareroll - rerolls the IDEA module. Requires host % @ # & ~`,
`/mafia ideapick [selection], [role] - selects a role`,
`/mafia ideadiscards - shows the discarded roles`,
`/mafia ideadiscards off - hides discards from the players. Requires host % @ # & ~`,
`/mafia ideadiscards on - shows discards to the players. Requires host % @ # & ~`,
`/mafia customidea choices, picks (new line here, shift+enter)`,
`(comma or newline separated rolelist) - Starts an IDEA module with custom roles. Requires % @ # & ~`,
].join(' ');
buf += ``;
buf += `Mafia Room Specific Commands`;
buf += [
` Commands that are only useable in the Mafia Room: `,
`/mafia queue add, [user] - Adds the user to the host queue. Requires % @ # & ~.`,
`/mafia queue remove, [user] - Removes the user from the queue. You can remove yourself regardless of rank. Requires % @ # & ~.`,
`/mafia queue - Shows the list of users who are in queue to host.`,
`/mafia win (points) [user1], [user2], [user3], ... - Award the specified users points to the mafia leaderboard for this month. The amount of points can be negative to take points. Defaults to 10 points.`,
`/mafia winfaction (points), [faction] - Award the specified points to all the players in the given faction. Requires % @ # & ~`,
`/mafia mvp [user1], [user2], ... - Gives a MVP point and 10 leaderboard points to the users specified.`,
`/mafia unmvp [user1], [user2], ... - Takes away a MVP point and 10 leaderboard points from the users specified.`,
`/mafia [leaderboard|mvpladder] - View the leaderboard or MVP ladder for the current or last month.`,
`/mafia [hostlogs|playlogs] - View the host logs or play logs for the current or last month. Requires % @ # & ~`,
`/mafia hostban [user], [duration] - Ban a user from hosting games for [duration] days. Requires % @ # & ~`,
`/mafia unhostban [user] - Unbans a user from hosting games. Requires % @ # & ~`,
`/mafia hostbans - Checks current hostbans. Requires % @ # & ~`,
].join(' ');
buf += ``;
return this.sendReplyBox(buf);
},
};
module.exports = {
commands,
pages,
};
process.nextTick(() => {
Chat.multiLinePattern.register('/mafia customidea');
});