Add a shop system for users to spend XP on in-game items
Introduces a new XP shop system with user-facing commands (/shop) and admin management commands (/shop-manage). Also updates the manual documentation with details on the shop system and commands. Includes database table creation for shop items, user inventory, and user balances. Replit-Commit-Author: Agent Replit-Commit-Session-Id: aed2e46d-25bb-4b73-81a1-bb9e8437c261 Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Event-Id: 9c137adc-a98f-45ee-bd82-83bfe97bdcab 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
e5f6956392
commit
98aedc46e7
4 changed files with 1221 additions and 5 deletions
4
.replit
4
.replit
|
|
@ -22,10 +22,6 @@ externalPort = 80
|
|||
localPort = 8080
|
||||
externalPort = 8080
|
||||
|
||||
[[ports]]
|
||||
localPort = 35463
|
||||
externalPort = 3000
|
||||
|
||||
[workflows]
|
||||
runButton = "Project"
|
||||
|
||||
|
|
|
|||
535
aethex-bot/commands/shop-manage.js
Normal file
535
aethex-bot/commands/shop-manage.js
Normal file
|
|
@ -0,0 +1,535 @@
|
|||
const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } = require('discord.js');
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('shop-manage')
|
||||
.setDescription('Manage the XP shop (Admin only)')
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator)
|
||||
.addSubcommand(sub =>
|
||||
sub.setName('add')
|
||||
.setDescription('Add a new shop item')
|
||||
.addStringOption(opt =>
|
||||
opt.setName('name')
|
||||
.setDescription('Item name')
|
||||
.setRequired(true)
|
||||
)
|
||||
.addIntegerOption(opt =>
|
||||
opt.setName('price')
|
||||
.setDescription('Price in XP')
|
||||
.setRequired(true)
|
||||
.setMinValue(1)
|
||||
)
|
||||
.addStringOption(opt =>
|
||||
opt.setName('type')
|
||||
.setDescription('Item type')
|
||||
.setRequired(true)
|
||||
.addChoices(
|
||||
{ name: '🏅 Badge', value: 'badge' },
|
||||
{ name: '🏷️ Title', value: 'title' },
|
||||
{ name: '🎨 Background', value: 'background' },
|
||||
{ name: '⚡ XP Booster', value: 'booster' },
|
||||
{ name: '👑 Role', value: 'role' },
|
||||
{ name: '✨ Special', value: 'special' }
|
||||
)
|
||||
)
|
||||
.addStringOption(opt =>
|
||||
opt.setName('description')
|
||||
.setDescription('Item description')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addIntegerOption(opt =>
|
||||
opt.setName('stock')
|
||||
.setDescription('Limited stock (leave empty for unlimited)')
|
||||
.setRequired(false)
|
||||
.setMinValue(1)
|
||||
)
|
||||
.addIntegerOption(opt =>
|
||||
opt.setName('level_required')
|
||||
.setDescription('Minimum level to purchase')
|
||||
.setRequired(false)
|
||||
.setMinValue(1)
|
||||
)
|
||||
.addIntegerOption(opt =>
|
||||
opt.setName('prestige_required')
|
||||
.setDescription('Minimum prestige to purchase')
|
||||
.setRequired(false)
|
||||
.setMinValue(1)
|
||||
)
|
||||
.addRoleOption(opt =>
|
||||
opt.setName('role_required')
|
||||
.setDescription('Role required to purchase')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addRoleOption(opt =>
|
||||
opt.setName('grant_role')
|
||||
.setDescription('Role to grant on purchase (for role type items)')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addNumberOption(opt =>
|
||||
opt.setName('booster_multiplier')
|
||||
.setDescription('XP multiplier for boosters (e.g., 1.5 for 50% bonus)')
|
||||
.setRequired(false)
|
||||
.setMinValue(1.1)
|
||||
.setMaxValue(5)
|
||||
)
|
||||
.addIntegerOption(opt =>
|
||||
opt.setName('booster_hours')
|
||||
.setDescription('Duration in hours for boosters')
|
||||
.setRequired(false)
|
||||
.setMinValue(1)
|
||||
.setMaxValue(168)
|
||||
)
|
||||
.addStringOption(opt =>
|
||||
opt.setName('badge_emoji')
|
||||
.setDescription('Emoji for badge items')
|
||||
.setRequired(false)
|
||||
)
|
||||
)
|
||||
.addSubcommand(sub =>
|
||||
sub.setName('remove')
|
||||
.setDescription('Remove a shop item')
|
||||
.addIntegerOption(opt =>
|
||||
opt.setName('item_id')
|
||||
.setDescription('The ID of the item to remove')
|
||||
.setRequired(true)
|
||||
)
|
||||
)
|
||||
.addSubcommand(sub =>
|
||||
sub.setName('edit')
|
||||
.setDescription('Edit an existing shop item')
|
||||
.addIntegerOption(opt =>
|
||||
opt.setName('item_id')
|
||||
.setDescription('The ID of the item to edit')
|
||||
.setRequired(true)
|
||||
)
|
||||
.addStringOption(opt =>
|
||||
opt.setName('name')
|
||||
.setDescription('New item name')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addIntegerOption(opt =>
|
||||
opt.setName('price')
|
||||
.setDescription('New price in XP')
|
||||
.setRequired(false)
|
||||
.setMinValue(1)
|
||||
)
|
||||
.addStringOption(opt =>
|
||||
opt.setName('description')
|
||||
.setDescription('New item description')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addIntegerOption(opt =>
|
||||
opt.setName('stock')
|
||||
.setDescription('New stock amount (-1 for unlimited)')
|
||||
.setRequired(false)
|
||||
.setMinValue(-1)
|
||||
)
|
||||
.addBooleanOption(opt =>
|
||||
opt.setName('enabled')
|
||||
.setDescription('Enable or disable the item')
|
||||
.setRequired(false)
|
||||
)
|
||||
)
|
||||
.addSubcommand(sub =>
|
||||
sub.setName('list')
|
||||
.setDescription('List all shop items with details')
|
||||
)
|
||||
.addSubcommand(sub =>
|
||||
sub.setName('stats')
|
||||
.setDescription('View shop statistics')
|
||||
),
|
||||
|
||||
async execute(interaction, supabase) {
|
||||
if (!supabase) {
|
||||
return interaction.reply({ content: 'Database not configured.', ephemeral: true });
|
||||
}
|
||||
|
||||
const subcommand = interaction.options.getSubcommand();
|
||||
|
||||
switch (subcommand) {
|
||||
case 'add':
|
||||
return handleAdd(interaction, supabase);
|
||||
case 'remove':
|
||||
return handleRemove(interaction, supabase);
|
||||
case 'edit':
|
||||
return handleEdit(interaction, supabase);
|
||||
case 'list':
|
||||
return handleList(interaction, supabase);
|
||||
case 'stats':
|
||||
return handleStats(interaction, supabase);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async function handleAdd(interaction, supabase) {
|
||||
await interaction.deferReply({ ephemeral: true });
|
||||
|
||||
const guildId = interaction.guildId;
|
||||
const name = interaction.options.getString('name');
|
||||
const price = interaction.options.getInteger('price');
|
||||
const itemType = interaction.options.getString('type');
|
||||
const description = interaction.options.getString('description') || '';
|
||||
const stock = interaction.options.getInteger('stock') || null;
|
||||
const levelRequired = interaction.options.getInteger('level_required') || 0;
|
||||
const prestigeRequired = interaction.options.getInteger('prestige_required') || 0;
|
||||
const roleRequired = interaction.options.getRole('role_required');
|
||||
const grantRole = interaction.options.getRole('grant_role');
|
||||
const boosterMultiplier = interaction.options.getNumber('booster_multiplier');
|
||||
const boosterHours = interaction.options.getInteger('booster_hours');
|
||||
const badgeEmoji = interaction.options.getString('badge_emoji');
|
||||
|
||||
const itemData = {};
|
||||
if (itemType === 'role' && grantRole) {
|
||||
itemData.role_id = grantRole.id;
|
||||
itemData.role_name = grantRole.name;
|
||||
}
|
||||
if (itemType === 'booster') {
|
||||
itemData.multiplier = boosterMultiplier || 1.5;
|
||||
itemData.duration_hours = boosterHours || 24;
|
||||
}
|
||||
if (itemType === 'badge' && badgeEmoji) {
|
||||
itemData.emoji = badgeEmoji;
|
||||
}
|
||||
|
||||
try {
|
||||
const { data: newItem, error } = await supabase
|
||||
.from('shop_items')
|
||||
.insert({
|
||||
guild_id: guildId,
|
||||
name: name,
|
||||
description: description,
|
||||
item_type: itemType,
|
||||
price: price,
|
||||
stock: stock,
|
||||
level_required: levelRequired,
|
||||
prestige_required: prestigeRequired,
|
||||
role_required: roleRequired?.id || null,
|
||||
item_data: itemData,
|
||||
enabled: true
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0x00ff00)
|
||||
.setTitle('✅ Item Added to Shop')
|
||||
.addFields(
|
||||
{ name: 'Item ID', value: `#${newItem.id}`, inline: true },
|
||||
{ name: 'Name', value: name, inline: true },
|
||||
{ name: 'Price', value: `${price.toLocaleString()} XP`, inline: true },
|
||||
{ name: 'Type', value: itemType, inline: true },
|
||||
{ name: 'Stock', value: stock ? stock.toString() : 'Unlimited', inline: true }
|
||||
);
|
||||
|
||||
if (description) {
|
||||
embed.addFields({ name: 'Description', value: description, inline: false });
|
||||
}
|
||||
|
||||
if (levelRequired > 0 || prestigeRequired > 0) {
|
||||
const reqs = [];
|
||||
if (levelRequired > 0) reqs.push(`Level ${levelRequired}`);
|
||||
if (prestigeRequired > 0) reqs.push(`Prestige ${prestigeRequired}`);
|
||||
embed.addFields({ name: 'Requirements', value: reqs.join(', '), inline: true });
|
||||
}
|
||||
|
||||
embed.setTimestamp();
|
||||
|
||||
await interaction.editReply({ embeds: [embed] });
|
||||
} catch (error) {
|
||||
console.error('Shop add error:', error);
|
||||
await interaction.editReply({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(0xff0000)
|
||||
.setDescription('Failed to add item to shop.')
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRemove(interaction, supabase) {
|
||||
await interaction.deferReply({ ephemeral: true });
|
||||
|
||||
const guildId = interaction.guildId;
|
||||
const itemId = interaction.options.getInteger('item_id');
|
||||
|
||||
try {
|
||||
const { data: item } = await supabase
|
||||
.from('shop_items')
|
||||
.select('*')
|
||||
.eq('id', itemId)
|
||||
.eq('guild_id', guildId)
|
||||
.maybeSingle();
|
||||
|
||||
if (!item) {
|
||||
return interaction.editReply({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(0xff6b6b)
|
||||
.setDescription('Item not found.')
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
await supabase
|
||||
.from('shop_items')
|
||||
.delete()
|
||||
.eq('id', itemId)
|
||||
.eq('guild_id', guildId);
|
||||
|
||||
await interaction.editReply({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(0x00ff00)
|
||||
.setTitle('🗑️ Item Removed')
|
||||
.setDescription(`**${item.name}** has been removed from the shop.`)
|
||||
.setTimestamp()
|
||||
]
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Shop remove error:', error);
|
||||
await interaction.editReply({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(0xff0000)
|
||||
.setDescription('Failed to remove item.')
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function handleEdit(interaction, supabase) {
|
||||
await interaction.deferReply({ ephemeral: true });
|
||||
|
||||
const guildId = interaction.guildId;
|
||||
const itemId = interaction.options.getInteger('item_id');
|
||||
const newName = interaction.options.getString('name');
|
||||
const newPrice = interaction.options.getInteger('price');
|
||||
const newDescription = interaction.options.getString('description');
|
||||
const newStock = interaction.options.getInteger('stock');
|
||||
const enabled = interaction.options.getBoolean('enabled');
|
||||
|
||||
try {
|
||||
const { data: item } = await supabase
|
||||
.from('shop_items')
|
||||
.select('*')
|
||||
.eq('id', itemId)
|
||||
.eq('guild_id', guildId)
|
||||
.maybeSingle();
|
||||
|
||||
if (!item) {
|
||||
return interaction.editReply({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(0xff6b6b)
|
||||
.setDescription('Item not found.')
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
const updates = { updated_at: new Date().toISOString() };
|
||||
const changes = [];
|
||||
|
||||
if (newName !== null) {
|
||||
updates.name = newName;
|
||||
changes.push(`Name: ${item.name} → ${newName}`);
|
||||
}
|
||||
if (newPrice !== null) {
|
||||
updates.price = newPrice;
|
||||
changes.push(`Price: ${item.price} → ${newPrice} XP`);
|
||||
}
|
||||
if (newDescription !== null) {
|
||||
updates.description = newDescription;
|
||||
changes.push(`Description updated`);
|
||||
}
|
||||
if (newStock !== null) {
|
||||
updates.stock = newStock === -1 ? null : newStock;
|
||||
changes.push(`Stock: ${item.stock || 'Unlimited'} → ${newStock === -1 ? 'Unlimited' : newStock}`);
|
||||
}
|
||||
if (enabled !== null) {
|
||||
updates.enabled = enabled;
|
||||
changes.push(`Enabled: ${item.enabled} → ${enabled}`);
|
||||
}
|
||||
|
||||
if (changes.length === 0) {
|
||||
return interaction.editReply({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(0xfbbf24)
|
||||
.setDescription('No changes specified.')
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
await supabase
|
||||
.from('shop_items')
|
||||
.update(updates)
|
||||
.eq('id', itemId);
|
||||
|
||||
await interaction.editReply({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(0x00ff00)
|
||||
.setTitle('✏️ Item Updated')
|
||||
.setDescription(`**${item.name}** has been updated.`)
|
||||
.addFields({ name: 'Changes', value: changes.join('\n') })
|
||||
.setTimestamp()
|
||||
]
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Shop edit error:', error);
|
||||
await interaction.editReply({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(0xff0000)
|
||||
.setDescription('Failed to edit item.')
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function handleList(interaction, supabase) {
|
||||
await interaction.deferReply({ ephemeral: true });
|
||||
|
||||
const guildId = interaction.guildId;
|
||||
|
||||
try {
|
||||
const { data: items, error } = await supabase
|
||||
.from('shop_items')
|
||||
.select('*')
|
||||
.eq('guild_id', guildId)
|
||||
.order('item_type')
|
||||
.order('price');
|
||||
|
||||
if (error || !items || items.length === 0) {
|
||||
return interaction.editReply({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(0xfbbf24)
|
||||
.setTitle('📋 Shop Items')
|
||||
.setDescription('No items in the shop yet.\n\nUse `/shop-manage add` to create items.')
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0x7c3aed)
|
||||
.setTitle('📋 Shop Items (Admin View)')
|
||||
.setDescription(`Total items: **${items.length}**`)
|
||||
.setTimestamp();
|
||||
|
||||
const itemLines = items.map(item => {
|
||||
const status = item.enabled ? '✅' : '❌';
|
||||
const stock = item.stock !== null ? ` [${item.stock} left]` : '';
|
||||
const reqs = [];
|
||||
if (item.level_required > 0) reqs.push(`L${item.level_required}`);
|
||||
if (item.prestige_required > 0) reqs.push(`P${item.prestige_required}`);
|
||||
const reqStr = reqs.length > 0 ? ` (${reqs.join('/')})` : '';
|
||||
return `${status} \`#${item.id}\` **${item.name}** - ${item.price.toLocaleString()} XP${stock}${reqStr}`;
|
||||
});
|
||||
|
||||
const chunks = [];
|
||||
let current = '';
|
||||
for (const line of itemLines) {
|
||||
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 ? 'Items' : '\u200b',
|
||||
value: chunks[i],
|
||||
inline: false
|
||||
});
|
||||
}
|
||||
|
||||
if (chunks.length > 5) {
|
||||
embed.setFooter({ text: `Showing first ${Math.min(items.length, 25)} items` });
|
||||
}
|
||||
|
||||
await interaction.editReply({ embeds: [embed] });
|
||||
} catch (error) {
|
||||
console.error('Shop list error:', error);
|
||||
await interaction.editReply({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(0xff0000)
|
||||
.setDescription('Failed to list items.')
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function handleStats(interaction, supabase) {
|
||||
await interaction.deferReply({ ephemeral: true });
|
||||
|
||||
const guildId = interaction.guildId;
|
||||
|
||||
try {
|
||||
const { data: items } = await supabase
|
||||
.from('shop_items')
|
||||
.select('*')
|
||||
.eq('guild_id', guildId);
|
||||
|
||||
const { data: purchases } = await supabase
|
||||
.from('user_inventory')
|
||||
.select('*, shop_items(*)')
|
||||
.eq('guild_id', guildId);
|
||||
|
||||
const { data: balances } = await supabase
|
||||
.from('user_balance')
|
||||
.select('*')
|
||||
.eq('guild_id', guildId);
|
||||
|
||||
const totalItems = items?.length || 0;
|
||||
const enabledItems = items?.filter(i => i.enabled).length || 0;
|
||||
const totalPurchases = purchases?.length || 0;
|
||||
const totalSpent = balances?.reduce((sum, b) => sum + (b.total_spent || 0), 0) || 0;
|
||||
const uniqueBuyers = new Set(purchases?.map(p => p.user_id)).size;
|
||||
|
||||
const itemCounts = {};
|
||||
for (const p of purchases || []) {
|
||||
const name = p.shop_items?.name || 'Unknown';
|
||||
itemCounts[name] = (itemCounts[name] || 0) + 1;
|
||||
}
|
||||
|
||||
const topItems = Object.entries(itemCounts)
|
||||
.sort(([, a], [, b]) => b - a)
|
||||
.slice(0, 5)
|
||||
.map(([name, count], i) => `${i + 1}. **${name}** (${count} purchases)`)
|
||||
.join('\n');
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0x7c3aed)
|
||||
.setTitle('📊 Shop Statistics')
|
||||
.addFields(
|
||||
{ name: '🛍️ Total Items', value: `${totalItems} (${enabledItems} active)`, inline: true },
|
||||
{ name: '🛒 Total Purchases', value: totalPurchases.toString(), inline: true },
|
||||
{ name: '👥 Unique Buyers', value: uniqueBuyers.toString(), inline: true },
|
||||
{ name: '💰 Total XP Spent', value: totalSpent.toLocaleString(), inline: true }
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
if (topItems) {
|
||||
embed.addFields({ name: '🏆 Top Items', value: topItems, inline: false });
|
||||
}
|
||||
|
||||
await interaction.editReply({ embeds: [embed] });
|
||||
} catch (error) {
|
||||
console.error('Shop stats error:', error);
|
||||
await interaction.editReply({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(0xff0000)
|
||||
.setDescription('Failed to get statistics.')
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
630
aethex-bot/commands/shop.js
Normal file
630
aethex-bot/commands/shop.js
Normal file
|
|
@ -0,0 +1,630 @@
|
|||
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 <item_id> 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: `<t:${Math.floor(new Date(expiresAt).getTime() / 1000)}:R>`, 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 <item_id> 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 <t:${Math.floor(new Date(inv.expires_at).getTime() / 1000)}:R>)` : '';
|
||||
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] });
|
||||
}
|
||||
|
|
@ -324,6 +324,53 @@ Create custom server achievements with various triggers:
|
|||
/achievements revoke @user [name] # Revoke achievement
|
||||
```
|
||||
|
||||
### XP Shop System
|
||||
|
||||
Spend your hard-earned XP on cosmetics, perks, and exclusive items!
|
||||
|
||||
**User Commands:**
|
||||
```
|
||||
/shop browse [category] # Browse available items
|
||||
/shop buy <item_id> # Purchase an item
|
||||
/shop inventory # View your purchased items
|
||||
/shop equip <item_id> # Equip a badge, title, or background
|
||||
/shop balance # Check your XP balance
|
||||
```
|
||||
|
||||
**Item Categories:**
|
||||
| Category | Description |
|
||||
|----------|-------------|
|
||||
| 🏅 Badges | Collectible profile badges |
|
||||
| 🏷️ Titles | Custom titles for your profile |
|
||||
| 🎨 Backgrounds | Profile card backgrounds |
|
||||
| ⚡ Boosters | Temporary XP multipliers |
|
||||
| 👑 Roles | Purchasable server roles |
|
||||
| ✨ Special | Limited edition items |
|
||||
|
||||
**How It Works:**
|
||||
- Your XP balance is based on your total earned XP minus what you've spent
|
||||
- Purchasing an item deducts from your balance, not your level
|
||||
- Some items may have level, prestige, or role requirements
|
||||
- Limited stock items are first-come, first-served
|
||||
- Boosters can be stacked and have expiration times
|
||||
|
||||
**Admin Commands (shop-manage):**
|
||||
```
|
||||
/shop-manage add [name] [price] [type] # Add new item
|
||||
/shop-manage edit <item_id> [options] # Edit existing item
|
||||
/shop-manage remove <item_id> # Remove item from shop
|
||||
/shop-manage list # List all items (admin view)
|
||||
/shop-manage stats # View shop statistics
|
||||
```
|
||||
|
||||
**Adding Items:**
|
||||
- `name`: Item display name
|
||||
- `price`: Cost in XP
|
||||
- `type`: badge, title, background, booster, role, or special
|
||||
- Optional: description, stock limit, level/prestige requirements
|
||||
- For roles: specify `grant_role` to give users a role on purchase
|
||||
- For boosters: set `booster_multiplier` and `booster_hours`
|
||||
|
||||
---
|
||||
|
||||
## Moderation Tools
|
||||
|
|
@ -622,12 +669,20 @@ Opens a form to set:
|
|||
| `/post` | Create a community feed post |
|
||||
| `/help` | View categorized commands |
|
||||
|
||||
### Leveling Commands (3)
|
||||
### Leveling Commands (5)
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/rank` | View your level and XP |
|
||||
| `/daily` | Claim daily XP bonus |
|
||||
| `/badges` | View earned badges |
|
||||
| `/prestige` | Prestige system |
|
||||
| `/achievements` | Achievement management |
|
||||
|
||||
### XP Shop Commands (2)
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/shop browse/buy/inventory/equip/balance` | XP shop for cosmetics and perks |
|
||||
| `/shop-manage add/edit/remove/list/stats` | Admin shop management |
|
||||
|
||||
### Moderation Commands (5)
|
||||
| Command | Description |
|
||||
|
|
|
|||
Loading…
Reference in a new issue