mirror of
https://github.com/ahmadk953/poixpixel-discord-bot.git
synced 2025-07-05 03:46:00 +00:00
feat: add achievement system
Signed-off-by: Ahmad <103906421+ahmadk953@users.noreply.github.com>
This commit is contained in:
parent
830838a6a1
commit
2f5c3499e7
15 changed files with 1966 additions and 37 deletions
303
src/util/achievementManager.ts
Normal file
303
src/util/achievementManager.ts
Normal file
|
@ -0,0 +1,303 @@
|
|||
import {
|
||||
Message,
|
||||
Client,
|
||||
EmbedBuilder,
|
||||
GuildMember,
|
||||
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';
|
||||
|
||||
/**
|
||||
* 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<void> {
|
||||
if (message.author.bot) return;
|
||||
|
||||
const userData = await getUserLevel(message.author.id);
|
||||
const allAchievements = await getAllAchievements();
|
||||
|
||||
const messageAchievements = allAchievements.filter(
|
||||
(a) => a.requirementType === 'message_count',
|
||||
);
|
||||
|
||||
for (const achievement of messageAchievements) {
|
||||
const progress = Math.min(
|
||||
100,
|
||||
(userData.messagesSent / 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
newLevel: number,
|
||||
guild: Guild,
|
||||
): Promise<void> {
|
||||
const allAchievements = await getAllAchievements();
|
||||
|
||||
const levelAchievements = allAchievements.filter(
|
||||
(a) => a.requirementType === 'level',
|
||||
);
|
||||
|
||||
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 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,
|
||||
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,
|
||||
);
|
||||
|
||||
for (const achievement of commandAchievements) {
|
||||
const userAchievements = await getUserAchievements(userId);
|
||||
const existingAchievement = userAchievements.find(
|
||||
(a) => a.achievementId === achievement.id && a.earnedAt !== null,
|
||||
);
|
||||
|
||||
if (!existingAchievement) {
|
||||
const awarded = await awardAchievement(userId, achievement.id);
|
||||
if (awarded) {
|
||||
await announceAchievement(guild, userId, achievement);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
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 === 0) return;
|
||||
|
||||
const reactionCount = await getUserReactionCount(userId);
|
||||
|
||||
for (const achievement of reactionAchievements) {
|
||||
const progress = Math.max(
|
||||
0,
|
||||
Math.min(100, (reactionCount / achievement.threshold) * 100),
|
||||
);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue