From 5e9811428dd66c858be04614fe6d9e004abb5746 Mon Sep 17 00:00:00 2001 From: James Tanner Date: Thu, 6 Jul 2023 22:32:31 +0000 Subject: [PATCH 1/3] Implement a bunch of apl values --- proto/api.proto | 1 + proto/apl.proto | 39 ++++++++- sim/core/apl_value.go | 22 +++++ sim/core/apl_values_aura.go | 77 ++++++++++++++++ sim/core/apl_values_gcd.go | 41 +++++++++ sim/core/apl_values_spell.go | 74 ++++++++++++++++ sim/core/aura.go | 8 ++ sim/core/character.go | 1 + .../individual_sim_ui/apl_helpers.ts | 28 ++++-- .../individual_sim_ui/apl_values.ts | 87 ++++++++++++++++++- 10 files changed, 371 insertions(+), 7 deletions(-) create mode 100644 sim/core/apl_values_aura.go create mode 100644 sim/core/apl_values_gcd.go create mode 100644 sim/core/apl_values_spell.go diff --git a/proto/api.proto b/proto/api.proto index 14420eb6d1..e0c34a7a93 100644 --- a/proto/api.proto +++ b/proto/api.proto @@ -295,6 +295,7 @@ message ComputeStatsRequest { } message AuraStats { ActionID id = 1; + int32 max_stacks = 2; } message SpellStats { ActionID id = 1; diff --git a/proto/apl.proto b/proto/apl.proto index 90e395efe2..ce9acc2dd2 100644 --- a/proto/apl.proto +++ b/proto/apl.proto @@ -55,6 +55,20 @@ message APLValue { APLValueCurrentEnergy current_energy = 15; APLValueCurrentComboPoints current_combo_points = 16; + // GCD values + APLValueGCDIsReady gcd_is_ready = 17; + APLValueGCDTimeToReady gcd_time_to_ready = 18; + + // Spell values + APLValueSpellCanCast spell_can_cast = 19; + APLValueSpellIsReady spell_is_ready = 20; + APLValueSpellTimeToReady spell_time_to_ready = 21; + + // Aura values + APLValueAuraIsActive aura_is_active = 22; + APLValueAuraRemainingTime aura_remaining_time = 23; + APLValueAuraNumStacks aura_num_stacks = 24; + // Dot values APLValueDotIsActive dot_is_active = 6; APLValueDotRemainingTime dot_remaining_time = 13; @@ -148,9 +162,32 @@ message APLValueCurrentRage {} message APLValueCurrentEnergy {} message APLValueCurrentComboPoints {} +message APLValueGCDIsReady {} +message APLValueGCDTimeToReady {} + +message APLValueSpellCanCast { + ActionID spell_id = 1; +} +message APLValueSpellIsReady { + ActionID spell_id = 1; +} +message APLValueSpellTimeToReady { + ActionID spell_id = 1; +} + +message APLValueAuraIsActive { + ActionID aura_id = 1; +} +message APLValueAuraRemainingTime { + ActionID aura_id = 1; +} +message APLValueAuraNumStacks { + ActionID aura_id = 1; +} + message APLValueDotIsActive { ActionID spell_id = 1; } message APLValueDotRemainingTime { ActionID spell_id = 1; -} \ No newline at end of file +} diff --git a/sim/core/apl_value.go b/sim/core/apl_value.go index fa3af31cd6..6e1bea09be 100644 --- a/sim/core/apl_value.go +++ b/sim/core/apl_value.go @@ -79,6 +79,28 @@ func (unit *Unit) newAPLValue(config *proto.APLValue) APLValue { case *proto.APLValue_CurrentComboPoints: return unit.newValueCurrentComboPoints(config.GetCurrentComboPoints()) + // GCD + case *proto.APLValue_GcdIsReady: + return unit.newValueGCDIsReady(config.GetGcdIsReady()) + case *proto.APLValue_GcdTimeToReady: + return unit.newValueGCDTimeToReady(config.GetGcdTimeToReady()) + + // Spells + case *proto.APLValue_SpellCanCast: + return unit.newValueSpellCanCast(config.GetSpellCanCast()) + case *proto.APLValue_SpellIsReady: + return unit.newValueSpellIsReady(config.GetSpellIsReady()) + case *proto.APLValue_SpellTimeToReady: + return unit.newValueSpellTimeToReady(config.GetSpellTimeToReady()) + + // Auras + case *proto.APLValue_AuraIsActive: + return unit.newValueAuraIsActive(config.GetAuraIsActive()) + case *proto.APLValue_AuraRemainingTime: + return unit.newValueAuraRemainingTime(config.GetAuraRemainingTime()) + case *proto.APLValue_AuraNumStacks: + return unit.newValueAuraNumStacks(config.GetAuraNumStacks()) + // Dots case *proto.APLValue_DotIsActive: return unit.newValueDotIsActive(config.GetDotIsActive()) diff --git a/sim/core/apl_values_aura.go b/sim/core/apl_values_aura.go new file mode 100644 index 0000000000..54f2601ccd --- /dev/null +++ b/sim/core/apl_values_aura.go @@ -0,0 +1,77 @@ +package core + +import ( + "time" + + "github.com/wowsims/wotlk/sim/core/proto" +) + +func (unit *Unit) aplGetAura(auraId *proto.ActionID) *Aura { + return unit.GetAuraByID(ProtoToActionID(auraId)) +} + +type APLValueAuraIsActive struct { + defaultAPLValueImpl + aura *Aura +} + +func (unit *Unit) newValueAuraIsActive(config *proto.APLValueAuraIsActive) APLValue { + aura := unit.aplGetAura(config.AuraId) + if aura == nil { + return nil + } + return &APLValueAuraIsActive{ + aura: aura, + } +} +func (value *APLValueAuraIsActive) Type() proto.APLValueType { + return proto.APLValueType_ValueTypeBool +} +func (value *APLValueAuraIsActive) GetBool(sim *Simulation) bool { + return value.aura.IsActive() +} + +type APLValueAuraRemainingTime struct { + defaultAPLValueImpl + aura *Aura +} + +func (unit *Unit) newValueAuraRemainingTime(config *proto.APLValueAuraRemainingTime) APLValue { + aura := unit.aplGetAura(config.AuraId) + if aura == nil { + return nil + } + return &APLValueAuraRemainingTime{ + aura: aura, + } +} +func (value *APLValueAuraRemainingTime) Type() proto.APLValueType { + return proto.APLValueType_ValueTypeDuration +} +func (value *APLValueAuraRemainingTime) GetDuration(sim *Simulation) time.Duration { + return value.aura.RemainingDuration(sim) +} + +type APLValueAuraNumStacks struct { + defaultAPLValueImpl + aura *Aura +} + +func (unit *Unit) newValueAuraNumStacks(config *proto.APLValueAuraNumStacks) APLValue { + aura := unit.aplGetAura(config.AuraId) + if aura == nil { + return nil + } + if aura.MaxStacks == 0 { + return nil + } + return &APLValueAuraNumStacks{ + aura: aura, + } +} +func (value *APLValueAuraNumStacks) Type() proto.APLValueType { + return proto.APLValueType_ValueTypeInt +} +func (value *APLValueAuraNumStacks) GetInt(sim *Simulation) int32 { + return value.aura.GetStacks() +} diff --git a/sim/core/apl_values_gcd.go b/sim/core/apl_values_gcd.go new file mode 100644 index 0000000000..ec4a982465 --- /dev/null +++ b/sim/core/apl_values_gcd.go @@ -0,0 +1,41 @@ +package core + +import ( + "time" + + "github.com/wowsims/wotlk/sim/core/proto" +) + +type APLValueGCDIsReady struct { + defaultAPLValueImpl + unit *Unit +} + +func (unit *Unit) newValueGCDIsReady(config *proto.APLValueGCDIsReady) APLValue { + return &APLValueGCDIsReady{ + unit: unit, + } +} +func (value *APLValueGCDIsReady) Type() proto.APLValueType { + return proto.APLValueType_ValueTypeBool +} +func (value *APLValueGCDIsReady) GetBool(sim *Simulation) bool { + return value.unit.GCD.IsReady(sim) +} + +type APLValueGCDTimeToReady struct { + defaultAPLValueImpl + unit *Unit +} + +func (unit *Unit) newValueGCDTimeToReady(config *proto.APLValueGCDTimeToReady) APLValue { + return &APLValueGCDTimeToReady{ + unit: unit, + } +} +func (value *APLValueGCDTimeToReady) Type() proto.APLValueType { + return proto.APLValueType_ValueTypeDuration +} +func (value *APLValueGCDTimeToReady) GetDuration(sim *Simulation) time.Duration { + return value.unit.GCD.TimeToReady(sim) +} diff --git a/sim/core/apl_values_spell.go b/sim/core/apl_values_spell.go new file mode 100644 index 0000000000..850d6e8656 --- /dev/null +++ b/sim/core/apl_values_spell.go @@ -0,0 +1,74 @@ +package core + +import ( + "time" + + "github.com/wowsims/wotlk/sim/core/proto" +) + +func (unit *Unit) aplGetSpell(spellId *proto.ActionID) *Spell { + return unit.GetSpell(ProtoToActionID(spellId)) +} + +type APLValueSpellCanCast struct { + defaultAPLValueImpl + spell *Spell +} + +func (unit *Unit) newValueSpellCanCast(config *proto.APLValueSpellCanCast) APLValue { + spell := unit.aplGetSpell(config.SpellId) + if spell == nil { + return nil + } + return &APLValueSpellCanCast{ + spell: spell, + } +} +func (value *APLValueSpellCanCast) Type() proto.APLValueType { + return proto.APLValueType_ValueTypeBool +} +func (value *APLValueSpellCanCast) GetBool(sim *Simulation) bool { + return value.spell.CanCast(sim, value.spell.Unit.CurrentTarget) +} + +type APLValueSpellIsReady struct { + defaultAPLValueImpl + spell *Spell +} + +func (unit *Unit) newValueSpellIsReady(config *proto.APLValueSpellIsReady) APLValue { + spell := unit.aplGetSpell(config.SpellId) + if spell == nil { + return nil + } + return &APLValueSpellIsReady{ + spell: spell, + } +} +func (value *APLValueSpellIsReady) Type() proto.APLValueType { + return proto.APLValueType_ValueTypeBool +} +func (value *APLValueSpellIsReady) GetBool(sim *Simulation) bool { + return value.spell.IsReady(sim) +} + +type APLValueSpellTimeToReady struct { + defaultAPLValueImpl + spell *Spell +} + +func (unit *Unit) newValueSpellTimeToReady(config *proto.APLValueSpellTimeToReady) APLValue { + spell := unit.aplGetSpell(config.SpellId) + if spell == nil { + return nil + } + return &APLValueSpellTimeToReady{ + spell: spell, + } +} +func (value *APLValueSpellTimeToReady) Type() proto.APLValueType { + return proto.APLValueType_ValueTypeDuration +} +func (value *APLValueSpellTimeToReady) GetDuration(sim *Simulation) time.Duration { + return value.spell.TimeToReady(sim) +} diff --git a/sim/core/aura.go b/sim/core/aura.go index 8c55e46741..6e0816edf1 100644 --- a/sim/core/aura.go +++ b/sim/core/aura.go @@ -323,6 +323,14 @@ func (at *auraTracker) GetAura(label string) *Aura { } return nil } +func (at *auraTracker) GetAuraByID(actionID ActionID) *Aura { + for _, aura := range at.auras { + if aura.ActionID.SameAction(actionID) { + return aura + } + } + return nil +} func (at *auraTracker) HasAura(label string) bool { aura := at.GetAura(label) return aura != nil diff --git a/sim/core/character.go b/sim/core/character.go index 06fe69f7f7..6baa999bfa 100644 --- a/sim/core/character.go +++ b/sim/core/character.go @@ -518,6 +518,7 @@ func (character *Character) FillPlayerStats(playerStats *proto.PlayerStats) { playerStats.Auras = MapSlice(aplAuras, func(aura *Aura) *proto.AuraStats { return &proto.AuraStats{ Id: aura.ActionID.ToProto(), + MaxStacks: aura.MaxStacks, } }) } diff --git a/ui/core/components/individual_sim_ui/apl_helpers.ts b/ui/core/components/individual_sim_ui/apl_helpers.ts index 10a9a98435..bd524bd7e8 100644 --- a/ui/core/components/individual_sim_ui/apl_helpers.ts +++ b/ui/core/components/individual_sim_ui/apl_helpers.ts @@ -7,12 +7,32 @@ import { DropdownPicker, DropdownPickerConfig, DropdownValueConfig, TextDropdown import { Input, InputConfig } from '../input.js'; import { ActionID } from '../../proto/common.js'; -export type ACTION_ID_SET = 'castable_spells' | 'dot_spells'; +export type ACTION_ID_SET = 'auras' | 'stackable_auras' | 'castable_spells' | 'dot_spells'; const actionIdSets: Record) => Promise>>, }> = { + ['auras']: { + defaultLabel: 'Aura', + getActionIDs: async (player) => { + return player.getAuras().map(actionId => { + return { + value: actionId.id, + }; + }); + }, + }, + ['stackable_auras']: { + defaultLabel: 'Aura', + getActionIDs: async (player) => { + return player.getAuras().filter(aura => aura.data.maxStacks > 0).map(actionId => { + return { + value: actionId.id, + }; + }); + }, + }, ['castable_spells']: { defaultLabel: 'Spell', getActionIDs: async (player) => { @@ -46,9 +66,7 @@ const actionIdSets: Record { - const dotSpells = player.getSpells().filter(spell => spell.data.hasDot); - - return dotSpells.map(actionId => { + return player.getSpells().filter(spell => spell.data.hasDot).map(actionId => { return { value: actionId.id, }; @@ -199,4 +217,4 @@ export function aplInputBuilder(newValue: () => T, fields: Array, ValueTypeConfig fields: [], }), + // GCD + ['gcdIsReady']: inputBuilder({ + label: 'GCD Is Ready', + submenu: ['GCD'], + shortDescription: 'True if the GCD is not on cooldown, otherwise False.', + newValue: APLValueGCDIsReady.create, + fields: [], + }), + ['gcdTimeToReady']: inputBuilder({ + label: 'GCD Time To Ready', + submenu: ['GCD'], + shortDescription: 'Amount of time remaining before the GCD comes off cooldown, or 0 if it is not on cooldown.', + newValue: APLValueGCDTimeToReady.create, + fields: [], + }), + + // Spells + ['spellCanCast']: inputBuilder({ + label: 'Can Cast', + submenu: ['Spell'], + shortDescription: 'True if all requirements for casting the spell are currently met, otherwise False.', + fullDescription: ` +

The Cast Spell action does not need to be conditioned on this, because it applies this check automatically.

+ `, + newValue: APLValueSpellCanCast.create, + fields: [ + AplHelpers.actionIdFieldConfig('spellId', 'castable_spells'), + ], + }), + ['spellIsReady']: inputBuilder({ + label: 'Is Ready', + submenu: ['Spell'], + shortDescription: 'True if the spell is not on cooldown, otherwise False.', + newValue: APLValueSpellIsReady.create, + fields: [ + AplHelpers.actionIdFieldConfig('spellId', 'castable_spells'), + ], + }), + ['spellTimeToReady']: inputBuilder({ + label: 'Time To Ready', + submenu: ['Spell'], + shortDescription: 'Amount of time remaining before the spell comes off cooldown, or 0 if it is not on cooldown.', + newValue: APLValueSpellTimeToReady.create, + fields: [ + AplHelpers.actionIdFieldConfig('spellId', 'castable_spells'), + ], + }), + + // Auras + ['auraIsActive']: inputBuilder({ + label: 'Aura Is Active', + submenu: ['Aura'], + shortDescription: 'True if the aura is currently active on self, otherwise False.', + newValue: APLValueAuraIsActive.create, + fields: [ + AplHelpers.actionIdFieldConfig('auraId', 'auras'), + ], + }), + ['auraRemainingTime']: inputBuilder({ + label: 'Aura Remaining Time', + submenu: ['Aura'], + shortDescription: 'Time remaining before this aura will expire, or 0 if the aura is not currently active on self.', + newValue: APLValueAuraRemainingTime.create, + fields: [ + AplHelpers.actionIdFieldConfig('auraId', 'auras'), + ], + }), + ['auraNumStacks']: inputBuilder({ + label: 'Aura Num Stacks', + submenu: ['Aura'], + shortDescription: 'Number of stacks of the aura on self.', + newValue: APLValueAuraNumStacks.create, + fields: [ + AplHelpers.actionIdFieldConfig('auraId', 'stackable_auras'), + ], + }), + // DoT ['dotIsActive']: inputBuilder({ label: 'Dot Is Active', @@ -380,4 +465,4 @@ const valueTypeFactories: Record, ValueTypeConfig AplHelpers.actionIdFieldConfig('spellId', 'dot_spells'), ], }), -}; \ No newline at end of file +}; From a70e9c14ea4a05f87db745f3dc45c83848f5373c Mon Sep 17 00:00:00 2001 From: James Tanner Date: Fri, 7 Jul 2023 00:13:18 +0000 Subject: [PATCH 2/3] Work on support for multidotting --- proto/apl.proto | 6 +++++ sim/core/apl_action.go | 2 ++ sim/core/apl_actions_core.go | 45 ++++++++++++++++++++++++++++++++++++ sim/core/apl_values_core.go | 8 +++++++ 4 files changed, 61 insertions(+) diff --git a/proto/apl.proto b/proto/apl.proto index ce9acc2dd2..f9fa41ce73 100644 --- a/proto/apl.proto +++ b/proto/apl.proto @@ -28,6 +28,7 @@ message APLAction { APLActionStrictSequence strict_sequence = 6; APLActionCastSpell cast_spell = 3; + APLActionMultidot multidot = 8; APLActionAutocastOtherCooldowns autocast_other_cooldowns = 7; APLActionWait wait = 4; } @@ -102,6 +103,11 @@ message APLActionCastSpell { ActionID spell_id = 1; } +message APLActionMultidot { + ActionID spell_id = 1; + int32 max_dots = 2; +} + message APLActionAutocastOtherCooldowns { } diff --git a/sim/core/apl_action.go b/sim/core/apl_action.go index a47f7e9627..f5f28963f2 100644 --- a/sim/core/apl_action.go +++ b/sim/core/apl_action.go @@ -66,6 +66,8 @@ func (unit *Unit) newAPLActionImpl(config *proto.APLAction) APLActionImpl { return unit.newActionStrictSequence(config.GetStrictSequence()) case *proto.APLAction_CastSpell: return unit.newActionCastSpell(config.GetCastSpell()) + case *proto.APLAction_Multidot: + return unit.newActionMultidot(config.GetMultidot()) case *proto.APLAction_AutocastOtherCooldowns: return unit.newActionAutocastOtherCooldowns(config.GetAutocastOtherCooldowns()) case *proto.APLAction_Wait: diff --git a/sim/core/apl_actions_core.go b/sim/core/apl_actions_core.go index d7f6541a54..55ca8f2056 100644 --- a/sim/core/apl_actions_core.go +++ b/sim/core/apl_actions_core.go @@ -1,6 +1,8 @@ package core import ( + "time" + "github.com/wowsims/wotlk/sim/core/proto" ) @@ -28,6 +30,49 @@ func (action *APLActionCastSpell) Execute(sim *Simulation) { action.spell.Cast(sim, action.spell.Unit.CurrentTarget) } +type APLActionMultidot struct { + spell *Spell + maxDots int32 + refreshWindow time.Duration + + nextTarget *Unit +} + +func (unit *Unit) newActionMultidot(config *proto.APLActionMultidot) APLActionImpl { + spell := unit.aplGetMultidotSpell(config.SpellId) + + refreshWindow := time.Duration(0) + canRollover := false + if canRollover { + refreshWindow = time.Second*3 + } + return &APLActionMultidot{ + spell: spell, + maxDots: MinInt32(config.MaxDots, unit.Env.GetNumTargets()), + refreshWindow: refreshWindow, + } +} +func (action *APLActionMultidot) GetInnerActions() []*APLAction { return nil } +func (action *APLActionMultidot) Finalize() {} +func (action *APLActionMultidot) Reset(*Simulation) { + action.nextTarget = nil +} +func (action *APLActionMultidot) IsReady(sim *Simulation) bool { + for i := 0; i < action.maxDots; i++ { + target := sim.Encounter.GetTarget(i) + dot := action.spell.Dot(target) + shouldPreserveSnapshot := dot. + if dot.IsActive() && dot.RemainingDuration(sim) < time.Second*3 { + action.nextTarget = target + return true + } + } + return false +} +func (action *APLActionMultidot) Execute(sim *Simulation) { + action.spell.Cast(sim, action.nextTarget) +} + type APLActionAutocastOtherCooldowns struct { character *Character diff --git a/sim/core/apl_values_core.go b/sim/core/apl_values_core.go index b4cb8ddae0..949cdea38d 100644 --- a/sim/core/apl_values_core.go +++ b/sim/core/apl_values_core.go @@ -19,6 +19,14 @@ func (unit *Unit) aplGetDot(spellId *proto.ActionID) *Dot { } } +func (unit *Unit) aplGetMultidotSpell(spellId *proto.ActionID) *Spell { + spell := unit.GetSpell(ProtoToActionID(spellId)) + if spell == nil || spell.CurDot() == nil { + return nil + } + return spell +} + type APLValueDotIsActive struct { defaultAPLValueImpl dot *Dot From aeae39ac970b9c1449f5511e1e7351c0d0023126 Mon Sep 17 00:00:00 2001 From: James Tanner Date: Thu, 6 Jul 2023 18:45:52 -0700 Subject: [PATCH 3/3] Multidot implemented for APL --- proto/apl.proto | 3 +- sim/core/apl_actions_core.go | 32 ++++++++-------- .../individual_sim_ui/apl_actions.ts | 38 ++++++++++++++++++- .../individual_sim_ui/apl_helpers.ts | 22 ++++++++++- .../individual_sim_ui/apl_values.ts | 3 +- .../_apl_rotation_picker.scss | 8 +++- 6 files changed, 83 insertions(+), 23 deletions(-) diff --git a/proto/apl.proto b/proto/apl.proto index f9fa41ce73..e0e127317f 100644 --- a/proto/apl.proto +++ b/proto/apl.proto @@ -105,7 +105,8 @@ message APLActionCastSpell { message APLActionMultidot { ActionID spell_id = 1; - int32 max_dots = 2; + int32 max_dots = 2; + APLValue max_overlap = 3; } message APLActionAutocastOtherCooldowns { diff --git a/sim/core/apl_actions_core.go b/sim/core/apl_actions_core.go index 55ca8f2056..cd271ee530 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" ) @@ -31,25 +29,24 @@ func (action *APLActionCastSpell) Execute(sim *Simulation) { } type APLActionMultidot struct { - spell *Spell - maxDots int32 - refreshWindow time.Duration + spell *Spell + maxDots int32 + maxOverlap APLValue nextTarget *Unit } func (unit *Unit) newActionMultidot(config *proto.APLActionMultidot) APLActionImpl { spell := unit.aplGetMultidotSpell(config.SpellId) - - refreshWindow := time.Duration(0) - canRollover := false - if canRollover { - refreshWindow = time.Second*3 + maxOverlap := unit.coerceTo(unit.newAPLValue(config.MaxOverlap), proto.APLValueType_ValueTypeDuration) + if maxOverlap == nil { + maxOverlap = unit.newValueConst(&proto.APLValueConst{Val: "0ms"}) } + return &APLActionMultidot{ - spell: spell, - maxDots: MinInt32(config.MaxDots, unit.Env.GetNumTargets()), - refreshWindow: refreshWindow, + spell: spell, + maxDots: MinInt32(config.MaxDots, unit.Env.GetNumTargets()), + maxOverlap: maxOverlap, } } func (action *APLActionMultidot) GetInnerActions() []*APLAction { return nil } @@ -58,11 +55,12 @@ func (action *APLActionMultidot) Reset(*Simulation) { action.nextTarget = nil } func (action *APLActionMultidot) IsReady(sim *Simulation) bool { - for i := 0; i < action.maxDots; i++ { - target := sim.Encounter.GetTarget(i) + maxOverlap := action.maxOverlap.GetDuration(sim) + + for i := int32(0); i < action.maxDots; i++ { + target := sim.Encounter.TargetUnits[i] dot := action.spell.Dot(target) - shouldPreserveSnapshot := dot. - if dot.IsActive() && dot.RemainingDuration(sim) < time.Second*3 { + if (!dot.IsActive() || dot.RemainingDuration(sim) < maxOverlap) && action.spell.CanCast(sim, target) { action.nextTarget = target return true } diff --git a/ui/core/components/individual_sim_ui/apl_actions.ts b/ui/core/components/individual_sim_ui/apl_actions.ts index f1bbf57395..6fd087c219 100644 --- a/ui/core/components/individual_sim_ui/apl_actions.ts +++ b/ui/core/components/individual_sim_ui/apl_actions.ts @@ -4,6 +4,7 @@ import { APLActionSequence, APLActionResetSequence, APLActionStrictSequence, + APLActionMultidot, APLActionAutocastOtherCooldowns, APLActionWait, APLValue, @@ -261,6 +262,32 @@ export const actionTypeFactories: Record, ActionTypeC actionListFieldConfig('actions'), ], }), + ['multidot']: inputBuilder({ + label: 'Multi Dot', + shortDescription: 'Keeps a DoT active on multiple targets by casting the specified spell.', + newValue: () => APLActionMultidot.create({ + maxDots: 3, + maxOverlap: { + value: { + oneofKind: 'const', + const: { + val: '0ms', + }, + }, + }, + }), + fields: [ + AplHelpers.actionIdFieldConfig('spellId', 'dot_spells'), + AplHelpers.numberFieldConfig('maxDots', { + label: 'Max Dots', + labelTooltip: 'Maximum number of DoTs to simultaneously apply.', + }), + AplValues.valueFieldConfig('maxOverlap', { + label: 'Overlap', + labelTooltip: 'Maximum amount of time before a DoT expires when it may be refreshed.', + }), + ], + }), ['autocastOtherCooldowns']: inputBuilder({ label: 'Autocast Other Cooldowns', submenu: ['Misc'], @@ -278,7 +305,16 @@ export const actionTypeFactories: Record, ActionTypeC label: 'Wait', submenu: ['Misc'], shortDescription: 'Pauses the GCD for a specified amount of time.', - newValue: APLActionWait.create, + newValue: () => APLActionWait.create({ + duration: { + value: { + oneofKind: 'const', + const: { + val: '1000ms', + }, + }, + }, + }), fields: [ AplValues.valueFieldConfig('duration'), ], diff --git a/ui/core/components/individual_sim_ui/apl_helpers.ts b/ui/core/components/individual_sim_ui/apl_helpers.ts index bd524bd7e8..bbe869b968 100644 --- a/ui/core/components/individual_sim_ui/apl_helpers.ts +++ b/ui/core/components/individual_sim_ui/apl_helpers.ts @@ -3,6 +3,7 @@ import { Player } from '../../player.js'; import { EventID, TypedEvent } from '../../typed_event.js'; import { bucket } from '../../utils.js'; import { AdaptiveStringPicker } from '../string_picker.js'; +import { NumberPicker } from '../number_picker.js'; import { DropdownPicker, DropdownPickerConfig, DropdownValueConfig, TextDropdownPicker } from '../dropdown_picker.js'; import { Input, InputConfig } from '../input.js'; import { ActionID } from '../../proto/common.js'; @@ -118,6 +119,9 @@ export interface APLPickerBuilderFieldConfig { field: F, newValue: () => T[F], factory: (parent: HTMLElement, player: Player, config: InputConfig, T[F]>) => Input, T[F]> + + label?: string, + labelTooltip?: string, } export interface APLPickerBuilderConfig extends InputConfig, T> { @@ -155,6 +159,9 @@ export class APLPickerBuilder extends Input, T> { return { ...fieldConfig, picker: fieldConfig.factory(builder.rootElem, builder.modObject, { + label: fieldConfig.label, + labelTooltip: fieldConfig.labelTooltip, + inline: true, changedEvent: (player: Player) => player.rotationChangeEmitter, getValue: () => { const source = builder.getSourceValue(); @@ -190,7 +197,7 @@ export class APLPickerBuilder extends Input, T> { } } -export function actionIdFieldConfig(field: string, actionIdSet: ACTION_ID_SET): APLPickerBuilderFieldConfig { +export function actionIdFieldConfig(field: string, actionIdSet: ACTION_ID_SET, options?: Partial>): APLPickerBuilderFieldConfig { return { field: field, newValue: () => ActionID.create(), @@ -198,14 +205,25 @@ export function actionIdFieldConfig(field: string, actionIdSet: ACTION_ID_SET): ...config, actionIdSet: actionIdSet, }), + ...(options || {}), + }; +} + +export function numberFieldConfig(field: string, options?: Partial>): APLPickerBuilderFieldConfig { + return { + field: field, + newValue: () => 0, + factory: (parent, player, config) => new NumberPicker(parent, player, config), + ...(options || {}), }; } -export function stringFieldConfig(field: string): APLPickerBuilderFieldConfig { +export function stringFieldConfig(field: string, options?: Partial>): APLPickerBuilderFieldConfig { return { field: field, newValue: () => '', factory: (parent, player, config) => new AdaptiveStringPicker(parent, player, config), + ...(options || {}), }; } diff --git a/ui/core/components/individual_sim_ui/apl_values.ts b/ui/core/components/individual_sim_ui/apl_values.ts index 9998d2e4bd..ecb8d77d7e 100644 --- a/ui/core/components/individual_sim_ui/apl_values.ts +++ b/ui/core/components/individual_sim_ui/apl_values.ts @@ -196,11 +196,12 @@ function comparisonOperatorFieldConfig(field: string): AplHelpers.APLPickerBuild }; } -export function valueFieldConfig(field: string): AplHelpers.APLPickerBuilderFieldConfig { +export function valueFieldConfig(field: string, options?: Partial>): AplHelpers.APLPickerBuilderFieldConfig { return { field: field, newValue: APLValue.create, factory: (parent, player, config) => new APLValuePicker(parent, player, config), + ...(options || {}), }; } 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 aa9937911b..33f94db96f 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 @@ -60,13 +60,19 @@ flex-direction: column; } +.apl-picker-builder-root { + .form-label { + margin: 0 1px 0 5px; + } +} + .apl-value-picker-root { * { font-size: calc(var(--bs-btn-font-size) - 0.2rem); } .form-label { - margin: 0; + margin: 0 1px 0 5px; } }