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
605 lines
18 KiB
JavaScript
605 lines
18 KiB
JavaScript
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.')
|
|
]
|
|
});
|
|
}
|
|
}
|