"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var mafia_exports = {};
__export(mafia_exports, {
commands: () => commands,
pages: () => pages,
roomSettings: () => roomSettings
});
module.exports = __toCommonJS(mafia_exports);
var import_lib = require("../../lib");
const MafiaEliminateType = {
ELIMINATE: "was eliminated",
// standard (vote + faction kill + anything else)
KICK: "was kicked from the game",
// staff kick
TREESTUMP: "was treestumped",
// can still talk
SPIRIT: "became a restless spirit",
// can still vote
SPIRITSTUMP: "became a restless treestump"
// treestump + spirit
};
const DATA_FILE = "config/chat-plugins/mafia-data.json";
const LOGS_FILE = "config/chat-plugins/mafia-logs.json";
const VALID_IMAGES = [
"cop",
"dead",
"doctor",
"fool",
"godfather",
"goon",
"hooker",
"mafia",
"mayor",
"villager",
"werewolf"
];
let MafiaData = /* @__PURE__ */ Object.create(null);
let logs = { leaderboard: {}, mvps: {}, hosts: {}, plays: {}, leavers: {} };
Punishments.addRoomPunishmentType({
type: "MAFIAGAMEBAN",
desc: "banned from playing mafia games"
});
Punishments.addRoomPunishmentType({
type: "MAFIAHOSTBAN",
desc: "banned from hosting mafia games"
});
const hostQueue = [];
const IDEA_TIMER = 60 * 1e3;
function readFile(path) {
try {
const json = (0, import_lib.FS)(path).readIfExistsSync();
if (!json) {
return false;
}
return Object.assign(/* @__PURE__ */ Object.create(null), JSON.parse(json));
} catch (e) {
if (e.code !== "ENOENT")
throw e;
}
}
function writeFile(path, data) {
(0, import_lib.FS)(path).writeUpdate(() => JSON.stringify(data));
}
MafiaData = readFile(DATA_FILE) || { alignments: {}, roles: {}, themes: {}, IDEAs: {}, terms: {}, aliases: {} };
if (!MafiaData.alignments.town) {
MafiaData.alignments.town = {
name: "town",
plural: "town",
memo: [`This alignment is required for the script to function properly.`]
};
}
if (!MafiaData.alignments.solo) {
MafiaData.alignments.solo = {
name: "solo",
plural: "solo",
memo: [`This alignment is required for the script to function properly.`]
};
}
logs = readFile(LOGS_FILE) || { leaderboard: {}, mvps: {}, hosts: {}, plays: {}, leavers: {} };
const tables = ["leaderboard", "mvps", "hosts", "plays", "leavers"];
for (const section of tables) {
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) {
const keys = import_lib.Utils.sortBy(Object.keys(logs[section]), (key) => {
const [monthStr, yearStr] = key.split("/");
return [parseInt(yearStr), parseInt(monthStr)];
});
while (keys.length > 2) {
const curKey = keys.shift();
if (!curKey)
break;
delete logs[section][curKey];
}
}
}
writeFile(LOGS_FILE, logs);
class MafiaPlayer extends Rooms.RoomGamePlayer {
constructor(user, game) {
super(user, game);
this.safeName = import_lib.Utils.escapeHTML(this.name);
this.role = null;
this.voting = "";
this.hammerRestriction = null;
this.lastVote = 0;
this.eliminated = null;
this.eliminationOrder = 0;
this.silenced = false;
this.nighttalk = false;
this.revealed = "";
this.IDEA = null;
this.action = null;
this.actionArr = [];
}
/**
* Called if the player's name changes.
*/
updateSafeName() {
this.safeName = import_lib.Utils.escapeHTML(this.name);
}
getStylizedRole(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 `${this.role.safeName}`;
}
isEliminated() {
return this.eliminated !== null;
}
isTreestump() {
return this.eliminated === MafiaEliminateType.TREESTUMP || this.eliminated === MafiaEliminateType.SPIRITSTUMP;
}
isSpirit() {
return this.eliminated === MafiaEliminateType.SPIRIT || this.eliminated === MafiaEliminateType.SPIRITSTUMP;
}
// Only call updateHtmlRoom if the player is still involved in the game in some way
tryUpdateHtmlRoom() {
if ([
null,
MafiaEliminateType.SPIRIT,
MafiaEliminateType.TREESTUMP,
MafiaEliminateType.SPIRITSTUMP
].includes(this.eliminated)) {
this.updateHtmlRoom();
}
}
/**
* Updates the mafia HTML room for this player.
* @param id Only provided during the destruction process to update the HTML one last time after player.id is cleared.
*/
updateHtmlRoom() {
if (this.game.ended)
return this.closeHtmlRoom();
const user = Users.get(this.id);
if (!user?.connected)
return;
for (const conn of user.connections) {
void Chat.resolvePage(`view-mafia-${this.game.room.roomid}`, user, conn);
}
}
closeHtmlRoom() {
const user = Users.get(this.id);
if (!user?.connected)
return;
return user.send(`>view-mafia-${this.game.room.roomid}
|deinit`);
}
updateHtmlVotes() {
const user = Users.get(this.id);
if (!user?.connected)
return;
const votes = this.game.voteBoxFor(this.id);
user.send(`>view-mafia-${this.game.room.roomid}
|selectorhtml|#mafia-votes|` + votes);
}
}
class Mafia extends Rooms.RoomGame {
constructor(room, host) {
super(room);
this.gameid = "mafia";
this.title = "Mafia";
this.playerCap = 20;
this.allowRenames = false;
this.started = false;
this.theme = null;
this.hostid = host.id;
this.host = import_lib.Utils.escapeHTML(host.name);
this.cohostids = [];
this.cohosts = [];
this.subs = [];
this.autoSub = true;
this.requestedSub = [];
this.hostRequestedSub = [];
this.played = [];
this.hammerCount = 0;
this.votes = /* @__PURE__ */ Object.create(null);
this.voteModifiers = /* @__PURE__ */ Object.create(null);
this.hammerModifiers = /* @__PURE__ */ Object.create(null);
this.hasPlurality = null;
this.enableNV = true;
this.voteLock = false;
this.votingEnabled = true;
this.forceVote = false;
this.closedSetup = false;
this.noReveal = true;
this.selfEnabled = false;
this.takeIdles = true;
this.originalRoles = [];
this.originalRoleString = "";
this.roles = [];
this.roleString = "";
this.phase = "signups";
this.dayNum = 0;
this.timer = null;
this.dlAt = 0;
this.IDEA = {
data: null,
timer: null,
discardsHidden: false,
discardsHTML: "",
waitingPick: []
};
this.sendHTML(this.roomWindow());
}
join(user, staffAdd = null, force = false) {
if (this.phase !== "signups" && !staffAdd) {
return this.sendUser(user, `|error|The game of ${this.title} has already started.`);
}
this.canJoin(user, !staffAdd, force);
if (this.playerCount >= this.playerCap)
return this.sendUser(user, `|error|The game of ${this.title} is full.`);
const player = this.addPlayer(user);
if (!player)
return this.sendUser(user, `|error|You have already joined the game of ${this.title}.`);
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);
this.played.push(player.id);
} else {
this.originalRoles = [];
this.originalRoleString = "";
this.roles = [];
this.roleString = "";
}
if (this.subs.includes(user.id))
this.subs.splice(this.subs.indexOf(user.id), 1);
player.updateHtmlRoom();
if (staffAdd) {
this.sendDeclare(`${player.name} has been added to the game by ${staffAdd.name}.`);
this.logAction(staffAdd, "added player");
} else {
this.sendRoom(`${player.name} has joined the game.`);
}
}
leave(user) {
const player = this.getPlayer(user.id);
if (!player) {
return this.sendUser(user, `|error|You have not joined the game of ${this.title}.`);
}
if (this.phase !== "signups")
return this.sendUser(user, `|error|The game of ${this.title} has already started.`);
this.removePlayer(player);
let subIndex = this.requestedSub.indexOf(user.id);
if (subIndex !== -1)
this.requestedSub.splice(subIndex, 1);
subIndex = this.hostRequestedSub.indexOf(user.id);
if (subIndex !== -1)
this.hostRequestedSub.splice(subIndex, 1);
this.sendRoom(`${user.name} has left the game.`);
for (const conn of user.connections) {
void Chat.resolvePage(`view-mafia-${this.room.roomid}`, user, conn);
}
}
static isGameBanned(room, user) {
return Punishments.hasRoomPunishType(room, toID(user), "MAFIAGAMEBAN");
}
static gameBan(room, user, reason, duration) {
Punishments.roomPunish(room, user, {
type: "MAFIAGAMEBAN",
id: toID(user),
expireTime: Date.now() + duration * 24 * 60 * 60 * 1e3,
reason
});
}
static ungameBan(room, user) {
Punishments.roomUnpunish(room, toID(user), "MAFIAGAMEBAN", false);
}
static isHostBanned(room, user) {
return Mafia.isGameBanned(room, user) || Punishments.hasRoomPunishType(room, toID(user), "MAFIAGAMEBAN");
}
static hostBan(room, user, reason, duration) {
Punishments.roomPunish(room, user, {
type: "MAFIAHOSTBAN",
id: toID(user),
expireTime: Date.now() + duration * 24 * 60 * 60 * 1e3,
reason
});
}
static unhostBan(room, user) {
Punishments.roomUnpunish(room, toID(user), "MAFIAHOSTBAN", false);
}
makePlayer(user) {
return new MafiaPlayer(user, this);
}
getPlayer(userid) {
const matches = this.players.filter((p) => p.id === userid);
if (matches.length > 1) {
throw new Error(`Duplicate player IDs in Mafia game! Matches: ${matches.map((p) => p.id).join(", ")}`);
}
return matches.length > 0 ? matches[0] : null;
}
setRoles(user, roleString, force = false, reset = false) {
let roles = roleString.split(",").map((x) => x.trim());
if (roles.length === 1) {
let themeName = toID(roles[0]);
if (themeName in MafiaData.aliases)
themeName = MafiaData.aliases[themeName];
if (themeName in MafiaData.themes) {
const theme = MafiaData.themes[themeName];
if (!theme[this.playerCount]) {
return user.sendTo(
this.room,
`|error|The theme "${theme.name}" does not have a role list for ${this.playerCount} players.`
);
}
const themeRoles = theme[this.playerCount];
roles = themeRoles.split(",").map((x) => x.trim());
this.theme = theme;
} else if (themeName in MafiaData.IDEAs) {
const IDEA = MafiaData.IDEAs[themeName];
roles = IDEA.roles;
this.theme = null;
} else {
return this.sendUser(user, `|error|${roles[0]} is not a valid theme or IDEA.`);
}
} else {
this.theme = null;
}
if (roles.length < this.playerCount) {
return this.sendUser(user, `|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) => ({
name: r,
safeName: import_lib.Utils.escapeHTML(r),
id: toID(r),
alignment: "solo",
image: "",
memo: [`To learn more about your role, PM the host (${this.host}).`]
}));
import_lib.Utils.sortBy(this.originalRoles, (role) => role.name);
this.roles = this.originalRoles.slice();
this.originalRoleString = this.originalRoles.map(
(r) => `${r.safeName}`
).join(", ");
this.roleString = this.originalRoleString;
this.sendRoom(`The roles have been ${reset ? "re" : ""}set.`);
if (reset)
this.distributeRoles();
return;
}
const newRoles = [];
const problems = [];
const alignments = [];
const cache = /* @__PURE__ */ Object.create(null);
for (const roleName of roles) {
const roleId = roleName.toLowerCase().replace(/[^\w\d\s]/g, "");
if (roleId in cache) {
newRoles.push({ ...cache[roleId] });
} else {
const role = Mafia.parseRole(roleName);
if (role.problems.length) {
problems.push(...role.problems);
}
if (!alignments.includes(role.role.alignment))
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) {
this.sendUser(user, `|error|${problem}`);
}
return this.sendUser(user, `|error|To forcibly set the roles, use /mafia force${reset ? "re" : ""}setroles`);
}
this.IDEA.data = null;
this.originalRoles = newRoles;
import_lib.Utils.sortBy(this.originalRoles, (role) => [role.alignment, role.name]);
this.roles = this.originalRoles.slice();
this.originalRoleString = this.originalRoles.map(
(r) => `${r.safeName}`
).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();
}
resetGame() {
this.clearVotes();
this.dayNum = 0;
this.phase = "night";
for (const hostid of [...this.cohostids, this.hostid]) {
const host = Users.get(hostid);
if (host?.connected)
host.send(`>${this.room.roomid}
|notify|It's night in your game of Mafia!`);
}
for (const player of this.players) {
const user = Users.get(player.id);
if (user?.connected) {
this.sendUser(user, `|notify|It's night in the game of Mafia! Send in an action or idle.`);
}
player.actionArr.length = 0;
}
if (this.timer)
this.setDeadline(0);
this.sendDeclare(`The game has been reset.`);
this.distributeRoles();
if (this.takeIdles) {
this.sendDeclare(`Night ${this.dayNum}. Submit whether you are using an action or idle. If you are using an action, DM your action to the host.`);
} else {
this.sendDeclare(`Night ${this.dayNum}. PM the host your action, or idle.`);
}
}
static parseRole(roleString) {
const roleName = roleString.replace(/solo/, "").trim();
const role = {
name: roleName,
safeName: import_lib.Utils.escapeHTML(roleName),
id: toID(roleName),
image: "",
memo: ["During the Day, you may vote for someone to be eliminated."],
alignment: ""
};
const problems = [];
let modAlignment = "";
const roleWords = roleString.replace(/\(.+?\)/g, "").split(" ").map(toID);
let iters = 0;
outer:
while (roleWords.length) {
const currentWord = roleWords.slice();
while (currentWord.length) {
if (iters++ === 1e3)
throw new Error(`Infinite loop.`);
let currentSearch = currentWord.join("");
if (currentSearch in MafiaData.aliases)
currentSearch = MafiaData.aliases[currentSearch];
if (currentSearch in MafiaData.roles) {
const mod = MafiaData.roles[currentSearch];
if (mod.memo)
role.memo.push(...mod.memo);
if (mod.alignment && !modAlignment)
modAlignment = mod.alignment;
if (mod.image && !role.image)
role.image = mod.image;
roleWords.splice(0, currentWord.length);
continue outer;
} else if (currentSearch in MafiaData.alignments) {
if (role.alignment && role.alignment !== currentSearch) {
problems.push(`The role ${roleString} has multiple possible alignments (${role.alignment} and ${currentSearch})`);
}
role.alignment = currentSearch;
roleWords.splice(0, currentWord.length);
continue outer;
}
currentWord.pop();
}
roleWords.shift();
}
role.alignment = role.alignment || modAlignment;
if (!role.alignment) {
role.alignment = "town";
}
if (problems.length) {
role.alignment = "solo";
role.memo.push(`Your role has multiple conflicting alignments, ask the host for details.`);
} else {
const alignment = MafiaData.alignments[role.alignment];
if (alignment) {
role.memo.push(...MafiaData.alignments[role.alignment].memo);
if (alignment.image && !role.image)
role.image = alignment.image;
} else {
problems.push(`Alignment desync: role ${role.name}'s alignment ${role.alignment} doesn't exist in data. Please report this to a mod.`);
}
}
return { role, problems };
}
start(user, day = false) {
if (!user)
return;
if (this.phase !== "locked" && this.phase !== "IDEAlocked") {
if (this.phase === "signups")
return this.sendUser(user, `You need to close the signups first.`);
if (this.phase === "IDEApicking") {
return this.sendUser(user, `You must wait for IDEA picks to finish before starting.`);
}
return this.sendUser(user, `The game is already started!`);
}
if (this.playerCount < 2)
return this.sendUser(user, `You need at least 2 players to start.`);
if (this.phase === "IDEAlocked") {
for (const p of this.players) {
if (!p.role)
return this.sendUser(user, `|error|Not all players have a role.`);
}
} else {
if (!Object.keys(this.roles).length)
return this.sendUser(user, `You need to set the roles before starting.`);
if (Object.keys(this.roles).length < this.playerCount) {
return this.sendUser(user, `You have not provided enough roles for the players.`);
}
}
this.started = true;
this.sendDeclare(`The game of ${this.title} is starting!`);
this.distributeRoles();
if (day) {
this.day(null, true);
} else {
this.night(false, true);
}
if (this.IDEA.data && !this.IDEA.discardsHidden) {
this.room.add(`|html|
IDEA discards:${this.IDEA.discardsHTML}
`).update();
}
}
distributeRoles() {
const roles = import_lib.Utils.shuffle(this.roles.slice());
if (roles.length) {
for (const p of this.players) {
const role = roles.shift();
p.role = role;
const u = Users.get(p.id);
p.revealed = "";
if (u?.connected) {
u.send(`>${this.room.roomid}
|notify|Your role is ${role.safeName}. For more details of your role, check your Role PM.`);
}
}
}
this.clearEliminations();
this.played = [this.hostid, ...this.cohostids, ...this.players.map((p) => p.id)];
this.sendDeclare(`The roles have been distributed.`);
this.updatePlayers();
}
getPartners(alignment, player) {
if (!player?.role || ["town", "solo", "traitor"].includes(player.role.alignment))
return "";
const partners = [];
for (const p of this.players) {
if (p.id === player.id)
continue;
const role = p.role;
if (role && role.alignment === player.role.alignment)
partners.push(p.name);
}
return partners.join(", ");
}
day(extension = null, initial = false) {
if (this.phase !== "night" && !initial)
return;
if (this.dayNum === 0 && extension !== null)
return this.sendUser(this.hostid, `|error|You cannot extend on day 0.`);
if (this.timer)
this.setDeadline(0);
if (extension === null) {
if (!isNaN(this.hammerCount))
this.hammerCount = Math.floor(this.getRemainingPlayers().length / 2) + 1;
this.clearVotes();
}
this.phase = "day";
if (extension !== null && !initial) {
this.setDeadline(extension);
} else {
this.dayNum++;
}
if (isNaN(this.hammerCount)) {
this.sendDeclare(`Day ${this.dayNum}. Hammering is disabled.`);
} else {
this.sendDeclare(`Day ${this.dayNum}. The hammer count is set at ${this.hammerCount}`);
}
for (const p of this.players) {
p.action = null;
}
this.sendPlayerList();
this.updatePlayers();
}
night(early = false, initial = false) {
if (this.phase !== "day" && !initial)
return;
if (this.timer)
this.setDeadline(0, true);
this.phase = "night";
for (const hostid of [...this.cohostids, this.hostid]) {
const host = Users.get(hostid);
if (host?.connected)
host.send(`>${this.room.roomid}
|notify|It's night in your game of Mafia!`);
}
for (const player of this.players) {
const user = Users.get(player.id);
if (user?.connected) {
this.sendUser(user, `|notify|It's night in the game of Mafia! Send in an action or idle.`);
}
}
if (this.takeIdles) {
this.sendDeclare(`Night ${this.dayNum}. Submit whether you are using an action or idle. If you are using an action, DM your action to the host.`);
} else {
this.sendDeclare(`Night ${this.dayNum}. PM the host your action, or idle.`);
}
const hasPlurality = this.getPlurality();
if (!early && hasPlurality) {
this.sendRoom(`Plurality is on ${this.getPlayer(hasPlurality)?.name || "No Vote"}`);
}
if (!early && !initial)
this.sendRoom(`|raw|
${this.voteBox()}
`);
if (initial && !isNaN(this.hammerCount))
this.hammerCount = Math.floor(this.getRemainingPlayers().length / 2) + 1;
this.updatePlayers();
}
vote(voter, targetId) {
if (!this.votingEnabled)
return this.sendUser(voter, `|error|Voting is not allowed.`);
if (this.phase !== "day")
return this.sendUser(voter, `|error|You can only vote during the day.`);
if (!voter || voter.isEliminated() && !voter.isSpirit())
return;
const target = this.getPlayer(targetId);
if ((!target || target.isEliminated()) && targetId !== "novote") {
return this.sendUser(voter, `|error|${targetId} is not a valid player.`);
}
if (!this.enableNV && targetId === "novote")
return this.sendUser(voter, `|error|No Vote is not allowed.`);
if (targetId === voter.id && !this.selfEnabled)
return this.sendUser(voter, `|error|Self voting is not allowed.`);
if (this.voteLock && voter.voting) {
return this.sendUser(voter, `|error|You cannot switch your vote because votes are locked.`);
}
const currentVotes = this.votes[targetId] ? this.votes[targetId].count : 0;
const hammering = currentVotes + 1 >= this.hammerCount;
if (targetId === voter.id && !hammering && this.selfEnabled === "hammer") {
return this.sendUser(voter, `|error|You may only vote yourself when placing the hammer vote.`);
}
if (voter.hammerRestriction !== null) {
if (voter.hammerRestriction && !hammering) {
return this.sendUser(voter, `|error|You can only vote when placing the hammer vote.`);
} else if (!voter.hammerRestriction && hammering) {
return this.sendUser(voter, `|error|You cannot place the hammer vote.`);
}
}
if (voter.lastVote + 2e3 >= Date.now()) {
return this.sendUser(
voter,
`|error|You must wait another ${Chat.toDurationString(voter.lastVote + 2e3 - Date.now()) || "1 second"} before you can change your vote.`
);
}
const previousVote = voter.voting;
if (previousVote)
this.unvote(voter, true);
let vote = this.votes[targetId];
if (!vote) {
this.votes[targetId] = {
count: 1,
trueCount: this.getVoteValue(voter),
lastVote: Date.now(),
dir: "up",
voters: [voter.id]
};
vote = this.votes[targetId];
} else {
vote.count++;
vote.trueCount += this.getVoteValue(voter);
vote.lastVote = Date.now();
vote.dir = "up";
vote.voters.push(voter.id);
}
voter.voting = targetId;
voter.lastVote = Date.now();
const name = voter.voting === "novote" ? "No Vote" : target?.name;
if (previousVote) {
this.sendTimestamp(`${voter.name} has shifted their vote from ${previousVote === "novote" ? "No Vote" : this.getPlayer(previousVote)?.name} to ${name}`);
} else {
this.sendTimestamp(
name === "No Vote" ? `${voter.name} has abstained from voting.` : `${voter.name} has voted ${name}.`
);
}
this.hasPlurality = null;
if (this.getHammerValue(targetId) <= vote.trueCount) {
this.sendDeclare(`Hammer! ${targetId === "novote" ? "Nobody" : import_lib.Utils.escapeHTML(name)} was voted out!`);
this.sendRoom(`|raw|
${this.voteBox()}
`);
if (targetId !== "novote")
this.eliminate(target, MafiaEliminateType.ELIMINATE);
this.night(true);
return;
}
this.updatePlayersVotes();
}
unvote(voter, force = false) {
if (!force) {
if (this.phase !== "day")
return this.sendUser(voter, `|error|You can only vote during the day.`);
if (voter.isEliminated() && !voter.isSpirit()) {
return;
}
if (!voter.isEliminated() && this.forceVote) {
return this.sendUser(voter, `|error|You can only shift your vote, not unvote.`);
}
if (this.voteLock && voter.voting) {
return this.sendUser(voter, `|error|You cannot unvote because votes are locked.`);
}
if (voter.lastVote + 2e3 >= Date.now()) {
return this.sendUser(
voter,
`|error|You must wait another ${Chat.toDurationString(voter.lastVote + 2e3 - Date.now()) || "1 second"} before you can change your vote.`
);
}
}
if (!voter.voting)
return this.sendUser(voter, `|error|You are not voting for anyone.`);
const vote = this.votes[voter.voting];
vote.count--;
vote.trueCount -= this.getVoteValue(voter);
if (vote.count <= 0) {
delete this.votes[voter.voting];
} else {
vote.lastVote = Date.now();
vote.dir = "down";
vote.voters.splice(vote.voters.indexOf(voter.id), 1);
}
const target = this.getPlayer(voter.voting);
if (!target && voter.voting !== "novote") {
throw new Error(`Unable to find target when unvoting. Voter: ${voter.id}, Target: ${voter.voting}`);
}
if (!force) {
this.sendTimestamp(
voter.voting === "novote" ? `${voter.name} is no longer abstaining from voting.` : `${voter.name} has unvoted ${target?.name}.`
);
}
voter.voting = "";
voter.lastVote = Date.now();
this.hasPlurality = null;
this.updatePlayersVotes();
}
/**
* Returns HTML code that contains information about the current vote.
*/
voteBox() {
if (!this.started)
return `The game has not started yet.`;
let buf = `Votes (Hammer: ${this.hammerCount || "Disabled"}) `;
const plur = this.getPlurality();
const list = import_lib.Utils.sortBy(Object.entries(this.votes), ([key, vote]) => [
key === plur,
-vote.count
]);
for (const [key, vote] of list) {
const player = this.getPlayer(toID(key));
buf += `${vote.count}${plur === key ? "*" : ""} ${player?.safeName || "No Vote"} (${vote.voters.map((a) => this.getPlayer(a)?.safeName || a).join(", ")}) `;
}
return buf;
}
voteBoxFor(userid) {
let buf = "";
buf += `
`;
}
return buf;
}
applyVoteModifier(requester, targetPlayer, mod) {
if (!targetPlayer)
return this.sendUser(requester, `|error|${targetPlayer} is not in the game of mafia.`);
const oldMod = this.voteModifiers[targetPlayer.id];
if (mod === oldMod || (isNaN(mod) || mod === 1) && oldMod === void 0) {
if (isNaN(mod) || mod === 1)
return this.sendUser(requester, `|error|${targetPlayer} already has no vote modifier.`);
return this.sendUser(requester, `|error|${targetPlayer} already has a vote modifier of ${mod}`);
}
const newMod = isNaN(mod) ? 1 : mod;
if (targetPlayer.voting) {
this.votes[targetPlayer.voting].trueCount += oldMod - newMod;
if (this.getHammerValue(targetPlayer.voting) <= this.votes[targetPlayer.voting].trueCount) {
this.sendRoom(`${targetPlayer.voting} has been voted out due to a modifier change! They have not been eliminated.`);
this.night(true);
}
}
if (newMod === 1) {
delete this.voteModifiers[targetPlayer.id];
return this.sendUser(requester, `${targetPlayer.name} has had their vote modifier removed.`);
} else {
this.voteModifiers[targetPlayer.id] = newMod;
return this.sendUser(requester, `${targetPlayer.name} has been given a vote modifier of ${newMod}`);
}
}
applyHammerModifier(user, target, mod) {
const oldMod = this.hammerModifiers[target.id];
if (mod === oldMod || (isNaN(mod) || mod === 0) && oldMod === void 0) {
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.votes[target.id]) {
if (this.hammerCount + newMod <= this.votes[target.id].trueCount) {
this.sendRoom(`${target} has been voted due to a modifier change! They have not been eliminated.`);
this.night(true);
}
}
if (newMod === 0) {
delete this.hammerModifiers[target.id];
return this.sendUser(user, `${target} has had their hammer modifier removed.`);
} else {
this.hammerModifiers[target.id] = newMod;
return this.sendUser(user, `${target} has been given a hammer modifier of ${newMod}`);
}
}
clearVoteModifiers(user) {
for (const player of this.players) {
if (this.voteModifiers[player.id])
this.applyVoteModifier(user, player, 1);
}
}
clearHammerModifiers(user) {
for (const player of this.players) {
if (this.hammerModifiers[player.id])
this.applyHammerModifier(user, player, 0);
}
}
getVoteValue(player) {
const mod = this.voteModifiers[player.id];
return mod === void 0 ? 1 : mod;
}
getHammerValue(player) {
const mod = this.hammerModifiers[player];
return mod === void 0 ? this.hammerCount : this.hammerCount + mod;
}
resetHammer() {
this.setHammer(Math.floor(this.players.length / 2) + 1);
}
setHammer(count) {
this.hammerCount = count;
if (isNaN(count)) {
this.sendDeclare(`Hammering has been disabled, and votes have been reset.`);
} else {
this.sendDeclare(`The hammer count has been set at ${this.hammerCount}, and votes have been reset.`);
}
this.clearVotes();
}
shiftHammer(count) {
this.hammerCount = count;
if (isNaN(count)) {
this.sendDeclare(`Hammering has been disabled. Votes have not been reset.`);
} else {
this.sendDeclare(`The hammer count has been shifted to ${this.hammerCount}. Votes have not been reset.`);
}
const hammered = [];
for (const vote in this.votes) {
if (this.votes[vote].trueCount >= this.getHammerValue(vote)) {
hammered.push(vote === "novote" ? "Nobody" : vote);
}
}
if (hammered.length) {
this.sendDeclare(`${Chat.count(hammered, "players have")} been hammered: ${hammered.join(", ")}. They have not been removed from the game.`);
this.night(true);
}
}
getPlurality() {
if (this.hasPlurality)
return this.hasPlurality;
if (!Object.keys(this.votes).length)
return null;
let max = 0;
let topVotes = [];
for (const [key, vote] of Object.entries(this.votes)) {
if (vote.count > max) {
max = vote.count;
topVotes = [[key, vote]];
} else if (vote.count === max) {
topVotes.push([key, vote]);
}
}
if (topVotes.length <= 1) {
[this.hasPlurality] = topVotes[0];
return this.hasPlurality;
}
topVotes = import_lib.Utils.sortBy(topVotes, ([key, vote]) => [
vote.dir === "down",
vote.dir === "up" ? vote.lastVote : -vote.lastVote
]);
[this.hasPlurality] = topVotes[0];
return this.hasPlurality;
}
removePlayer(player) {
player.closeHtmlRoom();
const result = super.removePlayer(player);
return result;
}
eliminate(toEliminate, ability) {
if (!this.started) {
this.sendDeclare(`${toEliminate.safeName} was kicked from the game!`);
if (this.hostRequestedSub.includes(toEliminate.id)) {
this.hostRequestedSub.splice(this.hostRequestedSub.indexOf(toEliminate.id), 1);
}
if (this.requestedSub.includes(toEliminate.id)) {
this.requestedSub.splice(this.requestedSub.indexOf(toEliminate.id), 1);
}
this.removePlayer(toEliminate);
return;
}
toEliminate.eliminationOrder = this.getEliminatedPlayers().map((p) => p.eliminationOrder).reduce((a, b) => Math.max(a, b), 0) + 1;
toEliminate.eliminated = ability;
if (toEliminate.voting)
this.unvote(toEliminate, true);
this.sendDeclare(`${toEliminate.safeName} ${ability}! ${!this.noReveal && ability === MafiaEliminateType.ELIMINATE ? `${toEliminate.safeName}'s role was ${toEliminate.getStylizedRole()}.` : ""}`);
if (toEliminate.role && !this.noReveal && ability === MafiaEliminateType.ELIMINATE) {
toEliminate.revealed = toEliminate.getStylizedRole();
}
const targetRole = toEliminate.role;
if (targetRole) {
for (const [roleIndex, role] of this.roles.entries()) {
if (role.id === targetRole.id) {
this.roles.splice(roleIndex, 1);
break;
}
}
}
this.clearVotes(toEliminate.id);
let subIndex = this.requestedSub.indexOf(toEliminate.id);
if (subIndex !== -1)
this.requestedSub.splice(subIndex, 1);
subIndex = this.hostRequestedSub.indexOf(toEliminate.id);
if (subIndex !== -1)
this.hostRequestedSub.splice(subIndex, 1);
this.updateRoleString();
if (ability === MafiaEliminateType.KICK) {
toEliminate.closeHtmlRoom();
this.removePlayer(toEliminate);
}
this.updatePlayers();
}
revealRole(user, toReveal, revealAs) {
if (!this.started) {
return this.sendUser(user, `|error|You may only reveal roles once the game has started.`);
}
if (!toReveal.role) {
return this.sendUser(user, `|error|The user ${toReveal.id} is not assigned a role.`);
}
toReveal.revealed = revealAs;
this.sendDeclare(`${toReveal.safeName}'s role ${toReveal.isEliminated() ? `was` : `is`} ${revealAs}.`);
this.updatePlayers();
}
revive(user, toRevive) {
if (this.phase === "IDEApicking") {
return this.sendUser(user, `|error|You cannot add or remove players while IDEA roles are being picked.`);
}
if (!toRevive.isEliminated()) {
this.sendUser(user, `|error|The user ${toRevive} is already a living player.`);
return;
}
toRevive.eliminated = null;
this.sendDeclare(`${toRevive.safeName} was revived!`);
const targetRole = toRevive.role;
if (targetRole) {
this.roles.push(targetRole);
} else {
toRevive.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(toRevive.role);
}
import_lib.Utils.sortBy(this.roles, (r) => [r.alignment, r.name]);
this.updateRoleString();
this.updatePlayers();
return true;
}
getRemainingPlayers() {
return this.players.filter((player) => !player.isEliminated());
}
getEliminatedPlayers() {
return this.players.filter((player) => player.isEliminated()).sort((a, b) => a.eliminationOrder - b.eliminationOrder);
}
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.sendTimestamp(`**The deadline has been cleared.**`);
return;
}
if (minutes < 1 || minutes > 20)
return;
if (this.timer)
clearTimeout(this.timer);
this.dlAt = Date.now() + minutes * 6e4;
if (minutes > 3) {
this.timer = setTimeout(() => {
this.sendTimestamp(`**3 minutes left!**`);
this.timer = setTimeout(() => {
this.sendTimestamp(`**1 minute left!**`);
this.timer = setTimeout(() => {
this.sendTimestamp(`**Time is up!**`);
this.night();
}, 6e4);
}, 2 * 6e4);
}, (minutes - 3) * 6e4);
} else if (minutes > 1) {
this.timer = setTimeout(() => {
this.sendTimestamp(`**1 minute left!**`);
this.timer = setTimeout(() => {
this.sendTimestamp(`**Time is up!**`);
if (this.phase === "day")
this.night();
}, 6e4);
}, (minutes - 1) * 6e4);
} else {
this.timer = setTimeout(() => {
this.sendTimestamp(`**Time is up!**`);
if (this.phase === "day")
this.night();
}, minutes * 6e4);
}
this.sendTimestamp(`**The deadline has been set for ${minutes} minute${minutes === 1 ? "" : "s"}.**`);
}
sub(player, newUser) {
const oldPlayerId = player.id;
const oldSafeName = player.safeName;
this.setPlayerUser(player, newUser);
player.updateSafeName();
if (player.voting) {
const vote = this.votes[player.voting];
vote.voters.splice(vote.voters.indexOf(oldPlayerId), 1);
vote.voters.push(player.id);
}
if (this.votes[oldPlayerId]) {
this.votes[player.id] = this.votes[oldPlayerId];
delete this.votes[oldPlayerId];
for (const p of this.players) {
if (p.voting === oldPlayerId) {
p.voting = player.id;
}
}
}
if (this.hasPlurality === oldPlayerId)
this.hasPlurality = player.id;
if (newUser?.connected) {
for (const conn of newUser.connections) {
void Chat.resolvePage(`view-mafia-${this.room.roomid}`, newUser, conn);
}
newUser.send(`>${this.room.roomid}
|notify|You have been substituted in the mafia game for ${oldSafeName}.`);
}
if (this.started)
this.played.push(player.id);
this.sendDeclare(`${oldSafeName} has been subbed out. ${player.safeName} has joined the game.`);
this.updatePlayers();
if (this.room.roomid === "mafia" && this.started) {
const month = new Date().toLocaleString("en-us", { month: "numeric", year: "numeric" });
if (!logs.leavers[month])
logs.leavers[month] = {};
if (!logs.leavers[month][player.id])
logs.leavers[month][player.id] = 0;
logs.leavers[month][player.id]++;
writeFile(LOGS_FILE, logs);
}
}
nextSub(userid = null) {
if (!this.subs.length || !this.hostRequestedSub.length && (!this.requestedSub.length || !this.autoSub) && !userid) {
return;
}
const nextSub = this.subs.shift();
if (!nextSub)
return;
const sub = Users.get(nextSub, true);
if (!sub?.connected || !sub.named || !this.room.users[sub.id])
return;
const toSubOut = this.getPlayer(userid || this.hostRequestedSub.shift() || this.requestedSub.shift() || "");
if (!toSubOut) {
this.subs.unshift(nextSub);
return;
}
if (this.hostRequestedSub.includes(toSubOut.id)) {
this.hostRequestedSub.splice(this.hostRequestedSub.indexOf(toSubOut.id), 1);
}
if (this.requestedSub.includes(toSubOut.id)) {
this.requestedSub.splice(this.requestedSub.indexOf(toSubOut.id), 1);
}
this.sub(toSubOut, sub);
}
customIdeaInit(user, choices, picks, rolesString) {
this.originalRoles = [];
this.originalRoleString = "";
this.roles = [];
this.roleString = "";
const roles = import_lib.Utils.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 custom IDEA`,
// already escaped
untrusted: true,
roles: roleList,
picks,
choices
};
return this.ideaDistributeRoles(user);
}
ideaInit(user, moduleID) {
this.originalRoles = [];
this.originalRoleString = "";
this.roles = [];
this.roleString = "";
if (moduleID in MafiaData.aliases)
moduleID = MafiaData.aliases[moduleID];
this.IDEA.data = MafiaData.IDEAs[moduleID];
if (!this.IDEA.data)
return this.sendUser(user, `|error|${moduleID} is not a valid IDEA.`);
return this.ideaDistributeRoles(user);
}
ideaDistributeRoles(user) {
if (!this.IDEA.data)
return this.sendUser(user, `|error|No IDEA module loaded`);
if (this.phase !== "locked" && this.phase !== "IDEAlocked") {
return this.sendUser(user, `|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 this.sendUser(user, `|error|Not enough roles in the IDEA module.`);
}
const roles = [];
const selectedIndexes = [];
for (let i = 0; i < neededRoles; i++) {
let randomIndex;
do {
randomIndex = Math.floor(Math.random() * this.IDEA.data.roles.length);
} while (selectedIndexes.includes(randomIndex));
roles.push(this.IDEA.data.roles[randomIndex]);
selectedIndexes.push(randomIndex);
}
import_lib.Utils.shuffle(roles);
this.IDEA.waitingPick = [];
for (const player of this.players) {
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(player.id);
}
const u = Users.get(player.id);
if (u?.connected)
u.send(`>${this.room.roomid}
|notify|Pick your role in the IDEA module.`);
}
this.phase = "IDEApicking";
this.updatePlayers();
this.sendDeclare(`${this.IDEA.data.name} roles have been distributed. You will have ${IDEA_TIMER / 1e3} seconds to make your picks.`);
this.IDEA.timer = setTimeout(() => {
this.ideaFinalizePicks();
}, IDEA_TIMER);
return ``;
}
ideaPick(user, selection) {
let buf = "";
if (this.phase !== "IDEApicking")
return "The game is not in the IDEA picking phase.";
if (!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.getPlayer(user.id);
if (!player?.IDEA) {
return this.sendRoom(`Trying to pick an IDEA role with no player IDEA object, user: ${user.id}. Please report this to a mod.`);
}
selection = selection.map(toID);
if (selection.length === 1 && this.IDEA.data.picks.length === 1)
selection = [this.IDEA.data.picks[0], selection[0]];
if (selection.length !== 2)
return this.sendUser(user, `|error|Invalid selection.`);
if (selection[1]) {
const roleIndex = player.IDEA.choices.map(toID).indexOf(selection[1]);
if (roleIndex === -1) {
return this.sendUser(user, `|error|${selection[1]} is not an available role, perhaps it is already selected?`);
}
selection[1] = player.IDEA.choices.splice(roleIndex, 1)[0];
} else {
selection[1] = "";
}
const selected = player.IDEA.picks[selection[0]];
if (selected) {
buf += `You have deselected ${selected}. `;
player.IDEA.choices.push(selected);
}
if (player.IDEA.picks[selection[0]] && !selection[1]) {
this.IDEA.waitingPick.push(player.id);
} else if (!player.IDEA.picks[selection[0]] && selection[1]) {
this.IDEA.waitingPick.splice(this.IDEA.waitingPick.indexOf(player.id), 1);
}
player.IDEA.picks[selection[0]] = selection[1];
if (selection[1])
buf += `You have selected ${selection[0]}: ${selection[1]}.`;
player.updateHtmlRoom();
if (!this.IDEA.waitingPick.length) {
if (this.IDEA.timer)
clearTimeout(this.IDEA.timer);
this.ideaFinalizePicks();
return;
}
return this.sendUser(user, buf);
}
ideaFinalizePicks() {
if (!this.IDEA?.data) {
return this.sendRoom(`Tried to finalize IDEA picks with no IDEA module running, please report this to a mod.`);
}
const randed = [];
for (const player of this.players) {
if (!player.IDEA) {
return this.sendRoom(`Trying to pick an IDEA role with no player IDEA object, user: ${player.id}. Please report this to a mod.`);
}
let randPicked = false;
const role = [];
for (const choice of this.IDEA.data.picks) {
if (!player.IDEA.picks[choice]) {
randPicked = true;
const randomChoice = player.IDEA.choices.shift();
if (randomChoice) {
player.IDEA.picks[choice] = randomChoice;
} else {
throw new Error(`No roles left to randomly assign from IDEA module choices.`);
}
this.sendUser(player.id, `You were randomly assigned ${choice}: ${randomChoice}`);
}
role.push(`${choice}: ${player.IDEA.picks[choice]}`);
}
if (randPicked)
randed.push(player.id);
let roleName = "";
if (this.IDEA.data.picks.length === 1) {
const pick = player.IDEA.picks[this.IDEA.data.picks[0]];
if (!pick)
throw new Error("Pick not found when parsing role selected in IDEA module.");
const parsedRole = Mafia.parseRole(pick);
if (parsedRole.problems.length) {
this.sendRoom(`Problems found when parsing IDEA role ${player.IDEA.picks[this.IDEA.data.picks[0]]}. Please report this to a mod.`);
}
player.role = parsedRole.role;
} else {
roleName = role.join("; ");
player.role = {
name: roleName,
safeName: import_lib.Utils.escapeHTML(roleName),
id: toID(roleName),
alignment: "solo",
memo: [`(Your role was set from an IDEA.)`],
image: ""
};
if (!this.IDEA.data.untrusted) {
for (const pick of role) {
if (pick.substr(0, 10) === "alignment:") {
const parsedRole = Mafia.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;
}
}
}
}
if (player.IDEA.choices.includes("Innocent Discard"))
player.role.alignment = "town";
}
this.IDEA.discardsHTML = `Discards: `;
for (const player of this.players.sort((a, b) => a.id.localeCompare(b.id))) {
const IDEA = player.IDEA;
if (!IDEA) {
return this.sendRoom(`No IDEA data for player ${player} when finalising IDEAs. Please report this to a mod.`);
}
this.IDEA.discardsHTML += `${player.safeName}: ${IDEA.choices.join(", ")} `;
}
this.phase = "IDEAlocked";
if (randed.length) {
this.sendDeclare(`${randed.join(", ")} did not pick a role in time and were randomly assigned one.`);
}
this.sendDeclare(`IDEA picks are locked!`);
this.sendRoom(`To start, use /mafia start, or to reroll use /mafia ideareroll`);
this.updatePlayers();
}
sendPlayerList() {
this.room.add(`|c:|${Math.floor(Date.now() / 1e3)}|~|**Players (${this.getRemainingPlayers().length})**: ${this.getRemainingPlayers().map((p) => p.name).sort().join(", ")}`).update();
}
updatePlayers() {
for (const p of this.players) {
p.tryUpdateHtmlRoom();
}
this.updateHost();
}
updatePlayersVotes() {
for (const p of this.players) {
p.tryUpdateHtmlRoom();
}
}
updateHost(...hosts) {
if (!hosts.length)
hosts = [...this.cohostids, this.hostid];
for (const hostid of hosts) {
const host = Users.get(hostid);
if (!host?.connected)
return;
for (const conn of host.connections) {
void Chat.resolvePage(`view-mafia-${this.room.roomid}`, host, conn);
}
}
}
updateRoleString() {
this.roleString = this.roles.map(
(r) => `${r.safeName}`
).join(", ");
}
sendRoom(message) {
this.room.add(message).update();
}
sendHTML(message) {
this.room.add(`|uhtml|mafia|${message}`).update();
}
sendDeclare(message) {
this.room.add(`|raw|
`;
return output;
}
canJoin(user, self = false, force = false) {
if (!user?.connected)
throw new Chat.ErrorMessage(`User not found.`);
const targetString = self ? `You are` : `${user.id} is`;
if (!this.room.users[user.id])
throw new Chat.ErrorMessage(`${targetString} not in the room.`);
for (const id of [user.id, ...user.previousIDs]) {
if (this.getPlayer(id))
throw new Chat.ErrorMessage(`${targetString} already in the game.`);
if (!force && this.played.includes(id)) {
throw new Chat.ErrorMessage(`${targetString} a previous player and cannot rejoin.`);
}
if (Mafia.isGameBanned(this.room, user)) {
throw new Chat.ErrorMessage(`${targetString} banned from joining mafia games.`);
}
if (this.hostid === id)
throw new Chat.ErrorMessage(`${targetString} the host.`);
if (this.cohostids.includes(id))
throw new Chat.ErrorMessage(`${targetString} a cohost.`);
}
for (const alt of user.getAltUsers(true)) {
if (!force && (this.getPlayer(alt.id) || this.played.includes(alt.id))) {
throw new Chat.ErrorMessage(`${self ? `You already have` : `${user.id} already has`} an alt in the game.`);
}
if (this.hostid === alt.id || this.cohostids.includes(alt.id)) {
throw new Chat.ErrorMessage(`${self ? `You have` : `${user.id} has`} an alt as a game host.`);
}
}
}
sendUser(user, message) {
let userObject;
if (user instanceof MafiaPlayer) {
userObject = user.getUser();
} else if (typeof user === "string") {
userObject = Users.get(user);
} else {
userObject = user;
}
if (!userObject?.connected)
return;
userObject.sendTo(this.room, message);
}
setSelfVote(user, setting) {
const from = this.selfEnabled;
if (from === setting) {
return user.sendTo(
this.room,
`|error|Selfvoting is already ${setting ? `set to Self${setting === "hammer" ? "hammering" : "voting"}` : "disabled"}.`
);
}
if (from) {
this.sendDeclare(`Self${from === "hammer" ? "hammering" : "voting"} has been ${setting ? `changed to Self${setting === "hammer" ? "hammering" : "voting"}` : "disabled"}.`);
} else {
this.sendDeclare(`Self${setting === "hammer" ? "hammering" : "voting"} has been ${setting ? "enabled" : "disabled"}.`);
}
this.selfEnabled = setting;
if (!setting) {
for (const player of this.players) {
if (player.voting === player.id)
this.unvote(player, true);
}
}
this.updatePlayers();
}
setNoVote(user, setting) {
if (this.enableNV === setting) {
return this.sendUser(user, `|error|No Vote is already ${setting ? "enabled" : "disabled"}.`);
}
this.enableNV = setting;
this.sendDeclare(`No Vote has been ${setting ? "enabled" : "disabled"}.`);
if (!setting)
this.clearVotes("novote");
this.updatePlayers();
}
setVotelock(user, setting) {
if (!this.started)
return this.sendUser(user, `The game has not started yet.`);
if (this.voteLock === setting) {
return this.sendUser(user, `|error|Votes are already ${setting ? "set to lock" : "set to not lock"}.`);
}
this.voteLock = setting;
this.clearVotes();
this.sendDeclare(`Votes are cleared and ${setting ? "set to lock" : "set to not lock"}.`);
this.updatePlayers();
}
setVoting(user, setting) {
if (!this.started)
return this.sendUser(user, `The game has not started yet.`);
if (this.votingEnabled === setting) {
return this.sendUser(user, `|error|Voting is already ${setting ? "allowed" : "disallowed"}.`);
}
this.votingEnabled = setting;
this.clearVotes();
this.sendDeclare(`Voting is now ${setting ? "allowed" : "disallowed"}.`);
this.updatePlayers();
}
clearVotes(target = "") {
if (target)
delete this.votes[target];
if (!target)
this.votes = /* @__PURE__ */ Object.create(null);
for (const player of this.players) {
if (player.isEliminated() && !player.isSpirit())
continue;
if (this.forceVote) {
if (!target || player.voting === target) {
player.voting = player.id;
this.votes[player.id] = {
count: 1,
trueCount: this.getVoteValue(player),
lastVote: Date.now(),
dir: "up",
voters: [player.id]
};
}
} else {
if (!target || player.voting === target)
player.voting = "";
}
}
this.hasPlurality = null;
}
/**
* Only intended to be used during pre-game setup.
*/
clearEliminations() {
for (const player of this.players) {
player.eliminated = null;
}
}
onChatMessage(message, user) {
const subIndex = this.hostRequestedSub.indexOf(user.id);
if (subIndex !== -1) {
this.hostRequestedSub.splice(subIndex, 1);
for (const hostid of [...this.cohostids, this.hostid]) {
this.sendUser(hostid, `${user.id} has spoken and been removed from the host sublist.`);
}
}
if (this.hostid === user.id || this.cohostids.includes(user.id) || !this.started) {
return;
}
const player = this.getPlayer(user.id);
const eliminated = player?.isEliminated();
const staff = user.can("mute", null, this.room);
if (!player) {
if (staff) {
return;
} else {
return `You cannot talk while a game of ${this.title} is going on.`;
}
}
if (player.silenced) {
return `You are silenced and cannot speak.${staff ? " You can remove this with /mafia unsilence." : ""}`;
}
if (eliminated) {
if (!player.isTreestump()) {
return `You are dead.${staff ? " You can treestump yourself with /mafia treestump." : ""}`;
}
}
if (this.phase === "night") {
if (!player.nighttalk) {
return `You cannot talk at night.${staff ? " You can bypass this using /mafia nighttalk." : ""}`;
}
}
}
onConnect(user) {
this.sendUser(user, `|uhtml|mafia|${this.roomWindow()}`);
}
onJoin(user) {
const player = this.getPlayer(user.id);
if (player) {
return player.updateHtmlRoom();
}
if (user.id === this.hostid || this.cohostids.includes(user.id))
return this.updateHost(user.id);
}
removeBannedUser(user) {
if (!this.getPlayer(user.id))
return;
this.requestedSub.unshift(user.id);
this.nextSub();
}
forfeit(user) {
const player = this.getPlayer(user.id);
if (!player || player.isEliminated())
return;
this.requestedSub.push(user.id);
this.nextSub();
}
end() {
this.setEnded();
this.sendHTML(this.roomWindow());
this.updatePlayers();
if (this.room.roomid === "mafia" && this.started) {
const played = this.players.map((p) => p.id);
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.cohostids, 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() {
if (this.timer)
clearTimeout(this.timer);
if (this.IDEA.timer)
clearTimeout(this.IDEA.timer);
super.destroy();
}
}
const pages = {
mafia(query, user) {
if (!user.named)
return Rooms.RETRY_AFTER_LOGIN;
if (!query.length)
return this.close();
let roomid = query.shift();
if (roomid === "groupchat")
roomid += `-${query.shift()}-${query.shift()}`;
const room = Rooms.get(roomid);
const game = room?.getGame(Mafia);
if (!room?.users[user.id] || !game || game.ended) {
return this.close();
}
const isPlayer = game.getPlayer(user.id);
const isHost = user.id === game.hostid || game.cohostids.includes(user.id);
const players = game.getRemainingPlayers();
this.title = game.title;
let buf = `
Please note that anything inside (parentheses) is ignored by the role parser.
`;
buf += `
If you have roles that have conflicting alignments or base roles, you can use /mafia forcesetroles [comma seperated list of roles] to forcibly set the roles.
`;
buf += `
Please note that you will have to PM all the players their alignment, partners (if any), and other information about their role because the server will not provide it.
`;
buf += `
`;
buf += `
Players who will be subbed unless they talk: ${game.hostRequestedSub.join(", ")}
`;
buf += `
Players who are requesting a sub: ${game.requestedSub.join(", ")}
`;
}
buf += `
Sub List: ${game.subs.join(", ")}
`;
if (!isHost) {
if (game.phase === "signups") {
if (isPlayer) {
buf += ``;
} else {
buf += ``;
}
} else if (!isPlayer?.isEliminated()) {
if (!isPlayer && game.subs.includes(user.id) || isPlayer && !game.requestedSub.includes(user.id)) {
buf += `
${isPlayer ? "Request to be subbed out" : "Cancel sub request"}`;
buf += `
`;
} else {
buf += `
${isPlayer ? "Cancel sub request" : "Join the game as a sub"}`;
buf += `
Mafia ${ladder.title} for ${date.toLocaleString("en-us", { month: "long" })} ${date.getFullYear()}
`;
buf += `
User
${ladder.type}
`;
for (const [key, value] of entries) {
buf += `
${key}
${value}
`;
}
return buf + `
`;
}
};
const commands = {
mafia: {
""(target, room, user) {
room = this.requireRoom();
const game = room.getGame(Mafia);
if (game) {
if (!this.runBroadcast())
return;
return this.sendReply(`|html|${game.roomWindow()}`);
}
return this.parse("/help mafia");
},
forcehost: "host",
nexthost: "host",
host(target, room, user, connection, cmd) {
room = this.requireRoom();
if (room.settings.mafiaDisabled)
return this.errorReply(`Mafia is disabled for this room.`);
this.checkChat();
if (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.`);
const nextHost = room.roomid === "mafia" && cmd === "nexthost";
if (nextHost || !room.auth.has(user.id))
this.checkCan("show", null, room);
let targetUser;
let targetUsername;
if (nextHost) {
if (!hostQueue.length)
return this.errorReply(`Nobody is on the host queue.`);
const skipped = [];
let hostid;
while (hostid = hostQueue.shift()) {
({ targetUser, targetUsername } = this.splitUser(hostid, { exactName: true }));
if (!targetUser?.connected || !room.users[targetUser.id] || Mafia.isHostBanned(room, targetUser)) {
skipped.push(hostid);
targetUser = null;
} else {
break;
}
}
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 (!targetUser)
return this.errorReply(`Nobody on the host queue could be hosted.`);
} else {
({ targetUser, targetUsername } = this.splitUser(target, { exactName: true }));
if (room.roomid === "mafia" && hostQueue.length && toID(targetUsername) !== hostQueue[0]) {
if (!cmd.includes("force")) {
return this.errorReply(`${targetUsername} isn't the next host on the queue. Use /mafia forcehost if you're sure.`);
}
}
}
if (!targetUser?.connected) {
return this.errorReply(`The user "${targetUsername}" was not found.`);
}
if (!nextHost && targetUser.id !== user.id)
this.checkCan("mute", null, room);
if (!room.users[targetUser.id]) {
return this.errorReply(`${targetUsername} is not in this room, and cannot be hosted.`);
}
if (Mafia.isHostBanned(room, targetUser)) {
return this.errorReply(`${targetUsername} is banned from hosting mafia games.`);
}
room.game = new Mafia(room, targetUser);
for (const conn of targetUser.connections) {
void Chat.resolvePage(`view-mafia-${room.roomid}`, targetUser, conn);
}
room.addByUser(user, `${targetUser.name} was appointed the mafia host by ${user.name}.`);
if (room.roomid === "mafia") {
const queueIndex = hostQueue.indexOf(targetUser.id);
if (queueIndex > -1)
hostQueue.splice(queueIndex, 1);
room.add(`|c:|${Math.floor(Date.now() / 1e3)}|~|**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 whitelist + % @ # ~, drivers+ can host other people.`
],
q: "queue",
queue(target, room, user) {
room = this.requireRoom("mafia");
if (room.settings.mafiaDisabled)
return this.errorReply(`Mafia is disabled for this room.`);
const [command, targetUserID] = target.split(",").map(toID);
switch (command) {
case "forceadd":
case "add":
this.checkChat();
if (targetUserID === user.id) {
if (!room.auth.has(user.id))
this.checkCan("show", null, room);
} else {
this.checkCan("mute", null, room);
}
if (!targetUserID)
return this.parse(`/help mafia queue`);
const targetUser = Users.get(targetUserID);
if (!targetUser?.connected && !command.includes("force")) {
return this.errorReply(`User ${targetUserID} not found. To forcefully add the user to the queue, use /mafia queue forceadd, ${targetUserID}`);
}
if (hostQueue.includes(targetUserID))
return this.errorReply(`User ${targetUserID} is already on the host queue.`);
if (targetUser && Mafia.isHostBanned(room, targetUser)) {
return this.errorReply(`User ${targetUserID} is banned from hosting mafia games.`);
}
hostQueue.push(targetUserID);
room.add(`User ${targetUserID} has been added to the host queue by ${user.name}.`).update();
break;
case "del":
case "delete":
case "remove":
if (targetUserID !== user.id)
this.checkCan("mute", null, room);
const index = hostQueue.indexOf(targetUserID);
if (index === -1)
return this.errorReply(`User ${targetUserID} is not on the host queue.`);
hostQueue.splice(index, 1);
room.add(`User ${targetUserID} has been removed from the host queue by ${user.name}.`).update();
break;
case "":
case "show":
case "view":
if (!this.runBroadcast())
return;
this.sendReplyBox(`Host Queue: ${hostQueue.join(", ")}`);
break;
default:
this.parse("/help mafia queue");
}
},
queuehelp: [
`/mafia queue - Shows the upcoming users who are going to host.`,
`/mafia queue add, (user) - Adds the user to the hosting queue. Requires whitelist + % @ # ~`,
`/mafia queue remove, (user) - Removes the user from the hosting queue. Requires whitelist + % @ # ~`
],
qadd: "queueadd",
qforceadd: "queueadd",
queueforceadd: "queueadd",
queueadd(target, room, user, connection, cmd) {
this.parse(`/mafia queue ${cmd.includes("force") ? `forceadd` : `add`}, ${target}`);
},
qdel: "queueremove",
qdelete: "queueremove",
qremove: "queueremove",
queueremove(target, room, user) {
this.parse(`/mafia queue remove, ${target}`);
},
join(target, room, user) {
room = this.requireRoom();
const game = this.requireGame(Mafia);
this.checkChat(null, room);
game.join(user);
},
joinhelp: [`/mafia join - Join the game.`],
leave(target, room, user) {
room = this.requireRoom();
const game = this.requireGame(Mafia);
game.leave(user);
},
leavehelp: [`/mafia leave - Leave the game. Can only be done while signups are open.`],
playercap(target, room, user) {
room = this.requireRoom();
const game = this.requireGame(Mafia);
if (game.hostid !== user.id && !game.cohostids.includes(user.id))
this.checkCan("mute", null, room);
if (game.phase !== "signups")
return this.errorReply(`Signups are already closed.`);
const num = parseInt(target);
if (isNaN(num) || num > 50 || 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.sendDeclare(`Player cap has been set to ${game.playerCap}`);
game.logAction(user, `set playercap to ${num}`);
},
playercaphelp: [
`/mafia playercap [cap]- Limit the number of players being able to join the game. Player cap cannot be more than 50 or less than 2. Default is 20. Requires host % @ # ~`
],
close(target, room, user) {
room = this.requireRoom();
const game = this.requireGame(Mafia);
if (game.hostid !== user.id && !game.cohostids.includes(user.id))
this.checkCan("mute", null, room);
if (game.phase !== "signups")
return this.errorReply(`Signups are already closed.`);
if (game.playerCount < 2)
return this.errorReply(`You need at least 2 players to start.`);
game.phase = "locked";
game.sendHTML(game.roomWindow());
game.updatePlayers();
game.logAction(user, `closed signups`);
},
closehelp: [`/mafia close - Closes signups for the current game. Requires host % @ # ~`],
cs: "closedsetup",
closedsetup(target, room, user) {
room = this.requireRoom();
const game = this.requireGame(Mafia);
if (game.hostid !== user.id && !game.cohostids.includes(user.id))
this.checkCan("mute", null, room);
const action = toID(target);
if (!["on", "off"].includes(action))
return this.parse("/help mafia closedsetup");
if (game.started) {
return this.errorReply(`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 this.errorReply(`Closed setup is already ${game.closedSetup ? "enabled" : "disabled"}.`);
}
game.closedSetup = action === "on";
game.sendDeclare(`The game is ${action === "on" ? "now" : "no longer"} a closed setup.`);
game.updateHost();
game.logAction(user, `${game.closedSetup ? "enabled" : "disabled"} closed setup`);
},
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(target, room, user) {
room = this.requireRoom();
const game = this.requireGame(Mafia);
if (game.hostid !== user.id && !game.cohostids.includes(user.id))
this.checkCan("mute", null, room);
const action = toID(target);
if (!["on", "off"].includes(action))
return this.parse("/help mafia reveal");
if (action === "off" && game.noReveal || action === "on" && !game.noReveal) {
return user.sendTo(
room,
`|error|Revealing of roles is already ${game.noReveal ? "disabled" : "enabled"}.`
);
}
game.noReveal = action === "off";
game.sendDeclare(`Revealing of roles has been ${action === "off" ? "disabled" : "enabled"}.`);
game.updatePlayers();
game.logAction(user, `${game.noReveal ? "disabled" : "enabled"} reveals`);
},
revealhelp: [`/mafia reveal [on|off] - Sets if roles reveal on death or not. Requires host % @ # ~`],
takeidles(target, room, user) {
room = this.requireRoom();
const game = this.requireGame(Mafia);
if (game.hostid !== user.id && !game.cohostids.includes(user.id))
this.checkCan("mute", null, room);
const action = toID(target);
if (!["on", "off"].includes(action))
return this.parse("/help mafia takeidles");
if (action === "off" && !game.takeIdles || action === "on" && game.takeIdles) {
return this.errorReply(`Actions and idles are already ${game.takeIdles ? "" : "not "}being accepted.`);
}
game.takeIdles = action === "on";
game.sendDeclare(`Actions and idles are ${game.takeIdles ? "now" : "no longer"} being accepted.`);
game.updatePlayers();
},
takeidleshelp: [`/mafia takeidles [on|off] - Sets if idles are accepted by the script or not. Requires host % @ # ~`],
resetroles: "setroles",
forceresetroles: "setroles",
forcesetroles: "setroles",
setroles(target, room, user, connection, cmd) {
room = this.requireRoom();
const game = this.requireGame(Mafia);
if (game.hostid !== user.id && !game.cohostids.includes(user.id))
this.checkCan("mute", null, room);
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);
game.logAction(user, `${reset ? "re" : ""}set roles`);
},
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.`
],
resetgame: "gamereset",
forceresetgame: "gamereset",
gamereset(target, room, user, connection) {
room = this.requireRoom();
const game = this.requireGame(Mafia);
if (game.hostid !== user.id && !game.cohostids.includes(user.id))
this.checkCan("mute", null, room);
if (target)
return this.parse("/help mafia resetgame");
if (game.phase !== "day" && game.phase !== "night")
return this.errorReply(`The game has not started yet.`);
if (game.IDEA.data)
return this.errorReply(`You cannot use this command in IDEA.`);
game.resetGame();
game.logAction(user, "reset the game state");
},
resetgamehelp: [
`/mafia resetgame - Resets game data. Does not change settings from the host (besides deadlines) or add/remove any players. Requires host % @ # ~`
],
idea(target, room, user) {
room = this.requireRoom();
const game = this.requireGame(Mafia);
this.checkCan("show", null, room);
if (!user.can("mute", null, room) && game.hostid !== user.id && !game.cohostids.includes(user.id)) {
return this.errorReply(`/mafia idea - Access denied.`);
}
if (game.started)
return this.errorReply(`You cannot start an IDEA after the game has started.`);
if (game.phase !== "locked" && game.phase !== "IDEAlocked") {
return this.errorReply(`You need to close the signups first.`);
}
game.ideaInit(user, toID(target));
game.logAction(user, `started an IDEA`);
},
ideahelp: [
`/mafia idea [idea] - starts the IDEA module [idea]. Requires + % @ # ~, voices can only start for themselves`,
`/mafia ideareroll - rerolls the IDEA module. Requires host % @ # ~`,
`/mafia ideapick [selection], [role] - selects a role`,
`/mafia ideadiscards - shows the discarded roles`
],
customidea(target, room, user) {
room = this.requireRoom();
this.checkCan("mute", null, room);
const game = this.requireGame(Mafia);
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] = import_lib.Utils.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 % @ # ~`,
`choices refers to the number of roles you get to pick from. In GI, this is 2, in GestI, this is 3.`,
`picks refers to what you choose. In GI, this should be 'role', in GestI, this should be 'role, alignment'`
],
ideapick(target, room, user) {
room = this.requireRoom();
const args = target.split(",");
const game = this.requireGame(Mafia);
const player = game.getPlayer(user.id);
if (!player) {
return user.sendTo(room, "|error|You are not a player in the game.");
}
if (game.phase !== "IDEApicking") {
return this.errorReply(`The game is not in the IDEA picking phase.`);
}
game.ideaPick(user, args);
},
ideareroll(target, room, user) {
room = this.requireRoom();
const game = this.requireGame(Mafia);
if (game.hostid !== user.id && !game.cohostids.includes(user.id))
this.checkCan("mute", null, room);
game.ideaDistributeRoles(user);
game.logAction(user, `rerolled an IDEA`);
},
idearerollhelp: [`/mafia ideareroll - rerolls the roles for the current IDEA module. Requires host % @ # ~`],
discards: "ideadiscards",
ideadiscards(target, room, user) {
room = this.requireRoom();
const game = this.requireGame(Mafia);
if (!game.IDEA.data)
return this.errorReply(`There is no IDEA module in the mafia game.`);
if (target) {
if (game.hostid !== user.id && !game.cohostids.includes(user.id))
this.checkCan("mute", null, room);
if (this.meansNo(target)) {
if (game.IDEA.discardsHidden)
return this.errorReply(`IDEA discards are already hidden.`);
game.IDEA.discardsHidden = true;
} else if (this.meansYes(target)) {
if (!game.IDEA.discardsHidden)
return this.errorReply(`IDEA discards are already visible.`);
game.IDEA.discardsHidden = false;
} else {
return this.parse("/help mafia ideadiscards");
}
game.logAction(user, `${game.IDEA.discardsHidden ? "hid" : "unhid"} IDEA discards`);
return this.sendReply(`IDEA discards are now ${game.IDEA.discardsHidden ? "hidden" : "visible"}.`);
}
if (game.IDEA.discardsHidden)
return this.errorReply(`Discards are not visible.`);
if (!game.IDEA.discardsHTML)
return this.errorReply(`The IDEA module does not have finalised discards yet.`);
if (!this.runBroadcast())
return;
this.sendReplyBox(`IDEA discards:${game.IDEA.discardsHTML}`);
},
ideadiscardshelp: [
`/mafia ideadiscards - shows the discarded roles`,
`/mafia ideadiscards off - hides discards from the players. Requires host % @ # ~`,
`/mafia ideadiscards on - shows discards to the players. Requires host % @ # ~`
],
daystart: "start",
nightstart: "start",
start(target, room, user, connection, cmd) {
room = this.requireRoom();
const game = this.requireGame(Mafia);
if (game.hostid !== user.id && !game.cohostids.includes(user.id))
this.checkCan("mute", null, room);
if (target) {
this.parse(`/mafia close`);
this.parse(`/mafia setroles ${target}`);
this.parse(`/mafia ${cmd}`);
return;
}
game.start(user, cmd === "daystart");
game.logAction(user, `started the game`);
},
starthelp: [`/mafia start - Start the game of mafia. Signups must be closed. Requires host % @ # ~`],
extend: "day",
night: "day",
day(target, room, user, connection, cmd) {
room = this.requireRoom();
const game = this.requireGame(Mafia);
if (game.hostid !== user.id && !game.cohostids.includes(user.id))
this.checkCan("mute", null, room);
if (cmd === "night") {
game.night();
} else {
let extension = parseInt(toID(target));
if (isNaN(extension)) {
extension = 0;
} else {
if (extension < 1)
extension = 1;
if (extension > 10)
extension = 10;
}
if (cmd === "extend") {
for (const player of game.getRemainingPlayers()) {
player.actionArr[game.dayNum] = "";
}
}
game.day(cmd === "extend" ? extension : null);
}
game.logAction(user, `set day/night`);
},
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 % @ # ~`
],
prod(target, room, user) {
room = this.requireRoom();
const game = this.requireGame(Mafia);
if (game.hostid !== user.id && !game.cohostids.includes(user.id))
this.checkCan("mute", null, room);
if (game.phase !== "night")
return;
for (const player of game.getRemainingPlayers()) {
const playerid = Users.get(player.id);
if (playerid?.connected && player.action === null) {
playerid.sendTo(room, `|notify|Send in an action or idle!`);
playerid.sendTo(room, `Send in an action or idle, or else you will get subbed out!`);
}
}
game.sendDeclare(`Unsubmitted players have been reminded to submit an action or idle.`);
},
prodhelp: [
`/mafia prod - Notifies players that they must submit an action or idle if they haven't yet. Requires host % @ # ~`
],
v: "vote",
vote(target, room, user) {
room = this.requireRoom();
const game = this.requireGame(Mafia);
this.checkChat(null, room);
const player = game.getPlayer(user.id);
if (!player || player.isEliminated() && !player.isSpirit()) {
return this.errorReply(`You are not in the game of ${game.title}.`);
}
game.vote(player, toID(target));
},
votehelp: [`/mafia vote [player|novote] - Vote the specified player or abstain from voting.`],
uv: "unvote",
unv: "unvote",
unnovote: "unvote",
unvote(target, room, user) {
room = this.requireRoom();
const game = this.requireGame(Mafia);
this.checkChat(null, room);
const player = game.getPlayer(user.id);
if (!player || player.isEliminated() && !player.isSpirit()) {
return this.errorReply(`You are not in the game of ${game.title}.`);
}
game.unvote(player);
},
unvotehelp: [`/mafia unvote - Withdraw your vote. Fails if you're not voting anyone`],
nv: "novote",
novote() {
this.parse("/mafia vote novote");
},
enableself: "selfvote",
selfvote(target, room, user, connection, cmd) {
room = this.requireRoom();
const game = this.requireGame(Mafia);
if (game.hostid !== user.id && !game.cohostids.includes(user.id))
this.checkCan("mute", null, room);
const action = toID(target);
if (!action)
return this.parse(`/help mafia selfvote`);
if (this.meansYes(action)) {
game.setSelfVote(user, true);
} else if (this.meansNo(action)) {
game.setSelfVote(user, false);
} else if (action === "hammer") {
game.setSelfVote(user, "hammer");
} else {
return this.parse(`/help mafia selfvote`);
}
game.logAction(user, `changed selfvote`);
},
selfvotehelp: [
`/mafia selfvote [on|hammer|off] - Allows players to self vote themselves either at hammer or anytime. Requires host % @ # ~`
],
treestump: "kill",
spirit: "kill",
spiritstump: "kill",
kick: "kill",
kill(target, room, user, connection, cmd) {
room = this.requireRoom();
const game = this.requireGame(Mafia);
if (game.hostid !== user.id && !game.cohostids.includes(user.id))
this.checkCan("mute", null, room);
if (game.phase === "IDEApicking") {
return this.errorReply(`You cannot add or remove players while IDEA roles are being picked.`);
}
if (!target)
return this.parse("/help mafia kill");
const player = game.getPlayer(toID(target));
if (!player) {
return this.errorReply(`${target.trim()} is not a player.`);
}
let repeat, elimType;
switch (cmd) {
case "treestump":
elimType = MafiaEliminateType.TREESTUMP;
repeat = player.isTreestump() && !player.isSpirit();
break;
case "spirit":
elimType = MafiaEliminateType.SPIRIT;
repeat = !player.isTreestump() && player.isSpirit();
break;
case "spiritstump":
elimType = MafiaEliminateType.SPIRITSTUMP;
repeat = player.isTreestump() && player.isSpirit();
break;
case "kick":
elimType = MafiaEliminateType.KICK;
break;
default:
elimType = MafiaEliminateType.ELIMINATE;
repeat = player.eliminated === MafiaEliminateType.ELIMINATE;
break;
}
if (repeat)
return this.errorReply(`${player.safeName} has already been ${cmd}ed.`);
game.eliminate(player, elimType);
game.logAction(user, `${cmd}ed ${player.safeName}`);
},
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 still.`,
`/mafia spiritstump [player] Kills a player, but allows them to talk and vote during the day.`
],
revealas: "revealrole",
revealrole(target, room, user, connection, cmd) {
const args = target.split(",");
room = this.requireRoom();
const game = this.requireGame(Mafia);
if (game.hostid !== user.id && !game.cohostids.includes(user.id))
this.checkCan("mute", null, room);
let revealAs = "";
let revealedRole = null;
if (cmd === "revealas") {
if (!args[0]) {
return this.parse("/help mafia revealas");
} else {
revealedRole = Mafia.parseRole(args.pop());
const color = MafiaData.alignments[revealedRole.role.alignment].color;
revealAs = `${revealedRole.role.safeName}`;
}
}
if (!args[0])
return this.parse("/help mafia revealas");
for (const targetUsername of args) {
const player = game.getPlayer(toID(targetUsername));
if (player) {
game.revealRole(user, player, `${revealAs || player.getStylizedRole()}`);
game.logAction(user, `revealed ${player.name}`);
if (revealedRole) {
game.secretLogAction(user, `fakerevealed ${player.name} as ${revealedRole.role.name}`);
}
} else {
this.errorReply(`${targetUsername} is not a player.`);
}
}
},
revealrolehelp: [
`/mafia revealrole [player] - Reveals the role of a player. Requires host % @ # ~`,
`/mafia revealas [player], [role] - Fakereveals the role of a player as a certain role. Requires host % @ # ~`
],
unidle: "idle",
unaction: "idle",
noresponse: "idle",
action: "idle",
idle(target, room, user, connection, cmd) {
room = this.requireRoom();
const game = this.requireGame(Mafia);
const player = game.getPlayer(user.id);
if (!player)
return this.errorReply(`You are not in the game of ${game.title}.`);
if (player.isEliminated()) {
return this.errorReply(`You have been eliminated from the game and cannot take any actions.`);
}
if (game.phase !== "night") {
return this.errorReply(`You can only submit an action or idle during the night phase.`);
}
if (!game.takeIdles) {
return this.errorReply(`The host is not accepting idles through the script. Send your action or idle to the host.`);
}
switch (cmd) {
case "idle":
player.action = false;
user.sendTo(room, `You have idled.`);
player.actionArr[game.dayNum] = "idle";
break;
case "action":
player.action = true;
if (target) {
player.action = target;
try {
this.checkBanwords(room, target);
} catch {
throw new Chat.ErrorMessage(`Your action submission contained a word banned by this room.`);
}
user.sendTo(room, `You have decided to use an action, with the following details: ${target}`);
} else {
user.sendTo(room, `You have decided to use an action. Please submit details about your action.`);
}
player.actionArr[game.dayNum] = target;
break;
case "noresponse":
case "unidle":
case "unaction":
player.action = null;
user.sendTo(room, `You are no longer submitting an action or idle.`);
player.actionArr[game.dayNum] = "";
break;
}
player.updateHtmlRoom();
},
actionhelp: "idlehelp",
idlehelp: [
`/mafia idle - Tells the host if you are idling.`,
`/mafia action [details] - Tells the host you are using an action with the given submission details.`
],
forceadd: "add",
add(target, room, user, connection, cmd) {
room = this.requireRoom();
const game = this.requireGame(Mafia);
if (game.hostid !== user.id && !game.cohostids.includes(user.id))
this.checkCan("mute", null, room);
if (!toID(target))
return this.parse("/help mafia add");
const targetUser = Users.get(target);
if (!targetUser) {
throw new Chat.ErrorMessage(`The user "${target}" was not found.`);
}
game.join(targetUser, user, cmd === "forceadd");
},
addhelp: [
`/mafia add [player] - Add a new player to the game. Requires host % @ # ~`
],
revive(target, room, user, connection, cmd) {
room = this.requireRoom();
const game = this.requireGame(Mafia);
if (game.hostid !== user.id && !game.cohostids.includes(user.id))
this.checkCan("mute", null, room);
if (!toID(target))
return this.parse("/help mafia revive");
const player = game.getPlayer(toID(target));
if (!player) {
throw new Chat.ErrorMessage(`"${target}" is not currently playing`);
}
if (!player.isEliminated()) {
throw new Chat.ErrorMessage(`${player.name} has not been eliminated.`);
}
game.revive(user, player);
},
revivehelp: [
`/mafia revive [player] - Revives a player who was eliminated. Requires host % @ # ~`
],
dl: "deadline",
deadline(target, room, user) {
room = this.requireRoom();
const game = this.requireGame(Mafia);
target = toID(target);
if (target && game.hostid !== user.id && !game.cohostids.includes(user.id))
this.checkCan("mute", null, room);
if (target === "off") {
game.setDeadline(0);
} else {
const num = parseInt(target);
if (isNaN(num)) {
if (game.hostid === user.id || game.cohostids.includes(user.id)) {
this.broadcastMessage = this.message.toLowerCase().replace(/[^a-z0-9\s!,]/g, "");
}
if (!this.runBroadcast())
return false;
if (game.dlAt - Date.now() > 0) {
return this.sendReply(`|raw|The deadline is in ${Chat.toDurationString(game.dlAt - Date.now()) || "0 seconds"}.`);
} else {
return this.parse(`/help mafia deadline`);
}
}
if (num < 1 || num > 20)
return this.errorReply(`The deadline must be between 1 and 20 minutes.`);
game.setDeadline(num);
}
game.logAction(user, `changed deadline`);
},
deadlinehelp: [
`/mafia deadline [minutes|off] - Sets or removes the deadline for the game. Cannot be more than 20 minutes.`
],
applyvotemodifier: "applyhammermodifier",
applyhammermodifier(target, room, user, connection, cmd) {
room = this.requireRoom();
const game = this.requireGame(Mafia);
if (game.hostid !== user.id && !game.cohostids.includes(user.id))
this.checkCan("mute", null, room);
if (!game.started)
return this.errorReply(`The game has not started yet.`);
const [playerId, mod] = target.split(",");
const player = game.getPlayer(toID(playerId));
if (!player) {
throw new Chat.ErrorMessage(`The player "${playerId}" does not exist.`);
}
if (cmd === "applyhammermodifier") {
game.applyHammerModifier(user, player, parseInt(mod));
game.secretLogAction(user, `changed a hammer modifier`);
} else {
game.applyVoteModifier(user, player, parseInt(mod));
game.secretLogAction(user, `changed a vote modifier`);
}
},
clearvotemodifiers: "clearhammermodifiers",
clearhammermodifiers(target, room, user, connection, cmd) {
room = this.requireRoom();
const game = this.requireGame(Mafia);
if (game.hostid !== user.id && !game.cohostids.includes(user.id))
this.checkCan("mute", null, room);
if (!game.started)
return this.errorReply(`The game has not started yet.`);
if (cmd === "clearhammermodifiers") {
game.clearHammerModifiers(user);
game.secretLogAction(user, `cleared hammer modifiers`);
} else {
game.clearVoteModifiers(user);
game.secretLogAction(user, `cleared vote modifiers`);
}
},
hate: "love",
unhate: "love",
unlove: "love",
removehammermodifier: "love",
love(target, room, user, connection, cmd) {
let mod;
switch (cmd) {
case "hate":
mod = -1;
break;
case "love":
mod = 1;
break;
case "unhate":
case "unlove":
case "removehammermodifier":
mod = 0;
break;
}
this.parse(`/mafia applyhammermodifier ${target}, ${mod}`);
},
doublevoter: "mayor",
voteless: "mayor",
unvoteless: "mayor",
unmayor: "mayor",
removevotemodifier: "mayor",
mayor(target, room, user, connection, cmd) {
let mod;
switch (cmd) {
case "doublevoter":
case "mayor":
mod = 2;
break;
case "voteless":
mod = 0;
break;
case "unvoteless":
case "unmayor":
case "removevotemodifier":
mod = 1;
break;
}
this.parse(`/mafia applyvotemodifier ${target}, ${mod}`);
},
unsilence: "silence",
silence(target, room, user, connection, cmd) {
room = this.requireRoom();
const game = this.requireGame(Mafia);
if (game.hostid !== user.id && !game.cohostids.includes(user.id))
this.checkCan("mute", null, room);
if (!game.started)
return this.errorReply(`The game has not started yet.`);
target = toID(target);
const targetPlayer = game.getPlayer(target);
const silence = cmd === "silence";
if (!targetPlayer)
return this.errorReply(`${target} is not in the game of mafia.`);
if (silence === targetPlayer.silenced) {
return this.errorReply(`${targetPlayer.name} is already ${!silence ? "not" : ""} silenced.`);
}
targetPlayer.silenced = silence;
this.sendReply(`${targetPlayer.name} has been ${!silence ? "un" : ""}silenced.`);
game.logAction(user, `${!silence ? "un" : ""}silenced a player`);
},
silencehelp: [
`/mafia silence [player] - Silences [player], preventing them from talking at all. Requires host % @ # ~`,
`/mafia unsilence [player] - Removes a silence on [player], allowing them to talk again. Requires host % @ # ~`
],
insomniac: "nighttalk",
uninsomniac: "nighttalk",
unnighttalk: "nighttalk",
nighttalk(target, room, user, connection, cmd) {
room = this.requireRoom();
const game = this.requireGame(Mafia);
if (game.hostid !== user.id && !game.cohostids.includes(user.id))
this.checkCan("mute", null, room);
if (!game.started)
return this.errorReply(`The game has not started yet.`);
target = toID(target);
const targetPlayer = game.getPlayer(target);
const nighttalk = !cmd.startsWith("un");
if (!targetPlayer)
return this.errorReply(`${target} is not in the game of mafia.`);
if (nighttalk === targetPlayer.nighttalk) {
return this.errorReply(`${targetPlayer.name} is already ${!nighttalk ? "not" : ""} able to talk during the night.`);
}
targetPlayer.nighttalk = nighttalk;
this.sendReply(`${targetPlayer.name} can ${!nighttalk ? "no longer" : "now"} talk during the night.`);
game.logAction(user, `${!nighttalk ? "un" : ""}insomniacd a player`);
},
nighttalkhelp: [
`/mafia nighttalk [player] - Makes [player] an insomniac, allowing them to talk freely during the night. Requires host % @ # ~`,
`/mafia unnighttalk [player] - Removes [player] as an insomniac, preventing them from talking during the night. Requires host % @ # ~`
],
actor: "priest",
unactor: "priest",
unpriest: "priest",
priest(target, room, user, connection, cmd) {
room = this.requireRoom();
const game = this.requireGame(Mafia);
if (game.hostid !== user.id && !game.cohostids.includes(user.id))
this.checkCan("mute", null, room);
if (!game.started)
return this.errorReply(`The game has not started yet.`);
target = toID(target);
const targetPlayer = game.getPlayer(target);
if (!targetPlayer)
return this.errorReply(`${target} is not in the game of mafia.`);
const actor = cmd.endsWith("actor");
const remove = cmd.startsWith("un");
if (remove) {
if (targetPlayer.hammerRestriction === null) {
return this.errorReply(`${targetPlayer.name} already has no voting restrictions.`);
}
if (actor !== targetPlayer.hammerRestriction) {
return this.errorReply(`${targetPlayer.name} is ${targetPlayer.hammerRestriction ? "an actor" : "a priest"}.`);
}
targetPlayer.hammerRestriction = null;
return this.sendReply(`${targetPlayer}'s hammer restriction was removed.`);
}
if (actor === targetPlayer.hammerRestriction) {
return this.errorReply(`${targetPlayer.name} is already ${targetPlayer.hammerRestriction ? "an actor" : "a priest"}.`);
}
targetPlayer.hammerRestriction = actor;
this.sendReply(`${targetPlayer.name} is now ${targetPlayer.hammerRestriction ? "an actor (can only hammer)" : "a priest (can't hammer)"}.`);
if (actor) {
game.unvote(targetPlayer, true);
}
game.logAction(user, `made a player actor/priest`);
},
priesthelp: [
`/mafia (un)priest [player] - Makes [player] a priest, preventing them from placing the hammer vote. Requires host % @ # ~`,
`/mafia (un)actor [player] - Makes [player] an actor, preventing them from placing non-hammer votes. Requires host % @ # ~`
],
shifthammer: "hammer",
resethammer: "hammer",
hammer(target, room, user, connection, cmd) {
room = this.requireRoom();
const game = this.requireGame(Mafia);
if (game.hostid !== user.id && !game.cohostids.includes(user.id))
this.checkCan("mute", null, room);
if (!game.started)
return this.errorReply(`The game has not started yet.`);
const hammer = parseInt(target);
if (toID(cmd) !== `resethammer` && (isNaN(hammer) && !this.meansNo(target) || hammer < 1)) {
return this.errorReply(`${target} is not a valid hammer count.`);
}
switch (cmd.toLowerCase()) {
case "shifthammer":
game.shiftHammer(hammer);
break;
case "hammer":
game.setHammer(hammer);
break;
default:
game.resetHammer();
break;
}
game.logAction(user, `changed the hammer`);
},
hammerhelp: [
`/mafia hammer [hammer] - sets the hammer count to [hammer] and resets votes`,
`/mafia hammer off - disables hammering`,
`/mafia shifthammer [hammer] - sets the hammer count to [hammer] without resetting votes`,
`/mafia resethammer - sets the hammer to the default, resetting votes`
],
vl: "votelock",
votelock(target, room, user, connection, cmd) {
room = this.requireRoom();
const game = this.requireGame(Mafia);
if (game.hostid !== user.id && !game.cohostids.includes(user.id))
this.checkCan("mute", null, room);
const action = toID(target);
if (this.meansYes(action)) {
game.setVotelock(user, true);
} else if (this.meansNo(action)) {
game.setVotelock(user, false);
} else {
return this.parse("/help mafia votelock");
}
game.logAction(user, `changed votelock status`);
},
votelockhelp: [
`/mafia votelock [on|off] - Allows or disallows players to change their vote. Requires host % @ # ~`
],
voting: "votesall",
votesall(target, room, user, connection, cmd) {
room = this.requireRoom();
const game = this.requireGame(Mafia);
if (game.hostid !== user.id && !game.cohostids.includes(user.id))
this.checkCan("mute", null, room);
const action = toID(target);
if (this.meansYes(action)) {
game.setVoting(user, true);
} else if (this.meansNo(action)) {
game.setVoting(user, false);
} else {
return this.parse("/help mafia voting");
}
game.logAction(user, `changed voting status`);
},
votinghelp: [
`/mafia voting [on|off] - Allows or disallows players to vote. Requires host % @ # ~`
],
enablenv: "enablenl",
disablenv: "enablenl",
disablenl: "enablenl",
enablenl(target, room, user, connection, cmd) {
room = this.requireRoom();
const game = this.requireGame(Mafia);
if (game.hostid !== user.id && !game.cohostids.includes(user.id))
this.checkCan("mute", null, room);
if (cmd === "enablenl" || cmd === "enablenv") {
game.setNoVote(user, true);
} else {
game.setNoVote(user, false);
}
game.logAction(user, `changed novote status`);
},
enablenlhelp: [
`/mafia [enablenv|disablenv] - Allows or disallows players abstain from voting. Requires host % @ # ~`
],
forcevote(target, room, user) {
room = this.requireRoom();
const game = this.requireGame(Mafia);
if (game.hostid !== user.id && !game.cohostids.includes(user.id))
this.checkCan("mute", null, room);
target = toID(target);
if (this.meansYes(target)) {
if (game.forceVote)
return this.errorReply(`Forcevoting is already enabled.`);
game.forceVote = true;
if (game.started)
game.resetHammer();
game.sendDeclare(`Forcevoting has been enabled. Your vote will start on yourself, and you cannot unvote!`);
} else if (this.meansNo(target)) {
if (!game.forceVote)
return this.errorReply(`Forcevoting is already disabled.`);
game.forceVote = false;
game.sendDeclare(`Forcevoting has been disabled. You can vote normally now!`);
} else {
this.parse("/help mafia forcevote");
}
game.logAction(user, `changed forcevote status`);
},
forcevotehelp: [
`/mafia forcevote [yes/no] - Forces players' votes onto themselves, and prevents unvoting. Requires host % @ # ~`
],
votes(target, room, user) {
room = this.requireRoom();
const game = this.requireGame(Mafia);
if (!game.started)
return this.errorReply(`The game of mafia has not started yet.`);
if (game.hostid === user.id || game.cohostids.includes(user.id)) {
this.broadcastMessage = this.message.toLowerCase().replace(/[^a-z0-9\s!,]/g, "");
}
if (!this.runBroadcast())
return false;
this.sendReplyBox(game.voteBox());
},
pl: "players",
players(target, room, user) {
room = this.requireRoom();
const game = this.requireGame(Mafia);
if (game.hostid === user.id || game.cohostids.includes(user.id)) {
this.broadcastMessage = this.message.toLowerCase().replace(/[^a-z0-9\s!,]/g, "");
}
if (!this.runBroadcast())
return false;
if (this.broadcasting) {
game.sendPlayerList();
} else {
const players = game.getRemainingPlayers();
this.sendReplyBox(`Players (${players.length}): ${players.map((p) => p.safeName).sort().join(", ")}`);
}
},
originalrolelist: "rolelist",
orl: "rolelist",
rl: "rolelist",
rolelist(target, room, user, connection, cmd) {
room = this.requireRoom();
const game = this.requireGame(Mafia);
if (game.closedSetup)
return this.errorReply(`You cannot show roles in a closed setup.`);
if (!this.runBroadcast())
return false;
if (game.IDEA.data) {
const buf = `IDEA roles:${game.IDEA.data.roles.join(` `)}`;
return this.sendReplyBox(buf);
}
const showOrl = ["orl", "originalrolelist"].includes(cmd) || game.noReveal;
const roleString = import_lib.Utils.sortBy(showOrl ? game.originalRoles : game.roles, (role) => role.alignment).map((role) => role.safeName).join(", ");
this.sendReplyBox(`${showOrl ? `Original Rolelist: ` : `Rolelist: `}${roleString}`);
},
playerroles(target, room, user) {
room = this.requireRoom();
const game = this.requireGame(Mafia);
if (game.hostid !== user.id && !game.cohostids.includes(user.id)) {
return this.errorReply(`Only the host can view roles.`);
}
if (!game.started)
return this.errorReply(`The game has not started.`);
this.sendReplyBox(game.players.map(
(p) => `${p.safeName}: ${p.role ? (p.role.alignment === "solo" ? "Solo " : "") + p.role.safeName : "No role"}`
).join(" "));
},
spectate: "view",
view(target, room, user, connection) {
room = this.requireRoom();
this.requireGame(Mafia);
if (!this.runBroadcast())
return;
if (this.broadcasting) {
return this.sendReplyBox(``);
}
return this.parse(`/join view-mafia-${room.roomid}`);
},
refreshvotes(target, room, user, connection) {
room = this.requireRoom();
const game = this.requireGame(Mafia);
const votes = game.voteBoxFor(user.id);
user.send(`>view-mafia-${game.room.roomid}
|selectorhtml|#mafia-votes|` + votes);
},
forcesub: "sub",
sub(target, room, user, connection, cmd) {
room = this.requireRoom();
const game = this.requireGame(Mafia);
const args = target.split(",");
const action = toID(args.shift());
const player = game.getPlayer(user.id);
switch (action) {
case "in":
if (player) {
if (!game.requestedSub.includes(user.id)) {
return this.errorReply(`You have not requested to be subbed out.`);
}
game.requestedSub.splice(game.requestedSub.indexOf(user.id), 1);
this.errorReply(`You have cancelled your request to sub out.`);
player.updateHtmlRoom();
} else {
this.checkChat(null, room);
if (game.subs.includes(user.id))
return this.errorReply(`You are already on the sub list.`);
if (game.played.includes(user.id))
return this.errorReply(`You cannot sub back into the game.`);
game.canJoin(user, true);
game.subs.push(user.id);
game.nextSub();
this.parse(`/join view-mafia-${room.roomid}`);
}
break;
case "out":
if (player) {
if (player.isEliminated()) {
return this.errorReply(`You cannot request to be subbed out once eliminated.`);
}
if (game.requestedSub.includes(user.id)) {
return this.errorReply(`You have already requested to be subbed out.`);
}
game.requestedSub.push(user.id);
player.updateHtmlRoom();
game.nextSub();
} else {
if (game.hostid === user.id || game.cohostids.includes(user.id)) {
return this.errorReply(`The host cannot sub out of the game.`);
}
if (!game.subs.includes(user.id))
return this.errorReply(`You are not on the sub list.`);
game.subs.splice(game.subs.indexOf(user.id), 1);
this.parse(`/join view-mafia-${room.roomid}`);
}
break;
case "next":
if (game.hostid !== user.id && !game.cohostids.includes(user.id))
this.checkCan("mute", null, room);
const toSub = args.shift();
if (!game.getPlayer(toID(toSub)))
return this.errorReply(`${toSub} is not in the game.`);
if (!game.subs.length) {
if (game.hostRequestedSub.includes(toID(toSub))) {
return this.errorReply(`${toSub} is already on the list to be subbed out.`);
}
user.sendTo(
room,
`|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));
}
game.logAction(user, `requested a sub for a player`);
break;
case "remove":
if (game.hostid !== user.id && !game.cohostids.includes(user.id))
this.checkCan("mute", null, room);
for (const toRemove of args) {
const toRemoveIndex = game.subs.indexOf(toID(toRemove));
if (toRemoveIndex === -1) {
user.sendTo(room, `|error|${toRemove} is not on the sub list.`);
continue;
}
game.subs.splice(toRemoveIndex, 1);
user.sendTo(room, `${toRemove} has been removed from the sublist`);
game.logAction(user, `removed a player from the sublist`);
}
break;
case "unrequest":
if (game.hostid !== user.id && !game.cohostids.includes(user.id))
this.checkCan("mute", null, room);
const toUnrequest = toID(args.shift());
const userIndex = game.requestedSub.indexOf(toUnrequest);
const hostIndex = game.hostRequestedSub.indexOf(toUnrequest);
if (userIndex < 0 && hostIndex < 0)
return user.sendTo(room, `|error|${toUnrequest} is not requesting a sub.`);
if (userIndex > -1) {
game.requestedSub.splice(userIndex, 1);
user.sendTo(room, `${toUnrequest}'s sub request has been removed.`);
}
if (hostIndex > -1) {
game.hostRequestedSub.splice(userIndex, 1);
user.sendTo(room, `${toUnrequest} has been removed from the host sublist.`);
}
break;
default:
if (game.hostid !== user.id && !game.cohostids.includes(user.id))
this.checkCan("mute", null, room);
const toSubOut = game.getPlayer(action);
const toSubIn = toID(args.shift());
if (!toSubOut)
return this.errorReply(`${toSubOut} is not in the game.`);
const targetUser = Users.get(toSubIn);
if (!targetUser)
return this.errorReply(`The user "${toSubIn}" was not found.`);
game.canJoin(targetUser, false, cmd === "forcesub");
if (game.subs.includes(targetUser.id)) {
game.subs.splice(game.subs.indexOf(targetUser.id), 1);
}
if (game.hostRequestedSub.includes(toSubOut.id)) {
game.hostRequestedSub.splice(game.hostRequestedSub.indexOf(toSubOut.id), 1);
}
if (game.requestedSub.includes(toSubOut.id)) {
game.requestedSub.splice(game.requestedSub.indexOf(toSubOut.id), 1);
}
game.sub(toSubOut, targetUser);
game.logAction(user, `substituted a player`);
}
},
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(target, room, user) {
room = this.requireRoom();
const game = this.requireGame(Mafia);
if (game.hostid !== user.id && !game.cohostids.includes(user.id))
this.checkCan("mute", null, room);
if (this.meansYes(toID(target))) {
if (game.autoSub)
return this.errorReply(`Automatic subbing of players is already enabled.`);
game.autoSub = true;
user.sendTo(room, `Automatic subbing of players has been enabled.`);
game.nextSub();
} else if (this.meansNo(toID(target))) {
if (!game.autoSub)
return this.errorReply(`Automatic subbing of players is already disabled.`);
game.autoSub = false;
user.sendTo(room, `Automatic subbing of players has been disabled.`);
} else {
return this.parse(`/help mafia autosub`);
}
game.logAction(user, `changed autosub status`);
},
autosubhelp: [
`/mafia autosub [yes|no] - Sets if players will automatically sub out if a user is on the sublist. Requires host % @ # ~`
],
cohost: "subhost",
subhost(target, room, user, connection, cmd) {
room = this.requireRoom();
const game = this.requireGame(Mafia);
this.checkChat();
if (!target)
return this.parse(`/help mafia ${cmd}`);
this.checkCan("mute", null, room);
const { targetUser } = this.requireUser(target);
if (!room.users[targetUser.id])
return this.errorReply(`${targetUser.name} is not in this room, and cannot be hosted.`);
if (game.hostid === targetUser.id)
return this.errorReply(`${targetUser.name} is already the host.`);
if (game.cohostids.includes(targetUser.id))
return this.errorReply(`${targetUser.name} is already a cohost.`);
if (game.getPlayer(targetUser.id)) {
return this.errorReply(`${targetUser.name} cannot become a host because they are playing.`);
}
if (game.subs.includes(targetUser.id))
game.subs.splice(game.subs.indexOf(targetUser.id), 1);
if (cmd.includes("cohost")) {
game.cohostids.push(targetUser.id);
game.cohosts.push(import_lib.Utils.escapeHTML(targetUser.name));
game.sendDeclare(import_lib.Utils.html`${targetUser.name} has been added as a cohost by ${user.name}`);
for (const conn of targetUser.connections) {
void Chat.resolvePage(`view-mafia-${room.roomid}`, targetUser, conn);
}
this.modlog("MAFIACOHOST", targetUser, null, { noalts: true, noip: true });
} else {
const oldHostid = game.hostid;
const oldHost = Users.get(game.hostid);
if (oldHost)
oldHost.send(`>view-mafia-${room.roomid}
|deinit`);
const queueIndex = hostQueue.indexOf(targetUser.id);
if (queueIndex > -1)
hostQueue.splice(queueIndex, 1);
game.host = import_lib.Utils.escapeHTML(targetUser.name);
game.hostid = targetUser.id;
game.played.push(targetUser.id);
for (const conn of targetUser.connections) {
void Chat.resolvePage(`view-mafia-${room.roomid}`, targetUser, conn);
}
game.sendDeclare(import_lib.Utils.html`${targetUser.name} has been substituted as the new host, replacing ${oldHostid}.`);
this.modlog("MAFIASUBHOST", targetUser, `replacing ${oldHostid}`, { noalts: true, noip: true });
}
},
subhosthelp: [`/mafia subhost [user] - Substitues the user as the new game host.`],
cohosthelp: [
`/mafia cohost [user] - Adds the user as a cohost. Cohosts can talk during the game, as well as perform host actions.`
],
uncohost: "removecohost",
removecohost(target, room, user) {
room = this.requireRoom();
const game = this.requireGame(Mafia);
this.checkChat();
if (!target)
return this.parse("/help mafia subhost");
this.checkCan("mute", null, room);
const targetID = toID(target);
const cohostIndex = game.cohostids.indexOf(targetID);
if (cohostIndex < 0) {
if (game.hostid === targetID) {
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.cohostids.splice(cohostIndex, 1);
game.sendDeclare(import_lib.Utils.html`${target} was removed as a cohost by ${user.name}`);
this.modlog("MAFIAUNCOHOST", target, null, { noalts: true, noip: true });
},
end(target, room, user) {
room = this.requireRoom();
const game = this.requireGame(Mafia);
if (game.hostid !== user.id && !game.cohostids.includes(user.id))
this.checkCan("show", null, room);
game.end();
this.modlog("MAFIAEND", null);
},
endhelp: [`/mafia end - End the current game of mafia. Requires host + % @ # ~`],
role: "data",
alignment: "data",
theme: "data",
term: "data",
dt: "data",
data(target, room, user, connection, cmd) {
if (room?.settings.mafiaDisabled)
return this.errorReply(`Mafia is disabled for this room.`);
if (cmd === "role" && !target && room) {
const game2 = room.getGame(Mafia);
if (!game2) {
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]`);
}
const player = game2.getPlayer(user.id);
if (!player)
return this.errorReply(`You are not in the game of ${game2.title}.`);
const role = player.role;
if (!role)
return this.errorReply(`You do not have a role yet.`);
return this.sendReplyBox(`Your role is: ${role.safeName}`);
}
const game = room?.getGame(Mafia);
if (game && (game.hostid === user.id || game.cohostids.includes(user.id))) {
this.broadcastMessage = this.message.toLowerCase().replace(/[^a-z0-9\s!,]/g, "");
}
if (!this.runBroadcast())
return false;
if (!target)
return this.parse(`/help mafia data`);
target = toID(target);
if (target in MafiaData.aliases)
target = MafiaData.aliases[target];
let result = null;
let dataType = cmd;
const cmdTypes = {
role: "roles",
alignment: "alignments",
theme: "themes",
term: "terms",
idea: "IDEAs"
};
if (cmd in cmdTypes) {
const toSearch = MafiaData[cmdTypes[cmd]];
result = toSearch[target];
} else {
for (const [cmdType, dataKey] of Object.entries(cmdTypes)) {
if (target in MafiaData[dataKey]) {
result = MafiaData[dataKey][target];
dataType = cmdType;
break;
}
}
}
if (!result)
return this.errorReply(`"${target}" is not a valid mafia alignment, role, theme, or IDEA.`);
let buf = `
${result.name}
Type: ${dataType} `;
if (dataType === "theme") {
if (result.desc) {
buf += `Description: ${result.desc} Setups:`;
}
for (const i in result) {
const num = parseInt(i);
if (isNaN(num))
continue;
buf += `${i}: `;
const count = {};
const roles = [];
for (const role of result[num].split(",").map((x) => x.trim())) {
count[role] = count[role] ? count[role] + 1 : 1;
}
for (const role in count) {
roles.push(count[role] > 1 ? `${count[role]}x ${role}` : role);
}
buf += `${roles.join(", ")} `;
}
} else if (dataType === "idea") {
if (result.picks && result.choices) {
buf += `Number of Picks: ${result.picks.length} (${result.picks.join(", ")}) `;
buf += `Number of Choices: ${result.choices} `;
}
buf += `Roles:`;
for (const idearole of result.roles) {
buf += `${idearole} `;
}
} else {
if (result.memo)
buf += `${result.memo.join(" ")}`;
}
return this.sendReplyBox(buf);
},
datahelp: [
`/mafia data [alignment|role|modifier|theme|term] - Get information on a mafia alignment, role, modifier, theme, or term.`
],
winfaction: "win",
unwinfaction: "win",
unwin: "win",
win(target, room, user, connection, cmd) {
const isUnwin = cmd.startsWith("unwin");
room = this.requireRoom("mafia");
if (!room || room.settings.mafiaDisabled)
return this.errorReply(`Mafia is disabled for this room.`);
this.checkCan("mute", null, room);
const args = target.split(",");
let points = parseInt(args[0]);
if (isUnwin) {
points *= -1;
}
if (isNaN(points)) {
points = 10;
if (isUnwin) {
points *= -1;
}
} else {
if (points > 100 || points < -100) {
return this.errorReply(`You cannot give or take more than 100 points at a time.`);
}
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 < 0 ? points * -1 : points} point${Chat.plural(points, "s were", " was")} ${points <= 0 ? "taken from " : "awarded to "} `;
if (cmd === "winfaction" || cmd === "unwinfaction") {
const game = this.requireGame(Mafia);
for (let faction of args) {
faction = toID(faction);
const inFaction = [];
for (const player of game.players) {
if (player.role && toID(player.role.alignment) === faction) {
toGiveTo.push(player.id);
inFaction.push(player.id);
}
}
if (inFaction.length)
buf += ` the ${faction} faction: ${inFaction.join(", ")};`;
}
} else {
toGiveTo = args;
buf += toGiveTo.join(", ");
}
if (!toGiveTo.length)
return this.parse("/help mafia win");
let gavePoints = false;
for (let u of toGiveTo) {
u = toID(u);
if (!u)
continue;
if (!gavePoints)
gavePoints = true;
if (!logs.leaderboard[month][u])
logs.leaderboard[month][u] = 0;
logs.leaderboard[month][u] += points;
if (logs.leaderboard[month][u] === 0)
delete logs.leaderboard[month][u];
}
if (!gavePoints)
return this.parse("/help mafia win");
writeFile(LOGS_FILE, logs);
this.modlog(`MAFIAPOINTS`, null, `${points < 0 ? points * -1 : points} points were ${points < 0 ? "taken from" : "awarded to"} ${Chat.toListString(toGiveTo)}`);
room.add(buf).update();
},
winhelp: [
`/mafia (un)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 (un)winfaction (points), [faction] - Award the specified points to all the players in the given faction.`
],
unmvp: "mvp",
mvp(target, room, user, connection, cmd) {
room = this.requireRoom("mafia");
if (!room || room.settings.mafiaDisabled)
return this.errorReply(`Mafia is disabled for this room.`);
this.checkCan("mute", null, room);
const args = target.split(",");
if (!args.length)
return this.parse("/help mafia mvp");
const month = new Date().toLocaleString("en-us", { month: "numeric", year: "numeric" });
if (!logs.mvps[month])
logs.mvps[month] = {};
if (!logs.leaderboard[month])
logs.leaderboard[month] = {};
let gavePoints = false;
for (let u of args) {
u = toID(u);
if (!u)
continue;
if (!gavePoints)
gavePoints = true;
if (!logs.leaderboard[month][u])
logs.leaderboard[month][u] = 0;
if (!logs.mvps[month][u])
logs.mvps[month][u] = 0;
if (cmd === "unmvp") {
logs.mvps[month][u]--;
logs.leaderboard[month][u] -= 10;
if (logs.mvps[month][u] === 0)
delete logs.mvps[month][u];
if (logs.leaderboard[month][u] === 0)
delete logs.leaderboard[month][u];
} else {
logs.mvps[month][u]++;
logs.leaderboard[month][u] += 10;
}
}
if (!gavePoints)
return this.parse("/help mafia mvp");
writeFile(LOGS_FILE, logs);
this.modlog(`MAFIA${cmd.toUpperCase()}`, null, `MVP and 10 points were ${cmd === "unmvp" ? "taken from" : "awarded to"} ${Chat.toListString(args)}`);
room.add(`MVP and 10 points were ${cmd === "unmvp" ? "taken from" : "awarded to"}: ${Chat.toListString(args)}`).update();
},
mvphelp: [
`/mafia mvp [user1], [user2], ... - Gives a MVP point and 10 leaderboard points to the users specified.`,
`/mafia unmvp [user1], [user2], ... - Takes away a MVP point and 10 leaderboard points from the users specified.`
],
hostlogs: "leaderboard",
playlogs: "leaderboard",
leaverlogs: "leaderboard",
mvpladder: "leaderboard",
lb: "leaderboard",
leaderboard(target, room, user, connection, cmd) {
room = this.requireRoom("mafia");
if (!room || room.settings.mafiaDisabled)
return this.errorReply(`Mafia is disabled for this room.`);
if (["hostlogs", "playlogs", "leaverlogs"].includes(cmd)) {
this.checkCan("mute", null, room);
} else {
if (!this.runBroadcast())
return;
}
if (cmd === "lb")
cmd = "leaderboard";
if (this.broadcasting) {
return this.sendReplyBox(``);
}
return this.parse(`/join view-mafialadder-${cmd}`);
},
leaderboardhelp: [
`/mafia [leaderboard|mvpladder] - View the leaderboard or MVP ladder for the current or last month.`,
`/mafia [hostlogs|playlogs|leaverlogs] - View the host, play, or leaver logs for the current or last month. Requires % @ # ~`
],
gameban: "hostban",
hostban(target, room, user, connection, cmd) {
if (!target)
return this.parse("/help mafia hostban");
room = this.requireRoom();
this.checkCan("warn", null, room);
const { targetUser, rest } = this.requireUser(target);
const [string1, string2] = this.splitOne(rest);
let duration, reason;
if (parseInt(string1)) {
duration = parseInt(string1);
reason = string2;
} else {
duration = parseInt(string2);
reason = string1;
}
if (!duration)
duration = 2;
if (!reason)
reason = "";
if (reason.length > 300) {
return this.errorReply("The reason is too long. It cannot exceed 300 characters.");
}
const userid = toID(targetUser);
if (Punishments.hasRoomPunishType(room, userid, `MAFIA${this.cmd.toUpperCase()}`)) {
return this.errorReply(`User '${targetUser.name}' is already ${this.cmd}ned in this room.`);
} else if (Punishments.hasRoomPunishType(room, userid, `MAFIAGAMEBAN`)) {
return this.errorReply(`User '${targetUser.name}' is already gamebanned in this room, which also means they can't host.`);
} else if (Punishments.hasRoomPunishType(room, userid, `MAFIAHOSTBAN`)) {
user.sendTo(room, `User '${targetUser.name}' is already hostbanned in this room, but they will now be gamebanned.`);
this.parse(`/mafia unhostban ${targetUser.name}`);
}
if (cmd === "hostban") {
Mafia.hostBan(room, targetUser, reason, duration);
} else {
Mafia.gameBan(room, targetUser, reason, duration);
}
this.modlog(`MAFIA${cmd.toUpperCase()}`, targetUser, reason);
this.privateModAction(`${targetUser.name} was banned from ${cmd === "hostban" ? "hosting" : "playing"} mafia games by ${user.name}.`);
},
hostbanhelp: [
`/mafia (un)hostban [user], [reason], [duration] - Ban a user from hosting games for [duration] days. Requires % @ # ~`,
`/mafia (un)gameban [user], [reason], [duration] - Ban a user from playing games for [duration] days. Requires % @ # ~`
],
ban: "gamebanhelp",
banhelp: "gamebanhelp",
gamebanhelp() {
this.parse("/mafia hostbanhelp");
},
ungameban: "unhostban",
unhostban(target, room, user, connection, cmd) {
if (!target)
return this.parse("/help mafia hostban");
room = this.requireRoom();
this.checkCan("warn", null, room);
const { targetUser } = this.requireUser(target, { allowOffline: true });
if (!Mafia.isGameBanned(room, targetUser) && cmd === "ungameban") {
return this.errorReply(`User '${targetUser.name}' isn't banned from playing mafia games.`);
} else if (!Mafia.isHostBanned(room, targetUser) && cmd === "unhostban") {
return this.errorReply(`User '${targetUser.name}' isn't banned from hosting mafia games.`);
}
if (cmd === "unhostban")
Mafia.unhostBan(room, targetUser);
else
Mafia.ungameBan(room, targetUser);
this.privateModAction(`${targetUser.name} was unbanned from ${cmd === "unhostban" ? "hosting" : "playing"} mafia games by ${user.name}.`);
this.modlog(`MAFIA${cmd.toUpperCase()}`, targetUser, null, { noip: 1, noalts: 1 });
},
overwriterole: "addrole",
addrole(target, room, user, connection, cmd) {
room = this.requireRoom("mafia");
this.checkCan("mute", null, room);
const overwrite = cmd === "overwriterole";
const [name, alignment, image, ...memo] = target.split("|").map((e) => e.trim());
const id = toID(name);
if (!id || !memo.length)
return this.parse(`/help mafia addrole`);
if (alignment && !(alignment in MafiaData.alignments))
return this.errorReply(`${alignment} is not a valid alignment.`);
if (image && !VALID_IMAGES.includes(image))
return this.errorReply(`${image} is not a valid image.`);
if (!overwrite && id in MafiaData.roles) {
return this.errorReply(`${name} is already a role. Use /mafia overwriterole to overwrite.`);
}
if (id in MafiaData.alignments)
return this.errorReply(`${name} is already an alignment.`);
if (id in MafiaData.aliases) {
return this.errorReply(`${name} is already an alias (pointing to ${MafiaData.aliases[id]}).`);
}
const role = { name, memo };
if (alignment)
role.alignment = alignment;
if (image)
role.image = image;
MafiaData.roles[id] = role;
writeFile(DATA_FILE, MafiaData);
this.modlog(`MAFIAADDROLE`, null, id, { noalts: true, noip: true });
this.sendReply(`The role ${id} was added to the database.`);
},
addrolehelp: [
`/mafia addrole name|alignment|image|memo1|memo2... - adds a role to the database. Name, memo are required. Requires % @ # ~`
],
overwritealignment: "addalignment",
addalignment(target, room, user, connection, cmd) {
room = this.requireRoom("mafia");
this.checkCan("mute", null, room);
const overwrite = cmd === "overwritealignment";
const [name, plural, color, buttonColor, image, ...memo] = target.split("|").map((e) => e.trim());
const id = toID(name);
if (!id || !plural || !memo.length)
return this.parse(`/help mafia addalignment`);
if (image && !VALID_IMAGES.includes(image))
return this.errorReply(`${image} is not a valid image.`);
if (!overwrite && id in MafiaData.alignments) {
return this.errorReply(`${name} is already an alignment. Use /mafia overwritealignment to overwrite.`);
}
if (id in MafiaData.roles)
return this.errorReply(`${name} is already a role.`);
if (id in MafiaData.aliases) {
return this.errorReply(`${name} is already an alias (pointing to ${MafiaData.aliases[id]})`);
}
const alignment = { name, plural, memo };
if (color)
alignment.color = color;
if (buttonColor)
alignment.buttonColor = buttonColor;
if (image)
alignment.image = image;
MafiaData.alignments[id] = alignment;
writeFile(DATA_FILE, MafiaData);
this.modlog(`MAFIAADDALIGNMENT`, null, id, { noalts: true, noip: true });
this.sendReply(`The alignment ${id} was added to the database.`);
},
addalignmenthelp: [
`/mafia addalignment name|plural|color|button color|image|memo1|memo2... - adds a memo to the database. Name, plural, memo are required. Requires % @ # ~`
],
overwritetheme: "addtheme",
addtheme(target, room, user, connection, cmd) {
room = this.requireRoom("mafia");
this.checkCan("mute", null, room);
const overwrite = cmd === "overwritetheme";
const [name, desc, ...rolelists] = target.split("|").map((e) => e.trim());
const id = toID(name);
if (!id || !desc || !rolelists.length)
return this.parse(`/help mafia addtheme`);
if (!overwrite && id in MafiaData.themes) {
return this.errorReply(`${name} is already a theme. Use /mafia overwritetheme to overwrite.`);
}
if (id in MafiaData.IDEAs)
return this.errorReply(`${name} is already an IDEA.`);
if (id in MafiaData.aliases) {
return this.errorReply(`${name} is already an alias (pointing to ${MafiaData.aliases[id]})`);
}
const rolelistsMap = {};
const uniqueRoles = /* @__PURE__ */ new Set();
for (const rolelist of rolelists) {
const [players, roles] = import_lib.Utils.splitFirst(rolelist, ":", 2).map((e) => e.trim());
const playersNum = parseInt(players);
for (const role of roles.split(",")) {
uniqueRoles.add(role.trim());
}
rolelistsMap[playersNum] = roles;
}
const problems = [];
for (const role of uniqueRoles) {
const parsedRole = Mafia.parseRole(role);
if (parsedRole.problems.length)
problems.push(...parsedRole.problems);
}
if (problems.length)
return this.errorReply(`Problems found when parsing roles:
${problems.join("\n")}`);
const theme = { name, desc, ...rolelistsMap };
MafiaData.themes[id] = theme;
writeFile(DATA_FILE, MafiaData);
this.modlog(`MAFIAADDTHEME`, null, id, { noalts: true, noip: true });
this.sendReply(`The theme ${id} was added to the database.`);
},
addthemehelp: [
`/mafia addtheme name|description|players:rolelist|players:rolelist... - adds a theme to the database. Requires % @ # ~`
],
overwriteidea: "addidea",
addidea(target, room, user, connection, cmd) {
room = this.requireRoom("mafia");
this.checkCan("mute", null, room);
const overwrite = cmd === "overwriteidea";
let [meta, ...roles] = target.split("\n");
roles = roles.map((e) => e.trim());
if (!meta || !roles.length)
return this.parse(`/help mafia addidea`);
const [name, choicesStr, ...picks] = meta.split("|");
const id = toID(name);
const choices = parseInt(choicesStr);
if (!id || !choices || !picks.length)
return this.parse(`/help mafia addidea`);
if (choices <= picks.length)
return this.errorReply(`You need to have more choices than picks.`);
if (!overwrite && id in MafiaData.IDEAs) {
return this.errorReply(`${name} is already an IDEA. Use /mafia overwriteidea to overwrite.`);
}
if (id in MafiaData.themes)
return this.errorReply(`${name} is already a theme.`);
if (id in MafiaData.aliases) {
return this.errorReply(`${name} is already an alias (pointing to ${MafiaData.aliases[id]})`);
}
const checkedRoles = [];
const problems = [];
for (const role of roles) {
if (checkedRoles.includes(role))
continue;
const parsedRole = Mafia.parseRole(role);
if (parsedRole.problems.length)
problems.push(...parsedRole.problems);
checkedRoles.push(role);
}
if (problems.length)
return this.errorReply(`Problems found when parsing roles:
${problems.join("\n")}`);
const IDEA = { name, choices, picks, roles };
MafiaData.IDEAs[id] = IDEA;
writeFile(DATA_FILE, MafiaData);
this.modlog(`MAFIAADDIDEA`, null, id, { noalts: true, noip: true });
this.sendReply(`The IDEA ${id} was added to the database.`);
},
addideahelp: [
`/mafia addidea name|choices (number)|pick1|pick2... (new line here)`,
`(newline separated rolelist) - Adds an IDEA to the database. Requires % @ # ~`
],
overwriteterm: "addterm",
addterm(target, room, user, connection, cmd) {
room = this.requireRoom("mafia");
this.checkCan("mute", null, room);
const overwrite = cmd === "overwriteterm";
const [name, ...memo] = target.split("|").map((e) => e.trim());
const id = toID(name);
if (!id || !memo.length)
return this.parse(`/help mafia addterm`);
if (!overwrite && id in MafiaData.terms) {
return this.errorReply(`${name} is already a term. Use /mafia overwriteterm to overwrite.`);
}
if (id in MafiaData.aliases) {
return this.errorReply(`${name} is already an alias (pointing to ${MafiaData.aliases[id]})`);
}
const term = { name, memo };
MafiaData.terms[id] = term;
writeFile(DATA_FILE, MafiaData);
this.modlog(`MAFIAADDTERM`, null, id, { noalts: true, noip: true });
this.sendReply(`The term ${id} was added to the database.`);
},
addtermhelp: [`/mafia addterm name|memo1|memo2... - Adds a term to the database. Requires % @ # ~`],
overwritealias: "addalias",
addalias(target, room, user, connection, cmd) {
room = this.requireRoom("mafia");
this.checkCan("mute", null, room);
const [from, to] = target.split(",").map(toID);
if (!from || !to)
return this.parse(`/help mafia addalias`);
if (from in MafiaData.aliases) {
return this.errorReply(`${from} is already an alias (pointing to ${MafiaData.aliases[from]})`);
}
let foundTarget = false;
for (const entry of ["alignments", "roles", "themes", "IDEAs", "terms"]) {
const dataEntry = MafiaData[entry];
if (from in dataEntry)
return this.errorReply(`${from} is already a ${entry.slice(0, -1)}`);
if (to in dataEntry)
foundTarget = true;
}
if (!foundTarget)
return this.errorReply(`No database entry exists with the key ${to}.`);
MafiaData.aliases[from] = to;
writeFile(DATA_FILE, MafiaData);
this.modlog(`MAFIAADDALIAS`, null, `${from}: ${to}`, { noalts: true, noip: true });
this.sendReply(`The alias ${from} was added, pointing to ${to}.`);
},
addaliashelp: [
`/mafia addalias from,to - Adds an alias to the database, redirecting (from) to (to). Requires % @ # ~`
],
deletedata(target, room, user) {
room = this.requireRoom("mafia");
this.checkCan("mute", null, room);
let [source, entry] = target.split(",");
entry = toID(entry);
if (!(source in MafiaData)) {
return this.errorReply(`Invalid source. Valid sources are ${Object.keys(MafiaData).join(", ")}`);
}
const dataSource = MafiaData[source];
if (!(entry in dataSource))
return this.errorReply(`${entry} does not exist in ${source}.`);
let buf = "";
if (dataSource === MafiaData.alignments) {
if (entry === "solo" || entry === "town")
return this.errorReply(`You cannot delete the solo or town alignments.`);
for (const key in MafiaData.roles) {
if (MafiaData.roles[key].alignment === entry) {
buf += `Removed alignment of role ${key}.`;
delete MafiaData.roles[key].alignment;
}
}
}
if (dataSource !== MafiaData.aliases) {
for (const key in MafiaData.aliases) {
if (MafiaData.aliases[key] === entry) {
buf += `Removed alias ${key}`;
delete MafiaData.aliases[key];
}
}
}
delete dataSource[entry];
writeFile(DATA_FILE, MafiaData);
if (buf)
this.sendReply(buf);
this.modlog(`MAFIADELETEDATA`, null, `${entry} from ${source}`, { noalts: true, noip: true });
this.sendReply(`The entry ${entry} was deleted from the ${source} database.`);
},
deletedatahelp: [`/mafia deletedata source,entry - Removes an entry from the database. Requires % @ # ~`],
listdata(target, room, user) {
if (!(target in MafiaData)) {
return this.errorReply(`Invalid source. Valid sources are ${Object.keys(MafiaData).join(", ")}`);
}
const dataSource = MafiaData[target];
if (dataSource === MafiaData.aliases) {
const aliases = Object.entries(MafiaData.aliases).map(([from, to]) => `${from}: ${to}`).join(" ");
return this.sendReplyBox(`Mafia aliases: ${aliases}`);
} else {
const entries = Object.entries(dataSource).map(([key, data]) => ``).join("");
return this.sendReplyBox(`Mafia ${target}: ${entries}`);
}
},
disable(target, room, user) {
room = this.requireRoom();
this.checkCan("gamemanagement", null, room);
if (room.settings.mafiaDisabled) {
return this.errorReply("Mafia is already disabled.");
}
room.settings.mafiaDisabled = true;
room.saveSettings();
this.modlog("MAFIADISABLE", null);
return this.sendReply("Mafia has been disabled for this room.");
},
disablehelp: [`/mafia disable - Disables mafia in this room. Requires # ~`],
enable(target, room, user) {
room = this.requireRoom();
this.checkCan("gamemanagement", null, room);
if (!room.settings.mafiaDisabled) {
return this.errorReply("Mafia is already enabled.");
}
room.settings.mafiaDisabled = false;
room.saveSettings();
this.modlog("MAFIAENABLE", null);
return this.sendReply("Mafia has been enabled for this room.");
},
enablehelp: [`/mafia enable - Enables mafia in this room. Requires # ~`]
},
mafiahelp(target, room, user) {
if (!this.runBroadcast())
return;
let buf = `Commands for the Mafia Plugin Most commands are used through buttons in the game screen.
`;
buf += `General Commands`;
buf += [
` General Commands for the Mafia Plugin: `,
`/mafia host [user] - Create a game of Mafia with [user] as the host. Roomvoices can only host themselves. Requires + % @ # ~`,
`/mafia nexthost - Host the next user in the host queue. Only works in the Mafia Room. Requires + % @ # ~`,
`/mafia forcehost [user] - Bypass the host queue and host [user]. Only works in the Mafia Room. Requires % @ # ~`,
`/mafia sub [in|out] - Request to sub into the game, or cancel a request to sub out.`,
`/mafia spectate - Spectate the game of mafia.`,
`/mafia votes - Display the current vote count, and who's voting who.`,
`/mafia players - Display the current list of players, will highlight players.`,
`/mafia [rl|orl] - Display the role list or the original role list for the current game.`,
`/mafia data [alignment|role|modifier|theme|term] - Get information on a mafia alignment, role, modifier, theme, or term.`,
`/mafia subhost [user] - Substitues the user as the new game host. Requires % @ # ~`,
`/mafia (un)cohost [user] - Adds/removes the user as a cohost. Cohosts can talk during the game, as well as perform host actions. Requires % @ # ~`,
`/mafia [enable|disable] - Enables/disables mafia in this room. Requires # ~`
].join(" ");
buf += `Player Commands`;
buf += [
` Commands that players can use: `,
`/mafia [join|leave] - Joins/leaves the game. Can only be done while signups are open.`,
`/mafia vote [player|novote] - Vote the specified player or abstain from voting.`,
`/mafia unvote - Withdraw your vote. Fails if you're not voting 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 idle - Tells the host if you are idling.`,
`/mafia action [details] - Tells the host you are using an action with the given submission details.`
].join(" ");
buf += `Host Commands`;
buf += [
` Commands for game hosts and Cohosts to use: `,
`/mafia playercap [cap|none]- Limit the number of players able to join the game. Player cap cannot be more than 20 or less than 2. Requires host % @ # ~`,
`/mafia close - Closes signups for the current game. Requires host % @ # ~`,
`/mafia closedsetup [on|off] - Sets if the game is a closed setup. Closed setups don't show the role list to players. Requires host % @ # ~`,
`/mafia takeidles [on|off] - Sets if idles are accepted by the script or not. Requires host % @ # ~`,
`/mafia prod - Notifies players that they must submit an action or idle if they haven't yet. Requires host % @ # ~`,
`/mafia reveal [on|off] - Sets if roles reveal on death or not. Requires host % @ # ~`,
`/mafia selfvote [on|hammer|off] - Allows players to self vote either at hammer or anytime. Requires host % @ # ~`,
`/mafia [enablenl|disablenl] - Allows or disallows players abstain from voting. Requires host % @ # ~`,
`/mafia votelock [on|off] - Allows or disallows players to change their vote. Requires host % @ # ~`,
`/mafia voting [on|off] - Allows or disallows voting. Requires host % @ # ~`,
`/mafia forcevote [yes/no] - Forces players' votes onto themselves, and prevents unvoting. Requires host % @ # ~`,
`/mafia setroles [comma seperated roles] - Set the roles for a game of mafia. You need to provide one role per player. Requires host % @ # ~`,
`/mafia forcesetroles [comma seperated roles] - Forcibly set the roles for a game of mafia. No role PM information or alignment will be set. Requires host % @ # ~`,
`/mafia start - Start the game of mafia. Signups must be closed. Requires host % @ # ~`,
`/mafia [day|night] - Move to the next game day or 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 still. Requires host % @ # ~`,
`/mafia spiritstump [player] - Kills a player, but allows them to talk and vote during the day. Requires host % @ # ~`,
`/mafia kick [player] - Kicks a player from the game without revealing their role. Requires host % @ # ~`,
`/mafia revive [player] - Revives a player who was eliminated. Requires host % @ # ~`,
`/mafia add [player] - Adds a new player to the game. Requires host % @ # ~`,
`/mafia revealrole [player] - Reveals the role of a player. Requires host % @ # ~`,
`/mafia revealas [player], [role] - Fakereveals the role of a player as a certain role. Requires host % @ # ~`,
`/mafia (un)silence [player] - Silences [player], preventing them from talking at all. Requires host % @ # ~`,
`/mafia (un)nighttalk [player] - Allows [player] to talk freely during the night. Requires host % @ # ~`,
`/mafia (un)[priest|actor] [player] - Makes [player] a priest (can't hammer) or actor (can only hammer). 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 (un)[love|hate] [player] - Makes it take 1 more (love) or less (hate) vote to hammer [player]. Requires host % @ # ~`,
`/mafia (un)[mayor|voteless] [player] - Makes [player]'s' vote worth 2 votes (mayor) or makes [player]'s vote worth 0 votes (voteless). Requires host % @ # ~`,
`/mafia hammer [hammer] - sets the hammer count to [hammer] and resets votes`,
`/mafia hammer off - disables hammering`,
`/mafia shifthammer [hammer] - sets the hammer count to [hammer] without resetting votes`,
`/mafia resethammer - sets the hammer to the default, resetting votes`,
`/mafia playerroles - View all the player's roles in chat. Requires host`,
`/mafia resetgame - Resets game data. Does not change settings from the host besides deadlines or add/remove any players. Requires host % @ # ~`,
`/mafia end - End the current game of mafia. Requires host + % @ # ~`
].join(" ");
buf += `IDEA Module Commands`;
buf += [
` Commands for using IDEA modules `,
`/mafia idea [idea] - starts the IDEA module [idea]. Requires + % @ # ~, voices can only start for themselves`,
`/mafia ideareroll - rerolls the IDEA module. Requires host % @ # ~`,
`/mafia ideapick [selection], [role] - selects a role`,
`/mafia ideadiscards - shows the discarded roles`,
`/mafia ideadiscards [off|on] - hides discards from the players. Requires host % @ # ~`,
`/mafia customidea choices, picks (new line here, shift+enter)`,
`(comma or newline separated rolelist) - Starts an IDEA module with custom roles. Requires % @ # ~`
].join(" ");
buf += ``;
buf += `Mafia Room Specific Commands`;
buf += [
` Commands that are only useable in the Mafia Room: `,
`/mafia queue add, [user] - Adds the user to the host queue. Requires + % @ # ~, voices can only add themselves.`,
`/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 (un)mvp [user1], [user2], ... - Gives a MVP point and 10 leaderboard points to the users specified.`,
`/mafia [leaderboard|mvpladder] - View the leaderboard or MVP ladder for the current or last month.`,
`/mafia [hostlogs|playlogs] - View the host logs or play logs for the current or last month. Requires % @ # ~`,
`/mafia (un)hostban [user], [duration] - Ban a user from hosting games for [duration] days. Requires % @ # ~`,
`/mafia (un)gameban [user], [duration] - Ban a user from playing games for [duration] days. Requires % @ # ~`
].join(" ");
buf += ``;
return this.sendReplyBox(buf);
}
};
const roomSettings = (room) => ({
label: "Mafia",
permission: "editroom",
options: [
[`disabled`, room.settings.mafiaDisabled || "mafia disable"],
[`enabled`, !room.settings.mafiaDisabled || "mafia enable"]
]
});
process.nextTick(() => {
Chat.multiLinePattern.register("/mafia (custom|add|overwrite)idea");
});
//# sourceMappingURL=mafia.js.map