UNPKG

@zerospacegg/vynthra

Version:
642 lines (533 loc) • 15.9 kB
import type { Ability, Attack, Building, Entity, Heal, Map, Unit } from "@zerospacegg/iolin"; import { getSearchIndex } from "@zerospacegg/iolin/meta/search-index"; import chalk from "chalk"; import type { SearchResult } from "./types.js"; /** * Emoji mappings for different entity types and stats */ const EMOJIS = { // Entity types faction: "šŸ›ļø", unit: "āš”ļø", building: "šŸ—ļø", map: "šŸ—ŗļø", mission: "šŸŽÆ", // Unit stats health: "ā¤ļø", speed: "⚔", vision: "šŸ‘ļø", supply: "šŸž", damage: "āš”ļø", range: "šŸŽÆ", armor: "šŸ›”ļø", shields: "šŸ”®", // Resources hexite: "šŸ’Ž", flux: "🟣", energy: "šŸ”‹", buildTime: "ā±ļø", // Abilities cooldown: "ā±ļø", duration: "ā°", energyCost: "šŸ”‹", classicEnergy: "šŸ”‹", abesEnergy: "⚔", topbarEnergy: "šŸ”“", healthEnergy: "ā¤ļø", dps: "šŸ”„", splash: "šŸ’„", bonus: "šŸŽÆ", // Domains ground: "šŸŒ", air: "ā˜ļø", // Tiers T0: "0ļøāƒ£", T1: "1ļøāƒ£", T2: "2ļøāƒ£", T3: "3ļøāƒ£", "T1.5": "šŸ”ø", "T2.5": "šŸ”¹", "T3.5": "šŸ”·", T4: "4ļøāƒ£", // General check: "āœ…", cross: "āŒ", arrow: "āžœ", info: "ā„¹ļø", link: "šŸ”—", shield: "šŸ›”ļø", sword: "āš”ļø", lightning: "⚔", tag: "šŸ·ļø", creates: "šŸ­", createdBy: "šŸ”§", unlocks: "šŸ”“", unlockedBy: "šŸ”’", upgrades: "ā¬†ļø", biomass: "🌱", mapInfo: "šŸ—ŗļø", players: "šŸ‘„", towers: "šŸ—¼", ladder: "šŸ†", }; /** * Format duration in seconds to readable format */ function formatDuration(seconds: number): string { return `${seconds}s`; } /** * Calculate DPS from damage and cooldown */ function calculateDPS(damage: number, cooldown: number): number { if (cooldown <= 0) return 0; return damage / cooldown; } /** * Get energy emoji based on energy type */ function getEnergyEmoji(energyType?: string): string { switch (energyType) { case "classic": return EMOJIS.classicEnergy; case "abes": return EMOJIS.abesEnergy; case "topbar": return EMOJIS.topbarEnergy; case "health": return EMOJIS.healthEnergy; default: return EMOJIS.energy; } } /** * Get entity display name from search index */ function getEntityDisplayName(entityIdOrSlug: string): string { const searchIndex = getSearchIndex(); // Try direct lookup first let entity = searchIndex.all[entityIdOrSlug]; if (entity) return entity.name; // check by slug const fullId = searchIndex.ids[entityIdOrSlug]; if (fullId) { entity = searchIndex.all[fullId]; if (entity) return entity.name; } // Fallback to the original string, formatted return entityIdOrSlug .replace(/-/g, " ") .replace(/\b\w/g, (l) => l.toUpperCase()); } /** * Get faction display name from search index */ function getFactionDisplayName(factionSlug: string): string { const searchIndex = getSearchIndex(); const factionEntity = searchIndex.all[`faction/${factionSlug}`]; return factionEntity ? factionEntity.name : factionSlug; } /** * Render entity basic information as rich markdown */ function renderEntityBasic(entity: any): string { const parts: string[] = []; // Title with emoji and info in one line const typeEmoji = EMOJIS[entity.type as keyof typeof EMOJIS] || EMOJIS.info; let title = `# ${typeEmoji} **${entity.name}**`; // Build the info section const infoParts: string[] = []; // Add faction name first if available if (entity.faction) { const factionName = getFactionDisplayName(entity.faction); infoParts.push(factionName); } // Add tier if available if (entity.tier) { infoParts.push(entity.tier); } // Add type and subtype let typeText = entity.type; if (entity.subtype) { typeText = `${entity.subtype} ${entity.type}`; } // Capitalize each word typeText = typeText .split(" ") .map((word: string) => word.charAt(0).toUpperCase() + word.slice(1)) .join(" "); infoParts.push(typeText); // Combine title and info if (infoParts.length > 0) { title += ` • ${infoParts.join(" ")}`; } parts.push(title); return parts.join("\n"); } /** * Render cost information (simplified for now) */ function renderCost(entity: any): string { const costParts: string[] = []; // Core resource costs if (entity.hexiteCost) { costParts.push(`${EMOJIS.hexite} ${entity.hexiteCost} hexite`); } if (entity.fluxCost) { costParts.push(`${EMOJIS.flux} ${entity.fluxCost} flux`); } // Build time (separate from resource costs) if (entity.buildTime) { costParts.push( `${EMOJIS.buildTime} ${formatDuration(entity.buildTime)} build` ); } if (costParts.length === 0) return ""; return `**Cost:** ${costParts.join(" • ")}`; } /** * Render tags as simple display (simplified for now) */ function renderTags(entity: any): string { if (!entity.tags || entity.tags.length === 0) return ""; const displayTags = entity.tags .filter((tag: string) => !tag.startsWith("tier:") && !tag.startsWith("faction:")) .slice(0, 8); // Limit to avoid overwhelming display if (displayTags.length === 0) return ""; return `**${EMOJIS.tag} Tags:** ${displayTags.join(" • ")}`; } /** * Render basic unit stats (simplified - no combat for now) */ function renderUnitBasic(unit: Unit): string { const parts: string[] = []; const basicParts: string[] = []; // Basic stats that should be safe to access if (unit.hp) { basicParts.push(`${EMOJIS.health} HP ${unit.hp}`); } if (unit.speed) { basicParts.push(`${EMOJIS.speed} Speed ${unit.speed}`); } if (unit.supply) { basicParts.push(`${EMOJIS.supply} Supply ${unit.supply}`); } if (basicParts.length > 0) { parts.push(`**Stats:** ${basicParts.join(" • ")}`); } return parts.join("\n"); } /** * Render building basic stats (enhanced) */ function renderBuildingBasic(building: Building): string { const parts: string[] = []; const basicParts: string[] = []; if (building.hp) { basicParts.push(`${EMOJIS.health} HP ${building.hp}`); } if (building.providesSupply) { basicParts.push(`${EMOJIS.supply} +${building.providesSupply} supply`); } if (building.providesBiomass) { basicParts.push(`${EMOJIS.biomass} +${building.providesBiomass} biomass`); } if (building.gathersFlux) { basicParts.push(`${EMOJIS.flux} ${building.gathersFlux} flux/min`); } if (building.gathersRichFlux) { basicParts.push(`${EMOJIS.flux} ${building.gathersRichFlux} rich flux/min`); } if (basicParts.length > 0) { parts.push(`**Stats:** ${basicParts.join(" • ")}`); } return parts.join("\n"); } /** * Render map details (new for Phase 5) */ function renderMapDetails(map: Map): string { const parts: string[] = []; const mapParts: string[] = []; if (map.players) { mapParts.push(`${EMOJIS.players} ${map.players} Players`); } if (map.xpTowers) { mapParts.push(`${EMOJIS.towers} ${map.xpTowers} XP Towers`); } if (map.fluxDistance) { mapParts.push(`${EMOJIS.flux} Flux Distance: ${map.fluxDistance}`); } if (map.mapSize) { mapParts.push(`${EMOJIS.mapInfo} Size: ${map.mapSize}`); } if (map.inLadderPool) { mapParts.push(`${EMOJIS.ladder} In Ladder Pool`); } if (mapParts.length > 0) { parts.push(`**Map Info:** ${mapParts.join(" • ")}`); } return parts.join("\n"); } /** * Render building production capabilities (new for Phase 5) */ function renderBuildingProduction(building: Building): string { const parts: string[] = []; // Units this building can produce if (building.produces && building.produces.length > 0) { const unitsList = building.produces .map((slug) => getEntityDisplayName(slug)) .join(", "); parts.push(`**${EMOJIS.creates} Produces:** ${unitsList}`); } // Buildings this building can create if (building.creates && building.creates.length > 0) { const buildingsList = building.creates .map((slug) => getEntityDisplayName(slug)) .join(", "); parts.push(`**${EMOJIS.creates} Creates:** ${buildingsList}`); } // What this building unlocks if (building.unlocks && building.unlocks.length > 0) { const unlocksList = building.unlocks .map((slug) => getEntityDisplayName(slug)) .join(", "); parts.push(`**${EMOJIS.unlocks} Unlocks:** ${unlocksList}`); } return parts.join("\n"); } /** * Render weapon/ability information in compact list format */ function renderAbility(ability: Ability, index: number): string { const parts: string[] = []; // Ability header with name and key stats let header = `• **${ability.name || `Ability ${index + 1}`}**`; // Core stats in parentheses const coreStats: string[] = []; // Basic damage/healing stats - need to cast to specific types if (ability.abilityType === "attack") { const attack = ability as Attack; if (attack.damage) { coreStats.push(`${EMOJIS.damage} ${attack.damage}`); } } if (ability.abilityType === "heal") { const heal = ability as Heal; if (heal.healAmount) { coreStats.push(`ā¤ļø ${heal.healAmount}`); } } if (ability.cooldown) { coreStats.push(`${EMOJIS.cooldown} ${formatDuration(ability.cooldown)}`); } if (ability.range) { coreStats.push(`${EMOJIS.range} ${ability.range}`); } if (ability.energyCost) { const energyEmoji = getEnergyEmoji(ability.energyType); coreStats.push(`${energyEmoji} ${ability.energyCost}`); } if (coreStats.length > 0) { header += ` (${coreStats.join(" • ")})`; } parts.push(header); // Description if available if (ability.description) { parts.push(` *${ability.description}*`); } // Additional effects and bonuses const effects: string[] = []; // DPS calculation for attacks if (ability.abilityType === "attack" && ability.cooldown) { const attack = ability as Attack; if (attack.damage) { const dps = calculateDPS(attack.damage, ability.cooldown); effects.push(`${EMOJIS.dps} ${dps.toFixed(1)} DPS`); } } // Duration if (ability.duration) { effects.push(`Duration: ${formatDuration(ability.duration)}`); } // Targets if (ability.targets && ability.targets.length > 0) { effects.push(`Targets: ${ability.targets.join(", ")}`); } if (effects.length > 0) { parts.push(` ${effects.join(" • ")}`); } return parts.join("\n"); } /** * Render abilities from ability collections */ function renderAbilities(entity: Unit): string { const parts: string[] = []; let abilityIndex = 0; // Render attacks const attacks = Object.values(entity.attacks || {}); if (attacks.length > 0) { parts.push("\n## Attacks"); attacks.forEach((attack) => { parts.push(renderAbility(attack, abilityIndex++)); }); } // Render heals const heals = Object.values(entity.heals || {}); if (heals.length > 0) { parts.push("\n## Heals"); heals.forEach((heal) => { parts.push(renderAbility(heal, abilityIndex++)); }); } // Render spells const spells = Object.values(entity.spells || {}); if (spells.length > 0) { parts.push("\n## Spells"); spells.forEach((spell) => { parts.push(renderAbility(spell, abilityIndex++)); }); } // Render passives const passives = Object.values(entity.passives || {}); if (passives.length > 0) { parts.push("\n## Passives"); passives.forEach((passive) => { parts.push(renderAbility(passive, abilityIndex++)); }); } // Render sieges const sieges = Object.values(entity.sieges || {}); if (sieges.length > 0) { parts.push("\n## Siege Abilities"); sieges.forEach((siege) => { parts.push(renderAbility(siege, abilityIndex++)); }); } return parts.join("\n"); } /** * Render full entity data with basic stats (Phase 3 - simplified) */ function renderEntityFull(entity: any, fullEntity: Entity): string { const parts: string[] = []; // Basic info parts.push(renderEntityBasic(entity)); if (!fullEntity || fullEntity === entity) { return parts.join("\n\n"); } // Basic stats for units (no combat details yet) if (entity.type === "unit") { const unitStats = renderUnitBasic(fullEntity as Unit); if (unitStats) parts.push(unitStats); } // Basic stats for buildings if (entity.type === "building") { const buildingStats = renderBuildingBasic(fullEntity as Building); if (buildingStats) parts.push(buildingStats); } // Map details for maps if (entity.type === "map") { const mapDetails = renderMapDetails(fullEntity as Map); if (mapDetails) parts.push(mapDetails); } // Cost information (only for gamepieces) if (entity.type === "unit" || entity.type === "building") { const costInfo = renderCost(fullEntity); if (costInfo) parts.push(costInfo); } // Tags (simplified) const tagsDisplay = renderTags(fullEntity); if (tagsDisplay) parts.push(tagsDisplay); // Abilities for units if (entity.type === "unit") { const abilities = renderAbilities(fullEntity as Unit); if (abilities) parts.push(abilities); } // Production capabilities for buildings if (entity.type === "building") { const production = renderBuildingProduction(fullEntity as Building); if (production) parts.push(production); } // Footer with link if (entity.id) { const url = `https://zerospace.gg/library/${entity.id}`; const entityType = entity.type === "unit" ? "unit" : entity.type; parts.push( `\n${EMOJIS.link} See full ${entityType} info: <${url}>` ); } return parts.join("\n\n"); } /** * Render search results to markdown */ export function renderSearchResult(result: SearchResult): string { switch (result.type) { case "single": return renderEntityFull(result.entity, result.fullEntity); case "multi": const parts: string[] = [ `# ${EMOJIS.info} Multiple Matches Found (${result.matches.length})`, "", ]; result.matches.forEach((match, index) => { const typeEmoji = EMOJIS[match.type as keyof typeof EMOJIS] || EMOJIS.info; parts.push(`${index + 1}. ${typeEmoji} **${match.name}** (${match.type})`); }); parts.push(""); parts.push("šŸ’” *Try a more specific search term to get detailed info*"); return parts.join("\n"); case "none": return `# ${EMOJIS.cross} No Results Found\n\nNo matches found for "${result.query}".\n\nšŸ’” *Try different search terms or check spelling*`; } } /** * Render markdown to terminal with colors */ export function renderToTerminal(markdown: string): string { const lines = markdown.split("\n"); const formattedLines: string[] = []; for (const line of lines) { // Handle headers if (line.startsWith("# ")) { formattedLines.push(chalk.bold.blue(line.slice(2))); } else if (line.startsWith("## ")) { formattedLines.push(chalk.bold.green(line.slice(3))); } else if (line.startsWith("### ")) { formattedLines.push(chalk.bold.yellow(line.slice(4))); } else if (line.startsWith("**") && line.endsWith("**")) { // Bold text formattedLines.push(chalk.bold(line.slice(2, -2))); } else if (line.startsWith("*") && line.endsWith("*")) { // Italic text formattedLines.push(chalk.italic(line.slice(1, -1))); } else { formattedLines.push(line); } } return formattedLines.join("\n"); } /** * Render search results to terminal with colors */ export function renderSearchResultToTerminal(result: SearchResult): string { const markdown = renderSearchResult(result); return renderToTerminal(markdown); } /** * Render search results as markdown (passthrough) */ export function renderSearchResultAsMarkdown(result: SearchResult): string { return renderSearchResult(result); }