94 lines
4.8 KiB
JavaScript
94 lines
4.8 KiB
JavaScript
#!/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)}.`);
|