Update Healer Man server

This commit is contained in:
phenom committed 2026-09-30 09:57:13 -04:00
1 parent 35dfdaed61
commit 4fb7a0b9c8
161 files changed
+24618 -529

No files matched your search

+125 -11
View File
@@ -19,6 +19,18 @@ import type { PartyMember } from "./partyStore";
import { baseResourcesFor } from "./wotlkStats";
import { companionResourceMaximum } from "./combatActorAuthority";
import { currentCompanionCooldownPolicy, type CompanionCooldownMechanicRule } from "./companionCooldownPolicy";
import { WOW335_MAGE_ARCANE_CONCENTRATION_TAG } from "./wow335MageTalentRuntime";
import {
WOW335_MAGE_IMPROVED_COUNTERSPELL_TALENT_ID,
wow335MageImprovedCounterspellCooldownReductionMs,
} from "./wow335MageImprovedCounterspellRuntime";
import { WOW335_HUNTER_LOCK_AND_LOAD_READY_TAG } from "./wow335HunterRuntime";
import {
romPriestWarriorStanceRequired,
romPriestWarriorStanceUseAllowed,
} from "./romPriestKnightOverrides";
import { romKnightWarriorWhirlwindAdjustment } from "./romKnightOverrides";
import { applyTalentModifierValue, talentModifiersForRanks } from "./talentModifiers";
export interface PartyAbilityContext {
readonly now: number;
@@ -51,9 +63,82 @@ function currentResource(member: PartyMember, type: ResourceType): number {
return member.resourcePools[type] ?? (type === member.resourceType ? member.resource : 0);
}
function abilityCosts(member: PartyMember, ability: AbilityDefinition): readonly { resource: ResourceType; amount: number }[] {
const auras = useCombatStore.getState().auras.filter((aura) => aura.targetId === member.id);
return (ability.costs?.length ? ability.costs : [ability.cost]).map((cost) => {
export function partyHunterLockAndLoadReady(
member: PartyMember,
ability: AbilityDefinition,
now?: number,
): boolean {
if (member.classId !== "hunter"
|| !["Arcane Shot", "Explosive Shot"].includes(ability.name)
|| Math.max(0, Math.trunc(member.talentRanks["talent-1306"] ?? 0)) <= 0) return false;
return useCombatStore.getState().auras.some((aura) => aura.targetId === member.id
&& aura.stacks > 0
&& aura.definition.utilityTags?.includes(WOW335_HUNTER_LOCK_AND_LOAD_READY_TAG)
&& (now === undefined || aura.expiresAt === null || aura.expiresAt > now));
}
export function partyAbilityWithHunterLockAndLoad(
member: PartyMember,
ability: AbilityDefinition,
now: number,
): AbilityDefinition {
if (!partyHunterLockAndLoadReady(member, ability, now)) return ability;
const costs = (ability.costs?.length ? ability.costs : [ability.cost]).map((cost) => (
cost.resource === "mana" ? { ...cost, amount: 0 } : cost
));
return { ...ability, cooldownMs: 0, cost: costs[0] ?? ability.cost, costs };
}
function partyKnightWarriorWhirlwindAdjustment(
member: PartyMember,
ability: AbilityDefinition,
): ReturnType<typeof romKnightWarriorWhirlwindAdjustment> {
if (member.classId !== "rom-knight" || member.secondaryClassId !== "rom-warrior") {
return { rageCostReduction: 0, cooldownReductionMs: 0 };
}
const learnedPassiveSpellIds = abilitiesForCharacter(member.classId, member.secondaryClassId)
.filter((candidate) => candidate.source === "runewaker"
&& candidate.classId === "rom-knight"
&& candidate.secondaryClassId === "rom-warrior"
&& candidate.dbcSpellId === 491639
&& candidate.passive
&& learnedAbilityForBuild(candidate, member.level, member.talentRanks) !== null)
.map((candidate) => candidate.dbcSpellId);
return romKnightWarriorWhirlwindAdjustment(
member.classId,
member.secondaryClassId,
ability,
learnedPassiveSpellIds,
);
}
function partyAbilityWithKnightWarriorWhirlwind(
member: PartyMember,
ability: AbilityDefinition,
): AbilityDefinition {
const adjustment = partyKnightWarriorWhirlwindAdjustment(member, ability);
if (adjustment.rageCostReduction <= 0 && adjustment.cooldownReductionMs <= 0) return ability;
const costs = (ability.costs?.length ? ability.costs : [ability.cost]).map((cost) => (
cost.resource === "rage" && !cost.percentage
? { ...cost, amount: Math.max(0, cost.amount - adjustment.rageCostReduction) }
: cost
));
return {
...ability,
cooldownMs: Math.max(0, ability.cooldownMs - adjustment.cooldownReductionMs),
cost: costs[0] ?? ability.cost,
costs,
};
}
export function partyAbilityCosts(member: PartyMember, ability: AbilityDefinition, now?: number): readonly { resource: ResourceType; amount: number }[] {
const adjustedAbility = partyAbilityWithKnightWarriorWhirlwind(member, ability);
const auras = useCombatStore.getState().auras.filter((aura) => aura.targetId === member.id
&& (now === undefined || aura.expiresAt === null || aura.expiresAt > now));
const mageClearcasting = member.classId === "mage" && auras.some((aura) => (
aura.definition.utilityTags?.includes(WOW335_MAGE_ARCANE_CONCENTRATION_TAG)
));
return (adjustedAbility.costs?.length ? adjustedAbility.costs : [adjustedAbility.cost]).map((cost) => {
const maximum = cost.percentageBasis === "base-mana"
? baseResourcesFor(member.classId, member.level).mana
: cost.resource === "health" ? member.maxHealth : companionResourceMaximum(member, cost.resource);
@@ -64,7 +149,12 @@ function abilityCosts(member: PartyMember, ability: AbilityDefinition): readonly
"power",
"cost",
);
return { resource: cost.resource, amount: Math.max(0, Math.round(adjusted)) };
return {
resource: cost.resource,
amount: cost.resource === "mana" && partyHunterLockAndLoadReady(member, ability, now)
? 0
: cost.resource !== "health" && mageClearcasting ? 0 : Math.max(0, Math.round(adjusted)),
};
});
}
@@ -75,10 +165,17 @@ export function partyAbilityIsReady(
ignoreResourceCosts = false,
): boolean {
if (ability.passive || !learnedAbilityForBuild(ability, member.level, member.talentRanks)) return false;
if ((ability.gcdMs > 0 && member.globalCooldownEndsAt > now) || (member.cooldowns[ability.id] ?? 0) > now) return false;
if (romPriestWarriorStanceRequired(ability)) {
const activeAuraTags = useCombatStore.getState().auras
.filter((aura) => aura.targetId === member.id && (aura.expiresAt === null || aura.expiresAt > now))
.flatMap((aura) => aura.definition.utilityTags ?? []);
if (!romPriestWarriorStanceUseAllowed(ability, activeAuraTags)) return false;
}
if ((ability.gcdMs > 0 && member.globalCooldownEndsAt > now)
|| (!partyHunterLockAndLoadReady(member, ability, now) && (member.cooldowns[ability.id] ?? 0) > now)) return false;
if ((member.schoolLockedUntil[ability.castRules?.school ?? "physical"] ?? 0) > now) return false;
if (aiHintsForAbility(ability).categories.includes("taunt") && member.tauntReadyAt > now) return false;
if (!ignoreResourceCosts && abilityCosts(member, ability).some(cost => cost.resource === "health"
if (!ignoreResourceCosts && partyAbilityCosts(member, ability, now).some(cost => cost.resource === "health"
? member.health <= cost.amount : currentResource(member, cost.resource) < cost.amount)) return false;
const runeCost = member.classId === "death-knight" ? runeCostForAbility(ability) : null;
return !runeCost || spendDeathKnightRunes(member.runes, runeCost, now) !== null;
@@ -116,7 +213,7 @@ function shouldReserveForHigherPriority(
if ((member.schoolLockedUntil[candidate.castRules?.school ?? "physical"] ?? 0) > now) return false;
const categories = aiHintsForAbility(candidate).categories;
if (!categories.includes("damage") || categories.includes("execute")) return false;
const blockedCosts = abilityCosts(member, candidate).filter((cost) => (
const blockedCosts = partyAbilityCosts(member, candidate).filter((cost) => (
cost.amount > currentResource(member, cost.resource)
));
if (!blockedCosts.length) return false;
@@ -216,7 +313,7 @@ export function commitPartyAbilityUse(
const lastResourceSpentAt = { ...member.lastResourceSpentAt };
let resource = member.resource;
let health = member.health;
for (const cost of options.ignoreResourceCosts ? [] : abilityCosts(member, ability)) {
for (const cost of options.ignoreResourceCosts ? [] : partyAbilityCosts(member, ability, now)) {
const amount = cost.amount;
if (cost.resource === "health") { health -= amount; continue; }
const current = currentResource(member, cost.resource);
@@ -227,6 +324,14 @@ export function commitPartyAbilityUse(
const runeCost = member.classId === "death-knight" ? runeCostForAbility(ability) : null;
const runes = runeCost ? spendDeathKnightRunes(member.runes, runeCost, now) : member.runes;
if (!runes) return null;
const cooldownMs = member.classId === "mage"
? Math.max(0, ability.cooldownMs - wow335MageImprovedCounterspellCooldownReductionMs(
member.talentRanks[WOW335_MAGE_IMPROVED_COUNTERSPELL_TALENT_ID] ?? 0,
ability.name,
))
: ability.cooldownMs;
const knightWarriorCooldownReductionMs = partyKnightWarriorWhirlwindAdjustment(member, ability).cooldownReductionMs;
const adjustedCooldownMs = Math.max(0, cooldownMs - knightWarriorCooldownReductionMs);
return {
...member,
health,
@@ -234,8 +339,8 @@ export function commitPartyAbilityUse(
resourcePools,
lastResourceSpentAt,
runes,
cooldowns: ability.cooldownMs > 0
? { ...member.cooldowns, [ability.id]: now + ability.cooldownMs }
cooldowns: adjustedCooldownMs > 0
? { ...member.cooldowns, [ability.id]: now + adjustedCooldownMs }
: member.cooldowns,
globalCooldownEndsAt: ability.gcdMs > 0 ? now + ability.gcdMs : member.globalCooldownEndsAt,
tauntReadyAt: aiHintsForAbility(ability).categories.includes("taunt")
@@ -273,11 +378,20 @@ export function partyAbilityAmount(
: effect.kind === "finisher-damage"
? effect.baseCoefficient + effect.perComboCoefficient * member.comboPoints
: 0;
return equipmentAdjustedCombatAmount(
const baseAmount = equipmentAdjustedCombatAmount(
"coefficient" in effect ? effect : coefficient,
member.level,
member.gearStats,
member.classId,
ability,
) + (effect.kind === "damage" ? effect.bonusFlat ?? 0 : 0);
if (member.classId === "druid" && (effect.kind === "damage" || effect.kind === "dot")) {
return applyTalentModifierValue(
baseAmount,
talentModifiersForRanks(member.talentRanks, "druid"),
"damage-percent",
{ abilityName: ability.name },
);
}
return baseAmount;
}