import { ApplicationCommandOptionType } from 'discord.js'; import { sendPlainMessage } from '#utils/globalUtils/sendMessageUtils.js'; import { leaderboardGraveyard } from '#commands/subCommands/leaderboard/graveyard.js'; import { createLogger } from '#utils/globalUtils/logger.js'; const logger = createLogger('LeaderboardCommand'); export default { name: 'leaderboard', description: 'View leaderboards for different systems', options: [ { type: ApplicationCommandOptionType.Subcommand, name: 'graveyard', description: 'See the Graveyard leaderboard', }, ], /** * Executes the leaderboard command and delegates to the appropriate subcommand. * * @param {import('discord.js').Client} client - The Discord client instance * @param {import('discord.js').ChatInputCommandInteraction} interaction - The interaction object * @returns {Promise} */ async execute(client, interaction) { try { const sub = interaction.options.getSubcommand(); /** * Subcommand handlers mapping. * @type {Object>} */ const subcommandHandlers = { graveyard: leaderboardGraveyard, }; const handler = subcommandHandlers[sub]; if (handler) return handler(client, interaction); return sendPlainMessage( interaction, '❓ Unknown leaderboard. Try `/leaderboard graveyard`.' ); } catch (err) { logger.error( `Failed to execute /leaderboard in guild ${interaction.guild?.name} (${interaction.guildId}):`, err.message ); return sendPlainMessage( interaction, '📉 The records have scattered into the void… something went wrong while summoning the leaderboard.' ); } }, }; // from here the code is in subCommands foldeR: import { EmbedBuilder } from 'discord.js'; import GraveyardUserState from '#models/GraveyardUserState.js'; import { sendEmbedMessage, sendPlainMessage } from '#utils/globalUtils/sendMessageUtils.js'; import { getOrFetchGuild } from '#utils/globalUtils/getOrFetchGuild.js'; import { createLogger } from '#utils/globalUtils/logger.js'; import { COLOR_EMBEDS } from '#config'; const logger = createLogger('GraveyardLeaderboard'); /** * Converts milliseconds to hours (1 decimal place). * @param {number} ms * @returns {string} */ const formatMsToHrs = (ms) => (ms / 3600000).toFixed(1); /** * Formats a mention if the user exists, or shows "Unknown User" with their ID. * @param {import('discord.js').Client} client * @param {string} userId * @returns {Promise} */ const formatMention = async (client, userId) => { try { await client.users.fetch(userId); return `<@${userId}>`; } catch { return `Unknown User (\`${userId}\`)`; } }; /** * Formats a leaderboard list for display. * @param {Array} entries * @param {(entry: any) => string|number} getValFn * @param {string} unitLabel * @param {import('discord.js').Client} client * @returns {Promise} */ const formatList = async (entries, getValFn, unitLabel, client) => { return Promise.all( entries.map(async (entry, idx) => { const mention = await formatMention(client, entry.userId); const val = getValFn(entry); return `**#${idx + 1}** - ${mention}: ${val}${unitLabel}`; }) ); }; /** * Sends the graveyard leaderboard (Gravekeeper & Lingering Soul) to the user. * * @param {import('discord.js').Client} client - The Discord client * @param {import('discord.js').ChatInputCommandInteraction} interaction - The interaction object */ export async function leaderboardGraveyard(client, interaction) { await interaction.deferReply(); const guildId = interaction.guildId; let guild = interaction.guild; if (!guild || !guild.available) { guild = await getOrFetchGuild(client, guildId); if (!guild) { logger.warn(`Could not resolve guild ${guildId} for leaderboard command.`); return sendPlainMessage(interaction, '⚠️ Could not load guild info to display the leaderboard.'); } } try { const selectFields = 'userId stats'; const [gkByTime, gkByClaims, lsByTime, lsByClaims] = await Promise.all([ GraveyardUserState.find({ guildId }).select(selectFields).sort({ 'stats.totalGravekeeperTime': -1 }).limit(10).lean(), GraveyardUserState.find({ guildId }).select(selectFields).sort({ 'stats.gravekeeperClaims': -1 }).limit(10).lean(), GraveyardUserState.find({ guildId }).select(selectFields).sort({ 'stats.totalLingeringTime': -1 }).limit(10).lean(), GraveyardUserState.find({ guildId }).select(selectFields).sort({ 'stats.lingeringSoulClaims': -1 }).limit(10).lean(), ]); const gkTimeList = await formatList(gkByTime, (e) => formatMsToHrs(e.stats.totalGravekeeperTime), 'h', client); const gkClaimList = await formatList(gkByClaims, (e) => e.stats.gravekeeperClaims, 'x', client); const lsTimeList = await formatList(lsByTime, (e) => formatMsToHrs(e.stats.totalLingeringTime), 'h', client); const lsClaimList = await formatList(lsByClaims, (e) => e.stats.lingeringSoulClaims, 'x', client); const gkRole = guild.roles.cache.find((r) => r.name.toLowerCase().includes('gravekeeper')); const lsRole = guild.roles.cache.find((r) => r.name.toLowerCase().includes('lingering')); const gkColor = gkRole?.color || COLOR_EMBEDS.PRIMARY; const lsColor = lsRole?.color || COLOR_EMBEDS.INFO; const gkEmbed = new EmbedBuilder() .setColor(gkColor) .setTitle('🏆 Gravekeeper Leaderboard') .setDescription( `**⏱️ Time Held:**\n${gkTimeList.join('\n') || '*No data*'}\n\n` + `**🏅 Claims:**\n${gkClaimList.join('\n') || '*No data*'}` ); const lsEmbed = new EmbedBuilder() .setColor(lsColor) .setTitle('👻 Lingering Souls Leaderboard') .setDescription( `**⏱️ Time Lingering:**\n${lsTimeList.join('\n') || '*No data*'}\n\n` + `**🏅 Claims:**\n${lsClaimList.join('\n') || '*No data*'}` ) .setFooter({ text: guild.name, iconURL: guild.iconURL() }) .setTimestamp(); return sendEmbedMessage(interaction, [gkEmbed, lsEmbed], [], false); } catch (err) { logger.error(`Failed to send graveyard leaderboard for guild ${guild?.name ?? guildId} (${guildId}):`, err); return sendPlainMessage(interaction, '❌ Something went wrong while generating the leaderboard.'); } }