diff --git a/ui/core/components/individual_sim_ui/apl_helpers.ts b/ui/core/components/individual_sim_ui/apl_helpers.ts index 2e03a3a963..e57c0e6c7e 100644 --- a/ui/core/components/individual_sim_ui/apl_helpers.ts +++ b/ui/core/components/individual_sim_ui/apl_helpers.ts @@ -20,7 +20,7 @@ const actionIdSets: Record { - return player.getAuras().map(actionId => { + return player.getMetadata().getAuras().map(actionId => { return { value: actionId.id, }; @@ -30,7 +30,7 @@ const actionIdSets: Record { - return player.getAuras().filter(aura => aura.data.maxStacks > 0).map(actionId => { + return player.getMetadata().getAuras().filter(aura => aura.data.maxStacks > 0).map(actionId => { return { value: actionId.id, }; @@ -40,7 +40,7 @@ const actionIdSets: Record { - return player.getAuras().filter(aura => aura.data.hasIcd).map(actionId => { + return player.getMetadata().getAuras().filter(aura => aura.data.hasIcd).map(actionId => { return { value: actionId.id, }; @@ -50,7 +50,7 @@ const actionIdSets: Record { - const castableSpells = player.getSpells().filter(spell => spell.data.isCastable); + const castableSpells = player.getMetadata().getSpells().filter(spell => spell.data.isCastable); // Split up non-cooldowns and cooldowns into separate sections for easier browsing. const { 'spells': spells, 'cooldowns': cooldowns } = bucket(castableSpells, spell => spell.data.isMajorCooldown ? 'cooldowns' : 'spells'); @@ -96,7 +96,7 @@ const actionIdSets: Record { - return player.getSpells().filter(spell => spell.data.hasDot).map(actionId => { + return player.getMetadata().getSpells().filter(spell => spell.data.hasDot).map(actionId => { return { value: actionId.id, }; @@ -145,7 +145,7 @@ export class APLActionIDPicker extends DropdownPicker, ActionID, Act this.setOptions(values); }; updateValues(); - player.currentSpellsAndAurasEmitter.on(updateValues); + player.sim.unitMetadataEmitter.on(updateValues); } } diff --git a/ui/core/components/individual_sim_ui/cooldowns_picker.ts b/ui/core/components/individual_sim_ui/cooldowns_picker.ts index 1bb87c668c..b581705879 100644 --- a/ui/core/components/individual_sim_ui/cooldowns_picker.ts +++ b/ui/core/components/individual_sim_ui/cooldowns_picker.ts @@ -20,7 +20,7 @@ export class CooldownsPicker extends Component { this.player = player; this.cooldownPickers = []; - TypedEvent.onAny([this.player.cooldownsChangeEmitter, this.player.currentSpellsAndAurasEmitter]).on(eventID => { + TypedEvent.onAny([this.player.cooldownsChangeEmitter, this.player.sim.unitMetadataEmitter]).on(eventID => { this.update(); }); this.update(); @@ -127,7 +127,7 @@ export class CooldownsPicker extends Component { } private makeActionPicker(parentElem: HTMLElement, cooldownIndex: number): IconEnumPicker, ActionIdProto> { - const availableCooldowns = this.player.getSpells().filter(spell => spell.data.isMajorCooldown).map(spell => spell.id); + const availableCooldowns = this.player.getMetadata().getSpells().filter(spell => spell.data.isMajorCooldown).map(spell => spell.id); const actionPicker = new IconEnumPicker, ActionIdProto>(parentElem, this.player, { extraCssClasses: [ diff --git a/ui/core/encounter.ts b/ui/core/encounter.ts index ad4946d85d..3799bb884c 100644 --- a/ui/core/encounter.ts +++ b/ui/core/encounter.ts @@ -12,6 +12,7 @@ import { Stats } from './proto_utils/stats.js'; import * as Mechanics from './constants/mechanics.js'; import { Sim } from './sim.js'; +import { UnitMetadataList } from './player.js'; import { EventID, TypedEvent } from './typed_event.js'; // Manages all the settings for an Encounter. @@ -25,6 +26,7 @@ export class Encounter { private executeProportion35: number = 0.35; private useHealth: boolean = false; targets: Array; + targetsMetadata: UnitMetadataList; readonly targetsChangeEmitter = new TypedEvent(); readonly durationChangeEmitter = new TypedEvent(); @@ -36,6 +38,7 @@ export class Encounter { constructor(sim: Sim) { this.sim = sim; this.targets = [Encounter.defaultTargetProto()]; + this.targetsMetadata = new UnitMetadataList(); [ this.targetsChangeEmitter, diff --git a/ui/core/player.ts b/ui/core/player.ts index a9d30738e9..58d4f11b4d 100644 --- a/ui/core/player.ts +++ b/ui/core/player.ts @@ -27,6 +27,7 @@ import { import { AuraStats as AuraStatsProto, SpellStats as SpellStatsProto, + UnitMetadata as UnitMetadataProto, } from './proto/api.js'; import { APLRotation, @@ -96,6 +97,81 @@ export interface SpellStats { id: ActionId, } +export class UnitMetadata { + private auras: Array; + private spells: Array; + + constructor() { + this.auras = []; + this.spells = []; + } + + getAuras(): Array { + return this.auras.slice(); + } + + getSpells(): Array { + return this.spells.slice(); + } + + // Returns whether any updates were made. + async update(metadata: UnitMetadataProto): Promise { + let newSpells = metadata!.spells.map(spell => { + return { + data: spell, + id: ActionId.fromProto(spell.id!), + }; + }); + let newAuras = metadata!.auras.map(aura => { + return { + data: aura, + id: ActionId.fromProto(aura.id!), + }; + }); + + await Promise.all([...newSpells, ...newAuras].map(newSpell => newSpell.id.fill().then(newId => newSpell.id = newId))); + + newSpells = newSpells.sort((a, b) => stringComparator(a.id.name, b.id.name)) + newAuras = newAuras.sort((a, b) => stringComparator(a.id.name, b.id.name)) + + let anyUpdates = false; + if (newSpells.length != this.spells.length || newSpells.some((newSpell, i) => !newSpell.id.equals(this.spells[i].id))) { + this.spells = newSpells; + anyUpdates = true; + } + if (newAuras.length != this.auras.length || newAuras.some((newAura, i) => !newAura.id.equals(this.auras[i].id))) { + this.auras = newAuras; + anyUpdates = true; + } + + return anyUpdates; + } +} + +export class UnitMetadataList { + private metadatas: Array; + + constructor() { + this.metadatas = []; + } + + async update(newMetadatas: Array): Promise { + const oldLen = this.metadatas.length; + + if (newMetadatas.length > oldLen) { + for (let i = oldLen; i < newMetadatas.length; i++) { + this.metadatas.push(new UnitMetadata()); + } + } else if (newMetadatas.length < oldLen) { + this.metadatas = this.metadatas.slice(0, newMetadatas.length); + } + + const anyUpdates = await Promise.all(newMetadatas.map((metadata, i) => this.metadatas[i].update(metadata))); + + return oldLen != this.metadatas.length || anyUpdates.some(v => v); + } +} + // Manages all the gear / consumes / other settings for a single Player. export class Player { readonly sim: Sim; @@ -135,8 +211,8 @@ export class Player { private epRatios: Array = new Array(Player.numEpRatios).fill(0); private epWeights: Stats = new Stats(); private currentStats: PlayerStats = PlayerStats.create(); - private spells: Array = []; - private auras: Array = []; + private metadata: UnitMetadata = new UnitMetadata(); + private petMetadatas: UnitMetadataList = new UnitMetadataList(); readonly nameChangeEmitter = new TypedEvent('PlayerName'); readonly buffsChangeEmitter = new TypedEvent('PlayerBuffs'); @@ -156,7 +232,6 @@ export class Player { readonly epWeightsChangeEmitter = new TypedEvent('PlayerEpWeights'); readonly currentStatsEmitter = new TypedEvent('PlayerCurrentStats'); - readonly currentSpellsAndAurasEmitter = new TypedEvent('PlayerCurrentSpellsAndAuras'); readonly epRatiosChangeEmitter = new TypedEvent('PlayerEpRatios'); // Emits when any of the above emitters emit. @@ -322,51 +397,19 @@ export class Player { setCurrentStats(eventID: EventID, newStats: PlayerStats) { this.currentStats = newStats; - this.updateSpellsAndAuras(eventID); - this.currentStatsEmitter.emit(eventID); } - getSpells(): Array { - return this.spells.slice(); + getMetadata(): UnitMetadata { + return this.metadata; } - getAuras(): Array { - return this.auras.slice(); - } - - private async updateSpellsAndAuras(eventID: EventID) { - let newSpells = this.currentStats.metadata!.spells.map(spell => { - return { - data: spell, - id: ActionId.fromProto(spell.id!), - }; - }); - let newAuras = this.currentStats.metadata!.auras.map(aura => { - return { - data: aura, - id: ActionId.fromProto(aura.id!), - }; - }); - - await Promise.all([...newSpells, ...newAuras].map(newSpell => newSpell.id.fill().then(newId => newSpell.id = newId))); - - newSpells = newSpells.sort((a, b) => stringComparator(a.id.name, b.id.name)) - newAuras = newAuras.sort((a, b) => stringComparator(a.id.name, b.id.name)) - - let anyUpdates = false; - if (newSpells.length != this.spells.length || newSpells.some((newSpell, i) => !newSpell.id.equals(this.spells[i].id))) { - this.spells = newSpells; - anyUpdates = true; - } - if (newAuras.length != this.auras.length || newAuras.some((newAura, i) => !newAura.id.equals(this.auras[i].id))) { - this.auras = newAuras; - anyUpdates = true; - } - - if (anyUpdates) { - this.currentSpellsAndAurasEmitter.emit(eventID); - } + async updateMetadata(): Promise { + const playerPromise = this.metadata.update(this.currentStats.metadata!); + const petsPromise = this.petMetadatas.update(this.currentStats.pets.map(p => p.metadata!)); + const playerUpdated = await playerPromise; + const petsUpdated = await petsPromise; + return playerUpdated || petsUpdated; } getName(): string { diff --git a/ui/core/sim.ts b/ui/core/sim.ts index 5b3a979e78..eca6a8ff2d 100644 --- a/ui/core/sim.ts +++ b/ui/core/sim.ts @@ -73,6 +73,9 @@ export class Sim { // Emits when any of the settings change (but not the raid / encounter). readonly settingsChangeEmitter: TypedEvent; + // Emits when any player, target, or pet has metadata changes (spells or auras). + readonly unitMetadataEmitter = new TypedEvent('UnitMetadata'); + // Emits when any of the above emitters emit. readonly changeEmitter: TypedEvent; @@ -288,11 +291,27 @@ export class Sim { return; } - TypedEvent.freezeAllAndDo(() => { - result.raidStats!.parties - .forEach((partyStats, partyIndex) => - partyStats.players.forEach((playerStats, playerIndex) => - players[partyIndex * 5 + playerIndex]?.setCurrentStats(eventID, playerStats))); + TypedEvent.freezeAllAndDo(async () => { + const playerUpdatePromises = result.raidStats!.parties + .map((partyStats, partyIndex) => + partyStats.players.map((playerStats, playerIndex) => { + const player = players[partyIndex * 5 + playerIndex]; + if (player) { + player.setCurrentStats(eventID, playerStats); + return player.updateMetadata(); + } else { + return null; + } + })) + .flat() + .filter(p => p != null) as Array>; + + const targetUpdatePromise = this.encounter.targetsMetadata.update(result.encounterStats!.targets.map(t => t.metadata!)); + + const anyUpdates = await Promise.all(playerUpdatePromises.concat([targetUpdatePromise])); + if (anyUpdates.some(v => v)) { + this.unitMetadataEmitter.emit(eventID); + } }); }