/** * Prunes the mirrored everness data down to what the app actually needs. * * Input data-src/{espers,arcs,arc_effects}.json (committed, not shipped) * Output src/generated/gamedata.json (committed, bundled) * * Everything dropped here is presentation the app never renders: voice lines, * fashion, profile text, and the ability descriptions. What stays is the stat * curves, the console grid, the console trait, and the ids that join to a * capture. */ import { readFileSync, writeFileSync, mkdirSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; const root = join(dirname(fileURLToPath(import.meta.url)), ".."); const read = (name: string) => JSON.parse(readFileSync(join(root, "data-src", name), "utf8")); interface StatCurve { id_stats: string; name: string; bShowPercent: boolean; values: number[]; } interface TraitStat { id_stats: string; name: string; bShowPercent: boolean; value: number; } export interface Esper { id: number; /** The `GA__*` name the capture marks character blocks with. */ abilityKey: string; name: string; element: string | null; rarity: number; /** Small list avatar. */ icon: string; /** 256px avatar. */ iconBig: string | null; /** Full card art, used as the character view's header. */ iconGacha: string | null; /** Per-level curves, indexed by level - 1. */ stats: StatCurve[]; /** 7x7, -1 blocked and 0 free. */ slots: number[][]; /** * The module type the console trait keys off - 2 or 3, NOT the grid type. * Prydwen's "Console Grid Type" means this same number. */ ownerGridCount: number; /** Never assume Type III / CRIT DMG: seven different stats appear here. */ trait: TraitStat[]; /** Levels at which a breakthrough is taken. */ breakthroughLevels: number[]; } export interface Arc { id: string; name: string; icon: string; quality: number; desc: string; stats: unknown; values: unknown; } const espersRaw = read("espers.json") as any[]; const arcsRaw = read("arcs.json") as Record | any[]; const arcEffects = read("arc_effects.json") as Record; function abilityKey(esper: any): string { const id: string | undefined = esper?.melee?.[0]?.id; const match = /^GA_(.+?)_Melee$/.exec(id ?? ""); if (!match) throw new Error(`no GA__Melee id on esper ${esper?.name}`); return match[1]!; } const espers: Esper[] = espersRaw.map((esper) => ({ id: esper.id, abilityKey: abilityKey(esper), name: esper.name, element: esper.element?.[0]?.element_id ?? null, rarity: esper.rarity, icon: esper.icon, iconBig: esper.iconBig ?? null, iconGacha: esper.iconGacha ?? null, stats: (esper.stats ?? []).map((stat: StatCurve) => ({ id_stats: stat.id_stats, name: stat.name, bShowPercent: stat.bShowPercent, values: stat.values, })), slots: esper.equip_slots.slots, ownerGridCount: esper.equip_slots.OwnerGridCount, trait: (esper.equip_slots.stats ?? []).map((stat: TraitStat) => ({ id_stats: stat.id_stats, name: stat.name, bShowPercent: stat.bShowPercent, value: stat.value, })), breakthroughLevels: (esper.breakthrough ?? []).map((step: { level: string }) => Number(step.level), ), })); const arcEntries = Array.isArray(arcsRaw) ? arcsRaw : Object.values(arcsRaw); const arcs: Arc[] = arcEntries.map((arc: any) => ({ id: arc.id, name: arc.name, icon: arc.icon, quality: arc.quality, desc: arc.desc, stats: arc.stats, values: arc.effect?.values ?? arc.effect ?? null, })); /** * How the game itself names each stat id, harvested rather than hand-written. * * Every `{ id_stats, name }` pair anywhere in the mirror agrees with every * other, so this is the game's own label for a stat, not a guess. Ids the * mirror never names are filled in by `domain/statlabels.ts`, which says on * each one where the label came from. */ function harvestStatNames(): Record { const found: Record = {}; const walk = (node: unknown): void => { if (Array.isArray(node)) { for (const child of node) walk(child); return; } if (!node || typeof node !== "object") return; const record = node as Record; const id = record.id_stats; const name = record.name; if (typeof id === "string" && typeof name === "string" && name.trim()) { found[id] ??= { name, percent: record.bShowPercent === true }; } for (const child of Object.values(record)) walk(child); }; walk(espersRaw); walk(arcsRaw); return found; } const statNames = harvestStatNames(); const gamedata = { format: "nte-gamedata", format_version: 1, generated: new Date().toISOString().slice(0, 10), statNames, espers, arcs, arcEffects, }; const outDir = join(root, "src", "generated"); mkdirSync(outDir, { recursive: true }); const json = JSON.stringify(gamedata); writeFileSync(join(outDir, "gamedata.json"), json + "\n"); const sourceBytes = ["espers.json", "arcs.json"].reduce( (total, name) => total + readFileSync(join(root, "data-src", name)).byteLength, 0, ); console.log( `gamedata.json ${espers.length} espers, ${arcs.length} arcs ` + `${(sourceBytes / 1024).toFixed(0)} KB -> ${(json.length / 1024).toFixed(0)} KB`, );