mirror of
https://github.com/ahmadk953/poixpixel-discord-bot.git
synced 2025-07-04 19:36:00 +00:00
fix(bot): achievement system fixes and improvements
This commit is contained in:
commit
8d14616f39
4 changed files with 113 additions and 170 deletions
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
|
@ -1,3 +1,2 @@
|
||||||
github: ahmadk953
|
github: ahmadk953
|
||||||
patreon: poixpixel
|
|
||||||
thanks_dev: u/gh/ahmadk953
|
thanks_dev: u/gh/ahmadk953
|
||||||
|
|
|
@ -26,7 +26,6 @@ const command = {
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
.setName('achievement')
|
.setName('achievement')
|
||||||
.setDescription('Manage server achievements')
|
.setDescription('Manage server achievements')
|
||||||
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild)
|
|
||||||
.addSubcommand((subcommand) =>
|
.addSubcommand((subcommand) =>
|
||||||
subcommand
|
subcommand
|
||||||
.setName('create')
|
.setName('create')
|
||||||
|
@ -185,6 +184,13 @@ async function handleCreateAchievement(
|
||||||
const rewardType = interaction.options.getString('reward_type');
|
const rewardType = interaction.options.getString('reward_type');
|
||||||
const rewardValue = interaction.options.getString('reward_value');
|
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) {
|
if (requirementType === 'command_usage' && !commandName) {
|
||||||
await interaction.editReply(
|
await interaction.editReply(
|
||||||
'Command name is required for command_usage type achievements.',
|
'Command name is required for command_usage type achievements.',
|
||||||
|
@ -252,6 +258,13 @@ async function handleDeleteAchievement(
|
||||||
) {
|
) {
|
||||||
const achievementId = interaction.options.getInteger('id')!;
|
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 {
|
try {
|
||||||
const success = await deleteAchievement(achievementId);
|
const success = await deleteAchievement(achievementId);
|
||||||
|
|
||||||
|
@ -278,6 +291,13 @@ async function handleAwardAchievement(
|
||||||
const user = interaction.options.getUser('user')!;
|
const user = interaction.options.getUser('user')!;
|
||||||
const achievementId = interaction.options.getInteger('achievement_id')!;
|
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 {
|
try {
|
||||||
const allAchievements = await getAllAchievements();
|
const allAchievements = await getAllAchievements();
|
||||||
const achievement = allAchievements.find((a) => a.id === achievementId);
|
const achievement = allAchievements.find((a) => a.id === achievementId);
|
||||||
|
@ -659,6 +679,13 @@ async function handleUnawardAchievement(
|
||||||
const user = interaction.options.getUser('user')!;
|
const user = interaction.options.getUser('user')!;
|
||||||
const achievementId = interaction.options.getInteger('achievement_id')!;
|
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 {
|
try {
|
||||||
const allAchievements = await getAllAchievements();
|
const allAchievements = await getAllAchievements();
|
||||||
const achievement = allAchievements.find((a) => a.id === achievementId);
|
const achievement = allAchievements.find((a) => a.id === achievementId);
|
||||||
|
|
|
@ -129,7 +129,6 @@ export async function updateAchievementProgress(
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
await ensureDbInitialized();
|
await ensureDbInitialized();
|
||||||
|
|
||||||
if (!db) {
|
if (!db) {
|
||||||
console.error(
|
console.error(
|
||||||
'Database not initialized, cannot update achievement progress',
|
'Database not initialized, cannot update achievement progress',
|
||||||
|
@ -149,21 +148,15 @@ export async function updateAchievementProgress(
|
||||||
.then((rows) => rows[0]);
|
.then((rows) => rows[0]);
|
||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
if (existing.earnedAt) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.update(schema.userAchievementsTable)
|
.update(schema.userAchievementsTable)
|
||||||
.set({
|
.set({ progress })
|
||||||
progress: Math.floor(progress) > 100 ? 100 : Math.floor(progress),
|
|
||||||
})
|
|
||||||
.where(eq(schema.userAchievementsTable.id, existing.id));
|
.where(eq(schema.userAchievementsTable.id, existing.id));
|
||||||
} else {
|
} else {
|
||||||
await db.insert(schema.userAchievementsTable).values({
|
await db.insert(schema.userAchievementsTable).values({
|
||||||
discordId: userId,
|
discordId: userId,
|
||||||
achievementId: achievementId,
|
achievementId,
|
||||||
progress: Math.floor(progress) > 100 ? 100 : Math.floor(progress),
|
progress,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,4 @@
|
||||||
import {
|
import { Message, EmbedBuilder, TextChannel, Guild } from 'discord.js';
|
||||||
Message,
|
|
||||||
Client,
|
|
||||||
EmbedBuilder,
|
|
||||||
GuildMember,
|
|
||||||
TextChannel,
|
|
||||||
Guild,
|
|
||||||
} from 'discord.js';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
addXpToUser,
|
addXpToUser,
|
||||||
|
@ -21,99 +14,63 @@ import { loadConfig } from './configLoader.js';
|
||||||
import { generateAchievementCard } from './achievementCardGenerator.js';
|
import { generateAchievementCard } from './achievementCardGenerator.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check and process achievements for a user based on a message
|
* Handle achievement progress updates
|
||||||
* @param message - The message that triggered the check
|
* @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
|
||||||
*/
|
*/
|
||||||
export async function processMessageAchievements(
|
async function handleProgress(
|
||||||
message: Message,
|
userId: string,
|
||||||
|
guild: Guild | null,
|
||||||
|
achievement: schema.achievementDefinitionsTableTypes,
|
||||||
|
progress: number,
|
||||||
|
options: { skipAward?: boolean } = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (message.author.bot) return;
|
const { skipAward = false } = options;
|
||||||
|
const userAchievements = await getUserAchievements(userId);
|
||||||
const userData = await getUserLevel(message.author.id);
|
const existing = userAchievements.find(
|
||||||
const allAchievements = await getAllAchievements();
|
(a) => a.achievementId === achievement.id && a.earnedAt !== null,
|
||||||
|
|
||||||
const messageAchievements = allAchievements.filter(
|
|
||||||
(a) => a.requirementType === 'message_count',
|
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const achievement of messageAchievements) {
|
await updateAchievementProgress(userId, achievement.id, progress);
|
||||||
const progress = Math.min(
|
|
||||||
100,
|
|
||||||
(userData.messagesSent / achievement.threshold) * 100,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (progress >= 100) {
|
if (progress === 100 && !existing && !skipAward) {
|
||||||
const userAchievements = await getUserAchievements(message.author.id);
|
const awarded = await awardAchievement(userId, achievement.id);
|
||||||
const existingAchievement = userAchievements.find(
|
if (awarded && guild) {
|
||||||
(a) => a.achievementId === achievement.id && a.earnedAt !== null,
|
await announceAchievement(guild, userId, achievement);
|
||||||
);
|
|
||||||
|
|
||||||
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
|
* Process message achievements based on user activity
|
||||||
* @param memberId - Member ID who leveled up
|
* @param message - The message object from Discord
|
||||||
* @param newLevel - New level value
|
*/
|
||||||
* @guild - Guild instance
|
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(
|
export async function processLevelUpAchievements(
|
||||||
memberId: string,
|
memberId: string,
|
||||||
|
@ -121,37 +78,19 @@ export async function processLevelUpAchievements(
|
||||||
guild: Guild,
|
guild: Guild,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const allAchievements = await getAllAchievements();
|
const allAchievements = await getAllAchievements();
|
||||||
|
for (const ach of allAchievements.filter(
|
||||||
const levelAchievements = allAchievements.filter(
|
|
||||||
(a) => a.requirementType === 'level',
|
(a) => a.requirementType === 'level',
|
||||||
);
|
)) {
|
||||||
|
const progress = Math.min(100, (newLevel / ach.threshold) * 100);
|
||||||
for (const achievement of levelAchievements) {
|
await handleProgress(memberId, guild, ach, progress);
|
||||||
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
|
* Process command usage achievements when a command is invoked
|
||||||
* @param userId - User ID who used the command
|
* @param userId - ID of the user who invoked the command
|
||||||
* @param commandName - Name of the command
|
* @param commandName - Name of the command invoked
|
||||||
* @param client - Guild instance
|
* @param guild - Guild instance where the command was invoked
|
||||||
*/
|
*/
|
||||||
export async function processCommandAchievements(
|
export async function processCommandAchievements(
|
||||||
userId: string,
|
userId: string,
|
||||||
|
@ -159,7 +98,6 @@ export async function processCommandAchievements(
|
||||||
guild: Guild,
|
guild: Guild,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const allAchievements = await getAllAchievements();
|
const allAchievements = await getAllAchievements();
|
||||||
|
|
||||||
const commandAchievements = allAchievements.filter(
|
const commandAchievements = allAchievements.filter(
|
||||||
(a) =>
|
(a) =>
|
||||||
a.requirementType === 'command_usage' &&
|
a.requirementType === 'command_usage' &&
|
||||||
|
@ -167,26 +105,31 @@ export async function processCommandAchievements(
|
||||||
(a.requirement as any).command === commandName,
|
(a.requirement as any).command === commandName,
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const achievement of commandAchievements) {
|
// fetch the user’s current achievement entries
|
||||||
const userAchievements = await getUserAchievements(userId);
|
const userAchievements = await getUserAchievements(userId);
|
||||||
const existingAchievement = userAchievements.find(
|
|
||||||
(a) => a.achievementId === achievement.id && a.earnedAt !== null,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!existingAchievement) {
|
for (const ach of commandAchievements) {
|
||||||
const awarded = await awardAchievement(userId, achievement.id);
|
// find existing progress, default to 0
|
||||||
if (awarded) {
|
const userAch = userAchievements.find((u) => u.achievementId === ach.id);
|
||||||
await announceAchievement(guild, userId, achievement);
|
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 achievements for reaction events (add or remove)
|
* Process reaction achievements when a user reacts to a message
|
||||||
* @param userId - User ID who added/removed the reaction
|
* @param userId - ID of the user who reacted
|
||||||
* @param guild - Guild instance
|
* @param guild - Guild instance where the reaction occurred
|
||||||
* @param isRemoval - Whether this is a reaction removal (true) or addition (false)
|
* @param isRemoval - Whether the reaction was removed (default: false)
|
||||||
*/
|
*/
|
||||||
export async function processReactionAchievements(
|
export async function processReactionAchievements(
|
||||||
userId: string,
|
userId: string,
|
||||||
|
@ -198,40 +141,21 @@ export async function processReactionAchievements(
|
||||||
if (member.user.bot) return;
|
if (member.user.bot) return;
|
||||||
|
|
||||||
const allAchievements = await getAllAchievements();
|
const allAchievements = await getAllAchievements();
|
||||||
|
|
||||||
const reactionAchievements = allAchievements.filter(
|
const reactionAchievements = allAchievements.filter(
|
||||||
(a) => a.requirementType === 'reactions',
|
(a) => a.requirementType === 'reactions',
|
||||||
);
|
);
|
||||||
|
if (!reactionAchievements.length) return;
|
||||||
if (reactionAchievements.length === 0) return;
|
|
||||||
|
|
||||||
const reactionCount = await getUserReactionCount(userId);
|
const reactionCount = await getUserReactionCount(userId);
|
||||||
|
|
||||||
for (const achievement of reactionAchievements) {
|
for (const ach of reactionAchievements) {
|
||||||
const progress = Math.max(
|
const progress = Math.max(
|
||||||
0,
|
0,
|
||||||
Math.min(100, (reactionCount / achievement.threshold) * 100),
|
Math.min(100, (reactionCount / ach.threshold) * 100),
|
||||||
);
|
);
|
||||||
|
await handleProgress(userId, guild, ach, progress, {
|
||||||
if (progress >= 100 && !isRemoval) {
|
skipAward: 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) {
|
} catch (error) {
|
||||||
console.error('Error processing reaction achievements:', error);
|
console.error('Error processing reaction achievements:', error);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue