Release Healer Man 0.1.14

This commit is contained in:
phenom committed 2026-09-05 11:40:50 -04:00
1 parent 67d7a8ebe7
commit 7b41b102fa
147 files changed
+169892 -1342

No files matched your search

@@ -194,6 +194,12 @@ function renderMarkdown(report) {
"- " + summary.sourceSpellTemplates + " templates preserve source spell evidence ("
+ summary.sourceSpellAssignments + " assignments; " + summary.uniqueSourceSpellIds
+ " unique IDs); " + summary.scriptedTemplates + " preserve AutoPlot/Lua hook evidence.",
"- " + summary.catalogBossMechanics + " modular boss mechanics are active: "
+ summary.sourceBackedBossMechanics + " source-backed and "
+ summary.sourceGapBossMechanics + " explicitly flagged source-gap fallbacks.",
"- Boss signaling includes " + summary.telegraphedBossMechanics
+ " spatial telegraphs and " + summary.healthTriggeredBossMechanics
+ " visible health-triggered mechanics.",
"",
"## Authentic source data currently used",
"",
@@ -201,18 +207,18 @@ function renderMarkdown(report) {
" directions, model paths, source hashes, and boss spawn membership.",
"- Navmesh-projected placements derived from each packaged environment anchor.",
"- Original static meshes; original GR2 clips only where the manifest and GLB prove they exist.",
"- Spell IDs, Lua hooks, AutoPlot names, and Lua signals are retained as evidence, not guessed.",
"- Spell IDs, Lua hooks, AutoPlot names, and Lua signals feed the modular boss-mechanic catalog.",
"",
"## HealerMan-generic or deferred",
"",
"- " + summary.genericDefaultAttacks + " entities use generated Basic Attack/Corrosive Slam.",
"- " + summary.builderAuthoredAttacks + " attacks are builder exceptions without source mapping.",
"- " + summary.builderAuthoredAttacks + " attacks are builder exceptions outside the modular boss catalog.",
"- " + summary.sourceBackedDamageFamilyAttacks
+ " executable attacks carry a conservative source-backed direct-damage family mapping.",
"- Source-backed mapping preserves spell identity, damage family, school, range, and cooldown;",
" HealerMan's 0.85 damage coefficient is intentionally generic, not claimed as source-exact.",
"- Scaling, movement/leash, clustering, aggro, death state, loot, and objective ordering are generic.",
"- Doors, phases, summons, patrols, resets, interactions, and source loot remain review-required.",
"- Unmapped complex doors, multi-actor phases, patrols, resets, interactions, and source loot remain review-required.",
"- Procedural animation states are HealerMan-authored; native GLBs must provide idle, move,",
" attack, cast, wound, and death families.",
"",
@@ -243,8 +249,8 @@ function renderMarkdown(report) {
...report.blockerGroups.navigation.derivedEntries.map((entry) => "- " + entry.dungeonId
+ ": " + entry.from + " -> " + entry.to + " (" + entry.orderKind + ")"),
"",
"Unimplemented source mechanics: " + report.blockerGroups.sourceMechanics.scriptedTemplates
+ " templates preserve AutoPlot/Lua evidence, with executable generation deliberately disabled.",
"Source-mechanics review: " + report.blockerGroups.sourceMechanics.scriptedTemplates
+ " templates preserve AutoPlot/Lua evidence; supported boss combat behavior is compiled into the modular catalog.",
"",
...Object.entries(report.blockerGroups.sourceMechanics.evidenceCounts)
.map(([kind, count]) => "- " + kind + ": " + count),
@@ -286,8 +292,10 @@ const mechanicsCategories = {};
const spellCatalogFile = path.join(
root, "scripts/runewaker-pipeline/snapshots/runewaker-npc-spell-catalog.json",
);
const bossMechanicsFile = path.join(root, "dungeon-pipeline/boss-mechanics.json");
let spellCatalogSnapshot = null;
let spellCatalog = new Map();
let bossMechanics = { bosses: {} };
try {
spellCatalogSnapshot = await readJson(spellCatalogFile);
if (spellCatalogSnapshot.status !== "portable"
@@ -305,6 +313,14 @@ try {
} catch (error) {
issues.push("Could not load the portable RuneWaker spell catalog: " + error.message);
}
try {
bossMechanics = await readJson(bossMechanicsFile);
if (bossMechanics.schemaVersion !== 1) {
issues.push("RuneWaker boss mechanics use an unsupported catalog schema.");
}
} catch (error) {
issues.push("Could not load the modular boss mechanic catalog: " + error.message);
}
const registrySource = await readFile(
path.join(root, "src/game/generated/runewakerDungeonDefinitions.generated.ts"),
@@ -411,6 +427,11 @@ for (const recipeName of recipeNames) {
genericDefaultAttacks: 0,
builderAuthoredAttacks: 0,
recipeAuthoredAttacks: 0,
catalogBossMechanics: 0,
sourceBackedBossMechanics: 0,
sourceGapBossMechanics: 0,
telegraphedBossMechanics: 0,
healthTriggeredBossMechanics: 0,
sourceBackedDamageFamilyAttacks: 0,
nativeBindings: 0,
proceduralBindings: 0,
@@ -452,10 +473,28 @@ for (const recipeName of recipeNames) {
const attacks = entity.combat?.attacks ?? [];
if (!attacks.length) issue(issues, dungeonId, entityId + " has no combat family.");
const authoredBoss = bossMechanics.bosses?.[entityId];
const authoredMechanicIds = new Set(
(authoredBoss?.mechanics ?? []).map((mechanic) => mechanic.id),
);
const authoredSourceSpellIds = new Set(
(authoredBoss?.source?.spellIds ?? []).map(Number),
);
if (entity.kind === "boss") {
if (!authoredBoss) issue(issues, dungeonId, entityId + " has no modular boss mechanic kit.");
const runtimeMechanicIds = entity.combat?.mechanicIds ?? [];
if (runtimeMechanicIds.length !== authoredMechanicIds.size
|| runtimeMechanicIds.some((id) => !authoredMechanicIds.has(id))) {
issue(issues, dungeonId, entityId + " mechanic id registry drifted from the modular catalog.");
}
}
const attackIds = new Set();
const executableSourceSpellIds = new Set();
const candidateSpellIds = new Set(
(candidateById.get(templateId)?.spells ?? []).map((spell) => Number(spell.id)),
[
...(candidateById.get(templateId)?.spells ?? []).map((spell) => Number(spell.id)),
...authoredSourceSpellIds,
],
);
for (const attack of attacks) {
counts.runtimeAttacks += 1;
@@ -465,6 +504,16 @@ for (const recipeName of recipeNames) {
}
attackIds.add(attack.id);
const recipeAttack = template?.attacks?.find((entry) => entry.id === attack.id);
const catalogAuthored = authoredMechanicIds.has(attack.id);
if (catalogAuthored) {
counts.catalogBossMechanics += 1;
if (authoredBoss.source?.kind === "runewaker") counts.sourceBackedBossMechanics += 1;
else counts.sourceGapBossMechanics += 1;
if (attack.telegraph) counts.telegraphedBossMechanics += 1;
if (attack.condition?.kind === "self-health-percent") {
counts.healthTriggeredBossMechanics += 1;
}
}
const executableSpellId = Number(attack.spellId ?? attack.source?.spellId ?? 0);
if (executableSpellId > 0) {
executableSourceSpellIds.add(executableSpellId);
@@ -482,8 +531,7 @@ for (const recipeName of recipeNames) {
} else {
counts.sourceBackedDamageFamilyAttacks += 1;
if (attack.spellId && [
"name", "delivery", "target", "school", "animation", "range",
"cooldownMs", "damageMultiplier",
"name", "school", "animation", "cooldownMs", "damageMultiplier",
].some((field) => attack[field] !== (
field === "name" ? catalogSpell.name : catalogSpell.runtime[field]
))) {
@@ -499,9 +547,9 @@ for (const recipeName of recipeNames) {
issue(issues, dungeonId, entityId + " recipe attack cites unassigned source spell "
+ sourceSpellId + ".");
}
} else if (attack.id.endsWith("-basic-attack")) {
} else if (attack.id === "basic-attack" || attack.id.endsWith("-basic-attack")) {
counts.genericDefaultAttacks += 1;
} else if (!attack.spellId) {
} else if (!catalogAuthored && !attack.spellId) {
counts.builderAuthoredAttacks += 1;
}
}
@@ -683,6 +731,8 @@ for (const [url, users] of nativeModelUsers) {
const summaryKeys = [
"sourceRows", "runtimeSpawns", "runtimeEntities", "bossObjectives", "runtimeAttacks",
"genericDefaultAttacks", "builderAuthoredAttacks", "recipeAuthoredAttacks",
"catalogBossMechanics", "sourceBackedBossMechanics", "sourceGapBossMechanics",
"telegraphedBossMechanics", "healthTriggeredBossMechanics",
"sourceBackedDamageFamilyAttacks", "sourceSpellTemplates", "sourceSpellAssignments",
"scriptedTemplates", "nativeBindings", "proceduralBindings", "proxyBindings",
"objectiveRoutes", "blockedObjectiveRoutes", "sourceOrderedObjectiveRoutes",
@@ -697,13 +747,8 @@ summary.uniqueSourceSpellIds = sourceSpellIds.size;
summary.portableSpellCatalogRecords = spellCatalog.size;
summary.portableDamageFamilyRecords = spellCatalogSnapshot?.damageFamilyCount ?? 0;
summary.portableEvidenceOnlyRecords = spellCatalogSnapshot?.evidenceOnlyCount ?? 0;
if (summary.sourceBackedDamageFamilyAttacks < summary.runtimeAttacks) {
reviewNotes.push((summary.runtimeAttacks - summary.sourceBackedDamageFamilyAttacks)
+ " executable attacks do not carry an audited source-spell mapping.");
}
if (dungeons.some((dungeon) => dungeon.bossObjectives && !dungeon.sourceBackedDamageFamilyAttacks)) {
reviewNotes.push("Boss objectives are spawn-authentic, but boss attacks and state machines remain generic.");
}
reviewNotes.push(summary.sourceGapBossMechanics
+ " boss mechanics are conservative, telegraphed fallbacks for preserved bosses whose scripts expose no executable combat spell.");
reviewNotes.push("Source-backed spell coefficients are HealerMan-generic; the original RuneWaker "
+ "formula/stat scaling has not been reconstructed.");
const navigationBlockerEntries = dungeons.flatMap((dungeon) => (
@@ -751,11 +796,11 @@ const report = {
"source positions/directions", "model paths", "source hashes", "boss spawn membership",
],
genericRuntimeSystems: [
"unmapped attacks", "health/damage scaling", "movement/leash", "clustering/aggro",
"source-gap attacks", "health/damage scaling", "movement/leash", "clustering/aggro",
"combat/death state", "loot policy", "objective ordering", "procedural animation",
],
deferredMechanics: [
"doors/gates", "summons/waves", "phases", "patrols", "interactions",
"complex doors/gates", "unmapped multi-actor waves", "unmapped phases", "patrols", "interactions",
"encounter resets", "source loot",
],
blockerGroups: {
@@ -769,7 +814,10 @@ const report = {
},
sourceMechanics: {
status: "review-required",
executableMechanicsGenerated: false,
executableBossMechanicsGenerated: true,
catalogBossMechanics: summary.catalogBossMechanics,
sourceBackedBossMechanics: summary.sourceBackedBossMechanics,
sourceGapBossMechanics: summary.sourceGapBossMechanics,
scriptedTemplates: summary.scriptedTemplates,
evidenceCounts: Object.fromEntries(Object.entries(mechanicsCategories).sort()),
},
@@ -779,7 +827,7 @@ const report = {
status: spellCatalogSnapshot?.status ?? "missing",
safety: spellCatalogSnapshot?.safety ?? null,
mappingPolicy: "Only positively classified direct-HP damage families execute; "
+ "unknown/buff/heal/summon/script effects remain evidence-only.",
+ "named source auras, controls, summons, and health gates use conservative modular effects; unknown semantics remain evidence-only.",
},
resolvedRuntimeObservations,
documentedExceptions,