├── assets ├── pointer.png └── Poppins │ ├── Poppins-Black.ttf │ ├── Poppins-Bold.ttf │ ├── Poppins-Italic.ttf │ ├── Poppins-Light.ttf │ ├── Poppins-Medium.ttf │ ├── Poppins-Thin.ttf │ ├── Poppins-Regular.ttf │ ├── Poppins-SemiBold.ttf │ ├── Poppins-BlackItalic.ttf │ ├── Poppins-BoldItalic.ttf │ ├── Poppins-ExtraBold.ttf │ ├── Poppins-ExtraLight.ttf │ ├── Poppins-LightItalic.ttf │ ├── Poppins-ThinItalic.ttf │ ├── Poppins-MediumItalic.ttf │ ├── Poppins-ExtraBoldItalic.ttf │ ├── Poppins-ExtraLightItalic.ttf │ └── Poppins-SemiBoldItalic.ttf ├── config.json ├── package.json ├── LICENSE ├── utils.js ├── wheel.js └── index.js /assets/pointer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wickstudio/Roulette-Discord-Bot/HEAD/assets/pointer.png -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "startTime": 10, 3 | "chooseTimeout": 15, 4 | "timeBetweenRounds": 10 5 | } 6 | -------------------------------------------------------------------------------- /assets/Poppins/Poppins-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wickstudio/Roulette-Discord-Bot/HEAD/assets/Poppins/Poppins-Black.ttf -------------------------------------------------------------------------------- /assets/Poppins/Poppins-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wickstudio/Roulette-Discord-Bot/HEAD/assets/Poppins/Poppins-Bold.ttf -------------------------------------------------------------------------------- /assets/Poppins/Poppins-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wickstudio/Roulette-Discord-Bot/HEAD/assets/Poppins/Poppins-Italic.ttf -------------------------------------------------------------------------------- /assets/Poppins/Poppins-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wickstudio/Roulette-Discord-Bot/HEAD/assets/Poppins/Poppins-Light.ttf -------------------------------------------------------------------------------- /assets/Poppins/Poppins-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wickstudio/Roulette-Discord-Bot/HEAD/assets/Poppins/Poppins-Medium.ttf -------------------------------------------------------------------------------- /assets/Poppins/Poppins-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wickstudio/Roulette-Discord-Bot/HEAD/assets/Poppins/Poppins-Thin.ttf -------------------------------------------------------------------------------- /assets/Poppins/Poppins-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wickstudio/Roulette-Discord-Bot/HEAD/assets/Poppins/Poppins-Regular.ttf -------------------------------------------------------------------------------- /assets/Poppins/Poppins-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wickstudio/Roulette-Discord-Bot/HEAD/assets/Poppins/Poppins-SemiBold.ttf -------------------------------------------------------------------------------- /assets/Poppins/Poppins-BlackItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wickstudio/Roulette-Discord-Bot/HEAD/assets/Poppins/Poppins-BlackItalic.ttf -------------------------------------------------------------------------------- /assets/Poppins/Poppins-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wickstudio/Roulette-Discord-Bot/HEAD/assets/Poppins/Poppins-BoldItalic.ttf -------------------------------------------------------------------------------- /assets/Poppins/Poppins-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wickstudio/Roulette-Discord-Bot/HEAD/assets/Poppins/Poppins-ExtraBold.ttf -------------------------------------------------------------------------------- /assets/Poppins/Poppins-ExtraLight.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wickstudio/Roulette-Discord-Bot/HEAD/assets/Poppins/Poppins-ExtraLight.ttf -------------------------------------------------------------------------------- /assets/Poppins/Poppins-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wickstudio/Roulette-Discord-Bot/HEAD/assets/Poppins/Poppins-LightItalic.ttf -------------------------------------------------------------------------------- /assets/Poppins/Poppins-ThinItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wickstudio/Roulette-Discord-Bot/HEAD/assets/Poppins/Poppins-ThinItalic.ttf -------------------------------------------------------------------------------- /assets/Poppins/Poppins-MediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wickstudio/Roulette-Discord-Bot/HEAD/assets/Poppins/Poppins-MediumItalic.ttf -------------------------------------------------------------------------------- /assets/Poppins/Poppins-ExtraBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wickstudio/Roulette-Discord-Bot/HEAD/assets/Poppins/Poppins-ExtraBoldItalic.ttf -------------------------------------------------------------------------------- /assets/Poppins/Poppins-ExtraLightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wickstudio/Roulette-Discord-Bot/HEAD/assets/Poppins/Poppins-ExtraLightItalic.ttf -------------------------------------------------------------------------------- /assets/Poppins/Poppins-SemiBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wickstudio/Roulette-Discord-Bot/HEAD/assets/Poppins/Poppins-SemiBoldItalic.ttf -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wheel-discord-bot", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "node index.js" 8 | }, 9 | "dependencies": { 10 | "canvas": "^2.11.2", 11 | "discord.js": "^14.9.0", 12 | "dotenv": "^16.0.3", 13 | "gif-encoder-2": "^1.0.5", 14 | "gifken": "^3.0.4", 15 | "open": "^9.1.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Wick Studio 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | const { ActionRowBuilder, Message } = require('discord.js'); 2 | const emojis = [ 3 | '1️⃣', 4 | '2️⃣', 5 | '3️⃣', 6 | '4️⃣', 7 | '5️⃣', 8 | '6️⃣', 9 | '7️⃣', 10 | '8️⃣', 11 | '9️⃣', 12 | '🔟', 13 | '<:eleven:989246551077564436>', 14 | '<:twelve:989246551929008200>', 15 | '<:thirteen:989246553451532340>', 16 | '<:fourteen:989246554529464400>', 17 | '<:fifteen:989246544370888754>', 18 | '<:sixteen:989246545281052724>', 19 | '<:seventeen:989246546644197406>', 20 | '<:eighteen:989246547873124442>', 21 | '<:nineteen:989246548904915034>', 22 | '<:twenty:989246550100279408>', 23 | ]; 24 | 25 | const commands = [ 26 | { 27 | name: 'roulette', 28 | description: 'roulette Game', 29 | options: [], 30 | }, 31 | ]; 32 | 33 | function createButtonRows(buttons) { 34 | const rows = []; 35 | let index = 0; 36 | 37 | while (index < buttons.length) { 38 | const row = new ActionRowBuilder(); 39 | for (let i = 0; i < 5 && index < buttons.length; i++) { 40 | row.addComponents(buttons[index]); 41 | index++; 42 | } 43 | rows.push(row); 44 | } 45 | 46 | return rows; 47 | } 48 | 49 | /** 50 | * 51 | * @param {Message} message 52 | * @param {string} username 53 | * @param {*} buttonNumber 54 | */ 55 | 56 | const editButton = (message, players, isleave = false, data = null) => { 57 | players.forEach((player) => { 58 | message.components.find((btn) => { 59 | const row = btn.components.some((row) => row.data.custom_id.includes(`_${player.buttonNumber}_`)); 60 | if (row) { 61 | const exRow = btn.components.find((row) => row.data.custom_id.includes(`_${player.buttonNumber}_`)); 62 | exRow.data.label = player.username; 63 | exRow.data.disabled = true; 64 | } 65 | }); 66 | }); 67 | if (isleave) { 68 | message.components.find((btn) => { 69 | const row = btn.components.some((row) => row.data.custom_id.includes(`_${data.buttonNumber}_`)); 70 | if (row) { 71 | const exRow = btn.components.find((row) => row.data.custom_id.includes(`_${data.buttonNumber}_`)); 72 | exRow.data.label = ''; 73 | exRow.data.disabled = false; 74 | } 75 | }); 76 | } 77 | return message; 78 | }; 79 | 80 | function sleep(ms) { 81 | return new Promise((resolve) => setTimeout(resolve, ms * 1000)); 82 | } 83 | 84 | module.exports = { commands, emojis, createButtonRows, editButton, sleep }; 85 | -------------------------------------------------------------------------------- /wheel.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | const { createCanvas, loadImage, registerFont } = require('canvas'); 3 | registerFont(join(__dirname, "assets", "Poppins", "Poppins-Bold.ttf"), { 4 | family: "PoppinsBold", 5 | }); 6 | 7 | registerFont(join(__dirname, "assets", "Poppins", "Poppins-Regular.ttf"), { 8 | family: "PoppinsReg", 9 | }); 10 | 11 | 12 | module.exports.createSpinWheel = async ( 13 | data, 14 | returnCanvas 15 | ) => { 16 | const canvas = createCanvas(500, 500); 17 | const ctx = canvas.getContext("2d"); 18 | 19 | const centerX = canvas.width / 2; 20 | const centerY = canvas.height / 2; 21 | const radius = 200; 22 | 23 | // Draw the circle 24 | ctx.beginPath(); 25 | ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI, false); 26 | ctx.fillStyle = "black"; 27 | ctx.fill(); 28 | ctx.lineWidth = 5; 29 | ctx.strokeStyle = "black"; 30 | ctx.stroke(); 31 | 32 | // this is the angle for each item 33 | const angle = (2 * Math.PI) / data.length; 34 | 35 | const winnerItem = data.find((i) => i.winner); 36 | const beforeArray = data.slice(0, data.indexOf(winnerItem)); 37 | const afterArray = data.slice(data.indexOf(winnerItem) + 1); 38 | data = [winnerItem, ...afterArray, ...beforeArray]; 39 | 40 | let startAngle = -angle / 2; 41 | let endAngle = startAngle + angle; 42 | for (let i = 0; i < data.length; i++) { 43 | const { color, label } = data[i]; 44 | 45 | // Draw the wedge 46 | ctx.beginPath(); 47 | ctx.moveTo(centerX, centerY); 48 | ctx.arc(centerX, centerY, radius, startAngle, endAngle, false); 49 | ctx.fillStyle = color; 50 | ctx.fill(); 51 | 52 | // Draw the line 53 | ctx.lineWidth = 1; 54 | ctx.strokeStyle = "black"; 55 | ctx.beginPath(); 56 | ctx.moveTo(centerX, centerY); 57 | ctx.arc(centerX, centerY, radius, startAngle, endAngle, false); 58 | ctx.stroke(); 59 | 60 | // Draw the last line 61 | if (i === data.length - 1) { 62 | ctx.beginPath(); 63 | ctx.moveTo(centerX, centerY); 64 | ctx.arc(centerX, centerY, radius, endAngle, endAngle + 0.01, false); 65 | ctx.stroke(); 66 | } 67 | 68 | // Draw the text 69 | ctx.save(); 70 | ctx.translate(centerX, centerY); 71 | ctx.rotate(startAngle + angle / 2); 72 | //get rgm from hex color 73 | const [r, g, b] = color 74 | .substring(1) 75 | .match(/.{2}/g) 76 | ?.map((x) => parseInt(x, 16)); 77 | if (r * 0.299 + g * 0.587 + b * 0.114 > 130) { 78 | ctx.fillStyle = "black"; 79 | } else ctx.fillStyle = "white"; 80 | ctx.font = "16px PoppinsReg"; 81 | let textWidth = ctx.measureText(label).width; 82 | while (textWidth > radius * 0.65) { 83 | ctx.font = `${parseInt(ctx.font) - 1}px PoppinsReg`; 84 | textWidth = ctx.measureText(label).width; 85 | } 86 | ctx.fillText(label, radius / 3, 10); 87 | ctx.restore(); 88 | 89 | // Update the angles 90 | startAngle = endAngle; 91 | endAngle = startAngle + angle; 92 | } 93 | 94 | ctx.beginPath(); 95 | ctx.moveTo(centerX, centerY); 96 | ctx.arc(centerX, centerY, 50, 0, 2 * Math.PI, false); 97 | ctx.fillStyle = "black"; 98 | ctx.fill(); 99 | 100 | if (returnCanvas) return canvas; 101 | 102 | return canvas.toBuffer("image/png"); 103 | }; 104 | 105 | module.exports.createWheel = async (data, userAvatar) => { 106 | 107 | let winnerIndex = data.findIndex((i) => i.winner); 108 | const winner = data[winnerIndex]; 109 | const itemsBeforeWinner = data.slice(0, winnerIndex); 110 | const itemsAfterWinner = data.slice(winnerIndex + 1); 111 | 112 | const items = [winner, ...itemsAfterWinner, ...itemsBeforeWinner]; 113 | 114 | const spinwheel = await module.exports.createSpinWheel(items, true); 115 | 116 | 117 | const canvas = createCanvas(500, 500); 118 | const ctx = canvas.getContext("2d"); 119 | 120 | const centerX = canvas.width / 2; 121 | const centerY = canvas.height / 2; 122 | 123 | const pointer = await loadImage( 124 | join(__dirname, "assets", "pointer.png") 125 | ); 126 | 127 | const drawWheel = async (angleInDegree, i) => { 128 | 129 | ctx.save(); 130 | ctx.translate(centerX, centerY); 131 | ctx.rotate((angleInDegree * Math.PI) / 180); 132 | ctx.drawImage(spinwheel, -centerX, -centerY, 500, 500); 133 | ctx.restore(); 134 | 135 | // Draw the pointer 136 | ctx.save(); 137 | ctx.translate(250, 250); 138 | ctx.rotate(Math.PI / 2); 139 | ctx.drawImage(pointer, -25, -240, 50, 50); 140 | ctx.restore(); 141 | 142 | // Draw the play button 143 | ctx.beginPath(); 144 | ctx.moveTo(centerX, centerY); 145 | ctx.arc(centerX, centerY, 50, 0, 2 * Math.PI, false); 146 | ctx.fillStyle = "black"; 147 | ctx.fill(); 148 | ctx.clip() 149 | 150 | // Draw the play button 151 | const playbutton = await loadImage(userAvatar); 152 | ctx.drawImage(playbutton, centerX - 50, centerY - 50, 100, 100); 153 | ctx.lineWidth = 0; 154 | ctx.strokeStyle = "black"; 155 | ctx.beginPath(); 156 | ctx.arc(centerX, centerY, 50, 0, 2 * Math.PI, false); 157 | ctx.stroke(); 158 | }; 159 | 160 | const segment_size = 360 / data.length; 161 | const min = segment_size / 2 - ((segment_size / 2) * 2); 162 | const max = segment_size / 2 163 | let randomNum = Math.random() * (max - min) + min; 164 | await drawWheel(randomNum); 165 | 166 | return canvas.toBuffer('image/png') 167 | } 168 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const { ButtonBuilder, EmbedBuilder, ButtonStyle, CommandInteraction, AttachmentBuilder, REST, Routes } = require('discord.js'); 3 | const { createButtonRows, editButton, commands, emojis, sleep } = require('./utils.js'); 4 | const { startTime, chooseTimeout, timeBetweenRounds } = require('./config.json'); 5 | const { createWheel } = require('./wheel.js'); 6 | const Discord = require('discord.js'); 7 | const http = require('http'); 8 | http 9 | .createServer(function (req, res) { 10 | res.write("I'm alive"); 11 | res.end(); 12 | }) 13 | .listen(8080); 14 | const client = new Discord.Client({ 15 | intents: [Discord.IntentsBitField.Flags.Guilds], 16 | }); 17 | 18 | const Games = new Map(); 19 | 20 | client.on('ready', async () => { 21 | // Construct and prepare an instance of the REST module 22 | const rest = new REST().setToken(process.env.TOKEN); 23 | 24 | try { 25 | console.log(`Started refreshing ${commands.length} application (/) commands.`); 26 | 27 | // The put method is used to fully refresh all commands in the guild with the current set 28 | const data = await rest.put(Routes.applicationCommands(client.user.id), { body: commands }); 29 | 30 | console.log(`Successfully reloaded ${data.length} application (/) commands.`); 31 | } catch (error) { 32 | // And of course, make sure you catch and log any errors! 33 | console.error(error); 34 | } 35 | 36 | console.log('I am ready!'); 37 | console.log('Bot By Wick Studio'); 38 | }); 39 | 40 | client.on('interactionCreate', async (interaction) => { 41 | if (interaction.isCommand()) { 42 | if (interaction.commandName == 'roulette') { 43 | if (await Games.get(interaction.guildId)) { 44 | // Send a message indicating that a game is already running in this server 45 | interaction.reply({ content: 'There is already a game in progress in this server.', ephemeral: true }); 46 | return; 47 | } 48 | const embed = new EmbedBuilder() 49 | .setTitle('Roulette') 50 | .setColor('#ccc666') 51 | .setDescription(`__**Players:**__\nNo players are participating in the game`) 52 | .addFields( 53 | { 54 | name: '__Player Instructions:__', 55 | value: `**1-** Join the game\n**2-** The first round will start, and a random player will be selected\n**3-** If you are the chosen player, you will select a player of your choice to be kicked from the game\n**4-** The player will be kicked, and a new round will start. When all players are kicked except for two, the wheel will spin, and the chosen player wins the game.`, 56 | }, 57 | { 58 | name: '__Game Starts In__:', 59 | value: `****`, 60 | } 61 | ); 62 | 63 | const buttons = Array.from(Array(20).keys()).map((i) => 64 | new ButtonBuilder() 65 | .setCustomId(`join_${i + 1}_roulette`) 66 | .setStyle(ButtonStyle.Secondary) 67 | .setEmoji(emojis[i]) 68 | ); 69 | 70 | // New buttons 71 | const randomButton = new ButtonBuilder().setCustomId(`join_random_roulette`).setLabel('Join Randomly').setStyle(ButtonStyle.Success); 72 | 73 | const leaveButton = new ButtonBuilder().setCustomId(`leave_roulette`).setLabel('Leave the Game').setStyle(ButtonStyle.Danger); 74 | 75 | const rows = createButtonRows([...buttons, randomButton, leaveButton]); 76 | await interaction.reply({ 77 | content: 'Starting Roulette Game', 78 | components: rows, 79 | embeds: [embed], 80 | }); 81 | Games.set(interaction.guildID, { players: [] }); 82 | const repliedMessage = await interaction.fetchReply(); 83 | setTimeout(async () => { 84 | repliedMessage.embeds[0].fields[1].value = `\`\`\`Started\`\`\``; 85 | repliedMessage.edit({ components: [], embeds: [repliedMessage.embeds[0]] }); 86 | startGame(interaction, true); 87 | }, startTime * 1000); 88 | } 89 | } else if (interaction.customId.startsWith('join')) { 90 | // Destructure the custom ID into separate variables 91 | var [, number] = interaction.customId.split('_'); 92 | 93 | // Retrieve the saved game based on guild ID 94 | const savedGame = await Games.get(interaction.guildID); 95 | 96 | // If no game is found, send a reply indicating no game running in this server 97 | if (!savedGame) { 98 | interaction.reply({ content: 'No game is currently running in this server.', ephemeral: true }); 99 | return; 100 | } 101 | 102 | // Check if the user has already joined the game 103 | if (savedGame.players.some((user) => user.user == interaction.user.id)) { 104 | interaction.reply({ content: 'You have already joined. Please leave before joining again.', ephemeral: true }); 105 | return; 106 | } 107 | 108 | if (number == 'random') { 109 | // Get avalibe random button number 110 | do { 111 | number = Math.floor(Math.random() * 20) + 1; 112 | } while (savedGame.players.some((player) => player.buttonNumber == number)); 113 | } 114 | 115 | if (savedGame.players.some((user) => user.buttonNumber === number)) { 116 | interaction.reply({ content: 'Number already taken, please try again.', ephemeral: true }); 117 | return; 118 | } 119 | // Add the user to the game's players list with the corresponding button number 120 | savedGame.players.push({ 121 | user: interaction.user.id, 122 | buttonNumber: number, 123 | username: interaction.user.username, 124 | avatar: interaction.user.displayAvatarURL({ size: 256, extension: 'png' }), 125 | color: interaction.user.hexAccentColor, 126 | }); 127 | Games.set(interaction.guildId, savedGame); 128 | 129 | // Edit the button label and disable it with the user's username 130 | const updatedRow = editButton(interaction.message, savedGame.players); 131 | interaction.message.edit({ components: updatedRow.components }); 132 | 133 | // Send a confirmation message indicating successful joining 134 | interaction.reply({ content: 'Joined successfully!', ephemeral: true }); 135 | } else if (interaction.customId.startsWith('leave')) { 136 | // Retrieve the saved game based on guild ID 137 | const savedGame = await Games.get(interaction.guildID); 138 | 139 | // If no game is found, send a reply indicating no game running in this server 140 | if (!savedGame) { 141 | interaction.reply({ content: 'No game is currently running in this server.', ephemeral: true }); 142 | return; 143 | } 144 | 145 | // Check if the user has not joined the game 146 | if (!savedGame.players.some((user) => user.user == interaction.user.id)) { 147 | interaction.reply({ content: 'You have not joined the game.', ephemeral: true }); 148 | return; 149 | } 150 | 151 | // Find the user in the game and remove them from the players array 152 | const user = savedGame.players.find((user) => user.user == interaction.user.id); 153 | savedGame.players = savedGame.players.filter((user) => user.user != interaction.user.id); 154 | await Games.set(interaction.guildId, savedGame); 155 | 156 | // Edit the button label and disable it 157 | const updatedRow = editButton(interaction.message, savedGame.players, true, user); 158 | interaction.message.edit({ components: updatedRow.components }); 159 | 160 | // Send a reply indicating successful leave 161 | interaction.reply({ content: 'You have left the game.', ephemeral: true }); 162 | } else if (interaction.customId.startsWith('withdrawal')) { 163 | const savedGame = await Games.get(interaction.guildId); 164 | // If no game is found, send a reply indicating no game running in this server 165 | if (!savedGame) { 166 | interaction.reply({ content: 'No game is currently running in this server.', ephemeral: true }); 167 | return; 168 | } 169 | 170 | if (interaction.user.id != savedGame?.winner.id) { 171 | // Send a message indicating that the user is not the winner 172 | interaction.reply({ content: 'You are not the winner of the game, so you cannot perform this action.', ephemeral: true }); 173 | return; 174 | } 175 | if (Date.now() > savedGame.winner.until) { 176 | // Send a message indicating that the winner has missed their turn 177 | interaction.reply({ content: 'You have missed your turn.', ephemeral: true }); 178 | return; 179 | } 180 | // Remove the user from the game 181 | savedGame.players = savedGame.players.filter((player) => player.user != interaction.user.id); 182 | savedGame.winner.id = ''; 183 | 184 | await Games.set(interaction.guildId, savedGame); 185 | 186 | // Send a confirmation message that the user has withdrawn 187 | interaction.reply({ content: 'You have successfully withdrawn from the game.', ephemeral: true }); 188 | interaction.channel.send(`💣 | <@${interaction.user.id}> has withdrawn from the game, the next round will start in a few seconds...`); 189 | 190 | // Start the next round of the game 191 | startGame(interaction); 192 | } else if (interaction.customId.startsWith('kick_')) { 193 | const [, kickedUser] = interaction.customId.split('_'); 194 | 195 | const savedGame = await Games.get(interaction.guildId); 196 | // If no game is found, send a reply indicating no game running in this server 197 | if (!savedGame) { 198 | interaction.reply({ content: 'No game is currently running in this server.', ephemeral: true }); 199 | return; 200 | } 201 | 202 | if (interaction.user.id != savedGame?.winner.id) { 203 | // Send a message indicating that the user is not the winner 204 | interaction.reply({ content: 'You are not the winner of the game, so you cannot kick players.', ephemeral: true }); 205 | return; 206 | } 207 | if (Date.now() > savedGame.winner.until) { 208 | // Send a message indicating that the winner has missed their turn 209 | interaction.reply({ content: 'You have missed your turn.', ephemeral: true }); 210 | return; 211 | } 212 | savedGame.players = savedGame.players.filter((player) => player.user != kickedUser); 213 | savedGame.winner.id = ''; 214 | 215 | interaction.reply({ content: 'Player has been kicked from the game.', ephemeral: true }); 216 | interaction.channel.send(`💣 | <@${kickedUser}> has been kicked from the game, the next round will start in a few seconds...`); 217 | startGame(interaction); 218 | } 219 | }); 220 | 221 | /** 222 | * @param {CommandInteraction} interaction 223 | */ 224 | const startGame = async (interaction, start = false) => { 225 | const { players } = (await Games.get(interaction.guildId)) || { players: [] }; 226 | if (players.length == 0) { 227 | await sleep(5); 228 | // Send a message indicating that the game has been canceled due to no players 229 | interaction.channel.send({ content: ':x: Game canceled: There are no players.' }); 230 | return; 231 | } 232 | if (start) { 233 | await interaction.channel.send({ 234 | content: `✅ | Numbers were distributed to each player. The first round will start in a few seconds...`, 235 | }); 236 | } 237 | await sleep(timeBetweenRounds); 238 | const colorsGradient = ['#32517f', '#4876a3', '#5d8ec7', '#74a6eb', '#8ac0ff']; 239 | 240 | const options = players.map((user, index) => ({ 241 | user: user, 242 | label: user.username, 243 | color: colorsGradient[index % colorsGradient.length], 244 | })); 245 | 246 | const winnerOption = options[Math.floor(Math.random() * options.length)]; 247 | const winnerIndex = options.indexOf(winnerOption); 248 | options[winnerIndex] = { 249 | ...winnerOption, 250 | winner: true, 251 | }; 252 | 253 | const savedData = await Games.get(interaction.guildId); 254 | const time = Date.now() + chooseTimeout * 1000; 255 | savedData.winner = { id: winnerOption.user.user, until: time }; 256 | await Games.set(interaction.guildId, savedData); 257 | const image = await createWheel(options, winnerOption.user.avatar); 258 | 259 | const buttons = players 260 | .filter((user) => user.username != winnerOption.label) 261 | .map((user) => 262 | new ButtonBuilder() 263 | .setCustomId(`kick_${user.user}`) 264 | .setStyle(ButtonStyle.Secondary) 265 | .setLabel(user.username) 266 | .setEmoji(emojis[Number(user.buttonNumber) - 1]) 267 | ); 268 | 269 | const leaveButton = new ButtonBuilder().setCustomId(`withdrawal`).setLabel('Withdrawal').setStyle(ButtonStyle.Danger); 270 | 271 | const rows = createButtonRows([...buttons, leaveButton]); 272 | 273 | const attachment = new AttachmentBuilder(image, { name: 'wheel.png' }); 274 | 275 | if (players.length <= 2) { 276 | const embed = new EmbedBuilder() 277 | .setImage('attachment://wheel.png') 278 | .setColor('#4876a3') 279 | .setDescription(`**:crown: This is the last round! The chosen player is the winning player of the game.**`); 280 | await interaction.channel.send({ 281 | content: `**${winnerOption.user.buttonNumber} - <@${winnerOption.user.user}> **`, 282 | embeds: [embed], 283 | files: [attachment], 284 | }); 285 | await Games.delete(interaction.guildId); 286 | } else { 287 | const embed = new EmbedBuilder() 288 | .setImage('attachment://wheel.png') 289 | .setColor('#4876a3') 290 | .setDescription(`**⏰ | You have ${chooseTimeout} seconds to choose a player to send off**`); 291 | await interaction.channel.send({ 292 | content: `**${winnerOption.user.buttonNumber} - <@${winnerOption.user.user}> **`, 293 | embeds: [embed], 294 | files: [attachment], 295 | components: rows, 296 | }); 297 | setTimeout(async () => { 298 | const checkUser = await Games.get(interaction.guildId); 299 | if (checkUser?.winner.id == winnerOption.user.user && checkUser.winner.until == time) { 300 | checkUser.players = checkUser.players.filter((player) => player.user != winnerOption.user.user); 301 | checkUser.winner.id = ''; 302 | 303 | // Update the game state after removing the user 304 | await Games.set(interaction.guildId, checkUser); 305 | 306 | // Send a message to the channel indicating that the user has been kicked for timeout 307 | interaction.channel.send( 308 | `⏰ | <@${winnerOption.user.user}> has been kicked from the game due to timeout. The next round will start shortly...` 309 | ); 310 | 311 | // Start the next round of the game 312 | startGame(interaction); 313 | } 314 | }, chooseTimeout * 1000); 315 | } 316 | }; 317 | 318 | client.login(process.env.TOKEN); 319 | --------------------------------------------------------------------------------