Update class mechanics, stat rulesets, and CoA reference catalogs

This commit is contained in:
phenom committed 2026-10-04 12:56:30 -04:00
1 parent b16821e92e
commit 39458d5de6
221 files changed
+2584962 -568

No files matched your search

+273 -23
View File
@@ -21,6 +21,7 @@ import type { CombatSummon, MobCombatState, TimedCombatEffect } from "./combatSt
import {
PLAYER_AURA_ENTITY_ID,
abilityDamageSchool,
auraAdjustedCharacterStats,
equipmentAdjustedCombatAmount,
refreshPriestPainAndSufferingDot,
stableCombatUnit,
@@ -50,7 +51,7 @@ import {
updatePartyRuntimePosition,
} from "./partyRuntimeRegistry";
import { usePartyStore, type PartyCommand, type PartyMember } from "./partyStore";
import { baseClassFor, classById } from "../app/characterCatalog";
import { baseClassFor, classById, isRomClassId } from "../app/characterCatalog";
import { useGameStore } from "./store";
import { useManastormStore } from "./manastormStore";
import { WOW335_GLYPH_OF_SWIFTMEND_ID } from "./wow335GlyphCatalog";
@@ -80,6 +81,11 @@ import {
type DispelCategory,
} from "./combatAuras";
import { romPriestHolyBlessingDamage, romPriestHolyBlessingStats } from "./romPriestHolyBlessingRuntime";
import { romKnightGodsVengeanceShouldResetShadowStep } from "./romKnightOverrides";
import {
romChampionHasForgeRankTwoAura,
romChampionRunePulseUseEffect,
} from "./romChampionForgeRuntime";
import {
ROM_DRUID_MAGE_INSPIRATION_PASSIVE_ID,
isRomDruidMagePair,
@@ -88,6 +94,11 @@ import {
romDruidMageInspirationRecipientIds,
} from "./romDruidMageInspirationRuntime";
import { ROM_MAGE_DRUID_GREEN_GUARDIAN_AURA_TAG, ROM_MAGE_DRUID_GREEN_GUARDIAN_SKILL_ID, isRomMageDruidPair } from "./romMageDruidGreenGuardianRuntime";
import {
romWardenRevivalBuildMatches,
romWardenRevivalNormalHeal,
romWardenRevivalStateIsActive,
} from "./romWardenRevivalRuntime";
import {
romWarlockMageSoulBrandEffects,
} from "./romWarlockMageSoulBrand";
@@ -301,6 +312,10 @@ import {
wow335ShamanTidalWavesProcChance,
} from "./wow335ShamanTidalWavesRuntime";
import { wow335ShamanFlurryIsActive } from "./wow335ShamanFlurryRuntime";
import {
WOW335_SHAMAN_NATURES_GUARDIAN_NEXT_NATURE_CRIT_TAG,
wow335ShamanNaturesGuardianNextCritIsReady,
} from "./wow335ShamanNaturesGuardianRuntime";
import {
WOW335_SHAMAN_MAELSTROM_WEAPON_AURA_TAG,
WOW335_SHAMAN_MAELSTROM_WEAPON_ELIGIBLE_SPELLS,
@@ -323,6 +338,13 @@ import {
ROM_ROGUE_OFF_HAND_MASTERY_DAMAGE_PERCENT,
ROM_ROGUE_OFF_HAND_MASTERY_TAG,
} from "./romRogueMechanics";
import {
ROM_CHAMPION_RUNE_SIPHON_ID,
romChampionDarkEnergyStrikeGroup0Survives,
romChampionDarkEnergyStrikeMatchesBuild,
romChampionDarkEnergyStrikePacketGroupAtDamageIndex,
romChampionDarkEnergyStrikeShouldResetRuneSiphon,
} from "./romChampionDarkEnergyStrikeRuntime";
import {
romPriestChampionRageControlGain,
romPriestVowOfSilenceDurationMs,
@@ -670,6 +692,16 @@ function applyPartyDeathKnightBladeBarrier(
}, now);
}
function applyPartyRomChampionRunePulseOnUse(
member: PartyMember,
ability: AbilityDefinition,
now: number,
): void {
const combat = useCombatStore.getState();
const effect = romChampionRunePulseUseEffect(ability, combat.auras, member.id, now);
if (effect) combat.applyActorAura(member.id, member.id, ability, effect, now);
}
function maybeProcPartyShamanTidalWaves(
member: PartyMember,
ability: AbilityDefinition,
@@ -964,6 +996,8 @@ function partyAbilityAmountWithHolyBlessing(
member.raceId,
{},
member.secondaryClassId,
member.gender,
member.statRuleset ?? "wow",
);
const auras = state.auras.filter((aura) => aura.targetId === member.id
&& (aura.expiresAt === null || aura.expiresAt > now));
@@ -974,7 +1008,7 @@ function partyAbilityAmountWithHolyBlessing(
if (member.classId !== "rom-priest") {
return partyAbilityAmount(member, ability, effect, poisonDoses, intellect);
}
const bonus = romPriestHolyBlessingStats(base, auras).spellPowerBonus;
const bonus = member.statRuleset === "rom" ? 0 : romPriestHolyBlessingStats(base, auras).spellPowerBonus;
if (bonus <= 0) return partyAbilityAmount(member, ability, effect, poisonDoses, intellect);
return partyAbilityAmount({
...member,
@@ -1005,6 +1039,12 @@ function partyWarlockAbilityAmount(
member.gearStats,
member.classId,
sourceAbility,
0,
undefined,
member.raceId,
member.gender,
member.statRuleset ?? "wow",
member.secondaryClassId,
);
return Math.round(tickAmount * burstSeconds * 1_000 / Math.max(1, conflagrateDot.intervalMs))
+ (effect.bonusFlat ?? 0);
@@ -1144,7 +1184,9 @@ export function activePartyTargetId(
export function partyAttackDamage(
member: Pick<PartyMember, "level" | "role">
& Partial<Pick<PartyMember, "classId" | "specialization" | "gearStats">>,
& Partial<Pick<PartyMember,
"id" | "classId" | "specialization" | "gearStats" | "raceId" | "gender" | "statRuleset" | "secondaryClassId"
>>,
): number {
const coefficient = member.role === "tank" ? 0.85 : member.role === "healer" ? 0.55 : 1.25;
const gear = member.gearStats;
@@ -1157,18 +1199,34 @@ export function partyAttackDamage(
})
? baseClassFor(classId) === "hunter" ? "ranged" : "spell"
: "melee";
const nativeStats = member.statRuleset === "rom" && isRomClassId(classId)
? auraAdjustedCharacterStats(deriveCharacterStats(
classId,
member.level,
gear,
member.raceId ?? "rom-human",
{},
member.secondaryClassId,
member.gender ?? "male",
"rom",
), useCombatStore.getState().auras, member.id ?? "").romNative
: undefined;
const weapon = kind === "ranged" ? gear.rangedWeapon : kind === "melee" ? gear.meleeWeapon : null;
const equipmentPower = kind === "spell"
? equipmentAttackPower(classId, gear, kind)
: equipmentAttackPower(classId, gear, kind) / 14 * ((weapon?.speedMs ?? 2400) / 1_000)
+ (weapon ? (weapon.damageMin + weapon.damageMax) / 2 * 0.35 : 0);
const crit = ratingPercent(
const equipmentPower = nativeStats
? kind === "spell"
? nativeStats.magicalDamage
: nativeStats.physicalAttack / 14 * ((weapon?.speedMs ?? 2400) / 1_000)
: kind === "spell"
? equipmentAttackPower(classId, gear, kind)
: equipmentAttackPower(classId, gear, kind) / 14 * ((weapon?.speedMs ?? 2400) / 1_000)
+ (weapon ? (weapon.damageMin + weapon.damageMax) / 2 * 0.35 : 0);
const crit = nativeStats ? 0 : ratingPercent(
gear.criticalStrikeRating,
kind === "spell" ? "spellCrit" : kind === "ranged" ? "rangedCrit" : "meleeCrit",
member.level,
classId,
);
const hit = ratingPercent(
const hit = nativeStats ? 0 : ratingPercent(
gear.hitRating,
kind === "spell" ? "spellHit" : kind === "ranged" ? "rangedHit" : "meleeHit",
member.level,
@@ -1697,7 +1755,7 @@ function memberSwingMs(member: PartyMember, now: number): number {
const hasteStat = partyUsesRangedAttacks(member)
? baseClassFor(member.classId) === "hunter" ? "ranged-haste" : "spell-haste"
: "melee-haste";
const hasteRating = ratingPercent(
const hasteRating = member.statRuleset === "rom" ? 0 : ratingPercent(
member.gearStats.hasteRating,
partyUsesRangedAttacks(member)
? baseClassFor(member.classId) === "hunter" ? "rangedHaste" : "spellHaste"
@@ -1786,6 +1844,8 @@ function partyRogueOffhandAttackProfile(
member.raceId,
{},
member.secondaryClassId,
member.gender,
member.statRuleset ?? "wow",
);
const auras = useCombatStore.getState().auras.filter((aura) => (
aura.targetId === member.id && (aura.expiresAt === null || aura.expiresAt > now)
@@ -1881,6 +1941,8 @@ function partyShamanWeaponAttackProfile(
member.raceId,
{},
member.secondaryClassId,
member.gender,
member.statRuleset ?? "wow",
);
const auras = useCombatStore.getState().auras.filter((aura) => (
aura.targetId === member.id && (aura.expiresAt === null || aura.expiresAt > now)
@@ -1920,6 +1982,8 @@ function partyShamanWeaponHitDamage(
member.raceId,
{},
member.secondaryClassId,
member.gender,
member.statRuleset ?? "wow",
);
const auras = useCombatStore.getState().auras.filter((aura) => (
aura.targetId === member.id && (aura.expiresAt === null || aura.expiresAt > now)
@@ -2235,10 +2299,20 @@ function advancePartyWeaponAttack(member: PartyMember, targetId: string, now: nu
? member.talentRanks[WOW335_HUNTER_GO_FOR_THE_THROAT_TALENT_ID] ?? 0
: 0;
const rogueDualWielding = member.classId === "rom-rogue" && Boolean(rogueOffhandWeapon);
const activeAuras = useCombatStore.getState().auras;
const forgeRankOneActive = member.classId === "rom-champion"
&& activeAuras.some((aura) => aura.targetId === member.id
&& aura.definition.id === "rom-champion-forge-active"
&& (aura.expiresAt === null || aura.expiresAt > now));
const forgeRankTwoActive = member.classId === "rom-champion"
&& romChampionHasForgeRankTwoAura(activeAuras, member.id, now);
const forgeAutoAttackActive = forgeRankOneActive || forgeRankTwoActive;
const needsAttackRoll = scentRank > 0 || bloodCakedBladeRank > 0 || bloodyVengeanceRank > 0
|| unbridledWrathRank > 0 || goForTheThroatRank > 0 || rogueDualWielding;
|| unbridledWrathRank > 0 || goForTheThroatRank > 0 || rogueDualWielding || forgeAutoAttackActive;
const weaponSpeedMs = member.gearStats.meleeWeapon?.speedMs ?? 2_400;
const seed = `${member.id}:${targetId}:${abilityId}:${now}:${rogueDualWielding ? "rom-rogue-dual-wield-main-hand" : "scent-of-blood-auto-attack"}`;
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 ownerAttackDamage = partyAttackDamage(member);
const result = useCombatStore.getState().damageMob(targetId, ownerAttackDamage, now,
@@ -2266,6 +2340,11 @@ function advancePartyWeaponAttack(member: PartyMember, targetId: string, now: nu
scentRank > 0 && (result.combatResult?.landed ?? result.amount > 0),
now,
);
if (forgeAutoAttackActive && result.amount > 0) {
// The combat event proc already grants Forge's Rage through the actor mirror.
// Carry that result forward before applying this swing's ordinary +6 Rage.
runtimeMember = memberFromCompanionActor(runtimeMember, useCombatStore.getState().actors[member.id]);
}
runtimeMember = maybeProcPartyWarriorUnbridledWrath(
runtimeMember,
Boolean(result.combatResult?.landed && result.amount > 0),
@@ -2385,7 +2464,8 @@ export function partyAbilityTargetIsValid(
&& effect.remainingTicks > 0 && effect.tag === requiredTargetStateTag)) return false;
if (ability.dbcSpellId === 491613 && mob?.creatureFamily === "living"
&& !(member.classId === "rom-knight" && member.secondaryClassId === "rom-scout" && member.level >= 60)) return false;
if (ability.dbcSpellId === 490151 && (member.comboTargetId !== targetId || member.comboPoints <= 0)) return false;
if ([490151, 499909].includes(ability.dbcSpellId ?? -1)
&& (member.comboTargetId !== targetId || member.comboPoints <= 0)) return false;
if (ability.dbcSpellId === ROM_ROGUE_ENERGIZED_HANDS_SKILL_ID || ability.dbcSpellId === ROM_ROGUE_POISON_SKILL_ID) {
const handsEffect = ability.effects.find((effect) => effect.kind === "apply-aura");
const activeHandsAura = combat.auras.find((aura) => aura.targetId === member.id
@@ -2645,6 +2725,31 @@ function choosePartySupportIntent(
return { ability: mendPet, targetId: member.id };
}
}
if (member.classId === "rom-rogue" && member.secondaryClassId === "rom-warden"
&& member.health > 0 && member.health < member.maxHealth) {
const catalogAbility = abilitiesForCharacter(member.classId, member.secondaryClassId)
.find((ability) => ability.id === "rom-rogue-rom-warden-elite-60");
const revival = catalogAbility
? learnedAbilityForBuild(catalogAbility, member.level, member.talentRanks)
: null;
if (revival && partyAbilityIsReady(member, revival, now)
&& partyAbilityTargetIsValid(member, revival, member.id, undefined, now)) {
return { ability: revival, targetId: member.id };
}
}
if (romWardenRevivalBuildMatches(member.classId, member.secondaryClassId)) {
const catalogAbility = abilitiesForCharacter(member.classId, member.secondaryClassId)
.find((ability) => ability.source === "runewaker" && ability.dbcSpellId === 499544);
const stateAbility = catalogAbility
? learnedAbilityForBuild(catalogAbility, member.level, member.talentRanks)
: null;
if (stateAbility
&& !romWardenRevivalStateIsActive(combat.auras, member.id, now)
&& partyAbilityIsReady(member, stateAbility, now)
&& partyAbilityTargetIsValid(member, stateAbility, member.id, undefined, now)) {
return { ability: stateAbility, targetId: member.id };
}
}
if (member.role !== "healer") return null;
const dispelTargetId = [
...(!simulatedPartyExcludesPlayer() && combat.health > 0 ? [PLAYER_AGGRO_ID] : []),
@@ -3011,6 +3116,12 @@ function executePartyAbility(
&& ["Lightning Bolt", "Chain Lightning"].includes(ability.name)
? abilityDamageSchool("shaman", ability)
: presentationSchoolForAbility(ability);
const consumesShamanNaturesGuardianCrit = member.classId === "shaman"
&& (hostileTargets.length > 0 || friendlyTargets.length > 0)
&& ability.effects.some((effect) => effect.kind === "damage"
? (effect.school ?? abilityDamageSchool("shaman", ability)) === "nature"
: effect.kind === "heal" && abilityDamageSchool("shaman", ability) === "nature")
&& wow335ShamanNaturesGuardianNextCritIsReady(combat.auras, member.id, now);
const usesCombustion = spellSchool === "fire" && hostileTargets.length > 0
&& ability.effects.some((effect) => effect.kind === "damage")
&& combat.auras.some((aura) => aura.targetId === member.id
@@ -3031,12 +3142,13 @@ function executePartyAbility(
const elementalOathRank = combat.classId === "shaman"
? Math.min(3, Math.max(0, Math.trunc(combat.talentRanks["talent-2049"] ?? 0)))
: 0;
const criticalChance = Math.max(0, ratingPercent(
const criticalChance = Math.max(0, (member.statRuleset === "rom" ? 0 : ratingPercent(
member.gearStats.criticalStrikeRating,
"spellCrit",
member.level,
member.classId,
) / 100 + elementalOathRank / 100 + (consumesInnerFocus ? 0.25 : 0)
) / 100) + elementalOathRank / 100 + (consumesInnerFocus ? 0.25 : 0)
+ (consumesShamanNaturesGuardianCrit ? 1 : 0)
+ wow335ShamanTidalWavesCriticalChanceBonusPercent(ability.name, tidalWavesChargesConsumed) / 100);
const masterOfElementsCritPercent = member.classId === "mage"
? Math.min(3, Math.max(0, Math.trunc(member.talentRanks[WOW335_MAGE_MASTER_OF_ELEMENTS_TALENT_ID] ?? 0)))
@@ -3077,6 +3189,13 @@ function executePartyAbility(
&& (member.activeCast?.romPriestHolyCandleCharged ?? false);
let deathKnightReapingProcAttemptedForCast = false;
let deathKnightReapingSpendRestoreAttemptedForCast = false;
const isRomChampionDarkEnergyStrike = romChampionDarkEnergyStrikeMatchesBuild(
member.classId,
member.secondaryClassId,
ability,
);
let romChampionDarkEnergyStrikeDamageIndex = 0;
const romChampionDarkEnergyStrikeGroup0SurvivedTargets = new Set<string>();
const attemptDeathKnightReapingOnLandedTrigger = (): void => {
if (member.classId !== "death-knight" || deathKnightReapingProcAttemptedForCast) return;
if (!deathKnightReapingSpendRestoreAttemptedForCast) {
@@ -3213,6 +3332,9 @@ function executePartyAbility(
}));
}
for (const effect of castEffects) {
const darkEnergyStrikeGroup = effect.kind === "damage" && isRomChampionDarkEnergyStrike
? romChampionDarkEnergyStrikePacketGroupAtDamageIndex(romChampionDarkEnergyStrikeDamageIndex++)
: null;
// Necrotic Wound's Sinister Request rider belongs to the completed cast.
// romEffectsForCast also appends it to each synthetic channel-pulse ability,
// so applying it here would reduce the cooldown once per hit plus once at
@@ -3236,6 +3358,8 @@ function executePartyAbility(
continue;
}
for (const hostileTargetId of hostileTargets) {
if (darkEnergyStrikeGroup === "dark"
&& !romChampionDarkEnergyStrikeGroup0SurvivedTargets.has(hostileTargetId)) continue;
if (effect.kind === "dot") {
if (!groundAreaCreated) combat.applyTimedActorEffect(
member.id, hostileTargetId, ability,
@@ -3402,6 +3526,7 @@ function executePartyAbility(
const partyWarriorVictoryRushRank = member.classId === "warrior" && ability.name === "Victory Rush"
? member.talentRanks[WOW335_WARRIOR_IMPROVED_VICTORY_RUSH_TALENT_ID] ?? 0
: 0;
const darkEnergyStrikePhysicalHit = darkEnergyStrikeGroup === "physical";
const spreadingPainCriticalBuffActive = effect.kind === "damage"
&& member.classId === "rom-warlock"
&& member.secondaryClassId === "rom-priest"
@@ -3418,8 +3543,10 @@ function executePartyAbility(
|| shamanElementalFocusHit
|| shamanLightningOverloadHit
|| godsSwordDirectPhysicalHit
|| darkEnergyStrikePhysicalHit
|| spreadingPainCriticalBuffActive
|| partyWarriorVictoryRushRank > 0
|| consumesShamanNaturesGuardianCrit
|| consumesColdBlood || usesCombustion || arcanePotencyCriticalChanceBonusPercent > 0
|| masterOfElementsCritPercent > 0
|| hunterGoForTheThroatAttackKind !== null
@@ -3432,6 +3559,9 @@ function executePartyAbility(
? member.talentRanks[WOW335_MAGE_MOLTEN_FURY_TALENT_ID] ?? 0
: 0;
const partyDamageTargetBeforeHit = useCombatStore.getState().mobs[hostileTargetId];
const damageSchool = effect.kind === "damage" && isRomChampionDarkEnergyStrike
? effect.school ?? spellSchool
: spellSchool;
const partyRawDirectDamageBase = mageTargetDamageAmount
+ sunderStacks * (effect.kind === "damage" ? effect.bonusPerSunderStack ?? 0 : 0);
const partyBlessingAuras = !partyUsesCombatRollContext && member.classId === "rom-priest"
@@ -3452,7 +3582,7 @@ function executePartyAbility(
? wow335MageMoltenFuryDamage(
partyRawDirectDamage,
partyMageMoltenFuryRank,
spellSchool,
damageSchool,
partyDamageTargetBeforeHit.health,
partyDamageTargetBeforeHit.maxHealth,
)
@@ -3472,7 +3602,7 @@ function executePartyAbility(
)
: partyDirectDamageWithMoltenFury;
const maelstromWeaponPvpDamageReductionPercent = partyDamageTargetBeforeHit?.characterDefender
&& spellSchool !== "physical"
&& damageSchool !== "physical"
? wow335ShamanMaelstromWeaponPvpDamageReductionPercent(
maelstromWeaponRank,
maelstromWeaponStacksConsumed,
@@ -3489,12 +3619,12 @@ function executePartyAbility(
role: member.role,
abilityId: ability.id,
},
spellSchool,
damageSchool,
partyUsesCombatRollContext ? {
attackKind: drumsOfWarShot ? "ranged"
: hunterGoForTheThroatAttackKind
?? (deathKnightScentOfBloodMeleeHit || deathKnightBloodGorgedHit
|| shamanWeaponMeleeHit || godsSwordDirectPhysicalHit
|| shamanWeaponMeleeHit || godsSwordDirectPhysicalHit || darkEnergyStrikePhysicalHit
|| (deathKnightBloodyVengeanceHit && spellSchool === "physical")
|| consumesColdBlood || deathKnightSuddenDoomTrigger
? "melee" : "spell"),
@@ -3503,6 +3633,10 @@ function executePartyAbility(
seed: `${member.id}:${ability.id}:${hostileTargetId}:${now}:special-crit${shamanWeaponHand ? `:${shamanWeaponHand}` : ""}`,
...(deathKnightBloodyVengeanceHit && channelPulse ? { periodic: true } : {}),
criticalChanceBonusPercent: (consumesColdBlood ? 100 : 0)
+ (consumesShamanNaturesGuardianCrit
&& (effect.kind === "damage"
? (effect.school ?? abilityDamageSchool("shaman", ability)) === "nature"
: abilityDamageSchool("shaman", ability) === "nature") ? 100 : 0)
+ arcanePotencyCriticalChanceBonusPercent
+ masterOfElementsCritPercent,
...(godsSwordDirectPhysicalHit || spreadingPainCriticalBuffActive
@@ -3520,6 +3654,13 @@ function executePartyAbility(
: ability.name === "Shiv" ? { rules: { canCrit: false } } : {}),
} : undefined,
);
if (darkEnergyStrikeGroup === "physical" && hostileTargetId === targetId
&& romChampionDarkEnergyStrikeGroup0Survives({
effectiveDamage: result.amount,
combatResult: result.combatResult,
})) {
romChampionDarkEnergyStrikeGroup0SurvivedTargets.add(hostileTargetId);
}
effectiveAmount += result.amount;
const partyHitLanded = result.combatResult?.landed ?? result.amount > 0;
if (!partyUsesCombatRollContext && effect.kind === "damage" && partyHitLanded && result.amount > 0) {
@@ -3856,6 +3997,39 @@ function executePartyAbility(
attemptDeathKnightReapingOnLandedTrigger();
combat = useCombatStore.getState();
}
} else if (effect.kind === "utility"
&& effect.action === "class-script"
&& ability.id === "rom-rogue-rom-warden-elite-60"
&& member.classId === "rom-rogue"
&& member.secondaryClassId === "rom-warden") {
const revivalCharge = combat.auras.find((aura) => (
aura.sourceId === member.id
&& aura.targetId === member.id
&& (aura.expiresAt === null || aura.expiresAt > now)
&& aura.definition.utilityTags?.includes("rom-revival-charge")
));
const requested = romWardenRevivalNormalHeal({
stacks: matchingAuraStacks(combat.auras, "rom-revival-charge", member.id, member.id, now),
targetMaxHealth: member.maxHealth,
});
// RuneWaker's FixPer heal is already max-HP based; skip generic healing
// done/taken modifiers and critical rolls just as the player path does.
const result = useCombatStore.getState().healActor(member.id, requested, {
sourceActorId: member.id,
abilityId: ability.id,
criticalChance: 0,
amountAlreadyModified: true,
seed: `${member.id}:${ability.id}:${now}:rom-warden-revival`,
now,
});
effectiveAmount += result.effectiveAmount;
if (revivalCharge) {
useCombatStore.setState((state) => ({
auras: state.auras.filter((aura) => aura.instanceId !== revivalCharge.instanceId),
}));
}
combat = useCombatStore.getState();
runtimeMember = memberFromCompanionActor(runtimeMember, combat.actors[member.id]);
} else if (effect.kind === "utility"
&& effect.action === "class-script"
&& ability.source === "runewaker"
@@ -4191,9 +4365,11 @@ function executePartyAbility(
effectiveAmount += useCombatStore.getState().dispelActor(recipientId, effect.categories, effect.maxCount);
}
} else if (effect.kind === "resource") {
const resourceRecipients = effect.scope === "party"
? friendlyTargets
: friendlyTargets.length ? friendlyTargets : [member.id];
const resourceRecipients = effect.scope === "caster"
? [member.id]
: effect.scope === "party"
? friendlyTargets
: friendlyTargets.length ? friendlyTargets : [member.id];
for (const friendlyTargetId of resourceRecipients) {
const resource = effect.resource ?? member.resourceType;
if (effect.scope === "party"
@@ -4241,13 +4417,43 @@ function executePartyAbility(
}
}
} else if (effect.kind === "combo") {
if (ability.dbcSpellId === 490151) {
if ([490151, 499909].includes(ability.dbcSpellId ?? -1)) {
useCombatStore.setState((state) => ({
effects: state.effects.filter((entry) => !(entry.kind === "dot"
&& entry.tag === "rom-holy-seal" && entry.sourceActorId === member.id
&& entry.targetId === targetId)),
}));
}
if (targetId && ability.dbcSpellId === 499909
&& member.classId === "rom-knight" && member.secondaryClassId === "rom-rogue") {
const pairAbilities = abilitiesForCharacter(member.classId, member.secondaryClassId);
const vengeanceBase = pairAbilities.find((candidate) => candidate.source === "runewaker"
&& candidate.classId === "rom-knight" && candidate.secondaryClassId === "rom-rogue"
&& candidate.dbcSpellId === 499934 && candidate.passive);
const shadowStepBase = pairAbilities.find((candidate) => candidate.source === "runewaker"
&& candidate.classId === "rom-rogue" && candidate.dbcSpellId === 490344);
const vengeance = vengeanceBase
? learnedAbilityForBuild(vengeanceBase, runtimeMember.level, runtimeMember.talentRanks)
: null;
const shadowStep = shadowStepBase
? learnedAbilityForBuild(shadowStepBase, runtimeMember.level, runtimeMember.talentRanks)
: null;
if (shadowStep && romKnightGodsVengeanceShouldResetShadowStep({
characterClassId: member.classId,
secondaryClassId: member.secondaryClassId,
ability,
learnedPassiveSpellIds: vengeance ? [vengeance.dbcSpellId ?? -1] : [],
shadowStepLearned: true,
targetSealStacks: runtimeMember.comboTargetId === targetId ? runtimeMember.comboPoints : 0,
sameTarget: runtimeMember.comboTargetId === targetId,
landed: landedDirectHitTargetIdsForCast.has(targetId),
})) {
runtimeMember = {
...runtimeMember,
cooldowns: { ...runtimeMember.cooldowns, [shadowStep.id]: 0 },
};
}
}
const current = runtimeMember.comboTargetId === targetId ? runtimeMember.comboPoints : 0;
const comboPoints = Math.max(0, Math.min(member.classId === "rom-knight" ? 4 : 5, current + effect.amount));
runtimeMember = {
@@ -4389,6 +4595,42 @@ function executePartyAbility(
}
}
}
if (isRomChampionDarkEnergyStrike && !channelPulse && targetId) {
const runeSiphonBase = abilitiesForCharacter(member.classId, member.secondaryClassId)
.find((candidate) => candidate.source === "runewaker"
&& candidate.classId === "rom-champion"
&& candidate.secondaryClassId === "rom-warlock"
&& candidate.dbcSpellId === ROM_CHAMPION_RUNE_SIPHON_ID);
const learnedRuneSiphon = runeSiphonBase
? learnedAbilityForBuild(runeSiphonBase, member.level, member.talentRanks)
: null;
if (romChampionDarkEnergyStrikeShouldResetRuneSiphon({
characterClassId: member.classId,
secondaryClassId: member.secondaryClassId,
ability,
runeSiphonLearned: learnedRuneSiphon !== null,
singleTargetCast: hostileTargets.length === 1 && hostileTargets[0] === targetId,
group0Survived: romChampionDarkEnergyStrikeGroup0SurvivedTargets.has(targetId),
roll: stablePartyUnit(
`${member.id}:${ability.id}:${targetId}:${now}:rom-champion-dark-energy-strike-rune-siphon`,
),
}) && learnedRuneSiphon) {
runtimeMember = {
...runtimeMember,
cooldowns: { ...runtimeMember.cooldowns, [learnedRuneSiphon.id]: 0 },
};
useCombatStore.setState((state) => {
const actor = state.actors[member.id];
return actor ? {
actors: {
...state.actors,
[member.id]: { ...actor, cooldowns: { ...actor.cooldowns, [learnedRuneSiphon.id]: 0 } },
},
} : {};
});
combat = useCombatStore.getState();
}
}
if (!channelPulse && depth === 0) {
maybeProcPartyShamanTidalWaves(member, ability, directHealingRawAmount, now);
}
@@ -4486,6 +4728,12 @@ function executePartyAbility(
effectiveAmount += useCombatStore.getState().triggerChimeraShot(member.id, targetId, ability.id, now);
}
if (consumesColdBlood) useCombatStore.getState().consumeActorAuraTag(member.id, "next-offensive-critical");
if (consumesShamanNaturesGuardianCrit) {
useCombatStore.getState().consumeActorAuraTag(
member.id,
WOW335_SHAMAN_NATURES_GUARDIAN_NEXT_NATURE_CRIT_TAG,
);
}
if (ability.dbcSpellId === 491171) useCombatStore.getState().consumeActorAuraTag(member.id, "rom-charged");
if (ability.dbcSpellId === 491172) useCombatStore.getState().consumeActorAuraTag(member.id, "rom-static-field-charge");
if (ability.dbcSpellId === 498759) {
@@ -4893,6 +5141,7 @@ export function advanceDungeonPartyCombat(now = Date.now()): void {
nextMembers[index] = { ...member, status: "ready", nextActionAt: now + 250 };
continue;
}
applyPartyRomChampionRunePulseOnUse(member, castAbility, now);
if (castAbility.gcdMs > 0 && castingHastePercent > 0) {
committed = {
...committed,
@@ -5184,6 +5433,7 @@ export function advanceDungeonPartyCombat(now = Date.now()): void {
nextMembers[index] = { ...runtimeMember, status: desiredStatus, nextActionAt: now + 250 };
continue;
}
applyPartyRomChampionRunePulseOnUse(runtimeMember, castAbility, now);
if (castAbility.gcdMs > 0 && castingHastePercent > 0) {
committed = {
...committed,