Release Healer Man 0.1.59

This commit is contained in:
phenom committed 2026-09-27 10:16:42 -04:00
1 parent 11934a3979
commit ab1434a3b8
44 files changed
+1242 -103

No files matched your search

+86 -8
View File
@@ -25,7 +25,7 @@ import {
ROGUE_DEADLY_POISON_DOSE_TAG,
ROGUE_ENVENOM_BUFF_TAG,
} from "./roguePoison";
import { talentNodeById } from "./talentCatalog";
import { runtimeTalentNodeById } from "./talentRuntimeCatalog";
import { baseResourcesFor } from "./wotlkStats";
export const BASE_CLASS_STARTER_SOURCE_LEVEL = 10;
@@ -930,9 +930,7 @@ function classScriptEffects(
case "priest:Mind Flay": return [{ kind: "damage", coefficient: 0.9 }];
case "priest:Penance": return [
{ kind: "heal", coefficient: 0.6 },
{ kind: "hot", coefficient: 0.6, ticks: 2, intervalMs: 1_000 },
{ kind: "damage", coefficient: 0.6 },
{ kind: "dot", coefficient: 0.6, ticks: 2, intervalMs: 1_000, tag: "penance" },
];
case "priest:Divine Hymn": return [{
kind: "heal", coefficient: 1, scope: "party", center: "caster", radius: 40, maximumTargets: 3,
@@ -1478,7 +1476,16 @@ function rankDefinition(
abilityName: string,
): AbilityRankDefinition {
const effects = effectsForRank(classId, rank, abilityName);
const channeling = rank.channelDurationMs > 0;
// Penance's learnable spell is a dummy wrapper; its triggered heal/damage
// children own the channel in the client DBC.
const penance = classId === "priest" && abilityName === "Penance";
const channeling = rank.channelDurationMs > 0 || penance;
const channelDurationMs = penance ? 2_000 : rank.channelDurationMs;
const channelTickMs = penance ? 1_000 : rank.effects
.map((effect) => effect.periodMs)
.filter((period) => period > 0)
.sort((left, right) => left - right)[0];
const castTimeMs = channeling ? Math.max(rank.castTimeMs, channelDurationMs) : rank.castTimeMs;
const rankResource = resourceForRank(resource, rank);
return {
rank: rank.rank || index + 1,
@@ -1486,7 +1493,12 @@ function rankDefinition(
level: gameplaySourceLevel(classId, rank.level),
description: rank.description,
castMode: channeling ? "channel" : rank.castTimeMs > 0 ? "cast" : "instant",
castTimeMs: channeling ? Math.max(rank.castTimeMs, rank.channelDurationMs) : rank.castTimeMs,
castTimeMs,
...(channeling && channelTickMs ? { channelTickMs } : {}),
...(channeling && channelTickMs
? { channelPulses: Math.max(1, Math.round(castTimeMs / channelTickMs) + (penance ? 1 : 0)) }
: {}),
...(penance ? { channelStartsWithPulse: true } : {}),
cooldownMs: rank.cooldownMs,
gcdMs: rank.gcdMs,
cost: {
@@ -1517,17 +1529,41 @@ function abilityDefinition(classId: BaseClassId, chain: Wow335Chain): AbilityDef
const sourceRanks = chain.ranks.some((rank) => rank.rank > 0)
? chain.ranks.filter((rank) => rank.rank > 0)
: chain.ranks;
const channelRanks = sourceRanks[0]?.channelDurationMs
? sourceRanks.filter((rank) => rank.channelDurationMs > 0)
: sourceRanks;
// The imported progression includes Hurricane's rank-five damage child
// (48466), but omits its player-cast wrapper (48467). Preserve the previous
// wrapper's channel/cost fields and point its periodic trigger at that child.
const hurricaneRankFive = classId === "druid" && chain.name === "Hurricane"
? sourceRanks.find((rank) => rank.rank === 5 && rank.spellId === 48466)
: undefined;
const previousHurricaneRank = channelRanks.at(-1);
const playerRanks = hurricaneRankFive && previousHurricaneRank?.rank === 4
? [...channelRanks, {
...previousHurricaneRank,
spellId: 48467,
rank: 5,
level: hurricaneRankFive.level,
sourceLevel: hurricaneRankFive.sourceLevel,
maxScalingLevel: hurricaneRankFive.maxScalingLevel,
description: hurricaneRankFive.description,
effects: previousHurricaneRank.effects.map((effect) => effect.auraType === 23
? { ...effect, triggerSpellId: hurricaneRankFive.spellId }
: effect),
}]
: channelRanks;
// Holy Nova's DBC chain stores damage and healing as separate same-rank
// spells. The damage spell is the cast (and pays mana); its healing partner
// is a second payload of that same cast, not another selectable rank.
const castRanks = classId === "priest" && chain.name === "Holy Nova"
? sourceRanks.filter((rank) => rank.effects.some((effect) => effect.effectType === 2))
? playerRanks.filter((rank) => rank.effects.some((effect) => effect.effectType === 2))
.map((rank) => {
const healing = sourceRanks.find((candidate) => candidate.rank === rank.rank
&& candidate.effects.some((effect) => effect.effectType === 10));
return healing ? { ...rank, effects: [...rank.effects, ...healing.effects] } : rank;
})
: sourceRanks;
: playerRanks;
const ranks = castRanks.map((rank, index) => rankDefinition(classId, resource, rank, index, chain.name));
const first = ranks[0];
const firstRaw = castRanks[0];
@@ -1558,6 +1594,9 @@ function abilityDefinition(classId: BaseClassId, chain: Wow335Chain): AbilityDef
...(radius > 0 ? { radius } : {}),
castMode: first.castMode,
castTimeMs: first.castTimeMs,
channelTickMs: first.channelTickMs,
channelPulses: first.channelPulses,
channelStartsWithPulse: first.channelStartsWithPulse,
cooldownMs: first.cooldownMs,
...(classicCooldownGroup(classId, chain.name)
? { cooldownGroup: classicCooldownGroup(classId, chain.name) }
@@ -1572,7 +1611,7 @@ function abilityDefinition(classId: BaseClassId, chain: Wow335Chain): AbilityDef
// Lash). They cannot be allocated by any legal local build, so expose
// those retained class actions at their source level instead of making
// them permanently unlearnable behind a dangling talent ID.
talentNodeId: chain.talentNodeId && talentNodeById(chain.talentNodeId)
talentNodeId: chain.talentNodeId && runtimeTalentNodeById(chain.talentNodeId)
? chain.talentNodeId
: undefined,
ranks,
@@ -1612,3 +1651,42 @@ export const WOW335_ABILITIES_BY_CLASS: Readonly<Record<BaseClassId, readonly Ab
.filter((ability): ability is AbilityDefinition => Boolean(ability))
.sort((left, right) => left.unlockLevel - right.unlockLevel || left.name.localeCompare(right.name)),
]))) as unknown as Readonly<Record<BaseClassId, readonly AbilityDefinition[]>>;
// These ground channels store their actual damage in same-name triggered
// child spells. Resolve the child once so each channel pulse can use its
// rank-specific DBC amount rather than creating a delayed area effect.
const GROUND_CHANNEL_CHILD_NAMES = new Set(["Blizzard", "Rain of Fire", "Hurricane"]);
export const WOW335_CHANNEL_TRIGGERED_ABILITIES_BY_SPELL_ID: Readonly<Record<number, AbilityDefinition>> =
Object.freeze(Object.fromEntries(BASE_CLASS_IDS.flatMap((classId) => data.classes[classId]
.filter((chain) => GROUND_CHANNEL_CHILD_NAMES.has(chain.name))
.flatMap((chain) => {
const ranksById = new Map(chain.ranks.map((rank) => [rank.spellId, rank]));
const childIds = new Set(chain.ranks.filter((rank) => rank.channelDurationMs === 0
&& rank.effects.some((effect) => effect.effectType === 2))
.map((rank) => rank.spellId));
return [...childIds].flatMap((spellId): Array<[number, AbilityDefinition]> => {
const child = ranksById.get(spellId);
if (!child) return [];
const effects = effectsForRank(classId, child, chain.name)
.filter((effect) => effect.kind === "damage" || effect.kind === "heal");
if (!effects.length) return [];
return [[spellId, {
id: `wow335-trigger-${spellId}`,
classId,
dbcSpellId: spellId,
name: `${chain.name} pulse`,
unlockLevel: 1,
icon: chain.iconPath ?? `/assets/ui/spells/${chain.iconBasename}.png`,
sigil: sigil(chain.name),
description: child.description,
target: "hostile",
range: { min: 0, max: 40 },
castMode: "instant",
castTimeMs: 0,
cooldownMs: 0,
gcdMs: 0,
cost: { resource: RESOURCE_BY_CLASS[classId], amount: 0 },
effects,
}]];
});
}))));