From dad5291d97be568e3180df2d1abf712134b894ba Mon Sep 17 00:00:00 2001 From: "Johnny C. Lam" Date: Fri, 10 Sep 2021 15:16:09 -0400 Subject: [PATCH 1/4] fix: rewrite DemonHunterDemonic module to properly handle state The `DemonHunterDemonic` module doesn't have to gain or lose fake auras to correctly track Demonic in Shadowlands. Only add a Metamorphosis buff of the correct duration within the simulator if a Demonic trigger spell was cast, for either Havoc or Vengeance specializations. --- src/ioc.ts | 3 +- src/states/DemonHunterDemonic.ts | 285 ++++++++++++++----------------- 2 files changed, 128 insertions(+), 160 deletions(-) diff --git a/src/ioc.ts b/src/ioc.ts index 020cce141..8cda56ea7 100644 --- a/src/ioc.ts +++ b/src/ioc.ts @@ -402,7 +402,8 @@ export class IoC { this.bossMod = new OvaleBossModClass(this.ovale, this.debug, combat); this.demonHunterDemonic = new OvaleDemonHunterDemonicClass( this.aura, - this.combatLogEvent, + this.paperDoll, + this.spellBook, this.ovale, this.debug ); diff --git a/src/states/DemonHunterDemonic.ts b/src/states/DemonHunterDemonic.ts index aca301706..3a0ecdee0 100644 --- a/src/states/DemonHunterDemonic.ts +++ b/src/states/DemonHunterDemonic.ts @@ -1,190 +1,157 @@ -import { OvaleAuraClass } from "./Aura"; import aceEvent, { AceEvent } from "@wowts/ace_event-3.0"; -import { - GetSpecialization, - GetSpecializationInfo, - GetTime, - GetTalentInfoByID, -} from "@wowts/wow-mock"; -import { huge } from "@wowts/math"; -import { select } from "@wowts/lua"; -import { OvaleClass } from "../Ovale"; +import { LuaArray, LuaObj } from "@wowts/lua"; +import { SpellId, TalentId } from "@wowts/wow-mock"; import { AceModule } from "@wowts/tsaddon"; -import { CombatLogEvent, SpellPayloadHeader } from "../engine/combat-log-event"; import { DebugTools, Tracer } from "../engine/debug"; +import { SpellCastEventHandler, StateModule } from "../engine/state"; +import { OvaleClass } from "../Ovale"; +import { OvaleAuraClass } from "./Aura"; +import { OvalePaperDollClass } from "./PaperDoll"; +import { OvaleSpellBookClass } from "./SpellBook"; -const infinity = huge; -const havocDemonicTalentId = 22547; -const havocSpecId = 577; -const havocEyeBeamSpellId = 198013; -const havocMetaBuffId = 162264; -const hiddenBuffId = -havocDemonicTalentId; -const hiddenBuffDuration = infinity; -const hiddenBuffExtendedByDemonic = "Extended by Demonic"; +const demonicTriggerId: LuaObj> = { + havoc: { + [SpellId.eye_beam]: true, + }, + vengeance: { + [SpellId.fel_devastation]: true, + }, +}; -export class OvaleDemonHunterDemonicClass { - playerGUID: string; - isDemonHunter = false; - isHavoc: boolean; - hasDemonic: boolean; +const metamorphosisId: LuaObj = { + havoc: SpellId.metamorphosis, + vengeance: SpellId.metamorphosis_vengeance, +}; +export class OvaleDemonHunterDemonicClass implements StateModule { private module: AceModule & AceEvent; - private debug: Tracer; + private tracer: Tracer; + + private specialization = "havoc"; + private hasDemonicTalent = false; constructor( - private ovaleAura: OvaleAuraClass, - private combatLogEvent: CombatLogEvent, + private aura: OvaleAuraClass, + private paperDoll: OvalePaperDollClass, + private spellBook: OvaleSpellBookClass, private ovale: OvaleClass, - ovaleDebug: DebugTools + debug: DebugTools ) { this.module = ovale.createModule( "OvaleDemonHunterDemonic", - this.handleInitialize, - this.handleDisable, + this.onEnable, + this.onDisable, aceEvent ); - this.debug = ovaleDebug.create(this.module.GetName()); - this.playerGUID = this.ovale.playerGUID; - this.isHavoc = false; - this.hasDemonic = false; + this.tracer = debug.create(this.module.GetName()); } - private handleInitialize = () => { - this.isDemonHunter = - (this.ovale.playerClass == "DEMONHUNTER" && true) || false; - if (this.isDemonHunter) { - this.debug.debug("playerGUID: (%s)", this.ovale.playerGUID); + private onEnable = () => { + if (this.ovale.playerClass == "DEMONHUNTER") { this.module.RegisterMessage( - "Ovale_TalentsChanged", - this.handleTalentsChanged + "Ovale_SpecializationChanged", + this.onOvaleSpecializationChanged ); + if (this.paperDoll.isSpecialization("havoc")) { + this.onOvaleSpecializationChanged("onEnable", "havoc", "havoc"); + } else if (this.paperDoll.isSpecialization("vengeance")) { + this.onOvaleSpecializationChanged( + "onEnable", + "vengeance", + "vengeance" + ); + } } }; - private handleDisable = () => { - this.module.UnregisterMessage("Ovale_TalentsChanged"); + + private onDisable = () => { + if (this.ovale.playerClass == "DEMONHUNTER") { + this.module.UnregisterMessage("Ovale_SpecializationChanged"); + this.module.UnregisterMessage("Ovale_TalentsChanged"); + this.hasDemonicTalent = false; + } }; - private handleTalentsChanged = (event: string) => { - this.isHavoc = - (this.isDemonHunter && - GetSpecializationInfo(GetSpecialization()) == havocSpecId && - true) || - false; - this.hasDemonic = - (this.isHavoc && - select( - 10, - GetTalentInfoByID(havocDemonicTalentId, havocSpecId) - ) && - true) || - false; - if (this.isHavoc && this.hasDemonic) { - this.debug.debug("We are a havoc DH with Demonic."); - this.combatLogEvent.registerEvent( - "SPELL_CAST_SUCCESS", - this, - this.handleCombatLogEvent - ); - this.combatLogEvent.registerEvent( - "SPELL_AURA_REMOVED", - this, - this.handleCombatLogEvent + + private onOvaleSpecializationChanged = ( + event: string, + newSpecialization: string, + oldSpecialization: string + ) => { + this.specialization = newSpecialization; + if (newSpecialization == "havoc" || newSpecialization == "vengeance") { + this.tracer.debug("Installing Demonic event handlers."); + this.module.RegisterMessage( + "Ovale_TalentsChanged", + this.onOvaleTalentsChanged ); + this.onOvaleTalentsChanged(event); } else { - if (!this.isHavoc) { - this.debug.debug("We are not a havoc DH."); - } else if (!this.hasDemonic) { - this.debug.debug("We don't have the Demonic talent."); - } - this.dropAura(); - this.combatLogEvent.unregisterAllEvents(this); + this.tracer.debug("Removing Demonic event handlers."); + this.module.UnregisterMessage("Ovale_TalentsChanged"); + this.hasDemonicTalent = false; } }; - private handleCombatLogEvent(cleuEvent: string) { - const cleu = this.combatLogEvent; - if (cleu.sourceGUID == this.playerGUID) { - if (cleuEvent == "SPELL_CAST_SUCCESS") { - const header = cleu.header as SpellPayloadHeader; - const spellId = header.spellId; - const spellName = header.spellName; - if (havocEyeBeamSpellId == spellId) { - this.debug.debug( - "Spell %d (%s) has successfully been cast. Gaining Aura (only during meta).", - spellId, - spellName - ); - this.gainAura(); - } - } else if (cleuEvent == "SPELL_AURA_REMOVED") { - const header = cleu.header as SpellPayloadHeader; - const spellId = header.spellId; - const spellName = header.spellName; - if (havocMetaBuffId == spellId) { - this.debug.debug( - "Aura %d (%s) is removed. Dropping Aura.", - spellId, - spellName - ); - this.dropAura(); - } + + private onOvaleTalentsChanged = (event: string) => { + const hasDemonicTalent = this.hasDemonicTalent; + if (this.specialization == "havoc") { + this.hasDemonicTalent = + this.spellBook.getTalentPoints(TalentId.demonic_talent) > 0; + } else if (this.specialization == "vengenace") { + this.hasDemonicTalent = + this.spellBook.getTalentPoints( + TalentId.demonic_talent_vengeance + ) > 0; + } else { + this.hasDemonicTalent = false; + } + if (hasDemonicTalent != this.hasDemonicTalent) { + if (this.hasDemonicTalent) { + this.tracer.debug("Gained Demonic talent."); + } else { + this.tracer.debug("Lost Demonic talent."); } } - } - gainAura() { - const now = GetTime(); - const auraMeta = this.ovaleAura.getAura( - "player", - havocMetaBuffId, - now, - "HELPFUL", - true - ); - if (auraMeta && this.ovaleAura.isActiveAura(auraMeta, now)) { - this.debug.debug( - "Adding '%s' (%d) buff to player %s.", - hiddenBuffExtendedByDemonic, - hiddenBuffId, - this.playerGUID - ); - const duration = hiddenBuffDuration; - const ending = now + hiddenBuffDuration; - this.ovaleAura.gainedAuraOnGUID( - this.playerGUID, - now, - hiddenBuffId, - this.playerGUID, - "HELPFUL", - false, - undefined, - 1, - undefined, - duration, - ending, - false, - hiddenBuffExtendedByDemonic, - undefined, - undefined, - undefined - ); - } else { - this.debug.debug( - "Aura 'Metamorphosis' (%d) is not present.", - havocMetaBuffId - ); + }; + + initializeState() {} + resetState() {} + cleanState() {} + + applySpellAfterCast: SpellCastEventHandler = ( + spellId, + targetGUID, + startCast, + endCast, + channel, + spellcast + ) => { + if ( + this.hasDemonicTalent && + demonicTriggerId[this.specialization][spellId] + ) { + /* + * Demonic grants 6 seconds of Metamorphosis, plus the + * duration of the channeled spell. + */ + const duration = 6 + ((channel && endCast - startCast) || 0); + const atTime = (channel && startCast) || endCast; + this.triggerMetamorphosis(atTime, duration); } - } - dropAura() { - const now = GetTime(); - this.debug.debug( - "Removing '%s' (%d) buff on player %s.", - hiddenBuffExtendedByDemonic, - hiddenBuffId, - this.playerGUID - ); - this.ovaleAura.lostAuraOnGUID( - this.playerGUID, - now, - hiddenBuffId, - this.playerGUID + }; + + private triggerMetamorphosis = (atTime: number, duration: number) => { + const auraId = metamorphosisId[this.specialization]; + this.tracer.log(`Triggering Demonic Metamorphosis (${auraId}).`); + this.aura.addAuraToGUID( + this.ovale.playerGUID, + auraId, + this.ovale.playerGUID, + "HELPFUL", + undefined, + atTime, + atTime + duration, + atTime ); - } + }; } From ab1580377d4376d001034510b4d7f92965cee69e Mon Sep 17 00:00:00 2001 From: "Johnny C. Lam" Date: Fri, 10 Sep 2021 16:37:47 -0400 Subject: [PATCH 2/4] fix: rewrite DemonHunterSigils module to properly handle state Keep track of current sigils that are charging (before activation) and also keep track of sigils in the simulator that are charging due to a spell cast within the simulator. Don't look for an activation spell because if a sigil doesn't hit anything, then it'll never seem like it activated. Just assume that it will activate after the charging time has ended. Also add Elysian Decree to the sigils that are tracked. --- src/ioc.ts | 14 +- src/states/DemonHunterSigils.ts | 282 ++++++++++++++++++++------------ 2 files changed, 188 insertions(+), 108 deletions(-) diff --git a/src/ioc.ts b/src/ioc.ts index 8cda56ea7..9d5d6352d 100644 --- a/src/ioc.ts +++ b/src/ioc.ts @@ -131,6 +131,7 @@ export class IoC { const runner = new Runner(this.debug, this.baseState, this.condition); this.data = new OvaleDataClass(runner, this.debug); this.combatLogEvent = new CombatLogEvent(this.ovale, this.debug); + this.guid = new Guids(this.ovale, this.debug); this.equipment = new OvaleEquipmentClass( this.ovale, this.debug, @@ -141,7 +142,9 @@ export class IoC { this.ovale, this.debug ); - this.guid = new Guids(this.ovale, this.debug); + const covenant = new Covenant(this.ovale, this.debug); + const runeforge = new Runeforge(this.ovale, this.debug, this.equipment); + const soulbind = new Soulbind(this.ovale, this.debug); this.spellBook = new OvaleSpellBookClass( this.ovale, this.debug, @@ -155,10 +158,10 @@ export class IoC { this.debug ); this.demonHunterSigils = new OvaleSigilClass( - this.paperDoll, this.ovale, - this.spellBook, - this.combatLogEvent + this.debug, + this.paperDoll, + this.spellBook ); const combat = new OvaleCombatClass( this.ovale, @@ -423,9 +426,6 @@ export class IoC { controls ); this.recount = new OvaleRecountClass(this.ovale, this.score); - const covenant = new Covenant(this.ovale, this.debug); - const runeforge = new Runeforge(this.ovale, this.debug, this.equipment); - const soulbind = new Soulbind(this.ovale, this.debug); this.conditions = new OvaleConditions( this.condition, this.data, diff --git a/src/states/DemonHunterSigils.ts b/src/states/DemonHunterSigils.ts index 83efc0bdd..66e79e948 100644 --- a/src/states/DemonHunterSigils.ts +++ b/src/states/DemonHunterSigils.ts @@ -1,163 +1,243 @@ -import { OvalePaperDollClass } from "./PaperDoll"; -import { OvaleSpellBookClass } from "./SpellBook"; import aceEvent, { AceEvent } from "@wowts/ace_event-3.0"; -import { ipairs, LuaObj, LuaArray, tonumber, lualength } from "@wowts/lua"; -import { insert, remove } from "@wowts/table"; -import { GetTime, TalentId } from "@wowts/wow-mock"; -import { OvaleClass } from "../Ovale"; +import { LuaArray, pairs } from "@wowts/lua"; import { AceModule } from "@wowts/tsaddon"; -import { CombatLogEvent, SpellPayloadHeader } from "../engine/combat-log-event"; -import { StateModule } from "../engine/state"; +import { GetTime, SpellId, TalentId } from "@wowts/wow-mock"; +import { OvaleClass } from "../Ovale"; +import { DebugTools, Tracer } from "../engine/debug"; +import { SpellCastEventHandler, States, StateModule } from "../engine/state"; +import { Queue } from "../tools/Queue"; +import { KeyCheck } from "../tools/tools"; +import { OvalePaperDollClass } from "./PaperDoll"; +import { OvaleSpellBookClass } from "./SpellBook"; + +class SigilData { + chains: Queue; + flame: Queue; + kyrian: Queue; + misery: Queue; + silence: Queue; -const updateDelay = 0.5; -const sigilActivationTime = 2; -type SigilType = "flame" | "silence" | "misery" | "chains"; -const activatedSigils: LuaObj> = {}; + constructor() { + this.chains = new Queue(); + this.flame = new Queue(); + this.kyrian = new Queue(); + this.misery = new Queue(); + this.silence = new Queue(); + } +} -interface Sigil { +type SigilType = keyof SigilData; + +const checkSigilType: KeyCheck = { + chains: true, + flame: true, + kyrian: true, + misery: true, + silence: true, +}; + +interface SigilInfo { type: SigilType; talent?: number; } -const sigilStart: LuaArray = { - [204513]: { - type: "flame", +const sigilTrigger: LuaArray = { + [SpellId.elysian_decree]: { + type: "kyrian", }, - [204596]: { - type: "flame", - }, - [189110]: { + [SpellId.infernal_strike]: { type: "flame", talent: TalentId.abyssal_strike_talent, }, - [202137]: { - type: "silence", - }, - [207684]: { - type: "misery", - }, - [202138]: { + [SpellId.sigil_of_chains]: { type: "chains", }, -}; -const sigilEnd: LuaArray = { - [204598]: { + [SpellId.sigil_of_flame]: { type: "flame", }, - [204490]: { - type: "silence", - }, - [207685]: { + [SpellId.sigil_of_misery]: { type: "misery", }, - [204834]: { - type: "chains", + [SpellId.sigil_of_silence]: { + type: "silence", }, }; -export class OvaleSigilClass implements StateModule { +export class OvaleSigilClass extends States implements StateModule { private module: AceModule & AceEvent; + private tracer: Tracer; + + // number of seconds a sigil charges before activation + private chargeDuration = 2; constructor( - private ovalePaperDoll: OvalePaperDollClass, private ovale: OvaleClass, - private ovaleSpellBook: OvaleSpellBookClass, - private combatLogEvent: CombatLogEvent + debug: DebugTools, + private paperDoll: OvalePaperDollClass, + private spellBook: OvaleSpellBookClass ) { + super(SigilData); this.module = ovale.createModule( "OvaleSigil", - this.handleInitialize, - this.handleDisable, + this.onEnable, + this.onDisable, aceEvent ); - activatedSigils["flame"] = {}; - activatedSigils["silence"] = {}; - activatedSigils["misery"] = {}; - activatedSigils["chains"] = {}; + this.tracer = debug.create(this.module.GetName()); } - private handleInitialize = () => { + private onEnable = () => { if (this.ovale.playerClass == "DEMONHUNTER") { + this.module.RegisterMessage( + "Ovale_SpecializationChanged", + this.onOvaleSpecializationChanged + ); this.module.RegisterEvent( "UNIT_SPELLCAST_SUCCEEDED", - this.handleUnitSpellCastSucceeded + this.onUnitSpellCastSucceeded ); - this.combatLogEvent.registerEvent( - "SPELL_AURA_APPLIED", - this, - this.handleSpellAuraApplied + + const specialization = this.paperDoll.getSpecialization(); + this.onOvaleSpecializationChanged( + "onEnable", + specialization, + specialization ); } }; - private handleDisable = () => { + + private onDisable = () => { if (this.ovale.playerClass == "DEMONHUNTER") { + this.module.UnregisterMessage("Ovale_SpecializationChanged"); + this.module.UnregisterMessage("Ovale_TalentsChanged"); this.module.UnregisterEvent("UNIT_SPELLCAST_SUCCEEDED"); - this.combatLogEvent.unregisterAllEvents(this); } }; - private handleSpellAuraApplied = (cleuEvent: string) => { - if (!this.ovalePaperDoll.isSpecialization("vengeance")) { - return; + private onOvaleSpecializationChanged = ( + event: string, + newSpecialization: string, + oldSpecialization: string + ) => { + if (newSpecialization === "vengeance") { + this.module.RegisterMessage( + "Ovale_TalentsChanged", + this.onOvaleTalentsChanged + ); + this.onOvaleTalentsChanged(event); } - const cleu = this.combatLogEvent; - if (cleu.sourceGUID == this.ovale.playerGUID) { - const header = cleu.header as SpellPayloadHeader; - const spellId = header.spellId; - if (sigilEnd[spellId] != undefined) { - const s = sigilEnd[spellId]; - const t = s.type; - remove(activatedSigils[t], 1); - } + }; + + private onOvaleTalentsChanged = (event: string) => { + const talent = TalentId.quickened_sigils_talent; + const hasQuickenedSigils = this.spellBook.getTalentPoints(talent) > 0; + this.chargeDuration = 2; + if (hasQuickenedSigils) { + // Quickened Sigils talent reduces activation time by 1 second. + this.chargeDuration -= 1; } }; - private handleUnitSpellCastSucceeded = ( + private onUnitSpellCastSucceeded = ( event: string, unitId: string, guid: string, - spellId: number, - ...parameters: any[] + spellId: number ) => { - if (!this.ovalePaperDoll.isSpecialization("vengeance")) { - return; - } - if (unitId == undefined || unitId != "player") { - return; - } - const id = tonumber(spellId); - if (sigilStart[id] != undefined) { - const s = sigilStart[id]; - const t = s.type; - const tal = s.talent || undefined; - if ( - tal == undefined || - this.ovaleSpellBook.getTalentPoints(tal) > 0 - ) { - insert(activatedSigils[t], GetTime()); + if (unitId == "player") { + if (sigilTrigger[spellId]) { + const info = sigilTrigger[spellId]; + const sigilType = info.type; + const talent = info.talent; + if (!talent || this.spellBook.getTalentPoints(talent) > 0) { + const now = GetTime(); + const state = this.current; + this.triggerSigil(state, sigilType, now); + const count = state[sigilType].length; + this.tracer.debug( + `"${sigilType}" (${count}) placed at ${now}` + ); + } } } }; - isSigilCharging(type: SigilType, atTime: number) { - if (lualength(activatedSigils[type]) == 0) { - return false; + private triggerSigil = ( + state: SigilData, + sigilType: SigilType, + atTime: number + ) => { + const queue = state[sigilType]; + let activationTime = queue.front(); + while (activationTime && activationTime < atTime) { + queue.shift(); + activationTime = queue.front(); } - let charging = false; - for (const [, v] of ipairs(activatedSigils[type])) { - let activationTime = sigilActivationTime + updateDelay; - if ( - this.ovaleSpellBook.getTalentPoints( - TalentId.quickened_sigils_talent - ) > 0 - ) { - activationTime = activationTime - 1; + activationTime = atTime + this.chargeDuration; + queue.push(activationTime); + }; + + initializeState(): void {} + + resetState() { + if (this.ovale.playerClass == "DEMONHUNTER") { + for (const [sigilType] of pairs(checkSigilType)) { + const current = this.current[sigilType as SigilType]; + const next = this.next[sigilType as SigilType]; + { + // TODO replace with next.clear() when available. + next.first = 0; + next.last = 0; + next.length = 0; + } + for (let i = 1; i <= current.length; i++) { + const activationTime = current.at(i); + if (activationTime) { + next.push(activationTime); + } + } } - charging = charging || atTime < v + activationTime; } - return charging; } + cleanState(): void {} - initializeState(): void {} - resetState(): void {} + + applySpellAfterCast: SpellCastEventHandler = ( + spellId, + targetGUID, + startCast, + endCast, + channel, + spellcast + ) => { + if (this.ovale.playerClass == "DEMONHUNTER") { + if (sigilTrigger[spellId]) { + const info = sigilTrigger[spellId]; + const sigilType = info.type; + const talent = info.talent; + if (!talent || this.spellBook.getTalentPoints(talent) > 0) { + const state = this.next; + this.triggerSigil(state, sigilType, endCast); + const count = state[sigilType].length; + this.tracer.log( + `"${sigilType}" (${count}) placed at ${endCast}` + ); + } + } + } + }; + + isSigilCharging(sigilType: SigilType, atTime: number) { + const queue = this.next[sigilType]; + for (let i = 1; i <= queue.length; i++) { + const activationTime = queue.at(i); + if (activationTime) { + const start = activationTime - this.chargeDuration; + if (start <= atTime && atTime < activationTime) { + return true; + } + } + } + return false; + } } From eda02c8dafe0ecec455d195270406a55d0fb28cf Mon Sep 17 00:00:00 2001 From: "Johnny C. Lam" Date: Sat, 11 Sep 2021 03:03:36 -0400 Subject: [PATCH 3/4] fix: rewrite DemonHunterSoulFragments module to properly handle state Keep track of pending Soul Fragments that have not yet spawned and also keep track of the total number of Soul Fragments after spells have been cast within the simulator. --- src/ioc.ts | 7 +- src/states/DemonHunterDemonic.ts | 15 +- src/states/DemonHunterSoulFragments.ts | 305 ++++++++++++++++++------- 3 files changed, 236 insertions(+), 91 deletions(-) diff --git a/src/ioc.ts b/src/ioc.ts index 9d5d6352d..ecb45ff6f 100644 --- a/src/ioc.ts +++ b/src/ioc.ts @@ -327,10 +327,11 @@ export class IoC { this.combatLogEvent ); this.demonHunterSoulFragments = new OvaleDemonHunterSoulFragmentsClass( - this.aura, this.ovale, - this.paperDoll, - this.combatLogEvent + this.debug, + this.aura, + this.combatLogEvent, + this.paperDoll ); this.runes = new OvaleRunesClass( this.ovale, diff --git a/src/states/DemonHunterDemonic.ts b/src/states/DemonHunterDemonic.ts index 3a0ecdee0..f7dde2fb1 100644 --- a/src/states/DemonHunterDemonic.ts +++ b/src/states/DemonHunterDemonic.ts @@ -52,15 +52,12 @@ export class OvaleDemonHunterDemonicClass implements StateModule { "Ovale_SpecializationChanged", this.onOvaleSpecializationChanged ); - if (this.paperDoll.isSpecialization("havoc")) { - this.onOvaleSpecializationChanged("onEnable", "havoc", "havoc"); - } else if (this.paperDoll.isSpecialization("vengeance")) { - this.onOvaleSpecializationChanged( - "onEnable", - "vengeance", - "vengeance" - ); - } + const specialization = this.paperDoll.getSpecialization(); + this.onOvaleSpecializationChanged( + "onEnable", + specialization, + specialization + ); } }; diff --git a/src/states/DemonHunterSoulFragments.ts b/src/states/DemonHunterSoulFragments.ts index d92ca7a4e..2c1802a91 100644 --- a/src/states/DemonHunterSoulFragments.ts +++ b/src/states/DemonHunterSoulFragments.ts @@ -1,120 +1,267 @@ -import { OvaleAuraClass } from "./Aura"; -import { GetTime } from "@wowts/wow-mock"; -import { LuaArray } from "@wowts/lua"; +import aceEvent, { AceEvent } from "@wowts/ace_event-3.0"; +import { LuaArray, pairs } from "@wowts/lua"; +import { AceModule } from "@wowts/tsaddon"; +import { GetTime, SpellId } from "@wowts/wow-mock"; import { OvaleClass } from "../Ovale"; -import { OvalePaperDollClass } from "./PaperDoll"; import { CombatLogEvent, SpellPayloadHeader } from "../engine/combat-log-event"; +import { DebugTools, Tracer } from "../engine/debug"; +import { SpellCastEventHandler, States, StateModule } from "../engine/state"; +import { OvaleAuraClass } from "./Aura"; +import { OvalePaperDollClass } from "./PaperDoll"; -const soulFragmentsBuffId = 203981; -const metamorphosisBuffId = 187827; -const soulFragmentSpells: LuaArray = { +const generator: LuaArray = { [225919]: 2, // Fracture - [203782]: 1, // Shear - [228477]: -2, // Soul Cleave + [SpellId.shear]: 1, }; -const soulFragmentFinishers: LuaArray = { - [247454]: true, // Spirit Bomb - [263648]: true, // Soul Barrier + +const spender: LuaArray = { + [SpellId.soul_barrier]: -5, + [SpellId.soul_cleave]: -2, + [SpellId.spirit_bomb]: -5, }; -export class OvaleDemonHunterSoulFragmentsClass { - estimatedCount = 0; - atTime?: number; - estimated?: boolean; +// Soul Fragments buff ID +const soulFragmentsId = 203981; + +const trackedBuff: LuaArray = { + [soulFragmentsId]: true, // Soul Fragments + [SpellId.metamorphosis_vengeance]: true, +}; + +class SoulFragmentsData { + count = 0; // total soul fragments, including pending spawns +} + +export class OvaleDemonHunterSoulFragmentsClass + extends States + implements StateModule +{ + private module: AceModule & AceEvent; + private tracer: Tracer; + + private hasSoulFragmentsHandlers = false; + private hasMetamorphosis = false; + private count = 0; // stack count of Soul Fragment buff + private pending = 0; // pending soul fragment spawns + // invariant: count + pending <= 5 constructor( - private ovaleAura: OvaleAuraClass, private ovale: OvaleClass, - private ovalePaperDoll: OvalePaperDollClass, - private combatLogEvent: CombatLogEvent + debug: DebugTools, + private aura: OvaleAuraClass, + private combatLogEvent: CombatLogEvent, + private paperDoll: OvalePaperDollClass ) { - ovale.createModule( + super(SoulFragmentsData); + this.module = ovale.createModule( "OvaleDemonHunterSoulFragments", - this.handleInitialize, - this.handleDisable + this.onEnable, + this.onDisable, + aceEvent ); + this.tracer = debug.create(this.module.GetName()); } - private handleInitialize = () => { + private onEnable = () => { + if (this.ovale.playerClass == "DEMONHUNTER") { + this.module.RegisterMessage( + "Ovale_SpecializationChanged", + this.onOvaleSpecializationChanged + ); + const specialization = this.paperDoll.getSpecialization(); + this.onOvaleSpecializationChanged( + "onEnable", + specialization, + specialization + ); + } + }; + + private onDisable = () => { if (this.ovale.playerClass == "DEMONHUNTER") { + this.module.UnregisterMessage("Ovale_SpecializationChanged"); + this.unregisterSoulFragmentsHandlers(); + } + }; + + private onOvaleSpecializationChanged = ( + event: string, + newSpecialization: string, + oldSpecialization: string + ) => { + if (newSpecialization == "vengeance") { + this.registerSoulFragmentsHandlers(); + const now = GetTime(); + for (const [auraId] of pairs(trackedBuff)) { + const aura = this.aura.getAura( + "player", + auraId, + now, + "HELPFUL", + true + ); + if (aura && this.aura.isActiveAura(aura, now)) { + this.onOvaleAuraEvent( + "Ovale_AuraChanged", + now, + aura.guid, + aura.spellId, + aura.source + ); + } + } + } else { + this.unregisterSoulFragmentsHandlers(); + } + }; + + private registerSoulFragmentsHandlers = () => { + if (!this.hasSoulFragmentsHandlers) { + this.module.RegisterMessage( + "Ovale_AuraAdded", + this.onOvaleAuraEvent + ); + this.module.RegisterMessage( + "Ovale_AuraChanged", + this.onOvaleAuraEvent + ); + this.module.RegisterMessage( + "Ovale_AuraRemoved", + this.onOvaleAuraEvent + ); this.combatLogEvent.registerEvent( "SPELL_CAST_SUCCESS", this, - this.handleSpellCastSuccess + this.onSpellCastSuccess ); + this.hasSoulFragmentsHandlers = true; } }; - private handleDisable = () => { - if (this.ovale.playerClass == "DEMONHUNTER") { + private unregisterSoulFragmentsHandlers = () => { + if (this.hasSoulFragmentsHandlers) { + this.module.UnregisterMessage("Ovale_AuraAdded"); + this.module.UnregisterMessage("Ovale_AuraChanged"); + this.module.UnregisterMessage("Ovale_AuraRemoved"); this.combatLogEvent.unregisterAllEvents(this); + this.hasSoulFragmentsHandlers = false; + + this.hasMetamorphosis = false; + this.count = 0; + this.pending = 0; + this.current.count = 0; } }; - private handleSpellCastSuccess = (cleuEvent: string) => { - if (!this.ovalePaperDoll.isSpecialization("vengeance")) { - return; + + private onOvaleAuraEvent = ( + event: string, + atTime: number, + guid: string, + auraId: number, + caster: string + ) => { + if (guid == this.ovale.playerGUID) { + if (auraId == SpellId.metamorphosis_vengeance) { + if ( + event == "Ovale_AuraAdded" || + event == "Ovale_AuraChanged" + ) { + this.hasMetamorphosis = true; + } else if (event == "Ovale_AuraRemoved") { + this.hasMetamorphosis = false; + } + } else if (auraId == soulFragmentsId) { + if ( + event == "Ovale_AuraAdded" || + event == "Ovale_AuraChanged" + ) { + const aura = this.aura.getAura( + "player", + auraId, + atTime, + "HELPFUL", + true + ); + if (aura && this.aura.isActiveAura(aura, atTime)) { + const gained = aura.stacks - this.count; + if (gained > 0) { + const pending = this.pending - gained; + this.pending = (pending > 0 && pending) || 0; + } + this.count = aura.stacks; + const count = this.count + this.pending; + this.current.count = (count < 5 && count) || 5; + this.pending = this.current.count - this.count; + this.tracer.debug( + `${this.current.count} = ${this.count} + ${this.pending}` + ); + } + } else if (event == "Ovale_AuraRemoved") { + this.count = 0; + this.current.count = this.pending; + this.tracer.debug( + `${this.current.count} = ${this.count} + ${this.pending}` + ); + } + } } + }; + + private onSpellCastSuccess = (cleuEvent: string) => { const cleu = this.combatLogEvent; if (cleu.sourceGUID == this.ovale.playerGUID) { const header = cleu.header as SpellPayloadHeader; const spellId = header.spellId; - if (soulFragmentSpells[spellId]) { - const now = GetTime(); - let fragments = soulFragmentSpells[spellId]; - if (fragments > 0 && this.hasMetamorphosis(now)) { + if (generator[spellId]) { + let fragments = generator[spellId]; + if (fragments > 0 && this.hasMetamorphosis) { + // Metamorphosis triggers an extra Lesser Soul Fragment fragments = fragments + 1; } - this.addPredictedSoulFragments(now, fragments); - } else if (soulFragmentFinishers[spellId]) { - const now = GetTime(); - this.setPredictedSoulFragment(now, 0); + this.pending += fragments; + const count = this.count + this.pending; + this.current.count = (count < 5 && count) || 5; + this.pending = this.current.count - this.count; + this.tracer.debug( + `${this.current.count} = ${this.count} + ${this.pending}` + ); } } }; - addPredictedSoulFragments(atTime: number, added: number) { - const currentCount = this.getSoulFragmentsBuffStacks(atTime) || 0; - this.setPredictedSoulFragment(atTime, currentCount + added); - } - setPredictedSoulFragment(atTime: number, count: number) { - this.estimatedCount = (count < 0 && 0) || (count > 5 && 5) || count; - this.atTime = atTime; - this.estimated = true; + + initializeState(): void {} + + resetState() { + if (this.hasSoulFragmentsHandlers) { + this.next.count = this.current.count; + } } - soulFragments(atTime: number) { - // TODO Need to add parameters greater and demon - let stacks = this.getSoulFragmentsBuffStacks(atTime); - if (this.estimated) { - if (atTime - (this.atTime || 0) < 1.2) { - stacks = this.estimatedCount; - } else { - this.estimated = false; + + cleanState(): void {} + + applySpellAfterCast: SpellCastEventHandler = ( + spellId, + targetGUID, + startCast, + endCast, + channel, + spellcast + ) => { + if (this.hasSoulFragmentsHandlers) { + if (generator[spellId]) { + const fragments = generator[spellId]; + const count = this.next.count + fragments; + this.next.count = (count < 5 && count) || 5; + } else if (spender[spellId]) { + const fragments = spender[spellId]; + const count = this.next.count + fragments; + this.next.count = (count > 0 && count) || 0; } } - return stacks; - } - getSoulFragmentsBuffStacks(atTime: number) { - const aura = this.ovaleAura.getAura( - "player", - soulFragmentsBuffId, - atTime, - "HELPFUL", - true - ); - const stacks = - (aura && - this.ovaleAura.isActiveAura(aura, atTime) && - aura.stacks) || - 0; - return stacks; - } - hasMetamorphosis(atTime: number) { - const aura = this.ovaleAura.getAura( - "player", - metamorphosisBuffId, - atTime, - "HELPFUL", - true - ); - return (aura && this.ovaleAura.isActiveAura(aura, atTime)) || false; + }; + + soulFragments(atTime: number) { + // TODO Need to add parameters greater and demon + return this.next.count; } } From bf69baa9a22f75d0ba59c3166c5353cc0de1a0ec Mon Sep 17 00:00:00 2001 From: "Johnny C. Lam" Date: Mon, 13 Sep 2021 13:29:31 -0400 Subject: [PATCH 4/4] fix: typo for "vengeance" --- src/states/DemonHunterDemonic.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/states/DemonHunterDemonic.ts b/src/states/DemonHunterDemonic.ts index f7dde2fb1..c3e7b418e 100644 --- a/src/states/DemonHunterDemonic.ts +++ b/src/states/DemonHunterDemonic.ts @@ -94,7 +94,7 @@ export class OvaleDemonHunterDemonicClass implements StateModule { if (this.specialization == "havoc") { this.hasDemonicTalent = this.spellBook.getTalentPoints(TalentId.demonic_talent) > 0; - } else if (this.specialization == "vengenace") { + } else if (this.specialization == "vengeance") { this.hasDemonicTalent = this.spellBook.getTalentPoints( TalentId.demonic_talent_vengeance