forked from phenom/healer-man
Update Healer Man server
This commit is contained in:
1 parent
4c768fbec6
commit
b16821e92e
330 files changed
+35406
-1760
No files matched your search
+227
-15
@@ -14,9 +14,11 @@ import {
|
||||
} from "./combatRotation";
|
||||
import { spendDeathKnightRunes } from "./combatResources";
|
||||
import { resolveResourceValue } from "./combatAuras";
|
||||
import { combatHasLineOfSight } from "./combatSpatial";
|
||||
import { equipmentAdjustedCombatAmount, useCombatStore, type MobCombatState } from "./combatStore";
|
||||
import { getMobPosition } from "./mobRuntimeRegistry";
|
||||
import type { PartyMember } from "./partyStore";
|
||||
import { baseResourcesFor } from "./wotlkStats";
|
||||
import { baseResourcesFor, deriveCharacterStats } from "./wotlkStats";
|
||||
import { companionResourceMaximum } from "./combatActorAuthority";
|
||||
import { currentCompanionCooldownPolicy, type CompanionCooldownMechanicRule } from "./companionCooldownPolicy";
|
||||
import { WOW335_MAGE_ARCANE_CONCENTRATION_TAG } from "./wow335MageTalentRuntime";
|
||||
@@ -24,13 +26,28 @@ import {
|
||||
WOW335_MAGE_IMPROVED_COUNTERSPELL_TALENT_ID,
|
||||
wow335MageImprovedCounterspellCooldownReductionMs,
|
||||
} from "./wow335MageImprovedCounterspellRuntime";
|
||||
import {
|
||||
WOW335_MAGE_IMPROVED_FIRE_BLAST_TALENT_ID,
|
||||
wow335MageImprovedFireBlastCooldownReductionMs,
|
||||
wow335MageImprovedFireBlastDamage,
|
||||
} from "./wow335MageImprovedFireBlastRuntime";
|
||||
import { WOW335_HUNTER_LOCK_AND_LOAD_READY_TAG } from "./wow335HunterRuntime";
|
||||
import {
|
||||
romPriestWarriorStanceRequired,
|
||||
romPriestWarriorStanceUseAllowed,
|
||||
} from "./romPriestKnightOverrides";
|
||||
import { romKnightWarriorWhirlwindAdjustment } from "./romKnightOverrides";
|
||||
import {
|
||||
romKnightCompassionWhirlwindShieldCooldownReductionMs,
|
||||
romKnightWarriorWhirlwindAdjustment,
|
||||
} from "./romKnightOverrides";
|
||||
import { romWarriorDruidWhirlwindAdjustment } from "./romWarriorDruidRageOfCalmness";
|
||||
import {
|
||||
ROM_ROGUE_COERCE_PASSIVE_SPELL_ID,
|
||||
romRogueCoerceAbilityAdjustment,
|
||||
} from "./romRogueMechanics";
|
||||
import { romPriestChampionCooldownOverride } from "./romPriestChampionOverrides";
|
||||
import { applyTalentModifierValue, talentModifiersForRanks } from "./talentModifiers";
|
||||
import { romWarlockMageSoulBrandStingHasWillpowerBladeState } from "./romWarlockMageSoulBrandSting";
|
||||
|
||||
export interface PartyAbilityContext {
|
||||
readonly now: number;
|
||||
@@ -52,6 +69,7 @@ export interface PartyAbilityContext {
|
||||
readonly activeAuraExclusiveGroups?: ReadonlySet<string>;
|
||||
readonly activeSummonAbilityIds?: ReadonlySet<string>;
|
||||
readonly hasActivePet?: boolean;
|
||||
readonly partyResourceDeficit?: (ability: AbilityDefinition) => boolean;
|
||||
readonly availableConditionalAbilityNames?: ReadonlySet<string>;
|
||||
readonly canUseAbility?: (ability: AbilityDefinition) => boolean;
|
||||
readonly incomingDefensiveMechanic?: CompanionCooldownMechanicRule | null;
|
||||
@@ -59,10 +77,52 @@ export interface PartyAbilityContext {
|
||||
readonly nextHealingSpellFree?: boolean;
|
||||
}
|
||||
|
||||
export interface PartyResourcePoolSnapshot {
|
||||
readonly resources: Readonly<Partial<Record<ResourceType, { readonly current: number; readonly maximum: number }>>>;
|
||||
}
|
||||
|
||||
export function partyAbilityHasDepletedPartyResource(
|
||||
ability: AbilityDefinition,
|
||||
recipients: readonly PartyResourcePoolSnapshot[],
|
||||
): boolean {
|
||||
return ability.effects.some((effect) => {
|
||||
if (effect.kind !== "resource" || effect.scope !== "party" || !effect.resource) return false;
|
||||
return recipients.some(({ resources }) => {
|
||||
const pool = resources[effect.resource!];
|
||||
return Boolean(pool && pool.current < pool.maximum);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function currentResource(member: PartyMember, type: ResourceType): number {
|
||||
return member.resourcePools[type] ?? (type === member.resourceType ? member.resource : 0);
|
||||
}
|
||||
|
||||
function partyRomPriestChampionCooldownOverride(
|
||||
member: PartyMember,
|
||||
ability: AbilityDefinition,
|
||||
): number | null {
|
||||
if (member.classId !== "rom-priest" || member.secondaryClassId !== "rom-champion") return null;
|
||||
const infusedLightChain = abilitiesForCharacter(member.classId, member.secondaryClassId).find((candidate) => (
|
||||
candidate.source === "runewaker"
|
||||
&& candidate.classId === "rom-priest"
|
||||
&& candidate.secondaryClassId === "rom-champion"
|
||||
&& candidate.passive
|
||||
&& candidate.dbcSpellId === 498610
|
||||
));
|
||||
if (!infusedLightChain
|
||||
|| !learnedAbilityForBuild(infusedLightChain, member.level, member.talentRanks)) return null;
|
||||
return romPriestChampionCooldownOverride(ability, [infusedLightChain.dbcSpellId]);
|
||||
}
|
||||
|
||||
function partyAbilityWithRomPriestChampionCooldown(
|
||||
member: PartyMember,
|
||||
ability: AbilityDefinition,
|
||||
): AbilityDefinition {
|
||||
const cooldownOverride = partyRomPriestChampionCooldownOverride(member, ability);
|
||||
return cooldownOverride === null ? ability : { ...ability, cooldownMs: cooldownOverride };
|
||||
}
|
||||
|
||||
export function partyHunterLockAndLoadReady(
|
||||
member: PartyMember,
|
||||
ability: AbilityDefinition,
|
||||
@@ -112,27 +172,107 @@ function partyKnightWarriorWhirlwindAdjustment(
|
||||
);
|
||||
}
|
||||
|
||||
function partyWarriorDruidWhirlwindAdjustment(
|
||||
member: PartyMember,
|
||||
ability: AbilityDefinition,
|
||||
): ReturnType<typeof romWarriorDruidWhirlwindAdjustment> {
|
||||
if (member.classId !== "rom-warrior" || member.secondaryClassId !== "rom-druid") {
|
||||
return { rageCostReduction: 0, cooldownReductionMs: 0 };
|
||||
}
|
||||
const learnedPassiveSpellIds = abilitiesForCharacter(member.classId, member.secondaryClassId)
|
||||
.filter((candidate) => candidate.source === "runewaker"
|
||||
&& candidate.classId === "rom-warrior"
|
||||
&& candidate.secondaryClassId === "rom-druid"
|
||||
&& candidate.dbcSpellId === 494377
|
||||
&& candidate.passive
|
||||
&& learnedAbilityForBuild(candidate, member.level, member.talentRanks) !== null)
|
||||
.map((candidate) => candidate.dbcSpellId);
|
||||
return romWarriorDruidWhirlwindAdjustment({
|
||||
classId: member.classId,
|
||||
secondaryClassId: member.secondaryClassId,
|
||||
learnedPassiveSpellIds,
|
||||
ability,
|
||||
});
|
||||
}
|
||||
|
||||
function partyKnightCompassionCooldownReductionMs(
|
||||
member: PartyMember,
|
||||
ability: AbilityDefinition,
|
||||
): number {
|
||||
if (member.classId !== "rom-knight" || member.secondaryClassId !== "rom-priest") return 0;
|
||||
const learnedPassiveSpellIds = abilitiesForCharacter(member.classId, member.secondaryClassId)
|
||||
.filter((candidate) => candidate.source === "runewaker"
|
||||
&& candidate.classId === "rom-knight"
|
||||
&& candidate.secondaryClassId === "rom-priest"
|
||||
&& candidate.dbcSpellId === 491635
|
||||
&& candidate.passive
|
||||
&& learnedAbilityForBuild(candidate, member.level, member.talentRanks) !== null)
|
||||
.map((candidate) => candidate.dbcSpellId);
|
||||
return romKnightCompassionWhirlwindShieldCooldownReductionMs(
|
||||
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 knightAdjustment = partyKnightWarriorWhirlwindAdjustment(member, ability);
|
||||
const warriorDruidAdjustment = partyWarriorDruidWhirlwindAdjustment(member, ability);
|
||||
const rageCostReduction = knightAdjustment.rageCostReduction + warriorDruidAdjustment.rageCostReduction;
|
||||
const cooldownReductionMs = knightAdjustment.cooldownReductionMs + warriorDruidAdjustment.cooldownReductionMs;
|
||||
if (rageCostReduction <= 0 && 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, amount: Math.max(0, cost.amount - rageCostReduction) }
|
||||
: cost
|
||||
));
|
||||
return {
|
||||
...ability,
|
||||
cooldownMs: Math.max(0, ability.cooldownMs - adjustment.cooldownReductionMs),
|
||||
cooldownMs: Math.max(0, ability.cooldownMs - cooldownReductionMs),
|
||||
cost: costs[0] ?? ability.cost,
|
||||
costs,
|
||||
};
|
||||
}
|
||||
|
||||
function partyRogueCoerceEnergyCostReduction(member: PartyMember, ability: AbilityDefinition): number {
|
||||
if (member.classId !== "rom-rogue" || member.secondaryClassId !== "rom-scout") return 0;
|
||||
const passiveSpellIds = abilitiesForCharacter(member.classId, member.secondaryClassId)
|
||||
.filter((candidate) => candidate.source === "runewaker"
|
||||
&& candidate.classId === "rom-rogue"
|
||||
&& candidate.secondaryClassId === "rom-scout"
|
||||
&& candidate.passive
|
||||
&& candidate.dbcSpellId === ROM_ROGUE_COERCE_PASSIVE_SPELL_ID
|
||||
&& learnedAbilityForBuild(candidate, member.level, member.talentRanks) !== null)
|
||||
.map((candidate) => candidate.dbcSpellId);
|
||||
return romRogueCoerceAbilityAdjustment(
|
||||
member.classId,
|
||||
member.secondaryClassId,
|
||||
ability.source,
|
||||
ability.dbcSpellId,
|
||||
passiveSpellIds,
|
||||
).energyCostReduction;
|
||||
}
|
||||
|
||||
function partyAbilityWithRogueCoerce(member: PartyMember, ability: AbilityDefinition): AbilityDefinition {
|
||||
const reduction = partyRogueCoerceEnergyCostReduction(member, ability);
|
||||
if (reduction <= 0) return ability;
|
||||
const costs = (ability.costs?.length ? ability.costs : [ability.cost]).map((cost) => (
|
||||
cost.resource === "energy" && !cost.percentage
|
||||
? { ...cost, amount: Math.max(0, cost.amount - reduction) }
|
||||
: cost
|
||||
));
|
||||
return { ...ability, 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 adjustedAbility = partyAbilityWithRogueCoerce(
|
||||
member,
|
||||
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) => (
|
||||
@@ -165,14 +305,22 @@ export function partyAbilityIsReady(
|
||||
ignoreResourceCosts = false,
|
||||
): boolean {
|
||||
if (ability.passive || !learnedAbilityForBuild(ability, member.level, member.talentRanks)) return false;
|
||||
if (member.classId === "rom-warlock" && member.secondaryClassId === "rom-mage"
|
||||
&& ability.source === "runewaker" && ability.dbcSpellId === 498710
|
||||
&& !romWarlockMageSoulBrandStingHasWillpowerBladeState(useCombatStore.getState().auras.filter((aura) => (
|
||||
aura.targetId === member.id && (aura.expiresAt === null || aura.expiresAt > 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;
|
||||
}
|
||||
const hasRomPriestChampionCooldownOverride = partyRomPriestChampionCooldownOverride(member, ability) !== null;
|
||||
if ((ability.gcdMs > 0 && member.globalCooldownEndsAt > now)
|
||||
|| (!partyHunterLockAndLoadReady(member, ability, now) && (member.cooldowns[ability.id] ?? 0) > now)) return false;
|
||||
|| (!partyHunterLockAndLoadReady(member, ability, now)
|
||||
&& !hasRomPriestChampionCooldownOverride
|
||||
&& (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 && partyAbilityCosts(member, ability, now).some(cost => cost.resource === "health"
|
||||
@@ -184,7 +332,8 @@ export function partyAbilityIsReady(
|
||||
function learnedAbilities(member: PartyMember): readonly AbilityDefinition[] {
|
||||
return abilitiesForCharacter(member.classId, member.secondaryClassId)
|
||||
.map((ability) => learnedAbilityForBuild(ability, member.level, member.talentRanks))
|
||||
.filter((ability): ability is AbilityDefinition => ability !== null);
|
||||
.filter((ability): ability is AbilityDefinition => ability !== null)
|
||||
.map((ability) => partyAbilityWithRomPriestChampionCooldown(member, ability));
|
||||
}
|
||||
|
||||
function isComboFinisher(ability: AbilityDefinition): boolean {
|
||||
@@ -244,6 +393,37 @@ export function choosePartyRotationDecision(
|
||||
.map((resource) => [resource, companionResourceMaximum(member, resource)]),
|
||||
);
|
||||
const learned = learnedAbilities(member);
|
||||
const reapingRank = member.classId === "death-knight"
|
||||
? Math.max(0, Math.trunc(member.talentRanks["talent-2001"] ?? 0))
|
||||
: 0;
|
||||
const activeConditionalAbilityNames = new Set(context.availableConditionalAbilityNames ?? []);
|
||||
const pestilence = learned.find((ability) => ability.name === "Pestilence");
|
||||
const targetPosition = context.target ? getMobPosition(context.target.id) : null;
|
||||
const hasNearbyPestilenceTarget = Boolean(targetPosition && pestilence && reapingRank > 0
|
||||
&& member.specializationId === "death-knight:blood"
|
||||
&& member.runes.some((rune) => rune.baseType === "blood" && rune.readyAt <= context.now)
|
||||
&& Object.values(useCombatStore.getState().mobs).some((mob) => {
|
||||
if (mob.id === context.target?.id || !mob.engaged || mob.dead) return false;
|
||||
const position = getMobPosition(mob.id);
|
||||
if (!position) return false;
|
||||
const dx = position[0] - targetPosition[0];
|
||||
const dy = position[1] - targetPosition[1];
|
||||
const dz = position[2] - targetPosition[2];
|
||||
const radius = pestilence!.radius ?? 10;
|
||||
return dx * dx + dy * dy + dz * dz <= radius * radius
|
||||
&& combatHasLineOfSight(targetPosition, position);
|
||||
}));
|
||||
const hasCasterOwnedDiseaseOnAnchor = Boolean(context.target && useCombatStore.getState().effects.some((effect) => (
|
||||
effect.kind === "dot"
|
||||
&& effect.sourceActorId === member.id
|
||||
&& effect.targetId === context.target?.id
|
||||
&& (effect.tag === "blood-plague" || effect.tag === "frost-fever")
|
||||
&& effect.remainingTicks > 0
|
||||
&& effect.nextTickAt + (effect.remainingTicks - 1) * effect.intervalMs > context.now
|
||||
)));
|
||||
if (hasNearbyPestilenceTarget && hasCasterOwnedDiseaseOnAnchor) {
|
||||
activeConditionalAbilityNames.add("Pestilence");
|
||||
}
|
||||
const decision = evaluateCombatRotation({
|
||||
now: context.now,
|
||||
classId: member.classId,
|
||||
@@ -261,6 +441,7 @@ export function choosePartyRotationDecision(
|
||||
comboPoints: member.comboPoints,
|
||||
comboTargetId: member.comboTargetId,
|
||||
runes: member.runes,
|
||||
deathKnightReapingRank: reapingRank,
|
||||
abilities: learned.filter((ability) => partyAbilityIsReady(
|
||||
member,
|
||||
ability,
|
||||
@@ -285,7 +466,10 @@ export function choosePartyRotationDecision(
|
||||
activeAuraExclusiveGroups: context.activeAuraExclusiveGroups,
|
||||
activeSummonAbilityIds: context.activeSummonAbilityIds,
|
||||
hasActivePet: context.hasActivePet,
|
||||
availableConditionalAbilityNames: context.availableConditionalAbilityNames,
|
||||
partyResourceDeficit: context.partyResourceDeficit,
|
||||
availableConditionalAbilityNames: activeConditionalAbilityNames.size > 0
|
||||
? activeConditionalAbilityNames
|
||||
: undefined,
|
||||
canUseAbility: context.canUseAbility,
|
||||
cooldownPolicy: currentCompanionCooldownPolicy(),
|
||||
incomingDefensiveMechanic: context.incomingDefensiveMechanic,
|
||||
@@ -324,14 +508,24 @@ 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 romCooldownOverride = partyRomPriestChampionCooldownOverride(member, ability);
|
||||
const cooldownMs = member.classId === "mage"
|
||||
? Math.max(0, ability.cooldownMs - wow335MageImprovedCounterspellCooldownReductionMs(
|
||||
member.talentRanks[WOW335_MAGE_IMPROVED_COUNTERSPELL_TALENT_ID] ?? 0,
|
||||
ability.name,
|
||||
) - wow335MageImprovedFireBlastCooldownReductionMs(
|
||||
member.talentRanks[WOW335_MAGE_IMPROVED_FIRE_BLAST_TALENT_ID] ?? 0,
|
||||
ability.name,
|
||||
))
|
||||
: ability.cooldownMs;
|
||||
: romCooldownOverride ?? ability.cooldownMs;
|
||||
const knightWarriorCooldownReductionMs = partyKnightWarriorWhirlwindAdjustment(member, ability).cooldownReductionMs;
|
||||
const adjustedCooldownMs = Math.max(0, cooldownMs - knightWarriorCooldownReductionMs);
|
||||
const warriorDruidCooldownReductionMs = partyWarriorDruidWhirlwindAdjustment(member, ability).cooldownReductionMs;
|
||||
const knightCompassionCooldownReductionMs = partyKnightCompassionCooldownReductionMs(member, ability);
|
||||
const adjustedCooldownMs = Math.max(0,
|
||||
cooldownMs - knightWarriorCooldownReductionMs - warriorDruidCooldownReductionMs - knightCompassionCooldownReductionMs);
|
||||
const cooldowns = { ...member.cooldowns };
|
||||
if (romCooldownOverride !== null) delete cooldowns[ability.id];
|
||||
if (adjustedCooldownMs > 0) cooldowns[ability.id] = now + adjustedCooldownMs;
|
||||
return {
|
||||
...member,
|
||||
health,
|
||||
@@ -339,9 +533,7 @@ export function commitPartyAbilityUse(
|
||||
resourcePools,
|
||||
lastResourceSpentAt,
|
||||
runes,
|
||||
cooldowns: adjustedCooldownMs > 0
|
||||
? { ...member.cooldowns, [ability.id]: now + adjustedCooldownMs }
|
||||
: member.cooldowns,
|
||||
cooldowns,
|
||||
globalCooldownEndsAt: ability.gcdMs > 0 ? now + ability.gcdMs : member.globalCooldownEndsAt,
|
||||
tauntReadyAt: aiHintsForAbility(ability).categories.includes("taunt")
|
||||
? now + Math.max(8_000, ability.cooldownMs)
|
||||
@@ -354,7 +546,16 @@ export function partyAbilityAmount(
|
||||
ability: AbilityDefinition,
|
||||
effect: AbilityEffect,
|
||||
poisonDoses = 0,
|
||||
intellectOverride?: number,
|
||||
): number {
|
||||
const sourceIntellect = intellectOverride ?? deriveCharacterStats(
|
||||
member.classId,
|
||||
member.level,
|
||||
member.gearStats,
|
||||
member.raceId,
|
||||
{},
|
||||
member.secondaryClassId,
|
||||
).intellect;
|
||||
if (effect.kind === "finisher-damage" && effect.poison) {
|
||||
const consumedDoses = Math.max(0, Math.min(
|
||||
effect.poison.maximumDoses,
|
||||
@@ -371,6 +572,8 @@ export function partyAbilityAmount(
|
||||
member.gearStats,
|
||||
member.classId,
|
||||
ability,
|
||||
0,
|
||||
sourceIntellect,
|
||||
);
|
||||
}
|
||||
const coefficient = "coefficient" in effect
|
||||
@@ -384,6 +587,8 @@ export function partyAbilityAmount(
|
||||
member.gearStats,
|
||||
member.classId,
|
||||
ability,
|
||||
0,
|
||||
sourceIntellect,
|
||||
) + (effect.kind === "damage" ? effect.bonusFlat ?? 0 : 0);
|
||||
if (member.classId === "druid" && (effect.kind === "damage" || effect.kind === "dot")) {
|
||||
return applyTalentModifierValue(
|
||||
@@ -393,5 +598,12 @@ export function partyAbilityAmount(
|
||||
{ abilityName: ability.name },
|
||||
);
|
||||
}
|
||||
if (member.classId === "mage" && effect.kind === "damage") {
|
||||
return wow335MageImprovedFireBlastDamage(
|
||||
baseAmount,
|
||||
member.talentRanks[WOW335_MAGE_IMPROVED_FIRE_BLAST_TALENT_ID] ?? 0,
|
||||
ability.name,
|
||||
);
|
||||
}
|
||||
return baseAmount;
|
||||
}
|
||||
Reference in new issue
Block a user