Add quest system for users to complete objectives and earn rewards
Implement a new quest system with commands for users and administrators, including tracking progress for various objectives and awarding XP. Replit-Commit-Author: Agent Replit-Commit-Session-Id: aed2e46d-25bb-4b73-81a1-bb9e8437c261 Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Event-Id: 3d62eaac-3ee6-4585-b52c-552b348253ee Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/aed2e46d-25bb-4b73-81a1-bb9e8437c261/zRLxuQq Replit-Helium-Checkpoint-Created: true
This commit is contained in:
parent
98aedc46e7
commit
7c10440040
8 changed files with 1227 additions and 4 deletions
|
|
@ -80,11 +80,14 @@ async function trackCommandForAchievements(discordUserId, guildId, member, supab
|
|||
|
||||
if (!link) return;
|
||||
|
||||
const { updateUserStats, getUserStats, calculateLevel } = require('./listeners/xpTracker');
|
||||
const { updateUserStats, getUserStats, calculateLevel, updateQuestProgress } = require('./listeners/xpTracker');
|
||||
const { checkAchievements } = require('./commands/achievements');
|
||||
|
||||
await updateUserStats(supabaseClient, link.user_id, guildId, { commandsUsed: 1 });
|
||||
|
||||
// Track quest progress for command usage
|
||||
await updateQuestProgress(supabaseClient, link.user_id, guildId, 'commands', 1);
|
||||
|
||||
const { data: profile } = await supabaseClient
|
||||
.from('user_profiles')
|
||||
.select('xp, prestige_level, total_xp_earned, daily_streak')
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
|
||||
const { checkAchievements } = require('./achievements');
|
||||
const { getUserStats, calculateLevel } = require('../listeners/xpTracker');
|
||||
const { getUserStats, calculateLevel, updateQuestProgress } = require('../listeners/xpTracker');
|
||||
|
||||
const DAILY_XP = 50;
|
||||
const STREAK_BONUS = 10;
|
||||
|
|
@ -134,6 +134,14 @@ module.exports = {
|
|||
|
||||
await checkAchievements(link.user_id, interaction.member, stats, supabase, guildId, client);
|
||||
|
||||
// Track quest progress for daily claims and XP earned
|
||||
await updateQuestProgress(supabase, link.user_id, guildId, 'daily_claims', 1);
|
||||
await updateQuestProgress(supabase, link.user_id, guildId, 'xp_earned', totalXp);
|
||||
|
||||
if (newLevel > oldLevel) {
|
||||
await updateQuestProgress(supabase, link.user_id, guildId, 'level_ups', 1);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Daily error:', error);
|
||||
await interaction.editReply({ content: 'Failed to claim daily reward.' });
|
||||
|
|
|
|||
605
aethex-bot/commands/quests-manage.js
Normal file
605
aethex-bot/commands/quests-manage.js
Normal file
|
|
@ -0,0 +1,605 @@
|
|||
const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } = require('discord.js');
|
||||
|
||||
const QUEST_TYPES = {
|
||||
daily: { emoji: '☀️', name: 'Daily' },
|
||||
weekly: { emoji: '📅', name: 'Weekly' },
|
||||
special: { emoji: '⭐', name: 'Special' }
|
||||
};
|
||||
|
||||
const OBJECTIVES = {
|
||||
messages: { emoji: '💬', name: 'Send Messages' },
|
||||
reactions: { emoji: '😄', name: 'Add Reactions' },
|
||||
voice_minutes: { emoji: '🎙️', name: 'Voice Chat (minutes)' },
|
||||
commands: { emoji: '⚡', name: 'Use Commands' },
|
||||
daily_claims: { emoji: '🎁', name: 'Claim Daily Rewards' },
|
||||
level_ups: { emoji: '📈', name: 'Level Up' },
|
||||
xp_earned: { emoji: '✨', name: 'Earn XP' }
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('quests-manage')
|
||||
.setDescription('Manage server quests (Admin only)')
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator)
|
||||
.addSubcommand(sub =>
|
||||
sub.setName('create')
|
||||
.setDescription('Create a new quest')
|
||||
.addStringOption(opt =>
|
||||
opt.setName('name')
|
||||
.setDescription('Quest name')
|
||||
.setRequired(true)
|
||||
)
|
||||
.addStringOption(opt =>
|
||||
opt.setName('type')
|
||||
.setDescription('Quest type')
|
||||
.setRequired(true)
|
||||
.addChoices(
|
||||
{ name: '☀️ Daily', value: 'daily' },
|
||||
{ name: '📅 Weekly', value: 'weekly' },
|
||||
{ name: '⭐ Special', value: 'special' }
|
||||
)
|
||||
)
|
||||
.addStringOption(opt =>
|
||||
opt.setName('objective')
|
||||
.setDescription('What users need to do')
|
||||
.setRequired(true)
|
||||
.addChoices(
|
||||
{ name: '💬 Send Messages', value: 'messages' },
|
||||
{ name: '😄 Add Reactions', value: 'reactions' },
|
||||
{ name: '🎙️ Voice Chat (minutes)', value: 'voice_minutes' },
|
||||
{ name: '⚡ Use Commands', value: 'commands' },
|
||||
{ name: '🎁 Claim Daily Rewards', value: 'daily_claims' },
|
||||
{ name: '📈 Level Up', value: 'level_ups' },
|
||||
{ name: '✨ Earn XP', value: 'xp_earned' }
|
||||
)
|
||||
)
|
||||
.addIntegerOption(opt =>
|
||||
opt.setName('target')
|
||||
.setDescription('Amount needed to complete')
|
||||
.setRequired(true)
|
||||
.setMinValue(1)
|
||||
)
|
||||
.addIntegerOption(opt =>
|
||||
opt.setName('xp_reward')
|
||||
.setDescription('XP reward for completion')
|
||||
.setRequired(true)
|
||||
.setMinValue(1)
|
||||
)
|
||||
.addStringOption(opt =>
|
||||
opt.setName('description')
|
||||
.setDescription('Quest description')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addRoleOption(opt =>
|
||||
opt.setName('role_reward')
|
||||
.setDescription('Role to grant on completion')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addBooleanOption(opt =>
|
||||
opt.setName('repeatable')
|
||||
.setDescription('Can users complete this quest multiple times?')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addIntegerOption(opt =>
|
||||
opt.setName('duration_hours')
|
||||
.setDescription('Quest duration in hours (auto-expires)')
|
||||
.setRequired(false)
|
||||
.setMinValue(1)
|
||||
)
|
||||
)
|
||||
.addSubcommand(sub =>
|
||||
sub.setName('delete')
|
||||
.setDescription('Delete a quest')
|
||||
.addIntegerOption(opt =>
|
||||
opt.setName('quest_id')
|
||||
.setDescription('The ID of the quest to delete')
|
||||
.setRequired(true)
|
||||
)
|
||||
)
|
||||
.addSubcommand(sub =>
|
||||
sub.setName('edit')
|
||||
.setDescription('Edit an existing quest')
|
||||
.addIntegerOption(opt =>
|
||||
opt.setName('quest_id')
|
||||
.setDescription('The ID of the quest to edit')
|
||||
.setRequired(true)
|
||||
)
|
||||
.addStringOption(opt =>
|
||||
opt.setName('name')
|
||||
.setDescription('New quest name')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addIntegerOption(opt =>
|
||||
opt.setName('xp_reward')
|
||||
.setDescription('New XP reward')
|
||||
.setRequired(false)
|
||||
.setMinValue(1)
|
||||
)
|
||||
.addIntegerOption(opt =>
|
||||
opt.setName('target')
|
||||
.setDescription('New target value')
|
||||
.setRequired(false)
|
||||
.setMinValue(1)
|
||||
)
|
||||
.addBooleanOption(opt =>
|
||||
opt.setName('active')
|
||||
.setDescription('Enable or disable the quest')
|
||||
.setRequired(false)
|
||||
)
|
||||
)
|
||||
.addSubcommand(sub =>
|
||||
sub.setName('list')
|
||||
.setDescription('List all quests')
|
||||
)
|
||||
.addSubcommand(sub =>
|
||||
sub.setName('reset')
|
||||
.setDescription('Reset quest progress for all users')
|
||||
.addIntegerOption(opt =>
|
||||
opt.setName('quest_id')
|
||||
.setDescription('The ID of the quest to reset (leave empty to reset all)')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addStringOption(opt =>
|
||||
opt.setName('type')
|
||||
.setDescription('Reset all quests of a type')
|
||||
.setRequired(false)
|
||||
.addChoices(
|
||||
{ name: '☀️ Daily', value: 'daily' },
|
||||
{ name: '📅 Weekly', value: 'weekly' }
|
||||
)
|
||||
)
|
||||
)
|
||||
.addSubcommand(sub =>
|
||||
sub.setName('stats')
|
||||
.setDescription('View quest statistics')
|
||||
),
|
||||
|
||||
async execute(interaction, supabase) {
|
||||
if (!supabase) {
|
||||
return interaction.reply({ content: 'Database not configured.', ephemeral: true });
|
||||
}
|
||||
|
||||
const subcommand = interaction.options.getSubcommand();
|
||||
|
||||
switch (subcommand) {
|
||||
case 'create':
|
||||
return handleCreate(interaction, supabase);
|
||||
case 'delete':
|
||||
return handleDelete(interaction, supabase);
|
||||
case 'edit':
|
||||
return handleEdit(interaction, supabase);
|
||||
case 'list':
|
||||
return handleList(interaction, supabase);
|
||||
case 'reset':
|
||||
return handleReset(interaction, supabase);
|
||||
case 'stats':
|
||||
return handleStats(interaction, supabase);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async function handleCreate(interaction, supabase) {
|
||||
await interaction.deferReply({ ephemeral: true });
|
||||
|
||||
const guildId = interaction.guildId;
|
||||
const name = interaction.options.getString('name');
|
||||
const questType = interaction.options.getString('type');
|
||||
const objective = interaction.options.getString('objective');
|
||||
const target = interaction.options.getInteger('target');
|
||||
const xpReward = interaction.options.getInteger('xp_reward');
|
||||
const description = interaction.options.getString('description') || '';
|
||||
const roleReward = interaction.options.getRole('role_reward');
|
||||
const repeatable = interaction.options.getBoolean('repeatable') || false;
|
||||
const durationHours = interaction.options.getInteger('duration_hours');
|
||||
|
||||
let expiresAt = null;
|
||||
if (durationHours) {
|
||||
expiresAt = new Date(Date.now() + durationHours * 60 * 60 * 1000).toISOString();
|
||||
}
|
||||
|
||||
try {
|
||||
const { data: newQuest, error } = await supabase
|
||||
.from('quests')
|
||||
.insert({
|
||||
guild_id: guildId,
|
||||
name: name,
|
||||
description: description,
|
||||
quest_type: questType,
|
||||
objective: objective,
|
||||
target_value: target,
|
||||
xp_reward: xpReward,
|
||||
role_reward: roleReward?.id || null,
|
||||
repeatable: repeatable,
|
||||
starts_at: new Date().toISOString(),
|
||||
expires_at: expiresAt,
|
||||
active: true
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
const typeInfo = QUEST_TYPES[questType] || { emoji: '📋', name: questType };
|
||||
const objInfo = OBJECTIVES[objective] || { emoji: '🎯', name: objective };
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0x00ff00)
|
||||
.setTitle('✅ Quest Created')
|
||||
.addFields(
|
||||
{ name: 'Quest ID', value: `#${newQuest.id}`, inline: true },
|
||||
{ name: 'Name', value: name, inline: true },
|
||||
{ name: 'Type', value: `${typeInfo.emoji} ${typeInfo.name}`, inline: true },
|
||||
{ name: 'Objective', value: `${objInfo.emoji} ${target}x ${objInfo.name}`, inline: true },
|
||||
{ name: 'XP Reward', value: `${xpReward.toLocaleString()} XP`, inline: true },
|
||||
{ name: 'Repeatable', value: repeatable ? 'Yes' : 'No', inline: true }
|
||||
);
|
||||
|
||||
if (description) {
|
||||
embed.addFields({ name: 'Description', value: description, inline: false });
|
||||
}
|
||||
|
||||
if (roleReward) {
|
||||
embed.addFields({ name: 'Role Reward', value: `<@&${roleReward.id}>`, inline: true });
|
||||
}
|
||||
|
||||
if (expiresAt) {
|
||||
embed.addFields({
|
||||
name: 'Expires',
|
||||
value: `<t:${Math.floor(new Date(expiresAt).getTime() / 1000)}:R>`,
|
||||
inline: true
|
||||
});
|
||||
}
|
||||
|
||||
embed.setTimestamp();
|
||||
|
||||
await interaction.editReply({ embeds: [embed] });
|
||||
} catch (error) {
|
||||
console.error('Quest create error:', error);
|
||||
await interaction.editReply({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(0xff0000)
|
||||
.setDescription('Failed to create quest.')
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(interaction, supabase) {
|
||||
await interaction.deferReply({ ephemeral: true });
|
||||
|
||||
const guildId = interaction.guildId;
|
||||
const questId = interaction.options.getInteger('quest_id');
|
||||
|
||||
try {
|
||||
const { data: quest } = await supabase
|
||||
.from('quests')
|
||||
.select('*')
|
||||
.eq('id', questId)
|
||||
.eq('guild_id', guildId)
|
||||
.maybeSingle();
|
||||
|
||||
if (!quest) {
|
||||
return interaction.editReply({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(0xff6b6b)
|
||||
.setDescription('Quest not found.')
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
await supabase
|
||||
.from('quests')
|
||||
.delete()
|
||||
.eq('id', questId)
|
||||
.eq('guild_id', guildId);
|
||||
|
||||
await interaction.editReply({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(0x00ff00)
|
||||
.setTitle('🗑️ Quest Deleted')
|
||||
.setDescription(`**${quest.name}** has been deleted.`)
|
||||
.setTimestamp()
|
||||
]
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Quest delete error:', error);
|
||||
await interaction.editReply({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(0xff0000)
|
||||
.setDescription('Failed to delete quest.')
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function handleEdit(interaction, supabase) {
|
||||
await interaction.deferReply({ ephemeral: true });
|
||||
|
||||
const guildId = interaction.guildId;
|
||||
const questId = interaction.options.getInteger('quest_id');
|
||||
const newName = interaction.options.getString('name');
|
||||
const newXpReward = interaction.options.getInteger('xp_reward');
|
||||
const newTarget = interaction.options.getInteger('target');
|
||||
const active = interaction.options.getBoolean('active');
|
||||
|
||||
try {
|
||||
const { data: quest } = await supabase
|
||||
.from('quests')
|
||||
.select('*')
|
||||
.eq('id', questId)
|
||||
.eq('guild_id', guildId)
|
||||
.maybeSingle();
|
||||
|
||||
if (!quest) {
|
||||
return interaction.editReply({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(0xff6b6b)
|
||||
.setDescription('Quest not found.')
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
const updates = { updated_at: new Date().toISOString() };
|
||||
const changes = [];
|
||||
|
||||
if (newName !== null) {
|
||||
updates.name = newName;
|
||||
changes.push(`Name: ${quest.name} → ${newName}`);
|
||||
}
|
||||
if (newXpReward !== null) {
|
||||
updates.xp_reward = newXpReward;
|
||||
changes.push(`XP Reward: ${quest.xp_reward} → ${newXpReward}`);
|
||||
}
|
||||
if (newTarget !== null) {
|
||||
updates.target_value = newTarget;
|
||||
changes.push(`Target: ${quest.target_value} → ${newTarget}`);
|
||||
}
|
||||
if (active !== null) {
|
||||
updates.active = active;
|
||||
changes.push(`Active: ${quest.active} → ${active}`);
|
||||
}
|
||||
|
||||
if (changes.length === 0) {
|
||||
return interaction.editReply({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(0xfbbf24)
|
||||
.setDescription('No changes specified.')
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
await supabase
|
||||
.from('quests')
|
||||
.update(updates)
|
||||
.eq('id', questId);
|
||||
|
||||
await interaction.editReply({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(0x00ff00)
|
||||
.setTitle('✏️ Quest Updated')
|
||||
.setDescription(`**${quest.name}** has been updated.`)
|
||||
.addFields({ name: 'Changes', value: changes.join('\n') })
|
||||
.setTimestamp()
|
||||
]
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Quest edit error:', error);
|
||||
await interaction.editReply({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(0xff0000)
|
||||
.setDescription('Failed to edit quest.')
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function handleList(interaction, supabase) {
|
||||
await interaction.deferReply({ ephemeral: true });
|
||||
|
||||
const guildId = interaction.guildId;
|
||||
|
||||
try {
|
||||
const { data: quests, error } = await supabase
|
||||
.from('quests')
|
||||
.select('*')
|
||||
.eq('guild_id', guildId)
|
||||
.order('quest_type')
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
if (error || !quests || quests.length === 0) {
|
||||
return interaction.editReply({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(0xfbbf24)
|
||||
.setTitle('📋 Server Quests')
|
||||
.setDescription('No quests created yet.\n\nUse `/quests-manage create` to add quests.')
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0x7c3aed)
|
||||
.setTitle('📋 Server Quests (Admin View)')
|
||||
.setDescription(`Total quests: **${quests.length}**`)
|
||||
.setTimestamp();
|
||||
|
||||
const questLines = quests.map(quest => {
|
||||
const status = quest.active ? '✅' : '❌';
|
||||
const typeInfo = QUEST_TYPES[quest.quest_type] || { emoji: '📋' };
|
||||
const objInfo = OBJECTIVES[quest.objective] || { emoji: '🎯', name: quest.objective };
|
||||
const expires = quest.expires_at && new Date(quest.expires_at) < new Date() ? ' ⏰' : '';
|
||||
return `${status} ${typeInfo.emoji} \`#${quest.id}\` **${quest.name}** - ${objInfo.emoji} ${quest.target_value}x → ${quest.xp_reward} XP${expires}`;
|
||||
});
|
||||
|
||||
const chunks = [];
|
||||
let current = '';
|
||||
for (const line of questLines) {
|
||||
if ((current + '\n' + line).length > 1000) {
|
||||
chunks.push(current);
|
||||
current = line;
|
||||
} else {
|
||||
current = current ? current + '\n' + line : line;
|
||||
}
|
||||
}
|
||||
if (current) chunks.push(current);
|
||||
|
||||
for (let i = 0; i < Math.min(chunks.length, 5); i++) {
|
||||
embed.addFields({
|
||||
name: i === 0 ? 'Quests' : '\u200b',
|
||||
value: chunks[i],
|
||||
inline: false
|
||||
});
|
||||
}
|
||||
|
||||
await interaction.editReply({ embeds: [embed] });
|
||||
} catch (error) {
|
||||
console.error('Quest list error:', error);
|
||||
await interaction.editReply({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(0xff0000)
|
||||
.setDescription('Failed to list quests.')
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function handleReset(interaction, supabase) {
|
||||
await interaction.deferReply({ ephemeral: true });
|
||||
|
||||
const guildId = interaction.guildId;
|
||||
const questId = interaction.options.getInteger('quest_id');
|
||||
const questType = interaction.options.getString('type');
|
||||
|
||||
try {
|
||||
let deleteQuery = supabase
|
||||
.from('user_quests')
|
||||
.delete()
|
||||
.eq('guild_id', guildId);
|
||||
|
||||
let description = '';
|
||||
|
||||
if (questId) {
|
||||
deleteQuery = deleteQuery.eq('quest_id', questId);
|
||||
description = `Progress reset for quest #${questId}`;
|
||||
} else if (questType) {
|
||||
const { data: quests } = await supabase
|
||||
.from('quests')
|
||||
.select('id')
|
||||
.eq('guild_id', guildId)
|
||||
.eq('quest_type', questType);
|
||||
|
||||
if (quests && quests.length > 0) {
|
||||
deleteQuery = deleteQuery.in('quest_id', quests.map(q => q.id));
|
||||
description = `Progress reset for all ${QUEST_TYPES[questType]?.name || questType} quests`;
|
||||
} else {
|
||||
return interaction.editReply({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(0xfbbf24)
|
||||
.setDescription('No quests found for that type.')
|
||||
]
|
||||
});
|
||||
}
|
||||
} else {
|
||||
description = 'Progress reset for all quests';
|
||||
}
|
||||
|
||||
await deleteQuery;
|
||||
|
||||
await interaction.editReply({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(0x00ff00)
|
||||
.setTitle('🔄 Quest Progress Reset')
|
||||
.setDescription(description)
|
||||
.setTimestamp()
|
||||
]
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Quest reset error:', error);
|
||||
await interaction.editReply({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(0xff0000)
|
||||
.setDescription('Failed to reset quest progress.')
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function handleStats(interaction, supabase) {
|
||||
await interaction.deferReply({ ephemeral: true });
|
||||
|
||||
const guildId = interaction.guildId;
|
||||
|
||||
try {
|
||||
const { data: quests } = await supabase
|
||||
.from('quests')
|
||||
.select('*')
|
||||
.eq('guild_id', guildId);
|
||||
|
||||
const { data: userQuests } = await supabase
|
||||
.from('user_quests')
|
||||
.select('*, quests(*)')
|
||||
.eq('guild_id', guildId);
|
||||
|
||||
const totalQuests = quests?.length || 0;
|
||||
const activeQuests = quests?.filter(q => q.active).length || 0;
|
||||
const totalCompletions = userQuests?.filter(uq => uq.completed).length || 0;
|
||||
const totalClaimed = userQuests?.filter(uq => uq.claimed).length || 0;
|
||||
const uniqueParticipants = new Set(userQuests?.map(uq => uq.user_id)).size;
|
||||
const totalXPRewarded = userQuests
|
||||
?.filter(uq => uq.claimed && uq.quests)
|
||||
.reduce((sum, uq) => sum + (uq.quests.xp_reward || 0), 0) || 0;
|
||||
|
||||
const questCompletions = {};
|
||||
for (const uq of userQuests || []) {
|
||||
if (uq.completed && uq.quests) {
|
||||
const name = uq.quests.name;
|
||||
questCompletions[name] = (questCompletions[name] || 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
const topQuests = Object.entries(questCompletions)
|
||||
.sort(([, a], [, b]) => b - a)
|
||||
.slice(0, 5)
|
||||
.map(([name, count], i) => `${i + 1}. **${name}** (${count} completions)`)
|
||||
.join('\n');
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0x7c3aed)
|
||||
.setTitle('📊 Quest Statistics')
|
||||
.addFields(
|
||||
{ name: '📜 Total Quests', value: `${totalQuests} (${activeQuests} active)`, inline: true },
|
||||
{ name: '✅ Total Completions', value: totalCompletions.toString(), inline: true },
|
||||
{ name: '🎁 Total Claims', value: totalClaimed.toString(), inline: true },
|
||||
{ name: '👥 Participants', value: uniqueParticipants.toString(), inline: true },
|
||||
{ name: '💰 XP Rewarded', value: totalXPRewarded.toLocaleString(), inline: true }
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
if (topQuests) {
|
||||
embed.addFields({ name: '🏆 Most Completed Quests', value: topQuests, inline: false });
|
||||
}
|
||||
|
||||
await interaction.editReply({ embeds: [embed] });
|
||||
} catch (error) {
|
||||
console.error('Quest stats error:', error);
|
||||
await interaction.editReply({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(0xff0000)
|
||||
.setDescription('Failed to get statistics.')
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
451
aethex-bot/commands/quests.js
Normal file
451
aethex-bot/commands/quests.js
Normal file
|
|
@ -0,0 +1,451 @@
|
|||
const { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js');
|
||||
|
||||
const QUEST_TYPES = {
|
||||
daily: { emoji: '☀️', name: 'Daily', color: 0xfbbf24 },
|
||||
weekly: { emoji: '📅', name: 'Weekly', color: 0x3b82f6 },
|
||||
special: { emoji: '⭐', name: 'Special', color: 0xa855f7 }
|
||||
};
|
||||
|
||||
const OBJECTIVES = {
|
||||
messages: { emoji: '💬', name: 'Send Messages' },
|
||||
reactions: { emoji: '😄', name: 'Add Reactions' },
|
||||
voice_minutes: { emoji: '🎙️', name: 'Voice Chat (minutes)' },
|
||||
commands: { emoji: '⚡', name: 'Use Commands' },
|
||||
daily_claims: { emoji: '🎁', name: 'Claim Daily Rewards' },
|
||||
level_ups: { emoji: '📈', name: 'Level Up' },
|
||||
xp_earned: { emoji: '✨', name: 'Earn XP' }
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('quests')
|
||||
.setDescription('View and manage your quests')
|
||||
.addSubcommand(sub =>
|
||||
sub.setName('view')
|
||||
.setDescription('View available quests')
|
||||
.addStringOption(opt =>
|
||||
opt.setName('type')
|
||||
.setDescription('Filter by quest type')
|
||||
.setRequired(false)
|
||||
.addChoices(
|
||||
{ name: '☀️ Daily', value: 'daily' },
|
||||
{ name: '📅 Weekly', value: 'weekly' },
|
||||
{ name: '⭐ Special', value: 'special' }
|
||||
)
|
||||
)
|
||||
)
|
||||
.addSubcommand(sub =>
|
||||
sub.setName('progress')
|
||||
.setDescription('View your quest progress')
|
||||
)
|
||||
.addSubcommand(sub =>
|
||||
sub.setName('claim')
|
||||
.setDescription('Claim rewards for completed quests')
|
||||
.addIntegerOption(opt =>
|
||||
opt.setName('quest_id')
|
||||
.setDescription('The ID of the quest to claim (leave empty to claim all)')
|
||||
.setRequired(false)
|
||||
)
|
||||
),
|
||||
|
||||
async execute(interaction, supabase) {
|
||||
if (!supabase) {
|
||||
return interaction.reply({ content: 'Database not configured.', ephemeral: true });
|
||||
}
|
||||
|
||||
const subcommand = interaction.options.getSubcommand();
|
||||
|
||||
switch (subcommand) {
|
||||
case 'view':
|
||||
return handleView(interaction, supabase);
|
||||
case 'progress':
|
||||
return handleProgress(interaction, supabase);
|
||||
case 'claim':
|
||||
return handleClaim(interaction, supabase);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async function getUserLink(supabase, discordId) {
|
||||
const { data } = await supabase
|
||||
.from('discord_links')
|
||||
.select('user_id')
|
||||
.eq('discord_id', discordId)
|
||||
.maybeSingle();
|
||||
return data;
|
||||
}
|
||||
|
||||
async function handleView(interaction, supabase) {
|
||||
await interaction.deferReply();
|
||||
|
||||
const guildId = interaction.guildId;
|
||||
const questType = interaction.options.getString('type');
|
||||
const now = new Date().toISOString();
|
||||
|
||||
let query = supabase
|
||||
.from('quests')
|
||||
.select('*')
|
||||
.eq('guild_id', guildId)
|
||||
.eq('active', true)
|
||||
.or(`expires_at.is.null,expires_at.gt.${now}`)
|
||||
.or(`starts_at.is.null,starts_at.lte.${now}`)
|
||||
.order('quest_type')
|
||||
.order('xp_reward', { ascending: false });
|
||||
|
||||
if (questType) {
|
||||
query = query.eq('quest_type', questType);
|
||||
}
|
||||
|
||||
const { data: quests, error } = await query;
|
||||
|
||||
if (error || !quests || quests.length === 0) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0xfbbf24)
|
||||
.setTitle('📜 Quests')
|
||||
.setDescription(questType
|
||||
? `No active ${QUEST_TYPES[questType]?.name || questType} quests available.`
|
||||
: 'No active quests available right now.\n\nCheck back later or ask a server admin to add quests!')
|
||||
.setTimestamp();
|
||||
|
||||
return interaction.editReply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
const link = await getUserLink(supabase, interaction.user.id);
|
||||
let userQuests = [];
|
||||
if (link) {
|
||||
const { data } = await supabase
|
||||
.from('user_quests')
|
||||
.select('*')
|
||||
.eq('user_id', link.user_id)
|
||||
.eq('guild_id', guildId);
|
||||
userQuests = data || [];
|
||||
}
|
||||
|
||||
const userQuestMap = {};
|
||||
for (const uq of userQuests) {
|
||||
userQuestMap[uq.quest_id] = uq;
|
||||
}
|
||||
|
||||
const groupedQuests = {};
|
||||
for (const quest of quests) {
|
||||
const type = quest.quest_type || 'special';
|
||||
if (!groupedQuests[type]) groupedQuests[type] = [];
|
||||
groupedQuests[type].push(quest);
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0x7c3aed)
|
||||
.setTitle('📜 Available Quests')
|
||||
.setDescription(link
|
||||
? 'Complete quests to earn XP rewards!'
|
||||
: 'Link your account with `/verify` to track quest progress!')
|
||||
.setThumbnail(interaction.guild.iconURL({ size: 128 }))
|
||||
.setFooter({ text: `Use /quests progress to see your progress • ${quests.length} quests available` })
|
||||
.setTimestamp();
|
||||
|
||||
for (const [type, typeQuests] of Object.entries(groupedQuests)) {
|
||||
const typeInfo = QUEST_TYPES[type] || { emoji: '📋', name: type };
|
||||
const questList = typeQuests.slice(0, 5).map(quest => {
|
||||
const objInfo = OBJECTIVES[quest.objective] || { emoji: '🎯', name: quest.objective };
|
||||
const userQuest = userQuestMap[quest.id];
|
||||
|
||||
let status = '';
|
||||
if (userQuest) {
|
||||
if (userQuest.claimed) {
|
||||
status = ' ✅ Claimed';
|
||||
} else if (userQuest.completed) {
|
||||
status = ' 🎁 Ready to claim!';
|
||||
} else {
|
||||
const percent = Math.min(100, Math.floor((userQuest.progress / quest.target_value) * 100));
|
||||
status = ` [${userQuest.progress}/${quest.target_value}] ${percent}%`;
|
||||
}
|
||||
}
|
||||
|
||||
const expires = quest.expires_at
|
||||
? ` (ends <t:${Math.floor(new Date(quest.expires_at).getTime() / 1000)}:R>)`
|
||||
: '';
|
||||
|
||||
return `\`#${quest.id}\` **${quest.name}**${status}\n${objInfo.emoji} ${quest.target_value}x ${objInfo.name} → **${quest.xp_reward.toLocaleString()} XP**${expires}`;
|
||||
}).join('\n\n');
|
||||
|
||||
const moreQuests = typeQuests.length > 5 ? `\n\n*...and ${typeQuests.length - 5} more*` : '';
|
||||
|
||||
embed.addFields({
|
||||
name: `${typeInfo.emoji} ${typeInfo.name} Quests`,
|
||||
value: questList + moreQuests,
|
||||
inline: false
|
||||
});
|
||||
}
|
||||
|
||||
await interaction.editReply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
async function handleProgress(interaction, supabase) {
|
||||
await interaction.deferReply();
|
||||
|
||||
const guildId = interaction.guildId;
|
||||
const link = await getUserLink(supabase, interaction.user.id);
|
||||
|
||||
if (!link) {
|
||||
return interaction.editReply({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(0xff6b6b)
|
||||
.setDescription('You need to link your account first! Use `/verify` to get started.')
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const { data: quests } = await supabase
|
||||
.from('quests')
|
||||
.select('*')
|
||||
.eq('guild_id', guildId)
|
||||
.eq('active', true)
|
||||
.or(`expires_at.is.null,expires_at.gt.${now}`)
|
||||
.or(`starts_at.is.null,starts_at.lte.${now}`);
|
||||
|
||||
if (!quests || quests.length === 0) {
|
||||
return interaction.editReply({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(0xfbbf24)
|
||||
.setTitle('📊 Quest Progress')
|
||||
.setDescription('No active quests to track.')
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
const questIds = quests.map(q => q.id);
|
||||
const { data: userQuests } = await supabase
|
||||
.from('user_quests')
|
||||
.select('*')
|
||||
.eq('user_id', link.user_id)
|
||||
.eq('guild_id', guildId)
|
||||
.in('quest_id', questIds);
|
||||
|
||||
const userQuestMap = {};
|
||||
for (const uq of userQuests || []) {
|
||||
userQuestMap[uq.quest_id] = uq;
|
||||
}
|
||||
|
||||
const inProgress = [];
|
||||
const completed = [];
|
||||
const notStarted = [];
|
||||
|
||||
for (const quest of quests) {
|
||||
const uq = userQuestMap[quest.id];
|
||||
if (!uq) {
|
||||
notStarted.push(quest);
|
||||
} else if (uq.claimed) {
|
||||
continue;
|
||||
} else if (uq.completed) {
|
||||
completed.push({ quest, userQuest: uq });
|
||||
} else {
|
||||
inProgress.push({ quest, userQuest: uq });
|
||||
}
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0x7c3aed)
|
||||
.setTitle('📊 Your Quest Progress')
|
||||
.setThumbnail(interaction.user.displayAvatarURL({ size: 128 }))
|
||||
.setTimestamp();
|
||||
|
||||
if (completed.length > 0) {
|
||||
const completedList = completed.slice(0, 5).map(({ quest }) => {
|
||||
const typeInfo = QUEST_TYPES[quest.quest_type] || { emoji: '📋' };
|
||||
return `${typeInfo.emoji} \`#${quest.id}\` **${quest.name}** - ${quest.xp_reward.toLocaleString()} XP 🎁`;
|
||||
}).join('\n');
|
||||
embed.addFields({
|
||||
name: '🎉 Ready to Claim!',
|
||||
value: completedList,
|
||||
inline: false
|
||||
});
|
||||
}
|
||||
|
||||
if (inProgress.length > 0) {
|
||||
const progressList = inProgress.slice(0, 5).map(({ quest, userQuest }) => {
|
||||
const typeInfo = QUEST_TYPES[quest.quest_type] || { emoji: '📋' };
|
||||
const percent = Math.min(100, Math.floor((userQuest.progress / quest.target_value) * 100));
|
||||
const progressBar = createProgressBar(percent);
|
||||
return `${typeInfo.emoji} \`#${quest.id}\` **${quest.name}**\n${progressBar} ${userQuest.progress}/${quest.target_value} (${percent}%)`;
|
||||
}).join('\n\n');
|
||||
embed.addFields({
|
||||
name: '🔄 In Progress',
|
||||
value: progressList,
|
||||
inline: false
|
||||
});
|
||||
}
|
||||
|
||||
if (notStarted.length > 0 && (completed.length + inProgress.length) < 5) {
|
||||
const notStartedList = notStarted.slice(0, 3).map(quest => {
|
||||
const typeInfo = QUEST_TYPES[quest.quest_type] || { emoji: '📋' };
|
||||
const objInfo = OBJECTIVES[quest.objective] || { emoji: '🎯', name: quest.objective };
|
||||
return `${typeInfo.emoji} \`#${quest.id}\` **${quest.name}** - ${quest.target_value}x ${objInfo.name}`;
|
||||
}).join('\n');
|
||||
embed.addFields({
|
||||
name: '📝 Not Started',
|
||||
value: notStartedList + (notStarted.length > 3 ? `\n*...and ${notStarted.length - 3} more*` : ''),
|
||||
inline: false
|
||||
});
|
||||
}
|
||||
|
||||
if (completed.length === 0 && inProgress.length === 0 && notStarted.length === 0) {
|
||||
embed.setDescription('You\'ve completed all available quests! Check back later for more.');
|
||||
} else {
|
||||
const totalXP = completed.reduce((sum, { quest }) => sum + quest.xp_reward, 0);
|
||||
if (totalXP > 0) {
|
||||
embed.setDescription(`You have **${totalXP.toLocaleString()} XP** waiting to be claimed! Use \`/quests claim\``);
|
||||
}
|
||||
}
|
||||
|
||||
embed.setFooter({ text: 'Use /quests claim to collect your rewards!' });
|
||||
|
||||
await interaction.editReply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
async function handleClaim(interaction, supabase) {
|
||||
await interaction.deferReply();
|
||||
|
||||
const guildId = interaction.guildId;
|
||||
const questId = interaction.options.getInteger('quest_id');
|
||||
|
||||
const link = await getUserLink(supabase, interaction.user.id);
|
||||
if (!link) {
|
||||
return interaction.editReply({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(0xff6b6b)
|
||||
.setDescription('You need to link your account first! Use `/verify` to get started.')
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
let query = supabase
|
||||
.from('user_quests')
|
||||
.select('*, quests(*)')
|
||||
.eq('user_id', link.user_id)
|
||||
.eq('guild_id', guildId)
|
||||
.eq('completed', true)
|
||||
.eq('claimed', false);
|
||||
|
||||
if (questId) {
|
||||
query = query.eq('quest_id', questId);
|
||||
}
|
||||
|
||||
const { data: claimable, error } = await query;
|
||||
|
||||
if (error || !claimable || claimable.length === 0) {
|
||||
return interaction.editReply({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(0xfbbf24)
|
||||
.setTitle('No Rewards to Claim')
|
||||
.setDescription(questId
|
||||
? 'This quest is not completed or already claimed.'
|
||||
: 'You don\'t have any completed quests to claim.\n\nComplete quests to earn rewards!')
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
let totalXP = 0;
|
||||
const claimedQuests = [];
|
||||
const rolesGranted = [];
|
||||
const now = new Date().toISOString();
|
||||
|
||||
for (const uq of claimable) {
|
||||
const quest = uq.quests;
|
||||
if (!quest) continue;
|
||||
|
||||
totalXP += quest.xp_reward;
|
||||
claimedQuests.push(quest.name);
|
||||
|
||||
await supabase
|
||||
.from('user_quests')
|
||||
.update({ claimed: true, claimed_at: now })
|
||||
.eq('id', uq.id);
|
||||
|
||||
if (quest.role_reward) {
|
||||
try {
|
||||
await interaction.member.roles.add(quest.role_reward);
|
||||
rolesGranted.push(quest.role_reward);
|
||||
} catch (e) {
|
||||
console.error('Failed to add quest role reward:', e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (totalXP > 0) {
|
||||
const { data: profile } = await supabase
|
||||
.from('user_profiles')
|
||||
.select('xp, total_xp_earned')
|
||||
.eq('id', link.user_id)
|
||||
.maybeSingle();
|
||||
|
||||
const currentXP = profile?.xp || 0;
|
||||
const currentTotal = profile?.total_xp_earned || currentXP;
|
||||
|
||||
await supabase
|
||||
.from('user_profiles')
|
||||
.update({
|
||||
xp: currentXP + totalXP,
|
||||
total_xp_earned: currentTotal + totalXP,
|
||||
updated_at: now
|
||||
})
|
||||
.eq('id', link.user_id);
|
||||
|
||||
const { data: balance } = await supabase
|
||||
.from('user_balance')
|
||||
.select('*')
|
||||
.eq('user_id', link.user_id)
|
||||
.eq('guild_id', guildId)
|
||||
.maybeSingle();
|
||||
|
||||
if (balance) {
|
||||
await supabase
|
||||
.from('user_balance')
|
||||
.update({
|
||||
balance: balance.balance + totalXP,
|
||||
total_earned: balance.total_earned + totalXP,
|
||||
updated_at: now
|
||||
})
|
||||
.eq('user_id', link.user_id)
|
||||
.eq('guild_id', guildId);
|
||||
}
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0x00ff00)
|
||||
.setTitle('🎉 Rewards Claimed!')
|
||||
.setDescription(`You claimed rewards for **${claimable.length}** quest${claimable.length > 1 ? 's' : ''}!`)
|
||||
.addFields(
|
||||
{ name: '💰 XP Earned', value: `**+${totalXP.toLocaleString()} XP**`, inline: true }
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
if (claimedQuests.length <= 5) {
|
||||
embed.addFields({
|
||||
name: '📜 Quests Completed',
|
||||
value: claimedQuests.map(n => `• ${n}`).join('\n'),
|
||||
inline: false
|
||||
});
|
||||
}
|
||||
|
||||
if (rolesGranted.length > 0) {
|
||||
embed.addFields({
|
||||
name: '👑 Roles Granted',
|
||||
value: rolesGranted.map(r => `<@&${r}>`).join(', '),
|
||||
inline: false
|
||||
});
|
||||
}
|
||||
|
||||
await interaction.editReply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
function createProgressBar(percent) {
|
||||
const filled = Math.floor(percent / 10);
|
||||
const empty = 10 - filled;
|
||||
return '█'.repeat(filled) + '░'.repeat(empty);
|
||||
}
|
||||
|
|
@ -371,6 +371,60 @@ Spend your hard-earned XP on cosmetics, perks, and exclusive items!
|
|||
- For roles: specify `grant_role` to give users a role on purchase
|
||||
- For boosters: set `booster_multiplier` and `booster_hours`
|
||||
|
||||
### Quest System
|
||||
|
||||
Complete quests to earn bonus XP! Quests provide rotating objectives that keep engagement fresh.
|
||||
|
||||
**User Commands:**
|
||||
```
|
||||
/quests view [type] # View available quests
|
||||
/quests progress # See your current progress
|
||||
/quests claim [quest_id] # Claim rewards for completed quests
|
||||
```
|
||||
|
||||
**Quest Types:**
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| Daily | Reset every day, quick objectives |
|
||||
| Weekly | Larger goals, higher rewards |
|
||||
| Special | Limited-time events and challenges |
|
||||
|
||||
**Quest Objectives:**
|
||||
| Objective | What to Do |
|
||||
|-----------|------------|
|
||||
| messages | Send messages in the server |
|
||||
| reactions | Add reactions to messages |
|
||||
| voice_minutes | Spend time in voice channels |
|
||||
| commands | Use bot commands |
|
||||
| daily_claims | Claim daily rewards |
|
||||
| level_ups | Level up your account |
|
||||
| xp_earned | Earn XP from any source |
|
||||
|
||||
**How Quests Work:**
|
||||
1. View available quests with `/quests view`
|
||||
2. Progress is tracked automatically as you participate
|
||||
3. Check your progress with `/quests progress`
|
||||
4. Once completed, claim your XP with `/quests claim`
|
||||
5. Some quests may also grant roles as rewards!
|
||||
|
||||
**Admin Commands (quests-manage):**
|
||||
```
|
||||
/quests-manage create [name] [type] [objective] [target] [xp_reward]
|
||||
/quests-manage edit <quest_id> [options]
|
||||
/quests-manage delete <quest_id>
|
||||
/quests-manage list
|
||||
/quests-manage reset [quest_id] [type]
|
||||
/quests-manage stats
|
||||
```
|
||||
|
||||
**Creating Quests:**
|
||||
- `name`: Quest display name
|
||||
- `type`: daily, weekly, or special
|
||||
- `objective`: What users need to do
|
||||
- `target`: Amount needed to complete
|
||||
- `xp_reward`: XP given on completion
|
||||
- Optional: description, role_reward, duration_hours, repeatable
|
||||
|
||||
---
|
||||
|
||||
## Moderation Tools
|
||||
|
|
@ -684,6 +738,12 @@ Opens a form to set:
|
|||
| `/shop browse/buy/inventory/equip/balance` | XP shop for cosmetics and perks |
|
||||
| `/shop-manage add/edit/remove/list/stats` | Admin shop management |
|
||||
|
||||
### Quest Commands (2)
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/quests view/progress/claim` | View and complete quests for XP |
|
||||
| `/quests-manage create/edit/delete/list/reset/stats` | Admin quest management |
|
||||
|
||||
### Moderation Commands (5)
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
const { checkMilestoneRoles, calculateLevel, updateUserStats, getUserStats, updatePeriodicXp } = require('./xpTracker');
|
||||
const { checkMilestoneRoles, calculateLevel, updateUserStats, getUserStats, updatePeriodicXp, updateQuestProgress } = require('./xpTracker');
|
||||
const { checkAchievements } = require('../commands/achievements');
|
||||
|
||||
const reactionCooldowns = new Map();
|
||||
|
|
@ -153,6 +153,12 @@ async function grantXp(supabase, discordUserId, xpAmount, client, member, config
|
|||
stats.dailyStreak = profile.daily_streak || 0;
|
||||
|
||||
await checkAchievements(link.user_id, member, stats, supabase, guildId, client);
|
||||
|
||||
// Track quest progress for reactions
|
||||
if (reactionType === 'giver') {
|
||||
await updateQuestProgress(supabase, link.user_id, guildId, 'reactions', 1);
|
||||
}
|
||||
await updateQuestProgress(supabase, link.user_id, guildId, 'xp_earned', finalXp);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
const { checkMilestoneRoles, calculateLevel, updateUserStats, getUserStats, updatePeriodicXp } = require('./xpTracker');
|
||||
const { checkMilestoneRoles, calculateLevel, updateUserStats, getUserStats, updatePeriodicXp, updateQuestProgress } = require('./xpTracker');
|
||||
const { checkAchievements } = require('../commands/achievements');
|
||||
|
||||
const voiceSessions = new Map();
|
||||
|
|
@ -233,6 +233,14 @@ async function grantVoiceXp(supabase, client, guildId, userId, config, member, m
|
|||
stats.dailyStreak = profile.daily_streak || 0;
|
||||
|
||||
await checkAchievements(link.user_id, member, stats, supabase, guildId, client);
|
||||
|
||||
// Track quest progress for voice chat
|
||||
await updateQuestProgress(supabase, link.user_id, guildId, 'voice_minutes', minutes);
|
||||
await updateQuestProgress(supabase, link.user_id, guildId, 'xp_earned', xpGain);
|
||||
|
||||
if (newLevel > oldLevel) {
|
||||
await updateQuestProgress(supabase, link.user_id, guildId, 'level_ups', 1);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,78 @@ const xpCooldowns = new Map();
|
|||
const xpConfigCache = new Map();
|
||||
const CACHE_TTL = 60000; // 1 minute cache
|
||||
|
||||
async function updateQuestProgress(supabase, userId, guildId, objective, amount = 1) {
|
||||
try {
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const { data: allQuests } = await supabase
|
||||
.from('quests')
|
||||
.select('*')
|
||||
.eq('guild_id', guildId)
|
||||
.eq('objective', objective)
|
||||
.eq('active', true);
|
||||
|
||||
if (!allQuests || allQuests.length === 0) return;
|
||||
|
||||
const activeQuests = allQuests.filter(quest => {
|
||||
const notExpired = !quest.expires_at || new Date(quest.expires_at) > new Date(now);
|
||||
const hasStarted = !quest.starts_at || new Date(quest.starts_at) <= new Date(now);
|
||||
return notExpired && hasStarted;
|
||||
});
|
||||
|
||||
if (!activeQuests || activeQuests.length === 0) return;
|
||||
|
||||
for (const quest of activeQuests) {
|
||||
const { data: userQuest } = await supabase
|
||||
.from('user_quests')
|
||||
.select('*')
|
||||
.eq('user_id', userId)
|
||||
.eq('guild_id', guildId)
|
||||
.eq('quest_id', quest.id)
|
||||
.maybeSingle();
|
||||
|
||||
if (userQuest) {
|
||||
if (userQuest.claimed && !quest.repeatable) continue;
|
||||
if (userQuest.completed && !quest.repeatable) continue;
|
||||
|
||||
const newProgress = (userQuest.progress || 0) + amount;
|
||||
const completed = newProgress >= quest.target_value;
|
||||
|
||||
const updates = {
|
||||
progress: Math.min(newProgress, quest.target_value)
|
||||
};
|
||||
|
||||
if (completed && !userQuest.completed) {
|
||||
updates.completed = true;
|
||||
updates.completed_at = now;
|
||||
}
|
||||
|
||||
await supabase
|
||||
.from('user_quests')
|
||||
.update(updates)
|
||||
.eq('id', userQuest.id);
|
||||
} else {
|
||||
const progress = Math.min(amount, quest.target_value);
|
||||
const completed = progress >= quest.target_value;
|
||||
|
||||
await supabase
|
||||
.from('user_quests')
|
||||
.insert({
|
||||
user_id: userId,
|
||||
guild_id: guildId,
|
||||
quest_id: quest.id,
|
||||
progress: progress,
|
||||
completed: completed,
|
||||
completed_at: completed ? now : null,
|
||||
started_at: now
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Silently ignore quest tracking errors
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
name: 'messageCreate',
|
||||
|
||||
|
|
@ -142,6 +214,15 @@ module.exports = {
|
|||
// Check for achievements
|
||||
await checkAchievements(link.user_id, message.member, stats, supabase, guildId, client);
|
||||
|
||||
// Track quest progress for messages and XP earned
|
||||
await updateQuestProgress(supabase, link.user_id, guildId, 'messages', 1);
|
||||
await updateQuestProgress(supabase, link.user_id, guildId, 'xp_earned', xpGain);
|
||||
|
||||
// Track level-up quest progress
|
||||
if (newLevel > oldLevel) {
|
||||
await updateQuestProgress(supabase, link.user_id, guildId, 'level_ups', 1);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('XP tracking error:', error.message);
|
||||
}
|
||||
|
|
@ -462,3 +543,4 @@ module.exports.checkMilestoneRoles = checkMilestoneRoles;
|
|||
module.exports.updateUserStats = updateUserStats;
|
||||
module.exports.getUserStats = getUserStats;
|
||||
module.exports.updatePeriodicXp = updatePeriodicXp;
|
||||
module.exports.updateQuestProgress = updateQuestProgress;
|
||||
|
|
|
|||
Loading…
Reference in a new issue