UNPKG

falgames

Version:

Falgames is a helpful package to enhance your discord bot with fun and interactive minigames

193 lines (170 loc) 8.93 kB
import { EmbedBuilder, AttachmentBuilder } from "discord.js" import events from "node:events" import { createCanvas, loadImage } from "canvas" /** * This class allows you to create and manage a Guess The Pokemon 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 GuessThePokemon * @param {GuessThePokemonOptions} options - The options for the Guess The Pokemon game. * * @extends {events} * @fires GuessThePokemon#gameOver * @typedef {Object} GuessThePokemonOptions */ export class GuessThePokemon extends events { /** * Represents a GuessThePokemon game. * @constructor * @param {Object} options - The options for the GuessThePokemon game. * @param {boolean} [options.isSlashGame=false] - Whether the game is played using slash commands. * @param {Object} options.message - The message object associated with the game. * @param {Object} [options.embed] - The embed options for the game. * @param {string} [options.embed.title='Who\'s The Pokemon?'] - The title of the embed. * @param {string} [options.embed.color='#551476'] - The color of the embed. * @param {number} [options.timeoutTime=60000] - The timeout time for the game. * @param {string} [options.winMessage='You guessed it right! It was a {pokemon}.'] - The win message. * @param {string} [options.loseMessage='Better luck next time! It was a {pokemon}.'] - The lose message. * @param {string} [options.errMessage='Unable to fetch pokemon data! Please try again.'] - The error message. * @param {string} [options.typesText='Types'] - The types text for the embed. * @param {string} [options.abilitiesText='Abilities'] - The abilities text for the embed. * */ 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 = "Who's The Pokemon?" if (!options.embed.color) options.embed.color = "#551476" if (!options.timeoutTime) options.timeoutTime = 60000 if (!options.winMessage) options.winMessage = "You guessed it right! It was a {pokemon}." if (!options.loseMessage) options.loseMessage = "Better luck next time! It was a {pokemon}." if (!options.errMessage) options.errMessage = "Unable to fetch pokemon data! Please try again." if (!options.typesText) options.typesText = "Types" if (!options.abilitiesText) options.abilitiesText = "Abilities" 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.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 (typeof options.typesText !== "string") throw new TypeError("INVALID_MESSAGE: Types Text option must be a string.") if (typeof options.abilitiesText !== "string") throw new TypeError("INVALID_MESSAGE: Abilities Text option must be a string.") super() this.options = options this.message = options.message /** * @typedef Pokemon * @type {Object} * @property {string} name - The name of the pokemon. * @property {string[]} types - The types of the pokemon. * @property {string[]} abilities - The abilities of the pokemon. * @property {string} answerImage - The image of the pokemon. * @property {Buffer} questionImage - The question image of the pokemon. */ /** @type {Pokemon} */ this.pokemon = {} } 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 } const API_URL = "https://pokeapi.co/api/v2/pokemon/" const result = await fetch(API_URL + this.randomInt(1, 1025)) .then((res) => res.json()) .catch((e) => { return null }) if (!result) return this.sendMessage({ content: this.options.errMessage }) this.pokemon.name = result.species.name this.pokemon.types = result.types.map((t) => t.type.name) this.pokemon.abilities = result.abilities.map((a) => a.ability.name) this.pokemon.answerImage = await this.getImage(result.sprites.other["official-artwork"].front_default) this.pokemon.questionImage = await this.createQuestionImage(result.sprites.other["official-artwork"].front_default) const embed = new EmbedBuilder() .setColor(this.options.embed.color) .setTitle(this.options.embed.title) .setImage("attachment://question-image.png") .addFields({ name: this.options.typesText, value: this.pokemon.types.join(", ") ?? "No Data", inline: true }) .addFields({ name: this.options.abilitiesText, value: this.pokemon.abilities.join(", ") ?? "No Data", inline: true, }) .setAuthor({ name: this.message.author.tag, iconURL: this.message.author.displayAvatarURL({ dynamic: true }) }) const attachment = new AttachmentBuilder(this.pokemon.questionImage, { name: "question-image.png" }) const msg = await this.sendMessage({ embeds: [embed], files: [attachment] }) const filter = (m) => m.author.id === this.message.author.id const collector = this.message.channel.createMessageCollector({ idle: this.options.timeoutTime, filter: filter }) collector.on("collect", (m) => { collector.stop() return this.gameOver(msg, m.content?.toLowerCase() === this.pokemon.name.toLowerCase()) }) collector.on("end", (_, reason) => { if (reason === "idle") return this.gameOver(msg, false) }) } async gameOver(msg, result) { const GuessThePokemonGame = { player: this.message.author, pokemon: this.pokemon } this.emit("gameOver", { result: result ? "win" : "lose", ...GuessThePokemonGame }) const resultMessage = result ? this.options.winMessage : this.options.loseMessage const embed = new EmbedBuilder() .setColor(this.options.embed.color) .setTitle(this.pokemon.name.charAt(0).toUpperCase() + this.pokemon.name.slice(1)) .setImage("attachment://answer-image.png") .addFields({ name: "Types", value: this.pokemon.types.join(", ") ?? "No Data", inline: true }) .addFields({ name: "Abilities", value: this.pokemon.abilities.join(", ") ?? "No Data", inline: true }) .setAuthor({ name: this.message.author.tag, iconURL: this.message.author.displayAvatarURL({ dynamic: true }) }) const attachment = new AttachmentBuilder(this.pokemon.answerImage, { name: "answer-image.png" }) return msg.edit({ content: resultMessage.replace("{pokemon}", this.pokemon.name), embeds: [embed], files: [attachment], }) } randomInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min } async getImage(pokemonImgURL) { const size = 475 // Create a canvas and draw the pokemon image on it const canvas = createCanvas(size, size) const ctx = canvas.getContext("2d") const img = await loadImage(pokemonImgURL) ctx.drawImage(img, 0, 0, canvas.width, canvas.height) return canvas.toBuffer() } async createQuestionImage(pokemonImgURL) { const size = 475 // Create a canvas and draw the pokemon image on it const canvas = createCanvas(size, size) const ctx = canvas.getContext("2d") const img = await loadImage(pokemonImgURL) ctx.drawImage(img, 0, 0, canvas.width, canvas.height) const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height) const data = imageData.data const pxColor = [0, 0, 0] // Black color // Loop through the image data and change the color of the non-transparent pixels to black for (let i = 0; i < data.length; i += 4) { if (data[i + 3] < 10) continue // skip transparent pixels (alpha < 10) data[i] = pxColor[0] data[i + 1] = pxColor[1] data[i + 2] = pxColor[2] } ctx.putImageData(imageData, 0, 0) return canvas.toBuffer() } }