mirror of
https://github.com/smogon/pokemon-showdown.git
synced 2026-03-21 17:25:10 -05:00
3099 lines
139 KiB
JavaScript
3099 lines
139 KiB
JavaScript
'use strict';
|
|
|
|
/** @typedef {{[date: string]: {[userid: string]: number}}} MafiaLogTable */
|
|
/** @typedef {'leaderboard' | 'mvps' | 'hosts' | 'plays' | 'leavers'} MafiaLogSection */
|
|
/** @typedef {{leaderboard: MafiaLogTable, mvps: MafiaLogTable, hosts: MafiaLogTable, plays: MafiaLogTable, leavers: MafiaLogTable}} MafiaLog */
|
|
/** @typedef {{[k: string]: number}} MafiaHostBans */
|
|
/**
|
|
* @typedef {Object} MafiaRole
|
|
* @property {string} name
|
|
* @property {string} safeName
|
|
* @property {string} id
|
|
* @property {string[]} memo
|
|
* @property {string} alignment
|
|
* @property {string} image
|
|
*/
|
|
/**
|
|
* @typedef {Object} MafiaParsedRole
|
|
* @property {MafiaRole} role
|
|
* @property {string[]} problems
|
|
*/
|
|
/**
|
|
* @typedef {Object} MafiaLynch
|
|
* @property {number} count
|
|
* @property {number} trueCount
|
|
* @property {number} lastLynch
|
|
* @property {string} dir
|
|
* @property {string[]} lynchers
|
|
*/
|
|
/**
|
|
* @typedef {Object} MafiaIDEAdata
|
|
* @property {string} name
|
|
* @property {boolean?} untrusted
|
|
* @property {string[]} roles
|
|
* @property {number} choices
|
|
* @property {string[]} picks
|
|
*/
|
|
/**
|
|
* @typedef {Object} MafiaIDEAModule
|
|
* @property {MafiaIDEAdata?} data
|
|
* @property {NodeJS.Timer?} timer
|
|
* @property {string} discardsHtml
|
|
* @property {string[]} waitingPick
|
|
*/
|
|
/**
|
|
* @typedef {Object} MafiaIDEAplayerData
|
|
* @property {string[]} choices
|
|
* @property {string[]} originalChoices
|
|
* @property {Object} picks
|
|
*/
|
|
|
|
const FS = require('./../lib/fs');
|
|
const LOGS_FILE = 'config/chat-plugins/mafia-logs.json';
|
|
const BANS_FILE = 'config/chat-plugins/mafia-bans.json';
|
|
const MafiaData = require('./mafia-data.js');
|
|
/** @type {MafiaLog} */
|
|
let logs = {leaderboard: {}, mvps: {}, hosts: {}, plays: {}, leavers: {}};
|
|
/** @type {MafiaHostBans} */
|
|
let hostBans = Object.create(null);
|
|
/** @type {string[]} */
|
|
let hostQueue = [];
|
|
|
|
const IDEA_TIMER = 120 * 1000;
|
|
|
|
/**
|
|
* @param {string} name
|
|
*/
|
|
function readFile(name) {
|
|
try {
|
|
const json = FS(name).readIfExistsSync();
|
|
if (!json) {
|
|
writeFile(name, "{}");
|
|
return false;
|
|
}
|
|
return Object.assign(Object.create(null), JSON.parse(json));
|
|
} catch (e) {
|
|
if (e.code !== 'ENOENT') throw e;
|
|
}
|
|
}
|
|
/**
|
|
* @param {string} name
|
|
* @param {object} data
|
|
*/
|
|
function writeFile(name, data) {
|
|
FS(name).writeUpdate(() => (
|
|
JSON.stringify(data)
|
|
));
|
|
}
|
|
|
|
// Load logs
|
|
logs = readFile(LOGS_FILE);
|
|
if (!logs) logs = {leaderboard: {}, mvps: {}, hosts: {}, plays: {}, leavers: {}};
|
|
/** @type {MafiaLogSection[]} */
|
|
const tables = ['leaderboard', 'mvps', 'hosts', 'plays', 'leavers'];
|
|
for (const section of tables) {
|
|
// Check to see if we need to eliminate an old month's data.
|
|
const month = new Date().toLocaleString("en-us", {month: "numeric", year: "numeric"});
|
|
if (!logs[section]) logs[section] = {};
|
|
if (!logs[section][month]) logs[section][month] = {};
|
|
if (Object.keys(logs[section]).length >= 3) {
|
|
// eliminate the oldest month(s)
|
|
let keys = Object.keys(logs[section]).sort((aKey, bKey) => {
|
|
const a = aKey.split('/');
|
|
const b = bKey.split('/');
|
|
if (a[1] !== b[1]) {
|
|
// year
|
|
if (parseInt(a[1]) < parseInt(b[1])) return -1;
|
|
return 1;
|
|
}
|
|
// month
|
|
if (parseInt(a[0]) < parseInt(b[0])) return -1;
|
|
return 1;
|
|
});
|
|
while (keys.length > 2) {
|
|
const curKey = keys.shift();
|
|
if (!curKey) break; // should never happen
|
|
delete logs[section][curKey];
|
|
}
|
|
}
|
|
}
|
|
writeFile(LOGS_FILE, logs);
|
|
|
|
// Load bans
|
|
hostBans = readFile(BANS_FILE);
|
|
if (!hostBans) hostBans = Object.create(null);
|
|
|
|
for (const userid in hostBans) {
|
|
if (hostBans[userid] < Date.now()) {
|
|
delete hostBans[userid];
|
|
}
|
|
}
|
|
writeFile(BANS_FILE, hostBans);
|
|
|
|
/**
|
|
* @param {string} userid
|
|
*/
|
|
function isHostBanned(userid) {
|
|
if (!(userid in hostBans)) return false;
|
|
if (hostBans[userid] < Date.now()) {
|
|
delete hostBans[userid];
|
|
writeFile(BANS_FILE, hostBans);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
class MafiaPlayer extends Rooms.RoomGamePlayer {
|
|
/**
|
|
* @param {User} user
|
|
* @param {RoomGame} game
|
|
*/
|
|
constructor(user, game) {
|
|
super(user, game);
|
|
this.safeName = Chat.escapeHTML(this.name);
|
|
/** @type {MafiaRole?} */
|
|
this.role = null;
|
|
this.lynching = '';
|
|
this.lastLynch = 0;
|
|
this.treestump = false;
|
|
this.restless = false;
|
|
/** @type {MafiaIDEAplayerData?} */
|
|
this.IDEA = null;
|
|
}
|
|
|
|
/**
|
|
* @param {boolean} button
|
|
*/
|
|
getRole(button = false) {
|
|
if (!this.role) return;
|
|
let color = MafiaData.alignments[this.role.alignment].color;
|
|
if (button && MafiaData.alignments[this.role.alignment].buttonColor) color = MafiaData.alignments[this.role.alignment].buttonColor;
|
|
return `<span style="font-weight:bold;color:${color}">${this.role.safeName}</span>`;
|
|
}
|
|
|
|
updateHtmlRoom() {
|
|
const user = Users(this.userid);
|
|
if (!user || !user.connected) return;
|
|
if (this.game.ended) return user.send(`>view-mafia-${this.game.room.id}\n|deinit`);
|
|
// @ts-ignore
|
|
const buf = Chat.pages.mafia([this.game.room.id], user);
|
|
this.send(`>view-mafia-${this.game.room.id}\n|init|html\n${buf}`);
|
|
}
|
|
}
|
|
|
|
class MafiaTracker extends Rooms.RoomGame {
|
|
/**
|
|
* @param {ChatRoom} room
|
|
* @param {User} host
|
|
*/
|
|
constructor(room, host) {
|
|
super(room);
|
|
|
|
this.gameid = 'mafia';
|
|
this.title = 'Mafia';
|
|
this.playerCap = 20;
|
|
this.allowRenames = false;
|
|
this.started = false;
|
|
this.ended = false;
|
|
/** @type {Object?} */
|
|
this.theme = null;
|
|
|
|
this.hostid = host.userid;
|
|
this.host = Chat.escapeHTML(host.name);
|
|
/** @type {string[]} */
|
|
this.cohosts = [];
|
|
|
|
/** @type {{[userid: string]: MafiaPlayer}} */
|
|
this.players = Object.create(null);
|
|
/** @type {{[userid: string]: MafiaPlayer}} */
|
|
this.dead = Object.create(null);
|
|
/** @type {string[]} */
|
|
this.subs = [];
|
|
this.autoSub = true;
|
|
/** @type {string[]} */
|
|
this.requestedSub = [];
|
|
/** @type {string[]} */
|
|
this.hostRequestedSub = [];
|
|
/** @type {string[]} */
|
|
this.played = [];
|
|
|
|
this.hammerCount = 0;
|
|
/** @type {{[userid: string]: MafiaLynch}} */
|
|
this.lynches = Object.create(null);
|
|
/** @type {{[userid: string]: number}} */
|
|
this.lynchModifiers = Object.create(null);
|
|
/** @type {{[userid: string]: number}} */
|
|
this.hammerModifiers = Object.create(null);
|
|
/** @type {string?} */
|
|
this.hasPlurality = null;
|
|
/** @type {boolean} */
|
|
this.enableNL = true;
|
|
|
|
/** @type {MafiaRole[]} */
|
|
this.originalRoles = [];
|
|
this.originalRoleString = '';
|
|
/** @type {MafiaRole[]} */
|
|
this.roles = [];
|
|
this.roleString = '';
|
|
|
|
/** @type {"signups" | "locked" | "IDEApicking" | "IDEAlocked" | "day" | "night"} */
|
|
this.phase = "signups";
|
|
this.dayNum = 0;
|
|
this.closedSetup = false;
|
|
this.noReveal = false;
|
|
/** @type {(boolean | "hammer")} */
|
|
this.selfEnabled = false;
|
|
/** @type {NodeJS.Timer?} */
|
|
this.timer = null;
|
|
/** @type {number} */
|
|
this.dlAt = 0;
|
|
|
|
/** @type {MafiaIDEAModule} */
|
|
this.IDEA = {
|
|
data: null,
|
|
timer: null,
|
|
discardsHtml: '',
|
|
waitingPick: [],
|
|
};
|
|
|
|
this.sendRoom(this.roomWindow(), {uhtml: true});
|
|
}
|
|
|
|
/**
|
|
* @param {User} user
|
|
* @return {void}
|
|
*/
|
|
join(user) {
|
|
if (this.phase !== 'signups') return user.sendTo(this.room, `|error|The game of ${this.title} has already started.`);
|
|
const canJoin = this.canJoin(user, true);
|
|
if (canJoin) return user.sendTo(this.room, `|error|${canJoin}`);
|
|
if (!this.addPlayer(user)) return user.sendTo(this.room, `|error|You have already joined the game of ${this.title}.`);
|
|
if (this.subs.includes(user.userid)) this.subs.splice(this.subs.indexOf(user.userid), 1);
|
|
this.players[user.userid].updateHtmlRoom();
|
|
this.sendRoom(`${this.players[user.userid].name} has joined the game.`);
|
|
}
|
|
|
|
/**
|
|
* @param {User} user
|
|
* @return {void}
|
|
*/
|
|
leave(user) {
|
|
if (!(user.userid in this.players)) return user.sendTo(this.room, `|error|You have not joined the game of ${this.title}.`);
|
|
if (this.phase !== 'signups') return user.sendTo(this.room, `|error|The game of ${this.title} has already started.`);
|
|
this.players[user.userid].destroy();
|
|
delete this.players[user.userid];
|
|
this.playerCount--;
|
|
let subIndex = this.requestedSub.indexOf(user.userid);
|
|
if (subIndex !== -1) this.requestedSub.splice(subIndex, 1);
|
|
subIndex = this.hostRequestedSub.indexOf(user.userid);
|
|
if (subIndex !== -1) this.hostRequestedSub.splice(subIndex, 1);
|
|
this.sendRoom(`${user.name} has left the game.`);
|
|
// @ts-ignore
|
|
user.send(`>view-mafia-${this.room.id}\n|init|html\n${Chat.pages.mafia([this.room.id], user)}`);
|
|
}
|
|
|
|
/**
|
|
* @param {User} user
|
|
* @return {MafiaPlayer}
|
|
*/
|
|
makePlayer(user) {
|
|
return new MafiaPlayer(user, this);
|
|
}
|
|
|
|
/**
|
|
* @param {User} user
|
|
* @param {string} roleString
|
|
* @param {boolean} force
|
|
* @param {boolean} reset
|
|
* @return {void}
|
|
*/
|
|
setRoles(user, roleString, force = false, reset = false) {
|
|
let roles = (/** @type {string[]} */roleString.split(',').map(x => x.trim()));
|
|
if (roles.length === 1) {
|
|
// Attempt to set roles from a theme
|
|
let theme = MafiaData.themes[toId(roles[0])];
|
|
if (typeof theme === 'string') theme = MafiaData.themes[theme];
|
|
if (typeof theme !== 'object') return user.sendTo(this.room, `|error|The theme "${roles[0]}" was not found.`);
|
|
if (!theme[this.playerCount]) return user.sendTo(this.room, `|error|The theme "${theme.name}" does not have a role list for ${this.playerCount} players.`);
|
|
/** @type {string} */
|
|
let themeRoles = theme[this.playerCount].slice();
|
|
roles = themeRoles.split(',').map(x => x.trim());
|
|
this.theme = theme;
|
|
} else {
|
|
this.theme = null;
|
|
}
|
|
if (roles.length < this.playerCount) {
|
|
return user.sendTo(this.room, `|error|You have not provided enough roles for the players.`);
|
|
} else if (roles.length > this.playerCount) {
|
|
user.sendTo(this.room, `|error|You have provided too many roles, ${roles.length - this.playerCount} ${Chat.plural(roles.length - this.playerCount, 'roles', 'role')} will not be assigned.`);
|
|
}
|
|
|
|
if (force) {
|
|
this.originalRoles = roles.map(r => {
|
|
return {
|
|
name: r,
|
|
safeName: Chat.escapeHTML(r),
|
|
id: toId(r),
|
|
alignment: 'solo',
|
|
image: '',
|
|
memo: [`To learn more about your role, PM the host (${this.host}).`],
|
|
};
|
|
});
|
|
this.roles = this.originalRoles.slice();
|
|
this.originalRoleString = this.originalRoles.slice().map(r => `<span style="font-weight:bold;color:${MafiaData.alignments[r.alignment].color || '#FFF'}">${r.safeName}</span>`).join(', ');
|
|
this.roleString = this.originalRoleString;
|
|
return this.sendRoom(`The roles have been set.`);
|
|
}
|
|
|
|
let newRoles = [];
|
|
/** @type {string[]} */
|
|
let problems = [];
|
|
/** @type {string[]} */
|
|
let alignments = [];
|
|
/** @type {{[k: string]: MafiaRole}} */
|
|
let cache = Object.create(null);
|
|
for (const string of roles) {
|
|
const roleId = string.toLowerCase().replace(/[^\w\d\s]/g, '');
|
|
if (roleId in cache) {
|
|
newRoles.push(Object.assign(Object.create(null), cache[roleId]));
|
|
} else {
|
|
const role = MafiaTracker.parseRole(string);
|
|
if (role.problems.length) problems = problems.concat(role.problems);
|
|
if (alignments.indexOf(role.role.alignment) === -1) alignments.push(role.role.alignment);
|
|
cache[roleId] = role.role;
|
|
newRoles.push(role.role);
|
|
}
|
|
}
|
|
if (alignments.length < 2 && alignments[0] !== 'solo') problems.push(`There must be at least 2 different alignments in a game!`);
|
|
if (problems.length) {
|
|
for (const problem of problems) {
|
|
user.sendTo(this.room, `|error|${problem}`);
|
|
}
|
|
return user.sendTo(this.room, `|error|To forcibly set the roles, use /mafia force${reset ? "re" : ""}setroles`);
|
|
}
|
|
|
|
this.IDEA.data = null;
|
|
|
|
this.originalRoles = newRoles;
|
|
this.roles = this.originalRoles.slice();
|
|
this.originalRoleString = this.originalRoles.slice().map(r => `<span style="font-weight:bold;color:${MafiaData.alignments[r.alignment].color || '#FFF'}">${r.safeName}</span>`).join(', ');
|
|
this.roleString = this.originalRoleString;
|
|
if (!reset) this.phase = 'locked';
|
|
this.updatePlayers();
|
|
this.sendRoom(`The roles have been ${reset ? 're' : ''}set.`);
|
|
if (reset) this.distributeRoles();
|
|
}
|
|
|
|
/**
|
|
* Parses a single role into an object
|
|
* @param {string} roleString
|
|
* @return {MafiaParsedRole}
|
|
*/
|
|
static parseRole(roleString) {
|
|
/** @type {MafiaRole} */
|
|
let role = {
|
|
name: roleString.split(' ').map(p => toId(p) === 'solo' ? '' : p).join(' '),
|
|
safeName: '', // MAKE SURE THESE ARE SET BELOW
|
|
id: '',
|
|
image: '',
|
|
memo: ['During the Day, you may vote for whomever you want lynched.'],
|
|
alignment: '',
|
|
};
|
|
roleString = roleString.replace(/\s*\(.*?\)\s*/g, ' ');
|
|
let target = roleString.toLowerCase().replace(/[^\w\d\s]/g, '').split(' ');
|
|
let problems = [];
|
|
role.safeName = Chat.escapeHTML(role.name);
|
|
role.id = toId(role.name);
|
|
for (let key in MafiaData.roles) {
|
|
if (key.includes('_')) {
|
|
let roleKey = target.slice().map(toId).join('_');
|
|
if (roleKey.includes(key)) {
|
|
let originalKey = key;
|
|
if (typeof MafiaData.roles[key] === 'string') key = MafiaData.roles[key];
|
|
if (!role.image && MafiaData.roles[key].image) role.image = MafiaData.roles[key].image;
|
|
if (MafiaData.roles[key].alignment) {
|
|
if (role.alignment && role.alignment !== MafiaData.roles[key].alignment) {
|
|
// A role cant have multiple alignments
|
|
problems.push(`The role "${role.name}" has multiple possible alignments (${MafiaData.roles[key].alignment} or ${role.alignment})`);
|
|
break;
|
|
}
|
|
role.alignment = MafiaData.roles[key].alignment;
|
|
}
|
|
if (MafiaData.roles[key].memo) role.memo = role.memo.concat(MafiaData.roles[key].memo);
|
|
let index = roleKey.split('_').indexOf(originalKey.split('_')[0]);
|
|
target.splice(index, originalKey.split('_').length);
|
|
}
|
|
} else if (target.includes(key)) {
|
|
let index = target.indexOf(key);
|
|
if (typeof MafiaData.roles[key] === 'string') key = MafiaData.roles[key];
|
|
if (!role.image && MafiaData.roles[key].image) role.image = MafiaData.roles[key].image;
|
|
if (MafiaData.roles[key].memo) role.memo = role.memo.concat(MafiaData.roles[key].memo);
|
|
target.splice(index, 1);
|
|
}
|
|
}
|
|
// Add modifiers
|
|
for (let key in MafiaData.modifiers) {
|
|
if (key.includes('_')) {
|
|
let roleKey = target.slice().map(toId).join('_');
|
|
if (roleKey.includes(key)) {
|
|
if (typeof MafiaData.modifiers[key] === 'string') key = MafiaData.modifiers[key];
|
|
if (!role.image && MafiaData.modifiers[key].image) role.image = MafiaData.modifiers[key].image;
|
|
if (MafiaData.modifiers[key].memo) role.memo = role.memo.concat(MafiaData.modifiers[key].memo);
|
|
let index = roleKey.split('_').indexOf(key.split('_')[0]);
|
|
target.splice(index, key.split('_').length);
|
|
}
|
|
} else if (key === 'xshot') {
|
|
// Special case for X-Shot modifier
|
|
for (let [i, xModifier] of target.entries()) {
|
|
if (toId(xModifier).endsWith('shot')) {
|
|
let num = parseInt(toId(xModifier).substring(0, toId(xModifier).length - 4));
|
|
if (isNaN(num)) continue;
|
|
let memo = MafiaData.modifiers.xshot.memo.slice();
|
|
memo = memo.map((/** @type {string} */m) => m.replace(/X/g, num.toString()));
|
|
role.memo = role.memo.concat(memo);
|
|
target.splice(i, 1);
|
|
i--;
|
|
}
|
|
}
|
|
} else if (target.includes(key)) {
|
|
let index = target.indexOf(key);
|
|
if (typeof MafiaData.modifiers[key] === 'string') key = MafiaData.modifiers[key];
|
|
if (!role.image && MafiaData.modifiers[key].image) role.image = MafiaData.modifiers[key].image;
|
|
if (MafiaData.modifiers[key].memo) role.memo = role.memo.concat(MafiaData.modifiers[key].memo);
|
|
target.splice(index, 1);
|
|
}
|
|
}
|
|
// Determine the role's alignment
|
|
for (let [j, targetId] of target.entries()) {
|
|
let id = toId(targetId);
|
|
if (MafiaData.alignments[id]) {
|
|
if (typeof MafiaData.alignments[id] === 'string') id = MafiaData.alignments[id];
|
|
if (role.alignment && role.alignment !== MafiaData.alignments[id].id) {
|
|
// A role cant have multiple alignments
|
|
problems.push(`The role "${role.name}" has multiple possible alignments (${MafiaData.alignments[id].id} or ${role.alignment})`);
|
|
break;
|
|
}
|
|
role.alignment = MafiaData.alignments[id].id;
|
|
role.memo = role.memo.concat(MafiaData.alignments[id].memo);
|
|
if (!role.image && MafiaData.alignments[id].image) role.image = MafiaData.alignments[id].image;
|
|
target.splice(j, 1);
|
|
j--;
|
|
}
|
|
}
|
|
if (!role.alignment) {
|
|
// Default to town
|
|
role.alignment = 'town';
|
|
role.memo = role.memo.concat(MafiaData.alignments.town.memo);
|
|
}
|
|
// Handle anything that is unknown
|
|
if (target.length) {
|
|
role.memo.push(`To learn more about your role, PM the host.`);
|
|
}
|
|
return {role, problems};
|
|
}
|
|
|
|
/**
|
|
* @param {User} user
|
|
* @return {void}
|
|
*/
|
|
start(user) {
|
|
if (!user) return;
|
|
if (this.phase !== 'locked' && this.phase !== 'IDEAlocked') {
|
|
if (this.phase === 'signups') return user.sendTo(this.room, `You need to close the signups first.`);
|
|
if (this.phase === 'IDEApicking') return user.sendTo(this.room, `You must wait for IDEA picks to finish before starting.`);
|
|
return user.sendTo(this.room, `The game is already started!`);
|
|
}
|
|
if (this.playerCount < 2) return user.sendTo(this.room, `You need at least 2 players to start.`);
|
|
if (this.phase === 'IDEAlocked') {
|
|
for (const p in this.players) {
|
|
if (!this.players[p].role) return user.sendTo(this.room, `|error|Not all players have a role.`);
|
|
}
|
|
} else {
|
|
if (!Object.keys(this.roles).length) return user.sendTo(this.room, `You need to set the roles before starting.`);
|
|
if (Object.keys(this.roles).length < this.playerCount) return user.sendTo(this.room, `You have not provided enough roles for the players.`);
|
|
}
|
|
this.started = true;
|
|
this.sendRoom(`The game of ${this.title} is starting!`, {declare: true});
|
|
// MafiaTracker#played gets set in distributeRoles
|
|
this.distributeRoles();
|
|
this.day(null, true);
|
|
if (this.IDEA.data) this.room.add(`|html|<div class="infobox"><details><summary>IDEA discards:</summary>${this.IDEA.discardsHtml}</details></div>`).update();
|
|
}
|
|
|
|
/**
|
|
* @return {void}
|
|
*/
|
|
distributeRoles() {
|
|
let roles = Dex.shuffle(this.roles.slice());
|
|
if (roles.length) {
|
|
for (let p in this.players) {
|
|
let role = roles.shift();
|
|
this.players[p].role = role;
|
|
let u = Users(p);
|
|
if (u && u.connected) u.send(`>${this.room.id}\n|notify|Your role is ${role.safeName}. For more details of your role, check your Role PM.`);
|
|
}
|
|
}
|
|
this.dead = {};
|
|
this.played = [this.hostid, ...this.cohosts, ...Object.keys(this.players)];
|
|
this.sendRoom(`The roles have been distributed.`, {declare: true});
|
|
this.updatePlayers();
|
|
}
|
|
|
|
/**
|
|
* @param {string} alignment
|
|
* @param {MafiaPlayer} player
|
|
* @return {string}
|
|
*/
|
|
getPartners(alignment, player) {
|
|
if (!player || !player.role || ['town', 'solo'].includes(player.role.alignment)) return "";
|
|
let partners = [];
|
|
for (let p in this.players) {
|
|
if (p === player.userid) continue;
|
|
const role = this.players[p].role;
|
|
if (role && role.alignment === player.role.alignment) partners.push(this.players[p].name);
|
|
}
|
|
return partners.join(", ");
|
|
}
|
|
|
|
/**
|
|
* @param {number?} extension
|
|
* @param {boolean} initial
|
|
* @return {void}
|
|
*/
|
|
day(extension = null, initial = false) {
|
|
if (this.phase !== 'night' && !initial) return;
|
|
if (this.timer) this.setDeadline(0);
|
|
if (extension === null) {
|
|
this.hammerCount = Math.floor(Object.keys(this.players).length / 2) + 1;
|
|
this.lynches = Object.create(null);
|
|
this.hasPlurality = null;
|
|
this.clearLynches();
|
|
}
|
|
this.phase = 'day';
|
|
if (extension !== null && !initial) {
|
|
// Day stays same
|
|
this.setDeadline(extension);
|
|
} else {
|
|
this.dayNum++;
|
|
}
|
|
this.sendRoom(`Day ${this.dayNum}. The hammer count is set at ${this.hammerCount}`, {declare: true});
|
|
this.sendPlayerList();
|
|
this.updatePlayers();
|
|
}
|
|
|
|
/**
|
|
* @param {boolean} early
|
|
* @return {void}
|
|
*/
|
|
night(early = false) {
|
|
if (this.phase !== 'day') return;
|
|
if (this.timer) this.setDeadline(0, true);
|
|
this.phase = 'night';
|
|
for (const hostid of [...this.cohosts, this.hostid]) {
|
|
let host = Users(hostid);
|
|
if (host && host.connected) host.send(`>${this.room.id}\n|notify|It's night in your game of Mafia!`);
|
|
}
|
|
this.sendRoom(`Night ${this.dayNum}. PM the host your action, or idle.`, {declare: true});
|
|
const hasPlurality = this.getPlurality();
|
|
if (!early && hasPlurality) this.sendRoom(`Plurality is on ${this.players[hasPlurality] ? this.players[hasPlurality].name : 'No Lynch'}`);
|
|
if (!early) this.sendRoom(`|raw|<div class="infobox">${this.lynchBox()}</div>`);
|
|
this.updatePlayers();
|
|
}
|
|
|
|
/**
|
|
* @param {string} userid
|
|
* @param {string} target
|
|
* @return {void}
|
|
*/
|
|
lynch(userid, target) {
|
|
if (this.phase !== 'day') return this.sendUser(userid, `|error|You can only lynch during the day.`);
|
|
let player = this.players[userid];
|
|
if (!player && this.dead[userid] && this.dead[userid].restless) player = this.dead[userid];
|
|
if (!player) return;
|
|
if (!(target in this.players) && target !== 'nolynch') return this.sendUser(userid, `|error|${target} is not a valid player.`);
|
|
if (!this.enableNL && target === 'nolynch') return this.sendUser(userid, `|error|No Lynch is not allowed.`);
|
|
if (target === player.userid && !this.selfEnabled) return this.sendUser(userid, `|error|Self lynching is not allowed.`);
|
|
if (target === player.userid && (this.hammerCount - 1 > (this.lynches[target] ? this.lynches[target].count : 0)) && this.selfEnabled === 'hammer') return this.sendUser(userid, `|error|You may only lynch yourself when you placing the hammer vote.`);
|
|
if (player.lastLynch + 2000 >= Date.now()) return this.sendUser(userid, `|error|You must wait another ${Chat.toDurationString((player.lastLynch + 2000) - Date.now()) || '1 second'} before you can change your lynch.`);
|
|
const previousLynch = player.lynching;
|
|
if (previousLynch) this.unlynch(userid, true);
|
|
let lynch = this.lynches[target];
|
|
if (!lynch) {
|
|
this.lynches[target] = {count: 1, trueCount: this.getLynchValue(userid), lastLynch: Date.now(), dir: 'up', lynchers: [userid]};
|
|
lynch = this.lynches[target];
|
|
} else {
|
|
lynch.count++;
|
|
lynch.trueCount += this.getLynchValue(userid);
|
|
lynch.lastLynch = Date.now();
|
|
lynch.dir = 'up';
|
|
lynch.lynchers.push(userid);
|
|
}
|
|
player.lynching = target;
|
|
let name = player.lynching === 'nolynch' ? 'No Lynch' : this.players[player.lynching].name;
|
|
const targetUser = Users(userid);
|
|
if (previousLynch) {
|
|
this.sendRoom(`${(targetUser ? targetUser.name : userid)} has shifted their lynch from ${previousLynch === 'nolynch' ? 'No Lynch' : this.players[previousLynch].name} to ${name}`, {timestamp: true});
|
|
} else {
|
|
this.sendRoom(name === 'No Lynch' ? `${(targetUser ? targetUser.name : userid)} has abstained from lynching.` : `${(targetUser ? targetUser.name : userid)} has lynched ${name}.`, {timestamp: true});
|
|
}
|
|
player.lastLynch = Date.now();
|
|
if (this.getHammerValue(target) <= lynch.trueCount) {
|
|
// HAMMER
|
|
this.sendRoom(`Hammer! ${target === 'nolynch' ? 'Nobody' : Chat.escapeHTML(name)} was lynched!`, {declare: true});
|
|
this.sendRoom(`|raw|<div class="infobox">${this.lynchBox()}</div>`);
|
|
if (target !== 'nolynch') this.eliminate(this.players[target], 'kill');
|
|
this.night(true);
|
|
return;
|
|
}
|
|
this.hasPlurality = null;
|
|
player.updateHtmlRoom();
|
|
}
|
|
|
|
/**
|
|
* @param {string} userid
|
|
* @param {boolean} force
|
|
* @return {void}
|
|
*/
|
|
unlynch(userid, force = false) {
|
|
if (this.phase !== 'day' && !force) return this.sendUser(userid, `|error|You can only lynch during the day.`);
|
|
let player = this.players[userid];
|
|
if (!player && this.dead[userid] && this.dead[userid].restless) player = this.dead[userid];
|
|
if (!player || !player.lynching) return this.sendUser(userid, `|error|You are not lynching anyone.`);
|
|
if (player.lastLynch + 2000 >= Date.now() && !force) return this.sendUser(userid, `|error|You must wait another ${Chat.toDurationString((player.lastLynch + 2000) - Date.now()) || '1 second'} before you can change your lynch.`);
|
|
let lynch = this.lynches[player.lynching];
|
|
lynch.count--;
|
|
lynch.trueCount -= this.getLynchValue(userid);
|
|
if (lynch.count <= 0) {
|
|
delete this.lynches[player.lynching];
|
|
} else {
|
|
lynch.lastLynch = Date.now();
|
|
lynch.dir = 'down';
|
|
lynch.lynchers.splice(lynch.lynchers.indexOf(userid), 1);
|
|
}
|
|
const targetUser = Users(userid);
|
|
if (!force) this.sendRoom(player.lynching === 'nolynch' ? `${(targetUser ? targetUser.name : userid)} is no longer abstaining from lynching.` : `${(targetUser ? targetUser.name : userid)} has unlynched ${this.players[player.lynching].name}.`, {timestamp: true});
|
|
player.lynching = '';
|
|
player.lastLynch = Date.now();
|
|
this.hasPlurality = null;
|
|
player.updateHtmlRoom();
|
|
}
|
|
|
|
/**
|
|
* Returns HTML code that contains information on the current lynch vote.
|
|
* @return {string}
|
|
*/
|
|
lynchBox() {
|
|
if (!this.started) return `<strong>The game has not started yet.</strong>`;
|
|
let buf = `<strong>Lynches (Hammer: ${this.hammerCount})</strong><br />`;
|
|
const plur = this.getPlurality();
|
|
const list = Object.keys(this.lynches).sort((a, b) => {
|
|
if (a === plur) return -1;
|
|
if (b === plur) return 1;
|
|
return this.lynches[b].count - this.lynches[a].count;
|
|
});
|
|
for (const key of list) {
|
|
buf += `${this.lynches[key].count}${plur === key ? '*' : ''} ${this.players[key] ? this.players[key].safeName : 'No Lynch'} (${this.lynches[key].lynchers.map(a => this.players[a] ? this.players[a].safeName : a).join(', ')})<br />`;
|
|
}
|
|
return buf;
|
|
}
|
|
|
|
/**
|
|
* @param {User} user
|
|
* @param {string} target
|
|
* @param {number} mod
|
|
*/
|
|
applyLynchModifier(user, target, mod) {
|
|
const targetPlayer = this.players[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.players || 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.players), ...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.players)]) {
|
|
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.players).length / 2) + 1);
|
|
}
|
|
|
|
/**
|
|
* @param {number} count
|
|
* @return {void}
|
|
*/
|
|
setHammer(count) {
|
|
this.hammerCount = count;
|
|
this.sendRoom(`The hammer count has been set at ${this.hammerCount}, and lynches have been reset.`, {declare: true});
|
|
this.lynches = Object.create(null);
|
|
this.hasPlurality = null;
|
|
this.clearLynches();
|
|
}
|
|
|
|
/**
|
|
* @param {number} count
|
|
* @return {void}
|
|
*/
|
|
shiftHammer(count) {
|
|
this.hammerCount = count;
|
|
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.userid in this.players)) 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.userid)) this.hostRequestedSub.splice(this.hostRequestedSub.indexOf(player.userid), 1);
|
|
if (this.requestedSub.includes(player.userid)) this.requestedSub.splice(this.requestedSub.indexOf(player.userid), 1);
|
|
player.destroy();
|
|
delete this.players[player.userid];
|
|
this.playerCount--;
|
|
player.updateHtmlRoom();
|
|
return;
|
|
}
|
|
this.dead[player.userid] = player;
|
|
let msg = `${player.safeName}`;
|
|
switch (ability) {
|
|
case 'treestump':
|
|
this.dead[player.userid].treestump = true;
|
|
msg += ` has been treestumped`;
|
|
break;
|
|
case 'spirit':
|
|
this.dead[player.userid].restless = true;
|
|
msg += ` became a restless spirit`;
|
|
break;
|
|
case 'spiritstump':
|
|
this.dead[player.userid].treestump = true;
|
|
this.dead[player.userid].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.userid, 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.userid);
|
|
delete this.players[player.userid];
|
|
let subIndex = this.requestedSub.indexOf(player.userid);
|
|
if (subIndex !== -1) this.requestedSub.splice(subIndex, 1);
|
|
subIndex = this.hostRequestedSub.indexOf(player.userid);
|
|
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.players) {
|
|
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.players[deadPlayer.userid] = 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);
|
|
}
|
|
delete this.dead[deadPlayer.userid];
|
|
} else {
|
|
const targetUser = Users(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.userid)) this.subs.splice(this.subs.indexOf(targetUser.userid), 1);
|
|
this.played.push(targetUser.userid);
|
|
this.players[targetUser.userid] = 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.players[player];
|
|
if (!oldPlayer) return; // should never happen
|
|
|
|
const newUser = Users(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.userid), 1);
|
|
lynch.lynchers.push(newPlayer.userid);
|
|
newPlayer.lynching = oldPlayer.lynching;
|
|
oldPlayer.lynching = '';
|
|
}
|
|
this.players[newPlayer.userid] = newPlayer;
|
|
this.players[oldPlayer.userid].destroy();
|
|
delete this.players[oldPlayer.userid];
|
|
// Transfer lynches on the old player to the new one
|
|
if (this.lynches[oldPlayer.userid]) {
|
|
this.lynches[newPlayer.userid] = this.lynches[oldPlayer.userid];
|
|
delete this.lynches[oldPlayer.userid];
|
|
for (let p in this.players) {
|
|
if (this.players[p].lynching === oldPlayer.userid) this.players[p].lynching = newPlayer.userid;
|
|
}
|
|
for (let p in this.dead) {
|
|
if (this.dead[p].restless && this.dead[p].lynching === oldPlayer.userid) this.dead[p].lynching = newPlayer.userid;
|
|
}
|
|
}
|
|
if (newUser && newUser.connected) {
|
|
// @ts-ignore
|
|
newUser.send(`>view-mafia-${this.room.id}\n|init|html\n${Chat.pages.mafia([this.room.id], newUser)}`);
|
|
newUser.send(`>${this.room.id}\n|notify|You have been substituted in the mafia game for ${oldPlayer.safeName}.`);
|
|
}
|
|
if (this.started) this.played.push(newPlayer.userid);
|
|
this.sendRoom(`${oldPlayer.safeName} has been subbed out. ${newPlayer.safeName} has joined the game.`, {declare: true});
|
|
this.updatePlayers();
|
|
|
|
if (this.room.id === '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(nextSub, true);
|
|
if (!sub || !sub.connected || !sub.named || !this.room.users[sub.userid]) 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.userid);
|
|
}
|
|
|
|
/**
|
|
* @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.players) {
|
|
const player = this.players[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(p);
|
|
// @ts-ignore guaranteed at this point
|
|
if (u && u.connected) u.send(`>${this.room.id}\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.players[user.userid];
|
|
if (!player.IDEA) return this.sendRoom(`Trying to pick an IDEA role with no player IDEA object, user: ${user.userid}. 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(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] = '';
|
|
}
|
|
|
|
if (player.IDEA.picks[selection[0]]) {
|
|
buf += `You have deselected ${player.IDEA.picks[selection[0]]}. `;
|
|
player.IDEA.choices.push(player.IDEA.picks[selection[0]]);
|
|
}
|
|
|
|
if (player.IDEA.picks[selection[0]] && !selection[1]) {
|
|
this.IDEA.waitingPick.push(player.userid);
|
|
} else if (!player.IDEA.picks[selection[0]] && selection[1]) {
|
|
this.IDEA.waitingPick.splice(this.IDEA.waitingPick.indexOf(player.userid), 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.players) {
|
|
const player = this.players[p];
|
|
if (!player.IDEA) return this.sendRoom(`Trying to pick an IDEA role with no player IDEA object, user: ${player.userid}. 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;
|
|
player.IDEA.picks[choice] = player.IDEA.choices.shift();
|
|
this.sendUser(player.userid, `You were randomly assigned ${choice}: ${player.IDEA.picks[choice]}`);
|
|
}
|
|
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) {
|
|
const role = MafiaTracker.parseRole(player.IDEA.picks[this.IDEA.data.picks[0]]);
|
|
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 = `<b>Discards:</b><br />`;
|
|
for (const p of Object.keys(this.players).sort()) {
|
|
const IDEA = this.players[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 += `<b>${this.players[p].safeName}:</b> ${IDEA.choices.join(', ')}<br />`;
|
|
}
|
|
|
|
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.players).map(p => this.players[p].name).join(', ')}`).update();
|
|
}
|
|
|
|
/**
|
|
* @return {void}
|
|
*/
|
|
updatePlayers() {
|
|
for (const p in this.players) {
|
|
this.players[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();
|
|
}
|
|
|
|
/**
|
|
* @return {void}
|
|
*/
|
|
updateHost() {
|
|
for (const hostid of [...this.cohosts, this.hostid]) {
|
|
const host = Users(hostid);
|
|
if (!host || !host.connected) return;
|
|
if (this.ended) return host.send(`>view-mafia-${this.room.id}\n|deinit`);
|
|
// @ts-ignore
|
|
const buf = Chat.pages.mafia([this.room.id], host);
|
|
host.send(`>view-mafia-${this.room.id}\n|init|html\n${buf}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return {void}
|
|
*/
|
|
updateRoleString() {
|
|
this.roleString = this.roles.slice().map(r => `<span style="font-weight:bold;color:${MafiaData.alignments[r.alignment].color || '#FFF'}">${r.safeName}</span>`).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|<div class="broadcast-blue">${message}</div>`).update();
|
|
if (options.strong) return this.room.add(`|raw|<strong>${message}</strong>`).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 `<div class="infobox">The game of ${this.title} has ended.</div>`;
|
|
let output = `<div class="broadcast-blue">`;
|
|
if (this.phase === 'signups') {
|
|
output += `<h2 style="text-align: center">A game of ${this.title} was created</h2><p style="text-align: center"><button class="button" name="send" value="/mafia join">Join the game</button> <button class="button" name="send" value="/join view-mafia-${this.room.id}">Spectate the game</button> <button class="button" name="send" value="/help mafia">Mafia Commands</button></p>`;
|
|
} else {
|
|
output += `<p style="font-weight: bold">A game of ${this.title} is in progress.</p><p><button class="button" name="send" value="/mafia sub ${this.room.id}, in">Become a substitute</button> <button class="button" name="send" value="/join view-mafia-${this.room.id}">Spectate the game</button> <button class="button" name="send" value="/help mafia">Mafia Commands</button></p>`;
|
|
}
|
|
output += `</div>`;
|
|
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.userid} is`;
|
|
if (!this.room.users[user.userid]) return `${targetString} not in the room.`;
|
|
if (this.players[user.userid]) return `${targetString} already in the game.`;
|
|
if (this.hostid === user.userid) return `${targetString} the host.`;
|
|
if (this.cohosts.includes(user.userid)) return `${targetString} a cohost.`;
|
|
if (!force) {
|
|
for (const alt of user.getAltUsers(true)) {
|
|
if (this.players[alt.userid] || this.played.includes(alt.userid)) return `${self ? `You already have` : `${user.userid} already has`} an alt in the game.`;
|
|
if (this.hostid === alt.userid || this.cohosts.includes(alt.userid)) return `${self ? `You have` : `${user.userid} 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(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.players)) {
|
|
if (player.lynching === player.userid) this.unlynch(player.userid, 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];
|
|
for (const player of Object.values(this.players)) {
|
|
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.userid);
|
|
if (subIndex !== -1) {
|
|
this.hostRequestedSub.splice(subIndex, 1);
|
|
for (const hostid of [...this.cohosts, this.hostid]) {
|
|
this.sendUser(hostid, `${user.userid} has spoken and been removed from the host sublist.`);
|
|
}
|
|
}
|
|
|
|
if (user.isStaff || (this.room.auth && this.room.auth[user.userid] && this.room.auth[user.userid] !== '+') || this.hostid === user.userid || this.cohosts.includes(user.userid) || !this.started) return false;
|
|
if (!this.players[user.userid] && (!this.dead[user.userid] || !this.dead[user.userid].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.userid in this.players) {
|
|
return this.players[user.userid].updateHtmlRoom();
|
|
}
|
|
if (user.userid === this.hostid) return this.updateHost();
|
|
}
|
|
|
|
/**
|
|
* @param {User} user
|
|
* @return {void}
|
|
*/
|
|
onLeave(user) {
|
|
if (this.subs.includes(user.userid)) this.subs.splice(this.subs.indexOf(user.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.userid in this.players)) return;
|
|
this.requestedSub.unshift(user.userid);
|
|
this.nextSub();
|
|
}
|
|
|
|
/**
|
|
* @param {User} user
|
|
* @return {void}
|
|
*/
|
|
forfeit(user) {
|
|
// Add the player to the sub list.
|
|
if (!(user.userid in this.players)) return;
|
|
this.requestedSub.push(user.userid);
|
|
this.nextSub();
|
|
}
|
|
|
|
/**
|
|
* @return {void}
|
|
*/
|
|
end() {
|
|
this.ended = true;
|
|
this.sendRoom(this.roomWindow(), {uhtml: true});
|
|
this.updatePlayers();
|
|
if (this.room.id === '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.players).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.players) {
|
|
this.players[i].destroy();
|
|
}
|
|
for (let i in this.dead) {
|
|
this.dead[i].destroy();
|
|
}
|
|
}
|
|
}
|
|
|
|
/** @type {PageTable} */
|
|
const pages = {
|
|
mafia: function (query, user) {
|
|
if (!user.named) return Rooms.RETRY_AFTER_LOGIN;
|
|
if (!query.length) return `|deinit`;
|
|
let roomid = query.shift();
|
|
if (roomid === 'groupchat') roomid += `-${query.shift()}-${query.shift()}`;
|
|
const room = /** @type {ChatRoom} */ (Rooms(roomid));
|
|
if (!room || !room.users[user.userid] || !room.game || room.game.gameid !== 'mafia' || room.game.ended) return `|deinit`;
|
|
const game = /** @type {MafiaTracker} */ (room.game);
|
|
const isPlayer = user.userid in game.players;
|
|
const isHost = user.userid === game.hostid || game.cohosts.includes(user.userid);
|
|
let buf = `|title|${game.title}\n|pagehtml|<div class="pad broadcast-blue">`;
|
|
buf += `<button class="button" name="send" value="/join view-mafia-${room.id}" style="float:left"><i class="fa fa-refresh"></i> Refresh</button>`;
|
|
buf += `<br/><br/><h1 style="text-align:center;">${game.title}</h1><h3>Host: ${game.host}</h3>`;
|
|
buf += `<p style="font-weight:bold;">Players (${game.playerCount}): ${Object.keys(game.players).sort().map(p => game.players[p].safeName).join(', ')}</p><hr/>`;
|
|
if (isPlayer && game.phase === 'IDEApicking') {
|
|
buf += `<p><b>IDEA information:</b><br />`;
|
|
const IDEA = game.players[user.userid].IDEA;
|
|
if (!IDEA) return game.sendRoom(`IDEA picking phase but no IDEA object for user: ${user.userid}. Please report this to a mod.`);
|
|
for (const pick of Object.keys(IDEA.picks)) {
|
|
buf += `<b>${pick}:</b> `;
|
|
if (!IDEA.picks[pick]) {
|
|
buf += `<button class="button disabled" style="font-weight:bold; color:#575757; font-weight:bold; background-color:#d3d3d3;">clear</button>`;
|
|
} else {
|
|
buf += `<button class="button" name="send" value="/mafia ideapick ${roomid}, ${pick},">clear</button>`;
|
|
}
|
|
const selectedIndex = IDEA.picks[pick] ? IDEA.originalChoices.indexOf(IDEA.picks[pick]) : -1;
|
|
for (let i = 0; i < IDEA.originalChoices.length; i++) {
|
|
const choice = IDEA.originalChoices[i];
|
|
if (i === selectedIndex) {
|
|
buf += `<button class="button disabled" style="font-weight:bold; color:#575757; font-weight:bold; background-color:#d3d3d3;">${choice}</button>`;
|
|
} else {
|
|
buf += `<button class="button" name="send" value="/mafia ideapick ${roomid}, ${pick}, ${toId(choice)}">${choice}</button>`;
|
|
}
|
|
}
|
|
buf += `<br />`;
|
|
}
|
|
buf += `</p>`;
|
|
buf += `<p><details><summary class="button" style="display:inline-block"><b>Role details:</b></summary><p>`;
|
|
for (const role of IDEA.originalChoices) {
|
|
const roleObject = MafiaTracker.parseRole(role).role;
|
|
buf += `<details><summary>${role}</summary>`;
|
|
buf += `<table><tr><td style="text-align:center;"><td style="text-align:left;width:100%"><ul>${roleObject.memo.map(m => `<li>${m}</li>`).join('')}</ul></td></tr></table>`;
|
|
buf += `</details>`;
|
|
}
|
|
buf += `</p></details></p>`;
|
|
}
|
|
if (game.IDEA.data) {
|
|
buf += `<p><details><summary class="button" style="text-align:left; display:inline-block">${game.IDEA.data.name} information</summary>`;
|
|
if (game.IDEA.discardsHtml) buf += `<details><summary class="button" style="text-align:left; display:inline-block">Discards:</summary><p>${game.IDEA.discardsHtml}</p></details>`;
|
|
buf += `<details><summary class="button" style="text-align:left; display:inline-block">Role list</summary><p>${game.IDEA.data.roles.join('<br />')}</p></details>`;
|
|
buf += `</details></p>`;
|
|
} else {
|
|
if (!game.closedSetup) {
|
|
if (game.theme) {
|
|
buf += `<p><span style="font-weight:bold;">Theme</span>: ${game.theme.name}</p>`;
|
|
buf += `<p>${game.theme.desc}</p>`;
|
|
}
|
|
if (game.noReveal) {
|
|
buf += `<p><span style="font-weight:bold;">Original Rolelist</span>: ${game.originalRoleString}</p>`;
|
|
} else {
|
|
buf += `<p><span style="font-weight:bold;">Rolelist</span>: ${game.roleString}</p>`;
|
|
}
|
|
}
|
|
}
|
|
if (isPlayer) {
|
|
const role = game.players[user.userid].role;
|
|
if (role) {
|
|
buf += `<h3>${game.players[user.userid].safeName}, you are a ${game.players[user.userid].getRole()}</h3>`;
|
|
if (!['town', 'solo'].includes(role.alignment)) buf += `<p><span style="font-weight:bold">Partners</span>: ${game.getPartners(role.alignment, game.players[user.userid])}</p>`;
|
|
buf += `<p><details><summary class="button" style="text-align:left; display:inline-block">Role Details</summary>`;
|
|
buf += `<table><tr><td style="text-align:center;">${role.image || `<img width="75" height="75" src="//play.pokemonshowdown.com/fx/mafia-villager.png"/>`}</td><td style="text-align:left;width:100%"><ul>${role.memo.map(m => `<li>${m}</li>`).join('')}</ul></td></tr></table>`;
|
|
buf += `</details></p>`;
|
|
}
|
|
}
|
|
if (game.phase === "day") {
|
|
buf += `<h3>Lynches (Hammer: ${game.hammerCount}) <button class="button" name="send" value="/join view-mafia-${room.id}"><i class="fa fa-refresh"></i> Refresh</button></h3>`;
|
|
let plur = game.getPlurality();
|
|
for (const key of Object.keys(game.players).concat((game.enableNL ? ['nolynch'] : []))) {
|
|
if (game.lynches[key]) {
|
|
buf += `<p style="font-weight:bold">${game.lynches[key].count}${plur === key ? '*' : ''} ${game.players[key] ? game.players[key].safeName : 'No Lynch'} (${game.lynches[key].lynchers.map(a => game.players[a] ? game.players[a].safeName : a).join(', ')}) `;
|
|
} else {
|
|
buf += `<p style="font-weight:bold">0 ${game.players[key] ? game.players[key].safeName : 'No Lynch'} `;
|
|
}
|
|
const isSpirit = (game.dead[user.userid] && game.dead[user.userid].restless);
|
|
if (isPlayer || isSpirit) {
|
|
if (isPlayer && game.players[user.userid].lynching === key || isSpirit && game.dead[user.userid].lynching === key) {
|
|
buf += `<button class="button" name="send" value="/mafia unlynch ${room.id}">Unlynch ${game.players[key] ? game.players[key].safeName : 'No Lynch'}</button>`;
|
|
} else if ((game.selfEnabled && !isSpirit) || user.userid !== key) {
|
|
buf += `<button class="button" name="send" value="/mafia lynch ${room.id}, ${key}">Lynch ${game.players[key] ? game.players[key].safeName : 'No Lynch'}</button>`;
|
|
}
|
|
} else if (isHost) {
|
|
const lynch = game.lynches[key];
|
|
if (lynch && lynch.count !== lynch.trueCount) buf += `(${lynch.trueCount})`;
|
|
if (game.hammerModifiers[key]) buf += `(${game.getHammerValue(key)} to hammer)`;
|
|
}
|
|
buf += `</p>`;
|
|
}
|
|
} else if (game.phase === "night" && isPlayer) {
|
|
buf += `<p style="font-weight:bold;">PM the host (${game.host}) the action you want to use tonight, and who you want to use it on. Or PM the host "idle".</p>`;
|
|
}
|
|
if (isHost) {
|
|
buf += `<h3>Host options</h3>`;
|
|
buf += `<p><details><summary class="button" style="text-align:left; display:inline-block">General Options</summary>`;
|
|
buf += `<h3>General Options</h3>`;
|
|
if (!game.started) {
|
|
buf += `<button class="button" name="send" value="/mafia closedsetup ${room.id}, ${game.closedSetup ? 'off' : 'on'}">${game.closedSetup ? 'Disable' : 'Enable'} Closed Setup</button>`;
|
|
if (game.phase === 'locked' || game.phase === 'IDEAlocked') {
|
|
buf += ` <button class="button" name="send" value="/mafia start ${room.id}">Start Game</button>`;
|
|
} else {
|
|
buf += ` <button class="button" name="send" value="/mafia close ${room.id}">Close Signups</button>`;
|
|
}
|
|
} else if (game.phase === 'day') {
|
|
buf += `<button class="button" name="send" value="/mafia night ${room.id}">Go to Night ${game.dayNum}</button>`;
|
|
} else if (game.phase === 'night') {
|
|
buf += `<button class="button" name="send" value="/mafia day ${room.id}">Go to Day ${game.dayNum + 1}</button> <button class="button" name="send" value="/mafia extend ${room.id}">Return to Day ${game.dayNum}</button>`;
|
|
}
|
|
buf += ` <button class="button" name="send" value="/mafia selflynch ${room.id}, ${game.selfEnabled === true ? 'off' : 'on'}">${game.selfEnabled === true ? 'Disable' : 'Enable'} self lynching</button> `;
|
|
buf += `<button class="button" name="send" value="/mafia ${game.enableNL ? 'disable' : 'enable'}nl ${room.id}">${game.enableNL ? 'Disable' : 'Enable'} No Lynch</button> `;
|
|
buf += `<button class="button" name="send" value="/mafia reveal ${room.id}, ${game.noReveal ? 'on' : 'off'}">${game.noReveal ? 'Enable' : 'Disable'} revealing of roles</button> `;
|
|
buf += `<button class="button" name="send" value="/mafia autosub ${room.id}, ${game.autoSub ? 'off' : 'on'}">${game.autoSub ? "Disable" : "Enable"} automatic subbing of players</button> `;
|
|
buf += `<button class="button" name="send" value="/mafia end ${room.id}">End Game</button>`;
|
|
buf += `<p>To set a deadline, use <strong>/mafia deadline [minutes]</strong>.<br />To clear the deadline use <strong>/mafia deadline off</strong>.</p><hr/></details></p>`;
|
|
buf += `<p><details><summary class="button" style="text-align:left; display:inline-block">Player Options</summary>`;
|
|
buf += `<h3>Player Options</h3>`;
|
|
for (let p in game.players) {
|
|
let player = game.players[p];
|
|
buf += `<p><details><summary class="button" style="text-align:left; display:inline-block"><span style="font-weight:bold;">`;
|
|
buf += `${player.safeName} (${player.role ? player.getRole(true) : ''})${game.lynchModifiers[p] !== undefined ? `(lynches worth ${game.getLynchValue(p)})` : ''}</summary>`;
|
|
buf += `<button class="button" name="send" value="/mafia kill ${room.id}, ${player.userid}">Kill</button> `;
|
|
buf += `<button class="button" name="send" value="/mafia treestump ${room.id}, ${player.userid}">Make a Treestump (Kill)</button> `;
|
|
buf += `<button class="button" name="send" value="/mafia spirit ${room.id}, ${player.userid}">Make a Restless Spirit (Kill)</button> `;
|
|
buf += `<button class="button" name="send" value="/mafia spiritstump ${room.id}, ${player.userid}">Make a Restless Treestump (Kill)</button> `;
|
|
buf += `<button class="button" name="send" value="/mafia sub ${room.id}, next, ${player.userid}">Force sub</button></span></details></p>`;
|
|
}
|
|
for (let d in game.dead) {
|
|
let dead = game.dead[d];
|
|
buf += `<p style="font-weight:bold;">${dead.safeName} (${dead.role ? dead.getRole() : ''})`;
|
|
if (dead.treestump) buf += ` (is a Treestump)`;
|
|
if (dead.restless) buf += ` (is a Restless Spirit)`;
|
|
if (game.lynchModifiers[d] !== undefined) buf += ` (lynches worth ${game.getLynchValue(d)})`;
|
|
buf += `: <button class="button" name="send" value="/mafia revive ${room.id}, ${dead.userid}">Revive</button></p>`;
|
|
}
|
|
buf += `<hr/></details></p>`;
|
|
buf += `<p><details><summary class="button" style="text-align:left; display:inline-block">How to setup roles</summary>`;
|
|
buf += `<h3>Setting the roles</h3>`;
|
|
buf += `<p>To set the roles, use /mafia setroles [comma seperated list of roles] OR /mafia setroles [theme] in ${room.title}.</p>`;
|
|
buf += `<p>If you set the roles from a theme, the role parser will get all the correct roles for you. (Not all themes are supported).</p>`;
|
|
buf += `<p>The following key words determine a role's alignment (If none are found, the default alignment is town):</p>`;
|
|
buf += `<p style="font-weight:bold">${Object.keys(MafiaData.alignments).map(a => `<span style="color:${MafiaData.alignments[a].color || '#FFF'}">${MafiaData.alignments[a].name}</span>`).join(', ')}</p>`;
|
|
buf += `<p>Please note that anything inside (parentheses) is ignored by the role parser.</p>`;
|
|
buf += `<p>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.</p>`;
|
|
buf += `<p>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.</p>`;
|
|
buf += `<hr/></details></p>`;
|
|
buf += `<p style="font-weight:bold;">Players who will be subbed unless they talk: ${game.hostRequestedSub.join(', ')}</p>`;
|
|
buf += `<p style="font-weight:bold;">Players who are requesting a sub: ${game.requestedSub.join(', ')}</p>`;
|
|
}
|
|
buf += `<p style="font-weight:bold;">Sub List: ${game.subs.join(', ')}</p>`;
|
|
if (!isHost) {
|
|
if (game.phase === 'signups') {
|
|
if (isPlayer) {
|
|
buf += `<p><button class="button" name="send" value="/mafia leave ${room.id}">Leave game</button></p>`;
|
|
} else {
|
|
buf += `<p><button class="button" name="send" value="/mafia join ${room.id}">Join game</button></p>`;
|
|
}
|
|
} else if ((!isPlayer && game.subs.includes(user.userid)) || (isPlayer && !game.requestedSub.includes(user.userid))) {
|
|
buf += `<p><details><summary class="button" style="text-align:left; display:inline-block">${isPlayer ? 'Request to be subbed out' : 'Cancel sub request'}</summary>`;
|
|
buf += `<button class="button" name="send" value="/mafia sub ${room.id}, out">${isPlayer ? 'Confirm request to be subbed out' : 'Confirm cancelation of sub request'}</button></details></p>`;
|
|
} else {
|
|
buf += `<p><details><summary class="button" style="text-align:left; display:inline-block">${isPlayer ? 'Cancel sub request' : 'Join the game as a sub'}</summary>`;
|
|
buf += `<button class="button" name="send" value="/mafia sub ${room.id}, in">${isPlayer ? 'Confirm cancelation of sub request' : 'Confirm that you want to join the game'}</button></details></p>`;
|
|
}
|
|
}
|
|
buf += `</div>`;
|
|
return buf;
|
|
},
|
|
mafialadder: function (query, user) {
|
|
if (!user.named) return Rooms.RETRY_AFTER_LOGIN;
|
|
if (!query.length || !Rooms('mafia')) return `|deinit`;
|
|
/** @type {{[k: string]: {title: string, type: string, section: MafiaLogSection}}} */
|
|
const headers = {
|
|
leaderboard: {title: 'Leaderboard', type: 'Points', section: 'leaderboard'},
|
|
mvpladder: {title: 'MVP Ladder', type: 'MVPs', section: 'mvps'},
|
|
hostlogs: {title: 'Host Logs', type: 'Hosts', section: 'hosts'},
|
|
playlogs: {title: 'Play Logs', type: 'Plays', section: 'plays'},
|
|
leaverlogs: {title: 'Leaver Logs', type: 'Leavers', section: 'leavers'},
|
|
};
|
|
let date = new Date();
|
|
if (query[1] === 'prev') date.setMonth(date.getMonth() - 1);
|
|
const month = date.toLocaleString("en-us", {month: "numeric", year: "numeric"});
|
|
const ladder = headers[query[0]];
|
|
if (!ladder) return `|deinit`;
|
|
const mafiaRoom = /** @type {ChatRoom?} */ (Rooms('mafia'));
|
|
if (['hosts', 'plays', 'leavers'].includes(ladder.section) && !user.can('mute', null, mafiaRoom)) return `|deinit`;
|
|
let buf = `|title|Mafia ${ladder.title} (${date.toLocaleString("en-us", {month: 'long'})} ${date.getFullYear()})\n|pagehtml|<div class="pad ladder">`;
|
|
buf += `${query[1] === 'prev' ? '' : `<button class="button" name="send" value="/join view-mafialadder-${query[0]}" style="float:left"><i class="fa fa-refresh"></i> Refresh</button> <button class="button" name="send" value="/join view-mafialadder-${query[0]}-prev" style="float:left">View last month's ${ladder.title}</button>`}`;
|
|
buf += `<br /><br />`;
|
|
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.</div>`;
|
|
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 += `<table style="margin-left: auto; margin-right: auto"><tbody><tr><th colspan="2"><h2 style="margin: 5px auto">Mafia ${ladder.title} for ${date.toLocaleString("en-us", {month: 'long'})} ${date.getFullYear()}</h1></th></tr>`;
|
|
buf += `<tr><th>User</th><th>${ladder.type}</th></tr>`;
|
|
for (const key of keys) {
|
|
buf += `<tr><td>${key}</td><td>${logs[section][month][key]}</td></tr>`;
|
|
}
|
|
return buf + `</table></div>`;
|
|
},
|
|
};
|
|
|
|
/** @type {ChatCommands} */
|
|
const commands = {
|
|
mafia: {
|
|
'': function (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: function (target, room, user, connection, cmd) {
|
|
if (!room.mafiaEnabled) 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.id === '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.userid] || isHostBanned(this.targetUser.userid)) {
|
|
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.userid !== user.userid && !this.can('mute', null, room)) return false;
|
|
if (!room.users[this.targetUser.userid]) return this.errorReply(`${this.targetUser.name} is not in this room, and cannot be hosted.`);
|
|
if (room.id === 'mafia' && isHostBanned(this.targetUser.userid)) return this.errorReply(`${this.targetUser.name} is banned from hosting games.`);
|
|
|
|
let targetUser = this.targetUser;
|
|
|
|
room.game = new MafiaTracker(room, targetUser);
|
|
|
|
// @ts-ignore
|
|
targetUser.send(`>view-mafia-${room.id}\n|init|html\n${Chat.pages.mafia([room.id], targetUser)}`);
|
|
room.addByUser(user, `${targetUser.name} was appointed the mafia host by ${user.name}.`);
|
|
if (room.id === 'mafia') {
|
|
const queueIndex = hostQueue.indexOf(targetUser.userid);
|
|
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: function (target, room, user) {
|
|
if (!room.mafiaEnabled) return this.errorReply(`Mafia is disabled for this room.`);
|
|
if (room.id !== '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.userid === args[1]) ? 'broadcast' : 'mute';
|
|
if (['forceadd', 'add'].includes(args[0]) && !this.can(permission, null, room)) return;
|
|
if (['remove', 'del', 'delete'].includes(args[0]) && user.userid !== 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(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(`<strong>Host Queue:</strong> ${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: function (target, room, user, connection, cmd) {
|
|
this.parse(`/mafia queue ${cmd.includes('force') ? `forceadd` : `add`}, ${target}`);
|
|
},
|
|
|
|
qdel: 'queueremove',
|
|
qdelete: 'queueremove',
|
|
qremove: 'queueremove',
|
|
queueremove: function (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: function (target, room, user) {
|
|
let targetRoom = /** @type {ChatRoom} */ (Rooms(target));
|
|
if (!targetRoom || targetRoom.type !== 'chat' || !targetRoom.users[user.userid]) {
|
|
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: function (target, room, user) {
|
|
let targetRoom /** @type {ChatRoom?} */ = (Rooms(target));
|
|
if (!targetRoom || targetRoom.type !== 'chat' || !targetRoom.users[user.userid]) {
|
|
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: function (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.userid && !game.cohosts.includes(user.userid) && !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: function (target, room, user) {
|
|
let targetRoom /** @type {ChatRoom?} */ = (Rooms(target));
|
|
if (!targetRoom || targetRoom.type !== 'chat' || !targetRoom.users[user.userid]) {
|
|
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.userid && !game.cohosts.includes(user.userid) && !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: function (target, room, user) {
|
|
const args = target.split(',');
|
|
let targetRoom /** @type {ChatRoom?} */ = (Rooms(args[0]));
|
|
if (!targetRoom || targetRoom.type !== 'chat' || !targetRoom.users[user.userid]) {
|
|
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.userid && !game.cohosts.includes(user.userid) && !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: function (target, room, user) {
|
|
const args = target.split(',');
|
|
let targetRoom /** @type {ChatRoom?} */ = (Rooms(args[0]));
|
|
if (!targetRoom || targetRoom.type !== 'chat' || !targetRoom.users[user.userid]) {
|
|
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.userid && !game.cohosts.includes(user.userid) && !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: function (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.userid && !game.cohosts.includes(user.userid) && !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: function (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.userid && !game.cohosts.includes(user.userid))) 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: function (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: function (target, room, user) {
|
|
const args = target.split(',');
|
|
let targetRoom /** @type {ChatRoom?} */ = (Rooms(args[0]));
|
|
if (!targetRoom || targetRoom.type !== 'chat' || !targetRoom.users[user.userid]) {
|
|
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.userid in game.players)) 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: function (target, room, user) {
|
|
let targetRoom /** @type {ChatRoom?} */ = (Rooms(target));
|
|
if (!targetRoom || targetRoom.type !== 'chat' || !targetRoom.users[user.userid]) {
|
|
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.userid && !game.cohosts.includes(user.userid) && !this.can('mute', null, room)) return;
|
|
game.ideaDistributeRoles(user);
|
|
},
|
|
idearerollhelp: [`/mafia ideareroll - rerolls the roles for the current IDEA module. Requires host % @ * # & ~`],
|
|
|
|
ideadiscards: function (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 (!game.IDEA.discardsHtml) return this.errorReply(`The IDEA module does not have finalised discards yet.`);
|
|
if (!this.runBroadcast()) return;
|
|
this.sendReplyBox(`<details><summary>IDEA discards:</summary>${game.IDEA.discardsHtml}</details>`);
|
|
},
|
|
|
|
'!start': true,
|
|
start: function (target, room, user) {
|
|
let targetRoom /** @type {ChatRoom?} */ = (Rooms(target));
|
|
if (!targetRoom || targetRoom.type !== 'chat' || !targetRoom.users[user.userid]) {
|
|
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.userid && !game.cohosts.includes(user.userid) && !this.can('mute', null, room)) return;
|
|
game.start(user);
|
|
},
|
|
starthelp: [`/mafia start - Start the game of mafia. Signups must be closed. Requires host % @ * # & ~`],
|
|
|
|
'!day': true,
|
|
extend: 'day',
|
|
night: 'day',
|
|
day: function (target, room, user, connection, cmd) {
|
|
const args = target.split(',');
|
|
let targetRoom /** @type {ChatRoom?} */ = (Rooms(args[0]));
|
|
if (!targetRoom || targetRoom.type !== 'chat' || !targetRoom.users[user.userid]) {
|
|
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.userid && !game.cohosts.includes(user.userid) && !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: function (target, room, user) {
|
|
const args = target.split(',');
|
|
let targetRoom /** @type {ChatRoom?} */ = (Rooms(args[0]));
|
|
if (!targetRoom || targetRoom.type !== 'chat' || !targetRoom.users[user.userid]) {
|
|
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.userid in game.players) && (!(user.userid in game.dead) || !game.dead[user.userid].restless)) return user.sendTo(targetRoom, `|error|You are not in the game of ${game.title}.`);
|
|
game.lynch(user.userid, 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: function (target, room, user) {
|
|
let targetRoom /** @type {ChatRoom?} */ = (Rooms(target));
|
|
if (!targetRoom || targetRoom.type !== 'chat' || !targetRoom.users[user.userid]) {
|
|
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.userid in game.players) && (!(user.userid in game.dead) || !game.dead[user.userid].restless)) return user.sendTo(targetRoom, `|error|You are not in the game of ${targetRoom.game.title}.`);
|
|
game.unlynch(user.userid);
|
|
},
|
|
unlynchhelp: [`/mafia unlynch - Withdraw your lynch vote. Fails if you're not voting to lynch anyone`],
|
|
|
|
nl: 'nolynch',
|
|
nolynch: function () {
|
|
this.parse('/mafia lynch nolynch');
|
|
},
|
|
|
|
'!selflynch': true,
|
|
enableself: 'selflynch',
|
|
selflynch: function (target, room, user, connection, cmd) {
|
|
const args = target.split(',');
|
|
let targetRoom /** @type {ChatRoom?} */ = (Rooms(args[0]));
|
|
if (!targetRoom || targetRoom.type !== 'chat' || !targetRoom.users[user.userid]) {
|
|
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.userid && !game.cohosts.includes(user.userid) && !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: function (target, room, user, connection, cmd) {
|
|
const args = target.split(',');
|
|
let targetRoom /** @type {ChatRoom?} */ = (Rooms(args[0]));
|
|
if (!targetRoom || targetRoom.type !== 'chat' || !targetRoom.users[user.userid]) {
|
|
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.userid && !game.cohosts.includes(user.userid) && !this.can('mute', null, room)) return;
|
|
const player = game.players[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: function (target, room, user, connection, cmd) {
|
|
const args = target.split(',');
|
|
let targetRoom /** @type {ChatRoom?} */ = (Rooms(args[0]));
|
|
if (!targetRoom || targetRoom.type !== 'chat' || !targetRoom.users[user.userid]) {
|
|
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.userid && !game.cohosts.includes(user.userid) && !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: function (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.userid && !game.cohosts.includes(user.userid) && !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.userid || game.cohosts.includes(user.userid)) && 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.id) + '|' + this.message);
|
|
room.lastBroadcastTime = Date.now();
|
|
room.lastBroadcast = broadcastMessage;
|
|
}
|
|
if (!this.runBroadcast()) return false;
|
|
if ((game.dlAt - Date.now()) > 0) {
|
|
return this.sendReply(`|raw|<strong>The deadline is in ${Chat.toDurationString(game.dlAt - Date.now()) || '0 seconds'}.</strong>`);
|
|
} 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: function (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.userid && !game.cohosts.includes(user.userid) && !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: function (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.userid && !game.cohosts.includes(user.userid) && !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',
|
|
removelynchmodifier: 'love',
|
|
love: function (target, room, user, connection, cmd) {
|
|
let mod;
|
|
switch (cmd) {
|
|
case 'hate':
|
|
mod = -1;
|
|
break;
|
|
case 'love':
|
|
mod = 1;
|
|
break;
|
|
case 'unhate': case 'unlove': case 'removelynchmodifier':
|
|
mod = 0;
|
|
break;
|
|
}
|
|
this.parse(`/mafia applyhammermodifier ${target}, ${mod}`);
|
|
},
|
|
doublevoter: 'mayor',
|
|
voteless: 'mayor',
|
|
unvoteless: 'mayor',
|
|
unmayor: 'mayor',
|
|
mayor: function (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':
|
|
mod = 1;
|
|
break;
|
|
}
|
|
this.parse(`/mafia applylynchmodifier ${target}, ${mod}`);
|
|
},
|
|
|
|
shifthammer: 'hammer',
|
|
resethammer: 'hammer',
|
|
hammer: function (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.userid && !game.cohosts.includes(user.userid) && !this.can('mute', null, room)) return;
|
|
if (!game.started) return this.errorReply(`The game has not started yet.`);
|
|
const hammer = parseInt(target);
|
|
if ((isNaN(hammer) || hammer < 1) && cmd.toLowerCase() !== `resethammer`) 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 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: function (target, room, user, connection, cmd) {
|
|
let targetRoom /** @type {ChatRoom?} */ = (Rooms(target));
|
|
if (!targetRoom || targetRoom.type !== 'chat' || !targetRoom.users[user.userid]) {
|
|
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.userid && !game.cohosts.includes(user.userid) && !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 % @ # & ~`],
|
|
|
|
lynches: function (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.userid || game.cohosts.includes(user.userid)) && 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.id) + '|' + this.message);
|
|
room.lastBroadcastTime = Date.now();
|
|
room.lastBroadcast = broadcastMessage;
|
|
}
|
|
if (!this.runBroadcast()) return false;
|
|
|
|
this.sendReplyBox(game.lynchBox());
|
|
},
|
|
|
|
pl: 'players',
|
|
players: function (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.userid || game.cohosts.includes(user.userid)) && 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.id) + '|' + 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.players).map(p => game.players[p].safeName).join(', ')}`);
|
|
}
|
|
},
|
|
|
|
originalrolelist: 'rolelist',
|
|
orl: 'rolelist',
|
|
rl: 'rolelist',
|
|
rolelist: function (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 = `<details><summary>IDEA roles:</summary>${game.IDEA.data.roles.join(`<br />`)}</details>`;
|
|
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: function (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.userid && !game.cohosts.includes(user.userid)) 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.players), ...Object.values(game.dead)];
|
|
this.sendReplyBox(players.map(p => `${p.safeName}: ${p.role ? p.role.safeName : 'No role'}`).join('<br/>'));
|
|
},
|
|
|
|
spectate: 'view',
|
|
view: function (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(`<button name="joinRoom" value="view-mafia-${room.id}" class="button"><strong>Spectate the game</strong></button>`);
|
|
return this.parse(`/join view-mafia-${room.id}`);
|
|
},
|
|
|
|
'!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: function (target, room, user, connection, cmd) {
|
|
const args = target.split(',');
|
|
let targetRoom /** @type {ChatRoom?} */ = (Rooms(args[0]));
|
|
if (!targetRoom || targetRoom.type !== 'chat' || !targetRoom.users[user.userid]) {
|
|
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.userid in game.players) {
|
|
// Check if they have requested to be subbed out.
|
|
if (!game.requestedSub.includes(user.userid)) return user.sendTo(targetRoom, `|error|You have not requested to be subbed out.`);
|
|
game.requestedSub.splice(game.requestedSub.indexOf(user.userid), 1);
|
|
user.sendTo(room, `|error|You have cancelled your request to sub out.`);
|
|
game.players[user.userid].updateHtmlRoom();
|
|
} else {
|
|
if (!this.canTalk(null, targetRoom)) return;
|
|
if (game.subs.includes(user.userid)) return user.sendTo(targetRoom, `|error|You are already on the sub list.`);
|
|
if (game.played.includes(user.userid)) 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.userid);
|
|
game.nextSub();
|
|
// Update spectator's view
|
|
this.parse(`/join view-mafia-${targetRoom.id}`);
|
|
}
|
|
break;
|
|
case 'out':
|
|
if (user.userid in game.players) {
|
|
if (game.requestedSub.includes(user.userid)) return user.sendTo(targetRoom, `|error|You have already requested to be subbed out.`);
|
|
game.requestedSub.push(user.userid);
|
|
game.players[user.userid].updateHtmlRoom();
|
|
game.nextSub();
|
|
} else {
|
|
if (game.hostid === user.userid || game.cohosts.includes(user.userid)) return user.sendTo(targetRoom, `|error|The host cannot sub out of the game.`);
|
|
if (!game.subs.includes(user.userid)) return user.sendTo(targetRoom, `|error|You are not on the sub list.`);
|
|
game.subs.splice(game.subs.indexOf(user.userid), 1);
|
|
// Update spectator's view
|
|
this.parse(`/join view-mafia-${targetRoom.id}`);
|
|
}
|
|
break;
|
|
case 'next':
|
|
if (game.hostid !== user.userid && !game.cohosts.includes(user.userid) && !this.can('mute', null, room)) return;
|
|
let toSub = args.shift();
|
|
if (!(toId(toSub) in game.players)) 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.userid && !game.cohosts.includes(user.userid) && !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.userid && !game.cohosts.includes(user.userid) && !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.userid && !game.cohosts.includes(user.userid) && !this.can('mute', null, room)) return;
|
|
const toSubOut = action;
|
|
const toSubIn = toId(args.shift());
|
|
if (!(toSubOut in game.players)) return user.sendTo(targetRoom, `|error|${toSubOut} is not in the game.`);
|
|
|
|
const targetUser = Users(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.userid)) game.subs.splice(game.subs.indexOf(targetUser.userid), 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: function (target, room, user) {
|
|
const args = target.split(',');
|
|
let targetRoom /** @type {ChatRoom?} */ = (Rooms(args[0]));
|
|
if (!targetRoom || targetRoom.type !== 'chat' || !targetRoom.users[user.userid]) {
|
|
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.userid && !game.cohosts.includes(user.userid) && !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: function (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.userid]) return this.errorReply(`${targetUser.name} is not in this room, and cannot be hosted.`);
|
|
if (game.hostid === targetUser.userid) return this.errorReply(`${targetUser.name} is already the host.`);
|
|
if (game.cohosts.includes(targetUser.userid)) return this.errorReply(`${targetUser.name} is already a cohost.`);
|
|
if (targetUser.userid in game.players) return this.errorReply(`The host cannot be ingame.`);
|
|
if (targetUser.userid 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.userid].lynching) game.unlynch(targetUser.userid);
|
|
game.dead[targetUser.userid].destroy();
|
|
delete game.dead[targetUser.userid];
|
|
}
|
|
if (cmd.includes('cohost')) {
|
|
game.cohosts.push(targetUser.userid);
|
|
game.sendRoom(`${Chat.escapeHTML(targetUser.name)} has been added as a cohost by ${Chat.escapeHTML(user.name)}`, {declare: true});
|
|
// @ts-ignore
|
|
targetUser.send(`>view-mafia-${room.id}\n|init|html\n|${Chat.pages.mafia([room.id], targetUser)}`);
|
|
this.modlog('MAFIACOHOST', targetUser, null, {noalts: true, noip: true});
|
|
} else {
|
|
const oldHostid = game.hostid;
|
|
const oldHost = Users(game.hostid);
|
|
if (oldHost) oldHost.send(`>view-mafia-${room.id}\n|deinit`);
|
|
if (game.subs.includes(targetUser.userid)) game.subs.splice(game.subs.indexOf(targetUser.userid), 1);
|
|
const queueIndex = hostQueue.indexOf(targetUser.userid);
|
|
if (queueIndex > -1) hostQueue.splice(queueIndex, 1);
|
|
game.host = Chat.escapeHTML(targetUser.name);
|
|
game.hostid = targetUser.userid;
|
|
game.played.push(targetUser.userid);
|
|
// @ts-ignore
|
|
targetUser.send(`>view-mafia-${room.id}\n|init|html\n${Chat.pages.mafia([room.id], targetUser)}`);
|
|
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: function (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: function (target, room, user) {
|
|
let targetRoom /** @type {ChatRoom?} */ = (Rooms(target));
|
|
if (!targetRoom || targetRoom.type !== 'chat' || !targetRoom.users[user.userid]) {
|
|
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.userid && !game.cohosts.includes(user.userid) && !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 + % @ * # & ~`],
|
|
|
|
role: 'data',
|
|
modifier: 'data',
|
|
alignment: 'data',
|
|
theme: 'data',
|
|
dt: 'data',
|
|
data: function (target, room, user, connection, cmd) {
|
|
if (!room || !room.mafiaEnabled) return this.errorReply(`Mafia is disabled for this room.`);
|
|
if (cmd === 'role' && !target) {
|
|
// Support /mafia role showing your current role if you're in a game
|
|
const game = /** @type {MafiaTracker} */ (room.game);
|
|
if (!game || game.id !== '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.userid in game.players)) return this.errorReply(`You are not in the game of ${game.title}.`);
|
|
const role = game.players[user.userid].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'};
|
|
let id = target.split(' ').map(toId).join('_');
|
|
let result = null;
|
|
let dataType = cmd;
|
|
if (cmd in types) {
|
|
let type = /** @type {'alignments' | 'roles' | 'modifiers' | 'themes'} */ (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'} */ (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 = `<h3${result.color ? ' style="color: ' + result.color + '"' : ``}>${result.name}</h3><b>Type</b>: ${dataType}<br/>`;
|
|
if (dataType === 'theme') {
|
|
buf += `<b>Description</b>: ${result.desc}<br/><details><summary class="button" style="font-weight: bold; display: inline-block">Setups:</summary>`;
|
|
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(', ')}<br/>`;
|
|
}
|
|
} else {
|
|
buf += `${result.memo.join('<br/>')}`;
|
|
}
|
|
return this.sendReplyBox(buf);
|
|
},
|
|
datahelp: [`/mafia data [alignment|role|modifier|theme] - Get information on a mafia alignment, role, modifier, or theme.`],
|
|
|
|
winfaction: 'win',
|
|
win: function (target, room, user, connection, cmd) {
|
|
if (!room || !room.mafiaEnabled) return this.errorReply(`Mafia is disabled for this room.`);
|
|
if (room.id !== '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.players), ...Object.values(game.dead)]) {
|
|
if (user.role && toId(user.role.alignment) === faction) {
|
|
toGiveTo.push(user.userid);
|
|
inFaction.push(user.userid);
|
|
}
|
|
}
|
|
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: function (target, room, user, connection, cmd) {
|
|
if (!room || !room.mafiaEnabled) return this.errorReply(`Mafia is disabled for this room.`);
|
|
if (room.id !== '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: function (target, room, user, connection, cmd) {
|
|
if (!room || !room.mafiaEnabled) return this.errorReply(`Mafia is disabled for this room.`);
|
|
if (room.id !== '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(`<button name="joinRoom" value="view-mafialadder-${cmd}" class="button"><strong>${cmd}</strong></button>`);
|
|
return this.parse(`/join view-mafialadder-${cmd}`);
|
|
},
|
|
leaderboardhelp: [
|
|
`/mafia [leaderboard|mvpladder] - View the leaderboard or MVP ladder for the current or last month.`,
|
|
`/mafia [hostlost|playlogs|leaverlogs] - View the host, play, or leaver logs for the current or last month. Requires % @ * # & ~`,
|
|
],
|
|
|
|
unhostban: 'hostban',
|
|
hostban: function (target, room, user, connection, cmd) {
|
|
if (!room || !room.mafiaEnabled) return this.errorReply(`Mafia is disabled for this room.`);
|
|
if (room.id !== 'mafia') return this.errorReply(`This command can only be used in the Mafia room.`);
|
|
|
|
const duration = parseInt(this.splitTarget(target, false));
|
|
if (!this.targetUser) return this.errorReply(`User ${target} not found.`);
|
|
if (!this.can('mute', this.targetUser, room)) return false;
|
|
|
|
const isUnban = (cmd.startsWith('un'));
|
|
if (isHostBanned(toId(this.targetUsername)) === !isUnban) return this.errorReply(`${this.targetUsername} is ${isUnban ? 'not' : 'already'} banned from hosting games.`);
|
|
|
|
if (isUnban) {
|
|
delete hostBans[toId(this.targetUsername)];
|
|
this.modlog(`MAFIAUNHOSTBAN`, this.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[toId(this.targetUsername)] = Date.now() + 1000 * 60 * 60 * 24 * duration;
|
|
this.modlog(`MAFIAHOSTBAN`, this.targetUser, `for ${duration} days.`);
|
|
const queueIndex = hostQueue.indexOf(toId(this.targetUsername));
|
|
if (queueIndex > -1) hostQueue.splice(queueIndex, 1);
|
|
}
|
|
writeFile(BANS_FILE, hostBans);
|
|
room.add(`${this.targetUsername} 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 % @ * # & ~`,
|
|
],
|
|
|
|
disable: function (target, room, user) {
|
|
if (!room || !this.can('gamemanagement', null, room)) return;
|
|
if (!room.mafiaEnabled) {
|
|
return this.errorReply("Mafia is already disabled.");
|
|
}
|
|
delete room.mafiaEnabled;
|
|
if (room.chatRoomData) {
|
|
delete room.chatRoomData.mafiaEnabled;
|
|
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: function (target, room, user) {
|
|
if (!room || !this.can('gamemanagement', null, room)) return;
|
|
if (room.mafiaEnabled) {
|
|
return this.errorReply("Mafia is already enabled.");
|
|
}
|
|
room.mafiaEnabled = true;
|
|
if (room.chatRoomData) {
|
|
room.chatRoomData.mafiaEnabled = true;
|
|
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: function (target, room, user) {
|
|
if (!this.runBroadcast()) return;
|
|
let buf = `<strong>Commands for the Mafia Plugin</strong><br/>Most commands are used through buttons in the game screen.<br/><br/>`;
|
|
buf += `<details><summary class="button">General Commands</summary>`;
|
|
buf += [
|
|
`<br/><strong>General Commands for the Mafia Plugin</strong>:<br/>`,
|
|
`/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 ideadiscards - Shows the discarded roles list for an IDEA module.`,
|
|
`/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] - Get information on a mafia alignment, role, modifier, or theme.`,
|
|
`/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('<br/>');
|
|
buf += `</details><details><summary class="button">Player Commands</summary>`;
|
|
buf += [
|
|
`<br/><strong>Commands that players can use</strong>:<br/>`,
|
|
`/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.`,
|
|
`/mafia ideapick [selection], [role] - Selects a role from an IDEA module`,
|
|
].join('<br/>');
|
|
buf += `</details><details><summary class="button">Host Commands</summary>`;
|
|
buf += [
|
|
`<br/><strong>Commands for game hosts and Cohosts to use</strong>:<br/>`,
|
|
`/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 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 idea [idea] - starts an IDEA module. Requires + % @ * # & ~, voices can only start for themselves`,
|
|
`/mafia ideareroll - rerolls the current IDEA module. Requires host % @ * # & ~`,
|
|
`/mafia customidea choices, picks (new line here, shift+enter)`,
|
|
`(comma or newline separated rolelist) - Starts an IDEA module with custom roles. Requires % @ # & ~`,
|
|
`/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 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 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('<br/>');
|
|
buf += `</details><details><summary class="button">Mafia Room Specific Commands</summary>`;
|
|
buf += [
|
|
`<br/><strong>Commands that are only useable in the Mafia Room</strong>:<br/>`,
|
|
`/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 [hostlost|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 % @ * # & ~`,
|
|
].join('<br/>');
|
|
buf += `</details>`;
|
|
|
|
return this.sendReplyBox(buf);
|
|
},
|
|
};
|
|
|
|
module.exports = {
|
|
commands,
|
|
pages,
|
|
};
|
|
|
|
process.nextTick(() => {
|
|
Chat.multiLinePattern.register('/mafia customidea');
|
|
});
|