From 1723e9dbb7c73376c3b9cb3ad38680f1eca1db03 Mon Sep 17 00:00:00 2001 From: Sean Smith Date: Sun, 17 Nov 2024 18:29:38 +1000 Subject: [PATCH] Enhancement 11.0.5 (#7181) * initial * progress * updating enhancement to 11.0.5 * elemental updates for 11.0.5 * initial * progress * updating enhancement to 11.0.5 * elemental updates for 11.0.5 --- scripts/talents/generate-talents.ts | 4 +- .../retail/shaman/elemental/CHANGELOG.tsx | 1 + .../retail/shaman/elemental/CONFIG.tsx | 2 +- .../shaman/elemental/CombatLogParser.tsx | 4 - .../elemental/guide/FlameShockSubSection.tsx | 4 +- .../shaman/elemental/modules/Abilities.tsx | 10 +- .../elemental/modules/talents/Ascendance.tsx | 330 ++++++++++++------ .../modules/talents/FlashOfLightning.tsx | 126 ------- .../talents/SkybreakersFieryDemise.tsx | 82 ----- .../modules/talents/StormElemental.tsx | 92 +++-- .../elemental/modules/talents/Stormkeeper.tsx | 37 +- .../retail/shaman/enhancement/CHANGELOG.tsx | 1 + .../retail/shaman/enhancement/CONFIG.tsx | 7 +- .../shaman/enhancement/CombatLogParser.tsx | 4 +- .../retail/shaman/enhancement/constants.ts | 1 + .../shaman/enhancement/modules/Abilities.tsx | 26 +- .../shaman/enhancement/modules/Buffs.tsx | 2 +- .../enhancement/modules/apl/Conditions.tsx | 41 +-- .../enhancement/modules/apl/Stormbringer.tsx | 108 ++++-- .../normalizers/EventLinkNormalizer.tsx | 16 + .../MaelstromWeaponResourceNormalizer.tsx | 41 ++- .../{Stormbringer.tsx => Stormsurge.tsx} | 99 +++--- .../modules/talents/Ascendance.tsx | 7 +- .../modules/talents/ElementalAssault.tsx | 3 +- .../modules/talents/ElementalBlastGuide.tsx | 30 +- .../modules/talents/ElementalSpirits.tsx | 12 +- .../enhancement/modules/talents/HotHand.tsx | 2 +- .../modules/talents/SwirlingMaelstrom.tsx | 3 +- .../modules/talents/ThorimsInvocation.tsx | 5 +- .../retail/shaman/shared/Abilities.tsx | 2 +- src/common/SPELLS/shaman.ts | 41 ++- src/common/TALENTS/shaman.ts | 190 ++++++---- src/parser/core/CASTS_THAT_ARENT_CASTS.ts | 2 + 33 files changed, 725 insertions(+), 610 deletions(-) delete mode 100644 src/analysis/retail/shaman/elemental/modules/talents/FlashOfLightning.tsx delete mode 100644 src/analysis/retail/shaman/elemental/modules/talents/SkybreakersFieryDemise.tsx rename src/analysis/retail/shaman/enhancement/modules/spells/{Stormbringer.tsx => Stormsurge.tsx} (53%) diff --git a/scripts/talents/generate-talents.ts b/scripts/talents/generate-talents.ts index c9371cce84c..98bcdb6607b 100644 --- a/scripts/talents/generate-talents.ts +++ b/scripts/talents/generate-talents.ts @@ -25,8 +25,8 @@ import { const LIVE_WOW_BUILD_NUMBER = '11.0.5.57388'; const LIVE_TALENT_DATA_URL = 'https://www.raidbots.com/static/data/live/talents.json'; const LIVE_SPELLPOWER_DATA_URL = `https://wago.tools/db2/SpellPower/csv?build=${LIVE_WOW_BUILD_NUMBER}`; -const PTR_WOW_BUILD_NUMBER = '11.0.2.55763'; -const PTR_TALENT_DATA_URL = 'https://www.raidbots.com/static/data/beta/talents.json'; +const PTR_WOW_BUILD_NUMBER = '11.0.5.56865'; +const PTR_TALENT_DATA_URL = `https://www.raidbots.com/static/data/${PTR_WOW_BUILD_NUMBER}/talents.json`; const PTR_SPELLPOWER_DATA_URL = `https://wago.tools/db2/SpellPower/csv?build=${PTR_WOW_BUILD_NUMBER}`; const classes: { [classId: number]: { name: string; baseMaxResource: number } } = { diff --git a/src/analysis/retail/shaman/elemental/CHANGELOG.tsx b/src/analysis/retail/shaman/elemental/CHANGELOG.tsx index 7bfcede489f..dd37fbee8df 100644 --- a/src/analysis/retail/shaman/elemental/CHANGELOG.tsx +++ b/src/analysis/retail/shaman/elemental/CHANGELOG.tsx @@ -7,6 +7,7 @@ import SpellLink from 'interface/SpellLink'; // prettier-ignore export default [ + change(date(2024, 11, 17), <>Initial support for 11.0.5, Seriousnes), change(date(2024, 9, 28), <>Fixed statistic incorrectly showing as , Seriousnes), change(date(2024, 9, 28), <>Updating analysis, Seriousnes), change(date(2024, 9, 27), <>Added guide section for usage., Seriousnes), diff --git a/src/analysis/retail/shaman/elemental/CONFIG.tsx b/src/analysis/retail/shaman/elemental/CONFIG.tsx index cf707325f79..1f1aae4e4cb 100644 --- a/src/analysis/retail/shaman/elemental/CONFIG.tsx +++ b/src/analysis/retail/shaman/elemental/CONFIG.tsx @@ -10,7 +10,7 @@ export default { contributors: [Awildfivreld, Periodic, Seriousnes], branch: GameBranch.Retail, // The WoW client patch this spec was last updated. - patchCompatibility: '11.0.2', + patchCompatibility: '11.0.5', supportLevel: SupportLevel.MaintainedFull, // Explain the status of this spec's analysis here. Try to mention how complete it is, and perhaps show links to places users can learn more. // If this spec's analysis does not show a complete picture please mention this in the `` component. diff --git a/src/analysis/retail/shaman/elemental/CombatLogParser.tsx b/src/analysis/retail/shaman/elemental/CombatLogParser.tsx index 36ffca94d90..46284fcc17e 100644 --- a/src/analysis/retail/shaman/elemental/CombatLogParser.tsx +++ b/src/analysis/retail/shaman/elemental/CombatLogParser.tsx @@ -24,7 +24,6 @@ import PrimalFireElemental from './modules/talents/PrimalFireElemental'; import PrimalStormElemental from './modules/talents/PrimalStormElemental'; import StormElemental from './modules/talents/StormElemental'; import Stormkeeper from './modules/talents/Stormkeeper'; -import FlashOfLightning from './modules/talents/FlashOfLightning'; import SurgeOfPower from './modules/talents/SurgeOfPower'; import ElementalOrbit from '../shared/talents/ElementalOrbit'; import EarthenHarmony from '../restoration/modules/talents/EarthenHarmony'; @@ -35,7 +34,6 @@ import SpenderWindow from './modules/features/SpenderWindow'; import MaelstromTracker from './modules/resources/MaelstromTracker'; import MaelstromDetails from './modules/resources/MaelstromDetails'; import MaelstromGraph from './modules/resources/MaelstromGraph'; -import SkybreakersFieryDemise from './modules/talents/SkybreakersFieryDemise'; import { StormbringerTab } from '../shared/hero/stormbringer/StormbringerTab'; import Tempest from '../shared/hero/stormbringer/Tempest'; import StormbringerEventOrderNormalizer from '../shared/hero/stormbringer/normalizers/StormbringerEventOrderNormalizer'; @@ -78,8 +76,6 @@ class CombatLogParser extends CoreCombatLogParser { stormkeeper: Stormkeeper, ascendance: Ascendance, manaSpring: ManaSpring, - skybreakersFieryDemise: SkybreakersFieryDemise, - flashOfLightning: FlashOfLightning, // hero talents stormbringerTab: StormbringerTab, diff --git a/src/analysis/retail/shaman/elemental/guide/FlameShockSubSection.tsx b/src/analysis/retail/shaman/elemental/guide/FlameShockSubSection.tsx index 4f348749202..82851577181 100644 --- a/src/analysis/retail/shaman/elemental/guide/FlameShockSubSection.tsx +++ b/src/analysis/retail/shaman/elemental/guide/FlameShockSubSection.tsx @@ -48,7 +48,7 @@ export const FlameShockSubSection = ({ {formatPercentage((modules.flameShock as FlameShock).uptime)}% uptime
{ @@ -59,7 +59,7 @@ export const FlameShockSubSection = ({ diff --git a/src/analysis/retail/shaman/elemental/modules/Abilities.tsx b/src/analysis/retail/shaman/elemental/modules/Abilities.tsx index 4ceb8d592bc..4dac1e919e4 100644 --- a/src/analysis/retail/shaman/elemental/modules/Abilities.tsx +++ b/src/analysis/retail/shaman/elemental/modules/Abilities.tsx @@ -71,7 +71,7 @@ class Abilities extends ClassAbilities { spell: TALENTS.ASCENDANCE_ELEMENTAL_TALENT.id, enabled: combatant.hasTalent(TALENTS.ASCENDANCE_ELEMENTAL_TALENT), category: SPELL_CATEGORY.COOLDOWNS, - cooldown: 180, + cooldown: 180 - (combatant.hasTalent(TALENTS.FIRST_ASCENDANT_TALENT) ? 60 : 0), gcd: { base: 1500, }, @@ -143,6 +143,14 @@ class Abilities extends ClassAbilities { static: 0, }, }, + { + spell: SPELLS.TEMPEST_CAST.id, + enabled: combatant.hasTalent(TALENTS.TEMPEST_TALENT), + category: SPELL_CATEGORY.ROTATIONAL, + gcd: { + base: 1500, + }, + }, ]; } } diff --git a/src/analysis/retail/shaman/elemental/modules/talents/Ascendance.tsx b/src/analysis/retail/shaman/elemental/modules/talents/Ascendance.tsx index 0e187546cc5..58ede39c0ef 100644 --- a/src/analysis/retail/shaman/elemental/modules/talents/Ascendance.tsx +++ b/src/analysis/retail/shaman/elemental/modules/talents/Ascendance.tsx @@ -3,7 +3,6 @@ import TALENTS from 'common/TALENTS/shaman'; import { Options, SELECTED_PLAYER } from 'parser/core/Analyzer'; import { ThresholdStyle, When } from 'parser/core/ParseResults'; import Enemies from 'parser/shared/modules/Enemies'; -import BoringSpellValueText from 'parser/ui/BoringSpellValueText'; import Statistic from 'parser/ui/Statistic'; import STATISTIC_CATEGORY from 'parser/ui/STATISTIC_CATEGORY'; import { STATISTIC_ORDER } from 'parser/ui/StatisticBox'; @@ -20,7 +19,10 @@ import Events, { RefreshBuffEvent, } from 'parser/core/Events'; import SpellUsable from 'parser/shared/modules/SpellUsable'; -import { QualitativePerformance } from 'parser/ui/QualitativePerformance'; +import { + evaluateQualitativePerformanceByThreshold, + QualitativePerformance, +} from 'parser/ui/QualitativePerformance'; import MajorCooldown, { CooldownTrigger } from 'parser/core/MajorCooldowns/MajorCooldown'; import CooldownUsage from 'parser/core/MajorCooldowns/CooldownUsage'; import { ChecklistUsageInfo, SpellUse } from 'parser/core/SpellUsage/core'; @@ -30,6 +32,12 @@ import EmbeddedTimelineContainer, { SpellTimeline, } from 'interface/report/Results/Timeline/EmbeddedTimeline'; import Casts from 'interface/report/Results/Timeline/Casts'; +import TalentSpellText from 'parser/ui/TalentSpellText'; +import Uptime from 'interface/icons/Uptime'; +import MaelstromTracker from '../resources/MaelstromTracker'; +import ResourceLink from 'interface/ResourceLink'; +import RESOURCE_TYPES from 'game/RESOURCE_TYPES'; +import Spell from 'common/SPELLS/Spell'; interface AscendanceTimeline { start: number; @@ -41,6 +49,17 @@ interface AscendanceTimeline { interface AscendanceCooldownCast extends CooldownTrigger { timeline: AscendanceTimeline; + endingMaelstrom: number; +} + +interface SpenderCasts { + count: number; + noProcBeforeEnd?: boolean | undefined; +} + +interface Spender { + spell: Spell & { maelstromCost: number }; + costReduction: number; } const maelstromSpenders: number[] = [ @@ -56,14 +75,22 @@ class Ascendance extends MajorCooldown { abilities: Abilities, enemies: Enemies, spellUsable: SpellUsable, + maelstromTracker: MaelstromTracker, }; - protected currentCooldown: AscendanceCooldownCast | null = null; - globalCooldownEnds: number = 0; - protected abilities!: Abilities; protected enemies!: Enemies; protected spellUsable!: SpellUsable; + protected maelstromTracker!: MaelstromTracker; + + protected castsBeforeAscendanceProc: SpenderCasts[] = []; + protected currentCooldown: AscendanceCooldownCast | null = null; + protected globalCooldownEnds: number = 0; + protected ascendanceWasCast: boolean = false; + protected spender: Spender = { + spell: TALENTS.EARTH_SHOCK_TALENT, + costReduction: this.selectedCombatant.hasTalent(TALENTS.EYE_OF_THE_STORM_TALENT) ? 5 : 0, + }; constructor(options: Options) { super({ spell: TALENTS.ASCENDANCE_ELEMENTAL_TALENT }, options); @@ -71,54 +98,111 @@ class Ascendance extends MajorCooldown { this.selectedCombatant.hasTalent(TALENTS.ASCENDANCE_ELEMENTAL_TALENT) || this.selectedCombatant.hasTalent(TALENTS.DEEPLY_ROOTED_ELEMENTS_TALENT); + if (this.selectedCombatant.hasTalent(TALENTS.ELEMENTAL_BLAST_ELEMENTAL_TALENT)) { + this.spender.spell = TALENTS.ELEMENTAL_BLAST_ELEMENTAL_TALENT; + this.spender.costReduction = this.selectedCombatant.hasTalent(TALENTS.EYE_OF_THE_STORM_TALENT) + ? 10 + : 0; + } + if (!this.active) { return; } - this.addEventListener(Events.any, this.onCast); - this.addEventListener( - Events.applybuff.by(SELECTED_PLAYER).spell(TALENTS.ASCENDANCE_ELEMENTAL_TALENT), - this.onApplyAscendance, - ); this.addEventListener( - Events.refreshbuff.by(SELECTED_PLAYER).spell(TALENTS.ASCENDANCE_ELEMENTAL_TALENT), + Events.cast.by(SELECTED_PLAYER).spell(TALENTS.ASCENDANCE_ELEMENTAL_TALENT), this.onApplyAscendance, ); + if (this.selectedCombatant.hasTalent(TALENTS.DEEPLY_ROOTED_ELEMENTS_TALENT)) { + this.addEventListener( + Events.applybuff.by(SELECTED_PLAYER).spell(TALENTS.ASCENDANCE_ELEMENTAL_TALENT), + this.onApplyAscendance, + ); + this.addEventListener( + Events.refreshbuff.by(SELECTED_PLAYER).spell(TALENTS.ASCENDANCE_ELEMENTAL_TALENT), + this.onApplyAscendance, + ); + } this.addEventListener( Events.removebuff.by(SELECTED_PLAYER).spell(TALENTS.ASCENDANCE_ELEMENTAL_TALENT), this.onAscendanceEnd, ); - this.addEventListener(Events.fightend, this.onAscendanceEnd); - this.addEventListener(Events.GlobalCooldown.by(SELECTED_PLAYER), this.onGlobalCooldown); + this.addEventListener(Events.fightend, this.onFightEnd); + + this.addEventListener(Events.any.by(SELECTED_PLAYER), this.onCast); + + if (this.selectedCombatant.hasTalent(TALENTS.DEEPLY_ROOTED_ELEMENTS_TALENT)) { + this.addEventListener( + Events.cast + .by(SELECTED_PLAYER) + .spell([ + TALENTS.EARTH_SHOCK_TALENT, + TALENTS.ELEMENTAL_BLAST_ELEMENTAL_TALENT, + TALENTS.EARTHQUAKE_1_ELEMENTAL_TALENT, + TALENTS.EARTHQUAKE_2_ELEMENTAL_TALENT, + ]), + this.onProcEligibleCast, + ); + } } onGlobalCooldown(event: GlobalCooldownEvent) { this.globalCooldownEnds = event.duration + event.timestamp; } - onApplyAscendance(event: ApplyBuffEvent | RefreshBuffEvent) { - this.currentCooldown ??= { - event: event, - timeline: { - start: Math.max(event.timestamp, this.globalCooldownEnds), - events: [], - }, - }; + onApplyAscendance(event: CastEvent | ApplyBuffEvent | RefreshBuffEvent) { + if (event.type === EventType.Cast) { + this.ascendanceWasCast = true; + } else if (this.ascendanceWasCast && this.currentCooldown) { + this.ascendanceWasCast = false; + return; + } + + if (!this.ascendanceWasCast && event.type !== EventType.Cast) { + this.castsBeforeAscendanceProc.push({ count: 0 }); + } + + if (!this.currentCooldown) { + this.currentCooldown = { + event: event, + timeline: { + start: Math.max(event.timestamp, this.globalCooldownEnds), + events: [], + }, + endingMaelstrom: this.maelstromTracker.current, + }; + } } onAscendanceEnd(event: AnyEvent | FightEndEvent) { if (this.currentCooldown) { this.currentCooldown.timeline.end = event.timestamp; + // this.currentCooldown.endingMaelstrom = this.maelstromTracker.current; this.recordCooldown(this.currentCooldown); this.currentCooldown = null; } } + onFightEnd(event: FightEndEvent) { + const cast = this.castsBeforeAscendanceProc.at(-1); + if (cast) { + cast.noProcBeforeEnd = true; + } + this.onAscendanceEnd(event); + } + + onProcEligibleCast(event: CastEvent) { + this.castsBeforeAscendanceProc.at(-1)!.count += 1; + } + onCast(event: AnyEvent) { if (this.currentCooldown) { if (event.type === EventType.Cast && !event.globalCooldown) { return; } + if (event.type === EventType.Cast) { + this.currentCooldown.endingMaelstrom = this.maelstromTracker.current; + } this.currentCooldown.timeline.events.push(event); } } @@ -136,10 +220,6 @@ class Ascendance extends MajorCooldown { ) as BeginCastEvent[]; } - get averageLavaBurstCasts() { - return this.spellCasts.length / this.casts.length; - } - get suggestionThresholds() { const otherCasts = this.spellCasts.filter( (e) => ![TALENTS.LAVA_BURST_TALENT.id, ...maelstromSpenders].includes(e.ability.guid), @@ -154,66 +234,69 @@ class Ascendance extends MajorCooldown { } statistic() { - const hasEB = this.selectedCombatant.hasTalent(TALENTS.ELEMENTAL_BLAST_ELEMENTAL_TALENT); - const totalCasts = this.spellCasts; - - return ( - - With a uptime of: {formatPercentage(this.AscendanceUptime)} %
- Casts while Ascendance was up: -
    -
  • - Lava Burst:{' '} - {totalCasts.filter((e) => e.ability.guid === TALENTS.LAVA_BURST_TALENT.id).length} -
  • - {(hasEB && ( -
  • - Elemental Blast:{' '} - { - totalCasts.filter( - (e) => e.ability.guid === TALENTS.ELEMENTAL_BLAST_ELEMENTAL_TALENT.id, - ).length - } -
  • - )) || ( -
  • - Earth Shock:{' '} - { - totalCasts.filter((e) => e.ability.guid === TALENTS.EARTH_SHOCK_TALENT.id) - .length - } -
  • - )} -
  • - Other Spells:{' '} - { - totalCasts.filter( - (e) => - ![ - TALENTS.LAVA_BURST_TALENT.id, - TALENTS.ELEMENTAL_BLAST_ELEMENTAL_TALENT.id, - TALENTS.EARTH_SHOCK_TALENT.id, - ].includes(e.ability.guid), - ).length - } -
  • -
- - } - > - - <> - On average {formatNumber(this.averageLavaBurstCasts)} Lava Bursts cast during - Ascendance. - - -
- ); + if (this.selectedCombatant.hasTalent(TALENTS.DEEPLY_ROOTED_ELEMENTS_TALENT)) { + // don't include casts that didn't lead to a proc in casts per proc statistic + const castsBeforeAscendanceProc = this.castsBeforeAscendanceProc + .filter((cast: SpenderCasts) => !cast.noProcBeforeEnd && cast.count > 0) + .map((cast: SpenderCasts) => cast.count); + const minToProc = Math.min(...castsBeforeAscendanceProc); + const maxToProc = Math.max(...castsBeforeAscendanceProc); + const median = getMedian(castsBeforeAscendanceProc)!; + // do include them in overall casts to get the expected procs based on simulation results + const totalCasts = this.castsBeforeAscendanceProc.reduce( + (total, current: SpenderCasts) => (total += current.count), + 0, + ); + return ( + 0 ? ( + <> +
    +
  • Min casts before proc: {minToProc}
  • +
  • Max casts before proc: {maxToProc}
  • +
  • Total casts: {totalCasts}
  • +
+ + ) : null + } + > + + {castsBeforeAscendanceProc.length > 0 ? ( + <> +
+ {formatNumber(median)} casts per proc +
+
+ {formatNumber( + this.castsBeforeAscendanceProc.filter((c) => !c.noProcBeforeEnd).length, + )}{' '} + + procs + +
+
+ {' '} + {formatPercentage( + this.selectedCombatant.getBuffUptime(TALENTS.ASCENDANCE_ELEMENTAL_TALENT.id) / + this.owner.fightDuration, + 2, + )} + % uptime +
+ + ) : ( + <> +
No procs after {totalCasts} casts.
+ + )} +
+
+ ); + } } description(): ReactNode { @@ -223,7 +306,7 @@ class Ascendance extends MajorCooldown { {' '} - is a powerful cooldown + analysis is a work in progress. Additional details will be added at a later date.

); @@ -231,8 +314,8 @@ class Ascendance extends MajorCooldown { explainPerformance(cast: AscendanceCooldownCast): SpellUse { const checklistItems: ChecklistUsageInfo[] = [ - this.explainLavaBurstPerformance(cast), this.explainSpenderPerformance(cast), + this.explainEndingMaelstrom(cast), ]; const timeline = ( @@ -266,48 +349,52 @@ class Ascendance extends MajorCooldown { }; } - explainSpenderPerformance(cast: AscendanceCooldownCast) { - const spendersCast = cast.timeline.events.filter( - (e) => e.type === EventType.BeginCast && maelstromSpenders.includes(e.ability.guid), - ).length; + get spenderCost() { + return this.spender.spell.maelstromCost - this.spender.costReduction; + } + + explainEndingMaelstrom(cast: AscendanceCooldownCast): ChecklistUsageInfo { return { - check: 'spender-casts', + check: 'ending-maelstrom', timestamp: cast.event.timestamp, - performance: spendersCast === 0 ? QualitativePerformance.Perfect : QualitativePerformance.Ok, - summary: <>Maelstrom spenders cast: {spendersCast}, + performance: evaluateQualitativePerformanceByThreshold({ + actual: cast.endingMaelstrom, + isLessThan: { + perfect: this.spenderCost, + ok: this.spenderCost + 1, + }, + }), + summary: ( + <> + Ended with {cast.endingMaelstrom} + + ), details: (
- You cast {spendersCast} maelstrom spender(s) during ascendance.{' '} - {spendersCast !== 0 && ( + You ended with{' '} + {cast.endingMaelstrom} .{' '} + {cast.endingMaelstrom > this.spenderCost ? ( <> - Try to minimise the number of non- - spells cast during{' '} - + You could have cast {Math.floor(cast.endingMaelstrom / this.spenderCost)} more{' '} + + 's - )} + ) : null}
), }; } - explainLavaBurstPerformance(cast: AscendanceCooldownCast) { - const lvbCasts = cast.timeline.events.filter( - (e) => e.type === EventType.BeginCast && e.ability.guid === TALENTS.LAVA_BURST_TALENT.id, + explainSpenderPerformance(cast: AscendanceCooldownCast) { + const spendersCast = cast.timeline.events.filter( + (e) => e.type === EventType.BeginCast && maelstromSpenders.includes(e.ability.guid), ).length; return { - check: 'lvb-casts', + check: 'spender-casts', timestamp: cast.event.timestamp, - performance: lvbCasts > 0 ? QualitativePerformance.Perfect : QualitativePerformance.Ok, - summary: ( - <> - casts: {lvbCasts} - - ), - details: ( -
- You cast {lvbCasts} during ascendance. -
- ), + performance: QualitativePerformance.Perfect, + summary: <>Maelstrom spenders cast: {spendersCast}, + details:
You cast {spendersCast} maelstrom spender(s) during ascendance.
, }; } @@ -336,4 +423,17 @@ class Ascendance extends MajorCooldown { } } +function getMedian(values: number[]): number | undefined { + if (values.length > 0) { + values.sort(function (a, b) { + return a - b; + }); + const half = Math.floor(values.length / 2); + if (values.length % 2) { + return values[half]; + } + return (values[half - 1] + values[half]) / 2.0; + } +} + export default Ascendance; diff --git a/src/analysis/retail/shaman/elemental/modules/talents/FlashOfLightning.tsx b/src/analysis/retail/shaman/elemental/modules/talents/FlashOfLightning.tsx deleted file mode 100644 index ae356d4f000..00000000000 --- a/src/analysis/retail/shaman/elemental/modules/talents/FlashOfLightning.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import TALENTS from 'common/TALENTS/shaman'; -import SPELLS from 'common/SPELLS/shaman'; -import Analyzer, { Options, SELECTED_PLAYER } from 'parser/core/Analyzer'; -import Events, { CastEvent } from 'parser/core/Events'; -import SpellUsable from 'parser/shared/modules/SpellUsable'; -import Statistic from 'parser/ui/Statistic'; -import STATISTIC_ORDER from 'parser/ui/STATISTIC_ORDER'; -import SpellLink from 'interface/SpellLink'; -import { formatNumber } from 'common/format'; -import TalentSpellText from 'parser/ui/TalentSpellText'; -import { UptimeIcon } from 'interface/icons'; - -const FLASH_OF_LIGHTING_AFFECTED_SPELL_IDS: number[] = [ - TALENTS.ANCESTRAL_GUIDANCE_TALENT.id, - TALENTS.ASTRAL_SHIFT_TALENT.id, - SPELLS.ANCESTRAL_SWIFTNESS_CAST.id, - SPELLS.BLOODLUST.id, - SPELLS.HEROISM.id, - TALENTS.CAPACITOR_TOTEM_TALENT.id, - TALENTS.CLEANSE_SPIRIT_TALENT.id, - SPELLS.EARTHBIND_TOTEM.id, - TALENTS.EARTHGRAB_TOTEM_TALENT.id, - TALENTS.EARTH_ELEMENTAL_TALENT.id, - SPELLS.FLAME_SHOCK.id, - TALENTS.GREATER_PURGE_TALENT.id, - TALENTS.GUST_OF_WIND_TALENT.id, - TALENTS.HEALING_STREAM_TOTEM_SHARED_TALENT.id, - TALENTS.HEX_TALENT.id, - TALENTS.LIGHTNING_LASSO_TALENT.id, - TALENTS.NATURES_SWIFTNESS_TALENT.id, - TALENTS.POISON_CLEANSING_TOTEM_TALENT.id, - TALENTS.PRIMORDIAL_WAVE_SPEC_TALENT.id, - SPELLS.REINCARNATION.id, - TALENTS.SPIRITWALKERS_GRACE_TALENT.id, - TALENTS.STONE_BULWARK_TOTEM_TALENT.id, - TALENTS.STORM_ELEMENTAL_TALENT.id, - SPELLS.STORMKEEPER_BUFF_AND_CAST.id, - TALENTS.THUNDERSTORM_TALENT.id, - TALENTS.TOTEMIC_PROJECTION_TALENT.id, - TALENTS.TOTEMIC_RECALL_TALENT.id, - TALENTS.TREMOR_TOTEM_TALENT.id, - TALENTS.WIND_RUSH_TOTEM_TALENT.id, - TALENTS.WIND_SHEAR_TALENT.id, -]; - -class FlashOfLightning extends Analyzer.withDependencies({ spellUsable: SpellUsable }) { - private readonly reducedCooldowns = new Map(); - - constructor(options: Options) { - super(options); - - this.active = this.selectedCombatant.hasTalent(TALENTS.FLASH_OF_LIGHTNING_TALENT); - if (!this.active) { - return; - } - - this.addEventListener( - Events.cast - .by(SELECTED_PLAYER) - .spell([SPELLS.LIGHTNING_BOLT, TALENTS.CHAIN_LIGHTNING_TALENT]), - this.onCast, - ); - } - - onCast(event: CastEvent) { - FLASH_OF_LIGHTING_AFFECTED_SPELL_IDS.forEach((spellId) => { - if (this.deps.spellUsable.isOnCooldown(spellId)) { - const cdr = this.deps.spellUsable.reduceCooldown(spellId, 1000, event.timestamp); - this.reducedCooldowns.set(spellId, (this.reducedCooldowns.get(spellId) ?? 0) + cdr); - } - }); - } - - statistic() { - return ( - - - - - - - - - - {Array.from(this.reducedCooldowns.entries()).map(([spellId, cdr]) => { - return ( - - - - - ); - })} - -
AbilityCooldown Reduction (sec)
- - - - {formatNumber(cdr / 1000)}
- - } - > - - <> -

- {' '} - {formatNumber( - Array.from(this.reducedCooldowns.values()).reduce( - (total, value) => (total += value), - 0, - ) / 1000, - )}{' '} - sec
- total effective reduction -

- -
-
- ); - } -} - -export default FlashOfLightning; diff --git a/src/analysis/retail/shaman/elemental/modules/talents/SkybreakersFieryDemise.tsx b/src/analysis/retail/shaman/elemental/modules/talents/SkybreakersFieryDemise.tsx deleted file mode 100644 index e25f8009f03..00000000000 --- a/src/analysis/retail/shaman/elemental/modules/talents/SkybreakersFieryDemise.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import TALENTS, { TALENTS_SHAMAN } from 'common/TALENTS/shaman'; -import SPELLS from 'common/SPELLS/shaman'; -import Analyzer, { Options, SELECTED_PLAYER } from 'parser/core/Analyzer'; -import Events, { DamageEvent } from 'parser/core/Events'; -import SpellUsable from 'parser/shared/modules/SpellUsable'; -import HIT_TYPES from 'game/HIT_TYPES'; -import Statistic from 'parser/ui/Statistic'; -import STATISTIC_ORDER from 'parser/ui/STATISTIC_ORDER'; -import STATISTIC_CATEGORY from 'parser/ui/STATISTIC_CATEGORY'; -import { formatNumber, formatPercentage } from 'common/format'; -import TalentSpellText from 'parser/ui/TalentSpellText'; -import UptimeIcon from 'interface/icons/Uptime'; - -class SkybreakersFieryDemise extends Analyzer { - static dependencies = { - spellUsable: SpellUsable, - }; - - private readonly spellUsable!: SpellUsable; - private effectiveCdr: number = 0; - private wastedCdr: number = 0; - private readonly elementalSpellId: number = 0; - - constructor(options: Options) { - super(options); - this.active = this.selectedCombatant.hasTalent(TALENTS.SKYBREAKERS_FIERY_DEMISE_TALENT); - - if (!this.active) { - return; - } - - this.elementalSpellId = this.selectedCombatant.hasTalent(TALENTS.STORM_ELEMENTAL_TALENT) - ? TALENTS.STORM_ELEMENTAL_TALENT.id - : TALENTS.FIRE_ELEMENTAL_TALENT.id; - this.addEventListener( - Events.damage.by(SELECTED_PLAYER).spell(SPELLS.FLAME_SHOCK), - this.onFlameShockDamage, - ); - } - - onFlameShockDamage(event: DamageEvent) { - if (event.tick && event.hitType === HIT_TYPES.CRIT) { - if (this.spellUsable.isOnCooldown(this.elementalSpellId)) { - const cdr = this.spellUsable.reduceCooldown(this.elementalSpellId, 1000, event.timestamp); - this.effectiveCdr += cdr; - this.wastedCdr += 1000 - cdr; - } else { - this.wastedCdr += 1000; - } - } - } - - get reduction() { - return (this.wastedCdr + this.effectiveCdr) / 1000; - } - - statistic() { - return ( - - - <> -

- {formatNumber(this.effectiveCdr / 1000)} sec
- total effective reduction -

-

- {formatNumber(this.wastedCdr / 1000)} sec ( - {formatPercentage(this.wastedCdr / 1000 / this.reduction)}%)
- wasted reduction. -

- -
-
- ); - } -} - -export default SkybreakersFieryDemise; diff --git a/src/analysis/retail/shaman/elemental/modules/talents/StormElemental.tsx b/src/analysis/retail/shaman/elemental/modules/talents/StormElemental.tsx index 570b3ad1288..7bae42d929d 100644 --- a/src/analysis/retail/shaman/elemental/modules/talents/StormElemental.tsx +++ b/src/analysis/retail/shaman/elemental/modules/talents/StormElemental.tsx @@ -1,32 +1,35 @@ -import { defineMessage } from '@lingui/macro'; -import { formatPercentage } from 'common/format'; +import { formatNumber, formatPercentage } from 'common/format'; import SPELLS from 'common/SPELLS'; import TALENTS from 'common/TALENTS/shaman'; import { SpellLink } from 'interface'; import Analyzer, { Options, SELECTED_PLAYER } from 'parser/core/Analyzer'; import Events, { CastEvent } from 'parser/core/Events'; -import { ThresholdStyle, When } from 'parser/core/ParseResults'; import Enemies from 'parser/shared/modules/Enemies'; import Statistic from 'parser/ui/Statistic'; import Abilities from '../Abilities'; +import { UptimeIcon } from 'interface/icons'; +import TalentSpellText from 'parser/ui/TalentSpellText'; +import typedKeys from 'common/typedKeys'; +import { maybeGetTalentOrSpell } from 'common/maybeGetTalentOrSpell'; class StormElemental extends Analyzer { static dependencies = { abilities: Abilities, enemies: Enemies, }; + stormElementalEndTime: number = 0; badFS = 0; - justEnteredSE = false; checkDelay = 0; - numCasts = { + numCasts: Record = { [TALENTS.STORM_ELEMENTAL_TALENT.id]: 0, [SPELLS.LIGHTNING_BOLT.id]: 0, [TALENTS.CHAIN_LIGHTNING_TALENT.id]: 0, [TALENTS.EARTH_SHOCK_TALENT.id]: 0, [TALENTS.EARTHQUAKE_1_ELEMENTAL_TALENT.id]: 0, [TALENTS.EARTHQUAKE_2_ELEMENTAL_TALENT.id]: 0, - others: 0, + [SPELLS.TEMPEST_CAST.id]: 0, + [TALENTS.LAVA_BURST_TALENT.id]: 0, }; protected enemies!: Enemies; protected abilities!: Abilities; @@ -41,7 +44,7 @@ class StormElemental extends Analyzer { Events.cast.by(SELECTED_PLAYER).spell(TALENTS.STORM_ELEMENTAL_TALENT), this.onSECast, ); - this.addEventListener(Events.cast.by(SELECTED_PLAYER), this.onSECast); + this.addEventListener(Events.cast.by(SELECTED_PLAYER), this.onCast); } get stormEleUptime() { @@ -64,74 +67,59 @@ class StormElemental extends Analyzer { ); } - get suggestionThresholds() { - return { - actual: this.numCasts.others, - isGreaterThan: { - major: 1, - }, - style: ThresholdStyle.NUMBER, - }; - } - onSECast(event: CastEvent) { - this.justEnteredSE = true; this.numCasts[TALENTS.STORM_ELEMENTAL_TALENT.id] += 1; + this.stormElementalEndTime = + event.timestamp + + 20000 * + (1 + (this.selectedCombatant.hasTalent(TALENTS.EVERLASTING_ELEMENTS_TALENT) ? 0.2 : 0)); } onCast(event: CastEvent) { - const spellId = event.ability.guid; - - if (this.numCasts[spellId] !== undefined) { - this.numCasts[spellId] += 1; - } else { - this.numCasts.others += 1; + if (event.timestamp <= this.stormElementalEndTime) { + const spellId = event.ability.guid; + if (this.numCasts[spellId] !== undefined) { + this.numCasts[spellId] += 1; + } } } statistic() { return ( With a uptime of: {formatPercentage(this.stormEleUptime)} %
Casts while Storm Elemental was up:
    -
  • Earth Shock: {this.numCasts[TALENTS.EARTH_SHOCK_TALENT.id]}
  • -
  • Lightning Bolt: {this.numCasts[SPELLS.LIGHTNING_BOLT.id]}
  • -
  • - Earthquake:{' '} - {this.numCasts[TALENTS.EARTHQUAKE_1_ELEMENTAL_TALENT.id] + - this.numCasts[TALENTS.EARTHQUAKE_2_ELEMENTAL_TALENT.id]} -
  • -
  • Chain Lightning: {this.numCasts[TALENTS.CHAIN_LIGHTNING_TALENT.id]}
  • -
  • Other Spells: {this.numCasts.others}
  • + {typedKeys(this.numCasts) + .filter((spellId) => this.numCasts[spellId] > 0) + .map((spellId) => { + const ability = maybeGetTalentOrSpell(spellId)!; + return ( +
  • + : {this.numCasts[spellId]} +
  • + ); + })}
} > - <> - You cast {this.averageLightningBoltCasts}{' '} - times per - + +
+ {formatNumber(this.averageLightningBoltCasts)}{' '} + +
+ + average casts per + +
+
); } - - suggestions(when: When) { - const abilities = `Lightning Bolt/Chain Lightning and Earth Shock/Earthquake`; - when(this.suggestionThresholds).addSuggestion((suggest, actual, recommended) => - suggest(Maximize your damage during Storm Elemental by only using {abilities}.) - .icon(TALENTS.STORM_ELEMENTAL_TALENT.icon) - .actual( - defineMessage({ - id: 'shaman.elemental.suggestions.stormElemental.badCasts', - message: `${actual} other casts with Storm Elemental up`, - }), - ) - .recommended(`Only cast ${abilities} while Storm Elemental is up.`), - ); - } } export default StormElemental; diff --git a/src/analysis/retail/shaman/elemental/modules/talents/Stormkeeper.tsx b/src/analysis/retail/shaman/elemental/modules/talents/Stormkeeper.tsx index a4dc1b1a85e..38c58b6e439 100644 --- a/src/analysis/retail/shaman/elemental/modules/talents/Stormkeeper.tsx +++ b/src/analysis/retail/shaman/elemental/modules/talents/Stormkeeper.tsx @@ -79,6 +79,11 @@ interface StormkeeperCast extends CooldownTrigger { base: 8, overload: 3, }, - }; + iceFury: { + base: 12, + overload: 4, + }, + } satisfies Record; constructor(options: Options) { super({ spell: TALENTS.STORMKEEPER_TALENT }, options); @@ -144,11 +153,6 @@ class Stormkeeper extends MajorCooldown { return; } - if (this.selectedCombatant.hasTalent(TALENTS.FLOW_OF_POWER_TALENT)) { - this.maelstromGeneration.lavaBurst.base += 2; - this.maelstromGeneration.lightningBolt.base += 2; - } - [Events.begincast, Events.applybuff].forEach((filter) => this.addEventListener( filter.by(SELECTED_PLAYER).spell(SPELLS.STORMKEEPER_BUFF_AND_CAST), @@ -340,13 +344,14 @@ class Stormkeeper extends MajorCooldown { , ); } else { - addInefficientCastReason( - event, - <> - should only be cast to consume{' '} - - , - ); + false && + addInefficientCastReason( + event, + <> + should only be cast to consume{' '} + + , + ); } } } @@ -640,8 +645,10 @@ class Stormkeeper extends MajorCooldown { ); } - guideSubsection(): JSX.Element { - return ; + guideSubsection() { + return this.selectedCombatant.hasTalent(TALENTS.SURGE_OF_POWER_TALENT) ? ( + + ) : null; } } diff --git a/src/analysis/retail/shaman/enhancement/CHANGELOG.tsx b/src/analysis/retail/shaman/enhancement/CHANGELOG.tsx index b47cd02d05b..2a73d555d5d 100644 --- a/src/analysis/retail/shaman/enhancement/CHANGELOG.tsx +++ b/src/analysis/retail/shaman/enhancement/CHANGELOG.tsx @@ -6,6 +6,7 @@ import SpellLink from 'interface/SpellLink'; // prettier-ignore export default [ + change(date(2024, 11, 13), <>Updating to 11.0.5, Seriousnes), change(date(2024, 10, 8), <>Fixing prepull casts of and non-zero starting , Seriousnes), change(date(2024, 10, 7), <>Fixing error when no is generated by either component of , Seriousnes), change(date(2024, 10, 3), <>Updating , , and guide sections, updating guide., Seriousnes), diff --git a/src/analysis/retail/shaman/enhancement/CONFIG.tsx b/src/analysis/retail/shaman/enhancement/CONFIG.tsx index 5497333ee63..d67a8ec4a9d 100644 --- a/src/analysis/retail/shaman/enhancement/CONFIG.tsx +++ b/src/analysis/retail/shaman/enhancement/CONFIG.tsx @@ -9,12 +9,12 @@ import CHANGELOG from './CHANGELOG'; const config: Config = { contributors: [Seriousnes], branch: GameBranch.Retail, - patchCompatibility: '11.0.2', + patchCompatibility: '11.0.5', supportLevel: SupportLevel.MaintainedFull, description: ( <> - Analytics are being developed for a level 70 dragonflight character on beta. Right now the + Analytics are being developed for a level 80 The War Within character on beta. Right now the Enhancement Analyzer is a work-in-progress, and only holds very basic functionality.
@@ -23,7 +23,8 @@ const config: Config = { the GitHub repo. ), - exampleReport: '/report/DCyQGgBxP3R8MaN9/28-Heroic+Smolderon+-+Kill+(3:46)/Seriousnes/standard', + exampleReport: + "/report/fPAk1jwMBC6Ym4aV/33-Mythic+Nexus-Princess+Ky'veza+-+Kill+(6:17)/Seriousnes/standard", spec: SPECS.ENHANCEMENT_SHAMAN, changelog: CHANGELOG, parser: () => diff --git a/src/analysis/retail/shaman/enhancement/CombatLogParser.tsx b/src/analysis/retail/shaman/enhancement/CombatLogParser.tsx index e0d2557940c..a426c20959a 100644 --- a/src/analysis/retail/shaman/enhancement/CombatLogParser.tsx +++ b/src/analysis/retail/shaman/enhancement/CombatLogParser.tsx @@ -22,7 +22,7 @@ import NaturesGuardian from './modules/talents/NaturesGuardian'; import Sundering from './modules/talents/Sundering'; import ElementalSpirits from './modules/talents/ElementalSpirits'; import ElementalAssault from './modules/talents/ElementalAssault'; -import Stormbringer from './modules/spells/Stormbringer'; +import Stormsurge from './modules/spells/Stormsurge'; import FeralSpirit from './modules/talents/FeralSpirit'; import ChainLightning from './modules/talents/ChainLightning'; import AplCheck from './modules/apl/AplCheck'; @@ -111,7 +111,7 @@ class CombatLogParser extends CoreCombatLogParser { elementalSpirits: ElementalSpirits, witchDoctorsAncestry: WitchDoctorsAncestry, feralSpirit: FeralSpirit, - stormbringer: Stormbringer, + stormbringer: Stormsurge, legacyOfTheFrostWitch: LegacyOfTheFrostWitch, thorimsInvocation: ThorimsInvocation, ashenCatalyst: AshenCatalyst, diff --git a/src/analysis/retail/shaman/enhancement/constants.ts b/src/analysis/retail/shaman/enhancement/constants.ts index 971b0761b08..713d2aae29a 100644 --- a/src/analysis/retail/shaman/enhancement/constants.ts +++ b/src/analysis/retail/shaman/enhancement/constants.ts @@ -62,6 +62,7 @@ export enum EnhancementEventLinks { MAELSTROM_SPENDER_LINK = 'maelstrom-spender', LIGHTNING_BOLT_LINK = 'lightning-bolt', MAELSTROM_GENERATOR_LINK = 'maelstrom-generator', + CRASH_LIGHTNING_LINK = 'crash-lightning', } export const GCD_TOLERANCE = 25; diff --git a/src/analysis/retail/shaman/enhancement/modules/Abilities.tsx b/src/analysis/retail/shaman/enhancement/modules/Abilities.tsx index f08262e7192..de9a357d5af 100644 --- a/src/analysis/retail/shaman/enhancement/modules/Abilities.tsx +++ b/src/analysis/retail/shaman/enhancement/modules/Abilities.tsx @@ -62,6 +62,7 @@ class Abilities extends ClassAbilities { spell: TALENTS.STORMSTRIKE_TALENT.id, category: SPELL_CATEGORY.ROTATIONAL, cooldown: (haste) => 7.5 / (1 + haste), + charges: 1 + (this.selectedCombatant.hasTalent(TALENTS.STORMS_WRATH_TALENT) ? 1 : 0), gcd: { base: 1500, }, @@ -83,7 +84,7 @@ class Abilities extends ClassAbilities { base: 1500, }, enabled: combatant.hasTalent(TALENTS.SUNDERING_TALENT), - cooldown: 40, + cooldown: 40 - (this.selectedCombatant.hasTalent(TALENTS.MOLTEN_THUNDER_TALENT) ? 10 : 0), }, { spell: TALENTS.FIRE_NOVA_TALENT.id, @@ -94,13 +95,24 @@ class Abilities extends ClassAbilities { enabled: combatant.hasTalent(TALENTS.FIRE_NOVA_TALENT), cooldown: (haste) => 15 / (1 + haste), }, + /** This version procs on spending maelstrom, has no cooldown, and has a 40 yard range */ { - spell: TALENTS.ICE_STRIKE_TALENT.id, + spell: TALENTS.ICE_STRIKE_1_ENHANCEMENT_TALENT.id, category: SPELL_CATEGORY.ROTATIONAL, gcd: { base: 1500, }, - enabled: combatant.hasTalent(TALENTS.ICE_STRIKE_TALENT), + enabled: combatant.hasTalent(TALENTS.ICE_STRIKE_1_ENHANCEMENT_TALENT), + range: 40, + }, + /** This version has a 15 sec cooldown and is melee range */ + { + spell: TALENTS.ICE_STRIKE_2_ENHANCEMENT_TALENT.id, + category: SPELL_CATEGORY.ROTATIONAL, + gcd: { + base: 1500, + }, + enabled: combatant.hasTalent(TALENTS.ICE_STRIKE_2_ENHANCEMENT_TALENT), cooldown: (haste) => 15 / (1 + haste), range: 5, }, @@ -182,7 +194,13 @@ class Abilities extends ClassAbilities { spell: TALENTS.ASCENDANCE_ENHANCEMENT_TALENT.id, category: SPELL_CATEGORY.COOLDOWNS, enabled: combatant.hasTalent(TALENTS.ASCENDANCE_ENHANCEMENT_TALENT), - cooldown: 180, + cooldown: + 180 - (this.selectedCombatant.hasTalent(TALENTS.THORIMS_INVOCATION_TALENT) ? 60 : 0), + }, + { + spell: SPELLS.VOLTAIC_BLAZE_CAST.id, + category: SPELL_CATEGORY.ROTATIONAL, + enabled: combatant.hasTalent(TALENTS.VOLTAIC_BLAZE_TALENT), }, ]; } diff --git a/src/analysis/retail/shaman/enhancement/modules/Buffs.tsx b/src/analysis/retail/shaman/enhancement/modules/Buffs.tsx index 837efeef57e..898fb98183e 100644 --- a/src/analysis/retail/shaman/enhancement/modules/Buffs.tsx +++ b/src/analysis/retail/shaman/enhancement/modules/Buffs.tsx @@ -14,7 +14,7 @@ class Buffs extends ClassBuffs { timelineHighlight: true, }, { - spellId: SPELLS.STORMBRINGER_BUFF.id, + spellId: SPELLS.STORMSURGE_BUFF.id, timelineHighlight: true, }, { diff --git a/src/analysis/retail/shaman/enhancement/modules/apl/Conditions.tsx b/src/analysis/retail/shaman/enhancement/modules/apl/Conditions.tsx index f6694bce9dd..12af328bf97 100644 --- a/src/analysis/retail/shaman/enhancement/modules/apl/Conditions.tsx +++ b/src/analysis/retail/shaman/enhancement/modules/apl/Conditions.tsx @@ -25,22 +25,13 @@ export function minimumMaelstromWeaponStacks(minStacks: number): Condition ( - <> - available and at least 6 stacks - - ), - ), - }, - { + const rules: Rule[] = []; + + if (combatant.hasTalent(TALENTS.ELEMENTAL_SPIRITS_TALENT)) { + rules.push({ spell: TALENTS.ELEMENTAL_BLAST_ELEMENTAL_TALENT, condition: and( - minimumMaelstromWeaponStacks(6), + minimumMaelstromWeaponStacks(8), repeatableBuffPresent( [ SPELLS.ELEMENTAL_SPIRITS_BUFF_MOLTEN_WEAPON, @@ -50,14 +41,20 @@ export function getSpenderBlock(combatant: Combatant): Rule[] { { atLeast: 4 }, ), ), - }, - { - spell: SPELLS.LIGHTNING_BOLT, - condition: describe(and(AtLeastFiveMSW, not(buffPresent(SPELLS.TEMPEST_BUFF))), () => ( + }); + } + + rules.push({ + spell: SPELLS.LIGHTNING_BOLT, + condition: describe( + and(minimumMaelstromWeaponStacks(8), not(buffPresent(SPELLS.TEMPEST_BUFF))), + () => ( <> - you have at least 5 stacks + you have at least 8 stacks - )), - }, - ]; + ), + ), + }); + + return rules; } diff --git a/src/analysis/retail/shaman/enhancement/modules/apl/Stormbringer.tsx b/src/analysis/retail/shaman/enhancement/modules/apl/Stormbringer.tsx index 5a5f474f27d..5f6f3f85a60 100644 --- a/src/analysis/retail/shaman/enhancement/modules/apl/Stormbringer.tsx +++ b/src/analysis/retail/shaman/enhancement/modules/apl/Stormbringer.tsx @@ -3,19 +3,38 @@ import { Apl, build, Rule } from 'parser/shared/metrics/apl'; import SPELLS from 'common/SPELLS/shaman'; import TALENTS from 'common/TALENTS/shaman'; import SpellLink from 'interface/SpellLink'; -import { and, buffPresent, debuffMissing, describe } from 'parser/shared/metrics/apl/conditions'; -import { MaxStacksMSW, getSpenderBlock } from './Conditions'; +import { + and, + buffPresent, + buffStacks, + debuffMissing, + describe, + not, + or, + spellCharges, +} from 'parser/shared/metrics/apl/conditions'; +import { getSpenderBlock, minimumMaelstromWeaponStacks } from './Conditions'; export function stormbringer(combatant: Combatant): Apl { + const iceStrikeRule = combatant.hasTalent(TALENTS.ICE_STRIKE_1_ENHANCEMENT_TALENT) + ? { + spell: SPELLS.ICE_STRIKE_1_CAST, + condition: buffPresent(SPELLS.ICE_STRIKE_1_USABLE_BUFF), + } + : TALENTS.ICE_STRIKE_2_ENHANCEMENT_TALENT; + const rules: Rule[] = [ - /** Tempest with 10 MSW */ + /** Tempest with 8 MSW */ { spell: SPELLS.TEMPEST_CAST, - condition: describe(and(buffPresent(SPELLS.TEMPEST_BUFF), MaxStacksMSW), () => ( - <> - available and at 10 stacks - - )), + condition: describe( + and(buffPresent(SPELLS.TEMPEST_BUFF), minimumMaelstromWeaponStacks(8)), + () => ( + <> + available and at least 8 stacks + + ), + ), }, /** Windstrike during ascendance */ { @@ -36,30 +55,78 @@ export function stormbringer(combatant: Combatant): Apl { /** For Lava Lash/Hot Hand builds, have higher priority for */ if (combatant.hasTalent(TALENTS.HOT_HAND_TALENT)) { + rules.push({ + spell: TALENTS.LAVA_LASH_TALENT, + condition: or( + buffPresent(SPELLS.HOT_HAND_BUFF), + buffStacks(SPELLS.ASHEN_CATALYST_BUFF, { atLeast: 7 }), + ), + }); + combatant.hasTalent(TALENTS.STORMBLAST_TALENT) && rules.push(TALENTS.STORMSTRIKE_TALENT); rules.push( { - spell: SPELLS.FLAME_SHOCK, - condition: debuffMissing(SPELLS.FLAME_SHOCK), - }, - { - spell: TALENTS.LAVA_LASH_TALENT, - condition: buffPresent(SPELLS.HOT_HAND_BUFF), + spell: SPELLS.VOLTAIC_BLAZE_CAST, + condition: describe(buffPresent(SPELLS.VOLTAIC_BLAZE_BUFF), () => <>, ''), }, - TALENTS.ICE_STRIKE_TALENT, + iceStrikeRule, { spell: TALENTS.FROST_SHOCK_TALENT, condition: buffPresent(SPELLS.HAILSTORM_BUFF), }, TALENTS.LAVA_LASH_TALENT, - TALENTS.STORMSTRIKE_TALENT, ); + !combatant.hasTalent(TALENTS.STORMBLAST_TALENT) && rules.push(TALENTS.STORMSTRIKE_TALENT); + rules.push({ + spell: SPELLS.LIGHTNING_BOLT, + condition: describe( + and(minimumMaelstromWeaponStacks(5), not(buffPresent(SPELLS.TEMPEST_BUFF))), + () => ( + <> + you have at least 5 stacks + + ), + ), + }); } else { + rules.push({ + spell: TALENTS.STORMSTRIKE_TALENT, + condition: spellCharges(TALENTS.STORMSTRIKE_TALENT, { atLeast: 2, atMost: 2 }), + }); + if (combatant.hasTalent(TALENTS.FLOWING_SPIRITS_TALENT)) { + rules.push( + { + spell: SPELLS.VOLTAIC_BLAZE_CAST, + condition: describe(buffPresent(SPELLS.VOLTAIC_BLAZE_BUFF), () => <>, ''), + }, + TALENTS.STORMSTRIKE_TALENT, + ); + } else { + rules.push( + { + spell: SPELLS.VOLTAIC_BLAZE_CAST, + condition: describe(buffPresent(SPELLS.VOLTAIC_BLAZE_BUFF), () => <>, ''), + }, + iceStrikeRule, + TALENTS.STORMSTRIKE_TALENT, + { + spell: TALENTS.FROST_SHOCK_TALENT, + condition: buffPresent(SPELLS.HAILSTORM_BUFF), + }, + TALENTS.CRASH_LIGHTNING_TALENT, + ); + } + rules.push( - TALENTS.STORMSTRIKE_TALENT, - TALENTS.ICE_STRIKE_TALENT, { - spell: TALENTS.FROST_SHOCK_TALENT, - condition: buffPresent(SPELLS.HAILSTORM_BUFF), + spell: SPELLS.LIGHTNING_BOLT, + condition: describe( + and(minimumMaelstromWeaponStacks(5), not(buffPresent(SPELLS.TEMPEST_BUFF))), + () => ( + <> + you have at least 5 stacks + + ), + ), }, { spell: SPELLS.FLAME_SHOCK, @@ -67,7 +134,6 @@ export function stormbringer(combatant: Combatant): Apl { }, TALENTS.LAVA_LASH_TALENT, TALENTS.CRASH_LIGHTNING_TALENT, - SPELLS.FLAME_SHOCK, ); } diff --git a/src/analysis/retail/shaman/enhancement/modules/normalizers/EventLinkNormalizer.tsx b/src/analysis/retail/shaman/enhancement/modules/normalizers/EventLinkNormalizer.tsx index 22ae363dd87..184174a1e4a 100644 --- a/src/analysis/retail/shaman/enhancement/modules/normalizers/EventLinkNormalizer.tsx +++ b/src/analysis/retail/shaman/enhancement/modules/normalizers/EventLinkNormalizer.tsx @@ -27,6 +27,7 @@ const thorimsInvocationCastLink: EventLink = { referencedEventType: [EventType.Damage], forwardBufferMs: EventLinkBuffers.MaelstromWeapon, anyTarget: true, + isActive: (c) => c.hasTalent(TALENTS.THORIMS_INVOCATION_TALENT), }; const stormStrikeLink: EventLink = { linkRelation: EnhancementEventLinks.STORMSTRIKE_LINK, @@ -36,6 +37,7 @@ const stormStrikeLink: EventLink = { referencedEventType: EventType.Damage, forwardBufferMs: EventLinkBuffers.Stormstrike, anyTarget: true, + isActive: (c) => c.hasTalent(TALENTS.STORMFLURRY_TALENT), }; const chainLightningDamageLink: EventLink = { linkRelation: EnhancementEventLinks.CHAIN_LIGHTNING_LINK, @@ -46,6 +48,16 @@ const chainLightningDamageLink: EventLink = { forwardBufferMs: EventLinkBuffers.CAST_DAMAGE_BUFFER, anyTarget: true, }; +const crashLightningDamageLink: EventLink = { + linkRelation: EnhancementEventLinks.CRASH_LIGHTNING_LINK, + linkingEventId: TALENTS.CRASH_LIGHTNING_TALENT.id, + linkingEventType: EventType.Cast, + referencedEventId: TALENTS.CRASH_LIGHTNING_TALENT.id, + referencedEventType: EventType.Damage, + forwardBufferMs: EventLinkBuffers.CAST_DAMAGE_BUFFER, + anyTarget: true, + isActive: (c) => c.hasTalent(TALENTS.UNRELENTING_STORMS_TALENT), +}; const tempestDamageLink: EventLink = { linkRelation: EnhancementEventLinks.TEMPEST_LINK, linkingEventId: SPELLS.TEMPEST_CAST.id, @@ -54,6 +66,7 @@ const tempestDamageLink: EventLink = { referencedEventType: EventType.Damage, forwardBufferMs: EventLinkBuffers.CAST_DAMAGE_BUFFER, anyTarget: true, + isActive: (c) => c.hasTalent(TALENTS.TEMPEST_TALENT), }; const primordialWaveLink: EventLink = { linkRelation: PRIMORDIAL_WAVE_LINK, @@ -65,6 +78,7 @@ const primordialWaveLink: EventLink = { forwardBufferMs: EventLinkBuffers.PrimordialWave, maximumLinks: 1, reverseLinkRelation: PRIMORDIAL_WAVE_LINK, + isActive: (c) => c.hasTalent(TALENTS.PRIMORDIAL_WAVE_SPEC_TALENT), }; const splinteredElements: EventLink = { linkRelation: SPLINTERED_ELEMENTS_LINK, @@ -75,6 +89,7 @@ const splinteredElements: EventLink = { anyTarget: true, forwardBufferMs: EventLinkBuffers.SPLINTERED_ELEMENTS_BUFFER, maximumLinks: 1, + isActive: (c) => c.hasTalent(TALENTS.SPLINTERED_ELEMENTS_TALENT), }; const lightningBoltLink: EventLink = { linkRelation: EnhancementEventLinks.LIGHTNING_BOLT_LINK, @@ -92,6 +107,7 @@ class EventLinkNormalizer extends BaseEventLinkNormalizer { thorimsInvocationCastLink, stormStrikeLink, chainLightningDamageLink, + crashLightningDamageLink, tempestDamageLink, primordialWaveLink, splinteredElements, diff --git a/src/analysis/retail/shaman/enhancement/modules/normalizers/MaelstromWeaponResourceNormalizer.tsx b/src/analysis/retail/shaman/enhancement/modules/normalizers/MaelstromWeaponResourceNormalizer.tsx index d765b3aa96a..c6c01e167b4 100644 --- a/src/analysis/retail/shaman/enhancement/modules/normalizers/MaelstromWeaponResourceNormalizer.tsx +++ b/src/analysis/retail/shaman/enhancement/modules/normalizers/MaelstromWeaponResourceNormalizer.tsx @@ -79,8 +79,7 @@ enum MatchMode { * Maelstrom gained from these sources are capped at the values provided */ const MAXIMUM_MAELSTROM_PER_EVENT = { - [TALENTS.STORM_SWELL_TALENT.id]: 3, - [TALENTS.SUPERCHARGE_TALENT.id]: 3, + [TALENTS.SUPERCHARGE_TALENT.id]: 2, [TALENTS.STATIC_ACCUMULATION_TALENT.id]: 10, }; @@ -557,7 +556,10 @@ class MaelstromWeaponResourceNormalizer extends EventsNormalizer { // if not debugging remove all maelstrom weapon buff events if (!DEBUG) { return events.filter( - (event) => !HasAbility(event) || event.ability.guid !== SPELLS.MAELSTROM_WEAPON_BUFF.id, + (event) => + !HasAbility(event) || + event.ability.guid !== SPELLS.MAELSTROM_WEAPON_BUFF.id || + event.type === EventType.ResourceChange, ); } return events; @@ -846,19 +848,6 @@ const MAELSTROM_ABILITIES = { linkToEventType: SPEND_EVENT_TYPES, searchDirection: SearchDirection.ForwardsFirst, }, - STORM_SWELL: { - spellId: SPELLS.TEMPEST_CAST.id, - enabled: (c) => c.hasTalent(TALENTS.STORM_SWELL_TALENT), - linkFromEventType: EventType.Damage, - spellIdOverride: TALENTS.STORM_SWELL_TALENT.id, - maximum: MAXIMUM_MAELSTROM_PER_EVENT[TALENTS.STORM_SWELL_TALENT.id], - requiresExact: true, - backwardsBufferMs: BufferMs.Cast, - forwardBufferMs: 5, - linkToEventType: GAIN_EVENT_TYPES, - searchDirection: SearchDirection.ForwardsFirst, - matchMode: MatchMode.MatchFirst, - }, SUPERCHARGE: { spellId: [ SPELLS.LIGHTNING_BOLT.id, @@ -907,9 +896,21 @@ const MAELSTROM_ABILITIES = { maximum: 1, requiresExact: true, }, + VOLTAIC_BLAZE: { + spellId: SPELLS.VOLTAIC_BLAZE_CAST.id, // TODO: Add voltaic blaze spell id + linkFromEventType: EventType.Cast, + linkToEventType: GAIN_EVENT_TYPES, + searchDirection: SearchDirection.ForwardsFirst, + maximum: 1, + requiresExact: true, + }, // Swirling maelstrom has higher priority than Elemental Assault, as the later can be a chance if 1 of 2 talents invested SWIRLING_MAELSTROM: { - spellId: [TALENTS.ICE_STRIKE_TALENT.id, TALENTS.FROST_SHOCK_TALENT.id], + spellId: [ + TALENTS.ICE_STRIKE_1_ENHANCEMENT_TALENT.id, + TALENTS.ICE_STRIKE_2_ENHANCEMENT_TALENT.id, + TALENTS.FROST_SHOCK_TALENT.id, + ], enabled: (c) => c.hasTalent(TALENTS.SWIRLING_MAELSTROM_TALENT), linkFromEventType: EventType.Cast, spellIdOverride: TALENTS.SWIRLING_MAELSTROM_TALENT.id, @@ -924,7 +925,8 @@ const MAELSTROM_ABILITIES = { TALENTS.STORMSTRIKE_TALENT.id, SPELLS.WINDSTRIKE_CAST.id, TALENTS.LAVA_LASH_TALENT.id, - TALENTS.ICE_STRIKE_TALENT.id, + TALENTS.ICE_STRIKE_1_ENHANCEMENT_TALENT.id, + TALENTS.ICE_STRIKE_2_ENHANCEMENT_TALENT.id, ], linkFromEventType: EventType.Cast, enabled: (c) => c.hasTalent(TALENTS.ELEMENTAL_ASSAULT_TALENT), @@ -985,7 +987,8 @@ const MAELSTROM_ABILITIES = { MELEE_WEAPON_ATTACK: { // anything classified as a melee hit goes here spellId: [ - TALENTS.ICE_STRIKE_TALENT.id, + TALENTS.ICE_STRIKE_1_ENHANCEMENT_TALENT.id, + TALENTS.ICE_STRIKE_2_ENHANCEMENT_TALENT.id, TALENTS.LAVA_LASH_TALENT.id, TALENTS.CRASH_LIGHTNING_TALENT.id, SPELLS.CRASH_LIGHTNING_BUFF.id, diff --git a/src/analysis/retail/shaman/enhancement/modules/spells/Stormbringer.tsx b/src/analysis/retail/shaman/enhancement/modules/spells/Stormsurge.tsx similarity index 53% rename from src/analysis/retail/shaman/enhancement/modules/spells/Stormbringer.tsx rename to src/analysis/retail/shaman/enhancement/modules/spells/Stormsurge.tsx index 7255cbd39c3..b7aaf92faa5 100644 --- a/src/analysis/retail/shaman/enhancement/modules/spells/Stormbringer.tsx +++ b/src/analysis/retail/shaman/enhancement/modules/spells/Stormsurge.tsx @@ -2,7 +2,12 @@ import SPELLS from 'common/SPELLS'; import TALENTS from 'common/TALENTS/shaman'; import { formatNumber } from 'common/format'; import Analyzer, { Options, SELECTED_PLAYER } from 'parser/core/Analyzer'; -import Events, { ApplyBuffEvent, RefreshBuffEvent } from 'parser/core/Events'; +import Events, { + ApplyBuffEvent, + ApplyBuffStackEvent, + RefreshBuffEvent, + RemoveBuffEvent, +} from 'parser/core/Events'; import UptimeIcon from 'interface/icons/Uptime'; import BoringSpellValueText from 'parser/ui/BoringSpellValueText'; import Statistic from 'parser/ui/Statistic'; @@ -11,60 +16,46 @@ import STATISTIC_CATEGORY from 'parser/ui/STATISTIC_CATEGORY'; import { SpellLink } from 'interface'; import SpellUsable from 'analysis/retail/shaman/enhancement/modules/core/SpellUsable'; -const debug = false; - -class Stormbringer extends Analyzer { - static dependencies = { - spellUsable: SpellUsable, - }; - - protected spellUsable!: SpellUsable; +class Stormsurge extends Analyzer.withDependencies({ + spellUsable: SpellUsable, +}) { protected stormStrikeResets: number = 0; protected windStrikeResets: number = 0; + protected sunderingResets: number = 0; protected wasted: number = 0; + protected hasMoltenThunder: boolean = false; + constructor(options: Options) { super(options); - this.addEventListener( - Events.applybuff.by(SELECTED_PLAYER).spell(SPELLS.STORMBRINGER_BUFF), - this.onStormbringerApplied, + this.hasMoltenThunder = this.selectedCombatant.hasTalent(TALENTS.MOLTEN_THUNDER_TALENT); + + [Events.applybuff, Events.applybuffstack, Events.refreshbuff].forEach((filter) => + this.addEventListener( + filter.by(SELECTED_PLAYER).spell(SPELLS.STORMSURGE_BUFF), + this.onStormsurgeApplied, + ), ); + this.addEventListener( - Events.refreshbuff.by(SELECTED_PLAYER).spell(SPELLS.STORMBRINGER_BUFF), - () => (this.wasted += 1), + Events.removebuff.by(SELECTED_PLAYER).spell(TALENTS.ASCENDANCE_ENHANCEMENT_TALENT), + this.onAscendanceEnd, ); } - onStormbringerApplied(event: ApplyBuffEvent | RefreshBuffEvent) { + onStormsurgeApplied(event: ApplyBuffEvent | ApplyBuffStackEvent | RefreshBuffEvent) { let used = false; - if ( - this.spellUsable.isOnCooldown(TALENTS.STORMSTRIKE_TALENT.id) && - !this.selectedCombatant.hasBuff(TALENTS.ASCENDANCE_ENHANCEMENT_TALENT.id) - ) { - debug && - console.log( - `Stormstrike reset by stormbringer at timestamp: ${ - event.timestamp - } (${this.owner.formatTimestamp(event.timestamp, 3)})`, - ); - this.spellUsable.endCooldown(TALENTS.STORMSTRIKE_TALENT.id, event.timestamp); - if (!this.selectedCombatant.hasBuff(TALENTS.ASCENDANCE_ENHANCEMENT_TALENT.id)) { - this.stormStrikeResets += 1; + if (this.selectedCombatant.hasBuff(TALENTS.ASCENDANCE_ENHANCEMENT_TALENT.id)) { + if (this.deps.spellUsable.isOnCooldown(SPELLS.WINDSTRIKE_CAST.id)) { + this.deps.spellUsable.endCooldown(SPELLS.WINDSTRIKE_CAST.id, event.timestamp); + this.windStrikeResets += 1; used = true; } - } - - if (this.spellUsable.isOnCooldown(SPELLS.WINDSTRIKE_CAST.id)) { - debug && - console.log( - `Windstrike reset by stormbringer at timestamp: ${ - event.timestamp - } (${this.owner.formatTimestamp(event.timestamp, 3)})`, - ); - this.spellUsable.endCooldown(SPELLS.WINDSTRIKE_CAST.id, event.timestamp); - if (this.selectedCombatant.hasBuff(TALENTS.ASCENDANCE_ENHANCEMENT_TALENT.id)) { - this.windStrikeResets += 1; + } else { + if (this.deps.spellUsable.isOnCooldown(TALENTS.STORMSTRIKE_TALENT.id)) { + this.deps.spellUsable.endCooldown(TALENTS.STORMSTRIKE_TALENT.id, event.timestamp); + this.stormStrikeResets += 1; used = true; } } @@ -74,6 +65,10 @@ class Stormbringer extends Analyzer { } } + onAscendanceEnd(event: RemoveBuffEvent) { + this.deps.spellUsable.endCooldown(TALENTS.STORMSTRIKE_TALENT.id, event.timestamp, true, true); + } + statistic() { return ( {this.windStrikeResets}{' '} resets + {this.hasMoltenThunder && ( +
  • + {this.sunderingResets}{' '} + resets +
  • + )} ) : ( @@ -113,15 +114,27 @@ class Stormbringer extends Analyzer { } > - + <> {formatNumber(this.stormStrikeResets + this.windStrikeResets)}{' '} - resets + + resets + + {this.hasMoltenThunder && ( + + <> + {formatNumber(this.sunderingResets)}{' '} + + resets + + + + )}
    ); } } -export default Stormbringer; +export default Stormsurge; diff --git a/src/analysis/retail/shaman/enhancement/modules/talents/Ascendance.tsx b/src/analysis/retail/shaman/enhancement/modules/talents/Ascendance.tsx index 546a2ab9314..8accdc2a093 100644 --- a/src/analysis/retail/shaman/enhancement/modules/talents/Ascendance.tsx +++ b/src/analysis/retail/shaman/enhancement/modules/talents/Ascendance.tsx @@ -108,6 +108,7 @@ class Ascendance extends MajorCooldown { spell: SPELLS.WINDSTRIKE_CAST.id, category: SPELL_CATEGORY.ROTATIONAL, cooldown: (haste: number) => 3 / (1 + haste), + charges: 1 + (this.selectedCombatant.hasTalent(TALENTS.STORMBLAST_TALENT) ? 1 : 0), gcd: { base: 1500, }, @@ -180,7 +181,9 @@ class Ascendance extends MajorCooldown { ); if (this.selectedCombatant.hasTalent(TALENTS.DEEPLY_ROOTED_ELEMENTS_TALENT)) { this.addEventListener( - Events.cast.by(SELECTED_PLAYER).spell([TALENTS.STORMSTRIKE_TALENT, SPELLS.WINDSTRIKE_CAST]), + Events.cast + .by(SELECTED_PLAYER) + .spell([SPELLS.LIGHTNING_BOLT, TALENTS.CHAIN_LIGHTNING_TALENT, SPELLS.TEMPEST_CAST]), this.onProcEligibleCast, ); } @@ -331,7 +334,7 @@ class Ascendance extends MajorCooldown { To minimise waste during{' '} , you will most likely need to spend inbetween casts if doesn't reset via{' '} - . + .

    An example sequence may look something like this: diff --git a/src/analysis/retail/shaman/enhancement/modules/talents/ElementalAssault.tsx b/src/analysis/retail/shaman/enhancement/modules/talents/ElementalAssault.tsx index e859aafe6e9..24d7479ecae 100644 --- a/src/analysis/retail/shaman/enhancement/modules/talents/ElementalAssault.tsx +++ b/src/analysis/retail/shaman/enhancement/modules/talents/ElementalAssault.tsx @@ -32,7 +32,8 @@ const ELEMENTAL_ASSAULT_RANKS: Record = { const BAR_COLORS: Record = { [TALENTS.STORMSTRIKE_TALENT.id]: '#3b7fb0', [TALENTS.LAVA_LASH_TALENT.id]: '#f37735', - [TALENTS.ICE_STRIKE_TALENT.id]: '#94d3ec', + [TALENTS.ICE_STRIKE_1_ENHANCEMENT_TALENT.id]: '#94d3ec', + [TALENTS.ICE_STRIKE_2_ENHANCEMENT_TALENT.id]: '#94d3ec', [-1]: '#532121', // wasted }; diff --git a/src/analysis/retail/shaman/enhancement/modules/talents/ElementalBlastGuide.tsx b/src/analysis/retail/shaman/enhancement/modules/talents/ElementalBlastGuide.tsx index ae7b310993d..d83d2292088 100644 --- a/src/analysis/retail/shaman/enhancement/modules/talents/ElementalBlastGuide.tsx +++ b/src/analysis/retail/shaman/enhancement/modules/talents/ElementalBlastGuide.tsx @@ -129,18 +129,17 @@ class ElementalBlastGuide extends MajorCooldown { getOverallCastPerformance(cast: ElementalBlastCastDetails) { const activeSpirits = this.getActiveElementalSpirits(cast.elementalSpiritsActive); - if (activeSpirits >= 4 && cast.maelstromUsed >= 6) { - return QualitativePerformance.Perfect; + if (cast.maelstromUsed >= 5) { + if (activeSpirits >= 4) { + return QualitativePerformance.Perfect; + } else if (activeSpirits > 0) { + return activeSpirits >= 3 + ? QualitativePerformance.Good + : activeSpirits >= 2 + ? QualitativePerformance.Ok + : QualitativePerformance.Fail; + } } - - if (activeSpirits > 0 && cast.maelstromUsed >= 5) { - return activeSpirits >= 3 - ? QualitativePerformance.Good - : activeSpirits >= 2 - ? QualitativePerformance.Ok - : QualitativePerformance.Fail; - } - return QualitativePerformance.Fail; } @@ -193,8 +192,7 @@ class ElementalBlastGuide extends MajorCooldown { performance: evaluateQualitativePerformanceByThreshold({ actual: cast.maelstromUsed, isGreaterThanOrEqual: { - perfect: 6, - ok: 5, + perfect: 5, }, }), summary: ( @@ -207,14 +205,10 @@ class ElementalBlastGuide extends MajorCooldown { {cast.maelstromUsed} used. {cast.maelstromUsed < 5 ? ( <> + {' '} You should never cast your maelstrom spells with less than 5{' '} - ) : cast.maelstromUsed < 6 ? ( - <> - Try to cast with 6 or more{' '} - . - ) : null}

    ), diff --git a/src/analysis/retail/shaman/enhancement/modules/talents/ElementalSpirits.tsx b/src/analysis/retail/shaman/enhancement/modules/talents/ElementalSpirits.tsx index 59d21f6f4db..05f0fca236a 100644 --- a/src/analysis/retail/shaman/enhancement/modules/talents/ElementalSpirits.tsx +++ b/src/analysis/retail/shaman/enhancement/modules/talents/ElementalSpirits.tsx @@ -276,19 +276,25 @@ class ElementalSpirits extends Analyzer { color: '#f37735', label: <>Molten Weapon, spellId: SPELLS.ELEMENTAL_SPIRITS_BUFF_MOLTEN_WEAPON.id, - value: this.moltenWeaponCount, + value: this.selectedCombatant.getBuffTriggerCount( + SPELLS.ELEMENTAL_SPIRITS_BUFF_MOLTEN_WEAPON.id, + ), }, { color: '#94d3ec', label: <>Icy Edge, spellId: SPELLS.ELEMENTAL_SPIRITS_BUFF_ICY_EDGE.id, - value: this.icyEdgeCount, + value: this.selectedCombatant.getBuffTriggerCount( + SPELLS.ELEMENTAL_SPIRITS_BUFF_ICY_EDGE.id, + ), }, { color: '#3b7fb0', label: <>Crackling Surge, spellId: SPELLS.ELEMENTAL_SPIRITS_BUFF_CRACKLING_SURGE.id, - value: this.cracklingSurgeCount, + value: this.selectedCombatant.getBuffTriggerCount( + SPELLS.ELEMENTAL_SPIRITS_BUFF_CRACKLING_SURGE.id, + ), }, ]; diff --git a/src/analysis/retail/shaman/enhancement/modules/talents/HotHand.tsx b/src/analysis/retail/shaman/enhancement/modules/talents/HotHand.tsx index bbf2a7cf3e6..dd1c61ae266 100644 --- a/src/analysis/retail/shaman/enhancement/modules/talents/HotHand.tsx +++ b/src/analysis/retail/shaman/enhancement/modules/talents/HotHand.tsx @@ -292,7 +292,7 @@ class HotHand extends MajorCooldown { An example sequence may look something like this:
    → - → + → diff --git a/src/analysis/retail/shaman/enhancement/modules/talents/SwirlingMaelstrom.tsx b/src/analysis/retail/shaman/enhancement/modules/talents/SwirlingMaelstrom.tsx index a54b1383a11..ac6f2d4cd90 100644 --- a/src/analysis/retail/shaman/enhancement/modules/talents/SwirlingMaelstrom.tsx +++ b/src/analysis/retail/shaman/enhancement/modules/talents/SwirlingMaelstrom.tsx @@ -20,7 +20,8 @@ import { maybeGetTalentOrSpell } from 'common/maybeGetTalentOrSpell'; const BAR_COLORS: Record = { [TALENTS.FROST_SHOCK_TALENT.id]: '#3b7fb0', [TALENTS.FIRE_NOVA_TALENT.id]: '#f37735', // placeholder for when fire nova is implemented - [TALENTS.ICE_STRIKE_TALENT.id]: '#94d3ec', + [TALENTS.ICE_STRIKE_1_ENHANCEMENT_TALENT.id]: '#94d3ec', + [TALENTS.ICE_STRIKE_2_ENHANCEMENT_TALENT.id]: '#94d3ec', [-1]: '#532121', // wasted }; diff --git a/src/analysis/retail/shaman/enhancement/modules/talents/ThorimsInvocation.tsx b/src/analysis/retail/shaman/enhancement/modules/talents/ThorimsInvocation.tsx index c8758511d0f..042cc67ff68 100644 --- a/src/analysis/retail/shaman/enhancement/modules/talents/ThorimsInvocation.tsx +++ b/src/analysis/retail/shaman/enhancement/modules/talents/ThorimsInvocation.tsx @@ -18,10 +18,11 @@ import RESOURCE_TYPES, { getResource } from 'game/RESOURCE_TYPES'; import typedKeys from 'common/typedKeys'; import { EnhancementEventLinks } from '../../constants'; -/** Lightning Bolt and Chain Lightning damage increased by 20%. +/** Lightning Bolt and Chain Lightning damage increased by 20% and reduces the cooldown of Ascendance + * by 60 sec, and causes Deeply Rooted Elements to last 2 sec longer. * * While Ascendance is active, Windstrike automatically consumes up to 5 Maelstrom Weapon stacks to - * discharge a Lightning Bolt or Chain Lightning at your enemy, whichever you most recently used. */ + * discharge a Lightning Bolt or Chain Lightning at 100% effectiveness at your enemy, whichever you most recently used. */ interface ThorimsInvocationProc { casts: number; diff --git a/src/analysis/retail/shaman/shared/Abilities.tsx b/src/analysis/retail/shaman/shared/Abilities.tsx index d0e3c1f966f..8777c2af285 100644 --- a/src/analysis/retail/shaman/shared/Abilities.tsx +++ b/src/analysis/retail/shaman/shared/Abilities.tsx @@ -122,7 +122,7 @@ class Abilities extends CoreAbilities { ? combatant.hasTalent(TALENTS.LAVA_BURST_TALENT) && !combatant.hasTalent(TALENTS.ELEMENTAL_BLAST_ENHANCEMENT_TALENT) : combatant.hasTalent(TALENTS.LAVA_BURST_TALENT), - cooldown: (_) => (combatant.hasBuff(TALENTS.ASCENDANCE_ELEMENTAL_TALENT.id) ? 0 : 8), + cooldown: 8, category: SPELL_CATEGORY.ROTATIONAL, gcd: { base: 1500, diff --git a/src/common/SPELLS/shaman.ts b/src/common/SPELLS/shaman.ts index 47da84c3a6b..fa04cd754b3 100644 --- a/src/common/SPELLS/shaman.ts +++ b/src/common/SPELLS/shaman.ts @@ -295,6 +295,11 @@ const spells = { name: 'Flame Shock', icon: 'spell_fire_flameshock', }, + FLAME_SHOCK_DUPLICATE: { + id: 470411, + name: 'Flame Shock', + icon: 'spell_fire_flameshock', + }, FROST_SHOCK_ENERGIZE: { icon: 'spell_frost_frostshock', id: 289439, @@ -473,11 +478,36 @@ const spells = { name: 'Fire Nova', icon: 'spell_shaman_improvedfirenova', }, - STORMBRINGER_BUFF: { + STORMSURGE_BUFF: { id: 201846, name: 'Stormbringer Buff', icon: 'spell_nature_stormreach', }, + VOLTAIC_BLAZE_CAST: { + id: 470057, + name: 'Voltaic Blaze', + icon: 'inv_10_dungeonjewelry_primalist_trinket_1ragingelement_fire', + }, + VOLTAIC_BLAZE_BUFF: { + id: 470058, + name: 'Voltaic Blaze', + icon: 'inv_10_dungeonjewelry_primalist_trinket_1ragingelement_fire', + }, + ICE_STRIKE_FROST_SHOCK_BUFF: { + id: 384357, + name: 'Ice Strike', + icon: 'spell_frost_frostbolt', + }, + ICE_STRIKE_1_CAST: { + id: 342240, + name: 'Ice Strike', + icon: 'spell_frost_frostbolt', + }, + ICE_STRIKE_1_USABLE_BUFF: { + id: 466469, + name: 'Ice Strike', + icon: 'spell_frost_frostbolt', + }, MAELSTROM_WEAPON: { id: 187890, name: 'Maelstrom Weapon', @@ -488,6 +518,11 @@ const spells = { name: 'Maelstrom Weapon', icon: 'spell_shaman_maelstromweapon', }, + DOOM_WINDS_TICK: { + id: 469270, + name: 'Doom Winds', + icon: 'ability_ironmaidens_swirlingvortex.jpg', + }, DOOM_VORTEX: { id: 199116, name: 'Doom Vortex', @@ -615,9 +650,9 @@ const spells = { name: 'Hot Hand', icon: 'spell_fire_playingwithfire', }, - STORMBRINGER: { + STORMSURGE: { id: 201845, - name: 'Stormbringer', + name: 'Stormsurge', icon: 'spell_nature_stormreach', }, LEGACY_OF_THE_FROST_WITCH_BUFF: { diff --git a/src/common/TALENTS/shaman.ts b/src/common/TALENTS/shaman.ts index 623979631fa..3af8a4edcce 100644 --- a/src/common/TALENTS/shaman.ts +++ b/src/common/TALENTS/shaman.ts @@ -112,16 +112,16 @@ const talents = { name: 'Arc Discharge', icon: 'ability_thunderking_balllightning', maxRanks: 1, - entryIds: [125616], - definitionIds: [{ id: 130448, specId: 263 }], + entryIds: [117482], + definitionIds: [{ id: 122494, specId: 263 }], }, ASCENDANCE_ELEMENTAL_TALENT: { id: 114050, name: 'Ascendance', icon: 'spell_fire_elementaldevastation', maxRanks: 1, - entryIds: [101877], - definitionIds: [{ id: 106827, specId: 262 }], + entryIds: [101860], + definitionIds: [{ id: 106820, specId: 262 }], }, ASCENDANCE_ENHANCEMENT_TALENT: { id: 114051, @@ -229,6 +229,14 @@ const talents = { entryIds: [127856], definitionIds: [{ id: 132665, specId: 264 }], }, + CHARGED_CONDUIT_TALENT: { + id: 468625, + name: 'Charged Conduit', + icon: 'ability_thunderking_thunderstruck', + maxRanks: 1, + entryIds: [101862], + definitionIds: [{ id: 106844, specId: 262 }], + }, CLEANSE_SPIRIT_TALENT: { id: 51886, name: 'Cleanse Spirit', @@ -247,10 +255,18 @@ const talents = { definitionIds: [{ id: 106926, specId: 264 }], manaCost: 43000, }, + COALESCING_WATER_TALENT: { + id: 470076, + name: 'Coalescing Water', + icon: 'inv_helm_mail_raidshamanmythic_s_01', + maxRanks: 1, + entryIds: [128332], + definitionIds: [{ id: 133139, specId: 264 }], + }, CONDUCTIVE_ENERGY_TALENT: { id: 455123, name: 'Conductive Energy', - icon: 'spell_shaman_thunderstorm', + icon: 'inv_rod_enchantedcobalt', maxRanks: 1, entryIds: [117465], definitionIds: [{ id: 122477, specId: 263 }], @@ -300,9 +316,9 @@ const talents = { name: 'Deeply Rooted Elements', icon: 'inv_misc_herb_liferoot_stem', maxRanks: 1, - entryIds: [127922, 101816, 101937], + entryIds: [101864, 101816, 101937], definitionIds: [ - { id: 132731, specId: 262 }, + { id: 106846, specId: 262 }, { id: 106894, specId: 263 }, { id: 106936, specId: 264 }, ], @@ -398,6 +414,14 @@ const talents = { definitionIds: [{ id: 132734, specId: 262 }], maelstromCost: 60, }, + EARTHSHATTER_TALENT: { + id: 468626, + name: 'Earthshatter', + icon: 'spell_nature_earthquake', + maxRanks: 1, + entryIds: [101867], + definitionIds: [{ id: 106821, specId: 262 }], + }, EARTHSURGE_TALENT: { id: 455590, name: 'Earthsurge', @@ -437,8 +461,8 @@ const talents = { name: 'Echoes of Great Sundering', icon: 'spell_shaman_earthquake', maxRanks: 1, - entryIds: [101862], - definitionIds: [{ id: 106844, specId: 262 }], + entryIds: [127922], + definitionIds: [{ id: 132731, specId: 262 }], }, ECHO_CHAMBER_TALENT: { id: 382032, @@ -467,6 +491,14 @@ const talents = { { id: 106941, specId: 264 }, ], }, + ELECTROSHOCK_TALENT: { + id: 454022, + name: 'Electroshock', + icon: 'ability_thunderking_overcharge', + maxRanks: 1, + entryIds: [128226], + definitionIds: [{ id: 133033, specId: 263 }], + }, ELEMENTAL_ASSAULT_TALENT: { id: 210853, name: 'Elemental Assault', @@ -560,7 +592,7 @@ const talents = { id: 384355, name: 'Elemental Weapons', icon: 'inv_mace_2h_draenorraid_d_03', - maxRanks: 2, + maxRanks: 1, entryIds: [101826], definitionIds: [{ id: 106861, specId: 263 }], }, @@ -580,6 +612,14 @@ const talents = { entryIds: [127881], definitionIds: [{ id: 132690, specId: 264 }], }, + ERUPTING_LAVA_TALENT: { + id: 468574, + name: 'Erupting Lava', + icon: 'ability_rhyolith_lavapool', + maxRanks: 1, + entryIds: [101881], + definitionIds: [{ id: 106848, specId: 262 }], + }, EVERLASTING_ELEMENTS_TALENT: { id: 462867, name: 'Everlasting Elements', @@ -593,8 +633,8 @@ const talents = { name: 'Eye of the Storm', icon: 'spell_shadow_soulleech_2', maxRanks: 1, - entryIds: [101867], - definitionIds: [{ id: 106821, specId: 262 }], + entryIds: [101877], + definitionIds: [{ id: 106827, specId: 262 }], }, FERAL_SPIRIT_TALENT: { id: 51533, @@ -642,9 +682,9 @@ const talents = { name: 'First Ascendant', icon: 'spell_shaman_astralshift', maxRanks: 1, - entryIds: [101876, 127679], + entryIds: [127921, 127679], definitionIds: [ - { id: 106826, specId: 262 }, + { id: 132730, specId: 262 }, { id: 132488, specId: 264 }, ], }, @@ -664,13 +704,13 @@ const talents = { entryIds: [101861], definitionIds: [{ id: 106843, specId: 262 }], }, - FLOW_OF_POWER_TALENT: { - id: 385923, - name: 'Flow of Power', - icon: 'spell_shaman_shockinglava', + FLOWING_SPIRITS_TALENT: { + id: 469314, + name: 'Flowing Spirits', + icon: 'inv_jewelry_frostwolftrinket_02', maxRanks: 1, - entryIds: [101871], - definitionIds: [{ id: 106816, specId: 262 }], + entryIds: [128236], + definitionIds: [{ id: 133043, specId: 263 }], }, FLOW_OF_THE_TIDES_TALENT: { id: 382039, @@ -717,8 +757,8 @@ const talents = { name: 'Fury of the Storms', icon: 'ability_thunderking_overcharge', maxRanks: 1, - entryIds: [127921], - definitionIds: [{ id: 132730, specId: 262 }], + entryIds: [101871], + definitionIds: [{ id: 106816, specId: 262 }], }, FUSION_OF_ELEMENTS_TALENT: { id: 462840, @@ -820,6 +860,14 @@ const talents = { entryIds: [117481], definitionIds: [{ id: 122493, specId: 264 }], }, + HERALD_OF_THE_STORMS_TALENT: { + id: 468571, + name: 'Herald of the Storms', + icon: 'achievement_raidprimalist_windelemental', + maxRanks: 1, + entryIds: [128223], + definitionIds: [{ id: 133030, specId: 262 }], + }, HEX_TALENT: { id: 51514, name: 'Hex', @@ -853,14 +901,22 @@ const talents = { entryIds: [101870], definitionIds: [{ id: 106817, specId: 262 }], }, - ICE_STRIKE_TALENT: { - id: 342240, + ICE_STRIKE_1_ENHANCEMENT_TALENT: { + id: 466467, name: 'Ice Strike', icon: 'spell_frost_frostbolt', maxRanks: 1, entryIds: [101821], definitionIds: [{ id: 106866, specId: 263 }], }, + ICE_STRIKE_2_ENHANCEMENT_TALENT: { + id: 470194, + name: 'Ice Strike', + icon: 'spell_frost_frostbolt', + maxRanks: 1, + entryIds: [128271], + definitionIds: [{ id: 133078, specId: 263 }], + }, IMBUEMENT_MASTERY_TALENT: { id: 445028, name: 'Imbuement Mastery', @@ -949,14 +1005,22 @@ const talents = { entryIds: [101815], definitionIds: [{ id: 106893, specId: 263 }], }, - LIGHTNING_CONDUIT_TALENT: { + LIGHTNING_CAPACITOR_TALENT: { id: 462862, - name: 'Lightning Conduit', + name: 'Lightning Capacitor', icon: 'spell_nature_lightningshield', maxRanks: 1, entryIds: [127913], definitionIds: [{ id: 132722, specId: 262 }], }, + LIGHTNING_CONDUIT_TALENT: { + id: 467778, + name: 'Lightning Conduit', + icon: 'spell_shaman_staticshock', + maxRanks: 1, + entryIds: [117460], + definitionIds: [{ id: 122472, specId: 263 }], + }, LIGHTNING_LASSO_TALENT: { id: 305483, name: 'Lightning Lasso', @@ -970,8 +1034,8 @@ const talents = { name: 'Lightning Rod', icon: 'inv_rod_enchantedcobalt', maxRanks: 1, - entryIds: [101864], - definitionIds: [{ id: 106846, specId: 262 }], + entryIds: [101889], + definitionIds: [{ id: 106838, specId: 262 }], }, LIQUID_MAGMA_TOTEM_TALENT: { id: 192222, @@ -1053,13 +1117,21 @@ const talents = { entryIds: [101806], definitionIds: [{ id: 106863, specId: 263 }], }, + MOLTEN_THUNDER_TALENT: { + id: 469344, + name: 'Molten Thunder', + icon: 'spell_fire_moltenblood', + maxRanks: 1, + entryIds: [128237], + definitionIds: [{ id: 133044, specId: 263 }], + }, MOUNTAINS_WILL_FALL_TALENT: { id: 381726, name: 'Mountains Will Fall', icon: 'inv_elementalearth2', maxRanks: 1, - entryIds: [101889], - definitionIds: [{ id: 106838, specId: 262 }], + entryIds: [101876], + definitionIds: [{ id: 106826, specId: 262 }], }, NATURAL_HARMONY_TALENT: { id: 443442, @@ -1170,9 +1242,9 @@ const talents = { name: 'Preeminence', icon: 'spell_shaman_improvedreincarnation', maxRanks: 1, - entryIds: [127923, 127675], + entryIds: [128224, 127675], definitionIds: [ - { id: 132732, specId: 262 }, + { id: 133031, specId: 262 }, { id: 132484, specId: 264 }, ], }, @@ -1235,7 +1307,7 @@ const talents = { PRIMORDIAL_WAVE_SPEC_TALENT: { id: 375982, name: 'Primordial Wave', - icon: 'ability_maldraxxus_shaman', + icon: 'inv_ability_shaman_primordialwave', maxRanks: 1, entryIds: [101891, 101830], definitionIds: [ @@ -1341,22 +1413,6 @@ const talents = { entryIds: [127910], definitionIds: [{ id: 132719, specId: 264 }], }, - SHOCKING_GRASP_TALENT: { - id: 454022, - name: 'Shocking Grasp', - icon: 'spell_shaman_crashlightning', - maxRanks: 1, - entryIds: [117460], - definitionIds: [{ id: 122472, specId: 263 }], - }, - SKYBREAKERS_FIERY_DEMISE_TALENT: { - id: 378310, - name: "Skybreaker's Fiery Demise", - icon: 'spell_fire_elemental_totem', - maxRanks: 1, - entryIds: [101881], - definitionIds: [{ id: 106848, specId: 262 }], - }, SPIRITWALKERS_AEGIS_TALENT: { id: 378077, name: "Spiritwalker's Aegis", @@ -1484,12 +1540,12 @@ const talents = { definitionIds: [{ id: 106869, specId: 263 }], }, STORMKEEPER_TALENT: { - id: 392714, + id: 191634, name: 'Stormkeeper', icon: 'ability_thunderking_lightningwhip', maxRanks: 1, - entryIds: [101860], - definitionIds: [{ id: 106820, specId: 262 }], + entryIds: [101859], + definitionIds: [{ id: 106819, specId: 262 }], }, STORMSTRIKE_TALENT: { id: 17364, @@ -1529,8 +1585,8 @@ const talents = { name: 'Storm Swell', icon: 'spell_nature_unrelentingstorm', maxRanks: 1, - entryIds: [117482], - definitionIds: [{ id: 122494, specId: 263 }], + entryIds: [117470], + definitionIds: [{ id: 122482, specId: 263 }], }, SUNDERING_TALENT: { id: 197214, @@ -1546,8 +1602,8 @@ const talents = { name: 'Supercharge', icon: 'inv_misc_stormlordsfavor', maxRanks: 1, - entryIds: [117470], - definitionIds: [{ id: 122482, specId: 263 }], + entryIds: [128225], + definitionIds: [{ id: 133032, specId: 263 }], }, SUPPORTIVE_IMBUEMENTS_TALENT: { id: 445033, @@ -1568,7 +1624,7 @@ const talents = { SURGING_CURRENTS_TALENT: { id: 454372, name: 'Surging Currents', - icon: 'spell_nature_stormreach', + icon: 'ability_thunderking_thunderstruck', maxRanks: 1, entryIds: [125617], definitionIds: [{ id: 130449, specId: 263 }], @@ -1797,13 +1853,13 @@ const talents = { entryIds: [117483], definitionIds: [{ id: 122495, specId: 263 }], }, - UNRELENTING_CALAMITY_TALENT: { - id: 382685, - name: 'Unrelenting Calamity', - icon: 'ability_vehicle_electrocharge', + UNRELENTING_STORMS_TALENT: { + id: 470490, + name: 'Unrelenting Storms', + icon: 'spell_nature_lightningoverload', maxRanks: 1, - entryIds: [101859], - definitionIds: [{ id: 106819, specId: 262 }], + entryIds: [128272], + definitionIds: [{ id: 133079, specId: 263 }], }, UNRULY_WINDS_TALENT: { id: 390288, @@ -1813,6 +1869,14 @@ const talents = { entryIds: [101833], definitionIds: [{ id: 106887, specId: 263 }], }, + VOLTAIC_BLAZE_TALENT: { + id: 470053, + name: 'Voltaic Blaze', + icon: 'inv_10_dungeonjewelry_primalist_trinket_1ragingelement_fire', + maxRanks: 1, + entryIds: [128270], + definitionIds: [{ id: 133077, specId: 263 }], + }, VOLTAIC_SURGE_TALENT: { id: 454919, name: 'Voltaic Surge', diff --git a/src/parser/core/CASTS_THAT_ARENT_CASTS.ts b/src/parser/core/CASTS_THAT_ARENT_CASTS.ts index 778d802d0d3..128d892b47b 100644 --- a/src/parser/core/CASTS_THAT_ARENT_CASTS.ts +++ b/src/parser/core/CASTS_THAT_ARENT_CASTS.ts @@ -102,6 +102,8 @@ const spells: number[] = [ TALENTS_SHAMAN.SPIRITWALKERS_GRACE_TALENT.id, SPELLS.FERAL_LUNGE_NOT_A_CAST.id, // duplicate event of regular Feral Lunge cast SPELLS.HEALING_RAIN_TOTEMIC.id, + SPELLS.DOOM_WINDS_TICK.id, + SPELLS.FLAME_SHOCK_DUPLICATE.id, //endregion //region warlock