#!/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)}.`);