From ad6642e3b060b5b33d4fa56ec909a583d1fdda85 Mon Sep 17 00:00:00 2001 From: Fretux Date: Mon, 26 Jan 2026 19:15:43 +0100 Subject: [PATCH 1/2] Enhance quiz with choices and rewards --- README.md | 4 +- commands/unique/quiz.js | 142 ++++++++++++++++++++++++++++++++------ commands/unique/quiz.json | 18 ++++- 3 files changed, 139 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index d8ddd1a..251a4fc 100644 --- a/README.md +++ b/README.md @@ -40,8 +40,8 @@ Im going to make a To-do List for the features i want to implement for my Bot. 3. **Minigames** - [ ] Implement a Quiz Game: - [x] Add a `/quiz` command to start the game. - - [ ] Provide multiple-choice or true/false questions. - - [ ] Award virtual currency for correct answers. + - [x] Provide multiple-choice or true/false questions. + - [x] Award virtual currency for correct answers. - [ ] Add a simple number-guessing game (e.g., `!guess [number]`). - [ ] Create a leaderboard for game winners. diff --git a/commands/unique/quiz.js b/commands/unique/quiz.js index 7bbccab..d8e0530 100644 --- a/commands/unique/quiz.js +++ b/commands/unique/quiz.js @@ -1,43 +1,145 @@ const quiz = require('./quiz.json'); -const {SlashCommandBuilder} = require("discord.js"); +const { + SlashCommandBuilder, + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + EmbedBuilder, +} = require('discord.js'); +const { connectToDatabase } = require('../../db.js'); + +const QUIZ_REWARD = 50; +const QUIZ_TIMEOUT_MS = 30000; + +const normalizeAnswer = (answer) => answer.trim().toLowerCase(); + +const buildRows = (choices) => { + const rows = []; + for (let i = 0; i < choices.length; i += 5) { + const slice = choices.slice(i, i + 5); + const row = new ActionRowBuilder().addComponents( + slice.map((choice, index) => + new ButtonBuilder() + .setCustomId(`quiz_choice_${i + index}`) + .setLabel(choice) + .setStyle(ButtonStyle.Primary) + ) + ); + rows.push(row); + } + return rows; +}; + +const disableRows = (rows) => + rows.map((row) => { + const disabledRow = ActionRowBuilder.from(row); + disabledRow.components = disabledRow.components.map((component) => + ButtonBuilder.from(component).setDisabled(true) + ); + return disabledRow; + }); + +const awardReward = async (userId) => { + const dbClient = await connectToDatabase(); + const db = dbClient.db('discord'); + const collection = db.collection('currency'); + + await collection.updateOne( + { userID: userId }, + { $inc: { balance: QUIZ_REWARD } }, + { upsert: true } + ); +}; module.exports = { data: new SlashCommandBuilder() - .setName("quiz") - .setDescription("Start a cool quiz"), + .setName('quiz') + .setDescription('Start a cool quiz'), category: 'Games', async execute(interaction) { const item = quiz[Math.floor(Math.random() * quiz.length)]; - await interaction.reply({ - content: `**Quiz Time!**\n${item.question}\n\n_You have 30 seconds to answer!_`, - }); + const answers = (item.answers ?? []) + .map(normalizeAnswer) + .filter(Boolean); + const hasChoices = Array.isArray(item.choices) && item.choices.length > 0; - // Define a filter to check the correct answer - const collectorFilter = (response) => { - return ( - response.author.id === interaction.user.id && // Ensure it's the same user - item.answers.some( - (answer) => answer.toLowerCase() === response.content.toLowerCase() - ) - ); - }; + const embed = new EmbedBuilder() + .setColor(0xf7b731) + .setTitle('Quiz Time!') + .setDescription(item.question) + .setFooter({ text: 'You have 30 seconds to answer.' }); + + if (!interaction.channel) { + return interaction.reply({ + content: 'I need a channel to run the quiz.', + ephemeral: true, + }); + } + + if (hasChoices) { + const choices = item.choices; + const rows = buildRows(choices); + const reply = await interaction.reply({ + embeds: [embed], + components: rows, + fetchReply: true, + }); + + try { + const selection = await reply.awaitMessageComponent({ + filter: (component) => component.user.id === interaction.user.id, + time: QUIZ_TIMEOUT_MS, + }); + + const choiceIndex = Number(selection.customId.split('_').pop()); + const selected = choices[choiceIndex]; + const isCorrect = answers.includes(normalizeAnswer(selected)); + + const resultText = isCorrect + ? `${selection.user} nailed it! **${selected}** is correct. +${QUIZ_REWARD} coins! 🎉` + : `Nice try! The correct answer was **${item.answers[0]}**.`; + + if (isCorrect) { + await awardReward(selection.user.id); + } + + await selection.update({ + content: resultText, + embeds: [], + components: disableRows(rows), + }); + } catch (err) { + await interaction.editReply({ + content: `⏳ Time's up! The correct answer was **${item.answers[0]}**.`, + embeds: [], + components: disableRows(rows), + }); + } + + return; + } + + await interaction.reply({ embeds: [embed] }); + + const collectorFilter = (response) => + response.author.id === interaction.user.id && + answers.includes(normalizeAnswer(response.content)); try { - // Await messages in the same channel const collected = await interaction.channel.awaitMessages({ filter: collectorFilter, max: 1, - time: 30000, // 30 seconds + time: QUIZ_TIMEOUT_MS, errors: ['time'], }); - // If a message is collected, reply with the correct response const correctAnswer = collected.first(); + await awardReward(correctAnswer.author.id); + await interaction.followUp( - `${correctAnswer.author} got the correct answer: **${correctAnswer.content}**! 🎉` + `${correctAnswer.author} got the correct answer: **${correctAnswer.content}**! +${QUIZ_REWARD} coins! 🎉` ); } catch (err) { - // If no one answers in time await interaction.followUp( `⏳ Time's up! Nobody got the correct answer. The correct answers were: **${item.answers.join(', ')}**.` ); diff --git a/commands/unique/quiz.json b/commands/unique/quiz.json index 260f757..08b5d40 100644 --- a/commands/unique/quiz.json +++ b/commands/unique/quiz.json @@ -1,10 +1,22 @@ [ { - "question": "What color is the sky?", + "question": "What color is the clear daytime sky?", + "choices": ["Blue", "Green", "Red", "Yellow"], "answers": ["blue"] }, { - "question": "How many letters are there in the alphabet?", - "answers": ["26", "twenty-six", "twenty six", "twentysix"] + "question": "How many letters are there in the English alphabet?", + "choices": ["24", "25", "26", "27"], + "answers": ["26"] + }, + { + "question": "True or False: The Earth orbits the Sun.", + "choices": ["True", "False"], + "answers": ["true"] + }, + { + "question": "Which planet is known as the Red Planet?", + "choices": ["Mars", "Venus", "Jupiter", "Mercury"], + "answers": ["mars"] } ] From aba21131b46a9aab0fb24b97b478e7fef2ada3ff Mon Sep 17 00:00:00 2001 From: Fretux Date: Tue, 27 Jan 2026 09:03:44 +0100 Subject: [PATCH 2/2] Add utility commands for uptime avatar and roll --- commands/utility/avatar.js | 25 ++++++++++++++++++ commands/utility/roll.js | 54 ++++++++++++++++++++++++++++++++++++++ commands/utility/uptime.js | 34 ++++++++++++++++++++++++ 3 files changed, 113 insertions(+) create mode 100644 commands/utility/avatar.js create mode 100644 commands/utility/roll.js create mode 100644 commands/utility/uptime.js diff --git a/commands/utility/avatar.js b/commands/utility/avatar.js new file mode 100644 index 0000000..b598cd3 --- /dev/null +++ b/commands/utility/avatar.js @@ -0,0 +1,25 @@ +const { SlashCommandBuilder, EmbedBuilder } = require('discord.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('avatar') + .setDescription('Display a user\'s avatar in full size.') + .addUserOption((option) => + option + .setName('user') + .setDescription('The user to fetch the avatar for.') + ), + category: 'Utility', + async execute(interaction) { + const targetUser = interaction.options.getUser('user') ?? interaction.user; + const avatarUrl = targetUser.displayAvatarURL({ size: 1024, extension: 'png' }); + + const embed = new EmbedBuilder() + .setColor(0xa55eea) + .setTitle(`${targetUser.username}'s Avatar`) + .setImage(avatarUrl) + .setTimestamp(); + + await interaction.reply({ embeds: [embed] }); + }, +}; diff --git a/commands/utility/roll.js b/commands/utility/roll.js new file mode 100644 index 0000000..b894056 --- /dev/null +++ b/commands/utility/roll.js @@ -0,0 +1,54 @@ +const { SlashCommandBuilder, EmbedBuilder } = require('discord.js'); + +const clamp = (value, min, max) => Math.min(Math.max(value, min), max); + +const rollDice = (count, sides) => { + const rolls = []; + for (let i = 0; i < count; i += 1) { + rolls.push(1 + Math.floor(Math.random() * sides)); + } + return rolls; +}; + +module.exports = { + data: new SlashCommandBuilder() + .setName('roll') + .setDescription('Roll one or more dice with a custom number of sides.') + .addIntegerOption((option) => + option + .setName('sides') + .setDescription('Number of sides on each die (2-100).') + .setMinValue(2) + .setMaxValue(100) + ) + .addIntegerOption((option) => + option + .setName('count') + .setDescription('How many dice to roll (1-6).') + .setMinValue(1) + .setMaxValue(6) + ), + category: 'Utility', + async execute(interaction) { + const sidesInput = interaction.options.getInteger('sides') ?? 6; + const countInput = interaction.options.getInteger('count') ?? 1; + + const sides = clamp(sidesInput, 2, 100); + const count = clamp(countInput, 1, 6); + const rolls = rollDice(count, sides); + const total = rolls.reduce((sum, roll) => sum + roll, 0); + + const embed = new EmbedBuilder() + .setColor(0x20bf6b) + .setTitle('Dice Roll') + .addFields( + { name: 'Sides', value: `${sides}`, inline: true }, + { name: 'Dice', value: `${count}`, inline: true }, + { name: 'Total', value: `${total}`, inline: true }, + { name: 'Rolls', value: rolls.join(', ') } + ) + .setTimestamp(); + + await interaction.reply({ embeds: [embed] }); + }, +}; diff --git a/commands/utility/uptime.js b/commands/utility/uptime.js new file mode 100644 index 0000000..e71c704 --- /dev/null +++ b/commands/utility/uptime.js @@ -0,0 +1,34 @@ +const { SlashCommandBuilder, EmbedBuilder } = require('discord.js'); + +const formatDuration = (totalSeconds) => { + const seconds = Math.floor(totalSeconds % 60); + const minutes = Math.floor((totalSeconds / 60) % 60); + const hours = Math.floor((totalSeconds / 3600) % 24); + const days = Math.floor(totalSeconds / 86400); + + const parts = [ + days ? `${days}d` : null, + hours ? `${hours}h` : null, + minutes ? `${minutes}m` : null, + `${seconds}s`, + ].filter(Boolean); + + return parts.join(' '); +}; + +module.exports = { + data: new SlashCommandBuilder() + .setName('uptime') + .setDescription('Display how long the bot has been running.'), + category: 'Utility', + async execute(interaction) { + const uptimeSeconds = process.uptime(); + const embed = new EmbedBuilder() + .setColor(0x45aaf2) + .setTitle('Bot Uptime') + .setDescription(`Online for **${formatDuration(uptimeSeconds)}**.`) + .setTimestamp(); + + await interaction.reply({ embeds: [embed], ephemeral: true }); + }, +};