From 9948bd32c860556421c7a6ad9da1a2ae221809d2 Mon Sep 17 00:00:00 2001 From: Ahmad <103906421+ahmadk953@users.noreply.github.com> Date: Tue, 24 Jun 2025 19:59:35 -0400 Subject: [PATCH] Revert "fix(bot): fixed achievement system" --- .github/FUNDING.yml | 1 + src/commands/fun/achievement.ts | 29 +-- src/db/functions/achievementFunctions.ts | 13 +- src/util/achievementManager.ts | 228 +++++++++++++++-------- 4 files changed, 164 insertions(+), 107 deletions(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 31791dc..01ce41d 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,2 +1,3 @@ github: ahmadk953 +patreon: poixpixel thanks_dev: u/gh/ahmadk953 diff --git a/src/commands/fun/achievement.ts b/src/commands/fun/achievement.ts index 5fea0f2..c1ef61b 100644 --- a/src/commands/fun/achievement.ts +++ b/src/commands/fun/achievement.ts @@ -26,6 +26,7 @@ const command = { data: new SlashCommandBuilder() .setName('achievement') .setDescription('Manage server achievements') + .setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild) .addSubcommand((subcommand) => subcommand .setName('create') @@ -184,13 +185,6 @@ async function handleCreateAchievement( const rewardType = interaction.options.getString('reward_type'); const rewardValue = interaction.options.getString('reward_value'); - if (!interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild)) { - await interaction.editReply( - 'You do not have permission to create achievements.', - ); - return; - } - if (requirementType === 'command_usage' && !commandName) { await interaction.editReply( 'Command name is required for command_usage type achievements.', @@ -258,13 +252,6 @@ async function handleDeleteAchievement( ) { const achievementId = interaction.options.getInteger('id')!; - if (!interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild)) { - await interaction.editReply( - 'You do not have permission to delete achievements.', - ); - return; - } - try { const success = await deleteAchievement(achievementId); @@ -291,13 +278,6 @@ async function handleAwardAchievement( const user = interaction.options.getUser('user')!; const achievementId = interaction.options.getInteger('achievement_id')!; - if (!interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild)) { - await interaction.editReply( - 'You do not have permission to award achievements.', - ); - return; - } - try { const allAchievements = await getAllAchievements(); const achievement = allAchievements.find((a) => a.id === achievementId); @@ -679,13 +659,6 @@ async function handleUnawardAchievement( const user = interaction.options.getUser('user')!; const achievementId = interaction.options.getInteger('achievement_id')!; - if (!interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild)) { - await interaction.editReply( - 'You do not have permission to unaward achievements.', - ); - return; - } - try { const allAchievements = await getAllAchievements(); const achievement = allAchievements.find((a) => a.id === achievementId); diff --git a/src/db/functions/achievementFunctions.ts b/src/db/functions/achievementFunctions.ts index 7d3ee6a..30cbc3b 100644 --- a/src/db/functions/achievementFunctions.ts +++ b/src/db/functions/achievementFunctions.ts @@ -129,6 +129,7 @@ export async function updateAchievementProgress( ): Promise { try { await ensureDbInitialized(); + if (!db) { console.error( 'Database not initialized, cannot update achievement progress', @@ -148,15 +149,21 @@ export async function updateAchievementProgress( .then((rows) => rows[0]); if (existing) { + if (existing.earnedAt) { + return false; + } + await db .update(schema.userAchievementsTable) - .set({ progress }) + .set({ + progress: Math.floor(progress) > 100 ? 100 : Math.floor(progress), + }) .where(eq(schema.userAchievementsTable.id, existing.id)); } else { await db.insert(schema.userAchievementsTable).values({ discordId: userId, - achievementId, - progress, + achievementId: achievementId, + progress: Math.floor(progress) > 100 ? 100 : Math.floor(progress), }); } diff --git a/src/util/achievementManager.ts b/src/util/achievementManager.ts index aaf3a05..adb78d9 100644 --- a/src/util/achievementManager.ts +++ b/src/util/achievementManager.ts @@ -1,4 +1,11 @@ -import { Message, EmbedBuilder, TextChannel, Guild } from 'discord.js'; +import { + Message, + Client, + EmbedBuilder, + GuildMember, + TextChannel, + Guild, +} from 'discord.js'; import { addXpToUser, @@ -14,63 +21,99 @@ import { loadConfig } from './configLoader.js'; import { generateAchievementCard } from './achievementCardGenerator.js'; /** - * Handle achievement progress updates - * @param userId - ID of the user - * @param guild - Guild instance (can be null if not applicable) - * @param achievement - Achievement definition - * @param progress - Progress percentage (0-100) - * @param options - Additional options - */ -async function handleProgress( - userId: string, - guild: Guild | null, - achievement: schema.achievementDefinitionsTableTypes, - progress: number, - options: { skipAward?: boolean } = {}, -): Promise { - const { skipAward = false } = options; - const userAchievements = await getUserAchievements(userId); - const existing = userAchievements.find( - (a) => a.achievementId === achievement.id && a.earnedAt !== null, - ); - - await updateAchievementProgress(userId, achievement.id, progress); - - if (progress === 100 && !existing && !skipAward) { - const awarded = await awardAchievement(userId, achievement.id); - if (awarded && guild) { - await announceAchievement(guild, userId, achievement); - } - } -} - -/** - * Process message achievements based on user activity - * @param message - The message object from Discord + * Check and process achievements for a user based on a message + * @param message - The message that triggered the check */ export async function processMessageAchievements( message: Message, ): Promise { if (message.author.bot) return; + const userData = await getUserLevel(message.author.id); const allAchievements = await getAllAchievements(); - for (const ach of allAchievements.filter( + const messageAchievements = allAchievements.filter( (a) => a.requirementType === 'message_count', - )) { + ); + + for (const achievement of messageAchievements) { const progress = Math.min( 100, - (userData.messagesSent / ach.threshold) * 100, + (userData.messagesSent / achievement.threshold) * 100, ); - await handleProgress(message.author.id, message.guild!, ach, progress); + + if (progress >= 100) { + const userAchievements = await getUserAchievements(message.author.id); + const existingAchievement = userAchievements.find( + (a) => a.achievementId === achievement.id && a.earnedAt !== null, + ); + + if (!existingAchievement) { + const awarded = await awardAchievement( + message.author.id, + achievement.id, + ); + if (awarded) { + await announceAchievement( + message.guild!, + message.author.id, + achievement, + ); + } + } + } else { + await updateAchievementProgress( + message.author.id, + achievement.id, + progress, + ); + } + } + + const levelAchievements = allAchievements.filter( + (a) => a.requirementType === 'level', + ); + + for (const achievement of levelAchievements) { + const progress = Math.min( + 100, + (userData.level / achievement.threshold) * 100, + ); + + if (progress >= 100) { + const userAchievements = await getUserAchievements(message.author.id); + const existingAchievement = userAchievements.find( + (a) => a.achievementId === achievement.id && a.earnedAt !== null, + ); + + if (!existingAchievement) { + const awarded = await awardAchievement( + message.author.id, + achievement.id, + ); + if (awarded) { + await announceAchievement( + message.guild!, + message.author.id, + achievement, + ); + } + } + } else { + await updateAchievementProgress( + message.author.id, + achievement.id, + progress, + ); + } } } /** - * Process level-up achievements when a user levels up - * @param memberId - ID of the member who leveled up - * @param newLevel - The new level the member has reached - * @param guild - Guild instance where the member belongs + * Check achievements for level-ups + * @param memberId - Member ID who leveled up + * @param newLevel - New level value + * @guild - Guild instance */ export async function processLevelUpAchievements( memberId: string, @@ -78,19 +121,37 @@ export async function processLevelUpAchievements( guild: Guild, ): Promise { const allAchievements = await getAllAchievements(); - for (const ach of allAchievements.filter( + + const levelAchievements = allAchievements.filter( (a) => a.requirementType === 'level', - )) { - const progress = Math.min(100, (newLevel / ach.threshold) * 100); - await handleProgress(memberId, guild, ach, progress); + ); + + for (const achievement of levelAchievements) { + const progress = Math.min(100, (newLevel / achievement.threshold) * 100); + + if (progress >= 100) { + const userAchievements = await getUserAchievements(memberId); + const existingAchievement = userAchievements.find( + (a) => a.achievementId === achievement.id && a.earnedAt !== null, + ); + + if (!existingAchievement) { + const awarded = await awardAchievement(memberId, achievement.id); + if (awarded) { + await announceAchievement(guild, memberId, achievement); + } + } + } else { + await updateAchievementProgress(memberId, achievement.id, progress); + } } } /** - * Process command usage achievements when a command is invoked - * @param userId - ID of the user who invoked the command - * @param commandName - Name of the command invoked - * @param guild - Guild instance where the command was invoked + * Process achievements for command usage + * @param userId - User ID who used the command + * @param commandName - Name of the command + * @param client - Guild instance */ export async function processCommandAchievements( userId: string, @@ -98,6 +159,7 @@ export async function processCommandAchievements( guild: Guild, ): Promise { const allAchievements = await getAllAchievements(); + const commandAchievements = allAchievements.filter( (a) => a.requirementType === 'command_usage' && @@ -105,31 +167,26 @@ export async function processCommandAchievements( (a.requirement as any).command === commandName, ); - // fetch the user’s current achievement entries - const userAchievements = await getUserAchievements(userId); + for (const achievement of commandAchievements) { + const userAchievements = await getUserAchievements(userId); + const existingAchievement = userAchievements.find( + (a) => a.achievementId === achievement.id && a.earnedAt !== null, + ); - for (const ach of commandAchievements) { - // find existing progress, default to 0 - const userAch = userAchievements.find((u) => u.achievementId === ach.id); - const oldProgress = userAch?.progress ?? 0; - - // compute how many times they've run this command so far - const timesRanSoFar = (oldProgress / 100) * ach.threshold; - const newCount = timesRanSoFar + 1; - - // convert back into a percentage - const newProgress = Math.min(100, (newCount / ach.threshold) * 100); - - // Delegate to handleProgress which will update or award - await handleProgress(userId, guild, ach, newProgress); + if (!existingAchievement) { + const awarded = await awardAchievement(userId, achievement.id); + if (awarded) { + await announceAchievement(guild, userId, achievement); + } + } } } /** - * Process reaction achievements when a user reacts to a message - * @param userId - ID of the user who reacted - * @param guild - Guild instance where the reaction occurred - * @param isRemoval - Whether the reaction was removed (default: false) + * Process achievements for reaction events (add or remove) + * @param userId - User ID who added/removed the reaction + * @param guild - Guild instance + * @param isRemoval - Whether this is a reaction removal (true) or addition (false) */ export async function processReactionAchievements( userId: string, @@ -141,21 +198,40 @@ export async function processReactionAchievements( if (member.user.bot) return; const allAchievements = await getAllAchievements(); + const reactionAchievements = allAchievements.filter( (a) => a.requirementType === 'reactions', ); - if (!reactionAchievements.length) return; + + if (reactionAchievements.length === 0) return; const reactionCount = await getUserReactionCount(userId); - for (const ach of reactionAchievements) { + for (const achievement of reactionAchievements) { const progress = Math.max( 0, - Math.min(100, (reactionCount / ach.threshold) * 100), + Math.min(100, (reactionCount / achievement.threshold) * 100), ); - await handleProgress(userId, guild, ach, progress, { - skipAward: isRemoval, - }); + + if (progress >= 100 && !isRemoval) { + const userAchievements = await getUserAchievements(userId); + const existingAchievement = userAchievements.find( + (a) => + a.achievementId === achievement.id && + a.earnedAt !== null && + a.earnedAt !== undefined && + new Date(a.earnedAt).getTime() > 0, + ); + + if (!existingAchievement) { + const awarded = await awardAchievement(userId, achievement.id); + if (awarded) { + await announceAchievement(guild, userId, achievement); + } + } + } + + await updateAchievementProgress(userId, achievement.id, progress); } } catch (error) { console.error('Error processing reaction achievements:', error);