Files
healer-man/scripts/extract-wow335-combat-ratings.mjs
2026-08-14 15:56:39 -04:00

98 lines
2.9 KiB
JavaScript

import fs from "node:fs";
import path from "node:path";
const [combatRatingsPath, classScalarsPath, outputPath] = process.argv.slice(2);
if (!combatRatingsPath || !classScalarsPath || !outputPath) {
throw new Error(
"Usage: node extract-wow335-combat-ratings.mjs <GtCombatRatings.dbc> "
+ "<GtOCTClassCombatRatingScalar.dbc> <output.json>",
);
}
function readWdbc(filePath) {
const data = fs.readFileSync(filePath);
if (data.toString("ascii", 0, 4) !== "WDBC") {
throw new Error(`${filePath} is not a WDBC file`);
}
const records = data.readUInt32LE(4);
const fields = data.readUInt32LE(8);
const recordSize = data.readUInt32LE(12);
const recordsEnd = 20 + records * recordSize;
if (recordsEnd > data.length) {
throw new Error(`${filePath} has a truncated record block`);
}
return { data, records, fields, recordSize };
}
const combatRatings = readWdbc(combatRatingsPath);
if (combatRatings.fields !== 1 || combatRatings.recordSize !== 4 || combatRatings.records < 2_500) {
throw new Error("GtCombatRatings.dbc has an unexpected 3.3.5a layout");
}
const classScalars = readWdbc(classScalarsPath);
if (classScalars.fields !== 2 || classScalars.recordSize !== 8) {
throw new Error("GtOCTClassCombatRatingScalar.dbc has an unexpected 3.3.5a layout");
}
const ratingIds = Object.freeze({
defense: 1,
dodge: 2,
parry: 3,
block: 4,
meleeHit: 5,
rangedHit: 6,
spellHit: 7,
meleeCrit: 8,
rangedCrit: 9,
spellCrit: 10,
resilience: 14,
meleeHaste: 17,
rangedHaste: 18,
spellHaste: 19,
expertise: 23,
armorPenetration: 24,
});
const maxLevel = 80;
const dbcLevelStride = 100;
const ratios = {};
for (const [name, ratingId] of Object.entries(ratingIds)) {
ratios[name] = Array.from({ length: maxLevel }, (_, index) => (
combatRatings.data.readFloatLE(20 + (ratingId * dbcLevelStride + index) * 4)
));
}
const scalarById = new Map();
for (let index = 0; index < classScalars.records; index += 1) {
const offset = 20 + index * classScalars.recordSize;
scalarById.set(
classScalars.data.readUInt32LE(offset),
classScalars.data.readFloatLE(offset + 4),
);
}
const classIds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 11];
const classRatingStride = 32;
const scalars = {};
for (const classId of classIds) {
scalars[classId] = {};
for (const [name, ratingId] of Object.entries(ratingIds)) {
const scalarId = (classId - 1) * classRatingStride + ratingId + 1;
const scalar = scalarById.get(scalarId);
if (!Number.isFinite(scalar)) {
throw new Error(`Missing class scalar ${scalarId} for class ${classId} and ${name}`);
}
scalars[classId][name] = scalar;
}
}
const output = {
schemaVersion: 1,
source: "World of Warcraft 3.3.5a client GtCombatRatings/GtOCTClassCombatRatingScalar",
maxLevel,
ratingIds,
ratios,
scalars,
};
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(outputPath, `${JSON.stringify(output)}\n`);