Update class mechanics, stat rulesets, and CoA reference catalogs
This commit is contained in:
1 parent
b16821e92e
commit
39458d5de6
221 files changed
+2584962
-568
No files matched your search
+273
-23
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user