Update Healer Man server

This commit is contained in:
phenom committed 2026-10-04 13:13:35 -04:00
1 parent 39458d5de6
commit 6cc1b9217f
203 files changed
+133929 -31271

No files matched your search

+152
View File
@@ -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));
}