Release Healer Man 0.1.49

This commit is contained in:
phenom committed 2026-09-17 09:13:15 -04:00
1 parent 39425f2494
commit 612170afd0
91 files changed
+2278 -263

No files matched your search

+56 -4
View File
@@ -70,6 +70,13 @@ export interface AbilityAiHints {
| "resurrection"
| "control"
| "debuff"
| "buff"
| "resource"
| "summon"
| "pet"
| "movement"
| "threat"
| "area-damage"
)[];
readonly priority?: number;
}
@@ -293,6 +300,9 @@ export interface AbilityDefinition {
/** The original client level before Healer Man's starter-kit remapping. */
readonly sourceUnlockLevel?: number;
readonly ranks?: readonly AbilityRankDefinition[];
/** Runtime talent node that must be learned before this ability is available. */
readonly talentNodeId?: string;
/** Legacy CoA source identifier; `abilityTalentNodeId` normalizes it. */
readonly talentEntryId?: number;
}
@@ -953,10 +963,51 @@ export function abilityRankForLevel(
?? ability.ranks[Math.min(ability.ranks.length, normalized) - 1]
?? ability.ranks[0];
}
return [...ability.ranks]
const eligible = [...ability.ranks]
.filter((rank) => rank.level <= playerLevel)
.sort((left, right) => right.rank - left.rank)[0]
?? ability.ranks[0];
.sort((left, right) => right.rank - left.rank);
const highestRank = eligible[0]?.rank;
if (highestRank === undefined) return ability.ranks[0];
const candidates = eligible.filter((rank) => rank.rank === highestRank);
if (candidates.length === 1) return candidates[0];
// Several Wrath spell-name chains contain both the player-facing rank and a
// same-rank triggered child (for example Lightning Shield's retaliation).
// Preserve the structural effect family of the chain's root spell so a
// triggered damage child cannot replace the executable aura at later ranks.
const effectStructure = (effects: readonly AbilityEffect[]): string => effects.map((effect) => (
effect.kind === "apply-aura"
? [effect.kind, effect.sourceEffectType, effect.sourceAuraType].join(":")
: effect.kind
)).join("|");
const rootStructure = effectStructure(ability.effects);
return candidates.find((rank) => effectStructure(rank.effects) === rootStructure) ?? candidates[0];
}
export function abilityTalentNodeId(ability: AbilityDefinition): string | null {
return ability.talentNodeId
?? (ability.talentEntryId === undefined ? null : `coa-entry-${ability.talentEntryId}`);
}
export function learnedAbilityRank(
ability: AbilityDefinition,
talentRanks: Readonly<Record<string, number>>,
): number | undefined {
const nodeId = abilityTalentNodeId(ability);
return nodeId ? Math.max(0, Math.trunc(talentRanks[nodeId] ?? 0)) : undefined;
}
export function learnedAbilityForBuild(
ability: AbilityDefinition,
playerLevel: number,
talentRanks: Readonly<Record<string, number>>,
): AbilityDefinition | null {
if (!isAbilityUnlocked(ability, playerLevel)) return null;
const nodeId = abilityTalentNodeId(ability);
const learnedRank = learnedAbilityRank(ability, talentRanks);
if (nodeId && !learnedRank) return null;
// CoA tree entries use the allocated talent rank as the spell rank. Classic
// active talents continue to learn their later spell ranks by character level.
return abilityAtLevel(ability, playerLevel, ability.talentEntryId === undefined ? undefined : learnedRank);
}
export function abilityAtLevel(
@@ -1040,7 +1091,7 @@ export function defaultActionBarForClass(classId: ClassId): readonly string[] {
.map(({ ability }) => ability)
.filter((ability) => (
!ability.passive
&& ability.source !== "coa-tree"
&& !abilityTalentNodeId(ability)
&& !ability.effects.every((effect) => effect.kind === "unsupported" || effect.kind === "scripted")
))
.map((ability) => ability.id);
@@ -1065,6 +1116,7 @@ export function defaultActionBarForCharacter(classId: ClassId, secondaryClassId?
.filter((ability) => (
!ability.passive
&& ability.unlockLevel <= level
&& !abilityTalentNodeId(ability)
&& !ability.effects.every((effect) => effect.kind === "unsupported" || effect.kind === "scripted")
))
.sort((left, right) => left.unlockLevel - right.unlockLevel || left.name.localeCompare(right.name))