Files
lbf-bot/apps/discord-bot/src/quests.ts
Pihkaal 4ce3e97727
Some checks failed
Build and Deploy / typecheck (push) Successful in 1m27s
Build and Deploy / deploy (push) Has been cancelled
Build and Deploy / build (push) Has been cancelled
feat(discord-bot): remove old gems rewards message
2026-06-01 21:58:02 +02:00

161 lines
6.3 KiB
TypeScript

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<string>): Promise<MessageCreateOptions> => {
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<StringSelectMenuBuilder>().addComponents(
new StringSelectMenuBuilder()
.setCustomId("grinders")
.setPlaceholder("Sélectionne les tricheurs")
.setMinValues(0)
.setMaxValues(selectOptions.length)
.addOptions(selectOptions),
);
const row2 = new ActionRowBuilder<ButtonBuilder>().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");
}
};