diff --git a/sim/rogue/feint.go b/sim/rogue/feint.go index 459902376c..363328ebb3 100644 --- a/sim/rogue/feint.go +++ b/sim/rogue/feint.go @@ -36,17 +36,4 @@ func (rogue *Rogue) registerFeintSpell() { spell.CalcAndDealOutcome(sim, target, spell.OutcomeMeleeSpecialHit) }, }) - // Feint - if rogue.Rotation.UseFeint { - rogue.AddMajorCooldown(core.MajorCooldown{ - Spell: rogue.Feint, - Priority: core.CooldownPriorityDefault, - Type: core.CooldownTypeDPS, - //don't feint if you're gonna waste energy by using the gcd - ShouldActivate: func(sim *core.Simulation, character *core.Character) bool { - thresh := 55.0 //55 simmed best with standard settings for now 3/12/2023, will refine with the rotational refinements. 55 was definitely best for combat, didn't make a difference for muti - return rogue.CurrentEnergy() <= thresh - }, - }) - } } diff --git a/sim/rogue/rogue.go b/sim/rogue/rogue.go index f41e5375ba..5804c9374e 100644 --- a/sim/rogue/rogue.go +++ b/sim/rogue/rogue.go @@ -1,7 +1,6 @@ package rogue import ( - "math" "time" "github.com/wowsims/wotlk/sim/core" @@ -39,21 +38,14 @@ const RogueBleedTag = "RogueBleed" type Rogue struct { core.Character - Talents *proto.RogueTalents - Options *proto.Rogue_Options - Rotation *proto.Rogue_Rotation - - rotation rotation + Talents *proto.RogueTalents + Options *proto.Rogue_Options bleedCategory *core.ExclusiveCategory sliceAndDiceDurations [6]time.Duration exposeArmorDurations [6]time.Duration - allMCDsDisabled bool - - maxEnergy float64 - Backstab *core.Spell BladeFlurry *core.Spell DeadlyPoison *core.Spell @@ -185,13 +177,6 @@ func (rogue *Rogue) Initialize() { rogue.registerVanishSpell() rogue.finishingMoveEffectApplier = rogue.makeFinishingMoveEffectApplier() - - if !rogue.IsUsingAPL && rogue.Rotation.TricksOfTheTradeFrequency != proto.Rogue_Rotation_Never && !rogue.HasSetBonus(Tier10, 2) { - rogue.RegisterPrepullAction(-10*time.Second, func(sim *core.Simulation) { - rogue.TricksOfTheTrade.Cast(sim, nil) - rogue.UpdateMajorCooldowns() - }) - } } func (rogue *Rogue) ApplyEnergyTickMultiplier(multiplier float64) { @@ -202,30 +187,6 @@ func (rogue *Rogue) Reset(sim *core.Simulation) { for _, mcd := range rogue.GetMajorCooldowns() { mcd.Disable() } - rogue.allMCDsDisabled = true - - if !rogue.IsUsingAPL { - // Stealth triggered effects (Overkill and Master of Subtlety) pre-pull activation - if rogue.Rotation.OpenWithGarrote || rogue.Rotation.OpenWithPremeditation { - rogue.AutoAttacks.CancelAutoSwing(sim) - rogue.StealthAura.Activate(sim) - } else { - if rogue.Options.StartingOverkillDuration > 0 { - if rogue.Talents.Overkill { - duration := time.Second * time.Duration(math.Min(float64(rogue.Options.StartingOverkillDuration), 20)) - rogue.OverkillAura.Activate(sim) - rogue.OverkillAura.UpdateExpires(duration) - } - if rogue.Talents.MasterOfSubtlety > 0 { - duration := time.Second * time.Duration(math.Min(float64(rogue.Options.StartingOverkillDuration), 6)) - rogue.MasterOfSubtletyAura.Activate(sim) - rogue.MasterOfSubtletyAura.UpdateExpires(duration) - } - } - } - } - - rogue.setupRotation(sim) } func (rogue *Rogue) MeleeCritMultiplier(applyLethality bool) float64 { @@ -248,13 +209,13 @@ func NewRogue(character *core.Character, options *proto.Player) *Rogue { Character: *character, Talents: &proto.RogueTalents{}, Options: rogueOptions.Options, - Rotation: rogueOptions.Rotation, } core.FillTalentsProto(rogue.Talents.ProtoReflect(), options.TalentsString, TalentTreeSizes) // Passive rogue threat reduction: https://wotlk.wowhead.com/spell=21184/rogue-passive-dnd rogue.PseudoStats.ThreatMultiplier *= 0.71 rogue.PseudoStats.CanParry = true + maxEnergy := 100.0 if rogue.Talents.Vigor { maxEnergy += 10 @@ -265,8 +226,7 @@ func NewRogue(character *core.Character, options *proto.Player) *Rogue { if rogue.HasSetBonus(Arena, 4) { maxEnergy += 10 } - rogue.maxEnergy = maxEnergy - rogue.EnableEnergyBar(maxEnergy, rogue.OnEnergyGain) + rogue.EnableEnergyBar(maxEnergy) rogue.ApplyEnergyTickMultiplier([]float64{0, 0.08, 0.16, 0.25}[rogue.Talents.Vitality]) rogue.EnableAutoAttacks(rogue, core.AutoAttackOptions{ @@ -303,11 +263,6 @@ func (rogue *Rogue) BreakStealth(sim *core.Simulation) { } } -// Can the rogue fulfil the weapon equipped requirement for Mutilate? -func (rogue *Rogue) CanMutilate() bool { - return rogue.Talents.Mutilate && rogue.HasDagger(core.MainHand) && rogue.HasDagger(core.OffHand) -} - // Does the rogue have a dagger equipped in the specified hand (main or offhand)? func (rogue *Rogue) HasDagger(hand core.Hand) bool { if hand == core.MainHand { diff --git a/sim/rogue/rogue_test.go b/sim/rogue/rogue_test.go index 2edf03bf44..9fdd9220c1 100644 --- a/sim/rogue/rogue_test.go +++ b/sim/rogue/rogue_test.go @@ -238,79 +238,67 @@ var SubtletyGlyphs = &proto.Glyphs{ var PlayerOptionsCombatDI = &proto.Player_Rogue{ Rogue: &proto.Rogue{ - Options: DeadlyInstant, - Rotation: &proto.Rogue_Rotation{}, + Options: DeadlyInstant, }, } var PlayerOptionsCombatDD = &proto.Player_Rogue{ Rogue: &proto.Rogue{ - Options: DeadlyDeadly, - Rotation: &proto.Rogue_Rotation{}, + Options: DeadlyDeadly, }, } var PlayerOptionsCombatID = &proto.Player_Rogue{ Rogue: &proto.Rogue{ - Options: InstantDeadly, - Rotation: &proto.Rogue_Rotation{}, + Options: InstantDeadly, }, } var PlayerOptionsCombatII = &proto.Player_Rogue{ Rogue: &proto.Rogue{ - Options: InstantInstant, - Rotation: &proto.Rogue_Rotation{}, + Options: InstantInstant, }, } var PlayerOptionsNoLethality = &proto.Player_Rogue{ Rogue: &proto.Rogue{ - Options: DeadlyInstant, - Rotation: &proto.Rogue_Rotation{}, + Options: DeadlyInstant, }, } var PlayerOptionsNoPotW = &proto.Player_Rogue{ Rogue: &proto.Rogue{ - Options: DeadlyInstant, - Rotation: &proto.Rogue_Rotation{}, + Options: DeadlyInstant, }, } var PlayerOptionsNoLethalityNoPotW = &proto.Player_Rogue{ Rogue: &proto.Rogue{ - Options: DeadlyInstant, - Rotation: &proto.Rogue_Rotation{}, + Options: DeadlyInstant, }, } var PlayerOptionsAssassinationDI = &proto.Player_Rogue{ Rogue: &proto.Rogue{ - Options: DeadlyInstant, - Rotation: &proto.Rogue_Rotation{}, + Options: DeadlyInstant, }, } var PlayerOptionsAssassinationDD = &proto.Player_Rogue{ Rogue: &proto.Rogue{ - Options: DeadlyDeadly, - Rotation: &proto.Rogue_Rotation{}, + Options: DeadlyDeadly, }, } var PlayerOptionsAssassinationID = &proto.Player_Rogue{ Rogue: &proto.Rogue{ - Options: InstantDeadly, - Rotation: &proto.Rogue_Rotation{}, + Options: InstantDeadly, }, } var PlayerOptionsAssassinationII = &proto.Player_Rogue{ Rogue: &proto.Rogue{ - Options: InstantInstant, - Rotation: &proto.Rogue_Rotation{}, + Options: InstantInstant, }, } var PlayerOptionsSubtletyID = &proto.Player_Rogue{ Rogue: &proto.Rogue{ - Options: InstantDeadly, - Rotation: &proto.Rogue_Rotation{}, + Options: InstantDeadly, }, } diff --git a/sim/rogue/rotation.go b/sim/rogue/rotation.go deleted file mode 100644 index 95190e3799..0000000000 --- a/sim/rogue/rotation.go +++ /dev/null @@ -1,77 +0,0 @@ -package rogue - -import ( - "github.com/wowsims/wotlk/sim/core" -) - -type rotation interface { - setup(sim *core.Simulation, rogue *Rogue) - run(sim *core.Simulation, rogue *Rogue) -} - -type PriorityAction int32 - -const ( - Skip PriorityAction = iota - Build - Cast - Wait - Once -) - -type prio struct { - check func(sim *core.Simulation, rogue *Rogue) PriorityAction - cast func(sim *core.Simulation, rogue *Rogue) bool - cost float64 -} - -func (rogue *Rogue) OnEnergyGain(sim *core.Simulation) { - if rogue.IsUsingAPL { - return - } - - if sim.CurrentTime < 0 { - return - } - - rogue.TryUseCooldowns(sim) - - if !rogue.GCD.IsReady(sim) { - return - } - - rogue.rotation.run(sim, rogue) -} - -func (rogue *Rogue) OnGCDReady(sim *core.Simulation) { - if rogue.IsUsingAPL { - return - } - rogue.TryUseCooldowns(sim) - - if rogue.IsWaitingForEnergy() { - rogue.DoNothing() - return - } - - rogue.rotation.run(sim, rogue) -} - -func (rogue *Rogue) setupRotation(sim *core.Simulation) { - if rogue.IsUsingAPL { - return - } - switch { - case rogue.Env.GetNumTargets() >= 3: - rogue.rotation = &rotation_multi{} // rotation multi will soon be removed - case rogue.CanMutilate(): - rogue.rotation = &rotation_assassination{} - case rogue.Talents.CombatPotency > 0: - rogue.rotation = &rotation_combat{} - case rogue.Talents.HonorAmongThieves > 0: - rogue.rotation = &rotation_subtlety{} - default: - rogue.rotation = &rotation_generic{} - } - rogue.rotation.setup(sim, rogue) -} diff --git a/sim/rogue/rotation_assassination.go b/sim/rogue/rotation_assassination.go deleted file mode 100644 index a4a98bc23d..0000000000 --- a/sim/rogue/rotation_assassination.go +++ /dev/null @@ -1,374 +0,0 @@ -package rogue - -import ( - "log" - "slices" - "time" - - "github.com/wowsims/wotlk/sim/core" - "github.com/wowsims/wotlk/sim/core/proto" -) - -type rotation_assassination struct { - prios []prio -} - -func (x *rotation_assassination) setup(sim *core.Simulation, rogue *Rogue) { - rogue.bleedCategory = rogue.CurrentTarget.GetExclusiveEffectCategory(core.BleedEffectCategory) - - x.prios = x.prios[:0] - - mutiCost := rogue.Mutilate.DefaultCast.Cost - rupCost := rogue.Rupture.DefaultCast.Cost - envCost := rogue.Envenom.DefaultCast.Cost - - // estimate of energy per second while nothing is cast - energyPerSecond := func() float64 { - if rogue.Talents.FocusedAttacks == 0 { - return 10 * rogue.EnergyTickMultiplier - } - - getCritChance := func(spell *core.Spell) float64 { - at := rogue.AttackTables[rogue.CurrentTarget.UnitIndex] - - critCap := 1.0 - at.BaseGlanceChance - if miss := at.BaseMissChance + 0.19 - spell.PhysicalHitChance(at); miss > 0 { - critCap -= miss - } - if dodge := at.BaseDodgeChance - spell.ExpertisePercentage() - rogue.PseudoStats.DodgeReduction; dodge > 0 { - critCap -= dodge - } - - critChance := spell.PhysicalCritChance(at) - if critChance > critCap { - critChance = critCap - } - return critChance - } - - critsPerSecond := getCritChance(rogue.AutoAttacks.MHAuto())/rogue.AutoAttacks.MainhandSwingSpeed().Seconds() + - getCritChance(rogue.AutoAttacks.OHAuto())/rogue.AutoAttacks.OffhandSwingSpeed().Seconds() - procChance := []float64{0, 0.33, 0.66, 1}[rogue.Talents.FocusedAttacks] - - return 10*rogue.EnergyTickMultiplier + critsPerSecond*procChance*2 - } - - // Garrote - if rogue.Rotation.OpenWithGarrote && !rogue.PseudoStats.InFrontOfTarget && rogue.IsStealthed() { - x.prios = append(x.prios, prio{ - func(sim *core.Simulation, rogue *Rogue) PriorityAction { - if rogue.CurrentEnergy() > rogue.Garrote.DefaultCast.Cost && rogue.IsStealthed() { - return Once - } - return Wait - }, - func(sim *core.Simulation, rogue *Rogue) bool { - return rogue.Garrote.Cast(sim, rogue.CurrentTarget) - }, - rogue.Garrote.DefaultCast.Cost, - }) - } - - // Slice And Dice - x.prios = append(x.prios, prio{ - func(sim *core.Simulation, rogue *Rogue) PriorityAction { - if rogue.SliceAndDiceAura.IsActive() { - return Skip - } - if rogue.ComboPoints() > 0 && rogue.CurrentEnergy() > rogue.SliceAndDice.DefaultCast.Cost { - return Cast - } - if rogue.ComboPoints() < 1 && rogue.CurrentEnergy() > mutiCost { - return Build - } - return Wait - }, - func(sim *core.Simulation, rogue *Rogue) bool { - return rogue.SliceAndDice.Cast(sim, rogue.CurrentTarget) - }, - rogue.SliceAndDice.DefaultCast.Cost, - }) - - // Hunger while planning - if rogue.Talents.HungerForBlood { - x.prios = append(x.prios, prio{ - func(sim *core.Simulation, rogue *Rogue) PriorityAction { - - prioExpose := rogue.Rotation.ExposeArmorFrequency == proto.Rogue_Rotation_Once || - rogue.Rotation.ExposeArmorFrequency == proto.Rogue_Rotation_Maintain - if prioExpose && !rogue.ExposeArmorAuras.Get(rogue.CurrentTarget).IsActive() { - return Skip - } - - if rogue.HungerForBloodAura.IsActive() { - return Skip - } - - if !x.targetHasBleed(sim, rogue) { - return Skip - } - - if x.targetHasBleed(sim, rogue) && rogue.CurrentEnergy() > rogue.HungerForBlood.DefaultCast.Cost { - return Cast - } - return Wait - }, - func(sim *core.Simulation, rogue *Rogue) bool { - return rogue.HungerForBlood.Cast(sim, rogue.CurrentTarget) - }, - rogue.HungerForBlood.DefaultCast.Cost, - }) - } - - // Expose armor - if rogue.Rotation.ExposeArmorFrequency == proto.Rogue_Rotation_Once || rogue.Rotation.ExposeArmorFrequency == proto.Rogue_Rotation_Maintain { - hasCastExpose := false - x.prios = append(x.prios, prio{ - func(sim *core.Simulation, rogue *Rogue) PriorityAction { - if hasCastExpose && rogue.Rotation.ExposeArmorFrequency == proto.Rogue_Rotation_Once { - return Skip - } - timeLeft := rogue.ExposeArmorAuras.Get(rogue.CurrentTarget).RemainingDuration(sim) - minPoints := max(1, min(rogue.Rotation.MinimumComboPointsExposeArmor, 5)) - if rogue.Rotation.ExposeArmorFrequency != proto.Rogue_Rotation_Once { - minPoints = 1 - } - if timeLeft <= 0 { - if rogue.ComboPoints() < minPoints { - if rogue.CurrentEnergy() >= mutiCost { - return Build - } else { - return Wait - } - } else { - if rogue.CurrentEnergy() >= rogue.ExposeArmor.DefaultCast.Cost { - return Cast - } else { - return Wait - } - } - } else { - energyGained := energyPerSecond() * timeLeft.Seconds() - cpGenerated := energyGained / mutiCost - currentCp := float64(rogue.ComboPoints()) - if currentCp+cpGenerated > 5 { - return Skip - } else { - if currentCp < 5 { - if rogue.CurrentEnergy() >= mutiCost { - return Build - } - } - return Wait - } - } - }, - func(sim *core.Simulation, rogue *Rogue) bool { - casted := rogue.ExposeArmor.Cast(sim, rogue.CurrentTarget) - if casted { - hasCastExpose = true - } - return casted - }, - rogue.ExposeArmor.DefaultCast.Cost, - }) - } - - // Rupture for Bleed - if rogue.Rotation.RuptureForBleed { - x.prios = append(x.prios, prio{ - func(sim *core.Simulation, rogue *Rogue) PriorityAction { - if x.targetHasBleed(sim, rogue) { - return Skip - } - if rogue.HungerForBloodAura.IsActive() { - return Skip - } - if rogue.ComboPoints() > 0 && rogue.CurrentEnergy() >= rupCost { - return Cast - } - if rogue.ComboPoints() < 1 && rogue.CurrentEnergy() >= mutiCost { - return Build - } - return Wait - }, - func(sim *core.Simulation, rogue *Rogue) bool { - return rogue.Rupture.Cast(sim, rogue.CurrentTarget) - }, - rupCost, - }) - } - - // Hunger for Blood - if rogue.Talents.HungerForBlood { - x.prios = append(x.prios, prio{ - func(sim *core.Simulation, rogue *Rogue) PriorityAction { - if rogue.HungerForBloodAura.IsActive() { - return Skip - } - - if !x.targetHasBleed(sim, rogue) { - return Skip - } - - if x.targetHasBleed(sim, rogue) && rogue.CurrentEnergy() >= rogue.HungerForBlood.DefaultCast.Cost { - return Cast - } - return Wait - }, - func(s *core.Simulation, r *Rogue) bool { - return rogue.HungerForBlood.Cast(sim, rogue.CurrentTarget) - }, - rogue.HungerForBlood.DefaultCast.Cost, - }) - } - - // Enable CDs - x.prios = append(x.prios, prio{ - func(sim *core.Simulation, rogue *Rogue) PriorityAction { - for _, mcd := range rogue.GetMajorCooldowns() { - if mcd.Spell != rogue.ColdBlood { - mcd.Enable() - } - } - return Once - }, - func(s *core.Simulation, r *Rogue) bool { - return true - }, - 0, - }) - - // Rupture - x.prios = append(x.prios, prio{ - func(sim *core.Simulation, rogue *Rogue) PriorityAction { - cp, e := rogue.ComboPoints(), rogue.CurrentEnergy() - - if rogue.Rotation.AssassinationFinisherPriority == proto.Rogue_Rotation_EnvenomRupture { - if rogue.Rupture.CurDot().IsActive() || sim.GetRemainingDuration() < rogue.RuptureDuration(4) { - return Skip - } - if !rogue.EnvenomAura.IsActive() || cp < 4 || rogue.Talents.Ruthlessness < 3 { - return Skip - } - - // use Rupture if you can re-cast Envenom with minimal delay, hoping for a Ruthlessness proc ;) - avail := e + rogue.EnvenomAura.RemainingDuration(sim).Seconds()*energyPerSecond() - cost := rupCost + mutiCost + envCost - if avail >= cost { - return Cast - } - return Skip - - } else { - if rogue.Rupture.CurDot().IsActive() || sim.GetRemainingDuration() < time.Second*18 { - return Skip - } - if cp >= 4 && e >= rupCost { - return Cast - } - if cp < 4 && e >= mutiCost { - return Build - } - return Wait - } - }, - func(sim *core.Simulation, rogue *Rogue) bool { - return rogue.Rupture.Cast(sim, rogue.CurrentTarget) - }, - rupCost, - }) - - // Envenom - x.prios = append(x.prios, prio{ - func(sim *core.Simulation, rogue *Rogue) PriorityAction { - e, cp := rogue.CurrentEnergy(), rogue.ComboPoints() - - // end of combat handling - possibly use low CP Envenoms instead of doing nothing - if dur := sim.GetRemainingDuration(); dur <= 10*time.Second { - avail := e + dur.Seconds()*energyPerSecond() - - if cp == 3 && avail < mutiCost+envCost && e >= envCost { - return Cast - } - - if cp >= 1 && avail < mutiCost && e >= envCost { - return Cast - } - } - - if cp >= 4 { - eps := energyPerSecond() - - if rogue.EnvenomAura.IsActive() { - // don't clip Envenom, unless you'd energy cap - if e < rogue.maxEnergy-eps && sim.GetRemainingDuration() >= rogue.EnvenomDuration(5) { - return Wait - } - return Cast - } - - // pool, so two Mutilate casts fit into the next uptime; this is a very minor DPS gain, and primarily for lower gear levels - cost := envCost + mutiCost + mutiCost - if cp == 5 && rogue.Talents.RelentlessStrikes == 5 { - cost -= 25 - } - avail := e + rogue.EnvenomDuration(cp).Seconds()*eps - if avail < cost { - return Wait - } - return Cast - } - - if e >= mutiCost { - return Build - } - return Wait - }, - func(sim *core.Simulation, rogue *Rogue) bool { - if rogue.ColdBlood.IsReady(sim) && rogue.ComboPoints() == 5 { - rogue.ColdBlood.Cast(sim, rogue.CurrentTarget) - } - return rogue.Envenom.Cast(sim, rogue.CurrentTarget) - }, - envCost, - }) -} - -func (x *rotation_assassination) run(sim *core.Simulation, rogue *Rogue) { - for i := 0; i < len(x.prios); i++ { - switch p := x.prios[i]; p.check(sim, rogue) { - case Skip: - continue - case Build: - if !rogue.Mutilate.Cast(sim, rogue.CurrentTarget) { - rogue.WaitForEnergy(sim, rogue.Mutilate.DefaultCast.Cost) - return - } - case Cast: - if !p.cast(sim, rogue) { - rogue.WaitForEnergy(sim, p.cost) - return - } - case Once: - if !p.cast(sim, rogue) { - rogue.WaitForEnergy(sim, p.cost) - return - } - x.prios = slices.Delete(x.prios, i, i+1) - i-- - case Wait: - rogue.DoNothing() - return - } - - if !rogue.GCD.IsReady(sim) { - return - } - } - log.Panic("skipped all prios") -} - -func (x *rotation_assassination) targetHasBleed(_ *core.Simulation, rogue *Rogue) bool { - return rogue.bleedCategory.AnyActive() || rogue.CurrentTarget.HasActiveAuraWithTag(RogueBleedTag) || rogue.Options.AssumeBleedActive -} diff --git a/sim/rogue/rotation_combat.go b/sim/rogue/rotation_combat.go deleted file mode 100644 index a38113ba66..0000000000 --- a/sim/rogue/rotation_combat.go +++ /dev/null @@ -1,454 +0,0 @@ -package rogue - -import ( - "log" - "slices" - "time" - - "github.com/wowsims/wotlk/sim/core" - "github.com/wowsims/wotlk/sim/core/proto" -) - -type rotation_combat struct { - prios []prio - - builder *core.Spell -} - -func (x *rotation_combat) setup(_ *core.Simulation, rogue *Rogue) { - x.prios = x.prios[:0] - - x.builder = rogue.SinisterStrike - if rogue.Rotation.CombatBuilder == proto.Rogue_Rotation_Backstab && rogue.HasDagger(core.MainHand) && !rogue.PseudoStats.InFrontOfTarget { - x.builder = rogue.Backstab - } - if rogue.Talents.Hemorrhage && rogue.Rotation.CombatBuilder == proto.Rogue_Rotation_HemorrhageCombat { - x.builder = rogue.Hemorrhage - } - - bldCost := x.builder.DefaultCast.Cost - sndCost := rogue.SliceAndDice.DefaultCast.Cost - rupCost := rogue.Rupture.DefaultCast.Cost - evisCost := rogue.Eviscerate.DefaultCast.Cost - - baseEps := 10 * rogue.EnergyTickMultiplier - maxPool := rogue.maxEnergy - 3*float64(rogue.Talents.CombatPotency) - - ruthCp := 0.2 * float64(rogue.Talents.Ruthlessness) - - // estimate of energy per second while nothing is cast - energyPerSecond := func() float64 { - if rogue.Talents.CombatPotency == 0 { - return 10 * rogue.EnergyTickMultiplier - } - - spell := rogue.AutoAttacks.OHAuto() - at := rogue.AttackTables[rogue.CurrentTarget.UnitIndex] - - landChance := 1.0 - if miss := at.BaseMissChance + 0.19 - spell.PhysicalHitChance(at); miss > 0 { - landChance -= miss - } - if dodge := at.BaseDodgeChance - spell.ExpertisePercentage() - spell.Unit.PseudoStats.DodgeReduction; dodge > 0 { - landChance -= dodge - } - - landsPerSecond := landChance / rogue.AutoAttacks.OffhandSwingSpeed().Seconds() - - return 10*rogue.EnergyTickMultiplier + landsPerSecond*0.2*float64(rogue.Talents.CombatPotency)*3 - } - - // Glyph of Backstab support - var bonusDuration float64 - rupRemaining := func(sim *core.Simulation) time.Duration { - if dot := rogue.Rupture.CurDot(); dot.IsActive() { - return dot.RemainingDuration(sim) - } - return 0 - } - - if x.builder == rogue.Backstab && rogue.HasMajorGlyph(proto.RogueMajorGlyph_GlyphOfBackstab) { - bonusDuration = 6 - rupRemaining = func(sim *core.Simulation) time.Duration { - if dot := rogue.Rupture.CurDot(); dot.IsActive() { - dur := dot.RemainingDuration(sim) - dur += dot.TickLength * time.Duration(dot.MaxStacks+3-dot.NumberOfTicks) - return dur - } - return 0 - } - } - - // Garrote - if rogue.Rotation.OpenWithGarrote && !rogue.PseudoStats.InFrontOfTarget && rogue.IsStealthed() { - x.prios = append(x.prios, prio{ - func(sim *core.Simulation, rogue *Rogue) PriorityAction { - if rogue.CurrentEnergy() > rogue.Garrote.DefaultCast.Cost && rogue.IsStealthed() { - return Once - } - return Wait - }, - func(sim *core.Simulation, rogue *Rogue) bool { - return rogue.Garrote.Cast(sim, rogue.CurrentTarget) - }, - rogue.Garrote.DefaultCast.Cost, - }) - } - - // Slice And Dice - x.prios = append(x.prios, prio{ - func(sim *core.Simulation, rogue *Rogue) PriorityAction { - cp, e := rogue.ComboPoints(), rogue.CurrentEnergy() - - if sndDur := rogue.SliceAndDiceAura.RemainingDuration(sim); sndDur > 0 { - if cp == 5 { // pool for snd if pooling for rupture fails - rupDur := rupRemaining(sim) - if e+rupDur.Seconds()*energyPerSecond() > maxPool { - if e+sndDur.Seconds()*energyPerSecond() <= maxPool { - return Wait - } - } - return Skip - } - - if cp >= 1 { // don't build if it reduces uptime - if e+sndDur.Seconds()*energyPerSecond() < sndCost+bldCost || sndDur < time.Second { - return Wait - } - } - return Skip - } - - // end of fight - heuristically, 2s of snd beat a 3 CP eviscerate for DPE, and 3s are close to a 5 CP one. - if cp >= 3 && sim.GetRemainingDuration() < time.Duration(2000+600*cp)*time.Millisecond { - return Skip - } - - if cp >= 1 && e >= sndCost { - return Cast - } - if cp < 1 && e >= bldCost { - return Build - } - return Wait - }, - func(sim *core.Simulation, rogue *Rogue) bool { - return rogue.SliceAndDice.Cast(sim, rogue.CurrentTarget) - }, - sndCost, - }) - - // Expose armor - update this as well - if rogue.Rotation.ExposeArmorFrequency == proto.Rogue_Rotation_Once || rogue.Rotation.ExposeArmorFrequency == proto.Rogue_Rotation_Maintain { - hasCastExpose := false - x.prios = append(x.prios, prio{ - func(sim *core.Simulation, rogue *Rogue) PriorityAction { - if hasCastExpose && rogue.Rotation.ExposeArmorFrequency == proto.Rogue_Rotation_Once { - return Skip - } - timeLeft := rogue.ExposeArmorAuras.Get(rogue.CurrentTarget).RemainingDuration(sim) - minPoints := max(1, min(rogue.Rotation.MinimumComboPointsExposeArmor, 5)) - if rogue.Rotation.ExposeArmorFrequency != proto.Rogue_Rotation_Once { - minPoints = 1 - } - if timeLeft <= 0 { - if rogue.ComboPoints() < minPoints { - if rogue.CurrentEnergy() >= bldCost { - return Build - } else { - return Wait - } - } else { - if rogue.CurrentEnergy() >= rogue.ExposeArmor.DefaultCast.Cost { - return Cast - } else { - return Wait - } - } - } else { - energyGained := energyPerSecond() * timeLeft.Seconds() - cpGenerated := energyGained / bldCost - currentCp := float64(rogue.ComboPoints()) - if currentCp+cpGenerated > 5 { - return Skip - } else { - if currentCp < 5 { - if rogue.CurrentEnergy() >= bldCost { - return Build - } - } - return Wait - } - } - }, - func(sim *core.Simulation, rogue *Rogue) bool { - casted := rogue.ExposeArmor.Cast(sim, rogue.CurrentTarget) - if casted { - hasCastExpose = true - } - return casted - }, - rogue.ExposeArmor.DefaultCast.Cost, - }) - } - - // Enable CDs - x.prios = append(x.prios, prio{ - func(sim *core.Simulation, rogue *Rogue) PriorityAction { - for _, mcd := range rogue.GetMajorCooldowns() { - mcd.Enable() - } - return Once - }, - func(s *core.Simulation, r *Rogue) bool { - return true - }, - 0, - }) - - if rogue.Rotation.CombatFinisherPriority == proto.Rogue_Rotation_EviscerateRupture { - // this is the pre-3.3.3 "rupture-less" rotation, essentially - x.prios = append(x.prios, prio{ - func(sim *core.Simulation, rogue *Rogue) PriorityAction { - e, cp := rogue.CurrentEnergy(), rogue.ComboPoints() - - if dur := sim.GetRemainingDuration(); dur <= 10*time.Second { - // end of fight handling - build towards a 3+ cp eviscerate, or just sinister strike - switch cp { - case 5: - if e >= evisCost { - return Cast - } - return Wait - default: - if e+dur.Seconds()*energyPerSecond() >= bldCost+evisCost { - return Build - } - if cp >= 3 && e >= evisCost { - return Cast - } - if cp < 3 && e >= bldCost { - return Build - } - } - return Wait - } - - if cp >= 5 { - sndDur := rogue.SliceAndDiceAura.RemainingDuration(sim) - // correcting with ruthCp seems to be a loss, so we just use bldCost directly - if e+sndDur.Seconds()*energyPerSecond() >= evisCost+bldCost+sndCost { - return Cast - } - return Wait - } - return Build - }, - func(sim *core.Simulation, rogue *Rogue) bool { - return rogue.Eviscerate.Cast(sim, rogue.CurrentTarget) - }, - evisCost, - }) - - return - } - - const ruptureMinDuration = time.Second * 10 // heuristically, 4-5 rupture ticks are better DPE than eviscerate - - // seconds a 5 cp rupture can be delayed to match a 4 cp rupture's dps. for rup4to5 and rup3to4, this delay is < 2s, - // which also means that clipping 3 or 4 cp ruptures is usually a dps loss - rup4to5 := (rogue.RuptureDuration(4).Seconds() + bonusDuration) * (1 - rogue.RuptureDamage(4)/rogue.RuptureDamage(5)) - rup3to4 := (rogue.RuptureDuration(3).Seconds() + bonusDuration) * (1 - rogue.RuptureDamage(3)/rogue.RuptureDamage(4)) - - // Rupture - x.prios = append(x.prios, prio{ - func(sim *core.Simulation, rogue *Rogue) PriorityAction { - cp, e := rogue.ComboPoints(), rogue.CurrentEnergy() - - if sim.GetRemainingDuration() < ruptureMinDuration { - return Skip - } - - rupDot := rogue.Rupture.CurDot() - if !rupDot.IsActive() { - if cp == 5 && e >= rupCost { - return Cast - } - if cp == 4 && e+rup4to5*energyPerSecond() < bldCost+rupCost { - return Cast - } - if cp == 3 && e+rup3to4*energyPerSecond() < bldCost+rupCost { - return Cast - } - if e >= bldCost { - return Build - } - return Wait - } - - // there's ample time to rebuild, simply skip - dur := rupRemaining(sim).Seconds() - if e+dur*baseEps > maxPool { - return Skip - } - - if cp == 5 { - if e+dur*energyPerSecond() > maxPool { - return Skip // can't pool any longer, maybe we can fit in Eviscerate - } - return Wait - } - if cp == 4 && e+(dur+rup4to5)*energyPerSecond() < bldCost+rupCost { - return Wait - } - if cp == 3 && e+(dur+rup3to4)*energyPerSecond() < bldCost+rupCost { - return Wait - } - if e >= bldCost { - return Build - } - return Wait - }, - func(sim *core.Simulation, rogue *Rogue) bool { - return rogue.Rupture.Cast(sim, rogue.CurrentTarget) - }, - rupCost, - }) - - bldPerCp := 1.0 - if x.builder == rogue.SinisterStrike && rogue.HasMajorGlyph(proto.RogueMajorGlyph_GlyphOfSinisterStrike) { - attackTable := rogue.AttackTables[rogue.CurrentTarget.UnitIndex] - crit := rogue.SinisterStrike.PhysicalCritChance(attackTable) - bldPerCp = 1 / (1 + crit*(0.5+0.2*float64(rogue.Talents.SealFate))) - } - if x.builder == rogue.Backstab && rogue.Talents.SealFate > 0 { - attackTable := rogue.AttackTables[rogue.CurrentTarget.UnitIndex] - crit := rogue.Backstab.PhysicalCritChance(attackTable) - bldPerCp = 1 / (1 + crit*(0.2*float64(rogue.Talents.SealFate))) - } - - // Eviscerate - x.prios = append(x.prios, prio{ - func(sim *core.Simulation, rogue *Rogue) PriorityAction { - e, cp := rogue.CurrentEnergy(), rogue.ComboPoints() - - if dur := sim.GetRemainingDuration(); dur <= ruptureMinDuration { - // end of fight handling - build towards a 3+ cp eviscerate, or just sinister strike - switch cp { - case 5: - if e >= evisCost { - return Cast - } - return Wait - default: - if e+dur.Seconds()*energyPerSecond() >= bldCost+evisCost { - return Build - } - if cp >= 3 && e >= evisCost { - return Cast - } - if cp < 3 && e >= bldCost { - return Build - } - } - return Wait - } - - // we only get here if there's ample time left on rupture, or rupture pooling failed: in these cases, we - // can try to fill in a 5 cp eviscerate, if it's not too disruptive. lower cp eviscerates aren't worth it, - // since sinister spam isn't all that much worse - if cp <= 4 { - return Build - } - - cost := evisCost + (4-ruthCp)*bldCost*bldPerCp + rupCost - - rupDur := rupRemaining(sim) - sndDur := rogue.SliceAndDiceAura.RemainingDuration(sim) - if sndDur < rupDur { - cost += sndCost + (1-ruthCp)*bldCost*bldPerCp - } - - if avail := e + rupDur.Seconds()*energyPerSecond(); avail >= cost { - return Cast - } - - // we'd lose a CP here, so we just wait... - if e <= maxPool { - return Wait - } - - // ... and if that doesn't work, allow to clip snd - if sndDur < rogue.sliceAndDiceDurations[2]-rogue.sliceAndDiceDurations[1] { - rogue.SliceAndDice.Cast(sim, rogue.CurrentTarget) - return Wait - } - - return Build - }, - func(sim *core.Simulation, rogue *Rogue) bool { - return rogue.Eviscerate.Cast(sim, rogue.CurrentTarget) - }, - evisCost, - }) -} - -func (x *rotation_combat) run(sim *core.Simulation, rogue *Rogue) { - for i := 0; i < len(x.prios); i++ { - switch p := x.prios[i]; p.check(sim, rogue) { - case Skip: - continue - case Build: - //Handle Ghostly Strike. This is badly copy-pasted code, and is not considered in a regular raid setting. - if rogue.Talents.GhostlyStrike && rogue.Rotation.UseGhostlyStrike && rogue.GhostlyStrike.IsReady(sim) { - x.builder = rogue.GhostlyStrike - - if !x.builder.Cast(sim, rogue.CurrentTarget) { - rogue.WaitForEnergy(sim, x.builder.DefaultCast.Cost) - - x.builder = rogue.SinisterStrike - if rogue.Rotation.CombatBuilder == proto.Rogue_Rotation_Backstab && rogue.HasDagger(core.MainHand) && !rogue.PseudoStats.InFrontOfTarget { - x.builder = rogue.Backstab - } - if rogue.Talents.Hemorrhage && rogue.Rotation.CombatBuilder == proto.Rogue_Rotation_HemorrhageCombat { - x.builder = rogue.Hemorrhage - } - - return - } - - x.builder = rogue.SinisterStrike - if rogue.Rotation.CombatBuilder == proto.Rogue_Rotation_Backstab && rogue.HasDagger(core.MainHand) && !rogue.PseudoStats.InFrontOfTarget { - x.builder = rogue.Backstab - } - if rogue.Talents.Hemorrhage && rogue.Rotation.CombatBuilder == proto.Rogue_Rotation_HemorrhageCombat { - x.builder = rogue.Hemorrhage - } - //Done with Ghostly Strike - } else if !x.builder.Cast(sim, rogue.CurrentTarget) { - rogue.WaitForEnergy(sim, x.builder.DefaultCast.Cost) - return - } - case Cast: - if !p.cast(sim, rogue) { - rogue.WaitForEnergy(sim, p.cost) - return - } - case Once: - if !p.cast(sim, rogue) { - rogue.WaitForEnergy(sim, p.cost) - return - } - x.prios = slices.Delete(x.prios, i, i+1) - i-- - case Wait: - rogue.DoNothing() - return - } - - if !rogue.GCD.IsReady(sim) { - return - } - } - log.Panic("skipped all prios") -} diff --git a/sim/rogue/rotation_generic.go b/sim/rogue/rotation_generic.go deleted file mode 100644 index a3ad5da4f7..0000000000 --- a/sim/rogue/rotation_generic.go +++ /dev/null @@ -1,430 +0,0 @@ -package rogue - -import ( - "log" - "slices" - "time" - - "github.com/wowsims/wotlk/sim/core" - "github.com/wowsims/wotlk/sim/core/proto" -) - -type rotation_generic struct { - prios []prio - - builder *core.Spell -} - -func (x *rotation_generic) setup(_ *core.Simulation, rogue *Rogue) { - x.prios = x.prios[:0] - - x.builder = rogue.SinisterStrike - if rogue.HasDagger(core.MainHand) && !rogue.PseudoStats.InFrontOfTarget { - x.builder = rogue.Backstab - } - if rogue.CanMutilate() { - x.builder = rogue.Mutilate - } - if rogue.Talents.Hemorrhage { - x.builder = rogue.Hemorrhage - } - - bldCost := x.builder.DefaultCast.Cost - sndCost := rogue.SliceAndDice.DefaultCast.Cost - rupCost := rogue.Rupture.DefaultCast.Cost - - baseEps := 10 * rogue.EnergyTickMultiplier - maxPool := rogue.maxEnergy - 3*float64(rogue.Talents.CombatPotency) - 2*float64(rogue.Talents.FocusedAttacks)/3.0 - - ruthCp := 0.2 * float64(rogue.Talents.Ruthlessness) - rsPerCp := float64(rogue.Talents.RelentlessStrikes) - - // estimate of energy per second while nothing is cast - energyPerSecond := func() float64 { - var eps float64 - if rogue.Talents.CombatPotency > 0 { - spell := rogue.AutoAttacks.OHAuto() - at := rogue.AttackTables[rogue.CurrentTarget.UnitIndex] - - landChance := 1.0 - if miss := at.BaseMissChance + 0.19 - spell.PhysicalHitChance(at); miss > 0 { - landChance -= miss - } - if dodge := at.BaseDodgeChance - spell.ExpertisePercentage() - spell.Unit.PseudoStats.DodgeReduction; dodge > 0 { - landChance -= dodge - } - - landsPerSecond := landChance / rogue.AutoAttacks.OffhandSwingSpeed().Seconds() - - eps += landsPerSecond * 0.2 * float64(rogue.Talents.CombatPotency) * 3 - } - if rogue.Talents.FocusedAttacks > 0 { - getCritChance := func(spell *core.Spell) float64 { - at := rogue.AttackTables[rogue.CurrentTarget.UnitIndex] - - critCap := 1.0 - at.BaseGlanceChance - if miss := at.BaseMissChance + 0.19 - spell.PhysicalHitChance(at); miss > 0 { - critCap -= miss - } - if dodge := at.BaseDodgeChance - spell.ExpertisePercentage() - rogue.PseudoStats.DodgeReduction; dodge > 0 { - critCap -= dodge - } - - critChance := spell.PhysicalCritChance(at) - if critChance > critCap { - critChance = critCap - } - return critChance - } - - critsPerSecond := getCritChance(rogue.AutoAttacks.MHAuto())/rogue.AutoAttacks.MainhandSwingSpeed().Seconds() + - getCritChance(rogue.AutoAttacks.OHAuto())/rogue.AutoAttacks.OffhandSwingSpeed().Seconds() - procChance := []float64{0, 0.33, 0.66, 1}[rogue.Talents.FocusedAttacks] - - eps += critsPerSecond * procChance * 2 - } - return 10*rogue.EnergyTickMultiplier + eps - } - - // Glyph of Backstab support - var bonusDuration float64 - rupRemaining := func(sim *core.Simulation) time.Duration { - if dot := rogue.Rupture.CurDot(); dot.IsActive() { - return dot.RemainingDuration(sim) - } - return 0 - } - - if x.builder == rogue.Backstab && rogue.HasMajorGlyph(proto.RogueMajorGlyph_GlyphOfBackstab) { - bonusDuration = 6 - rupRemaining = func(sim *core.Simulation) time.Duration { - if dot := rogue.Rupture.CurDot(); dot.IsActive() { - dur := dot.RemainingDuration(sim) - dur += dot.TickLength * time.Duration(dot.MaxStacks+3-dot.NumberOfTicks) - return dur - } - return 0 - } - } - - // Garrote - if rogue.Rotation.OpenWithGarrote && !rogue.PseudoStats.InFrontOfTarget { - x.prios = append(x.prios, prio{ - func(sim *core.Simulation, rogue *Rogue) PriorityAction { - if rogue.CurrentEnergy() > rogue.Garrote.DefaultCast.Cost { - return Once - } - return Wait - }, - func(sim *core.Simulation, rogue *Rogue) bool { - return rogue.Garrote.Cast(sim, rogue.CurrentTarget) - }, - rogue.Garrote.DefaultCast.Cost, - }) - } - - // Slice And Dice - x.prios = append(x.prios, prio{ - func(sim *core.Simulation, rogue *Rogue) PriorityAction { - cp, e := rogue.ComboPoints(), rogue.CurrentEnergy() - - if sndDur := rogue.SliceAndDiceAura.RemainingDuration(sim); sndDur > 0 { - if cp == 5 { // pool for snd if pooling for rupture fails - rupDur := rupRemaining(sim) - if e+rupDur.Seconds()*energyPerSecond() > maxPool { - if e+sndDur.Seconds()*energyPerSecond() <= maxPool { - return Wait - } - } - return Skip - } - - if cp >= 1 { // don't build if it reduces uptime - if e+sndDur.Seconds()*energyPerSecond() < sndCost+bldCost || sndDur < time.Second { - return Wait - } - } - return Skip - } - - // end of fight - heuristically, 2s of snd beat a 3 CP eviscerate for DPE, and 3s are close to a 5 CP one. - if cp >= 3 && sim.GetRemainingDuration() < time.Duration(2000+600*cp)*time.Millisecond { - return Skip - } - - if cp >= 1 && e >= sndCost { - return Cast - } - if cp < 1 && e >= bldCost { - return Build - } - return Wait - }, - func(sim *core.Simulation, rogue *Rogue) bool { - return rogue.SliceAndDice.Cast(sim, rogue.CurrentTarget) - }, - sndCost, - }) - - // Expose armor - update this as well - if rogue.Rotation.ExposeArmorFrequency == proto.Rogue_Rotation_Once || rogue.Rotation.ExposeArmorFrequency == proto.Rogue_Rotation_Maintain { - hasCastExpose := false - x.prios = append(x.prios, prio{ - func(sim *core.Simulation, rogue *Rogue) PriorityAction { - if hasCastExpose && rogue.Rotation.ExposeArmorFrequency == proto.Rogue_Rotation_Once { - return Skip - } - timeLeft := rogue.ExposeArmorAuras.Get(rogue.CurrentTarget).RemainingDuration(sim) - minPoints := max(1, min(rogue.Rotation.MinimumComboPointsExposeArmor, 5)) - if rogue.Rotation.ExposeArmorFrequency != proto.Rogue_Rotation_Once { - minPoints = 1 - } - if timeLeft <= 0 { - if rogue.ComboPoints() < minPoints { - if rogue.CurrentEnergy() >= bldCost { - return Build - } else { - return Wait - } - } else { - if rogue.CurrentEnergy() >= rogue.ExposeArmor.DefaultCast.Cost { - return Cast - } else { - return Wait - } - } - } else { - energyGained := energyPerSecond() * timeLeft.Seconds() - cpGenerated := energyGained / bldCost - currentCp := float64(rogue.ComboPoints()) - if currentCp+cpGenerated > 5 { - return Skip - } else { - if currentCp < 5 { - if rogue.CurrentEnergy() >= bldCost { - return Build - } - } - return Wait - } - } - }, - func(sim *core.Simulation, rogue *Rogue) bool { - casted := rogue.ExposeArmor.Cast(sim, rogue.CurrentTarget) - if casted { - hasCastExpose = true - } - return casted - }, - rogue.ExposeArmor.DefaultCast.Cost, - }) - } - - // Enable CDs - x.prios = append(x.prios, prio{ - func(sim *core.Simulation, rogue *Rogue) PriorityAction { - for _, mcd := range rogue.GetMajorCooldowns() { - mcd.Enable() - } - return Once - }, - func(s *core.Simulation, r *Rogue) bool { - return true - }, - 0, - }) - - const ruptureMinDuration = time.Second * 10 // heuristically, 4-5 rupture ticks are better DPE than eviscerate - - // seconds a 5 cp rupture can be delayed to match a 4 cp rupture's dps. for rup4to5 and rup3to4, this delay is < 2s, - // which also means that clipping 3 or 4 cp ruptures is usually a dps loss - rup4to5 := (rogue.RuptureDuration(4).Seconds() + bonusDuration) * (1 - rogue.RuptureDamage(4)/rogue.RuptureDamage(5)) - rup3to4 := (rogue.RuptureDuration(3).Seconds() + bonusDuration) * (1 - rogue.RuptureDamage(3)/rogue.RuptureDamage(4)) - - // Rupture - x.prios = append(x.prios, prio{ - func(sim *core.Simulation, rogue *Rogue) PriorityAction { - cp, e := rogue.ComboPoints(), rogue.CurrentEnergy() - - if sim.GetRemainingDuration() < ruptureMinDuration { - return Skip - } - - rupDot := rogue.Rupture.CurDot() - if !rupDot.IsActive() { - if cp == 5 && e >= rupCost { - return Cast - } - if cp == 4 && e+rup4to5*energyPerSecond() < bldCost+rupCost { - return Cast - } - if cp == 3 && e+rup3to4*energyPerSecond() < bldCost+rupCost { - return Cast - } - if e >= bldCost { - return Build - } - return Wait - } - - // there's ample time to rebuild, simply skip - dur := rupRemaining(sim).Seconds() - if e+dur*baseEps > maxPool { - return Skip - } - - if cp == 5 { - if e+dur*energyPerSecond() > maxPool { - return Skip // can't pool any longer, maybe we can fit in Eviscerate - } - return Wait - } - if cp == 4 && e+(dur+rup4to5)*energyPerSecond() < bldCost+rupCost { - return Wait - } - if cp == 3 && e+(dur+rup3to4)*energyPerSecond() < bldCost+rupCost { - return Wait - } - if e >= bldCost { - return Build - } - return Wait - }, - func(sim *core.Simulation, rogue *Rogue) bool { - return rogue.Rupture.Cast(sim, rogue.CurrentTarget) - }, - rupCost, - }) - - bldPerCp := 1.0 - if x.builder == rogue.SinisterStrike { - attackTable := rogue.AttackTables[rogue.CurrentTarget.UnitIndex] - crit := rogue.SinisterStrike.PhysicalCritChance(attackTable) - var extraChance float64 - if rogue.HasMajorGlyph(proto.RogueMajorGlyph_GlyphOfSinisterStrike) { - extraChance = 0.5 - } - bldPerCp = 1 / (1 + crit*(extraChance+0.2*float64(rogue.Talents.SealFate))) - } - if x.builder == rogue.Backstab { - attackTable := rogue.AttackTables[rogue.CurrentTarget.UnitIndex] - crit := rogue.Backstab.PhysicalCritChance(attackTable) - bldPerCp = 1 / (1 + crit*(0.2*float64(rogue.Talents.SealFate))) - } - if x.builder == rogue.Hemorrhage { - attackTable := rogue.AttackTables[rogue.CurrentTarget.UnitIndex] - crit := rogue.Hemorrhage.PhysicalCritChance(attackTable) - bldPerCp = 1 / (1 + crit*(0.2*float64(rogue.Talents.SealFate))) - } - if x.builder == rogue.Mutilate { - attackTable := rogue.AttackTables[rogue.CurrentTarget.UnitIndex] - critMH := rogue.MutilateMH.PhysicalCritChance(attackTable) - critOH := rogue.MutilateOH.PhysicalCritChance(attackTable) - crit := 1 - (1-critMH)*(1-critOH) - bldPerCp = 1 / (2 + crit*(0.2*float64(rogue.Talents.SealFate))) - } - - // direct damage finisher (Eviscerate/Envenom) - finisher := rogue.Eviscerate - if rogue.Talents.MasterPoisoner > 0 { - finisher = rogue.Envenom - } - finisherCost := finisher.DefaultCast.Cost - x.prios = append(x.prios, prio{ - func(sim *core.Simulation, rogue *Rogue) PriorityAction { - e, cp := rogue.CurrentEnergy(), rogue.ComboPoints() - - if dur := sim.GetRemainingDuration(); dur <= ruptureMinDuration { - // end of fight handling - build towards a 3+ cp finisher, or just spam the builder - switch cp { - case 5: - if e >= finisherCost { - return Cast - } - return Wait - default: - if e+dur.Seconds()*energyPerSecond() >= bldCost+finisherCost { - return Build - } - if cp >= 3 && e >= finisherCost { - return Cast - } - if cp < 3 && e >= bldCost { - return Build - } - } - return Wait - } - - // we only get here if there's ample time left on rupture, or rupture pooling failed: in these cases, we - // can try to fill in a 5 cp finisher, if it's not too disruptive. lower cp finishers aren't worth it, - // since builder spam isn't all that much worse - if cp <= 4 { - return Build - } - - cost := finisherCost - 5*rsPerCp + (4-ruthCp)*bldCost*bldPerCp + rupCost - - rupDur := rupRemaining(sim) - sndDur := rogue.SliceAndDiceAura.RemainingDuration(sim) - if sndDur < rupDur { - cost += sndCost - 1*rsPerCp + (1-ruthCp)*bldCost*bldPerCp - } - - if avail := e + rupDur.Seconds()*energyPerSecond(); avail >= cost { - return Cast - } - - // we'd lose a CP here, so we just wait... - if e <= maxPool { - return Wait - } - - // ... and if that doesn't work, allow to clip snd - if sndDur < rogue.sliceAndDiceDurations[2]-rogue.sliceAndDiceDurations[1] { - rogue.SliceAndDice.Cast(sim, rogue.CurrentTarget) - return Wait - } - - return Build - }, - func(sim *core.Simulation, rogue *Rogue) bool { - return finisher.Cast(sim, rogue.CurrentTarget) - }, - finisherCost, - }) -} - -func (x *rotation_generic) run(sim *core.Simulation, rogue *Rogue) { - for i := 0; i < len(x.prios); i++ { - switch p := x.prios[i]; p.check(sim, rogue) { - case Skip: - continue - case Build: - if !x.builder.Cast(sim, rogue.CurrentTarget) { - rogue.WaitForEnergy(sim, x.builder.DefaultCast.Cost) - return - } - case Cast: - if !p.cast(sim, rogue) { - rogue.WaitForEnergy(sim, p.cost) - return - } - case Once: - if !p.cast(sim, rogue) { - rogue.WaitForEnergy(sim, p.cost) - return - } - x.prios = slices.Delete(x.prios, i, i+1) - i-- - case Wait: - rogue.DoNothing() - return - } - - if !rogue.GCD.IsReady(sim) { - return - } - } - log.Panic("skipped all prios") -} diff --git a/sim/rogue/rotation_multi.go b/sim/rogue/rotation_multi.go deleted file mode 100644 index b9000428f8..0000000000 --- a/sim/rogue/rotation_multi.go +++ /dev/null @@ -1,405 +0,0 @@ -package rogue - -import ( - "math" - "time" - - "github.com/wowsims/wotlk/sim/core" - "github.com/wowsims/wotlk/sim/core/proto" -) - -type rogueRotationItem struct { - ExpiresAt time.Duration - MinimumBuildDuration time.Duration - MaximumBuildDuration time.Duration - PrioIndex int -} - -type roguePriorityItem struct { - Aura *core.Aura - CastCount int32 - EnergyCost float64 - PoolAmount float64 - GetDuration func(*Rogue, int32) time.Duration - GetSpell func(*Rogue, int32) *core.Spell - IsFiller bool - MaximumComboPoints int32 - MaxCasts int32 - MinimumComboPoints int32 -} - -type shouldCastRotationItemResult int32 - -const ( - ShouldNotCast shouldCastRotationItemResult = iota - ShouldBuild - ShouldCast - ShouldWait -) - -type rotation_multi struct { - priorityItems []roguePriorityItem - rotationItems []rogueRotationItem - - builder *core.Spell - builderPoints int32 -} - -func (x *rotation_multi) setup(sim *core.Simulation, rogue *Rogue) { - x.builder = rogue.SinisterStrike - x.builderPoints = 1 - - if rogue.CanMutilate() { - x.builder = rogue.Mutilate - x.builderPoints = 2 - } - - if rogue.Talents.Hemorrhage { - x.builder = rogue.Hemorrhage - x.builderPoints = 1 - } - - if rogue.Talents.SlaughterFromTheShadows > 0 && !rogue.Rotation.HemoWithDagger && !rogue.PseudoStats.InFrontOfTarget && rogue.HasDagger(core.MainHand) { - x.builder = rogue.Backstab - x.builderPoints = 1 - } - - // Slice and Dice - x.priorityItems = x.priorityItems[:0] - - sliceAndDice := roguePriorityItem{ - MinimumComboPoints: 1, - MaximumComboPoints: 5, - Aura: rogue.SliceAndDiceAura, - EnergyCost: rogue.SliceAndDice.DefaultCast.Cost, - GetDuration: func(rogue *Rogue, cp int32) time.Duration { - return rogue.sliceAndDiceDurations[cp] - }, - GetSpell: func(rogue *Rogue, cp int32) *core.Spell { - return rogue.SliceAndDice - }, - } - if rogue.Rotation.MultiTargetSliceFrequency != proto.Rogue_Rotation_Never { - sliceAndDice.MinimumComboPoints = max(1, rogue.Rotation.MinimumComboPointsMultiTargetSlice) - if rogue.Rotation.MultiTargetSliceFrequency == proto.Rogue_Rotation_Once { - sliceAndDice.MaxCasts = 1 - } - x.priorityItems = append(x.priorityItems, sliceAndDice) - } - - // Expose Armor - if rogue.Rotation.ExposeArmorFrequency == proto.Rogue_Rotation_Maintain || - rogue.Rotation.ExposeArmorFrequency == proto.Rogue_Rotation_Once { - minPoints := int32(1) - maxCasts := int32(0) - if rogue.Rotation.ExposeArmorFrequency == proto.Rogue_Rotation_Once { - minPoints = rogue.Rotation.MinimumComboPointsExposeArmor - maxCasts = 1 - } - x.priorityItems = append(x.priorityItems, roguePriorityItem{ - MaxCasts: maxCasts, - MaximumComboPoints: 5, - MinimumComboPoints: minPoints, - Aura: rogue.ExposeArmorAuras.Get(rogue.CurrentTarget), - EnergyCost: rogue.ExposeArmor.DefaultCast.Cost, - GetDuration: func(rogue *Rogue, cp int32) time.Duration { - return rogue.exposeArmorDurations[cp] - }, - GetSpell: func(rogue *Rogue, cp int32) *core.Spell { - return rogue.ExposeArmor - }, - }) - } - - // Hunger for Blood - if rogue.Talents.HungerForBlood { - x.priorityItems = append(x.priorityItems, roguePriorityItem{ - MaximumComboPoints: 0, - Aura: rogue.HungerForBloodAura, - EnergyCost: rogue.HungerForBlood.DefaultCast.Cost, - GetDuration: func(rogue *Rogue, cp int32) time.Duration { - return rogue.HungerForBloodAura.Duration - }, - GetSpell: func(rogue *Rogue, cp int32) *core.Spell { - return rogue.HungerForBlood - }, - }) - } - - // Dummy priority to enable CDs - x.priorityItems = append(x.priorityItems, roguePriorityItem{ - MaxCasts: 1, - MaximumComboPoints: 0, - GetDuration: func(rogue *Rogue, cp int32) time.Duration { - return 0 - }, - GetSpell: func(rogue *Rogue, cp int32) *core.Spell { - if rogue.allMCDsDisabled { - for _, mcd := range rogue.GetMajorCooldowns() { - mcd.Enable() - } - rogue.allMCDsDisabled = false - } - return nil - }, - }) - - x.priorityItems = append(x.priorityItems, roguePriorityItem{ - MaximumComboPoints: 0, - EnergyCost: rogue.FanOfKnives.DefaultCast.Cost, - GetSpell: func(rogue *Rogue, i int32) *core.Spell { - return rogue.FanOfKnives - }, - }) - x.rotationItems = x.planRotation(sim, rogue) -} - -func (x *rotation_multi) run(sim *core.Simulation, rogue *Rogue) { - if len(x.rotationItems) < 1 { - panic("Rotation is empty") - } - eps := x.getExpectedEnergyPerSecond(rogue) - shouldCast := x.shouldCastNextRotationItem(sim, rogue, eps) - item := x.rotationItems[0] - prio := x.priorityItems[item.PrioIndex] - - switch shouldCast { - case ShouldNotCast: - x.rotationItems = x.rotationItems[1:] - x.run(sim, rogue) - case ShouldBuild: - spell := x.builder - if spell == nil || spell.Cast(sim, rogue.CurrentTarget) { - if rogue.GCD.IsReady(sim) { - x.run(sim, rogue) - } - } else { - panic("Unexpected builder cast failure") - } - case ShouldCast: - spell := prio.GetSpell(rogue, rogue.ComboPoints()) - if spell == nil || spell.Cast(sim, rogue.CurrentTarget) { - x.priorityItems[item.PrioIndex].CastCount += 1 - x.rotationItems = x.planRotation(sim, rogue) - if rogue.GCD.IsReady(sim) { - x.run(sim, rogue) - } - } else { - panic("Unexpected cast failure") - } - case ShouldWait: - desiredEnergy := 100.0 - if rogue.ComboPoints() == 5 { - desiredEnergy = prio.EnergyCost - } else { - if rogue.CurrentEnergy() < prio.EnergyCost && rogue.ComboPoints() >= prio.MinimumComboPoints { - desiredEnergy = prio.EnergyCost - } else if rogue.ComboPoints() < 5 { - desiredEnergy = x.builder.DefaultCast.Cost - } - } - cdAvailableTime := time.Second * 10 - if sim.CurrentTime > cdAvailableTime { - cdAvailableTime = core.NeverExpires - } - nextExpiration := cdAvailableTime - for _, otherItem := range x.rotationItems { - if otherItem.ExpiresAt < nextExpiration { - nextExpiration = otherItem.ExpiresAt - } - } - neededEnergy := desiredEnergy - rogue.CurrentEnergy() - energyAvailableTime := time.Second*time.Duration(neededEnergy/eps) + 1*time.Second - energyAt := sim.CurrentTime + energyAvailableTime - if energyAt < nextExpiration { - rogue.WaitForEnergy(sim, desiredEnergy) - } else if nextExpiration > sim.CurrentTime { - rogue.WaitUntil(sim, nextExpiration) - } else { - rogue.DoNothing() - } - } -} - -func (x *rotation_multi) energyToBuild(points int32) float64 { - costPerBuilder := x.builder.DefaultCast.Cost - - buildersNeeded := math.Ceil(float64(points) / float64(x.builderPoints)) - return buildersNeeded * costPerBuilder -} - -func (x *rotation_multi) timeToBuild(points int32, builderPoints int32, eps float64, finisherCost float64) time.Duration { - energyNeeded := x.energyToBuild(points) + finisherCost - secondsNeeded := energyNeeded / eps - globalsNeeded := math.Ceil(float64(points)/float64(builderPoints)) + 1 - // Return greater of the time it takes to use the globals and the time it takes to build the energy - return max(time.Second*time.Duration(secondsNeeded), time.Second*time.Duration(globalsNeeded)) -} - -func (x *rotation_multi) shouldCastNextRotationItem(sim *core.Simulation, rogue *Rogue, eps float64) shouldCastRotationItemResult { - if len(x.rotationItems) == 0 { - panic("Empty rotation") - } - currentEnergy := rogue.CurrentEnergy() - comboPoints := rogue.ComboPoints() - currentTime := sim.CurrentTime - item := x.rotationItems[0] - prio := x.priorityItems[item.PrioIndex] - tte := item.ExpiresAt - currentTime - clippingThreshold := time.Second * 2 - timeUntilNextGCD := rogue.GCD.TimeToReady(sim) - - // Check to see if a higher prio item will expire - if len(x.rotationItems) >= 2 { - timeElapsed := time.Second * 1 - for _, nextItem := range x.rotationItems[1:] { - if nextItem.ExpiresAt <= currentTime+timeElapsed { - return ShouldNotCast - } - timeElapsed += nextItem.MinimumBuildDuration - } - } - - // Expires before next GCD - if tte <= timeUntilNextGCD { - if comboPoints >= prio.MinimumComboPoints && currentEnergy >= (prio.EnergyCost+prio.PoolAmount) { - return ShouldCast - } else if comboPoints < prio.MinimumComboPoints && currentEnergy >= x.builder.DefaultCast.Cost { - return ShouldBuild - } else { - return ShouldWait - } - } - if comboPoints >= prio.MaximumComboPoints { // Don't need CP - // Cast - if tte <= clippingThreshold && currentEnergy >= (prio.EnergyCost+prio.PoolAmount) { - return ShouldCast - } - // Pool energy - if tte <= clippingThreshold && currentEnergy < (prio.EnergyCost+prio.PoolAmount) { - return ShouldWait - } - // We have time to squeeze in another spell - if tte > item.MinimumBuildDuration { - // Find the first lower prio item that can be cast and use it - for lpi, lowerPrio := range x.priorityItems[item.PrioIndex+1:] { - if comboPoints > lowerPrio.MinimumComboPoints && currentEnergy > lowerPrio.EnergyCost && lowerPrio.MaxCasts == 0 { - x.rotationItems = append([]rogueRotationItem{ - {ExpiresAt: currentTime, PrioIndex: lpi + item.PrioIndex + 1}, - }, x.rotationItems...) - return ShouldCast - } - } - } - // Overcap CP with builder - if x.timeToBuild(1, x.builderPoints, eps, prio.EnergyCost+prio.PoolAmount) <= tte && currentEnergy >= x.builder.DefaultCast.Cost { - return ShouldBuild - } - } else if comboPoints < prio.MinimumComboPoints { // Need CP - if currentEnergy >= x.builder.DefaultCast.Cost { - return ShouldBuild - } else { - return ShouldWait - } - } else { // Between MinimumComboPoints and MaximumComboPoints - if currentEnergy >= prio.EnergyCost+prio.PoolAmount && tte <= timeUntilNextGCD { - return ShouldCast - } - ttb := x.timeToBuild(1, 2, eps, prio.EnergyCost+prio.PoolAmount-currentEnergy) - if currentEnergy >= x.builder.DefaultCast.Cost && tte > ttb { - return ShouldBuild - } - } - return ShouldWait -} - -func (x *rotation_multi) getExpectedEnergyPerSecond(rogue *Rogue) float64 { - const finishersPerSecond = 1.0 / 6 - const averageComboPointsSpendOnFinisher = 4.0 - bonusEnergyPerSecond := float64(rogue.Talents.CombatPotency) * 3 * 0.2 * 1.0 / (rogue.AutoAttacks.OH().SwingSpeed / 1.4) - bonusEnergyPerSecond += float64(rogue.Talents.FocusedAttacks) - bonusEnergyPerSecond += float64(rogue.Talents.RelentlessStrikes) * 0.04 * 25 * finishersPerSecond * averageComboPointsSpendOnFinisher - return (core.EnergyPerTick*rogue.EnergyTickMultiplier)/core.EnergyTickDuration.Seconds() + bonusEnergyPerSecond -} - -func (x *rotation_multi) planRotation(sim *core.Simulation, rogue *Rogue) []rogueRotationItem { - var rotationItems []rogueRotationItem - eps := x.getExpectedEnergyPerSecond(rogue) - for pi, prio := range x.priorityItems { - if prio.MaxCasts > 0 && prio.CastCount >= prio.MaxCasts { - continue - } - maxCP := prio.MaximumComboPoints - for maxCP > 0 && prio.GetDuration(rogue, maxCP)+sim.CurrentTime > sim.Duration { - maxCP-- - } - var expiresAt time.Duration - if prio.Aura != nil { - expiresAt = prio.Aura.ExpiresAt() - } else if prio.MaxCasts == 1 { - expiresAt = sim.CurrentTime // TODO looks fishy, repeated expiresAt = sim.CurrentTime - } else { - expiresAt = sim.CurrentTime - } - minimumBuildDuration := x.timeToBuild(prio.MinimumComboPoints, x.builderPoints, eps, prio.EnergyCost) - maximumBuildDuration := x.timeToBuild(maxCP, x.builderPoints, eps, prio.EnergyCost) - rotationItems = append(rotationItems, rogueRotationItem{ - ExpiresAt: expiresAt, - MaximumBuildDuration: maximumBuildDuration, - MinimumBuildDuration: minimumBuildDuration, - PrioIndex: pi, - }) - } - - currentTime := sim.CurrentTime - comboPoints := rogue.ComboPoints() - currentEnergy := rogue.CurrentEnergy() - - var prioStack []rogueRotationItem - for _, item := range rotationItems { - if item.ExpiresAt >= sim.Duration { - continue - } - prio := x.priorityItems[item.PrioIndex] - maxBuildAt := item.ExpiresAt - item.MaximumBuildDuration - if prio.Aura == nil { - timeValueOfResources := time.Duration((float64(comboPoints)*x.builder.DefaultCast.Cost/float64(x.builderPoints) + currentEnergy) / eps) - maxBuildAt = currentTime - item.MaximumBuildDuration - timeValueOfResources - } - if currentTime < maxBuildAt { - // Put it on the to cast stack - prioStack = append(prioStack, item) - if prio.MinimumComboPoints > 0 { - comboPoints = 0 - } - currentTime += item.MaximumBuildDuration - } else { - cpUsed := max(0, prio.MinimumComboPoints-comboPoints) - energyUsed := max(0, prio.EnergyCost-currentEnergy) - minBuildTime := x.timeToBuild(cpUsed, x.builderPoints, eps, energyUsed) - if currentTime+minBuildTime <= item.ExpiresAt || !prio.IsFiller { - prioStack = append(prioStack, item) - currentTime = max(item.ExpiresAt, currentTime+minBuildTime) - currentEnergy = 0 - if prio.MinimumComboPoints > 0 { - comboPoints = 0 - } - } else if len(prioStack) < 1 || (prio.Aura != nil && !prio.Aura.IsActive() && !prio.IsFiller) || prio.MaxCasts == 1 { - // Plan to cast it as soon as possible - prioStack = append(prioStack, item) - currentTime += item.MinimumBuildDuration - currentEnergy = 0 - if prio.MinimumComboPoints > 0 { - comboPoints = 0 - } - } - } - } - - // Reverse - for i, j := 0, len(prioStack)-1; i < j; i, j = i+1, j-1 { - prioStack[i], prioStack[j] = prioStack[j], prioStack[i] - } - - return prioStack -} diff --git a/sim/rogue/rotation_subtlety.go b/sim/rogue/rotation_subtlety.go deleted file mode 100644 index d1d5a51224..0000000000 --- a/sim/rogue/rotation_subtlety.go +++ /dev/null @@ -1,340 +0,0 @@ -package rogue - -import ( - "log" - "slices" - "time" - - "github.com/wowsims/wotlk/sim/core" - "github.com/wowsims/wotlk/sim/core/proto" -) - -type rotation_subtlety struct { - prios []prio - - builder *core.Spell -} - -func (x *rotation_subtlety) setup(sim *core.Simulation, rogue *Rogue) { - x.setSubtletyBuilder(sim, rogue) - - x.prios = x.prios[:0] - - secondsPerComboPoint := func() float64 { - honorAmongThievesChance := []float64{0, 0.33, 0.66, 1.0}[rogue.Talents.HonorAmongThieves] - return 1 + 1/(float64(rogue.Options.HonorOfThievesCritRate+100)/100*honorAmongThievesChance) - } - - comboPointsPerSecond := func() float64 { - return 1 / secondsPerComboPoint() - } - - energyPerSecond := func() float64 { - return 10 * rogue.EnergyTickMultiplier - } - - if rogue.Rotation.OpenWithPremeditation && rogue.Talents.Premeditation && rogue.IsStealthed() { - x.prios = append(x.prios, prio{ - func(s *core.Simulation, r *Rogue) PriorityAction { - if rogue.Premeditation.CanCast(s, r.CurrentTarget) { - return Once - } - return Wait - }, - func(s *core.Simulation, r *Rogue) bool { - return r.Premeditation.Cast(s, r.CurrentTarget) - }, - rogue.Premeditation.DefaultCast.Cost, - }) - } - - if rogue.Rotation.OpenWithShadowstep && rogue.Talents.Shadowstep { - x.prios = append(x.prios, prio{ - func(s *core.Simulation, r *Rogue) PriorityAction { - if rogue.CurrentEnergy() > rogue.Shadowstep.DefaultCast.Cost { - return Once - } - return Wait - }, - func(s *core.Simulation, r *Rogue) bool { - return rogue.Shadowstep.Cast(sim, rogue.CurrentTarget) - }, - rogue.Shadowstep.DefaultCast.Cost, - }) - } - - // Garrote - if rogue.Rotation.OpenWithGarrote && !rogue.PseudoStats.InFrontOfTarget && rogue.IsStealthed() { - x.prios = append(x.prios, prio{ - func(sim *core.Simulation, rogue *Rogue) PriorityAction { - if rogue.CurrentEnergy() > rogue.Garrote.DefaultCast.Cost && rogue.IsStealthed() { - return Once - } - return Wait - }, - func(sim *core.Simulation, rogue *Rogue) bool { - return rogue.Garrote.Cast(sim, rogue.CurrentTarget) - }, - rogue.Garrote.DefaultCast.Cost, - }) - } - - // Slice and Dice - x.prios = append(x.prios, prio{ - func(sim *core.Simulation, rogue *Rogue) PriorityAction { - if rogue.SliceAndDiceAura.IsActive() { - return Skip - } - // end of combat handling - prefer Eviscerate over a mostly wasted SnD - if rogue.ComboPoints() >= 2 && rogue.sliceAndDiceDurations[rogue.ComboPoints()] >= 2*sim.GetRemainingDuration() { - return Skip - } - if rogue.ComboPoints() >= 1 && rogue.CurrentEnergy() > rogue.SliceAndDice.DefaultCast.Cost { - return Cast - } - if rogue.ComboPoints() < 1 && rogue.CurrentEnergy() > x.builder.DefaultCast.Cost && comboPointsPerSecond() >= 0.7 { - return Wait - } - if rogue.ComboPoints() < 1 && rogue.CurrentEnergy() > x.builder.DefaultCast.Cost { - return Build - } - return Wait - }, - func(sim *core.Simulation, rogue *Rogue) bool { - return rogue.SliceAndDice.Cast(sim, rogue.CurrentTarget) - }, - rogue.SliceAndDice.DefaultCast.Cost, - }) - - // Expose armor - if rogue.Rotation.ExposeArmorFrequency == proto.Rogue_Rotation_Once || rogue.Rotation.ExposeArmorFrequency == proto.Rogue_Rotation_Maintain { - hasCastExpose := false - x.prios = append(x.prios, prio{ - func(sim *core.Simulation, rogue *Rogue) PriorityAction { - if hasCastExpose && rogue.Rotation.ExposeArmorFrequency == proto.Rogue_Rotation_Once { - return Skip - } - timeLeft := rogue.ExposeArmorAuras.Get(rogue.CurrentTarget).RemainingDuration(sim) - minPoints := max(1, min(rogue.Rotation.MinimumComboPointsExposeArmor, 5)) - if rogue.Rotation.ExposeArmorFrequency != proto.Rogue_Rotation_Once { - minPoints = 1 - } - if timeLeft <= 0 { - if rogue.ComboPoints() < minPoints { - if rogue.CurrentEnergy() >= x.builder.DefaultCast.Cost && comboPointsPerSecond() < 1 { - return Build - } else { - return Wait - } - } else { - if rogue.CurrentEnergy() >= rogue.ExposeArmor.DefaultCast.Cost { - return Cast - } else { - return Wait - } - } - } else { - energyGained := energyPerSecond() * timeLeft.Seconds() - comboGained := comboPointsPerSecond() * timeLeft.Seconds() - cpGenerated := energyGained/x.builder.DefaultCast.Cost + comboGained - currentCP := float64(rogue.ComboPoints()) - if currentCP+cpGenerated > 5 { - return Skip - } else { - if currentCP < 5 { - if rogue.CurrentEnergy() >= x.builder.DefaultCast.Cost { - return Build - } - } - return Wait - } - } - }, - func(sim *core.Simulation, rogue *Rogue) bool { - casted := rogue.ExposeArmor.Cast(sim, rogue.CurrentTarget) - if casted { - hasCastExpose = true - } - return casted - }, - rogue.ExposeArmor.DefaultCast.Cost, - }) - } - - // Enable CDS - x.prios = append(x.prios, prio{ - func(sim *core.Simulation, rogue *Rogue) PriorityAction { - for _, mcd := range rogue.GetMajorCooldowns() { - mcd.Enable() - } - return Once - }, - func(_ *core.Simulation, _ *Rogue) bool { - return true - }, - 0, - }) - - //Shadowstep - if rogue.Talents.Shadowstep { - x.prios = append(x.prios, prio{ - func(sim *core.Simulation, rogue *Rogue) PriorityAction { - if rogue.Shadowstep.IsReady(sim) { - // Can we cast Rupture now? - if !rogue.Rupture.CurDot().IsActive() && rogue.ComboPoints() >= 5 && rogue.CurrentEnergy() >= rogue.Rupture.DefaultCast.Cost+rogue.Shadowstep.DefaultCast.Cost { - return Cast - } else { - return Skip - } - } - return Skip - }, - func(sim *core.Simulation, rogue *Rogue) bool { - return rogue.Shadowstep.Cast(sim, rogue.CurrentTarget) - }, - rogue.Shadowstep.DefaultCast.Cost, - }) - } - - const ruptureMinDuration = time.Second * 8 // heuristically, 3-4 Rupture ticks are better DPE than Eviscerate or Envenom - - // Rupture - x.prios = append(x.prios, prio{ - func(sim *core.Simulation, rogue *Rogue) PriorityAction { - if rogue.Rupture.CurDot().IsActive() || sim.GetRemainingDuration() < ruptureMinDuration { - return Skip - } - if rogue.ComboPoints() >= 5 && rogue.CurrentEnergy() >= rogue.Rupture.DefaultCast.Cost { - return Cast - } - // don't explicitly wait here, to shorten downtime - if rogue.ComboPoints() < 5 && rogue.CurrentEnergy() >= x.builder.DefaultCast.Cost+rogue.Rupture.DefaultCast.Cost { - return Build - } - return Wait - }, - func(sim *core.Simulation, rogue *Rogue) bool { - return rogue.Rupture.Cast(sim, rogue.CurrentTarget) - }, - rogue.Rupture.DefaultCast.Cost, - }) - - //Envenom - if rogue.Rotation.SubtletyFinisherPriority == proto.Rogue_Rotation_SubtletyEnvenom { - x.prios = append(x.prios, prio{ - func(sim *core.Simulation, rogue *Rogue) PriorityAction { - if !rogue.DeadlyPoison.CurDot().Aura.IsActive() { - return Skip - } - if rogue.EnvenomAura.IsActive() { - return Skip - } - if rogue.ComboPoints() >= 5 && rogue.CurrentEnergy() >= rogue.Envenom.DefaultCast.Cost { - return Cast - } - if rogue.ComboPoints() < 5 && rogue.CurrentEnergy() >= x.builder.DefaultCast.Cost+rogue.Envenom.DefaultCast.Cost { - return Build - } - return Wait - }, - func(sim *core.Simulation, rogue *Rogue) bool { - return rogue.Envenom.Cast(sim, rogue.CurrentTarget) - }, - rogue.Envenom.DefaultCast.Cost, - }) - } - - // Eviscerate - x.prios = append(x.prios, prio{ - func(sim *core.Simulation, rogue *Rogue) PriorityAction { - // end of combat handling - prefer Eviscerate over Builder, heuristically - if sim.GetRemainingDuration().Seconds() < secondsPerComboPoint() && rogue.ComboPoints() >= 2 && rogue.CurrentEnergy() >= rogue.Eviscerate.DefaultCast.Cost { - return Cast - } - if rogue.ComboPoints() >= 5 && rogue.CurrentEnergy() >= rogue.Eviscerate.DefaultCast.Cost { - return Cast - } - if rogue.ComboPoints() < 5 && rogue.CurrentEnergy() >= x.builder.DefaultCast.Cost+rogue.Eviscerate.DefaultCast.Cost { - return Build - } - return Wait - }, - func(sim *core.Simulation, rogue *Rogue) bool { - return rogue.Eviscerate.Cast(sim, rogue.CurrentTarget) - }, - rogue.Eviscerate.DefaultCast.Cost, - }) -} - -func (x *rotation_subtlety) run(sim *core.Simulation, rogue *Rogue) { - for i := 0; i < len(x.prios); i++ { - switch p := x.prios[i]; p.check(sim, rogue) { - case Skip: - continue - case Build: - if rogue.ComboPoints() == 4 && rogue.CurrentEnergy() <= rogue.maxEnergy-10 { - // just wait for HaT proc - if it happens, a finisher will follow and often cost effectively 0 energy, - // so we add another GCD worth of energy headroom - rogue.DoNothing() - return - } - - x.setSubtletyBuilder(sim, rogue) - if !x.builder.Cast(sim, rogue.CurrentTarget) { - rogue.WaitForEnergy(sim, x.builder.DefaultCast.Cost) - return - } - case Cast: - if !p.cast(sim, rogue) { - rogue.WaitForEnergy(sim, p.cost) - return - } - case Once: - if !p.cast(sim, rogue) { - rogue.WaitForEnergy(sim, p.cost) - return - } - x.prios = slices.Delete(x.prios, i, i+1) - i-- - case Wait: - rogue.DoNothing() - return - } - - if !rogue.GCD.IsReady(sim) { - return - } - } - log.Panic("skipped all prios") -} - -func (x *rotation_subtlety) setSubtletyBuilder(sim *core.Simulation, rogue *Rogue) { - // Garrote - if !rogue.Garrote.CurDot().Aura.IsActive() && rogue.IsStealthed() && !rogue.PseudoStats.InFrontOfTarget { - x.builder = rogue.Garrote - return - } - // Ambush - if rogue.IsStealthed() && !rogue.PseudoStats.InFrontOfTarget && rogue.HasDagger(core.MainHand) { - x.builder = rogue.Ambush - return - } - // Backstab - if rogue.Rotation.SubtletyBuilder == proto.Rogue_Rotation_BackstabSub && !rogue.PseudoStats.InFrontOfTarget && rogue.HasDagger(core.MainHand) { - x.builder = rogue.Backstab - return - } - // Ghostly Strike -- should only be considered when glyphed - if rogue.Talents.GhostlyStrike && rogue.Rotation.UseGhostlyStrike && rogue.GhostlyStrike.IsReady(sim) { - x.builder = rogue.GhostlyStrike - return - } - // Hemorrhage - if rogue.Talents.Hemorrhage { - x.builder = rogue.Hemorrhage - return - } - - // Sinister Strike - x.builder = rogue.SinisterStrike -} diff --git a/sim/rogue/talents.go b/sim/rogue/talents.go index e45f63e03e..d2bf6b5479 100644 --- a/sim/rogue/talents.go +++ b/sim/rogue/talents.go @@ -510,12 +510,6 @@ func (rogue *Rogue) registerBladeFlurryCD() { Type: core.CooldownTypeDPS, Priority: core.CooldownPriorityDefault, ShouldActivate: func(sim *core.Simulation, character *core.Character) bool { - - if rogue.Rotation.MultiTargetSliceFrequency == proto.Rogue_Rotation_Never { - // Well let's just cast BF now, no need to optimize around slices that will never be cast - return true - } - if sim.GetRemainingDuration() > cooldownDur+dur { // We'll have enough time to cast another BF, so use it immediately to make sure we get the 2nd one. return true @@ -543,16 +537,10 @@ func (rogue *Rogue) registerAdrenalineRushCD() { OnGain: func(aura *core.Aura, sim *core.Simulation) { rogue.ResetEnergyTick(sim) rogue.ApplyEnergyTickMultiplier(1.0) - if r, ok := rogue.rotation.(*rotation_multi); ok { - r.planRotation(sim, rogue) - } }, OnExpire: func(aura *core.Aura, sim *core.Simulation) { rogue.ResetEnergyTick(sim) rogue.ApplyEnergyTickMultiplier(-1.0) - if r, ok := rogue.rotation.(*rotation_multi); ok { - r.planRotation(sim, rogue) - } }, }) diff --git a/sim/rogue/tricks_of_the_trade.go b/sim/rogue/tricks_of_the_trade.go index 60c87bb983..13116fc0b2 100644 --- a/sim/rogue/tricks_of_the_trade.go +++ b/sim/rogue/tricks_of_the_trade.go @@ -72,20 +72,4 @@ func (rogue *Rogue) registerTricksOfTheTradeSpell() { } }, }) - - if rogue.Rotation.TricksOfTheTradeFrequency != proto.Rogue_Rotation_Never { - // TODO: Support Rogue_Rotation_Once - rogue.AddMajorCooldown(core.MajorCooldown{ - Spell: rogue.TricksOfTheTrade, - Priority: core.CooldownPriorityBloodlust, - Type: core.CooldownTypeDPS, - ShouldActivate: func(sim *core.Simulation, character *core.Character) bool { - if hasShadowblades { - return rogue.CurrentEnergy() <= rogue.maxEnergy-15-rogue.EnergyTickMultiplier*10 - } else { - return true - } - }, - }) - } } diff --git a/sim/rogue/vanish.go b/sim/rogue/vanish.go index a3cc28cfbf..c22f370cbb 100644 --- a/sim/rogue/vanish.go +++ b/sim/rogue/vanish.go @@ -27,38 +27,12 @@ func (rogue *Rogue) registerVanishSpell() { rogue.AutoAttacks.CancelAutoSwing(sim) // Apply stealth rogue.StealthAura.Activate(sim) - - if !rogue.IsUsingAPL { - // Master of Subtlety - if rogue.Talents.MasterOfSubtlety > 0 { - _, premedCPs := checkPremediation(sim, rogue) - _, garroteCPs := checkGarrote(sim, rogue) - - if premedCPs > 0 && rogue.ComboPoints()+premedCPs+garroteCPs <= 5 { - rogue.Premeditation.Cast(sim, target) - } - - if garroteCPs > 0 { - rogue.Garrote.Cast(sim, target) - } - } - - // Break the Stealth effect automatically after a dely with an auto swing - pa := &core.PendingAction{ - NextActionAt: sim.CurrentTime + time.Second * time.Duration(rogue.Options.VanishBreakTime), - Priority: core.ActionPriorityAuto, - } - pa.OnAction = func(sim *core.Simulation) { - rogue.BreakStealth(sim) - rogue.AutoAttacks.EnableAutoSwing(sim) - } - } }, }) rogue.AddMajorCooldown(core.MajorCooldown{ - Spell: rogue.Vanish, - Type: core.CooldownTypeDPS, + Spell: rogue.Vanish, + Type: core.CooldownTypeDPS, Priority: core.CooldownPriorityDrums, ShouldActivate: func(s *core.Simulation, c *core.Character) bool {