Update class mechanics, stat rulesets, and CoA reference catalogs

This commit is contained in:
phenom committed 2026-10-04 12:56:30 -04:00
1 parent b16821e92e
commit 39458d5de6
221 files changed
+2584962 -568

No files matched your search

+131 -81
View File
@@ -1,5 +1,11 @@
import fs from "node:fs";
import path from "node:path";
import {
collectRealmSpellIds,
expandCoaSpellIds,
resolveCoaSourcePaths,
sharedRealmSpellIds,
} from "./coa-source-policy.mjs";
const projectRoot = path.resolve(import.meta.dirname, "..");
const outputPath = path.join(projectRoot, "src", "game", "coaLiveData.generated.json");
@@ -10,63 +16,18 @@ const extractedClientRoot = path.resolve(
"LadiksMPQEditor",
"client-current",
);
const realmSpellDbc = path.join(
extractedClientRoot,
"area-52",
"patch-D",
"DBFilesClient",
"Spell.dbc",
);
const globalSpellDbc = path.join(
extractedClientRoot,
"patch-T",
"DBFilesClient",
"Spell.dbc",
);
const legacySpellDbc = path.resolve(
projectRoot,
"..",
"LadiksMPQEditor",
"patch-T",
"DBFilesClient",
"Spell.dbc",
);
const realmSkillLineAbilityDbc = path.join(
extractedClientRoot,
"area-52",
"patch-D",
"DBFilesClient",
"SkillLineAbility.dbc",
);
const globalSkillLineAbilityDbc = path.join(
extractedClientRoot,
"patch-M",
"DBFilesClient",
"SkillLineAbility.dbc",
);
const legacySkillLineAbilityDbc = path.resolve(
projectRoot,
"..",
"LadiksMPQEditor",
"patch-M",
"DBFilesClient",
"SkillLineAbility.dbc",
);
const defaultSpellDbc = fs.existsSync(realmSpellDbc)
? realmSpellDbc
: fs.existsSync(globalSpellDbc)
? globalSpellDbc
: legacySpellDbc;
const defaultSkillLineAbilityDbc = fs.existsSync(realmSkillLineAbilityDbc)
? realmSkillLineAbilityDbc
: fs.existsSync(globalSkillLineAbilityDbc)
? globalSkillLineAbilityDbc
: legacySkillLineAbilityDbc;
const cliArguments = process.argv.slice(2);
const fetchLiveTooltips = process.env.ASCENSION_COA_FETCH_TOOLTIPS === "1"
|| cliArguments.includes("--tooltips");
const spellDbcArgument = cliArguments.find((argument) => argument !== "--tooltips");
const spellDbcPath = path.resolve(spellDbcArgument ?? defaultSpellDbc);
const legacyExtractedRoot = path.resolve(projectRoot, "..", "LadiksMPQEditor");
const sourcePaths = resolveCoaSourcePaths({
extractedClientRoot,
legacyExtractedRoot,
classicSpellOverride: spellDbcArgument,
});
const classicSpellDbcPath = sourcePaths.classicSpellDbcPath;
const coaSpellDbcPath = sourcePaths.coaSpellDbcPath;
const legacySupportDbcDirectory = path.resolve(
projectRoot,
"..",
@@ -314,6 +275,26 @@ function dbcRankCandidates(buffer, names, spellSkillLines) {
return candidates;
}
function rankCandidatesForRows(classSpellRows, spellDbcBuffer, skillLineAbilityDbcPath) {
const progressionRows = classSpellRows.filter((spell) => (
!spell.excluded
&& !spell.advancementType
&& /^Rank \d+$/i.test(spell.rank)
&& !isDeprecated(`${spell.name} ${spell.rank}`)
));
const progressionNames = new Set(progressionRows.map((spell) => spell.name));
const progressionSkillLines = new Map();
for (const spell of progressionRows) {
const skillLines = progressionSkillLines.get(spell.name) ?? new Set();
for (const skillLineId of spell.skillLineIds) skillLines.add(skillLineId);
progressionSkillLines.set(spell.name, skillLines);
}
const spellSkillLines = dbcSkillLines(skillLineAbilityDbcPath);
const candidates = dbcRankCandidates(spellDbcBuffer, progressionNames, spellSkillLines)
.filter((candidate) => candidate.skillLineIds.some((skillLineId) => progressionSkillLines.get(candidate.name)?.has(skillLineId)));
return { progressionNames, candidates };
}
function dbcSpells(buffer, wantedIds, castTimes, durations, radii, ranges) {
assert(buffer.toString("ascii", 0, 4) === "WDBC", "Spell.dbc is not a WDBC file");
const recordCount = buffer.readUInt32LE(4);
@@ -501,38 +482,55 @@ const entries = Object.values(realm.talents.entriesByTab)
.sort((left, right) => left.classId - right.classId || left.tabId - right.tabId || left.y - right.y || left.x - right.x || left.id - right.id);
assert(entries.length >= 3_500, `Only received ${entries.length} CoA tree entries`);
const progressionNames = new Set(classSpellRows
.filter((spell) => (
!spell.excluded
&& !spell.advancementType
&& /^Rank \d+$/i.test(spell.rank)
&& !isDeprecated(`${spell.name} ${spell.rank}`)
))
.map((spell) => spell.name));
assert(fs.existsSync(spellDbcPath), `Spell.dbc was not found at ${spellDbcPath}`);
const spellDbcBuffer = fs.readFileSync(spellDbcPath);
const siblingSkillLineAbilityDbc = path.join(path.dirname(spellDbcPath), "SkillLineAbility.dbc");
const spellSkillLines = dbcSkillLines(
fs.existsSync(siblingSkillLineAbilityDbc) ? siblingSkillLineAbilityDbc : defaultSkillLineAbilityDbc,
assert(fs.existsSync(classicSpellDbcPath), `Classic Spell.dbc was not found at ${classicSpellDbcPath}`);
assert(fs.existsSync(coaSpellDbcPath), `CoA base Spell.dbc was not found at ${coaSpellDbcPath}`);
assert(fs.existsSync(sourcePaths.classicSkillLineAbilityDbcPath), `Classic SkillLineAbility.dbc was not found at ${sourcePaths.classicSkillLineAbilityDbcPath}`);
assert(fs.existsSync(sourcePaths.coaSkillLineAbilityDbcPath), `CoA SkillLineAbility.dbc was not found at ${sourcePaths.coaSkillLineAbilityDbcPath}`);
const classicSpellDbcBuffer = fs.readFileSync(classicSpellDbcPath);
const coaSpellDbcBuffer = fs.readFileSync(coaSpellDbcPath);
const classicProgression = rankCandidatesForRows(
classSpellRows.filter((spell) => classicClassIds.includes(spell.classId)),
classicSpellDbcBuffer,
sourcePaths.classicSkillLineAbilityDbcPath,
);
const progressionSkillLines = new Map();
for (const spell of classSpellRows.filter((candidate) => progressionNames.has(candidate.name))) {
const skillLines = progressionSkillLines.get(spell.name) ?? new Set();
for (const skillLineId of spell.skillLineIds) skillLines.add(skillLineId);
progressionSkillLines.set(spell.name, skillLines);
}
const rankCandidates = dbcRankCandidates(spellDbcBuffer, progressionNames, spellSkillLines)
.filter((candidate) => candidate.skillLineIds.some((skillLineId) => progressionSkillLines.get(candidate.name)?.has(skillLineId)));
const coaProgression = rankCandidatesForRows(
classSpellRows.filter((spell) => coaClassIds.includes(spell.classId)),
coaSpellDbcBuffer,
sourcePaths.coaSkillLineAbilityDbcPath,
);
const progressionNames = new Set([...classicProgression.progressionNames, ...coaProgression.progressionNames]);
const classicRankCandidates = classicProgression.candidates;
const coaRankCandidates = coaProgression.candidates;
const rankCandidates = [...classicRankCandidates, ...coaRankCandidates];
const coaEntrySpellIds = entries.flatMap((entry) => entry.rankDescriptions.map((rank) => rank.spellId));
const { classicIds: classicSpellIds, coaIds: coaRootSpellIds } = collectRealmSpellIds({
classicClassIds,
coaClassIds,
classSpellRows,
classicRankCandidates,
coaRankCandidates,
coaEntrySpellIds,
});
const coaSpellIds = expandCoaSpellIds(coaSpellDbcBuffer, coaRootSpellIds);
const wantedSpellIds = new Set([
...classSpellRows.map((spell) => spell.id),
...entries.flatMap((entry) => entry.rankDescriptions.map((rank) => rank.spellId)),
...coaEntrySpellIds,
...rankCandidates.map((candidate) => candidate.id),
...coaSpellIds,
]);
const castTimes = dbcNumberTable(path.join(supportDbcDirectory, "SpellCastTimes.dbc"), 1);
const durations = dbcNumberTable(path.join(supportDbcDirectory, "SpellDuration.dbc"), 3);
const radii = dbcNumberTable(path.join(supportDbcDirectory, "SpellRadius.dbc"), 3, true);
const ranges = dbcRangeTable(path.join(supportDbcDirectory, "SpellRange.dbc"));
const dbcById = dbcSpells(spellDbcBuffer, wantedSpellIds, castTimes, durations, radii, ranges);
const classicDbcById = dbcSpells(classicSpellDbcBuffer, classicSpellIds, castTimes, durations, radii, ranges);
const coaDbcById = dbcSpells(coaSpellDbcBuffer, coaSpellIds, castTimes, durations, radii, ranges);
const dbcById = new Map();
for (const spellId of wantedSpellIds) {
const dbc = classicSpellIds.has(spellId)
? classicDbcById.get(spellId)
: coaDbcById.get(spellId);
if (dbc) dbcById.set(spellId, dbc);
}
const entryRankBySpellId = new Map(entries.flatMap((entry) => (
entry.rankDescriptions.map((rank) => [rank.spellId, { ...rank, entryId: entry.id }])
)));
@@ -554,8 +552,8 @@ const progressionTooltipIds = [...new Set([
...classSpellRows
.filter((spell) => progressionNames.has(spell.name) && /^Rank \d+$/i.test(spell.rank))
.map((spell) => spell.id),
...rankCandidates.map((candidate) => candidate.id),
])];
...classicRankCandidates.map((candidate) => candidate.id),
])].filter((id) => classicSpellIds.has(id));
if (fetchLiveTooltips) {
const uncached = progressionTooltipIds.filter((spellId) => !previousTooltips.has(spellId));
console.log(`Fetching ${uncached.length} live spell tooltips (${previousTooltips.size} cached)`);
@@ -582,12 +580,33 @@ if (fetchLiveTooltips) {
}
const spellClassIds = new Map();
const coaSpellClassIds = new Map();
for (const spell of classSpellRows) {
const existing = spellClassIds.get(spell.id) ?? new Set();
existing.add(spell.classId);
spellClassIds.set(spell.id, existing);
if (coaClassIds.includes(spell.classId)) {
const coaExisting = coaSpellClassIds.get(spell.id) ?? new Set();
coaExisting.add(spell.classId);
coaSpellClassIds.set(spell.id, coaExisting);
}
}
const classRowBySpellId = new Map(classSpellRows.map((spell) => [spell.id, spell]));
const coaClassSpellRows = classSpellRows.filter((spell) => coaClassIds.includes(spell.classId));
const coaClassRowBySpellId = new Map(coaClassSpellRows.map((spell) => [spell.id, spell]));
const coaEntryBySpellId = new Map();
for (const entry of entries) {
for (const rank of entry.rankDescriptions) {
coaSpellClassIds.set(rank.spellId, (coaSpellClassIds.get(rank.spellId) ?? new Set()).add(entry.classId));
if (!coaEntryBySpellId.has(rank.spellId)) coaEntryBySpellId.set(rank.spellId, entry);
}
}
for (const candidate of coaRankCandidates) {
for (const spell of coaClassSpellRows) {
if (spell.name !== candidate.name || !spell.skillLineIds.some((id) => candidate.skillLineIds.includes(id))) continue;
coaSpellClassIds.set(candidate.id, (coaSpellClassIds.get(candidate.id) ?? new Set()).add(spell.classId));
}
}
const spells = [...wantedSpellIds]
.sort((left, right) => left - right)
.map((id) => {
@@ -602,7 +621,29 @@ const spells = [...wantedSpellIds]
level: classRow?.level ?? dbc?.spellLevel ?? 0,
rank: classRow?.rank || dbc?.rankText || (entryRank ? `Rank ${entryRank.rank}` : ""),
description: entryRank?.description || dbc?.description || "",
...(previousTooltips.has(id) ? { liveTooltip: previousTooltips.get(id) } : {}),
...(classicSpellIds.has(id) && previousTooltips.has(id) ? { liveTooltip: previousTooltips.get(id) } : {}),
sourceIds: classRow?.sourceIds ?? [],
advancementType: classRow?.advancementType || "",
advancementTabId: classRow?.advancementTabId || 0,
talented: classRow?.talented ?? false,
excluded: classRow?.excluded ?? false,
...(dbc ? { dbc } : {}),
};
});
const coaSpellOverrides = sharedRealmSpellIds(classicSpellIds, coaSpellIds)
.map((id) => {
const classRow = coaClassRowBySpellId.get(id);
const entry = coaEntryBySpellId.get(id);
const entryRank = entryRankBySpellId.get(id);
const dbc = coaDbcById.get(id);
return {
id,
classIds: [...(coaSpellClassIds.get(id) ?? [])].sort((left, right) => left - right),
name: classRow?.name || entry?.name || dbc?.name || "",
iconBasename: classRow?.iconBasename || entry?.iconBasename || "",
level: classRow?.level ?? dbc?.spellLevel ?? 0,
rank: classRow?.rank || dbc?.rankText || (entryRank ? `Rank ${entryRank.rank}` : ""),
description: entryRank?.description || dbc?.description || "",
sourceIds: classRow?.sourceIds ?? [],
advancementType: classRow?.advancementType || "",
advancementTabId: classRow?.advancementTabId || 0,
@@ -683,6 +724,9 @@ for (const [classId, rows] of classPages) {
});
}
}
const classRankCandidates = classicClassIds.includes(classId)
? classicRankCandidates
: coaRankCandidates;
for (const chain of bySignature.values()) {
const {
name,
@@ -693,7 +737,7 @@ for (const [classId, rows] of classPages) {
advancementType,
ranks,
} = chain;
for (const candidate of rankCandidates.filter((rank) => (
for (const candidate of classRankCandidates.filter((rank) => (
rank.name === name
&& rank.skillLineIds.some((skillLineId) => chainSkillLines.has(skillLineId))
))) {
@@ -748,7 +792,11 @@ const output = {
maximumLevel: Number(realm.max_level),
schemaVersion: realm.schema_version,
synchronizedAt: new Date().toISOString(),
spellDbcPath: spellDbcPath.replaceAll("\\", "/"),
spellDbcPath: classicSpellDbcPath.replaceAll("\\", "/"),
classicSpellDbcPath: classicSpellDbcPath.replaceAll("\\", "/"),
classicSkillLineAbilityDbcPath: sourcePaths.classicSkillLineAbilityDbcPath.replaceAll("\\", "/"),
coaSpellDbcPath: coaSpellDbcPath.replaceAll("\\", "/"),
coaSkillLineAbilityDbcPath: sourcePaths.coaSkillLineAbilityDbcPath.replaceAll("\\", "/"),
supportDbcDirectory: supportDbcDirectory.replaceAll("\\", "/"),
},
counts: {
@@ -762,6 +810,7 @@ const output = {
classicClassSpellRows: classSpellRows.filter((spell) => classicClassIds.includes(spell.classId)).length,
coaClassSpellRows: classSpellRows.filter((spell) => coaClassIds.includes(spell.classId)).length,
uniqueSpells: spells.length,
coaSpellOverrides: coaSpellOverrides.length,
dbcMatchedSpells: spells.filter((spell) => spell.dbc).length,
progressionChains: progressionChains.length,
classicProgressionChains: progressionChains.filter((chain) => classicClassIds.includes(chain.classId)).length,
@@ -776,6 +825,7 @@ const output = {
entries,
progressionChains,
spells,
coaSpellOverrides,
};
fs.mkdirSync(path.dirname(outputPath), { recursive: true });