import { mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync, } from "node:fs"; import { basename, resolve } from "node:path"; import { CLASSES, COA_CLASS_IDS, type ClassId, type CoaClassId, } from "../src/app/characterCatalog"; import { TALENT_NODES, TALENT_TREES } from "../src/game/talentCatalog"; const projectRoot = resolve(import.meta.dirname, ".."); const talentOutputDirectory = resolve(projectRoot, "src/game/generated/talentUi"); const coaAbilityOutputDirectory = resolve(projectRoot, "src/game/generated/coaAbilities"); function writeJson(file: string, value: unknown): void { writeFileSync(file, `${JSON.stringify(value)}\n`, "utf8"); console.log(`Wrote ${file}`); } function removeStaleJson(directory: string, expectedNames: ReadonlySet): void { for (const name of readdirSync(directory)) { if (!name.endsWith(".json") || expectedNames.has(name)) continue; unlinkSync(resolve(directory, name)); } } mkdirSync(talentOutputDirectory, { recursive: true }); const talentClassIds = new Set(TALENT_TREES.map((tree) => tree.classId)); const talentFileNames = new Set([...talentClassIds].map((classId) => `${classId}.json`)); removeStaleJson(talentOutputDirectory, talentFileNames); for (const classId of talentClassIds) { const trees = TALENT_TREES.filter((tree) => tree.classId === classId).map((tree) => ({ id: tree.id, classId: tree.classId, name: tree.name, icon: tree.icon, background: tree.background, description: tree.description, system: tree.system, })); const treeIds = new Set(trees.map((tree) => tree.id)); const nodes = TALENT_NODES.filter((node) => treeIds.has(node.treeId)).map((node) => ({ id: node.id, treeId: node.treeId, index: node.index, prerequisiteId: node.prerequisiteId, prerequisiteRank: node.prerequisiteRank, prerequisites: node.prerequisites, column: node.column, row: node.row, icon: node.icon, maxRank: node.maxRank, name: node.name, description: node.description, rankDescriptions: node.rankDescriptions, system: node.system, entryType: node.entryType, abilityEssenceCost: node.abilityEssenceCost, talentEssenceCost: node.talentEssenceCost, requiredClassPoints: node.requiredClassPoints, requiredSpecPoints: node.requiredSpecPoints, requiredLevel: node.requiredLevel, isPassive: node.isPassive, })); writeJson(resolve(talentOutputDirectory, `${classId}.json`), { schemaVersion: 1, classId, trees, nodes, }); } interface CoaAbilitySnapshot { readonly schemaVersion: 1; readonly abilitiesByClass: Readonly>; readonly resourcesByClass: Readonly>; readonly triggeredAbilitiesBySpellId: Readonly>; } const coaAbilityInput = resolve(projectRoot, "src/game/generated/coaAbilityRuntimeCatalog.json"); const coaSnapshot = JSON.parse(readFileSync(coaAbilityInput, "utf8")) as CoaAbilitySnapshot; mkdirSync(coaAbilityOutputDirectory, { recursive: true }); const coaAbilityFileNames = new Set(COA_CLASS_IDS.map((classId) => `${classId}.json`)); removeStaleJson(coaAbilityOutputDirectory, coaAbilityFileNames); for (const classId of COA_CLASS_IDS) { const triggeredAbilitiesBySpellId = Object.fromEntries(Object.entries( coaSnapshot.triggeredAbilitiesBySpellId, ).filter(([, ability]) => ability.classId === classId)); writeJson(resolve(coaAbilityOutputDirectory, `${classId}.json`), { schemaVersion: 1, classId, abilities: coaSnapshot.abilitiesByClass[classId] ?? [], resource: coaSnapshot.resourcesByClass[classId], triggeredAbilitiesBySpellId, }); } console.log(`Generated ${talentFileNames.size} talent UI shards and ${coaAbilityFileNames.size} Conquest ability shards from ${basename(coaAbilityInput)}.`);