mirror of
https://github.com/smogon/pokemon-showdown.git
synced 2026-05-24 08:46:26 -05:00
610 lines
17 KiB
JavaScript
Executable File
610 lines
17 KiB
JavaScript
Executable File
// Mafia chat plugin.
|
|
// By bumbadadabum, with input from Zarel and art by crobat.
|
|
|
|
'use strict';
|
|
|
|
let MafiaData = require('./mafia-data.js');
|
|
|
|
const permission = 'ban';
|
|
|
|
const deadImage = '<img width="75" height="75" src="//play.pokemonshowdown.com/fx/mafia-dead.png" />';
|
|
const meetingMsg = {town: 'The town has lynched a subject!', mafia: 'The mafia strikes again!'};
|
|
|
|
class MafiaPlayer extends Rooms.RoomGamePlayer {
|
|
constructor(user, game) {
|
|
super(user, game);
|
|
|
|
this.voting = false;
|
|
this.targeting = false;
|
|
}
|
|
|
|
event(event) {
|
|
if (this.class[event].target) {
|
|
this.targeting = true;
|
|
this.toExecute = this.class[event].callback;
|
|
if (this.class[event].target.count === 'single') {
|
|
this.singleTarget(this.class[event].target.side);
|
|
}
|
|
this.targetWindow(this.class.image, this.class[event].flavorText);
|
|
} else {
|
|
this.toExecute = this.class[event].function;
|
|
}
|
|
|
|
this.game.executionOrder.push(this);
|
|
}
|
|
|
|
kill(message) {
|
|
if (this.invincible) return;
|
|
|
|
this.game.announcementWindow(deadImage, message + '<br/>' + Tools.escapeHTML(this.name + ', the ' + this.class.name) + ' lies dead on the ground.');
|
|
delete this.game.players[this.userid];
|
|
this.destroy();
|
|
}
|
|
|
|
eliminate() {
|
|
if (this.invincible) return;
|
|
|
|
if (this.game.gamestate === 'pregame') {
|
|
this.game.announcementWindow('', Tools.escapeHTML(this.name) + ' was kicked from the game.');
|
|
} else {
|
|
this.game.announcementWindow(deadImage, Tools.escapeHTML(this.name + ', the ' + this.class.name) + ' was eliminated from the game.');
|
|
}
|
|
delete this.game.players[this.userid];
|
|
this.destroy();
|
|
}
|
|
|
|
playerWindow(image, content) {
|
|
this.sendRoom('|html|' + this.game.mafiaWindow(image, content));
|
|
}
|
|
|
|
getRole() {
|
|
this.sendRoom('|html|' + this.game.mafiaWindow(this.class.image, Tools.escapeHTML(this.class.flavorText)));
|
|
}
|
|
|
|
targetWindow(image, content) {
|
|
let output = content;
|
|
output += '<br/><p>Who do you wish to target?</p>';
|
|
for (let i in this.validTargets) {
|
|
output += '<button value="/mafia target ' + this.validTargets[i].userid + '" name="send">' + Tools.escapeHTML(this.validTargets[i].name) + '</button>';
|
|
}
|
|
|
|
this.sendRoom('|uhtml|mafia' + this.game.room.gameNumber + 'target' + this.game.gamestate + this.game.day + '|' + this.game.mafiaWindow(image, output));
|
|
}
|
|
|
|
updateTarget(image) {
|
|
this.sendRoom('|uhtmlchange|mafia' + this.game.room.gameNumber + 'target' + this.game.gamestate + this.game.day + '|' + this.game.mafiaWindow(image, 'Targeting ' + Tools.escapeHTML(this.target.name) + '!'));
|
|
}
|
|
|
|
voteWindow(image, content) {
|
|
let output = content;
|
|
output += '<br/><p>Who do you wish to vote for?</p>';
|
|
for (let i in this.validVotes) {
|
|
output += '<button value="/mafia vote ' + this.validVotes[i].userid + '" name="send">' + Tools.escapeHTML(this.validVotes[i].name) + '</button>';
|
|
}
|
|
|
|
this.sendRoom('|uhtml|mafia' + this.game.room.gameNumber + 'vote' + this.game.gamestate + this.game.day + '|' + this.game.mafiaWindow(image, output));
|
|
}
|
|
|
|
// Targeting mechanics:
|
|
|
|
// Targets a single player of side side.
|
|
singleTarget(side, targetSelf) {
|
|
this.validTargets = {};
|
|
for (let i in this.game.players) {
|
|
let thisSide = this.game.players[i].class.side;
|
|
if ((side === 'any' || thisSide === side || (side === 'nomafia' && thisSide !== 'mafia')) && (targetSelf || this.game.players[i] !== this)) {
|
|
this.validTargets[i] = this.game.players[i];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Triggers after the user has selected their target.
|
|
onReceiveTarget(target) {
|
|
if (!this.targeting) {
|
|
return this.sendRoom("You're not selecting a target right now.");
|
|
}
|
|
|
|
if (target in this.validTargets) {
|
|
this.targeting = false;
|
|
this.target = target;
|
|
delete this.validTargets;
|
|
|
|
this.updateTarget(this.class.image);
|
|
|
|
for (let i in this.game.players) {
|
|
if (this.game.players[i].voting || this.game.players[i].targeting) {
|
|
return;
|
|
}
|
|
}
|
|
this.game.progress();
|
|
} else {
|
|
this.sendRoom("Invalid target");
|
|
}
|
|
}
|
|
|
|
// Triggers after the user has voted.
|
|
onReceiveVote(target) {
|
|
if (!this.voting) {
|
|
return;
|
|
}
|
|
|
|
if (target in this.validVotes) {
|
|
if (this.currentVote[target]) {
|
|
this.currentVote[target]++;
|
|
} else {
|
|
this.currentVote[target] = 1;
|
|
}
|
|
|
|
this.voting = false;
|
|
delete this.validVotes;
|
|
|
|
this.game.updateVotes();
|
|
|
|
for (let i in this.game.players) {
|
|
if (this.game.players[i].voting || this.game.players[i].targeting) {
|
|
return;
|
|
}
|
|
}
|
|
this.game.progress();
|
|
} else {
|
|
this.sendRoom("You can't vote for that person");
|
|
}
|
|
}
|
|
}
|
|
|
|
class Mafia extends Rooms.RoomGame {
|
|
constructor(room, max, roles) {
|
|
super(room);
|
|
|
|
if (room.gameNumber) {
|
|
room.gameNumber++;
|
|
} else {
|
|
room.gameNumber = 1;
|
|
}
|
|
|
|
this.gameid = 'hangman';
|
|
this.title = 'Mafia';
|
|
this.allowRenames = false;
|
|
this.playerCap = max;
|
|
this.PlayerClass = MafiaPlayer;
|
|
|
|
this.roles = roles;
|
|
this.day = 1;
|
|
this.gamestate = 'pregame';
|
|
this.timer = null;
|
|
|
|
this.room.send('|uhtml|mafia' + this.room.gameNumber + 'pregame|' + this.pregameWindow(false));
|
|
}
|
|
|
|
makePlayer(user) {
|
|
return new MafiaPlayer(user, this);
|
|
}
|
|
|
|
displayPregame() {
|
|
for (let i in this.room.users) {
|
|
let user = this.room.users[i];
|
|
|
|
if (user.userid in this.players) {
|
|
user.sendTo(this.room, '|uhtml|mafia' + this.room.gameNumber + 'pregame|' + this.pregameWindow(true));
|
|
} else {
|
|
user.sendTo(this.room, '|uhtml|mafia' + this.room.gameNumber + 'pregame|' + this.pregameWindow(false));
|
|
}
|
|
}
|
|
}
|
|
|
|
pregameWindow(joined) {
|
|
let output = '<div class="broadcast-blue"><h2>A game of mafia has been made!</h2><p>Participants: </p>';
|
|
let temp = Object.values(this.players);
|
|
for (let i = 0; i < temp.length; i++) {
|
|
output += Tools.escapeHTML(temp[i].name);
|
|
if (i < temp.length - 1) {
|
|
output += ', ';
|
|
}
|
|
}
|
|
if (joined) {
|
|
output += '<br/><button value="/mafia leave" name="send">Leave</button>';
|
|
} else {
|
|
output += '<br/><button value="/mafia join" name="send">Join</button>';
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
updatePregame() {
|
|
for (let i in this.room.users) {
|
|
let user = this.room.users[i];
|
|
|
|
if (user.userid in this.players) {
|
|
user.sendTo(this.room, '|uhtmlchange|mafia' + this.room.gameNumber + 'pregame|' + this.pregameWindow(true));
|
|
} else {
|
|
user.sendTo(this.room, '|uhtmlchange|mafia' + this.room.gameNumber + 'pregame|' + this.pregameWindow(false));
|
|
}
|
|
}
|
|
}
|
|
|
|
// UI
|
|
|
|
// Simple window, used for announcements and the likes.
|
|
mafiaWindow(image, content) {
|
|
let output = '<div class="broadcast-blue">';
|
|
output += '<h3>Day ' + this.day + '</h3>';
|
|
output += '<table><tr><td style="text-align:center;">' + image + '</td><td style="text-align:center;width:100%;">';
|
|
output += content;
|
|
output += '</td></tr></table></div>';
|
|
return output;
|
|
}
|
|
|
|
announcementWindow(image, content) {
|
|
this.room.add('|html|' + this.mafiaWindow(image, content));
|
|
this.room.update();
|
|
}
|
|
|
|
updateVotes() {
|
|
let text = '';
|
|
for (let i in this.currentVote) {
|
|
text += Tools.escapeHTML(this.players[i].name) + ': ' + this.currentVote[i] + ' votes.<br/>';
|
|
}
|
|
for (let i = 0; i < this.players.length; i++) {
|
|
let player = this.players[i];
|
|
if (player.voting) {
|
|
player.getUser().sendTo(this.room, '|uhtmlchange|mafia' + this.room.gameNumber + 'target' + this.gamestate + this.day + '|' + this.mafiaWindow('', text));
|
|
}
|
|
}
|
|
}
|
|
|
|
tallyVotes() {
|
|
let max = 0;
|
|
let toKill = null;
|
|
for (let i in this.currentVote) {
|
|
if (this.currentVote[i] > max) {
|
|
toKill = i;
|
|
} else if (this.currentVote[i] === max) {
|
|
toKill = null;
|
|
}
|
|
}
|
|
|
|
this.currentVote = null;
|
|
|
|
if (toKill) {
|
|
return this.players[toKill];
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Gamestate handling:
|
|
|
|
start() {
|
|
for (let i in this.players) {
|
|
let index = Math.floor(Math.random(this.roles.length));
|
|
this.players[i].class = MafiaData.MafiaClasses[this.roles[index]];
|
|
this.roles.splice(index, 1);
|
|
if (!this.players[i].class.atStart) {
|
|
this.players[i].getRole();
|
|
}
|
|
}
|
|
|
|
this.gameEvent('initial', 'atStart', 1);
|
|
}
|
|
|
|
end(image, content) {
|
|
this.announcementWindow(image, content);
|
|
|
|
clearTimeout(this.timer);
|
|
this.timer = null;
|
|
this.room.game = null;
|
|
this.destroy();
|
|
}
|
|
|
|
forceEnd() {
|
|
this.room.send("The game of mafia was forcibly ended.");
|
|
|
|
clearTimeout(this.timer);
|
|
this.timer = null;
|
|
this.room.game = null;
|
|
this.destroy();
|
|
}
|
|
|
|
progress() {
|
|
for (let i = 0; i < this.executionOrder.length; i++) {
|
|
let player = this.executionOrder[i];
|
|
if (player.targeting || player.voting) {
|
|
player.eliminate();
|
|
} else if (player.toExecute) {
|
|
if (player.roleBlocked) {
|
|
player.roleBlocked = false;
|
|
player.toExecute = null;
|
|
} else {
|
|
let output;
|
|
if (player.target) {
|
|
output = Tools.escapeHTML(player.toExecute(player.target));
|
|
} else {
|
|
output = Tools.escapeHTML(player.toExecute());
|
|
}
|
|
|
|
if (output) {
|
|
player.playerWindow(player.class.image, output);
|
|
}
|
|
player.toExecute = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
this.executionOrder = null;
|
|
|
|
if (this.meeting) {
|
|
let toKill = this.tallyVotes();
|
|
|
|
if (toKill) {
|
|
toKill.kill(meetingMsg[this.meeting]);
|
|
}
|
|
|
|
this.meeting = null;
|
|
}
|
|
|
|
let mafiaCount = 0;
|
|
let townCount = 0;
|
|
|
|
for (let i in this.players) {
|
|
let player = this.players[i];
|
|
|
|
if (player.invincible) {
|
|
player.invincible = false;
|
|
}
|
|
|
|
if (player.class.side === 'mafia') {
|
|
mafiaCount++;
|
|
} else if (player.class.side === 'town') {
|
|
townCount++;
|
|
}
|
|
}
|
|
|
|
if (mafiaCount > this.playerCount - mafiaCount) {
|
|
this.end(MafiaData.MafiaClasses.mafia.image, 'The mafia is victorious, how awful!');
|
|
return;
|
|
} else if (this.playerCount === 1) {
|
|
for (let i in this.players) {
|
|
if (this.players[i].class.side === 'solo') {
|
|
this.end(this.players[i].class.image, this.players[i].class.victoryText);
|
|
return;
|
|
}
|
|
}
|
|
} else if (!mafiaCount && (townCount === this.playerCount)) {
|
|
this.end(MafiaData.MafiaClasses.villager.image, 'The town has driven the mafia out succesfully!');
|
|
return;
|
|
}
|
|
|
|
if (this.timer) {
|
|
clearTimeout(this.timer);
|
|
this.timer = null;
|
|
}
|
|
|
|
switch (this.gamestate) {
|
|
case 'initial':
|
|
this.gameEvent('night', 'onNight', 2);
|
|
this.mafiaMeeting();
|
|
break;
|
|
case 'night':
|
|
this.gameEvent('day', 'onDay', 0.5);
|
|
break;
|
|
case 'day':
|
|
this.gameEvent('lynch', 'onLynch', 2);
|
|
this.townMeeting();
|
|
break;
|
|
case 'lynch':
|
|
this.day++;
|
|
this.gameEvent('night', 'onNight', 2);
|
|
this.mafiaMeeting();
|
|
}
|
|
}
|
|
|
|
setTimer(mins) {
|
|
this.timer = setTimeout((function () {
|
|
this.announcementWindow('', '10 seconds left!');
|
|
this.timer = setTimeout((function () {
|
|
this.progress();
|
|
}).bind(this), 10000);
|
|
}).bind(this), ((mins - 0.167) * 60000));
|
|
}
|
|
|
|
// Meetings:
|
|
|
|
mafiaMeeting() {
|
|
this.meeting = 'mafia';
|
|
this.currentVote = {};
|
|
let noMafia = {};
|
|
let mafia = [];
|
|
|
|
for (let i in this.players) {
|
|
let player = this.players[i];
|
|
|
|
if (player.class.side !== 'mafia') {
|
|
noMafia[i] = player;
|
|
} else {
|
|
mafia.push(player);
|
|
}
|
|
}
|
|
|
|
for (let i = 0; i < mafia.length; i++) {
|
|
mafia[i].voting = true;
|
|
mafia[i].validVotes = noMafia;
|
|
|
|
let flavorText = '';
|
|
if (mafia.length === 1) {
|
|
flavorText += 'As the only live member of the mafia, you have to be careful. Not careful enough to stop killing, though.';
|
|
} else if (mafia.length === 2) {
|
|
flavorText += 'You sit down with the only other member of the mafia, ' + (i === 0 ? Tools.escapeHTML(mafia[1].name) : Tools.escapeHTML(mafia[0].name)) + '.';
|
|
} else {
|
|
flavorText += 'You sit down with the other members of the mafia, ';
|
|
for (let j = 0; i < mafia.length; i++) {
|
|
if (i !== j) {
|
|
if (j === (mafia.length - 1) || (j < i && j === (mafia.length - 2))) {
|
|
flavorText += ' and ';
|
|
} else {
|
|
flavorText += ', ';
|
|
}
|
|
flavorText += Tools.escapeHTML(mafia[i].name);
|
|
}
|
|
}
|
|
}
|
|
|
|
mafia[i].voteWindow(mafia[i].class.image, flavorText);
|
|
}
|
|
}
|
|
|
|
townMeeting() {
|
|
this.meeting = 'mafia';
|
|
this.currentVote = {};
|
|
|
|
for (let i in this.players) {
|
|
let player = this.players[i];
|
|
|
|
if (this.currentVote[player.userid]) {
|
|
this.currentVote[player.userid]++;
|
|
} else {
|
|
this.currentVote[player.userid] = 1;
|
|
}
|
|
player.voting = true;
|
|
player.validVotes = this.players;
|
|
|
|
player.voteWindow(player.class.image, 'Outraged over the mafia\'s activity in town, the people decide to lynch a person they suspect of being involved with the mafia.');
|
|
}
|
|
}
|
|
|
|
gameEvent(gamestate, event, timer) {
|
|
this.gamestate = gamestate;
|
|
|
|
this.executionOrder = [];
|
|
|
|
for (let i in this.players) {
|
|
let player = this.players[i];
|
|
if (player.class[event]) {
|
|
player.event(event);
|
|
}
|
|
}
|
|
|
|
this.executionOrder.sort(function (a, b) {
|
|
return (b.class[event].priority - a.class[event].priority);
|
|
});
|
|
|
|
this.setTimer(timer);
|
|
}
|
|
}
|
|
|
|
exports.commands = {
|
|
mafia: {
|
|
create: 'new',
|
|
new: function (target, room, user) {
|
|
let params = target.split(',').map(function (param) { return param.toLowerCase().trim(); });
|
|
|
|
if (!this.can(permission, null, room)) return false;
|
|
if (!room.mafiaEnabled) return this.errorReply("Mafia is disabled for this room.");
|
|
if (!this.canTalk()) return this.errorReply("You cannot do this while unable to talk.");
|
|
if (room.game) return this.errorReply("There is already a game in progress in this room.");
|
|
|
|
// TODO: make a generator for a default setup.
|
|
if (!params) return this.errorReply("No roles entered.");
|
|
|
|
for (let i = 0; i < params.length; i++) {
|
|
if (!(params[i] in MafiaData.MafiaClasses)) {
|
|
return this.errorReply(Tools.escapeHTML(params[i]) + " is not a valid mafia class.");
|
|
}
|
|
}
|
|
|
|
room.game = new Mafia(room, params.length, params);
|
|
|
|
return this.privateModCommand("(A game of mafia was started by " + user.name + ".)");
|
|
},
|
|
|
|
join: function (target, room, user) {
|
|
if (!room.game || room.game.gameid !== 'mafia') return this.errorReply("There is no game of mafia running in this room.");
|
|
if (room.game.gamestate !== 'pregame') return this.errorReply("The game has started already.");
|
|
if (!this.canTalk()) return this.errorReply("You cannot do this while unable to talk.");
|
|
|
|
if (room.game.addPlayer(user)) {
|
|
room.game.updatePregame();
|
|
if (room.game.playerCount === room.game.playerCap) {
|
|
room.game.start();
|
|
}
|
|
} else {
|
|
return this.errorReply("You're already in the game.");
|
|
}
|
|
},
|
|
|
|
leave: function (target, room, user) {
|
|
if (!room.game || room.game.gameid !== 'mafia') return this.errorReply("There is no game of mafia running in this room.");
|
|
if (room.game.gamestate !== 'pregame') return this.errorReply("The game has started already.");
|
|
if (!this.canTalk()) return this.errorReply("You cannot do this while unable to talk.");
|
|
|
|
if (room.game.removePlayer(user)) {
|
|
room.game.updatePregame();
|
|
} else {
|
|
return this.errorReply("You're not in the game.");
|
|
}
|
|
},
|
|
|
|
display: function (target, room, user) {
|
|
if (!room.game || room.game.gameid !== 'mafia') return this.errorReply("There is no game of mafia running in this room.");
|
|
if (room.game.gamestate !== 'pregame') return this.errorReply("The game has started already.");
|
|
if (!this.canTalk()) return this.errorReply("You cannot do this while unable to talk.");
|
|
|
|
room.game.displayPregame();
|
|
},
|
|
|
|
target: function (target, room, user) {
|
|
if (!room.game || room.game.gameid !== 'mafia') return this.errorReply("There is no game of mafia running in this room.");
|
|
if (room.game.gamestate === 'pregame') return this.errorReply("The game hasn't started yet.");
|
|
if (!this.canTalk()) return this.errorReply("You cannot do this while unable to talk.");
|
|
|
|
if (user.userid in room.game.players && toId(target) in room.game.players) {
|
|
room.game.players[user.userid].onReceiveTarget(room.game.players[toId(target)]);
|
|
}
|
|
},
|
|
|
|
vote: function (target, room, user) {
|
|
if (!room.game || room.game.gameid !== 'mafia') return this.errorReply("There is no game of mafia running in this room.");
|
|
if (!room.game.gamestate === 'pregame') return this.errorReply("The game hasn't started yet.");
|
|
if (!this.canTalk()) return this.errorReply("You cannot do this while unable to talk.");
|
|
|
|
if (user.userid in room.game.players && toId(target) in room.game.players) {
|
|
room.game.players[user.userid].onReceiveVote(room.game.players[toId(target)]);
|
|
}
|
|
},
|
|
|
|
end: function (target, room, user) {
|
|
if (!this.can(permission, null, room)) return false;
|
|
if (!room.game || room.game.gameid !== 'mafia') return this.errorReply("There is no game of mafia running in this room.");
|
|
if (!this.canTalk()) return this.errorReply("You cannot do this while unable to talk.");
|
|
|
|
room.game.forceEnd();
|
|
return this.privateModCommand("(The game of mafia was forcibly ended by " + user.name + ".)");
|
|
},
|
|
|
|
disable: function (target, room, user) {
|
|
if (!this.can('mafiamanagement', null, room)) return;
|
|
if (!room.mafiaEnabled) {
|
|
return this.errorReply("Mafia is already disabled.");
|
|
}
|
|
delete room.mafiaEnabled;
|
|
if (room.chatRoomData) {
|
|
delete room.chatRoomData.mafiaEnabled;
|
|
Rooms.global.writeChatRoomData();
|
|
}
|
|
return this.sendReply("Mafia has been disabled for this room.");
|
|
},
|
|
|
|
enable: function (target, room, user) {
|
|
if (!this.can('mafiamanagement', null, room)) return;
|
|
if (room.mafiaEnabled) {
|
|
return this.errorReply("Mafia is already enabled.");
|
|
}
|
|
room.mafiaEnabled = true;
|
|
if (room.chatRoomData) {
|
|
room.chatRoomData.mafiaEnabled = true;
|
|
Rooms.global.writeChatRoomData();
|
|
}
|
|
return this.sendReply("Mafia has been enabled for this room.");
|
|
}
|
|
}
|
|
};
|