diff --git a/proto/apl.proto b/proto/apl.proto index cd7bc6dd33..7835173c8e 100644 --- a/proto/apl.proto +++ b/proto/apl.proto @@ -41,7 +41,7 @@ message APLListItem { APLAction action = 3; // The action to be performed. } -// NextIndex: 19 +// NextIndex: 21 message APLAction { APLValue condition = 1; // If set, action will only execute if value is true or != 0. @@ -73,6 +73,9 @@ message APLAction { // Class or Spec-specific actions APLActionCatOptimalRotationAction cat_optimal_rotation_action = 19; + + // Internal use only, not exposed in UI. + APLActionCustomRotation custom_rotation = 20; } } @@ -254,6 +257,9 @@ message APLActionMove { APLValue range_from_target = 1; } +message APLActionCustomRotation { +} + /////////////////////////////////////////////////////////////////////////// // VALUES /////////////////////////////////////////////////////////////////////////// diff --git a/sim/_shaman/fire_elemental_pet.go b/sim/_shaman/fire_elemental_pet.go index 6db1555453..cbe1e4fb23 100644 --- a/sim/_shaman/fire_elemental_pet.go +++ b/sim/_shaman/fire_elemental_pet.go @@ -80,7 +80,7 @@ func (fireElemental *FireElemental) Reset(_ *core.Simulation) { } -func (fireElemental *FireElemental) OnGCDReady(sim *core.Simulation) { +func (fireElemental *FireElemental) ExecuteCustomRotation(sim *core.Simulation) { /* TODO this is a little dirty, can probably clean this up, the rotation might go through some more overhauls, the random AI is hard to emulate. @@ -122,10 +122,6 @@ func (fireElemental *FireElemental) TryCast(sim *core.Simulation, target *core.U return false } - if !spell.IsReady(sim) { - return false - } - if !spell.Cast(sim, target) { return false } diff --git a/sim/_shaman/spirit_wolves.go b/sim/_shaman/spirit_wolves.go index ab9de1c834..4566893e03 100644 --- a/sim/_shaman/spirit_wolves.go +++ b/sim/_shaman/spirit_wolves.go @@ -91,8 +91,7 @@ func (spiritWolf *SpiritWolf) Initialize() { // Nothing } -func (spiritWolf *SpiritWolf) OnGCDReady(_ *core.Simulation) { - spiritWolf.DoNothing() +func (spiritWolf *SpiritWolf) ExecuteCustomRotation(_ *core.Simulation) { } func (spiritWolf *SpiritWolf) Reset(sim *core.Simulation) { diff --git a/sim/core/agent.go b/sim/core/agent.go index 905232b992..d357ec80e6 100644 --- a/sim/core/agent.go +++ b/sim/core/agent.go @@ -39,6 +39,10 @@ type Agent interface { // Should return nil when the config doesn't match any custom behaviors. NewAPLValue(rot *APLRotation, config *proto.APLValue) APLValue NewAPLAction(rot *APLRotation, config *proto.APLAction) APLActionImpl + + // Implements custom rotation behavior. Usually for pets and targets but can be used + // for players too. + ExecuteCustomRotation(sim *Simulation) } type ActionID struct { diff --git a/sim/core/apl.go b/sim/core/apl.go index c3549ae27c..aab49f2248 100644 --- a/sim/core/apl.go +++ b/sim/core/apl.go @@ -53,6 +53,19 @@ func (rot *APLRotation) doAndRecordWarnings(warningsList *[]string, isPrepull bo rot.parsingPrepull = false } +func (unit *Unit) newCustomRotation() *APLRotation { + return unit.newAPLRotation(&proto.APLRotation{ + Type: proto.APLRotation_TypeAPL, + PriorityList: []*proto.APLListItem{ + { + Action: &proto.APLAction{ + Action: &proto.APLAction_CustomRotation{}, + }, + }, + }, + }) +} + func (unit *Unit) newAPLRotation(config *proto.APLRotation) *APLRotation { if config == nil { return nil diff --git a/sim/core/apl_action.go b/sim/core/apl_action.go index fe33aaff4e..5a06cf95a5 100644 --- a/sim/core/apl_action.go +++ b/sim/core/apl_action.go @@ -132,7 +132,7 @@ func (rot *APLRotation) newAPLActionImpl(config *proto.APLAction) APLActionImpl return nil } - customAction := rot.unit.Env.Raid.GetPlayerFromUnit(rot.unit).NewAPLAction(rot, config) + customAction := rot.unit.Env.GetAgentFromUnit(rot.unit).NewAPLAction(rot, config) if customAction != nil { return customAction } @@ -179,6 +179,8 @@ func (rot *APLRotation) newAPLActionImpl(config *proto.APLAction) APLActionImpl return rot.newActionItemSwap(config.GetItemSwap()) case *proto.APLAction_Move: return rot.newActionMove(config.GetMove()) + case *proto.APLAction_CustomRotation: + return rot.newActionCustomRotation(config.GetCustomRotation()) default: return nil } diff --git a/sim/core/apl_actions_misc.go b/sim/core/apl_actions_misc.go index ec630e4a29..e0da996630 100644 --- a/sim/core/apl_actions_misc.go +++ b/sim/core/apl_actions_misc.go @@ -2,6 +2,7 @@ package core import ( "fmt" + "time" "github.com/wowsims/sod/sim/core/proto" ) @@ -188,3 +189,37 @@ func (action *APLActionMove) Execute(sim *Simulation) { func (action *APLActionMove) String() string { return fmt.Sprintf("Move(%s)", action.moveRange) } + +type APLActionCustomRotation struct { + defaultAPLActionImpl + unit *Unit + agent Agent + + lastExecutedAt time.Duration +} + +func (rot *APLRotation) newActionCustomRotation(config *proto.APLActionCustomRotation) APLActionImpl { + agent := rot.unit.Env.GetAgentFromUnit(rot.unit) + if agent == nil { + panic("Agent not found for custom rotation") + } + + return &APLActionCustomRotation{ + unit: rot.unit, + agent: agent, + } +} +func (action *APLActionCustomRotation) Reset(sim *Simulation) { + action.lastExecutedAt = -1 +} +func (action *APLActionCustomRotation) IsReady(sim *Simulation) bool { + // Prevent infinite loops by only allowing this action to be performed once at each timestamp. + return action.lastExecutedAt != sim.CurrentTime +} +func (action *APLActionCustomRotation) Execute(sim *Simulation) { + action.lastExecutedAt = sim.CurrentTime + action.agent.ExecuteCustomRotation(sim) +} +func (action *APLActionCustomRotation) String() string { + return "Custom Rotation()" +} diff --git a/sim/core/apl_value.go b/sim/core/apl_value.go index bb10ce5cab..9e50a5efdd 100644 --- a/sim/core/apl_value.go +++ b/sim/core/apl_value.go @@ -56,7 +56,7 @@ func (rot *APLRotation) newAPLValue(config *proto.APLValue) APLValue { return nil } - customValue := rot.unit.Env.Raid.GetPlayerFromUnit(rot.unit).NewAPLValue(rot, config) + customValue := rot.unit.Env.GetAgentFromUnit(rot.unit).NewAPLValue(rot, config) if customValue != nil { return customValue } diff --git a/sim/core/cast.go b/sim/core/cast.go index ebd74c8dbe..0fde0cb777 100644 --- a/sim/core/cast.go +++ b/sim/core/cast.go @@ -62,15 +62,13 @@ func (cast *Cast) EffectiveTime() time.Duration { type CastFunc func(*Simulation, *Unit) type CastSuccessFunc func(*Simulation, *Unit) bool -func (spell *Spell) castFailureHelper(sim *Simulation, gracefulFailure bool, message string, vals ...any) bool { +func (spell *Spell) castFailureHelper(sim *Simulation, message string, vals ...any) bool { if sim.CurrentTime < 0 && spell.Unit.Rotation != nil { spell.Unit.Rotation.ValidationWarning(fmt.Sprintf(spell.ActionID.String()+" failed to cast: "+message, vals...)) - } else if gracefulFailure { + } else { if sim.Log != nil && !spell.Flags.Matches(SpellFlagNoLogs) { spell.Unit.Log(sim, fmt.Sprintf(spell.ActionID.String()+" failed to cast: "+message, vals...)) } - } else { - panic(fmt.Sprintf(spell.ActionID.String()+" failed to cast: "+message, vals...)) } return false } @@ -151,13 +149,13 @@ func (spell *Spell) makeCastFunc(config CastConfig) CastSuccessFunc { if spell.ExtraCastCondition != nil { if !spell.ExtraCastCondition(sim, target) { - return spell.castFailureHelper(sim, true, "extra spell condition") + return spell.castFailureHelper(sim, "extra spell condition") } } if spell.Cost != nil { if !spell.Cost.MeetsRequirement(sim, spell) { - return spell.castFailureHelper(sim, true, spell.Cost.CostFailureReason(sim, spell)) + return spell.castFailureHelper(sim, spell.Cost.CostFailureReason(sim, spell)) } } @@ -169,7 +167,7 @@ func (spell *Spell) makeCastFunc(config CastConfig) CastSuccessFunc { if config.CD.Timer != nil { // By panicking if spell is on CD, we force each sim to properly check for their own CDs. if !spell.CD.IsReady(sim) { - return spell.castFailureHelper(sim, false, "still on cooldown for %s, curTime = %s", spell.CD.TimeToReady(sim), sim.CurrentTime) + return spell.castFailureHelper(sim, "still on cooldown for %s, curTime = %s", spell.CD.TimeToReady(sim), sim.CurrentTime) } spell.CD.Set(sim.CurrentTime + spell.CurCast.CastTime + spell.CD.Duration) } @@ -177,18 +175,18 @@ func (spell *Spell) makeCastFunc(config CastConfig) CastSuccessFunc { if config.SharedCD.Timer != nil { // By panicking if spell is on CD, we force each sim to properly check for their own CDs. if !spell.SharedCD.IsReady(sim) { - return spell.castFailureHelper(sim, false, "still on shared cooldown for %s, curTime = %s", spell.SharedCD.TimeToReady(sim), sim.CurrentTime) + return spell.castFailureHelper(sim, "still on shared cooldown for %s, curTime = %s", spell.SharedCD.TimeToReady(sim), sim.CurrentTime) } spell.SharedCD.Set(sim.CurrentTime + spell.CurCast.CastTime + spell.SharedCD.Duration) } // By panicking if spell is on CD, we force each sim to properly check for their own CDs. if spell.CurCast.GCD != 0 && !spell.Unit.GCD.IsReady(sim) { - return spell.castFailureHelper(sim, false, "GCD on cooldown for %s, curTime = %s", spell.Unit.GCD.TimeToReady(sim), sim.CurrentTime) + return spell.castFailureHelper(sim, "GCD on cooldown for %s, curTime = %s", spell.Unit.GCD.TimeToReady(sim), sim.CurrentTime) } if hc := spell.Unit.Hardcast; hc.Expires > sim.CurrentTime { - return spell.castFailureHelper(sim, false, "casting/channeling %v for %s, curTime = %s", hc.ActionID, hc.Expires-sim.CurrentTime, sim.CurrentTime) + return spell.castFailureHelper(sim, "casting/channeling %v for %s, curTime = %s", hc.ActionID, hc.Expires-sim.CurrentTime, sim.CurrentTime) } if effectiveTime := spell.CurCast.EffectiveTime(); effectiveTime != 0 { @@ -200,7 +198,7 @@ func (spell *Spell) makeCastFunc(config CastConfig) CastSuccessFunc { } if (spell.CurCast.CastTime > 0) && spell.Unit.Moving { - return spell.castFailureHelper(sim, false, "casting/channeling while moving not allowed!") + return spell.castFailureHelper(sim, "casting/channeling while moving not allowed!") } // TODO: Fix with removal of ChannelTime? @@ -274,14 +272,14 @@ func (spell *Spell) makeCastFuncSimple() CastSuccessFunc { return func(sim *Simulation, target *Unit) bool { if spell.ExtraCastCondition != nil { if !spell.ExtraCastCondition(sim, target) { - return spell.castFailureHelper(sim, true, "extra spell condition") + return spell.castFailureHelper(sim, "extra spell condition") } } if spell.CD.Timer != nil { // By panicking if spell is on CD, we force each sim to properly check for their own CDs. if !spell.CD.IsReady(sim) { - return spell.castFailureHelper(sim, false, "still on cooldown for %s, curTime = %s", spell.CD.TimeToReady(sim), sim.CurrentTime) + return spell.castFailureHelper(sim, "still on cooldown for %s, curTime = %s", spell.CD.TimeToReady(sim), sim.CurrentTime) } spell.CD.Set(sim.CurrentTime + spell.CD.Duration) @@ -290,7 +288,7 @@ func (spell *Spell) makeCastFuncSimple() CastSuccessFunc { if spell.SharedCD.Timer != nil { // By panicking if spell is on CD, we force each sim to properly check for their own CDs. if !spell.SharedCD.IsReady(sim) { - return spell.castFailureHelper(sim, false, "still on shared cooldown for %s, curTime = %s", spell.SharedCD.TimeToReady(sim), sim.CurrentTime) + return spell.castFailureHelper(sim, "still on shared cooldown for %s, curTime = %s", spell.SharedCD.TimeToReady(sim), sim.CurrentTime) } spell.SharedCD.Set(sim.CurrentTime + spell.SharedCD.Duration) diff --git a/sim/core/environment.go b/sim/core/environment.go index 1c200f2a3a..9687b93096 100644 --- a/sim/core/environment.go +++ b/sim/core/environment.go @@ -148,6 +148,9 @@ func (env *Environment) finalize(raidProto *proto.Raid, _ *proto.Encounter, raid for _, target := range env.Encounter.Targets { target.finalize() + if target.AI != nil { + target.Rotation = target.newCustomRotation() + } } for _, party := range env.Raid.Parties { @@ -156,6 +159,7 @@ func (env *Environment) finalize(raidProto *proto.Raid, _ *proto.Encounter, raid character.Finalize() for _, pet := range character.Pets { pet.Finalize() + pet.Rotation = pet.newCustomRotation() } } } @@ -264,6 +268,20 @@ func (env *Environment) NextTarget(target *Unit) *Target { func (env *Environment) NextTargetUnit(target *Unit) *Unit { return &env.NextTarget(target).Unit } +func (env *Environment) GetAgentFromUnit(unit *Unit) Agent { + raidAgent := env.Raid.GetPlayerFromUnit(unit) + if raidAgent != nil { + return raidAgent + } + + for _, target := range env.Encounter.Targets { + if unit == &target.Unit { + return target + } + } + + return nil +} func (env *Environment) GetUnit(ref *proto.UnitReference, contextUnit *Unit) *Unit { if ref == nil { diff --git a/sim/core/pet.go b/sim/core/pet.go index 82c44348d8..a3f1f02915 100644 --- a/sim/core/pet.go +++ b/sim/core/pet.go @@ -270,3 +270,4 @@ func (pet *Pet) AddRaidBuffs(_ *proto.RaidBuffs) {} func (pet *Pet) AddPartyBuffs(_ *proto.PartyBuffs) {} func (pet *Pet) ApplyTalents() {} func (pet *Pet) ApplyRunes() {} +func (pet *Pet) OnGCDReady(_ *Simulation) {} diff --git a/sim/core/target_ai.go b/sim/core/target_ai.go index 8a081cd11b..9b8ee84270 100644 --- a/sim/core/target_ai.go +++ b/sim/core/target_ai.go @@ -9,7 +9,7 @@ import ( type TargetAI interface { Initialize(*Target, *proto.Target) Reset(*Simulation) - DoAction(*Simulation) + ExecuteCustomRotation(*Simulation) } func (target *Target) initialize(config *proto.Target) { diff --git a/sim/core/target_dummy.go b/sim/core/target_dummy.go index ff6bc1e0c7..4a7dca1409 100644 --- a/sim/core/target_dummy.go +++ b/sim/core/target_dummy.go @@ -49,3 +49,4 @@ func (td *TargetDummy) ApplyTalents() {} func (td *TargetDummy) ApplyRunes() {} func (td *TargetDummy) Initialize() {} func (td *TargetDummy) Reset(sim *Simulation) {} +func (td *TargetDummy) ExecuteCustomRotation(sim *Simulation) {} diff --git a/sim/core/unit.go b/sim/core/unit.go index cda20ee5cd..4a6dc9f126 100644 --- a/sim/core/unit.go +++ b/sim/core/unit.go @@ -604,3 +604,7 @@ func (unit *Unit) GetMetadata() *proto.UnitMetadata { return metadata } + +func (unit *Unit) ExecuteCustomRotation(sim *Simulation) { + panic("Unimplemented ExecuteCustomRotation") +} diff --git a/sim/encounters/default_ai.go b/sim/encounters/default_ai.go index 33d85263b0..91f88495fd 100644 --- a/sim/encounters/default_ai.go +++ b/sim/encounters/default_ai.go @@ -52,7 +52,7 @@ func (ai *DefaultAI) Reset(sim *core.Simulation) { } -func (ai *DefaultAI) DoAction(sim *core.Simulation) { +func (ai *DefaultAI) ExecuteCustomRotation(sim *core.Simulation) { for _, ability := range ai.Abilities { if sim.CurrentTime < ability.InitialCD { continue diff --git a/sim/encounters/naxxramas/kelthuzad25_ai.go b/sim/encounters/naxxramas/kelthuzad25_ai.go index 46b43dcbeb..24df100827 100644 --- a/sim/encounters/naxxramas/kelthuzad25_ai.go +++ b/sim/encounters/naxxramas/kelthuzad25_ai.go @@ -56,5 +56,5 @@ func (ai *KelThuzad25AI) Initialize(target *core.Target, config *proto.Target) { func (ai *KelThuzad25AI) Reset(*core.Simulation) { } -func (ai *KelThuzad25AI) DoAction(sim *core.Simulation) { +func (ai *KelThuzad25AI) ExecuteCustomRotation(sim *core.Simulation) { } diff --git a/sim/encounters/naxxramas/loatheb25_ai.go b/sim/encounters/naxxramas/loatheb25_ai.go index b58e5d08ae..2a30d6fd99 100644 --- a/sim/encounters/naxxramas/loatheb25_ai.go +++ b/sim/encounters/naxxramas/loatheb25_ai.go @@ -56,5 +56,5 @@ func (ai *Loatheb25AI) Initialize(target *core.Target, config *proto.Target) { func (ai *Loatheb25AI) Reset(*core.Simulation) { } -func (ai *Loatheb25AI) DoAction(sim *core.Simulation) { +func (ai *Loatheb25AI) ExecuteCustomRotation(sim *core.Simulation) { } diff --git a/sim/encounters/naxxramas/patchwerk10_ai.go b/sim/encounters/naxxramas/patchwerk10_ai.go index 0a92ccb0b7..e50eeaf780 100644 --- a/sim/encounters/naxxramas/patchwerk10_ai.go +++ b/sim/encounters/naxxramas/patchwerk10_ai.go @@ -122,7 +122,7 @@ func (ai *Patchwerk10AI) registerFrenzySpell(target *core.Target) { }) } -func (ai *Patchwerk10AI) DoAction(sim *core.Simulation) { +func (ai *Patchwerk10AI) ExecuteCustomRotation(sim *core.Simulation) { if ai.Target.CurrentTarget == nil { return } diff --git a/sim/encounters/naxxramas/patchwerk25_ai.go b/sim/encounters/naxxramas/patchwerk25_ai.go index 010c871ba0..0b47b76b3f 100644 --- a/sim/encounters/naxxramas/patchwerk25_ai.go +++ b/sim/encounters/naxxramas/patchwerk25_ai.go @@ -123,7 +123,7 @@ func (ai *Patchwerk25AI) registerFrenzySpell(target *core.Target) { }) } -func (ai *Patchwerk25AI) DoAction(sim *core.Simulation) { +func (ai *Patchwerk25AI) ExecuteCustomRotation(sim *core.Simulation) { if ai.Target.CurrentTarget == nil { return } diff --git a/sim/encounters/naxxramas/thaddius25_ai.go b/sim/encounters/naxxramas/thaddius25_ai.go index 6795e4edf5..dae4e773c5 100644 --- a/sim/encounters/naxxramas/thaddius25_ai.go +++ b/sim/encounters/naxxramas/thaddius25_ai.go @@ -56,5 +56,5 @@ func (ai *Thaddius25AI) Initialize(target *core.Target, config *proto.Target) { func (ai *Thaddius25AI) Reset(*core.Simulation) { } -func (ai *Thaddius25AI) DoAction(sim *core.Simulation) { +func (ai *Thaddius25AI) ExecuteCustomRotation(sim *core.Simulation) { } diff --git a/sim/hunter/pet.go b/sim/hunter/pet.go index ad03365b67..d650688a68 100644 --- a/sim/hunter/pet.go +++ b/sim/hunter/pet.go @@ -106,7 +106,7 @@ func (hp *HunterPet) Reset(_ *core.Simulation) { hp.uptimePercent = min(1, max(0, hp.hunterOwner.Options.PetUptime)) } -func (hp *HunterPet) OnGCDReady(sim *core.Simulation) { +func (hp *HunterPet) ExecuteCustomRotation(sim *core.Simulation) { percentRemaining := sim.GetRemainingDurationPercent() if percentRemaining < 1.0-hp.uptimePercent { // once fight is % completed, disable pet. hp.Disable(sim) @@ -120,6 +120,7 @@ func (hp *HunterPet) OnGCDReady(sim *core.Simulation) { } target := hp.CurrentTarget + if hp.focusDump == nil { hp.specialAbility.Cast(sim, target) return diff --git a/sim/warlock/dps/TestAffliction.results b/sim/warlock/dps/TestAffliction.results index 9bc9aa069d..65472fb748 100644 --- a/sim/warlock/dps/TestAffliction.results +++ b/sim/warlock/dps/TestAffliction.results @@ -50,61 +50,91 @@ character_stats_results: { } dps_results: { key: "TestAffliction-AllItems-TwilightInvoker'sVestments" - value: {} + value: { + dps: 5.18892 + } } dps_results: { key: "TestAffliction-Average-Default" - value: {} + value: { + dps: 5.22068 + } } dps_results: { key: "TestAffliction-Settings-Orc-25-destruction-AffItemSwap--FullBuffs-LongMultiTarget" - value: {} + value: { + dps: 5.26458 + } } dps_results: { key: "TestAffliction-Settings-Orc-25-destruction-AffItemSwap--FullBuffs-LongSingleTarget" - value: {} + value: { + dps: 5.26458 + } } dps_results: { key: "TestAffliction-Settings-Orc-25-destruction-AffItemSwap--FullBuffs-ShortSingleTarget" - value: {} + value: { + dps: 8.13721 + } } dps_results: { key: "TestAffliction-Settings-Orc-25-destruction-AffItemSwap--NoBuffs-LongMultiTarget" - value: {} + value: { + dps: 2.74578 + } } dps_results: { key: "TestAffliction-Settings-Orc-25-destruction-AffItemSwap--NoBuffs-LongSingleTarget" - value: {} + value: { + dps: 2.74578 + } } dps_results: { key: "TestAffliction-Settings-Orc-25-destruction-AffItemSwap--NoBuffs-ShortSingleTarget" - value: {} + value: { + dps: 4.43909 + } } dps_results: { key: "TestAffliction-Settings-Orc-25-destruction-Affliction Warlock--FullBuffs-LongMultiTarget" - value: {} + value: { + dps: 5.26458 + } } dps_results: { key: "TestAffliction-Settings-Orc-25-destruction-Affliction Warlock--FullBuffs-LongSingleTarget" - value: {} + value: { + dps: 5.26458 + } } dps_results: { key: "TestAffliction-Settings-Orc-25-destruction-Affliction Warlock--FullBuffs-ShortSingleTarget" - value: {} + value: { + dps: 8.13721 + } } dps_results: { key: "TestAffliction-Settings-Orc-25-destruction-Affliction Warlock--NoBuffs-LongMultiTarget" - value: {} + value: { + dps: 2.74578 + } } dps_results: { key: "TestAffliction-Settings-Orc-25-destruction-Affliction Warlock--NoBuffs-LongSingleTarget" - value: {} + value: { + dps: 2.74578 + } } dps_results: { key: "TestAffliction-Settings-Orc-25-destruction-Affliction Warlock--NoBuffs-ShortSingleTarget" - value: {} + value: { + dps: 4.43909 + } } dps_results: { key: "TestAffliction-SwitchInFrontOfTarget-Default" - value: {} + value: { + dps: 5.26458 + } } diff --git a/sim/warlock/dps/TestDemonology.results b/sim/warlock/dps/TestDemonology.results index 0bd4512d88..2e53a29b6f 100644 --- a/sim/warlock/dps/TestDemonology.results +++ b/sim/warlock/dps/TestDemonology.results @@ -50,79 +50,97 @@ character_stats_results: { } dps_results: { key: "TestDemonology-AllItems-TwilightInvoker'sVestments" - value: {} + value: { + dps: 7.12805 + } } dps_results: { key: "TestDemonology-Average-Default" - value: {} + value: { + dps: 7.09776 + } } dps_results: { key: "TestDemonology-Settings-Orc-25-destruction-Demonology Warlock--FullBuffs-LongMultiTarget" - value: {} + value: { + dps: 7.08943 + } } dps_results: { key: "TestDemonology-Settings-Orc-25-destruction-Demonology Warlock--FullBuffs-LongSingleTarget" - value: {} + value: { + dps: 7.08943 + } } dps_results: { key: "TestDemonology-Settings-Orc-25-destruction-Demonology Warlock--FullBuffs-ShortSingleTarget" - value: {} + value: { + dps: 12.25988 + } } dps_results: { key: "TestDemonology-Settings-Orc-25-destruction-Demonology Warlock--NoBuffs-LongMultiTarget" - value: {} + value: { + dps: 3.31839 + } } dps_results: { key: "TestDemonology-Settings-Orc-25-destruction-Demonology Warlock--NoBuffs-LongSingleTarget" - value: {} + value: { + dps: 3.31839 + } } dps_results: { key: "TestDemonology-Settings-Orc-25-destruction-Demonology Warlock--NoBuffs-ShortSingleTarget" - value: {} + value: { + dps: 6.14755 + } } dps_results: { key: "TestDemonology-Settings-Orc-25-destruction-Demonology Warlock-destruction-FullBuffs-LongMultiTarget" value: { - dps: 183.07249 - tps: 448.83249 + dps: 191.67286 + tps: 447.60035 } } dps_results: { key: "TestDemonology-Settings-Orc-25-destruction-Demonology Warlock-destruction-FullBuffs-LongSingleTarget" value: { - dps: 147.23652 - tps: 160.52452 + dps: 155.49556 + tps: 158.88375 } } dps_results: { key: "TestDemonology-Settings-Orc-25-destruction-Demonology Warlock-destruction-FullBuffs-ShortSingleTarget" value: { - dps: 170.0934 - tps: 174.12006 + dps: 179.94431 + tps: 166.52762 } } dps_results: { key: "TestDemonology-Settings-Orc-25-destruction-Demonology Warlock-destruction-NoBuffs-LongMultiTarget" value: { - dps: 98.94392 - tps: 359.53059 + dps: 102.94086 + tps: 359.09518 } } dps_results: { key: "TestDemonology-Settings-Orc-25-destruction-Demonology Warlock-destruction-NoBuffs-LongSingleTarget" value: { - dps: 72.57656 - tps: 85.6059 + dps: 76.81762 + tps: 85.27772 } } dps_results: { key: "TestDemonology-Settings-Orc-25-destruction-Demonology Warlock-destruction-NoBuffs-ShortSingleTarget" value: { - dps: 91.49857 - tps: 97.31523 + dps: 97.03868 + tps: 94.02896 } } dps_results: { key: "TestDemonology-SwitchInFrontOfTarget-Default" - value: {} + value: { + dps: 7.08943 + } } diff --git a/sim/warlock/dps/TestDestruction.results b/sim/warlock/dps/TestDestruction.results index 5a2e73fbe5..f59ff9997b 100644 --- a/sim/warlock/dps/TestDestruction.results +++ b/sim/warlock/dps/TestDestruction.results @@ -50,79 +50,97 @@ character_stats_results: { } dps_results: { key: "TestDestruction-AllItems-TwilightInvoker'sVestments" - value: {} + value: { + dps: 7.12805 + } } dps_results: { key: "TestDestruction-Average-Default" - value: {} + value: { + dps: 7.09776 + } } dps_results: { key: "TestDestruction-Settings-Orc-25-destruction-Destruction Warlock--FullBuffs-LongMultiTarget" - value: {} + value: { + dps: 7.08943 + } } dps_results: { key: "TestDestruction-Settings-Orc-25-destruction-Destruction Warlock--FullBuffs-LongSingleTarget" - value: {} + value: { + dps: 7.08943 + } } dps_results: { key: "TestDestruction-Settings-Orc-25-destruction-Destruction Warlock--FullBuffs-ShortSingleTarget" - value: {} + value: { + dps: 12.25988 + } } dps_results: { key: "TestDestruction-Settings-Orc-25-destruction-Destruction Warlock--NoBuffs-LongMultiTarget" - value: {} + value: { + dps: 3.31839 + } } dps_results: { key: "TestDestruction-Settings-Orc-25-destruction-Destruction Warlock--NoBuffs-LongSingleTarget" - value: {} + value: { + dps: 3.31839 + } } dps_results: { key: "TestDestruction-Settings-Orc-25-destruction-Destruction Warlock--NoBuffs-ShortSingleTarget" - value: {} + value: { + dps: 6.14755 + } } dps_results: { key: "TestDestruction-Settings-Orc-25-destruction-Destruction Warlock-destruction-FullBuffs-LongMultiTarget" value: { - dps: 183.07249 - tps: 448.83249 + dps: 191.67286 + tps: 447.60035 } } dps_results: { key: "TestDestruction-Settings-Orc-25-destruction-Destruction Warlock-destruction-FullBuffs-LongSingleTarget" value: { - dps: 147.23652 - tps: 160.52452 + dps: 155.49556 + tps: 158.88375 } } dps_results: { key: "TestDestruction-Settings-Orc-25-destruction-Destruction Warlock-destruction-FullBuffs-ShortSingleTarget" value: { - dps: 170.0934 - tps: 174.12006 + dps: 179.94431 + tps: 166.52762 } } dps_results: { key: "TestDestruction-Settings-Orc-25-destruction-Destruction Warlock-destruction-NoBuffs-LongMultiTarget" value: { - dps: 98.94392 - tps: 359.53059 + dps: 102.94086 + tps: 359.09518 } } dps_results: { key: "TestDestruction-Settings-Orc-25-destruction-Destruction Warlock-destruction-NoBuffs-LongSingleTarget" value: { - dps: 72.57656 - tps: 85.6059 + dps: 76.81762 + tps: 85.27772 } } dps_results: { key: "TestDestruction-Settings-Orc-25-destruction-Destruction Warlock-destruction-NoBuffs-ShortSingleTarget" value: { - dps: 91.49857 - tps: 97.31523 + dps: 97.03868 + tps: 94.02896 } } dps_results: { key: "TestDestruction-SwitchInFrontOfTarget-Default" - value: {} + value: { + dps: 7.08943 + } } diff --git a/sim/warlock/pet.go b/sim/warlock/pet.go index 36303e2678..f286dbca91 100644 --- a/sim/warlock/pet.go +++ b/sim/warlock/pet.go @@ -188,7 +188,7 @@ func (wp *WarlockPet) Initialize() { func (wp *WarlockPet) Reset(_ *core.Simulation) { } -func (wp *WarlockPet) OnGCDReady(sim *core.Simulation) { +func (wp *WarlockPet) ExecuteCustomRotation(sim *core.Simulation) { if wp.manaPooling { maxPossibleCasts := sim.GetRemainingDuration().Seconds() / wp.primaryAbility.CurCast.CastTime.Seconds() diff --git a/sim/warlock/tank/TestAffliction.results b/sim/warlock/tank/TestAffliction.results index b7665e3087..c4ba3a03b5 100644 --- a/sim/warlock/tank/TestAffliction.results +++ b/sim/warlock/tank/TestAffliction.results @@ -50,145 +50,175 @@ character_stats_results: { } dps_results: { key: "TestAffliction-AllItems-TwilightInvoker'sVestments" - value: {} + value: { + dps: 5.17184 + } } dps_results: { key: "TestAffliction-Average-Default" - value: {} + value: { + dps: 5.25203 + } } dps_results: { key: "TestAffliction-Settings-Orc-25-affi.tank-AffItemSwap--FullBuffs-LongMultiTarget" - value: {} + value: { + dps: 5.27614 + } } dps_results: { key: "TestAffliction-Settings-Orc-25-affi.tank-AffItemSwap--FullBuffs-LongSingleTarget" - value: {} + value: { + dps: 5.27614 + } } dps_results: { key: "TestAffliction-Settings-Orc-25-affi.tank-AffItemSwap--FullBuffs-ShortSingleTarget" - value: {} + value: { + dps: 8.11172 + } } dps_results: { key: "TestAffliction-Settings-Orc-25-affi.tank-AffItemSwap--NoBuffs-LongMultiTarget" - value: {} + value: { + dps: 2.71874 + } } dps_results: { key: "TestAffliction-Settings-Orc-25-affi.tank-AffItemSwap--NoBuffs-LongSingleTarget" - value: {} + value: { + dps: 2.71874 + } } dps_results: { key: "TestAffliction-Settings-Orc-25-affi.tank-AffItemSwap--NoBuffs-ShortSingleTarget" - value: {} + value: { + dps: 4.41435 + } } dps_results: { key: "TestAffliction-Settings-Orc-25-affi.tank-AffItemSwap-affi.tank-FullBuffs-LongMultiTarget" value: { - dps: 167.28966 - tps: 578.5179 + dps: 171.43576 + tps: 576.91079 } } dps_results: { key: "TestAffliction-Settings-Orc-25-affi.tank-AffItemSwap-affi.tank-FullBuffs-LongSingleTarget" value: { - dps: 113.51915 - tps: 170.14875 + dps: 116.97501 + tps: 169.74169 } } dps_results: { key: "TestAffliction-Settings-Orc-25-affi.tank-AffItemSwap-affi.tank-FullBuffs-ShortSingleTarget" value: { - dps: 117.63789 - tps: 170.74929 + dps: 121.60168 + tps: 167.88712 } } dps_results: { key: "TestAffliction-Settings-Orc-25-affi.tank-AffItemSwap-affi.tank-NoBuffs-LongMultiTarget" value: { - dps: 107.17941 - tps: 517.71481 + dps: 109.5768 + tps: 515.51446 } } dps_results: { key: "TestAffliction-Settings-Orc-25-affi.tank-AffItemSwap-affi.tank-NoBuffs-LongSingleTarget" value: { - dps: 68.78945 - tps: 103.89169 + dps: 70.97275 + tps: 104.03623 } } dps_results: { key: "TestAffliction-Settings-Orc-25-affi.tank-AffItemSwap-affi.tank-NoBuffs-ShortSingleTarget" value: { - dps: 71.71283 - tps: 103.25802 + dps: 73.29689 + tps: 98.79096 } } dps_results: { key: "TestAffliction-Settings-Orc-25-affi.tank-Affliction Warlock--FullBuffs-LongMultiTarget" - value: {} + value: { + dps: 5.27614 + } } dps_results: { key: "TestAffliction-Settings-Orc-25-affi.tank-Affliction Warlock--FullBuffs-LongSingleTarget" - value: {} + value: { + dps: 5.27614 + } } dps_results: { key: "TestAffliction-Settings-Orc-25-affi.tank-Affliction Warlock--FullBuffs-ShortSingleTarget" - value: {} + value: { + dps: 8.11172 + } } dps_results: { key: "TestAffliction-Settings-Orc-25-affi.tank-Affliction Warlock--NoBuffs-LongMultiTarget" - value: {} + value: { + dps: 2.71874 + } } dps_results: { key: "TestAffliction-Settings-Orc-25-affi.tank-Affliction Warlock--NoBuffs-LongSingleTarget" - value: {} + value: { + dps: 2.71874 + } } dps_results: { key: "TestAffliction-Settings-Orc-25-affi.tank-Affliction Warlock--NoBuffs-ShortSingleTarget" - value: {} + value: { + dps: 4.41435 + } } dps_results: { key: "TestAffliction-Settings-Orc-25-affi.tank-Affliction Warlock-affi.tank-FullBuffs-LongMultiTarget" value: { - dps: 167.28966 - tps: 578.5179 + dps: 171.43576 + tps: 576.91079 } } dps_results: { key: "TestAffliction-Settings-Orc-25-affi.tank-Affliction Warlock-affi.tank-FullBuffs-LongSingleTarget" value: { - dps: 113.51915 - tps: 170.14875 + dps: 116.97501 + tps: 169.74169 } } dps_results: { key: "TestAffliction-Settings-Orc-25-affi.tank-Affliction Warlock-affi.tank-FullBuffs-ShortSingleTarget" value: { - dps: 117.63789 - tps: 170.74929 + dps: 121.60168 + tps: 167.88712 } } dps_results: { key: "TestAffliction-Settings-Orc-25-affi.tank-Affliction Warlock-affi.tank-NoBuffs-LongMultiTarget" value: { - dps: 107.17941 - tps: 517.71481 + dps: 109.5768 + tps: 515.51446 } } dps_results: { key: "TestAffliction-Settings-Orc-25-affi.tank-Affliction Warlock-affi.tank-NoBuffs-LongSingleTarget" value: { - dps: 68.78945 - tps: 103.89169 + dps: 70.97275 + tps: 104.03623 } } dps_results: { key: "TestAffliction-Settings-Orc-25-affi.tank-Affliction Warlock-affi.tank-NoBuffs-ShortSingleTarget" value: { - dps: 71.71283 - tps: 103.25802 + dps: 73.29689 + tps: 98.79096 } } dps_results: { key: "TestAffliction-SwitchInFrontOfTarget-Default" - value: {} + value: { + dps: 5.27614 + } } diff --git a/sim/warlock/tank/TestDemonology.results b/sim/warlock/tank/TestDemonology.results index 82177fe7fa..d3f95d9edf 100644 --- a/sim/warlock/tank/TestDemonology.results +++ b/sim/warlock/tank/TestDemonology.results @@ -50,37 +50,55 @@ character_stats_results: { } dps_results: { key: "TestDemonology-AllItems-TwilightInvoker'sVestments" - value: {} + value: { + dps: 5.17184 + } } dps_results: { key: "TestDemonology-Average-Default" - value: {} + value: { + dps: 5.25203 + } } dps_results: { key: "TestDemonology-Settings-Orc-25-destro.tank-Demonology Warlock--FullBuffs-LongMultiTarget" - value: {} + value: { + dps: 5.27614 + } } dps_results: { key: "TestDemonology-Settings-Orc-25-destro.tank-Demonology Warlock--FullBuffs-LongSingleTarget" - value: {} + value: { + dps: 5.27614 + } } dps_results: { key: "TestDemonology-Settings-Orc-25-destro.tank-Demonology Warlock--FullBuffs-ShortSingleTarget" - value: {} + value: { + dps: 8.11172 + } } dps_results: { key: "TestDemonology-Settings-Orc-25-destro.tank-Demonology Warlock--NoBuffs-LongMultiTarget" - value: {} + value: { + dps: 2.71874 + } } dps_results: { key: "TestDemonology-Settings-Orc-25-destro.tank-Demonology Warlock--NoBuffs-LongSingleTarget" - value: {} + value: { + dps: 2.71874 + } } dps_results: { key: "TestDemonology-Settings-Orc-25-destro.tank-Demonology Warlock--NoBuffs-ShortSingleTarget" - value: {} + value: { + dps: 4.41435 + } } dps_results: { key: "TestDemonology-SwitchInFrontOfTarget-Default" - value: {} + value: { + dps: 5.27614 + } } diff --git a/sim/warlock/tank/TestDestruction.results b/sim/warlock/tank/TestDestruction.results index 8340605130..72fac9b8ef 100644 --- a/sim/warlock/tank/TestDestruction.results +++ b/sim/warlock/tank/TestDestruction.results @@ -50,79 +50,97 @@ character_stats_results: { } dps_results: { key: "TestDestruction-AllItems-TwilightInvoker'sVestments" - value: {} + value: { + dps: 5.17184 + } } dps_results: { key: "TestDestruction-Average-Default" - value: {} + value: { + dps: 5.25203 + } } dps_results: { key: "TestDestruction-Settings-Orc-25-destro.tank-Destruction Warlock--FullBuffs-LongMultiTarget" - value: {} + value: { + dps: 5.27614 + } } dps_results: { key: "TestDestruction-Settings-Orc-25-destro.tank-Destruction Warlock--FullBuffs-LongSingleTarget" - value: {} + value: { + dps: 5.27614 + } } dps_results: { key: "TestDestruction-Settings-Orc-25-destro.tank-Destruction Warlock--FullBuffs-ShortSingleTarget" - value: {} + value: { + dps: 8.11172 + } } dps_results: { key: "TestDestruction-Settings-Orc-25-destro.tank-Destruction Warlock--NoBuffs-LongMultiTarget" - value: {} + value: { + dps: 2.71874 + } } dps_results: { key: "TestDestruction-Settings-Orc-25-destro.tank-Destruction Warlock--NoBuffs-LongSingleTarget" - value: {} + value: { + dps: 2.71874 + } } dps_results: { key: "TestDestruction-Settings-Orc-25-destro.tank-Destruction Warlock--NoBuffs-ShortSingleTarget" - value: {} + value: { + dps: 4.41435 + } } dps_results: { key: "TestDestruction-Settings-Orc-25-destro.tank-Destruction Warlock-destro.tank-FullBuffs-LongMultiTarget" value: { - dps: 105.69079 - tps: 530.68921 + dps: 113.47197 + tps: 532.1146 } } dps_results: { key: "TestDestruction-Settings-Orc-25-destro.tank-Destruction Warlock-destro.tank-FullBuffs-LongSingleTarget" value: { - dps: 72.43425 - tps: 176.68078 + dps: 79.91296 + tps: 176.73316 } } dps_results: { key: "TestDestruction-Settings-Orc-25-destro.tank-Destruction Warlock-destro.tank-FullBuffs-ShortSingleTarget" value: { - dps: 75.16322 - tps: 177.82554 + dps: 83.89505 + tps: 173.52299 } } dps_results: { key: "TestDestruction-Settings-Orc-25-destro.tank-Destruction Warlock-destro.tank-NoBuffs-LongMultiTarget" value: { - dps: 59.80328 - tps: 448.04074 + dps: 64.07491 + tps: 448.66235 } } dps_results: { key: "TestDestruction-Settings-Orc-25-destro.tank-Destruction Warlock-destro.tank-NoBuffs-LongSingleTarget" value: { - dps: 39.68099 - tps: 101.99939 + dps: 44.14443 + tps: 103.10609 } } dps_results: { key: "TestDestruction-Settings-Orc-25-destro.tank-Destruction Warlock-destro.tank-NoBuffs-ShortSingleTarget" value: { - dps: 43.07725 - tps: 104.2374 + dps: 48.04939 + tps: 101.54344 } } dps_results: { key: "TestDestruction-SwitchInFrontOfTarget-Default" - value: {} + value: { + dps: 5.27614 + } } diff --git a/ui/core/components/individual_sim_ui/apl_actions.ts b/ui/core/components/individual_sim_ui/apl_actions.ts index 434a96dcf5..7eec7ce4e4 100644 --- a/ui/core/components/individual_sim_ui/apl_actions.ts +++ b/ui/core/components/individual_sim_ui/apl_actions.ts @@ -1,5 +1,4 @@ import { - Class, Spec, } from '../../proto/common.js'; @@ -29,6 +28,7 @@ import { APLActionMove, APLActionCatOptimalRotationAction, + APLActionCustomRotation, APLValue, } from '../../proto/apl.js'; @@ -581,13 +581,22 @@ const actionKindFactories: {[f in NonNullable]: ActionKindConfig< }), ], }), + ['customRotation']: inputBuilder({ + label: 'Custom Rotation', + //submenu: ['Misc'], + shortDescription: 'INTERNAL ONLY', + includeIf: (_player: Player, _isPrepull: boolean) => false, // Never show this, because its internal only. + newValue: () => APLActionCustomRotation.create(), + fields: [ + ], + }), // Class/spec specific actions ['catOptimalRotationAction']: inputBuilder({ label: 'Optimal Rotation Action', submenu: ['Feral Druid'], shortDescription: 'Executes optimized Feral DPS rotation using hardcoded legacy algorithm.', - includeIf: (player: Player, isPrepull: boolean) => player.spec == Spec.SpecFeralDruid, + includeIf: (player: Player, _isPrepull: boolean) => player.spec == Spec.SpecFeralDruid, newValue: () => APLActionCatOptimalRotationAction.create({ minCombosForRip: 3, maxWaitTime: 2.0,