diff --git a/deploy-commands.js b/deploy-commands.js index 635ff80..49d2d21 100644 --- a/deploy-commands.js +++ b/deploy-commands.js @@ -1,5 +1,4 @@ -// deploycommands.js - +// deploy-commands.js const { REST, Routes, SlashCommandBuilder } = require('discord.js'); require('dotenv').config(); @@ -18,7 +17,17 @@ const commands = [ option.setName('game') .setDescription('Specify a game to view stats for') .setRequired(false)), - // Add more commands here + new SlashCommandBuilder() + .setName('matchhistory') + .setDescription('View match history') + .addStringOption(option => + option.setName('username') + .setDescription('The username to search for') + .setRequired(true)) + .addStringOption(option => + option.setName('game') + .setDescription('Filter by game') + .setRequired(false)), ]; const rest = new REST({ version: '10' }).setToken(process.env.BOT_TOKEN); diff --git a/index.js b/index.js index 316092a..5555569 100644 --- a/index.js +++ b/index.js @@ -1,324 +1,9 @@ // index.js - - -const { - Client, - GatewayIntentBits, - EmbedBuilder, - ActionRowBuilder, - ButtonBuilder, - ButtonStyle, -} = require('discord.js'); -const axios = require('axios'); require('dotenv').config(); +const Bot = require('./src/Bot'); -console.log('Starting bot...'); - -const client = new Client({ - intents: [ - GatewayIntentBits.Guilds, - GatewayIntentBits.GuildMessages, - GatewayIntentBits.MessageContent, - ], -}); - -console.log('Attempting to log in...'); - -client.once('ready', () => { - console.log(`Logged in as ${client.user.tag}!`); -}); - -client.login(process.env.BOT_TOKEN).then(() => { - console.log('Login successful'); -}).catch((error) => { - console.error('Login failed:', error); -}); - -// Function to fetch user data from the API -async function findUserByUsername(username) { - try { - const response = await axios.get( - `https://www.vrbattles.gg/api/get_player_data_by_username/${encodeURIComponent( - username - )}` - ); - return response.data; - } catch (error) { - console.error('Error fetching user data:', error); - return null; - } -} - -// Function to get badge image URL -function getBadgeImageUrl(badgeName) { - if (!badgeName) { - return 'https://www.vrbattles.gg/assets/images/badges/xp_badges/A/BADGE_A1_72p.png'; - } - - badgeName = badgeName.toUpperCase().replace(/ /g, '_'); - let badgeUrl = 'https://www.vrbattles.gg/assets/images/badges/xp_badges/'; - - if (badgeName.startsWith('A')) { - badgeUrl += 'A/'; - } else if (badgeName.startsWith('B')) { - badgeUrl += 'B/'; - } else if (badgeName.startsWith('C')) { - badgeUrl += 'C/'; - } else { - badgeUrl += 'A/'; - } - - badgeUrl += `BADGE_${badgeName}_72p.png`; - return badgeUrl; -} - -// Interaction handler -client.on('interactionCreate', async (interaction) => { - if (!interaction.isCommand()) return; - - const { commandName } = interaction; - - if (commandName === 'ping') { - await interaction.reply('Pong!'); - } else if (commandName === 'finduser') { - const username = interaction.options.getString('username'); - const gameFilter = interaction.options.getString('game'); - - await interaction.deferReply(); - - // Fetch data from the API using the provided username - const userData = await findUserByUsername(username); - - if (userData && userData.success) { - let playerData; - if (typeof userData.player_data === 'string') { - try { - playerData = JSON.parse(userData.player_data); - } catch (error) { - await interaction.editReply('Error parsing player data.'); - return; - } - } else { - playerData = userData.player_data; - } - - if (!playerData || !playerData.profile) { - await interaction.editReply( - 'User found but profile data is unavailable. They may need to log in to VRBattles to update their profile.' - ); - return; - } - - const user = playerData.profile || {}; - const badgeImageUrl = getBadgeImageUrl(user.current_level_badge); - - // Create the embed builder - const embed = new EmbedBuilder() - .setTitle(`User: ${playerData.username || 'Unknown'}`) - .setDescription(`Bio: ${user.bio || 'No bio available'}`) - .setColor('#0099ff'); - - // Set thumbnail if avatar is available - if (user.avatar) { - embed.setThumbnail( - `https://www.vrbattles.gg/assets/uploads/profile/${user.avatar}` - ); - } - - // Before adding fields to the embed, initialize a counter - let totalFields = 0; - - // Add profile fields dynamically - const profileFields = []; - - if (user.country) - profileFields.push({ - name: 'Country', - value: user.country, - inline: true, - }); - if (user.rank) - profileFields.push({ name: 'Rank', value: user.rank, inline: true }); - if (user.level) - profileFields.push({ - name: 'Level', - value: user.level.toString(), - inline: true, - }); - if (user.prestige) - profileFields.push({ - name: 'Prestige', - value: user.prestige.toString(), - inline: true, - }); - if (user.xp) - profileFields.push({ - name: 'Total XP', - value: user.xp.toString(), - inline: true, - }); - - if (profileFields.length > 0) { - embed.addFields(profileFields); - totalFields += profileFields.length; - } - - // Set badge image if available - if (badgeImageUrl) { - embed.setImage(badgeImageUrl); - } - - // Dynamically add game statistics - const games = playerData.stats?.games; - if (games && Object.keys(games).length > 0 && totalFields < 25) { - if (totalFields + 1 <= 25) { - embed.addFields({ name: '\u200B', value: '**Game Statistics**' }); - totalFields += 1; - } - - for (const [gameName, gameData] of Object.entries(games)) { - if ( - gameFilter && - gameName.toLowerCase() !== gameFilter.toLowerCase() - ) { - continue; // Skip games that don't match the filter - } - - for (const [modeName, modeStats] of Object.entries(gameData)) { - const statsFields = []; - - // Collect stats for the mode - if (modeStats.matches) - statsFields.push({ - name: 'Matches', - value: modeStats.matches.toString(), - inline: true, - }); - if (modeStats.wins) - statsFields.push({ - name: 'Wins', - value: modeStats.wins.toString(), - inline: true, - }); - if (modeStats.losses) - statsFields.push({ - name: 'Losses', - value: modeStats.losses.toString(), - inline: true, - }); - if (modeStats.win_rate) - statsFields.push({ - name: 'Win Rate', - value: modeStats.win_rate, - inline: true, - }); - if (modeStats.mmr) - statsFields.push({ - name: 'MMR', - value: modeStats.mmr.toString(), - inline: true, - }); - - // Calculate the number of fields to be added - const fieldsToAdd = 1 + statsFields.length; // 1 for the section header - - // Check if adding these fields would exceed the limit - if (totalFields + fieldsToAdd > 25) { - // You can choose to skip adding these fields or break the loop - break; // Stops adding more fields - } - - // Add the section header - embed.addFields({ - name: '\u200B', - value: `**${gameName} - ${modeName}**`, - }); - totalFields += 1; - - // Add the stats fields - embed.addFields(statsFields); - totalFields += statsFields.length; - - // Check after each addition - if (totalFields >= 25) { - break; - } - } - - if (totalFields >= 25) { - break; - } - } - } - - // Add teams dynamically - const teams = playerData.teams; - if (teams && Object.keys(teams).length > 0 && totalFields < 25) { - const teamList = Object.values(teams) - .map( - (team) => - `- **${team.team_name}** (${team.game_name} - ${team.game_mode})` - ) - .join('\n'); - - const teamsField = [ - { name: '\u200B', value: '**Teams**' }, - { name: 'Team List', value: teamList || 'No teams available' }, - ]; - - // Check if adding these fields would exceed the limit - const fieldsToAdd = teamsField.length; - if (totalFields + fieldsToAdd <= 25) { - embed.addFields(teamsField); - totalFields += fieldsToAdd; - } - } - - // Add recent matches dynamically - const matches = playerData.matches; - if (matches && Object.keys(matches).length > 0 && totalFields < 25) { - const matchList = Object.values(matches) - .sort((a, b) => new Date(b.start_time) - new Date(a.start_time)) // Sort by date descending - .slice(0, 3) // Limit to recent 3 matches - .map((match) => { - const date = new Date(match.start_time).toLocaleDateString(); - const team1 = match.team1_name || 'Team 1'; - const team2 = match.team2_name || 'Team 2'; - const score = match.score || 'N/A'; - return `- **${team1}** vs **${team2}** on ${date} - Result: ${score}`; - }) - .join('\n'); - - const matchesField = [ - { name: '\u200B', value: '**Recent Matches**' }, - { name: 'Match History', value: matchList || 'No recent matches' }, - ]; - - // Check if adding these fields would exceed the limit - const fieldsToAdd = matchesField.length; - if (totalFields + fieldsToAdd <= 25) { - embed.addFields(matchesField); - totalFields += fieldsToAdd; - } - } - - // Add action buttons - const row = new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setLabel('🔵 View Profile') - .setStyle(ButtonStyle.Link) - .setURL(`https://www.vrbattles.gg/profile/${playerData.username}`), - new ButtonBuilder() - .setLabel('🟡 Join Discord') - .setStyle(ButtonStyle.Link) - .setURL('https://discord.gg/j3DKVATPGQ') // Replace with your Discord invite URL - ); - - await interaction.editReply({ embeds: [embed], components: [row] }); - } else { - await interaction.editReply( - 'User not found or an error occurred while fetching data.' - ); - } - } -}); +const bot = new Bot(); +bot.start(process.env.BOT_TOKEN).catch(error => { + console.error('Failed to start bot:', error); + process.exit(1); +}); \ No newline at end of file diff --git a/index2.js b/index2.js new file mode 100644 index 0000000..76287fe --- /dev/null +++ b/index2.js @@ -0,0 +1,325 @@ +// index.js + + +const { + Client, + GatewayIntentBits, + EmbedBuilder, + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, +} = require('discord.js'); +const axios = require('axios'); +require('dotenv').config(); + +console.log('Starting bot...'); + +const client = new Client({ + intents: [ + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.MessageContent, + ], +}); + +console.log('Attempting to log in...'); + +client.once('ready', () => { + console.log(`Logged in as ${client.user.tag}!`); +}); + +client.login(process.env.BOT_TOKEN).then(() => { + console.log('Login successful'); +}).catch((error) => { + console.error('Login failed:', error); +}); + +// Function to fetch user data from the API +async function findUserByUsername(username) { + try { + const response = await axios.get( + `https://www.vrbattles.gg/api/get_player_data_by_username/${encodeURIComponent(username)}`, + { + timeout: 5000 // 5 second timeout + } + ); + return response.data; + } catch (error) { + console.error('Error fetching user data:', error); + return null; + } +} + +// Function to get badge image URL +function getBadgeImageUrl(badgeName) { + if (!badgeName) { + return 'https://www.vrbattles.gg/assets/images/badges/xp_badges/A/BADGE_A1_72p.png'; + } + + badgeName = badgeName.toUpperCase().replace(/ /g, '_'); + let badgeUrl = 'https://www.vrbattles.gg/assets/images/badges/xp_badges/'; + + if (badgeName.startsWith('A')) { + badgeUrl += 'A/'; + } else if (badgeName.startsWith('B')) { + badgeUrl += 'B/'; + } else if (badgeName.startsWith('C')) { + badgeUrl += 'C/'; + } else { + badgeUrl += 'A/'; + } + + badgeUrl += `BADGE_${badgeName}_72p.png`; + return badgeUrl; +} + +// Interaction handler +client.on('interactionCreate', async (interaction) => { + if (!interaction.isCommand()) return; + + const { commandName } = interaction; + + if (commandName === 'ping') { + await interaction.reply('Pong!'); + } else if (commandName === 'finduser') { + const username = interaction.options.getString('username'); + const gameFilter = interaction.options.getString('game'); + + await interaction.deferReply(); + + // Fetch data from the API using the provided username + const userData = await findUserByUsername(username); + + if (userData && userData.success) { + let playerData; + if (typeof userData.player_data === 'string') { + try { + playerData = JSON.parse(userData.player_data); + } catch (error) { + await interaction.editReply('Error parsing player data.'); + return; + } + } else { + playerData = userData.player_data; + } + + if (!playerData || !playerData.profile) { + await interaction.editReply( + 'User found but profile data is unavailable. They may need to log in to VRBattles to update their profile.' + ); + return; + } + + const user = playerData.profile || {}; + const badgeImageUrl = getBadgeImageUrl(user.current_level_badge); + + // Create the embed builder + const embed = new EmbedBuilder() + .setTitle(`User: ${playerData.username || 'Unknown'}`) + .setDescription(`Bio: ${user.bio || 'No bio available'}`) + .setColor('#0099ff'); + + // Set thumbnail if avatar is available + if (user.avatar) { + embed.setThumbnail( + `https://www.vrbattles.gg/assets/uploads/profile/${user.avatar}` + ); + } + + // Before adding fields to the embed, initialize a counter + let totalFields = 0; + + // Add profile fields dynamically + const profileFields = []; + + if (user.country) + profileFields.push({ + name: 'Country', + value: user.country, + inline: true, + }); + if (user.rank) + profileFields.push({ name: 'Rank', value: user.rank, inline: true }); + if (user.level) + profileFields.push({ + name: 'Level', + value: user.level.toString(), + inline: true, + }); + if (user.prestige) + profileFields.push({ + name: 'Prestige', + value: user.prestige.toString(), + inline: true, + }); + if (user.xp) + profileFields.push({ + name: 'Total XP', + value: user.xp.toString(), + inline: true, + }); + + if (profileFields.length > 0) { + embed.addFields(profileFields); + totalFields += profileFields.length; + } + + // Set badge image if available + if (badgeImageUrl) { + embed.setImage(badgeImageUrl); + } + + // Dynamically add game statistics + const games = playerData.stats?.games; + if (games && Object.keys(games).length > 0 && totalFields < 25) { + if (totalFields + 1 <= 25) { + embed.addFields({ name: '\u200B', value: '**Game Statistics**' }); + totalFields += 1; + } + + for (const [gameName, gameData] of Object.entries(games)) { + if ( + gameFilter && + gameName.toLowerCase() !== gameFilter.toLowerCase() + ) { + continue; // Skip games that don't match the filter + } + + for (const [modeName, modeStats] of Object.entries(gameData)) { + const statsFields = []; + + // Collect stats for the mode + if (modeStats.matches) + statsFields.push({ + name: 'Matches', + value: modeStats.matches.toString(), + inline: true, + }); + if (modeStats.wins) + statsFields.push({ + name: 'Wins', + value: modeStats.wins.toString(), + inline: true, + }); + if (modeStats.losses) + statsFields.push({ + name: 'Losses', + value: modeStats.losses.toString(), + inline: true, + }); + if (modeStats.win_rate) + statsFields.push({ + name: 'Win Rate', + value: modeStats.win_rate, + inline: true, + }); + if (modeStats.mmr) + statsFields.push({ + name: 'MMR', + value: modeStats.mmr.toString(), + inline: true, + }); + + // Calculate the number of fields to be added + const fieldsToAdd = 1 + statsFields.length; // 1 for the section header + + // Check if adding these fields would exceed the limit + if (totalFields + fieldsToAdd > 25) { + // You can choose to skip adding these fields or break the loop + break; // Stops adding more fields + } + + // Add the section header + embed.addFields({ + name: '\u200B', + value: `**${gameName} - ${modeName}**`, + }); + totalFields += 1; + + // Add the stats fields + embed.addFields(statsFields); + totalFields += statsFields.length; + + // Check after each addition + if (totalFields >= 25) { + break; + } + } + + if (totalFields >= 25) { + break; + } + } + } + + // Add teams dynamically + const teams = playerData.teams; + if (teams && Object.keys(teams).length > 0 && totalFields < 25) { + const teamList = Object.values(teams) + .map( + (team) => + `- **${team.team_name}** (${team.game_name} - ${team.game_mode})` + ) + .join('\n'); + + const teamsField = [ + { name: '\u200B', value: '**Teams**' }, + { name: 'Team List', value: teamList || 'No teams available' }, + ]; + + // Check if adding these fields would exceed the limit + const fieldsToAdd = teamsField.length; + if (totalFields + fieldsToAdd <= 25) { + embed.addFields(teamsField); + totalFields += fieldsToAdd; + } + } + + // Add recent matches dynamically + const matches = playerData.matches; + if (matches && Object.keys(matches).length > 0 && totalFields < 25) { + const matchList = Object.values(matches) + .sort((a, b) => new Date(b.start_time) - new Date(a.start_time)) // Sort by date descending + .slice(0, 3) // Limit to recent 3 matches + .map((match) => { + const date = new Date(match.start_time).toLocaleDateString(); + const team1 = match.team1_name || 'Team 1'; + const team2 = match.team2_name || 'Team 2'; + const score = match.score || 'N/A'; + return `- **${team1}** vs **${team2}** on ${date} - Result: ${score}`; + }) + .join('\n'); + + const matchesField = [ + { name: '\u200B', value: '**Recent Matches**' }, + { name: 'Match History', value: matchList || 'No recent matches' }, + ]; + + // Check if adding these fields would exceed the limit + const fieldsToAdd = matchesField.length; + if (totalFields + fieldsToAdd <= 25) { + embed.addFields(matchesField); + totalFields += fieldsToAdd; + } + } + + // Add action buttons + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setLabel('🔵 View Profile') + .setStyle(ButtonStyle.Link) + .setURL(`https://www.vrbattles.gg/profile/${playerData.username}`), + new ButtonBuilder() + .setLabel('🟡 Join Discord') + .setStyle(ButtonStyle.Link) + .setURL('https://discord.gg/j3DKVATPGQ') // Replace with your Discord invite URL + ); + + await interaction.editReply({ embeds: [embed], components: [row] }); + } else { + await interaction.editReply( + 'User not found or an error occurred while fetching data.' + ); + } + } +}); diff --git a/src/Bot.js b/src/Bot.js new file mode 100644 index 0000000..2026104 --- /dev/null +++ b/src/Bot.js @@ -0,0 +1,93 @@ +// src/bot.js +const { Client, GatewayIntentBits } = require('discord.js'); +// src/Bot.js - Update your import statements +const CommandHandler = require('./commands/CommandHandler.js'); // Add .js extension +const PlayerService = require('./services/PlayerService.js'); // Add .js extension +class Bot { + constructor() { + this.client = new Client({ + intents: [ + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.MessageContent, + ], + }); + + this.playerService = new PlayerService(); + this.commandHandler = new CommandHandler(this.playerService); + + this.setupEventHandlers(); + } + + setupEventHandlers() { + this.client.on('error', this.handleError.bind(this)); + this.client.once('ready', this.handleReady.bind(this)); + this.client.on('interactionCreate', this.handleInteraction.bind(this)); + } + + async start(token) { + console.log('Starting bot...'); + try { + await this.client.login(token); + console.log('Login successful'); + } catch (error) { + console.error('Login failed:', error); + throw error; + } + } + + handleError(error) { + console.error('Discord client error:', error); + } + + handleReady() { + console.log(`Logged in as ${this.client.user.tag}!`); + } + + async handleInteraction(interaction) { + if (!interaction.isCommand()) return; + + try { + switch (interaction.commandName) { + case 'ping': + await this.commandHandler.handlePing(interaction); + break; + case 'finduser': + await this.commandHandler.handleFindUser(interaction); + break; + case 'matchhistory': + await this.commandHandler.handleMatchHistory(interaction); + break; + default: + await interaction.reply({ + content: 'Unknown command', + ephemeral: true + }); + } + } catch (error) { + await this.handleCommandError(interaction, error); + } + } + + async handleCommandError(interaction, error) { + console.error('Command error:', error); + + if (error.code === 10062) { + console.log('Interaction expired'); + return; + } + + try { + const message = 'An error occurred while processing your command.'; + if (interaction.deferred) { + await interaction.editReply({ content: message, ephemeral: true }); + } else if (!interaction.replied) { + await interaction.reply({ content: message, ephemeral: true }); + } + } catch (e) { + console.error('Error while sending error message:', e); + } + } +} + +module.exports = Bot; \ No newline at end of file diff --git a/src/commands/CommandHandler.js b/src/commands/CommandHandler.js new file mode 100644 index 0000000..d810198 --- /dev/null +++ b/src/commands/CommandHandler.js @@ -0,0 +1,235 @@ +// src/commands/CommandHandler.js +const { EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js'); + +class CommandHandler { + constructor(playerService) { + this.playerService = playerService; + } + + async handlePing(interaction) { + await interaction.reply('Pong!'); + } + + async handleFindUser(interaction) { + await interaction.deferReply(); + + const username = interaction.options.getString('username'); + const gameFilter = interaction.options.getString('game'); + + const userData = await this.playerService.findUserByUsername(username); + if (!userData || !userData.success) { + await interaction.editReply('User not found or an error occurred while fetching data.'); + return; + } + + const playerData = typeof userData.player_data === 'string' + ? JSON.parse(userData.player_data) + : userData.player_data; + + const embed = await this.createUserEmbed(playerData, gameFilter); + const row = this.createActionRow(playerData.username); + + await interaction.editReply({ embeds: [embed], components: [row] }); + } + + async handleMatchHistory(interaction) { + await interaction.deferReply(); + + const username = interaction.options.getString('username'); + const gameFilter = interaction.options.getString('game'); + + try { + const userData = await this.playerService.findUserByUsername(username); + if (!userData || !userData.success) { + await interaction.editReply('User not found or an error occurred while fetching data.'); + return; + } + + const playerData = typeof userData.player_data === 'string' + ? JSON.parse(userData.player_data) + : userData.player_data; + + const matches = Object.values(playerData.matches || {}) + .filter(match => !gameFilter || match.game_name.toLowerCase() === gameFilter.toLowerCase()) + .sort((a, b) => new Date(b.start_time) - new Date(a.start_time)) + .slice(0, 10); + + if (matches.length === 0) { + await interaction.editReply( + gameFilter + ? `No matches found for ${username} in ${gameFilter}` + : `No matches found for ${username}` + ); + return; + } + + const embed = this.createMatchHistoryEmbed(playerData, matches); + const row = this.createActionRow(playerData.username); + + await interaction.editReply({ embeds: [embed], components: [row] }); + } catch (error) { + console.error('Error in handleMatchHistory:', error); + await interaction.editReply('An error occurred while processing the match history.'); + } + } + + createUserEmbed(playerData, gameFilter) { + const profile = playerData.profile || {}; + const embed = new EmbedBuilder() + .setTitle(`User: ${playerData.username || 'Unknown'}`) + .setDescription(profile.bio || 'No bio available') + .setColor('#0099ff'); + + // Add thumbnail (avatar) + if (profile.avatar) { + embed.setThumbnail( + `https://www.vrbattles.gg/assets/uploads/profile/${profile.avatar}` + ); + } + + // Add badge image using PlayerService + if (profile.current_level_badge) { + const badgeImageUrl = this.playerService.getBadgeImageUrl(profile.current_level_badge); + embed.setImage(badgeImageUrl); + } + + // Add profile fields + const profileFields = []; + if (profile.country) profileFields.push({ name: 'Country', value: profile.country, inline: true }); + if (profile.level) profileFields.push({ name: 'Level', value: profile.level.toString(), inline: true }); + if (profile.xp) profileFields.push({ name: 'Total XP', value: profile.xp.toString(), inline: true }); + if (profile.prestige) profileFields.push({ name: 'Prestige', value: profile.prestige.toString(), inline: true }); + + if (profileFields.length > 0) { + embed.addFields(profileFields); + } + + // Add game stats if available + const games = playerData.stats?.games; + if (games) { + Object.entries(games).forEach(([gameName, gameData]) => { + if (gameFilter && gameName.toLowerCase() !== gameFilter.toLowerCase()) return; + + embed.addFields({ name: gameName, value: '\u200B' }); + Object.entries(gameData).forEach(([mode, stats]) => { + if (mode === 'Overall') return; + + const statFields = []; + if (stats.matches) statFields.push(`Matches: ${stats.matches}`); + if (stats.wins) statFields.push(`Wins: ${stats.wins}`); + if (stats.losses) statFields.push(`Losses: ${stats.losses}`); + if (stats.win_rate) statFields.push(`Win Rate: ${stats.win_rate}`); + + if (statFields.length > 0) { + embed.addFields({ + name: mode, + value: statFields.join(' | '), + inline: true + }); + } + }); + }); + } + + // Add teams if available + const teams = playerData.teams; + if (teams && Object.keys(teams).length > 0) { + const teamList = Object.values(teams) + .map(team => `- **${team.team_name}** (${team.game_name} - ${team.game_mode})`) + .join('\n'); + + embed.addFields( + { name: '\u200B', value: '**Teams**' }, + { name: 'Team List', value: teamList || 'No teams available' } + ); + } + + // Add recent matches if available + const matches = playerData.matches; + if (matches && Object.keys(matches).length > 0) { + const matchList = Object.values(matches) + .sort((a, b) => new Date(b.start_time) - new Date(a.start_time)) + .slice(0, 3) + .map(match => { + const date = new Date(match.start_time).toLocaleDateString(); + const team1 = match.team1_name || 'Team 1'; + const team2 = match.team2_name || 'Team 2'; + const score = match.score || 'N/A'; + return `- **${team1}** vs **${team2}** on ${date} - Result: ${score}`; + }) + .join('\n'); + + embed.addFields( + { name: '\u200B', value: '**Recent Matches**' }, + { name: 'Match History', value: matchList || 'No recent matches' } + ); + } + + return embed; + } + + createMatchHistoryEmbed(playerData, matches) { + const embed = new EmbedBuilder() + .setTitle(`Match History for ${playerData.username}`) + .setColor('#0099ff') + .setDescription(`Last ${matches.length} matches`); + + if (playerData.profile?.avatar) { + embed.setThumbnail( + `https://www.vrbattles.gg/assets/uploads/profile/${playerData.profile.avatar}` + ); + } + + // Add match stats + const wins = matches.filter(m => { + const isTeam1 = m.team1_name === playerData.username; + return m.winner_id === (isTeam1 ? "1" : "2"); + }).length; + + const winRate = ((wins / matches.length) * 100).toFixed(1); + + embed.addFields( + { name: 'Recent Win Rate', value: `${winRate}%`, inline: true }, + { name: 'Matches Analyzed', value: matches.length.toString(), inline: true }, + { name: 'Recent Wins', value: wins.toString(), inline: true } + ); + + // Add individual matches + matches.forEach((match, index) => { + const date = new Date(match.start_time).toLocaleDateString(); + const isTeam1 = match.team1_name === playerData.username; + const opponent = isTeam1 ? match.team2_name : match.team1_name; + const won = match.winner_id === (isTeam1 ? "1" : "2"); + const score = match.score || '0 - 0'; + const gameMode = match.game_mode ? ` (${match.game_mode})` : ''; + + const matchResult = won ? '✅ Victory' : '❌ Defeat'; + + embed.addFields({ + name: `Match ${index + 1} - ${matchResult}`, + value: `🎮 **${match.game_name}${gameMode}**\n` + + `🆚 vs ${opponent}\n` + + `📊 Score: ${score}\n` + + `📅 ${date}`, + inline: false + }); + }); + + return embed; + } + + createActionRow(username) { + return new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setLabel('🔵 View Profile') + .setStyle(ButtonStyle.Link) + .setURL(this.playerService.getProfileUrl(username)), + new ButtonBuilder() + .setLabel('🟡 View Stats') + .setStyle(ButtonStyle.Link) + .setURL(this.playerService.getStatsUrl(username)) + ); + } +} + +module.exports = CommandHandler; \ No newline at end of file diff --git a/src/services/PlayerService.js b/src/services/PlayerService.js new file mode 100644 index 0000000..2c7b805 --- /dev/null +++ b/src/services/PlayerService.js @@ -0,0 +1,55 @@ +// src/services/PlayerService.js +const axios = require('axios'); + +class PlayerService { + constructor() { + this.baseUrl = 'https://www.vrbattles.gg'; + } + + async findUserByUsername(username) { + try { + const response = await axios.get( + `${this.baseUrl}/api/get_player_data_by_username/${encodeURIComponent(username)}`, + { + timeout: 5000 + } + ); + return response.data; + } catch (error) { + console.error('Error fetching user data:', error); + return null; + } + } + + getBadgeImageUrl(badgeName) { + if (!badgeName) { + return `${this.baseUrl}/assets/images/badges/xp_badges/A/BADGE_A1_72p.png`; + } + + badgeName = badgeName.toUpperCase().replace(/ /g, '_'); + let badgeUrl = `${this.baseUrl}/assets/images/badges/xp_badges/`; + + if (badgeName.startsWith('A')) { + badgeUrl += 'A/'; + } else if (badgeName.startsWith('B')) { + badgeUrl += 'B/'; + } else if (badgeName.startsWith('C')) { + badgeUrl += 'C/'; + } else { + badgeUrl += 'A/'; + } + + badgeUrl += `BADGE_${badgeName}_72p.png`; + return badgeUrl; + } + + getProfileUrl(username) { + return `${this.baseUrl}/profile/${username}`; + } + + getStatsUrl(username) { + return `${this.baseUrl}/profile/${username}/stats`; + } +} + +module.exports = PlayerService; \ No newline at end of file diff --git a/src/tests/test.js b/src/tests/test.js new file mode 100644 index 0000000..632809a --- /dev/null +++ b/src/tests/test.js @@ -0,0 +1,58 @@ +// src/test.js +const PlayerService = require('./services/PlayerService'); + +async function testPlayerService() { + console.log('Testing PlayerService...'); + + const playerService = new PlayerService(); + + try { + // Test user lookup + console.log('\nTesting user lookup...'); + const userData = await playerService.findUserByUsername('uthcowboys'); + console.log('User data received:', !!userData); + console.log('Success:', userData?.success); + + if (userData && userData.success) { + const playerData = JSON.parse(userData.player_data); + console.log('\nBasic user info:'); + console.log('Username:', playerData.username); + console.log('Level:', playerData.profile?.level); + console.log('Country:', playerData.profile?.country); + + // Test matches + console.log('\nRecent matches:'); + const matches = Object.values(playerData.matches || {}) + .sort((a, b) => new Date(b.start_time) - new Date(a.start_time)) + .slice(0, 3); + + matches.forEach(match => { + console.log(`- ${match.game_name} (${match.game_mode}): ${match.score} on ${new Date(match.start_time).toLocaleDateString()}`); + }); + + // Test stats + console.log('\nGame stats:'); + const games = playerData.stats?.games || {}; + Object.entries(games).forEach(([gameName, gameData]) => { + console.log(`\n${gameName}:`); + Object.entries(gameData).forEach(([mode, stats]) => { + if (mode !== 'Overall') { + console.log(` ${mode}:`); + console.log(` Matches: ${stats.matches}`); + console.log(` Win Rate: ${stats.win_rate}`); + } + }); + }); + } + } catch (error) { + console.error('Test failed:', error); + } +} + +// Run tests +console.log('Starting tests...\n'); +testPlayerService().then(() => { + console.log('\nTests completed.'); +}).catch(error => { + console.error('Tests failed:', error); +}); \ No newline at end of file diff --git a/src/tests/testcommands.js b/src/tests/testcommands.js new file mode 100644 index 0000000..11067e3 --- /dev/null +++ b/src/tests/testcommands.js @@ -0,0 +1,69 @@ +// src/testCommands.js +const CommandHandler = require('./commands/CommandHandler'); +const PlayerService = require('./services/PlayerService'); + +// Mock Discord interaction +class MockInteraction { + constructor(commandName, options = {}) { + this.commandName = commandName; + this.deferred = false; + this.replied = false; + this.options = { + getString: (name) => options[name] + }; + } + + async deferReply() { + this.deferred = true; + console.log('Interaction deferred'); + } + + async reply(content) { + this.replied = true; + console.log('Reply sent:', content); + } + + async editReply(content) { + console.log('Edit reply:', content); + if (content.embeds) { + console.log('\nEmbed fields:'); + content.embeds.forEach(embed => { + console.log('Title:', embed.data.title); + console.log('Description:', embed.data.description); + if (embed.data.fields) { + embed.data.fields.forEach(field => { + console.log(`${field.name}: ${field.value}`); + }); + } + }); + } + } +} + +async function testCommands() { + console.log('Testing commands...\n'); + + const playerService = new PlayerService(); + const commandHandler = new CommandHandler(playerService); + + // Test finduser command + console.log('Testing /finduser command...'); + const findUserInteraction = new MockInteraction('finduser', { + username: 'uthcowboys' + }); + await commandHandler.handleFindUser(findUserInteraction); + + // Test matchhistory command + console.log('\nTesting /matchhistory command...'); + const matchHistoryInteraction = new MockInteraction('matchhistory', { + username: 'uthcowboys' + }); + await commandHandler.handleMatchHistory(matchHistoryInteraction); +} + +// Run tests +testCommands().then(() => { + console.log('\nCommand tests completed.'); +}).catch(error => { + console.error('Command tests failed:', error); +}); \ No newline at end of file diff --git a/src/utils/helpers.js b/src/utils/helpers.js new file mode 100644 index 0000000..d0c8712 --- /dev/null +++ b/src/utils/helpers.js @@ -0,0 +1,12 @@ +// src/utils/helpers.js +function safeGet(obj, path, defaultValue = null) { + try { + return path.split('.').reduce((acc, part) => acc && acc[part], obj) ?? defaultValue; + } catch (e) { + return defaultValue; + } + } + + module.exports = { + safeGet + }; \ No newline at end of file