Added Basic Leveling System and QoL Updates

This commit is contained in:
Ahmad 2025-03-09 15:52:10 -04:00
parent 7af6d5914d
commit b5ce514397
No known key found for this signature in database
GPG key ID: 8FD8A93530D182BF
15 changed files with 970 additions and 39 deletions

View file

@ -74,6 +74,12 @@ const command: SubcommandCommand = {
execute: async (interaction) => {
if (!interaction.isChatInputCommand()) return;
await interaction.deferReply({
flags: ['Ephemeral'],
});
await interaction.editReply('Processing...');
const config = loadConfig();
const subcommand = interaction.options.getSubcommand();
@ -112,13 +118,15 @@ const command: SubcommandCommand = {
)
.setTimestamp();
const factId = await getLastInsertedFactId();
const approveButton = new ButtonBuilder()
.setCustomId(`approve_fact_${await getLastInsertedFactId()}`)
.setCustomId(`approve_fact_${factId}`)
.setLabel('Approve')
.setStyle(ButtonStyle.Success);
const rejectButton = new ButtonBuilder()
.setCustomId(`reject_fact_${await getLastInsertedFactId()}`)
.setCustomId(`reject_fact_${factId}`)
.setLabel('Reject')
.setStyle(ButtonStyle.Danger);
@ -136,11 +144,10 @@ const command: SubcommandCommand = {
}
}
await interaction.reply({
await interaction.editReply({
content: isAdmin
? 'Your fact has been automatically approved and added to the database!'
: 'Your fact has been submitted for approval!',
flags: ['Ephemeral'],
});
} else if (subcommand === 'approve') {
if (
@ -148,9 +155,8 @@ const command: SubcommandCommand = {
PermissionsBitField.Flags.ModerateMembers,
)
) {
await interaction.reply({
await interaction.editReply({
content: 'You do not have permission to approve facts.',
flags: ['Ephemeral'],
});
return;
}
@ -158,9 +164,8 @@ const command: SubcommandCommand = {
const id = interaction.options.getInteger('id', true);
await approveFact(id);
await interaction.reply({
await interaction.editReply({
content: `Fact #${id} has been approved!`,
flags: ['Ephemeral'],
});
} else if (subcommand === 'delete') {
if (
@ -168,9 +173,8 @@ const command: SubcommandCommand = {
PermissionsBitField.Flags.ModerateMembers,
)
) {
await interaction.reply({
await interaction.editReply({
content: 'You do not have permission to delete facts.',
flags: ['Ephemeral'],
});
return;
}
@ -178,9 +182,8 @@ const command: SubcommandCommand = {
const id = interaction.options.getInteger('id', true);
await deleteFact(id);
await interaction.reply({
await interaction.editReply({
content: `Fact #${id} has been deleted!`,
flags: ['Ephemeral'],
});
} else if (subcommand === 'pending') {
if (
@ -188,9 +191,8 @@ const command: SubcommandCommand = {
PermissionsBitField.Flags.ModerateMembers,
)
) {
await interaction.reply({
await interaction.editReply({
content: 'You do not have permission to view pending facts.',
flags: ['Ephemeral'],
});
return;
}
@ -198,9 +200,8 @@ const command: SubcommandCommand = {
const pendingFacts = await getPendingFacts();
if (pendingFacts.length === 0) {
await interaction.reply({
await interaction.editReply({
content: 'There are no pending facts.',
flags: ['Ephemeral'],
});
return;
}
@ -217,9 +218,8 @@ const command: SubcommandCommand = {
)
.setTimestamp();
await interaction.reply({
await interaction.editReply({
embeds: [embed],
flags: ['Ephemeral'],
});
} else if (subcommand === 'post') {
if (
@ -227,18 +227,16 @@ const command: SubcommandCommand = {
PermissionsBitField.Flags.Administrator,
)
) {
await interaction.reply({
await interaction.editReply({
content: 'You do not have permission to manually post facts.',
flags: ['Ephemeral'],
});
return;
}
await postFactOfTheDay(interaction.client);
await interaction.reply({
await interaction.editReply({
content: 'Fact of the day has been posted!',
flags: ['Ephemeral'],
});
}
},

View file

@ -0,0 +1,171 @@
import {
SlashCommandBuilder,
EmbedBuilder,
ButtonBuilder,
ActionRowBuilder,
ButtonStyle,
StringSelectMenuBuilder,
APIEmbed,
JSONEncodable,
} from 'discord.js';
import { OptionsCommand } from '../../types/CommandTypes.js';
import { getLevelLeaderboard } from '../../db/db.js';
const command: OptionsCommand = {
data: new SlashCommandBuilder()
.setName('leaderboard')
.setDescription('Shows the server XP leaderboard')
.addIntegerOption((option) =>
option
.setName('limit')
.setDescription('Number of users per page (default: 10)')
.setRequired(false),
),
execute: async (interaction) => {
if (!interaction.guild) return;
await interaction.deferReply();
try {
const usersPerPage =
(interaction.options.get('limit')?.value as number) || 10;
const allUsers = await getLevelLeaderboard(100);
if (allUsers.length === 0) {
const embed = new EmbedBuilder()
.setTitle('🏆 Server Leaderboard')
.setColor(0x5865f2)
.setDescription('No users found on the leaderboard yet.')
.setTimestamp();
await interaction.editReply({ embeds: [embed] });
}
const pages: (APIEmbed | JSONEncodable<APIEmbed>)[] = [];
for (let i = 0; i < allUsers.length; i += usersPerPage) {
const pageUsers = allUsers.slice(i, i + usersPerPage);
let leaderboardText = '';
for (let j = 0; j < pageUsers.length; j++) {
const user = pageUsers[j];
const position = i + j + 1;
try {
const member = await interaction.guild.members.fetch(
user.discordId,
);
leaderboardText += `**${position}.** ${member} - Level ${user.level} (${user.xp} XP)\n`;
} catch (error) {
leaderboardText += `**${position}.** <@${user.discordId}> - Level ${user.level} (${user.xp} XP)\n`;
}
}
const embed = new EmbedBuilder()
.setTitle('🏆 Server Leaderboard')
.setColor(0x5865f2)
.setDescription(leaderboardText)
.setTimestamp()
.setFooter({
text: `Page ${Math.floor(i / usersPerPage) + 1} of ${Math.ceil(allUsers.length / usersPerPage)}`,
});
pages.push(embed);
}
let currentPage = 0;
const getButtonActionRow = () =>
new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
.setCustomId('previous')
.setLabel('Previous')
.setStyle(ButtonStyle.Primary)
.setDisabled(currentPage === 0),
new ButtonBuilder()
.setCustomId('next')
.setLabel('Next')
.setStyle(ButtonStyle.Primary)
.setDisabled(currentPage === pages.length - 1),
);
const getSelectMenuRow = () => {
const options = pages.map((_, index) => ({
label: `Page ${index + 1}`,
value: index.toString(),
default: index === currentPage,
}));
const select = new StringSelectMenuBuilder()
.setCustomId('select_page')
.setPlaceholder('Jump to a page')
.addOptions(options);
return new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(
select,
);
};
const components =
pages.length > 1 ? [getButtonActionRow(), getSelectMenuRow()] : [];
const message = await interaction.editReply({
embeds: [pages[currentPage]],
components,
});
if (pages.length <= 1) return;
const collector = message.createMessageComponentCollector({
time: 60000,
});
collector.on('collect', async (i) => {
if (i.user.id !== interaction.user.id) {
await i.reply({
content: 'These controls are not for you!',
flags: ['Ephemeral'],
});
return;
}
if (i.isButton()) {
if (i.customId === 'previous' && currentPage > 0) {
currentPage--;
} else if (i.customId === 'next' && currentPage < pages.length - 1) {
currentPage++;
}
}
if (i.isStringSelectMenu()) {
const selected = parseInt(i.values[0]);
if (!isNaN(selected) && selected >= 0 && selected < pages.length) {
currentPage = selected;
}
}
await i.update({
embeds: [pages[currentPage]],
components: [getButtonActionRow(), getSelectMenuRow()],
});
});
collector.on('end', async () => {
if (message) {
try {
await interaction.editReply({ components: [] });
} catch (error) {
console.error('Error removing components:', error);
}
}
});
} catch (error) {
console.error('Error getting leaderboard:', error);
await interaction.editReply('Failed to get leaderboard information.');
}
},
};
export default command;

49
src/commands/fun/rank.ts Normal file
View file

@ -0,0 +1,49 @@
import { GuildMember, SlashCommandBuilder } from 'discord.js';
import { OptionsCommand } from '../../types/CommandTypes.js';
import {
generateRankCard,
getXpToNextLevel,
} from '../../util/levelingSystem.js';
import { getUserLevel } from '../../db/db.js';
const command: OptionsCommand = {
data: new SlashCommandBuilder()
.setName('rank')
.setDescription('Shows your current rank and level')
.addUserOption((option) =>
option
.setName('user')
.setDescription('The user to check rank for (defaults to yourself)')
.setRequired(false),
),
execute: async (interaction) => {
const member = await interaction.guild?.members.fetch(
(interaction.options.get('user')?.value as string) || interaction.user.id,
);
if (!member) {
await interaction.reply('User not found in this server.');
return;
}
await interaction.deferReply();
try {
const userData = await getUserLevel(member.id);
const rankCard = await generateRankCard(member, userData);
const xpToNextLevel = getXpToNextLevel(userData.level, userData.xp);
await interaction.editReply({
content: `${member}'s rank - Level ${userData.level} (${userData.xp} XP, ${xpToNextLevel} XP until next level)`,
files: [rankCard],
});
} catch (error) {
console.error('Error getting rank:', error);
await interaction.editReply('Failed to get rank information.');
}
},
};
export default command;

View file

@ -0,0 +1,36 @@
import { PermissionsBitField, SlashCommandBuilder } from 'discord.js';
import { Command } from '../../types/CommandTypes.js';
import { recalculateUserLevels } from '../../util/levelingSystem.js';
const command: Command = {
data: new SlashCommandBuilder()
.setName('recalculatelevels')
.setDescription('(Admin Only) Recalculate all user levels'),
execute: async (interaction) => {
if (
!interaction.memberPermissions?.has(
PermissionsBitField.Flags.Administrator,
)
) {
await interaction.reply({
content: 'You do not have permission to use this command.',
flags: ['Ephemeral'],
});
return;
}
await interaction.deferReply();
await interaction.editReply('Recalculating levels...');
try {
await recalculateUserLevels();
await interaction.editReply('Levels recalculated successfully!');
} catch (error) {
console.error('Error recalculating levels:', error);
await interaction.editReply('Failed to recalculate levels.');
}
},
};
export default command;

132
src/commands/util/xp.ts Normal file
View file

@ -0,0 +1,132 @@
import { SlashCommandBuilder } from 'discord.js';
import { SubcommandCommand } from '../../types/CommandTypes.js';
import { addXpToUser, getUserLevel } from '../../db/db.js';
import { loadConfig } from '../../util/configLoader.js';
const command: SubcommandCommand = {
data: new SlashCommandBuilder()
.setName('xp')
.setDescription('(Manager only) Manage user XP')
.addSubcommand((subcommand) =>
subcommand
.setName('add')
.setDescription('Add XP to a member')
.addUserOption((option) =>
option
.setName('user')
.setDescription('The user to add XP to')
.setRequired(true),
)
.addIntegerOption((option) =>
option
.setName('amount')
.setDescription('The amount of XP to add')
.setRequired(true),
),
)
.addSubcommand((subcommand) =>
subcommand
.setName('remove')
.setDescription('Remove XP from a member')
.addUserOption((option) =>
option
.setName('user')
.setDescription('The user to remove XP from')
.setRequired(true),
)
.addIntegerOption((option) =>
option
.setName('amount')
.setDescription('The amount of XP to remove')
.setRequired(true),
),
)
.addSubcommand((subcommand) =>
subcommand
.setName('set')
.setDescription('Set XP for a member')
.addUserOption((option) =>
option
.setName('user')
.setDescription('The user to set XP for')
.setRequired(true),
)
.addIntegerOption((option) =>
option
.setName('amount')
.setDescription('The amount of XP to set')
.setRequired(true),
),
)
.addSubcommand((subcommand) =>
subcommand
.setName('reset')
.setDescription('Reset XP for a member')
.addUserOption((option) =>
option
.setName('user')
.setDescription('The user to reset XP for')
.setRequired(true),
),
),
execute: async (interaction) => {
if (!interaction.isChatInputCommand()) return;
const commandUser = interaction.guild?.members.cache.get(
interaction.user.id,
);
const config = loadConfig();
const managerRoleId = config.roles.staffRoles.find(
(role) => role.name === 'Manager',
)?.roleId;
if (
!commandUser ||
!managerRoleId ||
commandUser.roles.highest.comparePositionTo(managerRoleId) < 0
) {
await interaction.reply({
content: 'You do not have permission to use this command',
flags: ['Ephemeral'],
});
return;
}
await interaction.deferReply({
flags: ['Ephemeral'],
});
await interaction.editReply('Processing...');
const subcommand = interaction.options.getSubcommand();
const user = interaction.options.getUser('user', true);
const amount = interaction.options.getInteger('amount', false);
const userData = await getUserLevel(user.id);
if (subcommand === 'add') {
await addXpToUser(user.id, amount!);
await interaction.editReply({
content: `Added ${amount} XP to <@${user.id}>`,
});
} else if (subcommand === 'remove') {
await addXpToUser(user.id, -amount!);
await interaction.editReply({
content: `Removed ${amount} XP from <@${user.id}>`,
});
} else if (subcommand === 'set') {
await addXpToUser(user.id, amount! - userData.xp);
await interaction.editReply({
content: `Set ${amount} XP for <@${user.id}>`,
});
} else if (subcommand === 'reset') {
await addXpToUser(user.id, userData.xp * -1);
await interaction.editReply({
content: `Reset XP for <@${user.id}>`,
});
}
},
};
export default command;