forked from phenom/healer-man
Update Healer Man server
This commit is contained in:
1 parent
9d7e0e2f9e
commit
35dfdaed61
94 files changed
+17034
-418
No files matched your search
+112
-13
@@ -43,12 +43,36 @@ interface ParsedModifier extends Omit<TalentModifier, "sourceTalentId" | "source
|
||||
|
||||
const EXPLICIT_ACTIVATION_TALENTS = new Set([
|
||||
"Inner Focus",
|
||||
// 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",
|
||||
"Serendipity",
|
||||
"Shadow Weaving",
|
||||
// Eradication and Nightfall are conditional proc states, not passive haste,
|
||||
// damage, or resource-cost modifiers from their tooltip text.
|
||||
"Eradication",
|
||||
"Nightfall",
|
||||
// Backlash has passive crit chance plus a conditional next-cast proc. Its
|
||||
// proc damage and cost clauses must not become always-on bonuses.
|
||||
"Backlash",
|
||||
// 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",
|
||||
// The Beast Within activates from a pet Bestial Wrath proc; its tooltip
|
||||
// percentages must not become always-on modifiers.
|
||||
"The Beast Within",
|
||||
// These Shaman talents activate temporary effects from combat events;
|
||||
// their conditional percentages are applied by the Shaman runtime.
|
||||
"Elemental Focus",
|
||||
"Reverberation",
|
||||
"Maelstrom Weapon",
|
||||
]);
|
||||
|
||||
/**
|
||||
* These talents are modeled as on-use abilities and must never be interpreted
|
||||
* as always-on scalar bonuses merely because their activation tooltip contains
|
||||
* phrases such as "reduces cast time" or "reducing mana cost".
|
||||
* These talents have activation or proc behavior and must never be interpreted
|
||||
* as always-on scalar bonuses merely because their tooltip contains phrases
|
||||
* such as "reduces cast time" or "reducing mana cost".
|
||||
*/
|
||||
export function talentUsesExplicitActivation(talentName: string): boolean {
|
||||
return EXPLICIT_ACTIVATION_TALENTS.has(talentName);
|
||||
@@ -91,6 +115,11 @@ export function parseTalentModifiers(description: string): readonly ParsedModifi
|
||||
add({ kind: "damage-percent", amount, unit: "percent", scope });
|
||||
}
|
||||
if (/\bhealing\b|heals? (?:done|cast)/.test(lower)) add({ kind: "healing-percent", amount, unit: "percent", scope });
|
||||
// The generated Druid tooltip for Improved Rejuvenation describes its
|
||||
// healing as an increase to the spell's "effect" rather than naming heal.
|
||||
if (/\beffect of your rejuvenation spell\b/.test(lower)) {
|
||||
add({ kind: "healing-percent", amount, unit: "percent", scope });
|
||||
}
|
||||
if (/\barmor\b/.test(lower)) add({ kind: "armor-percent", amount, unit: "percent", scope });
|
||||
if (/\brange\b/.test(lower)) add({ kind: "range-percent", amount, unit: "percent", scope });
|
||||
if (/\bduration\b|lasts? longer/.test(lower)) add({ kind: "duration-percent", amount, unit: "percent", scope });
|
||||
@@ -134,7 +163,9 @@ export function parseTalentModifiers(description: string): readonly ParsedModifi
|
||||
|
||||
const castTime = sentence.match(/reduces? (?:the )?(?:casting|cast) time (?:of |for )?.*? by (\d+(?:\.\d+)?) sec/i);
|
||||
if (castTime) add({ kind: "cast-time-flat-ms", amount: -Number(castTime[1]) * 1000, unit: "milliseconds", scope: sentence });
|
||||
const cooldown = sentence.match(/reduces? (?:the )?cooldown (?:of |on )?.*? by (\d+(?:\.\d+)?) sec/i);
|
||||
const cooldown = /\bcasts?\s+reduce(?:s)?\s+(?:the\s+)?cooldown\b/i.test(sentence)
|
||||
? null
|
||||
: sentence.match(/reduces? (?:the )?cooldown (?:of |on )?.*? by (\d+(?:\.\d+)?) sec/i);
|
||||
if (cooldown) add({ kind: "cooldown-flat-ms", amount: -Number(cooldown[1]) * 1000, unit: "milliseconds", scope: sentence });
|
||||
}
|
||||
|
||||
@@ -143,17 +174,74 @@ export function parseTalentModifiers(description: string): readonly ParsedModifi
|
||||
|
||||
export function talentModifiersForRanks(ranks: TalentRanks, classId?: ClassId): readonly TalentModifier[] {
|
||||
return activeTalentRankEffects(ranks, classId).flatMap((active) => {
|
||||
if (active.talentName === "Backlash") {
|
||||
return [{
|
||||
kind: "critical-chance-percent" as const,
|
||||
amount: active.rank,
|
||||
unit: "percent" as const,
|
||||
scope: "your spells and abilities",
|
||||
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
|
||||
// rank-aware bonus at the individual target instead.
|
||||
if (active.talentName === "Test of Faith") return [];
|
||||
const override = talentBalanceOverride(active.talentId);
|
||||
const scale = (override?.effectScalePercent ?? 100) / 100;
|
||||
const parsed = parseTalentModifiers(active.description).map((modifier) => ({
|
||||
...modifier,
|
||||
amount: modifier.amount * scale,
|
||||
const parsed = parseTalentModifiers(active.description)
|
||||
// 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")
|
||||
.filter((modifier) => !(classId === "warrior" && active.talentName === "Unrelenting Assault"
|
||||
&& ["damage-percent", "cooldown-percent"].includes(modifier.kind)))
|
||||
.flatMap((modifier) => {
|
||||
if (classId === "warrior" && active.talentName === "Improved Execute") {
|
||||
// This tooltip combines a flat Rage reduction, damage percent, and a
|
||||
// conditional capstone crit. The broad parser merges the first two
|
||||
// clauses, so rebuild only the source-backed damage component here;
|
||||
// combatStore applies the flat Rage reduction when casting Execute.
|
||||
if (["resource-cost-percent", "damage-percent", "critical-chance-percent"].includes(modifier.kind)) {
|
||||
if (modifier.kind !== "damage-percent") return [];
|
||||
const increase = active.description.match(/increases its damage by (\d+(?:\.\d+)?)%/i);
|
||||
if (!increase) return [];
|
||||
return [{
|
||||
...modifier,
|
||||
amount: Number(increase[1]) * 2 * scale,
|
||||
scope: "damage of your Execute ability",
|
||||
sourceTalentId: active.talentId,
|
||||
sourceTalentName: active.talentName,
|
||||
rank: active.rank,
|
||||
description: active.description,
|
||||
}];
|
||||
}
|
||||
}
|
||||
return [{
|
||||
...modifier,
|
||||
amount: modifier.amount * scale,
|
||||
sourceTalentId: active.talentId,
|
||||
sourceTalentName: active.talentName,
|
||||
rank: active.rank,
|
||||
description: active.description,
|
||||
}];
|
||||
});
|
||||
const unrelentingAssaultDamage = classId === "warrior" && active.talentName === "Unrelenting Assault"
|
||||
? active.description.match(/increases the damage done by both abilities by (\d+(?:\.\d+)?)%/i)
|
||||
: null;
|
||||
const explicitScoped = unrelentingAssaultDamage ? [{
|
||||
kind: "damage-percent" as const,
|
||||
amount: Number(unrelentingAssaultDamage[1]) * scale,
|
||||
unit: "percent" as const,
|
||||
scope: "damage of your Overpower and Revenge abilities",
|
||||
sourceTalentId: active.talentId,
|
||||
sourceTalentName: active.talentName,
|
||||
rank: active.rank,
|
||||
description: active.description,
|
||||
}));
|
||||
}] : [];
|
||||
const supplemental = Object.entries(override?.modifiersPerRank ?? {}).flatMap(([kind, amount]) => {
|
||||
if (!Number.isFinite(amount) || amount === 0) return [];
|
||||
return [{
|
||||
@@ -167,7 +255,7 @@ export function talentModifiersForRanks(ranks: TalentRanks, classId?: ClassId):
|
||||
description: active.description,
|
||||
}];
|
||||
});
|
||||
return [...parsed, ...supplemental];
|
||||
return [...parsed, ...explicitScoped, ...supplemental];
|
||||
});
|
||||
}
|
||||
|
||||
@@ -181,8 +269,12 @@ function modifierApplies(modifier: TalentModifier, context: TalentModifierContex
|
||||
const tags = new Set((context.tags ?? []).map((tag) => tag.toLowerCase()));
|
||||
if (scope.includes("instant") && !tags.has("instant")) return false;
|
||||
if (scope.includes("periodic") && !tags.has("periodic")) return false;
|
||||
for (const school of ["arcane", "fire", "frost", "holy", "nature", "shadow"] as const) {
|
||||
if (scope.includes(school) && !tags.has(school)) return false;
|
||||
const schools = (["arcane", "fire", "frost", "holy", "nature", "shadow"] as const)
|
||||
.filter((school) => scope.includes(school));
|
||||
if (schools.length > 1) {
|
||||
if (!schools.some((school) => tags.has(school))) return false;
|
||||
} else if (schools.length === 1 && !tags.has(schools[0])) {
|
||||
return false;
|
||||
}
|
||||
if (context.abilityName && /\b(?:spell|ability)\b/.test(scope)) {
|
||||
const normalizedName = context.abilityName.toLowerCase();
|
||||
@@ -190,10 +282,17 @@ function modifierApplies(modifier: TalentModifier, context: TalentModifierContex
|
||||
if (namedScope && !namedScope.includes("your") && !scope.includes(normalizedName)) return false;
|
||||
}
|
||||
if (context.abilityName) {
|
||||
const namedScope = /(?:damage|healing|cost|cooldown|cast(?:ing)? time).*?\b(?:by|of|with) your\b/.test(scope);
|
||||
const normalizedName = context.abilityName.toLowerCase();
|
||||
const namedScope = /(?:damage|healing|cost|cooldown|cast(?:ing)? time|effect|critical (?:effect |strike )?chance|chance to critically).*?\b(?:by|of|with) your\b/.test(scope);
|
||||
const genericScope = /\byour (?:all |instant |periodic |healing |damage |helpful |harmful )*(?:spells?|abilities?|attacks?)\b/.test(scope)
|
||||
|| /\byour (?:arcane|fire|frost|holy|nature|shadow) spells?\b/.test(scope);
|
||||
if (namedScope && !genericScope && !scope.includes(context.abilityName.toLowerCase())) return false;
|
||||
const namedAbilityList = genericScope
|
||||
? undefined
|
||||
: scope.match(/\byour\s+(.+?)\s+abilit(?:y|ies)\b/)?.[1]
|
||||
?.split(/\s*(?:,|\band\b|\bor\b)\s*/)
|
||||
.map((name) => name.trim());
|
||||
if (namedAbilityList?.length && !namedAbilityList.includes(normalizedName)) return false;
|
||||
if (namedScope && !genericScope && !scope.includes(normalizedName)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user