const { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, StringSelectMenuBuilder } = require('discord.js'); const ITEMS_PER_PAGE = 5; const CATEGORY_INFO = { badge: { emoji: '🏅', name: 'Badges', description: 'Collectible profile badges' }, title: { emoji: '🏷️', name: 'Titles', description: 'Custom titles for your profile' }, background: { emoji: '🎨', name: 'Backgrounds', description: 'Profile card backgrounds' }, booster: { emoji: '⚡', name: 'Boosters', description: 'Temporary XP multipliers' }, role: { emoji: '👑', name: 'Roles', description: 'Purchasable server roles' }, special: { emoji: '✨', name: 'Special', description: 'Limited edition items' } }; module.exports = { data: new SlashCommandBuilder() .setName('shop') .setDescription('Browse and purchase items with your XP') .addSubcommand(sub => sub.setName('browse') .setDescription('Browse available items') .addStringOption(opt => opt.setName('category') .setDescription('Filter by category') .setRequired(false) .addChoices( { name: '🏅 Badges', value: 'badge' }, { name: '🏷️ Titles', value: 'title' }, { name: '🎨 Backgrounds', value: 'background' }, { name: '⚡ Boosters', value: 'booster' }, { name: '👑 Roles', value: 'role' }, { name: '✨ Special', value: 'special' } ) ) ) .addSubcommand(sub => sub.setName('buy') .setDescription('Purchase an item') .addIntegerOption(opt => opt.setName('item_id') .setDescription('The ID of the item to purchase') .setRequired(true) ) ) .addSubcommand(sub => sub.setName('inventory') .setDescription('View your purchased items') ) .addSubcommand(sub => sub.setName('equip') .setDescription('Equip an item from your inventory') .addIntegerOption(opt => opt.setName('item_id') .setDescription('The ID of the item to equip') .setRequired(true) ) ) .addSubcommand(sub => sub.setName('balance') .setDescription('Check your XP balance') ), async execute(interaction, supabase) { if (!supabase) { return interaction.reply({ content: 'Database not configured.', ephemeral: true }); } const subcommand = interaction.options.getSubcommand(); switch (subcommand) { case 'browse': return handleBrowse(interaction, supabase); case 'buy': return handleBuy(interaction, supabase); case 'inventory': return handleInventory(interaction, supabase); case 'equip': return handleEquip(interaction, supabase); case 'balance': return handleBalance(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 getUserBalance(supabase, userId, guildId) { const { data } = await supabase .from('user_balance') .select('*') .eq('user_id', userId) .eq('guild_id', guildId) .maybeSingle(); if (!data) { const { data: profile } = await supabase .from('user_profiles') .select('xp, total_xp_earned') .eq('id', userId) .maybeSingle(); const totalEarned = profile?.total_xp_earned || profile?.xp || 0; await supabase .from('user_balance') .insert({ user_id: userId, guild_id: guildId, balance: totalEarned, total_earned: totalEarned }); return { balance: totalEarned, total_spent: 0, total_earned: totalEarned }; } return data; } async function syncBalanceWithXP(supabase, userId, guildId) { const { data: profile } = await supabase .from('user_profiles') .select('xp, total_xp_earned') .eq('id', userId) .maybeSingle(); const totalXpEarned = profile?.total_xp_earned || profile?.xp || 0; const { data: balance } = await supabase .from('user_balance') .select('*') .eq('user_id', userId) .eq('guild_id', guildId) .maybeSingle(); if (balance) { const newBalance = totalXpEarned - balance.total_spent; await supabase .from('user_balance') .update({ balance: Math.max(0, newBalance), total_earned: totalXpEarned, updated_at: new Date().toISOString() }) .eq('user_id', userId) .eq('guild_id', guildId); return { balance: Math.max(0, newBalance), total_spent: balance.total_spent, total_earned: totalXpEarned }; } else { await supabase .from('user_balance') .insert({ user_id: userId, guild_id: guildId, balance: totalXpEarned, total_earned: totalXpEarned }); return { balance: totalXpEarned, total_spent: 0, total_earned: totalXpEarned }; } } async function handleBrowse(interaction, supabase) { await interaction.deferReply(); const guildId = interaction.guildId; const category = interaction.options.getString('category'); let query = supabase .from('shop_items') .select('*') .eq('guild_id', guildId) .eq('enabled', true) .order('item_type') .order('price'); if (category) { query = query.eq('item_type', category); } const { data: items, error } = await query; if (error || !items || items.length === 0) { const embed = new EmbedBuilder() .setColor(0xfbbf24) .setTitle('🛒 Shop') .setDescription(category ? `No items available in the **${CATEGORY_INFO[category]?.name || category}** category.` : 'No items available in the shop yet!\n\nAsk a server admin to add items with `/shop-manage add`.') .setTimestamp(); return interaction.editReply({ embeds: [embed] }); } const link = await getUserLink(supabase, interaction.user.id); let balanceInfo = null; if (link) { balanceInfo = await syncBalanceWithXP(supabase, link.user_id, guildId); } const groupedItems = {}; for (const item of items) { const cat = item.item_type || 'other'; if (!groupedItems[cat]) groupedItems[cat] = []; groupedItems[cat].push(item); } const embed = new EmbedBuilder() .setColor(0x7c3aed) .setTitle('🛒 AeThex Shop') .setDescription(balanceInfo ? `Your Balance: **${balanceInfo.balance.toLocaleString()} XP** 💰` : 'Link your account with `/verify` to purchase items!') .setThumbnail(interaction.guild.iconURL({ size: 128 })) .setFooter({ text: `Use /shop buy to purchase • ${items.length} items available` }) .setTimestamp(); for (const [cat, catItems] of Object.entries(groupedItems)) { const catInfo = CATEGORY_INFO[cat] || { emoji: '📦', name: cat }; const itemList = catItems.slice(0, 5).map(item => { const stockInfo = item.stock !== null ? ` (${item.stock} left)` : ''; const reqInfo = []; if (item.level_required > 0) reqInfo.push(`Lvl ${item.level_required}`); if (item.prestige_required > 0) reqInfo.push(`P${item.prestige_required}`); const reqStr = reqInfo.length > 0 ? ` [${reqInfo.join(', ')}]` : ''; return `\`#${item.id}\` **${item.name}** - ${item.price.toLocaleString()} XP${stockInfo}${reqStr}`; }).join('\n'); const moreItems = catItems.length > 5 ? `\n*...and ${catItems.length - 5} more*` : ''; embed.addFields({ name: `${catInfo.emoji} ${catInfo.name}`, value: itemList + moreItems, inline: false }); } await interaction.editReply({ embeds: [embed] }); } async function handleBuy(interaction, supabase) { await interaction.deferReply({ ephemeral: true }); const guildId = interaction.guildId; const itemId = interaction.options.getInteger('item_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.') ] }); } const { data: item, error: itemError } = await supabase .from('shop_items') .select('*') .eq('id', itemId) .eq('guild_id', guildId) .eq('enabled', true) .maybeSingle(); if (itemError || !item) { return interaction.editReply({ embeds: [ new EmbedBuilder() .setColor(0xff6b6b) .setDescription('Item not found or no longer available.') ] }); } const { data: profile } = await supabase .from('user_profiles') .select('xp, prestige_level') .eq('id', link.user_id) .maybeSingle(); const currentLevel = Math.floor(Math.sqrt((profile?.xp || 0) / 100)); const prestige = profile?.prestige_level || 0; if (item.level_required > 0 && currentLevel < item.level_required) { return interaction.editReply({ embeds: [ new EmbedBuilder() .setColor(0xff6b6b) .setTitle('❌ Level Requirement Not Met') .setDescription(`You need to be **Level ${item.level_required}** to purchase this item.\nYour current level: **${currentLevel}**`) ] }); } if (item.prestige_required > 0 && prestige < item.prestige_required) { return interaction.editReply({ embeds: [ new EmbedBuilder() .setColor(0xff6b6b) .setTitle('❌ Prestige Requirement Not Met') .setDescription(`You need to be **Prestige ${item.prestige_required}** to purchase this item.\nYour current prestige: **${prestige}**`) ] }); } if (item.role_required) { if (!interaction.member.roles.cache.has(item.role_required)) { return interaction.editReply({ embeds: [ new EmbedBuilder() .setColor(0xff6b6b) .setTitle('❌ Role Requirement Not Met') .setDescription(`You need a specific role to purchase this item.`) ] }); } } const { data: existingItem } = await supabase .from('user_inventory') .select('*') .eq('user_id', link.user_id) .eq('guild_id', guildId) .eq('item_id', itemId) .maybeSingle(); if (existingItem && item.item_type !== 'booster') { return interaction.editReply({ embeds: [ new EmbedBuilder() .setColor(0xfbbf24) .setTitle('Already Owned') .setDescription(`You already own **${item.name}**!`) ] }); } if (item.stock !== null && item.stock <= 0) { return interaction.editReply({ embeds: [ new EmbedBuilder() .setColor(0xff6b6b) .setTitle('Out of Stock') .setDescription(`**${item.name}** is currently out of stock.`) ] }); } const balance = await syncBalanceWithXP(supabase, link.user_id, guildId); if (balance.balance < item.price) { return interaction.editReply({ embeds: [ new EmbedBuilder() .setColor(0xff6b6b) .setTitle('💸 Insufficient XP') .setDescription(`You need **${item.price.toLocaleString()} XP** to purchase this item.\nYour balance: **${balance.balance.toLocaleString()} XP**\nYou need **${(item.price - balance.balance).toLocaleString()}** more XP!`) ] }); } const newBalance = balance.balance - item.price; const newTotalSpent = (balance.total_spent || 0) + item.price; await supabase .from('user_balance') .update({ balance: newBalance, total_spent: newTotalSpent, updated_at: new Date().toISOString() }) .eq('user_id', link.user_id) .eq('guild_id', guildId); let expiresAt = null; if (item.item_type === 'booster' && item.item_data?.duration_hours) { expiresAt = new Date(Date.now() + item.item_data.duration_hours * 60 * 60 * 1000).toISOString(); } if (existingItem && item.item_type === 'booster') { await supabase .from('user_inventory') .update({ quantity: existingItem.quantity + 1, purchased_at: new Date().toISOString(), expires_at: expiresAt }) .eq('id', existingItem.id); } else { await supabase .from('user_inventory') .insert({ user_id: link.user_id, guild_id: guildId, item_id: itemId, quantity: 1, expires_at: expiresAt }); } if (item.stock !== null) { await supabase .from('shop_items') .update({ stock: item.stock - 1 }) .eq('id', itemId); } if (item.item_type === 'role' && item.item_data?.role_id) { try { await interaction.member.roles.add(item.item_data.role_id); } catch (e) { console.error('Failed to add role:', e.message); } } const embed = new EmbedBuilder() .setColor(0x00ff00) .setTitle('✅ Purchase Successful!') .setDescription(`You purchased **${item.name}**!`) .addFields( { name: 'Price Paid', value: `${item.price.toLocaleString()} XP`, inline: true }, { name: 'New Balance', value: `${newBalance.toLocaleString()} XP`, inline: true } ) .setTimestamp(); if (expiresAt) { embed.addFields({ name: '⏰ Expires', value: ``, inline: true }); } if (item.item_type === 'badge' || item.item_type === 'title' || item.item_type === 'background') { embed.setFooter({ text: 'Use /shop equip to equip this item!' }); } await interaction.editReply({ embeds: [embed] }); } async function handleInventory(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 { data: inventory, error } = await supabase .from('user_inventory') .select('*, shop_items(*)') .eq('user_id', link.user_id) .eq('guild_id', guildId); if (error || !inventory || inventory.length === 0) { return interaction.editReply({ embeds: [ new EmbedBuilder() .setColor(0xfbbf24) .setTitle('🎒 Your Inventory') .setDescription('Your inventory is empty!\n\nBrowse the shop with `/shop browse` to find items.') ] }); } const now = new Date(); const validItems = inventory.filter(inv => { if (inv.expires_at && new Date(inv.expires_at) < now) return false; return inv.shop_items; }); if (validItems.length === 0) { return interaction.editReply({ embeds: [ new EmbedBuilder() .setColor(0xfbbf24) .setTitle('🎒 Your Inventory') .setDescription('Your inventory is empty or all items have expired!') ] }); } const groupedItems = {}; for (const inv of validItems) { const cat = inv.shop_items.item_type || 'other'; if (!groupedItems[cat]) groupedItems[cat] = []; groupedItems[cat].push(inv); } const embed = new EmbedBuilder() .setColor(0x7c3aed) .setTitle('🎒 Your Inventory') .setDescription(`You own **${validItems.length}** items`) .setThumbnail(interaction.user.displayAvatarURL({ size: 128 })) .setFooter({ text: 'Use /shop equip to equip items' }) .setTimestamp(); for (const [cat, catItems] of Object.entries(groupedItems)) { const catInfo = CATEGORY_INFO[cat] || { emoji: '📦', name: cat }; const itemList = catItems.map(inv => { const item = inv.shop_items; const equipped = inv.equipped ? ' ✅' : ''; const qty = inv.quantity > 1 ? ` x${inv.quantity}` : ''; const expires = inv.expires_at ? ` (expires )` : ''; return `\`#${item.id}\` **${item.name}**${qty}${equipped}${expires}`; }).join('\n'); embed.addFields({ name: `${catInfo.emoji} ${catInfo.name}`, value: itemList, inline: false }); } await interaction.editReply({ embeds: [embed] }); } async function handleEquip(interaction, supabase) { await interaction.deferReply({ ephemeral: true }); const guildId = interaction.guildId; const itemId = interaction.options.getInteger('item_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.') ] }); } const { data: invItem, error } = await supabase .from('user_inventory') .select('*, shop_items(*)') .eq('user_id', link.user_id) .eq('guild_id', guildId) .eq('item_id', itemId) .maybeSingle(); if (error || !invItem) { return interaction.editReply({ embeds: [ new EmbedBuilder() .setColor(0xff6b6b) .setDescription('You don\'t own this item!') ] }); } if (invItem.expires_at && new Date(invItem.expires_at) < new Date()) { return interaction.editReply({ embeds: [ new EmbedBuilder() .setColor(0xff6b6b) .setDescription('This item has expired!') ] }); } const itemType = invItem.shop_items.item_type; if (!['badge', 'title', 'background'].includes(itemType)) { return interaction.editReply({ embeds: [ new EmbedBuilder() .setColor(0xfbbf24) .setDescription('This item type cannot be equipped.') ] }); } await supabase .from('user_inventory') .update({ equipped: false }) .eq('user_id', link.user_id) .eq('guild_id', guildId) .in('item_id', (await supabase .from('shop_items') .select('id') .eq('guild_id', guildId) .eq('item_type', itemType) ).data?.map(i => i.id) || [] ); const newEquipped = !invItem.equipped; await supabase .from('user_inventory') .update({ equipped: newEquipped }) .eq('id', invItem.id); const embed = new EmbedBuilder() .setColor(0x00ff00) .setTitle(newEquipped ? '✅ Item Equipped!' : '❌ Item Unequipped') .setDescription(`**${invItem.shop_items.name}** has been ${newEquipped ? 'equipped' : 'unequipped'}.`) .setTimestamp(); await interaction.editReply({ embeds: [embed] }); } async function handleBalance(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 balance = await syncBalanceWithXP(supabase, link.user_id, guildId); const { data: profile } = await supabase .from('user_profiles') .select('xp, prestige_level') .eq('id', link.user_id) .maybeSingle(); const level = Math.floor(Math.sqrt((profile?.xp || 0) / 100)); const prestige = profile?.prestige_level || 0; const embed = new EmbedBuilder() .setColor(0x7c3aed) .setTitle('💰 Your Balance') .setThumbnail(interaction.user.displayAvatarURL({ size: 128 })) .addFields( { name: '💵 Available XP', value: `**${balance.balance.toLocaleString()}** XP`, inline: true }, { name: '📈 Total Earned', value: `${balance.total_earned.toLocaleString()} XP`, inline: true }, { name: '🛒 Total Spent', value: `${balance.total_spent.toLocaleString()} XP`, inline: true }, { name: '📊 Level', value: `${level}${prestige > 0 ? ` (P${prestige})` : ''}`, inline: true } ) .setFooter({ text: 'Earn XP by chatting, reacting, and joining voice!' }) .setTimestamp(); await interaction.editReply({ embeds: [embed] }); }