Update class mechanics, stat rulesets, and CoA reference catalogs
This commit is contained in:
1 parent
b16821e92e
commit
39458d5de6
221 files changed
+2584962
-568
No files matched your search
@@ -0,0 +1,208 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { createRequire } from "node:module";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const { MpqArchive } = require("stormlib-js");
|
||||
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const defaultArchive = path.resolve(projectRoot, "..", "wow335a", "Data", "enUS", "patch-enUS.MPQ");
|
||||
const defaultOutput = path.join(projectRoot, "src", "game", "generated", "wow335PetFamilyData.json");
|
||||
const archivePath = path.resolve(process.argv[2] ?? defaultArchive);
|
||||
const outputPath = path.resolve(process.argv[3] ?? defaultOutput);
|
||||
|
||||
function assertDbc(buffer, name, minimumFields) {
|
||||
if (buffer.toString("ascii", 0, 4) !== "WDBC") throw new Error(`${name} is not WDBC data.`);
|
||||
const fieldCount = buffer.readUInt32LE(8);
|
||||
const recordSize = buffer.readUInt32LE(12);
|
||||
if (fieldCount < minimumFields || recordSize < fieldCount * 4) {
|
||||
throw new Error(`${name} has an unsupported schema.`);
|
||||
}
|
||||
}
|
||||
|
||||
function rowCount(buffer) {
|
||||
return buffer.readUInt32LE(4);
|
||||
}
|
||||
|
||||
function recordSize(buffer) {
|
||||
return buffer.readUInt32LE(12);
|
||||
}
|
||||
|
||||
function recordBase(index, size) {
|
||||
return 20 + index * size;
|
||||
}
|
||||
|
||||
function textAt(buffer, base, firstField, shift = 0) {
|
||||
const count = rowCount(buffer);
|
||||
const stringsOffset = 20 + count * recordSize(buffer);
|
||||
const stringSize = buffer.readUInt32LE(16);
|
||||
for (let locale = 0; locale < 16; locale += 1) {
|
||||
const offset = buffer.readUInt32LE(base + (firstField + shift + locale) * 4);
|
||||
if (!offset || offset >= stringSize) continue;
|
||||
const start = stringsOffset + offset;
|
||||
const end = buffer.indexOf(0, start);
|
||||
const value = buffer.toString("utf8", start, end < 0 ? buffer.length : end);
|
||||
if (value) return value;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function spellRows(buffer, durations) {
|
||||
assertDbc(buffer, "Spell.dbc", 234);
|
||||
const count = rowCount(buffer);
|
||||
const size = recordSize(buffer);
|
||||
const fields = buffer.readUInt32LE(8);
|
||||
const shift = fields >= 239 ? 6 : 0;
|
||||
const shifted = (field) => field >= 77 ? field + shift : field;
|
||||
const rows = new Map();
|
||||
for (let index = 0; index < count; index += 1) {
|
||||
const base = recordBase(index, size);
|
||||
const unsigned = (field) => buffer.readUInt32LE(base + field * 4);
|
||||
const signed = (field) => buffer.readInt32LE(base + field * 4);
|
||||
const float = (field) => buffer.readFloatLE(base + field * 4);
|
||||
const effects = Array.from({ length: 3 }, (_, effectIndex) => {
|
||||
const effectType = unsigned(71 + effectIndex);
|
||||
const auraType = unsigned(shifted(95 + effectIndex));
|
||||
const basePoints = signed(shifted(80 + effectIndex)) + 1;
|
||||
const triggerSpellId = unsigned(shifted(116 + effectIndex));
|
||||
return {
|
||||
index: effectIndex,
|
||||
effectType,
|
||||
auraType,
|
||||
mechanic: unsigned(shifted(83 + effectIndex)),
|
||||
basePoints,
|
||||
dieSides: signed(74 + effectIndex),
|
||||
pointsPerLevel: float(shifted(77 + effectIndex)),
|
||||
periodMs: unsigned(shifted(98 + effectIndex)),
|
||||
miscValue: signed(shifted(110 + effectIndex)),
|
||||
triggerSpellId,
|
||||
coefficient: float(shifted(229 + effectIndex)),
|
||||
};
|
||||
}).filter((effect) => effect.effectType || effect.auraType || effect.basePoints !== 1 || effect.triggerSpellId);
|
||||
const attributes0 = unsigned(4);
|
||||
rows.set(unsigned(0), {
|
||||
spellId: unsigned(0),
|
||||
name: textAt(buffer, base, 136, shift),
|
||||
rankText: textAt(buffer, base, 153, shift),
|
||||
mechanic: unsigned(3),
|
||||
requiredLevel: unsigned(39),
|
||||
passive: Boolean(attributes0 & 0x40),
|
||||
attributesEx1: unsigned(5),
|
||||
powerType: signed(41),
|
||||
powerCost: unsigned(42),
|
||||
cooldownMs: unsigned(29),
|
||||
categoryCooldownMs: unsigned(30),
|
||||
globalCooldownMs: unsigned(shifted(206)),
|
||||
auraInterruptFlags: unsigned(32),
|
||||
schoolMask: unsigned(shifted(225)),
|
||||
procFlags: unsigned(34),
|
||||
procChance: unsigned(35),
|
||||
procCharges: unsigned(36),
|
||||
durationMs: durations.get(unsigned(40)) ?? 0,
|
||||
effects,
|
||||
});
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
function durationTable(buffer) {
|
||||
assertDbc(buffer, "SpellDuration.dbc", 4);
|
||||
const durations = new Map();
|
||||
const size = recordSize(buffer);
|
||||
for (let index = 0; index < rowCount(buffer); index += 1) {
|
||||
const base = recordBase(index, size);
|
||||
durations.set(buffer.readUInt32LE(base), buffer.readInt32LE(base + 3 * 4));
|
||||
}
|
||||
return durations;
|
||||
}
|
||||
|
||||
function familyRows(buffer) {
|
||||
assertDbc(buffer, "CreatureFamily.dbc", 28);
|
||||
const families = [];
|
||||
const size = recordSize(buffer);
|
||||
for (let index = 0; index < rowCount(buffer); index += 1) {
|
||||
const base = recordBase(index, size);
|
||||
const minScale = buffer.readFloatLE(base + 1 * 4);
|
||||
const maxScale = buffer.readFloatLE(base + 3 * 4);
|
||||
const petTalentType = buffer.readInt32LE(base + 8 * 4);
|
||||
const name = textAt(buffer, base, 10);
|
||||
if (!name) continue;
|
||||
families.push({
|
||||
id: buffer.readUInt32LE(base),
|
||||
name,
|
||||
minScale,
|
||||
minScaleLevel: buffer.readUInt32LE(base + 2 * 4),
|
||||
maxScale,
|
||||
maxScaleLevel: buffer.readUInt32LE(base + 4 * 4),
|
||||
petFoodMask: buffer.readUInt32LE(base + 7 * 4),
|
||||
petTalentType,
|
||||
familySkillLineId: buffer.readUInt32LE(base + 5 * 4),
|
||||
commonPetSkillLineId: buffer.readUInt32LE(base + 6 * 4),
|
||||
categoryEnumId: buffer.readUInt32LE(base + 9 * 4),
|
||||
});
|
||||
}
|
||||
return families;
|
||||
}
|
||||
|
||||
function skillRows(buffer, lineIds, spells) {
|
||||
assertDbc(buffer, "SkillLineAbility.dbc", 14);
|
||||
const result = new Map([...lineIds].map((id) => [id, []]));
|
||||
const size = recordSize(buffer);
|
||||
for (let index = 0; index < rowCount(buffer); index += 1) {
|
||||
const base = recordBase(index, size);
|
||||
const lineId = buffer.readUInt32LE(base + 1 * 4);
|
||||
if (!result.has(lineId)) continue;
|
||||
const spellId = buffer.readUInt32LE(base + 2 * 4);
|
||||
const spell = spells.get(spellId);
|
||||
if (!spell) continue;
|
||||
result.get(lineId).push({
|
||||
...spell,
|
||||
acquireMethod: buffer.readUInt32LE(base + 9 * 4),
|
||||
raceMask: buffer.readUInt32LE(base + 3 * 4),
|
||||
classMask: buffer.readUInt32LE(base + 4 * 4),
|
||||
minSkill: buffer.readUInt32LE(base + 7 * 4),
|
||||
supersededBySpellId: buffer.readUInt32LE(base + 8 * 4),
|
||||
});
|
||||
}
|
||||
for (const rows of result.values()) {
|
||||
rows.sort((left, right) => left.requiredLevel - right.requiredLevel || left.spellId - right.spellId);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const archive = MpqArchive.open(archivePath);
|
||||
let output;
|
||||
try {
|
||||
const extract = (name) => archive.extractFile(`DBFilesClient\\${name}.dbc`);
|
||||
const familyBuffer = extract("CreatureFamily");
|
||||
const skillLineBuffer = extract("SkillLineAbility");
|
||||
const spellBuffer = extract("Spell");
|
||||
const durations = durationTable(extract("SpellDuration"));
|
||||
const spells = spellRows(spellBuffer, durations);
|
||||
const families = familyRows(familyBuffer);
|
||||
const lineIds = new Set(families.flatMap((family) => [family.familySkillLineId, family.commonPetSkillLineId]).filter(Boolean));
|
||||
const skills = skillRows(skillLineBuffer, lineIds, spells);
|
||||
output = {
|
||||
source: {
|
||||
product: "World of Warcraft",
|
||||
build: "3.3.5a.12340",
|
||||
archive: path.relative(projectRoot, archivePath).replaceAll("\\", "/"),
|
||||
familyRows: rowCount(familyBuffer),
|
||||
skillLineAbilityRows: rowCount(skillLineBuffer),
|
||||
spellRows: rowCount(spellBuffer),
|
||||
},
|
||||
families: families.map((family) => ({
|
||||
...family,
|
||||
hunterPet: family.petTalentType >= 0,
|
||||
familySkills: skills.get(family.familySkillLineId) ?? [],
|
||||
commonPetSkills: skills.get(family.commonPetSkillLineId) ?? [],
|
||||
})),
|
||||
};
|
||||
} finally {
|
||||
archive.close();
|
||||
}
|
||||
|
||||
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
||||
fs.writeFileSync(outputPath, `${JSON.stringify(output)}\n`);
|
||||
console.log(`Generated ${output.families.filter((family) => family.hunterPet).length} Hunter pet family records at ${outputPath}.`);
|
||||
Reference in new issue
Block a user