diff --git a/src/ioc.ts b/src/ioc.ts index 020cce141..ecb45ff6f 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, @@ -324,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, @@ -402,7 +406,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 ); @@ -422,9 +427,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/DemonHunterDemonic.ts b/src/states/DemonHunterDemonic.ts index aca301706..c3e7b418e 100644 --- a/src/states/DemonHunterDemonic.ts +++ b/src/states/DemonHunterDemonic.ts @@ -1,190 +1,154 @@ -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 + ); + const specialization = this.paperDoll.getSpecialization(); + this.onOvaleSpecializationChanged( + "onEnable", + specialization, + specialization ); } }; - 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 == "vengeance") { + 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 ); - } + }; } 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; + } } 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; } }