Update Healer Man server
This commit is contained in:
1 parent
6114a2295f
commit
69809f7228
593 files changed
+903643
-40774
No files matched your search
+288
-38
@@ -1,5 +1,15 @@
|
||||
import { nativeTrainingAutoAttackProfile } from "./romNativeCombatAmount";
|
||||
import { nativeDisarmedMainHandAutoAttackProfile, nativeTrainingAutoAttackProfile } from "./romNativeCombatAmount";
|
||||
import { romNativePlayerHitProfile } from "./romNativeCombatResolution";
|
||||
import { classifyEnemyNpcSpellBlockPhase } from "./enemyBlocking";
|
||||
import { isWow335ShieldBashDazeParent, wow335ShieldBashDazeTargetEligible } from "./wow335ShieldBashDaze";
|
||||
import {
|
||||
ENEMY_DISARM_UNARMED_MAIN_HAND,
|
||||
activeEnemyDisarmChannels,
|
||||
enemyDisarmAbilityWeaponChannel,
|
||||
enemyDisarmBlocksAbilityCast,
|
||||
enemyDisarmRequirementCandidates,
|
||||
} from "./enemyDisarmRuntime";
|
||||
import type { EnemyDisarmWeaponChannel } from "./enemyWeaponDebuffs";
|
||||
import {
|
||||
abilitiesForClass,
|
||||
abilitiesForCharacter,
|
||||
@@ -24,6 +34,7 @@ import {
|
||||
PLAYER_AURA_ENTITY_ID,
|
||||
abilityDamageSchool,
|
||||
auraAdjustedCharacterStats,
|
||||
effectiveActorAuras,
|
||||
equipmentAdjustedCombatAmount,
|
||||
refreshPriestPainAndSufferingDot,
|
||||
stableCombatUnit,
|
||||
@@ -40,6 +51,8 @@ import {
|
||||
wow335HunterGoForTheThroatAttackKindForAbility,
|
||||
} from "./wow335HunterGoForTheThroatRuntime";
|
||||
import { companionResourceMaximum, memberFromCompanionActor } from "./combatActorAuthority";
|
||||
import { wow335DruidShapeshiftFormForAuras } from "./wow335DruidMasterShapeshifterRuntime";
|
||||
import { normalizeCharacterStatRuleset } from "./characterStatRuleset";
|
||||
import type { MobVector3 } from "./mobPopulation";
|
||||
import type { WeaponStats } from "./itemStats";
|
||||
import { requireDungeonDefinition } from "./dungeonRegistry";
|
||||
@@ -55,7 +68,13 @@ import {
|
||||
import { usePartyStore, type PartyCommand, type PartyMember } from "./partyStore";
|
||||
import { baseClassFor, classById, isRomClassId } from "../app/characterCatalog";
|
||||
import { useGameStore } from "./store";
|
||||
import { coaBarbarianDecapitateTargetAllowed } from "./coaBarbarianExecuteRuntime";
|
||||
import { coaBarbarianBeheaderEnergyRefund, coaBarbarianDecapitateTargetAllowed, isCoaBarbarianDecapitate } from "./coaBarbarianExecuteRuntime";
|
||||
import { coaBarbarianDecapitateWeaponStrike, coaNormalizedMeleeWeaponSpeed } from "./coaBarbarianWeaponDamageRuntime";
|
||||
import { coaWitchDoctorBrewingAbilityForCast, coaWitchDoctorCanBrewIngredient } from "./coaWitchDoctorBrewingRuntime";
|
||||
import { coaBrewingPreparationAbility } from "./coaWitchDoctorBrewingAi";
|
||||
import { applyCoaGuardianVanguardEvent } from "./coaGuardianVanguardRuntime";
|
||||
import { applyCoaGuardianParagonEvent, projectCoaGuardianParagonAbility } from "./coaGuardianParagonRuntime";
|
||||
import { coaGuardianFormationAbilityForCast, hasCoaGuardianFormationAura } from "./coaGuardianFormationRuntime";
|
||||
import { useManastormStore } from "./manastormStore";
|
||||
import { WOW335_GLYPH_OF_SWIFTMEND_ID } from "./wow335GlyphCatalog";
|
||||
import {
|
||||
@@ -173,6 +192,7 @@ import { emitRotationDecision } from "./rotationTelemetry";
|
||||
import { imminentCooldownMechanic } from "./companionCooldownPolicy";
|
||||
import { normalizeActorResourcePool, type DeathKnightRuneState } from "./combatActors";
|
||||
import { enemyControlPreventsActions } from "./enemyAttacks";
|
||||
import { enemyControlAbilityCanEscapeAuras } from "./enemyControlImmunityPurge";
|
||||
import { advanceCombatResourcePool } from "./combatResources";
|
||||
import {
|
||||
advanceRomNativeRecoveryClock,
|
||||
@@ -1022,25 +1042,41 @@ function partyAbilityAmountWithHolyBlessing(
|
||||
member.gender,
|
||||
member.statRuleset ?? "wow",
|
||||
);
|
||||
const auras = state.auras.filter((aura) => aura.targetId === member.id
|
||||
&& (aura.expiresAt === null || aura.expiresAt > now));
|
||||
const channel = ability.wowSpellDefense
|
||||
? enemyDisarmAbilityWeaponChannel({
|
||||
damageClass: ability.wowSpellDefense.damageClass,
|
||||
attributes0: ability.wowSpellDefense.attributes0,
|
||||
attributes3: ability.wowSpellDefense.attributes3,
|
||||
equippedItemSubclassMask: ability.wowWeaponRequirements?.equippedItemSubclassMask,
|
||||
})
|
||||
: undefined;
|
||||
const auras = effectiveActorAuras(state.auras, member.id, now, channel)
|
||||
.filter((aura) => aura.targetId === member.id);
|
||||
const feralMainHand = member.classId === "druid"
|
||||
&& ["cat", "bear"].includes(wow335DruidShapeshiftFormForAuras(state.auras, member.id, now) ?? "");
|
||||
const disabledWeaponChannels = !feralMainHand
|
||||
? activeEnemyDisarmChannels(state.auras, member.id, now)
|
||||
: undefined;
|
||||
const intellect = ["intellect", "all-primary", "all-primary-attributes"].reduce(
|
||||
(value, stat) => resolveAuraStatValue(value, auras, stat),
|
||||
base.intellect,
|
||||
);
|
||||
if (member.statRuleset === "rom") {
|
||||
if (normalizeCharacterStatRuleset(member.statRuleset, member.classId) === "rom") {
|
||||
return partyAbilityAmount(member, ability, effect, poisonDoses, intellect,
|
||||
auraAdjustedCharacterStats(base, state.auras, member.id, now).romNative, nativeTargetContext);
|
||||
auraAdjustedCharacterStats(base, auras, member.id, now).romNative, nativeTargetContext,
|
||||
disabledWeaponChannels);
|
||||
}
|
||||
if (member.classId !== "rom-priest") {
|
||||
return partyAbilityAmount(member, ability, effect, poisonDoses, intellect);
|
||||
return partyAbilityAmount(member, ability, effect, poisonDoses, intellect,
|
||||
undefined, undefined, disabledWeaponChannels);
|
||||
}
|
||||
const bonus = romPriestHolyBlessingStats(base, auras).spellPowerBonus;
|
||||
if (bonus <= 0) return partyAbilityAmount(member, ability, effect, poisonDoses, intellect);
|
||||
if (bonus <= 0) return partyAbilityAmount(member, ability, effect, poisonDoses, intellect,
|
||||
undefined, undefined, disabledWeaponChannels);
|
||||
return partyAbilityAmount({
|
||||
...member,
|
||||
gearStats: { ...member.gearStats, spellPower: member.gearStats.spellPower + bonus },
|
||||
}, ability, effect, poisonDoses, intellect);
|
||||
}, ability, effect, poisonDoses, intellect, undefined, undefined, disabledWeaponChannels);
|
||||
}
|
||||
|
||||
function partyWarlockAbilityAmount(
|
||||
@@ -1214,8 +1250,10 @@ export function partyAttackDamage(
|
||||
member: Pick<PartyMember, "level" | "role">
|
||||
& Partial<Pick<PartyMember,
|
||||
"id" | "classId" | "specialization" | "gearStats" | "raceId" | "gender" | "statRuleset" | "secondaryClassId"
|
||||
>>,
|
||||
>>,
|
||||
now = Date.now(),
|
||||
weaponOverride?: WeaponStats,
|
||||
disarmedNativeMainHand = false,
|
||||
): number {
|
||||
const coefficient = member.role === "tank" ? 0.85 : member.role === "healer" ? 0.55 : 1.25;
|
||||
const gear = member.gearStats;
|
||||
@@ -1228,7 +1266,12 @@ export function partyAttackDamage(
|
||||
})
|
||||
? baseClassFor(classId) === "hunter" ? "ranged" : "spell"
|
||||
: "melee";
|
||||
const nativeStats = member.statRuleset === "rom" && isRomClassId(classId)
|
||||
const attackChannel: EnemyDisarmWeaponChannel | undefined = kind === "ranged"
|
||||
? "ranged" : kind === "melee" ? "main-hand" : undefined;
|
||||
const actorAuras = member.id
|
||||
? effectiveActorAuras(useCombatStore.getState().auras, member.id, now, attackChannel)
|
||||
: useCombatStore.getState().auras;
|
||||
const nativeStats = normalizeCharacterStatRuleset(member.statRuleset, classId) === "rom" && isRomClassId(classId)
|
||||
? auraAdjustedCharacterStats(deriveCharacterStats(
|
||||
classId,
|
||||
member.level,
|
||||
@@ -1238,11 +1281,16 @@ export function partyAttackDamage(
|
||||
member.secondaryClassId,
|
||||
member.gender ?? "male",
|
||||
"rom",
|
||||
), useCombatStore.getState().auras, member.id ?? "", now).romNative
|
||||
), actorAuras, member.id ?? "", now).romNative
|
||||
: undefined;
|
||||
const nativeAuto = kind !== "spell" ? nativeTrainingAutoAttackProfile(nativeStats, kind === "ranged" ? "ranged" : "main-hand") : undefined;
|
||||
const nativeSlot = kind === "ranged" ? "ranged" : "main-hand";
|
||||
const nativeAuto = kind === "spell" ? undefined
|
||||
: disarmedNativeMainHand && kind === "melee"
|
||||
? nativeDisarmedMainHandAutoAttackProfile(nativeStats)
|
||||
?? nativeTrainingAutoAttackProfile(nativeStats, nativeSlot)
|
||||
: nativeTrainingAutoAttackProfile(nativeStats, nativeSlot);
|
||||
if (nativeAuto) return Math.max(1, Math.round(nativeAuto.damage));
|
||||
const weapon = kind === "ranged" ? gear.rangedWeapon : kind === "melee" ? gear.meleeWeapon : null;
|
||||
const weapon = weaponOverride ?? (kind === "ranged" ? gear.rangedWeapon : kind === "melee" ? gear.meleeWeapon : null);
|
||||
const equipmentPower = nativeStats
|
||||
? kind === "spell"
|
||||
? nativeStats.magicalDamage
|
||||
@@ -1482,6 +1530,9 @@ function commitPartyAbilityUseWithRogueCooldown(
|
||||
now: number,
|
||||
options: Parameters<typeof commitPartyAbilityUse>[3] = {},
|
||||
): PartyMember | null {
|
||||
if (!coaWitchDoctorCanBrewIngredient(
|
||||
ability, useCombatStore.getState().auras, member.id, member.talentRanks, now,
|
||||
)) return null;
|
||||
const naturePowerSpent = options.ignoreResourceCosts ? 0 : partyAbilityCosts(member, ability, now)
|
||||
.filter((cost) => cost.resource === "nature-power" && cost.amount > 0)
|
||||
.reduce((total, cost) => total + cost.amount, 0);
|
||||
@@ -1775,13 +1826,24 @@ function tankPriorityTargetId(
|
||||
return loose[0]?.id ?? fallbackTargetId;
|
||||
}
|
||||
|
||||
function memberSwingMs(member: PartyMember, now: number): number {
|
||||
function memberSwingMs(member: PartyMember, now: number, disarmedMainHand = false): number {
|
||||
const attackChannel: EnemyDisarmWeaponChannel | undefined = member.classId === "rom-scout"
|
||||
|| (partyUsesRangedAttacks(member) && baseClassFor(member.classId) === "hunter")
|
||||
? "ranged"
|
||||
: partyUsesRangedAttacks(member) ? undefined : "main-hand";
|
||||
const sourceAuras = effectiveActorAuras(
|
||||
useCombatStore.getState().auras, member.id, now, attackChannel,
|
||||
);
|
||||
const native = member.statRuleset === "rom" ? auraAdjustedCharacterStats(deriveCharacterStats(
|
||||
member.classId, member.level, member.gearStats, member.raceId, {}, member.secondaryClassId, member.gender, "rom",
|
||||
), useCombatStore.getState().auras, member.id, now).romNative : undefined;
|
||||
), sourceAuras, member.id, now).romNative : undefined;
|
||||
const physicalAuto = !partyUsesRangedAttacks(member) || baseClassFor(member.classId) === "hunter";
|
||||
const nativeSlot = member.classId === "rom-scout" ? "ranged" : "main-hand";
|
||||
const nativeAuto = physicalAuto
|
||||
? nativeTrainingAutoAttackProfile(native, member.classId === "rom-scout" ? "ranged" : "main-hand")
|
||||
? disarmedMainHand && native && nativeSlot === "main-hand"
|
||||
? nativeDisarmedMainHandAutoAttackProfile(native)
|
||||
?? nativeTrainingAutoAttackProfile(native, nativeSlot)
|
||||
: nativeTrainingAutoAttackProfile(native, nativeSlot)
|
||||
: undefined;
|
||||
const base = member.role === "tank"
|
||||
? 1_800
|
||||
@@ -1801,12 +1863,19 @@ function memberSwingMs(member: PartyMember, now: number): number {
|
||||
member.level,
|
||||
member.classId,
|
||||
);
|
||||
const memberAuras = useCombatStore.getState().auras.filter((aura) => (
|
||||
const memberAuras = sourceAuras.filter((aura) => (
|
||||
aura.targetId === member.id && (aura.expiresAt === null || aura.expiresAt > now)
|
||||
));
|
||||
const effectiveHaste = resolveAuraStatValue(hasteRating, memberAuras, hasteStat);
|
||||
if (nativeAuto) return Math.max(500, Math.round(hastedDuration(nativeAuto.intervalMs, effectiveHaste)
|
||||
/ raceAffixHasteMultiplier(now)));
|
||||
if (disarmedMainHand && normalizeCharacterStatRuleset(member.statRuleset, member.classId) === "wow") {
|
||||
return Math.max(500, Math.round(hastedDuration(
|
||||
ENEMY_DISARM_UNARMED_MAIN_HAND.speedMs,
|
||||
effectiveHaste,
|
||||
500,
|
||||
) / raceAffixHasteMultiplier()));
|
||||
}
|
||||
return Math.round(hastedDuration(
|
||||
base,
|
||||
effectiveHaste,
|
||||
@@ -1887,10 +1956,12 @@ function partyRogueOffhandAttackProfile(
|
||||
member.gender,
|
||||
member.statRuleset ?? "wow",
|
||||
);
|
||||
const auras = useCombatStore.getState().auras.filter((aura) => (
|
||||
aura.targetId === member.id && (aura.expiresAt === null || aura.expiresAt > now)
|
||||
));
|
||||
const nativeAuto = nativeTrainingAutoAttackProfile(auraAdjustedCharacterStats(stats, useCombatStore.getState().auras, member.id, now).romNative, "off-hand");
|
||||
const auras = effectiveActorAuras(
|
||||
useCombatStore.getState().auras, member.id, now, "off-hand",
|
||||
).filter((aura) => aura.targetId === member.id && (aura.expiresAt === null || aura.expiresAt > now));
|
||||
const nativeAuto = nativeTrainingAutoAttackProfile(auraAdjustedCharacterStats(
|
||||
stats, auras, member.id, now,
|
||||
).romNative, "off-hand");
|
||||
const meleeHastePercent = resolveAuraStatValue(stats.meleeHastePercent, auras, "melee-haste");
|
||||
if (nativeAuto) return { damage: Math.max(1, Math.round(nativeAuto.damage
|
||||
* (masteryActive ? 1 + ROM_ROGUE_OFF_HAND_MASTERY_DAMAGE_PERCENT / 100 : 1))),
|
||||
@@ -1989,9 +2060,9 @@ function partyShamanWeaponAttackProfile(
|
||||
member.gender,
|
||||
member.statRuleset ?? "wow",
|
||||
);
|
||||
const auras = useCombatStore.getState().auras.filter((aura) => (
|
||||
aura.targetId === member.id && (aura.expiresAt === null || aura.expiresAt > now)
|
||||
));
|
||||
const auras = effectiveActorAuras(
|
||||
useCombatStore.getState().auras, member.id, now, hand,
|
||||
).filter((aura) => aura.targetId === member.id && (aura.expiresAt === null || aura.expiresAt > now));
|
||||
const attackPower = partyShamanAttackPower(stats.attackPower, auras);
|
||||
const meleeHastePercent = resolveAuraStatValue(stats.meleeHastePercent, auras, "melee-haste");
|
||||
const fullWeaponDamage = (weapon.damageMin + weapon.damageMax) / 2
|
||||
@@ -2030,9 +2101,9 @@ function partyShamanWeaponHitDamage(
|
||||
member.gender,
|
||||
member.statRuleset ?? "wow",
|
||||
);
|
||||
const auras = useCombatStore.getState().auras.filter((aura) => (
|
||||
aura.targetId === member.id && (aura.expiresAt === null || aura.expiresAt > now)
|
||||
));
|
||||
const auras = effectiveActorAuras(
|
||||
useCombatStore.getState().auras, member.id, now, hand,
|
||||
).filter((aura) => aura.targetId === member.id && (aura.expiresAt === null || aura.expiresAt > now));
|
||||
const fullWeaponDamage = (weapon.damageMin + weapon.damageMax) / 2
|
||||
+ partyShamanAttackPower(stats.attackPower, auras) / 14 * (weapon.speedMs / 1_000);
|
||||
return Math.max(1, Math.round(fullWeaponDamage * (hand === "off-hand" ? 0.5 : 1)));
|
||||
@@ -2231,6 +2302,23 @@ function advancePartyWeaponAttack(member: PartyMember, targetId: string, now: nu
|
||||
|| Math.abs(origin[1] - targetPosition[1]) > 8
|
||||
|| !combatHasLineOfSight(origin, targetPosition)
|
||||
|| (spatial && !isPositionInFacingArc(spatial, targetPosition, Math.PI))) return member;
|
||||
const disarmedWeaponChannels = activeEnemyDisarmChannels(
|
||||
useCombatStore.getState().auras, member.id, now,
|
||||
);
|
||||
const sourceFeralForm = member.classId === "druid"
|
||||
? wow335DruidShapeshiftFormForAuras(useCombatStore.getState().auras, member.id, now)
|
||||
: undefined;
|
||||
const sourceRuleset = normalizeCharacterStatRuleset(member.statRuleset, member.classId);
|
||||
const hasPhysicalMainHandAuto = !partyUsesRangedAttacks(member)
|
||||
|| (member.classId === "shaman" && hasMaelstromWeaponTalent);
|
||||
const sourceCanUseMainHand = hasPhysicalMainHandAuto
|
||||
&& !(sourceFeralForm === "cat" || sourceFeralForm === "bear");
|
||||
const sourceDisarmedMainHand = sourceRuleset === "wow"
|
||||
&& sourceCanUseMainHand
|
||||
&& disarmedWeaponChannels.has("main-hand");
|
||||
const sourceDisarmedNativeMainHand = sourceRuleset === "rom"
|
||||
&& sourceCanUseMainHand
|
||||
&& disarmedWeaponChannels.has("main-hand");
|
||||
const rogueOffhandWeapon = partyRogueOffhandWeapon(member);
|
||||
if (rogueOffhandWeapon) {
|
||||
const masteryActive = partyRogueOffHandMasteryActive(member, now);
|
||||
@@ -2283,11 +2371,20 @@ function advancePartyWeaponAttack(member: PartyMember, targetId: string, now: nu
|
||||
const isDualWielding = hands.some(({ hand }) => hand === "off-hand");
|
||||
for (const { hand, weapon } of hands) {
|
||||
const deadlineKey = `${member.id}:${hand}`;
|
||||
const swingWeapon = hand === "main-hand" && sourceDisarmedMainHand
|
||||
? ENEMY_DISARM_UNARMED_MAIN_HAND
|
||||
: weapon;
|
||||
if (disarmedWeaponChannels.has(hand) && hand !== "main-hand") {
|
||||
if (now >= (nextShamanWeaponAttackAt.get(deadlineKey) ?? 0)) {
|
||||
nextShamanWeaponAttackAt.set(deadlineKey, now + partyShamanWeaponAttackProfile(member, weapon, hand, now).intervalMs);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (now < (nextShamanWeaponAttackAt.get(deadlineKey) ?? 0)) continue;
|
||||
const flurryActiveBeforeSwing = wow335ShamanFlurryIsActive(
|
||||
useCombatStore.getState().auras, member.id, now,
|
||||
);
|
||||
const profile = partyShamanWeaponAttackProfile(member, weapon, hand, now);
|
||||
const profile = partyShamanWeaponAttackProfile(member, swingWeapon, hand, now);
|
||||
const abilityId = hand === "main-hand" ? "party-auto-attack" : "party-auto-attack-offhand";
|
||||
const seed = `${member.id}:${targetId}:${abilityId}:${now}:${hand}`;
|
||||
const result = useCombatStore.getState().damageMob(
|
||||
@@ -2314,7 +2411,7 @@ function advancePartyWeaponAttack(member: PartyMember, targetId: string, now: nu
|
||||
nextShamanWeaponAttackAt.set(deadlineKey, now + nextIntervalMs);
|
||||
if ((result.combatResult?.landed ?? result.amount > 0) && result.amount > 0) {
|
||||
maybeProcPartyShamanMaelstromWeapon(
|
||||
member, targetId, abilityId, hand, weapon.speedMs, now,
|
||||
member, targetId, abilityId, hand, swingWeapon.speedMs, now,
|
||||
);
|
||||
}
|
||||
for (const phase of ["release", "impact"] as const) {
|
||||
@@ -2326,7 +2423,15 @@ function advancePartyWeaponAttack(member: PartyMember, targetId: string, now: nu
|
||||
return member;
|
||||
}
|
||||
if (now < (nextWeaponAttackAt.get(member.id) ?? 0)) return member;
|
||||
nextWeaponAttackAt.set(member.id, now + memberSwingMs(member, now));
|
||||
const ownerAttackChannel = partyUsesRangedAttacks(member) && baseClassFor(member.classId) === "hunter"
|
||||
? "ranged" : "main-hand";
|
||||
if (ownerAttackChannel === "ranged" && disarmedWeaponChannels.has(ownerAttackChannel)) {
|
||||
nextWeaponAttackAt.set(member.id, now + memberSwingMs(member, now));
|
||||
return member;
|
||||
}
|
||||
nextWeaponAttackAt.set(member.id, now + memberSwingMs(
|
||||
member, now, sourceDisarmedMainHand || sourceDisarmedNativeMainHand,
|
||||
));
|
||||
const scoutRogueRangedAutoAttack = isRomScoutRogueBloodArrowOwner(member)
|
||||
&& partyUsesRangedAttacks(member);
|
||||
const bloodArrowAutoAttackCost = scoutRogueRangedAutoAttack
|
||||
@@ -2369,12 +2474,20 @@ function advancePartyWeaponAttack(member: PartyMember, targetId: string, now: nu
|
||||
const forgeAutoAttackActive = forgeRankOneActive || forgeRankTwoActive;
|
||||
const needsAttackRoll = scentRank > 0 || bloodCakedBladeRank > 0 || bloodyVengeanceRank > 0
|
||||
|| unbridledWrathRank > 0 || goForTheThroatRank > 0 || rogueDualWielding || forgeAutoAttackActive;
|
||||
const weaponSpeedMs = member.gearStats.meleeWeapon?.speedMs ?? 2_400;
|
||||
const ownerMainHandWeapon = sourceDisarmedMainHand
|
||||
? ENEMY_DISARM_UNARMED_MAIN_HAND
|
||||
: member.gearStats.meleeWeapon;
|
||||
const weaponSpeedMs = ownerMainHandWeapon?.speedMs ?? 2_400;
|
||||
const seed = `${member.id}:${targetId}:${abilityId}:${now}:${rogueDualWielding
|
||||
? "rom-rogue-dual-wield-main-hand"
|
||||
: forgeAutoAttackActive ? "rom-champion-forge-auto-attack" : "scent-of-blood-auto-attack"}`;
|
||||
useCombatStore.getState().notifyOwnerHostileAction(member.id, targetId);
|
||||
const baseOwnerAttackDamage = partyAttackDamage(member, now);
|
||||
const baseOwnerAttackDamage = partyAttackDamage(
|
||||
member,
|
||||
now,
|
||||
sourceDisarmedMainHand ? ENEMY_DISARM_UNARMED_MAIN_HAND : undefined,
|
||||
sourceDisarmedNativeMainHand,
|
||||
);
|
||||
const bloodArrowRangedAuras = scoutRogueRangedAutoAttack
|
||||
? activeAuras.filter((aura) => aura.sourceId === member.id
|
||||
&& aura.targetId === member.id
|
||||
@@ -2395,7 +2508,15 @@ function advancePartyWeaponAttack(member: PartyMember, targetId: string, now: nu
|
||||
abilityId,
|
||||
rules: { isAutoAttack: true, ...(rogueDualWielding ? { isDualWielding: true } : {}) },
|
||||
weaponSpeedMs,
|
||||
} : undefined);
|
||||
} : undefined, !needsAttackRoll
|
||||
? ownerAttackChannel === "ranged" ? "ranged"
|
||||
: !partyUsesRangedAttacks(member) ? "mainhand" : undefined
|
||||
: undefined);
|
||||
if (member.classId === "guardian" && (result.combatResult?.landed ?? result.amount > 0)) {
|
||||
useCombatStore.setState((state) => ({ auras: [...applyCoaGuardianVanguardEvent(state.auras, {
|
||||
id: member.id, classId: member.classId, talentRanks: member.talentRanks,
|
||||
}, "auto-attack-landed", now)] }));
|
||||
}
|
||||
if (member.classId === "death-knight"
|
||||
&& (result.combatResult?.landed ?? result.amount > 0)
|
||||
&& result.amount > 0) {
|
||||
@@ -2484,6 +2605,30 @@ export function partyAbilityTargetIsValid(
|
||||
castCanMove = false,
|
||||
): boolean {
|
||||
const combat = useCombatStore.getState();
|
||||
if (ability.wowWeaponRequirements) {
|
||||
const loadout = resolvedEquipmentLoadout(combat.equipment, combat.inventory, {
|
||||
ownerId: member.id,
|
||||
ownerKind: "party",
|
||||
classId: member.classId,
|
||||
secondaryClassId: member.secondaryClassId,
|
||||
specializationId: member.specializationId,
|
||||
role: member.role,
|
||||
level: member.level,
|
||||
});
|
||||
if (enemyDisarmBlocksAbilityCast(
|
||||
combat.auras,
|
||||
member.id,
|
||||
now,
|
||||
{
|
||||
...ability.wowWeaponRequirements,
|
||||
attributes3: ability.wowSpellDefense?.attributes3,
|
||||
passive: ability.passive,
|
||||
createsAura: ability.effects.some((effect) => effect.kind === "apply-aura"),
|
||||
},
|
||||
enemyDisarmRequirementCandidates(loadout),
|
||||
)) return false;
|
||||
}
|
||||
if (!coaWitchDoctorCanBrewIngredient(ability, combat.auras, member.id, member.talentRanks, now)) return false;
|
||||
if (member.classId === "warrior" && ability.name === "Victory Rush"
|
||||
&& !combat.auras.some((aura) => aura.targetId === member.id
|
||||
&& (aura.expiresAt === null || aura.expiresAt > now)
|
||||
@@ -2837,6 +2982,20 @@ function choosePartySupportIntent(
|
||||
return { ability: stateAbility, targetId: member.id };
|
||||
}
|
||||
}
|
||||
if (member.classId === "guardian" && member.role === "tank"
|
||||
&& member.specializationId === "guardian:vanguard"
|
||||
&& now >= member.nextActionAt
|
||||
&& !hasCoaGuardianFormationAura(combat.auras, member, "tower", now)
|
||||
&& !hasCoaGuardianFormationAura(combat.auras, member, "line", now)) {
|
||||
const source = abilitiesForCharacter(member.classId, member.secondaryClassId)
|
||||
.find((ability) => ability.dbcSpellId === 800317);
|
||||
const learned = source ? learnedAbilityForBuild(source, member.level, member.talentRanks) : null;
|
||||
const tower = learned ? coaGuardianFormationAbilityForCast(learned) : null;
|
||||
if (tower && partyAbilityIsReady(member, tower, now)
|
||||
&& partyAbilityTargetIsValid(member, tower, member.id, undefined, now)) {
|
||||
return { ability: tower, targetId: member.id };
|
||||
}
|
||||
}
|
||||
if (member.role !== "healer") return null;
|
||||
const dispelTargetId = [
|
||||
...(!simulatedPartyExcludesPlayer() && combat.health > 0 ? [PLAYER_AGGRO_ID] : []),
|
||||
@@ -2852,6 +3011,21 @@ function choosePartySupportIntent(
|
||||
return { ability, targetId: dispelTargetId };
|
||||
}
|
||||
}
|
||||
if (member.classId === "witch-doctor" && member.specializationId === "witch-doctor:brewing") {
|
||||
const allies = [
|
||||
...members.map((ally) => ({ health: ally.health, maxHealth: ally.maxHealth })),
|
||||
...(!simulatedPartyExcludesPlayer() ? [{ health: combat.health, maxHealth: combat.maxHealth }] : []),
|
||||
];
|
||||
const learned = abilitiesForCharacter(member.classId, member.secondaryClassId)
|
||||
.map((ability) => learnedAbilityForBuild(ability, member.level, member.talentRanks))
|
||||
.filter((ability): ability is AbilityDefinition => ability !== null);
|
||||
const brewing = coaBrewingPreparationAbility(member, learned, combat.auras, allies, now,
|
||||
Boolean(fallenTargetId || dispelTargetId || Object.values(combat.mobs).some((mob) => mob.engaged && !mob.dead && mob.activeCast)));
|
||||
if (brewing && partyAbilityIsReady(member, brewing, now)
|
||||
&& partyAbilityTargetIsValid(member, brewing, member.id, undefined, now)) {
|
||||
return { ability: brewing, targetId: member.id };
|
||||
}
|
||||
}
|
||||
// Preventive damage-taken procs (Earth Shield / Prayer of Mending-shaped
|
||||
// effects) belong on the tank before damage lands. Normal HoTs and direct
|
||||
// heals still wait for the injured-target path below.
|
||||
@@ -3168,6 +3342,18 @@ function executePartyAbility(
|
||||
illuminationBaseManaCostSnapshot?: number,
|
||||
): PartyAbilityExecutionResult {
|
||||
let combat = useCombatStore.getState();
|
||||
ability = coaWitchDoctorBrewingAbilityForCast(ability, combat.auras, member.id, now);
|
||||
ability = coaGuardianFormationAbilityForCast(ability);
|
||||
ability = projectCoaGuardianParagonAbility(ability, member, combat.auras, now);
|
||||
if (isCoaBarbarianDecapitate(ability)) {
|
||||
const mainHand = resolvedEquipmentLoadout(combat.equipment, combat.inventory, {
|
||||
ownerId: member.id, ownerKind: "party", classId: member.classId,
|
||||
secondaryClassId: member.secondaryClassId, specializationId: member.specializationId,
|
||||
role: member.role, level: member.level,
|
||||
})["main-hand"];
|
||||
ability = coaBarbarianDecapitateWeaponStrike(ability, member.gearStats.meleeWeapon,
|
||||
mainHand ? coaNormalizedMeleeWeaponSpeed(mainHand) : null);
|
||||
}
|
||||
if (ability.source === "runewaker") {
|
||||
for (const passive of abilitiesForCharacter(member.classId, member.secondaryClassId)
|
||||
.filter((candidate) => candidate.passive)
|
||||
@@ -3420,7 +3606,9 @@ function executePartyAbility(
|
||||
&& effect.sourceAbilityId === "rom-rogue-rom-warrior-elite-35")),
|
||||
}));
|
||||
}
|
||||
for (const effect of castEffects) {
|
||||
const utilityHit = hostileTargets.includes(targetId)
|
||||
? useCombatStore.getState().resolveMobUtilitySpellHit(targetId, member.id, ability, now) : null;
|
||||
for (const effect of utilityHit?.blocked ? [] : castEffects) {
|
||||
const darkEnergyStrikeGroup = effect.kind === "damage" && isRomChampionDarkEnergyStrike
|
||||
? romChampionDarkEnergyStrikePacketGroupAtDamageIndex(romChampionDarkEnergyStrikeDamageIndex++)
|
||||
: null;
|
||||
@@ -3624,7 +3812,15 @@ function executePartyAbility(
|
||||
&& combat.auras.some((aura) => aura.targetId === member.id
|
||||
&& aura.definition.utilityTags?.includes(ROM_WARLOCK_PRIEST_SPREADING_PAIN_CRIT_TAG)
|
||||
&& (aura.expiresAt === null || aura.expiresAt > now));
|
||||
const partyUsesCombatRollContext = member.statRuleset === "rom" || drumsOfWarShot || lightOfDayHit || deathKnightSuddenDoomTrigger
|
||||
const sourceNpcSpellTarget = combat.mobs[hostileTargetId];
|
||||
const sourceNpcSpellAttackKind: WotlkAttackKind | null = !channelPulse
|
||||
&& effect.kind === "damage" && spellSchool === "physical" && ability.wowSpellDefense
|
||||
&& sourceNpcSpellTarget?.serverEntry && !sourceNpcSpellTarget.characterDefender
|
||||
&& sourceNpcSpellTarget.runewakerNpcId === undefined && !sourceNpcSpellTarget.summonedBy
|
||||
&& classifyEnemyNpcSpellBlockPhase(ability.wowSpellDefense) === "direct-damage"
|
||||
? ability.wowSpellDefense.damageClass === 3 ? "ranged" : "melee" : null;
|
||||
const partyUsesCombatRollContext = sourceNpcSpellAttackKind !== null
|
||||
|| member.statRuleset === "rom" || drumsOfWarShot || lightOfDayHit || deathKnightSuddenDoomTrigger
|
||||
|| deathKnightScentOfBloodMeleeHit
|
||||
|| deathKnightBloodyVengeanceHit
|
||||
|| deathKnightBloodGorgedHit
|
||||
@@ -3737,7 +3933,7 @@ function executePartyAbility(
|
||||
? romNativePlayerHitProfile(effect.romNativeSource.skillId, effect.romNativeSource.componentId)?.damageChannel === "physical"
|
||||
? member.classId === "rom-scout" ? "ranged" : "melee" : "spell"
|
||||
: drumsOfWarShot ? "ranged"
|
||||
: hunterGoForTheThroatAttackKind
|
||||
: sourceNpcSpellAttackKind ?? hunterGoForTheThroatAttackKind
|
||||
?? (deathKnightScentOfBloodMeleeHit || deathKnightBloodGorgedHit
|
||||
|| shamanWeaponMeleeHit || godsSwordDirectPhysicalHit || darkEnergyStrikePhysicalHit
|
||||
|| (deathKnightBloodyVengeanceHit && spellSchool === "physical")
|
||||
@@ -3780,6 +3976,18 @@ function executePartyAbility(
|
||||
}
|
||||
effectiveAmount += result.amount;
|
||||
const partyHitLanded = result.combatResult?.landed ?? result.amount > 0;
|
||||
const beheaderRefund = !channelPulse && effect.kind === "damage"
|
||||
? coaBarbarianBeheaderEnergyRefund(ability, member.talentRanks, companionResourceMaximum(runtimeMember, "energy"), result)
|
||||
: 0;
|
||||
if (beheaderRefund > 0) {
|
||||
const maximumEnergy = companionResourceMaximum(runtimeMember, "energy");
|
||||
const energy = Math.min(maximumEnergy, (runtimeMember.resourcePools.energy ?? runtimeMember.resource) + beheaderRefund);
|
||||
runtimeMember = {
|
||||
...runtimeMember,
|
||||
...(runtimeMember.resourceType === "energy" ? { resource: energy } : {}),
|
||||
resourcePools: { ...runtimeMember.resourcePools, energy },
|
||||
};
|
||||
}
|
||||
if (!partyUsesCombatRollContext && effect.kind === "damage" && partyHitLanded && result.amount > 0) {
|
||||
const learnedSoulBrandPassives = member.classId === "rom-warlock"
|
||||
&& member.secondaryClassId === "rom-mage"
|
||||
@@ -4502,7 +4710,8 @@ function executePartyAbility(
|
||||
applyPartyMageArcanePotency(member, ability, "presence-of-mind", now);
|
||||
}
|
||||
if (effect.control && useCombatStore.getState().mobs[recipientId]) {
|
||||
useCombatStore.getState().controlMob(recipientId, ability.id, effect.control.kind, appliedEffect.aura.durationMs ?? 1_000, now);
|
||||
useCombatStore.getState().controlMob(recipientId, ability.id, effect.control.kind,
|
||||
appliedEffect.aura.durationMs ?? 1_000, now, false, member.id, effect.control.magnitude);
|
||||
}
|
||||
}
|
||||
} else if (effect.kind === "dispel") {
|
||||
@@ -4734,6 +4943,8 @@ function executePartyAbility(
|
||||
resource: resource === runtimeMember.resourceType ? resourcePools[resource] ?? runtimeMember.resource : runtimeMember.resource,
|
||||
};
|
||||
} else if (effect.kind === "trigger-spell" && !effect.expanded && depth < 2) {
|
||||
if (effect.spellId === 29703 && (!isWow335ShieldBashDazeParent(ability)
|
||||
|| !wow335ShieldBashDazeTargetEligible(useCombatStore.getState().mobs[targetId]))) continue;
|
||||
const triggered = abilitiesForClass(member.classId).find((candidate) => candidate.dbcSpellId === effect.spellId)
|
||||
?? triggeredAbilityBySpellId(effect.spellId);
|
||||
if (triggered && triggered.id !== ability.id) {
|
||||
@@ -4913,6 +5124,11 @@ function executePartyAbility(
|
||||
}
|
||||
}
|
||||
if (!channelPulse && depth === 0) applyPartyWarlockBackdraft(member, ability, now);
|
||||
if (!channelPulse && depth === 0) {
|
||||
useCombatStore.setState((state) => ({ auras: [...applyCoaGuardianParagonEvent(
|
||||
state.auras, runtimeMember, { type: "cast", ability, result: "success" }, now,
|
||||
).auras] }));
|
||||
}
|
||||
emitPartyAbility(member, ability, targetId, "release", now, !castStartAlreadyEmitted);
|
||||
emitPartyAbility(member, ability, targetId, "impact", now, !castStartAlreadyEmitted);
|
||||
if (effectiveAmount > 0 && ability.effects.some((effect) => ["heal", "hot", "shield", "resurrection"].includes(effect.kind))) {
|
||||
@@ -5185,6 +5401,40 @@ export function advanceDungeonPartyCombat(now = Date.now()): void {
|
||||
member.controlledUntil > now
|
||||
&& enemyControlPreventsActions(member.controlMechanic)
|
||||
) {
|
||||
const controlCombat = useCombatStore.getState();
|
||||
const berserkerRageBase = member.classId === "warrior"
|
||||
? abilitiesForCharacter(member.classId, member.secondaryClassId)
|
||||
.find((ability) => ability.dbcSpellId === 18499)
|
||||
: undefined;
|
||||
const berserkerRage = berserkerRageBase
|
||||
? learnedAbilityForBuild(berserkerRageBase, member.level, member.talentRanks)
|
||||
: null;
|
||||
const mechanic = member.controlMechanic;
|
||||
if (berserkerRage && !member.activeCast && (mechanic === "fear" || mechanic === "stun")
|
||||
&& enemyControlAbilityCanEscapeAuras(
|
||||
berserkerRage, mechanic, controlCombat.auras, member.id, now,
|
||||
)
|
||||
&& partyAbilityTargetIsValid(member, berserkerRage, member.id, undefined, now)) {
|
||||
const committed = commitPartyAbilityUseWithRogueCooldown(member, berserkerRage, now);
|
||||
if (committed) {
|
||||
const execution = executePartyAbility(committed, berserkerRage, member.id, now, nextMembers);
|
||||
const partyStateAfterGrant = usePartyStore.getState().members.find((candidate) => candidate.id === member.id);
|
||||
const actorAfterGrant = useCombatStore.getState().actors[member.id];
|
||||
const controlledUntil = actorAfterGrant?.controlledUntil
|
||||
?? partyStateAfterGrant?.controlledUntil
|
||||
?? execution.member.controlledUntil;
|
||||
const controlMechanic = partyStateAfterGrant?.controlMechanic
|
||||
?? execution.member.controlMechanic;
|
||||
nextMembers[index] = {
|
||||
...execution.member,
|
||||
controlledUntil,
|
||||
controlMechanic: controlledUntil > now ? controlMechanic : null,
|
||||
status: targetId ? member.role === "tank" ? "guarding" : "attacking" : "ready",
|
||||
nextActionAt: now + Math.max(250, berserkerRage.castTimeMs, berserkerRage.gcdMs),
|
||||
};
|
||||
continue;
|
||||
}
|
||||
}
|
||||
nextMembers[index] = member.status === "holding"
|
||||
? member
|
||||
: { ...member, status: "holding" };
|
||||
|
||||
Reference in new issue
Block a user