@zerospacegg/vynthra
Version:
Discord bot for ZeroSpace.gg data
642 lines (533 loc) ⢠15.9 kB
text/typescript
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);
}