forked from phenom/healer-man
Update Healer Man server
This commit is contained in:
1 parent
35dfdaed61
commit
4fb7a0b9c8
161 files changed
+24618
-529
No files matched your search
+119
-3
@@ -4,6 +4,9 @@ import {
|
||||
type RuntimeTalentRanks as TalentRanks,
|
||||
} from "./talentRuntimeCatalog";
|
||||
import { talentBalanceOverride } from "./balanceTuning";
|
||||
import { wow335MageFlameThrowingProfile } from "./wow335MageFlameThrowingRuntime";
|
||||
import { wow335DruidTalentModifierOverrides } from "./wow335DruidTalentRuntime";
|
||||
import { wow335ShamanConvectionModifiersForRank } from "./wow335ShamanTalentRuntime";
|
||||
|
||||
export type TalentModifierKind =
|
||||
| "damage-percent"
|
||||
@@ -21,6 +24,7 @@ export type TalentModifierKind =
|
||||
| "cooldown-percent"
|
||||
| "cooldown-flat-ms"
|
||||
| "armor-percent"
|
||||
| "range-flat-yards"
|
||||
| "attribute-percent"
|
||||
| "range-percent"
|
||||
| "duration-percent"
|
||||
@@ -30,7 +34,7 @@ export interface TalentModifier {
|
||||
readonly kind: TalentModifierKind;
|
||||
/** Signed amount: increases are positive and reductions are negative. */
|
||||
readonly amount: number;
|
||||
readonly unit: "percent" | "milliseconds";
|
||||
readonly unit: "percent" | "milliseconds" | "yards";
|
||||
readonly scope: string;
|
||||
readonly attribute?: "agility" | "intellect" | "spirit" | "stamina" | "strength";
|
||||
readonly sourceTalentId: string;
|
||||
@@ -43,6 +47,9 @@ interface ParsedModifier extends Omit<TalentModifier, "sourceTalentId" | "source
|
||||
|
||||
const EXPLICIT_ACTIVATION_TALENTS = new Set([
|
||||
"Inner Focus",
|
||||
// Arcane Potency exists only after Presence of Mind or Arcane Concentration
|
||||
// Clearcasting. The generated next-spell crit text is applied by its proc aura.
|
||||
"Arcane Potency",
|
||||
// These effects only exist after a proc or charge is earned; their tooltip
|
||||
// percentages must not be applied as passive stats while inactive.
|
||||
"Holy Concentration",
|
||||
@@ -55,10 +62,19 @@ const EXPLICIT_ACTIVATION_TALENTS = new Set([
|
||||
// Backlash has passive crit chance plus a conditional next-cast proc. Its
|
||||
// proc damage and cost clauses must not become always-on bonuses.
|
||||
"Backlash",
|
||||
// Backdraft's cast-time/GCD reduction applies only to its temporary three-cast proc.
|
||||
"Backdraft",
|
||||
// Brain Freeze mixes a conditional next Fireball/Frostfire Bolt proc with
|
||||
// separate school and target riders; the proc bonus is applied at cast time.
|
||||
"Brain Freeze",
|
||||
// Aura Mastery and Divine Favor carry scoped next-cast/aura effects. Their
|
||||
// activation tooltips must not be parsed as persistent global modifiers.
|
||||
"Aura Mastery",
|
||||
"Divine Favor",
|
||||
// Redoubt's Strength and damage-reduction clauses only exist during its
|
||||
// block-triggered ten-second proc; the shield block chance is handled by
|
||||
// the Paladin runtime with an equipped-shield check.
|
||||
"Redoubt",
|
||||
// The Beast Within activates from a pet Bestial Wrath proc; its tooltip
|
||||
// percentages must not become always-on modifiers.
|
||||
"The Beast Within",
|
||||
@@ -67,6 +83,19 @@ const EXPLICIT_ACTIVATION_TALENTS = new Set([
|
||||
"Elemental Focus",
|
||||
"Reverberation",
|
||||
"Maelstrom Weapon",
|
||||
// Tidal Waves only affects casts while its earned charge aura is active.
|
||||
"Tidal Waves",
|
||||
// Convection's comma-delimited named-spell list is represented by explicit
|
||||
// per-ability scopes so the generic parser cannot truncate it at Shock.
|
||||
"Convection",
|
||||
// Riptide's 25% rider only applies to the next Chain Heal on its HoT target.
|
||||
"Riptide",
|
||||
// Torment the Weak applies only to named damage spells and only against a
|
||||
// target that is currently slowed; its conditional part is target-aware.
|
||||
"Torment the Weak",
|
||||
// Feral Instinct has one named damage bonus plus two cast-state clauses;
|
||||
// only the named Swipe/Ravage clause is a persistent scalar modifier.
|
||||
"Feral Instinct",
|
||||
]);
|
||||
|
||||
/**
|
||||
@@ -174,6 +203,42 @@ export function parseTalentModifiers(description: string): readonly ParsedModifi
|
||||
|
||||
export function talentModifiersForRanks(ranks: TalentRanks, classId?: ClassId): readonly TalentModifier[] {
|
||||
return activeTalentRankEffects(ranks, classId).flatMap((active) => {
|
||||
if (classId === "druid" && active.talentId === "talent-799") {
|
||||
// Do not parse Tiger's Fury and Enrage's conditional bonuses as global
|
||||
// damage dealt/taken modifiers. Their temporary aura states are applied
|
||||
// by the Druid cast runtime.
|
||||
return wow335DruidTalentModifierOverrides(active.talentId, active.rank, active.description);
|
||||
}
|
||||
// Elemental Oath's generated rank text repeats one crit bonus for the
|
||||
// owner and the party. CombatStore applies the rank once to each spell
|
||||
// attack/heal source, so the broad scalar parser must not apply it again.
|
||||
if (classId === "shaman" && active.talentId === "talent-2049") return [];
|
||||
if (classId === "shaman" && active.talentId === "talent-564") {
|
||||
return wow335ShamanConvectionModifiersForRank(active.rank).map((modifier) => ({
|
||||
...modifier,
|
||||
sourceTalentId: active.talentId,
|
||||
sourceTalentName: active.talentName,
|
||||
rank: active.rank,
|
||||
description: active.description,
|
||||
}));
|
||||
}
|
||||
if (classId === "mage" && active.talentId === "talent-28") {
|
||||
const amount = wow335MageFlameThrowingProfile(active.rank).damagingSpellRangeBonusYards;
|
||||
return amount > 0 ? [{
|
||||
kind: "range-flat-yards" as const,
|
||||
amount,
|
||||
unit: "yards" as const,
|
||||
scope: "your damaging spells",
|
||||
sourceTalentId: active.talentId,
|
||||
sourceTalentName: active.talentName,
|
||||
rank: active.rank,
|
||||
description: active.description,
|
||||
}] : [];
|
||||
}
|
||||
// Molten Fury is active only while the individual target is below 35%
|
||||
// health. The target-health predicate is applied by the Mage damage path,
|
||||
// so keep its generated rank amount out of always-on scalar modifiers.
|
||||
if (classId === "mage" && active.talentId === "talent-1732") return [];
|
||||
if (active.talentName === "Backlash") {
|
||||
return [{
|
||||
kind: "critical-chance-percent" as const,
|
||||
@@ -186,6 +251,20 @@ export function talentModifiersForRanks(ranks: TalentRanks, classId?: ClassId):
|
||||
description: active.description,
|
||||
}];
|
||||
}
|
||||
if (active.talentName === "Backdraft") {
|
||||
const shadowburnDamage = active.description.match(/increases the damage of your Shadowburn by (\d+(?:\.\d+)?)%/i);
|
||||
if (!shadowburnDamage) return [];
|
||||
return [{
|
||||
kind: "damage-percent" as const,
|
||||
amount: Number(shadowburnDamage[1]),
|
||||
unit: "percent" as const,
|
||||
scope: "damage of your Shadowburn",
|
||||
sourceTalentId: active.talentId,
|
||||
sourceTalentName: active.talentName,
|
||||
rank: active.rank,
|
||||
description: active.description,
|
||||
}];
|
||||
}
|
||||
if (talentUsesExplicitActivation(active.talentName)) return [];
|
||||
// Its healing and damage bonuses only apply below a target-health threshold;
|
||||
// the scalar parser cannot encode that condition. CombatStore applies the
|
||||
@@ -193,7 +272,28 @@ export function talentModifiersForRanks(ranks: TalentRanks, classId?: ClassId):
|
||||
if (active.talentName === "Test of Faith") return [];
|
||||
const override = talentBalanceOverride(active.talentId);
|
||||
const scale = (override?.effectScalePercent ?? 100) / 100;
|
||||
const druidOverrides = classId === "druid"
|
||||
? wow335DruidTalentModifierOverrides(active.talentId, active.rank, active.description)
|
||||
: [];
|
||||
const overriddenKinds = new Set(druidOverrides.map((modifier) => modifier.kind));
|
||||
const parsed = parseTalentModifiers(active.description)
|
||||
.filter((modifier) => !overriddenKinds.has(modifier.kind))
|
||||
// Empowered Fire's generated tooltip names the three affected spells,
|
||||
// but this effect belongs to the caster. Combat applies it after
|
||||
// resolving the source actor so party Mage ranks cannot leak from the
|
||||
// player's scalar modifier list.
|
||||
.filter((modifier) => !(classId === "mage" && active.talentId === "talent-1734"
|
||||
&& modifier.kind === "damage-percent"))
|
||||
// Focused Will's healing-received rider only exists after taking a
|
||||
// critical hit. Preserve the rank's passive spell-crit modifier above,
|
||||
// while the conditional +3% healing stacks come from its runtime aura.
|
||||
.filter((modifier) => !(classId === "priest" && active.talentId === "talent-1858"
|
||||
&& modifier.kind === "healing-percent"))
|
||||
// Impact's passive bonus is restricted to Fire Blast and Meteor. The
|
||||
// generated tooltip names those abilities without saying "your", so the
|
||||
// general scope parser would otherwise treat the damage bonus as global.
|
||||
.filter((modifier) => !(classId === "mage" && active.talentId === "talent-30"
|
||||
&& modifier.kind === "damage-percent"))
|
||||
// Improved Shadow Bolt's 5% spell crit applies to the debuffed target;
|
||||
// only its named Shadow Bolt damage bonus is a persistent caster modifier.
|
||||
.filter((modifier) => active.talentName !== "Improved Shadow Bolt" || modifier.kind !== "critical-chance-percent")
|
||||
@@ -232,6 +332,19 @@ export function talentModifiersForRanks(ranks: TalentRanks, classId?: ClassId):
|
||||
const unrelentingAssaultDamage = classId === "warrior" && active.talentName === "Unrelenting Assault"
|
||||
? active.description.match(/increases the damage done by both abilities by (\d+(?:\.\d+)?)%/i)
|
||||
: null;
|
||||
const mageImpactDamage = classId === "mage" && active.talentId === "talent-30"
|
||||
? active.description.match(/increases the damage of Fire Blast and Meteor by (\d+(?:\.\d+)?)%/i)
|
||||
: null;
|
||||
const explicitMageImpact = mageImpactDamage ? [{
|
||||
kind: "damage-percent" as const,
|
||||
amount: Number(mageImpactDamage[1]) * scale,
|
||||
unit: "percent" as const,
|
||||
scope: "damage of your Fire Blast and Meteor abilities",
|
||||
sourceTalentId: active.talentId,
|
||||
sourceTalentName: active.talentName,
|
||||
rank: active.rank,
|
||||
description: active.description,
|
||||
}] : [];
|
||||
const explicitScoped = unrelentingAssaultDamage ? [{
|
||||
kind: "damage-percent" as const,
|
||||
amount: Number(unrelentingAssaultDamage[1]) * scale,
|
||||
@@ -255,7 +368,7 @@ export function talentModifiersForRanks(ranks: TalentRanks, classId?: ClassId):
|
||||
description: active.description,
|
||||
}];
|
||||
});
|
||||
return [...parsed, ...explicitScoped, ...supplemental];
|
||||
return [...parsed, ...explicitMageImpact, ...explicitScoped, ...druidOverrides, ...supplemental];
|
||||
});
|
||||
}
|
||||
|
||||
@@ -267,8 +380,11 @@ export interface TalentModifierContext {
|
||||
function modifierApplies(modifier: TalentModifier, context: TalentModifierContext): boolean {
|
||||
const scope = modifier.scope.toLowerCase();
|
||||
const tags = new Set((context.tags ?? []).map((tag) => tag.toLowerCase()));
|
||||
if (scope.includes("damaging spell") && !tags.has("damage")) return false;
|
||||
if (scope.includes("instant") && !tags.has("instant")) return false;
|
||||
if (scope.includes("periodic") && !tags.has("periodic")) return false;
|
||||
const coversDirectAndPeriodic = /\bdirect and periodic\b/.test(scope);
|
||||
if (scope.includes("periodic") && !coversDirectAndPeriodic && !tags.has("periodic")) return false;
|
||||
if (scope.includes("magic damage") && !tags.has("magic")) return false;
|
||||
const schools = (["arcane", "fire", "frost", "holy", "nature", "shadow"] as const)
|
||||
.filter((school) => scope.includes(school));
|
||||
if (schools.length > 1) {
|
||||
|
||||
Reference in new issue
Block a user