Update Healer Man server
This commit is contained in:
1 parent
39458d5de6
commit
6cc1b9217f
203 files changed
+133929
-31271
No files matched your search
@@ -0,0 +1,152 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { abilitiesForClass, abilityTalentNodeId } from "../../src/game/abilityCatalog";
|
||||
import { abilityHasRotationSemantics, rotationProfileFor } from "../../src/game/combatRotation";
|
||||
import { BOSS_TESTING_CATALOG } from "../../src/game/bossTesting";
|
||||
import {
|
||||
coaClassDefinition,
|
||||
coaEntriesForClass,
|
||||
coaSpellById,
|
||||
COA_LIVE_SOURCE,
|
||||
} from "../../src/game/coaLiveCatalog";
|
||||
import {
|
||||
COA_FIRST_PARTY_MEMBERS,
|
||||
coaFirstPartySimulationConfig,
|
||||
loadCoaFirstPartyBuilds,
|
||||
} from "../../src/game/benchmark/coaFirstParty";
|
||||
import type { CoaClassId } from "../../src/app/characterCatalog";
|
||||
|
||||
const root = fileURLToPath(new URL("../../", import.meta.url));
|
||||
const args = process.argv.slice(2);
|
||||
let level = 60;
|
||||
let listDungeon: string | undefined;
|
||||
const bossKeys: string[] = [];
|
||||
for (let index = 0; index < args.length; index += 2) {
|
||||
const flag = args[index];
|
||||
const value = args[index + 1];
|
||||
if (!value || value.startsWith("--")) throw new Error(`Missing value for ${flag}.`);
|
||||
if (flag === "--level") level = Number(value);
|
||||
else if (flag === "--boss") bossKeys.push(value);
|
||||
else if (flag === "--list-bosses") listDungeon = value;
|
||||
else throw new Error(`Unknown argument ${flag}.`);
|
||||
}
|
||||
|
||||
if (listDungeon) {
|
||||
if (bossKeys.length) throw new Error("Use either --list-bosses or explicit --boss keys.");
|
||||
const bosses = BOSS_TESTING_CATALOG.filter((boss) => boss.dungeonId === listDungeon && boss.category === "wow");
|
||||
if (!bosses.length) throw new Error(`No WoW bosses found for ${listDungeon}.`);
|
||||
console.log(JSON.stringify(bosses.map(({ key, name }) => ({ key, name })), null, 2));
|
||||
} else {
|
||||
// An explicit boss list prevents preparation from becoming an all-boss run.
|
||||
const selectedBosses = bossKeys.map((key) => {
|
||||
const boss = BOSS_TESTING_CATALOG.find((candidate) => candidate.key === key && candidate.category === "wow");
|
||||
if (!boss) throw new Error(`Unknown WoW boss key: ${key}.`);
|
||||
return { key: boss.key, name: boss.name };
|
||||
});
|
||||
const config = coaFirstPartySimulationConfig(level, bossKeys);
|
||||
const builds = await loadCoaFirstPartyBuilds(level);
|
||||
const sourceBytes = await readFile(resolve(root, "src/game/coaLiveData.generated.json"));
|
||||
const sourceSha256 = createHash("sha256").update(sourceBytes).digest("hex");
|
||||
const classes = COA_FIRST_PARTY_MEMBERS.map((member, slot) => {
|
||||
const classId = member.classId as CoaClassId;
|
||||
const definition = coaClassDefinition(classId);
|
||||
const abilities = abilitiesForClass(classId);
|
||||
const build = builds[slot];
|
||||
const firstPassTabs = new Set(definition.tabs
|
||||
.filter((tab) => tab.tabName === "Class" || tab.tabName === member.specialization)
|
||||
.map((tab) => tab.tabId));
|
||||
const nodes = coaEntriesForClass(classId).map((entry) => {
|
||||
const nodeId = `coa-entry-${entry.id}`;
|
||||
const mapped = abilities.filter((ability) => abilityTalentNodeId(ability) === nodeId);
|
||||
const effects = mapped.flatMap((ability) => [
|
||||
...ability.effects,
|
||||
...(ability.ranks ?? []).flatMap((rank) => rank.effects),
|
||||
]);
|
||||
const spellIds = [...new Set(entry.rankDescriptions.map((rank) => rank.spellId))];
|
||||
return {
|
||||
nodeId,
|
||||
sourceNodeId: entry.id,
|
||||
name: entry.name,
|
||||
tree: definition.tabs.find((tab) => tab.tabId === entry.tabId)?.tabName ?? String(entry.tabId),
|
||||
phase: firstPassTabs.has(entry.tabId) ? "first-party" : "remaining-trees",
|
||||
description: entry.description,
|
||||
rankDescriptions: entry.rankDescriptions,
|
||||
spellIds,
|
||||
missingGlobalSpellIds: spellIds.filter((id) => !coaSpellById(id)?.dbc),
|
||||
selectedRank: build.talentRanks[nodeId] ?? 0,
|
||||
runtimeAbilityIds: mapped.map((ability) => ability.id),
|
||||
effectKinds: [...new Set(effects.map((effect) => effect.kind))].sort(),
|
||||
hasRotationSemantics: mapped.some(abilityHasRotationSemantics),
|
||||
sourceOperationsNeedingReview: effects.filter((effect) => (
|
||||
effect.kind === "scripted" || effect.kind === "unsupported"
|
||||
|| (effect.kind === "utility" && effect.action === "class-script")
|
||||
)),
|
||||
// A catalog match cannot establish working class mechanics.
|
||||
mechanicVerification: "needs-review",
|
||||
};
|
||||
});
|
||||
return {
|
||||
...member,
|
||||
slot,
|
||||
buildId: build.id,
|
||||
talentRanks: build.talentRanks,
|
||||
talentAllocation: "generated role-weighted diagnostic build; requires authored review",
|
||||
rotationProfileId: rotationProfileFor(classId, member.specializationId, member.specialization)?.id ?? null,
|
||||
readiness: "needs-testing",
|
||||
counts: {
|
||||
classNodes: nodes.length,
|
||||
firstPartyNodes: nodes.filter((node) => node.phase === "first-party").length,
|
||||
nodesWithoutRuntimeAbility: nodes.filter((node) => node.runtimeAbilityIds.length === 0).length,
|
||||
nodesWithRetainedSourceOperations: nodes.filter((node) => node.sourceOperationsNeedingReview.length > 0).length,
|
||||
sourceGapNodes: nodes.filter((node) => node.missingGlobalSpellIds.length > 0).length,
|
||||
},
|
||||
nodes,
|
||||
};
|
||||
});
|
||||
const report = {
|
||||
schemaVersion: 1,
|
||||
milestone: "coa-first-party",
|
||||
level,
|
||||
source: {
|
||||
realm: COA_LIVE_SOURCE.realmName,
|
||||
capturedAt: COA_LIVE_SOURCE.synchronizedAt,
|
||||
catalogSha256: sourceSha256,
|
||||
spellDbcPath: COA_LIVE_SOURCE.coaSpellDbcPath,
|
||||
},
|
||||
scope: "Explicit five-class diagnostic; no readiness promotion or balance certification.",
|
||||
bosses: selectedBosses,
|
||||
classes,
|
||||
};
|
||||
const output = resolve(root, "artifacts/coa-first-party", `level-${level}`);
|
||||
await mkdir(output, { recursive: true });
|
||||
const saveJson = (name: string, value: unknown) => writeFile(resolve(output, name), `${JSON.stringify(value, null, 2)}\n`);
|
||||
await saveJson("audit.json", report);
|
||||
await saveJson("config.json", config);
|
||||
for (let slot = 0; slot < COA_FIRST_PARTY_MEMBERS.length; slot += 1) {
|
||||
await saveJson(`seat-${slot + 1}.json`, coaFirstPartySimulationConfig(level, bossKeys, slot));
|
||||
}
|
||||
const markdown = [
|
||||
"# First CoA party — mechanic review inventory",
|
||||
"",
|
||||
`Level ${level}; ${COA_LIVE_SOURCE.realmName} capture ${COA_LIVE_SOURCE.synchronizedAt}.`,
|
||||
"Catalog mappings below are review inventory, not implemented-mechanic counts.",
|
||||
"All five remain Needs Testing. Generated talent allocations need authored review.",
|
||||
"",
|
||||
"| Class | Specialization | Role | All class nodes | First-party nodes | Nodes with retained source operations | Global source-gap nodes |",
|
||||
"| --- | --- | --- | ---: | ---: | ---: | ---: |",
|
||||
...classes.map((entry) => `| ${entry.classId} | ${entry.specialization} | ${entry.role} | ${entry.counts.classNodes} | ${entry.counts.firstPartyNodes} | ${entry.counts.nodesWithRetainedSourceOperations} | ${entry.counts.sourceGapNodes} |`),
|
||||
"",
|
||||
"## Repeatable diagnostics",
|
||||
"",
|
||||
`Bosses: ${selectedBosses.map((boss) => `${boss.name} (${boss.key})`).join(", ")}.`,
|
||||
"Five seat configurations keep the same classes and build IDs while moving the active player.",
|
||||
"The full node checklist, spell IDs, descriptions, and source-operation review flags are in audit.json.",
|
||||
"No encounter simulation runs during preparation.",
|
||||
"",
|
||||
`Source catalog SHA-256: ${sourceSha256}.`,
|
||||
].join("\n");
|
||||
await writeFile(resolve(output, "audit.md"), `${markdown}\n`);
|
||||
console.log(JSON.stringify({ output, classes: classes.map(({ classId, specialization, counts }) => ({ classId, specialization, counts })), configurations: 5, simulatedEncounters: 0 }, null, 2));
|
||||
}
|
||||
Reference in new issue
Block a user