UNPKG

@zerospacegg/vynthra

Version:
438 lines (384 loc) • 15 kB
import type { BotSubcommand } from "./types.js"; import type { SlashCommandSubcommandBuilder, ChatInputCommandInteraction, } from "discord.js"; /** * Helper function to create a custom subcommand for the bot * * @param name - The name of the subcommand * @param description - The description of the subcommand * @param builder - Function to configure the subcommand options * @param execute - Function to handle the subcommand execution * @returns A BotSubcommand object ready to be registered * * @example * ```typescript * const mySubcommand = createSubcommand( * "mycommand", * "My custom command description", * (subcommand) => subcommand * .addStringOption(option => * option * .setName("input") * .setDescription("Some input") * .setRequired(true) * ), * async (interaction) => { * const input = interaction.options.getString("input", true); * await interaction.reply(`You said: ${input}`); * } * ); * ``` */ export function createSubcommand( name: string, description: string, builder: ( subcommand: SlashCommandSubcommandBuilder, ) => SlashCommandSubcommandBuilder, execute: (interaction: ChatInputCommandInteraction) => Promise<void>, ): BotSubcommand { return { name, description, builder: (subcommand) => builder(subcommand.setName(name).setDescription(description)), execute, }; } /** * Helper function to create a simple subcommand with just a query parameter * This is useful for commands that follow the same pattern as the stats command * * @param name - The name of the subcommand * @param description - The description of the subcommand * @param queryDescription - Description for the query parameter * @param execute - Function to handle the subcommand execution * @returns A BotSubcommand object ready to be registered * * @example * ```typescript * const searchSubcommand = createQuerySubcommand( * "search", * "Search for something", * "What to search for", * async (interaction) => { * const query = interaction.options.getString("query", true); * // Handle search logic here * await interaction.reply(`Searching for: ${query}`); * } * ); * ``` */ export function createQuerySubcommand( name: string, description: string, queryDescription: string, execute: (interaction: ChatInputCommandInteraction) => Promise<void>, ): BotSubcommand { return createSubcommand( name, description, (subcommand) => subcommand.addStringOption((option) => option .setName("query") .setDescription(queryDescription) .setRequired(true), ), execute, ); } /** * Validates that a subcommand has all required properties * * @param subcommand - The subcommand to validate * @throws Error if the subcommand is invalid */ export function validateSubcommand(subcommand: BotSubcommand): void { if (!subcommand.name || typeof subcommand.name !== "string") { throw new Error("Subcommand must have a valid name"); } if (!subcommand.description || typeof subcommand.description !== "string") { throw new Error("Subcommand must have a valid description"); } if (!subcommand.builder || typeof subcommand.builder !== "function") { throw new Error("Subcommand must have a valid builder function"); } if (!subcommand.execute || typeof subcommand.execute !== "function") { throw new Error("Subcommand must have a valid execute function"); } // Validate command name follows Discord's naming rules if (!/^[a-z][a-z0-9_-]{0,31}$/.test(subcommand.name)) { throw new Error( "Subcommand name must be 1-32 characters long, start with a lowercase letter, and contain only lowercase letters, numbers, hyphens, and underscores", ); } // Validate description length (Discord limit is 100 characters) if (subcommand.description.length > 100) { throw new Error("Subcommand description must be 100 characters or less"); } } /** * Validates an array of subcommands * * @param subcommands - Array of subcommands to validate * @throws Error if any subcommand is invalid or if there are duplicate names */ export function validateSubcommands(subcommands: BotSubcommand[]): void { const names = new Set<string>(); for (const subcommand of subcommands) { validateSubcommand(subcommand); if (names.has(subcommand.name)) { throw new Error(`Duplicate subcommand name: ${subcommand.name}`); } names.add(subcommand.name); } } /** * ZS.GG faction colors for Discord embeds */ const FACTION_COLORS = { // Main factions 'grell': 0x71e725, // --gg-lime 'protectorate': 0x147ced, // --gg-sapphire 'legion': 0xde4a49, // --gg-rust // Mercenary factions 'arandi': 0x5a11ae, // --gg-purple 'chakru': 0x2cb1e3, // --gg-aqua 'dread': 0xfb2a43, // --gg-ruby 'koru': 0xc8ef24, // --gg-pickle 'marran': 0xed5f13, // --gg-orange 'sanctuary': 0x06c998, // --gg-teal 'valkaru': 0x90409a, // --gg-plum 'xol': 0xf5146e, // --gg-rose // Neutral/other 'neutral': 0x697380, // --gg-steel 'jungle-ai': 0xecca0d, // --gg-banana // ZS.GG brand colors 'zsgg': 0x920075, // --gg-fuchsia 'zsgg-alt': 0xdc8642, // --gg-tangerine }; /** * Get faction color for Discord embeds */ export function getFactionColor(factionName?: string): number { if (!factionName) return FACTION_COLORS.neutral; return FACTION_COLORS[factionName.toLowerCase() as keyof typeof FACTION_COLORS] || FACTION_COLORS.neutral; } /** * Render search result as Discord embed */ export function renderSearchResultAsEmbed(searchResult: any, query: string): { embeds: any[] } { const { type, entity, fullEntity, matches } = searchResult; if (type === "single") { return renderEntityAsEmbed(entity, fullEntity); } else if (type === "multi") { return renderMultipleResultsAsEmbed(matches, query); } else if (type === "none") { return renderNoResultsAsEmbed(query); } // Fallback return { embeds: [{ color: FACTION_COLORS.zsgg, title: "šŸ“Š Stats Search Results", description: `Search query: **${query}**`, timestamp: new Date().toISOString(), }] }; } function renderEntityAsEmbed(entity: any, fullEntity: any): { embeds: any[] } { const entityData = fullEntity || entity; // Get entity type icon let typeIcon = "šŸ“¦"; if (entityData?.subtype === "hero") typeIcon = "⭐"; else if (entityData?.type === "unit") typeIcon = "šŸŖ–"; else if (entityData?.type === "building") typeIcon = "šŸ—ļø"; else if (entityData?.type === "faction") typeIcon = "āš”ļø"; else if (entityData?.type === "map") typeIcon = "šŸ—ŗļø"; else if (entityData?.type === "mercenary") typeIcon = "šŸ¤"; const title = `${typeIcon} ${entityData?.name || "Unknown"}`; const color = getFactionColor(entityData?.faction); let description = entityData?.typeDesc || `${entityData?.type || "Unknown"} • ${entityData?.factionName || entityData?.faction || "Various"}`; // Add basic stats if available const statParts: string[] = []; if (entityData?.hp) statParts.push(`ā¤ļø HP ${entityData.hp}`); if (entityData?.speed) statParts.push(`⚔ Speed ${entityData.speed}`); if (entityData?.supply) statParts.push(`šŸž Supply ${entityData.supply}`); if (statParts.length > 0) { description += `\n\n**Stats:** ${statParts.join(' • ')}`; } // Add cost info if available const costParts: string[] = []; if (entityData?.buildTime) costParts.push(`ā±ļø ${entityData.buildTime}s build`); if (entityData?.rebuildTime && entityData.rebuildTime !== entityData.buildTime) { costParts.push(`šŸ”„ ${entityData.rebuildTime}s rebuild`); } if (costParts.length > 0) { description += `\n\n**Cost:** ${costParts.join(' • ')}`; } // Add key abilities if available if (entityData?.attacks && Object.keys(entityData.attacks).length > 0) { const attack = Object.values(entityData.attacks)[0] as any; if (attack?.damage && attack?.cooldown) { const dps = Math.round(attack.damage / attack.cooldown * 10) / 10; description += `\n\n**Attack:** ${attack.damage} dmg (${dps} DPS)`; if (attack.range) description += ` • šŸŽÆ ${attack.range} range`; } } // Add spells if available if (entityData?.spells && Object.keys(entityData.spells).length > 0) { const spells = Object.values(entityData.spells).slice(0, 5); // Limit to 5 abilities description += `\n\n**Abilities:**`; spells.forEach((spell: any) => { const spellParts: string[] = []; // Add cooldown if available if (spell.cooldown) spellParts.push(`ā±ļø ${spell.cooldown}s`); // Add targets if available if (spell.targets && spell.targets.length > 0) { const targetStr = spell.targets.join(', ').replace(/:/g, ' '); spellParts.push(`šŸŽÆ ${targetStr}`); } // Add damage if available if (spell.damage) spellParts.push(`āš”ļø ${spell.damage} dmg`); // Add healing if available if (spell.healing) spellParts.push(`ā¤ļø ${spell.healing} heal`); // Build the ability line const statsStr = spellParts.length > 0 ? ` (${spellParts.join(' • ')})` : ''; const desc = spell.description ? ` - ${spell.description}` : ''; description += `\n• **${spell.name}**${statsStr}${desc}`; }); } // Add tags (simplified) if (entityData?.tags && entityData.tags.length > 0) { const importantTags = entityData.tags.filter((tag: string) => !tag.startsWith('faction:') && !tag.startsWith('tier:') && tag !== 'unit' && tag !== 'building' ).slice(0, 5); if (importantTags.length > 0) { description += `\n\n**šŸ·ļø Tags:** ${importantTags.join(' • ')}`; } } // Add ZS.GG link if available if (entityData?.zsggPath) { description += `\n\nšŸ”— [View on ZS.GG](${entityData.zsggPath})`; } return { embeds: [{ color, title, description, timestamp: new Date().toISOString(), }] }; } function renderMultipleResultsAsEmbed(matches: any[], query: string): { embeds: any[] } { const title = `šŸ“Š Multiple Results (${matches.length})`; const description = `Found ${matches.length} matches for **${query}**\n\n` + matches.slice(0, 10).map((match, index) => { const typeEmoji = match.type === "unit" ? "šŸŖ–" : match.type === "building" ? "šŸ—ļø" : match.type === "map" ? "šŸ—ŗļø" : "šŸ“¦"; return `${index + 1}. ${typeEmoji} **${match.name}** (${match.type})`; }).join('\n') + (matches.length > 10 ? `\n\n*...and ${matches.length - 10} more*` : '') + '\n\nšŸ’” *Try a more specific search term to get detailed info*'; return { embeds: [{ color: 0xf39c12, // Orange for multiple results title, description, timestamp: new Date().toISOString(), }] }; } function renderNoResultsAsEmbed(query: string): { embeds: any[] } { return { embeds: [{ color: 0xe74c3c, // Red for no results title: "āŒ No Results Found", description: `No matches found for **${query}**.\n\nšŸ’” *Try different search terms or check spelling*`, timestamp: new Date().toISOString(), }] }; } /** * Render match response as Discord embed */ export function renderMatchResponseAsEmbed(result: any, generationTime: number): { embeds: any[] } { const { gameSet } = result; const { gameType, gamesCount, games, players } = gameSet; const mainEmbed = { color: FACTION_COLORS.zsgg, title: "🌟⚔ I'Olin's All-Random MatchMaker Results! šŸŽ®āœØ", description: `**${gameType.toUpperCase()}** (${getGameTypeDescription(gameType)}) • Best of ${gamesCount} • ${players.join(", ")}`, timestamp: new Date().toISOString(), footer: { text: `Generated in ${generationTime}ms • Portal hop complete! Ready for cosmic domination!`, }, fields: [] as any[] }; // Add each game as a field for (let i = 0; i < games.length; i++) { const game = games[i]; let fieldValue = `**Host:** ${game.host}\n**Map:** ${getEntityName(game.map)}\n\n`; if (gameType === "2v2") { // Special formatting for 2v2 teams for (let teamIdx = 0; teamIdx < 2; teamIdx++) { const teamName = teamIdx === 0 ? "Team 1" : "Team 2"; fieldValue += `**${teamName}:**\n`; for (let playerIdx = 0; playerIdx < 2; playerIdx++) { const gamePlayerIdx = teamIdx * 2 + playerIdx; const player = game.players[gamePlayerIdx]; const playerName = players[gamePlayerIdx]; const factionName = getFactionEmoji(player.faction) + " " + getEntityName(player.faction); const mercName = getMercEmoji(player.merc) + " " + getEntityName(player.merc); const heroName = "⭐ " + getEntityName(player.hero); fieldValue += `${playerName}: ${factionName} + ${mercName} → ${heroName}\n`; } fieldValue += "\n"; } } else { // Standard formatting for other game types for (let j = 0; j < game.players.length; j++) { const player = game.players[j]; const playerName = players[j]; const factionName = getFactionEmoji(player.faction) + " " + getEntityName(player.faction); const mercName = getMercEmoji(player.merc) + " " + getEntityName(player.merc); const heroName = "⭐ " + getEntityName(player.hero); fieldValue += `**${playerName}:** ${factionName} + ${mercName} → ${heroName}\n`; } } mainEmbed.fields.push({ name: `Game ${i + 1}`, value: fieldValue.trim(), inline: games.length > 1 ? true : false }); } return { embeds: [mainEmbed] }; } // Helper functions for match embed rendering function getGameTypeDescription(gameType: string): string { switch (gameType) { case "solo": return "Practice vs AI"; case "1v1": return "Two players"; case "2v2": return "Four players, teams"; case "ffa": return "Four players, free for all"; default: return gameType; } } function getEntityName(entityId: string): string { const parts = entityId.split("/"); const name = parts[parts.length - 1]; return name .split("-") .map(word => word.charAt(0).toUpperCase() + word.slice(1)) .join(" "); } function getFactionEmoji(factionId: string): string { if (factionId.includes("grell")) return "🧬"; if (factionId.includes("protectorate")) return "šŸŽ–ļø"; if (factionId.includes("legion")) return "šŸ”„"; return "āš”ļø"; } function getMercEmoji(mercId: string): string { if (mercId.includes("arandi")) return "🌟"; if (mercId.includes("valkaru")) return "šŸ”Ø"; if (mercId.includes("marran")) return "šŸŽÆ"; if (mercId.includes("chakru")) return "šŸ¦€"; if (mercId.includes("dread")) return "šŸ’€"; return "šŸ¤"; }