@zerospacegg/anrubic
Version:
Anrubic - ZeroSpace.gg MCP Server for AI agents to access game data
428 lines (424 loc) ⢠18.9 kB
JavaScript
import { readFileSync } from "fs";
import { dirname, join } from "path";
import { fileURLToPath } from "url";
// Get the directory containing this script
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Load game data from iolin JSON files
function loadGameData() {
const iolinPath = join(__dirname, "../../iolin/dist/json");
try {
// Load all entities
const allData = JSON.parse(readFileSync(join(iolinPath, "all.json"), "utf8"));
const tagsData = JSON.parse(readFileSync(join(iolinPath, "tags.json"), "utf8"));
// Load mechanics data
let mechanicsData = null;
try {
mechanicsData = JSON.parse(readFileSync(join(iolinPath, "meta/mechanics.json"), "utf8"));
}
catch (mechanicsError) {
console.error("ā ļø Could not load mechanics data:", mechanicsError);
}
// Build our structured data
const entities = [];
const entitiesByFaction = {};
const entitiesByType = {};
const entitiesById = {};
// Process entities from all.json
for (const [category, categoryData] of Object.entries(allData)) {
if (typeof categoryData === "object" && categoryData !== null) {
for (const [key, entityData] of Object.entries(categoryData)) {
if (typeof entityData === "object" && entityData !== null && "id" in entityData) {
const entity = {
id: entityData.id || key,
name: entityData.name || key,
slug: entityData.slug || key,
type: entityData.type || category,
faction: entityData.faction || "unknown",
tags: Array.isArray(entityData.tags) ? entityData.tags : [],
description: entityData.description,
lore: entityData.lore,
tier: entityData.tier,
health: typeof entityData.health === "number" ? entityData.health : undefined,
shields: typeof entityData.shields === "number" ? entityData.shields : undefined,
attack: typeof entityData.attack === "number" ? entityData.attack : undefined,
armor: typeof entityData.armor === "number" ? entityData.armor : undefined,
speed: typeof entityData.speed === "number" ? entityData.speed : undefined,
supply: typeof entityData.supply === "number" ? entityData.supply : undefined,
flux: typeof entityData.flux === "number" ? entityData.flux : undefined,
buildTime: typeof entityData.buildTime === "number" ? entityData.buildTime : undefined,
};
entities.push(entity);
entitiesById[entity.id] = entity;
// Group by faction
if (!entitiesByFaction[entity.faction]) {
entitiesByFaction[entity.faction] = [];
}
entitiesByFaction[entity.faction].push(entity);
// Group by type
if (!entitiesByType[entity.type]) {
entitiesByType[entity.type] = [];
}
entitiesByType[entity.type].push(entity);
}
}
}
}
return {
entities,
tags: tagsData.tags || {},
entitiesByFaction,
entitiesByType,
entitiesById,
mechanics: mechanicsData,
};
}
catch (error) {
console.error("Failed to load game data:", error);
process.exit(1);
}
}
// CLI Commands
function showHelp() {
console.log(`
Anrubic CLI - ZeroSpace Game Data Tool
Usage: anrubic <command> [options]
Commands:
status Show server status and data statistics
tags List all available tags
factions List all factions with entity counts
faction <name> Show all entities for a faction
search <query> [limit] Search entities by name/description/tags
get <id> Get entity details by ID
types List all entity types
type <name> Show all entities of a specific type
ids [type] [faction] List all entity IDs (optionally filtered)
mechanics [term] Show game mechanics (all or specific term)
Examples:
anrubic status
anrubic tags
anrubic faction "Terran Alliance"
anrubic search marine 5
anrubic get marine
anrubic type unit
anrubic ids unit "Xol Collective"
anrubic mechanics
anrubic mechanics ABES
anrubic mechanics Infuse
š ZeroSpace.gg - Cosmic Data Empire
`);
}
function formatEntity(entity, detailed = false) {
let output = `${entity.name} (${entity.id})`;
if (detailed) {
output += `\n Type: ${entity.type}`;
output += `\n Faction: ${entity.faction}`;
if (entity.description)
output += `\n Description: ${entity.description}`;
if (entity.lore)
output += `\n Lore: ${entity.lore}`;
if (entity.tier)
output += `\n Tier: ${entity.tier}`;
if (entity.health)
output += `\n Health: ${entity.health}`;
if (entity.shields)
output += `\n Shields: ${entity.shields}`;
if (entity.attack)
output += `\n Attack: ${entity.attack}`;
if (entity.armor)
output += `\n Armor: ${entity.armor}`;
if (entity.speed)
output += `\n Speed: ${entity.speed}`;
if (entity.supply)
output += `\n Supply: ${entity.supply}`;
if (entity.flux)
output += `\n Flux: ${entity.flux}`;
if (entity.buildTime)
output += `\n Build Time: ${entity.buildTime}s`;
if (entity.tags.length > 0)
output += `\n Tags: ${entity.tags.join(", ")}`;
}
else {
output += ` [${entity.type}/${entity.faction}]`;
}
return output;
}
// Parse command line arguments
const args = process.argv.slice(2);
const command = args[0];
const isJsonOutput = args.includes('--json');
// Initialize game data
if (!isJsonOutput) {
console.error("Loading ZeroSpace game data...");
}
const gameData = loadGameData();
if (!isJsonOutput) {
console.error(`Loaded ${gameData.entities.length} entities from ${Object.keys(gameData.entitiesByFaction).length} factions`);
}
switch (command) {
case "status":
console.log("š Anrubic Status");
console.log("================");
console.log(`Entities: ${gameData.entities.length}`);
console.log(`Factions: ${Object.keys(gameData.entitiesByFaction).length}`);
console.log(`Types: ${Object.keys(gameData.entitiesByType).length}`);
console.log(`Tags: ${Object.keys(gameData.tags).length}`);
const basicCount = gameData.mechanics?.mechanics?.basicMechanics
? Object.keys(gameData.mechanics.mechanics.basicMechanics).length
: 0;
const transformCount = gameData.mechanics?.mechanics?.transformations
? Object.keys(gameData.mechanics.mechanics.transformations).length
: 0;
console.log(`Mechanics: ${basicCount} basic, ${transformCount} transformations`);
console.log(`Timestamp: ${new Date().toISOString()}`);
break;
case "tags":
console.log("š·ļø Available Tags");
console.log("=================");
for (const [tag, info] of Object.entries(gameData.tags)) {
console.log(`${tag}: ${info.label}`);
if (info.description && info.description !== info.label) {
console.log(` ${info.description}`);
}
}
break;
case "factions":
console.log("šļø Available Factions");
console.log("====================");
for (const [faction, entities] of Object.entries(gameData.entitiesByFaction)) {
console.log(`${faction}: ${entities.length} entities`);
}
break;
case "faction":
const factionName = args[1];
if (!factionName) {
console.error("Please specify a faction name");
console.error("Available factions:", Object.keys(gameData.entitiesByFaction).join(", "));
process.exit(1);
}
const factionEntities = gameData.entitiesByFaction[factionName];
if (!factionEntities) {
console.error(`Faction "${factionName}" not found`);
console.error("Available factions:", Object.keys(gameData.entitiesByFaction).join(", "));
process.exit(1);
}
console.log(`šļø ${factionName} (${factionEntities.length} entities)`);
console.log("=".repeat(factionName.length + 20));
// Group by type
const factionByType = {};
for (const entity of factionEntities) {
if (!factionByType[entity.type]) {
factionByType[entity.type] = [];
}
factionByType[entity.type].push(entity);
}
for (const [type, entities] of Object.entries(factionByType)) {
console.log(`\n${type.toUpperCase()}S:`);
for (const entity of entities) {
console.log(` ${formatEntity(entity)}`);
}
}
break;
case "search":
const query = args[1];
const limit = parseInt(args[2] || "10") || 10;
if (!query) {
console.error("Please specify a search query");
process.exit(1);
}
const lowerQuery = query.toLowerCase();
const matches = gameData.entities
.filter((entity) => entity.name.toLowerCase().includes(lowerQuery) ||
entity.description?.toLowerCase().includes(lowerQuery) ||
entity.lore?.toLowerCase().includes(lowerQuery) ||
entity.tags.some((tag) => tag.toLowerCase().includes(lowerQuery)))
.slice(0, limit);
console.log(`š Search Results for "${query}" (${matches.length} matches)`);
console.log("=".repeat(30 + query.length));
if (matches.length === 0) {
console.log("No matches found");
}
else {
for (const entity of matches) {
console.log(` ${formatEntity(entity)}`);
}
}
break;
case "get":
const entityId = args[1];
if (!entityId) {
if (isJsonOutput) {
console.log(JSON.stringify({ error: "Please specify an entity ID" }, null, 2));
}
else {
console.error("Please specify an entity ID");
}
process.exit(1);
}
const entity = gameData.entitiesById[entityId];
if (!entity) {
if (isJsonOutput) {
console.log(JSON.stringify({ error: `Entity "${entityId}" not found` }, null, 2));
}
else {
console.error(`Entity "${entityId}" not found`);
}
process.exit(1);
}
if (isJsonOutput) {
console.log(JSON.stringify(entity, null, 2));
}
else {
console.log(`Entity Details: ${entity.name}`);
console.log("=".repeat(20 + entity.name.length));
console.log(formatEntity(entity, true));
}
break;
case "types":
console.log("š¦ Available Types");
console.log("=================");
for (const [type, entities] of Object.entries(gameData.entitiesByType)) {
console.log(`${type}: ${entities.length} entities`);
}
break;
case "type":
const typeName = args[1];
if (!typeName) {
console.error("Please specify a type name");
console.error("Available types:", Object.keys(gameData.entitiesByType).join(", "));
process.exit(1);
}
const typeEntities = gameData.entitiesByType[typeName];
if (!typeEntities) {
console.error(`Type "${typeName}" not found`);
console.error("Available types:", Object.keys(gameData.entitiesByType).join(", "));
process.exit(1);
}
console.log(`š¦ ${typeName.toUpperCase()} (${typeEntities.length} entities)`);
console.log("=".repeat(typeName.length + 20));
for (const entity of typeEntities) {
console.log(` ${formatEntity(entity)}`);
}
break;
case "ids":
const filterType = args[1];
const filterFaction = args[2];
let filteredEntities = gameData.entities;
if (filterType) {
filteredEntities = filteredEntities.filter((e) => e.type === filterType);
}
if (filterFaction) {
filteredEntities = filteredEntities.filter((e) => e.faction === filterFaction);
}
console.log(`š Entity IDs (${filteredEntities.length} total)`);
if (filterType || filterFaction) {
console.log(`Filters: ${filterType ? `type=${filterType}` : ""}${filterType && filterFaction ? ", " : ""}${filterFaction ? `faction=${filterFaction}` : ""}`);
}
console.log("=".repeat(30));
for (const entity of filteredEntities) {
console.log(`${entity.id} - ${entity.name} [${entity.type}/${entity.faction}]`);
}
break;
case "mechanics":
const mechanicTerm = args[1];
if (!gameData.mechanics) {
console.error("Mechanics data not available");
process.exit(1);
}
// Flatten hierarchical mechanics for search
const allMechanics = {};
if (gameData.mechanics.mechanics?.basicMechanics) {
Object.assign(allMechanics, gameData.mechanics.mechanics.basicMechanics);
}
if (gameData.mechanics.mechanics?.transformations) {
Object.assign(allMechanics, gameData.mechanics.mechanics.transformations);
}
if (mechanicTerm) {
// Show specific mechanic
const mechanic = allMechanics[mechanicTerm];
if (!mechanic) {
console.error(`Mechanic "${mechanicTerm}" not found`);
console.error("Available mechanics:", Object.keys(allMechanics).join(", "));
process.exit(1);
}
console.log(`Mechanic: ${mechanic.term || mechanic.name}`);
console.log("=".repeat(15 + (mechanic.term || mechanic.name).length));
if (mechanic.term) {
// Basic mechanic
console.log(`Keywords: ${mechanic.keywords?.join(", ") || "None"}`);
console.log(`Description: ${mechanic.description || "No description"}`);
}
else if (mechanic.name) {
// Transformation
console.log(`Type: ${mechanic.type}`);
console.log(`Keywords: ${mechanic.keywords?.join(", ") || "None"}`);
console.log(`Description: ${mechanic.description || "No description"}`);
console.log(`Stackable: ${mechanic.stackable ? "Yes" : "No"}${mechanic.maxStacks ? ` (max ${mechanic.maxStacks})` : ""}`);
if (mechanic.statModifications) {
console.log("\nStat Modifications:");
if (mechanic.statModifications.hpMultiplier) {
console.log(` HP: ${mechanic.statModifications.hpMultiplier}x`);
}
if (mechanic.statModifications.damageMultiplier) {
console.log(` Damage: ${mechanic.statModifications.damageMultiplier}x`);
}
}
if (mechanic.hasPhases && (mechanic.preDeathPhase || mechanic.postDeathPhase)) {
console.log("\nPhases:");
if (mechanic.preDeathPhase) {
console.log(` Pre-death: Speed ${mechanic.preDeathPhase.speedMultiplier}x, Cooldowns ${mechanic.preDeathPhase.cooldownMultiplier}x`);
}
if (mechanic.postDeathPhase) {
console.log(` Post-death: Speed ${mechanic.postDeathPhase.speedMultiplier}x, Cooldowns ${mechanic.postDeathPhase.cooldownMultiplier}x`);
}
}
if (mechanic.notes) {
console.log(`\nNotes: ${mechanic.notes}`);
}
}
}
else {
// Show all mechanics
const basicCount = gameData.mechanics.mechanics?.basicMechanics
? Object.keys(gameData.mechanics.mechanics.basicMechanics).length
: 0;
const transformCount = gameData.mechanics.mechanics?.transformations
? Object.keys(gameData.mechanics.mechanics.transformations).length
: 0;
console.log(`ZeroSpace Game Mechanics (${basicCount + transformCount} total)`);
console.log("===============================");
console.log(`${basicCount} basic mechanics, ${transformCount} transformations\n`);
if (gameData.mechanics.mechanics?.basicMechanics) {
console.log("BASIC MECHANICS:");
for (const [key, mechanic] of Object.entries(gameData.mechanics.mechanics.basicMechanics)) {
const typedMechanic = mechanic;
console.log(` ${typedMechanic.term || key} - ${(typedMechanic.description || "").substring(0, 80)}${(typedMechanic.description || "").length > 80 ? "..." : ""}`);
}
console.log();
}
if (gameData.mechanics.mechanics?.transformations) {
console.log("TRANSFORMATIONS:");
for (const [key, transformation] of Object.entries(gameData.mechanics.mechanics.transformations)) {
const typedTransformation = transformation;
console.log(` ${typedTransformation.name || key} - ${(typedTransformation.description || "").substring(0, 80)}${(typedTransformation.description || "").length > 80 ? "..." : ""}`);
}
}
}
break;
case "help":
case "--help":
case "-h":
showHelp();
break;
default:
if (!command) {
showHelp();
}
else {
console.error(`Unknown command: ${command}`);
showHelp();
process.exit(1);
}
}
//# sourceMappingURL=cli.js.map