98 lines
2.9 KiB
JavaScript
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`);
|