forked from phenom/healer-man
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
+131
-81
@@ -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 });
|
||||
|
||||
Reference in new issue
Block a user