Add server mode configuration and dynamic status updates
Introduces a new server mode configuration system (Federation/Standalone) with associated command changes, dynamic status rotation for the bot, and adds new commands and features. Replit-Commit-Author: Agent Replit-Commit-Session-Id: aed2e46d-25bb-4b73-81a1-bb9e8437c261 Replit-Commit-Checkpoint-Type: intermediate_checkpoint Replit-Commit-Event-Id: b08e6ba5-7498-4b9f-b1c9-7dc11b362ddd Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/3bdfff67-975a-46ad-9845-fbb6b4a4c4b5/aed2e46d-25bb-4b73-81a1-bb9e8437c261/R9PkDi8 Replit-Helium-Checkpoint-Created: true
This commit is contained in:
parent
d7d8da51af
commit
c2a34f398e
42 changed files with 4960 additions and 746 deletions
4
.replit
4
.replit
|
|
@ -21,10 +21,6 @@ externalPort = 80
|
|||
localPort = 8080
|
||||
externalPort = 8080
|
||||
|
||||
[[ports]]
|
||||
localPort = 34949
|
||||
externalPort = 3001
|
||||
|
||||
[workflows]
|
||||
runButton = "Project"
|
||||
|
||||
|
|
|
|||
|
|
@ -718,7 +718,7 @@ if (fs.existsSync(sentinelPath)) {
|
|||
// =============================================================================
|
||||
|
||||
const listenersPath = path.join(__dirname, "listeners");
|
||||
const generalListenerFiles = ['welcome.js', 'goodbye.js', 'xpTracker.js', 'reactionXp.js', 'voiceXp.js'];
|
||||
const generalListenerFiles = ['welcome.js', 'goodbye.js', 'xpTracker.js', 'reactionXp.js', 'voiceXp.js', 'starboard.js'];
|
||||
for (const file of generalListenerFiles) {
|
||||
const filePath = path.join(listenersPath, file);
|
||||
if (fs.existsSync(filePath)) {
|
||||
|
|
@ -2499,6 +2499,36 @@ client.login(token).catch((error) => {
|
|||
process.exit(1);
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// DYNAMIC STATUS ROTATION
|
||||
// =============================================================================
|
||||
|
||||
function startDynamicStatus(client) {
|
||||
const statuses = [
|
||||
() => ({ name: `${client.guilds.cache.size} servers`, type: 3 }), // Watching
|
||||
() => ({ name: `${client.guilds.cache.reduce((sum, g) => sum + g.memberCount, 0).toLocaleString()} members`, type: 3 }), // Watching
|
||||
() => ({ name: '/help | aethex.studio', type: 0 }), // Playing
|
||||
() => ({ name: '🛡️ Guarding the Federation', type: 4 }), // Custom
|
||||
() => ({ name: 'for threats', type: 3 }), // Watching
|
||||
() => ({ name: '⚔️ Warden • Free Forever', type: 4 }), // Custom
|
||||
];
|
||||
|
||||
let currentIndex = 0;
|
||||
|
||||
const updateStatus = () => {
|
||||
try {
|
||||
const status = statuses[currentIndex]();
|
||||
client.user.setActivity(status.name, { type: status.type });
|
||||
currentIndex = (currentIndex + 1) % statuses.length;
|
||||
} catch (e) {
|
||||
console.error('[Status] Error updating status:', e.message);
|
||||
}
|
||||
};
|
||||
|
||||
updateStatus();
|
||||
setInterval(updateStatus, 30000); // Rotate every 30 seconds
|
||||
}
|
||||
|
||||
client.once("clientReady", async () => {
|
||||
console.log(`Bot logged in as ${client.user.tag}`);
|
||||
console.log(`Bot ID: ${client.user.id}`);
|
||||
|
|
@ -2520,13 +2550,14 @@ client.once("clientReady", async () => {
|
|||
console.error("Failed to register commands:", regResult.error);
|
||||
}
|
||||
|
||||
client.user.setActivity("Protecting the Federation", { type: 3 });
|
||||
// Dynamic rotating status
|
||||
startDynamicStatus(client);
|
||||
|
||||
if (setupFeedListener && supabase) {
|
||||
setupFeedListener(client);
|
||||
}
|
||||
|
||||
sendAlert(`AeThex Bot is now online! Watching ${client.guilds.cache.size} servers.`);
|
||||
sendAlert(`Warden is now online! Watching ${client.guilds.cache.size} servers.`);
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
|
|
|
|||
61
aethex-bot/commands/8ball.js
Normal file
61
aethex-bot/commands/8ball.js
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
|
||||
const { getServerMode, getEmbedColor } = require('../utils/modeHelper');
|
||||
|
||||
const RESPONSES = [
|
||||
{ text: 'It is certain.', positive: true },
|
||||
{ text: 'It is decidedly so.', positive: true },
|
||||
{ text: 'Without a doubt.', positive: true },
|
||||
{ text: 'Yes, definitely.', positive: true },
|
||||
{ text: 'You may rely on it.', positive: true },
|
||||
{ text: 'As I see it, yes.', positive: true },
|
||||
{ text: 'Most likely.', positive: true },
|
||||
{ text: 'Outlook good.', positive: true },
|
||||
{ text: 'Yes.', positive: true },
|
||||
{ text: 'Signs point to yes.', positive: true },
|
||||
{ text: 'Reply hazy, try again.', positive: null },
|
||||
{ text: 'Ask again later.', positive: null },
|
||||
{ text: 'Better not tell you now.', positive: null },
|
||||
{ text: 'Cannot predict now.', positive: null },
|
||||
{ text: 'Concentrate and ask again.', positive: null },
|
||||
{ text: "Don't count on it.", positive: false },
|
||||
{ text: 'My reply is no.', positive: false },
|
||||
{ text: 'My sources say no.', positive: false },
|
||||
{ text: 'Outlook not so good.', positive: false },
|
||||
{ text: 'Very doubtful.', positive: false },
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('8ball')
|
||||
.setDescription('Ask the magic 8-ball a question')
|
||||
.addStringOption(option =>
|
||||
option.setName('question')
|
||||
.setDescription('Your question for the 8-ball')
|
||||
.setRequired(true)
|
||||
.setMaxLength(256)
|
||||
),
|
||||
|
||||
async execute(interaction, supabase, client) {
|
||||
const question = interaction.options.getString('question');
|
||||
const response = RESPONSES[Math.floor(Math.random() * RESPONSES.length)];
|
||||
|
||||
const mode = await getServerMode(supabase, interaction.guildId);
|
||||
|
||||
let color;
|
||||
if (response.positive === true) color = 0x22C55E;
|
||||
else if (response.positive === false) color = 0xEF4444;
|
||||
else color = 0xF59E0B;
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(color)
|
||||
.setTitle('🎱 Magic 8-Ball')
|
||||
.addFields(
|
||||
{ name: '❓ Question', value: question },
|
||||
{ name: '🔮 Answer', value: response.text }
|
||||
)
|
||||
.setFooter({ text: `Asked by ${interaction.user.username}` })
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.reply({ embeds: [embed] });
|
||||
},
|
||||
};
|
||||
96
aethex-bot/commands/afk.js
Normal file
96
aethex-bot/commands/afk.js
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
|
||||
const { getServerMode, getEmbedColor } = require('../utils/modeHelper');
|
||||
|
||||
const afkUsers = new Map();
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('afk')
|
||||
.setDescription('Set your AFK status')
|
||||
.addStringOption(option =>
|
||||
option.setName('reason')
|
||||
.setDescription('Reason for being AFK')
|
||||
.setRequired(false)
|
||||
.setMaxLength(200)
|
||||
),
|
||||
|
||||
afkUsers,
|
||||
|
||||
async execute(interaction, supabase, client) {
|
||||
const reason = interaction.options.getString('reason') || 'AFK';
|
||||
const userId = interaction.user.id;
|
||||
const guildId = interaction.guildId;
|
||||
|
||||
const mode = await getServerMode(supabase, interaction.guildId);
|
||||
|
||||
if (afkUsers.has(`${guildId}-${userId}`)) {
|
||||
afkUsers.delete(`${guildId}-${userId}`);
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0x22C55E)
|
||||
.setTitle('👋 Welcome Back!')
|
||||
.setDescription('Your AFK status has been removed.')
|
||||
.setTimestamp();
|
||||
|
||||
return interaction.reply({ embeds: [embed], ephemeral: true });
|
||||
}
|
||||
|
||||
afkUsers.set(`${guildId}-${userId}`, {
|
||||
reason,
|
||||
timestamp: Date.now(),
|
||||
username: interaction.user.username
|
||||
});
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(getEmbedColor(mode))
|
||||
.setTitle('💤 AFK Status Set')
|
||||
.setDescription(`You are now AFK: **${reason}**`)
|
||||
.addFields({
|
||||
name: '💡 Tip',
|
||||
value: 'Use `/afk` again or send a message to remove your AFK status.'
|
||||
})
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.reply({ embeds: [embed] });
|
||||
},
|
||||
|
||||
checkAfk(message) {
|
||||
const guildId = message.guildId;
|
||||
const userId = message.author.id;
|
||||
const key = `${guildId}-${userId}`;
|
||||
|
||||
if (afkUsers.has(key)) {
|
||||
const afkData = afkUsers.get(key);
|
||||
afkUsers.delete(key);
|
||||
|
||||
const duration = Math.floor((Date.now() - afkData.timestamp) / 1000);
|
||||
let timeStr;
|
||||
if (duration < 60) timeStr = `${duration} seconds`;
|
||||
else if (duration < 3600) timeStr = `${Math.floor(duration / 60)} minutes`;
|
||||
else timeStr = `${Math.floor(duration / 3600)} hours`;
|
||||
|
||||
message.reply({
|
||||
content: `👋 Welcome back! You were AFK for ${timeStr}.`,
|
||||
allowedMentions: { repliedUser: false }
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
const mentions = message.mentions.users;
|
||||
mentions.forEach(user => {
|
||||
const mentionKey = `${guildId}-${user.id}`;
|
||||
if (afkUsers.has(mentionKey)) {
|
||||
const afkData = afkUsers.get(mentionKey);
|
||||
const ago = Math.floor((Date.now() - afkData.timestamp) / 1000);
|
||||
let timeStr;
|
||||
if (ago < 60) timeStr = `${ago} seconds ago`;
|
||||
else if (ago < 3600) timeStr = `${Math.floor(ago / 60)} minutes ago`;
|
||||
else timeStr = `${Math.floor(ago / 3600)} hours ago`;
|
||||
|
||||
message.reply({
|
||||
content: `💤 **${afkData.username}** is AFK: ${afkData.reason} (set ${timeStr})`,
|
||||
allowedMentions: { repliedUser: false }
|
||||
}).catch(() => {});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
187
aethex-bot/commands/birthday.js
Normal file
187
aethex-bot/commands/birthday.js
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
|
||||
const { getServerMode, getEmbedColor, EMBED_COLORS } = require('../utils/modeHelper');
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('birthday')
|
||||
.setDescription('Set or view birthdays')
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('set')
|
||||
.setDescription('Set your birthday')
|
||||
.addIntegerOption(option =>
|
||||
option.setName('month')
|
||||
.setDescription('Birth month (1-12)')
|
||||
.setRequired(true)
|
||||
.setMinValue(1)
|
||||
.setMaxValue(12)
|
||||
)
|
||||
.addIntegerOption(option =>
|
||||
option.setName('day')
|
||||
.setDescription('Birth day (1-31)')
|
||||
.setRequired(true)
|
||||
.setMinValue(1)
|
||||
.setMaxValue(31)
|
||||
)
|
||||
)
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('view')
|
||||
.setDescription('View someone\'s birthday')
|
||||
.addUserOption(option =>
|
||||
option.setName('user')
|
||||
.setDescription('The user to check')
|
||||
.setRequired(false)
|
||||
)
|
||||
)
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('upcoming')
|
||||
.setDescription('View upcoming birthdays in this server')
|
||||
)
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('remove')
|
||||
.setDescription('Remove your birthday')
|
||||
),
|
||||
|
||||
async execute(interaction, supabase, client) {
|
||||
const subcommand = interaction.options.getSubcommand();
|
||||
const mode = await getServerMode(supabase, interaction.guildId);
|
||||
const guildId = interaction.guildId;
|
||||
const userId = interaction.user.id;
|
||||
|
||||
if (subcommand === 'set') {
|
||||
const month = interaction.options.getInteger('month');
|
||||
const day = interaction.options.getInteger('day');
|
||||
|
||||
const daysInMonth = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
||||
if (day > daysInMonth[month - 1]) {
|
||||
return interaction.reply({
|
||||
content: `Invalid day for month ${month}. Maximum is ${daysInMonth[month - 1]}.`,
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
if (supabase) {
|
||||
try {
|
||||
await supabase.from('birthdays').upsert({
|
||||
guild_id: guildId,
|
||||
user_id: userId,
|
||||
username: interaction.user.username,
|
||||
month: month,
|
||||
day: day,
|
||||
updated_at: new Date().toISOString()
|
||||
}, { onConflict: 'guild_id,user_id' });
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
const monthNames = ['January', 'February', 'March', 'April', 'May', 'June',
|
||||
'July', 'August', 'September', 'October', 'November', 'December'];
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(EMBED_COLORS.success)
|
||||
.setTitle('🎂 Birthday Set!')
|
||||
.setDescription(`Your birthday is now set to **${monthNames[month - 1]} ${day}**!`)
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.reply({ embeds: [embed], ephemeral: true });
|
||||
}
|
||||
|
||||
else if (subcommand === 'view') {
|
||||
const targetUser = interaction.options.getUser('user') || interaction.user;
|
||||
|
||||
if (!supabase) {
|
||||
return interaction.reply({ content: 'Birthday system unavailable.', ephemeral: true });
|
||||
}
|
||||
|
||||
const { data } = await supabase
|
||||
.from('birthdays')
|
||||
.select('month, day')
|
||||
.eq('guild_id', guildId)
|
||||
.eq('user_id', targetUser.id)
|
||||
.maybeSingle();
|
||||
|
||||
if (!data) {
|
||||
return interaction.reply({
|
||||
content: targetUser.id === userId
|
||||
? 'You haven\'t set your birthday yet! Use `/birthday set`.'
|
||||
: `${targetUser.username} hasn't set their birthday.`,
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
const monthNames = ['January', 'February', 'March', 'April', 'May', 'June',
|
||||
'July', 'August', 'September', 'October', 'November', 'December'];
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(getEmbedColor(mode))
|
||||
.setTitle('🎂 Birthday')
|
||||
.setDescription(`**${targetUser.username}**'s birthday is on **${monthNames[data.month - 1]} ${data.day}**!`)
|
||||
.setThumbnail(targetUser.displayAvatarURL({ size: 128 }))
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.reply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
else if (subcommand === 'upcoming') {
|
||||
if (!supabase) {
|
||||
return interaction.reply({ content: 'Birthday system unavailable.', ephemeral: true });
|
||||
}
|
||||
|
||||
const { data } = await supabase
|
||||
.from('birthdays')
|
||||
.select('user_id, username, month, day')
|
||||
.eq('guild_id', guildId);
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
return interaction.reply({ content: 'No birthdays set in this server yet!', ephemeral: true });
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const currentMonth = now.getMonth() + 1;
|
||||
const currentDay = now.getDate();
|
||||
|
||||
const sorted = data.map(b => {
|
||||
let daysUntil = (b.month - currentMonth) * 30 + (b.day - currentDay);
|
||||
if (daysUntil < 0) daysUntil += 365;
|
||||
return { ...b, daysUntil };
|
||||
}).sort((a, b) => a.daysUntil - b.daysUntil).slice(0, 10);
|
||||
|
||||
const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
|
||||
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||
|
||||
const list = sorted.map((b, i) => {
|
||||
const emoji = b.daysUntil === 0 ? '🎉' : '🎂';
|
||||
const status = b.daysUntil === 0 ? '**TODAY!**' : `in ${b.daysUntil} days`;
|
||||
return `${i + 1}. ${emoji} **${b.username}** - ${monthNames[b.month - 1]} ${b.day} (${status})`;
|
||||
}).join('\n');
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(getEmbedColor(mode))
|
||||
.setTitle('🎂 Upcoming Birthdays')
|
||||
.setDescription(list || 'No upcoming birthdays.')
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.reply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
else if (subcommand === 'remove') {
|
||||
if (supabase) {
|
||||
await supabase
|
||||
.from('birthdays')
|
||||
.delete()
|
||||
.eq('guild_id', guildId)
|
||||
.eq('user_id', userId);
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(EMBED_COLORS.success)
|
||||
.setTitle('🗑️ Birthday Removed')
|
||||
.setDescription('Your birthday has been removed from this server.')
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.reply({ embeds: [embed], ephemeral: true });
|
||||
}
|
||||
},
|
||||
};
|
||||
44
aethex-bot/commands/coinflip.js
Normal file
44
aethex-bot/commands/coinflip.js
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
|
||||
const { getServerMode, getEmbedColor } = require('../utils/modeHelper');
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('coinflip')
|
||||
.setDescription('Flip a coin')
|
||||
.addStringOption(option =>
|
||||
option.setName('call')
|
||||
.setDescription('Call heads or tails before the flip')
|
||||
.setRequired(false)
|
||||
.addChoices(
|
||||
{ name: 'Heads', value: 'heads' },
|
||||
{ name: 'Tails', value: 'tails' }
|
||||
)
|
||||
),
|
||||
|
||||
async execute(interaction, supabase, client) {
|
||||
const call = interaction.options.getString('call');
|
||||
const result = Math.random() < 0.5 ? 'heads' : 'tails';
|
||||
const emoji = result === 'heads' ? '🪙' : '💿';
|
||||
|
||||
const mode = await getServerMode(supabase, interaction.guildId);
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(getEmbedColor(mode))
|
||||
.setTitle(`${emoji} Coin Flip`)
|
||||
.setDescription(`The coin landed on **${result.toUpperCase()}**!`)
|
||||
.setTimestamp();
|
||||
|
||||
if (call) {
|
||||
const won = call === result;
|
||||
embed.addFields({
|
||||
name: won ? '🎉 Result' : '😔 Result',
|
||||
value: won ? `You called it! You win!` : `You called ${call}, better luck next time!`
|
||||
});
|
||||
embed.setColor(won ? 0x22C55E : 0xEF4444);
|
||||
}
|
||||
|
||||
embed.setFooter({ text: `Flipped by ${interaction.user.username}` });
|
||||
|
||||
await interaction.reply({ embeds: [embed] });
|
||||
},
|
||||
};
|
||||
122
aethex-bot/commands/color.js
Normal file
122
aethex-bot/commands/color.js
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
const { SlashCommandBuilder, EmbedBuilder, AttachmentBuilder } = require('discord.js');
|
||||
const { getServerMode } = require('../utils/modeHelper');
|
||||
|
||||
function hexToRgb(hex) {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
return result ? {
|
||||
r: parseInt(result[1], 16),
|
||||
g: parseInt(result[2], 16),
|
||||
b: parseInt(result[3], 16)
|
||||
} : null;
|
||||
}
|
||||
|
||||
function rgbToHsl(r, g, b) {
|
||||
r /= 255; g /= 255; b /= 255;
|
||||
const max = Math.max(r, g, b), min = Math.min(r, g, b);
|
||||
let h, s, l = (max + min) / 2;
|
||||
|
||||
if (max === min) {
|
||||
h = s = 0;
|
||||
} else {
|
||||
const d = max - min;
|
||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||
switch (max) {
|
||||
case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break;
|
||||
case g: h = ((b - r) / d + 2) / 6; break;
|
||||
case b: h = ((r - g) / d + 4) / 6; break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
h: Math.round(h * 360),
|
||||
s: Math.round(s * 100),
|
||||
l: Math.round(l * 100)
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('color')
|
||||
.setDescription('View color information')
|
||||
.addStringOption(option =>
|
||||
option.setName('hex')
|
||||
.setDescription('Hex color code (e.g., #FF5733 or FF5733)')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addIntegerOption(option =>
|
||||
option.setName('red')
|
||||
.setDescription('Red value (0-255)')
|
||||
.setRequired(false)
|
||||
.setMinValue(0)
|
||||
.setMaxValue(255)
|
||||
)
|
||||
.addIntegerOption(option =>
|
||||
option.setName('green')
|
||||
.setDescription('Green value (0-255)')
|
||||
.setRequired(false)
|
||||
.setMinValue(0)
|
||||
.setMaxValue(255)
|
||||
)
|
||||
.addIntegerOption(option =>
|
||||
option.setName('blue')
|
||||
.setDescription('Blue value (0-255)')
|
||||
.setRequired(false)
|
||||
.setMinValue(0)
|
||||
.setMaxValue(255)
|
||||
),
|
||||
|
||||
async execute(interaction, supabase, client) {
|
||||
const hexInput = interaction.options.getString('hex');
|
||||
const red = interaction.options.getInteger('red');
|
||||
const green = interaction.options.getInteger('green');
|
||||
const blue = interaction.options.getInteger('blue');
|
||||
|
||||
let r, g, b, hexColor;
|
||||
|
||||
if (hexInput) {
|
||||
const rgb = hexToRgb(hexInput);
|
||||
if (!rgb) {
|
||||
return interaction.reply({
|
||||
content: 'Invalid hex color. Use format like #FF5733 or FF5733.',
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
r = rgb.r;
|
||||
g = rgb.g;
|
||||
b = rgb.b;
|
||||
hexColor = hexInput.replace('#', '').toUpperCase();
|
||||
} else if (red !== null && green !== null && blue !== null) {
|
||||
r = red;
|
||||
g = green;
|
||||
b = blue;
|
||||
hexColor = ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase();
|
||||
} else if (red !== null || green !== null || blue !== null) {
|
||||
return interaction.reply({
|
||||
content: 'Please provide all RGB values (red, green, blue) or use a hex code.',
|
||||
ephemeral: true
|
||||
});
|
||||
} else {
|
||||
r = Math.floor(Math.random() * 256);
|
||||
g = Math.floor(Math.random() * 256);
|
||||
b = Math.floor(Math.random() * 256);
|
||||
hexColor = ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase();
|
||||
}
|
||||
|
||||
const hsl = rgbToHsl(r, g, b);
|
||||
const colorInt = parseInt(hexColor, 16);
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(colorInt)
|
||||
.setTitle(`🎨 Color: #${hexColor}`)
|
||||
.addFields(
|
||||
{ name: 'Hex', value: `\`#${hexColor}\``, inline: true },
|
||||
{ name: 'RGB', value: `\`rgb(${r}, ${g}, ${b})\``, inline: true },
|
||||
{ name: 'HSL', value: `\`hsl(${hsl.h}, ${hsl.s}%, ${hsl.l}%)\``, inline: true },
|
||||
{ name: 'Integer', value: `\`${colorInt}\``, inline: true }
|
||||
)
|
||||
.setThumbnail(`https://singlecolorimage.com/get/${hexColor}/100x100`)
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.reply({ embeds: [embed] });
|
||||
},
|
||||
};
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits, ChannelType } = require('discord.js');
|
||||
const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits, ChannelType, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js');
|
||||
const { getServerMode, setServerMode, getEmbedColor, getModeDisplayName, getModeEmoji, EMBED_COLORS } = require('../utils/modeHelper');
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
|
|
@ -73,6 +74,10 @@ module.exports = {
|
|||
.setMinValue(1)
|
||||
.setMaxValue(100)
|
||||
)
|
||||
)
|
||||
.addSubcommand(sub =>
|
||||
sub.setName('mode')
|
||||
.setDescription('Switch between Federation and Standalone mode')
|
||||
),
|
||||
|
||||
async execute(interaction, supabase, client) {
|
||||
|
|
@ -84,6 +89,86 @@ module.exports = {
|
|||
await interaction.deferReply({ ephemeral: true });
|
||||
|
||||
try {
|
||||
if (subcommand === 'mode') {
|
||||
const currentMode = await getServerMode(supabase, interaction.guildId);
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(getEmbedColor(currentMode))
|
||||
.setTitle('Server Mode Configuration')
|
||||
.setDescription(
|
||||
`${getModeEmoji(currentMode)} Currently running in **${getModeDisplayName(currentMode)}** mode\n\n` +
|
||||
'Choose your server mode:'
|
||||
)
|
||||
.addFields(
|
||||
{
|
||||
name: '🌐 Federation Mode',
|
||||
value: '• Unified XP across all AeThex servers\n• Cross-server profiles and leaderboards\n• Realm selection and role sync',
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: '🏠 Standalone Mode',
|
||||
value: '• Isolated XP system for this server\n• Local leaderboards only\n• Full moderation features',
|
||||
inline: true
|
||||
}
|
||||
)
|
||||
.setFooter({ text: 'This affects how XP and profiles work in your server' });
|
||||
|
||||
const row = new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId('config_mode_federated')
|
||||
.setLabel('Federation')
|
||||
.setStyle(currentMode === 'federated' ? ButtonStyle.Primary : ButtonStyle.Secondary)
|
||||
.setEmoji('🌐')
|
||||
.setDisabled(currentMode === 'federated'),
|
||||
new ButtonBuilder()
|
||||
.setCustomId('config_mode_standalone')
|
||||
.setLabel('Standalone')
|
||||
.setStyle(currentMode === 'standalone' ? ButtonStyle.Primary : ButtonStyle.Secondary)
|
||||
.setEmoji('🏠')
|
||||
.setDisabled(currentMode === 'standalone')
|
||||
);
|
||||
|
||||
const response = await interaction.editReply({ embeds: [embed], components: [row] });
|
||||
|
||||
const collector = response.createMessageComponentCollector({ time: 60000 });
|
||||
|
||||
collector.on('collect', async (i) => {
|
||||
if (i.user.id !== interaction.user.id) {
|
||||
return i.reply({ content: 'Only the command user can change this.', ephemeral: true });
|
||||
}
|
||||
|
||||
const newMode = i.customId === 'config_mode_federated' ? 'federated' : 'standalone';
|
||||
const success = await setServerMode(supabase, interaction.guildId, newMode);
|
||||
|
||||
if (success) {
|
||||
const confirmEmbed = new EmbedBuilder()
|
||||
.setColor(getEmbedColor(newMode))
|
||||
.setTitle(`${getModeEmoji(newMode)} Mode Changed!`)
|
||||
.setDescription(
|
||||
`Server is now running in **${getModeDisplayName(newMode)}** mode.\n\n` +
|
||||
(newMode === 'federated'
|
||||
? 'XP will now count globally across the AeThex ecosystem.'
|
||||
: 'XP is now tracked locally for this server only.')
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
await i.update({ embeds: [confirmEmbed], components: [] });
|
||||
collector.stop();
|
||||
} else {
|
||||
await i.reply({ content: 'Failed to change mode. Please try again.', ephemeral: true });
|
||||
}
|
||||
});
|
||||
|
||||
collector.on('end', async (collected, reason) => {
|
||||
if (reason === 'time') {
|
||||
await interaction.editReply({ components: [] }).catch(() => {});
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (subcommand === 'view') {
|
||||
const { data: config } = await supabase
|
||||
.from('server_config')
|
||||
|
|
@ -91,10 +176,13 @@ module.exports = {
|
|||
.eq('guild_id', interaction.guildId)
|
||||
.single();
|
||||
|
||||
const currentMode = config?.mode || 'federated';
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0x7c3aed)
|
||||
.setColor(getEmbedColor(currentMode))
|
||||
.setTitle('Server Configuration')
|
||||
.addFields(
|
||||
{ name: 'Server Mode', value: `${getModeEmoji(currentMode)} ${getModeDisplayName(currentMode)}`, inline: true },
|
||||
{ name: 'Welcome Channel', value: config?.welcome_channel ? `<#${config.welcome_channel}>` : 'Not set', inline: true },
|
||||
{ name: 'Goodbye Channel', value: config?.goodbye_channel ? `<#${config.goodbye_channel}>` : 'Not set', inline: true },
|
||||
{ name: 'Mod Log Channel', value: config?.modlog_channel ? `<#${config.modlog_channel}>` : 'Not set', inline: true },
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
|
||||
const { checkAchievements } = require('./achievements');
|
||||
const { getUserStats, calculateLevel, updateQuestProgress } = require('../listeners/xpTracker');
|
||||
const { getServerMode, EMBED_COLORS } = require('../utils/modeHelper');
|
||||
const { claimStandaloneDaily, getStandaloneXp, calculateLevel: calcStandaloneLevel } = require('../utils/standaloneXp');
|
||||
|
||||
const DAILY_XP = 50;
|
||||
const STREAK_BONUS = 10;
|
||||
|
|
@ -19,129 +21,13 @@ module.exports = {
|
|||
await interaction.deferReply();
|
||||
|
||||
try {
|
||||
const { data: link } = await supabase
|
||||
.from('discord_links')
|
||||
.select('user_id')
|
||||
.eq('discord_id', interaction.user.id)
|
||||
.single();
|
||||
const mode = await getServerMode(supabase, interaction.guildId);
|
||||
|
||||
if (!link) {
|
||||
return interaction.editReply({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(0xff6b6b)
|
||||
.setDescription('You need to link your account first! Use `/verify` to get started.')
|
||||
]
|
||||
});
|
||||
if (mode === 'standalone') {
|
||||
return handleStandaloneDaily(interaction, supabase);
|
||||
} else {
|
||||
return handleFederatedDaily(interaction, supabase, client);
|
||||
}
|
||||
|
||||
const { data: profile } = await supabase
|
||||
.from('user_profiles')
|
||||
.select('xp, daily_streak, last_daily, prestige_level, total_xp_earned')
|
||||
.eq('id', link.user_id)
|
||||
.single();
|
||||
|
||||
const now = new Date();
|
||||
const lastDaily = profile?.last_daily ? new Date(profile.last_daily) : null;
|
||||
const currentXp = profile?.xp || 0;
|
||||
const prestige = profile?.prestige_level || 0;
|
||||
let streak = profile?.daily_streak || 0;
|
||||
|
||||
if (lastDaily) {
|
||||
const hoursSince = (now - lastDaily) / (1000 * 60 * 60);
|
||||
|
||||
if (hoursSince < 20) {
|
||||
const nextClaim = new Date(lastDaily.getTime() + 20 * 60 * 60 * 1000);
|
||||
return interaction.editReply({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(0xfbbf24)
|
||||
.setTitle('Already Claimed!')
|
||||
.setDescription(`You've already claimed your daily XP.\nNext claim: <t:${Math.floor(nextClaim.getTime() / 1000)}:R>`)
|
||||
.addFields({ name: 'Current Streak', value: `🔥 ${streak} days` })
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
if (hoursSince > 48) {
|
||||
streak = 0;
|
||||
}
|
||||
}
|
||||
|
||||
streak += 1;
|
||||
const streakBonus = Math.min(streak * STREAK_BONUS, MAX_STREAK_BONUS);
|
||||
|
||||
// Prestige level 4+ gets bonus daily XP (+25)
|
||||
const prestigeDailyBonus = prestige >= 4 ? 25 : 0;
|
||||
|
||||
// Base total before prestige multiplier
|
||||
let totalXp = DAILY_XP + streakBonus + prestigeDailyBonus;
|
||||
|
||||
// Apply prestige XP bonus (+5% per prestige level)
|
||||
if (prestige > 0) {
|
||||
const prestigeMultiplier = 1 + (prestige * 0.05);
|
||||
totalXp = Math.floor(totalXp * prestigeMultiplier);
|
||||
}
|
||||
|
||||
const newXp = currentXp + totalXp;
|
||||
const totalEarned = (profile?.total_xp_earned || currentXp) + totalXp;
|
||||
|
||||
await supabase
|
||||
.from('user_profiles')
|
||||
.update({
|
||||
xp: newXp,
|
||||
daily_streak: streak,
|
||||
last_daily: now.toISOString(),
|
||||
total_xp_earned: totalEarned
|
||||
})
|
||||
.eq('id', link.user_id);
|
||||
|
||||
const newLevel = Math.floor(Math.sqrt(newXp / 100));
|
||||
const oldLevel = Math.floor(Math.sqrt(currentXp / 100));
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(prestige > 0 ? getPrestigeColor(prestige) : 0x00ff00)
|
||||
.setTitle('Daily Reward Claimed!')
|
||||
.setDescription(`You received **+${totalXp} XP**!${prestige > 0 ? ` *(includes P${prestige} bonus)*` : ''}`)
|
||||
.addFields(
|
||||
{ name: 'Base XP', value: `+${DAILY_XP}`, inline: true },
|
||||
{ name: 'Streak Bonus', value: `+${streakBonus}`, inline: true },
|
||||
{ name: 'Current Streak', value: `🔥 ${streak} days`, inline: true },
|
||||
{ name: 'Total XP', value: newXp.toLocaleString(), inline: true },
|
||||
{ name: 'Level', value: `${newLevel}`, inline: true }
|
||||
);
|
||||
|
||||
if (prestige > 0) {
|
||||
embed.addFields({ name: 'Prestige Bonus', value: `+${prestige * 5}% XP${prestigeDailyBonus > 0 ? ` + ${prestigeDailyBonus} daily bonus` : ''}`, inline: true });
|
||||
}
|
||||
|
||||
embed.setFooter({ text: 'Come back tomorrow to keep your streak!' })
|
||||
.setTimestamp();
|
||||
|
||||
if (newLevel > oldLevel) {
|
||||
embed.addFields({ name: '🎉 Level Up!', value: `You reached level ${newLevel}!` });
|
||||
}
|
||||
|
||||
await interaction.editReply({ embeds: [embed] });
|
||||
|
||||
// Check achievements with updated stats
|
||||
const guildId = interaction.guildId;
|
||||
const stats = await getUserStats(supabase, link.user_id, guildId);
|
||||
stats.level = newLevel;
|
||||
stats.prestige = prestige;
|
||||
stats.totalXp = totalEarned;
|
||||
stats.dailyStreak = streak;
|
||||
|
||||
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.' });
|
||||
|
|
@ -149,6 +35,164 @@ module.exports = {
|
|||
},
|
||||
};
|
||||
|
||||
async function handleStandaloneDaily(interaction, supabase) {
|
||||
const result = await claimStandaloneDaily(
|
||||
supabase,
|
||||
interaction.user.id,
|
||||
interaction.guildId,
|
||||
interaction.user.username
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
return interaction.editReply({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(EMBED_COLORS.warning)
|
||||
.setTitle('Already Claimed!')
|
||||
.setDescription(result.message)
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
const level = calcStandaloneLevel(result.totalXp, 'normal');
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(EMBED_COLORS.success)
|
||||
.setTitle('Daily Reward Claimed!')
|
||||
.setDescription(`You received **+${result.xpGained} XP**!`)
|
||||
.addFields(
|
||||
{ name: 'Base XP', value: `+50`, inline: true },
|
||||
{ name: 'Streak Bonus', value: `+${Math.min((result.streak - 1) * 5, 100)}`, inline: true },
|
||||
{ name: 'Current Streak', value: `${result.streak} days`, inline: true },
|
||||
{ name: 'Total XP', value: result.totalXp.toLocaleString(), inline: true },
|
||||
{ name: 'Level', value: `${level}`, inline: true }
|
||||
)
|
||||
.setFooter({ text: `🏠 Standalone Mode • Come back tomorrow!` })
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.editReply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
async function handleFederatedDaily(interaction, supabase, client) {
|
||||
const { data: link } = await supabase
|
||||
.from('discord_links')
|
||||
.select('user_id')
|
||||
.eq('discord_id', interaction.user.id)
|
||||
.single();
|
||||
|
||||
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: profile } = await supabase
|
||||
.from('user_profiles')
|
||||
.select('xp, daily_streak, last_daily, prestige_level, total_xp_earned')
|
||||
.eq('id', link.user_id)
|
||||
.single();
|
||||
|
||||
const now = new Date();
|
||||
const lastDaily = profile?.last_daily ? new Date(profile.last_daily) : null;
|
||||
const currentXp = profile?.xp || 0;
|
||||
const prestige = profile?.prestige_level || 0;
|
||||
let streak = profile?.daily_streak || 0;
|
||||
|
||||
if (lastDaily) {
|
||||
const hoursSince = (now - lastDaily) / (1000 * 60 * 60);
|
||||
|
||||
if (hoursSince < 20) {
|
||||
const nextClaim = new Date(lastDaily.getTime() + 20 * 60 * 60 * 1000);
|
||||
return interaction.editReply({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(0xfbbf24)
|
||||
.setTitle('Already Claimed!')
|
||||
.setDescription(`You've already claimed your daily XP.\nNext claim: <t:${Math.floor(nextClaim.getTime() / 1000)}:R>`)
|
||||
.addFields({ name: 'Current Streak', value: `${streak} days` })
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
if (hoursSince > 48) {
|
||||
streak = 0;
|
||||
}
|
||||
}
|
||||
|
||||
streak += 1;
|
||||
const streakBonus = Math.min(streak * STREAK_BONUS, MAX_STREAK_BONUS);
|
||||
|
||||
const prestigeDailyBonus = prestige >= 4 ? 25 : 0;
|
||||
|
||||
let totalXp = DAILY_XP + streakBonus + prestigeDailyBonus;
|
||||
|
||||
if (prestige > 0) {
|
||||
const prestigeMultiplier = 1 + (prestige * 0.05);
|
||||
totalXp = Math.floor(totalXp * prestigeMultiplier);
|
||||
}
|
||||
|
||||
const newXp = currentXp + totalXp;
|
||||
const totalEarned = (profile?.total_xp_earned || currentXp) + totalXp;
|
||||
|
||||
await supabase
|
||||
.from('user_profiles')
|
||||
.update({
|
||||
xp: newXp,
|
||||
daily_streak: streak,
|
||||
last_daily: now.toISOString(),
|
||||
total_xp_earned: totalEarned
|
||||
})
|
||||
.eq('id', link.user_id);
|
||||
|
||||
const newLevel = Math.floor(Math.sqrt(newXp / 100));
|
||||
const oldLevel = Math.floor(Math.sqrt(currentXp / 100));
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(prestige > 0 ? getPrestigeColor(prestige) : 0x00ff00)
|
||||
.setTitle('Daily Reward Claimed!')
|
||||
.setDescription(`You received **+${totalXp} XP**!${prestige > 0 ? ` *(includes P${prestige} bonus)*` : ''}`)
|
||||
.addFields(
|
||||
{ name: 'Base XP', value: `+${DAILY_XP}`, inline: true },
|
||||
{ name: 'Streak Bonus', value: `+${streakBonus}`, inline: true },
|
||||
{ name: 'Current Streak', value: `${streak} days`, inline: true },
|
||||
{ name: 'Total XP', value: newXp.toLocaleString(), inline: true },
|
||||
{ name: 'Level', value: `${newLevel}`, inline: true }
|
||||
);
|
||||
|
||||
if (prestige > 0) {
|
||||
embed.addFields({ name: 'Prestige Bonus', value: `+${prestige * 5}% XP${prestigeDailyBonus > 0 ? ` + ${prestigeDailyBonus} daily bonus` : ''}`, inline: true });
|
||||
}
|
||||
|
||||
embed.setFooter({ text: '🌐 Federation • Come back tomorrow to keep your streak!' })
|
||||
.setTimestamp();
|
||||
|
||||
if (newLevel > oldLevel) {
|
||||
embed.addFields({ name: 'Level Up!', value: `You reached level ${newLevel}!` });
|
||||
}
|
||||
|
||||
await interaction.editReply({ embeds: [embed] });
|
||||
|
||||
const guildId = interaction.guildId;
|
||||
const stats = await getUserStats(supabase, link.user_id, guildId);
|
||||
stats.level = newLevel;
|
||||
stats.prestige = prestige;
|
||||
stats.totalXp = totalEarned;
|
||||
stats.dailyStreak = streak;
|
||||
|
||||
await checkAchievements(link.user_id, interaction.member, stats, supabase, guildId, client);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
function getPrestigeColor(level) {
|
||||
const colors = [0x6b7280, 0xcd7f32, 0xc0c0c0, 0xffd700, 0xe5e4e2, 0xb9f2ff, 0xff4500, 0x9400d3, 0xffd700, 0xff69b4, 0x7c3aed];
|
||||
return colors[Math.min(level, 10)] || 0x00ff00;
|
||||
|
|
|
|||
76
aethex-bot/commands/define.js
Normal file
76
aethex-bot/commands/define.js
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
|
||||
const { getServerMode, getEmbedColor, EMBED_COLORS } = require('../utils/modeHelper');
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('define')
|
||||
.setDescription('Look up the definition of a word')
|
||||
.addStringOption(option =>
|
||||
option.setName('word')
|
||||
.setDescription('The word to define')
|
||||
.setRequired(true)
|
||||
.setMaxLength(100)
|
||||
),
|
||||
|
||||
async execute(interaction, supabase, client) {
|
||||
const word = interaction.options.getString('word').toLowerCase().trim();
|
||||
const mode = await getServerMode(supabase, interaction.guildId);
|
||||
|
||||
await interaction.deferReply();
|
||||
|
||||
try {
|
||||
const response = await fetch(`https://api.dictionaryapi.dev/api/v2/entries/en/${encodeURIComponent(word)}`);
|
||||
|
||||
if (!response.ok) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(EMBED_COLORS.error)
|
||||
.setTitle('📖 Word Not Found')
|
||||
.setDescription(`Could not find a definition for "**${word}**".`)
|
||||
.setTimestamp();
|
||||
return interaction.editReply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const entry = data[0];
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(getEmbedColor(mode))
|
||||
.setTitle(`📖 ${entry.word}`)
|
||||
.setTimestamp();
|
||||
|
||||
if (entry.phonetic) {
|
||||
embed.setDescription(`*${entry.phonetic}*`);
|
||||
}
|
||||
|
||||
const meanings = entry.meanings.slice(0, 3);
|
||||
for (const meaning of meanings) {
|
||||
const definitions = meaning.definitions.slice(0, 2);
|
||||
const defText = definitions.map((d, i) => {
|
||||
let text = `${i + 1}. ${d.definition}`;
|
||||
if (d.example) {
|
||||
text += `\n *"${d.example}"*`;
|
||||
}
|
||||
return text;
|
||||
}).join('\n');
|
||||
|
||||
embed.addFields({
|
||||
name: `${meaning.partOfSpeech}`,
|
||||
value: defText.substring(0, 1024)
|
||||
});
|
||||
}
|
||||
|
||||
if (entry.sourceUrls && entry.sourceUrls[0]) {
|
||||
embed.setFooter({ text: 'Source: Wiktionary' });
|
||||
}
|
||||
|
||||
await interaction.editReply({ embeds: [embed] });
|
||||
} catch (e) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(EMBED_COLORS.error)
|
||||
.setTitle('📖 Error')
|
||||
.setDescription('Failed to fetch definition. Please try again.')
|
||||
.setTimestamp();
|
||||
await interaction.editReply({ embeds: [embed] });
|
||||
}
|
||||
},
|
||||
};
|
||||
176
aethex-bot/commands/duel.js
Normal file
176
aethex-bot/commands/duel.js
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
const { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js');
|
||||
const { getServerMode, getEmbedColor, EMBED_COLORS } = require('../utils/modeHelper');
|
||||
const { updateStandaloneXp } = require('../utils/standaloneXp');
|
||||
|
||||
const activeDuels = new Map();
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('duel')
|
||||
.setDescription('Challenge someone to a duel!')
|
||||
.addUserOption(option =>
|
||||
option.setName('opponent')
|
||||
.setDescription('The user to challenge')
|
||||
.setRequired(true)
|
||||
)
|
||||
.addIntegerOption(option =>
|
||||
option.setName('bet')
|
||||
.setDescription('XP to bet (0-100)')
|
||||
.setRequired(false)
|
||||
.setMinValue(0)
|
||||
.setMaxValue(100)
|
||||
),
|
||||
|
||||
async execute(interaction, supabase, client) {
|
||||
const opponent = interaction.options.getUser('opponent');
|
||||
const bet = interaction.options.getInteger('bet') || 0;
|
||||
const challenger = interaction.user;
|
||||
const mode = await getServerMode(supabase, interaction.guildId);
|
||||
const guildId = interaction.guildId;
|
||||
|
||||
if (opponent.id === challenger.id) {
|
||||
return interaction.reply({ content: "You can't duel yourself!", ephemeral: true });
|
||||
}
|
||||
|
||||
if (opponent.bot) {
|
||||
return interaction.reply({ content: "You can't duel bots!", ephemeral: true });
|
||||
}
|
||||
|
||||
const duelKey = `${guildId}-${challenger.id}`;
|
||||
if (activeDuels.has(duelKey)) {
|
||||
return interaction.reply({ content: 'You already have an active duel!', ephemeral: true });
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(getEmbedColor(mode))
|
||||
.setTitle('⚔️ Duel Challenge!')
|
||||
.setDescription(`${challenger} challenges ${opponent} to a duel!`)
|
||||
.addFields(
|
||||
{ name: '💰 Bet', value: bet > 0 ? `${bet} XP` : 'No bet', inline: true },
|
||||
{ name: '⏱️ Expires', value: 'In 60 seconds', inline: true }
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
const row = new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId(`duel_accept_${challenger.id}_${opponent.id}`)
|
||||
.setLabel('Accept')
|
||||
.setStyle(ButtonStyle.Success),
|
||||
new ButtonBuilder()
|
||||
.setCustomId(`duel_decline_${challenger.id}_${opponent.id}`)
|
||||
.setLabel('Decline')
|
||||
.setStyle(ButtonStyle.Danger)
|
||||
);
|
||||
|
||||
const message = await interaction.reply({
|
||||
content: `${opponent}`,
|
||||
embeds: [embed],
|
||||
components: [row],
|
||||
fetchReply: true
|
||||
});
|
||||
|
||||
activeDuels.set(duelKey, { opponent: opponent.id, bet });
|
||||
|
||||
const collector = message.createMessageComponentCollector({
|
||||
filter: i => i.user.id === opponent.id && i.customId.includes(challenger.id),
|
||||
time: 60000,
|
||||
max: 1
|
||||
});
|
||||
|
||||
collector.on('collect', async (i) => {
|
||||
activeDuels.delete(duelKey);
|
||||
|
||||
if (i.customId.startsWith('duel_decline')) {
|
||||
const declineEmbed = new EmbedBuilder()
|
||||
.setColor(EMBED_COLORS.error)
|
||||
.setTitle('⚔️ Duel Declined')
|
||||
.setDescription(`${opponent} declined the duel challenge.`)
|
||||
.setTimestamp();
|
||||
|
||||
await i.update({ embeds: [declineEmbed], components: [] });
|
||||
return;
|
||||
}
|
||||
|
||||
const challengerRoll = Math.floor(Math.random() * 100) + 1;
|
||||
const opponentRoll = Math.floor(Math.random() * 100) + 1;
|
||||
|
||||
let winner, loser, winnerRoll, loserRoll;
|
||||
if (challengerRoll > opponentRoll) {
|
||||
winner = challenger;
|
||||
loser = opponent;
|
||||
winnerRoll = challengerRoll;
|
||||
loserRoll = opponentRoll;
|
||||
} else if (opponentRoll > challengerRoll) {
|
||||
winner = opponent;
|
||||
loser = challenger;
|
||||
winnerRoll = opponentRoll;
|
||||
loserRoll = challengerRoll;
|
||||
} else {
|
||||
const tieEmbed = new EmbedBuilder()
|
||||
.setColor(EMBED_COLORS.warning)
|
||||
.setTitle('⚔️ It\'s a Tie!')
|
||||
.setDescription(`Both rolled **${challengerRoll}**! Nobody wins.`)
|
||||
.addFields(
|
||||
{ name: `${challenger.username}`, value: `🎲 ${challengerRoll}`, inline: true },
|
||||
{ name: `${opponent.username}`, value: `🎲 ${opponentRoll}`, inline: true }
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
await i.update({ embeds: [tieEmbed], components: [] });
|
||||
return;
|
||||
}
|
||||
|
||||
if (bet > 0) {
|
||||
if (mode === 'standalone') {
|
||||
await updateStandaloneXp(supabase, winner.id, guildId, bet, winner.username);
|
||||
} else if (supabase) {
|
||||
try {
|
||||
const { data: winnerProfile } = await supabase
|
||||
.from('user_profiles')
|
||||
.select('xp')
|
||||
.eq('discord_id', winner.id)
|
||||
.maybeSingle();
|
||||
|
||||
if (winnerProfile) {
|
||||
await supabase
|
||||
.from('user_profiles')
|
||||
.update({ xp: (winnerProfile.xp || 0) + bet })
|
||||
.eq('discord_id', winner.id);
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
const resultEmbed = new EmbedBuilder()
|
||||
.setColor(EMBED_COLORS.success)
|
||||
.setTitle('⚔️ Duel Results!')
|
||||
.setDescription(`🏆 **${winner.username}** wins the duel!`)
|
||||
.addFields(
|
||||
{ name: `${challenger.username}`, value: `🎲 Rolled: **${challengerRoll}**`, inline: true },
|
||||
{ name: `${opponent.username}`, value: `🎲 Rolled: **${opponentRoll}**`, inline: true }
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
if (bet > 0) {
|
||||
resultEmbed.addFields({ name: '💰 Reward', value: `${winner.username} wins **${bet} XP**!` });
|
||||
}
|
||||
|
||||
await i.update({ embeds: [resultEmbed], components: [] });
|
||||
});
|
||||
|
||||
collector.on('end', async (collected) => {
|
||||
if (collected.size === 0) {
|
||||
activeDuels.delete(duelKey);
|
||||
|
||||
const timeoutEmbed = new EmbedBuilder()
|
||||
.setColor(EMBED_COLORS.warning)
|
||||
.setTitle('⚔️ Duel Expired')
|
||||
.setDescription(`${opponent} didn't respond in time.`)
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.editReply({ embeds: [timeoutEmbed], components: [] });
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } = require('discord.js');
|
||||
const { getServerMode, EMBED_COLORS } = require('../utils/modeHelper');
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
|
|
@ -32,6 +33,16 @@ module.exports = {
|
|||
),
|
||||
|
||||
async execute(interaction, supabase, client) {
|
||||
const mode = await getServerMode(supabase, interaction.guildId);
|
||||
|
||||
if (mode === 'standalone') {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(EMBED_COLORS.standalone)
|
||||
.setTitle('🏠 Standalone Mode')
|
||||
.setDescription('Federation features are disabled in standalone mode.\n\nThis server operates independently and does not sync roles across the AeThex network.\n\nUse `/config mode` to switch to federated mode.');
|
||||
return interaction.reply({ embeds: [embed], ephemeral: true });
|
||||
}
|
||||
|
||||
const subcommand = interaction.options.getSubcommand();
|
||||
|
||||
if (subcommand === 'link') {
|
||||
|
|
|
|||
98
aethex-bot/commands/gift.js
Normal file
98
aethex-bot/commands/gift.js
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
|
||||
const { getServerMode, getEmbedColor, EMBED_COLORS } = require('../utils/modeHelper');
|
||||
const { updateStandaloneXp, getStandaloneXp } = require('../utils/standaloneXp');
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('gift')
|
||||
.setDescription('Gift XP to another user')
|
||||
.addUserOption(option =>
|
||||
option.setName('user')
|
||||
.setDescription('User to gift XP to')
|
||||
.setRequired(true)
|
||||
)
|
||||
.addIntegerOption(option =>
|
||||
option.setName('amount')
|
||||
.setDescription('Amount of XP to gift')
|
||||
.setRequired(true)
|
||||
.setMinValue(1)
|
||||
.setMaxValue(1000)
|
||||
),
|
||||
|
||||
async execute(interaction, supabase, client) {
|
||||
const recipient = interaction.options.getUser('user');
|
||||
const amount = interaction.options.getInteger('amount');
|
||||
const sender = interaction.user;
|
||||
const mode = await getServerMode(supabase, interaction.guildId);
|
||||
const guildId = interaction.guildId;
|
||||
|
||||
if (recipient.id === sender.id) {
|
||||
return interaction.reply({ content: "You can't gift XP to yourself!", ephemeral: true });
|
||||
}
|
||||
|
||||
if (recipient.bot) {
|
||||
return interaction.reply({ content: "You can't gift XP to bots!", ephemeral: true });
|
||||
}
|
||||
|
||||
if (!supabase) {
|
||||
return interaction.reply({ content: 'Gift system unavailable.', ephemeral: true });
|
||||
}
|
||||
|
||||
try {
|
||||
let senderXp = 0;
|
||||
|
||||
if (mode === 'standalone') {
|
||||
const senderData = await getStandaloneXp(supabase, sender.id, guildId);
|
||||
senderXp = senderData?.xp || 0;
|
||||
} else {
|
||||
const { data } = await supabase
|
||||
.from('user_profiles')
|
||||
.select('xp')
|
||||
.eq('discord_id', sender.id)
|
||||
.maybeSingle();
|
||||
senderXp = data?.xp || 0;
|
||||
}
|
||||
|
||||
if (senderXp < amount) {
|
||||
return interaction.reply({
|
||||
content: `You don't have enough XP! You have ${senderXp} XP.`,
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
if (mode === 'standalone') {
|
||||
await updateStandaloneXp(supabase, sender.id, guildId, -amount, sender.username);
|
||||
await updateStandaloneXp(supabase, recipient.id, guildId, amount, recipient.username);
|
||||
} else {
|
||||
await supabase
|
||||
.from('user_profiles')
|
||||
.update({ xp: senderXp - amount })
|
||||
.eq('discord_id', sender.id);
|
||||
|
||||
const { data: recipientData } = await supabase
|
||||
.from('user_profiles')
|
||||
.select('xp')
|
||||
.eq('discord_id', recipient.id)
|
||||
.maybeSingle();
|
||||
|
||||
if (recipientData) {
|
||||
await supabase
|
||||
.from('user_profiles')
|
||||
.update({ xp: (recipientData.xp || 0) + amount })
|
||||
.eq('discord_id', recipient.id);
|
||||
}
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(EMBED_COLORS.success)
|
||||
.setTitle('🎁 Gift Sent!')
|
||||
.setDescription(`${sender} gifted **${amount} XP** to ${recipient}!`)
|
||||
.setThumbnail(recipient.displayAvatarURL({ size: 128 }))
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.reply({ embeds: [embed] });
|
||||
} catch (e) {
|
||||
await interaction.reply({ content: 'Failed to send gift.', ephemeral: true });
|
||||
}
|
||||
},
|
||||
};
|
||||
189
aethex-bot/commands/heist.js
Normal file
189
aethex-bot/commands/heist.js
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
const { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js');
|
||||
const { getServerMode, getEmbedColor, EMBED_COLORS } = require('../utils/modeHelper');
|
||||
const { updateStandaloneXp } = require('../utils/standaloneXp');
|
||||
|
||||
const activeHeists = new Map();
|
||||
|
||||
const TARGETS = [
|
||||
{ name: 'Corner Store', emoji: '🏪', difficulty: 0.7, minReward: 20, maxReward: 50 },
|
||||
{ name: 'Gas Station', emoji: '⛽', difficulty: 0.6, minReward: 30, maxReward: 70 },
|
||||
{ name: 'Jewelry Store', emoji: '💎', difficulty: 0.5, minReward: 50, maxReward: 120 },
|
||||
{ name: 'Bank', emoji: '🏦', difficulty: 0.4, minReward: 80, maxReward: 200 },
|
||||
{ name: 'Casino', emoji: '🎰', difficulty: 0.3, minReward: 100, maxReward: 300 },
|
||||
{ name: 'Federal Reserve', emoji: '🏛️', difficulty: 0.2, minReward: 150, maxReward: 500 },
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('heist')
|
||||
.setDescription('Start a group heist!')
|
||||
.addStringOption(option =>
|
||||
option.setName('target')
|
||||
.setDescription('Choose your heist target')
|
||||
.setRequired(true)
|
||||
.addChoices(
|
||||
{ name: '🏪 Corner Store (Easy)', value: '0' },
|
||||
{ name: '⛽ Gas Station', value: '1' },
|
||||
{ name: '💎 Jewelry Store', value: '2' },
|
||||
{ name: '🏦 Bank (Medium)', value: '3' },
|
||||
{ name: '🎰 Casino', value: '4' },
|
||||
{ name: '🏛️ Federal Reserve (Hard)', value: '5' }
|
||||
)
|
||||
),
|
||||
|
||||
async execute(interaction, supabase, client) {
|
||||
const targetIndex = parseInt(interaction.options.getString('target'));
|
||||
const target = TARGETS[targetIndex];
|
||||
const leader = interaction.user;
|
||||
const mode = await getServerMode(supabase, interaction.guildId);
|
||||
const guildId = interaction.guildId;
|
||||
|
||||
const heistKey = `${guildId}-heist`;
|
||||
if (activeHeists.has(heistKey)) {
|
||||
return interaction.reply({
|
||||
content: 'There\'s already an active heist in this server! Wait for it to finish.',
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
const participants = new Set([leader.id]);
|
||||
activeHeists.set(heistKey, {
|
||||
target,
|
||||
leader: leader.id,
|
||||
participants,
|
||||
usernames: { [leader.id]: leader.username }
|
||||
});
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(getEmbedColor(mode))
|
||||
.setTitle(`${target.emoji} Heist: ${target.name}`)
|
||||
.setDescription(`${leader} is planning a heist!\n\nClick **Join Heist** to participate.\nMore participants = higher success chance!`)
|
||||
.addFields(
|
||||
{ name: '🎯 Target', value: target.name, inline: true },
|
||||
{ name: '💰 Reward', value: `${target.minReward}-${target.maxReward} XP each`, inline: true },
|
||||
{ name: '📊 Base Success', value: `${Math.round(target.difficulty * 100)}%`, inline: true },
|
||||
{ name: '👥 Participants', value: `1. ${leader.username}` }
|
||||
)
|
||||
.setFooter({ text: 'Heist starts in 60 seconds!' })
|
||||
.setTimestamp();
|
||||
|
||||
const row = new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId(`heist_join_${guildId}`)
|
||||
.setLabel('Join Heist')
|
||||
.setStyle(ButtonStyle.Primary)
|
||||
.setEmoji('🔫'),
|
||||
new ButtonBuilder()
|
||||
.setCustomId(`heist_start_${guildId}`)
|
||||
.setLabel('Start Now')
|
||||
.setStyle(ButtonStyle.Success)
|
||||
.setEmoji('🚀')
|
||||
);
|
||||
|
||||
const message = await interaction.reply({ embeds: [embed], components: [row], fetchReply: true });
|
||||
|
||||
const collector = message.createMessageComponentCollector({
|
||||
filter: i => i.customId.startsWith('heist_'),
|
||||
time: 60000
|
||||
});
|
||||
|
||||
collector.on('collect', async (i) => {
|
||||
const heistData = activeHeists.get(heistKey);
|
||||
if (!heistData) return;
|
||||
|
||||
if (i.customId.startsWith('heist_join')) {
|
||||
if (heistData.participants.has(i.user.id)) {
|
||||
return i.reply({ content: 'You\'re already in this heist!', ephemeral: true });
|
||||
}
|
||||
|
||||
heistData.participants.add(i.user.id);
|
||||
heistData.usernames[i.user.id] = i.user.username;
|
||||
|
||||
const participantList = Array.from(heistData.participants)
|
||||
.map((id, idx) => `${idx + 1}. ${heistData.usernames[id]}`)
|
||||
.join('\n');
|
||||
|
||||
const bonusChance = (heistData.participants.size - 1) * 5;
|
||||
const totalChance = Math.min(95, Math.round(target.difficulty * 100) + bonusChance);
|
||||
|
||||
embed.spliceFields(3, 1, { name: '👥 Participants', value: participantList });
|
||||
embed.spliceFields(2, 1, { name: '📊 Success Chance', value: `${totalChance}%`, inline: true });
|
||||
|
||||
await i.update({ embeds: [embed] });
|
||||
}
|
||||
|
||||
if (i.customId.startsWith('heist_start') && i.user.id === heistData.leader) {
|
||||
collector.stop('started');
|
||||
}
|
||||
});
|
||||
|
||||
collector.on('end', async (collected, reason) => {
|
||||
const heistData = activeHeists.get(heistKey);
|
||||
if (!heistData) return;
|
||||
|
||||
activeHeists.delete(heistKey);
|
||||
|
||||
const participantCount = heistData.participants.size;
|
||||
const bonusChance = (participantCount - 1) * 5;
|
||||
const successChance = Math.min(95, target.difficulty * 100 + bonusChance) / 100;
|
||||
|
||||
const success = Math.random() < successChance;
|
||||
|
||||
if (success) {
|
||||
const baseReward = Math.floor(Math.random() * (target.maxReward - target.minReward + 1)) + target.minReward;
|
||||
const teamBonus = Math.floor(baseReward * (participantCount - 1) * 0.1);
|
||||
const totalReward = baseReward + teamBonus;
|
||||
|
||||
for (const participantId of heistData.participants) {
|
||||
if (mode === 'standalone') {
|
||||
await updateStandaloneXp(supabase, participantId, guildId, totalReward, heistData.usernames[participantId]);
|
||||
} else if (supabase) {
|
||||
try {
|
||||
const { data: profile } = await supabase
|
||||
.from('user_profiles')
|
||||
.select('xp')
|
||||
.eq('discord_id', participantId)
|
||||
.maybeSingle();
|
||||
|
||||
if (profile) {
|
||||
await supabase
|
||||
.from('user_profiles')
|
||||
.update({ xp: (profile.xp || 0) + totalReward })
|
||||
.eq('discord_id', participantId);
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
const participantMentions = Array.from(heistData.participants)
|
||||
.map(id => `<@${id}>`)
|
||||
.join(', ');
|
||||
|
||||
const successEmbed = new EmbedBuilder()
|
||||
.setColor(EMBED_COLORS.success)
|
||||
.setTitle(`${target.emoji} Heist Successful!`)
|
||||
.setDescription(`The crew pulled off the ${target.name} heist!`)
|
||||
.addFields(
|
||||
{ name: '👥 Crew', value: participantMentions },
|
||||
{ name: '💰 Each Earned', value: `${totalReward} XP` }
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.editReply({ embeds: [successEmbed], components: [] });
|
||||
} else {
|
||||
const failEmbed = new EmbedBuilder()
|
||||
.setColor(EMBED_COLORS.error)
|
||||
.setTitle(`${target.emoji} Heist Failed!`)
|
||||
.setDescription(`The ${target.name} heist went wrong! The crew escaped empty-handed.`)
|
||||
.addFields({
|
||||
name: '👥 Crew',
|
||||
value: Array.from(heistData.participants).map(id => heistData.usernames[id]).join(', ')
|
||||
})
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.editReply({ embeds: [failEmbed], components: [] });
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
const { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, StringSelectMenuBuilder } = require("discord.js");
|
||||
const { getServerMode, getEmbedColor, getModeEmoji, getModeDisplayName } = require('../utils/modeHelper');
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
|
|
@ -13,27 +14,33 @@ module.exports = {
|
|||
{ name: '⚔️ Realms', value: 'realms' },
|
||||
{ name: '📊 Community', value: 'community' },
|
||||
{ name: '⭐ Leveling', value: 'leveling' },
|
||||
{ name: '🎮 Fun & Games', value: 'fun' },
|
||||
{ name: '💰 Economy', value: 'economy' },
|
||||
{ name: '👥 Social', value: 'social' },
|
||||
{ name: '🛡️ Moderation', value: 'moderation' },
|
||||
{ name: '🔧 Utility', value: 'utility' },
|
||||
{ name: '⚙️ Admin', value: 'admin' }
|
||||
)
|
||||
),
|
||||
|
||||
async execute(interaction) {
|
||||
async execute(interaction, supabase) {
|
||||
const category = interaction.options.getString('category');
|
||||
const mode = await getServerMode(supabase, interaction.guildId);
|
||||
|
||||
if (category) {
|
||||
const embed = getCategoryEmbed(category);
|
||||
const embed = getCategoryEmbed(category, mode);
|
||||
return interaction.reply({ embeds: [embed], ephemeral: true });
|
||||
}
|
||||
|
||||
const modeIndicator = `${getModeEmoji(mode)} ${getModeDisplayName(mode)} Mode`;
|
||||
|
||||
const mainEmbed = new EmbedBuilder()
|
||||
.setColor(0x7c3aed)
|
||||
.setColor(getEmbedColor(mode))
|
||||
.setAuthor({
|
||||
name: 'AeThex Bot Help',
|
||||
iconURL: interaction.client.user.displayAvatarURL()
|
||||
})
|
||||
.setDescription('Welcome to AeThex Bot! Select a category below to view commands.')
|
||||
.setDescription(`Welcome to AeThex Bot! Select a category below to view commands.\n\n**Current Mode:** ${modeIndicator}`)
|
||||
.addFields(
|
||||
{
|
||||
name: "🔗 Account",
|
||||
|
|
@ -42,52 +49,52 @@ module.exports = {
|
|||
},
|
||||
{
|
||||
name: "⚔️ Realms",
|
||||
value: "`/set-realm` `/verify-role` `/refresh-roles`",
|
||||
value: "`/set-realm` `/federation` `/refresh-roles`",
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "📊 Community",
|
||||
value: "`/stats` `/leaderboard` `/post` `/poll`",
|
||||
value: "`/stats` `/leaderboard` `/poll` `/post`",
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "⭐ Leveling",
|
||||
value: "`/rank` `/daily` `/badges`",
|
||||
value: "`/rank` `/daily` `/prestige` `/badges`",
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "🎮 Fun & Games",
|
||||
value: "`/8ball` `/roll` `/trivia` `/duel` `/slots`",
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "💰 Economy",
|
||||
value: "`/work` `/heist` `/gift` `/shop`",
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "👥 Social",
|
||||
value: "`/rep` `/hug` `/birthday` `/remind`",
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "🛡️ Moderation",
|
||||
value: "`/warn` `/kick` `/ban` `/timeout` `/modlog`",
|
||||
value: "`/warn` `/kick` `/ban` `/timeout`",
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "🔧 Utility",
|
||||
value: "`/userinfo` `/serverinfo` `/avatar`",
|
||||
value: "`/translate` `/define` `/math` `/qr`",
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "⚙️ Admin",
|
||||
value: "`/config` `/announce` `/embed` `/rolepanel`",
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "🎉 Fun",
|
||||
value: "`/giveaway` `/schedule`",
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "🛡️ Auto-Mod",
|
||||
value: "`/automod`",
|
||||
value: "`/config` `/starboard` `/automod`",
|
||||
inline: true,
|
||||
}
|
||||
)
|
||||
.addFields({
|
||||
name: "🔗 Quick Links",
|
||||
value: "[AeThex Platform](https://aethex.dev) • [Creator Directory](https://aethex.dev/creators) • [Community Feed](https://aethex.dev/community/feed)",
|
||||
inline: false,
|
||||
})
|
||||
.setFooter({
|
||||
text: "Use /help [category] for detailed commands • AeThex Bot",
|
||||
text: "Use /help [category] for detailed commands",
|
||||
iconURL: interaction.client.user.displayAvatarURL()
|
||||
})
|
||||
.setTimestamp();
|
||||
|
|
@ -100,6 +107,9 @@ module.exports = {
|
|||
{ label: 'Realms', description: 'Realm selection and roles', emoji: '⚔️', value: 'realms' },
|
||||
{ label: 'Community', description: 'Community features', emoji: '📊', value: 'community' },
|
||||
{ label: 'Leveling', description: 'XP and leveling system', emoji: '⭐', value: 'leveling' },
|
||||
{ label: 'Fun & Games', description: 'Fun commands and minigames', emoji: '🎮', value: 'fun' },
|
||||
{ label: 'Economy', description: 'Earn and spend XP', emoji: '💰', value: 'economy' },
|
||||
{ label: 'Social', description: 'Social interactions', emoji: '👥', value: 'social' },
|
||||
{ label: 'Moderation', description: 'Moderation tools', emoji: '🛡️', value: 'moderation' },
|
||||
{ label: 'Utility', description: 'Utility commands', emoji: '🔧', value: 'utility' },
|
||||
{ label: 'Admin', description: 'Admin and config', emoji: '⚙️', value: 'admin' },
|
||||
|
|
@ -111,7 +121,7 @@ module.exports = {
|
|||
},
|
||||
};
|
||||
|
||||
function getCategoryEmbed(category) {
|
||||
function getCategoryEmbed(category, mode = 'federated') {
|
||||
const categories = {
|
||||
account: {
|
||||
title: '🔗 Account Commands',
|
||||
|
|
@ -119,16 +129,17 @@ function getCategoryEmbed(category) {
|
|||
commands: [
|
||||
{ name: '/verify', desc: 'Link your Discord account to AeThex' },
|
||||
{ name: '/unlink', desc: 'Disconnect your Discord from AeThex' },
|
||||
{ name: '/profile [@user]', desc: 'View your or another user\'s AeThex profile' },
|
||||
{ name: '/profile [@user]', desc: 'View your or another user\'s profile' },
|
||||
]
|
||||
},
|
||||
realms: {
|
||||
title: '⚔️ Realm Commands',
|
||||
color: 0xf97316,
|
||||
commands: [
|
||||
{ name: '/set-realm', desc: 'Choose your primary realm (Labs, GameForge, Corp, Foundation, Dev-Link)' },
|
||||
{ name: '/verify-role', desc: 'Check your assigned Discord roles' },
|
||||
{ name: '/set-realm', desc: 'Choose your primary realm (Federation mode only)' },
|
||||
{ name: '/federation', desc: 'Manage cross-server role sync' },
|
||||
{ name: '/refresh-roles', desc: 'Sync your roles based on AeThex profile' },
|
||||
{ name: '/verify-role', desc: 'Check your assigned Discord roles' },
|
||||
]
|
||||
},
|
||||
community: {
|
||||
|
|
@ -147,9 +158,47 @@ function getCategoryEmbed(category) {
|
|||
title: '⭐ Leveling Commands',
|
||||
color: 0xfbbf24,
|
||||
commands: [
|
||||
{ name: '/rank [@user]', desc: 'View your level and unified XP' },
|
||||
{ name: '/rank [@user]', desc: 'View your level and XP' },
|
||||
{ name: '/daily', desc: 'Claim your daily XP bonus (+50 base + streak)' },
|
||||
{ name: '/badges', desc: 'View earned badges across platforms' },
|
||||
{ name: '/prestige', desc: 'Prestige at Level 50 for permanent bonuses' },
|
||||
{ name: '/badges', desc: 'View earned badges' },
|
||||
{ name: '/achievements', desc: 'View available achievements' },
|
||||
{ name: '/quests', desc: 'View and track your quests' },
|
||||
]
|
||||
},
|
||||
fun: {
|
||||
title: '🎮 Fun & Games Commands',
|
||||
color: 0xec4899,
|
||||
commands: [
|
||||
{ name: '/8ball [question]', desc: 'Ask the magic 8-ball a question' },
|
||||
{ name: '/coinflip [call]', desc: 'Flip a coin' },
|
||||
{ name: '/roll [dice]', desc: 'Roll dice (e.g., 2d6, d20, 3d8+5)' },
|
||||
{ name: '/trivia [category]', desc: 'Answer trivia questions for XP' },
|
||||
{ name: '/duel @user [bet]', desc: 'Challenge someone to a duel' },
|
||||
{ name: '/slots [bet]', desc: 'Try your luck at the slot machine' },
|
||||
{ name: '/afk [reason]', desc: 'Set your AFK status' },
|
||||
]
|
||||
},
|
||||
economy: {
|
||||
title: '💰 Economy Commands',
|
||||
color: 0x10b981,
|
||||
commands: [
|
||||
{ name: '/work', desc: 'Work to earn XP (hourly)' },
|
||||
{ name: '/heist [target]', desc: 'Start a group heist' },
|
||||
{ name: '/gift @user [amount]', desc: 'Gift XP to another user' },
|
||||
{ name: '/shop', desc: 'Browse and purchase items' },
|
||||
{ name: '/inventory [@user]', desc: 'View your inventory' },
|
||||
{ name: '/trade @user [offer] [request]', desc: 'Trade items with another user' },
|
||||
]
|
||||
},
|
||||
social: {
|
||||
title: '👥 Social Commands',
|
||||
color: 0x8b5cf6,
|
||||
commands: [
|
||||
{ name: '/rep @user [reason]', desc: 'Give reputation to someone' },
|
||||
{ name: '/hug @user', desc: 'Give someone a virtual hug' },
|
||||
{ name: '/birthday set/view/upcoming', desc: 'Manage birthdays' },
|
||||
{ name: '/remind set/list/cancel', desc: 'Set personal reminders' },
|
||||
]
|
||||
},
|
||||
moderation: {
|
||||
|
|
@ -166,28 +215,35 @@ function getCategoryEmbed(category) {
|
|||
},
|
||||
utility: {
|
||||
title: '🔧 Utility Commands',
|
||||
color: 0x8b5cf6,
|
||||
color: 0x6366f1,
|
||||
commands: [
|
||||
{ name: '/translate [text] [to]', desc: 'Translate text to another language' },
|
||||
{ name: '/define [word]', desc: 'Look up word definitions' },
|
||||
{ name: '/math [expression]', desc: 'Calculate math expressions' },
|
||||
{ name: '/color [hex/rgb]', desc: 'View color information' },
|
||||
{ name: '/qr [text]', desc: 'Generate a QR code' },
|
||||
{ name: '/userinfo [@user]', desc: 'View detailed user information' },
|
||||
{ name: '/serverinfo', desc: 'View server statistics and info' },
|
||||
{ name: '/serverinfo', desc: 'View server statistics' },
|
||||
{ name: '/avatar [@user]', desc: 'Get a user\'s avatar' },
|
||||
{ name: '/status', desc: 'View network status' },
|
||||
{ name: '/status', desc: 'View bot status' },
|
||||
]
|
||||
},
|
||||
admin: {
|
||||
title: '⚙️ Admin Commands',
|
||||
color: 0x6b7280,
|
||||
commands: [
|
||||
{ name: '/config', desc: 'View and edit server configuration' },
|
||||
{ name: '/announce', desc: 'Send cross-server announcements' },
|
||||
{ name: '/config', desc: 'View and edit server configuration (including mode)' },
|
||||
{ name: '/starboard setup/disable/status', desc: 'Configure the starboard' },
|
||||
{ name: '/announce', desc: 'Send announcements' },
|
||||
{ name: '/embed', desc: 'Create custom embed messages' },
|
||||
{ name: '/rolepanel', desc: 'Create role button panels' },
|
||||
{ name: '/giveaway', desc: 'Create and manage giveaways' },
|
||||
{ name: '/schedule', desc: 'Schedule messages for later' },
|
||||
{ name: '/automod', desc: 'Configure auto-moderation' },
|
||||
{ name: '/admin', desc: 'Bot administration commands' },
|
||||
{ name: '/federation', desc: 'Manage cross-server role sync' },
|
||||
{ name: '/ticket', desc: 'Manage support tickets' },
|
||||
{ name: '/xp-settings', desc: 'Configure XP system' },
|
||||
{ name: '/level-roles', desc: 'Set up level-up role rewards' },
|
||||
{ name: '/quests-manage', desc: 'Manage quests' },
|
||||
{ name: '/shop-manage', desc: 'Manage shop items' },
|
||||
]
|
||||
}
|
||||
};
|
||||
|
|
|
|||
47
aethex-bot/commands/hug.js
Normal file
47
aethex-bot/commands/hug.js
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
|
||||
const { getServerMode, getEmbedColor } = require('../utils/modeHelper');
|
||||
|
||||
const HUG_GIFS = [
|
||||
'https://media.giphy.com/media/l2QDM9Jnim1YVILXa/giphy.gif',
|
||||
'https://media.giphy.com/media/od5H3PmEG5EVq/giphy.gif',
|
||||
'https://media.giphy.com/media/ZQN9jsRWp1M76/giphy.gif',
|
||||
'https://media.giphy.com/media/lrr9rHuoJOE0w/giphy.gif',
|
||||
'https://media.giphy.com/media/3M4NpbLCTxBqU/giphy.gif',
|
||||
];
|
||||
|
||||
const HUG_MESSAGES = [
|
||||
'{user} gives {target} a warm hug!',
|
||||
'{user} wraps {target} in a cozy embrace!',
|
||||
'{user} hugs {target} tightly!',
|
||||
'{user} sends virtual hugs to {target}!',
|
||||
'{user} gives {target} the biggest hug ever!',
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('hug')
|
||||
.setDescription('Give someone a virtual hug')
|
||||
.addUserOption(option =>
|
||||
option.setName('user')
|
||||
.setDescription('The user to hug')
|
||||
.setRequired(true)
|
||||
),
|
||||
|
||||
async execute(interaction, supabase, client) {
|
||||
const targetUser = interaction.options.getUser('user');
|
||||
const mode = await getServerMode(supabase, interaction.guildId);
|
||||
|
||||
const gif = HUG_GIFS[Math.floor(Math.random() * HUG_GIFS.length)];
|
||||
const message = HUG_MESSAGES[Math.floor(Math.random() * HUG_MESSAGES.length)]
|
||||
.replace('{user}', interaction.user.toString())
|
||||
.replace('{target}', targetUser.toString());
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0xFFB6C1)
|
||||
.setDescription(`🤗 ${message}`)
|
||||
.setImage(gif)
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.reply({ embeds: [embed] });
|
||||
},
|
||||
};
|
||||
59
aethex-bot/commands/inventory.js
Normal file
59
aethex-bot/commands/inventory.js
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
|
||||
const { getServerMode, getEmbedColor, EMBED_COLORS } = require('../utils/modeHelper');
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('inventory')
|
||||
.setDescription('View your inventory')
|
||||
.addUserOption(option =>
|
||||
option.setName('user')
|
||||
.setDescription('User to view inventory of')
|
||||
.setRequired(false)
|
||||
),
|
||||
|
||||
async execute(interaction, supabase, client) {
|
||||
const targetUser = interaction.options.getUser('user') || interaction.user;
|
||||
const mode = await getServerMode(supabase, interaction.guildId);
|
||||
const guildId = interaction.guildId;
|
||||
|
||||
if (!supabase) {
|
||||
return interaction.reply({ content: 'Inventory system unavailable.', ephemeral: true });
|
||||
}
|
||||
|
||||
try {
|
||||
const { data: items } = await supabase
|
||||
.from('user_inventory')
|
||||
.select('*, shop_items(*)')
|
||||
.eq('guild_id', guildId)
|
||||
.eq('user_id', targetUser.id);
|
||||
|
||||
if (!items || items.length === 0) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(getEmbedColor(mode))
|
||||
.setTitle(`🎒 ${targetUser.username}'s Inventory`)
|
||||
.setDescription('Inventory is empty. Buy items from the `/shop`!')
|
||||
.setThumbnail(targetUser.displayAvatarURL({ size: 128 }))
|
||||
.setTimestamp();
|
||||
return interaction.reply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
const itemList = items.map(inv => {
|
||||
const item = inv.shop_items;
|
||||
if (!item) return null;
|
||||
return `${item.emoji || '📦'} **${item.name}** x${inv.quantity}`;
|
||||
}).filter(Boolean).join('\n');
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(getEmbedColor(mode))
|
||||
.setTitle(`🎒 ${targetUser.username}'s Inventory`)
|
||||
.setDescription(itemList || 'No items')
|
||||
.setThumbnail(targetUser.displayAvatarURL({ size: 128 }))
|
||||
.setFooter({ text: `${items.length} unique item(s)` })
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.reply({ embeds: [embed] });
|
||||
} catch (e) {
|
||||
await interaction.reply({ content: 'Failed to fetch inventory.', ephemeral: true });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
@ -1,21 +1,23 @@
|
|||
const { SlashCommandBuilder, EmbedBuilder } = require("discord.js");
|
||||
const { getServerMode, EMBED_COLORS } = require("../utils/modeHelper");
|
||||
const { getStandaloneLeaderboard, calculateLevel } = require("../utils/standaloneXp");
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName("leaderboard")
|
||||
.setDescription("View the top AeThex contributors")
|
||||
.setDescription("View the top contributors")
|
||||
.addStringOption((option) =>
|
||||
option
|
||||
.setName("category")
|
||||
.setDescription("Leaderboard category")
|
||||
.setRequired(false)
|
||||
.addChoices(
|
||||
{ name: "⭐ XP Leaders (All-Time)", value: "xp" },
|
||||
{ name: "📅 This Week", value: "weekly" },
|
||||
{ name: "📆 This Month", value: "monthly" },
|
||||
{ name: "🔥 Most Active (Posts)", value: "posts" },
|
||||
{ name: "❤️ Most Liked", value: "likes" },
|
||||
{ name: "🎨 Top Creators", value: "creators" }
|
||||
{ name: "XP Leaders (All-Time)", value: "xp" },
|
||||
{ name: "This Week", value: "weekly" },
|
||||
{ name: "This Month", value: "monthly" },
|
||||
{ name: "Most Active (Posts)", value: "posts" },
|
||||
{ name: "Most Liked", value: "likes" },
|
||||
{ name: "Top Creators", value: "creators" }
|
||||
)
|
||||
),
|
||||
|
||||
|
|
@ -26,293 +28,420 @@ module.exports = {
|
|||
await interaction.deferReply();
|
||||
|
||||
try {
|
||||
const mode = await getServerMode(supabase, interaction.guildId);
|
||||
const category = interaction.options.getString("category") || "xp";
|
||||
const guildId = interaction.guildId;
|
||||
|
||||
let leaderboardData = [];
|
||||
let title = "";
|
||||
let emoji = "";
|
||||
let color = 0x7c3aed;
|
||||
let periodInfo = "";
|
||||
|
||||
if (category === "weekly") {
|
||||
title = "Weekly XP Leaderboard";
|
||||
emoji = "📅";
|
||||
color = 0x22c55e;
|
||||
|
||||
const now = new Date();
|
||||
const dayOfWeek = now.getDay();
|
||||
const diffToMonday = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
|
||||
const weekStart = new Date(now);
|
||||
weekStart.setDate(now.getDate() - diffToMonday);
|
||||
weekStart.setHours(0, 0, 0, 0);
|
||||
const weekStartStr = weekStart.toISOString().split('T')[0];
|
||||
|
||||
const weekEnd = new Date(weekStart);
|
||||
weekEnd.setDate(weekStart.getDate() + 6);
|
||||
periodInfo = `${weekStart.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} - ${weekEnd.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}`;
|
||||
|
||||
// Fetch weekly records using period_type
|
||||
const { data: periodicData } = await supabase
|
||||
.from("periodic_xp")
|
||||
.select("discord_id, weekly_xp, weekly_messages")
|
||||
.eq("guild_id", guildId)
|
||||
.eq("period_type", "week")
|
||||
.eq("period_start", weekStartStr);
|
||||
|
||||
// Aggregate per user (handles multiple records if they exist)
|
||||
const aggregated = {};
|
||||
for (const entry of periodicData || []) {
|
||||
if (!aggregated[entry.discord_id]) {
|
||||
aggregated[entry.discord_id] = { xp: 0, messages: 0 };
|
||||
}
|
||||
aggregated[entry.discord_id].xp += entry.weekly_xp || 0;
|
||||
aggregated[entry.discord_id].messages += entry.weekly_messages || 0;
|
||||
}
|
||||
|
||||
// Sort and limit after aggregation
|
||||
const sortedUsers = Object.entries(aggregated)
|
||||
.sort(([, a], [, b]) => b.xp - a.xp)
|
||||
.slice(0, 10);
|
||||
|
||||
for (const [discordId, data] of sortedUsers) {
|
||||
try {
|
||||
const member = await interaction.guild.members.fetch(discordId).catch(() => null);
|
||||
const displayName = member?.displayName || member?.user?.username || "Unknown User";
|
||||
leaderboardData.push({
|
||||
name: displayName,
|
||||
value: `${data.xp.toLocaleString()} XP • ${data.messages} msgs`,
|
||||
xp: data.xp
|
||||
});
|
||||
} catch (e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} else if (category === "monthly") {
|
||||
title = "Monthly XP Leaderboard";
|
||||
emoji = "📆";
|
||||
color = 0x3b82f6;
|
||||
|
||||
const now = new Date();
|
||||
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
const monthStartStr = monthStart.toISOString().split('T')[0];
|
||||
|
||||
periodInfo = monthStart.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
|
||||
|
||||
// Fetch monthly records using period_type
|
||||
const { data: periodicData } = await supabase
|
||||
.from("periodic_xp")
|
||||
.select("discord_id, monthly_xp, monthly_messages")
|
||||
.eq("guild_id", guildId)
|
||||
.eq("period_type", "month")
|
||||
.eq("period_start", monthStartStr);
|
||||
|
||||
// Aggregate all entries per user first
|
||||
const aggregated = {};
|
||||
for (const entry of periodicData || []) {
|
||||
if (!aggregated[entry.discord_id]) {
|
||||
aggregated[entry.discord_id] = { xp: 0, messages: 0 };
|
||||
}
|
||||
aggregated[entry.discord_id].xp += entry.monthly_xp || 0;
|
||||
aggregated[entry.discord_id].messages += entry.monthly_messages || 0;
|
||||
}
|
||||
|
||||
// Sort and limit AFTER aggregation
|
||||
const sortedUsers = Object.entries(aggregated)
|
||||
.sort(([, a], [, b]) => b.xp - a.xp)
|
||||
.slice(0, 10);
|
||||
|
||||
for (const [discordId, data] of sortedUsers) {
|
||||
try {
|
||||
const member = await interaction.guild.members.fetch(discordId).catch(() => null);
|
||||
const displayName = member?.displayName || member?.user?.username || "Unknown User";
|
||||
leaderboardData.push({
|
||||
name: displayName,
|
||||
value: `${data.xp.toLocaleString()} XP • ${data.messages} msgs`,
|
||||
xp: data.xp
|
||||
});
|
||||
} catch (e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} else if (category === "xp") {
|
||||
title = "XP Leaderboard (All-Time)";
|
||||
emoji = "⭐";
|
||||
color = 0xfbbf24;
|
||||
|
||||
const { data: profiles } = await supabase
|
||||
.from("user_profiles")
|
||||
.select("id, username, full_name, avatar_url, xp")
|
||||
.not("xp", "is", null)
|
||||
.order("xp", { ascending: false })
|
||||
.limit(10);
|
||||
|
||||
for (const profile of profiles || []) {
|
||||
const level = Math.floor(Math.sqrt((profile.xp || 0) / 100));
|
||||
leaderboardData.push({
|
||||
name: profile.full_name || profile.username || "Anonymous",
|
||||
value: `Level ${level} • ${(profile.xp || 0).toLocaleString()} XP`,
|
||||
username: profile.username,
|
||||
xp: profile.xp || 0
|
||||
});
|
||||
}
|
||||
} else if (category === "posts") {
|
||||
title = "Most Active Posters";
|
||||
emoji = "🔥";
|
||||
color = 0xef4444;
|
||||
|
||||
const { data: posts } = await supabase
|
||||
.from("community_posts")
|
||||
.select("user_id")
|
||||
.not("user_id", "is", null);
|
||||
|
||||
const postCounts = {};
|
||||
posts?.forEach((post) => {
|
||||
postCounts[post.user_id] = (postCounts[post.user_id] || 0) + 1;
|
||||
});
|
||||
|
||||
const sortedUsers = Object.entries(postCounts)
|
||||
.sort(([, a], [, b]) => b - a)
|
||||
.slice(0, 10);
|
||||
|
||||
for (const [userId, count] of sortedUsers) {
|
||||
const { data: profile } = await supabase
|
||||
.from("user_profiles")
|
||||
.select("username, full_name, avatar_url")
|
||||
.eq("id", userId)
|
||||
.single();
|
||||
|
||||
if (profile) {
|
||||
leaderboardData.push({
|
||||
name: profile.full_name || profile.username || "Anonymous",
|
||||
value: `${count} posts`,
|
||||
username: profile.username,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (category === "likes") {
|
||||
title = "Most Liked Users";
|
||||
emoji = "❤️";
|
||||
color = 0xec4899;
|
||||
|
||||
const { data: posts } = await supabase
|
||||
.from("community_posts")
|
||||
.select("user_id, likes_count")
|
||||
.not("user_id", "is", null)
|
||||
.order("likes_count", { ascending: false });
|
||||
|
||||
const likeCounts = {};
|
||||
posts?.forEach((post) => {
|
||||
likeCounts[post.user_id] =
|
||||
(likeCounts[post.user_id] || 0) + (post.likes_count || 0);
|
||||
});
|
||||
|
||||
const sortedUsers = Object.entries(likeCounts)
|
||||
.sort(([, a], [, b]) => b - a)
|
||||
.slice(0, 10);
|
||||
|
||||
for (const [userId, count] of sortedUsers) {
|
||||
const { data: profile } = await supabase
|
||||
.from("user_profiles")
|
||||
.select("username, full_name, avatar_url")
|
||||
.eq("id", userId)
|
||||
.single();
|
||||
|
||||
if (profile) {
|
||||
leaderboardData.push({
|
||||
name: profile.full_name || profile.username || "Anonymous",
|
||||
value: `${count.toLocaleString()} likes`,
|
||||
username: profile.username,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (category === "creators") {
|
||||
title = "Top Creators";
|
||||
emoji = "🎨";
|
||||
color = 0x8b5cf6;
|
||||
|
||||
const { data: creators } = await supabase
|
||||
.from("aethex_creators")
|
||||
.select("user_id, total_projects, verified, featured")
|
||||
.order("total_projects", { ascending: false })
|
||||
.limit(10);
|
||||
|
||||
for (const creator of creators || []) {
|
||||
const { data: profile } = await supabase
|
||||
.from("user_profiles")
|
||||
.select("username, full_name, avatar_url")
|
||||
.eq("id", creator.user_id)
|
||||
.single();
|
||||
|
||||
if (profile) {
|
||||
const badges = [];
|
||||
if (creator.verified) badges.push("✅");
|
||||
if (creator.featured) badges.push("⭐");
|
||||
|
||||
leaderboardData.push({
|
||||
name: profile.full_name || profile.username || "Anonymous",
|
||||
value: `${creator.total_projects || 0} projects ${badges.join(" ")}`,
|
||||
username: profile.username,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (mode === 'standalone') {
|
||||
return handleStandaloneLeaderboard(interaction, supabase, category);
|
||||
} else {
|
||||
return handleFederatedLeaderboard(interaction, supabase, category);
|
||||
}
|
||||
|
||||
const medals = ['🥇', '🥈', '🥉'];
|
||||
|
||||
const description = leaderboardData.length > 0
|
||||
? leaderboardData
|
||||
.map((user, index) => {
|
||||
const medal = index < 3 ? medals[index] : `\`${index + 1}.\``;
|
||||
return `${medal} **${user.name}**\n └ ${user.value}`;
|
||||
})
|
||||
.join("\n\n")
|
||||
: "No data available yet. Be the first to contribute!";
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(color)
|
||||
.setTitle(`${emoji} ${title}`)
|
||||
.setDescription(description)
|
||||
.setThumbnail(interaction.guild.iconURL({ size: 128 }))
|
||||
.setFooter({
|
||||
text: `${interaction.guild.name} • Updated in real-time`,
|
||||
iconURL: interaction.guild.iconURL({ size: 32 })
|
||||
})
|
||||
.setTimestamp();
|
||||
|
||||
if (periodInfo) {
|
||||
embed.addFields({
|
||||
name: '📊 Period',
|
||||
value: periodInfo,
|
||||
inline: true
|
||||
});
|
||||
}
|
||||
|
||||
if (leaderboardData.length > 0) {
|
||||
embed.addFields({
|
||||
name: '👥 Showing',
|
||||
value: `Top ${leaderboardData.length} contributors`,
|
||||
inline: true
|
||||
});
|
||||
}
|
||||
|
||||
if (category === "weekly" || category === "monthly") {
|
||||
embed.addFields({
|
||||
name: '💡 Tip',
|
||||
value: 'Leaderboards reset automatically at the start of each period!',
|
||||
inline: false
|
||||
});
|
||||
}
|
||||
|
||||
await interaction.editReply({ embeds: [embed] });
|
||||
} catch (error) {
|
||||
console.error("Leaderboard command error:", error);
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0xff0000)
|
||||
.setTitle("❌ Error")
|
||||
.setTitle("Error")
|
||||
.setDescription("Failed to fetch leaderboard. Please try again.");
|
||||
|
||||
await interaction.editReply({ embeds: [embed] });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
async function handleStandaloneLeaderboard(interaction, supabase, category) {
|
||||
const guildId = interaction.guildId;
|
||||
|
||||
if (category !== 'xp' && category !== 'weekly' && category !== 'monthly') {
|
||||
return interaction.editReply({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(EMBED_COLORS.standalone)
|
||||
.setTitle("🏠 Standalone Mode")
|
||||
.setDescription("This leaderboard category is only available in Federation mode.\n\nIn Standalone mode, only XP, Weekly, and Monthly leaderboards are available.")
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
let leaderboardData = [];
|
||||
let title = "";
|
||||
let color = EMBED_COLORS.standalone;
|
||||
|
||||
if (category === 'xp') {
|
||||
title = "Server XP Leaderboard";
|
||||
const data = await getStandaloneLeaderboard(supabase, guildId, 10);
|
||||
|
||||
for (const entry of data) {
|
||||
const level = calculateLevel(entry.xp || 0, 'normal');
|
||||
let displayName = entry.username || 'Unknown';
|
||||
|
||||
try {
|
||||
const member = await interaction.guild.members.fetch(entry.discord_id).catch(() => null);
|
||||
if (member) displayName = member.displayName;
|
||||
} catch (e) {}
|
||||
|
||||
leaderboardData.push({
|
||||
name: displayName,
|
||||
value: `Level ${level} • ${(entry.xp || 0).toLocaleString()} XP`,
|
||||
xp: entry.xp || 0
|
||||
});
|
||||
}
|
||||
} else if (category === 'weekly' || category === 'monthly') {
|
||||
const periodType = category === 'weekly' ? 'week' : 'month';
|
||||
title = category === 'weekly' ? 'Weekly XP Leaderboard' : 'Monthly XP Leaderboard';
|
||||
|
||||
const now = new Date();
|
||||
let periodStart;
|
||||
|
||||
if (category === 'weekly') {
|
||||
const dayOfWeek = now.getDay();
|
||||
const diffToMonday = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
|
||||
periodStart = new Date(now);
|
||||
periodStart.setDate(now.getDate() - diffToMonday);
|
||||
periodStart.setHours(0, 0, 0, 0);
|
||||
} else {
|
||||
periodStart = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
}
|
||||
|
||||
const periodStartStr = periodStart.toISOString().split('T')[0];
|
||||
|
||||
const { data: periodicData } = await supabase
|
||||
.from("periodic_xp")
|
||||
.select("discord_id, weekly_xp, monthly_xp")
|
||||
.eq("guild_id", guildId)
|
||||
.eq("period_type", periodType)
|
||||
.eq("period_start", periodStartStr);
|
||||
|
||||
const aggregated = {};
|
||||
for (const entry of periodicData || []) {
|
||||
if (!aggregated[entry.discord_id]) {
|
||||
aggregated[entry.discord_id] = 0;
|
||||
}
|
||||
aggregated[entry.discord_id] += category === 'weekly' ? (entry.weekly_xp || 0) : (entry.monthly_xp || 0);
|
||||
}
|
||||
|
||||
const sortedUsers = Object.entries(aggregated)
|
||||
.sort(([, a], [, b]) => b - a)
|
||||
.slice(0, 10);
|
||||
|
||||
for (const [discordId, xp] of sortedUsers) {
|
||||
let displayName = 'Unknown User';
|
||||
try {
|
||||
const member = await interaction.guild.members.fetch(discordId).catch(() => null);
|
||||
if (member) displayName = member.displayName;
|
||||
} catch (e) {}
|
||||
|
||||
leaderboardData.push({
|
||||
name: displayName,
|
||||
value: `${xp.toLocaleString()} XP`,
|
||||
xp: xp
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const medals = ['1.', '2.', '3.'];
|
||||
|
||||
const description = leaderboardData.length > 0
|
||||
? leaderboardData
|
||||
.map((user, index) => {
|
||||
const medal = index < 3 ? medals[index] : `${index + 1}.`;
|
||||
return `**${medal}** ${user.name}\n ${user.value}`;
|
||||
})
|
||||
.join("\n\n")
|
||||
: "No data available yet. Be the first to contribute!";
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(color)
|
||||
.setTitle(`🏠 ${title}`)
|
||||
.setDescription(description)
|
||||
.setThumbnail(interaction.guild.iconURL({ size: 128 }))
|
||||
.setFooter({
|
||||
text: `${interaction.guild.name} • Standalone Mode`,
|
||||
iconURL: interaction.guild.iconURL({ size: 32 })
|
||||
})
|
||||
.setTimestamp();
|
||||
|
||||
if (leaderboardData.length > 0) {
|
||||
embed.addFields({
|
||||
name: 'Showing',
|
||||
value: `Top ${leaderboardData.length} contributors`,
|
||||
inline: true
|
||||
});
|
||||
}
|
||||
|
||||
await interaction.editReply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
async function handleFederatedLeaderboard(interaction, supabase, category) {
|
||||
const guildId = interaction.guildId;
|
||||
|
||||
let leaderboardData = [];
|
||||
let title = "";
|
||||
let emoji = "";
|
||||
let color = 0x7c3aed;
|
||||
let periodInfo = "";
|
||||
|
||||
if (category === "weekly") {
|
||||
title = "Weekly XP Leaderboard";
|
||||
emoji = "";
|
||||
color = 0x22c55e;
|
||||
|
||||
const now = new Date();
|
||||
const dayOfWeek = now.getDay();
|
||||
const diffToMonday = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
|
||||
const weekStart = new Date(now);
|
||||
weekStart.setDate(now.getDate() - diffToMonday);
|
||||
weekStart.setHours(0, 0, 0, 0);
|
||||
const weekStartStr = weekStart.toISOString().split('T')[0];
|
||||
|
||||
const weekEnd = new Date(weekStart);
|
||||
weekEnd.setDate(weekStart.getDate() + 6);
|
||||
periodInfo = `${weekStart.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} - ${weekEnd.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}`;
|
||||
|
||||
const { data: periodicData } = await supabase
|
||||
.from("periodic_xp")
|
||||
.select("discord_id, weekly_xp, weekly_messages")
|
||||
.eq("guild_id", guildId)
|
||||
.eq("period_type", "week")
|
||||
.eq("period_start", weekStartStr);
|
||||
|
||||
const aggregated = {};
|
||||
for (const entry of periodicData || []) {
|
||||
if (!aggregated[entry.discord_id]) {
|
||||
aggregated[entry.discord_id] = { xp: 0, messages: 0 };
|
||||
}
|
||||
aggregated[entry.discord_id].xp += entry.weekly_xp || 0;
|
||||
aggregated[entry.discord_id].messages += entry.weekly_messages || 0;
|
||||
}
|
||||
|
||||
const sortedUsers = Object.entries(aggregated)
|
||||
.sort(([, a], [, b]) => b.xp - a.xp)
|
||||
.slice(0, 10);
|
||||
|
||||
for (const [discordId, data] of sortedUsers) {
|
||||
try {
|
||||
const member = await interaction.guild.members.fetch(discordId).catch(() => null);
|
||||
const displayName = member?.displayName || member?.user?.username || "Unknown User";
|
||||
leaderboardData.push({
|
||||
name: displayName,
|
||||
value: `${data.xp.toLocaleString()} XP • ${data.messages} msgs`,
|
||||
xp: data.xp
|
||||
});
|
||||
} catch (e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} else if (category === "monthly") {
|
||||
title = "Monthly XP Leaderboard";
|
||||
emoji = "";
|
||||
color = 0x3b82f6;
|
||||
|
||||
const now = new Date();
|
||||
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
const monthStartStr = monthStart.toISOString().split('T')[0];
|
||||
|
||||
periodInfo = monthStart.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
|
||||
|
||||
const { data: periodicData } = await supabase
|
||||
.from("periodic_xp")
|
||||
.select("discord_id, monthly_xp, monthly_messages")
|
||||
.eq("guild_id", guildId)
|
||||
.eq("period_type", "month")
|
||||
.eq("period_start", monthStartStr);
|
||||
|
||||
const aggregated = {};
|
||||
for (const entry of periodicData || []) {
|
||||
if (!aggregated[entry.discord_id]) {
|
||||
aggregated[entry.discord_id] = { xp: 0, messages: 0 };
|
||||
}
|
||||
aggregated[entry.discord_id].xp += entry.monthly_xp || 0;
|
||||
aggregated[entry.discord_id].messages += entry.monthly_messages || 0;
|
||||
}
|
||||
|
||||
const sortedUsers = Object.entries(aggregated)
|
||||
.sort(([, a], [, b]) => b.xp - a.xp)
|
||||
.slice(0, 10);
|
||||
|
||||
for (const [discordId, data] of sortedUsers) {
|
||||
try {
|
||||
const member = await interaction.guild.members.fetch(discordId).catch(() => null);
|
||||
const displayName = member?.displayName || member?.user?.username || "Unknown User";
|
||||
leaderboardData.push({
|
||||
name: displayName,
|
||||
value: `${data.xp.toLocaleString()} XP • ${data.messages} msgs`,
|
||||
xp: data.xp
|
||||
});
|
||||
} catch (e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} else if (category === "xp") {
|
||||
title = "XP Leaderboard (All-Time)";
|
||||
emoji = "";
|
||||
color = 0xfbbf24;
|
||||
|
||||
const { data: profiles } = await supabase
|
||||
.from("user_profiles")
|
||||
.select("id, username, full_name, avatar_url, xp")
|
||||
.not("xp", "is", null)
|
||||
.order("xp", { ascending: false })
|
||||
.limit(10);
|
||||
|
||||
for (const profile of profiles || []) {
|
||||
const level = Math.floor(Math.sqrt((profile.xp || 0) / 100));
|
||||
leaderboardData.push({
|
||||
name: profile.full_name || profile.username || "Anonymous",
|
||||
value: `Level ${level} • ${(profile.xp || 0).toLocaleString()} XP`,
|
||||
username: profile.username,
|
||||
xp: profile.xp || 0
|
||||
});
|
||||
}
|
||||
} else if (category === "posts") {
|
||||
title = "Most Active Posters";
|
||||
emoji = "";
|
||||
color = 0xef4444;
|
||||
|
||||
const { data: posts } = await supabase
|
||||
.from("community_posts")
|
||||
.select("user_id")
|
||||
.not("user_id", "is", null);
|
||||
|
||||
const postCounts = {};
|
||||
posts?.forEach((post) => {
|
||||
postCounts[post.user_id] = (postCounts[post.user_id] || 0) + 1;
|
||||
});
|
||||
|
||||
const sortedUsers = Object.entries(postCounts)
|
||||
.sort(([, a], [, b]) => b - a)
|
||||
.slice(0, 10);
|
||||
|
||||
for (const [userId, count] of sortedUsers) {
|
||||
const { data: profile } = await supabase
|
||||
.from("user_profiles")
|
||||
.select("username, full_name, avatar_url")
|
||||
.eq("id", userId)
|
||||
.single();
|
||||
|
||||
if (profile) {
|
||||
leaderboardData.push({
|
||||
name: profile.full_name || profile.username || "Anonymous",
|
||||
value: `${count} posts`,
|
||||
username: profile.username,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (category === "likes") {
|
||||
title = "Most Liked Users";
|
||||
emoji = "";
|
||||
color = 0xec4899;
|
||||
|
||||
const { data: posts } = await supabase
|
||||
.from("community_posts")
|
||||
.select("user_id, likes_count")
|
||||
.not("user_id", "is", null)
|
||||
.order("likes_count", { ascending: false });
|
||||
|
||||
const likeCounts = {};
|
||||
posts?.forEach((post) => {
|
||||
likeCounts[post.user_id] =
|
||||
(likeCounts[post.user_id] || 0) + (post.likes_count || 0);
|
||||
});
|
||||
|
||||
const sortedUsers = Object.entries(likeCounts)
|
||||
.sort(([, a], [, b]) => b - a)
|
||||
.slice(0, 10);
|
||||
|
||||
for (const [userId, count] of sortedUsers) {
|
||||
const { data: profile } = await supabase
|
||||
.from("user_profiles")
|
||||
.select("username, full_name, avatar_url")
|
||||
.eq("id", userId)
|
||||
.single();
|
||||
|
||||
if (profile) {
|
||||
leaderboardData.push({
|
||||
name: profile.full_name || profile.username || "Anonymous",
|
||||
value: `${count.toLocaleString()} likes`,
|
||||
username: profile.username,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (category === "creators") {
|
||||
title = "Top Creators";
|
||||
emoji = "";
|
||||
color = 0x8b5cf6;
|
||||
|
||||
const { data: creators } = await supabase
|
||||
.from("aethex_creators")
|
||||
.select("user_id, total_projects, verified, featured")
|
||||
.order("total_projects", { ascending: false })
|
||||
.limit(10);
|
||||
|
||||
for (const creator of creators || []) {
|
||||
const { data: profile } = await supabase
|
||||
.from("user_profiles")
|
||||
.select("username, full_name, avatar_url")
|
||||
.eq("id", creator.user_id)
|
||||
.single();
|
||||
|
||||
if (profile) {
|
||||
const badges = [];
|
||||
if (creator.verified) badges.push("Verified");
|
||||
if (creator.featured) badges.push("Featured");
|
||||
|
||||
leaderboardData.push({
|
||||
name: profile.full_name || profile.username || "Anonymous",
|
||||
value: `${creator.total_projects || 0} projects ${badges.join(" ")}`,
|
||||
username: profile.username,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const medals = ['1.', '2.', '3.'];
|
||||
|
||||
const description = leaderboardData.length > 0
|
||||
? leaderboardData
|
||||
.map((user, index) => {
|
||||
const medal = index < 3 ? medals[index] : `${index + 1}.`;
|
||||
return `**${medal}** ${user.name}\n ${user.value}`;
|
||||
})
|
||||
.join("\n\n")
|
||||
: "No data available yet. Be the first to contribute!";
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(color)
|
||||
.setTitle(`🌐 ${title}`)
|
||||
.setDescription(description)
|
||||
.setThumbnail(interaction.guild.iconURL({ size: 128 }))
|
||||
.setFooter({
|
||||
text: `${interaction.guild.name} • Federation Mode`,
|
||||
iconURL: interaction.guild.iconURL({ size: 32 })
|
||||
})
|
||||
.setTimestamp();
|
||||
|
||||
if (periodInfo) {
|
||||
embed.addFields({
|
||||
name: 'Period',
|
||||
value: periodInfo,
|
||||
inline: true
|
||||
});
|
||||
}
|
||||
|
||||
if (leaderboardData.length > 0) {
|
||||
embed.addFields({
|
||||
name: 'Showing',
|
||||
value: `Top ${leaderboardData.length} contributors`,
|
||||
inline: true
|
||||
});
|
||||
}
|
||||
|
||||
if (category === "weekly" || category === "monthly") {
|
||||
embed.addFields({
|
||||
name: 'Tip',
|
||||
value: 'Leaderboards reset automatically at the start of each period!',
|
||||
inline: false
|
||||
});
|
||||
}
|
||||
|
||||
await interaction.editReply({ embeds: [embed] });
|
||||
}
|
||||
|
|
|
|||
67
aethex-bot/commands/math.js
Normal file
67
aethex-bot/commands/math.js
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
|
||||
const { getServerMode, getEmbedColor, EMBED_COLORS } = require('../utils/modeHelper');
|
||||
|
||||
function safeEval(expression) {
|
||||
const sanitized = expression.replace(/[^0-9+\-*/().%^sqrt\s]/gi, '');
|
||||
|
||||
if (!sanitized || sanitized.trim() === '') {
|
||||
return { error: 'Invalid expression' };
|
||||
}
|
||||
|
||||
try {
|
||||
let processed = sanitized
|
||||
.replace(/\^/g, '**')
|
||||
.replace(/sqrt\(([^)]+)\)/gi, 'Math.sqrt($1)');
|
||||
|
||||
const result = Function('"use strict"; return (' + processed + ')')();
|
||||
|
||||
if (typeof result !== 'number' || isNaN(result) || !isFinite(result)) {
|
||||
return { error: 'Result is not a valid number' };
|
||||
}
|
||||
|
||||
return { result };
|
||||
} catch (e) {
|
||||
return { error: 'Invalid expression' };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('math')
|
||||
.setDescription('Calculate a math expression')
|
||||
.addStringOption(option =>
|
||||
option.setName('expression')
|
||||
.setDescription('The math expression (e.g., 2+2, sqrt(16), 5^2)')
|
||||
.setRequired(true)
|
||||
.setMaxLength(200)
|
||||
),
|
||||
|
||||
async execute(interaction, supabase, client) {
|
||||
const expression = interaction.options.getString('expression');
|
||||
const mode = await getServerMode(supabase, interaction.guildId);
|
||||
|
||||
const { result, error } = safeEval(expression);
|
||||
|
||||
if (error) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(EMBED_COLORS.error)
|
||||
.setTitle('🧮 Math Error')
|
||||
.setDescription(error)
|
||||
.setTimestamp();
|
||||
return interaction.reply({ embeds: [embed], ephemeral: true });
|
||||
}
|
||||
|
||||
const formattedResult = Number.isInteger(result) ? result.toString() : result.toFixed(6).replace(/\.?0+$/, '');
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(getEmbedColor(mode))
|
||||
.setTitle('🧮 Calculator')
|
||||
.addFields(
|
||||
{ name: '📥 Expression', value: `\`${expression}\`` },
|
||||
{ name: '📤 Result', value: `\`${formattedResult}\`` }
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.reply({ embeds: [embed] });
|
||||
},
|
||||
};
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
const { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js');
|
||||
const { getServerMode, EMBED_COLORS } = require('../utils/modeHelper');
|
||||
const { getStandaloneXp, prestigeStandalone, calculateLevel } = require('../utils/standaloneXp');
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
|
|
@ -28,22 +30,70 @@ module.exports = {
|
|||
}
|
||||
|
||||
const sub = interaction.options.getSubcommand();
|
||||
const mode = await getServerMode(supabase, interaction.guildId);
|
||||
|
||||
if (sub === 'view') {
|
||||
return viewPrestige(interaction, supabase);
|
||||
return viewPrestige(interaction, supabase, mode);
|
||||
} else if (sub === 'up') {
|
||||
return prestigeUp(interaction, supabase, client);
|
||||
return prestigeUp(interaction, supabase, client, mode);
|
||||
} else if (sub === 'rewards') {
|
||||
return viewRewards(interaction);
|
||||
return viewRewards(interaction, mode);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
async function viewPrestige(interaction, supabase) {
|
||||
async function viewPrestige(interaction, supabase, mode) {
|
||||
const target = interaction.options.getUser('user') || interaction.user;
|
||||
await interaction.deferReply();
|
||||
|
||||
try {
|
||||
if (mode === 'standalone') {
|
||||
const data = await getStandaloneXp(supabase, target.id, interaction.guildId);
|
||||
|
||||
if (!data) {
|
||||
return interaction.editReply({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(EMBED_COLORS.standalone)
|
||||
.setDescription(`${target.id === interaction.user.id ? 'You have' : `${target.tag} has`} no XP data yet.`)
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
const prestige = data.prestige_level || 0;
|
||||
const currentXp = data.xp || 0;
|
||||
const totalXpEarned = data.total_xp_earned || currentXp;
|
||||
const level = calculateLevel(currentXp, 'normal');
|
||||
|
||||
const prestigeInfo = getPrestigeInfo(prestige);
|
||||
const canPrestige = level >= 50;
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(prestigeInfo.color)
|
||||
.setTitle(`${prestigeInfo.icon} ${target.tag}'s Prestige`)
|
||||
.setThumbnail(target.displayAvatarURL({ size: 256 }))
|
||||
.addFields(
|
||||
{ name: 'Prestige Level', value: `**${prestigeInfo.name}** (${prestige})`, inline: true },
|
||||
{ name: 'XP Bonus', value: `+${prestige * 5}%`, inline: true },
|
||||
{ name: 'Current Level', value: `${level}`, inline: true },
|
||||
{ name: 'Total XP Earned', value: totalXpEarned.toLocaleString(), inline: true },
|
||||
{ name: 'Current XP', value: currentXp.toLocaleString(), inline: true },
|
||||
{ name: 'Can Prestige?', value: canPrestige ? 'Yes (Level 50+)' : `Need Level 50 (${level}/50)`, inline: true }
|
||||
)
|
||||
.setFooter({ text: `🏠 Standalone Mode • ${interaction.guild.name}` })
|
||||
.setTimestamp();
|
||||
|
||||
if (prestige > 0) {
|
||||
embed.addFields({
|
||||
name: 'Prestige Rewards Unlocked',
|
||||
value: getUnlockedRewards(prestige).join('\n') || 'None yet',
|
||||
inline: false
|
||||
});
|
||||
}
|
||||
|
||||
return interaction.editReply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
const { data: link } = await supabase
|
||||
.from('discord_links')
|
||||
.select('user_id')
|
||||
|
|
@ -72,7 +122,6 @@ async function viewPrestige(interaction, supabase) {
|
|||
const level = Math.floor(Math.sqrt(currentXp / 100));
|
||||
|
||||
const prestigeInfo = getPrestigeInfo(prestige);
|
||||
const nextPrestigeReq = getPrestigeRequirement(prestige + 1);
|
||||
const canPrestige = level >= 50;
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
|
|
@ -85,14 +134,14 @@ async function viewPrestige(interaction, supabase) {
|
|||
{ name: 'Current Level', value: `${level}`, inline: true },
|
||||
{ name: 'Total XP Earned', value: totalXpEarned.toLocaleString(), inline: true },
|
||||
{ name: 'Current XP', value: currentXp.toLocaleString(), inline: true },
|
||||
{ name: 'Can Prestige?', value: canPrestige ? '✅ Yes (Level 50+)' : `❌ Need Level 50 (${level}/50)`, inline: true }
|
||||
{ name: 'Can Prestige?', value: canPrestige ? 'Yes (Level 50+)' : `Need Level 50 (${level}/50)`, inline: true }
|
||||
)
|
||||
.setFooter({ text: `Next prestige requirement: Level 50` })
|
||||
.setFooter({ text: `🌐 Federation • Next prestige requirement: Level 50` })
|
||||
.setTimestamp();
|
||||
|
||||
if (prestige > 0) {
|
||||
embed.addFields({
|
||||
name: '🏆 Prestige Rewards Unlocked',
|
||||
name: 'Prestige Rewards Unlocked',
|
||||
value: getUnlockedRewards(prestige).join('\n') || 'None yet',
|
||||
inline: false
|
||||
});
|
||||
|
|
@ -105,10 +154,127 @@ async function viewPrestige(interaction, supabase) {
|
|||
}
|
||||
}
|
||||
|
||||
async function prestigeUp(interaction, supabase, client) {
|
||||
async function prestigeUp(interaction, supabase, client, mode) {
|
||||
await interaction.deferReply();
|
||||
|
||||
try {
|
||||
if (mode === 'standalone') {
|
||||
const data = await getStandaloneXp(supabase, interaction.user.id, interaction.guildId);
|
||||
|
||||
if (!data) {
|
||||
return interaction.editReply({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(EMBED_COLORS.error)
|
||||
.setDescription('You have no XP data yet. Start chatting to earn XP!')
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
const currentXp = data.xp || 0;
|
||||
const level = calculateLevel(currentXp, 'normal');
|
||||
const prestige = data.prestige_level || 0;
|
||||
|
||||
if (level < 50) {
|
||||
return interaction.editReply({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(EMBED_COLORS.error)
|
||||
.setTitle('Cannot Prestige')
|
||||
.setDescription(`You need to reach **Level 50** to prestige.\nCurrent level: **${level}**/50`)
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
if (prestige >= 10) {
|
||||
return interaction.editReply({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(0xffd700)
|
||||
.setTitle('Maximum Prestige!')
|
||||
.setDescription('You have reached the maximum prestige level!')
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
const newPrestige = prestige + 1;
|
||||
const newPrestigeInfo = getPrestigeInfo(newPrestige);
|
||||
|
||||
const confirmEmbed = new EmbedBuilder()
|
||||
.setColor(EMBED_COLORS.warning)
|
||||
.setTitle('Confirm Prestige')
|
||||
.setDescription(`Are you sure you want to prestige?\n\n**What will happen:**\n• Your XP will reset to **0**\n• Your level will reset to **0**\n• You gain **Prestige ${newPrestige}** (${newPrestigeInfo.name})\n• You get a permanent **+${newPrestige * 5}%** XP bonus\n\n**Current Stats:**\n• Level: ${level}\n• XP: ${currentXp.toLocaleString()}`)
|
||||
.setFooter({ text: 'This action cannot be undone!' });
|
||||
|
||||
const row = new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId('prestige_confirm_standalone')
|
||||
.setLabel('Prestige Up!')
|
||||
.setStyle(ButtonStyle.Success),
|
||||
new ButtonBuilder()
|
||||
.setCustomId('prestige_cancel_standalone')
|
||||
.setLabel('Cancel')
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
);
|
||||
|
||||
const response = await interaction.editReply({ embeds: [confirmEmbed], components: [row] });
|
||||
|
||||
try {
|
||||
const confirmation = await response.awaitMessageComponent({
|
||||
filter: i => i.user.id === interaction.user.id,
|
||||
time: 60000
|
||||
});
|
||||
|
||||
if (confirmation.customId === 'prestige_confirm_standalone') {
|
||||
const result = await prestigeStandalone(supabase, interaction.user.id, interaction.guildId);
|
||||
|
||||
if (!result.success) {
|
||||
return confirmation.update({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(EMBED_COLORS.error)
|
||||
.setDescription(result.message)
|
||||
],
|
||||
components: []
|
||||
});
|
||||
}
|
||||
|
||||
const successEmbed = new EmbedBuilder()
|
||||
.setColor(newPrestigeInfo.color)
|
||||
.setTitle(`${newPrestigeInfo.icon} Prestige ${result.newPrestige} Achieved!`)
|
||||
.setDescription(`Congratulations! You are now **${newPrestigeInfo.name}**!`)
|
||||
.addFields(
|
||||
{ name: 'XP Bonus', value: `+${result.bonus}%`, inline: true }
|
||||
)
|
||||
.setThumbnail(interaction.user.displayAvatarURL({ size: 256 }))
|
||||
.setFooter({ text: '🏠 Standalone Mode' })
|
||||
.setTimestamp();
|
||||
|
||||
await confirmation.update({ embeds: [successEmbed], components: [] });
|
||||
} else {
|
||||
await confirmation.update({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(EMBED_COLORS.standalone)
|
||||
.setDescription('Prestige cancelled.')
|
||||
],
|
||||
components: []
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
await interaction.editReply({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(EMBED_COLORS.standalone)
|
||||
.setDescription('Prestige request timed out.')
|
||||
],
|
||||
components: []
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const { data: link } = await supabase
|
||||
.from('discord_links')
|
||||
.select('user_id')
|
||||
|
|
@ -141,7 +307,7 @@ async function prestigeUp(interaction, supabase, client) {
|
|||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(0xff6b6b)
|
||||
.setTitle('❌ Cannot Prestige')
|
||||
.setTitle('Cannot Prestige')
|
||||
.setDescription(`You need to reach **Level 50** to prestige.\nCurrent level: **${level}**/50`)
|
||||
]
|
||||
});
|
||||
|
|
@ -152,7 +318,7 @@ async function prestigeUp(interaction, supabase, client) {
|
|||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(0xffd700)
|
||||
.setTitle('👑 Maximum Prestige!')
|
||||
.setTitle('Maximum Prestige!')
|
||||
.setDescription('You have reached the maximum prestige level! You are a true legend.')
|
||||
]
|
||||
});
|
||||
|
|
@ -164,7 +330,7 @@ async function prestigeUp(interaction, supabase, client) {
|
|||
|
||||
const confirmEmbed = new EmbedBuilder()
|
||||
.setColor(0xf59e0b)
|
||||
.setTitle('⚠️ Confirm Prestige')
|
||||
.setTitle('Confirm Prestige')
|
||||
.setDescription(`Are you sure you want to prestige?\n\n**What will happen:**\n• Your XP will reset to **0**\n• Your level will reset to **0**\n• You gain **Prestige ${newPrestige}** (${newPrestigeInfo.name})\n• You get a permanent **+${xpBonus}%** XP bonus\n• You unlock new **prestige rewards**\n\n**Current Stats:**\n• Level: ${level}\n• XP: ${currentXp.toLocaleString()}`)
|
||||
.setFooter({ text: 'This action cannot be undone!' });
|
||||
|
||||
|
|
@ -172,7 +338,7 @@ async function prestigeUp(interaction, supabase, client) {
|
|||
.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId('prestige_confirm')
|
||||
.setLabel('✨ Prestige Up!')
|
||||
.setLabel('Prestige Up!')
|
||||
.setStyle(ButtonStyle.Success),
|
||||
new ButtonBuilder()
|
||||
.setCustomId('prestige_cancel')
|
||||
|
|
@ -218,6 +384,7 @@ async function prestigeUp(interaction, supabase, client) {
|
|||
{ name: 'Total XP Earned (All Time)', value: totalEarned.toLocaleString(), inline: true }
|
||||
)
|
||||
.setThumbnail(interaction.user.displayAvatarURL({ size: 256 }))
|
||||
.setFooter({ text: '🌐 Federation' })
|
||||
.setTimestamp();
|
||||
|
||||
await confirmation.update({ embeds: [successEmbed], components: [] });
|
||||
|
|
@ -247,24 +414,24 @@ async function prestigeUp(interaction, supabase, client) {
|
|||
}
|
||||
}
|
||||
|
||||
async function viewRewards(interaction) {
|
||||
async function viewRewards(interaction, mode) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0x7c3aed)
|
||||
.setTitle('✨ Prestige Rewards')
|
||||
.setColor(mode === 'standalone' ? EMBED_COLORS.standalone : 0x7c3aed)
|
||||
.setTitle('Prestige Rewards')
|
||||
.setDescription('Each prestige level grants permanent rewards!\n\n**Requirements:** Level 50 to prestige')
|
||||
.addFields(
|
||||
{ name: '⭐ Prestige 1 - Bronze', value: '+5% XP bonus\n🏷️ Bronze Prestige badge', inline: true },
|
||||
{ name: '⭐ Prestige 2 - Silver', value: '+10% XP bonus\n🏷️ Silver Prestige badge', inline: true },
|
||||
{ name: '⭐ Prestige 3 - Gold', value: '+15% XP bonus\n🏷️ Gold Prestige badge', inline: true },
|
||||
{ name: '💎 Prestige 4 - Platinum', value: '+20% XP bonus\n🏷️ Platinum badge\n🎁 Bonus daily XP', inline: true },
|
||||
{ name: '💎 Prestige 5 - Diamond', value: '+25% XP bonus\n🏷️ Diamond badge\n⏱️ Reduced cooldowns', inline: true },
|
||||
{ name: '🔥 Prestige 6 - Master', value: '+30% XP bonus\n🏷️ Master badge\n🎯 XP milestone rewards', inline: true },
|
||||
{ name: '🔥 Prestige 7 - Grandmaster', value: '+35% XP bonus\n🏷️ Grandmaster badge\n💫 Special profile effects', inline: true },
|
||||
{ name: '👑 Prestige 8 - Champion', value: '+40% XP bonus\n🏷️ Champion badge\n🏆 Leaderboard priority', inline: true },
|
||||
{ name: '👑 Prestige 9 - Legend', value: '+45% XP bonus\n🏷️ Legend badge\n✨ Legendary profile aura', inline: true },
|
||||
{ name: '🌟 Prestige 10 - Mythic', value: '+50% XP bonus\n🏷️ Mythic badge\n🌈 All prestige perks!', inline: true }
|
||||
{ name: 'Prestige 1 - Bronze', value: '+5% XP bonus\nBronze Prestige badge', inline: true },
|
||||
{ name: 'Prestige 2 - Silver', value: '+10% XP bonus\nSilver Prestige badge', inline: true },
|
||||
{ name: 'Prestige 3 - Gold', value: '+15% XP bonus\nGold Prestige badge', inline: true },
|
||||
{ name: 'Prestige 4 - Platinum', value: '+20% XP bonus\nPlatinum badge\nBonus daily XP', inline: true },
|
||||
{ name: 'Prestige 5 - Diamond', value: '+25% XP bonus\nDiamond badge\nReduced cooldowns', inline: true },
|
||||
{ name: 'Prestige 6 - Master', value: '+30% XP bonus\nMaster badge\nXP milestone rewards', inline: true },
|
||||
{ name: 'Prestige 7 - Grandmaster', value: '+35% XP bonus\nGrandmaster badge\nSpecial profile effects', inline: true },
|
||||
{ name: 'Prestige 8 - Champion', value: '+40% XP bonus\nChampion badge\nLeaderboard priority', inline: true },
|
||||
{ name: 'Prestige 9 - Legend', value: '+45% XP bonus\nLegend badge\nLegendary profile aura', inline: true },
|
||||
{ name: 'Prestige 10 - Mythic', value: '+50% XP bonus\nMythic badge\nAll prestige perks!', inline: true }
|
||||
)
|
||||
.setFooter({ text: 'Use /prestige up when you reach Level 50!' })
|
||||
.setFooter({ text: `${mode === 'standalone' ? '🏠 Standalone' : '🌐 Federation'} • Use /prestige up when you reach Level 50!` })
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.reply({ embeds: [embed] });
|
||||
|
|
@ -272,52 +439,48 @@ async function viewRewards(interaction) {
|
|||
|
||||
function getPrestigeInfo(level) {
|
||||
const prestiges = [
|
||||
{ name: 'Unprestiged', icon: '⚪', color: 0x6b7280 },
|
||||
{ name: 'Bronze', icon: '🥉', color: 0xcd7f32 },
|
||||
{ name: 'Silver', icon: '🥈', color: 0xc0c0c0 },
|
||||
{ name: 'Gold', icon: '🥇', color: 0xffd700 },
|
||||
{ name: 'Platinum', icon: '💎', color: 0xe5e4e2 },
|
||||
{ name: 'Diamond', icon: '💠', color: 0xb9f2ff },
|
||||
{ name: 'Master', icon: '🔥', color: 0xff4500 },
|
||||
{ name: 'Grandmaster', icon: '⚔️', color: 0x9400d3 },
|
||||
{ name: 'Champion', icon: '👑', color: 0xffd700 },
|
||||
{ name: 'Legend', icon: '🌟', color: 0xff69b4 },
|
||||
{ name: 'Mythic', icon: '🌈', color: 0x7c3aed }
|
||||
{ name: 'Unprestiged', icon: '', color: 0x6b7280 },
|
||||
{ name: 'Bronze', icon: '', color: 0xcd7f32 },
|
||||
{ name: 'Silver', icon: '', color: 0xc0c0c0 },
|
||||
{ name: 'Gold', icon: '', color: 0xffd700 },
|
||||
{ name: 'Platinum', icon: '', color: 0xe5e4e2 },
|
||||
{ name: 'Diamond', icon: '', color: 0xb9f2ff },
|
||||
{ name: 'Master', icon: '', color: 0xff4500 },
|
||||
{ name: 'Grandmaster', icon: '', color: 0x9400d3 },
|
||||
{ name: 'Champion', icon: '', color: 0xffd700 },
|
||||
{ name: 'Legend', icon: '', color: 0xff69b4 },
|
||||
{ name: 'Mythic', icon: '', color: 0x7c3aed }
|
||||
];
|
||||
return prestiges[Math.min(level, 10)] || prestiges[0];
|
||||
}
|
||||
|
||||
function getPrestigeRequirement(level) {
|
||||
return 50;
|
||||
}
|
||||
|
||||
function getUnlockedRewards(prestige) {
|
||||
const rewards = [];
|
||||
if (prestige >= 1) rewards.push('🥉 Bronze Prestige badge');
|
||||
if (prestige >= 2) rewards.push('🥈 Silver Prestige badge');
|
||||
if (prestige >= 3) rewards.push('🥇 Gold Prestige badge');
|
||||
if (prestige >= 4) rewards.push('💎 Platinum badge + Bonus daily XP');
|
||||
if (prestige >= 5) rewards.push('💠 Diamond badge + Reduced cooldowns');
|
||||
if (prestige >= 6) rewards.push('🔥 Master badge + XP milestones');
|
||||
if (prestige >= 7) rewards.push('⚔️ Grandmaster badge + Profile effects');
|
||||
if (prestige >= 8) rewards.push('👑 Champion badge + Leaderboard priority');
|
||||
if (prestige >= 9) rewards.push('🌟 Legend badge + Legendary aura');
|
||||
if (prestige >= 10) rewards.push('🌈 Mythic badge + All perks unlocked');
|
||||
if (prestige >= 1) rewards.push('Bronze Prestige badge');
|
||||
if (prestige >= 2) rewards.push('Silver Prestige badge');
|
||||
if (prestige >= 3) rewards.push('Gold Prestige badge');
|
||||
if (prestige >= 4) rewards.push('Platinum badge + Bonus daily XP');
|
||||
if (prestige >= 5) rewards.push('Diamond badge + Reduced cooldowns');
|
||||
if (prestige >= 6) rewards.push('Master badge + XP milestones');
|
||||
if (prestige >= 7) rewards.push('Grandmaster badge + Profile effects');
|
||||
if (prestige >= 8) rewards.push('Champion badge + Leaderboard priority');
|
||||
if (prestige >= 9) rewards.push('Legend badge + Legendary aura');
|
||||
if (prestige >= 10) rewards.push('Mythic badge + All perks unlocked');
|
||||
return rewards;
|
||||
}
|
||||
|
||||
function getNewRewards(prestige) {
|
||||
const rewardMap = {
|
||||
1: ['🥉 Bronze Prestige badge', '+5% XP bonus on all XP gains'],
|
||||
2: ['🥈 Silver Prestige badge', '+10% XP bonus on all XP gains'],
|
||||
3: ['🥇 Gold Prestige badge', '+15% XP bonus on all XP gains'],
|
||||
4: ['💎 Platinum badge', '+20% XP bonus', '🎁 +25 bonus daily XP'],
|
||||
5: ['💠 Diamond badge', '+25% XP bonus', '⏱️ 10% reduced XP cooldowns'],
|
||||
6: ['🔥 Master badge', '+30% XP bonus', '🎯 XP milestone rewards'],
|
||||
7: ['⚔️ Grandmaster badge', '+35% XP bonus', '💫 Special profile effects'],
|
||||
8: ['👑 Champion badge', '+40% XP bonus', '🏆 Leaderboard priority display'],
|
||||
9: ['🌟 Legend badge', '+45% XP bonus', '✨ Legendary profile aura'],
|
||||
10: ['🌈 Mythic badge', '+50% XP bonus', '🌟 All prestige perks unlocked!']
|
||||
1: ['Bronze Prestige badge', '+5% XP bonus on all XP gains'],
|
||||
2: ['Silver Prestige badge', '+10% XP bonus on all XP gains'],
|
||||
3: ['Gold Prestige badge', '+15% XP bonus on all XP gains'],
|
||||
4: ['Platinum badge', '+20% XP bonus', '+25 bonus daily XP'],
|
||||
5: ['Diamond badge', '+25% XP bonus', '10% reduced XP cooldowns'],
|
||||
6: ['Master badge', '+30% XP bonus', 'XP milestone rewards'],
|
||||
7: ['Grandmaster badge', '+35% XP bonus', 'Special profile effects'],
|
||||
8: ['Champion badge', '+40% XP bonus', 'Leaderboard priority display'],
|
||||
9: ['Legend badge', '+45% XP bonus', 'Legendary profile aura'],
|
||||
10: ['Mythic badge', '+50% XP bonus', 'All prestige perks unlocked!']
|
||||
};
|
||||
return rewardMap[prestige] || [];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
const { SlashCommandBuilder, EmbedBuilder } = require("discord.js");
|
||||
const { getServerMode, EMBED_COLORS } = require("../utils/modeHelper");
|
||||
const { getStandaloneXp, calculateLevel } = require("../utils/standaloneXp");
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName("profile")
|
||||
.setDescription("View your AeThex profile in Discord")
|
||||
.setDescription("View your profile")
|
||||
.addUserOption(option =>
|
||||
option.setName('user')
|
||||
.setDescription('User to view profile of')
|
||||
|
|
@ -19,140 +21,18 @@ module.exports = {
|
|||
const targetUser = interaction.options.getUser('user') || interaction.user;
|
||||
|
||||
try {
|
||||
const { data: link } = await supabase
|
||||
.from("discord_links")
|
||||
.select("user_id, primary_arm")
|
||||
.eq("discord_id", targetUser.id)
|
||||
.single();
|
||||
const mode = await getServerMode(supabase, interaction.guildId);
|
||||
|
||||
if (!link) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0xff6b6b)
|
||||
.setTitle("❌ Not Linked")
|
||||
.setThumbnail(targetUser.displayAvatarURL({ size: 256 }))
|
||||
.setDescription(
|
||||
targetUser.id === interaction.user.id
|
||||
? "You must link your Discord account to AeThex first.\nUse `/verify` to get started."
|
||||
: `${targetUser.tag} hasn't linked their Discord account to AeThex yet.`
|
||||
);
|
||||
|
||||
return await interaction.editReply({ embeds: [embed] });
|
||||
if (mode === 'standalone') {
|
||||
return handleStandaloneProfile(interaction, supabase, targetUser);
|
||||
} else {
|
||||
return handleFederatedProfile(interaction, supabase, targetUser);
|
||||
}
|
||||
|
||||
const { data: profile } = await supabase
|
||||
.from("user_profiles")
|
||||
.select("*")
|
||||
.eq("id", link.user_id)
|
||||
.single();
|
||||
|
||||
if (!profile) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0xff6b6b)
|
||||
.setTitle("❌ Profile Not Found")
|
||||
.setDescription("The AeThex profile could not be found.");
|
||||
|
||||
return await interaction.editReply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
const armEmojis = {
|
||||
labs: "🧪",
|
||||
gameforge: "🎮",
|
||||
corp: "💼",
|
||||
foundation: "🤝",
|
||||
devlink: "💻",
|
||||
};
|
||||
|
||||
const armColors = {
|
||||
labs: 0x22c55e,
|
||||
gameforge: 0xf97316,
|
||||
corp: 0x3b82f6,
|
||||
foundation: 0xec4899,
|
||||
devlink: 0x8b5cf6,
|
||||
};
|
||||
|
||||
const xp = profile.xp || 0;
|
||||
const prestige = profile.prestige_level || 0;
|
||||
const level = Math.floor(Math.sqrt(xp / 100));
|
||||
const currentLevelXp = level * level * 100;
|
||||
const nextLevelXp = (level + 1) * (level + 1) * 100;
|
||||
const progressXp = xp - currentLevelXp;
|
||||
const neededXp = nextLevelXp - currentLevelXp;
|
||||
const progressPercent = Math.min(100, Math.floor((progressXp / neededXp) * 100));
|
||||
|
||||
const progressBar = createProgressBar(progressPercent);
|
||||
const prestigeInfo = getPrestigeInfo(prestige);
|
||||
|
||||
const badges = profile.badges || [];
|
||||
const badgeDisplay = badges.length > 0
|
||||
? badges.map(b => getBadgeEmoji(b)).join(' ')
|
||||
: 'No badges yet';
|
||||
|
||||
// Validate avatar URL - must be http/https, not base64
|
||||
let avatarUrl = targetUser.displayAvatarURL({ size: 256 });
|
||||
if (profile.avatar_url && profile.avatar_url.startsWith('http')) {
|
||||
avatarUrl = profile.avatar_url;
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(armColors[link.primary_arm] || 0x7c3aed)
|
||||
.setAuthor({
|
||||
name: `${profile.full_name || profile.username || 'AeThex User'}`,
|
||||
iconURL: targetUser.displayAvatarURL({ size: 64 })
|
||||
})
|
||||
.setThumbnail(avatarUrl)
|
||||
.setDescription(profile.bio || '*No bio set*')
|
||||
.addFields(
|
||||
{
|
||||
name: "👤 Username",
|
||||
value: `\`${profile.username || 'N/A'}\``,
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `${armEmojis[link.primary_arm] || "⚔️"} Realm`,
|
||||
value: capitalizeFirst(link.primary_arm) || "Not set",
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: "📊 Role",
|
||||
value: formatRole(profile.user_type),
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `${prestigeInfo.icon} Prestige`,
|
||||
value: prestige > 0 ? `**${prestigeInfo.name}** (P${prestige}) +${prestige * 5}% XP` : 'Not prestiged',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: `📈 Level ${level}`,
|
||||
value: `${progressBar}\n\`${xp.toLocaleString()}\` / \`${nextLevelXp.toLocaleString()}\` XP`,
|
||||
inline: false,
|
||||
},
|
||||
{
|
||||
name: "🏆 Badges",
|
||||
value: badgeDisplay,
|
||||
inline: false,
|
||||
}
|
||||
)
|
||||
.addFields({
|
||||
name: "🔗 Links",
|
||||
value: `[View Full Profile](https://aethex.dev/creators/${profile.username}) • [AeThex Platform](https://aethex.dev)`,
|
||||
})
|
||||
.setFooter({
|
||||
text: `AeThex | ${targetUser.tag}`,
|
||||
iconURL: 'https://aethex.dev/favicon.ico'
|
||||
})
|
||||
.setTimestamp();
|
||||
|
||||
if (profile.banner_url) {
|
||||
embed.setImage(profile.banner_url);
|
||||
}
|
||||
|
||||
await interaction.editReply({ embeds: [embed] });
|
||||
} catch (error) {
|
||||
console.error("Profile command error:", error);
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0xff0000)
|
||||
.setTitle("❌ Error")
|
||||
.setTitle("Error")
|
||||
.setDescription("Failed to fetch profile. Please try again.");
|
||||
|
||||
await interaction.editReply({ embeds: [embed] });
|
||||
|
|
@ -160,6 +40,175 @@ module.exports = {
|
|||
},
|
||||
};
|
||||
|
||||
async function handleStandaloneProfile(interaction, supabase, targetUser) {
|
||||
const data = await getStandaloneXp(supabase, targetUser.id, interaction.guildId);
|
||||
|
||||
if (!data) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(EMBED_COLORS.standalone)
|
||||
.setTitle("No Profile Found")
|
||||
.setThumbnail(targetUser.displayAvatarURL({ size: 256 }))
|
||||
.setDescription(
|
||||
targetUser.id === interaction.user.id
|
||||
? "You don't have any XP yet. Start chatting to build your profile!"
|
||||
: `${targetUser.tag} hasn't earned any XP yet in this server.`
|
||||
);
|
||||
|
||||
return await interaction.editReply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
const xp = data.xp || 0;
|
||||
const prestige = data.prestige_level || 0;
|
||||
const totalXpEarned = data.total_xp_earned || xp;
|
||||
const level = calculateLevel(xp, 'normal');
|
||||
const dailyStreak = data.daily_streak || 0;
|
||||
|
||||
const currentLevelXp = level * level * 100;
|
||||
const nextLevelXp = (level + 1) * (level + 1) * 100;
|
||||
const progressXp = xp - currentLevelXp;
|
||||
const neededXp = nextLevelXp - currentLevelXp;
|
||||
const progressPercent = Math.min(100, Math.floor((progressXp / neededXp) * 100));
|
||||
|
||||
const progressBar = createProgressBar(progressPercent);
|
||||
const prestigeInfo = getPrestigeInfo(prestige);
|
||||
|
||||
const { count: rankPosition } = await supabase
|
||||
.from('guild_user_xp')
|
||||
.select('*', { count: 'exact', head: true })
|
||||
.eq('guild_id', interaction.guildId)
|
||||
.gt('xp', xp);
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(EMBED_COLORS.standalone)
|
||||
.setAuthor({
|
||||
name: targetUser.tag,
|
||||
iconURL: targetUser.displayAvatarURL({ size: 64 })
|
||||
})
|
||||
.setThumbnail(targetUser.displayAvatarURL({ size: 256 }))
|
||||
.addFields(
|
||||
{ name: "Username", value: `\`${data.username || targetUser.username}\``, inline: true },
|
||||
{ name: "Server Rank", value: `#${(rankPosition || 0) + 1}`, inline: true },
|
||||
{ name: "Daily Streak", value: `${dailyStreak} days`, inline: true },
|
||||
{ name: `${prestigeInfo.icon || ''} Prestige`, value: prestige > 0 ? `**${prestigeInfo.name}** (P${prestige}) +${prestige * 5}% XP` : 'Not prestiged', inline: true },
|
||||
{ name: `Level ${level}`, value: `${progressBar}\n\`${xp.toLocaleString()}\` / \`${nextLevelXp.toLocaleString()}\` XP`, inline: false },
|
||||
{ name: "Total XP Earned", value: totalXpEarned.toLocaleString(), inline: true }
|
||||
)
|
||||
.setFooter({
|
||||
text: `🏠 Standalone Mode • ${interaction.guild.name}`,
|
||||
iconURL: interaction.guild.iconURL({ size: 32 })
|
||||
})
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.editReply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
async function handleFederatedProfile(interaction, supabase, targetUser) {
|
||||
const { data: link } = await supabase
|
||||
.from("discord_links")
|
||||
.select("user_id, primary_arm")
|
||||
.eq("discord_id", targetUser.id)
|
||||
.single();
|
||||
|
||||
if (!link) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0xff6b6b)
|
||||
.setTitle("Not Linked")
|
||||
.setThumbnail(targetUser.displayAvatarURL({ size: 256 }))
|
||||
.setDescription(
|
||||
targetUser.id === interaction.user.id
|
||||
? "You must link your Discord account to AeThex first.\nUse `/verify` to get started."
|
||||
: `${targetUser.tag} hasn't linked their Discord account to AeThex yet.`
|
||||
);
|
||||
|
||||
return await interaction.editReply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
const { data: profile } = await supabase
|
||||
.from("user_profiles")
|
||||
.select("*")
|
||||
.eq("id", link.user_id)
|
||||
.single();
|
||||
|
||||
if (!profile) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0xff6b6b)
|
||||
.setTitle("Profile Not Found")
|
||||
.setDescription("The AeThex profile could not be found.");
|
||||
|
||||
return await interaction.editReply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
const armEmojis = {
|
||||
labs: "",
|
||||
gameforge: "",
|
||||
corp: "",
|
||||
foundation: "",
|
||||
devlink: "",
|
||||
};
|
||||
|
||||
const armColors = {
|
||||
labs: 0x22c55e,
|
||||
gameforge: 0xf97316,
|
||||
corp: 0x3b82f6,
|
||||
foundation: 0xec4899,
|
||||
devlink: 0x8b5cf6,
|
||||
};
|
||||
|
||||
const xp = profile.xp || 0;
|
||||
const prestige = profile.prestige_level || 0;
|
||||
const level = Math.floor(Math.sqrt(xp / 100));
|
||||
const currentLevelXp = level * level * 100;
|
||||
const nextLevelXp = (level + 1) * (level + 1) * 100;
|
||||
const progressXp = xp - currentLevelXp;
|
||||
const neededXp = nextLevelXp - currentLevelXp;
|
||||
const progressPercent = Math.min(100, Math.floor((progressXp / neededXp) * 100));
|
||||
|
||||
const progressBar = createProgressBar(progressPercent);
|
||||
const prestigeInfo = getPrestigeInfo(prestige);
|
||||
|
||||
const badges = profile.badges || [];
|
||||
const badgeDisplay = badges.length > 0
|
||||
? badges.map(b => getBadgeEmoji(b)).join(' ')
|
||||
: 'No badges yet';
|
||||
|
||||
let avatarUrl = targetUser.displayAvatarURL({ size: 256 });
|
||||
if (profile.avatar_url && profile.avatar_url.startsWith('http')) {
|
||||
avatarUrl = profile.avatar_url;
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(armColors[link.primary_arm] || 0x7c3aed)
|
||||
.setAuthor({
|
||||
name: `${profile.full_name || profile.username || 'AeThex User'}`,
|
||||
iconURL: targetUser.displayAvatarURL({ size: 64 })
|
||||
})
|
||||
.setThumbnail(avatarUrl)
|
||||
.setDescription(profile.bio || '*No bio set*')
|
||||
.addFields(
|
||||
{ name: "Username", value: `\`${profile.username || 'N/A'}\``, inline: true },
|
||||
{ name: `${armEmojis[link.primary_arm] || ""} Realm`, value: capitalizeFirst(link.primary_arm) || "Not set", inline: true },
|
||||
{ name: "Role", value: formatRole(profile.user_type), inline: true },
|
||||
{ name: `${prestigeInfo.icon || ''} Prestige`, value: prestige > 0 ? `**${prestigeInfo.name}** (P${prestige}) +${prestige * 5}% XP` : 'Not prestiged', inline: true },
|
||||
{ name: `Level ${level}`, value: `${progressBar}\n\`${xp.toLocaleString()}\` / \`${nextLevelXp.toLocaleString()}\` XP`, inline: false },
|
||||
{ name: "Badges", value: badgeDisplay, inline: false }
|
||||
)
|
||||
.addFields({
|
||||
name: "Links",
|
||||
value: `[View Full Profile](https://aethex.dev/creators/${profile.username}) • [AeThex Platform](https://aethex.dev)`,
|
||||
})
|
||||
.setFooter({
|
||||
text: `🌐 Federation • ${targetUser.tag}`,
|
||||
iconURL: 'https://aethex.dev/favicon.ico'
|
||||
})
|
||||
.setTimestamp();
|
||||
|
||||
if (profile.banner_url) {
|
||||
embed.setImage(profile.banner_url);
|
||||
}
|
||||
|
||||
await interaction.editReply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
function createProgressBar(percent) {
|
||||
const filled = Math.floor(percent / 10);
|
||||
const empty = 10 - filled;
|
||||
|
|
@ -178,36 +227,36 @@ function formatRole(role) {
|
|||
|
||||
function getBadgeEmoji(badge) {
|
||||
const badgeMap = {
|
||||
'verified': '✅',
|
||||
'founder': '👑',
|
||||
'early_adopter': '🌟',
|
||||
'contributor': '💎',
|
||||
'creator': '🎨',
|
||||
'developer': '💻',
|
||||
'moderator': '🛡️',
|
||||
'partner': '🤝',
|
||||
'premium': '💫',
|
||||
'top_poster': '📝',
|
||||
'helpful': '❤️',
|
||||
'bug_hunter': '🐛',
|
||||
'event_winner': '🏆',
|
||||
'verified': 'Verified',
|
||||
'founder': 'Founder',
|
||||
'early_adopter': 'Early Adopter',
|
||||
'contributor': 'Contributor',
|
||||
'creator': 'Creator',
|
||||
'developer': 'Developer',
|
||||
'moderator': 'Moderator',
|
||||
'partner': 'Partner',
|
||||
'premium': 'Premium',
|
||||
'top_poster': 'Top Poster',
|
||||
'helpful': 'Helpful',
|
||||
'bug_hunter': 'Bug Hunter',
|
||||
'event_winner': 'Event Winner',
|
||||
};
|
||||
return badgeMap[badge] || `[${badge}]`;
|
||||
}
|
||||
|
||||
function getPrestigeInfo(level) {
|
||||
const prestiges = [
|
||||
{ name: 'Unprestiged', icon: '⚪', color: 0x6b7280 },
|
||||
{ name: 'Bronze', icon: '🥉', color: 0xcd7f32 },
|
||||
{ name: 'Silver', icon: '🥈', color: 0xc0c0c0 },
|
||||
{ name: 'Gold', icon: '🥇', color: 0xffd700 },
|
||||
{ name: 'Platinum', icon: '💎', color: 0xe5e4e2 },
|
||||
{ name: 'Diamond', icon: '💠', color: 0xb9f2ff },
|
||||
{ name: 'Master', icon: '🔥', color: 0xff4500 },
|
||||
{ name: 'Grandmaster', icon: '⚔️', color: 0x9400d3 },
|
||||
{ name: 'Champion', icon: '👑', color: 0xffd700 },
|
||||
{ name: 'Legend', icon: '🌟', color: 0xff69b4 },
|
||||
{ name: 'Mythic', icon: '🌈', color: 0x7c3aed }
|
||||
{ name: 'Unprestiged', icon: '', color: 0x6b7280 },
|
||||
{ name: 'Bronze', icon: '', color: 0xcd7f32 },
|
||||
{ name: 'Silver', icon: '', color: 0xc0c0c0 },
|
||||
{ name: 'Gold', icon: '', color: 0xffd700 },
|
||||
{ name: 'Platinum', icon: '', color: 0xe5e4e2 },
|
||||
{ name: 'Diamond', icon: '', color: 0xb9f2ff },
|
||||
{ name: 'Master', icon: '', color: 0xff4500 },
|
||||
{ name: 'Grandmaster', icon: '', color: 0x9400d3 },
|
||||
{ name: 'Champion', icon: '', color: 0xffd700 },
|
||||
{ name: 'Legend', icon: '', color: 0xff69b4 },
|
||||
{ name: 'Mythic', icon: '', color: 0x7c3aed }
|
||||
];
|
||||
return prestiges[Math.min(level, 10)] || prestiges[0];
|
||||
}
|
||||
|
|
|
|||
40
aethex-bot/commands/qr.js
Normal file
40
aethex-bot/commands/qr.js
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
|
||||
const { getServerMode, getEmbedColor } = require('../utils/modeHelper');
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('qr')
|
||||
.setDescription('Generate a QR code')
|
||||
.addStringOption(option =>
|
||||
option.setName('text')
|
||||
.setDescription('The text or URL to encode')
|
||||
.setRequired(true)
|
||||
.setMaxLength(500)
|
||||
)
|
||||
.addIntegerOption(option =>
|
||||
option.setName('size')
|
||||
.setDescription('QR code size (default: 200)')
|
||||
.setRequired(false)
|
||||
.setMinValue(100)
|
||||
.setMaxValue(500)
|
||||
),
|
||||
|
||||
async execute(interaction, supabase, client) {
|
||||
const text = interaction.options.getString('text');
|
||||
const size = interaction.options.getInteger('size') || 200;
|
||||
const mode = await getServerMode(supabase, interaction.guildId);
|
||||
|
||||
const encodedText = encodeURIComponent(text);
|
||||
const qrUrl = `https://api.qrserver.com/v1/create-qr-code/?size=${size}x${size}&data=${encodedText}`;
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(getEmbedColor(mode))
|
||||
.setTitle('📱 QR Code Generated')
|
||||
.setDescription(`\`${text.length > 100 ? text.substring(0, 100) + '...' : text}\``)
|
||||
.setImage(qrUrl)
|
||||
.setFooter({ text: `Size: ${size}x${size}` })
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.reply({ embeds: [embed] });
|
||||
},
|
||||
};
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
|
||||
const { getServerMode, getEmbedColor, EMBED_COLORS } = require('../utils/modeHelper');
|
||||
const { getStandaloneXp, calculateLevel } = require('../utils/standaloneXp');
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('rank')
|
||||
.setDescription('View your unified level and XP across all platforms')
|
||||
.setDescription('View your level and XP')
|
||||
.addUserOption(option =>
|
||||
option.setName('user')
|
||||
.setDescription('User to check (defaults to yourself)')
|
||||
|
|
@ -19,71 +21,13 @@ module.exports = {
|
|||
await interaction.deferReply();
|
||||
|
||||
try {
|
||||
const { data: link } = await supabase
|
||||
.from('discord_links')
|
||||
.select('user_id, primary_arm')
|
||||
.eq('discord_id', target.id)
|
||||
.single();
|
||||
const mode = await getServerMode(supabase, interaction.guildId);
|
||||
|
||||
if (!link) {
|
||||
return interaction.editReply({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(0xff6b6b)
|
||||
.setDescription(`${target.id === interaction.user.id ? 'You are' : `${target.tag} is`} not linked to AeThex. Use \`/verify\` to link your account.`)
|
||||
]
|
||||
});
|
||||
if (mode === 'standalone') {
|
||||
return handleStandaloneRank(interaction, supabase, target);
|
||||
} else {
|
||||
return handleFederatedRank(interaction, supabase, target);
|
||||
}
|
||||
|
||||
const { data: profile } = await supabase
|
||||
.from('user_profiles')
|
||||
.select('username, avatar_url, xp, bio, prestige_level, total_xp_earned')
|
||||
.eq('id', link.user_id)
|
||||
.single();
|
||||
|
||||
const xp = profile?.xp || 0;
|
||||
const prestige = profile?.prestige_level || 0;
|
||||
const totalXpEarned = profile?.total_xp_earned || xp;
|
||||
const level = Math.floor(Math.sqrt(xp / 100));
|
||||
const currentLevelXp = level * level * 100;
|
||||
const nextLevelXp = (level + 1) * (level + 1) * 100;
|
||||
const progress = xp - currentLevelXp;
|
||||
const needed = nextLevelXp - currentLevelXp;
|
||||
const progressPercent = Math.floor((progress / needed) * 100);
|
||||
|
||||
const progressBar = createProgressBar(progressPercent);
|
||||
const prestigeInfo = getPrestigeInfo(prestige);
|
||||
|
||||
const { count: rankPosition } = await supabase
|
||||
.from('user_profiles')
|
||||
.select('*', { count: 'exact', head: true })
|
||||
.gt('xp', xp);
|
||||
|
||||
// Validate avatar URL - must be http/https, not base64
|
||||
let avatarUrl = target.displayAvatarURL();
|
||||
if (profile?.avatar_url && profile.avatar_url.startsWith('http')) {
|
||||
avatarUrl = profile.avatar_url;
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(prestigeInfo.color)
|
||||
.setTitle(`${prestigeInfo.icon} ${profile?.username || target.tag}'s Rank`)
|
||||
.setThumbnail(avatarUrl)
|
||||
.addFields(
|
||||
{ name: 'Prestige', value: prestige > 0 ? `**${prestigeInfo.name}** (P${prestige})` : 'Not prestiged', inline: true },
|
||||
{ name: 'Level', value: `**${level}**`, inline: true },
|
||||
{ name: 'Rank', value: `#${(rankPosition || 0) + 1}`, inline: true },
|
||||
{ name: 'Current XP', value: `**${xp.toLocaleString()}**`, inline: true },
|
||||
{ name: 'XP Bonus', value: prestige > 0 ? `+${prestige * 5}%` : 'None', inline: true },
|
||||
{ name: 'Total XP Earned', value: totalXpEarned.toLocaleString(), inline: true },
|
||||
{ name: 'Progress to Next Level', value: `${progressBar}\n${progress.toLocaleString()} / ${needed.toLocaleString()} XP (${progressPercent}%)` },
|
||||
{ name: 'Primary Realm', value: link.primary_arm || 'None set', inline: true }
|
||||
)
|
||||
.setFooter({ text: prestige >= 1 ? `Prestige ${prestige} | XP earned across Discord & AeThex platforms` : 'XP earned across Discord & AeThex platforms' })
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.editReply({ embeds: [embed] });
|
||||
|
||||
} catch (error) {
|
||||
console.error('Rank error:', error);
|
||||
await interaction.editReply({ content: 'Failed to fetch rank data.' });
|
||||
|
|
@ -91,6 +35,123 @@ module.exports = {
|
|||
},
|
||||
};
|
||||
|
||||
async function handleStandaloneRank(interaction, supabase, target) {
|
||||
const data = await getStandaloneXp(supabase, target.id, interaction.guildId);
|
||||
|
||||
if (!data) {
|
||||
return interaction.editReply({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(EMBED_COLORS.standalone)
|
||||
.setDescription(`${target.id === interaction.user.id ? 'You have' : `${target.tag} has`} no XP yet. Start chatting to earn XP!`)
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
const xp = data.xp || 0;
|
||||
const prestige = data.prestige_level || 0;
|
||||
const totalXpEarned = data.total_xp_earned || xp;
|
||||
const level = calculateLevel(xp, 'normal');
|
||||
const currentLevelXp = level * level * 100;
|
||||
const nextLevelXp = (level + 1) * (level + 1) * 100;
|
||||
const progress = xp - currentLevelXp;
|
||||
const needed = nextLevelXp - currentLevelXp;
|
||||
const progressPercent = Math.floor((progress / needed) * 100);
|
||||
|
||||
const progressBar = createProgressBar(progressPercent);
|
||||
const prestigeInfo = getPrestigeInfo(prestige);
|
||||
|
||||
const { count: rankPosition } = await supabase
|
||||
.from('guild_user_xp')
|
||||
.select('*', { count: 'exact', head: true })
|
||||
.eq('guild_id', interaction.guildId)
|
||||
.gt('xp', xp);
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(prestigeInfo.color)
|
||||
.setTitle(`${prestigeInfo.icon} ${target.tag}'s Rank`)
|
||||
.setThumbnail(target.displayAvatarURL())
|
||||
.addFields(
|
||||
{ name: 'Prestige', value: prestige > 0 ? `**${prestigeInfo.name}** (P${prestige})` : 'Not prestiged', inline: true },
|
||||
{ name: 'Level', value: `**${level}**`, inline: true },
|
||||
{ name: 'Server Rank', value: `#${(rankPosition || 0) + 1}`, inline: true },
|
||||
{ name: 'Current XP', value: `**${xp.toLocaleString()}**`, inline: true },
|
||||
{ name: 'XP Bonus', value: prestige > 0 ? `+${prestige * 5}%` : 'None', inline: true },
|
||||
{ name: 'Total XP Earned', value: totalXpEarned.toLocaleString(), inline: true },
|
||||
{ name: 'Progress to Next Level', value: `${progressBar}\n${progress.toLocaleString()} / ${needed.toLocaleString()} XP (${progressPercent}%)` }
|
||||
)
|
||||
.setFooter({ text: `🏠 Standalone Mode • ${interaction.guild.name}` })
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.editReply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
async function handleFederatedRank(interaction, supabase, target) {
|
||||
const { data: link } = await supabase
|
||||
.from('discord_links')
|
||||
.select('user_id, primary_arm')
|
||||
.eq('discord_id', target.id)
|
||||
.single();
|
||||
|
||||
if (!link) {
|
||||
return interaction.editReply({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(0xff6b6b)
|
||||
.setDescription(`${target.id === interaction.user.id ? 'You are' : `${target.tag} is`} not linked to AeThex. Use \`/verify\` to link your account.`)
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
const { data: profile } = await supabase
|
||||
.from('user_profiles')
|
||||
.select('username, avatar_url, xp, bio, prestige_level, total_xp_earned')
|
||||
.eq('id', link.user_id)
|
||||
.single();
|
||||
|
||||
const xp = profile?.xp || 0;
|
||||
const prestige = profile?.prestige_level || 0;
|
||||
const totalXpEarned = profile?.total_xp_earned || xp;
|
||||
const level = Math.floor(Math.sqrt(xp / 100));
|
||||
const currentLevelXp = level * level * 100;
|
||||
const nextLevelXp = (level + 1) * (level + 1) * 100;
|
||||
const progress = xp - currentLevelXp;
|
||||
const needed = nextLevelXp - currentLevelXp;
|
||||
const progressPercent = Math.floor((progress / needed) * 100);
|
||||
|
||||
const progressBar = createProgressBar(progressPercent);
|
||||
const prestigeInfo = getPrestigeInfo(prestige);
|
||||
|
||||
const { count: rankPosition } = await supabase
|
||||
.from('user_profiles')
|
||||
.select('*', { count: 'exact', head: true })
|
||||
.gt('xp', xp);
|
||||
|
||||
let avatarUrl = target.displayAvatarURL();
|
||||
if (profile?.avatar_url && profile.avatar_url.startsWith('http')) {
|
||||
avatarUrl = profile.avatar_url;
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(prestigeInfo.color)
|
||||
.setTitle(`${prestigeInfo.icon} ${profile?.username || target.tag}'s Rank`)
|
||||
.setThumbnail(avatarUrl)
|
||||
.addFields(
|
||||
{ name: 'Prestige', value: prestige > 0 ? `**${prestigeInfo.name}** (P${prestige})` : 'Not prestiged', inline: true },
|
||||
{ name: 'Level', value: `**${level}**`, inline: true },
|
||||
{ name: 'Global Rank', value: `#${(rankPosition || 0) + 1}`, inline: true },
|
||||
{ name: 'Current XP', value: `**${xp.toLocaleString()}**`, inline: true },
|
||||
{ name: 'XP Bonus', value: prestige > 0 ? `+${prestige * 5}%` : 'None', inline: true },
|
||||
{ name: 'Total XP Earned', value: totalXpEarned.toLocaleString(), inline: true },
|
||||
{ name: 'Progress to Next Level', value: `${progressBar}\n${progress.toLocaleString()} / ${needed.toLocaleString()} XP (${progressPercent}%)` },
|
||||
{ name: 'Primary Realm', value: link.primary_arm || 'None set', inline: true }
|
||||
)
|
||||
.setFooter({ text: prestige >= 1 ? `🌐 Federation • Prestige ${prestige} | XP earned across Discord & AeThex platforms` : '🌐 Federation • XP earned across Discord & AeThex platforms' })
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.editReply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
function createProgressBar(percent) {
|
||||
const filled = Math.floor(percent / 10);
|
||||
const empty = 10 - filled;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
const { SlashCommandBuilder, EmbedBuilder } = require("discord.js");
|
||||
const { assignRoleByArm, getUserArm } = require("../utils/roleManager");
|
||||
const { getServerMode, EMBED_COLORS } = require("../utils/modeHelper");
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
|
|
@ -12,6 +13,17 @@ module.exports = {
|
|||
if (!supabase) {
|
||||
return interaction.reply({ content: "This feature requires Supabase to be configured.", ephemeral: true });
|
||||
}
|
||||
|
||||
const mode = await getServerMode(supabase, interaction.guildId);
|
||||
|
||||
if (mode === 'standalone') {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(EMBED_COLORS.standalone)
|
||||
.setTitle('🏠 Standalone Mode')
|
||||
.setDescription('Role syncing is disabled in standalone mode.\n\nThis server operates independently without AeThex role federation.\n\nUse `/config mode` to switch to federated mode.');
|
||||
return interaction.reply({ embeds: [embed], ephemeral: true });
|
||||
}
|
||||
|
||||
await interaction.deferReply({ ephemeral: true });
|
||||
|
||||
try {
|
||||
|
|
|
|||
193
aethex-bot/commands/remind.js
Normal file
193
aethex-bot/commands/remind.js
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
|
||||
const { getServerMode, getEmbedColor, EMBED_COLORS } = require('../utils/modeHelper');
|
||||
|
||||
const activeReminders = new Map();
|
||||
|
||||
function parseTime(timeStr) {
|
||||
const regex = /^(\d+)(s|m|h|d)$/i;
|
||||
const match = timeStr.match(regex);
|
||||
if (!match) return null;
|
||||
|
||||
const value = parseInt(match[1]);
|
||||
const unit = match[2].toLowerCase();
|
||||
|
||||
const multipliers = {
|
||||
's': 1000,
|
||||
'm': 60 * 1000,
|
||||
'h': 60 * 60 * 1000,
|
||||
'd': 24 * 60 * 60 * 1000
|
||||
};
|
||||
|
||||
return value * multipliers[unit];
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('remind')
|
||||
.setDescription('Set a personal reminder')
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('set')
|
||||
.setDescription('Set a new reminder')
|
||||
.addStringOption(option =>
|
||||
option.setName('time')
|
||||
.setDescription('When to remind (e.g., 30m, 2h, 1d)')
|
||||
.setRequired(true)
|
||||
)
|
||||
.addStringOption(option =>
|
||||
option.setName('message')
|
||||
.setDescription('What to remind you about')
|
||||
.setRequired(true)
|
||||
.setMaxLength(500)
|
||||
)
|
||||
)
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('list')
|
||||
.setDescription('List your active reminders')
|
||||
)
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('cancel')
|
||||
.setDescription('Cancel a reminder')
|
||||
.addStringOption(option =>
|
||||
option.setName('id')
|
||||
.setDescription('The reminder ID to cancel')
|
||||
.setRequired(true)
|
||||
)
|
||||
),
|
||||
|
||||
async execute(interaction, supabase, client) {
|
||||
const subcommand = interaction.options.getSubcommand();
|
||||
const mode = await getServerMode(supabase, interaction.guildId);
|
||||
const userId = interaction.user.id;
|
||||
|
||||
if (subcommand === 'set') {
|
||||
const timeStr = interaction.options.getString('time');
|
||||
const message = interaction.options.getString('message');
|
||||
|
||||
const duration = parseTime(timeStr);
|
||||
if (!duration) {
|
||||
return interaction.reply({
|
||||
content: 'Invalid time format. Use formats like: 30s, 5m, 2h, 1d',
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
if (duration < 10000) {
|
||||
return interaction.reply({
|
||||
content: 'Reminder must be at least 10 seconds in the future.',
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
if (duration > 30 * 24 * 60 * 60 * 1000) {
|
||||
return interaction.reply({
|
||||
content: 'Reminder cannot be more than 30 days in the future.',
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
const reminderId = `${Date.now()}-${Math.random().toString(36).substr(2, 6)}`;
|
||||
const reminderTime = Date.now() + duration;
|
||||
|
||||
const timeout = setTimeout(async () => {
|
||||
try {
|
||||
const user = await client.users.fetch(userId);
|
||||
const reminderEmbed = new EmbedBuilder()
|
||||
.setColor(EMBED_COLORS.warning)
|
||||
.setTitle('⏰ Reminder!')
|
||||
.setDescription(message)
|
||||
.addFields({
|
||||
name: '📍 Set',
|
||||
value: `<t:${Math.floor((reminderTime - duration) / 1000)}:R>`
|
||||
})
|
||||
.setTimestamp();
|
||||
|
||||
await user.send({ embeds: [reminderEmbed] });
|
||||
} catch (e) {
|
||||
try {
|
||||
const channel = await client.channels.fetch(interaction.channelId);
|
||||
await channel.send({
|
||||
content: `<@${userId}> ⏰ **Reminder:** ${message}`
|
||||
});
|
||||
} catch (e2) {}
|
||||
}
|
||||
|
||||
const userReminders = activeReminders.get(userId) || [];
|
||||
activeReminders.set(userId, userReminders.filter(r => r.id !== reminderId));
|
||||
}, duration);
|
||||
|
||||
const userReminders = activeReminders.get(userId) || [];
|
||||
userReminders.push({
|
||||
id: reminderId,
|
||||
message,
|
||||
time: reminderTime,
|
||||
timeout,
|
||||
channelId: interaction.channelId
|
||||
});
|
||||
activeReminders.set(userId, userReminders);
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(EMBED_COLORS.success)
|
||||
.setTitle('⏰ Reminder Set!')
|
||||
.setDescription(`I'll remind you: **${message}**`)
|
||||
.addFields(
|
||||
{ name: '⏱️ In', value: timeStr, inline: true },
|
||||
{ name: '📅 At', value: `<t:${Math.floor(reminderTime / 1000)}:f>`, inline: true },
|
||||
{ name: '🆔 ID', value: `\`${reminderId}\``, inline: true }
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.reply({ embeds: [embed], ephemeral: true });
|
||||
}
|
||||
|
||||
else if (subcommand === 'list') {
|
||||
const userReminders = activeReminders.get(userId) || [];
|
||||
|
||||
if (userReminders.length === 0) {
|
||||
return interaction.reply({
|
||||
content: 'You have no active reminders.',
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
const list = userReminders.map((r, i) => {
|
||||
return `${i + 1}. **${r.message.substring(0, 50)}${r.message.length > 50 ? '...' : ''}**\n ⏰ <t:${Math.floor(r.time / 1000)}:R> | ID: \`${r.id}\``;
|
||||
}).join('\n\n');
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(getEmbedColor(mode))
|
||||
.setTitle('⏰ Your Reminders')
|
||||
.setDescription(list)
|
||||
.setFooter({ text: `${userReminders.length} active reminder(s)` })
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.reply({ embeds: [embed], ephemeral: true });
|
||||
}
|
||||
|
||||
else if (subcommand === 'cancel') {
|
||||
const reminderId = interaction.options.getString('id');
|
||||
const userReminders = activeReminders.get(userId) || [];
|
||||
|
||||
const reminder = userReminders.find(r => r.id === reminderId);
|
||||
if (!reminder) {
|
||||
return interaction.reply({
|
||||
content: 'Reminder not found. Use `/remind list` to see your reminders.',
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
clearTimeout(reminder.timeout);
|
||||
activeReminders.set(userId, userReminders.filter(r => r.id !== reminderId));
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(EMBED_COLORS.success)
|
||||
.setTitle('🗑️ Reminder Cancelled')
|
||||
.setDescription(`Cancelled reminder: **${reminder.message.substring(0, 100)}**`)
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.reply({ embeds: [embed], ephemeral: true });
|
||||
}
|
||||
},
|
||||
};
|
||||
83
aethex-bot/commands/rep.js
Normal file
83
aethex-bot/commands/rep.js
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
|
||||
const { getServerMode, getEmbedColor, EMBED_COLORS } = require('../utils/modeHelper');
|
||||
|
||||
const repCooldowns = new Map();
|
||||
const REP_COOLDOWN = 12 * 60 * 60 * 1000;
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('rep')
|
||||
.setDescription('Give a reputation point to someone')
|
||||
.addUserOption(option =>
|
||||
option.setName('user')
|
||||
.setDescription('The user to give rep to')
|
||||
.setRequired(true)
|
||||
)
|
||||
.addStringOption(option =>
|
||||
option.setName('reason')
|
||||
.setDescription('Why are you giving them rep?')
|
||||
.setRequired(false)
|
||||
.setMaxLength(200)
|
||||
),
|
||||
|
||||
async execute(interaction, supabase, client) {
|
||||
const targetUser = interaction.options.getUser('user');
|
||||
const reason = interaction.options.getString('reason');
|
||||
const userId = interaction.user.id;
|
||||
const guildId = interaction.guildId;
|
||||
const mode = await getServerMode(supabase, interaction.guildId);
|
||||
|
||||
if (targetUser.id === userId) {
|
||||
return interaction.reply({
|
||||
content: "You can't give rep to yourself!",
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
if (targetUser.bot) {
|
||||
return interaction.reply({
|
||||
content: "You can't give rep to bots!",
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
const cooldownKey = `${guildId}-${userId}`;
|
||||
const lastRep = repCooldowns.get(cooldownKey);
|
||||
if (lastRep && Date.now() - lastRep < REP_COOLDOWN) {
|
||||
const remaining = REP_COOLDOWN - (Date.now() - lastRep);
|
||||
const hours = Math.floor(remaining / (60 * 60 * 1000));
|
||||
const minutes = Math.floor((remaining % (60 * 60 * 1000)) / (60 * 1000));
|
||||
return interaction.reply({
|
||||
content: `You can give rep again in ${hours}h ${minutes}m.`,
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
repCooldowns.set(cooldownKey, Date.now());
|
||||
|
||||
if (supabase) {
|
||||
try {
|
||||
await supabase.from('reputation').insert({
|
||||
guild_id: guildId,
|
||||
giver_id: userId,
|
||||
receiver_id: targetUser.id,
|
||||
reason: reason,
|
||||
created_at: new Date().toISOString()
|
||||
});
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(EMBED_COLORS.success)
|
||||
.setTitle('⭐ Reputation Given!')
|
||||
.setDescription(`${interaction.user} gave a rep point to ${targetUser}!`)
|
||||
.setThumbnail(targetUser.displayAvatarURL({ size: 128 }))
|
||||
.setTimestamp();
|
||||
|
||||
if (reason) {
|
||||
embed.addFields({ name: '💬 Reason', value: reason });
|
||||
}
|
||||
|
||||
await interaction.reply({ embeds: [embed] });
|
||||
},
|
||||
};
|
||||
98
aethex-bot/commands/roll.js
Normal file
98
aethex-bot/commands/roll.js
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
|
||||
const { getServerMode, getEmbedColor } = require('../utils/modeHelper');
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('roll')
|
||||
.setDescription('Roll dice')
|
||||
.addStringOption(option =>
|
||||
option.setName('dice')
|
||||
.setDescription('Dice notation (e.g., 2d6, d20, 3d8+5)')
|
||||
.setRequired(false)
|
||||
)
|
||||
.addIntegerOption(option =>
|
||||
option.setName('sides')
|
||||
.setDescription('Number of sides (default: 6)')
|
||||
.setRequired(false)
|
||||
.setMinValue(2)
|
||||
.setMaxValue(1000)
|
||||
)
|
||||
.addIntegerOption(option =>
|
||||
option.setName('count')
|
||||
.setDescription('Number of dice to roll (default: 1)')
|
||||
.setRequired(false)
|
||||
.setMinValue(1)
|
||||
.setMaxValue(100)
|
||||
),
|
||||
|
||||
async execute(interaction, supabase, client) {
|
||||
const diceNotation = interaction.options.getString('dice');
|
||||
let sides = interaction.options.getInteger('sides') || 6;
|
||||
let count = interaction.options.getInteger('count') || 1;
|
||||
let modifier = 0;
|
||||
|
||||
if (diceNotation) {
|
||||
const match = diceNotation.match(/^(\d*)d(\d+)([+-]\d+)?$/i);
|
||||
if (match) {
|
||||
count = match[1] ? parseInt(match[1]) : 1;
|
||||
sides = parseInt(match[2]);
|
||||
modifier = match[3] ? parseInt(match[3]) : 0;
|
||||
|
||||
if (count > 100) count = 100;
|
||||
if (sides > 1000) sides = 1000;
|
||||
} else {
|
||||
return interaction.reply({
|
||||
content: 'Invalid dice notation. Use format like `2d6`, `d20`, or `3d8+5`',
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const rolls = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
rolls.push(Math.floor(Math.random() * sides) + 1);
|
||||
}
|
||||
|
||||
const sum = rolls.reduce((a, b) => a + b, 0);
|
||||
const total = sum + modifier;
|
||||
|
||||
const mode = await getServerMode(supabase, interaction.guildId);
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(getEmbedColor(mode))
|
||||
.setTitle('🎲 Dice Roll')
|
||||
.setTimestamp()
|
||||
.setFooter({ text: `Rolled by ${interaction.user.username}` });
|
||||
|
||||
if (count === 1 && modifier === 0) {
|
||||
embed.setDescription(`You rolled a **${total}**!`);
|
||||
} else {
|
||||
const rollsDisplay = rolls.length <= 20
|
||||
? rolls.map(r => `\`${r}\``).join(' + ')
|
||||
: `${rolls.slice(0, 20).map(r => `\`${r}\``).join(' + ')} ... (+${rolls.length - 20} more)`;
|
||||
|
||||
let formula = `${count}d${sides}`;
|
||||
if (modifier > 0) formula += `+${modifier}`;
|
||||
else if (modifier < 0) formula += modifier;
|
||||
|
||||
embed.addFields(
|
||||
{ name: '🎯 Formula', value: formula, inline: true },
|
||||
{ name: '📊 Total', value: `**${total}**`, inline: true }
|
||||
);
|
||||
|
||||
if (rolls.length <= 20) {
|
||||
embed.addFields({ name: '🎲 Rolls', value: rollsDisplay });
|
||||
}
|
||||
|
||||
if (modifier !== 0) {
|
||||
embed.addFields({
|
||||
name: '➕ Calculation',
|
||||
value: `${sum} ${modifier >= 0 ? '+' : ''}${modifier} = **${total}**`,
|
||||
inline: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await interaction.reply({ embeds: [embed] });
|
||||
},
|
||||
};
|
||||
|
|
@ -5,6 +5,7 @@ const {
|
|||
ActionRowBuilder,
|
||||
} = require("discord.js");
|
||||
const { assignRoleByArm } = require("../utils/roleManager");
|
||||
const { getServerMode, EMBED_COLORS } = require("../utils/modeHelper");
|
||||
|
||||
const REALMS = [
|
||||
{ value: "labs", label: "🧪 Labs", description: "Research & Development" },
|
||||
|
|
@ -35,6 +36,17 @@ module.exports = {
|
|||
if (!supabase) {
|
||||
return interaction.reply({ content: "This feature requires Supabase to be configured.", ephemeral: true });
|
||||
}
|
||||
|
||||
const mode = await getServerMode(supabase, interaction.guildId);
|
||||
|
||||
if (mode === 'standalone') {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(EMBED_COLORS.standalone)
|
||||
.setTitle('🏠 Standalone Mode')
|
||||
.setDescription('Realm selection is disabled in standalone mode.\n\nThis server operates independently without AeThex realm affiliation.\n\nUse `/config mode` to switch to federated mode.');
|
||||
return interaction.reply({ embeds: [embed], ephemeral: true });
|
||||
}
|
||||
|
||||
await interaction.deferReply({ ephemeral: true });
|
||||
|
||||
try {
|
||||
|
|
|
|||
127
aethex-bot/commands/slots.js
Normal file
127
aethex-bot/commands/slots.js
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
|
||||
const { getServerMode, getEmbedColor, EMBED_COLORS } = require('../utils/modeHelper');
|
||||
const { updateStandaloneXp } = require('../utils/standaloneXp');
|
||||
|
||||
const SYMBOLS = ['🍒', '🍋', '🍊', '🍇', '⭐', '💎', '7️⃣'];
|
||||
const WEIGHTS = [30, 25, 20, 15, 7, 2, 1];
|
||||
|
||||
const PAYOUTS = {
|
||||
'🍒🍒🍒': 5,
|
||||
'🍋🍋🍋': 10,
|
||||
'🍊🍊🍊': 15,
|
||||
'🍇🍇🍇': 20,
|
||||
'⭐⭐⭐': 50,
|
||||
'💎💎💎': 100,
|
||||
'7️⃣7️⃣7️⃣': 250,
|
||||
};
|
||||
|
||||
const slotsCooldowns = new Map();
|
||||
const COOLDOWN = 30000;
|
||||
|
||||
function spin() {
|
||||
const totalWeight = WEIGHTS.reduce((a, b) => a + b, 0);
|
||||
const random = Math.random() * totalWeight;
|
||||
let cumulativeWeight = 0;
|
||||
|
||||
for (let i = 0; i < SYMBOLS.length; i++) {
|
||||
cumulativeWeight += WEIGHTS[i];
|
||||
if (random < cumulativeWeight) {
|
||||
return SYMBOLS[i];
|
||||
}
|
||||
}
|
||||
return SYMBOLS[0];
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('slots')
|
||||
.setDescription('Try your luck at the slot machine!')
|
||||
.addIntegerOption(option =>
|
||||
option.setName('bet')
|
||||
.setDescription('XP to bet (10-50)')
|
||||
.setRequired(false)
|
||||
.setMinValue(10)
|
||||
.setMaxValue(50)
|
||||
),
|
||||
|
||||
async execute(interaction, supabase, client) {
|
||||
const bet = interaction.options.getInteger('bet') || 10;
|
||||
const userId = interaction.user.id;
|
||||
const guildId = interaction.guildId;
|
||||
const mode = await getServerMode(supabase, interaction.guildId);
|
||||
|
||||
const cooldownKey = `${guildId}-${userId}`;
|
||||
const lastSpin = slotsCooldowns.get(cooldownKey);
|
||||
if (lastSpin && Date.now() - lastSpin < COOLDOWN) {
|
||||
const remaining = Math.ceil((COOLDOWN - (Date.now() - lastSpin)) / 1000);
|
||||
return interaction.reply({
|
||||
content: `You can spin again in ${remaining} seconds.`,
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
slotsCooldowns.set(cooldownKey, Date.now());
|
||||
|
||||
const slot1 = spin();
|
||||
const slot2 = spin();
|
||||
const slot3 = spin();
|
||||
const result = `${slot1}${slot2}${slot3}`;
|
||||
|
||||
const payout = PAYOUTS[result];
|
||||
let netGain = 0;
|
||||
let resultMessage = '';
|
||||
let color = getEmbedColor(mode);
|
||||
|
||||
if (payout) {
|
||||
netGain = bet * payout;
|
||||
resultMessage = `🎉 JACKPOT! You won **${netGain} XP**!`;
|
||||
color = EMBED_COLORS.success;
|
||||
} else if (slot1 === slot2 || slot2 === slot3 || slot1 === slot3) {
|
||||
netGain = Math.floor(bet * 0.5);
|
||||
resultMessage = `Nice! Two matching! You won **${netGain} XP**!`;
|
||||
color = 0xF59E0B;
|
||||
} else {
|
||||
netGain = -bet;
|
||||
resultMessage = `No luck this time. You lost **${bet} XP**.`;
|
||||
color = EMBED_COLORS.error;
|
||||
}
|
||||
|
||||
if (netGain !== 0) {
|
||||
if (mode === 'standalone') {
|
||||
await updateStandaloneXp(supabase, userId, guildId, netGain, interaction.user.username);
|
||||
} else if (supabase) {
|
||||
try {
|
||||
const { data: profile } = await supabase
|
||||
.from('user_profiles')
|
||||
.select('xp')
|
||||
.eq('discord_id', userId)
|
||||
.maybeSingle();
|
||||
|
||||
if (profile) {
|
||||
const newXp = Math.max(0, (profile.xp || 0) + netGain);
|
||||
await supabase
|
||||
.from('user_profiles')
|
||||
.update({ xp: newXp })
|
||||
.eq('discord_id', userId);
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(color)
|
||||
.setTitle('🎰 Slot Machine')
|
||||
.setDescription(`
|
||||
╔═══════════╗
|
||||
║ ${slot1} ${slot2} ${slot3} ║
|
||||
╚═══════════╝
|
||||
|
||||
${resultMessage}
|
||||
`)
|
||||
.addFields({ name: '💰 Bet', value: `${bet} XP`, inline: true })
|
||||
.setFooter({ text: `${interaction.user.username}` })
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.reply({ embeds: [embed] });
|
||||
},
|
||||
};
|
||||
113
aethex-bot/commands/starboard.js
Normal file
113
aethex-bot/commands/starboard.js
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits, ChannelType } = require('discord.js');
|
||||
const { EMBED_COLORS } = require('../utils/modeHelper');
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('starboard')
|
||||
.setDescription('Configure the starboard system')
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild)
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('setup')
|
||||
.setDescription('Set up or update the starboard channel')
|
||||
.addChannelOption(option =>
|
||||
option.setName('channel')
|
||||
.setDescription('The starboard channel')
|
||||
.setRequired(true)
|
||||
.addChannelTypes(ChannelType.GuildText)
|
||||
)
|
||||
.addIntegerOption(option =>
|
||||
option.setName('threshold')
|
||||
.setDescription('Minimum stars needed (default: 3)')
|
||||
.setRequired(false)
|
||||
.setMinValue(1)
|
||||
.setMaxValue(50)
|
||||
)
|
||||
)
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('disable')
|
||||
.setDescription('Disable the starboard')
|
||||
)
|
||||
.addSubcommand(subcommand =>
|
||||
subcommand
|
||||
.setName('status')
|
||||
.setDescription('View current starboard settings')
|
||||
),
|
||||
|
||||
async execute(interaction, supabase, client) {
|
||||
const subcommand = interaction.options.getSubcommand();
|
||||
const guildId = interaction.guildId;
|
||||
|
||||
if (!supabase) {
|
||||
return interaction.reply({ content: 'Starboard system unavailable.', ephemeral: true });
|
||||
}
|
||||
|
||||
if (subcommand === 'setup') {
|
||||
const channel = interaction.options.getChannel('channel');
|
||||
const threshold = interaction.options.getInteger('threshold') || 3;
|
||||
|
||||
await supabase.from('starboard_config').upsert({
|
||||
guild_id: guildId,
|
||||
channel_id: channel.id,
|
||||
threshold: threshold,
|
||||
enabled: true,
|
||||
updated_at: new Date().toISOString()
|
||||
}, { onConflict: 'guild_id' });
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(EMBED_COLORS.success)
|
||||
.setTitle('⭐ Starboard Configured!')
|
||||
.addFields(
|
||||
{ name: '📍 Channel', value: `${channel}`, inline: true },
|
||||
{ name: '🎯 Threshold', value: `${threshold} stars`, inline: true }
|
||||
)
|
||||
.setDescription('Messages that receive enough ⭐ reactions will be posted to the starboard!')
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.reply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
else if (subcommand === 'disable') {
|
||||
await supabase
|
||||
.from('starboard_config')
|
||||
.update({ enabled: false })
|
||||
.eq('guild_id', guildId);
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(EMBED_COLORS.warning)
|
||||
.setTitle('⭐ Starboard Disabled')
|
||||
.setDescription('The starboard has been disabled. Use `/starboard setup` to re-enable.')
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.reply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
else if (subcommand === 'status') {
|
||||
const { data } = await supabase
|
||||
.from('starboard_config')
|
||||
.select('*')
|
||||
.eq('guild_id', guildId)
|
||||
.maybeSingle();
|
||||
|
||||
if (!data) {
|
||||
return interaction.reply({
|
||||
content: 'Starboard is not set up. Use `/starboard setup` to configure it.',
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(data.enabled ? EMBED_COLORS.success : EMBED_COLORS.warning)
|
||||
.setTitle('⭐ Starboard Status')
|
||||
.addFields(
|
||||
{ name: '📊 Status', value: data.enabled ? '✅ Enabled' : '❌ Disabled', inline: true },
|
||||
{ name: '📍 Channel', value: `<#${data.channel_id}>`, inline: true },
|
||||
{ name: '🎯 Threshold', value: `${data.threshold || 3} stars`, inline: true }
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.reply({ embeds: [embed] });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
|
||||
const { getServerMode, getEmbedColor, getModeDisplayName, getModeEmoji } = require('../utils/modeHelper');
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
|
|
@ -12,9 +13,30 @@ module.exports = {
|
|||
const hours = Math.floor(uptime / 3600);
|
||||
const minutes = Math.floor((uptime % 3600) / 60);
|
||||
const seconds = uptime % 60;
|
||||
|
||||
const serverMode = supabase ? await getServerMode(supabase, interaction.guildId) : 'standalone';
|
||||
const embedColor = getEmbedColor(serverMode);
|
||||
|
||||
let federatedCount = 0;
|
||||
let standaloneCount = 0;
|
||||
|
||||
if (supabase) {
|
||||
try {
|
||||
const { data: configs } = await supabase
|
||||
.from('server_config')
|
||||
.select('mode');
|
||||
|
||||
if (configs) {
|
||||
federatedCount = configs.filter(c => c.mode === 'federated' || !c.mode).length;
|
||||
standaloneCount = configs.filter(c => c.mode === 'standalone').length;
|
||||
}
|
||||
} catch (e) {
|
||||
federatedCount = guildCount;
|
||||
}
|
||||
}
|
||||
|
||||
const realmStatus = [];
|
||||
const REALM_GUILDS = client.REALM_GUILDS;
|
||||
const REALM_GUILDS = client.REALM_GUILDS || {};
|
||||
|
||||
for (const [realm, guildId] of Object.entries(REALM_GUILDS)) {
|
||||
if (!guildId) {
|
||||
|
|
@ -37,19 +59,34 @@ module.exports = {
|
|||
}));
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0x7c3aed)
|
||||
.setTitle('AeThex Network Status')
|
||||
.setDescription('Current status of the AeThex Federation')
|
||||
.setColor(embedColor)
|
||||
.setTitle('⚔️ Warden Status')
|
||||
.setDescription(`${getModeEmoji(serverMode)} This server is running in **${getModeDisplayName(serverMode)}** mode`)
|
||||
.addFields(
|
||||
{ name: 'Total Servers', value: `${guildCount}`, inline: true },
|
||||
{ name: 'Total Members', value: `${memberCount.toLocaleString()}`, inline: true },
|
||||
{ name: 'Uptime', value: `${hours}h ${minutes}m ${seconds}s`, inline: true },
|
||||
...realmFields,
|
||||
{ name: 'Sentinel Status', value: client.heatMap.size > 0 ? `⚠️ Monitoring ${client.heatMap.size} user(s)` : '🛡️ All Clear', inline: false },
|
||||
{ name: 'Active Tickets', value: `${client.activeTickets.size}`, inline: true },
|
||||
{ name: 'Federation Mappings', value: `${client.federationMappings.size}`, inline: true }
|
||||
)
|
||||
.setFooter({ text: 'AeThex Unified Bot' })
|
||||
{ name: 'Federated Servers', value: `🌐 ${federatedCount}`, inline: true },
|
||||
{ name: 'Standalone Servers', value: `🏠 ${standaloneCount}`, inline: true },
|
||||
{ name: '\u200b', value: '\u200b', inline: true }
|
||||
);
|
||||
|
||||
if (serverMode === 'federated' && realmFields.length > 0) {
|
||||
embed.addFields(
|
||||
{ name: '── Federation Realms ──', value: '\u200b', inline: false },
|
||||
...realmFields
|
||||
);
|
||||
}
|
||||
|
||||
embed.addFields(
|
||||
{ name: '── Security ──', value: '\u200b', inline: false },
|
||||
{ name: 'Sentinel Status', value: client.heatMap?.size > 0 ? `⚠️ Monitoring ${client.heatMap.size} user(s)` : '🛡️ All Clear', inline: true },
|
||||
{ name: 'Active Tickets', value: `${client.activeTickets?.size || 0}`, inline: true },
|
||||
{ name: 'Federation Mappings', value: `${client.federationMappings?.size || 0}`, inline: true }
|
||||
);
|
||||
|
||||
embed
|
||||
.setFooter({ text: `Warden • ${getModeDisplayName(serverMode)} Mode` })
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.reply({ embeds: [embed] });
|
||||
|
|
|
|||
218
aethex-bot/commands/trade.js
Normal file
218
aethex-bot/commands/trade.js
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
const { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js');
|
||||
const { getServerMode, getEmbedColor, EMBED_COLORS } = require('../utils/modeHelper');
|
||||
|
||||
const activeTrades = new Map();
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('trade')
|
||||
.setDescription('Trade items with another user')
|
||||
.addUserOption(option =>
|
||||
option.setName('user')
|
||||
.setDescription('User to trade with')
|
||||
.setRequired(true)
|
||||
)
|
||||
.addStringOption(option =>
|
||||
option.setName('offer')
|
||||
.setDescription('What you\'re offering (item name)')
|
||||
.setRequired(true)
|
||||
)
|
||||
.addStringOption(option =>
|
||||
option.setName('request')
|
||||
.setDescription('What you want in return (item name)')
|
||||
.setRequired(true)
|
||||
),
|
||||
|
||||
async execute(interaction, supabase, client) {
|
||||
const partner = interaction.options.getUser('user');
|
||||
const offer = interaction.options.getString('offer');
|
||||
const request = interaction.options.getString('request');
|
||||
const initiator = interaction.user;
|
||||
const mode = await getServerMode(supabase, interaction.guildId);
|
||||
const guildId = interaction.guildId;
|
||||
|
||||
if (partner.id === initiator.id) {
|
||||
return interaction.reply({ content: "You can't trade with yourself!", ephemeral: true });
|
||||
}
|
||||
|
||||
if (partner.bot) {
|
||||
return interaction.reply({ content: "You can't trade with bots!", ephemeral: true });
|
||||
}
|
||||
|
||||
if (!supabase) {
|
||||
return interaction.reply({ content: 'Trade system unavailable.', ephemeral: true });
|
||||
}
|
||||
|
||||
const tradeKey = `${guildId}-${initiator.id}`;
|
||||
if (activeTrades.has(tradeKey)) {
|
||||
return interaction.reply({ content: 'You already have an active trade!', ephemeral: true });
|
||||
}
|
||||
|
||||
const { data: initiatorItem } = await supabase
|
||||
.from('user_inventory')
|
||||
.select('*, shop_items(*)')
|
||||
.eq('guild_id', guildId)
|
||||
.eq('user_id', initiator.id)
|
||||
.ilike('shop_items.name', `%${offer}%`)
|
||||
.gt('quantity', 0)
|
||||
.maybeSingle();
|
||||
|
||||
if (!initiatorItem) {
|
||||
return interaction.reply({
|
||||
content: `You don't have "${offer}" in your inventory!`,
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
const { data: partnerItem } = await supabase
|
||||
.from('user_inventory')
|
||||
.select('*, shop_items(*)')
|
||||
.eq('guild_id', guildId)
|
||||
.eq('user_id', partner.id)
|
||||
.ilike('shop_items.name', `%${request}%`)
|
||||
.gt('quantity', 0)
|
||||
.maybeSingle();
|
||||
|
||||
if (!partnerItem) {
|
||||
return interaction.reply({
|
||||
content: `${partner.username} doesn't have "${request}" in their inventory!`,
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
activeTrades.set(tradeKey, {
|
||||
partner: partner.id,
|
||||
offer: initiatorItem,
|
||||
request: partnerItem
|
||||
});
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(getEmbedColor(mode))
|
||||
.setTitle('🔄 Trade Request')
|
||||
.setDescription(`${initiator} wants to trade with ${partner}!`)
|
||||
.addFields(
|
||||
{
|
||||
name: `${initiator.username} Offers`,
|
||||
value: `${initiatorItem.shop_items?.emoji || '📦'} ${initiatorItem.shop_items?.name}`,
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: `${partner.username} Offers`,
|
||||
value: `${partnerItem.shop_items?.emoji || '📦'} ${partnerItem.shop_items?.name}`,
|
||||
inline: true
|
||||
}
|
||||
)
|
||||
.setFooter({ text: 'Trade expires in 60 seconds' })
|
||||
.setTimestamp();
|
||||
|
||||
const row = new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId(`trade_accept_${initiator.id}`)
|
||||
.setLabel('Accept')
|
||||
.setStyle(ButtonStyle.Success),
|
||||
new ButtonBuilder()
|
||||
.setCustomId(`trade_decline_${initiator.id}`)
|
||||
.setLabel('Decline')
|
||||
.setStyle(ButtonStyle.Danger)
|
||||
);
|
||||
|
||||
const message = await interaction.reply({
|
||||
content: `${partner}`,
|
||||
embeds: [embed],
|
||||
components: [row],
|
||||
fetchReply: true
|
||||
});
|
||||
|
||||
const collector = message.createMessageComponentCollector({
|
||||
filter: i => i.user.id === partner.id && i.customId.includes(initiator.id),
|
||||
time: 60000,
|
||||
max: 1
|
||||
});
|
||||
|
||||
collector.on('collect', async (i) => {
|
||||
const tradeData = activeTrades.get(tradeKey);
|
||||
activeTrades.delete(tradeKey);
|
||||
|
||||
if (!tradeData) return;
|
||||
|
||||
if (i.customId.startsWith('trade_decline')) {
|
||||
const declineEmbed = new EmbedBuilder()
|
||||
.setColor(EMBED_COLORS.error)
|
||||
.setTitle('🔄 Trade Declined')
|
||||
.setDescription(`${partner} declined the trade.`)
|
||||
.setTimestamp();
|
||||
await i.update({ embeds: [declineEmbed], components: [] });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await supabase.rpc('execute_trade', {
|
||||
p_guild_id: guildId,
|
||||
p_user1_id: initiator.id,
|
||||
p_user2_id: partner.id,
|
||||
p_item1_id: tradeData.offer.item_id,
|
||||
p_item2_id: tradeData.request.item_id
|
||||
});
|
||||
|
||||
const successEmbed = new EmbedBuilder()
|
||||
.setColor(EMBED_COLORS.success)
|
||||
.setTitle('🔄 Trade Complete!')
|
||||
.setDescription(`${initiator} and ${partner} successfully traded items!`)
|
||||
.addFields(
|
||||
{ name: `${initiator.username} received`, value: `${partnerItem.shop_items?.emoji || '📦'} ${partnerItem.shop_items?.name}`, inline: true },
|
||||
{ name: `${partner.username} received`, value: `${initiatorItem.shop_items?.emoji || '📦'} ${initiatorItem.shop_items?.name}`, inline: true }
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
await i.update({ embeds: [successEmbed], components: [] });
|
||||
} catch (e) {
|
||||
await supabase
|
||||
.from('user_inventory')
|
||||
.update({ quantity: tradeData.offer.quantity - 1 })
|
||||
.eq('id', tradeData.offer.id);
|
||||
|
||||
await supabase
|
||||
.from('user_inventory')
|
||||
.update({ quantity: tradeData.request.quantity - 1 })
|
||||
.eq('id', tradeData.request.id);
|
||||
|
||||
await supabase.from('user_inventory').upsert({
|
||||
guild_id: guildId,
|
||||
user_id: partner.id,
|
||||
item_id: tradeData.offer.item_id,
|
||||
quantity: 1
|
||||
}, { onConflict: 'guild_id,user_id,item_id' });
|
||||
|
||||
await supabase.from('user_inventory').upsert({
|
||||
guild_id: guildId,
|
||||
user_id: initiator.id,
|
||||
item_id: tradeData.request.item_id,
|
||||
quantity: 1
|
||||
}, { onConflict: 'guild_id,user_id,item_id' });
|
||||
|
||||
const successEmbed = new EmbedBuilder()
|
||||
.setColor(EMBED_COLORS.success)
|
||||
.setTitle('🔄 Trade Complete!')
|
||||
.setDescription(`${initiator} and ${partner} successfully traded items!`)
|
||||
.setTimestamp();
|
||||
|
||||
await i.update({ embeds: [successEmbed], components: [] });
|
||||
}
|
||||
});
|
||||
|
||||
collector.on('end', async (collected) => {
|
||||
if (collected.size === 0) {
|
||||
activeTrades.delete(tradeKey);
|
||||
|
||||
const timeoutEmbed = new EmbedBuilder()
|
||||
.setColor(EMBED_COLORS.warning)
|
||||
.setTitle('🔄 Trade Expired')
|
||||
.setDescription(`${partner} didn't respond in time.`)
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.editReply({ embeds: [timeoutEmbed], components: [] });
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
93
aethex-bot/commands/translate.js
Normal file
93
aethex-bot/commands/translate.js
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
|
||||
const { getServerMode, getEmbedColor, EMBED_COLORS } = require('../utils/modeHelper');
|
||||
|
||||
const LANGUAGES = {
|
||||
'en': 'English',
|
||||
'es': 'Spanish',
|
||||
'fr': 'French',
|
||||
'de': 'German',
|
||||
'it': 'Italian',
|
||||
'pt': 'Portuguese',
|
||||
'ru': 'Russian',
|
||||
'ja': 'Japanese',
|
||||
'ko': 'Korean',
|
||||
'zh': 'Chinese',
|
||||
'ar': 'Arabic',
|
||||
'hi': 'Hindi',
|
||||
'nl': 'Dutch',
|
||||
'pl': 'Polish',
|
||||
'tr': 'Turkish',
|
||||
'vi': 'Vietnamese',
|
||||
'th': 'Thai',
|
||||
'sv': 'Swedish',
|
||||
'da': 'Danish',
|
||||
'fi': 'Finnish'
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('translate')
|
||||
.setDescription('Translate text to another language')
|
||||
.addStringOption(option =>
|
||||
option.setName('text')
|
||||
.setDescription('The text to translate')
|
||||
.setRequired(true)
|
||||
.setMaxLength(500)
|
||||
)
|
||||
.addStringOption(option =>
|
||||
option.setName('to')
|
||||
.setDescription('Target language')
|
||||
.setRequired(true)
|
||||
.addChoices(
|
||||
{ name: 'English', value: 'en' },
|
||||
{ name: 'Spanish', value: 'es' },
|
||||
{ name: 'French', value: 'fr' },
|
||||
{ name: 'German', value: 'de' },
|
||||
{ name: 'Italian', value: 'it' },
|
||||
{ name: 'Portuguese', value: 'pt' },
|
||||
{ name: 'Russian', value: 'ru' },
|
||||
{ name: 'Japanese', value: 'ja' },
|
||||
{ name: 'Korean', value: 'ko' },
|
||||
{ name: 'Chinese', value: 'zh' }
|
||||
)
|
||||
),
|
||||
|
||||
async execute(interaction, supabase, client) {
|
||||
const text = interaction.options.getString('text');
|
||||
const targetLang = interaction.options.getString('to');
|
||||
const mode = await getServerMode(supabase, interaction.guildId);
|
||||
|
||||
await interaction.deferReply();
|
||||
|
||||
try {
|
||||
const response = await fetch(`https://api.mymemory.translated.net/get?q=${encodeURIComponent(text)}&langpair=auto|${targetLang}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.responseStatus !== 200 || !data.responseData?.translatedText) {
|
||||
throw new Error('Translation failed');
|
||||
}
|
||||
|
||||
const translated = data.responseData.translatedText;
|
||||
const detectedLang = data.responseData.detectedLanguage || 'auto';
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(getEmbedColor(mode))
|
||||
.setTitle('🌐 Translation')
|
||||
.addFields(
|
||||
{ name: '📥 Original', value: text.substring(0, 1000) },
|
||||
{ name: `📤 ${LANGUAGES[targetLang] || targetLang}`, value: translated.substring(0, 1000) }
|
||||
)
|
||||
.setFooter({ text: `Translated to ${LANGUAGES[targetLang]}` })
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.editReply({ embeds: [embed] });
|
||||
} catch (e) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(EMBED_COLORS.error)
|
||||
.setTitle('🌐 Translation Error')
|
||||
.setDescription('Failed to translate text. Please try again.')
|
||||
.setTimestamp();
|
||||
await interaction.editReply({ embeds: [embed] });
|
||||
}
|
||||
},
|
||||
};
|
||||
283
aethex-bot/commands/trivia.js
Normal file
283
aethex-bot/commands/trivia.js
Normal file
|
|
@ -0,0 +1,283 @@
|
|||
const { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js');
|
||||
const { getServerMode, getEmbedColor, EMBED_COLORS } = require('../utils/modeHelper');
|
||||
const { updateStandaloneXp } = require('../utils/standaloneXp');
|
||||
|
||||
const TRIVIA_QUESTIONS = [
|
||||
{
|
||||
question: "What is the capital of Japan?",
|
||||
answers: ["Tokyo", "Osaka", "Kyoto", "Hiroshima"],
|
||||
correct: 0,
|
||||
category: "Geography"
|
||||
},
|
||||
{
|
||||
question: "Which planet is known as the Red Planet?",
|
||||
answers: ["Venus", "Mars", "Jupiter", "Saturn"],
|
||||
correct: 1,
|
||||
category: "Science"
|
||||
},
|
||||
{
|
||||
question: "What year did World War II end?",
|
||||
answers: ["1943", "1944", "1945", "1946"],
|
||||
correct: 2,
|
||||
category: "History"
|
||||
},
|
||||
{
|
||||
question: "What is the largest mammal on Earth?",
|
||||
answers: ["African Elephant", "Blue Whale", "Giraffe", "Hippopotamus"],
|
||||
correct: 1,
|
||||
category: "Science"
|
||||
},
|
||||
{
|
||||
question: "Who painted the Mona Lisa?",
|
||||
answers: ["Michelangelo", "Leonardo da Vinci", "Raphael", "Donatello"],
|
||||
correct: 1,
|
||||
category: "Art"
|
||||
},
|
||||
{
|
||||
question: "What is the chemical symbol for gold?",
|
||||
answers: ["Go", "Gd", "Au", "Ag"],
|
||||
correct: 2,
|
||||
category: "Science"
|
||||
},
|
||||
{
|
||||
question: "Which programming language was created by Brendan Eich?",
|
||||
answers: ["Python", "Java", "JavaScript", "Ruby"],
|
||||
correct: 2,
|
||||
category: "Technology"
|
||||
},
|
||||
{
|
||||
question: "What is the smallest country in the world?",
|
||||
answers: ["Monaco", "Vatican City", "San Marino", "Liechtenstein"],
|
||||
correct: 1,
|
||||
category: "Geography"
|
||||
},
|
||||
{
|
||||
question: "How many bones are in the adult human body?",
|
||||
answers: ["186", "206", "226", "246"],
|
||||
correct: 1,
|
||||
category: "Science"
|
||||
},
|
||||
{
|
||||
question: "What year was the first iPhone released?",
|
||||
answers: ["2005", "2006", "2007", "2008"],
|
||||
correct: 2,
|
||||
category: "Technology"
|
||||
},
|
||||
{
|
||||
question: "Which element has the atomic number 1?",
|
||||
answers: ["Helium", "Hydrogen", "Oxygen", "Carbon"],
|
||||
correct: 1,
|
||||
category: "Science"
|
||||
},
|
||||
{
|
||||
question: "What is the capital of Australia?",
|
||||
answers: ["Sydney", "Melbourne", "Canberra", "Perth"],
|
||||
correct: 2,
|
||||
category: "Geography"
|
||||
},
|
||||
{
|
||||
question: "Who wrote 'Romeo and Juliet'?",
|
||||
answers: ["Charles Dickens", "William Shakespeare", "Jane Austen", "Mark Twain"],
|
||||
correct: 1,
|
||||
category: "Literature"
|
||||
},
|
||||
{
|
||||
question: "What is the speed of light in km/s (approximately)?",
|
||||
answers: ["150,000", "200,000", "300,000", "400,000"],
|
||||
correct: 2,
|
||||
category: "Science"
|
||||
},
|
||||
{
|
||||
question: "Which company created Discord?",
|
||||
answers: ["Hammer & Chisel", "Meta", "Microsoft", "Google"],
|
||||
correct: 0,
|
||||
category: "Technology"
|
||||
}
|
||||
];
|
||||
|
||||
const activeTrivia = new Map();
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('trivia')
|
||||
.setDescription('Answer trivia questions to earn XP!')
|
||||
.addStringOption(option =>
|
||||
option.setName('category')
|
||||
.setDescription('Choose a category (optional)')
|
||||
.setRequired(false)
|
||||
.addChoices(
|
||||
{ name: 'All', value: 'all' },
|
||||
{ name: 'Science', value: 'Science' },
|
||||
{ name: 'Geography', value: 'Geography' },
|
||||
{ name: 'History', value: 'History' },
|
||||
{ name: 'Technology', value: 'Technology' },
|
||||
{ name: 'Art', value: 'Art' },
|
||||
{ name: 'Literature', value: 'Literature' }
|
||||
)
|
||||
),
|
||||
|
||||
async execute(interaction, supabase, client) {
|
||||
const category = interaction.options.getString('category') || 'all';
|
||||
const mode = await getServerMode(supabase, interaction.guildId);
|
||||
const userId = interaction.user.id;
|
||||
const guildId = interaction.guildId;
|
||||
|
||||
const activeKey = `${guildId}-${userId}`;
|
||||
if (activeTrivia.has(activeKey)) {
|
||||
return interaction.reply({
|
||||
content: 'You already have an active trivia question! Answer it first.',
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
let questions = TRIVIA_QUESTIONS;
|
||||
if (category !== 'all') {
|
||||
questions = TRIVIA_QUESTIONS.filter(q => q.category === category);
|
||||
}
|
||||
|
||||
if (questions.length === 0) {
|
||||
return interaction.reply({
|
||||
content: 'No questions available for that category.',
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
const question = questions[Math.floor(Math.random() * questions.length)];
|
||||
const shuffledIndexes = [0, 1, 2, 3].sort(() => Math.random() - 0.5);
|
||||
const shuffledAnswers = shuffledIndexes.map(i => question.answers[i]);
|
||||
const correctIndex = shuffledIndexes.indexOf(question.correct);
|
||||
|
||||
activeTrivia.set(activeKey, {
|
||||
correctIndex,
|
||||
question: question.question,
|
||||
startTime: Date.now()
|
||||
});
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(getEmbedColor(mode))
|
||||
.setTitle('🧠 Trivia Time!')
|
||||
.setDescription(`**${question.question}**`)
|
||||
.addFields(
|
||||
{ name: '📚 Category', value: question.category, inline: true },
|
||||
{ name: '⏱️ Time Limit', value: '30 seconds', inline: true },
|
||||
{ name: '🎁 Reward', value: '25-50 XP', inline: true }
|
||||
)
|
||||
.setFooter({ text: 'Click a button to answer!' })
|
||||
.setTimestamp();
|
||||
|
||||
const row = new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId(`trivia_0_${interaction.user.id}`)
|
||||
.setLabel(shuffledAnswers[0])
|
||||
.setStyle(ButtonStyle.Primary),
|
||||
new ButtonBuilder()
|
||||
.setCustomId(`trivia_1_${interaction.user.id}`)
|
||||
.setLabel(shuffledAnswers[1])
|
||||
.setStyle(ButtonStyle.Primary),
|
||||
new ButtonBuilder()
|
||||
.setCustomId(`trivia_2_${interaction.user.id}`)
|
||||
.setLabel(shuffledAnswers[2])
|
||||
.setStyle(ButtonStyle.Primary),
|
||||
new ButtonBuilder()
|
||||
.setCustomId(`trivia_3_${interaction.user.id}`)
|
||||
.setLabel(shuffledAnswers[3])
|
||||
.setStyle(ButtonStyle.Primary)
|
||||
);
|
||||
|
||||
const message = await interaction.reply({ embeds: [embed], components: [row], fetchReply: true });
|
||||
|
||||
const collector = message.createMessageComponentCollector({
|
||||
filter: i => i.customId.startsWith('trivia_') && i.customId.endsWith(`_${interaction.user.id}`),
|
||||
time: 30000,
|
||||
max: 1
|
||||
});
|
||||
|
||||
collector.on('collect', async (i) => {
|
||||
const triviaData = activeTrivia.get(activeKey);
|
||||
if (!triviaData) return;
|
||||
|
||||
const selectedIndex = parseInt(i.customId.split('_')[1]);
|
||||
const isCorrect = selectedIndex === triviaData.correctIndex;
|
||||
const timeTaken = (Date.now() - triviaData.startTime) / 1000;
|
||||
|
||||
activeTrivia.delete(activeKey);
|
||||
|
||||
let xpReward = 0;
|
||||
if (isCorrect) {
|
||||
xpReward = timeTaken < 5 ? 50 : timeTaken < 15 ? 35 : 25;
|
||||
|
||||
if (mode === 'standalone') {
|
||||
await updateStandaloneXp(supabase, userId, guildId, xpReward, interaction.user.username);
|
||||
} else if (supabase) {
|
||||
try {
|
||||
const { data: profile } = await supabase
|
||||
.from('user_profiles')
|
||||
.select('xp')
|
||||
.eq('discord_id', userId)
|
||||
.maybeSingle();
|
||||
|
||||
if (profile) {
|
||||
await supabase
|
||||
.from('user_profiles')
|
||||
.update({ xp: (profile.xp || 0) + xpReward })
|
||||
.eq('discord_id', userId);
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
const resultEmbed = new EmbedBuilder()
|
||||
.setColor(isCorrect ? EMBED_COLORS.success : EMBED_COLORS.error)
|
||||
.setTitle(isCorrect ? '✅ Correct!' : '❌ Incorrect!')
|
||||
.setDescription(isCorrect
|
||||
? `Great job! You answered in ${timeTaken.toFixed(1)} seconds.`
|
||||
: `The correct answer was: **${question.answers[question.correct]}**`
|
||||
)
|
||||
.addFields(
|
||||
{ name: '❓ Question', value: question.question }
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
if (isCorrect) {
|
||||
resultEmbed.addFields({ name: '🎁 XP Earned', value: `+${xpReward} XP`, inline: true });
|
||||
}
|
||||
|
||||
const disabledRow = new ActionRowBuilder()
|
||||
.addComponents(
|
||||
...row.components.map((btn, idx) =>
|
||||
ButtonBuilder.from(btn)
|
||||
.setStyle(idx === triviaData.correctIndex ? ButtonStyle.Success :
|
||||
(idx === selectedIndex && !isCorrect) ? ButtonStyle.Danger :
|
||||
ButtonStyle.Secondary)
|
||||
.setDisabled(true)
|
||||
)
|
||||
);
|
||||
|
||||
await i.update({ embeds: [resultEmbed], components: [disabledRow] });
|
||||
});
|
||||
|
||||
collector.on('end', async (collected) => {
|
||||
if (collected.size === 0) {
|
||||
activeTrivia.delete(activeKey);
|
||||
|
||||
const timeoutEmbed = new EmbedBuilder()
|
||||
.setColor(EMBED_COLORS.warning)
|
||||
.setTitle('⏰ Time\'s Up!')
|
||||
.setDescription(`You didn't answer in time. The correct answer was: **${question.answers[question.correct]}**`)
|
||||
.setTimestamp();
|
||||
|
||||
const disabledRow = new ActionRowBuilder()
|
||||
.addComponents(
|
||||
...row.components.map(btn =>
|
||||
ButtonBuilder.from(btn)
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
.setDisabled(true)
|
||||
)
|
||||
);
|
||||
|
||||
await interaction.editReply({ embeds: [timeoutEmbed], components: [disabledRow] });
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
76
aethex-bot/commands/work.js
Normal file
76
aethex-bot/commands/work.js
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
|
||||
const { getServerMode, getEmbedColor, EMBED_COLORS } = require('../utils/modeHelper');
|
||||
const { updateStandaloneXp } = require('../utils/standaloneXp');
|
||||
|
||||
const JOBS = [
|
||||
{ name: 'Developer', emoji: '💻', minXp: 15, maxXp: 35, message: 'You wrote some clean code' },
|
||||
{ name: 'Designer', emoji: '🎨', minXp: 12, maxXp: 30, message: 'You created a beautiful design' },
|
||||
{ name: 'Writer', emoji: '✍️', minXp: 10, maxXp: 25, message: 'You wrote an engaging article' },
|
||||
{ name: 'Streamer', emoji: '📺', minXp: 18, maxXp: 40, message: 'Your stream was a hit' },
|
||||
{ name: 'Chef', emoji: '👨🍳', minXp: 8, maxXp: 22, message: 'You cooked a delicious meal' },
|
||||
{ name: 'Teacher', emoji: '👨🏫', minXp: 12, maxXp: 28, message: 'You helped students learn' },
|
||||
{ name: 'Artist', emoji: '🖼️', minXp: 14, maxXp: 32, message: 'You created a masterpiece' },
|
||||
{ name: 'Musician', emoji: '🎵', minXp: 10, maxXp: 26, message: 'You performed an amazing song' },
|
||||
{ name: 'Photographer', emoji: '📷', minXp: 11, maxXp: 24, message: 'You captured a perfect shot' },
|
||||
{ name: 'YouTuber', emoji: '🎬', minXp: 16, maxXp: 38, message: 'Your video went viral' },
|
||||
];
|
||||
|
||||
const workCooldowns = new Map();
|
||||
const COOLDOWN = 60 * 60 * 1000;
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('work')
|
||||
.setDescription('Work to earn some XP!'),
|
||||
|
||||
async execute(interaction, supabase, client) {
|
||||
const userId = interaction.user.id;
|
||||
const guildId = interaction.guildId;
|
||||
const mode = await getServerMode(supabase, interaction.guildId);
|
||||
|
||||
const cooldownKey = `${guildId}-${userId}`;
|
||||
const lastWork = workCooldowns.get(cooldownKey);
|
||||
if (lastWork && Date.now() - lastWork < COOLDOWN) {
|
||||
const remaining = COOLDOWN - (Date.now() - lastWork);
|
||||
const minutes = Math.floor(remaining / 60000);
|
||||
const seconds = Math.floor((remaining % 60000) / 1000);
|
||||
return interaction.reply({
|
||||
content: `You're tired! You can work again in ${minutes}m ${seconds}s.`,
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
workCooldowns.set(cooldownKey, Date.now());
|
||||
|
||||
const job = JOBS[Math.floor(Math.random() * JOBS.length)];
|
||||
const xpEarned = Math.floor(Math.random() * (job.maxXp - job.minXp + 1)) + job.minXp;
|
||||
|
||||
if (mode === 'standalone') {
|
||||
await updateStandaloneXp(supabase, userId, guildId, xpEarned, interaction.user.username);
|
||||
} else if (supabase) {
|
||||
try {
|
||||
const { data: profile } = await supabase
|
||||
.from('user_profiles')
|
||||
.select('xp')
|
||||
.eq('discord_id', userId)
|
||||
.maybeSingle();
|
||||
|
||||
if (profile) {
|
||||
await supabase
|
||||
.from('user_profiles')
|
||||
.update({ xp: (profile.xp || 0) + xpEarned })
|
||||
.eq('discord_id', userId);
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(EMBED_COLORS.success)
|
||||
.setTitle(`${job.emoji} ${job.name}`)
|
||||
.setDescription(`${job.message} and earned **${xpEarned} XP**!`)
|
||||
.setFooter({ text: `${interaction.user.username} | Work again in 1 hour` })
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.reply({ embeds: [embed] });
|
||||
},
|
||||
};
|
||||
183
aethex-bot/events/guildSetup.js
Normal file
183
aethex-bot/events/guildSetup.js
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, PermissionFlagsBits } = require('discord.js');
|
||||
const { setServerMode, EMBED_COLORS } = require('../utils/modeHelper');
|
||||
|
||||
module.exports = {
|
||||
name: 'guildCreate',
|
||||
|
||||
async execute(guild, client, supabase) {
|
||||
if (!supabase) return;
|
||||
|
||||
const { data: existingConfig } = await supabase
|
||||
.from('server_config')
|
||||
.select('mode')
|
||||
.eq('guild_id', guild.id)
|
||||
.maybeSingle();
|
||||
|
||||
if (existingConfig?.mode) {
|
||||
return;
|
||||
}
|
||||
|
||||
let targetChannel = null;
|
||||
|
||||
if (guild.systemChannel && guild.systemChannel.permissionsFor(guild.members.me)?.has(PermissionFlagsBits.SendMessages)) {
|
||||
targetChannel = guild.systemChannel;
|
||||
}
|
||||
|
||||
if (!targetChannel) {
|
||||
const channels = guild.channels.cache
|
||||
.filter(c =>
|
||||
c.type === 0 &&
|
||||
c.permissionsFor(guild.members.me)?.has(PermissionFlagsBits.SendMessages)
|
||||
)
|
||||
.sort((a, b) => a.position - b.position);
|
||||
|
||||
targetChannel = channels.first();
|
||||
}
|
||||
|
||||
if (!targetChannel) {
|
||||
try {
|
||||
const owner = await guild.fetchOwner();
|
||||
await sendSetupDM(owner, guild, client, supabase);
|
||||
} catch (e) {
|
||||
console.log(`[Setup] Could not send setup prompt to ${guild.name}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await sendSetupEmbed(targetChannel, guild, client, supabase);
|
||||
}
|
||||
};
|
||||
|
||||
async function sendSetupEmbed(channel, guild, client, supabase) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(EMBED_COLORS.federated)
|
||||
.setTitle('⚔️ Welcome to Warden!')
|
||||
.setDescription(
|
||||
`Thanks for adding **Warden** to **${guild.name}**!\n\n` +
|
||||
`Choose how you want to run Warden:`
|
||||
)
|
||||
.addFields(
|
||||
{
|
||||
name: '🌐 Join the Federation',
|
||||
value:
|
||||
'• Unified XP across all AeThex servers\n' +
|
||||
'• Cross-server profiles and leaderboards\n' +
|
||||
'• Realm selection and role sync\n' +
|
||||
'• Part of the AeThex ecosystem',
|
||||
inline: true
|
||||
},
|
||||
{
|
||||
name: '🏠 Run Standalone',
|
||||
value:
|
||||
'• Isolated XP system for this server only\n' +
|
||||
'• Local leaderboards and profiles\n' +
|
||||
'• Full moderation and anti-nuke\n' +
|
||||
'• Independent from other servers',
|
||||
inline: true
|
||||
}
|
||||
)
|
||||
.setFooter({ text: 'You can change this later with /config mode' })
|
||||
.setTimestamp();
|
||||
|
||||
const row = new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId('setup_federated')
|
||||
.setLabel('Join Federation')
|
||||
.setStyle(ButtonStyle.Primary)
|
||||
.setEmoji('🌐'),
|
||||
new ButtonBuilder()
|
||||
.setCustomId('setup_standalone')
|
||||
.setLabel('Run Standalone')
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
.setEmoji('🏠')
|
||||
);
|
||||
|
||||
try {
|
||||
const message = await channel.send({ embeds: [embed], components: [row] });
|
||||
|
||||
const collector = message.createMessageComponentCollector({
|
||||
filter: (i) => {
|
||||
return i.member.permissions.has(PermissionFlagsBits.Administrator) ||
|
||||
i.member.id === guild.ownerId;
|
||||
},
|
||||
time: 86400000
|
||||
});
|
||||
|
||||
collector.on('collect', async (interaction) => {
|
||||
const mode = interaction.customId === 'setup_federated' ? 'federated' : 'standalone';
|
||||
|
||||
const success = await setServerMode(supabase, guild.id, mode);
|
||||
|
||||
if (success) {
|
||||
const confirmEmbed = new EmbedBuilder()
|
||||
.setColor(mode === 'federated' ? EMBED_COLORS.federated : EMBED_COLORS.standalone)
|
||||
.setTitle(mode === 'federated' ? '🌐 Federation Mode Activated!' : '🏠 Standalone Mode Activated!')
|
||||
.setDescription(
|
||||
mode === 'federated'
|
||||
? `**${guild.name}** is now part of the AeThex Federation!\n\n` +
|
||||
'• XP earned here counts globally\n' +
|
||||
'• Use `/verify` to link your AeThex account\n' +
|
||||
'• Use `/set-realm` to choose your realm\n' +
|
||||
'• Use `/help` to see all commands'
|
||||
: `**${guild.name}** is now running in Standalone mode!\n\n` +
|
||||
'• XP is tracked locally for this server\n' +
|
||||
'• Use `/rank` to check your server level\n' +
|
||||
'• Use `/leaderboard` to see top members\n' +
|
||||
'• Use `/help` to see all commands'
|
||||
)
|
||||
.setFooter({ text: 'Change mode anytime with /config mode' })
|
||||
.setTimestamp();
|
||||
|
||||
await interaction.update({ embeds: [confirmEmbed], components: [] });
|
||||
collector.stop();
|
||||
} else {
|
||||
await interaction.reply({
|
||||
content: 'Failed to set mode. Please try `/config mode` later.',
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
collector.on('end', async (collected, reason) => {
|
||||
if (reason === 'time' && collected.size === 0) {
|
||||
await setServerMode(supabase, guild.id, 'federated');
|
||||
|
||||
const timeoutEmbed = new EmbedBuilder()
|
||||
.setColor(EMBED_COLORS.federated)
|
||||
.setTitle('🌐 Federation Mode (Default)')
|
||||
.setDescription(
|
||||
`No selection was made, so **${guild.name}** has been set to Federation mode by default.\n\n` +
|
||||
'Use `/config mode` to change this anytime.'
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
await message.edit({ embeds: [timeoutEmbed], components: [] }).catch(() => {});
|
||||
}
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
console.error('[Setup] Error sending setup embed:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function sendSetupDM(owner, guild, client, supabase) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(EMBED_COLORS.federated)
|
||||
.setTitle('⚔️ Warden Setup Required')
|
||||
.setDescription(
|
||||
`Thanks for adding **Warden** to **${guild.name}**!\n\n` +
|
||||
`I couldn't find a channel to send the setup prompt. Please use \`/config mode\` in your server to choose:\n\n` +
|
||||
'**🌐 Federation** - Join the AeThex ecosystem with unified XP\n' +
|
||||
'**🏠 Standalone** - Independent XP for your server only\n\n' +
|
||||
'Until you choose, Federation mode is enabled by default.'
|
||||
)
|
||||
.setTimestamp();
|
||||
|
||||
try {
|
||||
await owner.send({ embeds: [embed] });
|
||||
await setServerMode(supabase, guild.id, 'federated');
|
||||
} catch (e) {
|
||||
await setServerMode(supabase, guild.id, 'federated');
|
||||
}
|
||||
}
|
||||
|
|
@ -162,10 +162,17 @@ async function syncMessageToFeed(message) {
|
|||
module.exports = {
|
||||
name: "messageCreate",
|
||||
async execute(message, client) {
|
||||
if (!supabase) return;
|
||||
|
||||
if (message.author.bot) return;
|
||||
|
||||
try {
|
||||
const afkCommand = require('../commands/afk');
|
||||
if (afkCommand && afkCommand.checkAfk) {
|
||||
afkCommand.checkAfk(message);
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
if (!supabase) return;
|
||||
|
||||
if (!message.content && message.attachments.size === 0) return;
|
||||
|
||||
if (!FEED_CHANNEL_ID) {
|
||||
|
|
|
|||
127
aethex-bot/listeners/starboard.js
Normal file
127
aethex-bot/listeners/starboard.js
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
const { EmbedBuilder } = require('discord.js');
|
||||
const { createClient } = require('@supabase/supabase-js');
|
||||
|
||||
let supabase = null;
|
||||
if (process.env.SUPABASE_URL && process.env.SUPABASE_SERVICE_ROLE) {
|
||||
supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_SERVICE_ROLE);
|
||||
}
|
||||
|
||||
const starboardCache = new Map();
|
||||
const STAR_EMOJI = '⭐';
|
||||
const DEFAULT_THRESHOLD = 3;
|
||||
|
||||
async function getStarboardConfig(guildId) {
|
||||
if (!supabase) return null;
|
||||
|
||||
const cached = starboardCache.get(guildId);
|
||||
if (cached && Date.now() - cached.timestamp < 60000) {
|
||||
return cached.config;
|
||||
}
|
||||
|
||||
try {
|
||||
const { data } = await supabase
|
||||
.from('starboard_config')
|
||||
.select('*')
|
||||
.eq('guild_id', guildId)
|
||||
.maybeSingle();
|
||||
|
||||
starboardCache.set(guildId, { config: data, timestamp: Date.now() });
|
||||
return data;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleStarboard(reaction, user) {
|
||||
if (reaction.emoji.name !== STAR_EMOJI) return;
|
||||
if (reaction.message.author?.bot) return;
|
||||
|
||||
const guildId = reaction.message.guildId;
|
||||
const config = await getStarboardConfig(guildId);
|
||||
|
||||
if (!config || !config.enabled || !config.channel_id) return;
|
||||
|
||||
const threshold = config.threshold || DEFAULT_THRESHOLD;
|
||||
const starCount = reaction.count;
|
||||
|
||||
if (starCount < threshold) return;
|
||||
|
||||
const message = reaction.message;
|
||||
const starboardChannel = message.guild.channels.cache.get(config.channel_id);
|
||||
|
||||
if (!starboardChannel) return;
|
||||
|
||||
try {
|
||||
const { data: existing } = await supabase
|
||||
.from('starboard_messages')
|
||||
.select('starboard_message_id')
|
||||
.eq('original_message_id', message.id)
|
||||
.maybeSingle();
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0xFFD700)
|
||||
.setAuthor({
|
||||
name: message.author.tag,
|
||||
iconURL: message.author.displayAvatarURL({ size: 128 })
|
||||
})
|
||||
.setDescription(message.content || '*No text content*')
|
||||
.addFields(
|
||||
{ name: '📍 Source', value: `[Jump to message](${message.url})`, inline: true },
|
||||
{ name: '⭐ Stars', value: `${starCount}`, inline: true }
|
||||
)
|
||||
.setTimestamp(message.createdAt)
|
||||
.setFooter({ text: `#${message.channel.name}` });
|
||||
|
||||
if (message.attachments.size > 0) {
|
||||
const attachment = message.attachments.first();
|
||||
if (attachment.contentType?.startsWith('image/')) {
|
||||
embed.setImage(attachment.url);
|
||||
}
|
||||
}
|
||||
|
||||
const starEmoji = starCount >= 10 ? '🌟' : starCount >= 5 ? '✨' : '⭐';
|
||||
const content = `${starEmoji} **${starCount}** | <#${message.channelId}>`;
|
||||
|
||||
if (existing?.starboard_message_id) {
|
||||
try {
|
||||
const starboardMessage = await starboardChannel.messages.fetch(existing.starboard_message_id);
|
||||
await starboardMessage.edit({ content, embeds: [embed] });
|
||||
} catch (e) {}
|
||||
} else {
|
||||
const starboardMessage = await starboardChannel.send({ content, embeds: [embed] });
|
||||
|
||||
await supabase.from('starboard_messages').insert({
|
||||
guild_id: guildId,
|
||||
original_message_id: message.id,
|
||||
starboard_message_id: starboardMessage.id,
|
||||
channel_id: message.channelId,
|
||||
author_id: message.author.id,
|
||||
star_count: starCount
|
||||
});
|
||||
}
|
||||
|
||||
if (existing) {
|
||||
await supabase
|
||||
.from('starboard_messages')
|
||||
.update({ star_count: starCount })
|
||||
.eq('original_message_id', message.id);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Starboard] Error:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
name: 'messageReactionAdd',
|
||||
async execute(reaction, user, client) {
|
||||
if (reaction.partial) {
|
||||
try {
|
||||
await reaction.fetch();
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await handleStarboard(reaction, user);
|
||||
}
|
||||
};
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
const { EmbedBuilder } = require('discord.js');
|
||||
const { checkAchievements } = require('../commands/achievements');
|
||||
const { getServerMode, getEmbedColor } = require('../utils/modeHelper');
|
||||
const { updateStandaloneXp, calculateLevel: standaloneCalcLevel } = require('../utils/standaloneXp');
|
||||
|
||||
const xpCooldowns = new Map();
|
||||
const xpConfigCache = new Map();
|
||||
|
|
@ -103,6 +105,16 @@ module.exports = {
|
|||
|
||||
if (now - lastXp < cooldownMs) return;
|
||||
|
||||
// Check server mode
|
||||
const serverMode = await getServerMode(supabase, guildId);
|
||||
|
||||
// Handle standalone mode separately
|
||||
if (serverMode === 'standalone') {
|
||||
await handleStandaloneXp(message, client, supabase, config, discordUserId, guildId, channelId, now);
|
||||
return;
|
||||
}
|
||||
|
||||
// FEDERATED MODE - requires linked account
|
||||
try {
|
||||
const { data: link, error: linkError } = await supabase
|
||||
.from('discord_links')
|
||||
|
|
@ -537,6 +549,201 @@ async function upsertPeriodXp(supabase, userId, guildId, discordId, periodType,
|
|||
}
|
||||
}
|
||||
|
||||
// STANDALONE MODE XP HANDLER
|
||||
async function handleStandaloneXp(message, client, supabase, config, discordUserId, guildId, channelId, now) {
|
||||
try {
|
||||
// Get existing standalone XP data
|
||||
const { data: existingXp } = await supabase
|
||||
.from('guild_user_xp')
|
||||
.select('*')
|
||||
.eq('discord_id', discordUserId)
|
||||
.eq('guild_id', guildId)
|
||||
.maybeSingle();
|
||||
|
||||
// Calculate base XP
|
||||
let xpGain = config.message_xp || 5;
|
||||
const prestige = existingXp?.prestige_level || 0;
|
||||
|
||||
// Apply channel bonus
|
||||
const bonusChannels = config.bonus_channels || [];
|
||||
const channelBonus = bonusChannels.find(c => c.channel_id === channelId);
|
||||
if (channelBonus) {
|
||||
xpGain = Math.floor(xpGain * channelBonus.multiplier);
|
||||
}
|
||||
|
||||
// Apply role multipliers (use highest)
|
||||
const multiplierRoles = config.multiplier_roles || [];
|
||||
let highestMultiplier = 1;
|
||||
for (const mr of multiplierRoles) {
|
||||
if (message.member?.roles.cache.has(mr.role_id)) {
|
||||
highestMultiplier = Math.max(highestMultiplier, mr.multiplier);
|
||||
}
|
||||
}
|
||||
|
||||
// Server boosters get 1.5x XP bonus automatically
|
||||
if (message.member?.premiumSince) {
|
||||
highestMultiplier = Math.max(highestMultiplier, 1.5);
|
||||
}
|
||||
|
||||
xpGain = Math.floor(xpGain * highestMultiplier);
|
||||
|
||||
// Apply prestige bonus (+5% per prestige level)
|
||||
if (prestige > 0) {
|
||||
const prestigeBonus = 1 + (prestige * 0.05);
|
||||
xpGain = Math.floor(xpGain * prestigeBonus);
|
||||
}
|
||||
|
||||
const currentXp = existingXp?.xp || 0;
|
||||
const newXp = currentXp + xpGain;
|
||||
|
||||
// Calculate levels
|
||||
const oldLevel = standaloneCalcLevel(currentXp, config.level_curve);
|
||||
const newLevel = standaloneCalcLevel(newXp, config.level_curve);
|
||||
|
||||
// Update or create standalone XP record
|
||||
const result = await updateStandaloneXp(supabase, discordUserId, guildId, xpGain, message.author.username);
|
||||
|
||||
if (!result) return;
|
||||
|
||||
// Update cooldown
|
||||
const cooldownKey = `${guildId}:${discordUserId}`;
|
||||
xpCooldowns.set(cooldownKey, now);
|
||||
|
||||
// Track XP for analytics
|
||||
if (client.trackXP) {
|
||||
client.trackXP(xpGain);
|
||||
}
|
||||
|
||||
// Level up announcement for standalone
|
||||
if (newLevel > oldLevel) {
|
||||
await sendStandaloneLevelUp(message, newLevel, newXp, config, client);
|
||||
await checkMilestoneRolesStandalone(message.member, {
|
||||
level: newLevel,
|
||||
prestige: prestige,
|
||||
totalXp: result.total_xp_earned || newXp
|
||||
}, supabase, guildId);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Standalone XP tracking error:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function sendStandaloneLevelUp(message, newLevel, newXp, config, client) {
|
||||
try {
|
||||
const messageTemplate = config.levelup_message || '🎉 Congratulations {user}! You reached **Level {level}**!';
|
||||
const channelId = config.levelup_channel_id;
|
||||
const sendDm = config.levelup_dm === true;
|
||||
const useEmbed = config.levelup_embed === true;
|
||||
const embedColor = '#6B7280'; // Standalone gray color
|
||||
|
||||
const formattedMessage = messageTemplate
|
||||
.replace(/{user}/g, message.author.toString())
|
||||
.replace(/{username}/g, message.author.username)
|
||||
.replace(/{level}/g, newLevel.toString())
|
||||
.replace(/{xp}/g, newXp.toLocaleString())
|
||||
.replace(/{server}/g, message.guild.name);
|
||||
|
||||
let messageContent;
|
||||
|
||||
if (useEmbed) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setDescription(formattedMessage)
|
||||
.setColor(parseInt(embedColor.replace('#', ''), 16))
|
||||
.setThumbnail(message.author.displayAvatarURL({ dynamic: true }))
|
||||
.setFooter({ text: '🏠 Standalone Mode' })
|
||||
.setTimestamp();
|
||||
|
||||
messageContent = { embeds: [embed] };
|
||||
} else {
|
||||
messageContent = { content: formattedMessage };
|
||||
}
|
||||
|
||||
if (sendDm) {
|
||||
const dmSent = await message.author.send(messageContent).catch(() => null);
|
||||
if (!dmSent) {
|
||||
await message.channel.send(messageContent).catch(() => {});
|
||||
}
|
||||
} else if (channelId) {
|
||||
const channel = await client.channels.fetch(channelId).catch(() => null);
|
||||
if (channel) {
|
||||
await channel.send(messageContent).catch(() => {});
|
||||
} else {
|
||||
await message.channel.send(messageContent).catch(() => {});
|
||||
}
|
||||
} else {
|
||||
await message.channel.send(messageContent).catch(() => {});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Standalone level-up announcement error:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function checkMilestoneRolesStandalone(member, milestones, supabase, guildId) {
|
||||
if (!member || !supabase) return;
|
||||
|
||||
try {
|
||||
const { data: allRoles, error } = await supabase
|
||||
.from('level_roles')
|
||||
.select('*')
|
||||
.eq('guild_id', guildId);
|
||||
|
||||
if (error || !allRoles || allRoles.length === 0) return;
|
||||
|
||||
const rolesToAdd = [];
|
||||
const rolesToRemove = [];
|
||||
|
||||
for (const roleConfig of allRoles) {
|
||||
const { role_id, milestone_type, milestone_value, stack_roles } = roleConfig;
|
||||
let qualifies = false;
|
||||
|
||||
switch (milestone_type) {
|
||||
case 'level':
|
||||
qualifies = milestones.level >= milestone_value;
|
||||
break;
|
||||
case 'prestige':
|
||||
qualifies = milestones.prestige >= milestone_value;
|
||||
break;
|
||||
case 'total_xp':
|
||||
qualifies = milestones.totalXp >= milestone_value;
|
||||
break;
|
||||
}
|
||||
|
||||
if (qualifies && !member.roles.cache.has(role_id)) {
|
||||
rolesToAdd.push({ role_id, milestone_type, milestone_value, stack_roles });
|
||||
}
|
||||
}
|
||||
|
||||
for (const roleToAdd of rolesToAdd) {
|
||||
try {
|
||||
await member.roles.add(roleToAdd.role_id);
|
||||
|
||||
if (!roleToAdd.stack_roles) {
|
||||
const sameTypeRoles = allRoles.filter(r =>
|
||||
r.milestone_type === roleToAdd.milestone_type &&
|
||||
r.milestone_value < roleToAdd.milestone_value &&
|
||||
member.roles.cache.has(r.role_id)
|
||||
);
|
||||
|
||||
for (const oldRole of sameTypeRoles) {
|
||||
if (!rolesToRemove.includes(oldRole.role_id)) {
|
||||
rolesToRemove.push(oldRole.role_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Role addition failed
|
||||
}
|
||||
}
|
||||
|
||||
for (const roleId of rolesToRemove) {
|
||||
await member.roles.remove(roleId).catch(() => {});
|
||||
}
|
||||
} catch (e) {
|
||||
// Silently ignore
|
||||
}
|
||||
}
|
||||
|
||||
// Export functions for use in other commands
|
||||
module.exports.calculateLevel = calculateLevel;
|
||||
module.exports.checkMilestoneRoles = checkMilestoneRoles;
|
||||
|
|
|
|||
|
|
@ -1324,7 +1324,7 @@
|
|||
<div class="stat-label">Unified Profiles</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-value" id="commandCount">44+</div>
|
||||
<div class="stat-value" id="commandCount">60+</div>
|
||||
<div class="stat-label">Commands</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
|
|
@ -1597,6 +1597,126 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fun & Games Features -->
|
||||
<div class="feature-category">
|
||||
<div class="category-header">
|
||||
<div class="category-icon">🎮</div>
|
||||
<h3>Fun & Games</h3>
|
||||
</div>
|
||||
<div class="features-grid">
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">🎱</div>
|
||||
<h3>8-Ball & Fortune</h3>
|
||||
<p>Ask the magic 8-ball questions, flip coins, and roll dice with custom notation.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">🧠</div>
|
||||
<h3>Trivia</h3>
|
||||
<p>Multiple categories, earn XP for correct answers. Test your knowledge daily.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">⚔️</div>
|
||||
<h3>Duels</h3>
|
||||
<p>Challenge others to 1v1 battles. Bet XP on the outcome for extra rewards.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">🎰</div>
|
||||
<h3>Slot Machine</h3>
|
||||
<p>Try your luck at slots. Match symbols for XP jackpots and winning streaks.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Social Features -->
|
||||
<div class="feature-category">
|
||||
<div class="category-header">
|
||||
<div class="category-icon">❤️</div>
|
||||
<h3>Social & Interaction</h3>
|
||||
</div>
|
||||
<div class="features-grid">
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">⭐</div>
|
||||
<h3>Reputation</h3>
|
||||
<p>Give and receive rep points. Build your community standing over time.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">🤗</div>
|
||||
<h3>Social Actions</h3>
|
||||
<p>Hugs, high-fives, and more with animated GIFs. Express yourself!</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">🎂</div>
|
||||
<h3>Birthdays</h3>
|
||||
<p>Set your birthday, view upcoming celebrations, get special recognition.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">⏰</div>
|
||||
<h3>Reminders</h3>
|
||||
<p>Set personal reminders. Never forget important events or tasks.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Economy Features -->
|
||||
<div class="feature-category">
|
||||
<div class="category-header">
|
||||
<div class="category-icon">💰</div>
|
||||
<h3>Economy & Trading</h3>
|
||||
</div>
|
||||
<div class="features-grid">
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">💼</div>
|
||||
<h3>Work System</h3>
|
||||
<p>Work hourly for XP rewards. Different jobs with varying payouts.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">🏦</div>
|
||||
<h3>Heists</h3>
|
||||
<p>Team up for group heists. Higher risk, higher rewards. Strategy matters.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">🎁</div>
|
||||
<h3>Gifting</h3>
|
||||
<p>Gift XP to friends and community members. Spread the wealth.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">🔄</div>
|
||||
<h3>Trading</h3>
|
||||
<p>Trade items between users. Full inventory system with secure trades.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Utility Features -->
|
||||
<div class="feature-category">
|
||||
<div class="category-header">
|
||||
<div class="category-icon">🔧</div>
|
||||
<h3>Utility Tools</h3>
|
||||
</div>
|
||||
<div class="features-grid">
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">🌐</div>
|
||||
<h3>Translation</h3>
|
||||
<p>Translate text between 100+ languages instantly. Break language barriers.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">📖</div>
|
||||
<h3>Definitions</h3>
|
||||
<p>Look up word definitions, synonyms, and usage examples.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">🔢</div>
|
||||
<h3>Calculator</h3>
|
||||
<p>Safe math expression evaluator. Complex calculations made easy.</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">📱</div>
|
||||
<h3>QR Codes</h3>
|
||||
<p>Generate QR codes for any text or URL. Share links instantly.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
|
|
|||
105
aethex-bot/utils/modeHelper.js
Normal file
105
aethex-bot/utils/modeHelper.js
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
const { EmbedBuilder } = require('discord.js');
|
||||
|
||||
const EMBED_COLORS = {
|
||||
federated: 0x4A90E2,
|
||||
standalone: 0x6B7280,
|
||||
success: 0x22C55E,
|
||||
error: 0xEF4444,
|
||||
warning: 0xF59E0B,
|
||||
};
|
||||
|
||||
const modeConfigCache = new Map();
|
||||
const MODE_CACHE_TTL = 60000;
|
||||
|
||||
async function getServerMode(supabase, guildId) {
|
||||
if (!supabase) return 'standalone';
|
||||
|
||||
const now = Date.now();
|
||||
const cached = modeConfigCache.get(guildId);
|
||||
|
||||
if (cached && (now - cached.timestamp < MODE_CACHE_TTL)) {
|
||||
return cached.mode;
|
||||
}
|
||||
|
||||
try {
|
||||
const { data } = await supabase
|
||||
.from('server_config')
|
||||
.select('mode')
|
||||
.eq('guild_id', guildId)
|
||||
.maybeSingle();
|
||||
|
||||
const mode = data?.mode || 'federated';
|
||||
modeConfigCache.set(guildId, { mode, timestamp: now });
|
||||
return mode;
|
||||
} catch (e) {
|
||||
return 'federated';
|
||||
}
|
||||
}
|
||||
|
||||
async function setServerMode(supabase, guildId, mode) {
|
||||
if (!supabase) return false;
|
||||
|
||||
try {
|
||||
await supabase.from('server_config').upsert({
|
||||
guild_id: guildId,
|
||||
mode: mode,
|
||||
mode_changed_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
}, { onConflict: 'guild_id' });
|
||||
|
||||
modeConfigCache.set(guildId, { mode, timestamp: Date.now() });
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error('Failed to set server mode:', e.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function clearModeCache(guildId) {
|
||||
if (guildId) {
|
||||
modeConfigCache.delete(guildId);
|
||||
} else {
|
||||
modeConfigCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
function getEmbedColor(mode) {
|
||||
return mode === 'standalone' ? EMBED_COLORS.standalone : EMBED_COLORS.federated;
|
||||
}
|
||||
|
||||
function createModeEmbed(mode, title, description) {
|
||||
return new EmbedBuilder()
|
||||
.setColor(getEmbedColor(mode))
|
||||
.setTitle(title)
|
||||
.setDescription(description)
|
||||
.setTimestamp();
|
||||
}
|
||||
|
||||
function isFederated(mode) {
|
||||
return mode === 'federated';
|
||||
}
|
||||
|
||||
function isStandalone(mode) {
|
||||
return mode === 'standalone';
|
||||
}
|
||||
|
||||
function getModeDisplayName(mode) {
|
||||
return mode === 'federated' ? 'Federation' : 'Standalone';
|
||||
}
|
||||
|
||||
function getModeEmoji(mode) {
|
||||
return mode === 'federated' ? '🌐' : '🏠';
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
EMBED_COLORS,
|
||||
getServerMode,
|
||||
setServerMode,
|
||||
clearModeCache,
|
||||
getEmbedColor,
|
||||
createModeEmbed,
|
||||
isFederated,
|
||||
isStandalone,
|
||||
getModeDisplayName,
|
||||
getModeEmoji,
|
||||
};
|
||||
230
aethex-bot/utils/standaloneXp.js
Normal file
230
aethex-bot/utils/standaloneXp.js
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
const { EmbedBuilder } = require('discord.js');
|
||||
const { getEmbedColor } = require('./modeHelper');
|
||||
|
||||
async function getStandaloneXp(supabase, discordId, guildId) {
|
||||
if (!supabase) return null;
|
||||
|
||||
try {
|
||||
const { data } = await supabase
|
||||
.from('guild_user_xp')
|
||||
.select('*')
|
||||
.eq('discord_id', discordId)
|
||||
.eq('guild_id', guildId)
|
||||
.maybeSingle();
|
||||
|
||||
return data || null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function updateStandaloneXp(supabase, discordId, guildId, xpGain, username) {
|
||||
if (!supabase) return null;
|
||||
|
||||
try {
|
||||
const { data: existing } = await supabase
|
||||
.from('guild_user_xp')
|
||||
.select('*')
|
||||
.eq('discord_id', discordId)
|
||||
.eq('guild_id', guildId)
|
||||
.maybeSingle();
|
||||
|
||||
if (existing) {
|
||||
const newXp = (existing.xp || 0) + xpGain;
|
||||
const totalEarned = (existing.total_xp_earned || 0) + xpGain;
|
||||
|
||||
const { data } = await supabase
|
||||
.from('guild_user_xp')
|
||||
.update({
|
||||
xp: newXp,
|
||||
total_xp_earned: totalEarned,
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
.eq('id', existing.id)
|
||||
.select()
|
||||
.single();
|
||||
|
||||
return data;
|
||||
} else {
|
||||
const { data } = await supabase
|
||||
.from('guild_user_xp')
|
||||
.insert({
|
||||
discord_id: discordId,
|
||||
guild_id: guildId,
|
||||
username: username,
|
||||
xp: xpGain,
|
||||
total_xp_earned: xpGain,
|
||||
prestige_level: 0,
|
||||
daily_streak: 0,
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
return data;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Standalone XP update error:', e.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function getStandaloneLeaderboard(supabase, guildId, limit = 10) {
|
||||
if (!supabase) return [];
|
||||
|
||||
try {
|
||||
const { data } = await supabase
|
||||
.from('guild_user_xp')
|
||||
.select('discord_id, username, xp, prestige_level')
|
||||
.eq('guild_id', guildId)
|
||||
.order('xp', { ascending: false })
|
||||
.limit(limit);
|
||||
|
||||
return data || [];
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function claimStandaloneDaily(supabase, discordId, guildId, username) {
|
||||
if (!supabase) return { success: false, message: 'Database not available' };
|
||||
|
||||
try {
|
||||
const { data: existing } = await supabase
|
||||
.from('guild_user_xp')
|
||||
.select('*')
|
||||
.eq('discord_id', discordId)
|
||||
.eq('guild_id', guildId)
|
||||
.maybeSingle();
|
||||
|
||||
const now = new Date();
|
||||
const today = now.toISOString().split('T')[0];
|
||||
|
||||
if (existing) {
|
||||
const lastClaim = existing.last_daily_claim ? existing.last_daily_claim.split('T')[0] : null;
|
||||
|
||||
if (lastClaim === today) {
|
||||
return { success: false, message: 'You already claimed your daily reward today!' };
|
||||
}
|
||||
|
||||
const yesterday = new Date(now);
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
const yesterdayStr = yesterday.toISOString().split('T')[0];
|
||||
|
||||
let newStreak = 1;
|
||||
if (lastClaim === yesterdayStr) {
|
||||
newStreak = (existing.daily_streak || 0) + 1;
|
||||
}
|
||||
|
||||
const baseXp = 50;
|
||||
const streakBonus = Math.min(newStreak * 5, 100);
|
||||
const totalXp = baseXp + streakBonus;
|
||||
|
||||
const newXp = (existing.xp || 0) + totalXp;
|
||||
const totalEarned = (existing.total_xp_earned || 0) + totalXp;
|
||||
|
||||
await supabase
|
||||
.from('guild_user_xp')
|
||||
.update({
|
||||
xp: newXp,
|
||||
total_xp_earned: totalEarned,
|
||||
daily_streak: newStreak,
|
||||
last_daily_claim: now.toISOString(),
|
||||
updated_at: now.toISOString(),
|
||||
})
|
||||
.eq('id', existing.id);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
xpGained: totalXp,
|
||||
streak: newStreak,
|
||||
totalXp: newXp,
|
||||
};
|
||||
} else {
|
||||
const baseXp = 50;
|
||||
|
||||
await supabase
|
||||
.from('guild_user_xp')
|
||||
.insert({
|
||||
discord_id: discordId,
|
||||
guild_id: guildId,
|
||||
username: username,
|
||||
xp: baseXp,
|
||||
total_xp_earned: baseXp,
|
||||
prestige_level: 0,
|
||||
daily_streak: 1,
|
||||
last_daily_claim: now.toISOString(),
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
xpGained: baseXp,
|
||||
streak: 1,
|
||||
totalXp: baseXp,
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Standalone daily claim error:', e.message);
|
||||
return { success: false, message: 'An error occurred' };
|
||||
}
|
||||
}
|
||||
|
||||
async function prestigeStandalone(supabase, discordId, guildId) {
|
||||
if (!supabase) return { success: false, message: 'Database not available' };
|
||||
|
||||
try {
|
||||
const { data: existing } = await supabase
|
||||
.from('guild_user_xp')
|
||||
.select('*')
|
||||
.eq('discord_id', discordId)
|
||||
.eq('guild_id', guildId)
|
||||
.maybeSingle();
|
||||
|
||||
if (!existing) {
|
||||
return { success: false, message: 'No XP data found' };
|
||||
}
|
||||
|
||||
const level = calculateLevel(existing.xp || 0, 'normal');
|
||||
if (level < 50) {
|
||||
return { success: false, message: `You need Level 50 to prestige! (Current: Level ${level})` };
|
||||
}
|
||||
|
||||
const newPrestige = (existing.prestige_level || 0) + 1;
|
||||
|
||||
await supabase
|
||||
.from('guild_user_xp')
|
||||
.update({
|
||||
xp: 0,
|
||||
prestige_level: newPrestige,
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
.eq('id', existing.id);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
newPrestige: newPrestige,
|
||||
bonus: newPrestige * 5,
|
||||
};
|
||||
} catch (e) {
|
||||
console.error('Standalone prestige error:', e.message);
|
||||
return { success: false, message: 'An error occurred' };
|
||||
}
|
||||
}
|
||||
|
||||
function calculateLevel(xp, curve = 'normal') {
|
||||
const bases = {
|
||||
easy: 50,
|
||||
normal: 100,
|
||||
hard: 200,
|
||||
};
|
||||
const base = bases[curve] || 100;
|
||||
return Math.floor(Math.sqrt(xp / base));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getStandaloneXp,
|
||||
updateStandaloneXp,
|
||||
getStandaloneLeaderboard,
|
||||
claimStandaloneDaily,
|
||||
prestigeStandalone,
|
||||
calculateLevel,
|
||||
};
|
||||
Loading…
Reference in a new issue