Implement leveling and economy features with associated models and commands
- Added new models for leveling and economy functionalities in the Prisma schema, including LevelingConfig, MemberLevel, LevelReward, EconomyConfig, MemberEconomy, ShopItem, and InventoryItem. - Introduced commands for managing XP, economy transactions, and shop interactions, enhancing user engagement. - Updated job handling to include reminders and poll closing functionalities. - Enhanced localization support for new commands and features in both German and English. - Improved the bot's job processing capabilities with a dedicated reminder worker for scheduled tasks.
This commit is contained in:
160
apps/bot/src/modules/leveling/commands.ts
Normal file
160
apps/bot/src/modules/leveling/commands.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import { AttachmentBuilder, EmbedBuilder, PermissionFlagsBits } from 'discord.js';
|
||||
import { t, tf } from '@nexumi/shared';
|
||||
import type { SlashCommand } from '../../types.js';
|
||||
import { getGuildLocale } from '../../i18n.js';
|
||||
import { requirePermission } from '../../permissions.js';
|
||||
import {
|
||||
addXp,
|
||||
formatLeaderboardLine,
|
||||
getConfig,
|
||||
getLeaderboard,
|
||||
getOrCreateMemberLevel,
|
||||
getRankPosition,
|
||||
memberProgress,
|
||||
removeXp,
|
||||
resetXp
|
||||
} from './service.js';
|
||||
import { renderRankCard } from './rank-card.js';
|
||||
import {
|
||||
leaderboardCommandData,
|
||||
rankCommandData,
|
||||
xpCommandData
|
||||
} from './command-definitions.js';
|
||||
|
||||
const rankCommand: SlashCommand = {
|
||||
data: rankCommandData,
|
||||
async execute(interaction, context) {
|
||||
const locale = await getGuildLocale(context.prisma, interaction.guildId);
|
||||
if (!interaction.inGuild() || !interaction.guild) {
|
||||
await interaction.reply({ content: t(locale, 'generic.guildOnly'), ephemeral: true });
|
||||
return;
|
||||
}
|
||||
|
||||
const targetUser = interaction.options.getUser('user') ?? interaction.user;
|
||||
const member = await interaction.guild.members.fetch(targetUser.id).catch(() => null);
|
||||
if (!member) {
|
||||
await interaction.reply({ content: t(locale, 'leveling.user.notFound'), ephemeral: true });
|
||||
return;
|
||||
}
|
||||
|
||||
const config = await getConfig(context.prisma, interaction.guild.id);
|
||||
const memberLevel = await getOrCreateMemberLevel(context.prisma, interaction.guild.id, member.id);
|
||||
const rank = await getRankPosition(context.prisma, interaction.guild.id, member.id);
|
||||
const progress = memberProgress(memberLevel);
|
||||
|
||||
const buffer = await renderRankCard({
|
||||
displayName: member.displayName,
|
||||
avatarUrl: member.user.displayAvatarURL({ extension: 'png', size: 256 }),
|
||||
level: progress.level,
|
||||
rank,
|
||||
intoLevel: progress.intoLevel,
|
||||
needed: progress.needed,
|
||||
totalXp: memberLevel.xp,
|
||||
accentColor: config.cardAccentColor,
|
||||
backgroundColor: config.cardBackgroundColor
|
||||
});
|
||||
|
||||
const attachment = new AttachmentBuilder(buffer, { name: 'rank.png' });
|
||||
await interaction.reply({ files: [attachment] });
|
||||
}
|
||||
};
|
||||
|
||||
const leaderboardCommand: SlashCommand = {
|
||||
data: leaderboardCommandData,
|
||||
async execute(interaction, context) {
|
||||
const locale = await getGuildLocale(context.prisma, interaction.guildId);
|
||||
if (!interaction.inGuild() || !interaction.guild) {
|
||||
await interaction.reply({ content: t(locale, 'generic.guildOnly'), ephemeral: true });
|
||||
return;
|
||||
}
|
||||
|
||||
const entries = await getLeaderboard(context.prisma, interaction.guild.id, 10);
|
||||
if (entries.length === 0) {
|
||||
await interaction.reply({ content: t(locale, 'leveling.leaderboard.empty'), ephemeral: true });
|
||||
return;
|
||||
}
|
||||
|
||||
const lines = await Promise.all(
|
||||
entries.map(async (entry, index) => {
|
||||
const user = await context.client.users.fetch(entry.userId).catch(() => null);
|
||||
const label = user ? `<@${entry.userId}>` : entry.userId;
|
||||
return formatLeaderboardLine(locale, index + 1, label, entry.level, entry.xp);
|
||||
})
|
||||
);
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(t(locale, 'leveling.leaderboard.title'))
|
||||
.setDescription(lines.join('\n'))
|
||||
.setFooter({ text: tf(locale, 'leveling.leaderboard.footer', { count: entries.length }) })
|
||||
.setColor(0x6366f1);
|
||||
|
||||
await interaction.reply({ embeds: [embed] });
|
||||
}
|
||||
};
|
||||
|
||||
const xpCommand: SlashCommand = {
|
||||
data: xpCommandData,
|
||||
async execute(interaction, context) {
|
||||
const locale = await getGuildLocale(context.prisma, interaction.guildId);
|
||||
if (!interaction.inGuild() || !interaction.guild) {
|
||||
await interaction.reply({ content: t(locale, 'generic.guildOnly'), ephemeral: true });
|
||||
return;
|
||||
}
|
||||
if (!(await requirePermission(interaction, PermissionFlagsBits.ManageGuild, locale))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sub = interaction.options.getSubcommand();
|
||||
const user = interaction.options.getUser('user', true);
|
||||
const member = await interaction.guild.members.fetch(user.id).catch(() => null);
|
||||
if (!member) {
|
||||
await interaction.reply({ content: t(locale, 'leveling.user.notFound'), ephemeral: true });
|
||||
return;
|
||||
}
|
||||
|
||||
const channelId = interaction.channelId ?? undefined;
|
||||
|
||||
if (sub === 'give') {
|
||||
const amount = interaction.options.getInteger('amount', true);
|
||||
const result = await addXp(context, interaction.guild, member, amount, {
|
||||
channelId,
|
||||
force: true
|
||||
});
|
||||
const level = result?.newLevel ?? (await getOrCreateMemberLevel(context.prisma, interaction.guild.id, member.id)).level;
|
||||
await interaction.reply({
|
||||
content: tf(locale, 'leveling.xp.give.success', {
|
||||
amount,
|
||||
user: `<@${member.id}>`,
|
||||
level
|
||||
}),
|
||||
ephemeral: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub === 'remove') {
|
||||
const amount = interaction.options.getInteger('amount', true);
|
||||
const result = await removeXp(context, interaction.guild, member, amount, channelId);
|
||||
const level =
|
||||
result?.newLevel ??
|
||||
(await getOrCreateMemberLevel(context.prisma, interaction.guild.id, member.id)).level;
|
||||
await interaction.reply({
|
||||
content: tf(locale, 'leveling.xp.remove.success', {
|
||||
amount,
|
||||
user: `<@${member.id}>`,
|
||||
level
|
||||
}),
|
||||
ephemeral: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await resetXp(context, interaction.guild, member, channelId);
|
||||
await interaction.reply({
|
||||
content: tf(locale, 'leveling.xp.reset.success', { user: `<@${member.id}>` }),
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const levelingCommands: SlashCommand[] = [rankCommand, leaderboardCommand, xpCommand];
|
||||
Reference in New Issue
Block a user