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
+125
-11
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user