Update Healer Man server

This commit is contained in:
phenom committed 2026-10-07 13:05:04 -04:00
1 parent 69809f7228
commit ff20ab8779
101 files changed
+16713 -945

No files matched your search

+201 -29
View File
@@ -1,3 +1,4 @@
import { healerManaRecoveryAmount, synchronizeHealerManaRecovery } from "./healerManaRecovery";
import { nativeDamageCasterAmount, nativeHolyHealCasterAmount, nativeHolyHealTargetAmount, nativePeriodicHealSnapshot, nativePeriodicDamageCasterSnapshot, nativeTrainingAutoAttackProfile, nativeDisarmedMainHandAutoAttackProfile, matchesNativeTrainingGear, type RomNativeDamageTargetContext } from "./romNativeCombatAmount";
import { romNativeNpcCombatant, romNativeTrainingCombatant, romNativePlayerHitProfile, romNativeNpcHitProfile, resolveRomNativeCombatResult,
nativeMagicInputsForType, type RomNativeCombatantSnapshot, type RomNativeHitProfile } from "./romNativeCombatResolution";
@@ -13,14 +14,19 @@ import { combatNow } from "./combatClock";
import { selectEnemyChainTargets } from "./enemyChainTargeting";
import { mergePassiveAuraResourceModifiers } from "./classPassiveResourceModifiers";
import { enemyPeriodicDamageSource, enemyPeriodicDamageTickAmount } from "./enemyPeriodicDamage";
import { enemyDamageTypeSourceForSpell } from "./mobDamageTypes";
import { applyEnemySourceAura, enemySourceAuraContract, supportedEnemySourceAuraDefinition, removeEnemyActorAuras } from "./enemySourceAuras";
import { enemyBlockAuraContract, ordinaryCreatureSelfTargetCriticalBlockAuraDefinition,
resolveEnemyNpcBlockAuraInputs } from "./enemyBlockAuras";
import { enemyUtilitySpellBlockDisposition, enemyUtilitySpellBlockSourceContract } from "./enemyUtilitySpellBlocking";
import { enemyUtilitySpellBlockAbilityContract, enemyUtilitySpellBlockDisposition } from "./enemyUtilitySpellBlocking";
import { isWow335ShieldBashDazeParent, wow335ShieldBashDazeTargetEligible } from "./wow335ShieldBashDaze";
import { sourceFearSummonPosition } from "./enemyFearSummonMovement";
import { enemyControlAbilityCanEscapeAuras, enemyControlImmunityPurgeProofForAbility,
purgeNamedEnemyControlShoutAuras } from "./enemyControlImmunityPurge";
import { WOW335_HUNTER_DETERRENCE_ABILITY_ID, WOW335_DETERRENCE_PARENT_AURA_IDS,
WOW335_DETERRENCE_PACKAGE_AURA_IDS, WOW335_HUNTER_DETERRENCE_LINKED_AURA_EFFECTS,
wow335DeterrenceAbilityContract, wow335DeterrenceActivePackage,
resolveWow335DeterrenceIncoming } from "./wow335DeterrenceRuntime";
import { enemyTriggeredShoutContract, planEnemyTriggeredShoutTargets,
enemyTriggeredShoutControlTag, enemyTriggeredShoutMovementSpeedEffect,
resolveEnemyTriggeredShoutControl } from "./enemyTriggeredShouts";
@@ -43,6 +49,7 @@ import {
ENEMY_DISARM_UNARMED_MAIN_HAND,
enemyDisarmBlocksAbilityCast,
enemyDisarmRequirementCandidates,
enemySpellHasMatchingEquippedItem,
activeEnemyDisarmChannels,
enemyDisarmItemDependentAuraSuppressionIds,
enemyDisarmAurasForWeaponChannel,
@@ -120,6 +127,7 @@ import {
type DeathKnightRuneState,
} from "./combatActors";
import {
companionResourceMaximum,
companionActorFromMember,
companionActorWithMemberRuntime,
memberFromCompanionActor,
@@ -750,7 +758,7 @@ import {
rogueInstantPoisonDamage,
roguePoisonApplicationChances,
} from "./roguePoison";
import { hunterExplosiveTrapDirectScaling } from "./wow335AbilityCatalog";
import { hunterExplosiveTrapDirectScaling, wow335HunterPreventionTypeForAbility } from "./wow335AbilityCatalog";
import {
wow335ShamanImprovedWaterShieldCappedRestore,
wow335ShamanImprovedWaterShieldShouldProc,
@@ -944,6 +952,7 @@ import {
baseMissChancePercent,
combatAttackerFromCharacterStats,
combatDefenderFromCharacterStats,
deflectLandedCombatResult,
resolveCombatResult,
type CombatAttackerStats,
type CombatDefenderStats,
@@ -1053,6 +1062,7 @@ export interface GameplaySettings extends GraphicsSettings {
readonly showOwnDamageText: boolean;
readonly showAllyDamageText: boolean;
readonly showIncomingDamageText: boolean;
readonly healerManaRecovery: boolean;
readonly autoTargetNearest: boolean;
readonly enableScreenShake: boolean;
readonly masterVolume: number;
@@ -1073,6 +1083,7 @@ export const DEFAULT_GAMEPLAY_SETTINGS: GameplaySettings = Object.freeze({
showOwnDamageText: true,
showAllyDamageText: false,
showIncomingDamageText: false,
healerManaRecovery: true,
autoTargetNearest: true,
enableScreenShake: true,
masterVolume: 0,
@@ -3117,7 +3128,7 @@ function resolveNpcUtilityBlockResult(
): CombatResult | null {
const mob = state.mobs[targetId];
const raw = ability.wowSpellDefense;
const contract = enemyUtilitySpellBlockSourceContract(ability.dbcSpellId, raw?.sourceProfile);
const contract = enemyUtilitySpellBlockAbilityContract(ability);
const member = usePartyStore.getState().members.find((candidate) => candidate.id === sourceActorId);
const casterKind = sourceActorId === PLAYER_AGGRO_ID ? "player" : member ? "party" : "unknown";
if (!contract || !raw || ability.source === "runewaker" || sourceStats.romNative
@@ -3503,6 +3514,7 @@ function combatResultDescription(result: CombatResult): string {
if (result.outcome === "dodge") return "was dodged";
if (result.outcome === "parry") return "was parried";
if (result.outcome === "resist") return "was resisted";
if (result.outcome === "deflect") return "was deflected";
if (result.outcome === "block") return `was blocked for ${result.blockedAmount}`;
if (result.outcome === "crit") return `critically hit for ${result.finalDamage}`
+ (result.blocked ? ` (${result.blockedAmount} blocked)` : "");
@@ -3730,8 +3742,30 @@ function resolveEnemyDamageResult(
nativeSourceDirect?: RomNativeEnemyDirectDamageValue,
targetOrdinal = 0,
applicationOnly = false,
defenderStunnedAtImpact?: boolean,
): CombatResult {
const attackKind = enemyAttackKind(attack);
const currentState = useCombatStore.getState();
const deterrenceAbility = deterrenceAbilityForActor(currentState, actorId);
const defenderStunned = defenderStunnedAtImpact ?? actorIsStunned(currentState, actorId, now);
const deterrencePackage = deterrenceAbility && wow335DeterrenceActivePackage({
ability: deterrenceAbility, auras: currentState.auras, defenderId: actorId, nowMs: now,
});
const hitSource = attack.damageOrigin === "wow335" && attack.spellId !== undefined
? enemyDamageTypeSourceForSpell("wow335", attack.spellId) : undefined;
const sourceFlags = hitSource && "hitSource" in hitSource && hitSource.hitSource && attack.wowHitSource
&& hitSource.damageClass === attack.damageClass && hitSource.schoolMask === attack.schoolMask
&& hitSource.hitSource.attributes0 === attack.wowHitSource.attributes0
&& hitSource.hitSource.attributes3 === attack.wowHitSource.attributes3
&& hitSource.hitSource.attributes7 === attack.wowHitSource.attributes7
? hitSource.hitSource : undefined;
const sourceAlwaysHit = Boolean(sourceFlags && (sourceFlags.attributes3 & 0x00040000) !== 0);
const sourceNoAttackMiss = Boolean(sourceFlags && [1, 2, 3].includes(attack.damageClass!)
&& (sourceFlags.attributes7 & 0x02000000) !== 0);
const sourceNoActiveDefense = Boolean(sourceFlags && (attack.damageClass === 2 || attack.damageClass === 3)
&& (sourceFlags.attributes0 & 0x00200000) !== 0);
const sourceCannotDodge = Boolean(sourceFlags && (sourceFlags.attributes7 & 0x00800000) !== 0);
const sourceCannotParry = Boolean(sourceFlags && (sourceFlags.attributes7 & 0x01000000) !== 0);
const incomingModifier = (stat: string) => defenderAuras.reduce((total, aura) => total
+ (aura.definition.modifiers ?? []).reduce((sum, modifier) => modifier.kind === "stat"
&& modifier.stat === stat && modifier.operation === "flat"
@@ -3795,7 +3829,7 @@ function resolveEnemyDamageResult(
const preMitigationAmount = amount > 0 ? resolveAuraDamageValue(amount, defenderAuras, {
direction: "taken", school: attack.school, attackKind, phase: "pre-mitigation",
}) : amount;
return resolveCombatResult({
const result = resolveCombatResult({
amount: preMitigationAmount,
attackKind,
damageSchool: attack.school,
@@ -3809,11 +3843,15 @@ function resolveEnemyDamageResult(
},
rules: {
isAutoAttack: !attack.periodicTick && attack.animation === "attack" && (attack.castTimeMs ?? 0) === 0,
canMiss: !attack.periodicTick && !nativeFallbackGatePassed,
canMiss: !attack.periodicTick && !nativeFallbackGatePassed && !sourceAlwaysHit && !sourceNoAttackMiss,
canCrit: !attack.periodicTick && !applicationOnly,
canDodge: !attack.periodicTick && !nativeFallbackGatePassed && attackKind === "melee",
canParry: !attack.periodicTick && attackKind === "melee",
canBlock: !attack.periodicTick && !applicationOnly && attackKind !== "spell",
canDodge: !attack.periodicTick && !nativeFallbackGatePassed && attackKind === "melee"
&& !sourceAlwaysHit && !sourceNoActiveDefense && !sourceCannotDodge,
canParry: !attack.periodicTick && attackKind === "melee"
&& !(deterrencePackage && defenderStunned)
&& !sourceAlwaysHit && !sourceNoActiveDefense && !sourceCannotParry,
canBlock: !attack.periodicTick && !applicationOnly && attackKind !== "spell"
&& !sourceAlwaysHit && !sourceNoActiveDefense,
...(attack.targetMitigationSnapshotted ? { ignoreArmor: true, resistanceMode: "none" as const } : {}),
...(attack.periodicTick && attack.periodicIgnoresArmor ? { ignoreArmor: true } : {}),
baseMissChancePercent: baseMissChancePercent(attackKind, mob.level, defender.level)
@@ -3821,6 +3859,80 @@ function resolveEnemyDamageResult(
},
roll: stableCombatRoll(`${mob.id}:${attack.id}:${actorId}:${attackRevision}:${now}`),
});
if (!deterrenceAbility || !result.landed || result.outcome === "deflect") return result;
const deflection = resolveWow335DeterrenceIncoming({
ability: deterrenceAbility, auras: currentState.auras, defenderId: actorId, nowMs: now,
incoming: { ...attack, attackKind,
isAutoAttack: isEnemyWeaponSwing(attack) },
baseOutcome: result.outcome, defenderStunned,
// The verified linked Aura288 makes the complete package facing-independent.
attackerInFront: false,
});
return deflection.outcome === "deflect" ? deflectLandedCombatResult(result) : result;
}
function deterrenceAbilityForActor(state: CombatState, actorId: string): AbilityDefinition | undefined {
const owner = actorId === PLAYER_AURA_ENTITY_ID
? state : usePartyStore.getState().members.find((member) => member.id === actorId);
if (!owner || owner.classId !== "hunter") return undefined;
const ability = abilityById(WOW335_HUNTER_DETERRENCE_ABILITY_ID);
return ability && isAbilityUnlocked(ability, owner.level) && wow335DeterrenceAbilityContract(ability)
? abilityAtLevel(ability, owner.level) : undefined;
}
function actorIsStunned(state: CombatState, actorId: string, now: number): boolean {
const owner = actorId === PLAYER_AURA_ENTITY_ID
? state : usePartyStore.getState().members.find((member) => member.id === actorId);
if (owner && owner.controlledUntil > now && owner.controlMechanic === "stun") return true;
return state.auras.some((aura) => aura.targetId === actorId
&& (aura.expiresAt === null || aura.expiresAt > now)
&& (aura.definition.utilityTags?.includes("enemy-control-source:stun")
|| (aura.definition.id.startsWith("enemy-control:") && aura.definition.id.endsWith(":stun"))));
}
function deterrencePreventsIncomingSpellEffects(
state: CombatState, actorId: string, attack: EnemyAttackDefinition, now: number,
): boolean {
const ability = deterrenceAbilityForActor(state, actorId);
if (!ability) return false;
// This exact package guarantees deflection after any prior miss/resist gate.
// Its hostile riders cannot land in either case; damage keeps its actual roll.
return resolveWow335DeterrenceIncoming({ ability, auras: state.auras,
defenderId: actorId, nowMs: now, baseOutcome: "hit",
incoming: { ...attack, attackKind: enemyAttackKind(attack), isAutoAttack: isEnemyWeaponSwing(attack) },
defenderStunned: actorIsStunned(state, actorId, now), attackerInFront: false,
}).outcome === "deflect";
}
function completeDeterrenceLinkedAuras(
auras: readonly ActiveAura[], ability: AbilityDefinition, sourceId: string, targetId: string, now: number,
): readonly ActiveAura[] {
if (sourceId !== targetId || !wow335DeterrenceAbilityContract(ability)
|| !WOW335_DETERRENCE_PARENT_AURA_IDS.every((id) => auras.some((aura) => (
aura.definition.id === id && aura.sourceId === sourceId && aura.targetId === targetId
&& aura.appliedAt === now && aura.expiresAt === now + 5_000
)))) return auras;
return WOW335_HUNTER_DETERRENCE_LINKED_AURA_EFFECTS.reduce((current, effect) => {
if (effect.kind !== "apply-aura") return current;
return applyCombatAura(current, { definition: auraDefinitionForAbility(effect.aura, ability),
sourceId, targetId, now }).auras;
}, auras);
}
function synchronizedDeterrenceAuras(state: CombatState, auras: readonly ActiveAura[], now: number): readonly ActiveAura[] {
const packageIds = new Set(WOW335_DETERRENCE_PACKAGE_AURA_IDS);
let result = auras.filter((aura) => !packageIds.has(aura.definition.id)
|| aura.sourceId === aura.targetId);
const candidates = result.filter((aura) => packageIds.has(aura.definition.id));
for (const candidate of candidates) {
const ability = deterrenceAbilityForActor(state, candidate.targetId);
if (!ability || !wow335DeterrenceActivePackage({ ability, auras: result,
defenderId: candidate.targetId, nowMs: now })) {
result = result.filter((aura) => !(packageIds.has(aura.definition.id)
&& aura.sourceId === candidate.sourceId && aura.targetId === candidate.targetId));
}
}
return result;
}
function talentAdjustedMaxResource(
@@ -4274,6 +4386,27 @@ export function characterCombatDefender(state: CombatState): CombatDefenderStats
return { ...defender, damageReductionPercent: (1 - (1 - (defender.damageReductionPercent ?? 0) / 100) * multiplier) * 100 };
}
function recoveryAuras(state: CombatState, now: number): readonly ActiveAura[] {
const includePlayer = useDungeonSimulationStore.getState().sessionMode === "play";
const actors = [
...(includePlayer ? [{ id: PLAYER_AURA_ENTITY_ID, health: state.health, role: state.gearRole,
maxMana: state.maxResourcePools.mana ?? (resourceProfileForClass(state.classId).type === "mana" ? state.maxResource : 0) }] : []),
...usePartyStore.getState().members.map((member) => ({ id: member.id, health: member.health,
role: member.role, maxMana: member.resourcePools.mana !== undefined ? companionResourceMaximum(member, "mana") : 0 })),
];
const inCombat = Object.values(state.mobs).some((mob) => !mob.dead
&& (mob.engaged || Object.values(mob.threatByActor).some((threat) => threat > 0)));
return synchronizeHealerManaRecovery(state.auras, actors, state.settings.healerManaRecovery, inCombat, now);
}
/** Refresh the shared buff before companion recovery, even without a player tick. */
export function refreshHealerManaRecovery(now: number): void {
useCombatStore.setState((state) => {
const auras = recoveryAuras(state, now);
return auras === state.auras ? state : { auras };
});
}
function normalizedSettings(
current: GameplaySettings,
patch: Partial<GameplaySettings>,
@@ -4291,6 +4424,7 @@ function normalizedSettings(
showOwnDamageText: patch.showOwnDamageText ?? current.showOwnDamageText,
showAllyDamageText: patch.showAllyDamageText ?? current.showAllyDamageText,
showIncomingDamageText: patch.showIncomingDamageText ?? current.showIncomingDamageText,
healerManaRecovery: patch.healerManaRecovery ?? current.healerManaRecovery,
autoTargetNearest: patch.autoTargetNearest ?? current.autoTargetNearest,
enableScreenShake: patch.enableScreenShake ?? current.enableScreenShake,
masterVolume: clamp(Number.isFinite(patch.masterVolume) ? patch.masterVolume! : current.masterVolume, 0, 1),
@@ -4320,6 +4454,7 @@ function gameplaySettingsEqual(left: GameplaySettings, right: GameplaySettings):
&& left.showOwnDamageText === right.showOwnDamageText
&& left.showAllyDamageText === right.showAllyDamageText
&& left.showIncomingDamageText === right.showIncomingDamageText
&& left.healerManaRecovery === right.healerManaRecovery
&& left.autoTargetNearest === right.autoTargetNearest
&& left.enableScreenShake === right.enableScreenShake
&& left.masterVolume === right.masterVolume
@@ -13466,14 +13601,16 @@ export const useCombatStore = create<CombatState>((set, get) => ({
&& enemyControlPreventsActions(state.controlMechanic)
&& ability?.dbcSpellId !== 498570
&& ability?.dbcSpellId !== 1953
&& !((state.controlMechanic === "fear" || state.controlMechanic === "stun")
&& !((state.controlMechanic === "fear" || state.controlMechanic === "stun" || state.controlMechanic === "confuse")
&& enemyControlAbilityCanEscapeAuras(ability, state.controlMechanic,
state.auras, PLAYER_AURA_ENTITY_ID, now))
) failure = "player-controlled";
else if (
effectiveTarget === "hostile"
&& abilityAttackKind(state.classId, ability) === "melee"
&& aurasForEntity(state.auras, PLAYER_AURA_ENTITY_ID).some((aura) => aura.definition.utilityTags?.includes("pacified"))
(wow335HunterPreventionTypeForAbility(ability) === 2
|| (effectiveTarget === "hostile" && abilityAttackKind(state.classId, ability) === "melee"))
&& aurasForEntity(state.auras, PLAYER_AURA_ENTITY_ID).some((aura) => (
(aura.expiresAt === null || aura.expiresAt > now) && aura.definition.utilityTags?.includes("pacified")
))
) failure = "player-controlled";
else if (state.activeCast && !completingCast) failure = "already-casting";
else if (!completingCast && !suddenDoomFreeDeathCoil
@@ -13527,6 +13664,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
: effectiveTarget === "friendly"
? state.selectedFriendlyActorId ?? party.selectedMemberId
: null;
if (ability?.id === WOW335_HUNTER_DETERRENCE_ABILITY_ID) targetId = null;
// Splash selects a destination. A stale, distant or fallen selected actor
// must not become a second target requirement for that ground cast.
if (ability && isCoaWitchDoctorSplashPotion(ability) && targeting?.shape === "ground") targetId = null;
@@ -13599,7 +13737,10 @@ export const useCombatStore = create<CombatState>((set, get) => ({
role: state.gearRole,
level: state.level,
});
if (enemyDisarmBlocksAbilityCast(
const equippedItems = enemyDisarmRequirementCandidates(loadout);
if ((ability.id === WOW335_HUNTER_DETERRENCE_ABILITY_ID
&& !enemySpellHasMatchingEquippedItem(ability.wowWeaponRequirements, equippedItems))
|| enemyDisarmBlocksAbilityCast(
state.auras,
PLAYER_AURA_ENTITY_ID,
now,
@@ -13609,7 +13750,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
passive: ability.passive,
createsAura: ability.effects.some((effect) => effect.kind === "apply-aura"),
},
enemyDisarmRequirementCandidates(loadout),
equippedItems,
)) failure = "condition-not-met";
}
const requiredTargetStateTag = ability ? romAbilityRequiredTargetStateTag(ability) : null;
@@ -16458,6 +16599,10 @@ export const useCombatStore = create<CombatState>((set, get) => ({
? { ...aura, stacks: 0 }
: aura)
: [...auraResult.auras];
if (ability.id === WOW335_HUNTER_DETERRENCE_ABILITY_ID) {
work.auras = [...completeDeterrenceLinkedAuras(work.auras, ability,
PLAYER_AURA_ENTITY_ID, recipientId, now)];
}
if (state.classId === "mage" && ability.name === "Presence of Mind"
&& wow335MageArcanePotencyCanProc(
mageArcanePotencyRank,
@@ -18318,7 +18463,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
},
resolveMobUtilitySpellHit: (id, sourceActorId, ability, now = combatNow()) => {
if (!enemyUtilitySpellBlockSourceContract(ability.dbcSpellId, ability.wowSpellDefense?.sourceProfile)) return null;
if (!enemyUtilitySpellBlockAbilityContract(ability)) return null;
let result: CombatResult | null = null;
set((state) => {
const member = usePartyStore.getState().members.find((candidate) => candidate.id === sourceActorId);
@@ -18769,6 +18914,13 @@ export const useCombatStore = create<CombatState>((set, get) => ({
applyActorAura: (sourceActorId, targetActorId, ability, effect, now = combatNow()) => {
const state = get();
if (!state.actors[sourceActorId] || (!state.actors[targetActorId] && !state.mobs[targetActorId])) return false;
if (ability.id === WOW335_HUNTER_DETERRENCE_ABILITY_ID
&& (sourceActorId !== targetActorId || !deterrenceAbilityForActor(state, sourceActorId)
|| !wow335DeterrenceAbilityContract(ability)
|| !ability.effects.some((ownEffect, index) => ownEffect.kind === "apply-aura"
&& ownEffect.aura.id === effect.aura.id && wow335DeterrenceAbilityContract({
...ability, effects: ability.effects.map((entry, ordinal) => ordinal === index ? effect : entry),
})))) return false;
const isRomPriestChampionBeacon = effect.aura.utilityTags?.includes(
ROM_PRIEST_CHAMPION_BEACON_OF_REGENERATION_TAG,
) ?? false;
@@ -18825,7 +18977,8 @@ export const useCombatStore = create<CombatState>((set, get) => ({
|| (effect.sourceAuraType === 147 && effect.miscValue === 1733);
const controlPurge = purgeNamedEnemyControlShoutAuras(unpurgedResult.auras, targetActorId,
sourceImmunityGrant ? enemyControlImmunityPurgeProofForAbility(ability) : undefined, now);
const result = { ...unpurgedResult, auras: controlPurge.auras };
const result = { ...unpurgedResult, auras: completeDeterrenceLinkedAuras(controlPurge.auras,
ability, sourceActorId, targetActorId, now) };
const grantedAbsorb = result.aura.absorbs.reduce((total, absorb) => total + absorb.remaining, 0);
let health = state.health;
let maxHealth = state.maxHealth;
@@ -20824,6 +20977,9 @@ export const useCombatStore = create<CombatState>((set, get) => ({
hasLineOfSight: combatHasLineOfSight,
}) : [];
const targets = attack.chain ? chainHits.map((hit) => hit.target.id) : primaryTargets;
const effectTargets = targets.filter((actorId) => (
!deterrencePreventsIncomingSpellEffects(state, actorId, attack, now)
));
const presentationTargetId = attack.target === "self" || attack.target === "nearby-friendly"
? id
: targets[0] ?? resolution.targetActorId;
@@ -20899,7 +21055,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
school: periodicSchool ?? attack.school, runewakerEffectType: periodicSource.component.effectType,
runewakerComponentId: periodicSource.component.componentId,
attackKind: periodicSource.component.effectType === 0 ? "spell" : "melee" } : attack;
for (const actorId of targets) {
for (const actorId of effectTargets) {
if (wowPeriodic) {
const member = usePartyStore.getState().members.find((candidate) => candidate.id === actorId);
const applicationDefender = actorId === PLAYER_AGGRO_ID
@@ -20993,7 +21149,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
instantCast: instantSourceCast,
});
}
if (targets.length) {
if (effectTargets.length) {
enemyHealing.push({
targetId: id,
amount: Math.max(1, Math.round(mob.attackDamage * effect.healMultiplier)),
@@ -21121,7 +21277,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
? enemySourceAuraContract(effect.wowAuraSource.spellId, effect.wowAuraSource.sourceProfile) : undefined;
const sourceDefinition = sourceAura ? supportedEnemySourceAuraDefinition(sourceAura) : undefined;
if (effect.wowAuraSource && !sourceDefinition) continue;
for (const actorId of targets) {
for (const actorId of effectTargets) {
if (deathKnightAntiMagicShellBlocksMagicEffect(namedAuras, actorId, effect.damageSchool ?? attack.school)) continue;
if (sourceDefinition) {
const debuff = effect.wowAuraSource
@@ -21296,7 +21452,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
});
}
}
for (const actorId of targets) {
for (const actorId of effectTargets) {
if (deathKnightAntiMagicShellBlocksMagicEffect(namedAuras, actorId, attack.school)) continue;
if (hasControlImmunity(namedAuras, actorId, effect.mechanic)) continue;
const auraTargetId = actorId === PLAYER_AGGRO_ID ? PLAYER_AURA_ENTITY_ID : actorId;
@@ -21358,7 +21514,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
];
} else if (effect.kind === "pull") {
const pulledPosition: CombatPosition = [position[0], position[1], position[2]];
for (const actorId of targets) {
for (const actorId of effectTargets) {
if (actorId === PLAYER_AGGRO_ID) {
set({ playerPosition: pulledPosition });
} else {
@@ -21619,7 +21775,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
};
if (hit.actorId === PLAYER_AGGRO_ID) {
const current = get();
const playerImpactAuras = aurasForEntity(current.auras, PLAYER_AURA_ENTITY_ID);
const playerImpactAuras = aurasForEntity(synchronizedDeterrenceAuras(current, current.auras, now), PLAYER_AURA_ENTITY_ID);
const learnedCoaPassives = learnedRomCoaPassives(
current.classId, current.secondaryClassId, current.level, current.talentRanks,
);
@@ -21674,9 +21830,11 @@ export const useCombatStore = create<CombatState>((set, get) => ({
hit.attackRevision,
now,
bonusCriticalChancePercent,
aurasForEntity(current.auras, PLAYER_AURA_ENTITY_ID),
playerImpactAuras,
hit.nativeSourceDirect,
hit.targetOrdinal,
false,
actorIsStunned(state, hit.actorId, now),
);
const combatResult = {
...resolvedCombatResult,
@@ -22729,7 +22887,7 @@ export const useCombatStore = create<CombatState>((set, get) => ({
);
const member = memberFromCompanionActor(storedMember, authoritativeActor);
if (member.health <= 0) continue;
const memberImpactAuras = aurasForEntity(currentBeforeHit.auras, member.id);
const memberImpactAuras = aurasForEntity(synchronizedDeterrenceAuras(currentBeforeHit, currentBeforeHit.auras, now), member.id);
const learnedMemberCoaPassives = learnedRomCoaPassives(
member.classId, member.secondaryClassId, member.level, member.talentRanks,
);
@@ -22783,9 +22941,11 @@ export const useCombatStore = create<CombatState>((set, get) => ({
hit.attackRevision,
now,
bonusCriticalChancePercent,
aurasForEntity(get().auras, member.id),
memberImpactAuras,
hit.nativeSourceDirect,
hit.targetOrdinal,
false,
actorIsStunned(state, hit.actorId, now),
);
const combatResult = {
...resolvedCombatResult,
@@ -23695,8 +23855,8 @@ export const useCombatStore = create<CombatState>((set, get) => ({
const warriorAngerManagementRageRemainderMs = angerManagementProfile
? angerManagementElapsedMs % angerManagementProfile.tickIntervalMs
: 0;
const auraExpiration = expireAuras(state.auras, now);
let activeAuras = [...auraExpiration.active];
const auraExpiration = expireAuras(recoveryAuras(state, now), now);
let activeAuras = [...synchronizedDeterrenceAuras(state, auraExpiration.active, now)];
const activeHunterPetIds = new Set(state.summons
.filter((summon) => summon.hunterPet && summon.health > 0 && summon.expiresAt > now)
.map((summon) => summon.id));
@@ -23863,6 +24023,12 @@ export const useCombatStore = create<CombatState>((set, get) => ({
}).current;
}
resourcePools[resourceProfile.type] = resource;
const recoveryManaMaximum = state.maxResourcePools.mana ?? (resourceProfile.type === "mana" ? state.maxResource : 0);
if (state.health > 0 && recoveryManaMaximum > 0) {
resourcePools.mana = Math.min(recoveryManaMaximum, (resourcePools.mana ?? 0)
+ healerManaRecoveryAmount(activeAuras, PLAYER_AURA_ENTITY_ID, recoveryManaMaximum, delta));
if (resourceProfile.type === "mana") resource = resourcePools.mana;
}
if (angerManagementProfile && angerManagementTicks > 0) {
const rageMaximum = state.maxResourcePools.rage ?? state.maxResource;
const rage = Math.min(
@@ -24026,6 +24192,8 @@ export const useCombatStore = create<CombatState>((set, get) => ({
const targetPosition = getMobPosition(autoShotTargetId);
const sourcePosition = combatSpatialActor(PLAYER_AGGRO_ID);
const canAutoShot = !activeEnemyDisarmChannels(work.auras, PLAYER_AURA_ENTITY_ID, now).has("ranged")
&& !aurasForEntity(work.auras, PLAYER_AURA_ENTITY_ID, now)
.some((aura) => aura.definition.utilityTags?.includes("pacified"))
&& !state.activeCast
&& (state.controlledUntil <= now || !enemyControlPreventsActions(state.controlMechanic))
&& Math.sqrt(mobRuntimeDistanceSquared(autoShotTargetId, state.playerPosition))
@@ -24116,6 +24284,8 @@ export const useCombatStore = create<CombatState>((set, get) => ({
const autoAttackSource = combatSpatialActor(PLAYER_AGGRO_ID);
const canAutoAttack = (
!state.activeCast
&& !aurasForEntity(work.auras, PLAYER_AURA_ENTITY_ID, now)
.some((aura) => aura.definition.utilityTags?.includes("pacified"))
&& !(state.classId === "rom-warden" && activeWardenNatureCrystal(work.summons, now))
&& (
state.controlledUntil <= now
@@ -26572,12 +26742,14 @@ export const useCombatStore = create<CombatState>((set, get) => ({
updateSettings: (patch) => set((state) => {
const settings = normalizedSettings(state.settings, patch);
return gameplaySettingsEqual(settings, state.settings) ? state : { settings };
return gameplaySettingsEqual(settings, state.settings) ? state : { settings,
auras: recoveryAuras({ ...state, settings }, combatNow()) };
}),
resetSettings: () => set((state) => (
gameplaySettingsEqual(state.settings, DEFAULT_GAMEPLAY_SETTINGS)
? state
: { settings: DEFAULT_GAMEPLAY_SETTINGS }
: { settings: DEFAULT_GAMEPLAY_SETTINGS,
auras: recoveryAuras({ ...state, settings: DEFAULT_GAMEPLAY_SETTINGS }, combatNow()) }
)),
}));