From a5fccc5cb7e3af109c0cd5e5b65fa27f01c52246 Mon Sep 17 00:00:00 2001 From: Sharrq Date: Thu, 4 Jan 2024 18:57:08 -0500 Subject: [PATCH 01/30] Cast Links + GS --- .../retail/mage/frost/CombatLogParser.ts | 2 + .../frost/normalizers/CastLinkNormalizer.ts | 125 +++++++++++++++ .../mage/frost/talents/GlacialSpike.tsx | 151 +++--------------- 3 files changed, 153 insertions(+), 125 deletions(-) create mode 100644 src/analysis/retail/mage/frost/normalizers/CastLinkNormalizer.ts diff --git a/src/analysis/retail/mage/frost/CombatLogParser.ts b/src/analysis/retail/mage/frost/CombatLogParser.ts index 75d267bf7f1..c5df5470261 100644 --- a/src/analysis/retail/mage/frost/CombatLogParser.ts +++ b/src/analysis/retail/mage/frost/CombatLogParser.ts @@ -42,6 +42,7 @@ import ThermalVoid from './talents/ThermalVoid'; //Normalizers import CometStormLinkNormalizer from './normalizers/CometStormLinkNormalizer'; +import CastLinkNormalizer from './normalizers/CastLinkNormalizer'; class CombatLogParser extends CoreCombatLogParser { static specModules = { @@ -50,6 +51,7 @@ class CombatLogParser extends CoreCombatLogParser { //Normalizers cometStormLinkNormalizer: CometStormLinkNormalizer, + castLinkNormalizer: CastLinkNormalizer, //Core abilities: Abilities, diff --git a/src/analysis/retail/mage/frost/normalizers/CastLinkNormalizer.ts b/src/analysis/retail/mage/frost/normalizers/CastLinkNormalizer.ts new file mode 100644 index 00000000000..8bb8ce709b4 --- /dev/null +++ b/src/analysis/retail/mage/frost/normalizers/CastLinkNormalizer.ts @@ -0,0 +1,125 @@ +import SPELLS from 'common/SPELLS'; +import TALENTS from 'common/TALENTS/mage'; +import EventLinkNormalizer, { EventLink } from 'parser/core/EventLinkNormalizer'; +import { + AbilityEvent, + BeginCastEvent, + RemoveBuffEvent, + CastEvent, + EventType, + GetRelatedEvents, + HasRelatedEvent, + HasTarget, +} from 'parser/core/Events'; +import { Options } from 'parser/core/Module'; +import { encodeTargetString } from 'parser/shared/modules/Enemies'; + +const CAST_BUFFER_MS = 75; + +export const BUFF_APPLY = 'BuffApply'; +export const BUFF_REMOVE = 'BuffRemove'; +export const BUFF_REFRESH = 'BuffRefresh'; +export const CAST_BEGIN = 'CastBegin'; +export const SPELL_CAST = 'SpellCast'; +export const PRE_CAST = 'PreCast'; +export const SPELL_DAMAGE = 'SpellDamage'; +export const CLEAVE_DAMAGE = 'CleaveDamage'; +export const EXPLODE_DEBUFF = 'ExplosionDebuff'; + +const EVENT_LINKS: EventLink[] = [ + { + reverseLinkRelation: SPELL_CAST, + linkingEventId: TALENTS.GLACIAL_SPIKE_TALENT.id, + linkingEventType: EventType.Cast, + linkRelation: SPELL_DAMAGE, + referencedEventId: SPELLS.GLACIAL_SPIKE_DAMAGE.id, + referencedEventType: EventType.Damage, + anyTarget: true, + additionalCondition(linkingEvent, referencedEvent): boolean { + if (!linkingEvent || !referencedEvent) { + return false; + } + const castTarget = + HasTarget(linkingEvent) && + encodeTargetString(linkingEvent.targetID, linkingEvent.targetInstance); + const damageTarget = + HasTarget(referencedEvent) && + encodeTargetString(referencedEvent.targetID, referencedEvent.targetInstance); + return castTarget === damageTarget; + }, + maximumLinks: 1, + forwardBufferMs: 3000, + backwardBufferMs: CAST_BUFFER_MS, + }, + { + reverseLinkRelation: SPELL_CAST, + linkingEventId: TALENTS.GLACIAL_SPIKE_TALENT.id, + linkingEventType: EventType.Cast, + linkRelation: CLEAVE_DAMAGE, + referencedEventId: SPELLS.GLACIAL_SPIKE_DAMAGE.id, + referencedEventType: EventType.Damage, + anyTarget: true, + additionalCondition(linkingEvent, referencedEvent): boolean { + if (!linkingEvent || !referencedEvent) { + return false; + } + const castTarget = + HasTarget(linkingEvent) && + encodeTargetString(linkingEvent.targetID, linkingEvent.targetInstance); + const damageTarget = + HasTarget(referencedEvent) && + encodeTargetString(referencedEvent.targetID, referencedEvent.targetInstance); + return castTarget !== damageTarget; + }, + maximumLinks: 1, + forwardBufferMs: 3000, + backwardBufferMs: CAST_BUFFER_MS, + }, +]; + +/** + * When a spell is cast on a target, the ordering of the Cast and ApplyBuff/RefreshBuff/(direct)Heal + * can be semi-arbitrary, making analysis difficult. + * + * This normalizer adds a _linkedEvent to the ApplyBuff/RefreshBuff/Heal linking back to the Cast event + * that caused it (if one can be found). + * + * This normalizer adds links for the buffs Rejuvenation, Regrowth, Wild Growth, Lifebloom, + * and for the direct heals of Swiftmend and Regrowth, and the self buff from Flourish. + * A special link key is used when the HoTs were applied by an Overgrowth cast instead of a normal hardcast. + */ +class CastLinkNormalizer extends EventLinkNormalizer { + constructor(options: Options) { + super(options, EVENT_LINKS); + } +} + +/** Returns true iff the given buff application or heal can be matched back to a hardcast */ +export function isFromHardcast(event: AbilityEvent): boolean { + return HasRelatedEvent(event, SPELL_CAST); +} + +export function isInstantCast(event: CastEvent): boolean { + const beginCast = GetRelatedEvents(event, CAST_BEGIN)[0]; + return !beginCast || event.timestamp - beginCast.timestamp <= CAST_BUFFER_MS; +} + +export function hasPreCast(event: AbilityEvent): boolean { + return HasRelatedEvent(event, PRE_CAST); +} + +/** Returns the hardcast event that caused this buff or heal, if there is one */ +export function getHardcast(event: AbilityEvent): CastEvent | undefined { + return GetRelatedEvents( + event, + SPELL_CAST, + (e): e is CastEvent => e.type === EventType.Cast, + ).pop(); +} + +export function isProcExpired(event: RemoveBuffEvent, spenderId: number): boolean { + const cast = GetRelatedEvents(event, SPELL_CAST)[0]; + return !cast || cast.ability.guid !== spenderId; +} + +export default CastLinkNormalizer; diff --git a/src/analysis/retail/mage/frost/talents/GlacialSpike.tsx b/src/analysis/retail/mage/frost/talents/GlacialSpike.tsx index 7a9c561f366..13fedc5256f 100644 --- a/src/analysis/retail/mage/frost/talents/GlacialSpike.tsx +++ b/src/analysis/retail/mage/frost/talents/GlacialSpike.tsx @@ -1,15 +1,8 @@ -import { Trans } from '@lingui/macro'; import { SHATTER_DEBUFFS } from 'analysis/retail/mage/shared'; -import { formatPercentage } from 'common/format'; -import SPELLS from 'common/SPELLS'; import TALENTS from 'common/TALENTS/mage'; -import { SpellLink } from 'interface'; -import { TooltipElement } from 'interface'; import Analyzer, { SELECTED_PLAYER, Options } from 'parser/core/Analyzer'; -import Events, { CastEvent, DamageEvent, FightEndEvent, HasTarget } from 'parser/core/Events'; -import { When, ThresholdStyle } from 'parser/core/ParseResults'; -import AbilityTracker from 'parser/shared/modules/AbilityTracker'; -import Enemies, { encodeTargetString } from 'parser/shared/modules/Enemies'; +import Events, { CastEvent, DamageEvent, GetRelatedEvent } from 'parser/core/Events'; +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'; @@ -17,15 +10,15 @@ import STATISTIC_CATEGORY from 'parser/ui/STATISTIC_CATEGORY'; class GlacialSpike extends Analyzer { static dependencies = { enemies: Enemies, - abilityTracker: AbilityTracker, }; protected enemies!: Enemies; - protected abilityTracker!: AbilityTracker; - lastCastEvent?: CastEvent; - lastCastDidDamage = false; - spikeShattered = 0; - spikeNotShattered = 0; + glacialSpike: { + timestamp: number; + shattered: boolean; + damage: DamageEvent | undefined; + cleave: DamageEvent | undefined; + }[] = []; constructor(options: Options) { super(options); @@ -35,122 +28,28 @@ class GlacialSpike extends Analyzer { Events.cast.by(SELECTED_PLAYER).spell(TALENTS.GLACIAL_SPIKE_TALENT), this.onGlacialSpikeCast, ); - this.addEventListener( - Events.damage.by(SELECTED_PLAYER).spell(SPELLS.GLACIAL_SPIKE_DAMAGE), - this.onGlacialSpikeDamage, - ); - this.addEventListener(Events.fightend, this.onFightEnd); } onGlacialSpikeCast(event: CastEvent) { - if (this.lastCastEvent) { - this.flagTimeline(this.lastCastEvent); - } - - this.lastCastEvent = event; - this.lastCastDidDamage = false; - } - - onGlacialSpikeDamage(event: DamageEvent) { - if (!this.lastCastEvent) { - return; - } - if (!HasTarget(this.lastCastEvent)) { - return; - } - - const castTarget = encodeTargetString( - this.lastCastEvent.targetID, - this.lastCastEvent.targetInstance, - ); - const damageTarget = encodeTargetString(event.targetID, event.targetInstance); - - //We dont care about the Glacial Spikes that split to something else via Splitting Ice. - if (castTarget !== damageTarget) { - return; - } - - this.lastCastDidDamage = true; - const enemy: any = this.enemies.getEntity(event); - if (enemy && SHATTER_DEBUFFS.some((effect) => enemy.hasBuff(effect.id, event.timestamp))) { - this.spikeShattered += 1; - } else { - this.spikeNotShattered += 1; - this.flagTimeline(this.lastCastEvent); - } - this.lastCastEvent = undefined; + const damage: DamageEvent | undefined = GetRelatedEvent(event, 'SpellDamage'); + const enemy = damage && this.enemies.getEntity(damage); + const cleave: DamageEvent | undefined = GetRelatedEvent(event, 'CleaveDamage'); + this.glacialSpike.push({ + timestamp: event.timestamp, + shattered: + (enemy && SHATTER_DEBUFFS.some((effect) => enemy.hasBuff(effect.id, event.timestamp))) || + false, + damage: damage, + cleave: cleave, + }); } - onFightEnd(event: FightEndEvent) { - if (this.lastCastEvent) { - this.flagTimeline(this.lastCastEvent); - } - } - - flagTimeline(event: CastEvent) { - if (!this.lastCastEvent) { - return; - } - - event.meta = event.meta || {}; - event.meta.isInefficientCast = true; - if (this.lastCastDidDamage) { - event.meta.inefficientCastReason = `You cast Glacial Spike without shattering it. You should wait until it is frozen or you are able to use a Brain Freeze proc to maximize its damage.`; - } else { - event.meta.inefficientCastReason = - 'The target died before Glacial Spike hit it. You should avoid this by casting faster spells on very low-health targets, it is important to not waste potential Glacial Spike damage.'; - } - } - - get utilPercentage() { - return this.spikeShattered / this.totalCasts || 0; + get shatteredCasts() { + return this.glacialSpike.filter((gs) => gs.shattered).length; } get totalCasts() { - return this.abilityTracker.getAbility(TALENTS.GLACIAL_SPIKE_TALENT.id).casts; - } - - get glacialSpikeUtilizationThresholds() { - return { - actual: this.utilPercentage, - isLessThan: { - minor: 1.0, - average: 0.85, - major: 0.7, - }, - style: ThresholdStyle.PERCENTAGE, - }; - } - - suggestions(when: When) { - when(this.glacialSpikeUtilizationThresholds).addSuggestion((suggest, actual, recommended) => - suggest( - <> - You cast without{' '} - - ing it {this.spikeNotShattered} times. Because it is such a potent ability, it is - important to maximize it's damage by only casting it if the target is - - Winter's Chill, Frost Nova, Ice Nova, Ring of Frost, and your pet's Freeze will all - cause the target to be frozen or act as frozen. - - } - > - Frozen or acting as Frozen - - . - , - ) - .icon(TALENTS.GLACIAL_SPIKE_TALENT.icon) - .actual( - - {formatPercentage(actual, 1)}% utilization - , - ) - .recommended(`${formatPercentage(recommended, 1)}% is recommended`), - ); + return this.glacialSpike.length; } statistic() { @@ -160,13 +59,15 @@ class GlacialSpike extends Analyzer { size="flexible" tooltip={ <> - You cast Glacial Spike {this.totalCasts} times, {this.spikeShattered} casts of which + You cast Glacial Spike {this.totalCasts} times, {this.shatteredCasts} casts of which were Shattered } > - {`${formatPercentage(this.utilPercentage, 0)}%`} Cast utilization + {this.shatteredCasts} Shattered Casts +
+ {this.totalCasts - this.shatteredCasts} Non-Shattered Casts
); From 8a4b310c305e4f538f4b6f4b956d69a96ffb9b9f Mon Sep 17 00:00:00 2001 From: Sharrq Date: Thu, 4 Jan 2024 19:00:24 -0500 Subject: [PATCH 02/30] Remove GS from Checklist --- .../retail/mage/frost/checklist/Component.tsx | 26 ------------------- .../retail/mage/frost/checklist/Module.tsx | 4 --- 2 files changed, 30 deletions(-) diff --git a/src/analysis/retail/mage/frost/checklist/Component.tsx b/src/analysis/retail/mage/frost/checklist/Component.tsx index 95fbf565cef..51978a31aca 100644 --- a/src/analysis/retail/mage/frost/checklist/Component.tsx +++ b/src/analysis/retail/mage/frost/checklist/Component.tsx @@ -109,32 +109,6 @@ const FrostMageChecklist = ({ combatant, castEfficiency, thresholds }: Checklist tooltip="Munching a proc refers to a situation where you have a Fingers of Frost proc at the same time that Winters Chill is on the target. This essentially leads to a wasted Fingers of Frost proc since Fingers of Frost and Winter's Chill both do the same thing, and casting Ice Lance will remove both a Fingers of Frost proc and a stack of Winter's Chill. This is sometimes unavoidable, but if you have both a Fingers of Frost proc and a Brain Freeze proc, you can minimize this by ensuring that you use the Fingers of Frost procs first before you start casting Frostbolt and Flurry to use the Brain Freeze proc." /> - - When talented into you should always - ensure that you are getting the most out of it, because a large part of your damage will - come from making sure that you are handling Glacial Spike properly. As a rule, once you - have Glacial Spike available, you should not cast it unless you can cast{' '} - alongside it ( - {'>'}{' '} - {'>'} - ) or if you also have the{' '} - and the Glacial Spike will hit a - second target. If neither of those are true, then you should continue casting{' '} - until {' '} - is available or you get a proc. - - } - > - {combatant.hasTalent(TALENTS.GLACIAL_SPIKE_TALENT) && ( - - )} - Date: Thu, 4 Jan 2024 19:34:02 -0500 Subject: [PATCH 03/30] Brain Freeze --- .../retail/mage/frost/core/BrainFreeze.tsx | 91 ++++++++++++------- .../frost/normalizers/CastLinkNormalizer.ts | 22 +++++ 2 files changed, 81 insertions(+), 32 deletions(-) diff --git a/src/analysis/retail/mage/frost/core/BrainFreeze.tsx b/src/analysis/retail/mage/frost/core/BrainFreeze.tsx index 639c7376a72..ab830975bb4 100644 --- a/src/analysis/retail/mage/frost/core/BrainFreeze.tsx +++ b/src/analysis/retail/mage/frost/core/BrainFreeze.tsx @@ -4,8 +4,14 @@ import { formatNumber, formatPercentage } from 'common/format'; import SPELLS from 'common/SPELLS'; import TALENTS from 'common/TALENTS/mage'; import { SpellLink } from 'interface'; -import Analyzer from 'parser/core/Analyzer'; -import { EventType } from 'parser/core/Events'; +import Analyzer, { Options, SELECTED_PLAYER } from 'parser/core/Analyzer'; +import Events, { + CastEvent, + ApplyBuffEvent, + RemoveBuffEvent, + RefreshBuffEvent, + GetRelatedEvent, +} from 'parser/core/Events'; import { ThresholdStyle, When } from 'parser/core/ParseResults'; import Enemies from 'parser/shared/modules/Enemies'; import EventHistory from 'parser/shared/modules/EventHistory'; @@ -23,41 +29,62 @@ class BrainFreeze extends Analyzer { protected eventHistory!: EventHistory; protected sharedCode!: SharedCode; - overlappedFlurries = () => { - let casts = this.eventHistory.getEvents(EventType.Cast, { - spell: TALENTS.FLURRY_TALENT, + brainFreezeRefreshes = 0; + flurry: { timestamp: number; overlapped: boolean }[] = []; + brainFreeze: { apply: ApplyBuffEvent; remove: RemoveBuffEvent | undefined; expired: boolean }[] = + []; + + constructor(options: Options) { + super(options); + this.addEventListener( + Events.cast.by(SELECTED_PLAYER).spell(TALENTS.FLURRY_TALENT), + this.onFlurryCast, + ); + this.addEventListener( + Events.applybuff.by(SELECTED_PLAYER).spell(SPELLS.BRAIN_FREEZE_BUFF), + this.onBrainFreeze, + ); + this.addEventListener( + Events.refreshbuff.by(SELECTED_PLAYER).spell(SPELLS.BRAIN_FREEZE_BUFF), + this.onBrainFreezeRefresh, + ); + } + + onFlurryCast(event: CastEvent) { + this.flurry.push({ + timestamp: event.timestamp, + overlapped: this.selectedCombatant.hasBuff(SPELLS.BRAIN_FREEZE_BUFF.id, event.timestamp - 10), }); - casts = casts.filter((c) => { - const enemy = this.enemies.getEntity(c); - return enemy && enemy.hasBuff(SPELLS.WINTERS_CHILL.id); + } + + onBrainFreeze(event: ApplyBuffEvent) { + const remove: RemoveBuffEvent | undefined = GetRelatedEvent(event, 'BuffRemove'); + const spender: CastEvent | undefined = remove && GetRelatedEvent(remove, 'SpellCast'); + this.brainFreeze.push({ + apply: event, + remove: remove || undefined, + expired: !spender, }); - return casts.length || 0; - }; + } - get expiredProcs() { - return ( - this.sharedCode.getExpiredProcs(SPELLS.BRAIN_FREEZE_BUFF, TALENTS.FLURRY_TALENT).length || 0 - ); + onBrainFreezeRefresh(event: RefreshBuffEvent) { + this.brainFreezeRefreshes += 1; } - get totalProcs() { - return ( - this.eventHistory.getEvents(EventType.ApplyBuff, { - spell: SPELLS.BRAIN_FREEZE_BUFF, - }).length || 0 - ); + get overlappedFlurries() { + return this.flurry.filter((f) => f.overlapped).length; } - get overwrittenProcs() { - return ( - this.eventHistory.getEvents(EventType.RefreshBuff, { - spell: SPELLS.BRAIN_FREEZE_BUFF, - }).length || 0 - ); + get expiredProcs() { + return this.brainFreeze.filter((bf) => bf.expired).length; + } + + get totalProcs() { + return this.brainFreeze.length; } get wastedPercent() { - return (this.overwrittenProcs + this.expiredProcs) / this.totalProcs || 0; + return (this.brainFreezeRefreshes + this.expiredProcs) / this.totalProcs || 0; } get utilPercent() { @@ -79,7 +106,7 @@ class BrainFreeze extends Analyzer { // Percentages lowered from .00, .08, .16; with the addition of the forgiveness window it is almost as bad as letting BF expire when you waste a proc get brainFreezeOverwritenThresholds() { return { - actual: this.overwrittenProcs / this.totalProcs || 0, + actual: this.brainFreezeRefreshes / this.totalProcs || 0, isGreaterThan: { minor: 0.0, average: 0.05, @@ -104,7 +131,7 @@ class BrainFreeze extends Analyzer { get overlappedFlurryThresholds() { return { - actual: this.overlappedFlurries(), + actual: this.brainFreezeRefreshes, isGreaterThan: { average: 0, major: 3, @@ -154,7 +181,7 @@ class BrainFreeze extends Analyzer { <> You cast and applied{' '} while the target still had the{' '} - debuff on them {this.overlappedFlurries()}{' '} + debuff on them {this.brainFreezeRefreshes}{' '} times. Casting applies 2 stacks of{' '} to the target so you should always ensure you are spending both stacks before you cast and @@ -178,8 +205,8 @@ class BrainFreeze extends Analyzer { <> You got {this.totalProcs} total procs.
    -
  • {this.totalProcs - this.expiredProcs - this.overwrittenProcs} used
  • -
  • {this.overwrittenProcs} overwritten
  • +
  • {this.totalProcs - this.expiredProcs - this.brainFreezeRefreshes} used
  • +
  • {this.brainFreezeRefreshes} overwritten
  • {this.expiredProcs} expired
diff --git a/src/analysis/retail/mage/frost/normalizers/CastLinkNormalizer.ts b/src/analysis/retail/mage/frost/normalizers/CastLinkNormalizer.ts index 8bb8ce709b4..78f36063c48 100644 --- a/src/analysis/retail/mage/frost/normalizers/CastLinkNormalizer.ts +++ b/src/analysis/retail/mage/frost/normalizers/CastLinkNormalizer.ts @@ -75,6 +75,28 @@ const EVENT_LINKS: EventLink[] = [ forwardBufferMs: 3000, backwardBufferMs: CAST_BUFFER_MS, }, + { + reverseLinkRelation: BUFF_APPLY, + linkingEventId: SPELLS.BRAIN_FREEZE_BUFF.id, + linkingEventType: EventType.ApplyBuff, + linkRelation: BUFF_REMOVE, + referencedEventId: SPELLS.BRAIN_FREEZE_BUFF.id, + referencedEventType: EventType.RemoveBuff, + maximumLinks: 1, + forwardBufferMs: 17_000, + backwardBufferMs: CAST_BUFFER_MS, + }, + { + reverseLinkRelation: BUFF_REMOVE, + linkingEventId: SPELLS.BRAIN_FREEZE_BUFF.id, + linkingEventType: EventType.RemoveBuff, + linkRelation: SPELL_CAST, + referencedEventId: TALENTS.FLURRY_TALENT.id, + referencedEventType: EventType.Cast, + maximumLinks: 1, + forwardBufferMs: CAST_BUFFER_MS, + backwardBufferMs: CAST_BUFFER_MS, + }, ]; /** From b4d812d47c1089d4e33be13605524c68a0b38823 Mon Sep 17 00:00:00 2001 From: Sharrq Date: Thu, 4 Jan 2024 19:35:41 -0500 Subject: [PATCH 04/30] remove brain freeze dependencies --- src/analysis/retail/mage/frost/core/BrainFreeze.tsx | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/analysis/retail/mage/frost/core/BrainFreeze.tsx b/src/analysis/retail/mage/frost/core/BrainFreeze.tsx index ab830975bb4..35fd9c21690 100644 --- a/src/analysis/retail/mage/frost/core/BrainFreeze.tsx +++ b/src/analysis/retail/mage/frost/core/BrainFreeze.tsx @@ -1,5 +1,4 @@ import { Trans } from '@lingui/macro'; -import { SharedCode } from 'analysis/retail/mage/shared'; import { formatNumber, formatPercentage } from 'common/format'; import SPELLS from 'common/SPELLS'; import TALENTS from 'common/TALENTS/mage'; @@ -13,22 +12,11 @@ import Events, { GetRelatedEvent, } from 'parser/core/Events'; import { ThresholdStyle, When } from 'parser/core/ParseResults'; -import Enemies from 'parser/shared/modules/Enemies'; -import EventHistory from 'parser/shared/modules/EventHistory'; import BoringSpellValueText from 'parser/ui/BoringSpellValueText'; import Statistic from 'parser/ui/Statistic'; import STATISTIC_ORDER from 'parser/ui/STATISTIC_ORDER'; class BrainFreeze extends Analyzer { - static dependencies = { - enemies: Enemies, - eventHistory: EventHistory, - sharedCode: SharedCode, - }; - protected enemies!: Enemies; - protected eventHistory!: EventHistory; - protected sharedCode!: SharedCode; - brainFreezeRefreshes = 0; flurry: { timestamp: number; overlapped: boolean }[] = []; brainFreeze: { apply: ApplyBuffEvent; remove: RemoveBuffEvent | undefined; expired: boolean }[] = From 465fde81fe77640a82041e9880d928a12fad4b1c Mon Sep 17 00:00:00 2001 From: Sharrq Date: Thu, 4 Jan 2024 21:57:27 -0500 Subject: [PATCH 05/30] Winter's Chill --- .../retail/mage/frost/core/WintersChill.tsx | 216 +++++++----------- .../frost/normalizers/CastLinkNormalizer.ts | 36 ++- 2 files changed, 115 insertions(+), 137 deletions(-) diff --git a/src/analysis/retail/mage/frost/core/WintersChill.tsx b/src/analysis/retail/mage/frost/core/WintersChill.tsx index 6cfcc1df547..13a0609ce6b 100644 --- a/src/analysis/retail/mage/frost/core/WintersChill.tsx +++ b/src/analysis/retail/mage/frost/core/WintersChill.tsx @@ -1,11 +1,16 @@ -import { Trans } from '@lingui/macro'; -import { formatPercentage, formatDuration } from 'common/format'; +import { formatPercentage } from 'common/format'; import SPELLS from 'common/SPELLS'; import TALENTS from 'common/TALENTS/mage'; import { SpellIcon } from 'interface'; import { SpellLink } from 'interface'; -import Analyzer from 'parser/core/Analyzer'; -import { EventType } from 'parser/core/Events'; +import Analyzer, { Options, SELECTED_PLAYER } from 'parser/core/Analyzer'; +import Events, { + ApplyDebuffEvent, + RemoveDebuffEvent, + CastEvent, + DamageEvent, + GetRelatedEvent, +} from 'parser/core/Events'; import { When, ThresholdStyle } from 'parser/core/ParseResults'; import Enemies from 'parser/shared/modules/Enemies'; import EventHistory from 'parser/shared/modules/EventHistory'; @@ -13,18 +18,7 @@ import BoringSpellValueText from 'parser/ui/BoringSpellValueText'; import Statistic from 'parser/ui/Statistic'; import STATISTIC_ORDER from 'parser/ui/STATISTIC_ORDER'; -const WINTERS_CHILL_SPENDERS = [ - SPELLS.ICE_LANCE_DAMAGE, - SPELLS.GLACIAL_SPIKE_DAMAGE, - TALENTS.ICE_NOVA_TALENT, - TALENTS.RAY_OF_FROST_TALENT, -]; - -const WINTERS_CHILL_PRECAST_CASTS = [SPELLS.FROSTBOLT, TALENTS.GLACIAL_SPIKE_TALENT]; - -const WINTERS_CHILL_PRECAST_DAMAGE = [SPELLS.FROSTBOLT_DAMAGE, SPELLS.GLACIAL_SPIKE_DAMAGE]; - -const debug = false; +const WINTERS_CHILL_SPENDERS = [SPELLS.ICE_LANCE_DAMAGE.id, SPELLS.GLACIAL_SPIKE_DAMAGE.id]; class WintersChill extends Analyzer { static dependencies = { @@ -35,124 +29,83 @@ class WintersChill extends Analyzer { protected eventHistory!: EventHistory; hasGlacialSpike: boolean = this.selectedCombatant.hasTalent(TALENTS.GLACIAL_SPIKE_TALENT); + wintersChill: { + apply: ApplyDebuffEvent; + remove: RemoveDebuffEvent | undefined; + precast: CastEvent | undefined; + precastIcicles: number; + damageEvents: DamageEvent[]; + }[] = []; + + constructor(options: Options) { + super(options); + this.addEventListener( + Events.applydebuff.by(SELECTED_PLAYER).spell(SPELLS.WINTERS_CHILL), + this.onWintersChill, + ); + this.addEventListener( + Events.damage + .by(SELECTED_PLAYER) + .spell([SPELLS.FROSTBOLT_DAMAGE, SPELLS.GLACIAL_SPIKE_DAMAGE, SPELLS.ICE_LANCE_DAMAGE]), + this.onDamage, + ); + } - wintersChillHardCasts = () => { - let debuffApplies = this.eventHistory.getEvents(EventType.ApplyDebuff, { - spell: SPELLS.WINTERS_CHILL, + onWintersChill(event: ApplyDebuffEvent) { + const remove: RemoveDebuffEvent | undefined = GetRelatedEvent(event, 'DebuffRemove'); + const flurry: CastEvent | undefined = GetRelatedEvent(event, 'SpellCast'); + const precast: CastEvent | undefined = GetRelatedEvent(event, 'PreCast'); + this.wintersChill.push({ + apply: event, + remove: remove, + precast: precast, + precastIcicles: + (flurry && + this.selectedCombatant.getBuff(SPELLS.ICICLES_BUFF.id, flurry.timestamp)?.stacks) || + 0, + damageEvents: [], }); + } - //Filter out buffs where there was not a valid precast before Winter's Chill was applied or the precast didnt land in Winter's Chill - debuffApplies = debuffApplies.filter((e) => { - const debuffRemoved = this.eventHistory.getEvents(EventType.RemoveDebuff, { - searchBackwards: false, - spell: SPELLS.WINTERS_CHILL, - count: 1, - startTimestamp: e.timestamp, - })[0]; - const preCast = this.eventHistory.getEvents(EventType.Cast, { - spell: WINTERS_CHILL_PRECAST_CASTS, - count: 1, - startTimestamp: e.timestamp, - duration: 1000, - })[0]; - if (!preCast) { - debug && - this.log( - 'PRECAST NOT FOUND @' + formatDuration(e.timestamp - this.owner.fight.start_time), - ); - return false; - } + onDamage(event: DamageEvent) { + const enemy = this.enemies.getEntity(event); + if (!enemy || !enemy.hasBuff(SPELLS.WINTERS_CHILL.id)) { + return; + } + const wintersChillDebuff: number | undefined = this.wintersChill.findIndex( + (d) => + d.apply.timestamp <= event.timestamp && d.remove && d.remove.timestamp >= event.timestamp, + ); + this.wintersChill[wintersChillDebuff].damageEvents?.push(event); + } - //Check to see if the precast landed in Winter's Chill - const duration = debuffRemoved - ? debuffRemoved.timestamp - e.timestamp - : this.owner.fight.end_time - e.timestamp; - const damageEvents = this.eventHistory.getEvents(EventType.Damage, { - searchBackwards: false, - spell: WINTERS_CHILL_PRECAST_DAMAGE, - startTimestamp: preCast.timestamp, - duration: duration, - }); - if (!damageEvents || damageEvents.length === 0) { - debug && - this.log( - 'PRECAST DAMAGE NOT FOUND @' + - formatDuration(e.timestamp - this.owner.fight.start_time), - ); - return false; - } + missedPreCasts = () => { + //If there is no Pre Cast, or if there is a Precast but it didnt land in Winter's Chlll + let missingPreCast = this.wintersChill.filter( + (w) => + !w.precast || + w.damageEvents.filter((d) => w.precast?.ability.guid === d.ability.guid).length > 0, + ); - //Check if the target had Winter's Chill - let preCastHits = 0; - damageEvents.forEach((d) => { - const enemy = this.enemies.getEntity(d); - if (enemy && enemy.hasBuff(SPELLS.WINTERS_CHILL.id, d.timestamp)) { - preCastHits += 1; - } - }); - if (preCastHits < 1) { - debug && - this.log( - 'PRECAST DAMAGE NOT SHATTERED @ ' + - formatDuration(e.timestamp - this.owner.fight.start_time), - ); - return false; - } - return true; - }); - return debuffApplies.length; + //If the player had exactly 4 Icicles, disregard it + missingPreCast = missingPreCast.filter((w) => w.precastIcicles !== 4); + + return missingPreCast.length; }; wintersChillShatters = () => { - let debuffApplies = this.eventHistory.getEvents(EventType.ApplyDebuff, { - spell: SPELLS.WINTERS_CHILL, - }); - - //Filter out buffs where both stacks of Winter's Chill were used before Winter's Chill expired - debuffApplies = debuffApplies.filter((e) => { - const debuffRemoved = this.eventHistory.getEvents(EventType.RemoveDebuff, { - searchBackwards: false, - spell: SPELLS.WINTERS_CHILL, - count: 1, - startTimestamp: e.timestamp, - })[0]; - const duration = debuffRemoved - ? debuffRemoved.timestamp - e.timestamp - : this.owner.fight.end_time - e.timestamp; - const damageEvents = this.eventHistory.getEvents(EventType.Damage, { - searchBackwards: false, - spell: WINTERS_CHILL_SPENDERS, - startTimestamp: e.timestamp, - duration: duration, - }); - if (!damageEvents) { - return false; - } + //Winter's Chill Debuffs where there are at least 2 damage hits of Glacial Spike and/or Ice Lance + const badDebuffs = this.wintersChill.filter( + (w) => + w.damageEvents.filter((d) => WINTERS_CHILL_SPENDERS.includes(d.ability.guid)).length >= 2, + ); - //Check if the target had Winter's Chill - let shatteredCasts = 0; - damageEvents.forEach((d) => { - const enemy = this.enemies.getEntity(d); - if (enemy && enemy.hasBuff(SPELLS.WINTERS_CHILL.id, d.timestamp)) { - shatteredCasts += 1; - } - }); - debug && - this.log( - 'Shattered Casts: ' + - shatteredCasts + - ' @ ' + - formatDuration(e.timestamp - this.owner.fight.start_time), - ); - return shatteredCasts >= 2; - }); - return debuffApplies.length; + return badDebuffs.length; }; get totalProcs() { - return this.eventHistory.getEvents(EventType.ApplyDebuff, { - spell: SPELLS.WINTERS_CHILL, - }).length; + this.log(this.wintersChill); + return this.wintersChill.length; } get missedShatters() { @@ -163,12 +116,8 @@ class WintersChill extends Analyzer { return this.wintersChillShatters() / this.totalProcs || 0; } - get missedPreCasts() { - return this.totalProcs - this.wintersChillHardCasts(); - } - get preCastPercent() { - return this.wintersChillHardCasts() / this.totalProcs || 0; + return 1 - this.missedPreCasts() / this.totalProcs; } // less strict than the ice lance suggestion both because it's less important, @@ -220,11 +169,7 @@ class WintersChill extends Analyzer { , ) .icon(TALENTS.ICE_LANCE_TALENT.icon) - .actual( - - {formatPercentage(1 - actual)}% Winter's Chill not shattered with Ice Lance - , - ) + .actual(`${formatPercentage(1 - actual)}% Winter's Chill not shattered with Ice Lance`) .recommended(`${formatPercentage(1 - recommended)}% is recommended`), ); when(this.wintersChillPreCastThresholds).addSuggestion((suggest, actual, recommended) => @@ -242,10 +187,9 @@ class WintersChill extends Analyzer { ) .icon(SPELLS.FROSTBOLT.icon) .actual( - - {formatPercentage(1 - actual)}% Winter's Chill not shattered with Frostbolt, Glacial - Spike, or Ebonbolt - , + `${formatPercentage( + 1 - actual, + )}% Winter's Chill not shattered with Frostbolt or Glacial Spike`, ) .recommended(`${formatPercentage(1 - recommended)}% is recommended`), ); diff --git a/src/analysis/retail/mage/frost/normalizers/CastLinkNormalizer.ts b/src/analysis/retail/mage/frost/normalizers/CastLinkNormalizer.ts index 78f36063c48..b368b809a79 100644 --- a/src/analysis/retail/mage/frost/normalizers/CastLinkNormalizer.ts +++ b/src/analysis/retail/mage/frost/normalizers/CastLinkNormalizer.ts @@ -19,12 +19,13 @@ const CAST_BUFFER_MS = 75; export const BUFF_APPLY = 'BuffApply'; export const BUFF_REMOVE = 'BuffRemove'; export const BUFF_REFRESH = 'BuffRefresh'; +export const DEBUFF_APPLY = 'DebuffApply'; +export const DEBUFF_REMOVE = 'DebuffRemove'; export const CAST_BEGIN = 'CastBegin'; export const SPELL_CAST = 'SpellCast'; export const PRE_CAST = 'PreCast'; export const SPELL_DAMAGE = 'SpellDamage'; export const CLEAVE_DAMAGE = 'CleaveDamage'; -export const EXPLODE_DEBUFF = 'ExplosionDebuff'; const EVENT_LINKS: EventLink[] = [ { @@ -97,6 +98,39 @@ const EVENT_LINKS: EventLink[] = [ forwardBufferMs: CAST_BUFFER_MS, backwardBufferMs: CAST_BUFFER_MS, }, + { + reverseLinkRelation: DEBUFF_APPLY, + linkingEventId: SPELLS.WINTERS_CHILL.id, + linkingEventType: EventType.ApplyDebuff, + linkRelation: DEBUFF_REMOVE, + referencedEventId: SPELLS.WINTERS_CHILL.id, + referencedEventType: EventType.RemoveDebuff, + maximumLinks: 1, + forwardBufferMs: 7000, + backwardBufferMs: CAST_BUFFER_MS, + }, + { + reverseLinkRelation: DEBUFF_APPLY, + linkingEventId: SPELLS.WINTERS_CHILL.id, + linkingEventType: EventType.ApplyDebuff, + linkRelation: SPELL_CAST, + referencedEventId: TALENTS.FLURRY_TALENT.id, + referencedEventType: EventType.Cast, + maximumLinks: 1, + forwardBufferMs: CAST_BUFFER_MS, + backwardBufferMs: 1000, + }, + { + reverseLinkRelation: DEBUFF_APPLY, + linkingEventId: SPELLS.WINTERS_CHILL.id, + linkingEventType: EventType.ApplyDebuff, + linkRelation: PRE_CAST, + referencedEventId: [SPELLS.FROSTBOLT.id, TALENTS.GLACIAL_SPIKE_TALENT.id], + referencedEventType: EventType.Cast, + maximumLinks: 1, + forwardBufferMs: CAST_BUFFER_MS, + backwardBufferMs: 1000, + }, ]; /** From 9031282490b2fd7fa504f3b70da97b0f54e5d80f Mon Sep 17 00:00:00 2001 From: Sharrq Date: Thu, 4 Jan 2024 22:06:07 -0500 Subject: [PATCH 06/30] remove logging --- src/analysis/retail/mage/frost/core/WintersChill.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/analysis/retail/mage/frost/core/WintersChill.tsx b/src/analysis/retail/mage/frost/core/WintersChill.tsx index 13a0609ce6b..1626471bcbd 100644 --- a/src/analysis/retail/mage/frost/core/WintersChill.tsx +++ b/src/analysis/retail/mage/frost/core/WintersChill.tsx @@ -104,7 +104,6 @@ class WintersChill extends Analyzer { }; get totalProcs() { - this.log(this.wintersChill); return this.wintersChill.length; } From f56f8bf2ef6c90c1f94ef23c57d5164728a2d632 Mon Sep 17 00:00:00 2001 From: Sharrq Date: Thu, 4 Jan 2024 22:06:54 -0500 Subject: [PATCH 07/30] fix flurry cast link --- src/analysis/retail/mage/frost/normalizers/CastLinkNormalizer.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/analysis/retail/mage/frost/normalizers/CastLinkNormalizer.ts b/src/analysis/retail/mage/frost/normalizers/CastLinkNormalizer.ts index b368b809a79..cff31ad282d 100644 --- a/src/analysis/retail/mage/frost/normalizers/CastLinkNormalizer.ts +++ b/src/analysis/retail/mage/frost/normalizers/CastLinkNormalizer.ts @@ -95,6 +95,7 @@ const EVENT_LINKS: EventLink[] = [ referencedEventId: TALENTS.FLURRY_TALENT.id, referencedEventType: EventType.Cast, maximumLinks: 1, + anyTarget: true, forwardBufferMs: CAST_BUFFER_MS, backwardBufferMs: CAST_BUFFER_MS, }, From 3edd5a4c9526edf30802a9b650ca348086224cce Mon Sep 17 00:00:00 2001 From: Sharrq Date: Thu, 4 Jan 2024 22:25:39 -0500 Subject: [PATCH 08/30] Fix Flurry Overlaps --- .../retail/mage/frost/core/BrainFreeze.tsx | 21 ++++++++++++++----- .../frost/normalizers/CastLinkNormalizer.ts | 11 ++++++++++ 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/analysis/retail/mage/frost/core/BrainFreeze.tsx b/src/analysis/retail/mage/frost/core/BrainFreeze.tsx index 35fd9c21690..1b96c31921a 100644 --- a/src/analysis/retail/mage/frost/core/BrainFreeze.tsx +++ b/src/analysis/retail/mage/frost/core/BrainFreeze.tsx @@ -6,19 +6,27 @@ import { SpellLink } from 'interface'; import Analyzer, { Options, SELECTED_PLAYER } from 'parser/core/Analyzer'; import Events, { CastEvent, + DamageEvent, ApplyBuffEvent, RemoveBuffEvent, RefreshBuffEvent, GetRelatedEvent, } from 'parser/core/Events'; 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_ORDER from 'parser/ui/STATISTIC_ORDER'; class BrainFreeze extends Analyzer { + static dependencies = { + enemies: Enemies, + }; + + protected enemies!: Enemies; + brainFreezeRefreshes = 0; - flurry: { timestamp: number; overlapped: boolean }[] = []; + flurry: { timestamp: number; damage: DamageEvent | undefined; overlapped: boolean }[] = []; brainFreeze: { apply: ApplyBuffEvent; remove: RemoveBuffEvent | undefined; expired: boolean }[] = []; @@ -39,9 +47,12 @@ class BrainFreeze extends Analyzer { } onFlurryCast(event: CastEvent) { + const damage: DamageEvent | undefined = GetRelatedEvent(event, 'SpellDamage'); + const enemy = damage && this.enemies.getEntity(damage); this.flurry.push({ timestamp: event.timestamp, - overlapped: this.selectedCombatant.hasBuff(SPELLS.BRAIN_FREEZE_BUFF.id, event.timestamp - 10), + damage: damage, + overlapped: enemy?.hasBuff(SPELLS.WINTERS_CHILL.id, event.timestamp - 10) || false, }); } @@ -119,7 +130,7 @@ class BrainFreeze extends Analyzer { get overlappedFlurryThresholds() { return { - actual: this.brainFreezeRefreshes, + actual: this.overlappedFlurries, isGreaterThan: { average: 0, major: 3, @@ -169,8 +180,8 @@ class BrainFreeze extends Analyzer { <> You cast and applied{' '} while the target still had the{' '} - debuff on them {this.brainFreezeRefreshes}{' '} - times. Casting applies 2 stacks of{' '} + debuff on them {this.overlappedFlurries} times. + Casting applies 2 stacks of{' '} to the target so you should always ensure you are spending both stacks before you cast and apply again. diff --git a/src/analysis/retail/mage/frost/normalizers/CastLinkNormalizer.ts b/src/analysis/retail/mage/frost/normalizers/CastLinkNormalizer.ts index cff31ad282d..cf5aa66a061 100644 --- a/src/analysis/retail/mage/frost/normalizers/CastLinkNormalizer.ts +++ b/src/analysis/retail/mage/frost/normalizers/CastLinkNormalizer.ts @@ -76,6 +76,17 @@ const EVENT_LINKS: EventLink[] = [ forwardBufferMs: 3000, backwardBufferMs: CAST_BUFFER_MS, }, + { + reverseLinkRelation: SPELL_CAST, + linkingEventId: TALENTS.FLURRY_TALENT.id, + linkingEventType: EventType.Cast, + linkRelation: SPELL_DAMAGE, + referencedEventId: SPELLS.FLURRY_DAMAGE.id, + referencedEventType: EventType.Damage, + anyTarget: true, + forwardBufferMs: 1500, + backwardBufferMs: CAST_BUFFER_MS, + }, { reverseLinkRelation: BUFF_APPLY, linkingEventId: SPELLS.BRAIN_FREEZE_BUFF.id, From 4f8d02e191056a83b377a6ac86841a522a2bbe01 Mon Sep 17 00:00:00 2001 From: Sharrq Date: Thu, 4 Jan 2024 22:30:22 -0500 Subject: [PATCH 09/30] remove apl --- src/analysis/retail/mage/frost/core/apl.tsx | 79 --------------------- 1 file changed, 79 deletions(-) delete mode 100644 src/analysis/retail/mage/frost/core/apl.tsx diff --git a/src/analysis/retail/mage/frost/core/apl.tsx b/src/analysis/retail/mage/frost/core/apl.tsx deleted file mode 100644 index be6cfe84d88..00000000000 --- a/src/analysis/retail/mage/frost/core/apl.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import SPELLS from 'common/SPELLS'; -import TALENTS from 'common/TALENTS/mage'; -import { SpellLink } from 'interface'; -import { suggestion as buildSuggestion } from 'parser/core/Analyzer'; -import { EventType } from 'parser/core/Events'; -import aplCheck, { build, Condition } from 'parser/shared/metrics/apl'; -import annotateTimeline from 'parser/shared/metrics/apl/annotate'; -import * as cnd from 'parser/shared/metrics/apl/conditions'; - -const precastFrostbolt: Condition<{ brainFreeze?: number; frostbolt?: number }> = { - key: 'precast-frostbolt', - init: () => ({}), - update: (state, event) => { - if ( - event.type === EventType.ApplyBuff && - event.ability.guid === TALENTS.BRAIN_FREEZE_TALENT.id - ) { - state.brainFreeze = event.timestamp; - } - - if ( - event.type === EventType.RemoveBuff && - event.ability.guid === TALENTS.BRAIN_FREEZE_TALENT.id - ) { - state.brainFreeze = undefined; - } - - if (event.type === EventType.Cast && event.ability.guid === SPELLS.FROSTBOLT.id) { - state.frostbolt = event.timestamp; - } - - return state; - }, - validate: (state, _event) => { - if (!state.brainFreeze) { - return false; - } - - // if brain freeze is up, did the previous cast overlap sufficiently? - if ((state.frostbolt || 0) > state.brainFreeze + 500) { - return false; - } - - // otherwise, brain freeze is up and the previous frostbolt doesn't count - return true; - }, - describe: () => ( - <> - was just applied - - ), -}; - -export const apl = build([ - { - spell: TALENTS.ICE_LANCE_TALENT, - condition: cnd.debuffPresent(SPELLS.WINTERS_CHILL), - }, - { - spell: SPELLS.FROSTBOLT, - condition: cnd.and(precastFrostbolt), - }, - { - spell: TALENTS.FLURRY_TALENT, - condition: cnd.buffPresent(TALENTS.BRAIN_FREEZE_TALENT), - }, - { spell: TALENTS.ICE_LANCE_TALENT, condition: cnd.buffPresent(TALENTS.FINGERS_OF_FROST_TALENT) }, - TALENTS.FROZEN_ORB_TALENT, - SPELLS.FROSTBOLT, -]); - -export const check = aplCheck(apl); - -export default buildSuggestion((events, info) => { - const { violations } = check(events, info); - annotateTimeline(violations); - - return undefined; -}); From 33ba8905b37bfe7311a434429c80576cbc1dea44 Mon Sep 17 00:00:00 2001 From: Sharrq Date: Thu, 4 Jan 2024 22:36:00 -0500 Subject: [PATCH 10/30] fix Ray of Frost cooldown --- src/analysis/retail/mage/frost/core/Abilities.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/analysis/retail/mage/frost/core/Abilities.tsx b/src/analysis/retail/mage/frost/core/Abilities.tsx index 27a61c9822d..4be7037c8e9 100644 --- a/src/analysis/retail/mage/frost/core/Abilities.tsx +++ b/src/analysis/retail/mage/frost/core/Abilities.tsx @@ -56,7 +56,7 @@ class Abilities extends CoreAbilities { gcd: { base: 1500, }, - cooldown: 80, + cooldown: 60, enabled: combatant.hasTalent(TALENTS.RAY_OF_FROST_TALENT), castEfficiency: { suggestion: true, From 9f04a5475d2948144e3accb37b1d94cf42b6c349 Mon Sep 17 00:00:00 2001 From: Sharrq Date: Thu, 4 Jan 2024 22:37:03 -0500 Subject: [PATCH 11/30] fix icy veins cooldown --- src/analysis/retail/mage/frost/core/Abilities.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/analysis/retail/mage/frost/core/Abilities.tsx b/src/analysis/retail/mage/frost/core/Abilities.tsx index 4be7037c8e9..d89f55f3130 100644 --- a/src/analysis/retail/mage/frost/core/Abilities.tsx +++ b/src/analysis/retail/mage/frost/core/Abilities.tsx @@ -112,7 +112,7 @@ class Abilities extends CoreAbilities { category: SPELL_CATEGORY.COOLDOWNS, enabled: combatant.hasTalent(TALENTS.ICY_VEINS_TALENT), gcd: null, - cooldown: 180, + cooldown: 120, castEfficiency: { suggestion: true, recommendedEfficiency: 0.9, From d356825bb97680b0272eda77649bd23440fa5ac0 Mon Sep 17 00:00:00 2001 From: Sharrq Date: Thu, 4 Jan 2024 22:52:06 -0500 Subject: [PATCH 12/30] add function for cleave damage --- .../frost/normalizers/CastLinkNormalizer.ts | 61 +++++++++++++------ 1 file changed, 41 insertions(+), 20 deletions(-) diff --git a/src/analysis/retail/mage/frost/normalizers/CastLinkNormalizer.ts b/src/analysis/retail/mage/frost/normalizers/CastLinkNormalizer.ts index cf5aa66a061..440572bdcbb 100644 --- a/src/analysis/retail/mage/frost/normalizers/CastLinkNormalizer.ts +++ b/src/analysis/retail/mage/frost/normalizers/CastLinkNormalizer.ts @@ -6,6 +6,7 @@ import { BeginCastEvent, RemoveBuffEvent, CastEvent, + DamageEvent, EventType, GetRelatedEvents, HasRelatedEvent, @@ -37,16 +38,7 @@ const EVENT_LINKS: EventLink[] = [ referencedEventType: EventType.Damage, anyTarget: true, additionalCondition(linkingEvent, referencedEvent): boolean { - if (!linkingEvent || !referencedEvent) { - return false; - } - const castTarget = - HasTarget(linkingEvent) && - encodeTargetString(linkingEvent.targetID, linkingEvent.targetInstance); - const damageTarget = - HasTarget(referencedEvent) && - encodeTargetString(referencedEvent.targetID, referencedEvent.targetInstance); - return castTarget === damageTarget; + return isCleaveDamage(linkingEvent as CastEvent, referencedEvent as DamageEvent) === false; }, maximumLinks: 1, forwardBufferMs: 3000, @@ -61,16 +53,7 @@ const EVENT_LINKS: EventLink[] = [ referencedEventType: EventType.Damage, anyTarget: true, additionalCondition(linkingEvent, referencedEvent): boolean { - if (!linkingEvent || !referencedEvent) { - return false; - } - const castTarget = - HasTarget(linkingEvent) && - encodeTargetString(linkingEvent.targetID, linkingEvent.targetInstance); - const damageTarget = - HasTarget(referencedEvent) && - encodeTargetString(referencedEvent.targetID, referencedEvent.targetInstance); - return castTarget !== damageTarget; + return isCleaveDamage(linkingEvent as CastEvent, referencedEvent as DamageEvent) === true; }, maximumLinks: 1, forwardBufferMs: 3000, @@ -87,6 +70,36 @@ const EVENT_LINKS: EventLink[] = [ forwardBufferMs: 1500, backwardBufferMs: CAST_BUFFER_MS, }, + { + reverseLinkRelation: SPELL_CAST, + linkingEventId: TALENTS.ICE_LANCE_TALENT.id, + linkingEventType: EventType.Cast, + linkRelation: SPELL_DAMAGE, + referencedEventId: SPELLS.ICE_LANCE_DAMAGE.id, + referencedEventType: EventType.Cast, + anyTarget: true, + additionalCondition(linkingEvent, referencedEvent): boolean { + return isCleaveDamage(linkingEvent as CastEvent, referencedEvent as DamageEvent) === false; + }, + maximumLinks: 1, + forwardBufferMs: CAST_BUFFER_MS, + backwardBufferMs: 1000, + }, + { + reverseLinkRelation: SPELL_CAST, + linkingEventId: TALENTS.ICE_LANCE_TALENT.id, + linkingEventType: EventType.Cast, + linkRelation: CLEAVE_DAMAGE, + referencedEventId: SPELLS.ICE_LANCE_DAMAGE.id, + referencedEventType: EventType.Cast, + anyTarget: true, + additionalCondition(linkingEvent, referencedEvent): boolean { + return isCleaveDamage(linkingEvent as CastEvent, referencedEvent as DamageEvent) === true; + }, + maximumLinks: 1, + forwardBufferMs: CAST_BUFFER_MS, + backwardBufferMs: 1000, + }, { reverseLinkRelation: BUFF_APPLY, linkingEventId: SPELLS.BRAIN_FREEZE_BUFF.id, @@ -190,4 +203,12 @@ export function isProcExpired(event: RemoveBuffEvent, spenderId: number): boolea return !cast || cast.ability.guid !== spenderId; } +export function isCleaveDamage(castEvent: CastEvent, damageEvent: DamageEvent): boolean { + const castTarget = + HasTarget(castEvent) && encodeTargetString(castEvent.targetID, castEvent.targetInstance); + const damageTarget = + HasTarget(damageEvent) && encodeTargetString(damageEvent.targetID, damageEvent.targetInstance); + return castTarget !== damageTarget; +} + export default CastLinkNormalizer; From 9505b6b2f455e6f46b3855b78626c49914223b02 Mon Sep 17 00:00:00 2001 From: Sharrq Date: Fri, 5 Jan 2024 20:29:08 -0500 Subject: [PATCH 13/30] Fingers of Frost --- .../retail/mage/frost/CombatLogParser.ts | 4 +- .../retail/mage/frost/checklist/Module.tsx | 10 +- .../{MunchedProcs.tsx => FingersOfFrost.tsx} | 112 +++++++++++------ .../retail/mage/frost/core/IceLance.tsx | 119 ++++-------------- .../frost/normalizers/CastLinkNormalizer.ts | 55 +++++++- 5 files changed, 150 insertions(+), 150 deletions(-) rename src/analysis/retail/mage/frost/core/{MunchedProcs.tsx => FingersOfFrost.tsx} (58%) diff --git a/src/analysis/retail/mage/frost/CombatLogParser.ts b/src/analysis/retail/mage/frost/CombatLogParser.ts index c5df5470261..988e5836d54 100644 --- a/src/analysis/retail/mage/frost/CombatLogParser.ts +++ b/src/analysis/retail/mage/frost/CombatLogParser.ts @@ -23,7 +23,7 @@ import Buffs from './core/Buffs'; import CooldownThroughputTracker from './core/CooldownThroughputTracker'; import IceLance from './core/IceLance'; import IcyVeins from './core/IcyVeins'; -import MunchedProcs from './core/MunchedProcs'; +import FingersOfFrost from './core/FingersOfFrost'; import WintersChill from './core/WintersChill'; //Talents @@ -64,7 +64,7 @@ class CombatLogParser extends CoreCombatLogParser { iceLance: IceLance, icyVeins: IcyVeins, arcaneIntellect: ArcaneIntellect, - munchedProcs: MunchedProcs, + fingersOfFrost: FingersOfFrost, // Talents - Frost boneChilling: BoneChilling, diff --git a/src/analysis/retail/mage/frost/checklist/Module.tsx b/src/analysis/retail/mage/frost/checklist/Module.tsx index 56e132f0d7f..8ed13f7da7a 100644 --- a/src/analysis/retail/mage/frost/checklist/Module.tsx +++ b/src/analysis/retail/mage/frost/checklist/Module.tsx @@ -8,7 +8,7 @@ import AlwaysBeCasting from '../core/AlwaysBeCasting'; import BrainFreeze from '../core/BrainFreeze'; import IceLance from '../core/IceLance'; import IcyVeins from '../core/IcyVeins'; -import MunchedProcs from '../core/MunchedProcs'; +import FingersOfFrost from '../core/FingersOfFrost'; import WaterElemental from '../talents/WaterElemental'; import WintersChill from '../core/WintersChill'; import ThermalVoid from '../talents/ThermalVoid'; @@ -20,7 +20,7 @@ class Checklist extends BaseChecklist { combatants: Combatants, castEfficiency: CastEfficiency, icyVeins: IcyVeins, - munchedProcs: MunchedProcs, + fingersOfFrost: FingersOfFrost, brainFreeze: BrainFreeze, iceLance: IceLance, thermalVoid: ThermalVoid, @@ -34,7 +34,7 @@ class Checklist extends BaseChecklist { protected combatants!: Combatants; protected castEfficiency!: CastEfficiency; protected icyVeins!: IcyVeins; - protected munchedProcs!: MunchedProcs; + protected fingersOfFrost!: FingersOfFrost; protected brainFreeze!: BrainFreeze; protected iceLance!: IceLance; protected thermalVoid!: ThermalVoid; @@ -55,11 +55,11 @@ class Checklist extends BaseChecklist { downtimeSuggestionThresholds: this.alwaysBeCasting.overrideDowntimeSuggestionThresholds, icyVeinsActiveTime: this.icyVeins.icyVeinsActiveTimeThresholds, - munchedProcs: this.munchedProcs.munchedProcsThresholds, + munchedProcs: this.fingersOfFrost.munchedProcsThresholds, brainFreezeUtilization: this.brainFreeze.brainFreezeUtilizationThresholds, brainFreezeOverwrites: this.brainFreeze.brainFreezeOverwritenThresholds, brainFreezeExpired: this.brainFreeze.brainFreezeExpiredThresholds, - fingersOfFrostUtilization: this.iceLance.fingersProcUtilizationThresholds, + fingersOfFrostUtilization: this.fingersOfFrost.fingersProcUtilizationThresholds, iceLanceNotShattered: this.iceLance.nonShatteredIceLanceThresholds, wintersChillShatter: this.wintersChill.wintersChillShatterThresholds, wintersChillHardCasts: this.wintersChill.wintersChillPreCastThresholds, diff --git a/src/analysis/retail/mage/frost/core/MunchedProcs.tsx b/src/analysis/retail/mage/frost/core/FingersOfFrost.tsx similarity index 58% rename from src/analysis/retail/mage/frost/core/MunchedProcs.tsx rename to src/analysis/retail/mage/frost/core/FingersOfFrost.tsx index 0dc3e0c6650..1b3ad8e6dc2 100644 --- a/src/analysis/retail/mage/frost/core/MunchedProcs.tsx +++ b/src/analysis/retail/mage/frost/core/FingersOfFrost.tsx @@ -1,37 +1,40 @@ -import { Trans } from '@lingui/macro'; -import { formatPercentage } from 'common/format'; +import { formatPercentage, formatNumber } from 'common/format'; import SPELLS from 'common/SPELLS'; import TALENTS from 'common/TALENTS/mage'; import { SpellLink } from 'interface'; import Analyzer, { SELECTED_PLAYER, Options } from 'parser/core/Analyzer'; -import Events, { DamageEvent, ApplyBuffEvent, ApplyBuffStackEvent } from 'parser/core/Events'; +import Events, { + DamageEvent, + CastEvent, + ApplyBuffEvent, + RemoveBuffEvent, + ApplyBuffStackEvent, + GetRelatedEvent, +} from 'parser/core/Events'; import { ThresholdStyle, When } from 'parser/core/ParseResults'; -import AbilityTracker from 'parser/shared/modules/AbilityTracker'; import Enemies from 'parser/shared/modules/Enemies'; -import EventHistory from 'parser/shared/modules/EventHistory'; import BoringSpellValueText from 'parser/ui/BoringSpellValueText'; import Statistic from 'parser/ui/Statistic'; import STATISTIC_ORDER from 'parser/ui/STATISTIC_ORDER'; +import { SHATTER_DEBUFFS } from '../../shared'; -class MunchedProcs extends Analyzer { +class FingersOfFrost extends Analyzer { static dependencies = { - abilityTracker: AbilityTracker, enemies: Enemies, - eventHistory: EventHistory, }; - protected abilityTracker!: AbilityTracker; - protected eventHistory!: EventHistory; protected enemies!: Enemies; - munchedProcs = 0; - totalFingersProcs = 0; + fingers: { + apply: ApplyBuffEvent | ApplyBuffStackEvent; + remove: RemoveBuffEvent | undefined; + spender: CastEvent | undefined; + expired: boolean; + munched: boolean; + spendDelay: number | undefined; + }[] = []; constructor(options: Options) { super(options); - this.addEventListener( - Events.damage.by(SELECTED_PLAYER).spell(SPELLS.ICE_LANCE_DAMAGE), - this.onIceLanceDamage, - ); this.addEventListener( Events.applybuff.by(SELECTED_PLAYER).spell(SPELLS.FINGERS_OF_FROST_BUFF), this.onFingersProc, @@ -42,28 +45,49 @@ class MunchedProcs extends Analyzer { ); } - onIceLanceDamage(event: DamageEvent) { - const enemy = this.enemies.getEntity(event); - if (!enemy || !enemy.hasBuff(SPELLS.WINTERS_CHILL.id)) { - return; - } + onFingersProc(event: ApplyBuffEvent | ApplyBuffStackEvent) { + const remove: RemoveBuffEvent | undefined = GetRelatedEvent(event, 'BuffRemove'); + const spender: CastEvent | undefined = remove && GetRelatedEvent(remove, 'SpellCast'); + const damage: DamageEvent | undefined = spender && GetRelatedEvent(spender, 'SpellDamage'); + const enemy = damage && this.enemies.getEntity(damage); + this.log(spender?.timestamp); + this.log(damage?.timestamp); + this.fingers.push({ + apply: event, + remove: remove, + spender: spender, + expired: !spender, + munched: + SHATTER_DEBUFFS.some((effect) => enemy?.hasBuff(effect.id, damage?.timestamp)) || false, + spendDelay: spender && spender.timestamp - event.timestamp, + }); + } - const iceLanceCast = this.eventHistory.last( - 1, - undefined, - Events.cast.by(SELECTED_PLAYER).spell(TALENTS.ICE_LANCE_TALENT), - )[0]; - if (this.selectedCombatant.hasBuff(SPELLS.FINGERS_OF_FROST_BUFF.id, iceLanceCast.timestamp)) { - this.munchedProcs += 1; - } + get expiredProcs() { + return this.fingers.filter((f) => f.expired).length; } - onFingersProc(event: ApplyBuffEvent | ApplyBuffStackEvent) { - this.totalFingersProcs += 1; + get averageSpendDelaySeconds() { + let spendDelay = 0; + this.fingers.forEach((f) => f.spendDelay && (spendDelay += f.spendDelay)); + this.log(spendDelay / this.fingers.filter((f) => f.spendDelay).length / 1000); + return spendDelay / this.fingers.filter((f) => f.spendDelay).length / 1000; + } + + get usedFingersProcs() { + return this.totalProcs - this.expiredProcs; + } + + get munchedProcs() { + return this.fingers.filter((f) => f.munched).length; + } + + get totalProcs() { + return this.fingers.length; } get munchedPercent() { - return this.munchedProcs / this.totalFingersProcs; + return this.munchedProcs / this.totalProcs; } get munchedProcsThresholds() { @@ -78,13 +102,25 @@ class MunchedProcs extends Analyzer { }; } + get fingersProcUtilizationThresholds() { + return { + actual: 1 - this.expiredProcs / this.totalProcs || 0, + isLessThan: { + minor: 0.95, + average: 0.85, + major: 0.7, + }, + style: ThresholdStyle.PERCENTAGE, + }; + } + suggestions(when: When) { when(this.munchedProcsThresholds).addSuggestion((suggest, actual, recommended) => suggest( <> You wasted (munched) {this.munchedProcs}{' '} procs ( - {formatPercentage(this.munchedPercent)} of total procs). Because of the way{' '} + {formatPercentage(this.munchedPercent)}% of total procs). Because of the way{' '} works, this is sometimes unavoidable (i.e. you get a proc while you are using a{' '} proc), but if you have both a{' '} @@ -97,11 +133,7 @@ class MunchedProcs extends Analyzer { , ) .icon(TALENTS.FINGERS_OF_FROST_TALENT.icon) - .actual( - - {formatPercentage(actual)}% procs wasted - , - ) + .actual(`${formatPercentage(actual)}% procs wasted`) .recommended(formatPercentage(recommended)), ); } @@ -125,10 +157,12 @@ class MunchedProcs extends Analyzer { > {formatPercentage(this.munchedPercent, 0)}% Munched Fingers of Frost procs +
+ {formatNumber(this.averageSpendDelaySeconds)}s Avg. delay to spend procs
); } } -export default MunchedProcs; +export default FingersOfFrost; diff --git a/src/analysis/retail/mage/frost/core/IceLance.tsx b/src/analysis/retail/mage/frost/core/IceLance.tsx index e79807220a0..9b96f65b67e 100644 --- a/src/analysis/retail/mage/frost/core/IceLance.tsx +++ b/src/analysis/retail/mage/frost/core/IceLance.tsx @@ -1,16 +1,12 @@ -import { Trans } from '@lingui/macro'; -import { MS_BUFFER_100, SHATTER_DEBUFFS } from 'analysis/retail/mage/shared'; +import { SHATTER_DEBUFFS } from 'analysis/retail/mage/shared'; import { formatPercentage } from 'common/format'; import SPELLS from 'common/SPELLS'; import TALENTS from 'common/TALENTS/mage'; import { SpellLink } from 'interface'; import Analyzer, { SELECTED_PLAYER, Options } from 'parser/core/Analyzer'; -import Events, { CastEvent, DamageEvent, ChangeBuffStackEvent } from 'parser/core/Events'; +import Events, { CastEvent, DamageEvent, GetRelatedEvent } from 'parser/core/Events'; import { When, ThresholdStyle } from 'parser/core/ParseResults'; -import AbilityTracker from 'parser/shared/modules/AbilityTracker'; -import Enemies, { encodeTargetString } from 'parser/shared/modules/Enemies'; -import EventHistory from 'parser/shared/modules/EventHistory'; -import SpellUsable from 'parser/shared/modules/SpellUsable'; +import Enemies from 'parser/shared/modules/Enemies'; import BoringSpellValueText from 'parser/ui/BoringSpellValueText'; import Statistic from 'parser/ui/Statistic'; import STATISTIC_ORDER from 'parser/ui/STATISTIC_ORDER'; @@ -18,23 +14,10 @@ import STATISTIC_ORDER from 'parser/ui/STATISTIC_ORDER'; class IceLance extends Analyzer { static dependencies = { enemies: Enemies, - abilityTracker: AbilityTracker, - eventHistory: EventHistory, - spellUsable: SpellUsable, }; protected enemies!: Enemies; - protected abilityTracker!: AbilityTracker; - protected eventHistory!: EventHistory; - protected spellUsable!: SpellUsable; - hadFingersProc = false; - iceLanceTargetId = ''; - nonShatteredCasts = 0; - - iceLanceCastTimestamp = 0; - totalFingersProcs = 0; - overwrittenFingersProcs = 0; - expiredFingersProcs = 0; + icelance: { shattered: boolean; hadFingers: boolean; cleaved: boolean }[] = []; constructor(options: Options) { super(options); @@ -42,90 +25,34 @@ class IceLance extends Analyzer { Events.cast.by(SELECTED_PLAYER).spell(TALENTS.ICE_LANCE_TALENT), this.onCast, ); - this.addEventListener( - Events.damage.by(SELECTED_PLAYER).spell(SPELLS.ICE_LANCE_DAMAGE), - this.onDamage, - ); - this.addEventListener( - Events.changebuffstack.by(SELECTED_PLAYER).spell(SPELLS.FINGERS_OF_FROST_BUFF), - this.onFingersStackChange, - ); } onCast(event: CastEvent) { - this.iceLanceCastTimestamp = event.timestamp; - if (event.targetID) { - this.iceLanceTargetId = encodeTargetString(event.targetID, event.targetInstance); - } - this.hadFingersProc = false; - if (this.selectedCombatant.hasBuff(SPELLS.FINGERS_OF_FROST_BUFF.id)) { - this.hadFingersProc = true; - } - } - - onDamage(event: DamageEvent) { - const damageTarget = encodeTargetString(event.targetID, event.targetInstance); - if (this.iceLanceTargetId !== damageTarget) { - return; - } - - const enemy = this.enemies.getEntity(event); - if ( - enemy && - !SHATTER_DEBUFFS.some((effect) => enemy.hasBuff(effect.id, event.timestamp)) && - !this.hadFingersProc - ) { - this.nonShatteredCasts += 1; - } - } - - onFingersStackChange(event: ChangeBuffStackEvent) { - // FoF overcaps don't show as a refreshbuff, instead they are a stack lost followed immediately by a gain - const stackChange = event.stacksGained; - if (stackChange > 0) { - this.totalFingersProcs += stackChange; - } else if ( - this.iceLanceCastTimestamp && - this.iceLanceCastTimestamp + MS_BUFFER_100 > event.timestamp - ) { - // just cast ice lance, so this stack removal probably a proc used - } else if (event.newStacks === 0) { - this.expiredFingersProcs += -stackChange; // stacks zero out, must be expiration - } else { - this.overwrittenFingersProcs += -stackChange; // stacks don't zero, this is an overwrite - } + const damage: DamageEvent | undefined = GetRelatedEvent(event, 'SpellDamage'); + const enemy = damage && this.enemies.getEntity(damage); + const cleave: DamageEvent | undefined = GetRelatedEvent(event, 'CleaveDamage'); + this.icelance.push({ + shattered: + SHATTER_DEBUFFS.some((effect) => enemy?.hasBuff(effect.id, damage?.timestamp)) || false, + hadFingers: this.selectedCombatant.hasBuff( + SPELLS.FINGERS_OF_FROST_BUFF.id, + event.timestamp - 10, + ), + cleaved: cleave ? true : false, + }); } - get wastedFingersProcs() { - return this.expiredFingersProcs + this.overwrittenFingersProcs; - } - - get usedFingersProcs() { - return this.totalFingersProcs - this.wastedFingersProcs; + get nonShatteredCasts() { + return this.icelance.filter((il) => !il.shattered).length; } get shatteredPercent() { - return ( - 1 - this.nonShatteredCasts / this.abilityTracker.getAbility(TALENTS.ICE_LANCE_TALENT.id).casts - ); - } - - get fingersProcUtilizationThresholds() { - return { - actual: 1 - this.wastedFingersProcs / this.totalFingersProcs || 0, - isLessThan: { - minor: 0.95, - average: 0.85, - major: 0.7, - }, - style: ThresholdStyle.PERCENTAGE, - }; + return 1 - this.nonShatteredCasts / this.icelance.length; } get nonShatteredIceLanceThresholds() { return { - actual: - this.nonShatteredCasts / this.abilityTracker.getAbility(TALENTS.ICE_LANCE_TALENT.id).casts, + actual: this.nonShatteredCasts / this.icelance.length, isGreaterThan: { minor: 0.05, average: 0.15, @@ -148,11 +75,7 @@ class IceLance extends Analyzer { , ) .icon(TALENTS.ICE_LANCE_TALENT.icon) - .actual( - - {formatPercentage(actual)}% missed - , - ) + .actual(`${formatPercentage(actual)}% missed`) .recommended(`<${formatPercentage(recommended)}% is recommended`), ); } diff --git a/src/analysis/retail/mage/frost/normalizers/CastLinkNormalizer.ts b/src/analysis/retail/mage/frost/normalizers/CastLinkNormalizer.ts index 440572bdcbb..cb8b42c1b37 100644 --- a/src/analysis/retail/mage/frost/normalizers/CastLinkNormalizer.ts +++ b/src/analysis/retail/mage/frost/normalizers/CastLinkNormalizer.ts @@ -76,14 +76,14 @@ const EVENT_LINKS: EventLink[] = [ linkingEventType: EventType.Cast, linkRelation: SPELL_DAMAGE, referencedEventId: SPELLS.ICE_LANCE_DAMAGE.id, - referencedEventType: EventType.Cast, + referencedEventType: EventType.Damage, anyTarget: true, additionalCondition(linkingEvent, referencedEvent): boolean { return isCleaveDamage(linkingEvent as CastEvent, referencedEvent as DamageEvent) === false; }, maximumLinks: 1, - forwardBufferMs: CAST_BUFFER_MS, - backwardBufferMs: 1000, + forwardBufferMs: 1000, + backwardBufferMs: CAST_BUFFER_MS, }, { reverseLinkRelation: SPELL_CAST, @@ -91,14 +91,14 @@ const EVENT_LINKS: EventLink[] = [ linkingEventType: EventType.Cast, linkRelation: CLEAVE_DAMAGE, referencedEventId: SPELLS.ICE_LANCE_DAMAGE.id, - referencedEventType: EventType.Cast, + referencedEventType: EventType.Damage, anyTarget: true, additionalCondition(linkingEvent, referencedEvent): boolean { return isCleaveDamage(linkingEvent as CastEvent, referencedEvent as DamageEvent) === true; }, maximumLinks: 1, - forwardBufferMs: CAST_BUFFER_MS, - backwardBufferMs: 1000, + forwardBufferMs: 1000, + backwardBufferMs: CAST_BUFFER_MS, }, { reverseLinkRelation: BUFF_APPLY, @@ -152,10 +152,53 @@ const EVENT_LINKS: EventLink[] = [ linkRelation: PRE_CAST, referencedEventId: [SPELLS.FROSTBOLT.id, TALENTS.GLACIAL_SPIKE_TALENT.id], referencedEventType: EventType.Cast, + anyTarget: true, maximumLinks: 1, forwardBufferMs: CAST_BUFFER_MS, backwardBufferMs: 1000, }, + { + reverseLinkRelation: BUFF_APPLY, + linkingEventId: SPELLS.FINGERS_OF_FROST_BUFF.id, + linkingEventType: [EventType.ApplyBuff, EventType.ApplyBuffStack], + linkRelation: BUFF_REMOVE, + referencedEventId: SPELLS.FINGERS_OF_FROST_BUFF.id, + referencedEventType: [EventType.RemoveBuff, EventType.RemoveBuffStack], + anyTarget: true, + additionalCondition(linkingEvent, referencedEvent): boolean { + return !HasRelatedEvent(referencedEvent, BUFF_APPLY); + }, + maximumLinks: 1, + forwardBufferMs: 18_000, + backwardBufferMs: CAST_BUFFER_MS, + }, + { + reverseLinkRelation: BUFF_REMOVE, + linkingEventId: SPELLS.FINGERS_OF_FROST_BUFF.id, + linkingEventType: [EventType.RemoveBuff, EventType.RemoveBuffStack], + linkRelation: SPELL_CAST, + referencedEventId: TALENTS.ICE_LANCE_TALENT.id, + referencedEventType: EventType.Cast, + anyTarget: true, + additionalCondition(linkingEvent, referencedEvent): boolean { + return !HasRelatedEvent(referencedEvent, BUFF_REMOVE); + }, + maximumLinks: 1, + forwardBufferMs: CAST_BUFFER_MS, + backwardBufferMs: CAST_BUFFER_MS, + }, + { + reverseLinkRelation: BUFF_REMOVE, + linkingEventId: SPELLS.FINGERS_OF_FROST_BUFF.id, + linkingEventType: EventType.RemoveBuff, + linkRelation: SPELL_CAST, + referencedEventId: TALENTS.ICE_LANCE_TALENT.id, + referencedEventType: EventType.Cast, + anyTarget: true, + maximumLinks: 1, + forwardBufferMs: CAST_BUFFER_MS, + backwardBufferMs: CAST_BUFFER_MS, + }, ]; /** From 2ebe9da57d0d5227975bb7902f9f8859d2944d30 Mon Sep 17 00:00:00 2001 From: Sharrq Date: Fri, 5 Jan 2024 20:44:32 -0500 Subject: [PATCH 14/30] Icy Veins --- .../retail/mage/frost/core/IcyVeins.tsx | 82 +++++++++++++------ .../frost/normalizers/CastLinkNormalizer.ts | 12 +++ 2 files changed, 69 insertions(+), 25 deletions(-) diff --git a/src/analysis/retail/mage/frost/core/IcyVeins.tsx b/src/analysis/retail/mage/frost/core/IcyVeins.tsx index 4161152b4b6..9207e3ddd24 100644 --- a/src/analysis/retail/mage/frost/core/IcyVeins.tsx +++ b/src/analysis/retail/mage/frost/core/IcyVeins.tsx @@ -4,7 +4,13 @@ import TALENTS from 'common/TALENTS/mage'; import { SpellLink } from 'interface'; import Analyzer, { Options } from 'parser/core/Analyzer'; import { SELECTED_PLAYER } from 'parser/core/EventFilter'; -import Events, { RemoveBuffEvent } from 'parser/core/Events'; +import Events, { + EventType, + GetRelatedEvent, + ApplyBuffEvent, + RemoveBuffEvent, + FightEndEvent, +} from 'parser/core/Events'; import { When, ThresholdStyle } from 'parser/core/ParseResults'; import AbilityTracker from 'parser/shared/modules/AbilityTracker'; import EventHistory from 'parser/shared/modules/EventHistory'; @@ -12,51 +18,77 @@ import FilteredActiveTime from 'parser/shared/modules/FilteredActiveTime'; import BoringSpellValueText from 'parser/ui/BoringSpellValueText'; import Statistic from 'parser/ui/Statistic'; import STATISTIC_ORDER from 'parser/ui/STATISTIC_ORDER'; +import AlwaysBeCasting from './AlwaysBeCasting'; class IcyVeins extends Analyzer { static dependencies = { eventHistory: EventHistory, filteredActiveTime: FilteredActiveTime, abilityTracker: AbilityTracker, + alwaysBeCasting: AlwaysBeCasting, }; protected eventHistory!: EventHistory; protected filteredActiveTime!: FilteredActiveTime; protected abilityTracker!: AbilityTracker; + protected alwaysBeCasting!: AlwaysBeCasting; - activeTime = 0; + activeTime: number[] = []; + buffApplies: number = 0; constructor(options: Options) { super(options); + this.addEventListener( + Events.applybuff.by(SELECTED_PLAYER).spell(TALENTS.ICY_VEINS_TALENT), + this.onIcyVeinsStart, + ); this.addEventListener( Events.removebuff.by(SELECTED_PLAYER).spell(TALENTS.ICY_VEINS_TALENT), - this.onIcyVeinsRemoved, + this.onIcyVeinsEnd, ); + this.addEventListener(Events.fightend, this.onFightEnd); } - onIcyVeinsRemoved(event: RemoveBuffEvent) { - const buffApplied = this.eventHistory.last( - 1, - undefined, - Events.applybuff.by(SELECTED_PLAYER).spell(TALENTS.ICY_VEINS_TALENT), - )[0].timestamp; - const uptime = this.filteredActiveTime.getActiveTime(buffApplied, event.timestamp); - this.activeTime += uptime; + onIcyVeinsStart(event: ApplyBuffEvent) { + this.buffApplies += 1; } - get buffUptime() { - return this.selectedCombatant.getBuffUptime(TALENTS.ICY_VEINS_TALENT.id); + onIcyVeinsEnd(event: RemoveBuffEvent) { + const buffApply: ApplyBuffEvent | undefined = GetRelatedEvent(event, 'BuffApply'); + if (!buffApply) { + return; + } + const icyVeinsDuration = event.timestamp - buffApply.timestamp; + this.activeTime[buffApply.timestamp] = + icyVeinsDuration - + this.alwaysBeCasting.getActiveTimeMillisecondsInWindow(buffApply.timestamp, event.timestamp); } - get percentActiveTime() { - return this.activeTime / this.buffUptime || 0; + onFightEnd(event: FightEndEvent) { + const buffApply = this.eventHistory.getEvents(EventType.ApplyBuff, { + spell: TALENTS.ICY_VEINS_TALENT, + count: 1, + })[0]; + if (!this.selectedCombatant.hasBuff(TALENTS.ICY_VEINS_TALENT.id) || !buffApply) { + return; + } + const icyVeinsDuration = event.timestamp - buffApply.timestamp; + this.activeTime[buffApply.timestamp] = + icyVeinsDuration - + this.alwaysBeCasting.getActiveTimeMillisecondsInWindow(buffApply.timestamp, event.timestamp); } - get downtimeSeconds() { - return (this.buffUptime - this.activeTime) / 1000; + icyVeinsDowntime = () => { + let activeTime = 0; + this.activeTime.forEach((c) => (activeTime += c)); + return activeTime / 1000; + }; + + get buffUptime() { + return this.selectedCombatant.getBuffUptime(TALENTS.ICY_VEINS_TALENT.id) / 1000; } - get averageDowntime() { - return this.downtimeSeconds / this.abilityTracker.getAbility(TALENTS.ICY_VEINS_TALENT.id).casts; + get percentActiveTime() { + return 1 - this.icyVeinsDowntime() / this.buffUptime; } get icyVeinsActiveTimeThresholds() { @@ -75,12 +107,12 @@ class IcyVeins extends Analyzer { when(this.icyVeinsActiveTimeThresholds).addSuggestion((suggest, actual, recommended) => suggest( <> - You spent {formatNumber(this.downtimeSeconds)} seconds ( - {formatNumber(this.averageDowntime)}s per cast) not casting anything while{' '} - was active. Because a large portion of your - damage comes from Icy Veins, you should ensure that you are getting the most out of it - every time it is cast. While sometimes this is out of your control (you got targeted by a - mechanic at the worst possible time), you should try to minimize that risk by casting{' '} + You spent {formatNumber(this.icyVeinsDowntime())} seconds ( + {formatNumber(this.icyVeinsDowntime() / this.buffApplies)}s per cast) not casting anything + while was active. Because a large portion + of your damage comes from Icy Veins, you should ensure that you are getting the most out + of it every time it is cast. While sometimes this is out of your control (you got targeted + by a mechanic at the worst possible time), you should try to minimize that risk by casting{' '} when you are at a low risk of being interrupted or when the target is vulnerable. , diff --git a/src/analysis/retail/mage/frost/normalizers/CastLinkNormalizer.ts b/src/analysis/retail/mage/frost/normalizers/CastLinkNormalizer.ts index cb8b42c1b37..ce6686eea47 100644 --- a/src/analysis/retail/mage/frost/normalizers/CastLinkNormalizer.ts +++ b/src/analysis/retail/mage/frost/normalizers/CastLinkNormalizer.ts @@ -199,6 +199,18 @@ const EVENT_LINKS: EventLink[] = [ forwardBufferMs: CAST_BUFFER_MS, backwardBufferMs: CAST_BUFFER_MS, }, + { + reverseLinkRelation: BUFF_APPLY, + linkingEventId: TALENTS.ICY_VEINS_TALENT.id, + linkingEventType: EventType.ApplyBuff, + linkRelation: BUFF_REMOVE, + referencedEventId: TALENTS.ICY_VEINS_TALENT.id, + referencedEventType: EventType.RemoveBuff, + anyTarget: true, + maximumLinks: 1, + forwardBufferMs: 60_000, + backwardBufferMs: CAST_BUFFER_MS, + }, ]; /** From b4f9ac023eb81258812c8564c6c4f795fc3defc2 Mon Sep 17 00:00:00 2001 From: Sharrq Date: Fri, 5 Jan 2024 20:46:20 -0500 Subject: [PATCH 15/30] IV dependencies --- src/analysis/retail/mage/frost/core/IcyVeins.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/analysis/retail/mage/frost/core/IcyVeins.tsx b/src/analysis/retail/mage/frost/core/IcyVeins.tsx index 9207e3ddd24..9283c58d68f 100644 --- a/src/analysis/retail/mage/frost/core/IcyVeins.tsx +++ b/src/analysis/retail/mage/frost/core/IcyVeins.tsx @@ -12,9 +12,7 @@ import Events, { FightEndEvent, } from 'parser/core/Events'; import { When, ThresholdStyle } from 'parser/core/ParseResults'; -import AbilityTracker from 'parser/shared/modules/AbilityTracker'; import EventHistory from 'parser/shared/modules/EventHistory'; -import FilteredActiveTime from 'parser/shared/modules/FilteredActiveTime'; import BoringSpellValueText from 'parser/ui/BoringSpellValueText'; import Statistic from 'parser/ui/Statistic'; import STATISTIC_ORDER from 'parser/ui/STATISTIC_ORDER'; @@ -23,13 +21,9 @@ import AlwaysBeCasting from './AlwaysBeCasting'; class IcyVeins extends Analyzer { static dependencies = { eventHistory: EventHistory, - filteredActiveTime: FilteredActiveTime, - abilityTracker: AbilityTracker, alwaysBeCasting: AlwaysBeCasting, }; protected eventHistory!: EventHistory; - protected filteredActiveTime!: FilteredActiveTime; - protected abilityTracker!: AbilityTracker; protected alwaysBeCasting!: AlwaysBeCasting; activeTime: number[] = []; From f88d65eba0a43aeb99b2cf89ef543fcf656a7384 Mon Sep 17 00:00:00 2001 From: Sharrq Date: Fri, 5 Jan 2024 20:47:07 -0500 Subject: [PATCH 16/30] Winters Chill dependencies --- src/analysis/retail/mage/frost/core/WintersChill.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/analysis/retail/mage/frost/core/WintersChill.tsx b/src/analysis/retail/mage/frost/core/WintersChill.tsx index 1626471bcbd..14f6f58dee4 100644 --- a/src/analysis/retail/mage/frost/core/WintersChill.tsx +++ b/src/analysis/retail/mage/frost/core/WintersChill.tsx @@ -13,7 +13,6 @@ import Events, { } from 'parser/core/Events'; import { When, ThresholdStyle } from 'parser/core/ParseResults'; import Enemies from 'parser/shared/modules/Enemies'; -import EventHistory from 'parser/shared/modules/EventHistory'; import BoringSpellValueText from 'parser/ui/BoringSpellValueText'; import Statistic from 'parser/ui/Statistic'; import STATISTIC_ORDER from 'parser/ui/STATISTIC_ORDER'; @@ -23,10 +22,8 @@ const WINTERS_CHILL_SPENDERS = [SPELLS.ICE_LANCE_DAMAGE.id, SPELLS.GLACIAL_SPIKE class WintersChill extends Analyzer { static dependencies = { enemies: Enemies, - eventHistory: EventHistory, }; protected enemies!: Enemies; - protected eventHistory!: EventHistory; hasGlacialSpike: boolean = this.selectedCombatant.hasTalent(TALENTS.GLACIAL_SPIKE_TALENT); wintersChill: { From f08d9f2a1172fca18735e3bd8f1206be0adec3ee Mon Sep 17 00:00:00 2001 From: Sharrq Date: Fri, 5 Jan 2024 20:53:16 -0500 Subject: [PATCH 17/30] Cold Front --- .../frost/normalizers/CastLinkNormalizer.ts | 12 ++++++++++++ .../retail/mage/frost/talents/ColdFront.tsx | 18 ++++-------------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/analysis/retail/mage/frost/normalizers/CastLinkNormalizer.ts b/src/analysis/retail/mage/frost/normalizers/CastLinkNormalizer.ts index ce6686eea47..b9aeb060a6b 100644 --- a/src/analysis/retail/mage/frost/normalizers/CastLinkNormalizer.ts +++ b/src/analysis/retail/mage/frost/normalizers/CastLinkNormalizer.ts @@ -211,6 +211,18 @@ const EVENT_LINKS: EventLink[] = [ forwardBufferMs: 60_000, backwardBufferMs: CAST_BUFFER_MS, }, + { + reverseLinkRelation: BUFF_APPLY, + linkingEventId: TALENTS.COLD_FRONT_TALENT.id, + linkingEventType: EventType.ApplyBuff, + linkRelation: BUFF_REMOVE, + referencedEventId: TALENTS.COLD_FRONT_TALENT.id, + referencedEventType: EventType.RemoveBuff, + anyTarget: true, + maximumLinks: 1, + forwardBufferMs: 500, + backwardBufferMs: CAST_BUFFER_MS, + }, ]; /** diff --git a/src/analysis/retail/mage/frost/talents/ColdFront.tsx b/src/analysis/retail/mage/frost/talents/ColdFront.tsx index d33bfda0a79..7118b7467cf 100644 --- a/src/analysis/retail/mage/frost/talents/ColdFront.tsx +++ b/src/analysis/retail/mage/frost/talents/ColdFront.tsx @@ -4,18 +4,12 @@ import TALENTS from 'common/TALENTS/mage'; import { SpellIcon } from 'interface'; import Analyzer, { Options } from 'parser/core/Analyzer'; import { SELECTED_PLAYER } from 'parser/core/EventFilter'; -import Events from 'parser/core/Events'; -import EventHistory from 'parser/shared/modules/EventHistory'; +import Events, { GetRelatedEvent, ApplyBuffEvent, RemoveBuffEvent } from 'parser/core/Events'; import BoringSpellValueText from 'parser/ui/BoringSpellValueText'; import Statistic from 'parser/ui/Statistic'; import STATISTIC_CATEGORY from 'parser/ui/STATISTIC_CATEGORY'; class ColdFront extends Analyzer { - static dependencies = { - eventHistory: EventHistory, - }; - protected eventHistory!: EventHistory; - bonusFrozenOrbs = 0; constructor(options: Options) { @@ -27,13 +21,9 @@ class ColdFront extends Analyzer { ); } - onBuffApplied() { - const buffRemovedEvent = this.eventHistory.last( - 1, - 500, - Events.removebuff.to(SELECTED_PLAYER).spell(SPELLS.COLD_FRONT_BUFF), - ); - if (buffRemovedEvent) { + onBuffApplied(event: ApplyBuffEvent) { + const remove: RemoveBuffEvent | undefined = GetRelatedEvent(event, 'BuffRemove'); + if (remove) { this.bonusFrozenOrbs += 1; } } From 26cbbe7ee367717550156f05f430f9f438ee0a4a Mon Sep 17 00:00:00 2001 From: Sharrq Date: Fri, 5 Jan 2024 21:22:10 -0500 Subject: [PATCH 18/30] Comet Storm --- .../frost/normalizers/CastLinkNormalizer.ts | 11 +++ .../retail/mage/frost/talents/CometStorm.tsx | 70 ++++++++----------- 2 files changed, 41 insertions(+), 40 deletions(-) diff --git a/src/analysis/retail/mage/frost/normalizers/CastLinkNormalizer.ts b/src/analysis/retail/mage/frost/normalizers/CastLinkNormalizer.ts index b9aeb060a6b..ee08f222e50 100644 --- a/src/analysis/retail/mage/frost/normalizers/CastLinkNormalizer.ts +++ b/src/analysis/retail/mage/frost/normalizers/CastLinkNormalizer.ts @@ -223,6 +223,17 @@ const EVENT_LINKS: EventLink[] = [ forwardBufferMs: 500, backwardBufferMs: CAST_BUFFER_MS, }, + { + reverseLinkRelation: SPELL_CAST, + linkingEventId: TALENTS.COMET_STORM_TALENT.id, + linkingEventType: EventType.Cast, + linkRelation: SPELL_DAMAGE, + referencedEventId: SPELLS.COMET_STORM_DAMAGE.id, + referencedEventType: EventType.Damage, + anyTarget: true, + forwardBufferMs: 3000, + backwardBufferMs: CAST_BUFFER_MS, + }, ]; /** diff --git a/src/analysis/retail/mage/frost/talents/CometStorm.tsx b/src/analysis/retail/mage/frost/talents/CometStorm.tsx index 9faf90e1eea..e3c3764f714 100644 --- a/src/analysis/retail/mage/frost/talents/CometStorm.tsx +++ b/src/analysis/retail/mage/frost/talents/CometStorm.tsx @@ -1,74 +1,68 @@ -import { Trans } from '@lingui/macro'; -import { COMET_STORM_AOE_MIN_TARGETS } from 'analysis/retail/mage/shared'; +import { COMET_STORM_AOE_MIN_TARGETS, SHATTER_DEBUFFS } from 'analysis/retail/mage/shared'; import { formatPercentage } from 'common/format'; import SPELLS from 'common/SPELLS'; import TALENTS from 'common/TALENTS/mage'; import { SpellLink } from 'interface'; import Analyzer, { Options } from 'parser/core/Analyzer'; import { SELECTED_PLAYER } from 'parser/core/EventFilter'; -import Events, { AnyEvent, CastEvent } from 'parser/core/Events'; +import Events, { CastEvent, DamageEvent, GetRelatedEvents } from 'parser/core/Events'; import { When, ThresholdStyle } from 'parser/core/ParseResults'; -import AbilityTracker from 'parser/shared/modules/AbilityTracker'; import Enemies from 'parser/shared/modules/Enemies'; -import { cometStormHits } from '../normalizers/CometStormLinkNormalizer'; - const MIN_SHATTERED_PROJECTILES_PER_CAST = 4; class CometStorm extends Analyzer { static dependencies = { - abilityTracker: AbilityTracker, enemies: Enemies, }; - protected abilityTracker!: AbilityTracker; protected enemies!: Enemies; - badCometStormCast = 0; + cometStorm: { enemiesHit: number[]; shatteredHits: number }[] = []; constructor(options: Options) { super(options); this.active = this.selectedCombatant.hasTalent(TALENTS.COMET_STORM_TALENT); this.addEventListener( Events.cast.by(SELECTED_PLAYER).spell(TALENTS.COMET_STORM_TALENT), - this.onCometStormCast, + this.onCometCast, ); } - onCometStormCast(event: CastEvent) { - const damageEvents: AnyEvent[] = cometStormHits(event); - const enemiesHit: number[] = []; - let projectilesShattered = 0; - - damageEvents.forEach((hit) => { - const enemy = this.enemies.getEntity(hit); - if (!enemy) { - return; - } - //Tracks each unique enemy that was hit by the cast - if (!enemiesHit.includes(enemy.guid)) { - enemiesHit.push(enemy.guid); + onCometCast(event: CastEvent) { + const damage: DamageEvent[] | undefined = GetRelatedEvents(event, 'SpellDamage'); + damage.forEach((d) => { + const enemy = this.enemies.getEntity(d); + const enemies: number[] = []; + if (enemy && enemies.includes(enemy.guid)) { + enemies.push(enemy.guid); } - //Tracks how many projectiles were shattered - if (enemy.hasBuff(SPELLS.WINTERS_CHILL.id)) { - projectilesShattered += 1; + let shattered = 0; + if (enemy && SHATTER_DEBUFFS.some((effect) => enemy.hasBuff(effect.id, d.timestamp))) { + shattered += 1; } + + this.cometStorm.push({ + enemiesHit: enemies, + shatteredHits: shattered, + }); }); + } - if ( - enemiesHit.length < COMET_STORM_AOE_MIN_TARGETS && - projectilesShattered < MIN_SHATTERED_PROJECTILES_PER_CAST - ) { - this.badCometStormCast += 1; - } + get badCasts() { + return this.cometStorm.filter( + (cs) => + cs.enemiesHit.length < COMET_STORM_AOE_MIN_TARGETS && + cs.shatteredHits < MIN_SHATTERED_PROJECTILES_PER_CAST, + ).length; } get totalCasts() { - return this.abilityTracker.getAbility(TALENTS.COMET_STORM_TALENT.id).casts; + return this.cometStorm.length; } get castUtilization() { - return 1 - this.badCometStormCast / this.totalCasts; + return 1 - this.badCasts / this.totalCasts; } get cometStormUtilizationThresholds() { @@ -88,7 +82,7 @@ class CometStorm extends Analyzer { suggest( <> You failed to get the most out of your {' '} - casts {this.badCometStormCast} times. Because the projectiles from{' '} + casts {this.badCasts} times. Because the projectiles from{' '} no longer remove your stacks of{' '} , you should always cast{' '} immediately after casting{' '} @@ -101,11 +95,7 @@ class CometStorm extends Analyzer { , ) .icon(TALENTS.COMET_STORM_TALENT.icon) - .actual( - - {formatPercentage(actual)}% Utilization - , - ) + .actual(`${formatPercentage(actual)}% Utilization`) .recommended(`${formatPercentage(recommended)}% is recommended`), ); } From 70b5f5f3ebf6ec3c1875ed895fab0ebbd01c150f Mon Sep 17 00:00:00 2001 From: Sharrq Date: Fri, 5 Jan 2024 21:24:40 -0500 Subject: [PATCH 19/30] Winter Chill Suggestion --- src/analysis/retail/mage/frost/core/WintersChill.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/analysis/retail/mage/frost/core/WintersChill.tsx b/src/analysis/retail/mage/frost/core/WintersChill.tsx index 14f6f58dee4..30a38b94f52 100644 --- a/src/analysis/retail/mage/frost/core/WintersChill.tsx +++ b/src/analysis/retail/mage/frost/core/WintersChill.tsx @@ -178,7 +178,8 @@ class WintersChill extends Analyzer { . Doing this will allow your pre-cast ability to hit the target after (unless you are standing too close to the target) allowing it to benefit from{' '} - . + . If you have 4 Icicles, it can be acceptable + to use without a pre-cast. , ) .icon(SPELLS.FROSTBOLT.icon) From 940aba235ec3b343fe2f04825af07afbcaabaf41 Mon Sep 17 00:00:00 2001 From: Sharrq Date: Fri, 5 Jan 2024 21:29:29 -0500 Subject: [PATCH 20/30] Remove Translations --- .../retail/mage/frost/core/BrainFreeze.tsx | 17 +++-------------- .../retail/mage/frost/core/IcyVeins.tsx | 7 +------ .../mage/frost/talents/WaterElemental.tsx | 15 +++++---------- 3 files changed, 9 insertions(+), 30 deletions(-) diff --git a/src/analysis/retail/mage/frost/core/BrainFreeze.tsx b/src/analysis/retail/mage/frost/core/BrainFreeze.tsx index 1b96c31921a..0f39b64da5f 100644 --- a/src/analysis/retail/mage/frost/core/BrainFreeze.tsx +++ b/src/analysis/retail/mage/frost/core/BrainFreeze.tsx @@ -1,4 +1,3 @@ -import { Trans } from '@lingui/macro'; import { formatNumber, formatPercentage } from 'common/format'; import SPELLS from 'common/SPELLS'; import TALENTS from 'common/TALENTS/mage'; @@ -152,11 +151,7 @@ class BrainFreeze extends Analyzer { , ) .icon(TALENTS.BRAIN_FREEZE_TALENT.icon) - .actual( - - {formatPercentage(actual)}% overwritten - , - ) + .actual(`${formatPercentage(actual)}% overwritten`) .recommended(`Overwriting none is recommended`), ); when(this.brainFreezeExpiredThresholds).addSuggestion((suggest, actual, recommended) => @@ -168,11 +163,7 @@ class BrainFreeze extends Analyzer { , ) .icon(TALENTS.BRAIN_FREEZE_TALENT.icon) - .actual( - - {formatPercentage(actual)}% expired - , - ) + .actual(`${formatPercentage(actual)}% expired`) .recommended(`Letting none expire is recommended`), ); when(this.overlappedFlurryThresholds).addSuggestion((suggest, actual, recommended) => @@ -188,9 +179,7 @@ class BrainFreeze extends Analyzer { , ) .icon(TALENTS.FLURRY_TALENT.icon) - .actual( - {formatNumber(actual)} casts, - ) + .actual(`${formatNumber(actual)} casts`) .recommended(`Casting none is recommended`), ); } diff --git a/src/analysis/retail/mage/frost/core/IcyVeins.tsx b/src/analysis/retail/mage/frost/core/IcyVeins.tsx index 9283c58d68f..6acc2002981 100644 --- a/src/analysis/retail/mage/frost/core/IcyVeins.tsx +++ b/src/analysis/retail/mage/frost/core/IcyVeins.tsx @@ -1,4 +1,3 @@ -import { Trans } from '@lingui/macro'; import { formatNumber, formatPercentage } from 'common/format'; import TALENTS from 'common/TALENTS/mage'; import { SpellLink } from 'interface'; @@ -112,11 +111,7 @@ class IcyVeins extends Analyzer { , ) .icon(TALENTS.ICY_VEINS_TALENT.icon) - .actual( - - {formatPercentage(this.percentActiveTime)}% Active Time during Icy Veins - , - ) + .actual(`${formatPercentage(this.percentActiveTime)}% Active Time during Icy Veins`) .recommended(`${formatPercentage(recommended)}% is recommended`), ); } diff --git a/src/analysis/retail/mage/frost/talents/WaterElemental.tsx b/src/analysis/retail/mage/frost/talents/WaterElemental.tsx index e63ae9e78b7..2324956ba30 100644 --- a/src/analysis/retail/mage/frost/talents/WaterElemental.tsx +++ b/src/analysis/retail/mage/frost/talents/WaterElemental.tsx @@ -1,4 +1,3 @@ -import { Trans } from '@lingui/macro'; import { formatPercentage, formatNumber, formatThousands, formatDuration } from 'common/format'; import SPELLS from 'common/SPELLS'; import { PLACEHOLDER_TALENT } from 'common/TALENTS/types'; @@ -142,11 +141,7 @@ class WaterElemental extends Analyzer { , ) .icon(PLACEHOLDER_TALENT.icon) - .actual( - - {formatPercentage(actual)}% uptime - , - ) + .actual(`${formatPercentage(actual)}% uptime`) .recommended( `mirroring your own uptime (${formatPercentage( this.abc.activeTimePercentage, @@ -162,13 +157,13 @@ class WaterElemental extends Analyzer { ) .icon(SPELLS.WATERBOLT.icon) .actual( - - {this._timestampFirstCast === 0 + `${ + this._timestampFirstCast === 0 ? 'Never attacked or not summoned' : 'First attack: ' + formatDuration(this._timestampFirstCast - this.owner.fight.start_time) + - ' into the fight'} - , + ' into the fight' + }`, ) .recommended(`summoning pre-fight is recommended`), ); From 24f1a15cac7b35cfe9fb992b194c16f64646ebe9 Mon Sep 17 00:00:00 2001 From: Sharrq Date: Fri, 5 Jan 2024 21:38:38 -0500 Subject: [PATCH 21/30] changelog --- src/analysis/retail/mage/frost/CHANGELOG.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/analysis/retail/mage/frost/CHANGELOG.tsx b/src/analysis/retail/mage/frost/CHANGELOG.tsx index d6e0b393ca2..5e024257f7b 100644 --- a/src/analysis/retail/mage/frost/CHANGELOG.tsx +++ b/src/analysis/retail/mage/frost/CHANGELOG.tsx @@ -6,6 +6,11 @@ import { Sharrq, ToppleTheNun } from 'CONTRIBUTORS'; // prettier-ignore export default [ + change(date(2024, 1, 5), <>Added a statistic for the average delay to use . This is just informational., Sharrq), + change(date(2024, 1, 5), <>Added tracking for ., Sharrq), + change(date(2024, 1, 5), <>Adjusted to change the spells that can be used to spend ., Sharrq), + change(date(2024, 1, 5), <>Updated to ignore pre-casts at 4 ., Sharrq), + change(date(2024, 1, 5), 'Rewrote most core frost functionality to use event links instead.', Sharrq), change(date(2023, 7, 10), 'Remove references to 10.1.5 removed talents.', Sharrq), change(date(2023, 7, 3), 'Update SpellLink usage.', ToppleTheNun), change(date(2023, 6, 27), <>Added to list of Bloodlust Buffs., Sharrq), From 795222568f0dd2d3ddb3e06ed8de8a023f8633bc Mon Sep 17 00:00:00 2001 From: Sharrq Date: Fri, 5 Jan 2024 22:38:36 -0500 Subject: [PATCH 22/30] Ray of Frost --- .../retail/mage/frost/CombatLogParser.ts | 2 + .../retail/mage/frost/core/WintersChill.tsx | 28 +++++- .../frost/normalizers/CastLinkNormalizer.ts | 11 +++ .../retail/mage/frost/talents/RayOfFrost.tsx | 92 +++++++++++++++++++ 4 files changed, 128 insertions(+), 5 deletions(-) create mode 100644 src/analysis/retail/mage/frost/talents/RayOfFrost.tsx diff --git a/src/analysis/retail/mage/frost/CombatLogParser.ts b/src/analysis/retail/mage/frost/CombatLogParser.ts index 988e5836d54..8d21902bba5 100644 --- a/src/analysis/retail/mage/frost/CombatLogParser.ts +++ b/src/analysis/retail/mage/frost/CombatLogParser.ts @@ -35,6 +35,7 @@ import ColdFront from './talents/ColdFront'; import IcyPropulsion from './talents/IcyPropulsion'; import BoneChilling from './talents/BoneChilling'; import CometStorm from './talents/CometStorm'; +import RayOfFrost from './talents/RayOfFrost'; import GlacialSpike from './talents/GlacialSpike'; import LonelyWinter from './talents/LonelyWinter'; import SplittingIce from './talents/SplittingIce'; @@ -74,6 +75,7 @@ class CombatLogParser extends CoreCombatLogParser { thermalVoid: ThermalVoid, glacialSpike: GlacialSpike, cometStorm: CometStorm, + rayOfFrost: RayOfFrost, icyPropulsion: IcyPropulsion, coldFront: ColdFront, mirrorImage: MirrorImage, diff --git a/src/analysis/retail/mage/frost/core/WintersChill.tsx b/src/analysis/retail/mage/frost/core/WintersChill.tsx index 30a38b94f52..6fed3cf7bec 100644 --- a/src/analysis/retail/mage/frost/core/WintersChill.tsx +++ b/src/analysis/retail/mage/frost/core/WintersChill.tsx @@ -43,7 +43,12 @@ class WintersChill extends Analyzer { this.addEventListener( Events.damage .by(SELECTED_PLAYER) - .spell([SPELLS.FROSTBOLT_DAMAGE, SPELLS.GLACIAL_SPIKE_DAMAGE, SPELLS.ICE_LANCE_DAMAGE]), + .spell([ + SPELLS.FROSTBOLT_DAMAGE, + SPELLS.GLACIAL_SPIKE_DAMAGE, + SPELLS.ICE_LANCE_DAMAGE, + TALENTS.RAY_OF_FROST_TALENT, + ]), this.onDamage, ); } @@ -92,10 +97,23 @@ class WintersChill extends Analyzer { wintersChillShatters = () => { //Winter's Chill Debuffs where there are at least 2 damage hits of Glacial Spike and/or Ice Lance - const badDebuffs = this.wintersChill.filter( - (w) => - w.damageEvents.filter((d) => WINTERS_CHILL_SPENDERS.includes(d.ability.guid)).length >= 2, - ); + let badDebuffs = this.wintersChill.filter((w) => { + const shatteredSpenders = w.damageEvents.filter((d) => + WINTERS_CHILL_SPENDERS.includes(d.ability.guid), + ); + return shatteredSpenders.length >= 2; + }); + + //If they shattered one spell but also they used Ray Of Frost, then disregard it. + badDebuffs = badDebuffs.filter((w) => { + const shatteredSpenders = w.damageEvents.filter((d) => + WINTERS_CHILL_SPENDERS.includes(d.ability.guid), + ); + const rayHits = w.damageEvents.filter( + (d) => d.ability.guid === TALENTS.RAY_OF_FROST_TALENT.id, + ); + return shatteredSpenders.length >= 1 && rayHits.length >= 2; + }); return badDebuffs.length; }; diff --git a/src/analysis/retail/mage/frost/normalizers/CastLinkNormalizer.ts b/src/analysis/retail/mage/frost/normalizers/CastLinkNormalizer.ts index ee08f222e50..eb4e6141058 100644 --- a/src/analysis/retail/mage/frost/normalizers/CastLinkNormalizer.ts +++ b/src/analysis/retail/mage/frost/normalizers/CastLinkNormalizer.ts @@ -234,6 +234,17 @@ const EVENT_LINKS: EventLink[] = [ forwardBufferMs: 3000, backwardBufferMs: CAST_BUFFER_MS, }, + { + reverseLinkRelation: SPELL_CAST, + linkingEventId: TALENTS.RAY_OF_FROST_TALENT.id, + linkingEventType: EventType.Cast, + linkRelation: SPELL_DAMAGE, + referencedEventId: TALENTS.RAY_OF_FROST_TALENT.id, + referencedEventType: EventType.Damage, + anyTarget: true, + forwardBufferMs: 7000, + backwardBufferMs: CAST_BUFFER_MS, + }, ]; /** diff --git a/src/analysis/retail/mage/frost/talents/RayOfFrost.tsx b/src/analysis/retail/mage/frost/talents/RayOfFrost.tsx new file mode 100644 index 00000000000..926082bb564 --- /dev/null +++ b/src/analysis/retail/mage/frost/talents/RayOfFrost.tsx @@ -0,0 +1,92 @@ +import { SHATTER_DEBUFFS } from 'analysis/retail/mage/shared'; +import { formatPercentage } from 'common/format'; +import SPELLS from 'common/SPELLS'; +import TALENTS from 'common/TALENTS/mage'; +import { SpellLink } from 'interface'; +import Analyzer, { Options } from 'parser/core/Analyzer'; +import { SELECTED_PLAYER } from 'parser/core/EventFilter'; +import Events, { CastEvent, DamageEvent, GetRelatedEvents } from 'parser/core/Events'; +import { When, ThresholdStyle } from 'parser/core/ParseResults'; +import Enemies from 'parser/shared/modules/Enemies'; + +class RayOfFrost extends Analyzer { + static dependencies = { + enemies: Enemies, + }; + protected enemies!: Enemies; + + rayOfFrost: { hits: number; shatteredHits: number }[] = []; + + constructor(options: Options) { + super(options); + this.active = this.selectedCombatant.hasTalent(TALENTS.RAY_OF_FROST_TALENT); + this.addEventListener( + Events.cast.by(SELECTED_PLAYER).spell(TALENTS.RAY_OF_FROST_TALENT), + this.onRayCast, + ); + } + + onRayCast(event: CastEvent) { + const damage: DamageEvent[] | undefined = GetRelatedEvents(event, 'SpellDamage'); + let shattered = 0; + damage.forEach((d) => { + const enemy = this.enemies.getEntity(d); + if (SHATTER_DEBUFFS.some((effect) => enemy?.hasBuff(effect.id, d.timestamp))) { + shattered += 1; + } + }); + + this.rayOfFrost.push({ + hits: damage.length, + shatteredHits: shattered, + }); + } + + get badCasts() { + return this.rayOfFrost.filter((r) => r.shatteredHits < 2 || r.hits < 4).length; + } + + get totalCasts() { + return this.rayOfFrost.length; + } + + get castUtilization() { + return 1 - this.badCasts / this.totalCasts; + } + + get rayOfFrostUtilizationThresholds() { + return { + actual: this.castUtilization, + isLessThan: { + minor: 0.9, + average: 0.8, + major: 0.7, + }, + style: ThresholdStyle.PERCENTAGE, + }; + } + + suggestions(when: When) { + when(this.rayOfFrostUtilizationThresholds).addSuggestion((suggest, actual, recommended) => + suggest( + <> + You failed to get the most out of your {' '} + casts {this.badCasts} times. Because the ticks from{' '} + do not remove your stacks of{' '} + , you should always cast{' '} + during{' '} + . However, because{' '} + has such a short duration and therefore will + likely naturally end before finishes, + you should spend your first stack of and then + cast instead of spending the 2nd stack. + , + ) + .icon(TALENTS.RAY_OF_FROST_TALENT.icon) + .actual(`${formatPercentage(actual)}% Utilization`) + .recommended(`${formatPercentage(recommended)}% is recommended`), + ); + } +} + +export default RayOfFrost; From e9c37e5346a2af83ba165c66933878d5650b220e Mon Sep 17 00:00:00 2001 From: Sharrq Date: Fri, 5 Jan 2024 22:38:50 -0500 Subject: [PATCH 23/30] Remove Fingers logging --- src/analysis/retail/mage/frost/core/FingersOfFrost.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/analysis/retail/mage/frost/core/FingersOfFrost.tsx b/src/analysis/retail/mage/frost/core/FingersOfFrost.tsx index 1b3ad8e6dc2..5e0cbd019d9 100644 --- a/src/analysis/retail/mage/frost/core/FingersOfFrost.tsx +++ b/src/analysis/retail/mage/frost/core/FingersOfFrost.tsx @@ -50,8 +50,6 @@ class FingersOfFrost extends Analyzer { const spender: CastEvent | undefined = remove && GetRelatedEvent(remove, 'SpellCast'); const damage: DamageEvent | undefined = spender && GetRelatedEvent(spender, 'SpellDamage'); const enemy = damage && this.enemies.getEntity(damage); - this.log(spender?.timestamp); - this.log(damage?.timestamp); this.fingers.push({ apply: event, remove: remove, @@ -70,7 +68,6 @@ class FingersOfFrost extends Analyzer { get averageSpendDelaySeconds() { let spendDelay = 0; this.fingers.forEach((f) => f.spendDelay && (spendDelay += f.spendDelay)); - this.log(spendDelay / this.fingers.filter((f) => f.spendDelay).length / 1000); return spendDelay / this.fingers.filter((f) => f.spendDelay).length / 1000; } From a1407b46750d53c2080c71f6f9ee2053101bc076 Mon Sep 17 00:00:00 2001 From: Sharrq Date: Fri, 5 Jan 2024 22:47:21 -0500 Subject: [PATCH 24/30] spec compat --- src/analysis/retail/mage/frost/CONFIG.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/analysis/retail/mage/frost/CONFIG.tsx b/src/analysis/retail/mage/frost/CONFIG.tsx index 200b6266521..4574441d39a 100644 --- a/src/analysis/retail/mage/frost/CONFIG.tsx +++ b/src/analysis/retail/mage/frost/CONFIG.tsx @@ -11,7 +11,7 @@ const config: Config = { expansion: Expansion.Dragonflight, // The WoW client patch this spec was last updated. patchCompatibility: '10.2.0', - isPartial: true, + isPartial: false, // 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. description: ( <> From 7f6609452c083c6e00a70491dc38c4cd3c6e85cc Mon Sep 17 00:00:00 2001 From: Sharrq Date: Fri, 5 Jan 2024 22:48:10 -0500 Subject: [PATCH 25/30] changelog --- src/analysis/retail/mage/frost/CHANGELOG.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/analysis/retail/mage/frost/CHANGELOG.tsx b/src/analysis/retail/mage/frost/CHANGELOG.tsx index 5e024257f7b..4fad1e322f0 100644 --- a/src/analysis/retail/mage/frost/CHANGELOG.tsx +++ b/src/analysis/retail/mage/frost/CHANGELOG.tsx @@ -6,6 +6,7 @@ import { Sharrq, ToppleTheNun } from 'CONTRIBUTORS'; // prettier-ignore export default [ + change(date(2024, 1, 5), 'Updated spec support to full 10.2 support.', Sharrq), change(date(2024, 1, 5), <>Added a statistic for the average delay to use . This is just informational., Sharrq), change(date(2024, 1, 5), <>Added tracking for ., Sharrq), change(date(2024, 1, 5), <>Adjusted to change the spells that can be used to spend ., Sharrq), From a3d12d8ac5588cf265b618af309a2f9b6e2433a6 Mon Sep 17 00:00:00 2001 From: Sharrq Date: Fri, 5 Jan 2024 22:53:39 -0500 Subject: [PATCH 26/30] more changelog --- src/analysis/retail/mage/frost/CHANGELOG.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/analysis/retail/mage/frost/CHANGELOG.tsx b/src/analysis/retail/mage/frost/CHANGELOG.tsx index 4fad1e322f0..9f903593651 100644 --- a/src/analysis/retail/mage/frost/CHANGELOG.tsx +++ b/src/analysis/retail/mage/frost/CHANGELOG.tsx @@ -7,6 +7,7 @@ import { Sharrq, ToppleTheNun } from 'CONTRIBUTORS'; // prettier-ignore export default [ change(date(2024, 1, 5), 'Updated spec support to full 10.2 support.', Sharrq), + change(date(2024, 1, 5), <>Fixed the cooldowns for and ., Sharrq), change(date(2024, 1, 5), <>Added a statistic for the average delay to use . This is just informational., Sharrq), change(date(2024, 1, 5), <>Added tracking for ., Sharrq), change(date(2024, 1, 5), <>Adjusted to change the spells that can be used to spend ., Sharrq), From 2f3f1422d30d88d54feea6a4f6d836c9c4ec7c18 Mon Sep 17 00:00:00 2001 From: Sharrq Date: Fri, 5 Jan 2024 23:11:30 -0500 Subject: [PATCH 27/30] fix Ice Lance --- .../retail/mage/frost/core/IceLance.tsx | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/analysis/retail/mage/frost/core/IceLance.tsx b/src/analysis/retail/mage/frost/core/IceLance.tsx index 9b96f65b67e..c63385dc532 100644 --- a/src/analysis/retail/mage/frost/core/IceLance.tsx +++ b/src/analysis/retail/mage/frost/core/IceLance.tsx @@ -42,17 +42,23 @@ class IceLance extends Analyzer { }); } - get nonShatteredCasts() { - return this.icelance.filter((il) => !il.shattered).length; - } + nonShatteredCasts = () => { + //Get casts that were not shattered + let badCasts = this.icelance.filter((il) => !il.shattered); + + //If they had Fingers of Frost, disregard it + badCasts = badCasts.filter((il) => !il.hadFingers); + + return badCasts.length; + }; get shatteredPercent() { - return 1 - this.nonShatteredCasts / this.icelance.length; + return 1 - this.nonShatteredCasts() / this.icelance.length; } get nonShatteredIceLanceThresholds() { return { - actual: this.nonShatteredCasts / this.icelance.length, + actual: this.nonShatteredCasts() / this.icelance.length, isGreaterThan: { minor: 0.05, average: 0.15, @@ -66,7 +72,7 @@ class IceLance extends Analyzer { when(this.nonShatteredIceLanceThresholds).addSuggestion((suggest, actual, recommended) => suggest( <> - You cast {this.nonShatteredCasts} times ( + You cast {this.nonShatteredCasts()} times ( {formatPercentage(actual)}%) without . Make sure that you are only casting Ice Lance when the target has{' '} (or other Shatter effects), if you have a{' '} From d0056b0a6e2a84237e1d73be11942497729be623 Mon Sep 17 00:00:00 2001 From: Sharrq Date: Fri, 5 Jan 2024 23:30:17 -0500 Subject: [PATCH 28/30] fixed winters chill --- src/analysis/retail/mage/frost/core/WintersChill.tsx | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/analysis/retail/mage/frost/core/WintersChill.tsx b/src/analysis/retail/mage/frost/core/WintersChill.tsx index 6fed3cf7bec..2a20611e969 100644 --- a/src/analysis/retail/mage/frost/core/WintersChill.tsx +++ b/src/analysis/retail/mage/frost/core/WintersChill.tsx @@ -95,13 +95,13 @@ class WintersChill extends Analyzer { return missingPreCast.length; }; - wintersChillShatters = () => { + missedShatters = () => { //Winter's Chill Debuffs where there are at least 2 damage hits of Glacial Spike and/or Ice Lance let badDebuffs = this.wintersChill.filter((w) => { const shatteredSpenders = w.damageEvents.filter((d) => WINTERS_CHILL_SPENDERS.includes(d.ability.guid), ); - return shatteredSpenders.length >= 2; + return shatteredSpenders.length < 2; }); //If they shattered one spell but also they used Ray Of Frost, then disregard it. @@ -112,7 +112,7 @@ class WintersChill extends Analyzer { const rayHits = w.damageEvents.filter( (d) => d.ability.guid === TALENTS.RAY_OF_FROST_TALENT.id, ); - return shatteredSpenders.length >= 1 && rayHits.length >= 2; + return shatteredSpenders.length !== 1 || rayHits.length < 2; }); return badDebuffs.length; @@ -122,12 +122,8 @@ class WintersChill extends Analyzer { return this.wintersChill.length; } - get missedShatters() { - return this.totalProcs - this.wintersChillShatters(); - } - get shatterPercent() { - return this.wintersChillShatters() / this.totalProcs || 0; + return 1 - this.missedShatters() / this.totalProcs; } get preCastPercent() { From a6213fdc394fed941765ecbbad97d20ba081e781 Mon Sep 17 00:00:00 2001 From: Sharrq Date: Fri, 5 Jan 2024 23:46:09 -0500 Subject: [PATCH 29/30] Add timeline highlights --- .../retail/mage/frost/core/IceLance.tsx | 7 ++++- .../retail/mage/frost/talents/CometStorm.tsx | 30 ++++++++++++------- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/analysis/retail/mage/frost/core/IceLance.tsx b/src/analysis/retail/mage/frost/core/IceLance.tsx index c63385dc532..f01e5b1cc49 100644 --- a/src/analysis/retail/mage/frost/core/IceLance.tsx +++ b/src/analysis/retail/mage/frost/core/IceLance.tsx @@ -3,6 +3,7 @@ import { formatPercentage } from 'common/format'; import SPELLS from 'common/SPELLS'; import TALENTS from 'common/TALENTS/mage'; import { SpellLink } from 'interface'; +import { highlightInefficientCast } from 'interface/report/Results/Timeline/Casts'; import Analyzer, { SELECTED_PLAYER, Options } from 'parser/core/Analyzer'; import Events, { CastEvent, DamageEvent, GetRelatedEvent } from 'parser/core/Events'; import { When, ThresholdStyle } from 'parser/core/ParseResults'; @@ -17,7 +18,7 @@ class IceLance extends Analyzer { }; protected enemies!: Enemies; - icelance: { shattered: boolean; hadFingers: boolean; cleaved: boolean }[] = []; + icelance: { cast: CastEvent; shattered: boolean; hadFingers: boolean; cleaved: boolean }[] = []; constructor(options: Options) { super(options); @@ -32,6 +33,7 @@ class IceLance extends Analyzer { const enemy = damage && this.enemies.getEntity(damage); const cleave: DamageEvent | undefined = GetRelatedEvent(event, 'CleaveDamage'); this.icelance.push({ + cast: event, shattered: SHATTER_DEBUFFS.some((effect) => enemy?.hasBuff(effect.id, damage?.timestamp)) || false, hadFingers: this.selectedCombatant.hasBuff( @@ -49,6 +51,9 @@ class IceLance extends Analyzer { //If they had Fingers of Frost, disregard it badCasts = badCasts.filter((il) => !il.hadFingers); + const tooltip = `This Ice Lance was not shattered.`; + badCasts.forEach((e) => e.cast && highlightInefficientCast(e.cast, tooltip)); + return badCasts.length; }; diff --git a/src/analysis/retail/mage/frost/talents/CometStorm.tsx b/src/analysis/retail/mage/frost/talents/CometStorm.tsx index e3c3764f714..8a4d585a139 100644 --- a/src/analysis/retail/mage/frost/talents/CometStorm.tsx +++ b/src/analysis/retail/mage/frost/talents/CometStorm.tsx @@ -3,6 +3,7 @@ import { formatPercentage } from 'common/format'; import SPELLS from 'common/SPELLS'; import TALENTS from 'common/TALENTS/mage'; import { SpellLink } from 'interface'; +import { highlightInefficientCast } from 'interface/report/Results/Timeline/Casts'; import Analyzer, { Options } from 'parser/core/Analyzer'; import { SELECTED_PLAYER } from 'parser/core/EventFilter'; import Events, { CastEvent, DamageEvent, GetRelatedEvents } from 'parser/core/Events'; @@ -17,7 +18,7 @@ class CometStorm extends Analyzer { }; protected enemies!: Enemies; - cometStorm: { enemiesHit: number[]; shatteredHits: number }[] = []; + cometStorm: { cast: CastEvent; enemiesHit: number[]; shatteredHits: number }[] = []; constructor(options: Options) { super(options); @@ -30,6 +31,8 @@ class CometStorm extends Analyzer { onCometCast(event: CastEvent) { const damage: DamageEvent[] | undefined = GetRelatedEvents(event, 'SpellDamage'); + let shattered = 0; + const enemies: number[] = []; damage.forEach((d) => { const enemy = this.enemies.getEntity(d); const enemies: number[] = []; @@ -37,32 +40,37 @@ class CometStorm extends Analyzer { enemies.push(enemy.guid); } - let shattered = 0; if (enemy && SHATTER_DEBUFFS.some((effect) => enemy.hasBuff(effect.id, d.timestamp))) { shattered += 1; } + }); - this.cometStorm.push({ - enemiesHit: enemies, - shatteredHits: shattered, - }); + this.cometStorm.push({ + cast: event, + enemiesHit: enemies, + shatteredHits: shattered, }); } - get badCasts() { - return this.cometStorm.filter( + badCasts = () => { + const badCasts = this.cometStorm.filter( (cs) => cs.enemiesHit.length < COMET_STORM_AOE_MIN_TARGETS && cs.shatteredHits < MIN_SHATTERED_PROJECTILES_PER_CAST, - ).length; - } + ); + + const tooltip = `This Comet Storm was not shattered and did not hit multiple enemies.`; + badCasts.forEach((e) => e.cast && highlightInefficientCast(e.cast, tooltip)); + + return badCasts.length; + }; get totalCasts() { return this.cometStorm.length; } get castUtilization() { - return 1 - this.badCasts / this.totalCasts; + return 1 - this.badCasts() / this.totalCasts; } get cometStormUtilizationThresholds() { From f1518497049adc1d443e1c4a9a9ebedaf200f662 Mon Sep 17 00:00:00 2001 From: Sharrq Date: Fri, 5 Jan 2024 23:52:05 -0500 Subject: [PATCH 30/30] fix winters chill suggestions --- src/analysis/retail/mage/frost/core/WintersChill.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/analysis/retail/mage/frost/core/WintersChill.tsx b/src/analysis/retail/mage/frost/core/WintersChill.tsx index 2a20611e969..c80bc10336e 100644 --- a/src/analysis/retail/mage/frost/core/WintersChill.tsx +++ b/src/analysis/retail/mage/frost/core/WintersChill.tsx @@ -161,8 +161,8 @@ class WintersChill extends Analyzer { suggest( <> You failed to properly take advantage of on - your target {this.missedShatters} times ({formatPercentage(1 - actual)}%). After debuffing - the target via and{' '} + your target {this.missedShatters()} times ({formatPercentage(1 - actual)}%). After + debuffing the target via and{' '} , you should ensure that you hit the target with{' '} {this.hasGlacialSpike ? ( @@ -186,9 +186,9 @@ class WintersChill extends Analyzer { suggest( <> You failed to use a pre-cast ability before {' '} - {this.missedPreCasts} times ({formatPercentage(1 - actual)}%). Because of the travel time - of , you should cast a damaging ability such as{' '} - immediately before using{' '} + {this.missedPreCasts()} times ({formatPercentage(1 - actual)}%). Because of the travel + time of , you should cast a damaging ability + such as immediately before using{' '} . Doing this will allow your pre-cast ability to hit the target after (unless you are standing too close to the target) allowing it to benefit from{' '}