mirror of
https://github.com/ahmadk953/poixpixel-discord-bot.git
synced 2025-07-04 19:36:00 +00:00
227 lines
6.9 KiB
TypeScript
227 lines
6.9 KiB
TypeScript
import { Message, EmbedBuilder, TextChannel, Guild } from 'discord.js';
|
||
|
||
import {
|
||
addXpToUser,
|
||
awardAchievement,
|
||
getAllAchievements,
|
||
getUserAchievements,
|
||
getUserLevel,
|
||
getUserReactionCount,
|
||
updateAchievementProgress,
|
||
} from '@/db/db.js';
|
||
import * as schema from '@/db/schema.js';
|
||
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<void> {
|
||
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
|
||
*/
|
||
export async function processMessageAchievements(
|
||
message: Message,
|
||
): Promise<void> {
|
||
if (message.author.bot) return;
|
||
const userData = await getUserLevel(message.author.id);
|
||
const allAchievements = await getAllAchievements();
|
||
|
||
for (const ach of allAchievements.filter(
|
||
(a) => a.requirementType === 'message_count',
|
||
)) {
|
||
const progress = Math.min(
|
||
100,
|
||
(userData.messagesSent / ach.threshold) * 100,
|
||
);
|
||
await handleProgress(message.author.id, message.guild!, ach, 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
|
||
*/
|
||
export async function processLevelUpAchievements(
|
||
memberId: string,
|
||
newLevel: number,
|
||
guild: Guild,
|
||
): Promise<void> {
|
||
const allAchievements = await getAllAchievements();
|
||
for (const ach of allAchievements.filter(
|
||
(a) => a.requirementType === 'level',
|
||
)) {
|
||
const progress = Math.min(100, (newLevel / ach.threshold) * 100);
|
||
await handleProgress(memberId, guild, ach, 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
|
||
*/
|
||
export async function processCommandAchievements(
|
||
userId: string,
|
||
commandName: string,
|
||
guild: Guild,
|
||
): Promise<void> {
|
||
const allAchievements = await getAllAchievements();
|
||
const commandAchievements = allAchievements.filter(
|
||
(a) =>
|
||
a.requirementType === 'command_usage' &&
|
||
a.requirement &&
|
||
(a.requirement as any).command === commandName,
|
||
);
|
||
|
||
// fetch the user’s current achievement entries
|
||
const userAchievements = await getUserAchievements(userId);
|
||
|
||
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);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 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)
|
||
*/
|
||
export async function processReactionAchievements(
|
||
userId: string,
|
||
guild: Guild,
|
||
isRemoval: boolean = false,
|
||
): Promise<void> {
|
||
try {
|
||
const member = await guild.members.fetch(userId);
|
||
if (member.user.bot) return;
|
||
|
||
const allAchievements = await getAllAchievements();
|
||
const reactionAchievements = allAchievements.filter(
|
||
(a) => a.requirementType === 'reactions',
|
||
);
|
||
if (!reactionAchievements.length) return;
|
||
|
||
const reactionCount = await getUserReactionCount(userId);
|
||
|
||
for (const ach of reactionAchievements) {
|
||
const progress = Math.max(
|
||
0,
|
||
Math.min(100, (reactionCount / ach.threshold) * 100),
|
||
);
|
||
await handleProgress(userId, guild, ach, progress, {
|
||
skipAward: isRemoval,
|
||
});
|
||
}
|
||
} catch (error) {
|
||
console.error('Error processing reaction achievements:', error);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Announce a newly earned achievement
|
||
* @param guild - Guild instance
|
||
* @param userId - ID of the user who earned the achievement
|
||
* @param achievement - Achievement definition
|
||
*/
|
||
export async function announceAchievement(
|
||
guild: Guild,
|
||
userId: string,
|
||
achievement: schema.achievementDefinitionsTableTypes,
|
||
): Promise<void> {
|
||
try {
|
||
const config = loadConfig();
|
||
|
||
if (!guild) {
|
||
console.error(`Guild ${guild} not found`);
|
||
return;
|
||
}
|
||
|
||
const member = await guild.members.fetch(userId);
|
||
if (!member) {
|
||
console.warn(`Member ${userId} not found in guild`);
|
||
return;
|
||
}
|
||
|
||
const achievementCard = await generateAchievementCard(achievement);
|
||
|
||
const embed = new EmbedBuilder()
|
||
.setColor(0xffd700)
|
||
.setDescription(
|
||
`**${member.user.username}** just unlocked the achievement: **${achievement.name}**! 🎉`,
|
||
)
|
||
.setImage('attachment://achievement.png')
|
||
.setTimestamp();
|
||
|
||
const advChannel = guild.channels.cache.get(config.channels.advancements);
|
||
if (advChannel?.isTextBased()) {
|
||
await (advChannel as TextChannel).send({
|
||
content: `Congratulations <@${userId}>!`,
|
||
embeds: [embed],
|
||
files: [achievementCard],
|
||
});
|
||
}
|
||
|
||
if (achievement.rewardType === 'xp' && achievement.rewardValue) {
|
||
const xpAmount = parseInt(achievement.rewardValue);
|
||
if (!isNaN(xpAmount)) {
|
||
await addXpToUser(userId, xpAmount);
|
||
}
|
||
} else if (achievement.rewardType === 'role' && achievement.rewardValue) {
|
||
try {
|
||
await member.roles.add(achievement.rewardValue);
|
||
} catch (err) {
|
||
console.error(
|
||
`Failed to add role ${achievement.rewardValue} to user ${userId}`,
|
||
err,
|
||
);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('Error announcing achievement:', error);
|
||
}
|
||
}
|