diff --git a/.vscode/launch.json b/.vscode/launch.json index f13f8e982..326c536d8 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,6 +4,13 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ + { + "type": "chrome", + "request": "launch", + "name": "frontend", + "url": "http://localhost:3001", + "webRoot": "${workspaceFolder}/react_main" + }, { "type": "node", "request": "launch", diff --git a/Games/core/ArrayHash.js b/Games/core/ArrayHash.js index bbdff4e49..fb9a26c52 100644 --- a/Games/core/ArrayHash.js +++ b/Games/core/ArrayHash.js @@ -45,6 +45,15 @@ module.exports = class ArrayHash { return this.array()[index]; } + indexOf(item) { + let arr = this.array() + for (let i in arr) + if (arr[i] == item) + return i + + return -1 + } + get length() { return Object.values(this).length; } diff --git a/Games/types/Ghost/Action.js b/Games/types/Ghost/Action.js new file mode 100644 index 000000000..c1a34c229 --- /dev/null +++ b/Games/types/Ghost/Action.js @@ -0,0 +1,9 @@ +const Action = require("../../core/Action"); + +module.exports = class GhostAction extends Action { + + constructor(options) { + super(options); + } + +} \ No newline at end of file diff --git a/Games/types/Ghost/Card.js b/Games/types/Ghost/Card.js new file mode 100644 index 000000000..3b32d14d3 --- /dev/null +++ b/Games/types/Ghost/Card.js @@ -0,0 +1,9 @@ +const Card = require("../../core/Card"); + +module.exports = class GhostCard extends Card { + + constructor(role) { + super(role); + } + +} \ No newline at end of file diff --git a/Games/types/Ghost/Game.js b/Games/types/Ghost/Game.js new file mode 100644 index 000000000..893efabfb --- /dev/null +++ b/Games/types/Ghost/Game.js @@ -0,0 +1,218 @@ +const Game = require("../../core/Game"); +const Player = require("./Player"); +const Action = require("./Action"); +const Queue = require("../../core/Queue"); +const Winners = require("../../core/Winners"); + +const Random = require("../../../lib/Random"); +const wordList = require("./data/words"); + +module.exports = class GhostGame extends Game { + + constructor(options) { + super(options); + + this.type = "Ghost"; + this.Player = Player; + this.states = [ + { + name: "Postgame" + }, + { + name: "Pregame" + }, + { + name: "Night", + length: options.settings.stateLengths["Night"], + skipChecks: [ + () => this.playerGivingClue + ] + }, + { + name: "Give Clue", + length: options.settings.stateLengths["Give Clue"], + }, + { + name: "Day", + length: options.settings.stateLengths["Day"], + skipChecks: [ + () => this.playerGivingClue + ] + }, + { + name: "Guess Word", + length: options.settings.stateLengths["Guess Word"], + skipChecks: [ + () => this.playerGivingClue + ] + } + ]; + + // game settings + this.configureWords = options.settings.configureWords; + this.wordLength = options.settings.wordLength; + this.townWord = options.settings.townWord; + this.foolWord = options.settings.townWord; + + // giving clue + this.playerGivingClue = false; + this.currentPlayerList = []; + this.startIndex = -1; + this.currentIndex = -1; + + this.responseHistory = []; + this.currentClueHistory = []; + } + + start() { + if (!this.configureWords) { + let wordPack = Random.randArrayVal(wordList); + let shuffledWordPack = Random.randomizeArray(wordPack); + this.townWord = shuffledWordPack[0]; + this.foolWord = shuffledWordPack[1]; + this.wordLength = this.townWord.length; + } + + super.start(); + } + + startRoundRobin(firstPick) { + if (this.currentClueHistory.length > 0) { + this.responseHistory.push({ + "type": "clue", + "data": this.currentClueHistory, + }) + this.currentClueHistory = []; + } + + this.currentPlayerList = this.alivePlayers(); + this.startIndex = this.currentPlayerList.indexOf(firstPick); + this.currentIndex = this.startIndex; + + firstPick.holdItem("Microphone"); + this.playerGivingClue = true; + } + + incrementCurrentIndex() { + this.currentIndex = (this.currentIndex + 1) % this.currentPlayerList.length; + } + + incrementState() { + let previousState = this.getStateName(); + + if (previousState == "Give Clue") { + this.incrementCurrentIndex(); + while (true) { + if (this.currentIndex == this.startIndex) { + this.playerGivingClue = false; + break; + } + + let nextPlayer = this.currentPlayerList[this.currentIndex]; + if (nextPlayer.alive) { + nextPlayer.holdItem("Microphone"); + break + } + this.incrementCurrentIndex(); + } + } + + super.incrementState(); + } + + recordClue(player, clue) { + this.currentClueHistory.push({ + "name": player.name, + "clue": clue + }) + } + + recordGuess(player, guess) { + let data = { + "name": player.name, + "guess": guess, + }; + + this.responseHistory.push({ + "type": "guess", + "data": data + }) + } + + // send player-specific state + broadcastState() { + for (let p of this.players) { + p.sendStateInfo(); + } + } + + getStateInfo(state) { + var info = super.getStateInfo(state); + info.extraInfo = { + "responseHistory": this.responseHistory, + "currentClueHistory": this.currentClueHistory + } + return info; + } + + // process player leaving immediately + async playerLeave(player) { + if (this.started) { + let action = new Action({ + actor: player, + target: player, + game: this, + run: function () { + this.target.kill("leave", this.actor, true); + } + }); + + this.instantAction(action); + } + + await super.playerLeave(player); + } + + checkWinConditions() { + var finished = false; + var counts = {}; + var winQueue = new Queue(); + var winners = new Winners(this); + var aliveCount = this.alivePlayers().length; + + for (let player of this.players) { + let alignment = player.role.alignment; + + if (!counts[alignment]) + counts[alignment] = 0; + + if (player.alive) + counts[alignment]++; + + winQueue.enqueue(player.role.winCheck); + } + + for (let winCheck of winQueue) { + winCheck.check(counts, winners, aliveCount); + } + + if (winners.groupAmt() > 0) + finished = true; + else if (aliveCount == 0) { + winners.addGroup("No one"); + finished = true; + } + + winners.determinePlayers(); + return [finished, winners]; + } + + getGameTypeOptions() { + return { + disableRehost: true, + }; + } + + + +} \ No newline at end of file diff --git a/Games/types/Ghost/Item.js b/Games/types/Ghost/Item.js new file mode 100644 index 000000000..0526fc119 --- /dev/null +++ b/Games/types/Ghost/Item.js @@ -0,0 +1,9 @@ +const Item = require("../../core/Item"); + +module.exports = class GhostItem extends Item { + + constructor(role) { + super(role); + } + +} \ No newline at end of file diff --git a/Games/types/Ghost/Meeting.js b/Games/types/Ghost/Meeting.js new file mode 100644 index 000000000..c62d9eff6 --- /dev/null +++ b/Games/types/Ghost/Meeting.js @@ -0,0 +1,9 @@ +const Meeting = require("../../core/Meeting"); + +module.exports = class GhostMeeting extends Meeting { + + constructor(game, name) { + super(game, name); + } + +}; \ No newline at end of file diff --git a/Games/types/Ghost/Player.js b/Games/types/Ghost/Player.js new file mode 100644 index 000000000..4fe8f12fd --- /dev/null +++ b/Games/types/Ghost/Player.js @@ -0,0 +1,17 @@ +const Player = require("../../core/Player"); + +module.exports = class GhostPlayer extends Player { + + constructor(user, game, isBot) { + super(user, game, isBot); + } + + // add player-specific state info + sendStateInfo() { + let info = this.game.getStateInfo(); + info.extraInfo.word = this.role?.word; + info.extraInfo.wordLength = this.game.wordLength; + this.send("state", info); + } + +} \ No newline at end of file diff --git a/Games/types/Ghost/Role.js b/Games/types/Ghost/Role.js new file mode 100644 index 000000000..0720b40d8 --- /dev/null +++ b/Games/types/Ghost/Role.js @@ -0,0 +1,8 @@ +const Role = require("../../core/Role"); + +module.exports = class GhostRole extends Role { + + constructor(name, player, data) { + super(name, player, data); + } +} \ No newline at end of file diff --git a/Games/types/Ghost/Winners.js b/Games/types/Ghost/Winners.js new file mode 100644 index 000000000..c210bd5a1 --- /dev/null +++ b/Games/types/Ghost/Winners.js @@ -0,0 +1,8 @@ +const Winners = require("../../core/Winners"); + +module.exports = class GhostWinners extends Winners { + + constructor(game) { + super(game); + } +} \ No newline at end of file diff --git a/Games/types/Ghost/data/words.js b/Games/types/Ghost/data/words.js new file mode 100644 index 000000000..9a3cf3a64 --- /dev/null +++ b/Games/types/Ghost/data/words.js @@ -0,0 +1,99 @@ +module.exports = [ + // adjectives + ["wet", "dry", "sad", "cry", "big", "fat"], + ["warm", "cold", "good", "evil", "near", "away", "love", "hate"], + ["tall", "flat"], + // emotions + ["smile", "happy", "anger"] + // colours + ["pink", "blue", "teal"], + ["green", "white", "black", "brown", "olive"], + ["yellow", "purple", "orange"], + // countries + ["china", "japan", "spain", "italy", "india"], + // actions + ["jump", "spin", "kick", "open", "fold"], + // food + ["egg", "ham", "jam", "pea", "pie", "yam"], + ["kiwi", "lime", "pear", "plum"], + ["apple", "dates", "grape", "lemon", "melon", "mango", "peach", "olive"], + ["oven", "bake", "cake", "fork", "bowl", "milk", "soup"], + ["pasta", "pizza", "sauce", "steak", "fries", "crust", "donut", "candy", "bread", "toast"], + ["meat", "pork"], + ["onion", "round"], + ["peanut", "butter"], + // music + ["harp", "beat", "gong", "drum", "sing", "song", "band", "tune"], + // animals + ["bee", "cat", "dog", "fox", "hen", "bat", "cow", "owl", "ant", "eel"], + ["wolf", "lion", "duck", "deer", "bear", "goat", "crab", "mole", "boar", "orca", "toad", "dove", "frog", "slug", "swan"], + ["monkey", "donkey", "beaver", "jaguar", "iguana", "baboon", "alpaca", "weasel", "rabbit", "spider", "beetle", "toucan", "falcon", "parrot", "shrimp", "urchin", "turtle", "walrus", "pigeon"], + // automobile + ["car", "van"], + ["road", "kill"], + // marine + ["war", "sea"], + ["boat", "fish", "sail", "ship", "port"], + ["ocean", "beach"], + // school + ["grade", "paper", "graph", "class", "tutor", "major"], + ["one", "two", "six", "ten"], + ["four", "five", "nine"], + ["math", "nerd"], + ["pencil", "eraser", "number", "letter"], + // sports + ["karate", "boxing", "kungfu", "taichi"], + ["bowling", "fencing", "surfing", "archery", "cricket", "cycling", "skating", "parkour", "frisbee", "sailing", "jogging"] + ["football", "swimming", "handball", "baseball", "climbing"], + // clothes + ["sock", "shoe"], + // garden + ["pond", "weed", "duck", "frog", "lily", "tree", "bush"], + // health and body + ["salt", "hurt", "heal"], + ["mask", "sick"], + ["virus", "covid", "cough"], + // minecraft/ runescape + ["iron", "gold", "rock", "farm"], + // computer + ["copy", "edit", "type", "code", "java", "site"], + // cartoons + ["pooh", "bear"], + ["winnie", "tigger", "eeyore", "piglet"], + // beyondmafia + ["town", "fool", "king", "jinx", "tree", "bomb", "chef", "cult"], + ["ghost", "mafia", "curse", "nurse", "clown", "mason", "thing", "alien"], + ["forger", "doctor", "granny", "oracle", "priest", "sniper", "yakuza", "lawyer", "tailor", "deputy", "turkey"], + ["janitor", "caroler", "mafioso", "sheriff", "tracker", "watcher", "actress", "slasher", "courier"], + ["suit", "bomb", "dawn", "lone", "loud"], + ["probe", "bread", "knife", "armor"], + ["humble", "astral", "famine"], + // others + ["rick", "roll"], + ["plan", "fail"], +] + +/* +uncategorised +smile,anger +cloak,smoke +hover,float +happy,peace +knife,spoon +angel,demon +power,plant +bread,crust +paper,plane +train,track +inner,outer +major,minor +place,space +awake,sleep +fairy,magic +sport,arena +world,globe +rocket,cannon +promise,destroy +parallel,sequence +magical,rainbow +chocolate,blueberry*/ diff --git a/Games/types/Ghost/items/Microphone.js b/Games/types/Ghost/items/Microphone.js new file mode 100644 index 000000000..7cd8fcc2c --- /dev/null +++ b/Games/types/Ghost/items/Microphone.js @@ -0,0 +1,36 @@ +const Item = require("../Item"); + +module.exports = class Microphone extends Item { + + constructor() { + super("Microphone"); + + this.meetings = { + "Give Clue": { + actionName: "Give Clue (1-50)", + states: ["Give Clue"], + flags: ["voting"], + inputType: "text", + textOptions: { + minLength: 1, + maxLength: 50, + submit: "Confirm" + }, + action: { + item: this, + run: function() { + this.game.recordClue(this.actor, this.target); + this.game.sendAlert(`${this.actor.name}: ${this.target}`); + this.item.drop(); + } + } + } + } + } + + hold(player) { + super.hold(player); + player.game.queueAlert(`${player.name} is giving a clue...`); + } + +}; \ No newline at end of file diff --git a/Games/types/Ghost/roles/Ghost/Ghost.js b/Games/types/Ghost/roles/Ghost/Ghost.js new file mode 100644 index 000000000..427c6118c --- /dev/null +++ b/Games/types/Ghost/roles/Ghost/Ghost.js @@ -0,0 +1,12 @@ +const Role = require("../../Role"); + +module.exports = class Ghost extends Role { + + constructor(player, data) { + super("Ghost", player, data); + + this.alignment = "Ghost"; + this.cards = ["TownCore", "WinWithGhost", "MeetingGhost", "GuessWordOnLynch"]; + } + +} \ No newline at end of file diff --git a/Games/types/Ghost/roles/Host/Host.js b/Games/types/Ghost/roles/Host/Host.js new file mode 100644 index 000000000..5c85d3bf2 --- /dev/null +++ b/Games/types/Ghost/roles/Host/Host.js @@ -0,0 +1,17 @@ +const Role = require("../../Role"); + +module.exports = class Host extends Role { + + constructor(player, data) { + super("Host", player, data); + + this.alignment = "Host"; + this.cards = ["TownCore"]; + this.meetingMods = { + "Village": { + canVote: false, + } + }; + } + +} \ No newline at end of file diff --git a/Games/types/Ghost/roles/Town/Fool.js b/Games/types/Ghost/roles/Town/Fool.js new file mode 100644 index 000000000..754ff8442 --- /dev/null +++ b/Games/types/Ghost/roles/Town/Fool.js @@ -0,0 +1,27 @@ +const Role = require("../../Role"); + +module.exports = class Fool extends Role { + + constructor(player, data) { + super("Fool", player, data); + + this.alignment = "Town"; + this.cards = ["TownCore", "WinWithTown", "AnnounceAndCheckWord"]; + this.appearance = { + self: "Town" + }; + + this.listeners = { + "roleAssigned": [ + function (player) { + if (player != this.player) { + return; + } + + this.word = this.game.foolWord; + } + ] + }; + } + +} \ No newline at end of file diff --git a/Games/types/Ghost/roles/Town/Town.js b/Games/types/Ghost/roles/Town/Town.js new file mode 100644 index 000000000..ba4679c35 --- /dev/null +++ b/Games/types/Ghost/roles/Town/Town.js @@ -0,0 +1,24 @@ +const Role = require("../../Role"); + +module.exports = class Town extends Role { + + constructor(player, data) { + super("Town", player, data); + + this.alignment = "Town"; + this.cards = ["TownCore", "WinWithTown", "AnnounceAndCheckWord"]; + + this.listeners = { + "roleAssigned": [ + function (player) { + if (player != this.player) { + return; + } + + this.word = this.game.townWord; + } + ] + }; + } + +} \ No newline at end of file diff --git a/Games/types/Ghost/roles/cards/AnnounceAndCheckWord.js b/Games/types/Ghost/roles/cards/AnnounceAndCheckWord.js new file mode 100644 index 000000000..61d1bb494 --- /dev/null +++ b/Games/types/Ghost/roles/cards/AnnounceAndCheckWord.js @@ -0,0 +1,21 @@ +const Card = require("../../Card"); + +module.exports = class AnnounceAndCheckWord extends Card { + + constructor(role) { + super(role); + + this.listeners = { + "start": function () { + this.player.queueAlert(`The secret word is: ${this.word}`); + } + }; + } + + speak(message) { + if (message.content.replace(' ', '').toLowerCase().includes(this.word)) { + this.player.sendAlert('Be careful not to say the secret word!'); + } + }; + +} diff --git a/Games/types/Ghost/roles/cards/GuessWordOnLynch.js b/Games/types/Ghost/roles/cards/GuessWordOnLynch.js new file mode 100644 index 000000000..b7124b3ac --- /dev/null +++ b/Games/types/Ghost/roles/cards/GuessWordOnLynch.js @@ -0,0 +1,47 @@ +const Card = require("../../Card"); + +module.exports = class GuessWordOnLynch extends Card { + + constructor(role) { + super(role); + + this.immunity["lynch"] = 1; + + this.listeners = { + "immune": function (action) { + if (action.target == this.player) { + this.dead = true; + } + } + }; + + this.meetings = { + "Guess Word": { + states: ["Guess Word"], + flags: ["instant", "voting"], + inputType: "text", + textOptions: { + minLength: 2, + maxLength: 20, + alphaOnly: true, + toLowerCase: true, + submit: "Confirm" + }, + action: { + run: function() { + let word = this.target.toLowerCase(); + this.game.recordGuess(this.actor, word); + + this.actor.role.guessedWord = word; + if (word !== this.game.townWord) { + this.actor.kill(); + } + } + }, + shouldMeet: function() { + return this.dead; + } + } + } + } +}; \ No newline at end of file diff --git a/Games/types/Ghost/roles/cards/MeetingGhost.js b/Games/types/Ghost/roles/cards/MeetingGhost.js new file mode 100644 index 000000000..9736b7a09 --- /dev/null +++ b/Games/types/Ghost/roles/cards/MeetingGhost.js @@ -0,0 +1,35 @@ +const Card = require("../../Card"); + +module.exports = class MeetingGhost extends Card { + + constructor(role) { + super(role); + + this.listeners = { + "start": function () { + for (let player of this.game.players) { + if (player.role.alignment == "Ghost" && player != this.player) { + this.revealToPlayer(player); + } + } + + this.player.queueAlert(`Guess the hidden word of length: ${this.game.wordLength}`); + } + } + + this.meetings = { + "Ghost": { + actionName: "Select Leader", + states: ["Night"], + flags: ["group", "speech", "voting", "mustAct"], + targets: { include: ["alive"] }, + action: { + run: function () { + this.game.startRoundRobin(this.target); + } + } + } + }; + } + +} diff --git a/Games/types/Ghost/roles/cards/TownCore.js b/Games/types/Ghost/roles/cards/TownCore.js new file mode 100644 index 000000000..27cfe8e3e --- /dev/null +++ b/Games/types/Ghost/roles/cards/TownCore.js @@ -0,0 +1,26 @@ +const Card = require("../../Card"); + +module.exports = class TownCore extends Card { + + constructor(role) { + super(role); + + this.meetings = { + "Village": { + states: ["Day"], + flags: ["group", "speech", "voting"], + targets: { include: ["alive"] }, + whileDead: true, + passiveDead: true, + action: { + labels: ["lynch"], + run: function () { + if (this.dominates()) + this.target.kill() + } + } + } + }; + } + +} diff --git a/Games/types/Ghost/roles/cards/WinWithGhost.js b/Games/types/Ghost/roles/cards/WinWithGhost.js new file mode 100644 index 000000000..b6f750700 --- /dev/null +++ b/Games/types/Ghost/roles/cards/WinWithGhost.js @@ -0,0 +1,19 @@ +const Card = require("../../Card"); + +module.exports = class WinWithGhost extends Card { + + constructor(role) { + super(role); + + this.winCheck = { + priority: 0, + check: function (counts, winners, aliveCount) { + if (aliveCount > 0 && (counts["Ghost"] >= aliveCount / 2) + || (this.guessedWord === this.game.townWord)) { + winners.addPlayer(this.player, "Ghost"); + } + } + }; + } + +} \ No newline at end of file diff --git a/Games/types/Ghost/roles/cards/WinWithTown.js b/Games/types/Ghost/roles/cards/WinWithTown.js new file mode 100644 index 000000000..9dbf48f80 --- /dev/null +++ b/Games/types/Ghost/roles/cards/WinWithTown.js @@ -0,0 +1,17 @@ +const Card = require("../../Card"); + +module.exports = class WinWithTown extends Card { + + constructor(role) { + super(role); + + this.winCheck = { + priority: 0, + check: function (counts, winners, aliveCount) { + if (aliveCount > 0 && counts["Town"] == aliveCount) + winners.addPlayer(this.player, "Town"); + } + }; + } + +} \ No newline at end of file diff --git a/Games/types/Ghost/templates/death.js b/Games/types/Ghost/templates/death.js new file mode 100644 index 000000000..82ae373cf --- /dev/null +++ b/Games/types/Ghost/templates/death.js @@ -0,0 +1,7 @@ +module.exports = function (type, name) { + const templates = { + "lynch": `${name} was executed by the town.`, + }; + + return templates[type]; +}; diff --git a/data/constants.js b/data/constants.js index 30707fb8a..e489ace9c 100644 --- a/data/constants.js +++ b/data/constants.js @@ -1,18 +1,20 @@ module.exports = { restart: null, - gameTypes: ["Mafia", "Split Decision", "Resistance", "One Night"], + gameTypes: ["Mafia", "Split Decision", "Resistance", "One Night", "Ghost"], lobbies: ["Main", "Sandbox", "Competitive", "Games"], alignments: { "Mafia": ["Village", "Mafia", "Monsters", "Independent"], "Split Decision": ["Blue", "Red", "Independent"], "Resistance": ["Resistance", "Spies"], "One Night": ["Village", "Werewolves", "Independent"], + "Ghost": ["Town", "Ghost"], }, startStates: { "Mafia": ["Night", "Day"], "Split Decision": ["Round"], "Resistance": ["Team Selection"], "One Night": ["Night"], + "Ghost": ["Night"], }, configurableStates: { "Mafia": { @@ -67,6 +69,28 @@ module.exports = { max: 10 * 60 * 1000, default: 2 * 60 * 1000 } + }, + "Ghost": { + "Night": { + min: 1 * 60 * 1000, + max: 1 * 60 * 1000, + default: 1 * 60 * 1000 + }, + "Give Clue": { + min: 1 * 60 * 1000, + max: 3 * 60 * 1000, + default: 2 * 60 * 1000 + }, + "Day": { + min: 1 * 60 * 1000, + max: 30 * 60 * 1000, + default: 10 * 60 * 1000 + }, + "Guess Word": { + min: 1 * 60 * 1000, + max: 3 * 60 * 1000, + default: 2 * 60 * 1000 + }, }, }, noQuotes: {}, @@ -94,6 +118,7 @@ module.exports = { "Split Decision": {}, "Resistance": {}, "One Night": {}, + "Ghost": {}, }, maxPlayers: 50, diff --git a/data/roles.js b/data/roles.js index fc1a18dc9..f2a586c4e 100644 --- a/data/roles.js +++ b/data/roles.js @@ -1292,6 +1292,29 @@ const roleData = { ] }, }, + "Ghost": { + "Town": { + alignment: "Town", + description: [ + "Knows the hidden word." + ] + }, + "Fool": { + alignment: "Town", + description: [ + "Knows the decoy word, which has the same number of letters as the hidden word.", + "Appears to self as Town, and does not know that their word is the decoy word.", + ] + }, + "Ghost": { + alignment: "Ghost", + description: [ + "Knows other Ghosts.", + "Only knows the number of letters in the hidden word.", + "Must blend in and guess the hidden word.", + ] + }, + } }; module.exports = roleData; diff --git a/react_main/src/Constants.jsx b/react_main/src/Constants.jsx index 93342eca9..5d72b11ad 100644 --- a/react_main/src/Constants.jsx +++ b/react_main/src/Constants.jsx @@ -1,4 +1,4 @@ -export const GameTypes = ["Mafia", "Split Decision", "Resistance", "One Night"]; +export const GameTypes = ["Mafia", "Split Decision", "Resistance", "One Night", "Ghost"]; export const Lobbies = ["Main", "Sandbox", "Competitive", "Games"]; export const Alignments = { @@ -6,6 +6,7 @@ export const Alignments = { "Split Decision": ["Blue", "Red", "Independent"], "Resistance": ["Resistance", "Spies"], "One Night": ["Village", "Werewolves", "Independent"], + "Ghost": ["Town", "Ghost"], }; export const GameStates = { @@ -13,6 +14,7 @@ export const GameStates = { "Split Decision": ["Initial Round", "Hostage Swap"], "Resistance": ["Team Selection", "Team Approval", "Mission"], "One Night": ["Day", "Night"], + "Ghost": ["Night", "Give Clue", "Day", "Guess Word"], }; export const RatingThresholds = { diff --git a/react_main/src/css/gameGhost.css b/react_main/src/css/gameGhost.css new file mode 100644 index 000000000..488d592cf --- /dev/null +++ b/react_main/src/css/gameGhost.css @@ -0,0 +1,35 @@ + +/* ghost */ + +.game .ghost { + display: flex; + flex-flow: column; + align-items: center; + + margin-bottom: 10px; + padding: 0px 5px; + + color: #bd4c4c; +} + +.game .ghost-name { + margin-top: 10px; + padding-top: 0px; + + text-align: center; + font-weight: bold; +} + +.game .ghost-history-group { + margin-bottom: 15px; +} + +.game .ghost-input span { + margin-right: 4px; + color: #bd4c4c; +} + +.game .ghost-input { + color: black; + size: 8px; +} \ No newline at end of file diff --git a/react_main/src/pages/Game/Game.jsx b/react_main/src/pages/Game/Game.jsx index c5151ab58..c819cd7e8 100644 --- a/react_main/src/pages/Game/Game.jsx +++ b/react_main/src/pages/Game/Game.jsx @@ -11,6 +11,7 @@ import MafiaGame from "./MafiaGame"; import SplitDecisionGame from "./SplitDecisionGame"; import ResistanceGame from "./ResistanceGame"; import OneNightGame from "./OneNightGame"; +import GhostGame from "./GhostGame"; import { GameContext, PopoverContext, SiteInfoContext, UserContext } from "../../Contexts"; import Dropdown, { useDropdown } from "../../components/Dropdown"; import Setup from "../../components/Setup"; @@ -671,6 +672,9 @@ function GameWrapper(props) { {gameType == "One Night" && } + {gameType == "Ghost" && + + } ); @@ -1898,6 +1902,8 @@ function ActionText(props) { const meeting = props.meeting; const self = props.self; + const disabled = meeting.finished; + // text settings const textOptions = meeting.textOptions || {} const minLength = textOptions.minLength || 0; @@ -1938,14 +1944,14 @@ function ActionText(props) {
{meeting.actionName}
-