adb-plugin-levels 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gitkeep +0 -0
- package/README.md +58 -0
- package/commands/leaderboard.js +48 -0
- package/commands/level-config.js +80 -0
- package/commands/level-roles.js +98 -0
- package/commands/level.js +57 -0
- package/index.js +106 -0
- package/models/Level.js +17 -0
- package/models/LevelConfig.js +14 -0
- package/models/LevelRole.js +15 -0
- package/package.json +10 -0
- package/plugin.json +42 -0
package/.gitkeep
ADDED
|
File without changes
|
package/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Levels & XP System
|
|
2
|
+
|
|
3
|
+
A plugin for AdvancedDiscordBot that adds experience points, levels, and role rewards based on user activity.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Track XP per user per guild
|
|
8
|
+
- Award XP on message activity (with configurable rate limiting)
|
|
9
|
+
- Level-up notifications
|
|
10
|
+
- Role rewards for reaching specific levels
|
|
11
|
+
- Leaderboard commands
|
|
12
|
+
- Admin configuration
|
|
13
|
+
|
|
14
|
+
## Commands
|
|
15
|
+
|
|
16
|
+
- `/level` - Check your or another user's level and XP
|
|
17
|
+
- `/leaderboard` - View the server XP ranking (top 10)
|
|
18
|
+
- `/level-config` - Admin: Configure XP rates, cooldowns, and level-up channel
|
|
19
|
+
- `/level-roles` - Admin: Manage role rewards for specific levels
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
1. Copy this folder to your bot's `plugins/` directory
|
|
24
|
+
2. Restart the bot (required for slash command registration)
|
|
25
|
+
3. Use `/level-config` to set up the plugin
|
|
26
|
+
|
|
27
|
+
## Configuration
|
|
28
|
+
|
|
29
|
+
- `xpPerMessage`: Amount of XP awarded per message (default: 5)
|
|
30
|
+
- `xpCooldown`: Seconds between XP awards for the same user (default: 60)
|
|
31
|
+
- `xpPerMinuteLimit`: Maximum XP a user can earn per minute (default: 100)
|
|
32
|
+
- `levelUpChannelId`: Channel ID for level-up announcements (optional)
|
|
33
|
+
|
|
34
|
+
## How It Works
|
|
35
|
+
|
|
36
|
+
XP is awarded based on message activity with anti-spam protection. Levels are calculated using the formula: `level = floor(sqrt(xp / 100))`.
|
|
37
|
+
|
|
38
|
+
When a user levels up:
|
|
39
|
+
- Their level is updated in the database
|
|
40
|
+
- A level-up event is emitted for other plugins to listen to
|
|
41
|
+
- A message is sent in the configured level-up channel (if set)
|
|
42
|
+
- Any role rewards for the new level are automatically assigned
|
|
43
|
+
|
|
44
|
+
## Database Models
|
|
45
|
+
|
|
46
|
+
The plugin creates three namespaced collections:
|
|
47
|
+
- `plugin_adb-plugin-levels_level` - Stores user XP and levels
|
|
48
|
+
- `plugin_adb-plugin-levels_levelconfig` - Stores guild configuration
|
|
49
|
+
- `plugin_adb-plugin-levels_leaderole` - Stores level-to-role mappings
|
|
50
|
+
|
|
51
|
+
## Dependencies
|
|
52
|
+
|
|
53
|
+
- discord.js ^14.14.1
|
|
54
|
+
- mongoose ^8.0.3
|
|
55
|
+
|
|
56
|
+
## License
|
|
57
|
+
|
|
58
|
+
MIT
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
|
|
2
|
+
|
|
3
|
+
const data = new SlashCommandBuilder()
|
|
4
|
+
.setName('leaderboard')
|
|
5
|
+
.setDescription('View the server XP leaderboard');
|
|
6
|
+
|
|
7
|
+
async function execute(interaction) {
|
|
8
|
+
try {
|
|
9
|
+
const guildId = interaction.guild.id;
|
|
10
|
+
const Level = require('../../models/Level');
|
|
11
|
+
|
|
12
|
+
const leaderboard = await Level.find({ guildId })
|
|
13
|
+
.sort({ xp: -1 })
|
|
14
|
+
.limit(10)
|
|
15
|
+
.populate('userId', 'username discriminator avatar');
|
|
16
|
+
|
|
17
|
+
if (leaderboard.length === 0) {
|
|
18
|
+
await interaction.reply({
|
|
19
|
+
content: 'No one has earned XP yet!',
|
|
20
|
+
ephemeral: true
|
|
21
|
+
});
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const embed = new EmbedBuilder()
|
|
26
|
+
.setTitle(`${interaction.guild.name} Leaderboard`)
|
|
27
|
+
.setColor(0xffd700);
|
|
28
|
+
|
|
29
|
+
const description = leaderboard.map((entry, index) => {
|
|
30
|
+
const medal = ['🥇', '🥈', '🥉'][index] || `${index + 1}.`;
|
|
31
|
+
const xp = (entry.xp || 0).toLocaleString();
|
|
32
|
+
const level = Math.floor(Math.sqrt((entry.xp || 0) / 100));
|
|
33
|
+
const username = interaction.guild.members.cache.get(entry.userId)?.user.username || 'Unknown User';
|
|
34
|
+
return `${medal} **${username}** - Level ${level} • ${xp} XP`;
|
|
35
|
+
}).join('\n');
|
|
36
|
+
|
|
37
|
+
embed.setDescription(description);
|
|
38
|
+
await interaction.reply({ embeds: [embed] });
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.error(error);
|
|
41
|
+
await interaction.reply({
|
|
42
|
+
content: 'There was an error while fetching the leaderboard!',
|
|
43
|
+
ephemeral: true
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = { data, execute };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } = require('discord.js');
|
|
2
|
+
|
|
3
|
+
const data = new SlashCommandBuilder()
|
|
4
|
+
.setName('level-config')
|
|
5
|
+
.setDescription('Configure the levels/XP system')
|
|
6
|
+
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild)
|
|
7
|
+
.addIntegerOption(option =>
|
|
8
|
+
option.setName('xp-per-message')
|
|
9
|
+
.setDescription('XP awarded per message (1-50)')
|
|
10
|
+
.setMinValue(1)
|
|
11
|
+
.setMaxValue(50))
|
|
12
|
+
.addIntegerOption(option =>
|
|
13
|
+
option.setName('xp-cooldown')
|
|
14
|
+
.setDescription('Seconds between XP awards (10-300)')
|
|
15
|
+
.setMinValue(10)
|
|
16
|
+
.setMaxValue(300))
|
|
17
|
+
.addIntegerOption(option =>
|
|
18
|
+
option.setName('xp-per-minute-limit')
|
|
19
|
+
.setDescription('Max XP per minute (10-1000)')
|
|
20
|
+
.setMinValue(10)
|
|
21
|
+
.setMaxValue(1000))
|
|
22
|
+
.addChannelOption(option =>
|
|
23
|
+
option.setName('level-up-channel')
|
|
24
|
+
.setDescription('Channel for level-up announcements')
|
|
25
|
+
.addChannelTypes(0)); // GUILD_TEXT
|
|
26
|
+
|
|
27
|
+
async function execute(interaction) {
|
|
28
|
+
try {
|
|
29
|
+
const guildId = interaction.guild.id;
|
|
30
|
+
const updates = {};
|
|
31
|
+
|
|
32
|
+
if (interaction.options.getInteger('xp-per-message') !== null) {
|
|
33
|
+
updates.xpPerMessage = interaction.options.getInteger('xp-per-message');
|
|
34
|
+
}
|
|
35
|
+
if (interaction.options.getInteger('xp-cooldown') !== null) {
|
|
36
|
+
updates.xpCooldown = interaction.options.getInteger('xp-cooldown');
|
|
37
|
+
}
|
|
38
|
+
if (interaction.options.getInteger('xp-per-minute-limit') !== null) {
|
|
39
|
+
updates.xpPerMinuteLimit = interaction.options.getInteger('xp-per-minute-limit');
|
|
40
|
+
}
|
|
41
|
+
if (interaction.options.getChannel('level-up-channel') !== null) {
|
|
42
|
+
updates.levelUpChannelId = interaction.options.getChannel('level-up-channel').id;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Update guild config
|
|
46
|
+
const config = await ctx.db.getServerConfig(guildId) || {};
|
|
47
|
+
if (!config.xpPerModule) config.xpPerModule = {};
|
|
48
|
+
config.xpPerModule.levels = { ...config.xpPerModule.levels || {}, ...updates };
|
|
49
|
+
await ctx.db.updateServerConfig(guildId, config);
|
|
50
|
+
|
|
51
|
+
const embed = new EmbedBuilder()
|
|
52
|
+
.setTitle('Levels Configuration Updated')
|
|
53
|
+
.setColor(0x00ff00)
|
|
54
|
+
.setDescription('The levels/XP system settings have been updated.');
|
|
55
|
+
|
|
56
|
+
if (updates.xpPerMessage !== undefined) {
|
|
57
|
+
embed.addFields({ name: 'XP per Message', value: `${updates.xpPerMessage}` });
|
|
58
|
+
}
|
|
59
|
+
if (updates.xpCooldown !== undefined) {
|
|
60
|
+
addFields({ name: 'XP Cooldown', value: `${updates.xpCooldown}s` });
|
|
61
|
+
}
|
|
62
|
+
if (updates.xpPerMinuteLimit !== undefined) {
|
|
63
|
+
embed.addFields({ name: 'XP per Minute Limit', value: `${updates.xpPerMinuteLimit}` });
|
|
64
|
+
}
|
|
65
|
+
if (updates.levelUpChannelId !== undefined) {
|
|
66
|
+
const channel = interaction.guild.channels.cache.get(updates.levelUpChannelId);
|
|
67
|
+
embed.addFields({ name: 'Level-Up Channel', value: `${channel}` });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
await interaction.reply({ embeds: [embed] });
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error(error);
|
|
73
|
+
await interaction.reply({
|
|
74
|
+
content: 'There was an error while updating the configuration!',
|
|
75
|
+
ephemeral: true
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = { data, execute };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } = require('discord.js');
|
|
2
|
+
|
|
3
|
+
const data = new SlashCommandBuilder()
|
|
4
|
+
.setName('level-roles')
|
|
5
|
+
.setDescription('Manage role rewards for levels')
|
|
6
|
+
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild)
|
|
7
|
+
.addSubcommand(subcommand =>
|
|
8
|
+
subcommand
|
|
9
|
+
.setName('add')
|
|
10
|
+
.setDescription('Add a role reward for a level')
|
|
11
|
+
.addIntegerOption(option =>
|
|
12
|
+
option.setName('level')
|
|
13
|
+
.setDescription('The level required')
|
|
14
|
+
.setMinValue(1)
|
|
15
|
+
.setRequired(true))
|
|
16
|
+
.addRoleOption(option =>
|
|
17
|
+
option.setName('role')
|
|
18
|
+
.setDescription('The role to reward')
|
|
19
|
+
.setRequired(true)))
|
|
20
|
+
.addSubcommand(subcommand =>
|
|
21
|
+
subcommand
|
|
22
|
+
.setName('remove')
|
|
23
|
+
.setDescription('Remove a role reward for a level')
|
|
24
|
+
.addIntegerOption(option =>
|
|
25
|
+
option.setName('level')
|
|
26
|
+
.setDescription('The level to remove')
|
|
27
|
+
.setMinValue(1)
|
|
28
|
+
.setRequired(true)))
|
|
29
|
+
.addSubcommand(subcommand =>
|
|
30
|
+
subcommand
|
|
31
|
+
.setName('list')
|
|
32
|
+
.setDescription('List all role rewards for this server'));
|
|
33
|
+
|
|
34
|
+
async function execute(interaction) {
|
|
35
|
+
try {
|
|
36
|
+
const guildId = interaction.guild.id;
|
|
37
|
+
const LevelRole = require('../../models/LevelRole');
|
|
38
|
+
|
|
39
|
+
const subcommand = interaction.options.getSubcommand();
|
|
40
|
+
|
|
41
|
+
if (subcommand === 'add') {
|
|
42
|
+
const level = interaction.options.getInteger('level');
|
|
43
|
+
const role = interaction.options.getRole('role');
|
|
44
|
+
|
|
45
|
+
await LevelRole.findOneAndUpdate(
|
|
46
|
+
{ guildId, level },
|
|
47
|
+
{ $set: { roleId: role.id } },
|
|
48
|
+
{ upsert: true, new: true }
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
await interaction.reply({
|
|
52
|
+
content: `Successfully set ${role} as reward for reaching level ${level}!`,
|
|
53
|
+
ephemeral: true
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
else if (subcommand === 'remove') {
|
|
57
|
+
const level = interaction.options.getInteger('level');
|
|
58
|
+
|
|
59
|
+
await LevelRole.deleteOne({ guildId, level });
|
|
60
|
+
|
|
61
|
+
await interaction.reply({
|
|
62
|
+
content: `Successfully removed role reward for level ${level}!`,
|
|
63
|
+
ephemeral: true
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
else if (subcommand === 'list') {
|
|
67
|
+
const roles = await LevelRole.find({ guildId }).sort({ level: 1 });
|
|
68
|
+
|
|
69
|
+
if (roles.length === 0) {
|
|
70
|
+
await interaction.reply({
|
|
71
|
+
content: 'No role rewards configured for this server.',
|
|
72
|
+
ephemeral: true
|
|
73
|
+
});
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const embed = new EmbedBuilder()
|
|
78
|
+
.setTitle('Level Role Rewards')
|
|
79
|
+
.setColor(0x9b59b6);
|
|
80
|
+
|
|
81
|
+
const description = roles.map(r => {
|
|
82
|
+
const role = interaction.guild.roles.cache.get(r.roleId);
|
|
83
|
+
return `**Level ${r.level}** → ${role ? role : '@deleted-role'}`;
|
|
84
|
+
}).join('\n');
|
|
85
|
+
|
|
86
|
+
embed.setDescription(description);
|
|
87
|
+
await interaction.reply({ embeds: [embed] });
|
|
88
|
+
}
|
|
89
|
+
} catch (error) {
|
|
90
|
+
console.error(error);
|
|
91
|
+
await interaction.reply({
|
|
92
|
+
content: 'There was an error while managing level roles!',
|
|
93
|
+
ephemeral: true
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = { data, execute };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
|
|
2
|
+
|
|
3
|
+
const data = new SlashCommandBuilder()
|
|
4
|
+
.setName('level')
|
|
5
|
+
.setDescription('Check your or another user\'s level and XP')
|
|
6
|
+
.addUserOption(option =>
|
|
7
|
+
option.setName('user')
|
|
8
|
+
.setDescription('The user to check')
|
|
9
|
+
.setRequired(false));
|
|
10
|
+
|
|
11
|
+
async function execute(interaction) {
|
|
12
|
+
try {
|
|
13
|
+
const targetUser = interaction.options.getUser('user') || interaction.user;
|
|
14
|
+
const guildId = interaction.guild.id;
|
|
15
|
+
const userId = targetUser.id;
|
|
16
|
+
|
|
17
|
+
const Level = require('../../models/Level');
|
|
18
|
+
const levelData = await Level.findOne({ guildId, userId });
|
|
19
|
+
|
|
20
|
+
if (!levelData) {
|
|
21
|
+
await interaction.reply({
|
|
22
|
+
content: `${targetUser} has not earned any XP yet.`,
|
|
23
|
+
ephemeral: true
|
|
24
|
+
});
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Simple level formula: level = floor(sqrt(xp / 100))
|
|
29
|
+
const xp = levelData.xp || 0;
|
|
30
|
+
const level = Math.floor(Math.sqrt(xp / 100));
|
|
31
|
+
const xpForCurrentLevel = level * level * 100;
|
|
32
|
+
const xpForNextLevel = (level + 1) * (level + 1) * 100;
|
|
33
|
+
const xpNeeded = xpForNextLevel - xp;
|
|
34
|
+
const progressPercent = ((xp - xpForCurrentLevel) / (xpForNextLevel - xpForCurrentLevel)) * 100;
|
|
35
|
+
|
|
36
|
+
const embed = new EmbedBuilder()
|
|
37
|
+
.setTitle(`${targetUser.username}'s Level`)
|
|
38
|
+
.setThumbnail(targetUser.displayAvatarURL())
|
|
39
|
+
.addFields(
|
|
40
|
+
{ name: 'Level', value: `${level}`, inline: true },
|
|
41
|
+
{ name: 'XP', value: `${xp.toLocaleString()} / ${xpForNextLevel.toLocaleString()}`, inline: true },
|
|
42
|
+
{ name: 'Progress', value: `${progressPercent.toFixed(1)}%`, inline: true }
|
|
43
|
+
)
|
|
44
|
+
.setColor(0x0099ff)
|
|
45
|
+
.setTimestamp();
|
|
46
|
+
|
|
47
|
+
await interaction.reply({ embeds: [embed] });
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.error(error);
|
|
50
|
+
await interaction.reply({
|
|
51
|
+
content: 'There was an error while fetching the level!',
|
|
52
|
+
ephemeral: true
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = { data, execute };
|
package/index.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
const { SlashCommandBuilder } = require('discord.js');
|
|
2
|
+
|
|
3
|
+
async function load(ctx) {
|
|
4
|
+
ctx.logger.info('Levels plugin loading...');
|
|
5
|
+
|
|
6
|
+
// Register commands
|
|
7
|
+
const levelCmd = require('./commands/level');
|
|
8
|
+
const leaderboardCmd = require('./commands/leaderboard');
|
|
9
|
+
const levelConfigCmd = require('./commands/level-config');
|
|
10
|
+
const levelRolesCmd = require('./commands/level-roles');
|
|
11
|
+
|
|
12
|
+
ctx.registerCommand(levelCmd);
|
|
13
|
+
ctx.registerCommand(leaderboardCmd);
|
|
14
|
+
ctx.registerCommand(levelConfigCmd);
|
|
15
|
+
ctx.registerCommand(levelRolesCmd);
|
|
16
|
+
|
|
17
|
+
// Define models
|
|
18
|
+
const LevelSchema = require('./models/Level');
|
|
19
|
+
const LevelConfigSchema = require('./models/LevelConfig');
|
|
20
|
+
const LevelRoleSchema = require('./models/LevelRole');
|
|
21
|
+
|
|
22
|
+
const LevelModel = ctx.defineModel('Level', LevelSchema);
|
|
23
|
+
const LevelConfigModel = ctx.defineModel('LevelConfig', LevelConfigSchema);
|
|
24
|
+
const LevelRoleModel = ctx.defineModel('LevelRole', LevelRoleSchema);
|
|
25
|
+
|
|
26
|
+
// Listen for message events to award XP
|
|
27
|
+
ctx.client.on('messageCreate', async (message) => {
|
|
28
|
+
if (message.author.bot || !message.guild) return;
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
// Get guild config
|
|
32
|
+
const config = await ctx.db.getServerConfig(message.guild.id) || {};
|
|
33
|
+
const xpPerMessage = config.xpPerModule?.levels?.xpPerMessage ?? 5;
|
|
34
|
+
const xpCooldown = config.xpPerModule?.levels?.xpCooldown ?? 60;
|
|
35
|
+
const xpPerMinuteLimit = config.xpPerModule?.levels?.xpPerMinuteLimit ?? 100;
|
|
36
|
+
|
|
37
|
+
// Simple rate limiting (in production use Redis or similar)
|
|
38
|
+
const now = Date.now();
|
|
39
|
+
const key = `xp_${message.guild.id}_${message.author.id}`;
|
|
40
|
+
|
|
41
|
+
// For simplicity, we'll just award XP with basic cooldown per user
|
|
42
|
+
// A real implementation would use a proper rate limiter
|
|
43
|
+
const lastXp = await ctx.db.get(`xp:${key}`) || 0;
|
|
44
|
+
if (now - lastXp < xpCooldown * 1000) return;
|
|
45
|
+
|
|
46
|
+
// Award XP
|
|
47
|
+
const levelData = await LevelModel.findOneAndUpdate(
|
|
48
|
+
{ guildId: message.guild.id, userId: message.author.id },
|
|
49
|
+
{ $inc: { xp: xpPerMessage }, $set: { lastMessageAt: new Date() } },
|
|
50
|
+
{ upsert: true, new: true }
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
// Check for level up
|
|
54
|
+
const oldLevel = Math.floor(Math.sqrt(levelData.xp / 100)); // Simple level formula
|
|
55
|
+
const newLevel = Math.floor(Math.sqrt((levelData.xp + xpPerMessage) / 100));
|
|
56
|
+
|
|
57
|
+
if (newLevel > oldLevel) {
|
|
58
|
+
// Level up!
|
|
59
|
+
await LevelModel.updateOne(
|
|
60
|
+
{ guildId: message.guild.id, userId: message.author.id },
|
|
61
|
+
{ $set: { level: newLevel } }
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// Emit level up event for other plugins
|
|
65
|
+
ctx.hooks.emitHook('onLevelUp', {
|
|
66
|
+
user: message.author,
|
|
67
|
+
newLevel: newLevel,
|
|
68
|
+
guild: message.guild
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Send level up message if channel configured
|
|
72
|
+
const levelUpChannelId = config.xpPerModule?.levels?.levelUpChannelId;
|
|
73
|
+
if (levelUpChannelId) {
|
|
74
|
+
const channel = message.guild.channels.cache.get(levelUpChannelId);
|
|
75
|
+
if (channel) {
|
|
76
|
+
channel.send(`${message.author} has reached level ${newLevel}! :tada:`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Check for role rewards
|
|
81
|
+
const roleRewards = await LevelRoleModel.find({ guildId: message.guild.id, level: newLevel });
|
|
82
|
+
for (const reward of roleRewards) {
|
|
83
|
+
const role = message.guild.roles.cache.get(reward.roleId);
|
|
84
|
+
if (role && !message.member.roles.cache.has(role.id)) {
|
|
85
|
+
await message.member.roles.add(role);
|
|
86
|
+
try {
|
|
87
|
+
await message.author.send(`You earned the ${role.name} role for reaching level ${newLevel}!`);
|
|
88
|
+
} catch (e) {
|
|
89
|
+
// DMs closed
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Update XP timestamp
|
|
96
|
+
await ctx.db.set(`xp:${key}`, now);
|
|
97
|
+
|
|
98
|
+
} catch (error) {
|
|
99
|
+
ctx.logger.error('Error in levels plugin:', error);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
ctx.logger.info('Levels plugin loaded!');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = { load };
|
package/models/Level.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const { Schema, model } = require('mongoose');
|
|
2
|
+
|
|
3
|
+
const LevelSchema = new Schema({
|
|
4
|
+
guildId: { type: String, required: true, index: true },
|
|
5
|
+
userId: { type: String, required: true, index: true },
|
|
6
|
+
xp: { type: Number, default: 0 },
|
|
7
|
+
level: { type: Number, default: 0 },
|
|
8
|
+
lastMessageAt: { type: Date, default: Date.now }
|
|
9
|
+
}, {
|
|
10
|
+
collection: 'plugin_adb-plugin-levels_level',
|
|
11
|
+
timestamps: true
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// Compound index for efficient queries
|
|
15
|
+
LevelSchema.index({ guildId: 1, userId: 1 }, { unique: true });
|
|
16
|
+
|
|
17
|
+
module.exports = LevelSchema;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const { Schema, model } = require('mongoose');
|
|
2
|
+
|
|
3
|
+
const LevelConfigSchema = new Schema({
|
|
4
|
+
guildId: { type: String, required: true, unique: true },
|
|
5
|
+
xpPerMessage: { type: Number, default: 5 },
|
|
6
|
+
xpCooldown: { type: Number, default: 60 },
|
|
7
|
+
xpPerMinuteLimit: { type: Number, default: 100 },
|
|
8
|
+
levelUpChannelId: { type: String, default: null }
|
|
9
|
+
}, {
|
|
10
|
+
collection: 'plugin_adb-plugin-levels_levelconfig',
|
|
11
|
+
timestamps: true
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
module.exports = LevelConfigSchema;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const { Schema, model } = require('mongoose');
|
|
2
|
+
|
|
3
|
+
const LevelRoleSchema = new Schema({
|
|
4
|
+
guildId: { type: String, required: true, index: true },
|
|
5
|
+
level: { type: Number, required: true, min: 1 },
|
|
6
|
+
roleId: { type: String, required: true }
|
|
7
|
+
}, {
|
|
8
|
+
collection: 'plugin_adb-plugin-levels_leaderole',
|
|
9
|
+
timestamps: true
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
// Compound index for efficient queries
|
|
13
|
+
LevelRoleSchema.index({ guildId: 1, level: 1 }, { unique: true });
|
|
14
|
+
|
|
15
|
+
module.exports = LevelRoleSchema;
|
package/package.json
ADDED
package/plugin.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "adb-plugin-levels",
|
|
3
|
+
"displayName": "Levels & XP System",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "Track user activity with XP, levels, and role rewards",
|
|
6
|
+
"author": "dead",
|
|
7
|
+
"main": "index.js",
|
|
8
|
+
"requiresRestart": false,
|
|
9
|
+
"permissions": [
|
|
10
|
+
"db.read",
|
|
11
|
+
"db.write",
|
|
12
|
+
"commands.register",
|
|
13
|
+
"events.register"
|
|
14
|
+
],
|
|
15
|
+
"configSchema": {
|
|
16
|
+
"type": "object",
|
|
17
|
+
"properties": {
|
|
18
|
+
"xpPerMessage": {
|
|
19
|
+
"type": "number",
|
|
20
|
+
"default": 5,
|
|
21
|
+
"minimum": 1,
|
|
22
|
+
"maximum": 50
|
|
23
|
+
},
|
|
24
|
+
"xpCooldown": {
|
|
25
|
+
"type": "number",
|
|
26
|
+
"default": 60,
|
|
27
|
+
"minimum": 10,
|
|
28
|
+
"maximum": 300
|
|
29
|
+
},
|
|
30
|
+
"xpPerMinuteLimit": {
|
|
31
|
+
"type": "number",
|
|
32
|
+
"default": 100,
|
|
33
|
+
"minimum": 10,
|
|
34
|
+
"maximum": 1000
|
|
35
|
+
},
|
|
36
|
+
"levelUpChannelId": {
|
|
37
|
+
"type": "string",
|
|
38
|
+
"default": null
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|