diff --git a/src/backend/games/builtin-game-loader.js b/src/backend/games/builtin-game-loader.js index cda4c7fef..809d56f11 100644 --- a/src/backend/games/builtin-game-loader.js +++ b/src/backend/games/builtin-game-loader.js @@ -4,12 +4,12 @@ const gameManager = require("./game-manager"); exports.loadGames = () => { [ - 'bid/bid', - 'heist/heist', - 'slots/slots', - 'trivia/trivia' - ].forEach(filename => { - const definition = require(`./builtin/${filename}.js`); + 'bid', + 'heist', + 'slots', + 'trivia' + ].forEach((gameName) => { + const definition = require(`./builtin/${gameName}/${gameName}`).default; gameManager.registerGame(definition); }); -}; \ No newline at end of file +}; diff --git a/src/backend/games/builtin/bid/bid-command.js b/src/backend/games/builtin/bid/bid-command.ts similarity index 82% rename from src/backend/games/builtin/bid/bid-command.js rename to src/backend/games/builtin/bid/bid-command.ts index 8b96bd048..3887a231b 100644 --- a/src/backend/games/builtin/bid/bid-command.js +++ b/src/backend/games/builtin/bid/bid-command.ts @@ -1,35 +1,47 @@ -"use strict"; - -const util = require("../../../utility"); -const twitchChat = require("../../../chat/twitch-chat"); -const commandManager = require("../../../chat/commands/command-manager"); -const gameManager = require("../../game-manager"); -const currencyAccess = require("../../../currency/currency-access").default; -const currencyManager = require("../../../currency/currency-manager"); -const moment = require("moment"); -const NodeCache = require("node-cache"); - -let activeBiddingInfo = { - "active": false, - "currentBid": 0, - "topBidder": "", - "topBidderDisplayName": "" +import util from "../../../utility"; +import twitchChat from "../../../chat/twitch-chat"; +import commandManager from "../../../chat/commands/command-manager"; +import gameManager from "../../game-manager"; +import currencyAccess from "../../../currency/currency-access"; +import currencyManager from "../../../currency/currency-manager"; +import { SystemCommand } from "../../../../types/commands"; +import { GameSettings } from "../../../../types/game-manager"; +import { BidSettings } from "./bid-settings"; +import moment from "moment"; +import NodeCache from "node-cache"; + +type GameData = { + active: boolean; + currentBid: number; + topBidder: string; + topBidderDisplayName: string; }; -let bidTimer; + +let activeBiddingInfo: GameData = { + active: false, + currentBid: 0, + topBidder: "", + topBidderDisplayName: "" +}; +let bidTimer: NodeJS.Timeout|null; const cooldownCache = new NodeCache({checkperiod: 5}); const BID_COMMAND_ID = "firebot:bid"; function purgeCaches() { cooldownCache.flushAll(); activeBiddingInfo = { - "active": false, - "currentBid": 0, - "topBidder": "" + active: false, + currentBid: 0, + topBidder: "", + topBidderDisplayName: "" }; } -async function stopBidding(chatter) { - clearTimeout(bidTimer); +async function stopBidding(chatter: string) { + if (bidTimer) { + clearTimeout(bidTimer); + bidTimer = null; + } if (activeBiddingInfo.topBidder) { await twitchChat.sendChatMessage(`${activeBiddingInfo.topBidderDisplayName} has won the bidding with ${activeBiddingInfo.currentBid}!`, null, chatter); } else { @@ -39,7 +51,7 @@ async function stopBidding(chatter) { purgeCaches(); } -const bidCommand = { +const bidCommand: SystemCommand = { definition: { id: BID_COMMAND_ID, name: "Bid", @@ -103,7 +115,7 @@ const bidCommand = { onTriggerEvent: async (event) => { const { chatMessage, userCommand } = event; - const bidSettings = gameManager.getGameSettings("firebot-bid"); + const bidSettings = gameManager.getGameSettings("firebot-bid") as GameSettings; const chatter = bidSettings.settings.chatSettings.chatter; const currencyId = bidSettings.settings.currencySettings.currencyId; @@ -130,9 +142,10 @@ const bidCommand = { } activeBiddingInfo = { - "active": true, - "currentBid": bidAmount, - "topBidder": "" + active: true, + currentBid: bidAmount, + topBidder: "", + topBidderDisplayName: "" }; const raiseMinimum = bidSettings.settings.currencySettings.minIncrement; @@ -176,11 +189,9 @@ const bidCommand = { } const minBid = bidSettings.settings.currencySettings.minBid; - if (minBid != null && minBid > 0) { - if (bidAmount < minBid) { - await twitchChat.sendChatMessage(`Bid amount must be at least ${minBid} ${currencyName}.`, null, chatter, chatMessage.id); - return; - } + if (minBid != null && minBid > 0 && bidAmount < minBid) { + await twitchChat.sendChatMessage(`Bid amount must be at least ${minBid} ${currencyName}.`, null, chatter, chatMessage.id); + return; } const userBalance = await currencyManager.getViewerCurrencyAmount(username, currencyId); @@ -228,15 +239,19 @@ function registerBidCommand() { } function unregisterBidCommand() { - commandManager.unregisterSystemCommand(BID_COMMAND_ID); + if (commandManager.hasSystemCommand(BID_COMMAND_ID)) { + commandManager.unregisterSystemCommand(BID_COMMAND_ID); + } } -function setNewHighBidder(username, userDisplayName, amount) { +function setNewHighBidder(username: string, userDisplayName: string, amount: number) { activeBiddingInfo.currentBid = amount; activeBiddingInfo.topBidder = username; activeBiddingInfo.topBidderDisplayName = userDisplayName; } -exports.purgeCaches = purgeCaches; -exports.registerBidCommand = registerBidCommand; -exports.unregisterBidCommand = unregisterBidCommand; \ No newline at end of file +export default { + purgeCaches, + registerBidCommand, + unregisterBidCommand +}; diff --git a/src/backend/games/builtin/bid/bid-settings.ts b/src/backend/games/builtin/bid/bid-settings.ts new file mode 100644 index 000000000..9802b8bb1 --- /dev/null +++ b/src/backend/games/builtin/bid/bid-settings.ts @@ -0,0 +1,16 @@ +export type BidSettings = { + currencySettings: { + currencyId: string; + minBid?: number; + minIncrement?: number; + }; + timeSettings: { + timeLimit?: number; + }; + cooldownSettings: { + cooldown?: number; + }; + chatSettings: { + chatter: string; + }; +}; diff --git a/src/backend/games/builtin/bid/bid.js b/src/backend/games/builtin/bid/bid.ts similarity index 94% rename from src/backend/games/builtin/bid/bid.js rename to src/backend/games/builtin/bid/bid.ts index 7972623ac..dfc6fb9d8 100644 --- a/src/backend/games/builtin/bid/bid.js +++ b/src/backend/games/builtin/bid/bid.ts @@ -1,11 +1,8 @@ -"use strict"; +import { FirebotGame } from "../../../../types/game-manager"; +import bidCommand from "./bid-command"; +import { BidSettings } from "./bid-settings"; -const bidCommand = require("./bid-command"); - -/** - * @type {import('../../game-manager').FirebotGame} - */ -module.exports = { +const bidGame: FirebotGame = { id: "firebot-bid", name: "Bid", subtitle: "Put something up for auction", @@ -107,4 +104,6 @@ module.exports = { onSettingsUpdate: () => { bidCommand.purgeCaches(); } -}; \ No newline at end of file +}; + +export default bidGame; diff --git a/src/backend/games/builtin/heist/heist-command.js b/src/backend/games/builtin/heist/heist-command.ts similarity index 60% rename from src/backend/games/builtin/heist/heist-command.js rename to src/backend/games/builtin/heist/heist-command.ts index 5726ee5bf..9853db25a 100644 --- a/src/backend/games/builtin/heist/heist-command.js +++ b/src/backend/games/builtin/heist/heist-command.ts @@ -1,24 +1,22 @@ -"use strict"; - - -const util = require("../../../utility"); -const twitchChat = require("../../../chat/twitch-chat"); -const twitchApi = require("../../../twitch-api/api"); -const commandManager = require("../../../chat/commands/command-manager"); -const gameManager = require("../../game-manager"); -const currencyAccess = require("../../../currency/currency-access").default; -const currencyManager = require("../../../currency/currency-manager"); -const customRolesManager = require("../../../roles/custom-roles-manager"); -const teamRolesManager = require("../../../roles/team-roles-manager"); -const twitchRolesManager = require("../../../../shared/twitch-roles"); -const moment = require("moment"); - -const heistRunner = require("./heist-runner"); -const logger = require("../../../logwrapper"); +import util from "../../../utility"; +import twitchChat from "../../../chat/twitch-chat"; +import twitchApi from "../../../twitch-api/api"; +import commandManager from "../../../chat/commands/command-manager"; +import gameManager from "../../game-manager"; +import currencyAccess from "../../../currency/currency-access"; +import currencyManager from "../../../currency/currency-manager"; +import customRolesManager from "../../../roles/custom-roles-manager"; +import teamRolesManager from "../../../roles/team-roles-manager"; +import twitchRolesManager from "../../../../shared/twitch-roles"; +import { SystemCommand } from "../../../../types/commands"; +import { GameSettings } from "../../../../types/game-manager"; +import heistRunner from "./heist-runner"; +import { HeistSettings } from "./heist-settings"; +import moment from "moment"; const HEIST_COMMAND_ID = "firebot:heist"; -const heistCommand = { +const heistCommand: SystemCommand = { definition: { id: HEIST_COMMAND_ID, name: "Heist", @@ -41,26 +39,25 @@ const heistCommand = { ] }, onTriggerEvent: async (event) => { - - const { chatEvent, userCommand } = event; + const { chatMessage, userCommand } = event; const username = userCommand.commandSender; - const user = await twitchApi.users.getUserByName(username); - if (user == null) { - logger.warn(`Could not process heist command for ${username}. User does not exist.`); - return; - } - - const heistSettings = gameManager.getGameSettings("firebot-heist"); + const user = { + displayName: chatMessage.userDisplayName ?? userCommand.commandSender, + id: chatMessage.userId + }; + const heistSettings = gameManager.getGameSettings("firebot-heist") as GameSettings; + const { currencySettings } = heistSettings.settings; const chatter = heistSettings.settings.chatSettings.chatter; - const currencyId = heistSettings.settings.currencySettings.currencyId; + const currencyId = currencySettings.currencyId; const currency = currencyAccess.getCurrencyById(currencyId); // make sure the currency still exists if (currency == null) { await twitchChat.sendChatMessage("Unable to start a Heist game as the selected currency appears to not exist anymore.", null, chatter); - await twitchApi.chat.deleteChatMessage(chatEvent.id); + await twitchApi.chat.deleteChatMessage(chatMessage.id); + return; } // see if the heist is on cooldown before doing anything else @@ -89,10 +86,10 @@ const heistCommand = { } // parse the wager amount - let wagerAmount; + let wagerAmount: number = undefined; if (event.userCommand.args.length < 1) { - const defaultWager = heistSettings.settings.currencySettings.defaultWager; - if ((defaultWager == null || defaultWager < 1)) { + const { defaultWager } = currencySettings; + if (defaultWager == null || defaultWager < 1) { if (heistSettings.settings.entryMessages.noWagerAmount) { const noWagerAmountMsg = heistSettings.settings.entryMessages.noWagerAmount .replace("{user}", user.displayName); @@ -106,7 +103,9 @@ const heistCommand = { } else if (event.userCommand.subcommandId === "wagerAmount") { const triggeredArg = userCommand.args[0]; wagerAmount = parseInt(triggeredArg); - } else { + } + + if (wagerAmount == null || Number.isNaN(wagerAmount) || !Number.isFinite(wagerAmount) || wagerAmount < 0) { if (heistSettings.settings.entryMessages.invalidWagerAmount) { const invalidWagerAmountMsg = heistSettings.settings.entryMessages.invalidWagerAmount .replace("{user}", user.displayName); @@ -117,36 +116,37 @@ const heistCommand = { return; } - wagerAmount = Math.floor(wagerAmount || 0); + wagerAmount = Math.floor(wagerAmount); - // make sure wager doesnt violate min or max values - const minWager = heistSettings.settings.currencySettings.minWager || 1; - if (minWager != null && minWager > 0) { - if (wagerAmount < minWager) { - if (heistSettings.settings.entryMessages.wagerAmountTooLow) { - const wagerAmountTooLowMsg = heistSettings.settings.entryMessages.wagerAmountTooLow - .replace("{user}", user.displayName) - .replace("{minWager}", minWager); + // make sure wager doesn't violate min or max values + const minWager = currencySettings.minWager && !Number.isNaN(currencySettings.minWager) && currencySettings.minWager > 0 + ? currencySettings.minWager + : 1; + if (wagerAmount < minWager) { + if (heistSettings.settings.entryMessages.wagerAmountTooLow) { + const wagerAmountTooLowMsg = heistSettings.settings.entryMessages.wagerAmountTooLow + .replace("{user}", user.displayName) + .replace("{minWager}", `${minWager}`); - await twitchChat.sendChatMessage(wagerAmountTooLowMsg, null, chatter); - } - - return; + await twitchChat.sendChatMessage(wagerAmountTooLowMsg, null, chatter); } - } - const maxWager = heistSettings.settings.currencySettings.maxWager; - if (maxWager != null && maxWager > 0) { - if (wagerAmount > maxWager) { - if (heistSettings.settings.entryMessages.wagerAmountTooHigh) { - const wagerAmountTooHighMsg = heistSettings.settings.entryMessages.wagerAmountTooHigh - .replace("{user}", user.displayName) - .replace("{maxWager}", maxWager); - - await twitchChat.sendChatMessage(wagerAmountTooHighMsg, null, chatter); - } - return; + return; + } + const maxWager = currencySettings.maxWager && !Number.isNaN(currencySettings.maxWager) && currencySettings.maxWager > minWager + ? currencySettings.maxWager + : Number.MAX_SAFE_INTEGER; + const maxWagerText = maxWager !== Number.MAX_SAFE_INTEGER ? util.commafy(maxWager) : "unlimited"; + if (wagerAmount > maxWager) { + if (heistSettings.settings.entryMessages.wagerAmountTooHigh) { + const wagerAmountTooHighMsg = heistSettings.settings.entryMessages.wagerAmountTooHigh + .replace("{user}", user.displayName) + .replace("{maxWager}", maxWagerText); + + await twitchChat.sendChatMessage(wagerAmountTooHighMsg, null, chatter); } + + return; } // check users balance @@ -163,7 +163,7 @@ const heistCommand = { } // deduct wager from user balance - await currencyManager.adjustCurrencyForViewerById(user.id, currencyId, 0 - Math.abs(wagerAmount)); + await currencyManager.adjustCurrencyForViewerById(user.id, currencyId, -wagerAmount); // get all user roles const userCustomRoles = customRolesManager.getAllCustomRolesForViewer(user.id) || []; @@ -179,11 +179,11 @@ const heistCommand = { // get the users success percentage let successChance = 50; - const successChancesSettings = heistSettings.settings.successChanceSettings.successChances; - if (successChancesSettings) { - successChance = successChancesSettings.basePercent; + const { successChances } = heistSettings.settings.successChanceSettings; + if (successChances) { + successChance = successChances.basePercent; - for (const role of successChancesSettings.roles) { + for (const role of successChances.roles) { if (allRoles.some(r => r.id === role.roleId)) { successChance = role.percent; break; @@ -207,18 +207,17 @@ const heistCommand = { // Ensure the game has been started and the lobby ready if (!heistRunner.lobbyOpen) { - const startDelay = heistSettings.settings.generalSettings.startDelay || 1; heistRunner.triggerLobbyStart(startDelay); - const teamCreationMessage = heistSettings.settings.generalMessages.teamCreation - .replace("{user}", user.displayName) - .replace("{command}", userCommand.trigger) - .replace("{maxWager}", maxWager) - .replace("{minWager}", minWager) - .replace("{requiredUsers}", heistSettings.settings.generalSettings.minimumUsers); + if (heistSettings.settings.generalMessages.teamCreation) { + const teamCreationMessage = heistSettings.settings.generalMessages.teamCreation + .replace("{user}", user.displayName) + .replace("{command}", userCommand.trigger) + .replace("{maxWager}", maxWagerText) + .replace("{minWager}", util.commafy(minWager)) + .replace("{requiredUsers}", `${heistSettings.settings.generalSettings.minimumUsers ?? "1"}`); - if (teamCreationMessage) { await twitchChat.sendChatMessage(teamCreationMessage, null, chatter); } } @@ -232,12 +231,12 @@ const heistCommand = { winnings: Math.floor(wagerAmount * winningsMultiplier) }); - const onJoinMessage = heistSettings.settings.entryMessages.onJoin - .replace("{user}", user.displayName) - .replace("{wager}", util.commafy(wagerAmount)) - .replace("{currency}", currency.name); + if (heistSettings.settings.entryMessages.onJoin) { + const onJoinMessage = heistSettings.settings.entryMessages.onJoin + .replace("{user}", user.displayName) + .replace("{wager}", util.commafy(wagerAmount)) + .replace("{currency}", currency.name); - if (onJoinMessage) { await twitchChat.sendChatMessage(onJoinMessage, null, chatter); } } @@ -250,13 +249,17 @@ function registerHeistCommand() { } function unregisterHeistCommand() { - commandManager.unregisterSystemCommand(HEIST_COMMAND_ID); + if (commandManager.hasSystemCommand(HEIST_COMMAND_ID)) { + commandManager.unregisterSystemCommand(HEIST_COMMAND_ID); + } } function clearCooldown() { heistRunner.clearCooldowns(); } -exports.clearCooldown = clearCooldown; -exports.registerHeistCommand = registerHeistCommand; -exports.unregisterHeistCommand = unregisterHeistCommand; \ No newline at end of file +export default { + clearCooldown, + registerHeistCommand, + unregisterHeistCommand +}; diff --git a/src/backend/games/builtin/heist/heist-runner.js b/src/backend/games/builtin/heist/heist-runner.ts similarity index 71% rename from src/backend/games/builtin/heist/heist-runner.js rename to src/backend/games/builtin/heist/heist-runner.ts index 7e3ab06c5..92ac4b2e0 100644 --- a/src/backend/games/builtin/heist/heist-runner.js +++ b/src/backend/games/builtin/heist/heist-runner.ts @@ -1,41 +1,42 @@ -"use strict"; -const moment = require("moment"); -const gameManager = require("../../game-manager"); -const twitchChat = require("../../../chat/twitch-chat"); -const commandManager = require("../../../chat/commands/command-manager"); -const currencyManager = require("../../../currency/currency-manager"); -const util = require("../../../utility"); - -/** - * @typedef HeistUser - * @property {string} username - The user's name - * @property {string} userDisplayName - The user's display name - * @property {number} wager - The amount the user wagered - * @property {number} successPercentage - The users win percentage - * @property {number} winnings - The winnings the user will receive should they win - * - */ - -/**@type {HeistUser[]} */ -let usersInHeist = []; - -exports.cooldownExpireTime = null; -let cooldownTimeoutId = null; - -let startDelayTimeoutId = null; -exports.lobbyOpen = false; +import moment, { Moment } from "moment"; +import gameManager from "../../game-manager"; +import twitchChat from "../../../chat/twitch-chat"; +import commandManager from "../../../chat/commands/command-manager"; +import currencyManager from "../../../currency/currency-manager"; +import util from "../../../utility"; +import { GameSettings } from "../../../../types/game-manager"; +import { HeistSettings } from "./heist-settings"; + +type HeistUser = { + /** The user's name. */ + username: string; + /** The user's display name. */ + userDisplayName: string; + /** The amount the user wagered. */ + wager: number; + /** The users win percentage. */ + successPercentage: number; + /** The winnings the user will receive should they win. */ + winnings: number; +}; + +let usersInHeist: HeistUser[] = []; +let cooldownExpireTime: Moment | null = null; +let cooldownTimeoutId: NodeJS.Timeout | null = null; +let startDelayTimeoutId: NodeJS.Timeout | null = null; +let lobbyOpen = false; function triggerCooldown() { - const heistSettings = gameManager.getGameSettings("firebot-heist"); + const heistSettings = gameManager.getGameSettings("firebot-heist") as GameSettings; const chatter = heistSettings.settings.chatSettings.chatter; const cooldownMins = heistSettings.settings.generalSettings.cooldown || 1; const expireTime = moment().add(cooldownMins, 'minutes'); - exports.cooldownExpireTime = expireTime; + cooldownExpireTime = expireTime; const trigger = commandManager.getSystemCommandTrigger("firebot:heist"); const cooldownOverMessage = heistSettings.settings.generalMessages.cooldownOver - .replace("{command}", trigger ? trigger : '!heist'); + .replace("{command}", trigger ?? '!heist'); if (cooldownOverMessage) { cooldownTimeoutId = setTimeout(async (msg) => { @@ -45,7 +46,7 @@ function triggerCooldown() { } async function runHeist() { - const heistSettings = gameManager.getGameSettings("firebot-heist"); + const heistSettings = gameManager.getGameSettings("firebot-heist") as GameSettings; const chatter = heistSettings.settings.chatSettings.chatter; const startMessage = heistSettings.settings.generalMessages.startMessage; @@ -57,18 +58,18 @@ async function runHeist() { // wait a few secs for suspense await util.wait(7 * 1000); - const survivers = []; + const survivors: HeistUser[] = []; for (const user of usersInHeist) { const successful = util.getRandomInt(1, 100) <= user.successPercentage; if (successful) { - survivers.push(user); + survivors.push(user); } } - const percentSurvived = (survivers.length / usersInHeist.length) * 100; + const percentSurvived = (survivors.length / usersInHeist.length) * 100; - let messages; + let messages: string[] = []; if (percentSurvived >= 100) { if (usersInHeist.length > 1) { messages = heistSettings.settings.groupOutcomeMessages.hundredPercent; @@ -105,13 +106,13 @@ async function runHeist() { } const currencyId = heistSettings.settings.currencySettings.currencyId; - for (const user of survivers) { + for (const user of survivors) { await currencyManager.adjustCurrencyForViewer(user.username, currencyId, user.winnings); } let winningsString; if (percentSurvived > 0) { - winningsString = survivers + winningsString = survivors .map(s => `${s.userDisplayName} (${util.commafy(s.winnings)})`) .join(", "); } else { @@ -129,7 +130,7 @@ async function runHeist() { if (winningsMessage) { await twitchChat.sendChatMessage(winningsMessage, null, chatter); } - } catch (error) { + } catch { //weird error } @@ -137,22 +138,21 @@ async function runHeist() { usersInHeist = []; } - -exports.triggerLobbyStart = (startDelayMins) => { - if (exports.lobbyOpen) { +function triggerLobbyStart(startDelayMins: number) { + if (lobbyOpen) { return; } - exports.lobbyOpen = true; + lobbyOpen = true; if (startDelayTimeoutId != null) { clearTimeout(startDelayTimeoutId); } startDelayTimeoutId = setTimeout(async () => { - exports.lobbyOpen = false; + lobbyOpen = false; startDelayTimeoutId = null; - const heistSettings = gameManager.getGameSettings("firebot-heist"); + const heistSettings = gameManager.getGameSettings("firebot-heist") as GameSettings; const minTeamSize = heistSettings.settings.generalSettings.minimumUsers; if (usersInHeist.length < minTeamSize - 1) { // user is added to usersInHeist after triggerLobbyStart is called in heist-command @@ -177,29 +177,21 @@ exports.triggerLobbyStart = (startDelayMins) => { triggerCooldown(); - runHeist(); + await runHeist(); }, startDelayMins * 60000); -}; +} -/** - * - * @param {HeistUser} user - */ -exports.addUser = (user) => { - if (user == null) { - return; - } - if (usersInHeist.some(u => u.username === user.username)) { - return; +function addUser(user: HeistUser) { + if (user && !usersInHeist.some(u => u.username === user.username)) { + usersInHeist.push(user); } - usersInHeist.push(user); -}; +} -exports.userOnTeam = (username) => { - return usersInHeist.some(e => e.username === username); -}; +function userOnTeam(username: string): boolean { + return usersInHeist.some(u => u.username === username); +} -exports.clearCooldowns = () => { +function clearCooldowns() { if (cooldownTimeoutId != null) { clearTimeout(cooldownTimeoutId); cooldownTimeoutId = null; @@ -210,6 +202,16 @@ exports.clearCooldowns = () => { clearTimeout(startDelayTimeoutId); startDelayTimeoutId = null; } - exports.lobbyOpen = false; + lobbyOpen = false; usersInHeist = []; -}; \ No newline at end of file +} + +export default { + cooldownExpireTime, + lobbyOpen, + + addUser, + clearCooldowns, + triggerLobbyStart, + userOnTeam +}; diff --git a/src/backend/games/builtin/heist/heist-settings.ts b/src/backend/games/builtin/heist/heist-settings.ts new file mode 100644 index 000000000..05729fc69 --- /dev/null +++ b/src/backend/games/builtin/heist/heist-settings.ts @@ -0,0 +1,52 @@ +import { RoleNumberParameterValue, RolePercentageParameterValue } from "../../../../types/game-manager"; + +export type HeistSettings = { + currencySettings: { + currencyId: string; + defaultWager?: number; + minWager?: number; + maxWager?: number; + }; + successChanceSettings: { + successChances: RolePercentageParameterValue; + }; + winningsMultiplierSettings: { + multipliers: RoleNumberParameterValue; + }; + generalSettings: { + minimumUsers?: number; + startDelay?: number; + cooldown?: number; + }; + entryMessages: { + onJoin?: string; + alreadyJoined?: string; + noWagerAmount?: string; + invalidWagerAmount?: string; + wagerAmountTooLow?: string; + wagerAmountTooHigh?: string; + notEnoughToWager?: string; + }; + generalMessages: { + teamCreation?: string; + onCooldown?: string; + cooldownOver?: string; + startMessage?: string; + teamTooSmall?: string; + heistWinnings?: string; + }; + groupOutcomeMessages: { + hundredPercent: string[]; + top25Percent: string[]; + mid50Percent: string[]; + bottom25Percent: string[]; + zeroPercent: string[]; + }; + soloOutcomeMessages: { + soloSuccess: string[]; + soloFail: string[]; + }; + chatSettings: { + chatter: "Streamer" | "Bot"; + }; +}; diff --git a/src/backend/games/builtin/heist/heist.js b/src/backend/games/builtin/heist/heist.ts similarity index 98% rename from src/backend/games/builtin/heist/heist.js rename to src/backend/games/builtin/heist/heist.ts index b5d86ebae..95c1170db 100644 --- a/src/backend/games/builtin/heist/heist.js +++ b/src/backend/games/builtin/heist/heist.ts @@ -1,11 +1,8 @@ -"use strict"; +import { FirebotGame } from "../../../../types/game-manager"; +import heistCommand from "./heist-command"; +import { HeistSettings } from "./heist-settings"; -const heistCommand = require("./heist-command"); - -/** - * @type {import('../../game-manager').FirebotGame} - */ -module.exports = { +const heistGame: FirebotGame = { id: "firebot-heist", name: "Heist", subtitle: "Score big as a team", @@ -64,7 +61,7 @@ module.exports = { settings: { successChances: { type: "role-percentages", - description: "The chances the viewer has of being surviving a heist", + description: "The chances the viewer has of surviving a heist", tip: "The success chance for the first user role a viewer has in this list is used, so ordering is important!" } } @@ -394,4 +391,6 @@ module.exports = { onSettingsUpdate: () => { heistCommand.clearCooldown(); } -}; \ No newline at end of file +}; + +export default heistGame; diff --git a/src/backend/games/builtin/slots/slot-machine.js b/src/backend/games/builtin/slots/slot-machine.ts similarity index 58% rename from src/backend/games/builtin/slots/slot-machine.js rename to src/backend/games/builtin/slots/slot-machine.ts index 806225fdc..c189f7084 100644 --- a/src/backend/games/builtin/slots/slot-machine.js +++ b/src/backend/games/builtin/slots/slot-machine.ts @@ -1,11 +1,14 @@ -"use strict"; -const twitchChat = require("../../../chat/twitch-chat"); -const util = require("../../../utility"); +import twitchChat from "../../../chat/twitch-chat"; +import util from "../../../utility"; const SPIN_COUNT = 3; -async function spin(showSpinInActionMsg, spinInActionMsg, successChance, chatter) { - +async function spin( + showSpinInActionMsg: boolean, + spinInActionMsg: string | null | undefined, + successChance: number, + chatter: "Streamer" | "Bot" | null | undefined +): Promise { let successCount = 0; if (showSpinInActionMsg) { @@ -13,7 +16,6 @@ async function spin(showSpinInActionMsg, spinInActionMsg, successChance, chatter } for (let currentSpin = 1; currentSpin <= SPIN_COUNT; currentSpin++) { - await util.wait(750); const successfulRoll = util.getRandomInt(1, 100) <= successChance; @@ -26,4 +28,6 @@ async function spin(showSpinInActionMsg, spinInActionMsg, successChance, chatter return successCount; } -exports.spin = spin; \ No newline at end of file +export default { + spin +}; diff --git a/src/backend/games/builtin/slots/slot-settings.ts b/src/backend/games/builtin/slots/slot-settings.ts new file mode 100644 index 000000000..630cfd586 --- /dev/null +++ b/src/backend/games/builtin/slots/slot-settings.ts @@ -0,0 +1,32 @@ +import { RolePercentageParameterValue } from "../../../../types/game-manager"; + +export type SlotSettings = { + currencySettings: { + currencyId: string; + defaultWager?: number; + minWager?: number; + maxWager?: number; + }; + spinSettings: { + successChances: RolePercentageParameterValue; + multiplier: number; + }; + cooldownSettings: { + cooldown?: number; + }; + generalMessages: { + alreadySpinning?: string; + onCooldown?: string; + noWagerAmount?: string; + invalidWagerAmount?: string; + moreThanZero?: string; + minWager?: string; + maxWager?: string; + notEnough?: string; + spinInAction?: string; + spinSuccessful?: string; + }; + chatSettings: { + chatter: "Streamer" | "Bot"; + }; +}; diff --git a/src/backend/games/builtin/slots/slots.js b/src/backend/games/builtin/slots/slots.ts similarity index 97% rename from src/backend/games/builtin/slots/slots.js rename to src/backend/games/builtin/slots/slots.ts index e2baa6f89..348671344 100644 --- a/src/backend/games/builtin/slots/slots.js +++ b/src/backend/games/builtin/slots/slots.ts @@ -1,11 +1,8 @@ -"use strict"; +import { FirebotGame } from "../../../../types/game-manager"; +import { SlotSettings } from "./slot-settings"; +import spinCommand from "./spin-command"; -const spinCommand = require("./spin-command"); - -/** - * @type {import('../../game-manager').FirebotGame} - */ -module.exports = { +const slotsGame: FirebotGame = { id: "firebot-slots", name: "Slots", subtitle: "Spin to win", @@ -216,4 +213,6 @@ module.exports = { onSettingsUpdate: () => { spinCommand.purgeCaches(); } -}; \ No newline at end of file +}; + +export default slotsGame; diff --git a/src/backend/games/builtin/slots/spin-command.js b/src/backend/games/builtin/slots/spin-command.ts similarity index 82% rename from src/backend/games/builtin/slots/spin-command.js rename to src/backend/games/builtin/slots/spin-command.ts index 54e21b34d..8c77e1ba9 100644 --- a/src/backend/games/builtin/slots/spin-command.js +++ b/src/backend/games/builtin/slots/spin-command.ts @@ -1,26 +1,26 @@ -"use strict"; - -const util = require("../../../utility"); -const twitchChat = require("../../../chat/twitch-chat"); -const commandManager = require("../../../chat/commands/command-manager"); -const gameManager = require("../../game-manager"); -const currencyAccess = require("../../../currency/currency-access").default; -const currencyManager = require("../../../currency/currency-manager"); -const customRolesManager = require("../../../roles/custom-roles-manager"); -const teamRolesManager = require("../../../roles/team-roles-manager"); -const twitchRolesManager = require("../../../../shared/twitch-roles"); -const slotMachine = require("./slot-machine"); -const logger = require("../../../logwrapper"); -const moment = require("moment"); -const NodeCache = require("node-cache"); -const twitchApi = require("../../../twitch-api/api"); +import util from "../../../utility"; +import twitchChat from "../../../chat/twitch-chat"; +import commandManager from "../../../chat/commands/command-manager"; +import gameManager from "../../game-manager"; +import currencyAccess from "../../../currency/currency-access"; +import currencyManager from "../../../currency/currency-manager"; +import customRolesManager from "../../../roles/custom-roles-manager"; +import teamRolesManager from "../../../roles/team-roles-manager"; +import twitchRolesManager from "../../../../shared/twitch-roles"; +import { SystemCommand } from "../../../../types/commands"; +import { GameSettings } from "../../../../types/game-manager"; +import slotMachine from "./slot-machine"; +import { SlotSettings } from "./slot-settings"; +import logger from "../../../logwrapper"; +import moment from "moment"; +import NodeCache from "node-cache"; const activeSpinners = new NodeCache({checkperiod: 2}); const cooldownCache = new NodeCache({checkperiod: 5}); const SPIN_COMMAND_ID = "firebot:spin"; -const spinCommand = { +const spinCommand: SystemCommand = { definition: { id: SPIN_COMMAND_ID, name: "Spin (Slots)", @@ -42,20 +42,18 @@ const spinCommand = { ] }, onTriggerEvent: async (event) => { + const { chatMessage, userCommand } = event; - const { userCommand } = event; - - const slotsSettings = gameManager.getGameSettings("firebot-slots"); + const slotsSettings = gameManager.getGameSettings("firebot-slots") as GameSettings; const chatter = slotsSettings.settings.chatSettings.chatter; - const username = userCommand.commandSender; - const user = await twitchApi.users.getUserByName(username); - if (user == null) { - logger.warn(`Could not process spin command for ${username}. User does not exist.`); - return; - } + const username = chatMessage.username; + const user = { + id: chatMessage.userId, + displayName: chatMessage.userDisplayName ?? username + }; // parse the wager amount - let wagerAmount; + let wagerAmount: number; if (event.userCommand.args.length < 1) { const defaultWager = slotsSettings.settings.currencySettings.defaultWager; if (defaultWager == null || defaultWager < 1) { @@ -99,7 +97,8 @@ const spinCommand = { if (slotsSettings.settings.generalMessages.onCooldown) { const timeRemainingDisplay = util.secondsForHumans(Math.abs(moment().diff(cooldownExpireTime, 'seconds'))); const cooldownMsg = slotsSettings.settings.generalMessages.onCooldown - .replace("{username}", user.displayName).replace("{timeRemaining}", timeRemainingDisplay); + .replace("{username}", user.displayName) + .replace("{timeRemaining}", timeRemainingDisplay); await twitchChat.sendChatMessage(cooldownMsg, null, chatter); } @@ -123,7 +122,8 @@ const spinCommand = { if (wagerAmount < minWager) { if (slotsSettings.settings.generalMessages.minWager) { const minWagerMsg = slotsSettings.settings.generalMessages.minWager - .replace("{username}", user.displayName).replace("{minWager}", minWager); + .replace("{username}", user.displayName) + .replace("{minWager}", `${minWager}`); await twitchChat.sendChatMessage(minWagerMsg, null, chatter); } @@ -136,7 +136,8 @@ const spinCommand = { if (wagerAmount > maxWager) { if (slotsSettings.settings.generalMessages.maxWager) { const maxWagerMsg = slotsSettings.settings.generalMessages.maxWager - .replace("{username}", user.displayName).replace("{maxWager}", maxWager); + .replace("{username}", user.displayName) + .replace("{maxWager}", `${maxWager}`); await twitchChat.sendChatMessage(maxWagerMsg, null, chatter); } @@ -146,7 +147,7 @@ const spinCommand = { } const currencyId = slotsSettings.settings.currencySettings.currencyId; - let userBalance; + let userBalance: number; try { userBalance = await currencyManager.getViewerCurrencyAmount(username, currencyId); } catch (error) { @@ -228,14 +229,13 @@ const spinCommand = { const spinSuccessfulMsg = slotsSettings.settings.generalMessages.spinSuccessful .replace("{username}", user.displayName) - .replace("{successfulRolls}", successfulRolls) + .replace("{successfulRolls}", `${successfulRolls}`) .replace("{winningsAmount}", util.commafy(winnings)) .replace("{currencyName}", currency.name); await twitchChat.sendChatMessage(spinSuccessfulMsg, null, chatter); } activeSpinners.del(username); - } }; @@ -246,7 +246,9 @@ function registerSpinCommand() { } function unregisterSpinCommand() { - commandManager.unregisterSystemCommand(SPIN_COMMAND_ID); + if (commandManager.hasSystemCommand(SPIN_COMMAND_ID)) { + commandManager.unregisterSystemCommand(SPIN_COMMAND_ID); + } } function purgeCaches() { @@ -254,6 +256,8 @@ function purgeCaches() { activeSpinners.flushAll(); } -exports.purgeCaches = purgeCaches; -exports.registerSpinCommand = registerSpinCommand; -exports.unregisterSpinCommand = unregisterSpinCommand; \ No newline at end of file +export default { + purgeCaches, + registerSpinCommand, + unregisterSpinCommand +}; diff --git a/src/backend/games/builtin/trivia/trivia-command.js b/src/backend/games/builtin/trivia/trivia-command.ts similarity index 79% rename from src/backend/games/builtin/trivia/trivia-command.js rename to src/backend/games/builtin/trivia/trivia-command.ts index 10e8559b5..38d0605a6 100644 --- a/src/backend/games/builtin/trivia/trivia-command.js +++ b/src/backend/games/builtin/trivia/trivia-command.ts @@ -1,24 +1,38 @@ -"use strict"; - -const util = require("../../../utility"); -const twitchChat = require("../../../chat/twitch-chat"); -const twitchListeners = require("../../../chat/chat-listeners/twitch-chat-listeners"); -const commandManager = require("../../../chat/commands/command-manager"); -const gameManager = require("../../game-manager"); -const currencyAccess = require("../../../currency/currency-access").default; -const currencyManager = require("../../../currency/currency-manager"); -const customRolesManager = require("../../../roles/custom-roles-manager"); -const teamRolesManager = require("../../../roles/team-roles-manager"); -const twitchRolesManager = require("../../../../shared/twitch-roles"); -const logger = require("../../../logwrapper"); -const moment = require("moment"); -const triviaHelper = require("./trivia-helper"); -const NodeCache = require("node-cache"); -const twitchApi = require("../../../twitch-api/api"); - -let fiveSecTimeoutId; -let answerTimeoutId; -let currentQuestion = null; +import util from "../../../utility"; +import twitchChat from "../../../chat/twitch-chat"; +import twitchListeners from "../../../chat/chat-listeners/twitch-chat-listeners"; +import commandManager from "../../../chat/commands/command-manager"; +import gameManager from "../../game-manager"; +import currencyAccess from "../../../currency/currency-access"; +import currencyManager from "../../../currency/currency-manager"; +import customRolesManager from "../../../roles/custom-roles-manager"; +import teamRolesManager from "../../../roles/team-roles-manager"; +import twitchRolesManager from "../../../../shared/twitch-roles"; +import { FirebotChatMessage } from "../../../../types/chat"; +import { SystemCommand } from "../../../../types/commands"; +import { GameSettings } from "../../../../types/game-manager"; +import logger from "../../../logwrapper"; +import triviaHelper from "./trivia-helper"; +import { TriviaSettings } from "./trivia-settings"; +import moment from "moment"; +import NodeCache from "node-cache"; + +type TriviaQuestion = { + username: string; + question: { + answers: string[]; + correctIndex: number; + }; + wager: number; + winningsMultiplier: number; + currencyId: string; + chatter: "Streamer" | "Bot"; + postCorrectAnswer: boolean; +}; + +let fiveSecTimeoutId: NodeJS.Timeout | null = null; +let answerTimeoutId: NodeJS.Timeout | null = null; +let currentQuestion: TriviaQuestion | null = null; function clearCurrentQuestion() { currentQuestion = null; @@ -32,9 +46,7 @@ function clearCurrentQuestion() { } } -twitchListeners.events.on("chat-message", async (data) => { - /**@type {import("../../../../types/chat").FirebotChatMessage} */ - const chatMessage = data; +async function onChatMessage(chatMessage: FirebotChatMessage) { if (!currentQuestion) { return; } @@ -67,18 +79,21 @@ twitchListeners.events.on("chat-message", async (data) => { const currency = currencyAccess.getCurrencyById(currencyId); - await twitchChat.sendChatMessage(`${chatMessage.userDisplayName ?? username}, that is correct! You have won ${util.commafy(winnings)} ${currency.name}`, null, chatter); + await twitchChat.sendChatMessage(`${chatMessage.userDisplayName ?? username}, that is correct! You have won ${ + util.commafy(winnings)} ${currency.name}`, null, chatter); } else { - await twitchChat.sendChatMessage(`Sorry ${chatMessage.userDisplayName ?? username}, that is incorrect.${postCorrectAnswer ? ` The correct answer was ${question.answers[question.correctIndex - 1]}.` : ""} Better luck next time!`, null, chatter); + await twitchChat.sendChatMessage(`Sorry ${chatMessage.userDisplayName ?? username}, that is incorrect.${ + postCorrectAnswer ? ` The correct answer was ${question.answers[question.correctIndex - 1]}.` : "" + } Better luck next time!`, null, chatter); } clearCurrentQuestion(); -}); +} const cooldownCache = new NodeCache({ checkperiod: 5 }); const TRIVIA_COMMAND_ID = "firebot:trivia"; -const triviaCommand = { +const triviaCommand: SystemCommand = { definition: { id: TRIVIA_COMMAND_ID, name: "Trivia", @@ -103,15 +118,14 @@ const triviaCommand = { const { userCommand } = event; - const triviaSettings = gameManager.getGameSettings("firebot-trivia"); + const triviaSettings = gameManager.getGameSettings("firebot-trivia") as GameSettings; const chatter = triviaSettings.settings.chatSettings.chatter; const username = userCommand.commandSender; - const user = await twitchApi.users.getUserByName(username); - if (user == null) { - logger.warn(`Could not process trivia command for ${username}. User does not exist.`); - return; - } + const user = { + displayName: event.chatMessage.userDisplayName ?? username, + id: event.chatMessage.userId + }; if (event.userCommand.subcommandId === "wagerAmount") { const triggeredArg = userCommand.args[0]; @@ -139,7 +153,7 @@ const triviaCommand = { } const minWager = triviaSettings.settings.currencySettings.minWager; - if (minWager != null & minWager > 0) { + if (minWager != null && minWager > 0) { if (wagerAmount < minWager) { await twitchChat.sendChatMessage(`${user.displayName}, your wager amount must be at least ${minWager}.`, null, chatter); return; @@ -272,11 +286,15 @@ const triviaCommand = { function registerTriviaCommand() { if (!commandManager.hasSystemCommand(TRIVIA_COMMAND_ID)) { commandManager.registerSystemCommand(triviaCommand); + twitchListeners.events.addListener("chat-message", onChatMessage); } } function unregisterTriviaCommand() { - commandManager.unregisterSystemCommand(TRIVIA_COMMAND_ID); + if (commandManager.hasSystemCommand(TRIVIA_COMMAND_ID)) { + commandManager.unregisterSystemCommand(TRIVIA_COMMAND_ID); + twitchListeners.events.removeListener("chat-message", onChatMessage); + } } function purgeCaches() { @@ -284,6 +302,8 @@ function purgeCaches() { clearCurrentQuestion(); } -exports.purgeCaches = purgeCaches; -exports.registerTriviaCommand = registerTriviaCommand; -exports.unregisterTriviaCommand = unregisterTriviaCommand; \ No newline at end of file +export default { + purgeCaches, + registerTriviaCommand, + unregisterTriviaCommand +}; diff --git a/src/backend/games/builtin/trivia/trivia-helper.js b/src/backend/games/builtin/trivia/trivia-helper.ts similarity index 55% rename from src/backend/games/builtin/trivia/trivia-helper.js rename to src/backend/games/builtin/trivia/trivia-helper.ts index 0130e1c2e..3494343fc 100644 --- a/src/backend/games/builtin/trivia/trivia-helper.js +++ b/src/backend/games/builtin/trivia/trivia-helper.ts @@ -1,31 +1,56 @@ -"use strict"; +import logger from "../../../logwrapper"; +import utils from "../../../utility"; -const logger = require("../../../logwrapper"); -const utils = require("../../../utility"); +export type Difficulty = "easy" | "medium" | "hard"; +export type QuestionType = "boolean" | "multiple"; -const getRandomItem = (array) => { +type OpenTdbMultipleChoiceQuestion = { + correct_answer: string; + incorrect_answers: string[]; + type: "multiple"; +}; +type OpenTdbBooleanQuestion = { + correct_answer: "True" | "False"; + type: "boolean"; +}; +type OpenTdbQuestion = (OpenTdbMultipleChoiceQuestion | OpenTdbBooleanQuestion) & { + category: string; + difficulty: Difficulty; + question: string; +}; +type OpenTdbQuestionResponse = { + response_code: number; + results?: OpenTdbQuestion[]; +}; +type OpenTdbTokenResponse = { + response_code: number; + token: string; +}; + +function getRandomItem(array: T[]): T | null { if (array == null || !array.length) { return null; } const randomIndex = utils.getRandomInt(0, array.length - 1); return array[randomIndex]; -}; +} -const fetchQuestion = async (randomCategory, randomDifficulty, randomType, sessionToken) => { - const url = `https://opentdb.com/api.php?encode=url3986&amount=1&category=${randomCategory}&difficulty=${randomDifficulty}&type=${randomType}${sessionToken ? `&token=${sessionToken}` : ''}`; +async function fetchQuestion (category: number, difficulty: Difficulty, type: QuestionType, token: string) { + const url = `https://opentdb.com/api.php?encode=url3986&amount=1&category=${category}&difficulty=${difficulty}&type=${type}${token ? `&token=${token}` : ''}`; try { - const response = await fetch(url); if (response.ok) { - const data = await response.json(); + const data = await response.json() as OpenTdbQuestionResponse; const responseCode = data.response_code; const results = (data.results || []).map((q) => { q.category = decodeURIComponent(q.category); q.question = decodeURIComponent(q.question); // eslint-disable-next-line camelcase q.correct_answer = decodeURIComponent(q.correct_answer); - // eslint-disable-next-line camelcase - q.incorrect_answers = q.incorrect_answers.map(a => decodeURIComponent(a)); + if (q.type === "multiple" && q.incorrect_answers) { + // eslint-disable-next-line camelcase + q.incorrect_answers = q.incorrect_answers.map(a => decodeURIComponent(a)); + } return q; }); return { @@ -39,17 +64,18 @@ const fetchQuestion = async (randomCategory, randomDifficulty, randomType, sessi logger.error("Unable to fetch question from Trivia API:", error.message); } return null; -}; +} -let sessionToken = null; +let sessionToken: string = null; async function getSessionToken(forceNew = false) { - if (sessionToken == null || forceNew) { + let resultToken = sessionToken; + if (forceNew || !resultToken) { try { const tokenResponse = await fetch("https://opentdb.com/api_token.php?command=request"); if (tokenResponse.ok) { - const data = await tokenResponse.json(); + const data = await tokenResponse.json() as OpenTdbTokenResponse; if (data?.response_code === 0) { - sessionToken = data.token; + resultToken = data.token; } } else { throw new Error(`Request failed with status ${tokenResponse.status}`); @@ -58,35 +84,43 @@ async function getSessionToken(forceNew = false) { logger.error("Unable to get session token for trivia:", error.message); } } - return sessionToken; + return resultToken; } -exports.getQuestion = async (categories, difficulties, types) => { +async function getQuestion( + categories: number[], + difficulties: Difficulty[], + types: QuestionType[] +) { const randomCategory = getRandomItem(categories); const randomDifficulty = getRandomItem(difficulties); const randomType = getRandomItem(types); - const sessionToken = await getSessionToken(); + sessionToken ??= await getSessionToken(); + let questionResponse = await fetchQuestion(randomCategory, randomDifficulty, randomType, sessionToken); if (questionResponse) { + // Code 3: Token Not Found; Session Token does not exist. + // Code 4: Token Empty; Session Token has returned all possible questions for the specified query. Resetting the Token is necessary. if (questionResponse.responseCode === 3 || questionResponse.responseCode === 4) { - const sessionToken = await getSessionToken(true); + sessionToken = await getSessionToken(true); questionResponse = await fetchQuestion(randomCategory, randomDifficulty, randomType, sessionToken); } if (questionResponse && questionResponse.responseCode === 0 && !!questionResponse.results.length) { - const questionData = questionResponse.results[0]; - let answers; - let correctIndex; + const questionData = questionResponse.results[0] as OpenTdbQuestion; + let answers: string[]; + let correctIndex: number; if (questionData.type === "boolean") { answers = ["True", "False"]; // using 1 based index since this is how users will answer correctIndex = questionData.correct_answer === "True" ? 1 : 2; - } else { - answers = utils.shuffleArray([...questionData.incorrect_answers, questionData.correct_answer]); + } else if (questionData.type === "multiple") { + answers = [...questionData.incorrect_answers, questionData.correct_answer]; + answers = utils.shuffleArray(answers as []); // using 1 based index since this is how users will answer correctIndex = answers.findIndex(a => a === questionData.correct_answer) + 1; } @@ -102,4 +136,8 @@ exports.getQuestion = async (categories, difficulties, types) => { } } return null; +} + +export default { + getQuestion }; diff --git a/src/backend/games/builtin/trivia/trivia-settings.ts b/src/backend/games/builtin/trivia/trivia-settings.ts new file mode 100644 index 000000000..f8e2bbbe8 --- /dev/null +++ b/src/backend/games/builtin/trivia/trivia-settings.ts @@ -0,0 +1,30 @@ +import { RoleNumberParameterValue } from "../../../../types/game-manager"; +import { Difficulty, QuestionType } from "./trivia-helper"; + +export type TriviaSettings = { + currencySettings: { + currencyId: string; + defaultWager?: number; + minWager?: number; + maxWager?: number; + }; + questionSettings: { + enabledCategories: number[]; + enabledDifficulties: Difficulty[]; + enabledTypes: QuestionType[]; + answerTime: number; + }; + multiplierSettings: { + easyMultipliers: RoleNumberParameterValue; + mediumMultipliers: RoleNumberParameterValue; + hardMultipliers: RoleNumberParameterValue; + }; + cooldownSettings: { + cooldown?: number; + }; + chatSettings: { + chatter: "Streamer" | "Bot"; + noWagerMessage: string; + postCorrectAnswer: boolean; + }; +}; diff --git a/src/backend/games/builtin/trivia/trivia.js b/src/backend/games/builtin/trivia/trivia.ts similarity index 98% rename from src/backend/games/builtin/trivia/trivia.js rename to src/backend/games/builtin/trivia/trivia.ts index 79a591f4a..468cadf17 100644 --- a/src/backend/games/builtin/trivia/trivia.js +++ b/src/backend/games/builtin/trivia/trivia.ts @@ -1,9 +1,8 @@ -"use strict"; -const triviaCommand = require("./trivia-command"); -/** - * @type {import('../../game-manager').FirebotGame} - */ -module.exports = { +import { FirebotGame } from "../../../../types/game-manager"; +import triviaCommand from "./trivia-command"; +import { TriviaSettings } from "./trivia-settings"; + +const triviaGame: FirebotGame = { id: "firebot-trivia", name: "Trivia", subtitle: "Knowledge is power", @@ -335,4 +334,6 @@ module.exports = { onSettingsUpdate: () => { triviaCommand.purgeCaches(); } -}; \ No newline at end of file +}; + +export default triviaGame; diff --git a/src/backend/games/game-manager.js b/src/backend/games/game-manager.js index 0ae7a9f2b..e8e34db75 100644 --- a/src/backend/games/game-manager.js +++ b/src/backend/games/game-manager.js @@ -3,8 +3,6 @@ const profileManager = require("../common/profile-manager"); const frontendCommunicator = require("../common/frontend-communicator"); -const getGameDb = () => profileManager.getJsonDbInProfile("/games"); - /** * @typedef {"string" | "number" | "boolean" | "enum" | "filepath" | "currency-select" | "chatter-select" | "editable-list" | "role-percentages" | "role-numbers"} SettingType */ @@ -57,6 +55,10 @@ const getGameDb = () => profileManager.getJsonDbInProfile("/games"); * @property {GameFn} onSettingsUpdate - Called whenever the settings from settingCategories are updated by the user. */ +/** + * @return {Object.} + */ +const getGameDb = () => profileManager.getJsonDbInProfile("/games"); /** * @type {Object.} @@ -96,6 +98,25 @@ function registerGame(game) { registeredGames.push(game); } +/** + * @param {string} gameId + */ +function unregisterGame(gameId) { + const gameIdx = registeredGames.findIndex(g => g.id === gameId); + if (gameIdx >= 0) { + if (registeredGames[gameIdx].onUnload) { + const gameSettings = allGamesSettings[gameId]; + gameSettings.active = false; + registeredGames[gameIdx].onUnload(gameSettings); + } + registeredGames.splice(gameIdx, 1); + } +} + +/** + * @param {FirebotGame} game + * @param {Object.>} savedSettings + */ function buildGameSettings(game, savedSettings) { let settingsData = { active: game.active, @@ -121,6 +142,10 @@ function buildGameSettings(game, savedSettings) { return settingsData; } +/** + * @param {Object.} settingCategories + * @param {Object.>} savedSettings + */ function setGameSettingValues(settingCategories, savedSettings) { if (settingCategories && savedSettings) { for (const categoryId of Object.keys(settingCategories)) { @@ -134,6 +159,10 @@ function setGameSettingValues(settingCategories, savedSettings) { return settingCategories; } +/** + * @param {Object.} settingCategories + * @param {Object.>} savedSettings + */ function getGameSettingsFromValues(settingCategories, savedSettings) { if (settingCategories && savedSettings) { for (const categoryId of Object.keys(settingCategories)) { @@ -178,7 +207,7 @@ function saveAllGameSettings() { } function getGames() { - return registeredGames.map(g => { + return registeredGames.map((g) => { return { id: g.id, name: g.name, @@ -195,6 +224,11 @@ frontendCommunicator.onAsync('get-games', async () => { return getGames(); }); +/** + * @param {string} gameId + * @param {Record} settingCategories + * @param {boolean} activeStatus + */ function updateGameSettings(gameId, settingCategories, activeStatus) { const game = registeredGames.find(g => g.id === gameId); @@ -262,4 +296,5 @@ frontendCommunicator.on('reset-game-to-defaults', (gameId) => { exports.loadGameSettings = loadGameSettings; exports.registerGame = registerGame; +exports.unregisterGame = unregisterGame; exports.getGameSettings = getGameSettings; diff --git a/src/types/commands.d.ts b/src/types/commands.d.ts index b46eeb5af..d111a041e 100644 --- a/src/types/commands.d.ts +++ b/src/types/commands.d.ts @@ -151,10 +151,15 @@ CommandDefinition, | "simple" >; -export type SystemCommandDefinition = CommandDefinition & { +type SystemSubcommand = SubCommand & { hideCooldowns?: boolean; }; +export type SystemCommandDefinition = Omit & { + hideCooldowns?: boolean; + subCommands?: SystemSubcommand[]; +}; + export type SystemCommand = { definition: SystemCommandDefinition; onTriggerEvent: ( diff --git a/src/types/game-manager.d.ts b/src/types/game-manager.d.ts new file mode 100644 index 000000000..6d71a18c1 --- /dev/null +++ b/src/types/game-manager.d.ts @@ -0,0 +1,276 @@ +export type RoleNumberParameterValue = { + base: number; + roles: Array<{ + roleId: string; + value: number; + }>; +}; +export type RolePercentageParameterValue = { + basePercent: number; + roles: Array<{ + roleId: string; + percent: number; + }>; +}; + +type SettingType = + | "string" + | "number" + | "boolean" + | "enum" + | "filepath" + | "currency-select" + | "chatter-select" + | "editable-list" + | "role-percentages" + | "role-numbers" + | "multiselect"; + +type BaseSettingDefinition = { + /** The type of the game settings value. */ + type: SettingType; + /** Text shown above the setting in bold text. */ + title?: string; + /** A short sub-text describing the purpose of the setting. */ + description?: string; + /** Display a line under the setting. */ + showBottomHr?: boolean; + /** A rank to tell the UI how to order settings. */ + sortRank?: number; + /** Human-readable tip, this is rendered below the field in smaller muted text. */ + tip?: string; + validation?: { + /** Whether or not the setting is required to be assigned. */ + required?: boolean; + }; +}; +type StringSettingDefinition = BaseSettingDefinition & { + type: "string"; + /** Text shown above the setting in bold text. */ + title: string; + /** The default value for the setting. */ + default?: string; + /** `true` to display a `