diff --git a/proto/api.proto b/proto/api.proto index d159f97d1b..14420eb6d1 100644 --- a/proto/api.proto +++ b/proto/api.proto @@ -293,6 +293,16 @@ message RaidSimResult { message ComputeStatsRequest { Raid raid = 1; } +message AuraStats { + ActionID id = 1; +} +message SpellStats { + ActionID id = 1; + + bool is_castable = 2; // Whether this spell may be cast by the APL logic. + bool is_major_cooldown = 3; // Whether this spell is a major cooldown. + bool has_dot = 4; // Whether this spell applies a DoT effect. +} message PlayerStats { // Stats UnitStats base_stats = 6; @@ -304,10 +314,9 @@ message PlayerStats { repeated string sets = 3; IndividualBuffs buffs = 4; - repeated ActionID cooldowns = 5; - repeated ActionID spells = 10; - repeated ActionID auras = 11; + repeated SpellStats spells = 10; + repeated AuraStats auras = 11; } message PartyStats { repeated PlayerStats players = 1; diff --git a/proto/apl.proto b/proto/apl.proto index ce6531a552..5368ba9563 100644 --- a/proto/apl.proto +++ b/proto/apl.proto @@ -62,7 +62,7 @@ message APLActionCastSpell { } message APLActionWait { - Duration duration = 1; + APLValue duration = 1; } /////////////////////////////////////////////////////////////////////////// diff --git a/sim/core/apl_actions_core.go b/sim/core/apl_actions_core.go index 6b2bcc6cd8..83f28fd91f 100644 --- a/sim/core/apl_actions_core.go +++ b/sim/core/apl_actions_core.go @@ -1,8 +1,6 @@ package core import ( - "time" - "github.com/wowsims/wotlk/sim/core/proto" ) @@ -29,18 +27,18 @@ func (action *APLActionCastSpell) Execute(sim *Simulation) { type APLActionWait struct { unit *Unit - duration time.Duration + duration APLValue } func (unit *Unit) newActionWait(config *proto.APLActionWait) APLActionImpl { return &APLActionWait{ unit: unit, - duration: DurationFromProto(config.Duration), + duration: unit.coerceTo(unit.newAPLValue(config.Duration), proto.APLValueType_ValueTypeDuration), } } func (action *APLActionWait) IsAvailable(sim *Simulation) bool { - return true + return action.duration != nil } func (action *APLActionWait) Execute(sim *Simulation) { - action.unit.WaitUntil(sim, sim.CurrentTime+action.duration) + action.unit.WaitUntil(sim, sim.CurrentTime+action.duration.GetDuration(sim)) } diff --git a/sim/core/character.go b/sim/core/character.go index d4b3f404e5..06fe69f7f7 100644 --- a/sim/core/character.go +++ b/sim/core/character.go @@ -501,20 +501,24 @@ func (character *Character) FillPlayerStats(playerStats *proto.PlayerStats) { } character.clearBuildPhaseAuras(CharacterBuildPhaseAll) playerStats.Sets = character.GetActiveSetBonusNames() - playerStats.Cooldowns = character.GetMajorCooldownIDs() - aplSpells := FilterSlice(character.Spellbook, func(spell *Spell) bool { - return spell.Flags.Matches(SpellFlagAPL) - }) - playerStats.Spells = MapSlice(aplSpells, func(spell *Spell) *proto.ActionID { - return spell.ActionID.ToProto() + playerStats.Spells = MapSlice(character.Spellbook, func(spell *Spell) *proto.SpellStats { + return &proto.SpellStats{ + Id: spell.ActionID.ToProto(), + + IsCastable: spell.Flags.Matches(SpellFlagAPL), + IsMajorCooldown: spell.Flags.Matches(SpellFlagMCD), + HasDot: spell.dots != nil, + } }) aplAuras := FilterSlice(character.auras, func(aura *Aura) bool { return !aura.ActionID.IsEmptyAction() }) - playerStats.Auras = MapSlice(aplAuras, func(aura *Aura) *proto.ActionID { - return aura.ActionID.ToProto() + playerStats.Auras = MapSlice(aplAuras, func(aura *Aura) *proto.AuraStats { + return &proto.AuraStats{ + Id: aura.ActionID.ToProto(), + } }) } diff --git a/ui/core/components/individual_sim_ui/apl_actions.ts b/ui/core/components/individual_sim_ui/apl_actions.ts index e3aae8e38d..ab27d964cf 100644 --- a/ui/core/components/individual_sim_ui/apl_actions.ts +++ b/ui/core/components/individual_sim_ui/apl_actions.ts @@ -10,6 +10,7 @@ import { EventID } from '../../typed_event.js'; import { Input, InputConfig } from '../input.js'; import { Player } from '../../player.js'; import { TextDropdownPicker } from '../dropdown_picker.js'; +import { ListItemPickerConfig, ListPicker } from '../list_picker.js'; import * as AplHelpers from './apl_helpers.js'; import * as AplValues from './apl_values.js'; @@ -40,6 +41,7 @@ export class APLActionPicker extends Input, APLAction> { player.rotationChangeEmitter.emit(eventID); }, }); + this.conditionPicker.rootElem.classList.add('apl-action-condition'); this.actionDiv = document.createElement('div'); this.actionDiv.classList.add('apl-action-picker-action'); @@ -145,16 +147,17 @@ export class APLActionPicker extends Input, APLAction> { player.rotationChangeEmitter.emit(eventID); }, }); + this.actionPicker.rootElem.classList.add('apl-action-' + newActionType); } } -type ActionTypeConfig = { +type ActionTypeConfig = { label: string, - newValue: () => object, - factory: (parent: HTMLElement, player: Player, config: InputConfig, any>) => Input, any>, + newValue: () => T, + factory: (parent: HTMLElement, player: Player, config: InputConfig, T>) => Input, T>, }; -function inputBuilder(label: string, newValue: () => T, fields: Array>): ActionTypeConfig { +function inputBuilder(label: string, newValue: () => T, fields: Array>): ActionTypeConfig { return { label: label, newValue: newValue, @@ -162,12 +165,35 @@ function inputBuilder(label: string, newValue: () => T, fields }; } -export const actionTypeFactories: Record, ActionTypeConfig> = { +export const actionTypeFactories: Record, ActionTypeConfig> = { ['castSpell']: inputBuilder('Cast', APLActionCastSpell.create, [ - AplHelpers.actionIdFieldConfig('spellId', 'all_spells'), + AplHelpers.actionIdFieldConfig('spellId', 'castable_spells'), ]), ['sequence']: inputBuilder('Sequence', APLActionSequence.create, [ + { + field: 'actions', + newValue: () => [], + factory: (parent, player, config) => new ListPicker, APLAction>(parent, player, { + ...config, + // Override setValue to replace undefined elements with default messages. + setValue: (eventID: EventID, player: Player, newValue: Array) => { + config.setValue(eventID, player, newValue.map(val => val || APLAction.create())); + }, + + itemLabel: 'Action', + newItem: APLAction.create, + copyItem: (oldValue: APLAction) => oldValue ? APLAction.clone(oldValue) : oldValue, + newItemPicker: (parent: HTMLElement, listPicker: ListPicker, APLAction>, index: number, config: ListItemPickerConfig, APLAction>) => new APLActionPicker(parent, player, config), + horizontalLayout: true, + allowedActions: ['create', 'delete'], + }), + }, ]), ['wait']: inputBuilder('Wait', APLActionWait.create, [ + { + field: 'duration', + newValue: APLValue.create, + factory: (parent, player, config) => new AplValues.APLValuePicker(parent, player, config), + }, ]), }; \ No newline at end of file diff --git a/ui/core/components/individual_sim_ui/apl_helpers.ts b/ui/core/components/individual_sim_ui/apl_helpers.ts index e06f1c22c0..79dc134f20 100644 --- a/ui/core/components/individual_sim_ui/apl_helpers.ts +++ b/ui/core/components/individual_sim_ui/apl_helpers.ts @@ -1,57 +1,52 @@ import { ActionId } from '../../proto_utils/action_id.js'; import { Player } from '../../player.js'; import { EventID, TypedEvent } from '../../typed_event.js'; -import { stringComparator } from '../../utils.js'; +import { bucket } from '../../utils.js'; import { DropdownPicker, DropdownPickerConfig, DropdownValueConfig, TextDropdownPicker } from '../dropdown_picker.js'; import { Input, InputConfig } from '../input.js'; import { ActionID } from '../../proto/common.js'; -export type ACTION_ID_SET = 'all_spells' | 'dots'; - -export interface APLActionIDPickerConfig extends Omit, 'defaultLabel' | 'equals' | 'setOptionContent' | 'values' | 'getValue' | 'setValue'> { - actionIdSet: ACTION_ID_SET, - getValue: (obj: ModObject) => ActionID, - setValue: (eventID: EventID, obj: ModObject, newValue: ActionID) => void, -} +export type ACTION_ID_SET = 'castable_spells' | 'dot_spells'; const actionIdSets: Record) => Promise>>, }> = { - ['all_spells']: { + ['castable_spells']: { defaultLabel: 'Spell', getActionIDs: async (player) => { - const playerStats = player.getCurrentStats(); - const spellPromises = Promise.all(playerStats.spells.map(spell => ActionId.fromProto(spell).fill())); - const cooldownPromises = Promise.all(playerStats.cooldowns.map(cd => ActionId.fromProto(cd).fill())); + const castableSpells = player.getSpells().filter(spell => spell.data.isCastable); - let [spells, cooldowns] = await Promise.all([spellPromises, cooldownPromises]); - spells = spells.sort((a, b) => stringComparator(a.name, b.name)) - cooldowns = cooldowns.sort((a, b) => stringComparator(a.name, b.name)) + // 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'); - return [...spells, ...cooldowns].map(actionId => { + return [...(spells || []), ...(cooldowns || [])].map(actionId => { return { - value: actionId, + value: actionId.id, }; }); }, }, - ['dots']: { + ['dot_spells']: { defaultLabel: 'DoT Spell', getActionIDs: async (player) => { - const playerStats = player.getCurrentStats(); - let spells = await Promise.all(playerStats.spells.map(spell => ActionId.fromProto(spell).fill())); - spells = spells.sort((a, b) => stringComparator(a.name, b.name)) + const dotSpells = player.getSpells().filter(spell => spell.data.hasDot); - return spells.map(actionId => { + return dotSpells.map(actionId => { return { - value: actionId, + value: actionId.id, }; }); }, }, }; +export interface APLActionIDPickerConfig extends Omit, 'defaultLabel' | 'equals' | 'setOptionContent' | 'values' | 'getValue' | 'setValue'> { + actionIdSet: ACTION_ID_SET, + getValue: (obj: ModObject) => ActionID, + setValue: (eventID: EventID, obj: ModObject, newValue: ActionID) => void, +} + export class APLActionIDPicker extends DropdownPicker, ActionId> { constructor(parent: HTMLElement, player: Player, config: APLActionIDPickerConfig>) { const actionIdSet = actionIdSets[config.actionIdSet]; @@ -81,7 +76,7 @@ export class APLActionIDPicker extends DropdownPicker, ActionId> { this.setOptions(values); }; updateValues(); - player.currentStatsEmitter.on(updateValues); + player.currentSpellsAndAurasEmitter.on(updateValues); } } diff --git a/ui/core/components/individual_sim_ui/apl_values.ts b/ui/core/components/individual_sim_ui/apl_values.ts index 1ea9e2693f..b66aa22258 100644 --- a/ui/core/components/individual_sim_ui/apl_values.ts +++ b/ui/core/components/individual_sim_ui/apl_values.ts @@ -13,7 +13,7 @@ import { EventID, TypedEvent } from '../../typed_event.js'; import { Input, InputConfig } from '../input.js'; import { Player } from '../../player.js'; import { TextDropdownPicker, TextDropdownValueConfig } from '../dropdown_picker.js'; -import { ListItemPickerConfig, ListPicker, ListItemAction } from '../list_picker.js'; +import { ListItemPickerConfig, ListPicker } from '../list_picker.js'; import * as AplHelpers from './apl_helpers.js'; @@ -286,6 +286,6 @@ const valueTypeFactories: Record, ValueTypeConfig }, ]), ['dotIsActive']: inputBuilder('Dot Is Active', APLValueDotIsActive.create, [ - AplHelpers.actionIdFieldConfig('spellId', 'dots'), + AplHelpers.actionIdFieldConfig('spellId', 'dot_spells'), ]), }; \ No newline at end of file diff --git a/ui/core/components/individual_sim_ui/cooldowns_picker.ts b/ui/core/components/individual_sim_ui/cooldowns_picker.ts index 33c85a0c1b..03998372a0 100644 --- a/ui/core/components/individual_sim_ui/cooldowns_picker.ts +++ b/ui/core/components/individual_sim_ui/cooldowns_picker.ts @@ -1,17 +1,11 @@ import { Component } from '../component.js'; import { IconEnumPicker, IconEnumValueConfig } from '../icon_enum_picker.js'; -import { Input, InputConfig } from '../input.js'; import { NumberListPicker } from '../number_list_picker.js'; import { Player } from '../../player.js'; import { EventID, TypedEvent } from '../../typed_event.js'; import { ActionID as ActionIdProto, ItemSlot } from '../../proto/common.js'; -import { Cooldowns } from '../../proto/common.js'; import { Cooldown } from '../../proto/common.js'; import { ActionId } from '../../proto_utils/action_id.js'; -import { Class } from '../../proto/common.js'; -import { Spec } from '../../proto/common.js'; -import { getEnumValues } from '../../utils.js'; -import { wait } from '../../utils.js'; import { Tooltip } from 'bootstrap'; import { NumberPicker } from '../number_picker.js'; import { Sim } from 'ui/core/sim.js'; @@ -26,7 +20,7 @@ export class CooldownsPicker extends Component { this.player = player; this.cooldownPickers = []; - TypedEvent.onAny([this.player.currentStatsEmitter]).on(eventID => { + TypedEvent.onAny([this.player.currentSpellsAndAurasEmitter]).on(eventID => { this.update(); }); this.update(); @@ -133,7 +127,7 @@ export class CooldownsPicker extends Component { } private makeActionPicker(parentElem: HTMLElement, cooldownIndex: number): IconEnumPicker, ActionIdProto> { - const availableCooldowns = this.player.getCurrentStats().cooldowns; + const availableCooldowns = this.player.getSpells().filter(spell => spell.data.isMajorCooldown).map(spell => spell.id); const actionPicker = new IconEnumPicker, ActionIdProto>(parentElem, this.player, { extraCssClasses: [ @@ -143,7 +137,7 @@ export class CooldownsPicker extends Component { values: ([ { color: '#grey', value: ActionIdProto.create() }, ] as Array, ActionIdProto>>).concat(availableCooldowns.map(cooldownAction => { - return { actionId: ActionId.fromProto(cooldownAction), value: cooldownAction }; + return { actionId: cooldownAction, value: cooldownAction.toProto() }; })), equals: (a: ActionIdProto, b: ActionIdProto) => ActionIdProto.equals(a, b), zeroValue: ActionIdProto.create(), diff --git a/ui/core/player.ts b/ui/core/player.ts index 614a2f358b..e6db954581 100644 --- a/ui/core/player.ts +++ b/ui/core/player.ts @@ -24,6 +24,10 @@ import { UnitStats, WeaponType, } from './proto/common.js'; +import { + AuraStats as AuraStatsProto, + SpellStats as SpellStatsProto, +} from './proto/api.js'; import { APLRotation, } from './proto/apl.js'; @@ -41,13 +45,13 @@ import { import { PlayerStats } from './proto/api.js'; import { Player as PlayerProto } from './proto/api.js'; import { StatWeightsResult } from './proto/api.js'; +import { ActionId } from './proto_utils/action_id.js'; import { EquippedItem, getWeaponDPS } from './proto_utils/equipped_item.js'; import { playerTalentStringToProto } from './talents/factory.js'; import { Gear, ItemSwapGear } from './proto_utils/gear.js'; import { isUnrestrictedGem, - gemEligibleForSocket, gemMatchesSocket, } from './proto_utils/gems.js'; import { Stats } from './proto_utils/stats.js'; @@ -64,14 +68,11 @@ import { classColors, emptyRaidTarget, enchantAppliesToItem, - getEligibleEnchantSlots, - getEligibleItemSlots, getTalentTree, getTalentTreeIcon, getMetaGemEffectEP, isTankSpec, newRaidTarget, - playerToSpec, raceToFaction, specToClass, specToEligibleRaces, @@ -80,12 +81,20 @@ import { } from './proto_utils/utils.js'; import { getLanguageCode } from './constants/lang.js'; -import { Listener } from './typed_event.js'; import { EventID, TypedEvent } from './typed_event.js'; import { Party, MAX_PARTY_SIZE } from './party.js'; import { Raid } from './raid.js'; import { Sim } from './sim.js'; -import { sum } from './utils.js'; +import { stringComparator, sum } from './utils.js'; + +export interface AuraStats { + data: AuraStatsProto, + id: ActionId, +} +export interface SpellStats { + data: SpellStatsProto, + id: ActionId, +} // Manages all the gear / consumes / other settings for a single Player. export class Player { @@ -126,6 +135,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 = []; readonly nameChangeEmitter = new TypedEvent('PlayerName'); readonly buffsChangeEmitter = new TypedEvent('PlayerBuffs'); @@ -145,6 +156,7 @@ 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. @@ -310,20 +322,51 @@ export class Player { setCurrentStats(eventID: EventID, newStats: PlayerStats) { this.currentStats = newStats; + this.updateSpellsAndAuras(eventID); + this.currentStatsEmitter.emit(eventID); + } - //// Remove item cooldowns if there is no cooldown available for the item. - //const availableCooldowns = this.currentStats.cooldowns; - //const newCooldowns = this.getCooldowns(); - //newCooldowns.cooldowns = newCooldowns.cooldowns.filter(cd => { - // if (cd.id && 'itemId' in cd.id.rawId) { - // return availableCooldowns.find(acd => ActionIdProto.equals(acd, cd.id)) != null; - // } else { - // return true; - // } - //}); - //// TODO: Reference the parent event ID - //this.setCooldowns(TypedEvent.nextEventID(), newCooldowns); + getSpells(): Array { + return this.spells.slice(); + } + + getAuras(): Array { + return this.auras.slice(); + } + + private async updateSpellsAndAuras(eventID: EventID) { + let newSpells = this.currentStats.spells.map(spell => { + return { + data: spell, + id: ActionId.fromProto(spell.id!), + }; + }); + let newAuras = this.currentStats.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); + } } getName(): string { diff --git a/ui/scss/core/components/individual_sim_ui/_apl_rotation_picker.scss b/ui/scss/core/components/individual_sim_ui/_apl_rotation_picker.scss index 268246e036..61dc4ed986 100644 --- a/ui/scss/core/components/individual_sim_ui/_apl_rotation_picker.scss +++ b/ui/scss/core/components/individual_sim_ui/_apl_rotation_picker.scss @@ -72,4 +72,8 @@ .hide-picker-root { margin: 0; +} + +.apl-picker-builder-root.apl-action-sequence .apl-action-condition { + display: none; } \ No newline at end of file