AeThex-Bot-Master/aethex-bot/commands/quests.js
sirpiglr 7c10440040 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
2025-12-08 22:13:28 +00:00

451 lines
14 KiB
JavaScript

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);
}