import { ActionRowBuilder, ButtonBuilder, ButtonStyle, ChannelType, ComponentType, EmbedBuilder, StringSelectMenuBuilder, type Client, type MessageCreateOptions } from "discord.js"; import { createLogger } from "@lbf-bot/utils"; import { env } from "~/env"; import { checkForNewQuest, type QuestResult } from "~/wov"; import { getAccountBalance, setAccountBalance } from "~/economy"; const logger = createLogger({ prefix: "quests" }); export const makeResultEmbed = async (result: QuestResult, exclude: Array): Promise => { const imageUrl = result.quest.promoImageUrl; const color = parseInt(result.quest.promoImagePrimaryColor.substring(1), 16); const participants = result.participants.toSorted((a, b) => b.xp - a.xp); let rewardsEmbed: EmbedBuilder | undefined; if (env.QUEST_REWARDS) { const rewarded = participants .map((x) => ({ id: x.playerId, username: x.username })) .filter((x) => !exclude.includes(x.username)); const medals = ["🥇", "🥈", "🥉"].concat(new Array(rewarded.length).fill("🏅")); const rewards = rewarded .slice(0, Math.min(rewarded.length, env.QUEST_REWARDS.length)) .map((x, i) => `- ${medals[i]} ${x.username} - ${env.QUEST_REWARDS![i]} ${env.QUEST_REWARDS_ARE_GEMS ? "gemmes" : ""}`); if (env.QUEST_REWARDS_ARE_GEMS) { const arr = rewarded.slice(0, Math.min(rewarded.length, env.QUEST_REWARDS.length)); for (let i = 0; i < arr.length; i++) { const balance = await getAccountBalance(arr[i].id); await setAccountBalance(arr[i].id, balance + parseInt(env.QUEST_REWARDS[i])); } } rewardsEmbed = new EmbedBuilder() .setTitle("Récompenses") .setDescription(rewards.join("\n")) .setColor(color); } return { content: `-# ||${env.DISCORD_MENTION}||`, embeds: [ new EmbedBuilder() .setDescription(`# Résultats de quête\n\nMerci à toutes et à tous d'avoir participé 🫡`) .setColor(color) .setImage(imageUrl), ...(rewardsEmbed ? [rewardsEmbed] : []), new EmbedBuilder() .setTitle("Classement") .setColor(color) .setDescription(participants .filter((x) => !exclude.includes(x.username) && x.xp > 0) .filter((_, i) => i < 8) .map((p, i) => `${i + 1}. ${p.username} - ${p.xp}xp`) .join("\n"), ), ], }; }; export const askForGrinders = async (quest: QuestResult, client: Client) => { const adminChannel = await client.channels.fetch(env.DISCORD_ADMIN_CHANNEL); if (!adminChannel || adminChannel.type !== ChannelType.GuildText) { return logger.fatal("Invalid 'DISCORD_ADMIN_CHANNEL'"); } let exclude: string[] = []; if (env.QUEST_REWARDS) { const color = parseInt(quest.quest.promoImagePrimaryColor.substring(1), 16); const top10 = quest.participants .filter((x) => !env.QUEST_EXCLUDE.includes(x.username) && x.xp > 0) .sort((a, b) => b.xp - a.xp) .slice(0, 10); if (!top10.length) { logger.error("No eligible participants for grinder selection"); return; } const selectOptions = top10 .map((p) => ({ label: `${p.username} (${p.xp}xp)`, value: p.username })); const row1 = new ActionRowBuilder().addComponents( new StringSelectMenuBuilder() .setCustomId("grinders") .setPlaceholder("Sélectionne les tricheurs") .setMinValues(0) .setMaxValues(selectOptions.length) .addOptions(selectOptions), ); const row2 = new ActionRowBuilder().addComponents( new ButtonBuilder() .setCustomId("validate") .setLabel("Valider") .setStyle(ButtonStyle.Primary), ); const top10Text = top10 .map((p, i) => `${i + 1}. ${p.username} - ${p.xp}xp`) .join("\n"); const msg = await adminChannel.send({ content: `-# ||${env.DISCORD_ADMIN_MENTION}||`, embeds: [ new EmbedBuilder() .setTitle("Quête terminée !") .setColor(color), new EmbedBuilder() .setTitle("Top 10 XP") .setDescription(top10Text) .setColor(color), new EmbedBuilder() .setTitle("Qui a grind ?") .setDescription("Sélectionne les joueurs qui ont grind (tricheurs en gros), puis clique sur **Valider**.") .setColor(color), ], components: [row1, row2], }); let grinders: string[] = []; let done = false; while (!done) { const interaction = await msg.awaitMessageComponent({ filter: (i) => !i.user.bot }); if (interaction.componentType === ComponentType.StringSelect) { grinders = interaction.values; await interaction.deferUpdate(); } else if (interaction.componentType === ComponentType.Button && interaction.customId === "validate") { await interaction.update({ components: [] }); done = true; } } exclude = grinders; } const embed = await makeResultEmbed(quest, [...env.QUEST_EXCLUDE, ...exclude]); const rewardChannel = await client.channels.fetch(env.DISCORD_REWARDS_CHANNEL); if (!rewardChannel || rewardChannel.type !== ChannelType.GuildText) { return logger.fatal("Invalid 'DISCORD_REWARDS_CHANNEL'"); } await rewardChannel.send(embed); if (env.QUEST_EXCLUDE) await adminChannel.send("Envoyé !"); logger.info(`Results posted at: ${new Date().toISOString()}`); }; export const questCheckCron = async (client: Client) => { logger.info("Checking for new quest"); const quest = await checkForNewQuest(); if (quest) { logger.info(`New quest found: '${quest.quest.id}'`); await askForGrinders(quest, client); } else { logger.info("No new quest found"); } };