Update class mechanics, stat rulesets, and CoA reference catalogs
This commit is contained in:
1 parent
b16821e92e
commit
39458d5de6
221 files changed
+2584962
-568
No files matched your search
@@ -0,0 +1,150 @@
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { buildLocalReference, resolveSourceRoots } from "./local-source.mjs";
|
||||
import { auditReference, renderAuditMarkdown, validateReference } from "./audit.mjs";
|
||||
import { downloadSupplements, readSupplements } from "./supplements.mjs";
|
||||
import { lookupReference, publishSnapshot, readSnapshot } from "./snapshot.mjs";
|
||||
|
||||
const projectRoot = path.resolve(import.meta.dirname, "../..");
|
||||
const usage = `CoA offline reference
|
||||
|
||||
build Build and atomically publish a local-first reference and audit
|
||||
fetch Explicitly cache the pinned August archive talent supplement
|
||||
fetch-spells Cache missing global spell records and their linked payloads
|
||||
audit Verify the current snapshot and repeat offline coverage checks
|
||||
lookup Search the current snapshot by --class, --tree, --spell, --ability
|
||||
inventory Inspect the resolved local source pair without changing files
|
||||
promote Promote the verified August Vol'Jin cache into the active CoA catalog
|
||||
|
||||
Options:
|
||||
--client-root PATH Existing Ascension client directory
|
||||
--extracted-root PATH Existing LadiksMPQEditor directory (supply both roots)
|
||||
--output-root PATH Default: data/coa-reference
|
||||
--cache-root PATH Default: data/coa-reference/cache
|
||||
--limit NUMBER Lookup results per collection, default 20
|
||||
--refresh Fetch command: revalidate the pinned supplement
|
||||
--help Show this help
|
||||
|
||||
Build, audit, lookup, inventory and promote never make network requests.
|
||||
Only promote writes the active gameplay catalog. Original client files stay intact.
|
||||
`;
|
||||
|
||||
export function parseArguments(args) {
|
||||
const hasCommand = Boolean(args[0] && !args[0].startsWith("--"));
|
||||
const result = { command: hasCommand ? args[0] : "help" };
|
||||
const valueFlags = new Set(["client-root", "extracted-root", "output-root", "cache-root", "class", "tree", "spell", "ability", "limit"]);
|
||||
for (let index = hasCommand ? 1 : 0; index < args.length; index += 1) {
|
||||
const flag = args[index];
|
||||
if (flag === "--help") result.command = "help";
|
||||
else if (flag === "--refresh") result.refresh = true;
|
||||
else if (flag.startsWith("--") && valueFlags.has(flag.slice(2))) {
|
||||
if (!args[index + 1] || args[index + 1].startsWith("--")) throw new Error(`Missing value for ${flag}`);
|
||||
result[flag.slice(2)] = args[++index];
|
||||
} else throw new Error(`Unknown argument: ${flag}`);
|
||||
}
|
||||
if (!["help", "build", "fetch", "fetch-spells", "audit", "lookup", "inventory", "promote"].includes(result.command)) {
|
||||
throw new Error(`Unknown command: ${result.command}`);
|
||||
}
|
||||
if (Boolean(result["client-root"]) !== Boolean(result["extracted-root"])) {
|
||||
throw new Error("Supply both --client-root and --extracted-root to avoid mixing source installations");
|
||||
}
|
||||
if (result.refresh && result.command !== "fetch") throw new Error("--refresh is only supported by fetch");
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Return unresolved spell roots owned by CoA plus explicit August rank references. */
|
||||
export function unresolvedCoaArchiveSpellIds(reference, extraNodes = []) {
|
||||
const spellById = new Map((reference?.spells ?? []).map((spell) => [Number(spell.id), spell]));
|
||||
const required = new Set();
|
||||
const add = (value) => {
|
||||
const id = Number(value);
|
||||
if (Number.isSafeInteger(id) && id > 0) required.add(id);
|
||||
};
|
||||
for (const node of extraNodes) {
|
||||
for (const rank of node?.rankDescriptions ?? []) add(rank?.spellId);
|
||||
}
|
||||
for (const chain of reference?.progressionChains ?? []) {
|
||||
for (const rank of chain?.ranks ?? []) add(rank?.spellId);
|
||||
}
|
||||
for (const spell of reference?.spells ?? []) {
|
||||
if ((spell?.classIds ?? []).some((classId) => Number(classId) > 11)) add(spell.id);
|
||||
}
|
||||
return [...required]
|
||||
.filter((id) => !spellById.get(id)?.dbc)
|
||||
.sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
export async function main(args = process.argv.slice(2)) {
|
||||
const options = parseArguments(args);
|
||||
if (options.command === "help") {
|
||||
process.stdout.write(usage);
|
||||
return;
|
||||
}
|
||||
const outputRoot = path.resolve(options["output-root"] ?? path.join(projectRoot, "data/coa-reference"));
|
||||
const cacheRoot = path.resolve(options["cache-root"] ?? path.join(outputRoot, "cache"));
|
||||
const sourceOptions = {
|
||||
projectRoot, clientRoot: options["client-root"], extractedRoot: options["extracted-root"],
|
||||
};
|
||||
if (options.command === "inventory") {
|
||||
console.log(JSON.stringify(resolveSourceRoots(sourceOptions), null, 2));
|
||||
return;
|
||||
}
|
||||
if (options.command === "fetch") {
|
||||
console.log(JSON.stringify(await downloadSupplements({ cacheRoot, refresh: options.refresh ?? false }), null, 2));
|
||||
return;
|
||||
}
|
||||
if (options.command === "promote") {
|
||||
const { promoteCachedCoa } = await import("./promotion-command.mjs");
|
||||
console.log(JSON.stringify(await promoteCachedCoa({ ...sourceOptions, cacheRoot }), null, 2));
|
||||
return;
|
||||
}
|
||||
if (options.command === "fetch-spells") {
|
||||
const { downloadArchiveSpells } = await import("./archive-spells.mjs");
|
||||
const { normalizeArchiveNode } = await import("./promote.mjs");
|
||||
const supplements = await readSupplements({ cacheRoot });
|
||||
if (!supplements) throw new Error("Verified talent cache is required; run coa:reference:fetch first");
|
||||
const extraNodes = supplements.classes.flatMap((definition) => definition.nodes.map(normalizeArchiveNode));
|
||||
const reference = buildLocalReference({ ...sourceOptions, extraNodes });
|
||||
const result = await downloadArchiveSpells({ cacheRoot, spellIds: unresolvedCoaArchiveSpellIds(reference, extraNodes) });
|
||||
console.log(JSON.stringify({ source: result.source, records: result.records.length, files: result.files.length, missingIds: result.missingIds }, null, 2));
|
||||
return;
|
||||
}
|
||||
if (options.command === "build") {
|
||||
const supplements = await readSupplements({ cacheRoot });
|
||||
const reference = await buildLocalReference(sourceOptions);
|
||||
reference.supplementSource = supplements?.source ?? null;
|
||||
const report = auditReference({ reference, supplements, projectRoot });
|
||||
const validation = report.validation ?? validateReference(reference);
|
||||
if (validation.errors?.length) {
|
||||
throw new Error(`Reference validation failed; previous snapshot retained:\n${validation.errors.map((item) => item.message).join("\n")}`);
|
||||
}
|
||||
const result = publishSnapshot({ outputRoot, reference, report, markdown: renderAuditMarkdown(report) });
|
||||
console.log(JSON.stringify({
|
||||
snapshotId: result.snapshotId, report: path.join(result.directory, "audit.md"),
|
||||
classes: reference.classes.length, nodes: reference.nodes.length, spells: reference.spells.length,
|
||||
sourceFiles: reference.sources.length, supplement: reference.supplementSource,
|
||||
validationErrors: validation.errors?.length ?? 0, validationWarnings: validation.warnings?.length ?? 0,
|
||||
}, null, 2));
|
||||
return;
|
||||
}
|
||||
const snapshot = readSnapshot(outputRoot);
|
||||
if (options.command === "audit") {
|
||||
const validation = validateReference(snapshot.reference);
|
||||
console.log(JSON.stringify({
|
||||
snapshotId: snapshot.manifest.snapshotId, report: path.join(snapshot.directory, "audit.md"),
|
||||
sourceFiles: snapshot.reference.sources.length,
|
||||
errors: validation.errors ?? [], warningCount: validation.warnings?.length ?? 0,
|
||||
warningSample: (validation.warnings ?? []).slice(0, 10),
|
||||
}, null, 2));
|
||||
if (validation.errors?.length) throw new Error("Reference coverage validation failed");
|
||||
return;
|
||||
}
|
||||
console.log(JSON.stringify(lookupReference(snapshot.reference, options), null, 2));
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
|
||||
main().catch((error) => {
|
||||
console.error(`CoA reference: ${error.message}`);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user