v1.2.11: Add tournaments command with filtering and improve error handling

Features:
- Add /tournaments command with status and game filters
- Add tournament API integration to PlayerService
- Add beautiful tournament embed builders with prize pools
- Protect sync-games from overwriting manual customizations
- Improve error handling with proper MessageFlags

Changes:
- Add getTournamentsData() and getTournamentData() to PlayerService
- Add tournament command handler with pagination support
- Add tournament embed builders with rich formatting
- Update deployment docs for v1.2.10 features
- Add issue tracker documentation
- Add tournament testing script
- Fix ephemeral flags throughout CommandHandler
- Improve permission checks with PermissionFlagsBits

Version: 1.2.11
This commit is contained in:
VinceC
2025-10-02 06:24:54 -05:00
parent 546127f91c
commit dd8aa456d9
11 changed files with 502 additions and 60 deletions

View File

@@ -1,8 +1,30 @@
# 🚀 VRBattles Discord Bot - Deployment Guide
## 📦 **Latest Version: v1.2.7**
## 📦 **Latest Version: v1.2.10**
### **What's New in v1.2.7:**
### **What's New in v1.2.10:**
- ✅ MAJOR FIX: Tournament status filter now works correctly and permanently
- ✅ Fixed sync-games script overwriting tournament status choices
- ✅ Tournament status uses actual API values (Published, In-progress, Ended)
- ✅ Excluded Unpublished status (admin-only tournaments)
- ✅ Protected manual choice customizations from future overwrites
- ✅ Added emojis to status filter choices for better UX
### **Previous in v1.2.9:**
- ✅ FIXED: Tournament status filter now works correctly
- ✅ Tournament status filter uses proper values (Published, In-progress, Ended)
- ✅ Removed Unpublished status from filter options
- ✅ Added emojis to status filter choices for better UX
### **Previous in v1.2.8:**
- ✅ NEW: `/tournaments` command with beautiful formatting
- ✅ Filter tournaments by status (Published, In-progress, Ended, Unpublished)
- ✅ Filter tournaments by game (VAIL, Echo Arena, Blacktop Hoops, etc.)
- ✅ Smart date formatting with relative times
- ✅ Rich embeds with prize pools, entry fees, and team sizes
- ✅ Enhanced PlayerService with tournaments API integration
### **Previous in v1.2.7:**
- ✅ Fixed Docker multi-platform build (x86_64 + ARM64)
- ✅ Resolved "exec format error" on Coolify deployment
- ✅ Enhanced deployment script with buildx support
@@ -18,7 +40,7 @@
### **Docker Images Available:**
```
far54/vrbattles-discord-bot:latest (always current)
far54/vrbattles-discord-bot:v1.2.7 (specific version)
far54/vrbattles-discord-bot:v1.2.10 (specific version)
```
### **Multi-Platform Support:**
@@ -63,6 +85,7 @@ NODE_ENV=production
- `/finduser` - Search players (9 game dropdown)
- `/findteam` - Search teams (9 game dropdown)
- `/matchhistory` - View match history (optional game filter)
- `/tournaments` - **NEW!** View all tournaments with filters
- `/subscribe` - Admin: Subscribe to game notifications
- `/unsubscribe` - Admin: Remove game subscriptions
- `/list_subscriptions` - Admin: View active subscriptions

View File

@@ -159,6 +159,37 @@ const commands = [
.setDescription("The team name to search for")
.setRequired(true)
),
new SlashCommandBuilder()
.setName("tournaments")
.setDescription("View all available tournaments")
.addStringOption((option) =>
option
.setName("status")
.setDescription("Filter tournaments by status (optional)")
.setRequired(false)
.addChoices(
{ name: "📢 Published", value: "Published" },
{ name: "⚡ In-progress", value: "In-progress" },
{ name: "🏁 Ended", value: "Ended" }
)
)
.addStringOption((option) =>
option
.setName("game")
.setDescription("Filter tournaments by game (optional)")
.setRequired(false)
.addChoices(
{ name: "Big Ballers VR", value: "Big Ballers VR" },
{ name: "Blacktop Hoops", value: "Blacktop Hoops" },
{ name: "Breachers", value: "Breachers" },
{ name: "Echo Arena", value: "Echo Arena" },
{ name: "Echo Combat", value: "Echo Combat" },
{ name: "Gun Raiders", value: "Gun Raiders" },
{ name: "Nock", value: "Nock" },
{ name: "Orion Drift", value: "Orion Drift" },
{ name: "VAIL", value: "VAIL" }
)
),
];
const rest = new REST({ version: "10" }).setToken(process.env.DISCORD_TOKEN);

