Add voice channel XP tracking and configuration options
Introduce voice XP tracking with interval-based awarding, configurable XP amounts, cooldowns, and enable/disable toggles via slash commands. Replit-Commit-Author: Agent Replit-Commit-Session-Id: aed2e46d-25bb-4b73-81a1-bb9e8437c261 Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Event-Id: a6df7c57-9848-41f4-b6cc-ef4096c91cf9 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
f96043dc21
commit
49f6c9fe13
4 changed files with 393 additions and 4 deletions
|
|
@ -50,6 +50,7 @@ const client = new Client({
|
||||||
GatewayIntentBits.MessageContent,
|
GatewayIntentBits.MessageContent,
|
||||||
GatewayIntentBits.DirectMessages,
|
GatewayIntentBits.DirectMessages,
|
||||||
GatewayIntentBits.GuildMessageReactions,
|
GatewayIntentBits.GuildMessageReactions,
|
||||||
|
GatewayIntentBits.GuildVoiceStates,
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -679,7 +680,7 @@ if (fs.existsSync(sentinelPath)) {
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
const listenersPath = path.join(__dirname, "listeners");
|
const listenersPath = path.join(__dirname, "listeners");
|
||||||
const generalListenerFiles = ['welcome.js', 'goodbye.js', 'xpTracker.js', 'reactionXp.js'];
|
const generalListenerFiles = ['welcome.js', 'goodbye.js', 'xpTracker.js', 'reactionXp.js', 'voiceXp.js'];
|
||||||
for (const file of generalListenerFiles) {
|
for (const file of generalListenerFiles) {
|
||||||
const filePath = path.join(listenersPath, file);
|
const filePath = path.join(listenersPath, file);
|
||||||
if (fs.existsSync(filePath)) {
|
if (fs.existsSync(filePath)) {
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,31 @@ module.exports = {
|
||||||
.addBooleanOption(opt =>
|
.addBooleanOption(opt =>
|
||||||
opt.setName('enabled')
|
opt.setName('enabled')
|
||||||
.setDescription('Enable or disable reaction XP')
|
.setDescription('Enable or disable reaction XP')
|
||||||
|
.setRequired(true)))
|
||||||
|
.addSubcommand(sub =>
|
||||||
|
sub.setName('voice-xp')
|
||||||
|
.setDescription('Set XP earned per minute in voice channels')
|
||||||
|
.addIntegerOption(opt =>
|
||||||
|
opt.setName('amount')
|
||||||
|
.setDescription('XP per minute (1-20)')
|
||||||
|
.setRequired(true)
|
||||||
|
.setMinValue(1)
|
||||||
|
.setMaxValue(20)))
|
||||||
|
.addSubcommand(sub =>
|
||||||
|
sub.setName('voice-cooldown')
|
||||||
|
.setDescription('Set cooldown between voice XP grants')
|
||||||
|
.addIntegerOption(opt =>
|
||||||
|
opt.setName('seconds')
|
||||||
|
.setDescription('Cooldown in seconds (30-300)')
|
||||||
|
.setRequired(true)
|
||||||
|
.setMinValue(30)
|
||||||
|
.setMaxValue(300)))
|
||||||
|
.addSubcommand(sub =>
|
||||||
|
sub.setName('voice-toggle')
|
||||||
|
.setDescription('Enable or disable voice XP')
|
||||||
|
.addBooleanOption(opt =>
|
||||||
|
opt.setName('enabled')
|
||||||
|
.setDescription('Enable or disable voice XP')
|
||||||
.setRequired(true))),
|
.setRequired(true))),
|
||||||
|
|
||||||
async execute(interaction, client, supabase) {
|
async execute(interaction, client, supabase) {
|
||||||
|
|
@ -138,6 +163,12 @@ module.exports = {
|
||||||
return handleReactionCooldown(interaction, supabase, guildId, config);
|
return handleReactionCooldown(interaction, supabase, guildId, config);
|
||||||
case 'reaction-toggle':
|
case 'reaction-toggle':
|
||||||
return handleReactionToggle(interaction, supabase, guildId, config);
|
return handleReactionToggle(interaction, supabase, guildId, config);
|
||||||
|
case 'voice-xp':
|
||||||
|
return handleVoiceXp(interaction, supabase, guildId, config);
|
||||||
|
case 'voice-cooldown':
|
||||||
|
return handleVoiceCooldown(interaction, supabase, guildId, config);
|
||||||
|
case 'voice-toggle':
|
||||||
|
return handleVoiceToggle(interaction, supabase, guildId, config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -165,7 +196,10 @@ async function getXpConfig(supabase, guildId) {
|
||||||
reaction_xp_enabled: true,
|
reaction_xp_enabled: true,
|
||||||
reaction_xp_received: 3,
|
reaction_xp_received: 3,
|
||||||
reaction_xp_given: 1,
|
reaction_xp_given: 1,
|
||||||
reaction_cooldown: 30
|
reaction_cooldown: 30,
|
||||||
|
voice_xp_enabled: true,
|
||||||
|
voice_xp: 2,
|
||||||
|
voice_cooldown: 60
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -183,7 +217,10 @@ async function getXpConfig(supabase, guildId) {
|
||||||
reaction_xp_enabled: true,
|
reaction_xp_enabled: true,
|
||||||
reaction_xp_received: 3,
|
reaction_xp_received: 3,
|
||||||
reaction_xp_given: 1,
|
reaction_xp_given: 1,
|
||||||
reaction_cooldown: 30
|
reaction_cooldown: 30,
|
||||||
|
voice_xp_enabled: true,
|
||||||
|
voice_xp: 2,
|
||||||
|
voice_cooldown: 60
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -204,6 +241,9 @@ async function saveXpConfig(supabase, guildId, config) {
|
||||||
reaction_xp_received: config.reaction_xp_received,
|
reaction_xp_received: config.reaction_xp_received,
|
||||||
reaction_xp_given: config.reaction_xp_given,
|
reaction_xp_given: config.reaction_xp_given,
|
||||||
reaction_cooldown: config.reaction_cooldown,
|
reaction_cooldown: config.reaction_cooldown,
|
||||||
|
voice_xp_enabled: config.voice_xp_enabled,
|
||||||
|
voice_xp: config.voice_xp,
|
||||||
|
voice_cooldown: config.voice_cooldown,
|
||||||
updated_at: new Date().toISOString()
|
updated_at: new Date().toISOString()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -238,6 +278,10 @@ async function handleView(interaction, config) {
|
||||||
const reactionGiven = config.reaction_xp_given ?? 1;
|
const reactionGiven = config.reaction_xp_given ?? 1;
|
||||||
const reactionCooldown = config.reaction_cooldown ?? 30;
|
const reactionCooldown = config.reaction_cooldown ?? 30;
|
||||||
|
|
||||||
|
const voiceEnabled = config.voice_xp_enabled !== false;
|
||||||
|
const voiceXp = config.voice_xp ?? 2;
|
||||||
|
const voiceCooldown = config.voice_cooldown ?? 60;
|
||||||
|
|
||||||
const embed = new EmbedBuilder()
|
const embed = new EmbedBuilder()
|
||||||
.setTitle('⚙️ XP Settings')
|
.setTitle('⚙️ XP Settings')
|
||||||
.setColor(config.xp_enabled ? 0x00ff88 : 0xff4444)
|
.setColor(config.xp_enabled ? 0x00ff88 : 0xff4444)
|
||||||
|
|
@ -251,7 +295,10 @@ async function handleView(interaction, config) {
|
||||||
{ name: '😀 Reaction XP', value: reactionEnabled ? '✅ Enabled' : '❌ Disabled', inline: true },
|
{ name: '😀 Reaction XP', value: reactionEnabled ? '✅ Enabled' : '❌ Disabled', inline: true },
|
||||||
{ name: '📥 Receive Reaction', value: `${reactionReceived} XP`, inline: true },
|
{ name: '📥 Receive Reaction', value: `${reactionReceived} XP`, inline: true },
|
||||||
{ name: '📤 Give Reaction', value: `${reactionGiven} XP`, inline: true },
|
{ name: '📤 Give Reaction', value: `${reactionGiven} XP`, inline: true },
|
||||||
{ name: '⏳ Reaction Cooldown', value: `${reactionCooldown}s`, inline: true }
|
{ name: '⏳ Reaction Cooldown', value: `${reactionCooldown}s`, inline: true },
|
||||||
|
{ name: '🎤 Voice XP', value: voiceEnabled ? '✅ Enabled' : '❌ Disabled', inline: true },
|
||||||
|
{ name: '🔊 XP per Minute', value: `${voiceXp} XP`, inline: true },
|
||||||
|
{ name: '⏰ Voice Cooldown', value: `${voiceCooldown}s`, inline: true }
|
||||||
)
|
)
|
||||||
.setFooter({ text: 'Use /xp-settings subcommands to modify' })
|
.setFooter({ text: 'Use /xp-settings subcommands to modify' })
|
||||||
.setTimestamp();
|
.setTimestamp();
|
||||||
|
|
@ -493,3 +540,59 @@ async function handleReactionToggle(interaction, supabase, guildId, config) {
|
||||||
ephemeral: true
|
ephemeral: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleVoiceXp(interaction, supabase, guildId, config) {
|
||||||
|
const amount = interaction.options.getInteger('amount');
|
||||||
|
config.voice_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: `✅ Voice XP set to **${amount} XP** per minute.`,
|
||||||
|
ephemeral: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleVoiceCooldown(interaction, supabase, guildId, config) {
|
||||||
|
const seconds = interaction.options.getInteger('seconds');
|
||||||
|
config.voice_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: `✅ Voice XP cooldown set to **${seconds} seconds**.`,
|
||||||
|
ephemeral: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleVoiceToggle(interaction, supabase, guildId, config) {
|
||||||
|
const enabled = interaction.options.getBoolean('enabled');
|
||||||
|
config.voice_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
|
||||||
|
? '✅ Voice XP is now **enabled**.'
|
||||||
|
: '❌ Voice XP is now **disabled**.',
|
||||||
|
ephemeral: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
280
aethex-bot/listeners/voiceXp.js
Normal file
280
aethex-bot/listeners/voiceXp.js
Normal file
|
|
@ -0,0 +1,280 @@
|
||||||
|
const voiceSessions = new Map();
|
||||||
|
const voiceConfigCache = new Map();
|
||||||
|
const CACHE_TTL = 60000;
|
||||||
|
const XP_INTERVAL = 60000;
|
||||||
|
|
||||||
|
let xpInterval = null;
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name: 'voiceStateUpdate',
|
||||||
|
|
||||||
|
async execute(oldState, newState, client, supabase) {
|
||||||
|
if (!supabase) return;
|
||||||
|
|
||||||
|
const userId = newState.member?.id || oldState.member?.id;
|
||||||
|
if (!userId) return;
|
||||||
|
|
||||||
|
const member = newState.member || oldState.member;
|
||||||
|
if (member?.user?.bot) return;
|
||||||
|
|
||||||
|
const guildId = newState.guild?.id || oldState.guild?.id;
|
||||||
|
if (!guildId) return;
|
||||||
|
|
||||||
|
const wasInVoice = oldState.channelId !== null;
|
||||||
|
const isInVoice = newState.channelId !== null;
|
||||||
|
const now = Date.now();
|
||||||
|
const key = `${guildId}:${userId}`;
|
||||||
|
|
||||||
|
if (!wasInVoice && isInVoice) {
|
||||||
|
const config = await getVoiceConfig(supabase, guildId);
|
||||||
|
if (!config.voice_xp_enabled) return;
|
||||||
|
|
||||||
|
if (isAfkChannel(newState.channelId, newState.guild)) return;
|
||||||
|
|
||||||
|
voiceSessions.set(key, {
|
||||||
|
joinTime: now,
|
||||||
|
lastAwardTime: now,
|
||||||
|
guildId,
|
||||||
|
userId
|
||||||
|
});
|
||||||
|
|
||||||
|
startXpInterval(client, supabase);
|
||||||
|
}
|
||||||
|
else if (wasInVoice && !isInVoice) {
|
||||||
|
const session = voiceSessions.get(key);
|
||||||
|
|
||||||
|
if (session) {
|
||||||
|
await awardRemainingXp(supabase, client, session, member, now);
|
||||||
|
voiceSessions.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (wasInVoice && isInVoice && oldState.channelId !== newState.channelId) {
|
||||||
|
const session = voiceSessions.get(key);
|
||||||
|
|
||||||
|
if (isAfkChannel(newState.channelId, newState.guild)) {
|
||||||
|
if (session) {
|
||||||
|
await awardRemainingXp(supabase, client, session, member, now);
|
||||||
|
voiceSessions.delete(key);
|
||||||
|
}
|
||||||
|
} else if (isAfkChannel(oldState.channelId, oldState.guild)) {
|
||||||
|
const config = await getVoiceConfig(supabase, guildId);
|
||||||
|
if (config.voice_xp_enabled) {
|
||||||
|
voiceSessions.set(key, {
|
||||||
|
joinTime: now,
|
||||||
|
lastAwardTime: now,
|
||||||
|
guildId,
|
||||||
|
userId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function startXpInterval(client, supabase) {
|
||||||
|
if (xpInterval) return;
|
||||||
|
|
||||||
|
xpInterval = setInterval(async () => {
|
||||||
|
if (voiceSessions.size === 0) {
|
||||||
|
clearInterval(xpInterval);
|
||||||
|
xpInterval = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
for (const [key, session] of voiceSessions.entries()) {
|
||||||
|
const { guildId, userId, lastAwardTime } = session;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const guild = client.guilds.cache.get(guildId);
|
||||||
|
if (!guild) continue;
|
||||||
|
|
||||||
|
const member = await guild.members.fetch(userId).catch(() => null);
|
||||||
|
if (!member) continue;
|
||||||
|
|
||||||
|
const voiceState = member.voice;
|
||||||
|
if (!voiceState?.channelId) {
|
||||||
|
voiceSessions.delete(key);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAfkChannel(voiceState.channelId, guild)) continue;
|
||||||
|
|
||||||
|
const config = await getVoiceConfig(supabase, guildId);
|
||||||
|
if (!config.voice_xp_enabled) continue;
|
||||||
|
|
||||||
|
const cooldownMs = (config.voice_cooldown || 60) * 1000;
|
||||||
|
const timeSinceLastAward = now - lastAwardTime;
|
||||||
|
|
||||||
|
if (timeSinceLastAward < cooldownMs) continue;
|
||||||
|
|
||||||
|
const minutesToAward = Math.floor(timeSinceLastAward / 60000);
|
||||||
|
if (minutesToAward < 1) continue;
|
||||||
|
|
||||||
|
await grantVoiceXp(supabase, client, guildId, userId, config, member, minutesToAward);
|
||||||
|
|
||||||
|
session.lastAwardTime += minutesToAward * 60000;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Voice XP interval error:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, XP_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function awardRemainingXp(supabase, client, session, member, now) {
|
||||||
|
const { guildId, userId, lastAwardTime } = session;
|
||||||
|
|
||||||
|
const config = await getVoiceConfig(supabase, guildId);
|
||||||
|
if (!config.voice_xp_enabled) return;
|
||||||
|
|
||||||
|
const timeSinceLastAward = now - lastAwardTime;
|
||||||
|
const minutesToAward = Math.floor(timeSinceLastAward / 60000);
|
||||||
|
|
||||||
|
if (minutesToAward < 1) return;
|
||||||
|
|
||||||
|
await grantVoiceXp(supabase, client, guildId, userId, config, member, minutesToAward);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function grantVoiceXp(supabase, client, guildId, userId, config, member, minutes = 1) {
|
||||||
|
try {
|
||||||
|
const { data: link, error: linkError } = await supabase
|
||||||
|
.from('discord_links')
|
||||||
|
.select('user_id')
|
||||||
|
.eq('discord_id', userId)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
if (linkError || !link) return;
|
||||||
|
|
||||||
|
const { data: profile, error: profileError } = await supabase
|
||||||
|
.from('user_profiles')
|
||||||
|
.select('xp')
|
||||||
|
.eq('id', link.user_id)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
if (profileError || !profile) return;
|
||||||
|
|
||||||
|
let xpGain = (config.voice_xp || 2) * minutes;
|
||||||
|
|
||||||
|
const multiplierRoles = config.multiplier_roles || [];
|
||||||
|
let highestMultiplier = 1;
|
||||||
|
for (const mr of multiplierRoles) {
|
||||||
|
if (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 + xpGain;
|
||||||
|
|
||||||
|
const oldLevel = calculateLevel(currentXp, config.level_curve);
|
||||||
|
const newLevel = calculateLevel(newXp, config.level_curve);
|
||||||
|
|
||||||
|
const { error: updateError } = await supabase
|
||||||
|
.from('user_profiles')
|
||||||
|
.update({ xp: newXp })
|
||||||
|
.eq('id', link.user_id);
|
||||||
|
|
||||||
|
if (updateError) return;
|
||||||
|
|
||||||
|
if (client.trackXP) {
|
||||||
|
client.trackXP(xpGain);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newLevel > oldLevel && member) {
|
||||||
|
const serverConfig = client.serverConfigs?.get(guildId);
|
||||||
|
const levelUpChannelId = serverConfig?.level_up_channel;
|
||||||
|
|
||||||
|
const levelUpMessage = `🎉 Congratulations ${member}! You reached **Level ${newLevel}** from voice chat activity!`;
|
||||||
|
|
||||||
|
if (levelUpChannelId) {
|
||||||
|
const channel = await client.channels.fetch(levelUpChannelId).catch(() => null);
|
||||||
|
if (channel) {
|
||||||
|
await channel.send(levelUpMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await checkLevelRoles(member, newLevel, supabase, guildId);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Voice XP grant error:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAfkChannel(channelId, guild) {
|
||||||
|
return guild?.afkChannelId === channelId;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getVoiceConfig(supabase, guildId) {
|
||||||
|
const now = Date.now();
|
||||||
|
const cached = voiceConfigCache.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) {
|
||||||
|
const defaults = getDefaultConfig();
|
||||||
|
voiceConfigCache.set(guildId, { config: defaults, timestamp: now });
|
||||||
|
return defaults;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = data || getDefaultConfig();
|
||||||
|
voiceConfigCache.set(guildId, { config, timestamp: now });
|
||||||
|
return config;
|
||||||
|
} catch (e) {
|
||||||
|
return getDefaultConfig();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultConfig() {
|
||||||
|
return {
|
||||||
|
voice_xp: 2,
|
||||||
|
voice_cooldown: 60,
|
||||||
|
voice_xp_enabled: true,
|
||||||
|
multiplier_roles: [],
|
||||||
|
level_curve: 'normal'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateLevel(xp, curve = 'normal') {
|
||||||
|
const bases = {
|
||||||
|
easy: 50,
|
||||||
|
normal: 100,
|
||||||
|
hard: 200
|
||||||
|
};
|
||||||
|
const base = bases[curve] || 100;
|
||||||
|
return Math.floor(Math.sqrt(xp / base));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkLevelRoles(member, level, supabase, guildId) {
|
||||||
|
if (!member || !supabase) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data: levelRoles, error } = await supabase
|
||||||
|
.from('level_roles')
|
||||||
|
.select('role_id, level_required')
|
||||||
|
.eq('guild_id', guildId)
|
||||||
|
.lte('level_required', level)
|
||||||
|
.order('level_required', { ascending: true });
|
||||||
|
|
||||||
|
if (error || !levelRoles || levelRoles.length === 0) return;
|
||||||
|
|
||||||
|
for (const lr of levelRoles) {
|
||||||
|
if (!member.roles.cache.has(lr.role_id)) {
|
||||||
|
await member.roles.add(lr.role_id).catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -80,6 +80,7 @@ aethex-bot/
|
||||||
│ ├── goodbye.js # Rich goodbye messages
|
│ ├── goodbye.js # Rich goodbye messages
|
||||||
│ ├── xpTracker.js # XP tracking on messages
|
│ ├── xpTracker.js # XP tracking on messages
|
||||||
│ ├── reactionXp.js # XP tracking for reactions
|
│ ├── reactionXp.js # XP tracking for reactions
|
||||||
|
│ ├── voiceXp.js # XP tracking for voice channels
|
||||||
│ └── sentinel/
|
│ └── sentinel/
|
||||||
│ ├── antiNuke.js # Channel delete monitor
|
│ ├── antiNuke.js # Channel delete monitor
|
||||||
│ ├── roleDelete.js # Role delete monitor
|
│ ├── roleDelete.js # Role delete monitor
|
||||||
|
|
@ -173,6 +174,7 @@ XP is earned across all platforms and stored in a single profile:
|
||||||
|
|
||||||
- **Discord Messages**: +5 XP per message (60s cooldown, configurable)
|
- **Discord Messages**: +5 XP per message (60s cooldown, configurable)
|
||||||
- **Reaction XP**: +3 XP for receiving reactions, +1 XP for giving (30s cooldown, configurable)
|
- **Reaction XP**: +3 XP for receiving reactions, +1 XP for giving (30s cooldown, configurable)
|
||||||
|
- **Voice Chat XP**: +2 XP per minute in voice channels (60s cooldown, configurable)
|
||||||
- **Daily Claims**: +50 XP base + streak bonus (up to +100)
|
- **Daily Claims**: +50 XP base + streak bonus (up to +100)
|
||||||
- **Platform Activity**: Posts, likes, comments on AeThex sites
|
- **Platform Activity**: Posts, likes, comments on AeThex sites
|
||||||
|
|
||||||
|
|
@ -184,6 +186,9 @@ Level formula: `level = floor(sqrt(xp / base))` where base is 50 (easy), 100 (no
|
||||||
- `reaction-xp` - Set XP for giving/receiving reactions
|
- `reaction-xp` - Set XP for giving/receiving reactions
|
||||||
- `reaction-cooldown` - Set reaction XP cooldown (5-120s)
|
- `reaction-cooldown` - Set reaction XP cooldown (5-120s)
|
||||||
- `reaction-toggle` - Enable/disable reaction XP
|
- `reaction-toggle` - Enable/disable reaction XP
|
||||||
|
- `voice-xp` - Set XP per minute in voice channels (1-20)
|
||||||
|
- `voice-cooldown` - Set voice XP cooldown (30-300s)
|
||||||
|
- `voice-toggle` - Enable/disable voice XP
|
||||||
- `multiplier-role` - Add role-based XP multipliers
|
- `multiplier-role` - Add role-based XP multipliers
|
||||||
- `bonus-channel` - Add channel-based XP bonuses
|
- `bonus-channel` - Add channel-based XP bonuses
|
||||||
- `level-curve` - Set leveling difficulty (easy/normal/hard)
|
- `level-curve` - Set leveling difficulty (easy/normal/hard)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue