Add xp settings command and update xp tracking logic
Introduces the `/xp-settings` command for server administrators to configure XP gain, cooldowns, and multipliers. Updates the `xpTracker.js` listener to fetch server-specific XP configurations from a new `xp_config` table, incorporating enhanced XP calculation logic including channel bonuses and role multipliers. Adds caching for XP configurations and updates command documentation in `replit.md`. Replit-Commit-Author: Agent Replit-Commit-Session-Id: aed2e46d-25bb-4b73-81a1-bb9e8437c261 Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Event-Id: 665c9c2a-b623-41ea-b686-e23a04285c6d Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/aed2e46d-25bb-4b73-81a1-bb9e8437c261/ZjyNKqu Replit-Helium-Checkpoint-Created: true
This commit is contained in:
parent
fbc5c0533d
commit
78647044bb
4 changed files with 487 additions and 17 deletions
4
.replit
4
.replit
|
|
@ -22,10 +22,6 @@ externalPort = 80
|
|||
localPort = 8080
|
||||
externalPort = 8080
|
||||
|
||||
[[ports]]
|
||||
localPort = 39769
|
||||
externalPort = 3000
|
||||
|
||||
[workflows]
|
||||
runButton = "Project"
|
||||
|
||||
|
|
|
|||
378
aethex-bot/commands/xp-settings.js
Normal file
378
aethex-bot/commands/xp-settings.js
Normal file
|
|
@ -0,0 +1,378 @@
|
|||
const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } = require('discord.js');
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('xp-settings')
|
||||
.setDescription('Configure XP settings for your server')
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator)
|
||||
.addSubcommand(sub =>
|
||||
sub.setName('view')
|
||||
.setDescription('View current XP settings'))
|
||||
.addSubcommand(sub =>
|
||||
sub.setName('message-xp')
|
||||
.setDescription('Set XP earned per message')
|
||||
.addIntegerOption(opt =>
|
||||
opt.setName('amount')
|
||||
.setDescription('XP per message (1-50)')
|
||||
.setRequired(true)
|
||||
.setMinValue(1)
|
||||
.setMaxValue(50)))
|
||||
.addSubcommand(sub =>
|
||||
sub.setName('cooldown')
|
||||
.setDescription('Set cooldown between XP gains (seconds)')
|
||||
.addIntegerOption(opt =>
|
||||
opt.setName('seconds')
|
||||
.setDescription('Cooldown in seconds (10-300)')
|
||||
.setRequired(true)
|
||||
.setMinValue(10)
|
||||
.setMaxValue(300)))
|
||||
.addSubcommand(sub =>
|
||||
sub.setName('multiplier-role')
|
||||
.setDescription('Add/remove a role with XP multiplier')
|
||||
.addRoleOption(opt =>
|
||||
opt.setName('role')
|
||||
.setDescription('The role to add/remove multiplier')
|
||||
.setRequired(true))
|
||||
.addNumberOption(opt =>
|
||||
opt.setName('multiplier')
|
||||
.setDescription('XP multiplier (1.1-5.0, or 0 to remove)')
|
||||
.setRequired(true)
|
||||
.setMinValue(0)
|
||||
.setMaxValue(5)))
|
||||
.addSubcommand(sub =>
|
||||
sub.setName('bonus-channel')
|
||||
.setDescription('Add/remove a channel with bonus XP')
|
||||
.addChannelOption(opt =>
|
||||
opt.setName('channel')
|
||||
.setDescription('The channel to add/remove bonus')
|
||||
.setRequired(true))
|
||||
.addNumberOption(opt =>
|
||||
opt.setName('multiplier')
|
||||
.setDescription('XP multiplier (1.1-5.0, or 0 to remove)')
|
||||
.setRequired(true)
|
||||
.setMinValue(0)
|
||||
.setMaxValue(5)))
|
||||
.addSubcommand(sub =>
|
||||
sub.setName('toggle')
|
||||
.setDescription('Enable or disable XP system')
|
||||
.addBooleanOption(opt =>
|
||||
opt.setName('enabled')
|
||||
.setDescription('Enable or disable XP')
|
||||
.setRequired(true)))
|
||||
.addSubcommand(sub =>
|
||||
sub.setName('level-curve')
|
||||
.setDescription('Set leveling difficulty curve')
|
||||
.addStringOption(opt =>
|
||||
opt.setName('curve')
|
||||
.setDescription('Leveling curve type')
|
||||
.setRequired(true)
|
||||
.addChoices(
|
||||
{ name: 'Easy - Faster leveling', value: 'easy' },
|
||||
{ name: 'Normal - Standard leveling', value: 'normal' },
|
||||
{ name: 'Hard - Slower leveling', value: 'hard' }
|
||||
))),
|
||||
|
||||
async execute(interaction, client, supabase) {
|
||||
if (!supabase) {
|
||||
return interaction.reply({
|
||||
content: '❌ Database not configured. XP settings require Supabase.',
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
const guildId = interaction.guildId;
|
||||
const subcommand = interaction.options.getSubcommand();
|
||||
|
||||
// Get current config or create default
|
||||
let config = await getXpConfig(supabase, guildId);
|
||||
|
||||
switch (subcommand) {
|
||||
case 'view':
|
||||
return handleView(interaction, config);
|
||||
case 'message-xp':
|
||||
return handleMessageXp(interaction, supabase, guildId, config);
|
||||
case 'cooldown':
|
||||
return handleCooldown(interaction, supabase, guildId, config);
|
||||
case 'multiplier-role':
|
||||
return handleMultiplierRole(interaction, supabase, guildId, config);
|
||||
case 'bonus-channel':
|
||||
return handleBonusChannel(interaction, supabase, guildId, config);
|
||||
case 'toggle':
|
||||
return handleToggle(interaction, supabase, guildId, config);
|
||||
case 'level-curve':
|
||||
return handleLevelCurve(interaction, supabase, guildId, config);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async function getXpConfig(supabase, guildId) {
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('xp_config')
|
||||
.select('*')
|
||||
.eq('guild_id', guildId)
|
||||
.maybeSingle();
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
if (!data) {
|
||||
// Return default config
|
||||
return {
|
||||
guild_id: guildId,
|
||||
message_xp: 5,
|
||||
message_cooldown: 60,
|
||||
multiplier_roles: [],
|
||||
bonus_channels: [],
|
||||
level_curve: 'normal',
|
||||
xp_enabled: true
|
||||
};
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
console.error('Failed to get XP config:', e.message);
|
||||
return {
|
||||
guild_id: guildId,
|
||||
message_xp: 5,
|
||||
message_cooldown: 60,
|
||||
multiplier_roles: [],
|
||||
bonus_channels: [],
|
||||
level_curve: 'normal',
|
||||
xp_enabled: true
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function saveXpConfig(supabase, guildId, config) {
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('xp_config')
|
||||
.upsert({
|
||||
guild_id: guildId,
|
||||
message_xp: config.message_xp,
|
||||
message_cooldown: config.message_cooldown,
|
||||
multiplier_roles: config.multiplier_roles,
|
||||
bonus_channels: config.bonus_channels,
|
||||
level_curve: config.level_curve,
|
||||
xp_enabled: config.xp_enabled,
|
||||
updated_at: new Date().toISOString()
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error('Failed to save XP config:', e.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleView(interaction, config) {
|
||||
const multiplierRoles = config.multiplier_roles || [];
|
||||
const bonusChannels = config.bonus_channels || [];
|
||||
|
||||
const rolesText = multiplierRoles.length > 0
|
||||
? multiplierRoles.map(r => `<@&${r.role_id}> → ${r.multiplier}x`).join('\n')
|
||||
: 'None configured';
|
||||
|
||||
const channelsText = bonusChannels.length > 0
|
||||
? bonusChannels.map(c => `<#${c.channel_id}> → ${c.multiplier}x`).join('\n')
|
||||
: 'None configured';
|
||||
|
||||
const curveInfo = {
|
||||
easy: 'Easy (50 XP per level²)',
|
||||
normal: 'Normal (100 XP per level²)',
|
||||
hard: 'Hard (200 XP per level²)'
|
||||
};
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('⚙️ XP Settings')
|
||||
.setColor(config.xp_enabled ? 0x00ff88 : 0xff4444)
|
||||
.addFields(
|
||||
{ name: '📊 Status', value: config.xp_enabled ? '✅ Enabled' : '❌ Disabled', inline: true },
|
||||
{ name: '💬 Message XP', value: `${config.message_xp} XP`, inline: true },
|
||||
{ name: '⏱️ Cooldown', value: `${config.message_cooldown}s`, inline: true },
|
||||
{ name: '📈 Level Curve', value: curveInfo[config.level_curve] || 'Normal', inline: false },
|
||||
{ name: '🎭 Multiplier Roles', value: rolesText, inline: false },
|
||||
{ name: '📢 Bonus Channels', value: channelsText, inline: false }
|
||||
)
|
||||
.setFooter({ text: 'Use /xp-settings subcommands to modify' })
|
||||
.setTimestamp();
|
||||
|
||||
return interaction.reply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
async function handleMessageXp(interaction, supabase, guildId, config) {
|
||||
const amount = interaction.options.getInteger('amount');
|
||||
config.message_xp = amount;
|
||||
|
||||
const saved = await saveXpConfig(supabase, guildId, config);
|
||||
if (!saved) {
|
||||
return interaction.reply({
|
||||
content: '❌ Failed to save settings. Please try again.',
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
return interaction.reply({
|
||||
content: `✅ Message XP set to **${amount} XP** per message.`,
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
async function handleCooldown(interaction, supabase, guildId, config) {
|
||||
const seconds = interaction.options.getInteger('seconds');
|
||||
config.message_cooldown = seconds;
|
||||
|
||||
const saved = await saveXpConfig(supabase, guildId, config);
|
||||
if (!saved) {
|
||||
return interaction.reply({
|
||||
content: '❌ Failed to save settings. Please try again.',
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
return interaction.reply({
|
||||
content: `✅ XP cooldown set to **${seconds} seconds**.`,
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
async function handleMultiplierRole(interaction, supabase, guildId, config) {
|
||||
const role = interaction.options.getRole('role');
|
||||
const multiplier = interaction.options.getNumber('multiplier');
|
||||
|
||||
let roles = config.multiplier_roles || [];
|
||||
|
||||
if (multiplier === 0) {
|
||||
// Remove role
|
||||
roles = roles.filter(r => r.role_id !== role.id);
|
||||
config.multiplier_roles = roles;
|
||||
|
||||
const saved = await saveXpConfig(supabase, guildId, config);
|
||||
if (!saved) {
|
||||
return interaction.reply({
|
||||
content: '❌ Failed to save settings. Please try again.',
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
return interaction.reply({
|
||||
content: `✅ Removed XP multiplier from ${role}.`,
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
// Add or update role
|
||||
const existing = roles.findIndex(r => r.role_id === role.id);
|
||||
if (existing >= 0) {
|
||||
roles[existing].multiplier = multiplier;
|
||||
} else {
|
||||
roles.push({ role_id: role.id, multiplier });
|
||||
}
|
||||
|
||||
config.multiplier_roles = roles;
|
||||
const saved = await saveXpConfig(supabase, guildId, config);
|
||||
if (!saved) {
|
||||
return interaction.reply({
|
||||
content: '❌ Failed to save settings. Please try again.',
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
return interaction.reply({
|
||||
content: `✅ ${role} now has a **${multiplier}x** XP multiplier.`,
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
async function handleBonusChannel(interaction, supabase, guildId, config) {
|
||||
const channel = interaction.options.getChannel('channel');
|
||||
const multiplier = interaction.options.getNumber('multiplier');
|
||||
|
||||
let channels = config.bonus_channels || [];
|
||||
|
||||
if (multiplier === 0) {
|
||||
// Remove channel
|
||||
channels = channels.filter(c => c.channel_id !== channel.id);
|
||||
config.bonus_channels = channels;
|
||||
|
||||
const saved = await saveXpConfig(supabase, guildId, config);
|
||||
if (!saved) {
|
||||
return interaction.reply({
|
||||
content: '❌ Failed to save settings. Please try again.',
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
return interaction.reply({
|
||||
content: `✅ Removed bonus XP from ${channel}.`,
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
// Add or update channel
|
||||
const existing = channels.findIndex(c => c.channel_id === channel.id);
|
||||
if (existing >= 0) {
|
||||
channels[existing].multiplier = multiplier;
|
||||
} else {
|
||||
channels.push({ channel_id: channel.id, multiplier });
|
||||
}
|
||||
|
||||
config.bonus_channels = channels;
|
||||
const saved = await saveXpConfig(supabase, guildId, config);
|
||||
if (!saved) {
|
||||
return interaction.reply({
|
||||
content: '❌ Failed to save settings. Please try again.',
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
return interaction.reply({
|
||||
content: `✅ ${channel} now has a **${multiplier}x** XP bonus.`,
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
async function handleToggle(interaction, supabase, guildId, config) {
|
||||
const enabled = interaction.options.getBoolean('enabled');
|
||||
config.xp_enabled = enabled;
|
||||
|
||||
const saved = await saveXpConfig(supabase, guildId, config);
|
||||
if (!saved) {
|
||||
return interaction.reply({
|
||||
content: '❌ Failed to save settings. Please try again.',
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
return interaction.reply({
|
||||
content: enabled
|
||||
? '✅ XP system is now **enabled**.'
|
||||
: '❌ XP system is now **disabled**.',
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
async function handleLevelCurve(interaction, supabase, guildId, config) {
|
||||
const curve = interaction.options.getString('curve');
|
||||
config.level_curve = curve;
|
||||
|
||||
const saved = await saveXpConfig(supabase, guildId, config);
|
||||
if (!saved) {
|
||||
return interaction.reply({
|
||||
content: '❌ Failed to save settings. Please try again.',
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
const curveNames = {
|
||||
easy: 'Easy (faster leveling)',
|
||||
normal: 'Normal (standard leveling)',
|
||||
hard: 'Hard (slower leveling)'
|
||||
};
|
||||
|
||||
return interaction.reply({
|
||||
content: `✅ Level curve set to **${curveNames[curve]}**.`,
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
const XP_PER_MESSAGE = 5;
|
||||
const XP_COOLDOWN_MS = 60000;
|
||||
const xpCooldowns = new Map();
|
||||
const xpConfigCache = new Map();
|
||||
const CACHE_TTL = 60000; // 1 minute cache
|
||||
|
||||
module.exports = {
|
||||
name: 'messageCreate',
|
||||
|
|
@ -11,10 +11,22 @@ module.exports = {
|
|||
if (!message.guild) return;
|
||||
|
||||
const discordUserId = message.author.id;
|
||||
const guildId = message.guildId;
|
||||
const channelId = message.channelId;
|
||||
const now = Date.now();
|
||||
const lastXp = xpCooldowns.get(discordUserId) || 0;
|
||||
|
||||
if (now - lastXp < XP_COOLDOWN_MS) return;
|
||||
// Get XP config for this server
|
||||
const config = await getXpConfig(supabase, guildId);
|
||||
|
||||
// Check if XP is enabled
|
||||
if (!config.xp_enabled) return;
|
||||
|
||||
// Check cooldown
|
||||
const cooldownKey = `${guildId}:${discordUserId}`;
|
||||
const lastXp = xpCooldowns.get(cooldownKey) || 0;
|
||||
const cooldownMs = (config.message_cooldown || 60) * 1000;
|
||||
|
||||
if (now - lastXp < cooldownMs) return;
|
||||
|
||||
try {
|
||||
const { data: link, error: linkError } = await supabase
|
||||
|
|
@ -33,10 +45,32 @@ module.exports = {
|
|||
|
||||
if (profileError || !profile) return;
|
||||
|
||||
// Calculate base XP
|
||||
let xpGain = config.message_xp || 5;
|
||||
|
||||
// Apply channel bonus
|
||||
const bonusChannels = config.bonus_channels || [];
|
||||
const channelBonus = bonusChannels.find(c => c.channel_id === channelId);
|
||||
if (channelBonus) {
|
||||
xpGain = Math.floor(xpGain * channelBonus.multiplier);
|
||||
}
|
||||
|
||||
// Apply role multipliers (use highest)
|
||||
const multiplierRoles = config.multiplier_roles || [];
|
||||
let highestMultiplier = 1;
|
||||
for (const mr of multiplierRoles) {
|
||||
if (message.member?.roles.cache.has(mr.role_id)) {
|
||||
highestMultiplier = Math.max(highestMultiplier, mr.multiplier);
|
||||
}
|
||||
}
|
||||
xpGain = Math.floor(xpGain * highestMultiplier);
|
||||
|
||||
const currentXp = profile.xp || 0;
|
||||
const newXp = currentXp + XP_PER_MESSAGE;
|
||||
const oldLevel = Math.floor(Math.sqrt(currentXp / 100));
|
||||
const newLevel = Math.floor(Math.sqrt(newXp / 100));
|
||||
const newXp = currentXp + xpGain;
|
||||
|
||||
// Calculate levels based on curve
|
||||
const oldLevel = calculateLevel(currentXp, config.level_curve);
|
||||
const newLevel = calculateLevel(newXp, config.level_curve);
|
||||
|
||||
const { error: updateError } = await supabase
|
||||
.from('user_profiles')
|
||||
|
|
@ -45,11 +79,16 @@ module.exports = {
|
|||
|
||||
if (updateError) return;
|
||||
|
||||
xpCooldowns.set(discordUserId, now);
|
||||
xpCooldowns.set(cooldownKey, now);
|
||||
|
||||
// Track XP for analytics
|
||||
if (client.trackXP) {
|
||||
client.trackXP(xpGain);
|
||||
}
|
||||
|
||||
if (newLevel > oldLevel) {
|
||||
const config = client.serverConfigs?.get(message.guildId);
|
||||
const levelUpChannelId = config?.level_up_channel;
|
||||
const serverConfig = client.serverConfigs?.get(guildId);
|
||||
const levelUpChannelId = serverConfig?.level_up_channel;
|
||||
|
||||
const levelUpMessage = `🎉 Congratulations ${message.author}! You reached **Level ${newLevel}**!`;
|
||||
|
||||
|
|
@ -62,7 +101,7 @@ module.exports = {
|
|||
await message.channel.send(levelUpMessage).catch(() => {});
|
||||
}
|
||||
|
||||
await checkLevelRoles(message.member, newLevel, supabase, message.guildId);
|
||||
await checkLevelRoles(message.member, newLevel, supabase, guildId);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
|
|
@ -71,6 +110,59 @@ module.exports = {
|
|||
}
|
||||
};
|
||||
|
||||
async function getXpConfig(supabase, guildId) {
|
||||
const now = Date.now();
|
||||
const cached = xpConfigCache.get(guildId);
|
||||
|
||||
if (cached && (now - cached.timestamp < CACHE_TTL)) {
|
||||
return cached.config;
|
||||
}
|
||||
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('xp_config')
|
||||
.select('*')
|
||||
.eq('guild_id', guildId)
|
||||
.maybeSingle();
|
||||
|
||||
if (error) {
|
||||
// Table might not exist - return defaults
|
||||
const defaults = getDefaultConfig();
|
||||
xpConfigCache.set(guildId, { config: defaults, timestamp: now });
|
||||
return defaults;
|
||||
}
|
||||
|
||||
const config = data || getDefaultConfig();
|
||||
xpConfigCache.set(guildId, { config, timestamp: now });
|
||||
return config;
|
||||
} catch (e) {
|
||||
return getDefaultConfig();
|
||||
}
|
||||
}
|
||||
|
||||
function getDefaultConfig() {
|
||||
return {
|
||||
message_xp: 5,
|
||||
message_cooldown: 60,
|
||||
multiplier_roles: [],
|
||||
bonus_channels: [],
|
||||
level_curve: 'normal',
|
||||
xp_enabled: true
|
||||
};
|
||||
}
|
||||
|
||||
function calculateLevel(xp, curve = 'normal') {
|
||||
// XP required per level: level² * base
|
||||
// So level = sqrt(xp / base)
|
||||
const bases = {
|
||||
easy: 50, // Faster leveling
|
||||
normal: 100, // Standard
|
||||
hard: 200 // Slower leveling
|
||||
};
|
||||
const base = bases[curve] || 100;
|
||||
return Math.floor(Math.sqrt(xp / base));
|
||||
}
|
||||
|
||||
async function checkLevelRoles(member, level, supabase, guildId) {
|
||||
if (!member || !supabase) return;
|
||||
|
||||
|
|
@ -93,3 +185,6 @@ async function checkLevelRoles(member, level, supabase, guildId) {
|
|||
// Table may not exist yet - silently ignore
|
||||
}
|
||||
}
|
||||
|
||||
// Export calculateLevel for use in other commands
|
||||
module.exports.calculateLevel = calculateLevel;
|
||||
|
|
|
|||
|
|
@ -69,7 +69,8 @@ aethex-bot/
|
|||
│ ├── userinfo.js # /userinfo - user details
|
||||
│ ├── verify-role.js # /verify-role - check roles
|
||||
│ ├── verify.js # /verify - link account
|
||||
│ └── warn.js # /warn - warn users
|
||||
│ ├── warn.js # /warn - warn users
|
||||
│ └── xp-settings.js # /xp-settings - configure XP per server
|
||||
├── events/
|
||||
│ └── messageCreate.js # Message event handler
|
||||
├── listeners/
|
||||
|
|
@ -87,7 +88,7 @@ aethex-bot/
|
|||
└── register-commands.js # Slash command registration
|
||||
```
|
||||
|
||||
## Commands (36 Total)
|
||||
## Commands (37 Total)
|
||||
|
||||
### Community Commands
|
||||
| Command | Description |
|
||||
|
|
|
|||
Loading…
Reference in a new issue