49
docs/issue-tracker.md Normal file
View File

@@ -0,0 +1,49 @@
# VRBattles Discord Bot Issue Tracker
Use this checklist to track code-quality fixes and consistency updates. Check items off as completed and commit this file with your changes.
## How to use
- Edit this file, change "[ ]" to "[x]" for completed items, and include a short note or PR link if helpful.
## Checklist
- [ ] Replace admin permission check with PermissionFlagsBits.Administrator
- Location: `src/commands/CommandHandler.js` (in `handleRegisterServer`)
- Desired: `interaction.member.permissions.has(PermissionFlagsBits.Administrator)`
- [ ] Replace legacy `flags` ephemeral usage with `ephemeral: true`
- Location: various replies in `src/commands/CommandHandler.js` and `src/commands/SubscriptionCommands.js`
- Notes: decide ephemerality at `reply`/`deferReply`; it cannot be changed via `editReply`
- [ ] Switch `interaction.isCommand()` to `interaction.isChatInputCommand()`
- Location: `src/Bot.js` (in `handleInteraction`)
- [ ] Use `PermissionFlagsBits` for channel permission checks
- Location: `src/services/NotificationService.js`
- Replace string permissions (e.g., 'SendMessages', 'ViewChannel') with `PermissionFlagsBits.*`
- [ ] Clean up `SubscriptionCommands` duplicates and constructor mismatch
- Location: `src/commands/SubscriptionCommands.js`
- Remove duplicated methods (`getGame`, `createOrUpdateSubscription`, `safeReply`), fix constructor to accept `(supabase, logger)` and use the injected logger
- [ ] Fix `PlayerService` logging
- Location: `src/services/PlayerService.js`
- Inject a logger or use a consistent logging strategy; remove `this.logger` references if not injected
- [ ] Align `/help` category choices with handler expectations
- Locations:
- `deploy-commands.js` (slash command choices for `help`)
- `src/commands/HelpCommand.js` (expects categories like `getting_started`, `search`, etc.)
- Decide whether to offer topic categories or game names and make both sides consistent
- [ ] Add guild-scoped slash command deployment for development
- Location: `deploy-commands.js` (or add a new `deploy-commands.dev.js`)
- Use `Routes.applicationGuildCommands(CLIENT_ID, GUILD_ID)` for fast iteration in dev
- [ ] Standardize interaction handlers and checks
- Ensure consistent use of `isChatInputCommand`, `isButton`, and ephemerality decisions across commands and buttons
## Notes
- Keep PRs small and focused (one checklist item per PR when possible).
- Update this file with any extra context, follow-ups, or links.

View File

