@zerospacegg/anrubic
Version:
Anrubic - ZeroSpace.gg MCP Server for AI agents to access game data
373 lines (324 loc) • 10.5 kB
text/typescript
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();