UNPKG

@zerospacegg/anrubic

Version:

Anrubic - ZeroSpace.gg MCP Server for AI agents to access game data

373 lines (324 loc) 10.5 kB
import { readFileSync } from "fs"; import { dirname, join } from "path"; import { fileURLToPath } from "url"; // Types for mechanics system export interface BasicMechanic { term: string; keywords: string[]; description: string; } export interface Transformation { name: string; type: string; category: string; keywords: string[]; description: string; stackable: boolean; maxStacks: number; conflictsWith: string[]; requirements: { eligibility: { allowedSubtypes: string[]; excludedTags: string[]; }; cost: { formula?: string; supplyMultiplier?: number; baseMultiplier?: number; hexiteMultiplier?: number; fluxMultiplier?: number; }; preventedByTags: string[]; }; statModifications?: { hpMultiplier?: number; damageMultiplier?: number; }; hasPhases: boolean; preDeathPhase?: { speedMultiplier: number; cooldownMultiplier: number; }; postDeathPhase?: { speedMultiplier: number; cooldownMultiplier: number; }; triggersOnDeath: boolean; tagsAdded: string[]; tagsRemoved: string[]; notes?: string; } export interface MechanicsData { mechanics: { basicMechanics: Record<string, BasicMechanic>; transformations: Record<string, Transformation>; }; } export interface FlatMechanic { key: string; category: "basic" | "transformation"; name: string; keywords: string[]; description: string; data: BasicMechanic | Transformation; } export class MarkdownFormatter { /** * Format a mechanic as markdown for MCP responses */ static formatMechanic(mechanic: FlatMechanic, detailed: boolean = false): string { let output = `# ${mechanic.name}`; if (mechanic.category === "transformation") { output += " Transformation"; } output += `\n\n`; if (mechanic.keywords.length > 0) { output += `**Keywords**: ${mechanic.keywords.join(", ")}\n\n`; } output += `**Description**: ${mechanic.description}\n\n`; if (detailed && mechanic.category === "transformation") { const transform = mechanic.data as Transformation; output += this.formatTransformationDetails(transform); } return output.trim(); } /** * Format transformation-specific details */ private static formatTransformationDetails(transform: Transformation): string { let output = ""; output += `**Type**: ${transform.type}\n`; output += `**Stackable**: ${transform.stackable ? "Yes" : "No"}`; if (transform.maxStacks) { output += ` (max ${transform.maxStacks})`; } output += `\n\n`; if (transform.statModifications) { output += `**Stat Modifications**:\n`; if (transform.statModifications.hpMultiplier) { output += `- HP: ${transform.statModifications.hpMultiplier}x\n`; } if (transform.statModifications.damageMultiplier) { output += `- Damage: ${transform.statModifications.damageMultiplier}x\n`; } output += `\n`; } if (transform.hasPhases && (transform.preDeathPhase || transform.postDeathPhase)) { output += `**Phases**:\n`; if (transform.preDeathPhase) { output += `- Pre-death: Speed ${transform.preDeathPhase.speedMultiplier}x, Cooldowns ${transform.preDeathPhase.cooldownMultiplier}x\n`; } if (transform.postDeathPhase) { output += `- Post-death: Speed ${transform.postDeathPhase.speedMultiplier}x, Cooldowns ${transform.postDeathPhase.cooldownMultiplier}x\n`; } output += `\n`; } if (transform.notes) { output += `**Notes**: ${transform.notes}`; } return output; } /** * Format mechanics overview with statistics */ static formatMechanicsOverview( mechanics: FlatMechanic[], stats: { basicMechanics: number; transformations: number; totalMechanics: number; }, ): string { const mechanicsList = mechanics.map((mechanic) => ({ category: mechanic.category === "basic" ? "Basic Mechanic" : "Transformation", term: mechanic.name, keywords: mechanic.keywords, description: (mechanic.description || "").length > 150 ? (mechanic.description || "").substring(0, 150) + "..." : mechanic.description || "", })); return `# ZeroSpace Game Mechanics Found ${stats.totalMechanics} documented mechanics: - ${stats.basicMechanics} basic mechanics - ${stats.transformations} transformations ${JSON.stringify(mechanicsList, null, 2)}`; } /** * Format error message for mechanic not found */ static formatMechanicNotFound(term: string, availableKeys: string[]): string { return `ERROR: Mechanic '${term}' not found. Available mechanics: ${availableKeys.join(", ")}`; } /** * Format general error message */ static formatError(message: string): string { return `ERROR: ${message}`; } } export class MechanicsService { private mechanicsData: MechanicsData | null = null; private __dirname: string; constructor() { const __filename = fileURLToPath(import.meta.url); this.__dirname = dirname(__filename); } /** * Load mechanics data from the JSON file */ async loadMechanics(): Promise<MechanicsData> { if (this.mechanicsData) { return this.mechanicsData; } try { const mechanicsPath = join( this.__dirname, "..", "..", "iolin", "dist", "json", "meta", "mechanics.json", ); const mechanicsFileContent = readFileSync(mechanicsPath, "utf-8"); const parsed = JSON.parse(mechanicsFileContent); // Validate structure if (!parsed.mechanics) { throw new Error("Invalid mechanics file: missing 'mechanics' root object"); } if (!parsed.mechanics.basicMechanics) { console.warn("Warning: No basicMechanics found in mechanics file"); parsed.mechanics.basicMechanics = {}; } if (!parsed.mechanics.transformations) { console.warn("Warning: No transformations found in mechanics file"); parsed.mechanics.transformations = {}; } this.mechanicsData = parsed; return this.mechanicsData!; } catch (error) { console.error("Failed to load mechanics data:", error); // Return empty structure on error const emptyData: MechanicsData = { mechanics: { basicMechanics: {}, transformations: {}, }, }; this.mechanicsData = emptyData; return emptyData; } } /** * Get flattened mechanics for easy searching */ async getFlattenedMechanics(): Promise<Record<string, FlatMechanic>> { const data = await this.loadMechanics(); const flattened: Record<string, FlatMechanic> = {}; // Add basic mechanics for (const [key, mechanic] of Object.entries(data.mechanics.basicMechanics)) { flattened[key] = { key, category: "basic", name: mechanic.term || key, keywords: mechanic.keywords || [], description: mechanic.description || "", data: mechanic, }; } // Add transformations for (const [key, transformation] of Object.entries(data.mechanics.transformations)) { flattened[key] = { key, category: "transformation", name: transformation.name || key, keywords: transformation.keywords || [], description: transformation.description || "", data: transformation, }; } return flattened; } /** * Search for a specific mechanic by key */ async getMechanic(key: string): Promise<FlatMechanic | null> { const flattened = await this.getFlattenedMechanics(); return flattened[key] || null; } /** * Get all mechanics with optional filtering */ async getAllMechanics(filter?: { category?: "basic" | "transformation"; searchTerm?: string; }): Promise<FlatMechanic[]> { const flattened = await this.getFlattenedMechanics(); let mechanics = Object.values(flattened); if (filter?.category) { mechanics = mechanics.filter((m) => m.category === filter.category); } if (filter?.searchTerm) { const searchLower = filter.searchTerm.toLowerCase(); mechanics = mechanics.filter( (m) => m.name.toLowerCase().includes(searchLower) || m.description.toLowerCase().includes(searchLower) || m.keywords.some((k) => k.toLowerCase().includes(searchLower)), ); } return mechanics; } /** * Get statistics about the mechanics system */ async getStats(): Promise<{ totalMechanics: number; basicMechanics: number; transformations: number; mechanicsWithKeywords: number; averageKeywords: number; }> { const data = await this.loadMechanics(); const flattened = await this.getFlattenedMechanics(); const basicCount = Object.keys(data.mechanics.basicMechanics).length; const transformCount = Object.keys(data.mechanics.transformations).length; const totalCount = basicCount + transformCount; const mechanicsWithKeywords = Object.values(flattened).filter( (m) => m.keywords.length > 0, ).length; const totalKeywords = Object.values(flattened).reduce((sum, m) => sum + m.keywords.length, 0); const averageKeywords = totalCount > 0 ? totalKeywords / totalCount : 0; return { totalMechanics: totalCount, basicMechanics: basicCount, transformations: transformCount, mechanicsWithKeywords, averageKeywords: Math.round(averageKeywords * 100) / 100, }; } /** * Format a mechanic for display (delegated to MarkdownFormatter) */ formatMechanic(mechanic: FlatMechanic, detailed: boolean = false): string { return MarkdownFormatter.formatMechanic(mechanic, detailed); } /** * Format mechanics list for overview (delegated to MarkdownFormatter) */ formatMechanicsList(mechanics: FlatMechanic[]): any[] { return mechanics.map((mechanic) => ({ category: mechanic.category === "basic" ? "Basic Mechanic" : "Transformation", term: mechanic.name, keywords: mechanic.keywords, description: (mechanic.description || "").length > 150 ? (mechanic.description || "").substring(0, 150) + "..." : mechanic.description || "", })); } } // Export singleton instance for convenience export const mechanicsService = new MechanicsService();