@@ -1,6 +1,6 @@
{
"name": "vrbattles-discord-bot",
"version": "1.2.7",
"version": "1.2.11",
"description": "VRBattles Discord Bot - Player search, team lookup, match notifications, and more for VR gaming communities",
"main": "index.js",
"scripts": {

View File

@@ -84,10 +84,11 @@ echo -e " 2. Update your application image to: ${FULL_IMAGE_NAME}:${VERSION}"
echo -e " 3. Restart the application"
echo ""
echo -e "${BLUE}🎯 Features in this version:${NC}"
echo -e " ✅ NEW: /tournaments command with filtering"
echo -e " ✅ Tournament status & game filters"
echo -e " ✅ Beautiful tournament embeds with prizes"
echo -e " ✅ All 9 games in dropdowns"
echo -e " ✅ Working help button navigation"
echo -e " ✅ No debug message spam"
echo -e " ✅ Automatic Supabase game sync"
echo -e " ✅ Optimized Docker image"
echo ""
echo -e "${GREEN}🎉 Deployment complete!${NC}"

View File

@@ -1,4 +1,4 @@
const { Client, GatewayIntentBits } = require('discord.js');
const { Client, GatewayIntentBits, MessageFlags } = require('discord.js');
const CommandHandler = require('./commands/CommandHandler');
const SubscriptionCommands = require('./commands/SubscriptionCommands');
const PlayerService = require('./services/PlayerService');
@@ -79,7 +79,7 @@ class Bot {
}
async handleInteraction(interaction) {
if (!interaction.isCommand() && !interaction.isButton()) {
if (!interaction.isChatInputCommand() && !interaction.isButton()) {
this.logger.debug('Ignoring non-command/button interaction');
return;
}
@@ -93,31 +93,19 @@ class Bot {
this.processingLock.set(lockKey, true);
try {
if (interaction.isCommand()) {
// Quick defer with timeout handling
if (interaction.isChatInputCommand()) {
// Defer once to acknowledge within the 3s window
const isPublicCommand = ['finduser', 'matchhistory', 'findteam'].includes(interaction.commandName);
try {
await Promise.race([
interaction.deferReply({ ephemeral: !isPublicCommand }),
new Promise((_, reject) => setTimeout(() => reject(new Error('Defer timeout')), 2500))
]);
await interaction.deferReply(!isPublicCommand ? { flags: MessageFlags.Ephemeral } : {});
} catch (deferError) {
this.logger.warn('Failed to defer interaction:', {
error: deferError.message,
command: interaction.commandName,
guild: interaction.guild?.name
});
// Try to reply immediately if defer failed
try {
await interaction.reply({
content: '⏳ Processing your request...',
ephemeral: !isPublicCommand
});
} catch (replyError) {
this.logger.error('Failed to reply after defer timeout:', replyError);
return; // Give up on this interaction
}
// If we failed to acknowledge, do not attempt another response
return;
}
this.logger.debug('Processing command', {

View File

@@ -1,4 +1,4 @@
const { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, PermissionFlagsBits } = require('discord.js');
const { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, PermissionFlagsBits, MessageFlags } = require('discord.js');
const EmbedBuilders = require('../utils/embedBuilders');
const HelpCommand = require('./HelpCommand');
const PaginationManager = require('../utils/PaginationManager');
@@ -22,9 +22,8 @@ class CommandHandler {
// Check cooldown
const cooldownTime = this.cooldownManager.checkCooldown(interaction);
if (cooldownTime > 0) {
await interaction.reply({
content: `⏰ Please wait ${cooldownTime.toFixed(1)} seconds before using this command again.`,
flags: ['Ephemeral']
await interaction.editReply({
content: `⏰ Please wait ${cooldownTime.toFixed(1)} seconds before using this command again.`
});
return;
}
@@ -60,10 +59,12 @@ class CommandHandler {
case 'version':
await this.handleVersion(interaction);
break;
case 'tournaments':
await this.handleTournaments(interaction);
break;
default:
await interaction.editReply({
content: '❌ Unknown command.',
flags: ['Ephemeral']
content: '❌ Unknown command.'
});
}
} catch (error) {
@@ -83,7 +84,7 @@ class CommandHandler {
} else {
await interaction.reply({
content: '❌ An error occurred while processing your command.',
flags: ['Ephemeral']
flags: MessageFlags.Ephemeral
});
}
} catch (replyError) {
@@ -164,10 +165,9 @@ class CommandHandler {
async handleRegisterServer(interaction) {
try {
// Check for admin permissions
if (!interaction.member.permissions.has('ADMINISTRATOR')) {
if (!interaction.member.permissions.has(PermissionFlagsBits.Administrator)) {
await interaction.editReply({
content: '❌ You need administrator permissions to register this server.',
ephemeral: true
content: '❌ You need administrator permissions to register this server.'
});
return;
}
@@ -197,8 +197,7 @@ class CommandHandler {
if (!result || typeof result !== 'object') {
this.logger.error('Invalid result from registerServer:', { result, guildId, serverName });
await interaction.editReply({
content: '❌ Server registration failed due to invalid response. Please try again or contact support.',
ephemeral: true
content: '❌ Server registration failed due to invalid response. Please try again or contact support.'
});
return;
}
@@ -211,8 +210,7 @@ class CommandHandler {
timestamp: new Date().toISOString()
});
await interaction.editReply({
content: '❌ Server registration failed - no server data returned. This might be a database permissions issue.',
ephemeral: true
content: '❌ Server registration failed - no server data returned. This might be a database permissions issue.'
});
return;
}
@@ -234,8 +232,7 @@ class CommandHandler {
}
await interaction.editReply({
content: message,
ephemeral: true
content: message
});
// Log the successful operation
@@ -260,8 +257,7 @@ class CommandHandler {
: '❌ An error occurred while registering the server. Please try again later.';
await interaction.editReply({
content: errorMessage,
ephemeral: true
content: errorMessage
});
throw error;
@@ -446,8 +442,7 @@ class CommandHandler {
// Input validation
if (!teamName || typeof teamName !== 'string') {
await interaction.editReply({
content: '❌ Invalid team name provided.',
ephemeral: false
content: '❌ Invalid team name provided.'
});
return;
}
@@ -460,8 +455,7 @@ class CommandHandler {
if (!sanitizedTeamName) {
await interaction.editReply({
content: '❌ Team name must contain valid characters (letters, numbers, spaces, hyphens, underscores, or periods).',
ephemeral: false
content: '❌ Team name must contain valid characters (letters, numbers, spaces, hyphens, underscores, or periods).'
});
return;
}
@@ -479,8 +473,7 @@ class CommandHandler {
if (!teamData || !teamData.success || !teamData.teams || teamData.teams.length === 0) {
await interaction.editReply({
content: '❌ No teams found matching your search criteria.',
ephemeral: false
content: '❌ No teams found matching your search criteria.'
});
return;
}
@@ -489,8 +482,7 @@ class CommandHandler {
await interaction.editReply({
embeds: embeds,
components: [],
ephemeral: false
components: []
});
} catch (error) {
this.logger.error('Error in handleFindTeam:', {
@@ -564,7 +556,7 @@ class CommandHandler {
await interaction.reply({
content: 'Select menu interactions are not yet implemented.',
flags: ['Ephemeral']
flags: MessageFlags.Ephemeral
});
} catch (error) {
this.logger.error('Error in handleSelectMenu:', {
@@ -640,8 +632,7 @@ class CommandHandler {
} catch (error) {
this.logger.error('Error in handleVersion:', error);
await interaction.editReply({
content: '❌ An error occurred while fetching version information.',
flags: ['Ephemeral']
content: '❌ An error occurred while fetching version information.'
});
}
}
@@ -654,6 +645,97 @@ class CommandHandler {
.setURL(`https://www.vrbattles.gg/profile/${username}`)
);
}
async handleTournaments(interaction) {
try {
this.logger.info('Processing tournaments command', {
userId: interaction.user.id,
guildId: interaction.guildId
});
// Get filter options
const statusFilter = interaction.options.getString('status');
const gameFilter = interaction.options.getString('game');
// Fetch tournaments data
const tournamentsData = await this.playerService.getTournamentsData();
if (!tournamentsData || tournamentsData.success === false) {
await interaction.editReply({
content: '❌ Failed to fetch tournaments data. Please try again later.',
});
return;
}
let tournaments = tournamentsData.data || tournamentsData;
if (!Array.isArray(tournaments)) {
await interaction.editReply({
content: '❌ Invalid tournaments data received from API.',
});
return;
}
// Apply filters
if (statusFilter) {
tournaments = tournaments.filter(t => t.tournament_status === statusFilter);
}
if (gameFilter) {
tournaments = tournaments.filter(t => t.name === gameFilter);
}
if (tournaments.length === 0) {
const filterText = [];
if (statusFilter) filterText.push(`status: ${statusFilter}`);
if (gameFilter) filterText.push(`game: ${gameFilter}`);
const filterMsg = filterText.length > 0 ? ` matching ${filterText.join(', ')}` : '';
await interaction.editReply({
content: `🏆 No tournaments found${filterMsg}.`,
});
return;
}
// Sort tournaments by date (upcoming first, then recent)
tournaments.sort((a, b) => {
const dateA = new Date(a.date_start);
const dateB = new Date(b.date_start);
const now = new Date();
// Upcoming tournaments first
if (dateA > now && dateB > now) {
return dateA - dateB; // Soonest first
}
// Past tournaments last (most recent first)
if (dateA < now && dateB < now) {
return dateB - dateA; // Most recent first
}
// Mix: upcoming before past
if (dateA > now && dateB < now) return -1;
if (dateA < now && dateB > now) return 1;
return dateA - dateB;
});
// Create tournament embed
const embed = EmbedBuilders.createTournamentsEmbed(tournaments, statusFilter, gameFilter);
await interaction.editReply({
embeds: [embed]
});
} catch (error) {
this.logger.error('Error in handleTournaments:', {
error: error.message,
stack: error.stack,
userId: interaction.user.id,
guildId: interaction.guildId
});
await interaction.editReply({
content: '❌ An error occurred while fetching tournaments.',
});
}
}
}
module.exports = CommandHandler;

View File

@@ -44,15 +44,23 @@ async function syncGameChoices() {
const deployPath = path.join(__dirname, '../../deploy-commands.js');
let deployContent = fs.readFileSync(deployPath, 'utf8');
// Pattern to match the game choices in addChoices()
const choicesPattern = /\.addChoices\(\s*([^)]+)\s*\)/g;
// Pattern to match ONLY game choice blocks (avoid tournament status choices)
// Look for game choice patterns in finduser, findteam, matchhistory, subscribe, unsubscribe commands
const gameChoiceCommands = ['finduser', 'findteam', 'matchhistory', 'subscribe', 'unsubscribe'];
gameChoiceCommands.forEach(commandName => {
// Match the specific pattern for each game command
const commandPattern = new RegExp(
`(\\.setName\\("${commandName}"\\)[\\s\\S]*?\\.setName\\("game"\\)[\\s\\S]*?)\\.addChoices\\(\\s*([^)]+)\\s*\\)`,
'g'
);
// Replace all instances with updated choices
const newChoicesBlock = `.addChoices(
${choicesCode}
)`;
deployContent = deployContent.replace(choicesPattern, newChoicesBlock);
deployContent = deployContent.replace(commandPattern, `$1${newChoicesBlock}`);
});
// Write updated file
fs.writeFileSync(deployPath, deployContent);

View File

@@ -76,6 +76,41 @@ class PlayerService {
return `${this.baseUrl}/profile/${username}/stats`;
}
async getTournamentsData() {
try {
console.log('Fetching tournaments data...');
const url = `${this.baseUrl}/api/fetch/tournaments`;
console.log(`API URL: ${url}`);
const response = await axios.get(url, {
timeout: 5000,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
validateStatus: function (status) {
return status >= 200 && status < 300;
}
});
console.log('Tournaments API Response:', JSON.stringify(response.data, null, 2));
return response.data;
} catch (error) {
console.error('Error fetching tournaments data:', {
message: error.message,
response: error.response?.data,
status: error.response?.status
});
return { success: false, error: 'Failed to fetch tournaments data' };
}
}
async getTournamentData(tournamentId) {
const url = `${this.baseUrl}/api/get_tournament_data/${tournamentId}`;
const response = await axios.get(url);
return response.data;
}
async findTeamByName(teamName, gameFilter) {
try {
// Double-check sanitization here as well for defense in depth

View File

@@ -0,0 +1,90 @@
// Test script for tournaments functionality
require('dotenv').config();
const PlayerService = require('../services/PlayerService');
async function testTournamentsAPI() {
console.log('🏆 Testing Tournaments API Functions...\n');
const playerService = new PlayerService();
try {
// Test 1: Get all tournaments
console.log('📋 Test 1: Fetching all tournaments...');
console.log('=' .repeat(50));
const tournamentsData = await playerService.getTournamentsData();
if (tournamentsData && tournamentsData.success !== false) {
console.log('✅ Successfully fetched tournaments data!');
console.log(`📊 Response structure:`, Object.keys(tournamentsData));
// Show some basic info about the tournaments
if (Array.isArray(tournamentsData.tournaments)) {
console.log(`🎯 Found ${tournamentsData.tournaments.length} tournaments`);
// Show first few tournaments as examples
const sampleSize = Math.min(3, tournamentsData.tournaments.length);
console.log(`\n📖 Sample tournaments (showing ${sampleSize}):`);
for (let i = 0; i < sampleSize; i++) {
const tournament = tournamentsData.tournaments[i];
console.log(` ${i + 1}. ${tournament.name || tournament.title || 'Unknown'} (ID: ${tournament.id || 'N/A'})`);
}
} else if (Array.isArray(tournamentsData)) {
console.log(`🎯 Found ${tournamentsData.length} tournaments (direct array)`);
const sampleSize = Math.min(3, tournamentsData.length);
console.log(`\n📖 Sample tournaments (showing ${sampleSize}):`);
for (let i = 0; i < sampleSize; i++) {
const tournament = tournamentsData[i];
console.log(` ${i + 1}. ${tournament.name || tournament.title || 'Unknown'} (ID: ${tournament.id || 'N/A'})`);
}
} else {
console.log('📊 Response data:', JSON.stringify(tournamentsData, null, 2));
}
} else {
console.log('❌ Failed to fetch tournaments data');
console.log('Error:', tournamentsData?.error || 'Unknown error');
}
console.log('\n' + '=' .repeat(50));
// Test 2: Try to get specific tournament data (if we have any tournament IDs)
console.log('\n🎯 Test 2: Testing single tournament fetch...');
console.log('=' .repeat(50));
// Try with a sample tournament ID (you might need to adjust this based on actual data)
const testTournamentId = '1'; // Start with ID 1 as a common starting point
console.log(`📋 Fetching tournament with ID: ${testTournamentId}`);
const singleTournament = await playerService.getTournamentData(testTournamentId);
if (singleTournament && singleTournament.success !== false) {
console.log('✅ Successfully fetched single tournament data!');
console.log('📊 Tournament details:');
console.log(` Name: ${singleTournament.name || singleTournament.title || 'Unknown'}`);
console.log(` Game: ${singleTournament.game || singleTournament.game_name || 'Unknown'}`);
console.log(` Status: ${singleTournament.status || 'Unknown'}`);
} else {
console.log(`❌ Failed to fetch tournament with ID: ${testTournamentId}`);
console.log('Error:', singleTournament?.error || 'Unknown error');
console.log('💡 This might be normal if tournament ID 1 doesn\'t exist');
}
} catch (error) {
console.error('❌ Test failed with error:', {
message: error.message,
stack: error.stack
});
}
console.log('\n🏁 Tournament API testing completed!');
}
// Run the test
if (require.main === module) {
testTournamentsAPI().catch(console.error);
}
module.exports = { testTournamentsAPI };

View File

@@ -353,6 +353,141 @@ class EmbedBuilders {
return embed;
}
static createTournamentsEmbed(tournaments, statusFilter, gameFilter) {
const embed = new EmbedBuilder()
.setTitle('🏆 VRBattles Tournaments')
.setColor('#FFD700') // Gold color for tournaments
.setTimestamp();
// Add filter info in description if filters are applied
let description = '';
const filterInfo = [];
if (statusFilter) filterInfo.push(`**Status:** ${statusFilter}`);
if (gameFilter) filterInfo.push(`**Game:** ${gameFilter}`);
if (filterInfo.length > 0) {
description = `*Filtered by: ${filterInfo.join(' | ')}*\n\n`;
}
description += `Found **${tournaments.length}** tournament${tournaments.length !== 1 ? 's' : ''}`;
embed.setDescription(description);
// Group tournaments by status for better organization
const grouped = {
'Published': [],
'In-progress': [],
'Ended': [],
'Unpublished': []
};
tournaments.forEach(tournament => {
const status = tournament.tournament_status || 'Unknown';
if (!grouped[status]) grouped[status] = [];
grouped[status].push(tournament);
});
// Add tournaments by status (show active ones first)
const statusOrder = ['Published', 'In-progress', 'Ended', 'Unpublished'];
statusOrder.forEach(status => {
if (grouped[status].length === 0) return;
// Status emoji mapping
const statusEmoji = {
'Published': '📢',
'In-progress': '⚡',
'Ended': '🏁',
'Unpublished': '📝'
};
const emoji = statusEmoji[status] || '🎯';
embed.addFields({
name: `${emoji} ${status} Tournaments (${grouped[status].length})`,
value: '\u200B',
inline: false
});
// Add each tournament in this status
grouped[status].slice(0, 10).forEach(tournament => {
const tournamentName = tournament.tournament_name || 'Unknown Tournament';
const gameName = tournament.name || 'Unknown Game';
const prize = tournament.prize ? `$${tournament.prize}` : 'No prize';
const entryFee = tournament.entry_cost === '0' ? 'Free' : `$${tournament.entry_cost}`;
const teamSize = tournament.team_size_local || tournament.team_size || 'Unknown';
const maxTeams = tournament.max_teams || 'Unknown';
const tournamentType = tournament.type || 'Unknown';
// Format date
let dateInfo = '';
if (tournament.date_start) {
const startDate = new Date(tournament.date_start);
const now = new Date();
if (startDate > now) {
dateInfo = `🗓️ Starts: <t:${Math.floor(startDate.getTime() / 1000)}:R>`;
} else {
dateInfo = `🗓️ Started: <t:${Math.floor(startDate.getTime() / 1000)}:R>`;
}
}
// Build tournament info
const tournamentInfo = [
`🎮 **${gameName}**`,
`💰 Prize: **${prize}** | Entry: **${entryFee}**`,
`👥 **${teamSize}** (Max: ${maxTeams}) | Type: **${tournamentType}**`,
dateInfo
].filter(info => info).join('\n');
embed.addFields({
name: `**${tournamentName}** (ID: ${tournament.id})`,
value: tournamentInfo,
inline: true
});
});
// Add a separator if there are more statuses coming
const currentIndex = statusOrder.indexOf(status);
const hasMoreStatuses = statusOrder.slice(currentIndex + 1).some(s => grouped[s].length > 0);
if (hasMoreStatuses) {
embed.addFields({ name: '\u200B', value: '▬▬▬▬▬▬▬▬▬▬', inline: false });
}
});
// Add footer with helpful info
const totalTournaments = tournaments.length;
if (totalTournaments > 30) {
embed.setFooter({
text: `Showing first 30 tournaments. Total: ${totalTournaments} | Use filters to narrow results`
});
} else {
embed.setFooter({
text: 'Use /tournaments status:<filter> or /tournaments game:<filter> to filter results'
});
}
// Set thumbnail based on game filter or use default
if (gameFilter) {
// You could add game-specific thumbnails here
switch (gameFilter) {
case 'VAIL':
embed.setThumbnail('https://www.vrbattles.gg/assets/images/vrb%20image%20assets/--%20Logos/VAIL_Icon_Color.png');
break;
case 'Echo Arena':
embed.setThumbnail('https://www.vrbattles.gg/assets/images/vrb%20image%20assets/echovr.png');
break;
case 'Blacktop Hoops':
embed.setThumbnail('https://www.vrbattles.gg/assets/images/vrb%20image%20assets/Blacktop%20Hoops.png');
break;
default:
embed.setThumbnail('https://www.vrbattles.gg/assets/images/vrb%20image%20assets/--%20Logos/VAIL_Icon_Color.png');
}
} else {
embed.setThumbnail('https://www.vrbattles.gg/assets/images/vrb%20image%20assets/--%20Logos/VAIL_Icon_Color.png');
}
return embed;
}
}
module.exports = EmbedBuilders;