falgames
Version:
Falgames is a helpful package to enhance your discord bot with fun and interactive minigames
246 lines (218 loc) • 9.84 kB
JavaScript
import { EmbedBuilder, ActionRowBuilder } from "discord.js"
import { disableButtons, shuffleArray, formatMessage, ButtonBuilder } from "../utils/utils.js"
import events from "node:events"
/**
* This class allows you to create and manage a Match Pairs game in Discord, including handling user interactions and game logic.
* It extends the Node.js `events` module to allow for event-driven programming, specifically emitting a `gameOver` event when the game ends.
*
* @class MatchPairs
* @param {MatchPairsOptions} options - The options for the Match Pairs game.
*
* @extends {events}
* @fires MatchPairs#gameOver
* @typedef {Object} MatchPairsOptions
*/
export class MatchPairs extends events {
/**
* Represents a MatchPairs game.
* @constructor
* @param {Object} options - Options to set for the MatchPairs game.
* @param {Object} options.message - The message object associated with the game.
* @param {boolean} [options.isSlashGame=false] - Whether the game is played using slash commands.
* @param {Object} [options.embed] - The embed options for the game.
* @param {string} [options.embed.title='Match Pairs'] - The title of the embed.
* @param {string} [options.embed.color='#551476'] - The color of the embed.
* @param {string} [options.embed.description='**Click on the buttons to match emojis with their pairs.**'] - The description of the embed.
* @param {number} [options.timeoutTime=60000] - The timeout time for the game.
* @param {string[]} [options.emojis] - The emojis to use for the game.
* @param {string} [options.winMessage='**You won the Game! You turned a total of `{tilesTurned}` tiles.**'] - The win message for the game.
* @param {string} [options.loseMessage='**You lost the Game! You turned a total of `{tilesTurned}` tiles.**'] - The lose message for the game.
* @param {string} [options.playerOnlyMessage='Only {player} can use these buttons.'] - The message to show when someone else tries to use the buttons.
*/
constructor(options = {}) {
if (!options.isSlashGame) options.isSlashGame = false
if (!options.message) throw new TypeError("NO_MESSAGE: No message option was provided.")
if (typeof options.message !== "object") throw new TypeError("INVALID_MESSAGE: message option must be an object.")
if (typeof options.isSlashGame !== "boolean")
throw new TypeError("INVALID_COMMAND_TYPE: isSlashGame option must be a boolean.")
if (!options.embed) options.embed = {}
if (!options.embed.title) options.embed.title = "Match Pairs"
if (!options.embed.color) options.embed.color = "#551476"
if (!options.embed.description)
options.embed.description = "**Click on the buttons to match emojis with their pairs.**"
if (!options.timeoutTime) options.timeoutTime = 60000
if (!options.emojis)
options.emojis = [
"🍉",
"🍇",
"🍊",
"🍋",
"🥭",
"🍎",
"🍏",
"🥝",
"🥥",
"🍓",
"🍒",
"🫐",
"🍍",
"🍅",
"🍐",
"🥔",
"🌽",
"🥕",
"🥬",
"🥦",
]
if (!options.winMessage) options.winMessage = "**You won the Game! You turned a total of `{tilesTurned}` tiles.**"
if (!options.loseMessage)
options.loseMessage = "**You lost the Game! You turned a total of `{tilesTurned}` tiles.**"
if (typeof options.embed !== "object") throw new TypeError("INVALID_EMBED: embed option must be an object.")
if (typeof options.embed.title !== "string") throw new TypeError("INVALID_EMBED: embed title must be a string.")
if (typeof options.embed.description !== "string")
throw new TypeError("INVALID_EMBED: embed description must be a string.")
if (typeof options.timeoutTime !== "number")
throw new TypeError("INVALID_TIME: Timeout time option must be a number.")
if (typeof options.winMessage !== "string")
throw new TypeError("INVALID_MESSAGE: Win Message option must be a string.")
if (typeof options.loseMessage !== "string")
throw new TypeError("INVALID_MESSAGE: Lose Message option must be a string.")
if (!Array.isArray(options.emojis)) throw new TypeError("INVALID_EMOJIS: emojis option must be an array.")
if (options.emojis.length < 12)
throw new RangeError("INVALID_EMOJIS: Emojis option must contain at least 12 emojis.")
if (options.playerOnlyMessage !== false) {
if (!options.playerOnlyMessage) options.playerOnlyMessage = "Only {player} can use these buttons."
if (typeof options.playerOnlyMessage !== "string")
throw new TypeError("INVALID_MESSAGE: playerOnly Message option must be a string.")
}
super()
this.options = options
this.message = options.message
this.emojis = options.emojis
this.remainingPairs = 12
this.components = []
this.selected = null
this.tilesTurned = 0
this.length = 5
}
async sendMessage(content) {
if (this.options.isSlashGame) return await this.message.editReply(content)
else return await this.message.channel.send(content)
}
async startGame() {
if (this.options.isSlashGame || !this.message.author) {
if (!this.message.deferred) await this.message.deferReply().catch((e) => {})
this.message.author = this.message.user
this.options.isSlashGame = true
}
this.emojis = shuffleArray(this.emojis).slice(0, 12)
this.emojis.push(...this.emojis, "🃏")
this.emojis = shuffleArray(this.emojis)
this.components = this.getComponents()
const embed = new EmbedBuilder()
.setColor(this.options.embed.color)
.setTitle(this.options.embed.title)
.setDescription(this.options.embed.description)
.setAuthor({ name: this.message.author.tag, iconURL: this.message.author.displayAvatarURL({ dynamic: true }) })
const msg = await this.sendMessage({ embeds: [embed], components: this.components })
return this.handleButtons(msg)
}
getPairEmoji(emoji) {
const emojis = []
for (let y = 0; y < this.length; y++) {
for (let x = 0; x < this.length; x++) {
const index = y * this.length + x
if (this.emojis[index] === emoji) emojis.push({ x: x, y: y, id: index })
}
}
return emojis
}
getComponents() {
const components = []
for (let y = 0; y < this.length; y++) {
const row = new ActionRowBuilder()
for (let x = 0; x < this.length; x++) {
const btn = new ButtonBuilder()
.setStyle("SECONDARY")
.setLabel("\u200b")
.setCustomId("matchpairs_" + x + "_" + y)
row.addComponents(btn)
}
components.push(row)
}
return components
}
gameOver(msg, result) {
const MatchPairsGame = {
player: this.message.author,
tilesTurned: this.tilesTurned,
remainingPairs: this.remainingPairs,
}
const GameOverMessage = result ? this.options.winMessage : this.options.loseMessage
this.emit("gameOver", { result: result ? "win" : "lose", ...MatchPairsGame })
const embed = new EmbedBuilder()
.setColor(this.options.embed.color)
.setTitle(this.options.embed.title)
.setDescription(GameOverMessage.replace("{tilesTurned}", this.tilesTurned))
.setAuthor({ name: this.message.author.tag, iconURL: this.message.author.displayAvatarURL({ dynamic: true }) })
return msg.edit({ embeds: [embed], components: disableButtons(this.components) })
}
async handleButtons(msg) {
const collector = msg.createMessageComponentCollector({ idle: this.options.timeoutTime })
collector.on("collect", async (btn) => {
await btn.deferUpdate().catch((e) => {})
if (btn.user.id !== this.message.author.id) {
if (this.options.playerOnlyMessage)
btn.followUp({ content: formatMessage(this.options, "playerOnlyMessage"), ephemeral: true })
return
}
const x = parseInt(btn.customId.split("_")[1])
const y = parseInt(btn.customId.split("_")[2])
const id = y * this.length + x
const emoji = this.emojis[id]
const emojiBtn = this.components[y].components[x]
this.tilesTurned += 1
if (!this.selected) {
this.selected = { x: x, y: y, id: id }
emojiBtn.setEmoji(emoji).setStyle("PRIMARY").removeLabel()
} else if (this.selected.id === id) {
this.selected = null
emojiBtn.removeEmoji().setStyle("SECONDARY").setLabel("\u200b")
} else {
const selectedEmoji = this.emojis[this.selected.id]
const selectedBtn = this.components[this.selected.y].components[this.selected.x]
const matched = emoji === selectedEmoji || selectedEmoji === "🃏" || emoji === "🃏"
if (selectedEmoji === "🃏" || emoji === "🃏") {
const joker = emoji === "🃏" ? this.selected : { x: x, y: y, id: id }
const pair = this.getPairEmoji(this.emojis[joker.id]).filter((b) => b.id !== joker.id)[0]
const pairBtn = this.components[pair.y].components[pair.x]
pairBtn.setEmoji(this.emojis[pair.id]).setStyle("SUCCESS").setDisabled(true).removeLabel()
}
emojiBtn
.setEmoji(emoji)
.setStyle(matched ? "SUCCESS" : "DANGER")
.setDisabled(matched)
.removeLabel()
selectedBtn
.setEmoji(selectedEmoji)
.setStyle(matched ? "SUCCESS" : "DANGER")
.setDisabled(matched)
.removeLabel()
if (!matched) {
await msg.edit({ components: this.components })
emojiBtn.removeEmoji().setStyle("SECONDARY").setLabel("\u200b")
selectedBtn.removeEmoji().setStyle("SECONDARY").setLabel("\u200b")
return (this.selected = null)
}
this.remainingPairs -= 1
this.selected = null
}
if (this.remainingPairs === 0) return collector.stop()
return await msg.edit({ components: this.components })
})
collector.on("end", async (_, reason) => {
if (reason === "idle") return this.gameOver(msg, false)
if (reason === "user") return this.gameOver(msg, true)
})
}
}