From 78647044bb8a123b0a3715374c33c469ff9f148c Mon Sep 17 00:00:00 2001 From: sirpiglr <49359077-sirpiglr@users.noreply.replit.com> Date: Mon, 8 Dec 2025 21:00:29 +0000 Subject: [PATCH] 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 --- .replit | 4 - aethex-bot/commands/xp-settings.js | 378 +++++++++++++++++++++++++++++ aethex-bot/listeners/xpTracker.js | 117 ++++++++- replit.md | 5 +- 4 files changed, 487 insertions(+), 17 deletions(-) create mode 100644 aethex-bot/commands/xp-settings.js diff --git a/.replit b/.replit index 7cfa9d1..5b6f647 100644 --- a/.replit +++ b/.replit @@ -22,10 +22,6 @@ externalPort = 80 localPort = 8080 externalPort = 8080 -[[ports]] -localPort = 39769 -externalPort = 3000 - [workflows] runButton = "Project" diff --git a/aethex-bot/commands/xp-settings.js b/aethex-bot/commands/xp-settings.js new file mode 100644 index 0000000..08a3b0d --- /dev/null +++ b/aethex-bot/commands/xp-settings.js @@ -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 + }); +} diff --git a/aethex-bot/listeners/xpTracker.js b/aethex-bot/listeners/xpTracker.js index 42a8475..fada51c 100644 --- a/aethex-bot/listeners/xpTracker.js +++ b/aethex-bot/listeners/xpTracker.js @@ -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; diff --git a/replit.md b/replit.md index fba6fa2..59ee086 100644 --- a/replit.md +++ b/replit.md @@ -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 |