diff --git a/server/chat-plugins/hangman.js b/server/chat-plugins/hangman.ts similarity index 86% rename from server/chat-plugins/hangman.js rename to server/chat-plugins/hangman.ts index 68df279c96..6189f3c387 100644 --- a/server/chat-plugins/hangman.js +++ b/server/chat-plugins/hangman.ts @@ -8,30 +8,32 @@ const maxMistakes = 6; class Hangman extends Rooms.RoomGame { - /** - * @param {ChatRoom | GameRoom} room - * @param {User} user - * @param {string} word - * @param {string?} [hint] - */ - constructor(room, user, word, hint = '') { + gameNumber: number; + creator: ID; + word: string; + hint: string; + incorrectGuesses: number; + + guesses: string[]; + letterGuesses: string[]; + lastGuesser: string; + wordSoFar: string[]; + + constructor(room: ChatRoom | GameRoom, user: User, word: string, hint = '') { super(room); this.gameNumber = ++room.gameNumber; - this.gameid = /** @type {ID} */ ('hangman'); + this.gameid = 'hangman' as ID; this.title = 'Hangman'; this.creator = user.id; this.word = word; this.hint = hint; this.incorrectGuesses = 0; - /** @type {string[]} */ this.guesses = []; - /** @type {string[]} */ this.letterGuesses = []; this.lastGuesser = ''; - /** @type {string[]} */ this.wordSoFar = []; for (const letter of word) { @@ -43,15 +45,11 @@ class Hangman extends Rooms.RoomGame { } } - /** - * @param {string} word - * @param {User} user - */ - guess(word, user) { + guess(word: string, user: User) { if (user.id === this.creator) return user.sendTo(this.room, "You can't guess in your own hangman game."); - let sanitized = word.replace(/[^A-Za-z ]/g, ''); - let normalized = toID(sanitized); + const sanitized = word.replace(/[^A-Za-z ]/g, ''); + const normalized = toID(sanitized); if (normalized.length < 1) return user.sendTo(this.room, "Guess too short."); if (sanitized.length > 30) return user.sendTo(this.room, "Guess too long."); @@ -72,11 +70,7 @@ class Hangman extends Rooms.RoomGame { } } - /** - * @param {string} letter - * @param {string} guesser - */ - guessLetter(letter, guesser) { + guessLetter(letter: string, guesser: string) { letter = letter.toUpperCase(); if (this.guesses.includes(letter)) return false; if (this.word.toUpperCase().includes(letter)) { @@ -106,15 +100,11 @@ class Hangman extends Rooms.RoomGame { return true; } - /** - * @param {string} word - * @param {string} guesser - */ - guessWord(word, guesser) { - let ourWord = toID(this.word); - let guessedWord = toID(word); + guessWord(word: string, guesser: string) { + const ourWord = toID(this.word); + const guessedWord = toID(word); if (ourWord === guessedWord) { - for (let [i, letter] of this.wordSoFar.entries()) { + for (const [i, letter] of this.wordSoFar.entries()) { if (letter === '_') { this.wordSoFar[i] = this.word[i]; } @@ -155,7 +145,7 @@ class Hangman extends Rooms.RoomGame { let wordString = this.wordSoFar.join(''); if (result === 1) { - let word = this.word; + const word = this.word; wordString = wordString.replace(/_+/g, (match, offset) => `${word.substr(offset, match.length)}` ); @@ -184,11 +174,7 @@ class Hangman extends Rooms.RoomGame { return output; } - /** - * @param {User} user - * @param {boolean} [broadcast] - */ - display(user, broadcast = false) { + display(user: User, broadcast = false) { if (broadcast) { this.room.add(`|uhtml|hangman${this.gameNumber}|${this.generateWindow()}`); } else { @@ -217,23 +203,21 @@ class Hangman extends Rooms.RoomGame { } } -/** @type {ChatCommands} */ -const commands = { +export const commands: ChatCommands = { hangman: { create: 'new', new(target, room, user, connection) { - let text = this.filter(target); + const text = this.filter(target); if (target !== text) return this.errorReply("You are not allowed to use filtered words in hangmans."); - let params = text.split(','); + const params = text.split(','); if (!this.can('minigame', null, room)) return false; - // @ts-ignore if (room.hangmanDisabled) return this.errorReply("Hangman is disabled for this room."); if (!this.canTalk()) return; if (room.game) return this.errorReply(`There is already a game of ${room.game.title} in progress in this room.`); if (!params) return this.errorReply("No word entered."); - let word = params[0].replace(/[^A-Za-z '-]/g, ''); + const word = params[0].replace(/[^A-Za-z '-]/g, ''); if (word.replace(/ /g, '').length < 1) return this.errorReply("Enter a valid word"); if (word.length > 30) return this.errorReply("Phrase must be less than 30 characters."); if (word.split(' ').some(w => w.length > 20)) return this.errorReply("Each word in the phrase must be less than 20 characters."); @@ -259,7 +243,7 @@ const commands = { if (!room.game || room.game.gameid !== 'hangman') return this.errorReply("There is no game of hangman running in this room."); if (!this.canTalk()) return; - const game = /** @type {Hangman} */ (room.game); + const game = room.game as Hangman; game.guess(target, user); }, guesshelp: [ @@ -273,7 +257,7 @@ const commands = { if (!this.canTalk()) return; if (!room.game || room.game.gameid !== 'hangman') return this.errorReply("There is no game of hangman running in this room."); - const game = /** @type {Hangman} */ (room.game); + const game = room.game as Hangman; game.end(); this.modlog('ENDHANGMAN'); return this.privateModAction(`(The game of hangman was ended by ${user.name}.)`); @@ -282,14 +266,11 @@ const commands = { disable(target, room, user) { if (!this.can('gamemanagement', null, room)) return; - // @ts-ignore if (room.hangmanDisabled) { return this.errorReply("Hangman is already disabled."); } - // @ts-ignore room.hangmanDisabled = true; if (room.chatRoomData) { - // @ts-ignore room.chatRoomData.hangmanDisabled = true; Rooms.global.writeChatRoomData(); } @@ -298,14 +279,11 @@ const commands = { enable(target, room, user) { if (!this.can('gamemanagement', null, room)) return; - // @ts-ignore if (!room.hangmanDisabled) { return this.errorReply("Hangman is already enabled."); } - // @ts-ignore delete room.hangmanDisabled; if (room.chatRoomData) { - // @ts-ignore delete room.chatRoomData.hangmanDisabled; Rooms.global.writeChatRoomData(); } @@ -317,7 +295,7 @@ const commands = { if (!this.runBroadcast()) return; room.update(); - const game = /** @type {Hangman} */ (room.game); + const game = room.game as Hangman; game.display(user, this.broadcasting); }, @@ -340,11 +318,9 @@ const commands = { guess(target, room, user) { if (!room.game) return this.errorReply("There is no game running in this room."); if (!this.canTalk()) return; - // @ts-ignore - if (!room.game.guess) return this.errorReply("You can't guess anything in this game."); + if (!(room.game as Hangman).guess) return this.errorReply("You can't guess anything in this game."); - // @ts-ignore - room.game.guess(target, user); + (room.game as Hangman).guess(target, user); }, guesshelp: [ `/guess - Shortcut for /hangman guess.`, @@ -352,8 +328,8 @@ const commands = { `/hangman guess [word] - Same as a letter, but guesses an entire word.`, ], }; -/** @type {SettingsHandler} */ -const roomSettings = room => ({ + +export const roomSettings: SettingsHandler = room => ({ label: "Hangman", permission: 'editroom', options: [ @@ -361,8 +337,3 @@ const roomSettings = room => ({ [`enabled`, !room.hangmanDisabled || 'hangman enable'], ], }); - -module.exports = { - commands, - roomSettings, -}; diff --git a/server/chat-plugins/othermetas.js b/server/chat-plugins/othermetas.ts similarity index 82% rename from server/chat-plugins/othermetas.js rename to server/chat-plugins/othermetas.ts index 135b83a71c..e79cc124fe 100644 --- a/server/chat-plugins/othermetas.js +++ b/server/chat-plugins/othermetas.ts @@ -1,18 +1,21 @@ // Other Metas plugin by Spandan 'use strict'; -/** - * @param {string} stone - * @param {string} mod - * @return {Item | null} - */ -function getMegaStone(stone, mod = 'gen8') { +interface StoneDeltas { + baseStats: {[stat in StatName]: number}; + weighthg: number; + type?: string; +} + +type TierShiftTiers = 'UU' | 'RUBL' | 'RU' | 'NUBL' | 'NU' | 'PUBL' | 'PU' | 'NFE' | 'LC Uber' | 'LC'; + +function getMegaStone(stone: string, mod = 'gen8'): Item | null { let dex = Dex; if (mod && toID(mod) in Dex.dexes) dex = Dex.mod(toID(mod)); - let item = dex.getItem(stone); + const item = dex.getItem(stone); if (!item.exists) { if (toID(stone) === 'dragonascent') { - let move = dex.getMove(stone); + const move = dex.getMove(stone); return { id: move.id, name: move.name, @@ -34,8 +37,7 @@ function getMegaStone(stone, mod = 'gen8') { return item; } -/** @type {ChatCommands} */ -const commands = { +export const commands: ChatCommands = { '!othermetas': true, om: 'othermetas', othermetas(target, room, user) { @@ -51,7 +53,7 @@ const commands = { buffer += `- Other Metagames Forum
`; if (!target) return this.sendReplyBox(buffer); } - let showMonthly = (target === 'all' || target === 'omofthemonth' || target === 'omotm' || target === 'month'); + const showMonthly = (target === 'all' || target === 'omofthemonth' || target === 'omotm' || target === 'month'); if (target === 'all') { // Display OMotM formats, with forum thread links as caption @@ -87,7 +89,7 @@ const commands = { const template = dex.getTemplate(sep[0]); if (!stone || (dex.gen >= 8 && ['redorb', 'blueorb'].includes(stone.id))) return this.errorReply(`Error: Mega Stone not found.`); if (!template.exists) return this.errorReply(`Error: Pokemon not found.`); - let banlist = Dex.getFormat('gen8mixandmega').banlist; + const banlist = Dex.getFormat('gen8mixandmega').banlist; if (banlist.includes(stone.name)) { this.errorReply(`Warning: ${stone.name} is banned from Mix and Mega.`); } @@ -107,14 +109,12 @@ const commands = { megaTemplate = dex.getTemplate("Kyogre-Primal"); baseTemplate = dex.getTemplate("Kyogre"); } - /** @type {{baseStats: {[k: string]: number}, weighthg: number, type?: string}} */ - let deltas = { - baseStats: {}, + const deltas: StoneDeltas = { + baseStats: Object.create(null), weighthg: megaTemplate.weighthg - baseTemplate.weighthg, }; - for (let statId in megaTemplate.baseStats) { - // @ts-ignore - deltas.baseStats[statId] = megaTemplate.baseStats[statId] - baseTemplate.baseStats[statId]; + for (const statId in megaTemplate.baseStats) { + deltas.baseStats[statId as StatName] = megaTemplate.baseStats[statId as StatName] - baseTemplate.baseStats[statId as StatName]; } if (megaTemplate.types.length > baseTemplate.types.length) { deltas.type = megaTemplate.types[1]; @@ -124,8 +124,8 @@ const commands = { deltas.type = megaTemplate.types[1]; } ////////////////////////////////////////// - let mixedTemplate = Dex.deepClone(template); - mixedTemplate.abilities = Object.assign({}, megaTemplate.abilities); + const mixedTemplate = Dex.deepClone(template); + mixedTemplate.abilities = Dex.deepClone(megaTemplate.abilities); if (mixedTemplate.types[0] === deltas.type) { // Add any type gains mixedTemplate.types = [deltas.type]; } else if (deltas.type === 'mono') { @@ -133,8 +133,8 @@ const commands = { } else if (deltas.type) { mixedTemplate.types = [mixedTemplate.types[0], deltas.type]; } - for (let statName in template.baseStats) { // Add the changed stats and weight - mixedTemplate.baseStats[statName] = Dex.clampIntRange(mixedTemplate.baseStats[statName] + deltas.baseStats[statName], 1, 255); + for (const statName in template.baseStats) { // Add the changed stats and weight + mixedTemplate.baseStats[statName] = Dex.clampIntRange(mixedTemplate.baseStats[statName] + deltas.baseStats[statName as StatName], 1, 255); } mixedTemplate.weighthg = Math.max(1, template.weighthg + deltas.weighthg); mixedTemplate.tier = "MnM"; @@ -150,12 +150,11 @@ const commands = { } else if (mixedTemplate.weighthg >= 100) { weighthit = 40; } - /** @type {{[k: string]: string}} */ - let details = { + const details: {[k: string]: string} = { "Dex#": '' + mixedTemplate.num, - "Gen": '' + mixedTemplate.gen, - "Height": mixedTemplate.heightm + " m", - "Weight": mixedTemplate.weighthg / 10 + " kg (" + weighthit + " BP)", + Gen: '' + mixedTemplate.gen, + Height: mixedTemplate.heightm + " m", + Weight: mixedTemplate.weighthg / 10 + " kg (" + weighthit + " BP)", "Dex Colour": mixedTemplate.color, }; if (mixedTemplate.eggGroups) details["Egg Group(s)"] = mixedTemplate.eggGroups.join(", "); @@ -173,18 +172,18 @@ const commands = { megastone: 'stone', stone(target) { if (!this.runBroadcast()) return; - let sep = target.split(','); + const sep = target.split(','); let dex = Dex; if (sep[1] && toID(sep[1]) in Dex.dexes) dex = Dex.mod(toID(sep[1])); - let targetid = toID(sep[0]); + const targetid = toID(sep[0]); if (!targetid) return this.parse('/help stone'); - let stone = getMegaStone(targetid, sep[1]); + const stone = getMegaStone(targetid, sep[1]); if (!stone || (dex.gen >= 8 && ['redorb', 'blueorb'].includes(stone.id))) return this.errorReply(`Error: Mega Stone not found.`); - let banlist = Dex.getFormat('gen8mixandmega').banlist; + const banlist = Dex.getFormat('gen8mixandmega').banlist; if (banlist.includes(stone.name)) { this.errorReply(`Warning: ${stone.name} is banned from Mix and Mega.`); } - let restrictedStones = Dex.getFormat('gen8mixandmega').restrictedStones || []; + const restrictedStones = Dex.getFormat('gen8mixandmega').restrictedStones || []; if (restrictedStones.includes(stone.name)) { if (dex.getTemplate(stone.megaEvolves).isNonstandard === "Past") { this.errorReply(`Warning: ${stone.name} is restricted in Mix and Mega.`); @@ -212,14 +211,12 @@ const commands = { megaTemplate = dex.getTemplate("Kyogre-Primal"); baseTemplate = dex.getTemplate("Kyogre"); } - /** @type {{baseStats: {[k: string]: number}, weighthg: number, type?: string}} */ - let deltas = { - baseStats: {}, + const deltas: StoneDeltas = { + baseStats: Object.create(null), weighthg: megaTemplate.weighthg - baseTemplate.weighthg, }; - for (let statId in megaTemplate.baseStats) { - // @ts-ignore - deltas.baseStats[statId] = megaTemplate.baseStats[statId] - baseTemplate.baseStats[statId]; + for (const statId in megaTemplate.baseStats) { + deltas.baseStats[statId as StatName] = megaTemplate.baseStats[statId as StatName] - baseTemplate.baseStats[statId as StatName]; } if (megaTemplate.types.length > baseTemplate.types.length) { deltas.type = megaTemplate.types[1]; @@ -228,9 +225,9 @@ const commands = { } else if (megaTemplate.types[1] !== baseTemplate.types[1]) { deltas.type = megaTemplate.types[1]; } - let details = { - "Gen": stone.gen, - "Weight": (deltas.weighthg < 0 ? "" : "+") + deltas.weighthg / 10 + " kg", + const details = { + Gen: stone.gen, + Weight: (deltas.weighthg < 0 ? "" : "+") + deltas.weighthg / 10 + " kg", }; let tier; if (['redorb', 'blueorb'].includes(stone.id)) { @@ -281,17 +278,17 @@ const commands = { stonehelp: [`/stone [, generation] - Shows the changes that a mega stone/orb applies to a Pokemon.`], '!350cup': true, - '350': '350cup', + 350: '350cup', '350cup'(target, room, user) { if (!this.runBroadcast()) return; if (!toID(target)) return this.parse('/help 350cup'); - let template = Dex.deepClone(Dex.getTemplate(target)); + const template = Dex.deepClone(Dex.getTemplate(target)); if (!template.exists) return this.errorReply("Error: Pokemon not found."); let bst = 0; - for (let i in template.baseStats) { + for (const i in template.baseStats) { bst += template.baseStats[i]; } - for (let i in template.baseStats) { + for (const i in template.baseStats) { template.baseStats[i] = template.baseStats[i] * (bst <= 350 ? 2 : 1); } this.sendReply(`|html|${Chat.getDataPokemonHTML(template)}`); @@ -303,26 +300,25 @@ const commands = { tiershift(target, room, user) { if (!this.runBroadcast()) return; if (!toID(target)) return this.parse('/help tiershift'); - let template = Dex.deepClone(Dex.mod('gen7').getTemplate(target)); + const template = Dex.deepClone(Dex.mod('gen7').getTemplate(target)); if (!template.exists) return this.errorReply("Error: Pokemon not found."); - /** @type {{[k: string]: number}} */ - let boosts = { - 'UU': 10, - 'RUBL': 10, - 'RU': 20, - 'NUBL': 20, - 'NU': 30, - 'PUBL': 30, - 'PU': 40, - 'NFE': 40, + const boosts: {[tier in TierShiftTiers]: number} = { + UU: 10, + RUBL: 10, + RU: 20, + NUBL: 20, + NU: 30, + PUBL: 30, + PU: 40, + NFE: 40, 'LC Uber': 40, - 'LC': 40, + LC: 40, }; let tier = template.tier; if (tier[0] === '(') tier = tier.slice(1, -1); if (!(tier in boosts)) return this.sendReply(`|html|${Chat.getDataPokemonHTML(template)}`); - let boost = boosts[tier]; - for (let statName in template.baseStats) { + const boost = boosts[tier as TierShiftTiers]; + for (const statName in template.baseStats) { if (statName === 'hp') continue; template.baseStats[statName] = Dex.clampIntRange(template.baseStats[statName] + boost, 1, 255); } @@ -335,11 +331,11 @@ const commands = { scalemons(target, room, user) { if (!this.runBroadcast()) return; if (!toID(target)) return this.parse(`/help scalemons`); - let template = Dex.deepClone(Dex.getTemplate(target)); + const template = Dex.deepClone(Dex.getTemplate(target)); if (!template.exists) return this.errorReply(`Error: Pokemon ${target} not found.`); - let stats = ['atk', 'def', 'spa', 'spd', 'spe']; - let pst = stats.map(stat => template.baseStats[stat]).reduce((x, y) => x + y); - let scale = 600 - template.baseStats['hp']; + const stats = ['atk', 'def', 'spa', 'spd', 'spe']; + const pst = stats.map(stat => template.baseStats[stat]).reduce((x, y) => x + y); + const scale = 600 - template.baseStats['hp']; for (const stat of stats) { template.baseStats[stat] = Dex.clampIntRange(template.baseStats[stat] * scale / pst, 1, 255); } @@ -351,15 +347,15 @@ const commands = { ns: 'natureswap', natureswap(target, room, user) { if (!this.runBroadcast()) return; - let nature = target.trim().split(' ')[0]; - let pokemon = target.trim().split(' ')[1]; + const nature = target.trim().split(' ')[0]; + const pokemon = target.trim().split(' ')[1]; if (!toID(nature) || !toID(pokemon)) return this.parse(`/help natureswap`); - let natureObj = /** @type {{name: string, plus?: string | undefined, minus?: string | undefined, exists?: boolean}} */ Dex.getNature(nature); + const natureObj: {name: string, plus?: string | undefined, minus?: string | undefined, exists?: boolean} = Dex.getNature(nature); if (!natureObj.exists) return this.errorReply(`Error: Nature ${nature} not found.`); - let template = Dex.deepClone(Dex.getTemplate(pokemon)); + const template = Dex.deepClone(Dex.getTemplate(pokemon)); if (!template.exists) return this.errorReply(`Error: Pokemon ${pokemon} not found.`); if (natureObj.minus && natureObj.plus) { - let swap = template.baseStats[natureObj.minus]; + const swap = template.baseStats[natureObj.minus]; template.baseStats[natureObj.minus] = template.baseStats[natureObj.plus]; template.baseStats[natureObj.plus] = swap; template.tier = 'NS'; @@ -406,9 +402,8 @@ const commands = { const mixedTemplate = Dex.deepClone(template); mixedTemplate.abilities = Dex.deepClone(crossTemplate.abilities); mixedTemplate.baseStats = Dex.deepClone(mixedTemplate.baseStats); - for (let statName in template.baseStats) { - // @ts-ignore - mixedTemplate.baseStats[statName] += crossTemplate.baseStats[statName] - prevo.baseStats[statName]; + for (const statName in template.baseStats) { + mixedTemplate.baseStats[statName as StatName] += crossTemplate.baseStats[statName as StatName] - prevo.baseStats[statName as StatName]; } mixedTemplate.types = [template.types[0]]; if (template.types[1]) mixedTemplate.types.push(template.types[1]); @@ -419,8 +414,8 @@ const commands = { if (mixedTemplate.weighthg < 1) { mixedTemplate.weighthg = 1; } - for (const stat of Object.values(mixedTemplate.baseStats)) { - if (stat < 1 || stat > 255) { + for (const stat in mixedTemplate.baseStats) { + if (mixedTemplate.baseStats[stat] < 1 || mixedTemplate.baseStats[stat] > 255) { this.errorReply(`Warning: This Cross Evolution cannot happen since a stat goes below 0 or above 255.`); break; } @@ -438,12 +433,11 @@ const commands = { } else if (mixedTemplate.weighthg >= 100) { weighthit = 40; } - /** @type {{[k: string]: string}} */ - let details = { + const details: {[k: string]: string} = { "Dex#": mixedTemplate.num, - "Gen": mixedTemplate.gen, - "Height": mixedTemplate.heightm + " m", - "Weight": mixedTemplate.weighthg / 10 + " kg (" + weighthit + " BP)", + Gen: mixedTemplate.gen, + Height: mixedTemplate.heightm + " m", + Weight: mixedTemplate.weighthg / 10 + " kg (" + weighthit + " BP)", "Dex Colour": mixedTemplate.color, }; if (mixedTemplate.eggGroups) details["Egg Group(s)"] = mixedTemplate.eggGroups.join(", "); @@ -456,5 +450,3 @@ const commands = { }, crossevolvehelp: ["/crossevo , - Shows the type and stats for the Cross Evolved Pokemon."], }; - -exports.commands = commands; diff --git a/server/chat-plugins/uno.js b/server/chat-plugins/uno.ts similarity index 70% rename from server/chat-plugins/uno.js rename to server/chat-plugins/uno.ts index a04fbf67b9..58d9068fc2 100644 --- a/server/chat-plugins/uno.js +++ b/server/chat-plugins/uno.ts @@ -9,99 +9,101 @@ 'use strict'; +type Color = 'Green' | 'Yellow' | 'Red' | 'Blue' | 'Black'; +interface Card { + value: string; + color: Color; + changedColor?: Color; + name: string; +} + const maxTime = 60; // seconds -const rgbGradients = { - 'Green': "rgba(0, 122, 0, 1), rgba(0, 185, 0, 0.9)", - 'Yellow': "rgba(255, 225, 0, 1), rgba(255, 255, 85, 0.9)", - 'Blue': "rgba(40, 40, 255, 1), rgba(125, 125, 255, 0.9)", - 'Red': "rgba(255, 0, 0, 1), rgba(255, 125, 125, 0.9)", - 'Black': "rgba(0, 0, 0, 1), rgba(0, 0, 0, 0.55)", +const rgbGradients: {[k in Color]: string} = { + Green: "rgba(0, 122, 0, 1), rgba(0, 185, 0, 0.9)", + Yellow: "rgba(255, 225, 0, 1), rgba(255, 255, 85, 0.9)", + Blue: "rgba(40, 40, 255, 1), rgba(125, 125, 255, 0.9)", + Red: "rgba(255, 0, 0, 1), rgba(255, 125, 125, 0.9)", + Black: "rgba(0, 0, 0, 1), rgba(0, 0, 0, 0.55)", }; -const textColors = { - 'Green': "rgb(0, 128, 0)", - 'Yellow': "rgb(175, 165, 40)", - 'Blue': "rgb(75, 75, 255)", - 'Red': "rgb(255, 0, 0)", - 'Black': 'inherit', +const textColors: {[k in Color]: string} = { + Green: "rgb(0, 128, 0)", + Yellow: "rgb(175, 165, 40)", + Blue: "rgb(75, 75, 255)", + Red: "rgb(255, 0, 0)", + Black: 'inherit', }; const textShadow = 'text-shadow: 1px 0px black, -1px 0px black, 0px -1px black, 0px 1px black, 2px -2px black;'; -/** @typedef {'Green' | 'Yellow' | 'Red' | 'Blue' | 'Black'} Color */ -/** @typedef {{value: string, color: Color, changedColor?: Color, name: string}} Card */ - -/** - * @param {Card} card - * @param {boolean} fullsize - * @return {string} - */ -function cardHTML(card, fullsize) { +function cardHTML(card: Card, fullsize: boolean) { let surface = card.value.replace(/[^A-Z0-9+]/g, ""); - let background = rgbGradients[card.color]; + const background = rgbGradients[card.color]; if (surface === 'R') surface = ''; return ``; } -/** - * @return {Card[]} - */ function createDeck() { - /** @type {Color[]} */ const colors = ['Red', 'Blue', 'Green', 'Yellow']; const values = ['1', '2', '3', '4', '5', '6', '7', '8', '9', 'Reverse', 'Skip', '+2']; - let basic = /** @type {Card[]} */ ([]); + const basic: Card[] = []; for (const color of colors) { basic.push(...values.map(v => { - /** @type {Card} */ - let c = {value: v, color: color, name: color + " " + v}; + const c: Card = {value: v, color: color as Color, name: color + " " + v}; return c; })); } return [ // two copies of the basic stuff (total 96) - ...basic, - ...basic, + ...basic as Card[], + ...basic as Card[], // The four 0s ...[0, 1, 2, 3].map(v => { - /** @type {Card} */ - let c = {color: colors[v], value: '0', name: colors[v] + ' 0'}; + const c: Card = {color: colors[v] as Color, value: '0', name: colors[v] + ' 0'}; return c; }), // Wild cards ...[0, 1, 2, 3].map(v => { - /** @type {Card} */ - let c = {color: 'Black', value: 'Wild', name: 'Wild'}; + const c: Card = {color: 'Black', value: 'Wild', name: 'Wild'}; return c; }), // Wild +4 cards ...[0, 1, 2, 3].map(v => { - /** @type {Card} */ - let c = {color: 'Black', value: '+4', name: 'Wild +4'}; + const c: Card = {color: 'Black', value: '+4', name: 'Wild +4'}; return c; }), ]; // 108 cards } class UnoGame extends Rooms.RoomGame { - /** - * @param {ChatRoom | GameRoom} room - * @param {number} cap - * @param {boolean} suppressMessages - */ - constructor(room, cap, suppressMessages) { + playerTable: {[userid: string]: UnoGamePlayer}; + players: UnoGamePlayer[]; + playerCap: number; + allowRenames: boolean; + maxTime: number; + timer: NodeJS.Timer | null; + autostartTimer: NodeJS.Timer | null; + state: string; + currentPlayerid: ID; + deck: Card[]; + discards: Card[]; + topCard: Card | null; + awaitUno: string | null; + unoId: ID | null; + direction: number; + suppressMessages: boolean; + spectators: {[k: string]: number}; + isPlusFour: boolean; + + constructor(room: ChatRoom | GameRoom, cap: number, suppressMessages: boolean) { super(room); - // TypeScript bug: no `T extends RoomGamePlayer` - /** @type {{[userid: string]: UnoGamePlayer}} */ this.playerTable = Object.create(null); - // TypeScript bug: no `T extends RoomGamePlayer` - /** @type {UnoGamePlayer[]} */ this.players = []; if (room.gameNumber) { @@ -110,34 +112,23 @@ class UnoGame extends Rooms.RoomGame { room.gameNumber = 1; } - /** @type {number} */ this.playerCap = cap; this.allowRenames = true; - /** @type {number} */ this.maxTime = maxTime; - /** @type {NodeJS.Timer?} */ this.timer = null; - /** @type {NodeJS.Timer?} */ this.autostartTimer = null; - this.gameid = /** @type {ID} */ ('uno'); + this.gameid = 'uno' as ID; this.title = 'UNO'; - /** @type {string} */ this.state = 'signups'; - - /** @type {string} */ this.currentPlayerid = ''; - /** @type {Card[]} */ this.deck = Dex.shuffle(createDeck()); - /** @type {Card[]} */ this.discards = []; - /** @type {Card?} */ this.topCard = null; - /** @type {string?} */ this.awaitUno = null; - /** @type {string?} */ this.unoId = null; + this.isPlusFour = false; this.direction = 1; @@ -149,21 +140,28 @@ class UnoGame extends Rooms.RoomGame { onUpdateConnection() {} - /** - * @param {User} user - * @param {Connection} connection - */ - onConnect(user, connection) { + onConnect(user: User, connection: Connection) { if (this.state === 'signups') { - connection.sendTo(this.room, `|uhtml|uno-${this.room.gameNumber}|

A new game of UNO is starting!


Or use /uno join to join the game.

${(this.suppressMessages ? `

Game messages will be shown to only players. If you would like to spectate the game, use /uno spectate

` : '')}
`); + connection.sendTo( + this.room, + `|uhtml|uno-${this.room.gameNumber}|
` + + `

A new game of UNO is starting!

` + + `

` + + `
Or use /uno join to join the game.

` + + `${this.suppressMessages ? + `

Game messages will be shown to only players. ` + + `If you would like to spectate the game, use /uno spectate

` : ''}
` + ); } else if (this.onSendHand(user) === false) { - connection.sendTo(this.room, `|uhtml|uno-${this.room.gameNumber}|

A UNO game is currently in progress.

${(this.suppressMessages ? `

Game messages will be shown to only players. If you would like to spectate the game, use /uno spectate

` : '')}
`); + connection.sendTo( + this.room, + `|uhtml|uno-${this.room.gameNumber}|

A UNO game is currently in progress.

` + + `${(this.suppressMessages ? `

Game messages will be shown to only players. ` + + `If you would like to spectate the game, use /uno spectate

` : '')}
` + ); } } - /** - * @return {false | void} - */ onStart() { if (this.playerCount < 2) return false; if (this.autostartTimer) clearTimeout(this.autostartTimer); @@ -173,7 +171,7 @@ class UnoGame extends Rooms.RoomGame { this.onNextPlayer(); // determines the first player // give cards to the players - for (let i in this.playerTable) { + for (const i in this.playerTable) { this.playerTable[i].hand.push(...this.drawCard(7)); } @@ -189,11 +187,7 @@ class UnoGame extends Rooms.RoomGame { this.nextTurn(true); } - /** - * @param {User} user - * @return {boolean} - */ - joinGame(user) { + joinGame(user: User) { if (this.state === 'signups' && this.addPlayer(user)) { this.sendToRoom(`${user.name} has joined the game of UNO.`); return true; @@ -201,11 +195,7 @@ class UnoGame extends Rooms.RoomGame { return false; } - /** - * @param {User} user - * @return {boolean} - */ - leaveGame(user) { + leaveGame(user: User) { if (!(user.id in this.playerTable)) return false; if (this.state === 'signups' && this.removePlayer(user)) { this.sendToRoom(`${user.name} has left the game of UNO.`); @@ -216,21 +206,12 @@ class UnoGame extends Rooms.RoomGame { /** * Overwrite the default makePlayer so it makes an UnoGamePlayer instead. - * @param {User} user - * @return {UnoGamePlayer} */ - makePlayer(user) { + makePlayer(user: User) { return new UnoGamePlayer(user, this); } - /** - * @param {User} user - * @param {string} oldUserid - * @param {boolean} isJoining - * @param {boolean} isForceRenamed - * @return {false | void} - */ - onRename(user, oldUserid, isJoining, isForceRenamed) { + onRename(user: User, oldUserid: ID, isJoining: boolean, isForceRenamed: boolean) { if (!(oldUserid in this.playerTable) || user.id === oldUserid) return false; if (!user.named && !isForceRenamed) { user.games.delete(this.roomid); @@ -238,7 +219,8 @@ class UnoGame extends Rooms.RoomGame { return; // dont set users to their guest accounts. } this.playerTable[user.id] = this.playerTable[oldUserid]; - if (user.id !== oldUserid) delete this.playerTable[oldUserid]; // only run if it's a rename that involves a change of userid + // only run if it's a rename that involves a change of userid + if (user.id !== oldUserid) delete this.playerTable[oldUserid]; // update the user's name information this.playerTable[user.id].name = user.name; @@ -248,14 +230,10 @@ class UnoGame extends Rooms.RoomGame { if (this.currentPlayerid === oldUserid) this.currentPlayerid = user.id; } - /** - * @param {string} userid - * @return {string | false} - */ - eliminate(userid) { + eliminate(userid: ID) { if (!(userid in this.playerTable)) return false; - let name = this.playerTable[userid].name; + const name = this.playerTable[userid].name; if (this.playerCount === 2) { this.removePlayer(this.playerTable[userid]); @@ -286,46 +264,39 @@ class UnoGame extends Rooms.RoomGame { return name; } - /** - * @param {string} msg - * @param {boolean} [overrideSuppress] - */ - sendToRoom(msg, overrideSuppress = false) { + sendToRoom(msg: string, overrideSuppress = false) { if (!this.suppressMessages || overrideSuppress) { this.room.add(msg).update(); } else { // send to the players first - for (let i in this.playerTable) { + for (const i in this.playerTable) { this.playerTable[i].sendRoom(msg); } // send to spectators - for (let i in this.spectators) { + for (const i in this.spectators) { if (i in this.playerTable) continue; // don't double send to users already in the game. - let user = Users.getExact(i); + const user = Users.getExact(i); if (user) user.sendTo(this.roomid, msg); } } } - /** - * @param {boolean} [showCards] - * @return {string[]} - */ - getPlayers(showCards) { + getPlayers(showCards?: boolean): string[] { let playerList = Object.keys(this.playerTable); if (!showCards) { return playerList.sort().map(id => Chat.escapeHTML(this.playerTable[id].name)); } if (this.direction === -1) playerList = playerList.reverse(); - return playerList.map(id => `${(this.currentPlayerid === id ? '' : '')}${Chat.escapeHTML(this.playerTable[id].name)} (${this.playerTable[id].hand.length}) ${(this.currentPlayerid === id ? '' : "")}`); + return playerList.map(id => + `${(this.currentPlayerid === id ? '' : '')}` + + `${Chat.escapeHTML(this.playerTable[id].name)} (${this.playerTable[id].hand.length})` + + `${(this.currentPlayerid === id ? '' : "")}` + ); } - /** - * @return {Promise} - */ onAwaitUno() { - return new Promise((resolve, reject) => { + return new Promise(resolve => { if (!this.awaitUno) return resolve(); this.state = "uno"; @@ -339,16 +310,13 @@ class UnoGame extends Rooms.RoomGame { }); } - /** - * @param {boolean} [starting] - */ - nextTurn(starting) { - this.onAwaitUno() - .then(() => { + nextTurn(starting?: boolean) { + void this.onAwaitUno() + .then(x => { if (!starting) this.onNextPlayer(); if (this.timer) clearTimeout(this.timer); - let player = this.playerTable[this.currentPlayerid]; + const player = this.playerTable[this.currentPlayerid]; this.sendToRoom(`|c:|${(Math.floor(Date.now() / 1000))}|~|${player.name}'s turn.`); this.state = 'play'; @@ -365,32 +333,25 @@ class UnoGame extends Rooms.RoomGame { onNextPlayer() { // if none is set if (!this.currentPlayerid) { - let userList = Object.keys(this.playerTable); - this.currentPlayerid = userList[Math.floor(this.playerCount * Math.random())]; + const userList = Object.keys(this.playerTable); + this.currentPlayerid = userList[Math.floor(this.playerCount * Math.random())] as ID; } this.currentPlayerid = this.getNextPlayer(); } - /** - * @return {string} - */ getNextPlayer() { - let userList = Object.keys(this.playerTable); + const userList = Object.keys(this.playerTable); - let player = userList[(userList.indexOf(this.currentPlayerid) + this.direction)]; + let player: ID = userList[(userList.indexOf(this.currentPlayerid) + this.direction)] as ID; if (!player) { - player = this.direction === 1 ? userList[0] : userList[this.playerCount - 1]; + player = toID(this.direction === 1 ? userList[0] : userList[this.playerCount - 1]); } return player; } - /** - * @param {UnoGamePlayer} player - * @return {boolean | void} - */ - onDraw(player) { + onDraw(player: UnoGamePlayer) { if (this.currentPlayerid !== player.id || this.state !== 'play') return false; if (player.cardLock) return true; @@ -398,20 +359,15 @@ class UnoGame extends Rooms.RoomGame { this.sendToRoom(`${player.name} has drawn a card.`); - let card = this.onDrawCard(player, 1); + const card = this.onDrawCard(player, 1); player.sendDisplay(); player.cardLock = card[0].name; } - /** - * @param {UnoGamePlayer} player - * @param {string} cardName - * @return {false | string | void} - */ - onPlay(player, cardName) { + onPlay(player: UnoGamePlayer, cardName: string) { if (this.currentPlayerid !== player.id || this.state !== 'play') return false; - let card = player.hasCard(cardName); + const card = player.hasCard(cardName); if (!card) return "You do not have that card."; // check for legal play @@ -420,8 +376,18 @@ class UnoGame extends Rooms.RoomGame { throw new Error(`No top card in the discard pile.`); } if (player.cardLock && player.cardLock !== cardName) return `You can only play ${player.cardLock} after drawing.`; - if (card.color !== 'Black' && card.color !== (this.topCard.changedColor || this.topCard.color) && card.value !== this.topCard.value) return `You cannot play this card - you can only play: Wild cards, ${(this.topCard.changedColor ? 'and' : '')} ${(this.topCard.changedColor || this.topCard.color)} cards${this.topCard.changedColor ? "" : ` and ${this.topCard.value}'s`}.`; - if (card.value === '+4' && !player.canPlayWildFour()) return "You cannot play Wild +4 when you still have a card with the same color as the top card."; + if ( + card.color !== 'Black' && + card.color !== (this.topCard.changedColor || this.topCard.color) && + card.value !== this.topCard.value + ) { + return `You cannot play this card - you can only play: Wild cards, ` + + `${(this.topCard.changedColor ? 'and' : '')} ${(this.topCard.changedColor || this.topCard.color)} ` + + `cards${this.topCard.changedColor ? "" : ` and ${this.topCard.value}'s`}.`; + } + if (card.value === '+4' && !player.canPlayWildFour()) { + return "You cannot play Wild +4 when you still have a card with the same color as the top card."; + } if (this.timer) clearTimeout(this.timer); // reset the autodq timer. @@ -435,7 +401,7 @@ class UnoGame extends Rooms.RoomGame { // update the unoId here, so when the display is sent to the player when the play is made if (player.hand.length === 1) { this.awaitUno = player.id; - this.unoId = Math.floor(Math.random() * 100).toString(); + this.unoId = Math.floor(Math.random() * 100).toString() as ID; } player.sendDisplay(); // update display without the card in it for purposes such as choosing colors @@ -453,18 +419,15 @@ class UnoGame extends Rooms.RoomGame { if (this.state === 'play') this.nextTurn(); } - /** - * @param {string} value - * @param {boolean} [initialize] - */ - onRunEffect(value, initialize) { + onRunEffect(value: string, initialize?: boolean) { const colorDisplay = `|uhtml|uno-hand|
`; switch (value) { case 'Reverse': this.direction *= -1; this.sendToRoom("The direction of the game has changed."); - if (!initialize && this.playerCount === 2) this.onNextPlayer(); // in 2 player games, reverse sends the turn back to the player. + // in 2 player games, reverse sends the turn back to the player. + if (!initialize && this.playerCount === 2) this.onNextPlayer(); break; case 'Skip': this.onNextPlayer(); @@ -479,7 +442,7 @@ class UnoGame extends Rooms.RoomGame { this.playerTable[this.currentPlayerid].sendRoom(colorDisplay); this.state = 'color'; // apply to the next in line, since the current player still has to choose the color - let next = this.getNextPlayer(); + const next = this.getNextPlayer(); this.sendToRoom(this.playerTable[next].name + " has been forced to draw 4 cards."); this.onDrawCard(this.playerTable[next], 4); this.isPlusFour = true; @@ -500,13 +463,14 @@ class UnoGame extends Rooms.RoomGame { if (initialize) this.onNextPlayer(); } - /** - * @param {UnoGamePlayer} player - * @param {Color} color - * @return {false | void} - */ - onSelectColor(player, color) { - if (!['Red', 'Blue', 'Green', 'Yellow'].includes(color) || player.id !== this.currentPlayerid || this.state !== 'color') return false; + onSelectColor(player: UnoGamePlayer, color: Color) { + if ( + !['Red', 'Blue', 'Green', 'Yellow'].includes(color) || + player.id !== this.currentPlayerid || + this.state !== 'color' + ) { + return false; + } if (!this.topCard) { // should never happen throw new Error(`No top card in the discard pile.`); @@ -526,33 +490,28 @@ class UnoGame extends Rooms.RoomGame { this.nextTurn(); } - /** - * @param {UnoGamePlayer} player - * @param {number} count - * @return {Card[]} - */ - onDrawCard(player, count) { + onDrawCard(player: UnoGamePlayer, count: number) { if (typeof count === 'string') count = parseInt(count); if (!count || isNaN(count) || count < 1) count = 1; - let drawnCards = /** @type {Card[]} */ (this.drawCard(count)); + const drawnCards = this.drawCard(count); player.hand.push(...drawnCards); - player.sendRoom(`|raw|You have drawn the following card${Chat.plural(drawnCards)}: ${drawnCards.map(card => `${card.name}`).join(', ')}.`); + player.sendRoom( + `|raw|You have drawn the following card${Chat.plural(drawnCards)}: ` + + `${drawnCards.map(card => `${card.name}`).join(', ')}.` + ); return drawnCards; } - /** - * @param {number} count - * @return {Card[]} - */ - drawCard(count) { + drawCard(count: number) { if (typeof count === 'string') count = parseInt(count); if (!count || isNaN(count) || count < 1) count = 1; - let drawnCards = /** @type {Card[]} */ ([]); + const drawnCards: Card[] = []; for (let i = 0; i < count; i++) { if (!this.deck.length) { - this.deck = this.discards.length ? Dex.shuffle(this.discards) : Dex.shuffle(createDeck()); // shuffle the cards back into the deck, or if there are no discards, add another deck into the game. + // shuffle the cards back into the deck, or if there are no discards, add another deck into the game. + this.deck = this.discards.length ? Dex.shuffle(this.discards) : Dex.shuffle(createDeck()); this.discards = []; // clear discard pile } drawnCards.push(this.deck[this.deck.length - 1]); @@ -561,12 +520,7 @@ class UnoGame extends Rooms.RoomGame { return drawnCards; } - /** - * @param {UnoGamePlayer} player - * @param {string} unoId - * @return {false | void} - */ - onUno(player, unoId) { + onUno(player: UnoGamePlayer, unoId: ID) { // uno id makes spamming /uno uno impossible if (this.unoId !== unoId || player.id !== this.awaitUno) return false; this.sendToRoom(Chat.html`|raw|UNO! ${player.name} is down to their last card!`); @@ -586,21 +540,17 @@ class UnoGame extends Rooms.RoomGame { } } - /** - * @param {User} user - * @return {false | void} - */ - onSendHand(user) { + onSendHand(user: User) { if (!(user.id in this.playerTable) || this.state === 'signups') return false; this.playerTable[user.id].sendDisplay(); } - /** - * @param {UnoGamePlayer} player - */ - onWin(player) { - this.sendToRoom(Chat.html`|raw|
Congratulations to ${player.name} for winning the game of UNO!
`, true); + onWin(player: UnoGamePlayer) { + this.sendToRoom( + Chat.html`|raw|
Congratulations to ${player.name} for winning the game of UNO!
`, + true + ); this.destroy(); } @@ -610,7 +560,7 @@ class UnoGame extends Rooms.RoomGame { this.sendToRoom(`|uhtmlchange|uno-${this.room.gameNumber}|
The game of UNO has ended.
`, true); // deallocate games for each player. - for (let i in this.playerTable) { + for (const i in this.playerTable) { this.playerTable[i].destroy(); } delete this.room.game; @@ -618,43 +568,33 @@ class UnoGame extends Rooms.RoomGame { } class UnoGamePlayer extends Rooms.RoomGamePlayer { - /** - * @param {User} user - * @param {UnoGame} game - */ - constructor(user, game) { + hand: Card[]; + game: UnoGame; + cardLock: string | null; + + constructor(user: User, game: UnoGame) { super(user, game); - this.hand = /** @type {Card[]} */ ([]); + this.hand = []; this.game = game; - /** @type {string?} */ this.cardLock = null; } - /** - * @return {boolean} - */ canPlayWildFour() { if (!this.game.topCard) { // should never happen throw new Error(`No top card in the discard pile.`); } - let color = (this.game.topCard.changedColor || this.game.topCard.color); + const color = (this.game.topCard.changedColor || this.game.topCard.color); if (this.hand.some(c => c.color === color)) return false; return true; } - /** - * @param {string} cardName - */ - hasCard(cardName) { + hasCard(cardName: string) { return this.hand.find(card => card.name === cardName); } - /** - * @param {string} cardName - */ - removeCard(cardName) { + removeCard(cardName: string) { for (const [i, card] of this.hand.entries()) { if (card.name === cardName) { this.hand.splice(i, 1); @@ -663,26 +603,23 @@ class UnoGamePlayer extends Rooms.RoomGamePlayer { } } - /** - * @return {string[]} - */ buildHand() { return this.hand.sort((a, b) => a.color.localeCompare(b.color) || a.value.localeCompare(b.value)) .map((c, i) => cardHTML(c, i === this.hand.length - 1)); } sendDisplay() { - let hand = this.buildHand().join(''); - let players = `

Players (${this.game.playerCount}):

${this.game.getPlayers(true).join('
')}`; - let draw = ''; - let pass = ''; - let uno = ``; + const hand = this.buildHand().join(''); + const players = `

Players (${this.game.playerCount}):

${this.game.getPlayers(true).join('
')}`; + const draw = ''; + const pass = ''; + const uno = ``; if (!this.game.topCard) { // should never happen throw new Error(`No top card in the discard pile.`); } - let top = `Top Card: ${this.game.topCard.name}`; + const top = `Top Card: ${this.game.topCard.name}`; // clear previous display and show new display this.sendRoom("|uhtmlchange|uno-hand|"); @@ -694,8 +631,7 @@ class UnoGamePlayer extends Rooms.RoomGamePlayer { } } -/** @type {ChatCommands} */ -const commands = { +export const commands: ChatCommands = { uno: { // roomowner commands off: 'disable', @@ -738,7 +674,7 @@ const commands = { if (room.unoDisabled) return this.errorReply("UNO is currently disabled for this room."); if (room.game) return this.errorReply("There is already a game in progress in this room."); - let suppressMessages = cmd.includes('private') || !(cmd.includes('public') || room.roomid === 'gamecorner'); + const suppressMessages = cmd.includes('private') || !(cmd.includes('public') || room.roomid === 'gamecorner'); let cap = parseInt(target); if (isNaN(cap)) cap = 6; @@ -750,8 +686,10 @@ const commands = { start(target, room, user) { if (!this.can('minigame', null, room)) return; - const game = /** @type {UnoGame} */ (room.game); - if (!game || game.gameid !== 'uno' || game.state !== 'signups') return this.errorReply("There is no UNO game in signups phase in this room."); + const game = room.game as UnoGame; + if (!game || game.gameid !== 'uno' || game.state !== 'signups') { + return this.errorReply("There is no UNO game in signups phase in this room."); + } if (game.onStart()) { this.privateModAction(`(The game of UNO was started by ${user.name}.)`); this.modlog('UNO START'); @@ -770,9 +708,9 @@ const commands = { timer(target, room, user) { if (!this.can('minigame', null, room)) return; - const game = /** @type {UnoGame} */ (room.game); + const game = room.game as UnoGame; if (!game || game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room."); - let amount = parseInt(target); + const amount = parseInt(target); if (!amount || amount < 5 || amount > 300) return this.errorReply("The amount must be a number between 5 and 300."); game.maxTime = amount; @@ -786,7 +724,7 @@ const commands = { autostart(target, room, user) { if (!this.can('minigame', null, room)) return; - const game = /** @type {UnoGame} */ (room.game); + const game = room.game as UnoGame; if (!game || game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); if (toID(target) === 'off') { if (!game.autostartTimer) return this.errorReply("There is no autostart timer running on."); @@ -795,7 +733,9 @@ const commands = { return; } const amount = parseInt(target); - if (!amount || amount < 30 || amount > 600) return this.errorReply("The amount must be a number between 30 and 600 seconds."); + if (!amount || amount < 30 || amount > 600) { + return this.errorReply("The amount must be a number between 30 and 600 seconds."); + } if (game.state !== 'signups') return this.errorReply("The game of UNO has already started."); if (game.autostartTimer) clearTimeout(game.autostartTimer); game.autostartTimer = setTimeout(() => { @@ -807,10 +747,10 @@ const commands = { dq: 'disqualify', disqualify(target, room, user) { if (!this.can('minigame', null, room)) return; - const game = /** @type {UnoGame} */ (room.game); + const game = room.game as UnoGame; if (!game || game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); - let disqualified = game.eliminate(toID(target)); + const disqualified = game.eliminate(toID(target)); if (disqualified === false) return this.errorReply(`Unable to disqualify ${target}.`); this.privateModAction(`(${user.name} has disqualified ${disqualified} from the UNO game.)`); this.modlog('UNO DQ', toID(target)); @@ -822,7 +762,7 @@ const commands = { // TypeScript doesn't like 'join' being defined as a function join: 'unojoin', unojoin(target, room, user) { - const game = /** @type {UnoGame} */ (room.game); + const game = room.game as UnoGame; if (!game || game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); if (!this.canTalk()) return false; if (!game.joinGame(user)) return this.errorReply("Unable to join the game."); @@ -832,38 +772,35 @@ const commands = { l: 'leave', leave(target, room, user) { - const game = /** @type {UnoGame} */ (room.game); + const game = room.game as UnoGame; if (!game || game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); if (!game.leaveGame(user)) return this.errorReply("Unable to leave the game."); return this.sendReply("You have left the game of UNO."); }, play(target, room, user) { - const game = /** @type {UnoGame} */ (room.game); + const game = room.game as UnoGame; if (!game || game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); - /** @type {UnoGamePlayer | undefined} */ - let player = game.playerTable[user.id]; + const player: UnoGamePlayer | undefined = game.playerTable[user.id]; if (!player) return this.errorReply(`You are not in the game of UNO.`); - let error = game.onPlay(player, target); + const error = game.onPlay(player, target); if (error) this.errorReply(error); }, draw(target, room, user) { - const game = /** @type {UnoGame} */ (room.game); + const game = room.game as UnoGame; if (!game || game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); - /** @type {UnoGamePlayer | undefined} */ - let player = game.playerTable[user.id]; + const player: UnoGamePlayer | undefined = game.playerTable[user.id]; if (!player) return this.errorReply(`You are not in the game of UNO.`); - let error = game.onDraw(player); + const error = game.onDraw(player); if (error) return this.errorReply("You have already drawn a card this turn."); }, pass(target, room, user) { - const game = /** @type {UnoGame} */ (room.game); + const game = room.game as UnoGame; if (!game || game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); if (game.currentPlayerid !== user.id) return this.errorReply("It is currently not your turn."); - /** @type {UnoGamePlayer | undefined} */ - let player = game.playerTable[user.id]; + const player: UnoGamePlayer | undefined = game.playerTable[user.id]; if (!player) return this.errorReply(`You are not in the game of UNO.`); if (!player.cardLock) return this.errorReply("You cannot pass until you draw a card."); if (game.state === 'color') return this.errorReply("You cannot pass until you choose a color."); @@ -873,13 +810,11 @@ const commands = { }, color(target, room, user) { - const game = /** @type {UnoGame} */ (room.game); + const game = room.game as UnoGame; if (!game || game.gameid !== 'uno') return false; - /** @type {UnoGamePlayer | undefined} */ - let player = game.playerTable[user.id]; + const player: UnoGamePlayer | undefined = game.playerTable[user.id]; if (!player) return this.errorReply(`You are not in the game of UNO.`); - /** @type {Color} */ - let color; + let color: Color; if (target === 'Red' || target === 'Green' || target === 'Blue' || target === 'Yellow' || target === 'Black') { color = target; } else { @@ -889,18 +824,17 @@ const commands = { }, uno(target, room, user) { - const game = /** @type {UnoGame} */ (room.game); + const game = room.game as UnoGame; if (!game || game.gameid !== 'uno') return false; - /** @type {UnoGamePlayer | undefined} */ - let player = game.playerTable[user.id]; + const player: UnoGamePlayer | undefined = game.playerTable[user.id]; if (!player) return this.errorReply(`You are not in the game of UNO.`); - game.onUno(player, target); + game.onUno(player, toID(target)); }, // information commands '': 'hand', hand(target, room, user) { - const game = /** @type {UnoGame} */ (room.game); + const game = room.game as UnoGame; if (!game || game.gameid !== 'uno') return this.parse("/help uno"); game.onSendHand(user); }, @@ -909,7 +843,7 @@ const commands = { users: 'getusers', getplayers: 'getusers', getusers(target, room, user) { - const game = /** @type {UnoGame} */ (room.game); + const game = room.game as UnoGame; if (!game || game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); if (!this.runBroadcast()) return false; this.sendReplyBox(`Players (${game.playerCount}):
${game.getPlayers().join(', ')}`); @@ -921,15 +855,19 @@ const commands = { // suppression commands suppress(target, room, user) { - const game = /** @type {UnoGame} */ (room.game); + const game = room.game as UnoGame; if (!game || game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); if (!this.can('minigame', null, room)) return; target = toID(target); - let state = target === 'on' ? true : target === 'off' ? false : undefined; + const state = target === 'on' ? true : target === 'off' ? false : undefined; - if (state === undefined) return this.sendReply(`Suppression of UNO game messages is currently ${(game.suppressMessages ? 'on' : 'off')}.`); - if (state === game.suppressMessages) return this.errorReply(`Suppression of UNO game messages is already ${(game.suppressMessages ? 'on' : 'off')}.`); + if (state === undefined) { + return this.sendReply(`Suppression of UNO game messages is currently ${(game.suppressMessages ? 'on' : 'off')}.`); + } + if (state === game.suppressMessages) { + return this.errorReply(`Suppression of UNO game messages is already ${(game.suppressMessages ? 'on' : 'off')}.`); + } game.suppressMessages = state; @@ -938,7 +876,7 @@ const commands = { }, spectate(target, room, user) { - const game = /** @type {UnoGame} */ (room.game); + const game = room.game as UnoGame; if (!game || game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); if (!game.suppressMessages) return this.errorReply("The current UNO game is not suppressing messages."); @@ -949,7 +887,7 @@ const commands = { }, unspectate(target, room, user) { - const game = /** @type {UnoGame} */ (room.game); + const game = room.game as UnoGame; if (!game || game.gameid !== 'uno') return this.errorReply("There is no UNO game going on in this room right now."); if (!game.suppressMessages) return this.errorReply("The current UNO game is not suppressing messages."); @@ -973,8 +911,8 @@ const commands = { `/uno suppress [on|off] - Toggles suppression of game messages.`, ], }; -/** @type {SettingsHandler} */ -const roomSettings = room => ({ + +export const roomSettings: SettingsHandler = room => ({ label: "UNO", permission: 'editroom', options: [ @@ -982,8 +920,3 @@ const roomSettings = room => ({ [`enabled`, !room.unoDisabled || 'uno enable'], ], }); - -module.exports = { - commands, - roomSettings, -}; diff --git a/tsconfig.json b/tsconfig.json index 86542808f4..3f38c88c09 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -27,14 +27,11 @@ "./server/chat-plugins/*.ts", "./server/chat-plugins/chat-monitor.js", "./server/chat-plugins/daily-spotlight.js", - "./server/chat-plugins/hangman.js", "./server/chat-plugins/helptickets.js", "./server/chat-plugins/mafia-data.js", - "./server/chat-plugins/othermetas.js", "./server/chat-plugins/room-faqs.js", "./server/chat-plugins/thecafe.js", "./server/chat-plugins/thing-of-the-day.js", - "./server/chat-plugins/uno.js", "./server/chat-plugins/wifi.js", "./sim/**/*", "./tools/set-import/*.ts"