Update Healer Man server
This commit is contained in:
1 parent
69809f7228
commit
ff20ab8779
101 files changed
+16713
-945
No files matched your search
+201
-29
@@ -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()) }
|
||||
)),
|
||||
}));
|
||||
|
||||
|
||||
Reference in new issue
Block a user