Files
healer-man/scripts/runewaker-pipeline/export-rom-native-damage-profiles.mjs
T
2026-10-04 13:13:35 -04:00

94 lines
4.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
import { createHash } from "node:crypto";
import { readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
const scriptDirectory = path.dirname(fileURLToPath(import.meta.url));
const projectRoot = path.resolve(scriptDirectory, "../..");
const inputPath = path.join(projectRoot, "src", "game", "generated", "romHeirloomReference.json");
const outputPath = path.join(projectRoot, "src", "game", "generated", "romNativeDamageTrainingProfiles.generated.json");
const classes = ["rom-knight", "rom-priest", "rom-scout", "rom-rogue", "rom-mage"];
const slots = ["main-hand", "off-hand", "ranged"];
const sha256 = (buffer) => createHash("sha256").update(buffer).digest("hex");
const inputBuffer = await readFile(inputPath);
const catalog = JSON.parse(inputBuffer.toString("utf8"));
const selectedItemIds = new Set(Object.values(catalog.profiles).flatMap((profile) =>
Object.values(profile).flatMap((curve) => curve.flatMap((row) => row.referenceItemIds))));
const selectedItems = catalog.sourceItems.filter((item) => selectedItemIds.has(item.itemId));
const itemById = new Map(selectedItems.map((item) => [item.itemId, item]));
if (selectedItems.length !== selectedItemIds.size || selectedItems.some((item) =>
item.unmappedEffects?.length || item.unresolvedAttributeIds?.length)) {
throw new Error("Training reference has unresolved native body channels.");
}
const profiles = {};
for (const classId of classes) {
const sourceProfile = catalog.profiles?.[classId];
if (!sourceProfile) throw new Error(`Missing source training profile for ${classId}.`);
const classProfile = {};
for (const slot of slots) {
const curve = sourceProfile[slot];
if (!curve) continue;
if (curve.length !== 79) throw new Error(`${classId}/${slot} must cover levels 1–79.`);
let previousShieldDefense = 0;
classProfile[slot] = curve.map((entry, index) => {
const weaponDamage = entry.stats?.weaponDamage ?? 0;
const magicalWeaponDamage = entry.stats?.magicalWeaponDamage ?? 0;
const speedTenths = entry.speedTenths ?? null;
const referenceItems = entry.referenceItemIds.map((id) => itemById.get(id));
const weaponType = referenceItems.length && referenceItems.every((item) => item?.sourceCatalog === "weaponobject.db"
&& item.type === referenceItems[0].type) ? referenceItems[0].type : null;
const shields = referenceItems.length && referenceItems.every((item) => item?.sourceCatalog === "armorobject.db"
&& item.type === 5) ? [...referenceItems].sort((left, right) => left.level - right.level) : [];
if (shields.length) {
const level = index + 1;
const lower = shields[0];
const upper = shields.at(-1);
const blend = level < lower.level ? level / lower.level : level > upper.level ? level / upper.level
: lower.level === upper.level ? 0 : (level - lower.level) / (upper.level - lower.level);
const rawDefense = level < lower.level ? (upper.baseStats.physicalDefense ?? 0) * blend
: level > upper.level ? (lower.baseStats.physicalDefense ?? 0) * blend
: (lower.baseStats.physicalDefense ?? 0) * (1 - blend) + (upper.baseStats.physicalDefense ?? 0) * blend;
previousShieldDefense = Math.max(previousShieldDefense, rawDefense > 0 ? Math.max(1, Math.round(rawDefense)) : 0);
}
if (![weaponDamage, magicalWeaponDamage].every(Number.isFinite)
|| (speedTenths !== null && !Number.isFinite(speedTenths))) {
throw new Error(`Non-finite source training weapon row ${classId}/${slot}/${index + 1}.`);
}
return {
level: index + 1,
sourceItemIds: entry.referenceItemIds,
weaponDamage,
magicalWeaponDamage,
speedTenths,
weaponType,
...(shields.length ? { shieldDefense: previousShieldDefense } : {}),
};
});
}
profiles[classId] = classProfile;
}
const output = {
schemaVersion: 1,
status: "compact source-selected training weapon references; simulator gear profile and level interpolation policy",
source: {
file: "src/game/generated/romHeirloomReference.json",
sha256: sha256(inputBuffer),
sourceItemDatabases: catalog.sourceFiles,
quality: catalog.quality,
levelCap: 79,
selectedItemCount: selectedItems.length,
// Selection excludes unmapped effects, including typed MPow/Resist/AllRange.
// These are item contributions only; actor auras and racial/source base inputs remain separate.
trainingItemBodyDefaults: {
perSchoolMagicPowerRate: 1, perSchoolResistance: 0, perSchoolResistanceRate: 1,
allRangeDamageRate: 1, throwDamageRate: 1, throwingWeaponDamage: 0,
},
},
profiles,
};
await writeFile(outputPath, `${JSON.stringify(output, null, 2)}\n`);
console.log(`Wrote compact native training weapon profiles to ${path.relative(projectRoot, outputPath)}.`);