diff --git a/proto/api.proto b/proto/api.proto index a25b6496ee..e9a0e6e9eb 100644 --- a/proto/api.proto +++ b/proto/api.proto @@ -304,6 +304,7 @@ message SpellStats { 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. + bool prepull_only = 5; // Whether this spell may only be cast during prepull. } message APLActionStats { repeated string warnings = 1; diff --git a/proto/apl.proto b/proto/apl.proto index 683f17e111..c66b1746c8 100644 --- a/proto/apl.proto +++ b/proto/apl.proto @@ -13,6 +13,12 @@ message APLRotation { repeated APLListItem priority_list = 2; } +message APLPrepullAction { + APLAction action = 1; + string do_at = 2; // Should be a negative value. Uses the same syntax as APLValueConst. + bool hide = 3; // Causes this item to be ignored. +} + message APLListItem { bool hide = 1; // Causes this item to be ignored. string notes = 2; // Comments for the reader. @@ -76,11 +82,6 @@ message APLValue { } } -message APLPrepullAction { - APLAction action = 1; - Duration do_at = 2; // Should be a negative value. -} - /////////////////////////////////////////////////////////////////////////// // ACTIONS /////////////////////////////////////////////////////////////////////////// diff --git a/proto/common.proto b/proto/common.proto index 104189be97..19c3fac726 100644 --- a/proto/common.proto +++ b/proto/common.proto @@ -777,6 +777,7 @@ enum OtherAction { OtherActionFrostRuneGain = 14; // Indicates healing received from healing model. OtherActionUnholyRuneGain = 15; // Indicates healing received from healing model. OtherActionDeathRuneGain = 16; // Indicates healing received from healing model. + OtherActionPotion = 17; // Used by APL to generically refer to either the prepull or combat potion. } message ActionID { diff --git a/sim/core/apl.go b/sim/core/apl.go index d0f2dedb40..712567e6cb 100644 --- a/sim/core/apl.go +++ b/sim/core/apl.go @@ -9,12 +9,16 @@ import ( ) type APLRotation struct { - unit *Unit - priorityList []*APLAction + unit *Unit + prepullActions []*APLAction + priorityList []*APLAction // Current strict sequence strictSequence *APLActionStrictSequence + // Used inside of actions/value to determine whether they will occur during the prepull or regular rotation. + parsingPrepull bool + // Validation warnings that occur during proto parsing. // We return these back to the user for display in the UI. curWarnings []string @@ -36,6 +40,33 @@ func (unit *Unit) newAPLRotation(config *proto.APLRotation) *APLRotation { unit: unit, } + // Parse prepull actions + rotation.parsingPrepull = true + for _, prepullItem := range config.PrepullActions { + if !prepullItem.Hide { + doAt := time.Duration(1) + if durVal, err := time.ParseDuration(prepullItem.DoAt); err == nil { + doAt = durVal + } + if doAt > 0 { + rotation.validationWarning("Invalid time for 'Do At', ignoring this Prepull Action") + } else { + action := rotation.newAPLAction(prepullItem.Action) + if action != nil { + rotation.prepullActions = append(rotation.prepullActions, action) + unit.RegisterPrepullAction(doAt, func(sim *Simulation) { + action.Execute(sim) + }) + } + } + } + + rotation.prepullWarnings = append(rotation.prepullWarnings, rotation.curWarnings) + rotation.curWarnings = nil + } + rotation.parsingPrepull = false + + // Parse priority list var configIdxs []int for i, aplItem := range config.PriorityList { if !aplItem.Hide { @@ -50,19 +81,43 @@ func (unit *Unit) newAPLRotation(config *proto.APLRotation) *APLRotation { rotation.curWarnings = nil } + // Finalize + for _, action := range rotation.prepullActions { + action.impl.Finalize(rotation) + rotation.curWarnings = nil + } for i, action := range rotation.priorityList { action.impl.Finalize(rotation) rotation.priorityListWarnings[configIdxs[i]] = append(rotation.priorityListWarnings[configIdxs[i]], rotation.curWarnings...) rotation.curWarnings = nil + } - // Remove MCDs that are referenced by APL actions. - character := unit.Env.Raid.GetPlayerFromUnit(unit).GetCharacter() + // Remove MCDs that are referenced by APL actions, so that the Autocast Other Cooldowns + // action does not include them. + character := unit.Env.Raid.GetPlayerFromUnit(unit).GetCharacter() + for _, action := range rotation.allAPLActions() { if castSpellAction, ok := action.impl.(*APLActionCastSpell); ok { character.removeInitialMajorCooldown(castSpellAction.spell.ActionID) } } + // If user has a Prepull potion set but does not use it in their APL settings, we enable it here. + prepotSpell := rotation.aplGetSpell(ActionID{OtherID: proto.OtherAction_OtherActionPotion}.ToProto()) + if prepotSpell != nil { + found := false + for _, prepullAction := range rotation.allPrepullActions() { + if castSpellAction, ok := prepullAction.impl.(*APLActionCastSpell); ok && castSpellAction.spell == prepotSpell { + found = true + } + } + if !found { + unit.RegisterPrepullAction(-1*time.Second, func(sim *Simulation) { + prepotSpell.Cast(sim, nil) + }) + } + } + return rotation } func (rot *APLRotation) getStats() *proto.APLStats { @@ -77,6 +132,11 @@ func (rot *APLRotation) allAPLActions() []*APLAction { return Flatten(MapSlice(rot.priorityList, func(action *APLAction) []*APLAction { return action.GetAllActions() })) } +// Returns all action objects from the prepull as an unstructured list. Used for easily finding specific actions. +func (rot *APLRotation) allPrepullActions() []*APLAction { + return Flatten(MapSlice(rot.prepullActions, func(action *APLAction) []*APLAction { return action.GetAllActions() })) +} + func (rot *APLRotation) reset(sim *Simulation) { rot.strictSequence = nil for _, action := range rot.allAPLActions() { diff --git a/sim/core/apl_values_core.go b/sim/core/apl_values_core.go index 96506d8996..ca893b3a9c 100644 --- a/sim/core/apl_values_core.go +++ b/sim/core/apl_values_core.go @@ -7,9 +7,31 @@ import ( ) func (rot *APLRotation) aplGetSpell(spellId *proto.ActionID) *Spell { - spell := rot.unit.GetSpell(ProtoToActionID(spellId)) + actionID := ProtoToActionID(spellId) + var spell *Spell + + if actionID.IsOtherAction(proto.OtherAction_OtherActionPotion) { + if rot.parsingPrepull { + for _, s := range rot.unit.Spellbook { + if s.Flags.Matches(SpellFlagPrepullPotion) { + spell = s + break + } + } + } else { + for _, s := range rot.unit.Spellbook { + if s.Flags.Matches(SpellFlagCombatPotion) { + spell = s + break + } + } + } + } else { + spell = rot.unit.GetSpell(actionID) + } + if spell == nil { - rot.validationWarning("%s does not know spell %s", rot.unit.Label, ProtoToActionID(spellId)) + rot.validationWarning("%s does not know spell %s", rot.unit.Label, actionID) } return spell } diff --git a/sim/core/character.go b/sim/core/character.go index 6d4dcd6677..63de326730 100644 --- a/sim/core/character.go +++ b/sim/core/character.go @@ -510,6 +510,7 @@ func (character *Character) FillPlayerStats(playerStats *proto.PlayerStats) { IsCastable: spell.Flags.Matches(SpellFlagAPL), IsMajorCooldown: spell.Flags.Matches(SpellFlagMCD), HasDot: spell.dots != nil, + PrepullOnly: spell.Flags.Matches(SpellFlagPrepullOnly), } }) diff --git a/sim/core/consumes.go b/sim/core/consumes.go index a40cf08d41..99ee716961 100644 --- a/sim/core/consumes.go +++ b/sim/core/consumes.go @@ -456,19 +456,22 @@ func registerPotionCD(agent Agent, consumes *proto.Consumes) { startingMCD := makePotionActivation(startingPotion, character, potionCD) if startingMCD.Spell != nil { - character.RegisterPrepullAction(-1*time.Second, func(sim *Simulation) { - startingMCD.Spell.Cast(sim, nil) - if startingPotion == proto.Potions_IndestructiblePotion { - potionCD.Set(sim.CurrentTime + 2*time.Minute) - } else { - potionCD.Set(sim.CurrentTime + time.Minute) - } - character.UpdateMajorCooldowns() - }) + startingMCD.Spell.Flags |= SpellFlagPrepullPotion + if !character.IsUsingAPL { + character.RegisterPrepullAction(-1*time.Second, func(sim *Simulation) { + startingMCD.Spell.Cast(sim, nil) + }) + } } - defaultMCD := makePotionActivation(defaultPotion, character, potionCD) + var defaultMCD MajorCooldown + if defaultPotion == startingPotion { + defaultMCD = startingMCD + } else { + defaultMCD = makePotionActivation(defaultPotion, character, potionCD) + } if defaultMCD.Spell != nil { + defaultMCD.Spell.Flags |= SpellFlagCombatPotion character.AddMajorCooldown(defaultMCD) } } @@ -484,6 +487,25 @@ func (character *Character) HasAlchStone() bool { } func makePotionActivation(potionType proto.Potions, character *Character, potionCD *Timer) MajorCooldown { + mcd := makePotionActivationInternal(potionType, character, potionCD) + if mcd.Spell != nil { + oldApplyEffects := mcd.Spell.ApplyEffects + mcd.Spell.ApplyEffects = func(sim *Simulation, target *Unit, spell *Spell) { + oldApplyEffects(sim, target, spell) + if sim.CurrentTime < 0 { + if potionType == proto.Potions_IndestructiblePotion { + potionCD.Set(sim.CurrentTime + 2*time.Minute) + } else { + potionCD.Set(sim.CurrentTime + time.Minute) + } + character.UpdateMajorCooldowns() + } + } + } + return mcd +} + +func makePotionActivationInternal(potionType proto.Potions, character *Character, potionCD *Timer) MajorCooldown { alchStoneEquipped := character.HasAlchStone() hasEngi := character.HasProfession(proto.Profession_Engineering) diff --git a/sim/core/flags.go b/sim/core/flags.go index 4812fb6d88..34dc6a531c 100644 --- a/sim/core/flags.go +++ b/sim/core/flags.go @@ -201,6 +201,9 @@ const ( SpellFlagAPL // Indicates this spell can be used from an APL rotation. SpellFlagMCD // Indicates this spell is a MajorCooldown. SpellFlagNoOnDamageDealt // Disables OnSpellHitDealt and OnPeriodicDamageDealt aura callbacks for this spell. + SpellFlagPrepullOnly // Indicates this spell should only be used during prepull. Not enforced, just a signal for the APL UI. + SpellFlagPrepullPotion // Indicates this spell is the prepull potion. + SpellFlagCombatPotion // Indicates this spell is the combat potion. // Used to let agents categorize their spells. SpellFlagAgentReserved1 diff --git a/sim/hunter/TestAPL.results b/sim/hunter/TestAPL.results index 802bd4a773..5f250bebae 100644 --- a/sim/hunter/TestAPL.results +++ b/sim/hunter/TestAPL.results @@ -46,807 +46,807 @@ character_stats_results: { dps_results: { key: "TestAPL-AllItems-Ahn'KaharBloodHunter'sBattlegear" value: { - dps: 6808.77453 - tps: 5951.31643 + dps: 6838.6695 + tps: 5986.5155 } } dps_results: { key: "TestAPL-AllItems-Althor'sAbacus-50359" value: { - dps: 6359.36194 - tps: 5471.50199 + dps: 6355.359 + tps: 5460.07044 } } dps_results: { key: "TestAPL-AllItems-Althor'sAbacus-50366" value: { - dps: 6359.36194 - tps: 5471.50199 + dps: 6355.359 + tps: 5460.07044 } } dps_results: { key: "TestAPL-AllItems-AshtongueTalismanofSwiftness-32487" value: { - dps: 6401.66941 - tps: 5509.98309 + dps: 6397.56576 + tps: 5498.45589 } } dps_results: { key: "TestAPL-AllItems-AustereEarthsiegeDiamond" value: { - dps: 6476.09893 - tps: 5569.567 + dps: 6479.17829 + tps: 5568.30803 } } dps_results: { key: "TestAPL-AllItems-Bandit'sInsignia-40371" value: { - dps: 6518.52984 - tps: 5621.47074 + dps: 6515.5783 + tps: 5610.94284 } } dps_results: { key: "TestAPL-AllItems-BaubleofTrueBlood-50354" value: { - dps: 6359.36194 - tps: 5471.50199 + dps: 6355.359 + tps: 5460.07044 } } dps_results: { key: "TestAPL-AllItems-BaubleofTrueBlood-50726" value: { - dps: 6359.36194 - tps: 5471.50199 + dps: 6355.359 + tps: 5460.07044 } } dps_results: { key: "TestAPL-AllItems-BeamingEarthsiegeDiamond" value: { - dps: 6483.39371 - tps: 5579.4438 + dps: 6486.49257 + tps: 5577.97427 } } dps_results: { key: "TestAPL-AllItems-Beast-tamer'sShoulders-30892" value: { - dps: 6411.99969 - tps: 5511.97463 + dps: 6428.50839 + tps: 5536.26057 } } dps_results: { key: "TestAPL-AllItems-BlackBowoftheBetrayer-32336" value: { - dps: 6169.11074 - tps: 5271.18471 + dps: 6209.95468 + tps: 5311.52782 } } dps_results: { key: "TestAPL-AllItems-BlackBruise-50035" value: { - dps: 6264.01592 - tps: 5375.28203 + dps: 6278.37018 + tps: 5383.7009 } } dps_results: { key: "TestAPL-AllItems-BlackBruise-50692" value: { - dps: 6255.39788 - tps: 5366.95981 + dps: 6269.74263 + tps: 5375.37115 } } dps_results: { key: "TestAPL-AllItems-BlessedGarboftheUndeadSlayer" value: { - dps: 5403.44665 - tps: 4651.88139 + dps: 5368.73782 + tps: 4622.12887 } } dps_results: { key: "TestAPL-AllItems-BlessedRegaliaofUndeadCleansing" value: { - dps: 5175.48813 - tps: 4441.17796 + dps: 5207.19101 + tps: 4475.86528 } } dps_results: { key: "TestAPL-AllItems-BracingEarthsiegeDiamond" value: { - dps: 6468.00821 - tps: 5454.04685 + dps: 6471.08232 + tps: 5452.82058 } } dps_results: { key: "TestAPL-AllItems-Bryntroll,theBoneArbiter-50415" value: { - dps: 6673.44565 - tps: 5754.70275 + dps: 6674.6941 + tps: 5751.28087 } } dps_results: { key: "TestAPL-AllItems-Bryntroll,theBoneArbiter-50709" value: { - dps: 6676.53614 - tps: 5756.81039 + dps: 6677.78591 + tps: 5753.38561 } } dps_results: { key: "TestAPL-AllItems-ChaoticSkyflareDiamond" value: { - dps: 6617.14966 - tps: 5713.25276 + dps: 6619.42702 + tps: 5710.95436 } } dps_results: { key: "TestAPL-AllItems-CorpseTongueCoin-50349" value: { - dps: 6359.36194 - tps: 5471.50199 + dps: 6355.359 + tps: 5460.07044 } } dps_results: { key: "TestAPL-AllItems-CorpseTongueCoin-50352" value: { - dps: 6359.36194 - tps: 5471.50199 + dps: 6355.359 + tps: 5460.07044 } } dps_results: { key: "TestAPL-AllItems-CorrodedSkeletonKey-50356" value: { - dps: 6417.25183 - tps: 5510.78045 + dps: 6413.27004 + tps: 5499.23417 } } dps_results: { key: "TestAPL-AllItems-CryptstalkerBattlegear" value: { - dps: 5957.9859 - tps: 5113.55528 + dps: 5942.8098 + tps: 5095.41903 } } dps_results: { key: "TestAPL-AllItems-DarkmoonCard:Berserker!-42989" value: { - dps: 6440.94101 - tps: 5552.16163 + dps: 6442.14902 + tps: 5547.06894 } } dps_results: { key: "TestAPL-AllItems-DarkmoonCard:Death-42990" value: { - dps: 6494.81846 - tps: 5605.90621 + dps: 6497.90414 + tps: 5602.66324 } } dps_results: { key: "TestAPL-AllItems-DarkmoonCard:Greatness-44255" value: { - dps: 6555.74219 - tps: 5654.16571 + dps: 6558.7488 + tps: 5650.76071 } } dps_results: { key: "TestAPL-AllItems-Death'sChoice-47464" value: { - dps: 6751.96667 - tps: 5835.05235 + dps: 6759.87434 + tps: 5837.50203 } } dps_results: { key: "TestAPL-AllItems-DeathKnight'sAnguish-38212" value: { - dps: 6415.2667 - tps: 5526.32996 + dps: 6422.39047 + tps: 5527.21437 } } dps_results: { key: "TestAPL-AllItems-Deathbringer'sWill-50362" value: { - dps: 6664.41633 - tps: 5762.30817 + dps: 6659.41889 + tps: 5753.59864 } } dps_results: { key: "TestAPL-AllItems-Deathbringer'sWill-50363" value: { - dps: 6709.59999 - tps: 5807.39803 + dps: 6707.62778 + tps: 5800.46531 } } dps_results: { key: "TestAPL-AllItems-Defender'sCode-40257" value: { - dps: 6359.36194 - tps: 5471.50199 + dps: 6355.359 + tps: 5460.07044 } } dps_results: { key: "TestAPL-AllItems-DestructiveSkyflareDiamond" value: { - dps: 6486.84545 - tps: 5582.97038 + dps: 6491.55886 + tps: 5583.1141 } } dps_results: { key: "TestAPL-AllItems-DislodgedForeignObject-50348" value: { - dps: 6492.39114 - tps: 5599.09197 + dps: 6530.15083 + tps: 5630.60498 } } dps_results: { key: "TestAPL-AllItems-DislodgedForeignObject-50353" value: { - dps: 6496.89004 - tps: 5605.17405 + dps: 6501.42773 + tps: 5606.401 } } dps_results: { key: "TestAPL-AllItems-EffulgentSkyflareDiamond" value: { - dps: 6476.09893 - tps: 5569.567 + dps: 6479.17829 + tps: 5568.30803 } } dps_results: { key: "TestAPL-AllItems-EmberSkyflareDiamond" value: { - dps: 6474.14013 - tps: 5569.61325 + dps: 6477.21163 + tps: 5568.35821 } } dps_results: { key: "TestAPL-AllItems-EnigmaticSkyflareDiamond" value: { - dps: 6483.39371 - tps: 5579.49682 + dps: 6486.49257 + tps: 5578.01991 } } dps_results: { key: "TestAPL-AllItems-EnigmaticStarflareDiamond" value: { - dps: 6481.59988 - tps: 5577.68472 + dps: 6484.20549 + tps: 5575.70918 } } dps_results: { key: "TestAPL-AllItems-EphemeralSnowflake-50260" value: { - dps: 6359.36194 - tps: 5471.50199 + dps: 6355.359 + tps: 5460.07044 } } dps_results: { key: "TestAPL-AllItems-EssenceofGossamer-37220" value: { - dps: 6387.54517 - tps: 5490.6244 + dps: 6383.55253 + tps: 5479.13699 } } dps_results: { key: "TestAPL-AllItems-EternalEarthsiegeDiamond" value: { - dps: 6468.00821 - tps: 5564.0972 + dps: 6471.08232 + tps: 5562.84298 } } dps_results: { key: "TestAPL-AllItems-ExtractofNecromanticPower-40373" value: { - dps: 6498.22487 - tps: 5609.52601 + dps: 6499.50167 + tps: 5604.23671 } } dps_results: { key: "TestAPL-AllItems-EyeoftheBroodmother-45308" value: { - dps: 6430.90529 - tps: 5542.0668 + dps: 6434.94561 + tps: 5539.76036 } } dps_results: { key: "TestAPL-AllItems-Figurine-SapphireOwl-42413" value: { - dps: 6383.54955 - tps: 5493.28962 + dps: 6379.49908 + tps: 5481.78289 } } dps_results: { key: "TestAPL-AllItems-ForethoughtTalisman-40258" value: { - dps: 6359.36194 - tps: 5471.50199 + dps: 6355.359 + tps: 5460.07044 } } dps_results: { key: "TestAPL-AllItems-ForgeEmber-37660" value: { - dps: 6414.07778 - tps: 5525.08561 + dps: 6422.31406 + tps: 5526.97043 } } dps_results: { key: "TestAPL-AllItems-ForlornSkyflareDiamond" value: { - dps: 6468.00821 - tps: 5564.0972 + dps: 6471.08232 + tps: 5562.84298 } } dps_results: { key: "TestAPL-AllItems-ForlornStarflareDiamond" value: { - dps: 6468.00821 - tps: 5564.0972 + dps: 6471.08232 + tps: 5562.84298 } } dps_results: { key: "TestAPL-AllItems-FuryoftheFiveFlights-40431" value: { - dps: 6519.72222 - tps: 5616.95797 + dps: 6515.50466 + tps: 5605.15566 } } dps_results: { key: "TestAPL-AllItems-FuturesightRune-38763" value: { - dps: 6359.36194 - tps: 5471.50199 + dps: 6355.359 + tps: 5460.07044 } } dps_results: { key: "TestAPL-AllItems-Gladiator'sPursuit" value: { - dps: 6385.99104 - tps: 5523.79228 + dps: 6418.76133 + tps: 5550.18052 } } dps_results: { key: "TestAPL-AllItems-GlowingTwilightScale-54573" value: { - dps: 6359.36194 - tps: 5471.50199 + dps: 6355.359 + tps: 5460.07044 } } dps_results: { key: "TestAPL-AllItems-GlowingTwilightScale-54589" value: { - dps: 6359.36194 - tps: 5471.50199 + dps: 6355.359 + tps: 5460.07044 } } dps_results: { key: "TestAPL-AllItems-GnomishLightningGenerator-41121" value: { - dps: 6423.90095 - tps: 5534.99229 + dps: 6428.79572 + tps: 5533.51949 } } dps_results: { key: "TestAPL-AllItems-Gronnstalker'sArmor" value: { - dps: 4806.57617 - tps: 4091.4369 + dps: 4850.03905 + tps: 4136.16636 } } dps_results: { key: "TestAPL-AllItems-Heartpierce-49982" value: { - dps: 6699.31811 - tps: 5783.542 + dps: 6700.51413 + tps: 5780.08045 } } dps_results: { key: "TestAPL-AllItems-Heartpierce-50641" value: { - dps: 6702.70192 - tps: 5786.09043 + dps: 6703.89803 + tps: 5782.62537 } } dps_results: { key: "TestAPL-AllItems-IllustrationoftheDragonSoul-40432" value: { - dps: 6359.36194 - tps: 5471.50199 + dps: 6355.359 + tps: 5460.07044 } } dps_results: { key: "TestAPL-AllItems-ImpassiveSkyflareDiamond" value: { - dps: 6483.39371 - tps: 5579.49682 + dps: 6486.49257 + tps: 5578.01991 } } dps_results: { key: "TestAPL-AllItems-ImpassiveStarflareDiamond" value: { - dps: 6481.59988 - tps: 5577.68472 + dps: 6484.20549 + tps: 5575.70918 } } dps_results: { key: "TestAPL-AllItems-IncisorFragment-37723" value: { - dps: 6436.84606 - tps: 5541.74041 + dps: 6432.69086 + tps: 5530.10369 } } dps_results: { key: "TestAPL-AllItems-InsightfulEarthsiegeDiamond" value: { - dps: 6480.02036 - tps: 5580.70402 + dps: 6483.08937 + tps: 5579.33837 } } dps_results: { key: "TestAPL-AllItems-InvigoratingEarthsiegeDiamond" value: { - dps: 6491.1576 - tps: 5585.19355 + dps: 6494.43612 + tps: 5584.16462 hps: 12.04564 } } dps_results: { key: "TestAPL-AllItems-Lavanthor'sTalisman-37872" value: { - dps: 6359.36194 - tps: 5471.50199 + dps: 6355.359 + tps: 5460.07044 } } dps_results: { key: "TestAPL-AllItems-MajesticDragonFigurine-40430" value: { - dps: 6359.36194 - tps: 5471.50199 + dps: 6355.359 + tps: 5460.07044 } } dps_results: { key: "TestAPL-AllItems-MeteoriteWhetstone-37390" value: { - dps: 6467.24776 - tps: 5576.53563 + dps: 6462.25495 + tps: 5567.98855 } } dps_results: { key: "TestAPL-AllItems-NevermeltingIceCrystal-50259" value: { - dps: 6359.36194 - tps: 5471.50199 + dps: 6355.359 + tps: 5460.07044 } } dps_results: { key: "TestAPL-AllItems-OfferingofSacrifice-37638" value: { - dps: 6359.36194 - tps: 5471.50199 + dps: 6355.359 + tps: 5460.07044 } } dps_results: { key: "TestAPL-AllItems-PersistentEarthshatterDiamond" value: { - dps: 6485.68844 - tps: 5580.10728 + dps: 6488.75503 + tps: 5578.83916 } } dps_results: { key: "TestAPL-AllItems-PersistentEarthsiegeDiamond" value: { - dps: 6489.84849 - tps: 5583.87436 + dps: 6492.91331 + tps: 5582.60297 } } dps_results: { key: "TestAPL-AllItems-PetrifiedTwilightScale-54571" value: { - dps: 6359.36194 - tps: 5471.50199 + dps: 6355.359 + tps: 5460.07044 } } dps_results: { key: "TestAPL-AllItems-PetrifiedTwilightScale-54591" value: { - dps: 6359.36194 - tps: 5471.50199 + dps: 6355.359 + tps: 5460.07044 } } dps_results: { key: "TestAPL-AllItems-PowerfulEarthshatterDiamond" value: { - dps: 6474.58192 - tps: 5568.54141 + dps: 6477.6603 + tps: 5567.28333 } } dps_results: { key: "TestAPL-AllItems-PowerfulEarthsiegeDiamond" value: { - dps: 6476.09893 - tps: 5569.567 + dps: 6479.17829 + tps: 5568.30803 } } dps_results: { key: "TestAPL-AllItems-PurifiedShardoftheGods" value: { - dps: 6359.36194 - tps: 5471.50199 + dps: 6355.359 + tps: 5460.07044 } } dps_results: { key: "TestAPL-AllItems-ReignoftheDead-47316" value: { - dps: 6359.36194 - tps: 5471.50199 + dps: 6355.359 + tps: 5460.07044 } } dps_results: { key: "TestAPL-AllItems-ReignoftheDead-47477" value: { - dps: 6359.36194 - tps: 5471.50199 + dps: 6355.359 + tps: 5460.07044 } } dps_results: { key: "TestAPL-AllItems-RelentlessEarthsiegeDiamond" value: { - dps: 6631.9816 - tps: 5726.42524 + dps: 6633.21224 + tps: 5723.04225 } } dps_results: { key: "TestAPL-AllItems-RevitalizingSkyflareDiamond" value: { - dps: 6468.00821 - tps: 5563.86232 + dps: 6471.08232 + tps: 5562.61261 } } dps_results: { key: "TestAPL-AllItems-RuneofRepulsion-40372" value: { - dps: 6359.36194 - tps: 5471.50199 + dps: 6355.359 + tps: 5460.07044 } } dps_results: { key: "TestAPL-AllItems-ScourgestalkerBattlegear" value: { - dps: 6383.13257 - tps: 5520.4217 + dps: 6412.43297 + tps: 5542.26384 } } dps_results: { key: "TestAPL-AllItems-SealofthePantheon-36993" value: { - dps: 6359.36194 - tps: 5471.50199 + dps: 6355.359 + tps: 5460.07044 } } dps_results: { key: "TestAPL-AllItems-Shadowmourne-49623" value: { - dps: 6845.44848 - tps: 5924.25015 + dps: 6857.00012 + tps: 5931.25685 } } dps_results: { key: "TestAPL-AllItems-ShinyShardoftheGods" value: { - dps: 6359.36194 - tps: 5471.50199 + dps: 6355.359 + tps: 5460.07044 } } dps_results: { key: "TestAPL-AllItems-Sindragosa'sFlawlessFang-50361" value: { - dps: 6417.25183 - tps: 5510.78045 + dps: 6413.27004 + tps: 5499.23417 } } dps_results: { key: "TestAPL-AllItems-SliverofPureIce-50339" value: { - dps: 6359.36194 - tps: 5471.50199 + dps: 6355.359 + tps: 5460.07044 } } dps_results: { key: "TestAPL-AllItems-SliverofPureIce-50346" value: { - dps: 6359.36194 - tps: 5471.50199 + dps: 6355.359 + tps: 5460.07044 } } dps_results: { key: "TestAPL-AllItems-SoulPreserver-37111" value: { - dps: 6359.36194 - tps: 5471.50199 + dps: 6355.359 + tps: 5460.07044 } } dps_results: { key: "TestAPL-AllItems-SouloftheDead-40382" value: { - dps: 6433.14592 - tps: 5544.34631 + dps: 6437.4706 + tps: 5542.30696 } } dps_results: { key: "TestAPL-AllItems-SparkofLife-37657" value: { - dps: 6399.62835 - tps: 5504.19673 + dps: 6422.90726 + tps: 5522.15266 } } dps_results: { key: "TestAPL-AllItems-SphereofRedDragon'sBlood-37166" value: { - dps: 6359.36194 - tps: 5471.50199 + dps: 6355.359 + tps: 5460.07044 } } dps_results: { key: "TestAPL-AllItems-StormshroudArmor" value: { - dps: 5089.75019 - tps: 4368.62277 + dps: 5042.71269 + tps: 4314.48044 } } dps_results: { key: "TestAPL-AllItems-SwiftSkyflareDiamond" value: { - dps: 6489.84849 - tps: 5583.87436 + dps: 6492.91331 + tps: 5582.60297 } } dps_results: { key: "TestAPL-AllItems-SwiftStarflareDiamond" value: { - dps: 6485.68844 - tps: 5580.10728 + dps: 6488.75503 + tps: 5578.83916 } } dps_results: { key: "TestAPL-AllItems-SwiftWindfireDiamond" value: { - dps: 6478.40834 - tps: 5573.5149 + dps: 6481.47803 + tps: 5572.2525 } } dps_results: { key: "TestAPL-AllItems-TalismanofTrollDivinity-37734" value: { - dps: 6359.36194 - tps: 5471.50199 + dps: 6355.359 + tps: 5460.07044 } } dps_results: { key: "TestAPL-AllItems-TearsoftheVanquished-47215" value: { - dps: 6407.73716 - tps: 5515.09666 + dps: 6403.63916 + tps: 5503.51298 } } dps_results: { key: "TestAPL-AllItems-TheFistsofFury" value: { - dps: 6301.39112 - tps: 5410.95644 + dps: 6319.94969 + tps: 5423.67433 } } dps_results: { key: "TestAPL-AllItems-TheGeneral'sHeart-45507" value: { - dps: 6359.36194 - tps: 5471.50199 + dps: 6355.359 + tps: 5460.07044 } } dps_results: { key: "TestAPL-AllItems-TheTwinBladesofAzzinoth" value: { - dps: 6412.31455 - tps: 5522.23553 + dps: 6437.39722 + tps: 5540.03717 } } dps_results: { key: "TestAPL-AllItems-ThunderingSkyflareDiamond" value: { - dps: 6479.34973 - tps: 5575.06464 + dps: 6497.83567 + tps: 5592.76456 } } dps_results: { key: "TestAPL-AllItems-TinyAbominationinaJar-50351" value: { - dps: 6359.36194 - tps: 5471.50199 + dps: 6355.359 + tps: 5460.07044 } } dps_results: { key: "TestAPL-AllItems-TinyAbominationinaJar-50706" value: { - dps: 6359.36194 - tps: 5471.50199 + dps: 6355.359 + tps: 5460.07044 } } dps_results: { key: "TestAPL-AllItems-TirelessSkyflareDiamond" value: { - dps: 6468.00821 - tps: 5564.0972 + dps: 6471.08232 + tps: 5562.84298 } } dps_results: { key: "TestAPL-AllItems-TirelessStarflareDiamond" value: { - dps: 6468.00821 - tps: 5564.0972 + dps: 6471.08232 + tps: 5562.84298 } } dps_results: { key: "TestAPL-AllItems-TomeofArcanePhenomena-36972" value: { - dps: 6359.36194 - tps: 5471.50199 + dps: 6355.359 + tps: 5460.07044 } } dps_results: { key: "TestAPL-AllItems-TrenchantEarthshatterDiamond" value: { - dps: 6468.00821 - tps: 5564.0972 + dps: 6471.08232 + tps: 5562.84298 } } dps_results: { key: "TestAPL-AllItems-TrenchantEarthsiegeDiamond" value: { - dps: 6468.00821 - tps: 5564.0972 + dps: 6471.08232 + tps: 5562.84298 } } dps_results: { key: "TestAPL-AllItems-UndeadSlayer'sBlessedArmor" value: { - dps: 5384.29001 - tps: 4632.82276 + dps: 5355.98676 + tps: 4603.86686 } } dps_results: { key: "TestAPL-AllItems-Val'anyr,HammerofAncientKings-46017" value: { - dps: 6307.95447 - tps: 5425.27301 + dps: 6344.58873 + tps: 5457.4629 } } dps_results: { key: "TestAPL-AllItems-Windrunner'sPursuit" value: { - dps: 6528.11628 - tps: 5645.80409 + dps: 6568.58579 + tps: 5687.50824 } } dps_results: { key: "TestAPL-AllItems-WingedTalisman-37844" value: { - dps: 6359.36194 - tps: 5471.50199 + dps: 6355.359 + tps: 5460.07044 } } dps_results: { key: "TestAPL-AllItems-Zod'sRepeatingLongbow-50034" value: { - dps: 6854.57594 - tps: 5947.58849 + dps: 6878.19177 + tps: 5971.83342 } } dps_results: { key: "TestAPL-AllItems-Zod'sRepeatingLongbow-50638" value: { - dps: 7002.16346 - tps: 6090.23602 + dps: 7021.25916 + tps: 6115.4102 } } dps_results: { key: "TestAPL-Average-Default" value: { - dps: 6603.02449 - tps: 5696.07466 + dps: 6618.30018 + tps: 5711.77873 } } dps_results: { @@ -936,7 +936,7 @@ dps_results: { dps_results: { key: "TestAPL-SwitchInFrontOfTarget-Default" value: { - dps: 6512.76308 - tps: 5672.38337 + dps: 6565.16566 + tps: 5723.85102 } } diff --git a/sim/hunter/explosive_trap.go b/sim/hunter/explosive_trap.go index 0ec11f4762..9feb96a5d0 100644 --- a/sim/hunter/explosive_trap.go +++ b/sim/hunter/explosive_trap.go @@ -59,12 +59,28 @@ func (hunter *Hunter) registerExplosiveTrapSpell(timer *core.Timer) { }, ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { - for _, aoeTarget := range sim.Encounter.TargetUnits { - baseDamage := sim.Roll(523, 671) + 0.1*spell.RangedAttackPower(aoeTarget) - baseDamage *= sim.Encounter.AOECapMultiplier() - spell.CalcAndDealDamage(sim, aoeTarget, baseDamage, spell.OutcomeRangedHitAndCrit) + if sim.CurrentTime < 0 { + // If using this on prepull, the trap effect will go off when the fight starts + // instead of immediately. + core.StartDelayedAction(sim, core.DelayedActionOptions{ + DoAt: 0, + OnAction: func(sim *core.Simulation) { + for _, aoeTarget := range sim.Encounter.TargetUnits { + baseDamage := sim.Roll(523, 671) + 0.1*spell.RangedAttackPower(aoeTarget) + baseDamage *= sim.Encounter.AOECapMultiplier() + spell.CalcAndDealDamage(sim, aoeTarget, baseDamage, spell.OutcomeRangedHitAndCrit) + } + hunter.ExplosiveTrap.AOEDot().Apply(sim) + }, + }) + } else { + for _, aoeTarget := range sim.Encounter.TargetUnits { + baseDamage := sim.Roll(523, 671) + 0.1*spell.RangedAttackPower(aoeTarget) + baseDamage *= sim.Encounter.AOECapMultiplier() + spell.CalcAndDealDamage(sim, aoeTarget, baseDamage, spell.OutcomeRangedHitAndCrit) + } + hunter.ExplosiveTrap.AOEDot().Apply(sim) } - hunter.ExplosiveTrap.AOEDot().Apply(sim) }, }) @@ -75,6 +91,10 @@ func (hunter *Hunter) registerExplosiveTrapSpell(timer *core.Timer) { Flags: core.SpellFlagNoOnCastComplete | core.SpellFlagNoMetrics | core.SpellFlagNoLogs | core.SpellFlagAPL, ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { + if sim.CurrentTime < 0 { + hunter.mayMoveAt = sim.CurrentTime + } + // Assume we started running after the most recent ranged auto, so that time // can be subtracted from the run in. reachLocationAt := hunter.mayMoveAt + halfWeaveTime diff --git a/sim/hunter/pet_talents.go b/sim/hunter/pet_talents.go index d4dedb01b4..3a53c15cca 100644 --- a/sim/hunter/pet_talents.go +++ b/sim/hunter/pet_talents.go @@ -145,10 +145,7 @@ func (hp *HunterPet) applyFeedingFrenzy() { } func (hp *HunterPet) registerRoarOfRecoveryCD() { - if !hp.Talents().RoarOfRecovery { - return - } - + // This CD is enabled even if not talented, for prepull. See below. hunter := hp.hunterOwner actionID := core.ActionID{SpellID: 53517} manaMetrics := hunter.NewManaMetrics(actionID) @@ -163,7 +160,7 @@ func (hp *HunterPet) registerRoarOfRecoveryCD() { }, }, ExtraCastCondition: func(sim *core.Simulation, target *core.Unit) bool { - return hp.IsEnabled() && hunter.CurrentManaPercent() < 0.6 + return sim.CurrentTime < 0 || (hp.IsEnabled() && hunter.CurrentManaPercent() < 0.6) }, ApplyEffects: func(sim *core.Simulation, _ *core.Unit, _ *core.Spell) { @@ -177,6 +174,13 @@ func (hp *HunterPet) registerRoarOfRecoveryCD() { }, }) + // If not talented, still create the spell but don't make the MCD. This lets it be + // selected as a Prepull Action in the APL UI. + if !hp.Talents().RoarOfRecovery { + rorSpell.Flags |= core.SpellFlagAPL | core.SpellFlagMCD | core.SpellFlagPrepullOnly + return + } + hunter.AddMajorCooldown(core.MajorCooldown{ Spell: rorSpell, Type: core.CooldownTypeMana, @@ -260,10 +264,7 @@ func (hp *HunterPet) registerRabidCD() { } func (hp *HunterPet) registerCallOfTheWildCD() { - if !hp.Talents().CallOfTheWild { - return - } - + // This CD is enabled even if not talented, for prepull. See below. hunter := hp.hunterOwner actionID := core.ActionID{SpellID: 53434} @@ -302,7 +303,7 @@ func (hp *HunterPet) registerCallOfTheWildCD() { }, }, ExtraCastCondition: func(sim *core.Simulation, target *core.Unit) bool { - return hp.IsEnabled() + return sim.CurrentTime < 0 || hp.IsEnabled() }, ApplyEffects: func(sim *core.Simulation, _ *core.Unit, _ *core.Spell) { @@ -311,6 +312,13 @@ func (hp *HunterPet) registerCallOfTheWildCD() { }, }) + // If not talented, still create the spell but don't make the MCD. This lets it be + // selected as a Prepull Action in the APL UI. + if !hp.Talents().CallOfTheWild { + cotwSpell.Flags |= core.SpellFlagAPL | core.SpellFlagMCD | core.SpellFlagPrepullOnly + return + } + hunter.AddMajorCooldown(core.MajorCooldown{ Spell: cotwSpell, Type: core.CooldownTypeDPS, diff --git a/ui/core/components/dropdown_picker.ts b/ui/core/components/dropdown_picker.ts index aee5e86ae6..41edcd4e6e 100644 --- a/ui/core/components/dropdown_picker.ts +++ b/ui/core/components/dropdown_picker.ts @@ -9,6 +9,7 @@ export interface DropdownValueConfig { submenu?: Array, headerText?: string, tooltip?: string, + extraCssClasses?: Array, } export interface DropdownPickerConfig extends InputConfig { @@ -72,6 +73,9 @@ export class DropdownPicker extends Input { this.submenus = []; valueConfigs.forEach(valueConfig => { const itemElem = document.createElement('li'); + if (valueConfig.extraCssClasses) { + itemElem.classList.add(...valueConfig.extraCssClasses); + } if (valueConfig.headerText) { itemElem.classList.add('dropdown-picker-header'); diff --git a/ui/core/components/individual_sim_ui/apl_actions.ts b/ui/core/components/individual_sim_ui/apl_actions.ts index 20e8390a5a..05283c1644 100644 --- a/ui/core/components/individual_sim_ui/apl_actions.ts +++ b/ui/core/components/individual_sim_ui/apl_actions.ts @@ -46,16 +46,20 @@ export class APLActionPicker extends Input, APLAction> { player.rotationChangeEmitter.emit(eventID); }, }); - this.conditionPicker.rootElem.classList.add('apl-action-condition'); + this.conditionPicker.rootElem.classList.add('apl-action-condition', 'apl-priority-list-only'); this.actionDiv = document.createElement('div'); this.actionDiv.classList.add('apl-action-picker-action'); this.rootElem.appendChild(this.actionDiv); + const isPrepull = this.rootElem.closest('.apl-prepull-action-picker') != null; + const allActionTypes = Object.keys(actionTypeFactories) as Array>; this.typePicker = new TextDropdownPicker(this.actionDiv, player, { defaultLabel: 'Action', - values: allActionTypes.map(actionType => { + values: allActionTypes + .filter(actionType => actionTypeFactories[actionType].isPrepull == undefined || actionTypeFactories[actionType].isPrepull === isPrepull) + .map(actionType => { const factory = actionTypeFactories[actionType]; return { value: actionType, @@ -164,6 +168,7 @@ type ActionTypeConfig = { submenu?: Array, shortDescription: string, fullDescription?: string, + isPrepull?: boolean, newValue: () => T, factory: (parent: HTMLElement, player: Player, config: InputConfig, T>) => Input, T>, }; @@ -202,6 +207,7 @@ function inputBuilder(config: { submenu?: Array, shortDescription: string, fullDescription?: string, + isPrepull?: boolean, newValue: () => T, fields: Array>, }): ActionTypeConfig { @@ -210,6 +216,7 @@ function inputBuilder(config: { submenu: config.submenu, shortDescription: config.shortDescription, fullDescription: config.fullDescription, + isPrepull: config.isPrepull, newValue: config.newValue, factory: AplHelpers.aplInputBuilder(config.newValue, config.fields), }; @@ -227,6 +234,7 @@ export const actionTypeFactories: Record, ActionTypeC ['multidot']: inputBuilder({ label: 'Multi Dot', shortDescription: 'Keeps a DoT active on multiple targets by casting the specified spell.', + isPrepull: false, newValue: () => APLActionMultidot.create({ maxDots: 3, maxOverlap: { @@ -258,6 +266,7 @@ export const actionTypeFactories: Record, ActionTypeC

Once one of the sub-actions has been performed, the next sub-action will not necessarily be immediately executed next. The system will restart at the beginning of the whole actions list (not the sequence). If the sequence is executed again, it will perform the next sub-action.

When all actions have been performed, the sequence does NOT automatically reset; instead, it will be skipped from now on. Use the Reset Sequence action to reset it, if desired.

`, + isPrepull: false, newValue: APLActionSequence.create, fields: [ AplHelpers.stringFieldConfig('name'), @@ -271,6 +280,7 @@ export const actionTypeFactories: Record, ActionTypeC fullDescription: `

Use the name field to refer to the sequence to be reset. The desired sequence must have the same (non-empty) value for its name.

`, + isPrepull: false, newValue: APLActionResetSequence.create, fields: [ AplHelpers.stringFieldConfig('sequenceName'), @@ -283,6 +293,7 @@ export const actionTypeFactories: Record, ActionTypeC fullDescription: `

Strict Sequences do not begin unless ALL sub-actions are ready.

`, + isPrepull: false, newValue: APLActionStrictSequence.create, fields: [ actionListFieldConfig('actions'), @@ -298,6 +309,7 @@ export const actionTypeFactories: Record, ActionTypeC
  • Cooldowns are usually cast immediately upon becoming ready, but there are some basic smart checks in place, e.g. don't use Mana CDs when near full mana.
  • `, + isPrepull: false, newValue: APLActionAutocastOtherCooldowns.create, fields: [], }), @@ -305,6 +317,7 @@ export const actionTypeFactories: Record, ActionTypeC label: 'Wait', submenu: ['Misc'], shortDescription: 'Pauses the GCD for a specified amount of time.', + isPrepull: false, newValue: () => APLActionWait.create({ duration: { value: { diff --git a/ui/core/components/individual_sim_ui/apl_helpers.ts b/ui/core/components/individual_sim_ui/apl_helpers.ts index f7b8dd5bec..199a48389d 100644 --- a/ui/core/components/individual_sim_ui/apl_helpers.ts +++ b/ui/core/components/individual_sim_ui/apl_helpers.ts @@ -1,3 +1,4 @@ +import { OtherAction } from '../../proto/common.js'; import { ActionId } from '../../proto_utils/action_id.js'; import { Player } from '../../player.js'; import { EventID, TypedEvent } from '../../typed_event.js'; @@ -42,6 +43,10 @@ const actionIdSets: Record spell.data.isMajorCooldown ? 'cooldowns' : 'spells'); + const placeholders: Array = [ + ActionId.fromOtherId(OtherAction.OtherActionPotion), + ]; + return [ [{ value: ActionId.fromEmpty(), @@ -50,6 +55,7 @@ const actionIdSets: Record { return { value: actionId.id, + extraCssClasses: (actionId.data.prepullOnly ? ['apl-prepull-actions-only'] : []), }; }), [{ @@ -59,6 +65,17 @@ const actionIdSets: Record { return { value: actionId.id, + extraCssClasses: (actionId.data.prepullOnly ? ['apl-prepull-actions-only'] : []), + }; + }), + [{ + value: ActionId.fromEmpty(), + headerText: 'Placeholders', + }], + placeholders.map(actionId => { + return { + value: actionId, + tooltip: 'The Prepull Potion if CurrentTime < 0, or the Combat Potion if combat has started.', }; }), ].flat(); diff --git a/ui/core/components/individual_sim_ui/apl_rotation_picker.ts b/ui/core/components/individual_sim_ui/apl_rotation_picker.ts index ba957a3fec..d21e3e5338 100644 --- a/ui/core/components/individual_sim_ui/apl_rotation_picker.ts +++ b/ui/core/components/individual_sim_ui/apl_rotation_picker.ts @@ -3,10 +3,11 @@ import { Tooltip } from 'bootstrap'; import { EventID, TypedEvent } from '../../typed_event.js'; import { Player } from '../../player.js'; import { ListItemPickerConfig, ListPicker } from '../list_picker.js'; +import { AdaptiveStringPicker } from '../string_picker.js'; import { APLListItem, APLAction, - APLRotation, + APLPrepullAction, } from '../../proto/apl.js'; import { Component } from '../component.js'; @@ -20,6 +21,26 @@ export class APLRotationPicker extends Component { constructor(parent: HTMLElement, simUI: SimUI, modPlayer: Player) { super(parent, 'apl-rotation-picker-root'); + new ListPicker, APLPrepullAction>(this.rootElem, modPlayer, { + extraCssClasses: ['apl-prepull-action-picker'], + title: 'Prepull Actions', + titleTooltip: 'Actions to perform before the pull.', + itemLabel: 'Prepull Action', + changedEvent: (player: Player) => player.rotationChangeEmitter, + getValue: (player: Player) => player.aplRotation.prepullActions, + setValue: (eventID: EventID, player: Player, newValue: Array) => { + player.aplRotation.prepullActions = newValue; + player.rotationChangeEmitter.emit(eventID); + }, + newItem: () => APLPrepullAction.create({ + action: {}, + doAt: '-1s', + }), + copyItem: (oldItem: APLPrepullAction) => APLPrepullAction.clone(oldItem), + newItemPicker: (parent: HTMLElement, listPicker: ListPicker, APLPrepullAction>, index: number, config: ListItemPickerConfig, APLPrepullAction>) => new APLPrepullActionPicker(parent, modPlayer, config, index), + inlineMenuBar: true, + }); + new ListPicker, APLListItem>(this.rootElem, modPlayer, { extraCssClasses: ['apl-list-item-picker'], title: 'Priority List', @@ -43,37 +64,26 @@ export class APLRotationPicker extends Component { } } -class APLListItemPicker extends Input, APLListItem> { +class APLPrepullActionPicker extends Input, APLPrepullAction> { private readonly player: Player; - private readonly index: number; private readonly hidePicker: Input, boolean>; + private readonly doAtPicker: Input, string>; private readonly actionPicker: APLActionPicker; - private readonly warningsElem: HTMLElement; - private readonly warningsTooltip: Tooltip; - - private getItem(): APLListItem { - return this.getSourceValue() || APLListItem.create({ + private getItem(): APLPrepullAction { + return this.getSourceValue() || APLPrepullAction.create({ action: {}, }); } - constructor(parent: HTMLElement, player: Player, config: ListItemPickerConfig, APLListItem>, index: number) { + constructor(parent: HTMLElement, player: Player, config: ListItemPickerConfig, APLPrepullAction>, index: number) { config.enableWhen = () => !this.getItem().hide; super(parent, 'apl-list-item-picker-root', player, config); this.player = player; - this.index = index; const itemHeaderElem = ListPicker.getItemHeaderElem(this); - - this.warningsElem = ListPicker.makeActionElem('apl-warnings', 'Warnings', 'fa-exclamation-triangle'); - this.warningsElem.classList.add('warnings', 'link-warning'); - this.warningsElem.setAttribute('data-bs-html', 'true'); - this.warningsTooltip = Tooltip.getOrCreateInstance(this.warningsElem, { - customClass: 'dropdown-tooltip', - }); - itemHeaderElem.appendChild(this.warningsElem); + makeListItemWarnings(itemHeaderElem, player, player => player.getCurrentStats().rotationStats?.prepullActions[index]?.warnings || []); this.hidePicker = new HidePicker(itemHeaderElem, player, { changedEvent: () => this.player.rotationChangeEmitter, @@ -84,6 +94,19 @@ class APLListItemPicker extends Input, APLListItem> { }, }); + this.doAtPicker = new AdaptiveStringPicker(this.rootElem, this.player, { + label: 'Do At', + labelTooltip: 'Time before pull to do the action. Should be negative, and formatted like, \'-1s\' or \'-2500ms\'.', + extraCssClasses: ['apl-prepull-actions-doat'], + changedEvent: () => this.player.rotationChangeEmitter, + getValue: () => this.getItem().doAt, + setValue: (eventID: EventID, player: Player, newValue: string) => { + this.getItem().doAt = newValue; + this.player.rotationChangeEmitter.emit(eventID); + }, + inline: true, + }); + this.actionPicker = new APLActionPicker(this.rootElem, this.player, { changedEvent: () => this.player.rotationChangeEmitter, getValue: () => this.getItem().action!, @@ -93,26 +116,69 @@ class APLListItemPicker extends Input, APLListItem> { }, }); this.init(); + } - this.updateWarnings(); - player.currentStatsEmitter.on(() => this.updateWarnings()); + getInputElem(): HTMLElement | null { + return this.rootElem; } - private async updateWarnings() { - this.warningsTooltip.setContent({'.tooltip-inner': ''}); - const warnings = this.player.getCurrentStats().rotationStats?.priorityList[this.index]?.warnings || []; - if (warnings.length == 0) { - this.warningsElem.style.visibility = 'hidden'; - } else { - this.warningsElem.style.visibility = 'visible'; - const formattedWarnings = await Promise.all(warnings.map(w => ActionId.replaceAllInString(w))); - this.warningsTooltip.setContent({'.tooltip-inner': ` -

    This action has warnings, and might not behave as expected.

    -
      - ${formattedWarnings.map(w => `
    • ${w}
    • `).join('')} -
    - `}); + getInputValue(): APLPrepullAction { + const item = APLPrepullAction.create({ + hide: this.hidePicker.getInputValue(), + doAt: this.doAtPicker.getInputValue(), + action: this.actionPicker.getInputValue(), + }); + return item; + } + + setInputValue(newValue: APLPrepullAction) { + if (!newValue) { + return; } + this.hidePicker.setInputValue(newValue.hide); + this.doAtPicker.setInputValue(newValue.doAt); + this.actionPicker.setInputValue(newValue.action || APLAction.create()); + } +} + +class APLListItemPicker extends Input, APLListItem> { + private readonly player: Player; + + private readonly hidePicker: Input, boolean>; + private readonly actionPicker: APLActionPicker; + + private getItem(): APLListItem { + return this.getSourceValue() || APLListItem.create({ + action: {}, + }); + } + + constructor(parent: HTMLElement, player: Player, config: ListItemPickerConfig, APLListItem>, index: number) { + config.enableWhen = () => !this.getItem().hide; + super(parent, 'apl-list-item-picker-root', player, config); + this.player = player; + + const itemHeaderElem = ListPicker.getItemHeaderElem(this); + makeListItemWarnings(itemHeaderElem, player, player => player.getCurrentStats().rotationStats?.priorityList[index]?.warnings || []); + + this.hidePicker = new HidePicker(itemHeaderElem, player, { + changedEvent: () => this.player.rotationChangeEmitter, + getValue: () => this.getItem().hide, + setValue: (eventID: EventID, player: Player, newValue: boolean) => { + this.getItem().hide = newValue; + this.player.rotationChangeEmitter.emit(eventID); + }, + }); + + this.actionPicker = new APLActionPicker(this.rootElem, this.player, { + changedEvent: () => this.player.rotationChangeEmitter, + getValue: () => this.getItem().action!, + setValue: (eventID: EventID, player: Player, newValue: APLAction) => { + this.getItem().action = newValue; + this.player.rotationChangeEmitter.emit(eventID); + }, + }); + this.init(); } getInputElem(): HTMLElement | null { @@ -136,6 +202,35 @@ class APLListItemPicker extends Input, APLListItem> { } } +function makeListItemWarnings(itemHeaderElem: HTMLElement, player: Player, getWarnings: (player: Player) => Array) { + const warningsElem = ListPicker.makeActionElem('apl-warnings', 'Warnings', 'fa-exclamation-triangle'); + warningsElem.classList.add('warnings', 'link-warning'); + warningsElem.setAttribute('data-bs-html', 'true'); + const warningsTooltip = Tooltip.getOrCreateInstance(warningsElem, { + customClass: 'dropdown-tooltip', + }); + itemHeaderElem.appendChild(warningsElem); + + const updateWarnings = async () => { + warningsTooltip.setContent({'.tooltip-inner': ''}); + const warnings = getWarnings(player); + if (warnings.length == 0) { + warningsElem.style.visibility = 'hidden'; + } else { + warningsElem.style.visibility = 'visible'; + const formattedWarnings = await Promise.all(warnings.map(w => ActionId.replaceAllInString(w))); + warningsTooltip.setContent({'.tooltip-inner': ` +

    This action has warnings, and might not behave as expected.

    +
      + ${formattedWarnings.map(w => `
    • ${w}
    • `).join('')} +
    + `}); + } + }; + updateWarnings(); + player.currentStatsEmitter.on(updateWarnings); +} + class HidePicker extends Input, boolean> { private readonly inputElem: HTMLElement; private readonly iconElem: HTMLElement; diff --git a/ui/core/proto_utils/action_id.ts b/ui/core/proto_utils/action_id.ts index 6133e9da83..7db5acb6cf 100644 --- a/ui/core/proto_utils/action_id.ts +++ b/ui/core/proto_utils/action_id.ts @@ -105,6 +105,10 @@ export class ActionId { baseName = 'Death Rune Gain'; iconUrl = 'https://wow.zamimg.com/images/wow/icons/medium/spell_deathknight_empowerruneblade.jpg'; break; + case OtherAction.OtherActionPotion: + baseName = 'Potion'; + iconUrl = 'https://wow.zamimg.com/images/wow/icons/large/inv_alchemy_elixir_04.jpg'; + break; } this.baseName = baseName; this.name = name || baseName; diff --git a/ui/hunter/presets.ts b/ui/hunter/presets.ts index 62a90f8aa6..f366ecad8f 100644 --- a/ui/hunter/presets.ts +++ b/ui/hunter/presets.ts @@ -115,6 +115,12 @@ export const ROTATION_PRESET_DEFAULT = { })), rotation: APLRotation.fromJsonString(`{ "enabled": true, + "prepullActions": [ + { + "action":{"castSpell":{"spellId":{"otherId":"OtherActionPotion"}}}, + "doAt":"-1s" + } + ], "priorityList": [ {"action": { "condition": {"cmp": {"op": "OpGt", "lhs": {"currentTime": {}}, "rhs": { "const": {"val": "10s"}}}}, 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 95a70938dc..20691db37a 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 @@ -3,7 +3,7 @@ @import "./apl_helpers"; .apl-rotation-picker-root { - .apl-list-item-picker { + .apl-list-item-picker, .apl-prepull-action-picker { flex-wrap: wrap; align-items: flex-start !important; @@ -16,20 +16,33 @@ .list-picker-new-button { width: 100%; } - } - .apl-list-item-picker > * > .list-picker-item-container { - background-color: rgba(21, 23, 30, 0.8); - padding: 5px; + >* > .list-picker-item-container { + background-color: rgba(21, 23, 30, 0.8); + padding: 5px; + + >.list-picker-item { + flex-grow: 1; + } + } } - .apl-list-item-picker > * > * > .list-picker-item { - flex-grow: 1; + .apl-list-item-picker .apl-prepull-actions-only, .apl-prepull-action-picker .apl-priority-list-only { + display: none; } .adaptive-string-picker-root > input { text-align: center; } + + .apl-prepull-actions-doat { + width: auto; + margin: 3px; + + .form-label { + margin: 0 5px 0 0; + } + } } .apl-list-item-picker-root {