diff --git a/chat-plugins/mafia-data.js b/chat-plugins/mafia-data.js new file mode 100755 index 0000000000..2055ab3718 --- /dev/null +++ b/chat-plugins/mafia-data.js @@ -0,0 +1,99 @@ +// Data for the mafia chat plugin. + +'use strict'; + +// This object contains all functions that execute in the callback function of any mafia class. Executed from the context of the executing player. +// Target is a MafiaPlayer object. +let MafiaFunctions = { + copReport: function (target) { + if (target.class.side === 'village') { + return 'After investigating ' + target.name + ' you find out they\'re sided with the village.'; + } else if (target.class.side === 'village') { + return 'After investigating ' + target.name + ' you find out they\'re sided with the mafia.'; + } else { + return 'After investigating ' + target.name + ' you find out they\'re not sided with the village or mafia.'; + } + }, + roleBlock: function (target) { + target.roleBlocked = true; + return 'You visit ' + target.name + ' during the night.'; + }, + protect: function (target) { + target.invincible = true; + return 'You give ' + target.name + ' their daily dose of medicine to keep them safe and sound.'; + }, + killTarget: function (target) { + target.kill('The werewolf has eaten a tasty snack!'); + } +}; + +// Every role has a side they belong to, as well as all functions they have. These functions are objects with the targeting mechanics and a callback. +// events are atStart, onNight, onDay, onLynch. +exports.MafiaClasses = { + villager: { + name: "Villager", + side: 'town', + image: '', + flavorText: 'You are a villager. You live peacefully in the town, which with the mafia activity hasn\'t been all too peaceful, actually.' + }, + + mafia: { + name: "Mafia", + side: 'mafia', + image: '', + flavorText: 'You are a member of the mafia. Every night, you get together with the other mafia members to eliminate someone in the town. The townsfolk aren\'t all that happy with that, however.' + }, + + hooker: { + name: "Hooker", + side: 'town', + image: '', + flavorText: 'You are the hooker. Every night, you can visit someone in the town. The person you visit can\'t execute any actions that night.', + + onNight: { + target: {side: 'any', count: 'single'}, + priority: 5, + callback: MafiaFunctions.roleBlock + } + }, + + doctor: { + name: "Doctor", + side: 'town', + image: '', + flavorText: 'You are the doctor. Every night, you can visit someone in the town. This person can\'t die that night.', + + onNight: { + target: {side: 'any', count: 'single'}, + priority: 4, + callback: MafiaFunctions.protected + } + }, + + cop: { + name: "Cop", + side: 'town', + image: '', + flavorText: 'You are the cop. Every night, you can visit someone in town. When the night is over, you\'ll receive a report with that person\'s alignment.', + + onNight: { + target: {side: 'any', count: 'single'}, + priority: -1, + callback: MafiaFunctions.copReport + } + }, + + werewolf: { + name: "Werewolf", + side: 'solo', + image: '', + flavorText: 'You are the werewolf. You\'re not aligned with either town or mafia, and instead kill someone every night. You win if you\'re the only remaining player.', + victoryText: 'The wolf howls victorious, knowing he came out of this mess alive, and with some lunch as well.', + + onNight: { + target: {side: 'any', count: 'single'}, + priority: 2, + callback: MafiaFunctions.killTarget + } + } +}; diff --git a/chat-plugins/mafia.js b/chat-plugins/mafia.js new file mode 100755 index 0000000000..7918c380fa --- /dev/null +++ b/chat-plugins/mafia.js @@ -0,0 +1,609 @@ +// 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 = ''; +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 + '
' + 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 += '

Who do you wish to target?

'; + for (let i in this.validTargets) { + output += ''; + } + + 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 += '

Who do you wish to vote for?

'; + for (let i in this.validVotes) { + output += ''; + } + + 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 = '

A game of mafia has been made!

Participants:

'; + 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 += '
'; + } else { + output += '
'; + } + + 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 = '
'; + output += '

Day ' + this.day + '

'; + output += '
' + image + ''; + output += content; + output += '
'; + 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.
'; + } + 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."); + } + } +};