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 " + " ", ); } 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`);