Release Healer Man 0.1.49
This commit is contained in:
1 parent
39425f2494
commit
612170afd0
91 files changed
+2278
-263
No files matched your search
@@ -25,6 +25,19 @@ const CLASS_SOURCES = Object.freeze({
|
||||
druid: { classMask: 1024, skillLines: [574, 134, 573] },
|
||||
});
|
||||
|
||||
const CLASS_BY_TALENT_TAB = new Map([
|
||||
[161, "warrior"], [164, "warrior"], [163, "warrior"],
|
||||
[382, "paladin"], [383, "paladin"], [381, "paladin"],
|
||||
[361, "hunter"], [363, "hunter"], [362, "hunter"],
|
||||
[182, "rogue"], [181, "rogue"], [183, "rogue"],
|
||||
[201, "priest"], [202, "priest"], [203, "priest"],
|
||||
[398, "death-knight"], [399, "death-knight"], [400, "death-knight"],
|
||||
[261, "shaman"], [263, "shaman"], [262, "shaman"],
|
||||
[81, "mage"], [41, "mage"], [61, "mage"],
|
||||
[302, "warlock"], [303, "warlock"], [301, "warlock"],
|
||||
[283, "druid"], [281, "druid"], [282, "druid"],
|
||||
]);
|
||||
|
||||
// Triggered subspells can appear on a class skill line even though the player
|
||||
// never learns them directly. Their owning active spell/talent remains present.
|
||||
const TRIGGERED_ONLY_NAMES = new Set([
|
||||
@@ -202,16 +215,19 @@ function parseSpells(buffer, castTimes, durations, radii, ranges, icons) {
|
||||
return spells;
|
||||
}
|
||||
|
||||
function talentSpellIds(buffer) {
|
||||
function talentNodesBySpellId(buffer) {
|
||||
assertDbc(buffer, "Talent.dbc", 23);
|
||||
const count = buffer.readUInt32LE(4);
|
||||
const recordSize = buffer.readUInt32LE(12);
|
||||
const result = new Set();
|
||||
const result = new Map();
|
||||
for (let index = 0; index < count; index += 1) {
|
||||
const base = 20 + index * recordSize;
|
||||
const talentNodeId = `talent-${buffer.readUInt32LE(base)}`;
|
||||
const classId = CLASS_BY_TALENT_TAB.get(buffer.readUInt32LE(base + 4));
|
||||
if (!classId) continue;
|
||||
for (let rank = 0; rank < 9; rank += 1) {
|
||||
const spellId = buffer.readUInt32LE(base + (4 + rank) * 4);
|
||||
if (spellId) result.add(spellId);
|
||||
if (spellId) result.set(spellId, { talentNodeId, classId });
|
||||
}
|
||||
}
|
||||
return result;
|
||||
@@ -271,7 +287,10 @@ function slimRank(spell) {
|
||||
castTimeMs: spell.castTimeMs,
|
||||
cooldownMs: Math.max(spell.recoveryTimeMs, spell.categoryRecoveryTimeMs),
|
||||
gcdMs: spell.startRecoveryTimeMs || 1500,
|
||||
durationMs: Math.abs(spell.durationMs),
|
||||
// SpellDuration.dbc uses negative durations for permanent/toggled effects.
|
||||
// Preserve those as zero so the runtime converts them to a non-expiring aura
|
||||
// instead of a one-millisecond aura that the AI immediately recasts.
|
||||
durationMs: Math.max(0, spell.durationMs),
|
||||
channelDurationMs: spell.channelInterruptFlags ? Math.max(0, spell.durationMs) : 0,
|
||||
powerType: spell.powerType,
|
||||
powerCost: spell.powerCost,
|
||||
@@ -285,6 +304,15 @@ function slimRank(spell) {
|
||||
};
|
||||
}
|
||||
|
||||
function iconPath(iconBasename) {
|
||||
const candidates = [
|
||||
`/assets/ui/spells/${iconBasename}.png`,
|
||||
`/assets/ui/talents/icons/${iconBasename}.png`,
|
||||
];
|
||||
return candidates.find((candidate) => fs.existsSync(path.join(projectRoot, "public", candidate.slice(1))))
|
||||
?? "/assets/ui/spells/inv_misc_questionmark.png";
|
||||
}
|
||||
|
||||
const archive = MpqArchive.open(archivePath);
|
||||
let result;
|
||||
try {
|
||||
@@ -300,7 +328,8 @@ try {
|
||||
rangeTable(extract("SpellRange")),
|
||||
stringTable(extract("SpellIcon")),
|
||||
);
|
||||
const talentIds = talentSpellIds(talentBuffer);
|
||||
const talentNodeBySpellId = talentNodesBySpellId(talentBuffer);
|
||||
const talentIds = new Set(talentNodeBySpellId.keys());
|
||||
const passiveTalentNames = new Set(
|
||||
[...talentIds]
|
||||
.map((spellId) => spells.get(spellId))
|
||||
@@ -309,19 +338,24 @@ try {
|
||||
);
|
||||
const classes = {};
|
||||
for (const [classId, source] of Object.entries(CLASS_SOURCES)) {
|
||||
const selected = [...classSpellIds(skillLineBuffer, source)]
|
||||
.map((spellId) => spells.get(spellId))
|
||||
const classSpellSet = classSpellIds(skillLineBuffer, source);
|
||||
const activeTalentSpellIds = new Set(
|
||||
[...talentNodeBySpellId.entries()]
|
||||
.filter(([, owner]) => owner.classId === classId)
|
||||
.map(([spellId]) => spellId),
|
||||
);
|
||||
const selected = [...spells.values()]
|
||||
.filter((spell) => (
|
||||
spell
|
||||
&& spell.name
|
||||
&& spell.spellLevel > 0
|
||||
&& (classSpellSet.has(spell.id) || activeTalentSpellIds.has(spell.id))
|
||||
&& (spell.spellLevel > 0 || talentIds.has(spell.id))
|
||||
&& spell.spellLevel <= 80
|
||||
&& (spell.attributes[0] & 0x40) === 0
|
||||
));
|
||||
const talentNames = new Set(selected.filter((spell) => talentIds.has(spell.id)).map((spell) => spell.name));
|
||||
const chains = new Map();
|
||||
for (const spell of selected) {
|
||||
if (talentNames.has(spell.name) || TRIGGERED_ONLY_NAMES.has(spell.name)) continue;
|
||||
if (TRIGGERED_ONLY_NAMES.has(spell.name)) continue;
|
||||
const ranks = chains.get(spell.name) ?? [];
|
||||
ranks.push(spell);
|
||||
chains.set(spell.name, ranks);
|
||||
@@ -334,6 +368,9 @@ try {
|
||||
|| left.id - right.id
|
||||
));
|
||||
const first = ranks[0];
|
||||
const talentNodeId = chainSpells
|
||||
.map((spell) => talentNodeBySpellId.get(spell.id)?.talentNodeId)
|
||||
.find(Boolean);
|
||||
// Passive talents sometimes publish a separate, non-passive proc spell
|
||||
// with the same name on the class skill line. That proc has no player
|
||||
// activation mechanics, so retain it for the spellbook but classify it
|
||||
@@ -349,8 +386,10 @@ try {
|
||||
id: `wow335-${classId}-${name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "")}`,
|
||||
name,
|
||||
iconBasename: first.iconBasename,
|
||||
sourceUnlockLevel: Math.min(...ranks.map((spell) => spell.spellLevel)),
|
||||
iconPath: iconPath(first.iconBasename),
|
||||
sourceUnlockLevel: Math.max(1, Math.min(...ranks.map((spell) => spell.spellLevel))),
|
||||
...(passive ? { passive: true } : {}),
|
||||
...(talentNodeId ? { talentNodeId } : {}),
|
||||
ranks: ranks.map(slimRank),
|
||||
};
|
||||
}).sort((left, right) => (
|
||||
|
||||
Reference in new issue
Block a user