diff --git a/src/CHANGELOG.tsx b/src/CHANGELOG.tsx index 5addcc6e6fc..c5c21ff6978 100644 --- a/src/CHANGELOG.tsx +++ b/src/CHANGELOG.tsx @@ -35,6 +35,7 @@ export default [ change(date(2024, 1, 7), <>Add checklist support for ., ToppleTheNun), change(date(2024, 1, 4), <>Add statistics for ., Arbixal), change(date(2024, 1, 2), 'Remove Shadowlands food and augment rune support.', ToppleTheNun), + change(date(2023, 12, 31), <>Add resource initialization and granularity support for ResourceTracker module., Vollmer), change(date(2023, 12, 30), 'Fix errors caused by ESLint update.', ToppleTheNun), change(date(2023, 12, 28), 'Improve internal handling of phases', emallson), change(date(2023, 12, 21), <>Mark as a max rank enchant., ToppleTheNun), diff --git a/src/analysis/retail/evoker/augmentation/CHANGELOG.tsx b/src/analysis/retail/evoker/augmentation/CHANGELOG.tsx index 7a2321840ac..508aaf3ee09 100644 --- a/src/analysis/retail/evoker/augmentation/CHANGELOG.tsx +++ b/src/analysis/retail/evoker/augmentation/CHANGELOG.tsx @@ -1,10 +1,12 @@ import { change, date } from 'common/changelog'; import { Pants, Vollmer } from 'CONTRIBUTORS'; -import { SpellLink } from 'interface'; +import { ResourceLink, SpellLink } from 'interface'; import TALENTS from 'common/TALENTS/evoker'; import SPELLS from 'common/SPELLS/evoker'; +import RESOURCE_TYPES from 'game/RESOURCE_TYPES'; export default [ + change(date(2023, 12, 31), <>Implemented Graph., Vollmer), change(date(2023, 12, 22), <>Update blacklist for Helpers to increase accuracy., Vollmer), change(date(2023, 12, 13), <>Added Boss filter button for Buff Helper, and improved loading speed., Vollmer), change(date(2023, 12, 7), <>Update blacklist for Helpers to increase accuracy., Vollmer), diff --git a/src/analysis/retail/evoker/augmentation/CombatLogParser.ts b/src/analysis/retail/evoker/augmentation/CombatLogParser.ts index 49582bb1561..0d41e3e1807 100644 --- a/src/analysis/retail/evoker/augmentation/CombatLogParser.ts +++ b/src/analysis/retail/evoker/augmentation/CombatLogParser.ts @@ -35,13 +35,22 @@ import EbonMightNormalizer from './modules/normalizers/EbonMightNormalizer'; import T31Augmentation4P from './modules/dragonflight/T31Augmentation4P'; //Shared -import { LeapingFlamesNormalizer, LeapingFlames } from 'analysis/retail/evoker/shared'; +import { + LeapingFlamesNormalizer, + LeapingFlames, + SpellEssenceCost, + EssenceTracker, + EssenceGraph, +} from 'analysis/retail/evoker/shared'; class CombatLogParser extends MainCombatLogParser { static specModules = { // Shared leapingFlamesNormalizer: LeapingFlamesNormalizer, leapingFlames: LeapingFlames, + spellEssenceCost: SpellEssenceCost, + essenceTracker: EssenceTracker, + essenceGraph: EssenceGraph, // Normalizers castLinkNormalizer: CastLinkNormalizer, diff --git a/src/analysis/retail/evoker/augmentation/modules/guide/CoreRotation.tsx b/src/analysis/retail/evoker/augmentation/modules/guide/CoreRotation.tsx index 53cb7479e92..3dd3ed5217c 100644 --- a/src/analysis/retail/evoker/augmentation/modules/guide/CoreRotation.tsx +++ b/src/analysis/retail/evoker/augmentation/modules/guide/CoreRotation.tsx @@ -2,13 +2,16 @@ import { GuideProps, Section, SubSection } from 'interface/guide'; import { TALENTS_EVOKER } from 'common/TALENTS'; import SPELLS from 'common/SPELLS'; import CombatLogParser from '../../CombatLogParser'; -import { SpellLink } from 'interface'; +import { ResourceLink, SpellLink } from 'interface'; import HideExplanationsToggle from 'interface/guide/components/HideExplanationsToggle'; import HideGoodCastsToggle from 'interface/guide/components/HideGoodCastsToggle'; import { RoundedPanel } from 'interface/guide/components/GuideDivs'; import ExplanationRow from 'interface/guide/components/ExplanationRow'; import Explanation from 'interface/guide/components/Explanation'; import CooldownUsage from 'parser/core/MajorCooldowns/CooldownUsage'; +import { QualitativePerformance } from 'parser/ui/QualitativePerformance'; +import RESOURCE_TYPES from 'game/RESOURCE_TYPES'; +import PerformancePercentage from 'analysis/retail/evoker/devastation/modules/guide/PerformancePercentage'; export function CoreRotationSection({ modules, events, info }: GuideProps) { return ( @@ -34,6 +37,8 @@ export function CoreRotationSection({ modules, events, info }: GuideProps + + This graph shows how many Ebon Mights and Presciences you had active over the course of the encounter, with rule lines showing when you used Breath of Eon. Use this to ensure that you @@ -56,7 +61,7 @@ export function CoreRotationSection({ modules, events, info }: GuideProps) { +function BlisteringScalesSection({ modules }: GuideProps) { return ( @@ -83,3 +88,48 @@ function BlisteringScalesSection({ modules, events, info }: GuideProps ); } + +function EssenceGraphSection({ modules, events, info }: GuideProps) { + const percentAtCap = modules.essenceTracker.percentAtCap; + const essenceWasted = modules.essenceTracker.wasted; + + const perfectTimeAtEssenceCap = 0.1; + const goodTimeAtEssenceCap = 0.15; + const okTimeAtEssenceCap = 0.2; + + const percentAtCapPerformance = + percentAtCap <= perfectTimeAtEssenceCap + ? QualitativePerformance.Perfect + : percentAtCap <= goodTimeAtEssenceCap + ? QualitativePerformance.Good + : percentAtCap <= okTimeAtEssenceCap + ? QualitativePerformance.Ok + : QualitativePerformance.Fail; + + return ( + +

+ Your primary resource is . You should avoid + overcapping - lost{' '} + generation is lost DPS. Sometimes it will be + impossible to avoid overcapping - due to + handling mechanics, high rolling procs + or during intermission phases. +

+

+ The chart below shows your over the course + of the encounter. You wasted{' '} + {' '} + of your . +

+ {modules.essenceGraph.plot} +
+ ); +} diff --git a/src/analysis/retail/evoker/devastation/CHANGELOG.tsx b/src/analysis/retail/evoker/devastation/CHANGELOG.tsx index f8e999135b1..deb60f47ea2 100644 --- a/src/analysis/retail/evoker/devastation/CHANGELOG.tsx +++ b/src/analysis/retail/evoker/devastation/CHANGELOG.tsx @@ -1,10 +1,12 @@ import { change, date } from 'common/changelog'; import { ToppleTheNun, Tyndi, Vireve, Vollmer } from 'CONTRIBUTORS'; -import { SpellLink } from 'interface'; +import { ResourceLink, SpellLink } from 'interface'; import TALENTS from 'common/TALENTS/evoker'; import SPELLS from 'common/SPELLS/evoker'; +import RESOURCE_TYPES from 'game/RESOURCE_TYPES'; export default [ + change(date(2023, 12, 31), <>Improved tracking of for a more precise representation on the Graph., Vollmer), change(date(2023, 12, 6), <>Update APL Check., Vollmer), change(date(2023, 11, 30), <>Update guide section., Vollmer), change(date(2023, 11, 12), <>Properly track dropped ticks when only 1 tick is remaining for graph., Vollmer), diff --git a/src/analysis/retail/evoker/devastation/CombatLogParser.ts b/src/analysis/retail/evoker/devastation/CombatLogParser.ts index f9cdb880b65..b1f441a43bc 100644 --- a/src/analysis/retail/evoker/devastation/CombatLogParser.ts +++ b/src/analysis/retail/evoker/devastation/CombatLogParser.ts @@ -6,8 +6,6 @@ import ShatteringStar from './modules/abilities/ShatteringStar'; import Buffs from './modules/Buffs'; import Guide from './Guide'; import AplCheck from './modules/AplCheck'; -import EssenceTracker from '../preservation/modules/features/EssenceTracker'; -import EssenceGraph from './modules/guide/EssenceGraph/EssenceGraph'; import Disintegrate from './modules/abilities/Disintegrate'; import EssenceBurst from './modules/abilities/EssenceBurst'; import Burnout from './modules/abilities/Burnout'; @@ -33,13 +31,22 @@ import Iridescence from './modules/talents/Iridescence'; import T31DevaTier from './modules/dragonflight/tier/T31DevaTier'; // Shared -import { LeapingFlamesNormalizer, LeapingFlames } from 'analysis/retail/evoker/shared'; +import { + LeapingFlamesNormalizer, + LeapingFlames, + SpellEssenceCost, + EssenceTracker, + EssenceGraph, +} from 'analysis/retail/evoker/shared'; class CombatLogParser extends MainCombatLogParser { static specModules = { // Shared leapingFlamesNormalizer: LeapingFlamesNormalizer, leapingFlames: LeapingFlames, + spellEssenceCost: SpellEssenceCost, + essenceTracker: EssenceTracker, + essenceGraph: EssenceGraph, // Core abilities: Abilities, @@ -50,8 +57,6 @@ class CombatLogParser extends MainCombatLogParser { essenceBurstNormalizer: EssenceBurstNormalizer, // features - essenceTracker: EssenceTracker, - essenceGraph: EssenceGraph, apls: AplCheck, cooldownThroughputTracker: CooldownThroughputTracker, diff --git a/src/analysis/retail/evoker/devastation/Guide.tsx b/src/analysis/retail/evoker/devastation/Guide.tsx index c8611aee995..e1fc92895cb 100644 --- a/src/analysis/retail/evoker/devastation/Guide.tsx +++ b/src/analysis/retail/evoker/devastation/Guide.tsx @@ -4,7 +4,7 @@ import PreparationSection from 'interface/guide/components/Preparation/Preparati import { CooldownSection } from './modules/guide/Cooldown'; import { CoreRotation } from './modules/guide/CoreRotation'; import { DamageEfficiency } from './modules/guide/DamageEfficiencySection'; -import { EssenceGraphSection } from './modules/guide/EssenceGraph/EssenceGraphSection'; +import { EssenceGraphSection } from './modules/guide/EssenceGraphSection'; import { DragonRageSection } from './modules/guide/DragonRageSection'; import { IntroSection } from './modules/guide/IntroSection'; @@ -12,10 +12,10 @@ export default function Guide({ modules, events, info }: GuideProps + - diff --git a/src/analysis/retail/evoker/devastation/constants.tsx b/src/analysis/retail/evoker/devastation/constants.tsx index dd82e6d7707..3be125fe9e0 100644 --- a/src/analysis/retail/evoker/devastation/constants.tsx +++ b/src/analysis/retail/evoker/devastation/constants.tsx @@ -29,3 +29,6 @@ export const CAUSALITY_DISINTEGRATE_CDR_MS = 500; export const CAUSALITY_PYRE_CDR_MS = 400; export const DEVA_T31_2PC_MULTIPLER = 0.05; + +export const POWER_SWELL_REGEN_FACTOR = 1; +export const DENSE_ENERGY_ESSENCE_REDUCTION = 1; diff --git a/src/analysis/retail/evoker/devastation/modules/guide/EssenceGraph/EssenceGraphSection.tsx b/src/analysis/retail/evoker/devastation/modules/guide/EssenceGraph/EssenceGraphSection.tsx deleted file mode 100644 index 2d8eb508195..00000000000 --- a/src/analysis/retail/evoker/devastation/modules/guide/EssenceGraph/EssenceGraphSection.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import CombatLogParser from '../../../CombatLogParser'; -import { GuideProps, Section } from 'interface/guide'; -import { formatPercentage } from 'common/format'; - -export function EssenceGraphSection({ modules, events, info }: GuideProps) { - return ( -
-

- This documents your Essence level over time. You shouldn't have long windows of overcapped - essence except in some unique cases in Dragon Rage. You were capped on Essence for{' '} - {formatPercentage(modules.essenceTracker.percentAtCap, 1)}% of the - encounter. -

- {modules.essenceGraph.plot} -
- ); -} diff --git a/src/analysis/retail/evoker/devastation/modules/guide/EssenceGraphSection.tsx b/src/analysis/retail/evoker/devastation/modules/guide/EssenceGraphSection.tsx new file mode 100644 index 00000000000..5a90058224e --- /dev/null +++ b/src/analysis/retail/evoker/devastation/modules/guide/EssenceGraphSection.tsx @@ -0,0 +1,73 @@ +import CombatLogParser from '../../CombatLogParser'; +import { GuideProps, Section } from 'interface/guide'; +import { QualitativePerformance } from 'parser/ui/QualitativePerformance'; +import PerformancePercentage from './PerformancePercentage'; +import { ResourceLink, SpellLink, Tooltip } from 'interface'; +import RESOURCE_TYPES from 'game/RESOURCE_TYPES'; +import { TIERS } from 'game/TIERS'; +import { InformationIcon } from 'interface/icons'; +import SPELLS from 'common/SPELLS/evoker'; + +export function EssenceGraphSection({ modules, events, info }: GuideProps) { + const hasT31 = info.combatant.has4PieceByTier(TIERS.T31); + const percentAtCap = modules.essenceTracker.percentAtCap; + const essenceWasted = modules.essenceTracker.wasted; + + const perfectTimeAtEssenceCap = 0.1 + (hasT31 ? 0.05 : 0); + const goodTimeAtEssenceCap = 0.15 + (hasT31 ? 0.05 : 0); + const okTimeAtEssenceCap = 0.2 + (hasT31 ? 0.05 : 0); + + const percentAtCapPerformance = + percentAtCap <= perfectTimeAtEssenceCap + ? QualitativePerformance.Perfect + : percentAtCap <= goodTimeAtEssenceCap + ? QualitativePerformance.Good + : percentAtCap <= okTimeAtEssenceCap + ? QualitativePerformance.Ok + : QualitativePerformance.Fail; + + return ( +
+

+ Your primary resource is . You should avoid + overcapping - lost{' '} + generation is lost DPS. Sometimes it will be + impossible to avoid overcapping - due to + handling mechanics, high rolling procs + or during intermission phases. +

+

+ The chart below shows your over the course + of the encounter. You wasted{' '} + {' '} + of your . + {hasT31 && ( + <> + {' '} + + Since you have T31 4pc an extra 5% grace period is added, as it is expected that + more will go to waste, due to the + extra amount of generated. + + } + > +

+ +
+ + + )} +

+ {modules.essenceGraph.plot} +
+ ); +} diff --git a/src/analysis/retail/evoker/devastation/modules/guide/PerformancePercentage.tsx b/src/analysis/retail/evoker/devastation/modules/guide/PerformancePercentage.tsx new file mode 100644 index 00000000000..f0447413e6c --- /dev/null +++ b/src/analysis/retail/evoker/devastation/modules/guide/PerformancePercentage.tsx @@ -0,0 +1,45 @@ +import { PerformanceMark } from 'interface/guide'; +import { QualitativePerformance } from 'parser/ui/QualitativePerformance'; +import { formatNumber, formatPercentage } from 'common/format'; +import PerformanceStrongWithTooltip from 'interface/PerformanceStrongWithTooltip'; + +interface Props { + performance: QualitativePerformance; + perfectPercentage: number; + goodPercentage: number; + okPercentage: number; + percentage: number; + flatAmount: number; +} +const PerformancePercentage = ({ + performance, + perfectPercentage, + goodPercentage, + okPercentage, + percentage, + flatAmount, +}: Props) => { + const perfectSign = perfectPercentage > 0 ? '<=' : '='; + + return ( + + Perfect usage {perfectSign}{' '} + {formatPercentage(perfectPercentage, 0)}% +
+ Good usage <={' '} + {formatPercentage(goodPercentage, 0)}% +
+ OK usage <={' '} + {formatPercentage(okPercentage, 0)}% + + } + > + {formatNumber(flatAmount)} ({formatPercentage(percentage)}%) +
+ ); +}; + +export default PerformancePercentage; diff --git a/src/analysis/retail/evoker/preservation/CHANGELOG.tsx b/src/analysis/retail/evoker/preservation/CHANGELOG.tsx index 67613f45a4d..0d86b8c4d90 100644 --- a/src/analysis/retail/evoker/preservation/CHANGELOG.tsx +++ b/src/analysis/retail/evoker/preservation/CHANGELOG.tsx @@ -1,10 +1,12 @@ import { change, date } from 'common/changelog'; import SPELLS from 'common/SPELLS'; import { TALENTS_EVOKER } from 'common/TALENTS'; -import { ToppleTheNun, Trevor, Tyndi, Vohrr } from 'CONTRIBUTORS'; -import { SpellLink } from 'interface'; +import { ToppleTheNun, Trevor, Tyndi, Vohrr, Vollmer } from 'CONTRIBUTORS'; +import RESOURCE_TYPES from 'game/RESOURCE_TYPES'; +import { ResourceLink, SpellLink } from 'interface'; export default [ + change(date(2023, 12, 31), <>Improved tracking of for a more precise representation of overcapped ., Vollmer), change(date(2023, 11, 25), <>Fixed bug in module, Trevor), change(date(2023, 11, 24), <>Add chart to T31 tier set 4PC module, Trevor), change(date(2023, 11, 11), <>Add graphic to guide, Trevor), diff --git a/src/analysis/retail/evoker/preservation/CombatLogParser.ts b/src/analysis/retail/evoker/preservation/CombatLogParser.ts index 02ae3b0bfc9..e0005d4ddc9 100644 --- a/src/analysis/retail/evoker/preservation/CombatLogParser.ts +++ b/src/analysis/retail/evoker/preservation/CombatLogParser.ts @@ -17,7 +17,6 @@ import HotRemovalNormalizer from './normalizers/HotRemovalNormalizer'; import Checklist from 'analysis/retail/evoker/preservation/modules/features/Checklist/Module'; import EssenceDetails from './modules/features/EssenceDetails'; -import EssenceTracker from './modules/features/EssenceTracker'; import GracePeriod from './modules/talents/GracePeriod'; import Reversion from './modules/talents/Reversion'; import CallOfYsera from './modules/talents/CallOfYsera'; @@ -48,7 +47,12 @@ import RegenerativeMagic from '../shared/modules/talents/RegenerativeMagic'; import AncientFlame from './modules/talents/AncientFlame'; import T31PrevokerSet from './modules/dragonflight/tier/T31TierSet'; import EchoTypeBreakdown from './modules/talents/EchoTypeBreakdown'; -import { LeapingFlamesNormalizer, LeapingFlames } from '../shared'; +import { + LeapingFlamesNormalizer, + LeapingFlames, + SpellEssenceCost, + EssenceTracker, +} from '../shared'; class CombatLogParser extends CoreCombatLogParser { static specModules = { @@ -68,6 +72,7 @@ class CombatLogParser extends CoreCombatLogParser { //resources essenceTracker: EssenceTracker, essenceDetails: EssenceDetails, + spellEssenceCost: SpellEssenceCost, manaTracker: ManaTracker, //features diff --git a/src/analysis/retail/evoker/preservation/modules/features/EssenceDetails.tsx b/src/analysis/retail/evoker/preservation/modules/features/EssenceDetails.tsx index 84eb28b58ef..153cdd7f784 100644 --- a/src/analysis/retail/evoker/preservation/modules/features/EssenceDetails.tsx +++ b/src/analysis/retail/evoker/preservation/modules/features/EssenceDetails.tsx @@ -6,10 +6,10 @@ import STATISTIC_ORDER from 'parser/ui/STATISTIC_ORDER'; import ResourceBreakdown from 'parser/shared/modules/resources/resourcetracker/ResourceBreakdown'; import { Panel } from 'interface'; -import EssenceTracker from './EssenceTracker'; import { defineMessage } from '@lingui/macro'; import { ThresholdStyle, When } from 'parser/core/ParseResults'; import { TALENTS_EVOKER } from 'common/TALENTS'; +import { EssenceTracker } from 'analysis/retail/evoker/shared'; class EssenceDetails extends Analyzer { static dependencies = { diff --git a/src/analysis/retail/evoker/preservation/modules/features/EssenceTracker.ts b/src/analysis/retail/evoker/preservation/modules/features/EssenceTracker.ts deleted file mode 100644 index 43c5e353f09..00000000000 --- a/src/analysis/retail/evoker/preservation/modules/features/EssenceTracker.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { - EMPATH_REGEN_FACTOR, - FLOW_STATE_FACTOR, -} from 'analysis/retail/evoker/preservation/constants'; -import { TALENTS_EVOKER } from 'common/TALENTS'; -import RESOURCE_TYPES from 'game/RESOURCE_TYPES'; -import { Options, SELECTED_PLAYER } from 'parser/core/Analyzer'; -import Events, { ApplyBuffEvent, CastEvent, RemoveBuffEvent } from 'parser/core/Events'; -import ResourceTracker from 'parser/shared/modules/resources/resourcetracker/ResourceTracker'; -import { - BASE_ESSENCE_REGEN, - BASE_MAX_ESSENCE, - INNATE_MAGIC_REGEN, -} from 'analysis/retail/evoker/shared/constants'; - -class EssenceTracker extends ResourceTracker { - static dependencies = { - ...ResourceTracker.dependencies, - }; - - constructor(options: Options) { - super(options); - this.resource = RESOURCE_TYPES.ESSENCE; - this.maxResource = - BASE_MAX_ESSENCE + - (this.selectedCombatant.hasTalent(TALENTS_EVOKER.FONT_OF_MAGIC_PRESERVATION_TALENT) ? 1 : 0); - this.baseRegenRate = - BASE_ESSENCE_REGEN * - (1 + - INNATE_MAGIC_REGEN * - this.selectedCombatant.getTalentRank(TALENTS_EVOKER.INNATE_MAGIC_TALENT)); - this.addEventListener( - Events.applybuff - .by(SELECTED_PLAYER) - .spell([TALENTS_EVOKER.EMPATH_TALENT, TALENTS_EVOKER.FLOW_STATE_TALENT]), - this.increaseEssenceRegen, - ); - this.addEventListener( - Events.removebuff - .by(SELECTED_PLAYER) - .spell([TALENTS_EVOKER.EMPATH_TALENT, TALENTS_EVOKER.FLOW_STATE_TALENT]), - this.decreaseEssenceRegen, - ); - } - - increaseEssenceRegen(event: ApplyBuffEvent) { - const spellId = event.ability.guid; - //the triggerRateChange function dynamically updates baseRegenRate - let newRate = this.currentRegenRate; - if (spellId === TALENTS_EVOKER.EMPATH_TALENT.id) { - newRate *= 1 + EMPATH_REGEN_FACTOR; - } else if (spellId === TALENTS_EVOKER.FLOW_STATE_TALENT.id) { - newRate *= 1 + FLOW_STATE_FACTOR; - } - this.triggerRateChange(newRate); - } - - decreaseEssenceRegen(event: RemoveBuffEvent) { - const spellId = event.ability.guid; - let newRate = this.currentRegenRate; - if (spellId === TALENTS_EVOKER.EMPATH_TALENT.id) { - newRate /= 1 + EMPATH_REGEN_FACTOR; - } else if (spellId === TALENTS_EVOKER.FLOW_STATE_TALENT.id) { - newRate /= 1 + FLOW_STATE_FACTOR; - } - this.triggerRateChange(newRate); - } - - getAdjustedCost(event: CastEvent) { - const cost = this.getResource(event)?.cost; - if (!cost) { - this._applySpender(event, 0); - return 0; - } - return cost; - } -} - -export default EssenceTracker; diff --git a/src/analysis/retail/evoker/shared/index.ts b/src/analysis/retail/evoker/shared/index.ts index b578dbab964..e5c723ec367 100644 --- a/src/analysis/retail/evoker/shared/index.ts +++ b/src/analysis/retail/evoker/shared/index.ts @@ -8,4 +8,7 @@ export { getWastedEssenceBurst, } from './modules/normalizers/LeapingFlamesNormalizer'; export { default as LeapingFlames } from './modules/talents/LeapingFlames'; +export { default as SpellEssenceCost } from './modules/core/essence/SpellEssenceCost'; +export { default as EssenceTracker } from './modules/core/essence/EssenceTracker'; +export { default as EssenceGraph } from './modules/core/essence/EssenceGraph'; export * from './constants'; diff --git a/src/analysis/retail/evoker/devastation/modules/guide/EssenceGraph/EssenceGraph.tsx b/src/analysis/retail/evoker/shared/modules/core/essence/EssenceGraph.ts similarity index 79% rename from src/analysis/retail/evoker/devastation/modules/guide/EssenceGraph/EssenceGraph.tsx rename to src/analysis/retail/evoker/shared/modules/core/essence/EssenceGraph.ts index c4131361486..738e6cf6bf6 100644 --- a/src/analysis/retail/evoker/devastation/modules/guide/EssenceGraph/EssenceGraph.tsx +++ b/src/analysis/retail/evoker/shared/modules/core/essence/EssenceGraph.ts @@ -1,4 +1,4 @@ -import EssenceTracker from 'analysis/retail/evoker/preservation/modules/features/EssenceTracker'; +import EssenceTracker from 'analysis/retail/evoker/shared/modules/core/essence/EssenceTracker'; import ResourceGraph from 'parser/shared/modules/ResourceGraph'; class EssenceGraph extends ResourceGraph { diff --git a/src/analysis/retail/evoker/shared/modules/core/essence/EssenceTracker.ts b/src/analysis/retail/evoker/shared/modules/core/essence/EssenceTracker.ts new file mode 100644 index 00000000000..1b5c2b745e7 --- /dev/null +++ b/src/analysis/retail/evoker/shared/modules/core/essence/EssenceTracker.ts @@ -0,0 +1,112 @@ +import { + EMPATH_REGEN_FACTOR, + FLOW_STATE_FACTOR, +} from 'analysis/retail/evoker/preservation/constants'; +import { TALENTS_EVOKER } from 'common/TALENTS'; +import SPELLS from 'common/SPELLS/evoker'; +import RESOURCE_TYPES from 'game/RESOURCE_TYPES'; +import { Options, SELECTED_PLAYER } from 'parser/core/Analyzer'; +import Events, { ApplyBuffEvent, CastEvent, RemoveBuffEvent } from 'parser/core/Events'; +import ResourceTracker from 'parser/shared/modules/resources/resourcetracker/ResourceTracker'; +import { + BASE_ESSENCE_REGEN, + BASE_MAX_ESSENCE, + INNATE_MAGIC_REGEN, +} from 'analysis/retail/evoker/shared/constants'; +import { POWER_SWELL_REGEN_FACTOR } from 'analysis/retail/evoker/devastation/constants'; +import SpellEssenceCost from './SpellEssenceCost'; + +const REGEN_BUFFS = { + [TALENTS_EVOKER.EMPATH_TALENT.id]: { + spell: TALENTS_EVOKER.EMPATH_TALENT, + regenFactor: EMPATH_REGEN_FACTOR, + }, + [TALENTS_EVOKER.FLOW_STATE_TALENT.id]: { + spell: TALENTS_EVOKER.FLOW_STATE_TALENT, + regenFactor: FLOW_STATE_FACTOR, + }, + [SPELLS.POWER_SWELL_BUFF.id]: { + spell: SPELLS.POWER_SWELL_BUFF, + regenFactor: POWER_SWELL_REGEN_FACTOR, + }, +}; + +class EssenceTracker extends ResourceTracker { + static dependencies = { + ...ResourceTracker.dependencies, + spellEssenceCost: SpellEssenceCost, + }; + protected spellEssenceCost!: SpellEssenceCost; + + constructor(options: Options) { + super(options); + this.resource = RESOURCE_TYPES.ESSENCE; + this.maxResource = + BASE_MAX_ESSENCE + + (this.selectedCombatant.hasTalent(TALENTS_EVOKER.POWER_NEXUS_TALENT) ? 1 : 0); + this.initialResources = this.maxResource; + + this.baseRegenRate = + BASE_ESSENCE_REGEN * + (1 + + INNATE_MAGIC_REGEN * + this.selectedCombatant.getTalentRank(TALENTS_EVOKER.INNATE_MAGIC_TALENT)); + + this.allowMultipleGainsInSameTimestamp = true; + this.useGranularity = true; + this.adjustResourceMismatch = true; + + const regenSpells = Object.entries(REGEN_BUFFS).map(([, regenBuff]) => regenBuff.spell); + + this.addEventListener( + Events.applybuff.by(SELECTED_PLAYER).spell(regenSpells), + this.increaseEssenceRegen, + ); + this.addEventListener( + Events.removebuff.by(SELECTED_PLAYER).spell(regenSpells), + this.decreaseEssenceRegen, + ); + } + + increaseEssenceRegen(event: ApplyBuffEvent) { + const regenRate = REGEN_BUFFS[event.ability.guid].regenFactor; + const newRate = (this.baseRegenRate *= 1 + regenRate); + + this.triggerRateChange(newRate); + } + + decreaseEssenceRegen(event: RemoveBuffEvent) { + const regenRate = REGEN_BUFFS[event.ability.guid].regenFactor; + const newRate = (this.baseRegenRate /= 1 + regenRate); + + this.triggerRateChange(newRate); + } + + getAdjustedCost(event: CastEvent) { + const cost = this.spellEssenceCost.getResourceCost(event); + + if (!cost) { + this._applySpender(event, 0); + return 0; + } + + return cost; + } + + onCast(event: CastEvent) { + /* Early return as to not count Prescience casts before the fight starts + * that Augmentation might have when setting up T31 4pc buff. */ + if ( + event.timestamp < this.owner.fight.start_time && + event.ability.guid === TALENTS_EVOKER.PRESCIENCE_TALENT.id + ) { + return; + } + const cost = this.getAdjustedCost(event); + if (cost) { + this._applySpender(event, cost, this.getResource(event)); + } + } +} + +export default EssenceTracker; diff --git a/src/analysis/retail/evoker/shared/modules/core/essence/SpellEssenceCost.ts b/src/analysis/retail/evoker/shared/modules/core/essence/SpellEssenceCost.ts new file mode 100644 index 00000000000..5791d535558 --- /dev/null +++ b/src/analysis/retail/evoker/shared/modules/core/essence/SpellEssenceCost.ts @@ -0,0 +1,35 @@ +import { Options } from 'parser/core/EventSubscriber'; +import SpellResourceCost from 'parser/shared/modules/SpellResourceCost'; +import TALENTS from 'common/TALENTS/evoker'; +import { CastEvent } from 'parser/core/Events'; +import RESOURCE_TYPES from 'game/RESOURCE_TYPES'; +import { DENSE_ENERGY_ESSENCE_REDUCTION } from 'analysis/retail/evoker/devastation/constants'; +import { VOLCANISM_ESSENCE_REDUCTION } from 'analysis/retail/evoker/augmentation/constants'; + +class SpellEssenceCost extends SpellResourceCost { + static resourceType = RESOURCE_TYPES.ESSENCE; + + hasDenseEnergy: boolean; + hasVolcanism: boolean; + + constructor(options: Options) { + super(options); + this.hasDenseEnergy = this.selectedCombatant.hasTalent(TALENTS.DENSE_ENERGY_TALENT); + this.hasVolcanism = this.selectedCombatant.hasTalent(TALENTS.VOLCANISM_TALENT); + } + + getResourceCost(event: CastEvent) { + let cost = super.getResourceCost(event); + + if (this.hasDenseEnergy && event.ability.guid === TALENTS.PYRE_TALENT.id) { + cost = Math.max(0, cost - DENSE_ENERGY_ESSENCE_REDUCTION); + } + if (this.hasVolcanism && event.ability.guid === TALENTS.ERUPTION_TALENT.id) { + cost = Math.max(0, cost - VOLCANISM_ESSENCE_REDUCTION); + } + + return cost; + } +} + +export default SpellEssenceCost; diff --git a/src/parser/shared/modules/resources/resourcetracker/ResourceTracker.ts b/src/parser/shared/modules/resources/resourcetracker/ResourceTracker.ts index 96468ec48c6..1810c51de45 100644 --- a/src/parser/shared/modules/resources/resourcetracker/ResourceTracker.ts +++ b/src/parser/shared/modules/resources/resourcetracker/ResourceTracker.ts @@ -132,6 +132,8 @@ export default class ResourceTracker extends Analyzer { /** The maximum amount of the resource. * This is only used as a starting value - it will be updated from event's classResources field. */ maxResource!: number; + /** The amount of resources you start the fight with. */ + initialResources = 0; /** Resource's base regeneration rate, in units per second. This is the value before haste. * Leave as 0 for non-regenerating resources. @@ -157,6 +159,15 @@ export default class ResourceTracker extends Analyzer { * timestamp as a valid gain. */ allowMultipleGainsInSameTimestamp = false; + /** Instead of calculating resource as rounded numbers, use decimal precision for granularity. + * This is needed for specs that have resource generation that isn't provided in whole numbers + * eg. Evoker's Essence */ + useGranularity = false; + /** amount of decimals to use for granularity */ + granularity = 2; + /** If true, will adjust the resource amount to account for any mismatch between the previous + * update's current and the new current. */ + adjustResourceMismatch = false; // END override values /** Data object for the whole fight - updated during analysis */ @@ -404,12 +415,46 @@ export default class ResourceTracker extends Analyzer { !this.allowMultipleGainsInSameTimestamp && prevUpdate && timestamp <= prevUpdate.timestamp + MULTI_UPDATE_BUFFER_MS; + /** If 'useGranularity' is true we use 'calculatedBeforeAmount', since 'reportedBeforeAmount' will always return a rounded + * amount, whilst 'calculatedBeforeAmount' will return the amount with the decimal precision specified by 'granularity'. */ const beforeAmount = - reportedBeforeAmount !== undefined && !withinMultiUpdateBuffer + reportedBeforeAmount !== undefined && !withinMultiUpdateBuffer && !this.useGranularity ? reportedBeforeAmount : calculatedBeforeAmount; const current = Math.max(Math.min(max, beforeAmount + change), 0); // current is the after amount + /** There may be a discrepancy between the previous update's current value and + * the new current value due to the time elapsed since the last resource generation calculation. + * This discrepancy can cause issues when plotting the resource graph. + * + * For instance, if prevUpdate.current = 100, + * and beforeAmount = 110 with a change of -105, + * + * Ideally, the plot should show: 100 -> 110 -> 5 + * However, it displays: 100 -> 5, despite reporting a change of -105. + * + * To address this, we generate a new update using the beforeAmount as the current value. + */ + if ( + this.adjustResourceMismatch && + prevUpdate && + prevUpdate.current < beforeAmount && + type === 'spend' && + change < 0 + ) { + this._logAndPushUpdate({ + type: 'gain', + timestamp: timestamp - (timestamp - prevUpdate.timestamp) / 2, + change: beforeAmount - prevUpdate.current, + current: beforeAmount, + max, + rate: this.currentRegenRate, + rateWaste: 0, + changeWaste: 0, + atCap: false, + }); + } + // if our resource regenerates and the beforeAmount was capped, // then we were wasting resources due to natural regeneration let rateWaste = 0; @@ -520,7 +565,7 @@ export default class ResourceTracker extends Analyzer { const lastUpdate = this.resourceUpdates.at(-1); if (!lastUpdate) { // there have been no updates so far, return a default - return 0; // TODO make some resources default to max? + return this.initialResources; } if (lastUpdate.rate === 0) { // resource doesn't naturally regenerate, so return the last seen val @@ -528,7 +573,10 @@ export default class ResourceTracker extends Analyzer { } // resource naturally regenerates, estimate current based on last seen val const timePassedSeconds = (this.owner.currentTimestamp - lastUpdate.timestamp) / 1000; - const naturalGain = Math.round(timePassedSeconds * lastUpdate.rate); // whole number amount of resources pls + const naturalGain = this.useGranularity + ? parseFloat((timePassedSeconds * lastUpdate.rate).toFixed(this.granularity)) + : Math.round(timePassedSeconds * lastUpdate.rate); // whole number amount of resources pls + return Math.min(lastUpdate.max, lastUpdate.current + naturalGain); }