From 86d72fb46ea4ecda926f180c8aeb685aa0a5b64c Mon Sep 17 00:00:00 2001 From: vigo Date: Fri, 24 Mar 2023 17:18:10 +0100 Subject: [PATCH] [rogue] glyph of backstab doesn't allow extending rupture for more than 6s [rogue] refactor dagger-specialized abilities [rogue] fix 4t10 to add combo points for slice'n dice casts as well [rogue] move rotation logic into their own files, implementing a rotation interface; a rotation is setup only once per reset(), and run() from the gcd and energy gained callbacks [rogue] add a "once" priority action; these are cast only once, then removed until the next setup() [rogue] add a specialized combat rotation (gated on combat potency) --- proto/rogue.proto | 5 +- sim/core/dot.go | 10 +- sim/rogue/TestAssassination.results | 16 +- sim/rogue/TestCombat.results | 568 +++++++++--------- sim/rogue/TestSubtlety.results | 468 +++++++-------- sim/rogue/backstab.go | 10 +- sim/rogue/fan_of_knives.go | 4 +- sim/rogue/ghostly_strike.go | 3 +- sim/rogue/hemorrhage.go | 3 +- sim/rogue/items.go | 23 +- sim/rogue/rogue.go | 46 +- sim/rogue/rotation.go | 540 ++--------------- ..._rotation.go => rotation_assassination.go} | 203 +++---- sim/rogue/rotation_combat.go | 339 +++++++++++ sim/rogue/rotation_generic.go | 517 ++++++++++++++++ ...tlety_rotation.go => rotation_subtlety.go} | 228 ++++--- sim/rogue/rupture.go | 27 +- sim/rogue/talents.go | 17 +- 18 files changed, 1665 insertions(+), 1362 deletions(-) rename sim/rogue/{assassination_rotation.go => rotation_assassination.go} (62%) create mode 100644 sim/rogue/rotation_combat.go create mode 100644 sim/rogue/rotation_generic.go rename sim/rogue/{subtlety_rotation.go => rotation_subtlety.go} (59%) diff --git a/proto/rogue.proto b/proto/rogue.proto index ebd0f8c203..413e24e811 100644 --- a/proto/rogue.proto +++ b/proto/rogue.proto @@ -181,13 +181,10 @@ message Rogue { Frequency MultiTargetSliceFrequency = 9; int32 minimum_combo_points_multi_target_slice = 10; - reserved 11, 19, 21, 22, 23; // the various envenom pooling parameters + reserved 11, 13, 14, 19, 21, 22, 23; // the various envenom pooling parameters bool use_feint = 12; - bool allow_cp_overcap = 13; - bool allow_cp_undercap = 14; - bool open_with_garrote = 15; bool open_with_premeditation = 16; bool open_with_shadowstep = 17; diff --git a/sim/core/dot.go b/sim/core/dot.go index 2b4a00e884..160f5e8996 100644 --- a/sim/core/dot.go +++ b/sim/core/dot.go @@ -30,7 +30,7 @@ type DotConfig struct { type Dot struct { Spell *Spell - // Embed Aura so we can use IsActive/Refresh/etc directly. + // Embed Aura, so we can use IsActive/Refresh/etc directly. *Aura NumberOfTicks int32 // number of ticks over the whole duration @@ -55,7 +55,7 @@ type Dot struct { lastTickTime time.Duration } -// TickPeriod is how fast the snapshotted dot ticks. +// TickPeriod is how fast the snapshot dot ticks. func (dot *Dot) TickPeriod() time.Duration { return dot.tickPeriod } @@ -72,7 +72,7 @@ func (dot *Dot) NumTicksRemaining(sim *Simulation) int { // Roll over = gets carried over with everlasting refresh and doesn't get applied if triggered when the spell is already up. // - Example: critical strike rating, internal % damage modifiers: buffs or debuffs on player -// Nevermelting Ice, Shadow Mastery (ISB), Trick of the Trades, Deaths Embrace, Thadius Polarity, Hera Spores, Crit on weapons from swapping +// Nevermelting Ice, Shadow Mastery (ISB), Trick of the Trades, Deaths Embrace, Thaddius Polarity, Hera Spores, Crit on weapons from swapping // Snapshot = calculation happens at refresh and application (stays up even if buff falls of, until new refresh or application) // - Example: Spell power, Haste rating @@ -83,8 +83,8 @@ func (dot *Dot) NumTicksRemaining(sim *Simulation) int { // Haunt, Curse of Shadow, Shadow Embrace // Rollover is used to reset the duration of a dot from an external spell (not casting the dot itself) -// This keeps the snapshotted crit and %dmg modifiers. -// However sp and haste are recalculated. +// This keeps the snapshot crit and %dmg modifiers. +// However, sp and haste are recalculated. func (dot *Dot) Rollover(sim *Simulation) { dot.TakeSnapshot(sim, true) diff --git a/sim/rogue/TestAssassination.results b/sim/rogue/TestAssassination.results index 4cbd67b38b..b0dce48a88 100644 --- a/sim/rogue/TestAssassination.results +++ b/sim/rogue/TestAssassination.results @@ -102,15 +102,15 @@ dps_results: { dps_results: { key: "TestAssassination-AllItems-BlackBruise-50035" value: { - dps: 5873.6918 - tps: 4170.32118 + dps: 5794.83443 + tps: 4114.33244 } } dps_results: { key: "TestAssassination-AllItems-BlackBruise-50692" value: { - dps: 5956.05754 - tps: 4228.80085 + dps: 5868.81783 + tps: 4166.86066 } } dps_results: { @@ -566,8 +566,8 @@ dps_results: { dps_results: { key: "TestAssassination-AllItems-Shadowblade'sBattlegear" value: { - dps: 7787.26234 - tps: 5528.95626 + dps: 7790.58277 + tps: 5531.31376 } } dps_results: { @@ -678,8 +678,8 @@ dps_results: { dps_results: { key: "TestAssassination-AllItems-TheFistsofFury" value: { - dps: 5100.8384 - tps: 3621.59526 + dps: 4991.69544 + tps: 3544.10376 } } dps_results: { diff --git a/sim/rogue/TestCombat.results b/sim/rogue/TestCombat.results index bad45363c2..655fcab79c 100644 --- a/sim/rogue/TestCombat.results +++ b/sim/rogue/TestCombat.results @@ -46,766 +46,766 @@ character_stats_results: { dps_results: { key: "TestCombat-AllItems-Althor'sAbacus-50359" value: { - dps: 6314.27703 - tps: 4483.13669 + dps: 6348.64628 + tps: 4507.53886 } } dps_results: { key: "TestCombat-AllItems-Althor'sAbacus-50366" value: { - dps: 6314.27703 - tps: 4483.13669 + dps: 6348.64628 + tps: 4507.53886 } } dps_results: { key: "TestCombat-AllItems-AshtongueTalismanofLethality-32492" value: { - dps: 6329.71453 - tps: 4494.09732 + dps: 6362.05402 + tps: 4517.05836 } } dps_results: { key: "TestCombat-AllItems-AustereEarthsiegeDiamond" value: { - dps: 6449.25308 - tps: 4578.96969 + dps: 6482.358 + tps: 4602.47418 } } dps_results: { key: "TestCombat-AllItems-Bandit'sInsignia-40371" value: { - dps: 6511.33641 - tps: 4623.04885 + dps: 6547.10577 + tps: 4648.4451 } } dps_results: { key: "TestCombat-AllItems-BaubleofTrueBlood-50354" value: { - dps: 6314.25904 - tps: 50548.00024 + dps: 6347.91854 + tps: 55850.02699 } } dps_results: { key: "TestCombat-AllItems-BaubleofTrueBlood-50726" value: { - dps: 6314.25904 - tps: 50548.00024 + dps: 6347.91854 + tps: 55850.02699 } } dps_results: { key: "TestCombat-AllItems-BeamingEarthsiegeDiamond" value: { - dps: 6473.20605 - tps: 4595.9763 + dps: 6508.06568 + tps: 4620.72663 } } dps_results: { key: "TestCombat-AllItems-BlackBruise-50035" value: { - dps: 6789.21033 - tps: 4820.33933 + dps: 6829.51419 + tps: 4848.95508 } } dps_results: { key: "TestCombat-AllItems-BlackBruise-50692" value: { - dps: 6901.80807 - tps: 4900.28373 + dps: 6941.8432 + tps: 4928.70867 } } dps_results: { key: "TestCombat-AllItems-BlessedRegaliaofUndeadCleansing" value: { - dps: 5013.37718 - tps: 3559.4978 + dps: 5030.21507 + tps: 3571.4527 } } dps_results: { key: "TestCombat-AllItems-BonescytheBattlegear" value: { - dps: 5987.892 - tps: 4251.40332 + dps: 6004.62008 + tps: 4263.28026 } } dps_results: { key: "TestCombat-AllItems-BracingEarthsiegeDiamond" value: { - dps: 6449.25308 - tps: 4487.39029 + dps: 6482.358 + tps: 4510.4247 } } dps_results: { key: "TestCombat-AllItems-Bryntroll,theBoneArbiter-50415" value: { - dps: 6594.67912 - tps: 4682.22218 + dps: 6631.97875 + tps: 4708.70491 } } dps_results: { key: "TestCombat-AllItems-Bryntroll,theBoneArbiter-50709" value: { - dps: 6594.67912 - tps: 4682.22218 + dps: 6631.97875 + tps: 4708.70491 } } dps_results: { key: "TestCombat-AllItems-ChaoticSkyflareDiamond" value: { - dps: 6589.09402 - tps: 4678.25675 + dps: 6624.81548 + tps: 4703.61899 } } dps_results: { key: "TestCombat-AllItems-CorpseTongueCoin-50349" value: { - dps: 6314.27703 - tps: 4483.13669 + dps: 6348.64628 + tps: 4507.53886 } } dps_results: { key: "TestCombat-AllItems-CorpseTongueCoin-50352" value: { - dps: 6314.27703 - tps: 4483.13669 + dps: 6348.64628 + tps: 4507.53886 } } dps_results: { key: "TestCombat-AllItems-CorrodedSkeletonKey-50356" value: { - dps: 6314.27703 - tps: 4483.13669 + dps: 6348.64628 + tps: 4507.53886 hps: 64 } } dps_results: { key: "TestCombat-AllItems-DarkmoonCard:Berserker!-42989" value: { - dps: 6441.55307 - tps: 4573.50268 + dps: 6476.22969 + tps: 4598.12308 } } dps_results: { key: "TestCombat-AllItems-DarkmoonCard:Death-42990" value: { - dps: 6475.41879 - tps: 4597.54734 + dps: 6510.0042 + tps: 4622.10298 } } dps_results: { key: "TestCombat-AllItems-DarkmoonCard:Greatness-44255" value: { - dps: 6463.85708 - tps: 4589.33852 + dps: 6496.13135 + tps: 4612.25326 } } dps_results: { key: "TestCombat-AllItems-Death'sChoice-47464" value: { - dps: 6790.4313 - tps: 4821.20622 + dps: 6829.60087 + tps: 4849.01662 } } dps_results: { key: "TestCombat-AllItems-DeathKnight'sAnguish-38212" value: { - dps: 6421.23943 - tps: 4559.07999 + dps: 6453.73571 + tps: 4582.15235 } } dps_results: { key: "TestCombat-AllItems-Deathbringer'sWill-50362" value: { - dps: 6731.20564 - tps: 4779.15601 + dps: 6762.4038 + tps: 4801.3067 } } dps_results: { key: "TestCombat-AllItems-Deathbringer'sWill-50363" value: { - dps: 6785.16751 - tps: 4817.46893 + dps: 6806.50608 + tps: 4832.61931 } } dps_results: { key: "TestCombat-AllItems-Defender'sCode-40257" value: { - dps: 6314.27703 - tps: 4483.13669 + dps: 6348.64628 + tps: 4507.53886 } } dps_results: { key: "TestCombat-AllItems-DestructiveSkyflareDiamond" value: { - dps: 6475.88298 - tps: 4597.87692 + dps: 6511.35274 + tps: 4623.06044 } } dps_results: { key: "TestCombat-AllItems-DislodgedForeignObject-50348" value: { - dps: 6524.47688 - tps: 4632.37859 + dps: 6557.57407 + tps: 4655.87759 } } dps_results: { key: "TestCombat-AllItems-DislodgedForeignObject-50353" value: { - dps: 6507.84514 - tps: 4620.57005 + dps: 6526.82196 + tps: 4634.04359 } } dps_results: { key: "TestCombat-AllItems-EffulgentSkyflareDiamond" value: { - dps: 6449.25308 - tps: 4578.96969 + dps: 6482.358 + tps: 4602.47418 } } dps_results: { key: "TestCombat-AllItems-EmberSkyflareDiamond" value: { - dps: 6449.25308 - tps: 4578.96969 + dps: 6482.358 + tps: 4602.47418 } } dps_results: { key: "TestCombat-AllItems-EnigmaticSkyflareDiamond" value: { - dps: 6473.20605 - tps: 4595.9763 + dps: 6508.06568 + tps: 4620.72663 } } dps_results: { key: "TestCombat-AllItems-EnigmaticStarflareDiamond" value: { - dps: 6468.35375 - tps: 4592.53116 + dps: 6503.62114 + tps: 4617.57101 } } dps_results: { key: "TestCombat-AllItems-EphemeralSnowflake-50260" value: { - dps: 6438.06377 - tps: 4571.02527 + dps: 6486.09525 + tps: 4605.12763 } } dps_results: { key: "TestCombat-AllItems-EssenceofGossamer-37220" value: { - dps: 6314.27703 - tps: 4483.13669 + dps: 6348.64628 + tps: 4507.53886 } } dps_results: { key: "TestCombat-AllItems-EternalEarthsiegeDiamond" value: { - dps: 6449.25308 - tps: 4578.96969 + dps: 6482.358 + tps: 4602.47418 } } dps_results: { key: "TestCombat-AllItems-ExtractofNecromanticPower-40373" value: { - dps: 6475.74023 - tps: 4597.77556 + dps: 6510.96443 + tps: 4622.78475 } } dps_results: { key: "TestCombat-AllItems-EyeoftheBroodmother-45308" value: { - dps: 6426.82228 - tps: 4563.04382 + dps: 6461.18626 + tps: 4587.44225 } } dps_results: { key: "TestCombat-AllItems-Figurine-SapphireOwl-42413" value: { - dps: 6316.67736 - tps: 4484.84093 + dps: 6345.67928 + tps: 4505.43229 } } dps_results: { key: "TestCombat-AllItems-ForethoughtTalisman-40258" value: { - dps: 6314.27703 - tps: 4483.13669 + dps: 6348.64628 + tps: 4507.53886 } } dps_results: { key: "TestCombat-AllItems-ForgeEmber-37660" value: { - dps: 6401.42683 - tps: 4545.01305 + dps: 6437.21636 + tps: 4570.42362 } } dps_results: { key: "TestCombat-AllItems-ForlornSkyflareDiamond" value: { - dps: 6449.25308 - tps: 4578.96969 + dps: 6482.358 + tps: 4602.47418 } } dps_results: { key: "TestCombat-AllItems-ForlornStarflareDiamond" value: { - dps: 6449.25308 - tps: 4578.96969 + dps: 6482.358 + tps: 4602.47418 } } dps_results: { key: "TestCombat-AllItems-FuryoftheFiveFlights-40431" value: { - dps: 6554.62283 - tps: 4653.78221 + dps: 6589.93946 + tps: 4678.85702 } } dps_results: { key: "TestCombat-AllItems-FuturesightRune-38763" value: { - dps: 6314.27703 - tps: 4483.13669 + dps: 6348.64628 + tps: 4507.53886 } } dps_results: { key: "TestCombat-AllItems-Gladiator'sVestments" value: { - dps: 6384.42691 - tps: 4532.9431 + dps: 6436.11792 + tps: 4569.64372 } } dps_results: { key: "TestCombat-AllItems-GlowingTwilightScale-54573" value: { - dps: 6314.27703 - tps: 4483.13669 + dps: 6348.64628 + tps: 4507.53886 } } dps_results: { key: "TestCombat-AllItems-GlowingTwilightScale-54589" value: { - dps: 6314.27703 - tps: 4483.13669 + dps: 6348.64628 + tps: 4507.53886 } } dps_results: { key: "TestCombat-AllItems-GnomishLightningGenerator-41121" value: { - dps: 6451.02608 - tps: 4580.22852 + dps: 6486.37804 + tps: 4605.32841 } } dps_results: { key: "TestCombat-AllItems-Heartpierce-49982" value: { - dps: 6594.67912 - tps: 4682.22218 + dps: 6631.97875 + tps: 4708.70491 } } dps_results: { key: "TestCombat-AllItems-Heartpierce-50641" value: { - dps: 6594.67912 - tps: 4682.22218 + dps: 6631.97875 + tps: 4708.70491 } } dps_results: { key: "TestCombat-AllItems-IllustrationoftheDragonSoul-40432" value: { - dps: 6314.27703 - tps: 4483.13669 + dps: 6348.64628 + tps: 4507.53886 } } dps_results: { key: "TestCombat-AllItems-ImpassiveSkyflareDiamond" value: { - dps: 6473.20605 - tps: 4595.9763 + dps: 6508.06568 + tps: 4620.72663 } } dps_results: { key: "TestCombat-AllItems-ImpassiveStarflareDiamond" value: { - dps: 6468.35375 - tps: 4592.53116 + dps: 6503.62114 + tps: 4617.57101 } } dps_results: { key: "TestCombat-AllItems-IncisorFragment-37723" value: { - dps: 6494.34763 - tps: 4610.98682 + dps: 6530.23233 + tps: 4636.46495 } } dps_results: { key: "TestCombat-AllItems-InsightfulEarthsiegeDiamond" value: { - dps: 6449.25308 - tps: 4578.96969 + dps: 6482.358 + tps: 4602.47418 } } dps_results: { key: "TestCombat-AllItems-InvigoratingEarthsiegeDiamond" value: { - dps: 6480.71016 - tps: 4601.30421 + dps: 6514.58482 + tps: 4625.35522 hps: 9.14161 } } dps_results: { key: "TestCombat-AllItems-Lavanthor'sTalisman-37872" value: { - dps: 6314.27703 - tps: 4483.13669 + dps: 6348.64628 + tps: 4507.53886 } } dps_results: { key: "TestCombat-AllItems-MajesticDragonFigurine-40430" value: { - dps: 6314.27703 - tps: 4483.13669 + dps: 6348.64628 + tps: 4507.53886 } } dps_results: { key: "TestCombat-AllItems-MeteoriteWhetstone-37390" value: { - dps: 6529.14201 - tps: 4635.69083 + dps: 6563.74796 + tps: 4660.26105 } } dps_results: { key: "TestCombat-AllItems-NevermeltingIceCrystal-50259" value: { - dps: 6362.79759 - tps: 4517.58629 + dps: 6399.20442 + tps: 4543.43514 } } dps_results: { key: "TestCombat-AllItems-OfferingofSacrifice-37638" value: { - dps: 6314.27703 - tps: 4483.13669 + dps: 6348.64628 + tps: 4507.53886 } } dps_results: { key: "TestCombat-AllItems-PersistentEarthshatterDiamond" value: { - dps: 6474.94936 - tps: 4597.21405 + dps: 6508.14647 + tps: 4620.78399 } } dps_results: { key: "TestCombat-AllItems-PersistentEarthsiegeDiamond" value: { - dps: 6480.99555 - tps: 4601.50684 + dps: 6514.21434 + tps: 4625.09218 } } dps_results: { key: "TestCombat-AllItems-PetrifiedTwilightScale-54571" value: { - dps: 6314.27703 - tps: 4483.13669 + dps: 6348.64628 + tps: 4507.53886 } } dps_results: { key: "TestCombat-AllItems-PetrifiedTwilightScale-54591" value: { - dps: 6314.27703 - tps: 4483.13669 + dps: 6348.64628 + tps: 4507.53886 } } dps_results: { key: "TestCombat-AllItems-PowerfulEarthshatterDiamond" value: { - dps: 6449.25308 - tps: 4578.96969 + dps: 6482.358 + tps: 4602.47418 } } dps_results: { key: "TestCombat-AllItems-PowerfulEarthsiegeDiamond" value: { - dps: 6449.25308 - tps: 4578.96969 + dps: 6482.358 + tps: 4602.47418 } } dps_results: { key: "TestCombat-AllItems-PurifiedShardoftheGods" value: { - dps: 6314.27703 - tps: 4483.13669 + dps: 6348.64628 + tps: 4507.53886 } } dps_results: { key: "TestCombat-AllItems-ReignoftheDead-47316" value: { - dps: 6463.10784 - tps: 4588.80657 + dps: 6495.54913 + tps: 4611.83988 } } dps_results: { key: "TestCombat-AllItems-ReignoftheDead-47477" value: { - dps: 6481.96152 - tps: 4602.19268 + dps: 6514.026 + tps: 4624.95846 } } dps_results: { key: "TestCombat-AllItems-RelentlessEarthsiegeDiamond" value: { - dps: 6594.67912 - tps: 4682.22218 + dps: 6631.97875 + tps: 4708.70491 } } dps_results: { key: "TestCombat-AllItems-RevitalizingSkyflareDiamond" value: { - dps: 6449.25308 - tps: 4578.96969 + dps: 6482.358 + tps: 4602.47418 } } dps_results: { key: "TestCombat-AllItems-RuneofRepulsion-40372" value: { - dps: 6314.27703 - tps: 4483.13669 + dps: 6348.64628 + tps: 4507.53886 } } dps_results: { key: "TestCombat-AllItems-SealofthePantheon-36993" value: { - dps: 6314.27703 - tps: 4483.13669 + dps: 6348.64628 + tps: 4507.53886 } } dps_results: { key: "TestCombat-AllItems-Shadowblade'sBattlegear" value: { - dps: 6631.34434 - tps: 4708.25448 + dps: 6673.97963 + tps: 4738.52554 } } dps_results: { key: "TestCombat-AllItems-Shadowmourne-49623" value: { - dps: 6594.67912 - tps: 4682.22218 + dps: 6631.97875 + tps: 4708.70491 } } dps_results: { key: "TestCombat-AllItems-ShinyShardoftheGods" value: { - dps: 6314.27703 - tps: 4483.13669 + dps: 6348.64628 + tps: 4507.53886 } } dps_results: { key: "TestCombat-AllItems-Sindragosa'sFlawlessFang-50361" value: { - dps: 6314.27703 - tps: 4483.13669 + dps: 6348.64628 + tps: 4507.53886 } } dps_results: { key: "TestCombat-AllItems-Slayer'sArmor" value: { - dps: 4939.74985 - tps: 3507.2224 + dps: 4958.47814 + tps: 3520.51948 } } dps_results: { key: "TestCombat-AllItems-SliverofPureIce-50339" value: { - dps: 6314.27703 - tps: 4483.13669 + dps: 6348.64628 + tps: 4507.53886 } } dps_results: { key: "TestCombat-AllItems-SliverofPureIce-50346" value: { - dps: 6314.27703 - tps: 4483.13669 + dps: 6348.64628 + tps: 4507.53886 } } dps_results: { key: "TestCombat-AllItems-SouloftheDead-40382" value: { - dps: 6430.49471 - tps: 4565.65124 + dps: 6465.99853 + tps: 4590.85896 } } dps_results: { key: "TestCombat-AllItems-SparkofLife-37657" value: { - dps: 6403.46617 - tps: 4546.46098 + dps: 6414.08913 + tps: 4554.00328 } } dps_results: { key: "TestCombat-AllItems-SphereofRedDragon'sBlood-37166" value: { - dps: 6506.48187 - tps: 4619.60213 + dps: 6521.6725 + tps: 4630.38747 } } dps_results: { key: "TestCombat-AllItems-StormshroudArmor" value: { - dps: 5046.08304 - tps: 3582.71896 + dps: 5072.1314 + tps: 3601.21329 } } dps_results: { key: "TestCombat-AllItems-SwiftSkyflareDiamond" value: { - dps: 6480.99555 - tps: 4601.50684 + dps: 6514.21434 + tps: 4625.09218 } } dps_results: { key: "TestCombat-AllItems-SwiftStarflareDiamond" value: { - dps: 6474.94936 - tps: 4597.21405 + dps: 6508.14647 + tps: 4620.78399 } } dps_results: { key: "TestCombat-AllItems-SwiftWindfireDiamond" value: { - dps: 6464.36854 - tps: 4589.70166 + dps: 6497.52769 + tps: 4613.24466 } } dps_results: { key: "TestCombat-AllItems-TalismanofTrollDivinity-37734" value: { - dps: 6314.27703 - tps: 4483.13669 + dps: 6348.64628 + tps: 4507.53886 } } dps_results: { key: "TestCombat-AllItems-TearsoftheVanquished-47215" value: { - dps: 6314.27703 - tps: 4483.13669 + dps: 6348.64628 + tps: 4507.53886 } } dps_results: { key: "TestCombat-AllItems-TerrorbladeBattlegear" value: { - dps: 6379.18145 - tps: 4529.21883 + dps: 6404.13508 + tps: 4546.93591 } } dps_results: { key: "TestCombat-AllItems-TheFistsofFury" value: { - dps: 5772.88045 - tps: 4098.74512 + dps: 5804.89636 + tps: 4121.47642 } } dps_results: { key: "TestCombat-AllItems-TheGeneral'sHeart-45507" value: { - dps: 6314.27703 - tps: 4483.13669 + dps: 6348.64628 + tps: 4507.53886 } } dps_results: { key: "TestCombat-AllItems-TheTwinBladesofAzzinoth" value: { - dps: 5874.0482 - tps: 4170.57422 + dps: 5906.59859 + tps: 4193.685 } } dps_results: { key: "TestCombat-AllItems-ThunderingSkyflareDiamond" value: { - dps: 6529.6204 - tps: 4636.03049 + dps: 6547.75419 + tps: 4648.90548 } } dps_results: { key: "TestCombat-AllItems-TinyAbominationinaJar-50351" value: { - dps: 6634.47279 - tps: 4710.47568 + dps: 6641.67295 + tps: 4715.5878 } } dps_results: { key: "TestCombat-AllItems-TinyAbominationinaJar-50706" value: { - dps: 6651.82679 - tps: 4722.79702 + dps: 6704.04922 + tps: 4759.87495 } } dps_results: { key: "TestCombat-AllItems-TirelessSkyflareDiamond" value: { - dps: 6449.25308 - tps: 4578.96969 + dps: 6482.358 + tps: 4602.47418 } } dps_results: { key: "TestCombat-AllItems-TirelessStarflareDiamond" value: { - dps: 6449.25308 - tps: 4578.96969 + dps: 6482.358 + tps: 4602.47418 } } dps_results: { key: "TestCombat-AllItems-TomeofArcanePhenomena-36972" value: { - dps: 6369.3563 - tps: 4522.24298 + dps: 6407.11866 + tps: 4549.05425 } } dps_results: { key: "TestCombat-AllItems-TrenchantEarthshatterDiamond" value: { - dps: 6449.25308 - tps: 4578.96969 + dps: 6482.358 + tps: 4602.47418 } } dps_results: { key: "TestCombat-AllItems-TrenchantEarthsiegeDiamond" value: { - dps: 6449.25308 - tps: 4578.96969 + dps: 6482.358 + tps: 4602.47418 } } dps_results: { key: "TestCombat-AllItems-UndeadSlayer'sBlessedArmor" value: { - dps: 5320.50285 - tps: 3777.55702 + dps: 5338.30606 + tps: 3790.19731 } } dps_results: { key: "TestCombat-AllItems-Val'anyr,HammerofAncientKings-46017" value: { - dps: 6213.50734 - tps: 4411.59021 + dps: 6231.43572 + tps: 4424.31936 } } dps_results: { key: "TestCombat-AllItems-VanCleef'sBattlegear" value: { - dps: 6129.17384 - tps: 4351.71343 + dps: 6145.65046 + tps: 4363.41183 } } dps_results: { key: "TestCombat-AllItems-WingedTalisman-37844" value: { - dps: 6314.27703 - tps: 4483.13669 + dps: 6348.64628 + tps: 4507.53886 } } dps_results: { key: "TestCombat-Average-Default" value: { - dps: 6576.05277 - tps: 4668.99747 + dps: 6603.93323 + tps: 4688.79259 } } dps_results: { @@ -818,15 +818,15 @@ dps_results: { dps_results: { key: "TestCombat-Settings-Human-P1-MH Deadly OH Deadly-FullBuffs-LongSingleTarget" value: { - dps: 4882.26732 - tps: 3466.4098 + dps: 4919.33217 + tps: 3492.72584 } } dps_results: { key: "TestCombat-Settings-Human-P1-MH Deadly OH Deadly-FullBuffs-ShortSingleTarget" value: { - dps: 5722.43506 - tps: 4062.92889 + dps: 5752.748 + tps: 4084.45108 } } dps_results: { @@ -839,15 +839,15 @@ dps_results: { dps_results: { key: "TestCombat-Settings-Human-P1-MH Deadly OH Deadly-NoBuffs-LongSingleTarget" value: { - dps: 2421.77391 - tps: 1719.45947 + dps: 2440.68778 + tps: 1732.88832 } } dps_results: { key: "TestCombat-Settings-Human-P1-MH Deadly OH Deadly-NoBuffs-ShortSingleTarget" value: { - dps: 2553.17757 - tps: 1812.75607 + dps: 2573.63663 + tps: 1827.28201 } } dps_results: { @@ -860,15 +860,15 @@ dps_results: { dps_results: { key: "TestCombat-Settings-Human-P1-MH Deadly OH Instant-FullBuffs-LongSingleTarget" value: { - dps: 6594.67912 - tps: 4682.22218 + dps: 6631.97875 + tps: 4708.70491 } } dps_results: { key: "TestCombat-Settings-Human-P1-MH Deadly OH Instant-FullBuffs-ShortSingleTarget" value: { - dps: 7752.3221 - tps: 5504.14869 + dps: 7772.32033 + tps: 5518.34743 } } dps_results: { @@ -881,15 +881,15 @@ dps_results: { dps_results: { key: "TestCombat-Settings-Human-P1-MH Deadly OH Instant-NoBuffs-LongSingleTarget" value: { - dps: 3257.97922 - tps: 2313.16524 + dps: 3281.20643 + tps: 2329.65656 } } dps_results: { key: "TestCombat-Settings-Human-P1-MH Deadly OH Instant-NoBuffs-ShortSingleTarget" value: { - dps: 3385.17885 - tps: 2403.47698 + dps: 3404.09501 + tps: 2416.90745 } } dps_results: { @@ -902,15 +902,15 @@ dps_results: { dps_results: { key: "TestCombat-Settings-Human-P1-MH Instant OH Deadly-FullBuffs-LongSingleTarget" value: { - dps: 6294.18911 - tps: 4468.87427 + dps: 6330.85583 + tps: 4494.90764 } } dps_results: { key: "TestCombat-Settings-Human-P1-MH Instant OH Deadly-FullBuffs-ShortSingleTarget" value: { - dps: 7334.52048 - tps: 5207.50954 + dps: 7363.47678 + tps: 5228.06851 } } dps_results: { @@ -923,15 +923,15 @@ dps_results: { dps_results: { key: "TestCombat-Settings-Human-P1-MH Instant OH Deadly-NoBuffs-LongSingleTarget" value: { - dps: 3105.94186 - tps: 2205.21872 + dps: 3127.44545 + tps: 2220.48627 } } dps_results: { key: "TestCombat-Settings-Human-P1-MH Instant OH Deadly-NoBuffs-ShortSingleTarget" value: { - dps: 3203.04439 - tps: 2274.16152 + dps: 3221.26105 + tps: 2287.09535 } } dps_results: { @@ -944,15 +944,15 @@ dps_results: { dps_results: { key: "TestCombat-Settings-Human-P1-MH Instant OH Instant-FullBuffs-LongSingleTarget" value: { - dps: 5645.50776 - tps: 4008.31051 + dps: 5686.76508 + tps: 4037.60321 } } dps_results: { key: "TestCombat-Settings-Human-P1-MH Instant OH Instant-FullBuffs-ShortSingleTarget" value: { - dps: 6778.63142 - tps: 4812.82831 + dps: 6813.61276 + tps: 4837.66506 } } dps_results: { @@ -965,15 +965,15 @@ dps_results: { dps_results: { key: "TestCombat-Settings-Human-P1-MH Instant OH Instant-NoBuffs-LongSingleTarget" value: { - dps: 2631.24941 - tps: 1868.18708 + dps: 2649.40412 + tps: 1881.07693 } } dps_results: { key: "TestCombat-Settings-Human-P1-MH Instant OH Instant-NoBuffs-ShortSingleTarget" value: { - dps: 2803.16527 - tps: 1990.24734 + dps: 2823.89479 + tps: 2004.9653 } } dps_results: { @@ -986,15 +986,15 @@ dps_results: { dps_results: { key: "TestCombat-Settings-Orc-P1-MH Deadly OH Deadly-FullBuffs-LongSingleTarget" value: { - dps: 4915.60536 - tps: 3490.07981 + dps: 4953.82148 + tps: 3517.21325 } } dps_results: { key: "TestCombat-Settings-Orc-P1-MH Deadly OH Deadly-FullBuffs-ShortSingleTarget" value: { - dps: 5799.58233 - tps: 4117.70346 + dps: 5832.53335 + tps: 4141.09868 } } dps_results: { @@ -1007,15 +1007,15 @@ dps_results: { dps_results: { key: "TestCombat-Settings-Orc-P1-MH Deadly OH Deadly-NoBuffs-LongSingleTarget" value: { - dps: 2439.96313 - tps: 1732.37382 + dps: 2459.04539 + tps: 1745.92222 } } dps_results: { key: "TestCombat-Settings-Orc-P1-MH Deadly OH Deadly-NoBuffs-ShortSingleTarget" value: { - dps: 2590.00498 - tps: 1838.90354 + dps: 2612.38635 + tps: 1854.79431 } } dps_results: { @@ -1028,15 +1028,15 @@ dps_results: { dps_results: { key: "TestCombat-Settings-Orc-P1-MH Deadly OH Instant-FullBuffs-LongSingleTarget" value: { - dps: 6639.35343 - tps: 4713.94093 + dps: 6677.76266 + tps: 4741.21149 } } dps_results: { key: "TestCombat-Settings-Orc-P1-MH Deadly OH Instant-FullBuffs-ShortSingleTarget" value: { - dps: 7854.04023 - tps: 5576.36856 + dps: 7876.72725 + tps: 5592.47635 } } dps_results: { @@ -1049,15 +1049,15 @@ dps_results: { dps_results: { key: "TestCombat-Settings-Orc-P1-MH Deadly OH Instant-NoBuffs-LongSingleTarget" value: { - dps: 3281.92498 - tps: 2330.16674 + dps: 3305.28306 + tps: 2346.75097 } } dps_results: { key: "TestCombat-Settings-Orc-P1-MH Deadly OH Instant-NoBuffs-ShortSingleTarget" value: { - dps: 3433.10144 - tps: 2437.50202 + dps: 3453.55644 + tps: 2452.02507 } } dps_results: { @@ -1070,15 +1070,15 @@ dps_results: { dps_results: { key: "TestCombat-Settings-Orc-P1-MH Instant OH Deadly-FullBuffs-LongSingleTarget" value: { - dps: 6336.94017 - tps: 4499.22752 + dps: 6375.01512 + tps: 4526.26073 } } dps_results: { key: "TestCombat-Settings-Orc-P1-MH Instant OH Deadly-FullBuffs-ShortSingleTarget" value: { - dps: 7430.75164 - tps: 5275.83366 + dps: 7462.43321 + tps: 5298.32758 } } dps_results: { @@ -1091,15 +1091,15 @@ dps_results: { dps_results: { key: "TestCombat-Settings-Orc-P1-MH Instant OH Deadly-NoBuffs-LongSingleTarget" value: { - dps: 3128.47872 - tps: 2221.21989 + dps: 3150.18943 + tps: 2236.6345 } } dps_results: { key: "TestCombat-Settings-Orc-P1-MH Instant OH Deadly-NoBuffs-ShortSingleTarget" value: { - dps: 3246.75821 - tps: 2305.19833 + dps: 3266.87036 + tps: 2319.47795 } } dps_results: { @@ -1112,15 +1112,15 @@ dps_results: { dps_results: { key: "TestCombat-Settings-Orc-P1-MH Instant OH Instant-FullBuffs-LongSingleTarget" value: { - dps: 5684.24805 - tps: 4035.81611 + dps: 5726.93425 + tps: 4066.12332 } } dps_results: { key: "TestCombat-Settings-Orc-P1-MH Instant OH Instant-FullBuffs-ShortSingleTarget" value: { - dps: 6870.62619 - tps: 4878.14459 + dps: 6908.16736 + tps: 4904.79883 } } dps_results: { @@ -1133,21 +1133,21 @@ dps_results: { dps_results: { key: "TestCombat-Settings-Orc-P1-MH Instant OH Instant-NoBuffs-LongSingleTarget" value: { - dps: 2650.97581 - tps: 1882.19283 + dps: 2669.21275 + tps: 1895.14105 } } dps_results: { key: "TestCombat-Settings-Orc-P1-MH Instant OH Instant-NoBuffs-ShortSingleTarget" value: { - dps: 2843.70961 - tps: 2019.03382 + dps: 2866.59019 + tps: 2035.27904 } } dps_results: { key: "TestCombat-SwitchInFrontOfTarget-Default" value: { - dps: 6349.16494 - tps: 4507.90711 + dps: 6359.18408 + tps: 4515.0207 } } diff --git a/sim/rogue/TestSubtlety.results b/sim/rogue/TestSubtlety.results index 8041c640a0..f59fed4d1b 100644 --- a/sim/rogue/TestSubtlety.results +++ b/sim/rogue/TestSubtlety.results @@ -46,821 +46,821 @@ character_stats_results: { dps_results: { key: "TestSubtlety-AllItems-Althor'sAbacus-50359" value: { - dps: 8035.95288 - tps: 5700.95502 + dps: 8049.6123 + tps: 5713.6272 } } dps_results: { key: "TestSubtlety-AllItems-Althor'sAbacus-50366" value: { - dps: 8035.95288 - tps: 5700.95502 + dps: 8049.6123 + tps: 5713.6272 } } dps_results: { key: "TestSubtlety-AllItems-AshtongueTalismanofLethality-32492" value: { - dps: 8057.36562 - tps: 5715.7738 + dps: 8072.92993 + tps: 5730.57232 } } dps_results: { key: "TestSubtlety-AllItems-AustereEarthsiegeDiamond" value: { - dps: 8261.76312 - tps: 5863.2263 + dps: 8299.40941 + tps: 5889.19725 } } dps_results: { key: "TestSubtlety-AllItems-Bandit'sInsignia-40371" value: { - dps: 8279.69255 - tps: 5877.16141 + dps: 8294.27788 + tps: 5886.09208 } } dps_results: { key: "TestSubtlety-AllItems-BaubleofTrueBlood-50354" value: { - dps: 8034.94424 - tps: 72791.54924 + dps: 8049.37398 + tps: 74730.02392 } } dps_results: { key: "TestSubtlety-AllItems-BaubleofTrueBlood-50726" value: { - dps: 8034.94424 - tps: 72791.54924 + dps: 8049.37398 + tps: 74730.02392 } } dps_results: { key: "TestSubtlety-AllItems-BeamingEarthsiegeDiamond" value: { - dps: 8322.88244 - tps: 5907.26765 + dps: 8312.67486 + tps: 5899.64383 } } dps_results: { key: "TestSubtlety-AllItems-BlackBruise-50035" value: { - dps: 8269.62115 - tps: 5867.75382 + dps: 8254.6154 + tps: 5856.12199 } } dps_results: { key: "TestSubtlety-AllItems-BlackBruise-50692" value: { - dps: 8366.89474 - tps: 5936.81516 + dps: 8347.47905 + tps: 5922.06382 } } dps_results: { key: "TestSubtlety-AllItems-BlessedRegaliaofUndeadCleansing" value: { - dps: 6005.17912 - tps: 4261.67841 + dps: 5977.49572 + tps: 4242.90596 } } dps_results: { key: "TestSubtlety-AllItems-BonescytheBattlegear" value: { - dps: 7096.7796 - tps: 5034.57281 + dps: 7128.20117 + tps: 5059.38962 } } dps_results: { key: "TestSubtlety-AllItems-BracingEarthsiegeDiamond" value: { - dps: 8261.76312 - tps: 5745.96178 + dps: 8299.40941 + tps: 5771.41331 } } dps_results: { key: "TestSubtlety-AllItems-ChaoticSkyflareDiamond" value: { - dps: 8474.93999 - tps: 6015.17185 + dps: 8464.54587 + tps: 6007.4135 } } dps_results: { key: "TestSubtlety-AllItems-CorpseTongueCoin-50349" value: { - dps: 8035.95288 - tps: 5700.95502 + dps: 8049.6123 + tps: 5713.6272 } } dps_results: { key: "TestSubtlety-AllItems-CorpseTongueCoin-50352" value: { - dps: 8035.95288 - tps: 5700.95502 + dps: 8049.6123 + tps: 5713.6272 } } dps_results: { key: "TestSubtlety-AllItems-CorrodedSkeletonKey-50356" value: { - dps: 8035.95288 - tps: 5700.95502 + dps: 8049.6123 + tps: 5713.6272 hps: 64 } } dps_results: { key: "TestSubtlety-AllItems-DarkmoonCard:Berserker!-42989" value: { - dps: 8188.02111 - tps: 5810.39223 + dps: 8226.12996 + tps: 5836.51777 } } dps_results: { key: "TestSubtlety-AllItems-DarkmoonCard:Death-42990" value: { - dps: 8237.2577 - tps: 5845.3974 + dps: 8245.43389 + tps: 5851.88277 } } dps_results: { key: "TestSubtlety-AllItems-DarkmoonCard:Greatness-44255" value: { - dps: 8211.34643 - tps: 5826.00187 + dps: 8228.13015 + tps: 5840.03271 } } dps_results: { key: "TestSubtlety-AllItems-Death'sChoice-47464" value: { - dps: 8629.27676 - tps: 6124.30476 + dps: 8613.02395 + tps: 6112.33016 } } dps_results: { key: "TestSubtlety-AllItems-DeathKnight'sAnguish-38212" value: { - dps: 8190.07852 - tps: 5813.05366 + dps: 8157.1724 + tps: 5789.07132 } } dps_results: { key: "TestSubtlety-AllItems-Deathbringer'sWill-50362" value: { - dps: 8523.07486 - tps: 6049.06323 + dps: 8520.38626 + tps: 6046.17479 } } dps_results: { key: "TestSubtlety-AllItems-Deathbringer'sWill-50363" value: { - dps: 8589.24175 - tps: 6096.22218 + dps: 8603.02781 + tps: 6106.48641 } } dps_results: { key: "TestSubtlety-AllItems-Defender'sCode-40257" value: { - dps: 8035.95288 - tps: 5700.95502 + dps: 8049.6123 + tps: 5713.6272 } } dps_results: { key: "TestSubtlety-AllItems-DestructiveSkyflareDiamond" value: { - dps: 8324.89704 - tps: 5908.58968 + dps: 8330.99537 + tps: 5912.68291 } } dps_results: { key: "TestSubtlety-AllItems-DislodgedForeignObject-50348" value: { - dps: 8235.88133 - tps: 5842.5696 + dps: 8259.93796 + tps: 5863.38533 } } dps_results: { key: "TestSubtlety-AllItems-DislodgedForeignObject-50353" value: { - dps: 8224.64072 - tps: 5838.1982 + dps: 8237.14712 + tps: 5848.37445 } } dps_results: { key: "TestSubtlety-AllItems-EffulgentSkyflareDiamond" value: { - dps: 8261.76312 - tps: 5863.2263 + dps: 8299.40941 + tps: 5889.19725 } } dps_results: { key: "TestSubtlety-AllItems-EmberSkyflareDiamond" value: { - dps: 8261.76312 - tps: 5863.2263 + dps: 8299.40941 + tps: 5889.19725 } } dps_results: { key: "TestSubtlety-AllItems-EnigmaticSkyflareDiamond" value: { - dps: 8322.88244 - tps: 5907.26765 + dps: 8312.67486 + tps: 5899.64383 } } dps_results: { key: "TestSubtlety-AllItems-EnigmaticStarflareDiamond" value: { - dps: 8316.91955 - tps: 5902.08392 + dps: 8310.90956 + tps: 5898.3291 } } dps_results: { key: "TestSubtlety-AllItems-EphemeralSnowflake-50260" value: { - dps: 8199.99716 - tps: 5820.17408 + dps: 8173.9795 + tps: 5802.75378 } } dps_results: { key: "TestSubtlety-AllItems-EssenceofGossamer-37220" value: { - dps: 8035.95288 - tps: 5700.95502 + dps: 8049.6123 + tps: 5713.6272 } } dps_results: { key: "TestSubtlety-AllItems-EternalEarthsiegeDiamond" value: { - dps: 8261.76312 - tps: 5863.2263 + dps: 8299.40941 + tps: 5889.19725 } } dps_results: { key: "TestSubtlety-AllItems-ExtractofNecromanticPower-40373" value: { - dps: 8253.46401 - tps: 5858.131 + dps: 8254.66307 + tps: 5860.41692 } } dps_results: { key: "TestSubtlety-AllItems-EyeoftheBroodmother-45308" value: { - dps: 8185.47363 - tps: 5808.16529 + dps: 8223.81375 + tps: 5833.92067 } } dps_results: { key: "TestSubtlety-AllItems-Figurine-SapphireOwl-42413" value: { - dps: 8035.95288 - tps: 5700.95502 + dps: 8049.6123 + tps: 5713.6272 } } dps_results: { key: "TestSubtlety-AllItems-ForethoughtTalisman-40258" value: { - dps: 8035.95288 - tps: 5700.95502 + dps: 8049.6123 + tps: 5713.6272 } } dps_results: { key: "TestSubtlety-AllItems-ForgeEmber-37660" value: { - dps: 8153.57674 - tps: 5787.12039 + dps: 8166.40265 + tps: 5794.36136 } } dps_results: { key: "TestSubtlety-AllItems-ForlornSkyflareDiamond" value: { - dps: 8261.76312 - tps: 5863.2263 + dps: 8299.40941 + tps: 5889.19725 } } dps_results: { key: "TestSubtlety-AllItems-ForlornStarflareDiamond" value: { - dps: 8261.76312 - tps: 5863.2263 + dps: 8299.40941 + tps: 5889.19725 } } dps_results: { key: "TestSubtlety-AllItems-FuryoftheFiveFlights-40431" value: { - dps: 8297.83394 - tps: 5886.76181 + dps: 8311.31918 + tps: 5899.39536 } } dps_results: { key: "TestSubtlety-AllItems-FuturesightRune-38763" value: { - dps: 8035.95288 - tps: 5700.95502 + dps: 8049.6123 + tps: 5713.6272 } } dps_results: { key: "TestSubtlety-AllItems-Gladiator'sVestments" value: { - dps: 7483.84854 - tps: 5310.30554 + dps: 7473.24684 + tps: 5303.86993 } } dps_results: { key: "TestSubtlety-AllItems-GlowingTwilightScale-54573" value: { - dps: 8035.95288 - tps: 5700.95502 + dps: 8049.6123 + tps: 5713.6272 } } dps_results: { key: "TestSubtlety-AllItems-GlowingTwilightScale-54589" value: { - dps: 8035.95288 - tps: 5700.95502 + dps: 8049.6123 + tps: 5713.6272 } } dps_results: { key: "TestSubtlety-AllItems-GnomishLightningGenerator-41121" value: { - dps: 8227.37204 - tps: 5838.77336 + dps: 8237.10224 + tps: 5845.90324 } } dps_results: { key: "TestSubtlety-AllItems-Heartpierce-49982" value: { - dps: 8454.06774 - tps: 5999.57668 + dps: 8485.82408 + tps: 6021.4541 } } dps_results: { key: "TestSubtlety-AllItems-Heartpierce-50641" value: { - dps: 8454.06774 - tps: 5999.57668 + dps: 8485.82408 + tps: 6021.4541 } } dps_results: { key: "TestSubtlety-AllItems-IllustrationoftheDragonSoul-40432" value: { - dps: 8035.95288 - tps: 5700.95502 + dps: 8049.6123 + tps: 5713.6272 } } dps_results: { key: "TestSubtlety-AllItems-ImpassiveSkyflareDiamond" value: { - dps: 8322.88244 - tps: 5907.26765 + dps: 8312.67486 + tps: 5899.64383 } } dps_results: { key: "TestSubtlety-AllItems-ImpassiveStarflareDiamond" value: { - dps: 8316.91955 - tps: 5902.08392 + dps: 8310.90956 + tps: 5898.3291 } } dps_results: { key: "TestSubtlety-AllItems-IncisorFragment-37723" value: { - dps: 8237.90827 - tps: 5844.28379 + dps: 8250.86039 + tps: 5856.49312 } } dps_results: { key: "TestSubtlety-AllItems-InsightfulEarthsiegeDiamond" value: { - dps: 8261.76312 - tps: 5863.2263 + dps: 8299.40941 + tps: 5889.19725 } } dps_results: { key: "TestSubtlety-AllItems-InvigoratingEarthsiegeDiamond" value: { - dps: 8297.78735 - tps: 5888.79335 + dps: 8335.55854 + tps: 5914.85086 hps: 10.24843 } } dps_results: { key: "TestSubtlety-AllItems-Lavanthor'sTalisman-37872" value: { - dps: 8035.95288 - tps: 5700.95502 + dps: 8049.6123 + tps: 5713.6272 } } dps_results: { key: "TestSubtlety-AllItems-MajesticDragonFigurine-40430" value: { - dps: 8035.95288 - tps: 5700.95502 + dps: 8049.6123 + tps: 5713.6272 } } dps_results: { key: "TestSubtlety-AllItems-MeteoriteWhetstone-37390" value: { - dps: 8284.91227 - tps: 5881.11417 + dps: 8318.31307 + tps: 5901.61904 } } dps_results: { key: "TestSubtlety-AllItems-NevermeltingIceCrystal-50259" value: { - dps: 8085.00956 - tps: 5738.67109 + dps: 8114.78252 + tps: 5757.12007 } } dps_results: { key: "TestSubtlety-AllItems-OfferingofSacrifice-37638" value: { - dps: 8035.95288 - tps: 5700.95502 + dps: 8049.6123 + tps: 5713.6272 } } dps_results: { key: "TestSubtlety-AllItems-PersistentEarthshatterDiamond" value: { - dps: 8290.92559 - tps: 5883.92344 + dps: 8328.67299 + tps: 5909.96446 } } dps_results: { key: "TestSubtlety-AllItems-PersistentEarthsiegeDiamond" value: { - dps: 8297.78735 - tps: 5888.79335 + dps: 8335.55854 + tps: 5914.85086 } } dps_results: { key: "TestSubtlety-AllItems-PetrifiedTwilightScale-54571" value: { - dps: 8035.95288 - tps: 5700.95502 + dps: 8049.6123 + tps: 5713.6272 } } dps_results: { key: "TestSubtlety-AllItems-PetrifiedTwilightScale-54591" value: { - dps: 8035.95288 - tps: 5700.95502 + dps: 8049.6123 + tps: 5713.6272 } } dps_results: { key: "TestSubtlety-AllItems-PowerfulEarthshatterDiamond" value: { - dps: 8261.76312 - tps: 5863.2263 + dps: 8299.40941 + tps: 5889.19725 } } dps_results: { key: "TestSubtlety-AllItems-PowerfulEarthsiegeDiamond" value: { - dps: 8261.76312 - tps: 5863.2263 + dps: 8299.40941 + tps: 5889.19725 } } dps_results: { key: "TestSubtlety-AllItems-PurifiedShardoftheGods" value: { - dps: 8035.95288 - tps: 5700.95502 + dps: 8049.6123 + tps: 5713.6272 } } dps_results: { key: "TestSubtlety-AllItems-ReignoftheDead-47316" value: { - dps: 8212.12797 - tps: 5830.14307 + dps: 8182.9789 + tps: 5806.63107 } } dps_results: { key: "TestSubtlety-AllItems-ReignoftheDead-47477" value: { - dps: 8232.61577 - tps: 5844.6894 + dps: 8203.35637 + tps: 5821.09908 } } dps_results: { key: "TestSubtlety-AllItems-RelentlessEarthsiegeDiamond" value: { - dps: 8454.06774 - tps: 5999.57668 + dps: 8485.82408 + tps: 6021.4541 } } dps_results: { key: "TestSubtlety-AllItems-RevitalizingSkyflareDiamond" value: { - dps: 8261.76312 - tps: 5863.2263 + dps: 8299.40941 + tps: 5889.19725 } } dps_results: { key: "TestSubtlety-AllItems-RuneofRepulsion-40372" value: { - dps: 8035.95288 - tps: 5700.95502 + dps: 8049.6123 + tps: 5713.6272 } } dps_results: { key: "TestSubtlety-AllItems-SealofthePantheon-36993" value: { - dps: 8035.95288 - tps: 5700.95502 + dps: 8049.6123 + tps: 5713.6272 } } dps_results: { key: "TestSubtlety-AllItems-Shadowblade'sBattlegear" value: { - dps: 7813.84533 - tps: 5546.3611 + dps: 7901.07659 + tps: 5607.30215 } } dps_results: { key: "TestSubtlety-AllItems-ShinyShardoftheGods" value: { - dps: 8035.95288 - tps: 5700.95502 + dps: 8049.6123 + tps: 5713.6272 } } dps_results: { key: "TestSubtlety-AllItems-Sindragosa'sFlawlessFang-50361" value: { - dps: 8035.95288 - tps: 5700.95502 + dps: 8049.6123 + tps: 5713.6272 } } dps_results: { key: "TestSubtlety-AllItems-Slayer'sArmor" value: { - dps: 5459.85722 - tps: 3874.37594 + dps: 5490.51321 + tps: 3895.42103 } } dps_results: { key: "TestSubtlety-AllItems-SliverofPureIce-50339" value: { - dps: 8035.95288 - tps: 5700.95502 + dps: 8049.6123 + tps: 5713.6272 } } dps_results: { key: "TestSubtlety-AllItems-SliverofPureIce-50346" value: { - dps: 8035.95288 - tps: 5700.95502 + dps: 8049.6123 + tps: 5713.6272 } } dps_results: { key: "TestSubtlety-AllItems-SouloftheDead-40382" value: { - dps: 8181.9152 - tps: 5805.6388 + dps: 8226.61702 + tps: 5836.75581 } } dps_results: { key: "TestSubtlety-AllItems-SparkofLife-37657" value: { - dps: 8131.0433 - tps: 5770.75727 + dps: 8165.73828 + tps: 5795.54841 } } dps_results: { key: "TestSubtlety-AllItems-SphereofRedDragon'sBlood-37166" value: { - dps: 8274.77948 - tps: 5870.60057 + dps: 8304.65184 + tps: 5894.33464 } } dps_results: { key: "TestSubtlety-AllItems-StormshroudArmor" value: { - dps: 6183.09079 - tps: 4387.29381 + dps: 6171.04692 + tps: 4379.43616 } } dps_results: { key: "TestSubtlety-AllItems-SwiftSkyflareDiamond" value: { - dps: 8297.78735 - tps: 5888.79335 + dps: 8335.55854 + tps: 5914.85086 } } dps_results: { key: "TestSubtlety-AllItems-SwiftStarflareDiamond" value: { - dps: 8290.92559 - tps: 5883.92344 + dps: 8328.67299 + tps: 5909.96446 } } dps_results: { key: "TestSubtlety-AllItems-SwiftWindfireDiamond" value: { - dps: 8278.91752 - tps: 5875.40109 + dps: 8316.62328 + tps: 5901.41326 } } dps_results: { key: "TestSubtlety-AllItems-TalismanofTrollDivinity-37734" value: { - dps: 8035.95288 - tps: 5700.95502 + dps: 8049.6123 + tps: 5713.6272 } } dps_results: { key: "TestSubtlety-AllItems-TearsoftheVanquished-47215" value: { - dps: 8035.95288 - tps: 5700.95502 + dps: 8049.6123 + tps: 5713.6272 } } dps_results: { key: "TestSubtlety-AllItems-TerrorbladeBattlegear" value: { - dps: 7600.75076 - tps: 5393.76142 + dps: 7606.71412 + tps: 5397.43663 } } dps_results: { key: "TestSubtlety-AllItems-TheFistsofFury" value: { - dps: 7154.67026 - tps: 5077.84718 + dps: 7177.50262 + tps: 5093.95169 } } dps_results: { key: "TestSubtlety-AllItems-TheGeneral'sHeart-45507" value: { - dps: 8035.95288 - tps: 5700.95502 + dps: 8049.6123 + tps: 5713.6272 } } dps_results: { key: "TestSubtlety-AllItems-ThunderingSkyflareDiamond" value: { - dps: 8334.3629 - tps: 5914.35939 + dps: 8357.93928 + tps: 5929.49372 } } dps_results: { key: "TestSubtlety-AllItems-TinyAbominationinaJar-50351" value: { - dps: 8515.69418 - tps: 6045.24495 + dps: 8487.89694 + tps: 6023.54112 } } dps_results: { key: "TestSubtlety-AllItems-TinyAbominationinaJar-50706" value: { - dps: 8570.48789 - tps: 6084.56421 + dps: 8543.25167 + tps: 6064.472 } } dps_results: { key: "TestSubtlety-AllItems-TirelessSkyflareDiamond" value: { - dps: 8261.76312 - tps: 5863.2263 + dps: 8299.40941 + tps: 5889.19725 } } dps_results: { key: "TestSubtlety-AllItems-TirelessStarflareDiamond" value: { - dps: 8261.76312 - tps: 5863.2263 + dps: 8299.40941 + tps: 5889.19725 } } dps_results: { key: "TestSubtlety-AllItems-TomeofArcanePhenomena-36972" value: { - dps: 8125.92317 - tps: 5764.32568 + dps: 8087.7673 + tps: 5740.34157 } } dps_results: { key: "TestSubtlety-AllItems-TrenchantEarthshatterDiamond" value: { - dps: 8261.76312 - tps: 5863.2263 + dps: 8299.40941 + tps: 5889.19725 } } dps_results: { key: "TestSubtlety-AllItems-TrenchantEarthsiegeDiamond" value: { - dps: 8261.76312 - tps: 5863.2263 + dps: 8299.40941 + tps: 5889.19725 } } dps_results: { key: "TestSubtlety-AllItems-UndeadSlayer'sBlessedArmor" value: { - dps: 6344.76232 - tps: 4504.70274 + dps: 6327.99992 + tps: 4492.17311 } } dps_results: { key: "TestSubtlety-AllItems-VanCleef'sBattlegear" value: { - dps: 7367.92421 - tps: 5228.58645 + dps: 7369.3106 + tps: 5231.2652 } } dps_results: { key: "TestSubtlety-AllItems-WingedTalisman-37844" value: { - dps: 8035.95288 - tps: 5700.95502 + dps: 8049.6123 + tps: 5713.6272 } } dps_results: { key: "TestSubtlety-Average-Default" value: { - dps: 8502.90599 - tps: 6034.44073 + dps: 8508.99372 + tps: 6039.04633 } } dps_results: { key: "TestSubtlety-Settings-BloodElf-P2 Subtlety-Subtlety-FullBuffs-LongMultiTarget" value: { - dps: 16082.10667 - tps: 11416.87126 + dps: 31218.16635 + tps: 22164.89811 } } dps_results: { key: "TestSubtlety-Settings-BloodElf-P2 Subtlety-Subtlety-FullBuffs-LongSingleTarget" value: { - dps: 8454.06774 - tps: 5999.57668 + dps: 8485.82408 + tps: 6021.4541 } } dps_results: { key: "TestSubtlety-Settings-BloodElf-P2 Subtlety-Subtlety-FullBuffs-ShortSingleTarget" value: { - dps: 9821.35525 - tps: 6956.74076 + dps: 9799.15972 + tps: 6943.4513 } } dps_results: { key: "TestSubtlety-Settings-BloodElf-P2 Subtlety-Subtlety-NoBuffs-LongMultiTarget" value: { - dps: 8893.80872 - tps: 6312.23233 + dps: 19204.58632 + tps: 13635.25629 } } dps_results: { key: "TestSubtlety-Settings-BloodElf-P2 Subtlety-Subtlety-NoBuffs-LongSingleTarget" value: { - dps: 4194.03777 - tps: 2975.25563 + dps: 4205.73521 + tps: 2983.85189 } } dps_results: { key: "TestSubtlety-Settings-BloodElf-P2 Subtlety-Subtlety-NoBuffs-ShortSingleTarget" value: { - dps: 4411.5374 - tps: 3119.62587 + dps: 4429.94747 + tps: 3129.99515 } } dps_results: { key: "TestSubtlety-Settings-Orc-P2 Subtlety-Subtlety-FullBuffs-LongMultiTarget" value: { - dps: 15890.74333 - tps: 11278.08768 + dps: 30990.84554 + tps: 22003.50033 } } dps_results: { key: "TestSubtlety-Settings-Orc-P2 Subtlety-Subtlety-FullBuffs-LongSingleTarget" value: { - dps: 8460.63724 - tps: 6004.41739 + dps: 8499.03312 + tps: 6031.524 } } dps_results: { key: "TestSubtlety-Settings-Orc-P2 Subtlety-Subtlety-FullBuffs-ShortSingleTarget" value: { - dps: 9912.79565 - tps: 7024.28261 + dps: 9862.01742 + tps: 6985.12024 } } dps_results: { key: "TestSubtlety-Settings-Orc-P2 Subtlety-Subtlety-NoBuffs-LongMultiTarget" value: { - dps: 8313.5236 - tps: 5899.77275 + dps: 18986.91332 + tps: 13480.70846 } } dps_results: { key: "TestSubtlety-Settings-Orc-P2 Subtlety-Subtlety-NoBuffs-LongSingleTarget" value: { - dps: 4187.52126 - tps: 2970.89071 + dps: 4209.17315 + tps: 2986.37609 } } dps_results: { key: "TestSubtlety-Settings-Orc-P2 Subtlety-Subtlety-NoBuffs-ShortSingleTarget" value: { - dps: 4412.80719 - tps: 3126.53048 + dps: 4410.85845 + tps: 3125.41912 } } dps_results: { key: "TestSubtlety-SwitchInFrontOfTarget-Default" value: { - dps: 3217.60828 - tps: 2284.50188 + dps: 7719.63138 + tps: 5478.60469 } } diff --git a/sim/rogue/backstab.go b/sim/rogue/backstab.go index a3ffbdc707..2711f377d3 100644 --- a/sim/rogue/backstab.go +++ b/sim/rogue/backstab.go @@ -54,12 +54,10 @@ func (rogue *Rogue) registerBackstabSpell() { if result.Landed() { rogue.AddComboPoints(sim, 1, spell.ComboPointMetrics()) - // FIXME: Extension of a Rupture Dot can occur up to 3 times - ruptureDot := rogue.Rupture.Dot(target) - if hasGlyph && ruptureDot.IsActive() { - ruptureDot.NumberOfTicks += 1 - ruptureDot.RecomputeAuraDuration() - ruptureDot.UpdateExpires(ruptureDot.ExpiresAt() + ruptureDot.TickLength) + if dot := rogue.Rupture.Dot(target); hasGlyph && dot.IsActive() && dot.NumberOfTicks < dot.MaxStacks+3 { + dot.NumberOfTicks += 1 + dot.RecomputeAuraDuration() + dot.UpdateExpires(dot.ExpiresAt() + dot.TickLength) } } else { spell.IssueRefund(sim) diff --git a/sim/rogue/fan_of_knives.go b/sim/rogue/fan_of_knives.go index 4bf935922a..13cb12a888 100644 --- a/sim/rogue/fan_of_knives.go +++ b/sim/rogue/fan_of_knives.go @@ -13,10 +13,10 @@ func (rogue *Rogue) makeFanOfKnivesWeaponHitSpell(isMH bool) *core.Spell { var procMask core.ProcMask var weaponMultiplier float64 if isMH { - weaponMultiplier = core.TernaryFloat64(rogue.Equip[proto.ItemSlot_ItemSlotMainHand].WeaponType == proto.WeaponType_WeaponTypeDagger, 1.05, 0.7) + weaponMultiplier = core.TernaryFloat64(rogue.HasDagger(core.MainHand), 1.05, 0.7) procMask = core.ProcMaskMeleeMHSpecial } else { - weaponMultiplier = core.TernaryFloat64(rogue.Equip[proto.ItemSlot_ItemSlotOffHand].WeaponType == proto.WeaponType_WeaponTypeDagger, 1.05, 0.7) + weaponMultiplier = core.TernaryFloat64(rogue.HasDagger(core.OffHand), 1.05, 0.7) weaponMultiplier *= rogue.dwsMultiplier() procMask = core.ProcMaskMeleeOHSpecial } diff --git a/sim/rogue/ghostly_strike.go b/sim/rogue/ghostly_strike.go index 1ad3389800..9f50b43ec6 100644 --- a/sim/rogue/ghostly_strike.go +++ b/sim/rogue/ghostly_strike.go @@ -15,7 +15,6 @@ func (rogue *Rogue) registerGhostlyStrikeSpell() { hasGlyph := rogue.HasMajorGlyph(proto.RogueMajorGlyph_GlyphOfGhostlyStrike) actionID := core.ActionID{SpellID: 14278} - daggerMH := rogue.Equip[proto.ItemSlot_ItemSlotMainHand].WeaponType == proto.WeaponType_WeaponTypeDagger rogue.GhostlyStrike = rogue.RegisterSpell(core.SpellConfig{ ActionID: actionID, @@ -41,7 +40,7 @@ func (rogue *Rogue) registerGhostlyStrikeSpell() { BonusCritRating: core.TernaryFloat64(rogue.HasSetBonus(ItemSetVanCleefs, 4), 5*core.CritRatingPerCritChance, 0) + []float64{0, 2, 4, 6}[rogue.Talents.TurnTheTables]*core.CritRatingPerCritChance, - DamageMultiplier: (core.TernaryFloat64(daggerMH, 1.8, 1.25) + core.TernaryFloat64(hasGlyph, 0.4, 0)) * (1 + 0.02*float64(rogue.Talents.FindWeakness)), + DamageMultiplier: core.TernaryFloat64(rogue.HasDagger(core.MainHand), 1.8, 1.25) * core.TernaryFloat64(hasGlyph, 1.4, 1) * (1 + 0.02*float64(rogue.Talents.FindWeakness)), CritMultiplier: rogue.MeleeCritMultiplier(true), ThreatMultiplier: 1, diff --git a/sim/rogue/hemorrhage.go b/sim/rogue/hemorrhage.go index acc7cc4a46..336f384514 100644 --- a/sim/rogue/hemorrhage.go +++ b/sim/rogue/hemorrhage.go @@ -51,7 +51,6 @@ func (rogue *Rogue) registerHemorrhageSpell() { }) } - daggerMH := rogue.Equip[proto.ItemSlot_ItemSlotMainHand].WeaponType == proto.WeaponType_WeaponTypeDagger rogue.Hemorrhage = rogue.RegisterSpell(core.SpellConfig{ ActionID: actionID, SpellSchool: core.SpellSchoolPhysical, @@ -72,7 +71,7 @@ func (rogue *Rogue) registerHemorrhageSpell() { BonusCritRating: core.TernaryFloat64(rogue.HasSetBonus(ItemSetVanCleefs, 4), 5*core.CritRatingPerCritChance, 0) + []float64{0, 2, 4, 6}[rogue.Talents.TurnTheTables]*core.CritRatingPerCritChance, - DamageMultiplier: core.TernaryFloat64(daggerMH, 1.6, 1.1) * (1 + + DamageMultiplier: core.TernaryFloat64(rogue.HasDagger(core.MainHand), 1.6, 1.1) * (1 + 0.02*float64(rogue.Talents.FindWeakness) + core.TernaryFloat64(rogue.HasSetBonus(ItemSetSlayers, 4), 0.06, 0)) * (1 + 0.02*float64(rogue.Talents.SinisterCalling)), diff --git a/sim/rogue/items.go b/sim/rogue/items.go index 315694668c..cd1cfa3ead 100644 --- a/sim/rogue/items.go +++ b/sim/rogue/items.go @@ -111,28 +111,7 @@ var ItemSetShadowblades = core.NewItemSet(core.ItemSet{ }, 4: func(agent core.Agent) { // Gives your melee finishing moves a 13% chance to add 3 combo points to your target. - actionID := core.ActionID{SpellID: 70803} - rogue := agent.(RogueAgent).GetRogue() - metrics := rogue.NewComboPointMetrics(actionID) - rogue.RegisterAura(core.Aura{ - Label: "Shadowblade's 4pc", - Duration: core.NeverExpires, - OnReset: func(aura *core.Aura, sim *core.Simulation) { - aura.Activate(sim) - }, - OnSpellHitDealt: func(aura *core.Aura, sim *core.Simulation, spell *core.Spell, result *core.SpellResult) { - if !result.Landed() { - return - } - if !spell.Flags.Matches(SpellFlagFinisher) { - return - } - if sim.RandomFloat("Shadowblades") > 0.13 { - return - } - rogue.AddComboPoints(sim, 3, metrics) - }, - }) + // Handled in the finishing move effect applier }, }, }) diff --git a/sim/rogue/rogue.go b/sim/rogue/rogue.go index bd43168183..f7a00cbece 100644 --- a/sim/rogue/rogue.go +++ b/sim/rogue/rogue.go @@ -29,9 +29,6 @@ const ( SpellFlagBuilder = core.SpellFlagAgentReserved2 SpellFlagFinisher = core.SpellFlagAgentReserved3 SpellFlagColdBlooded = core.SpellFlagAgentReserved4 - AssassinTree = 0 - CombatTree = 1 - SubtletyTree = 2 ) var TalentTreeSizes = [3]int{27, 28, 28} @@ -45,11 +42,9 @@ type Rogue struct { Options *proto.Rogue_Options Rotation *proto.Rogue_Rotation - priorityItems []roguePriorityItem - rotationItems []rogueRotationItem - assassinationPrios []assassinationPrio - subtletyPrios []subtletyPrio - bleedCategory *core.ExclusiveCategory + rotation rotation + + bleedCategory *core.ExclusiveCategory sliceAndDiceDurations [6]time.Duration exposeArmorDurations [6]time.Duration @@ -58,8 +53,6 @@ type Rogue struct { maxEnergy float64 - BuilderPoints int32 - Builder *core.Spell Backstab *core.Spell BladeFlurry *core.Spell DeadlyPoison *core.Spell @@ -189,28 +182,10 @@ func (rogue *Rogue) Initialize() { rogue.finishingMoveEffectApplier = rogue.makeFinishingMoveEffectApplier() } -func (rogue *Rogue) getExpectedEnergyPerSecond() 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 (rogue *Rogue) ApplyEnergyTickMultiplier(multiplier float64) { rogue.EnergyTickMultiplier += multiplier } -func (rogue *Rogue) getExpectedComboPointsPerSecond() float64 { - return 1 / rogue.getExpectedSecondsPerComboPoint() -} - -func (rogue *Rogue) getExpectedSecondsPerComboPoint() float64 { - honorAmongThievesChance := []float64{0, 0.33, 0.66, 1.0}[rogue.Talents.HonorAmongThieves] - return 1 + 1/(float64(rogue.Options.HonorOfThievesCritRate+100)/100*honorAmongThievesChance) -} - func (rogue *Rogue) Reset(sim *core.Simulation) { for _, mcd := range rogue.GetMajorCooldowns() { mcd.Disable() @@ -235,7 +210,8 @@ func (rogue *Rogue) Reset(sim *core.Simulation) { rogue.MasterOfSubtletyAura.UpdateExpires(sim.CurrentTime + dur) } } - rogue.setPriorityItems(sim) + + rogue.setupRotation(sim) } func (rogue *Rogue) MeleeCritMultiplier(applyLethality bool) float64 { @@ -304,10 +280,14 @@ func (rogue *Rogue) ApplyCutToTheChase(sim *core.Simulation) { } func (rogue *Rogue) CanMutilate() bool { - return rogue.Talents.Mutilate && - rogue.HasMHWeapon() && rogue.HasOHWeapon() && - rogue.GetMHWeapon().WeaponType == proto.WeaponType_WeaponTypeDagger && - rogue.GetOHWeapon().WeaponType == proto.WeaponType_WeaponTypeDagger + return rogue.Talents.Mutilate && rogue.HasDagger(core.MainHand) && rogue.HasDagger(core.OffHand) +} + +func (rogue *Rogue) HasDagger(hand core.Hand) bool { + if hand == core.MainHand { + return rogue.HasMHWeapon() && rogue.GetMHWeapon().WeaponType == proto.WeaponType_WeaponTypeDagger + } + return rogue.HasOHWeapon() && rogue.GetOHWeapon().WeaponType == proto.WeaponType_WeaponTypeDagger } func init() { diff --git a/sim/rogue/rotation.go b/sim/rogue/rotation.go index bff6d9a2cd..84706aebba 100644 --- a/sim/rogue/rotation.go +++ b/sim/rogue/rotation.go @@ -1,531 +1,61 @@ package rogue import ( - "math" - "time" - "github.com/wowsims/wotlk/sim/core" - "github.com/wowsims/wotlk/sim/core/proto" ) -func (rogue *Rogue) OnEnergyGain(sim *core.Simulation) { - if rogue.Talents.Mutilate && sim.GetNumTargets() <= 3 { - rogue.OnCanAct(sim) - return - } - if rogue.KillingSpreeAura.IsActive() { - rogue.DoNothing() - return - } - rogue.TryUseCooldowns(sim) - if rogue.GCD.IsReady(sim) { - if rogue.Talents.HonorAmongThieves > 0 { - rogue.doSubtletyRotation(sim) - } else { - rogue.rotation(sim) - } - } -} - -func (rogue *Rogue) OnGCDReady(sim *core.Simulation) { - if rogue.Talents.Mutilate && sim.GetNumTargets() <= 3 { - rogue.OnCanAct(sim) - return - } - if rogue.Talents.HonorAmongThieves > 0 && sim.GetNumTargets() <= 3 { - rogue.OnCanActSubtlety(sim) - return - } - if rogue.KillingSpreeAura.IsActive() { - rogue.DoNothing() - return - } - rogue.TryUseCooldowns(sim) - if rogue.IsWaitingForEnergy() { - rogue.DoNothing() - return - } - if rogue.GCD.IsReady(sim) { - rogue.rotation(sim) - } -} - -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 rotation interface { + setup(sim *core.Simulation, rogue *Rogue) + run(sim *core.Simulation, rogue *Rogue) } -type shouldCastRotationItemResult int32 +type PriorityAction int32 const ( - ShouldNotCast shouldCastRotationItemResult = iota - ShouldBuild - ShouldCast - ShouldWait + Skip PriorityAction = iota + Build + Cast + Wait + Once ) -func (rogue *Rogue) energyToBuild(points int32) float64 { - costPerBuilder := rogue.Builder.DefaultCast.Cost - - buildersNeeded := math.Ceil(float64(points) / float64(rogue.BuilderPoints)) - return buildersNeeded * costPerBuilder -} - -func (rogue *Rogue) timeToBuild(_ *core.Simulation, points int32, builderPoints int32, eps float64, finisherCost float64) time.Duration { - energyNeeded := rogue.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 core.MaxDuration(time.Second*time.Duration(secondsNeeded), time.Second*time.Duration(globalsNeeded)) -} - -func (rogue *Rogue) shouldCastNextRotationItem(sim *core.Simulation, eps float64) shouldCastRotationItemResult { - if len(rogue.rotationItems) == 0 { - panic("Empty rotation") - } - currentEnergy := rogue.CurrentEnergy() - comboPoints := rogue.ComboPoints() - currentTime := sim.CurrentTime - item := rogue.rotationItems[0] - prio := rogue.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(rogue.rotationItems) >= 2 { - timeElapsed := time.Second * 1 - for _, nextItem := range rogue.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 >= rogue.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 rogue.priorityItems[item.PrioIndex+1:] { - if comboPoints > lowerPrio.MinimumComboPoints && currentEnergy > lowerPrio.EnergyCost && lowerPrio.MaxCasts == 0 { - rogue.rotationItems = append([]rogueRotationItem{ - {ExpiresAt: currentTime, PrioIndex: lpi + item.PrioIndex + 1}, - }, rogue.rotationItems...) - return ShouldCast - } - } - } - // Overcap CP with builder - if rogue.timeToBuild(sim, 1, rogue.BuilderPoints, eps, prio.EnergyCost+prio.PoolAmount) <= tte && currentEnergy >= rogue.Builder.DefaultCast.Cost { - return ShouldBuild - } - } else if comboPoints < prio.MinimumComboPoints { // Need CP - if currentEnergy >= rogue.Builder.DefaultCast.Cost { - return ShouldBuild - } else { - return ShouldWait - } - } else { // Between MinimumComboPoints and MaximumComboPoints - if currentEnergy >= prio.EnergyCost+prio.PoolAmount && tte <= timeUntilNextGCD { - return ShouldCast - } - ttb := rogue.timeToBuild(sim, 1, 2, eps, prio.EnergyCost+prio.PoolAmount-currentEnergy) - if currentEnergy >= rogue.Builder.DefaultCast.Cost && tte > ttb { - return ShouldBuild - } - } - return ShouldWait +type prio struct { + check func(sim *core.Simulation, rogue *Rogue) PriorityAction + cast func(sim *core.Simulation, rogue *Rogue) bool + cost float64 } -func (rogue *Rogue) planRotation(sim *core.Simulation) []rogueRotationItem { - var rotationItems []rogueRotationItem - eps := rogue.getExpectedEnergyPerSecond() - for pi, prio := range rogue.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 := rogue.timeToBuild(sim, prio.MinimumComboPoints, rogue.BuilderPoints, eps, prio.EnergyCost) - maximumBuildDuration := rogue.timeToBuild(sim, maxCP, rogue.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 := rogue.priorityItems[item.PrioIndex] - maxBuildAt := item.ExpiresAt - item.MaximumBuildDuration - if prio.Aura == nil { - timeValueOfResources := time.Duration((float64(comboPoints)*rogue.Builder.DefaultCast.Cost/float64(rogue.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 := core.MaxInt32(0, prio.MinimumComboPoints-comboPoints) - energyUsed := core.MaxFloat(0, prio.EnergyCost-currentEnergy) - minBuildTime := rogue.timeToBuild(sim, cpUsed, rogue.BuilderPoints, eps, energyUsed) - if currentTime+minBuildTime <= item.ExpiresAt || !prio.IsFiller { - prioStack = append(prioStack, item) - currentTime = core.MaxDuration(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 - } - } - } - } +func (rogue *Rogue) OnEnergyGain(sim *core.Simulation) { + rogue.TryUseCooldowns(sim) - // Reverse - for i, j := 0, len(prioStack)-1; i < j; i, j = i+1, j-1 { - prioStack[i], prioStack[j] = prioStack[j], prioStack[i] + if !rogue.GCD.IsReady(sim) { + return } - return prioStack + rogue.rotation.run(sim, rogue) } -func (rogue *Rogue) setPriorityItems(sim *core.Simulation) { - rogue.Builder = rogue.SinisterStrike - rogue.BuilderPoints = 1 - if rogue.PrimaryTalentTree == AssassinTree { - if rogue.CanMutilate() { - rogue.Builder = rogue.Mutilate - rogue.BuilderPoints = 2 - } - rogue.setupAssassinationRotation(sim) - } - if rogue.PrimaryTalentTree == SubtletyTree { - rogue.setSubtletyBuilder(sim) - rogue.setupSubtletyRotation(sim) - } - isMultiTarget := sim.GetNumTargets() >= 3 - // Slice and Dice - rogue.priorityItems = rogue.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 isMultiTarget { - if rogue.Rotation.MultiTargetSliceFrequency != proto.Rogue_Rotation_Never { - sliceAndDice.MinimumComboPoints = core.MaxInt32(1, rogue.Rotation.MinimumComboPointsMultiTargetSlice) - if rogue.Rotation.MultiTargetSliceFrequency == proto.Rogue_Rotation_Once { - sliceAndDice.MaxCasts = 1 - } - rogue.priorityItems = append(rogue.priorityItems, sliceAndDice) - } - } else { - rogue.priorityItems = append(rogue.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 - } - rogue.priorityItems = append(rogue.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 { - rogue.priorityItems = append(rogue.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 - rogue.priorityItems = append(rogue.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 - }, - }) - - // Rupture - rupture := roguePriorityItem{ - MinimumComboPoints: 3, - MaximumComboPoints: 5, - Aura: rogue.Rupture.CurDot().Aura, - EnergyCost: rogue.Rupture.DefaultCast.Cost, - GetDuration: func(rogue *Rogue, cp int32) time.Duration { - return rogue.RuptureDuration(cp) - }, - GetSpell: func(rogue *Rogue, cp int32) *core.Spell { - return rogue.Rupture - }, - } +func (rogue *Rogue) OnGCDReady(sim *core.Simulation) { + rogue.TryUseCooldowns(sim) - // Eviscerate - eviscerate := roguePriorityItem{ - MinimumComboPoints: 1, - MaximumComboPoints: 5, - EnergyCost: rogue.Eviscerate.DefaultCast.Cost, - GetDuration: func(rogue *Rogue, cp int32) time.Duration { - return 0 - }, - GetSpell: func(rogue *Rogue, cp int32) *core.Spell { - return rogue.Eviscerate - }, + if rogue.IsWaitingForEnergy() { + rogue.DoNothing() + return } - if isMultiTarget { - rogue.priorityItems = append(rogue.priorityItems, roguePriorityItem{ - MaximumComboPoints: 0, - EnergyCost: rogue.FanOfKnives.DefaultCast.Cost, - GetSpell: func(rogue *Rogue, i int32) *core.Spell { - return rogue.FanOfKnives - }, - }) - - } else if rogue.Talents.MasterPoisoner > 0 || rogue.Talents.CutToTheChase > 0 { - // Envenom - envenom := roguePriorityItem{ - MinimumComboPoints: 1, - MaximumComboPoints: 5, - Aura: rogue.EnvenomAura, - EnergyCost: rogue.Envenom.DefaultCast.Cost, - GetDuration: func(rogue *Rogue, cp int32) time.Duration { - return rogue.EnvenomAura.Duration - }, - GetSpell: func(rogue *Rogue, cp int32) *core.Spell { - return rogue.Envenom - }, - } - switch rogue.Rotation.AssassinationFinisherPriority { - case proto.Rogue_Rotation_EnvenomRupture: - envenom.MinimumComboPoints = core.MaxInt32(1, rogue.Rotation.MinimumComboPointsPrimaryFinisher) - rogue.priorityItems = append(rogue.priorityItems, envenom) - rupture.MinimumComboPoints = rogue.Rotation.MinimumComboPointsSecondaryFinisher - rupture.IsFiller = true - if rupture.MinimumComboPoints > 0 && rupture.MinimumComboPoints <= 5 { - rogue.priorityItems = append(rogue.priorityItems, rupture) - } - case proto.Rogue_Rotation_RuptureEnvenom: - rupture.MinimumComboPoints = core.MaxInt32(1, rogue.Rotation.MinimumComboPointsPrimaryFinisher) - rogue.priorityItems = append(rogue.priorityItems, rupture) - envenom.MinimumComboPoints = rogue.Rotation.MinimumComboPointsSecondaryFinisher - envenom.IsFiller = true - if envenom.MinimumComboPoints > 0 && envenom.MinimumComboPoints <= 5 { - rogue.priorityItems = append(rogue.priorityItems, envenom) - } - } - eviscerate.IsFiller = true - eviscerate.MinimumComboPoints = 1 - rogue.priorityItems = append(rogue.priorityItems, eviscerate) - } else { - if rogue.PrimaryTalentTree == CombatTree { - switch rogue.Rotation.CombatFinisherPriority { - case proto.Rogue_Rotation_RuptureEviscerate: - rupture.MinimumComboPoints = core.MaxInt32(1, rogue.Rotation.MinimumComboPointsPrimaryFinisher) - rogue.priorityItems = append(rogue.priorityItems, rupture) - eviscerate.MinimumComboPoints = core.MaxInt32(1, rogue.Rotation.MinimumComboPointsSecondaryFinisher) - eviscerate.IsFiller = true - rogue.priorityItems = append(rogue.priorityItems, eviscerate) - case proto.Rogue_Rotation_EviscerateRupture: - eviscerate.MinimumComboPoints = core.MaxInt32(1, rogue.Rotation.MinimumComboPointsPrimaryFinisher) - rogue.priorityItems = append(rogue.priorityItems, eviscerate) - rupture.MinimumComboPoints = rogue.Rotation.MinimumComboPointsSecondaryFinisher - rupture.IsFiller = true - if rupture.MinimumComboPoints > 0 && rupture.MinimumComboPoints <= 5 { - rogue.priorityItems = append(rogue.priorityItems, rupture) - } - } - } else { - switch rogue.Rotation.SubtletyFinisherPriority { - case proto.Rogue_Rotation_SubtletyEviscerate: - rupture.MinimumComboPoints = core.MaxInt32(1, rogue.Rotation.MinimumComboPointsPrimaryFinisher) - rogue.priorityItems = append(rogue.priorityItems, rupture) - eviscerate.MinimumComboPoints = core.MaxInt32(1, rogue.Rotation.MinimumComboPointsSecondaryFinisher) - eviscerate.IsFiller = true - rogue.priorityItems = append(rogue.priorityItems, eviscerate) - case proto.Rogue_Rotation_SubtletyEnvenom: - eviscerate.MinimumComboPoints = core.MaxInt32(1, rogue.Rotation.MinimumComboPointsPrimaryFinisher) - rogue.priorityItems = append(rogue.priorityItems, eviscerate) - rupture.MinimumComboPoints = rogue.Rotation.MinimumComboPointsSecondaryFinisher - rupture.IsFiller = true - if rupture.MinimumComboPoints > 0 && rupture.MinimumComboPoints <= 5 { - rogue.priorityItems = append(rogue.priorityItems, rupture) - } - } - } - } - rogue.rotationItems = rogue.planRotation(sim) + rogue.rotation.run(sim, rogue) } -func (rogue *Rogue) rotation(sim *core.Simulation) { - if len(rogue.rotationItems) < 1 { - panic("Rotation is empty") - } - eps := rogue.getExpectedEnergyPerSecond() - shouldCast := rogue.shouldCastNextRotationItem(sim, eps) - item := rogue.rotationItems[0] - prio := rogue.priorityItems[item.PrioIndex] - - switch shouldCast { - case ShouldNotCast: - rogue.rotationItems = rogue.rotationItems[1:] - rogue.rotation(sim) - case ShouldBuild: - spell := rogue.Builder - if spell == nil || spell.Cast(sim, rogue.CurrentTarget) { - if rogue.GCD.IsReady(sim) { - rogue.rotation(sim) - } - } else { - panic("Unexpected builder cast failure") - } - case ShouldCast: - spell := prio.GetSpell(rogue, rogue.ComboPoints()) - if spell == nil || spell.Cast(sim, rogue.CurrentTarget) { - rogue.priorityItems[item.PrioIndex].CastCount += 1 - rogue.rotationItems = rogue.planRotation(sim) - if rogue.GCD.IsReady(sim) { - rogue.rotation(sim) - } - } 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 = rogue.Builder.DefaultCast.Cost - } - } - cdAvailableTime := time.Second * 10 - if sim.CurrentTime > cdAvailableTime { - cdAvailableTime = core.NeverExpires - } - nextExpiration := cdAvailableTime - for _, otherItem := range rogue.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 (rogue *Rogue) setupRotation(sim *core.Simulation) { + switch { + case rogue.CanMutilate() && rogue.Env.GetNumTargets() <= 3: + rogue.rotation = &rotation_assassination{} + case rogue.Talents.CombatPotency > 0 && rogue.Env.GetNumTargets() <= 3: + rogue.rotation = &rotation_combat{} + case rogue.Talents.HonorAmongThieves > 0 && rogue.Env.GetNumTargets() <= 3: + rogue.rotation = &rotation_subtlety{} + default: + rogue.rotation = &rotation_generic{} } + rogue.rotation.setup(sim, rogue) } diff --git a/sim/rogue/assassination_rotation.go b/sim/rogue/rotation_assassination.go similarity index 62% rename from sim/rogue/assassination_rotation.go rename to sim/rogue/rotation_assassination.go index b326ff74ea..9761b32fd3 100644 --- a/sim/rogue/assassination_rotation.go +++ b/sim/rogue/rotation_assassination.go @@ -2,64 +2,58 @@ package rogue import ( "github.com/wowsims/wotlk/sim/core/stats" + "golang.org/x/exp/slices" + "log" "time" "github.com/wowsims/wotlk/sim/core" "github.com/wowsims/wotlk/sim/core/proto" ) -type PriorityAction int32 +type rotation_assassination struct { + prios []prio +} -const ( - Skip PriorityAction = iota - Build - Cast - Wait -) +func (x *rotation_assassination) setup(sim *core.Simulation, rogue *Rogue) { + rogue.bleedCategory = rogue.CurrentTarget.GetExclusiveEffectCategory(core.BleedEffectCategory) -type GetAction func(*core.Simulation, *Rogue) PriorityAction -type DoAction func(*core.Simulation, *Rogue) bool + x.prios = x.prios[:0] -type assassinationPrio struct { - check GetAction - cast DoAction - cost float64 -} + mutiCost := rogue.Mutilate.DefaultCast.Cost + rupCost := rogue.Rupture.DefaultCast.Cost + envCost := rogue.Envenom.DefaultCast.Cost -func (rogue *Rogue) targetHasBleed(_ *core.Simulation) bool { - return rogue.bleedCategory.AnyActive() || rogue.CurrentTarget.HasActiveAuraWithTag(RogueBleedTag) -} + // estimate of energy per second while nothing is cast + energyPerSecond := func() float64 { + if rogue.Talents.FocusedAttacks == 0 { + return 10 * rogue.EnergyTickMultiplier + } -func (rogue *Rogue) setupAssassinationRotation(sim *core.Simulation) { - rogue.assassinationPrios = rogue.assassinationPrios[:0] - rogue.bleedCategory = rogue.CurrentTarget.GetExclusiveEffectCategory(core.BleedEffectCategory) + procChance := []float64{0, 0.33, 0.66, 1}[rogue.Talents.FocusedAttacks] + critSuppression := rogue.AttackTables[rogue.CurrentTarget.UnitIndex].CritSuppression + effectiveCrit := rogue.GetStat(stats.MeleeCrit)/(core.CritRatingPerCritChance*100) - critSuppression + critsPerSecond := effectiveCrit * (1/rogue.AutoAttacks.MainhandSwingSpeed().Seconds() + 1/rogue.AutoAttacks.OffhandSwingSpeed().Seconds()) + return 10*rogue.EnergyTickMultiplier + critsPerSecond*procChance*2 + } // Garrote - if rogue.Rotation.OpenWithGarrote { - hasCastGarrote := false - rogue.assassinationPrios = append(rogue.assassinationPrios, assassinationPrio{ + if rogue.Rotation.OpenWithGarrote && !rogue.PseudoStats.InFrontOfTarget { + x.prios = append(x.prios, prio{ func(sim *core.Simulation, rogue *Rogue) PriorityAction { - if hasCastGarrote { - return Skip - } if rogue.CurrentEnergy() > rogue.Garrote.DefaultCast.Cost { - return Cast + return Once } return Wait }, func(sim *core.Simulation, rogue *Rogue) bool { - casted := rogue.Garrote.Cast(sim, rogue.CurrentTarget) - if casted { - hasCastGarrote = true - } - return casted + return rogue.Garrote.Cast(sim, rogue.CurrentTarget) }, rogue.Garrote.DefaultCast.Cost, }) } // Slice And Dice - rogue.assassinationPrios = append(rogue.assassinationPrios, assassinationPrio{ + x.prios = append(x.prios, prio{ func(sim *core.Simulation, rogue *Rogue) PriorityAction { if rogue.SliceAndDiceAura.IsActive() { return Skip @@ -67,7 +61,7 @@ func (rogue *Rogue) setupAssassinationRotation(sim *core.Simulation) { if rogue.ComboPoints() > 0 && rogue.CurrentEnergy() > rogue.SliceAndDice.DefaultCast.Cost { return Cast } - if rogue.ComboPoints() < 1 && rogue.CurrentEnergy() > rogue.Builder.DefaultCast.Cost { + if rogue.ComboPoints() < 1 && rogue.CurrentEnergy() > mutiCost { return Build } return Wait @@ -80,7 +74,7 @@ func (rogue *Rogue) setupAssassinationRotation(sim *core.Simulation) { // Hunger while planning if rogue.Talents.HungerForBlood { - rogue.assassinationPrios = append(rogue.assassinationPrios, assassinationPrio{ + x.prios = append(x.prios, prio{ func(sim *core.Simulation, rogue *Rogue) PriorityAction { prioExpose := rogue.Rotation.ExposeArmorFrequency == proto.Rogue_Rotation_Once || @@ -93,11 +87,11 @@ func (rogue *Rogue) setupAssassinationRotation(sim *core.Simulation) { return Skip } - if !rogue.targetHasBleed(sim) { + if !x.targetHasBleed(sim, rogue) { return Skip } - if rogue.targetHasBleed(sim) && rogue.CurrentEnergy() > rogue.HungerForBlood.DefaultCast.Cost { + if x.targetHasBleed(sim, rogue) && rogue.CurrentEnergy() > rogue.HungerForBlood.DefaultCast.Cost { return Cast } return Wait @@ -110,10 +104,9 @@ func (rogue *Rogue) setupAssassinationRotation(sim *core.Simulation) { } // Expose armor - if rogue.Rotation.ExposeArmorFrequency == proto.Rogue_Rotation_Once || - rogue.Rotation.ExposeArmorFrequency == proto.Rogue_Rotation_Maintain { + if rogue.Rotation.ExposeArmorFrequency == proto.Rogue_Rotation_Once || rogue.Rotation.ExposeArmorFrequency == proto.Rogue_Rotation_Maintain { hasCastExpose := false - rogue.assassinationPrios = append(rogue.assassinationPrios, assassinationPrio{ + x.prios = append(x.prios, prio{ func(sim *core.Simulation, rogue *Rogue) PriorityAction { if hasCastExpose && rogue.Rotation.ExposeArmorFrequency == proto.Rogue_Rotation_Once { return Skip @@ -125,7 +118,7 @@ func (rogue *Rogue) setupAssassinationRotation(sim *core.Simulation) { } if timeLeft <= 0 { if rogue.ComboPoints() < minPoints { - if rogue.CurrentEnergy() >= rogue.Builder.DefaultCast.Cost { + if rogue.CurrentEnergy() >= mutiCost { return Build } else { return Wait @@ -138,14 +131,14 @@ func (rogue *Rogue) setupAssassinationRotation(sim *core.Simulation) { } } } else { - energyGained := rogue.getExpectedEnergyPerSecond() * timeLeft.Seconds() - cpGenerated := energyGained / rogue.Builder.DefaultCast.Cost + energyGained := energyPerSecond() * timeLeft.Seconds() + cpGenerated := energyGained / mutiCost currentCp := float64(rogue.ComboPoints()) if currentCp+cpGenerated > 5 { return Skip } else { if currentCp < 5 { - if rogue.CurrentEnergy() >= rogue.Builder.DefaultCast.Cost { + if rogue.CurrentEnergy() >= mutiCost { return Build } } @@ -166,18 +159,18 @@ func (rogue *Rogue) setupAssassinationRotation(sim *core.Simulation) { // Rupture for Bleed if rogue.Rotation.RuptureForBleed { - rogue.assassinationPrios = append(rogue.assassinationPrios, assassinationPrio{ + x.prios = append(x.prios, prio{ func(sim *core.Simulation, rogue *Rogue) PriorityAction { - if rogue.targetHasBleed(sim) { + if x.targetHasBleed(sim, rogue) { return Skip } if rogue.HungerForBloodAura.IsActive() { return Skip } - if rogue.ComboPoints() > 0 && rogue.CurrentEnergy() >= rogue.Rupture.DefaultCast.Cost { + if rogue.ComboPoints() > 0 && rogue.CurrentEnergy() >= rupCost { return Cast } - if rogue.ComboPoints() < 1 && rogue.CurrentEnergy() >= rogue.Builder.DefaultCast.Cost { + if rogue.ComboPoints() < 1 && rogue.CurrentEnergy() >= mutiCost { return Build } return Wait @@ -185,23 +178,23 @@ func (rogue *Rogue) setupAssassinationRotation(sim *core.Simulation) { func(sim *core.Simulation, rogue *Rogue) bool { return rogue.Rupture.Cast(sim, rogue.CurrentTarget) }, - rogue.Rupture.DefaultCast.Cost, + rupCost, }) } // Hunger for Blood if rogue.Talents.HungerForBlood { - rogue.assassinationPrios = append(rogue.assassinationPrios, assassinationPrio{ + x.prios = append(x.prios, prio{ func(sim *core.Simulation, rogue *Rogue) PriorityAction { if rogue.HungerForBloodAura.IsActive() { return Skip } - if !rogue.targetHasBleed(sim) { + if !x.targetHasBleed(sim, rogue) { return Skip } - if rogue.targetHasBleed(sim) && rogue.CurrentEnergy() >= rogue.HungerForBlood.DefaultCast.Cost { + if x.targetHasBleed(sim, rogue) && rogue.CurrentEnergy() >= rogue.HungerForBlood.DefaultCast.Cost { return Cast } return Wait @@ -214,39 +207,23 @@ func (rogue *Rogue) setupAssassinationRotation(sim *core.Simulation) { } // Enable CDs - rogue.assassinationPrios = append(rogue.assassinationPrios, assassinationPrio{ + x.prios = append(x.prios, prio{ func(sim *core.Simulation, rogue *Rogue) PriorityAction { - if rogue.allMCDsDisabled { - for _, mcd := range rogue.GetMajorCooldowns() { - if mcd.Spell != rogue.ColdBlood { - mcd.Enable() - } + for _, mcd := range rogue.GetMajorCooldowns() { + if mcd.Spell != rogue.ColdBlood { + mcd.Enable() } - rogue.allMCDsDisabled = false } - return Skip + return Once }, func(s *core.Simulation, r *Rogue) bool { - return false + return true }, 0, }) - // estimate of energy per second while nothing is cast - energyPerSecond := func() float64 { - if rogue.Talents.FocusedAttacks == 0 { - return 10 * rogue.EnergyTickMultiplier - } - - procChance := []float64{0, 0.33, 0.66, 1}[rogue.Talents.FocusedAttacks] - critSuppression := rogue.AttackTables[rogue.CurrentTarget.UnitIndex].CritSuppression - effectiveCrit := rogue.GetStat(stats.MeleeCrit)/(core.CritRatingPerCritChance*100) - critSuppression - critsPerSecond := effectiveCrit * procChance * (1/rogue.AutoAttacks.MainhandSwingSpeed().Seconds() + 1/rogue.AutoAttacks.OffhandSwingSpeed().Seconds()) - return 10*rogue.EnergyTickMultiplier + critsPerSecond*2 - } - // Rupture - rogue.assassinationPrios = append(rogue.assassinationPrios, assassinationPrio{ + x.prios = append(x.prios, prio{ func(sim *core.Simulation, rogue *Rogue) PriorityAction { cp, e := rogue.ComboPoints(), rogue.CurrentEnergy() @@ -260,7 +237,7 @@ func (rogue *Rogue) setupAssassinationRotation(sim *core.Simulation) { // 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 := rogue.Rupture.DefaultCast.Cost + rogue.Builder.DefaultCast.Cost + rogue.Envenom.DefaultCast.Cost + cost := rupCost + mutiCost + envCost if avail >= cost { return Cast } @@ -270,10 +247,10 @@ func (rogue *Rogue) setupAssassinationRotation(sim *core.Simulation) { if rogue.Rupture.CurDot().IsActive() || sim.GetRemainingDuration() < time.Second*18 { return Skip } - if cp >= 4 && e >= rogue.Rupture.DefaultCast.Cost { + if cp >= 4 && e >= rupCost { return Cast } - if cp < 4 && e >= rogue.Builder.DefaultCast.Cost { + if cp < 4 && e >= mutiCost { return Build } return Wait @@ -282,25 +259,23 @@ func (rogue *Rogue) setupAssassinationRotation(sim *core.Simulation) { func(sim *core.Simulation, rogue *Rogue) bool { return rogue.Rupture.Cast(sim, rogue.CurrentTarget) }, - rogue.Rupture.DefaultCast.Cost, + rupCost, }) // Envenom - rogue.assassinationPrios = append(rogue.assassinationPrios, assassinationPrio{ + x.prios = append(x.prios, prio{ func(sim *core.Simulation, rogue *Rogue) PriorityAction { e, cp := rogue.CurrentEnergy(), rogue.ComboPoints() - costEnv, costMut := rogue.Envenom.DefaultCast.Cost, rogue.Builder.DefaultCast.Cost - // 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 < costMut+costEnv && e >= costEnv { + if cp == 3 && avail < mutiCost+envCost && e >= envCost { return Cast } - if cp >= 1 && avail < costMut && e >= costEnv { + if cp >= 1 && avail < mutiCost && e >= envCost { return Cast } } @@ -317,7 +292,7 @@ func (rogue *Rogue) setupAssassinationRotation(sim *core.Simulation) { } // pool, so two Mutilate casts fit into the next uptime; this is a very minor DPS gain, and primarily for lower gear levels - cost := costEnv + costMut + costMut + cost := envCost + mutiCost + mutiCost if cp == 5 && rogue.Talents.RelentlessStrikes == 5 { cost -= 25 } @@ -328,7 +303,7 @@ func (rogue *Rogue) setupAssassinationRotation(sim *core.Simulation) { return Cast } - if e >= rogue.Builder.DefaultCast.Cost { + if e >= mutiCost { return Build } return Wait @@ -339,50 +314,44 @@ func (rogue *Rogue) setupAssassinationRotation(sim *core.Simulation) { } return rogue.Envenom.Cast(sim, rogue.CurrentTarget) }, - rogue.Envenom.DefaultCast.Cost, + envCost, }) } -func (rogue *Rogue) doAssassinationRotation(sim *core.Simulation) { - prioIndex := 0 - for prioIndex < len(rogue.assassinationPrios) { - prio := rogue.assassinationPrios[prioIndex] - switch prio.check(sim, rogue) { +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: - prioIndex += 1 + continue case Build: - if rogue.GCD.IsReady(sim) { - if !rogue.Builder.Cast(sim, rogue.CurrentTarget) { - rogue.WaitForEnergy(sim, rogue.Builder.DefaultCast.Cost) - return - } + if !rogue.Mutilate.Cast(sim, rogue.CurrentTarget) { + rogue.WaitForEnergy(sim, rogue.Mutilate.DefaultCast.Cost) + return } - rogue.DoNothing() - return case Cast: - if rogue.GCD.IsReady(sim) { - if !prio.cast(sim, rogue) { - rogue.WaitForEnergy(sim, prio.cost) - return - } + if !p.cast(sim, rogue) { + rogue.WaitForEnergy(sim, p.cost) + return } - rogue.DoNothing() - 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 + } } - rogue.DoNothing() + log.Panic("skipped all prios") } -func (rogue *Rogue) OnCanAct(sim *core.Simulation) { - if rogue.KillingSpreeAura.IsActive() { - rogue.DoNothing() - return - } - rogue.TryUseCooldowns(sim) - if rogue.GCD.IsReady(sim) { - rogue.doAssassinationRotation(sim) - } +func (x *rotation_assassination) targetHasBleed(_ *core.Simulation, rogue *Rogue) bool { + return rogue.bleedCategory.AnyActive() || rogue.CurrentTarget.HasActiveAuraWithTag(RogueBleedTag) } diff --git a/sim/rogue/rotation_combat.go b/sim/rogue/rotation_combat.go new file mode 100644 index 0000000000..00f777af11 --- /dev/null +++ b/sim/rogue/rotation_combat.go @@ -0,0 +1,339 @@ +package rogue + +import ( + "golang.org/x/exp/slices" + "log" + "time" + + "github.com/wowsims/wotlk/sim/core" + "github.com/wowsims/wotlk/sim/core/proto" +) + +type rotation_combat struct { + prios []prio +} + +func (x *rotation_combat) setup(_ *core.Simulation, rogue *Rogue) { + x.prios = x.prios[:0] + + ssCost := rogue.SinisterStrike.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) + + // estimate of energy per second while nothing is cast + energyPerSecond := func() float64 { + if rogue.Talents.CombatPotency == 0 { + return 10 * rogue.EnergyTickMultiplier + } + + attackTable := rogue.AttackTables[rogue.CurrentTarget.UnitIndex] + spell := rogue.AutoAttacks.OHAuto + + landChance := 1.0 + if miss := attackTable.BaseMissChance + 0.19 - spell.PhysicalHitChance(rogue.CurrentTarget); miss > 0 { + landChance -= miss + } + if dodge := attackTable.BaseDodgeChance - spell.ExpertisePercentage() - spell.Unit.PseudoStats.DodgeReduction; dodge > 0 { + landChance -= dodge + } + landsPerSecond := landChance * (1 / rogue.AutoAttacks.OffhandSwingSpeed().Seconds()) + return 10*rogue.EnergyTickMultiplier + landsPerSecond*0.2*float64(rogue.Talents.CombatPotency)*3 + } + + // 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 rogue.SliceAndDiceAura.IsActive() { + if cp == 5 { // pool for snd if pooling for rupture fails + rupDur := rogue.Rupture.CurDot().RemainingDuration(sim) + if e+rupDur.Seconds()*energyPerSecond() > maxPool { + sndDur := rogue.SliceAndDiceAura.RemainingDuration(sim) + if e+sndDur.Seconds()*energyPerSecond() <= maxPool { + return Wait + } + } + return Skip + } + + if cp >= 1 { // don't build if it reduces uptime + sndDur := rogue.SliceAndDiceAura.RemainingDuration(sim) + if e+sndDur.Seconds()*energyPerSecond() < sndCost+ssCost || 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 >= ssCost { + 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 := core.MaxInt32(1, core.MinInt32(rogue.Rotation.MinimumComboPointsExposeArmor, 5)) + if rogue.Rotation.ExposeArmorFrequency != proto.Rogue_Rotation_Once { + minPoints = 1 + } + if timeLeft <= 0 { + if rogue.ComboPoints() < minPoints { + if rogue.CurrentEnergy() >= ssCost { + 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 / ssCost + currentCp := float64(rogue.ComboPoints()) + if currentCp+cpGenerated > 5 { + return Skip + } else { + if currentCp < 5 { + if rogue.CurrentEnergy() >= ssCost { + 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() * (1 - rogue.RuptureDamage(4)/rogue.RuptureDamage(5)) + rup3to4 := rogue.RuptureDuration(3).Seconds() * (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() < ssCost+rupCost { + return Cast + } + if cp == 3 && e+rup3to4*energyPerSecond() < ssCost+rupCost { + return Cast + } + if e >= ssCost { + return Build + } + return Wait + } + + // there's ample time to rebuild, simply skip + dur := rupDot.RemainingDuration(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() < ssCost+rupCost { + return Wait + } + if cp == 3 && e+(dur+rup3to4)*energyPerSecond() < ssCost+rupCost { + return Wait + } + if e >= ssCost { + return Build + } + return Wait + }, + func(sim *core.Simulation, rogue *Rogue) bool { + return rogue.Rupture.Cast(sim, rogue.CurrentTarget) + }, + rupCost, + }) + + ssPerCp := 1.0 + if rogue.HasMajorGlyph(proto.RogueMajorGlyph_GlyphOfSinisterStrike) { + attackTable := rogue.AttackTables[rogue.CurrentTarget.UnitIndex] + crit := rogue.SinisterStrike.PhysicalCritChance(rogue.CurrentTarget, attackTable) + ssPerCp = 1 / (1 + crit*0.5) + } + + // 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() >= ssCost+evisCost { + return Build + } + if cp >= 3 && e >= evisCost { + return Cast + } + if cp < 3 && e >= ssCost { + 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 + } + + rupDot := rogue.Rupture.CurDot() + + ruthCP := 0.2 * float64(rogue.Talents.Ruthlessness) + cost := evisCost + (4-ruthCP)*ssCost*ssPerCp + rupCost + + rupDur := rupDot.RemainingDuration(sim) + sndDur := rogue.SliceAndDiceAura.RemainingDuration(sim) + if sndDur < rupDur { + cost += sndCost + (1-ruthCP)*ssCost*ssPerCp + } + + if avail := e + rupDur.Seconds()*energyPerSecond(); avail >= cost { + return Cast + } + 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) { + if rogue.KillingSpreeAura.IsActive() { + rogue.DoNothing() + return + } + + for i := 0; i < len(x.prios); i++ { + switch p := x.prios[i]; p.check(sim, rogue) { + case Skip: + continue + case Build: + if !rogue.SinisterStrike.Cast(sim, rogue.CurrentTarget) { + rogue.WaitForEnergy(sim, rogue.SinisterStrike.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 new file mode 100644 index 0000000000..2da18c5df0 --- /dev/null +++ b/sim/rogue/rotation_generic.go @@ -0,0 +1,517 @@ +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_generic struct { + priorityItems []roguePriorityItem + rotationItems []rogueRotationItem + + builder *core.Spell + builderPoints int32 +} + +func (x *rotation_generic) 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 + } + + isMultiTarget := sim.GetNumTargets() >= 3 + + // 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 isMultiTarget { + if rogue.Rotation.MultiTargetSliceFrequency != proto.Rogue_Rotation_Never { + sliceAndDice.MinimumComboPoints = core.MaxInt32(1, rogue.Rotation.MinimumComboPointsMultiTargetSlice) + if rogue.Rotation.MultiTargetSliceFrequency == proto.Rogue_Rotation_Once { + sliceAndDice.MaxCasts = 1 + } + x.priorityItems = append(x.priorityItems, sliceAndDice) + } + } else { + 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 + }, + }) + + // Rupture + rupture := roguePriorityItem{ + MinimumComboPoints: 3, + MaximumComboPoints: 5, + Aura: rogue.Rupture.CurDot().Aura, + EnergyCost: rogue.Rupture.DefaultCast.Cost, + GetDuration: func(rogue *Rogue, cp int32) time.Duration { + return rogue.RuptureDuration(cp) + }, + GetSpell: func(rogue *Rogue, cp int32) *core.Spell { + return rogue.Rupture + }, + } + + // Eviscerate + eviscerate := roguePriorityItem{ + MinimumComboPoints: 1, + MaximumComboPoints: 5, + EnergyCost: rogue.Eviscerate.DefaultCast.Cost, + GetDuration: func(rogue *Rogue, cp int32) time.Duration { + return 0 + }, + GetSpell: func(rogue *Rogue, cp int32) *core.Spell { + return rogue.Eviscerate + }, + } + + if isMultiTarget { + x.priorityItems = append(x.priorityItems, roguePriorityItem{ + MaximumComboPoints: 0, + EnergyCost: rogue.FanOfKnives.DefaultCast.Cost, + GetSpell: func(rogue *Rogue, i int32) *core.Spell { + return rogue.FanOfKnives + }, + }) + + } else if rogue.Talents.MasterPoisoner > 0 || rogue.Talents.CutToTheChase > 0 { + // Envenom + envenom := roguePriorityItem{ + MinimumComboPoints: 1, + MaximumComboPoints: 5, + Aura: rogue.EnvenomAura, + EnergyCost: rogue.Envenom.DefaultCast.Cost, + GetDuration: func(rogue *Rogue, cp int32) time.Duration { + return rogue.EnvenomAura.Duration + }, + GetSpell: func(rogue *Rogue, cp int32) *core.Spell { + return rogue.Envenom + }, + } + switch rogue.Rotation.AssassinationFinisherPriority { + case proto.Rogue_Rotation_EnvenomRupture: + envenom.MinimumComboPoints = core.MaxInt32(1, rogue.Rotation.MinimumComboPointsPrimaryFinisher) + x.priorityItems = append(x.priorityItems, envenom) + rupture.MinimumComboPoints = rogue.Rotation.MinimumComboPointsSecondaryFinisher + rupture.IsFiller = true + if rupture.MinimumComboPoints > 0 && rupture.MinimumComboPoints <= 5 { + x.priorityItems = append(x.priorityItems, rupture) + } + case proto.Rogue_Rotation_RuptureEnvenom: + rupture.MinimumComboPoints = core.MaxInt32(1, rogue.Rotation.MinimumComboPointsPrimaryFinisher) + x.priorityItems = append(x.priorityItems, rupture) + envenom.MinimumComboPoints = rogue.Rotation.MinimumComboPointsSecondaryFinisher + envenom.IsFiller = true + if envenom.MinimumComboPoints > 0 && envenom.MinimumComboPoints <= 5 { + x.priorityItems = append(x.priorityItems, envenom) + } + } + eviscerate.IsFiller = true + eviscerate.MinimumComboPoints = 1 + x.priorityItems = append(x.priorityItems, eviscerate) + } else { + if rogue.Talents.Vitality > 0 { + switch rogue.Rotation.CombatFinisherPriority { + case proto.Rogue_Rotation_RuptureEviscerate: + rupture.MinimumComboPoints = core.MaxInt32(1, rogue.Rotation.MinimumComboPointsPrimaryFinisher) + x.priorityItems = append(x.priorityItems, rupture) + eviscerate.MinimumComboPoints = core.MaxInt32(1, rogue.Rotation.MinimumComboPointsSecondaryFinisher) + eviscerate.IsFiller = true + x.priorityItems = append(x.priorityItems, eviscerate) + case proto.Rogue_Rotation_EviscerateRupture: + eviscerate.MinimumComboPoints = core.MaxInt32(1, rogue.Rotation.MinimumComboPointsPrimaryFinisher) + x.priorityItems = append(x.priorityItems, eviscerate) + rupture.MinimumComboPoints = rogue.Rotation.MinimumComboPointsSecondaryFinisher + rupture.IsFiller = true + if rupture.MinimumComboPoints > 0 && rupture.MinimumComboPoints <= 5 { + x.priorityItems = append(x.priorityItems, rupture) + } + } + } else { + switch rogue.Rotation.SubtletyFinisherPriority { + case proto.Rogue_Rotation_SubtletyEviscerate: + rupture.MinimumComboPoints = core.MaxInt32(1, rogue.Rotation.MinimumComboPointsPrimaryFinisher) + x.priorityItems = append(x.priorityItems, rupture) + eviscerate.MinimumComboPoints = core.MaxInt32(1, rogue.Rotation.MinimumComboPointsSecondaryFinisher) + eviscerate.IsFiller = true + x.priorityItems = append(x.priorityItems, eviscerate) + case proto.Rogue_Rotation_SubtletyEnvenom: + eviscerate.MinimumComboPoints = core.MaxInt32(1, rogue.Rotation.MinimumComboPointsPrimaryFinisher) + x.priorityItems = append(x.priorityItems, eviscerate) + rupture.MinimumComboPoints = rogue.Rotation.MinimumComboPointsSecondaryFinisher + rupture.IsFiller = true + if rupture.MinimumComboPoints > 0 && rupture.MinimumComboPoints <= 5 { + x.priorityItems = append(x.priorityItems, rupture) + } + } + } + } + x.rotationItems = x.planRotation(sim, rogue) +} + +func (x *rotation_generic) run(sim *core.Simulation, rogue *Rogue) { + if rogue.KillingSpreeAura.IsActive() { + rogue.DoNothing() + return + } + + 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_generic) energyToBuild(points int32) float64 { + costPerBuilder := x.builder.DefaultCast.Cost + + buildersNeeded := math.Ceil(float64(points) / float64(x.builderPoints)) + return buildersNeeded * costPerBuilder +} + +func (x *rotation_generic) 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 core.MaxDuration(time.Second*time.Duration(secondsNeeded), time.Second*time.Duration(globalsNeeded)) +} + +func (x *rotation_generic) 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_generic) 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_generic) 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 := core.MaxInt32(0, prio.MinimumComboPoints-comboPoints) + energyUsed := core.MaxFloat(0, prio.EnergyCost-currentEnergy) + minBuildTime := x.timeToBuild(cpUsed, x.builderPoints, eps, energyUsed) + if currentTime+minBuildTime <= item.ExpiresAt || !prio.IsFiller { + prioStack = append(prioStack, item) + currentTime = core.MaxDuration(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/subtlety_rotation.go b/sim/rogue/rotation_subtlety.go similarity index 59% rename from sim/rogue/subtlety_rotation.go rename to sim/rogue/rotation_subtlety.go index fc48db78c8..ba08e232ca 100644 --- a/sim/rogue/subtlety_rotation.go +++ b/sim/rogue/rotation_subtlety.go @@ -1,131 +1,86 @@ package rogue import ( + "golang.org/x/exp/slices" + "log" "time" "github.com/wowsims/wotlk/sim/core" "github.com/wowsims/wotlk/sim/core/proto" ) -type subtletyPrio struct { - check GetAction - cast DoAction - cost float64 +type rotation_subtlety struct { + prios []prio + + builder *core.Spell } -func (rogue *Rogue) setSubtletyBuilder(sim *core.Simulation) { - mhDagger := rogue.Equip[proto.ItemSlot_ItemSlotMainHand].WeaponType == proto.WeaponType_WeaponTypeDagger - // Garrote - if !rogue.Garrote.CurDot().Aura.IsActive() && rogue.ShadowDanceAura.IsActive() && !rogue.PseudoStats.InFrontOfTarget { - rogue.Builder = rogue.Garrote - rogue.BuilderPoints = 1 - return - } - // Ambush - if rogue.ShadowDanceAura.IsActive() && mhDagger && !rogue.PseudoStats.InFrontOfTarget { - rogue.Builder = rogue.Ambush - rogue.BuilderPoints = 2 - return - } - // Backstab - if mhDagger && !rogue.Rotation.HemoWithDagger && !rogue.PseudoStats.InFrontOfTarget { - rogue.Builder = rogue.Backstab - rogue.BuilderPoints = 1 - return - } - // Ghostly Strike - if rogue.Talents.GhostlyStrike && rogue.Rotation.UseGhostlyStrike && rogue.GhostlyStrike.IsReady(sim) { - rogue.Builder = rogue.GhostlyStrike - rogue.BuilderPoints = 1 - return +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) } - // Hemorrhage - if rogue.Talents.Hemorrhage { - rogue.Builder = rogue.Hemorrhage - rogue.BuilderPoints = 1 - } else - // Sinister Strike - { - rogue.Builder = rogue.SinisterStrike - rogue.BuilderPoints = 1 + + comboPointsPerSecond := func() float64 { + return 1 / secondsPerComboPoint() } -} -func (rogue *Rogue) setupSubtletyRotation(sim *core.Simulation) { - rogue.subtletyPrios = rogue.subtletyPrios[:0] + energyPerSecond := func() float64 { + return 10 * rogue.EnergyTickMultiplier + } if rogue.Rotation.OpenWithPremeditation && rogue.Talents.Premeditation { - hasCastPremeditation := false - rogue.subtletyPrios = append(rogue.subtletyPrios, subtletyPrio{ + x.prios = append(x.prios, prio{ func(s *core.Simulation, r *Rogue) PriorityAction { - if hasCastPremeditation { - return Skip - } if rogue.Premeditation.IsReady(s) { - return Cast + return Once } return Wait }, func(s *core.Simulation, r *Rogue) bool { - casted := r.Premeditation.Cast(s, r.CurrentTarget) - if casted { - hasCastPremeditation = true - } - return casted + return r.Premeditation.Cast(s, r.CurrentTarget) }, rogue.Premeditation.DefaultCast.Cost, }) } if rogue.Rotation.OpenWithShadowstep && rogue.Talents.Shadowstep { - hasCastShadowstep := false - rogue.subtletyPrios = append(rogue.subtletyPrios, subtletyPrio{ + x.prios = append(x.prios, prio{ func(s *core.Simulation, r *Rogue) PriorityAction { - if hasCastShadowstep { - return Skip - } if rogue.CurrentEnergy() > rogue.Shadowstep.DefaultCast.Cost { - return Cast + return Once } return Wait }, func(s *core.Simulation, r *Rogue) bool { - casted := rogue.Shadowstep.Cast(sim, rogue.CurrentTarget) - if casted { - hasCastShadowstep = true - } - return casted + return rogue.Shadowstep.Cast(sim, rogue.CurrentTarget) }, rogue.Shadowstep.DefaultCast.Cost, }) } // Garrote - if rogue.Rotation.OpenWithGarrote { - hasCastGarrote := false - rogue.subtletyPrios = append(rogue.subtletyPrios, subtletyPrio{ + if rogue.Rotation.OpenWithGarrote && !rogue.PseudoStats.InFrontOfTarget { + x.prios = append(x.prios, prio{ func(sim *core.Simulation, rogue *Rogue) PriorityAction { - if hasCastGarrote { - return Skip - } if rogue.CurrentEnergy() > rogue.Garrote.DefaultCast.Cost { - return Cast + return Once } return Wait }, func(sim *core.Simulation, rogue *Rogue) bool { - casted := rogue.Garrote.Cast(sim, rogue.CurrentTarget) - if casted { - hasCastGarrote = true - } - return casted + return rogue.Garrote.Cast(sim, rogue.CurrentTarget) }, rogue.Garrote.DefaultCast.Cost, }) } // Slice and Dice - rogue.subtletyPrios = append(rogue.subtletyPrios, subtletyPrio{ + x.prios = append(x.prios, prio{ func(sim *core.Simulation, rogue *Rogue) PriorityAction { if rogue.SliceAndDiceAura.IsActive() { return Skip @@ -137,10 +92,10 @@ func (rogue *Rogue) setupSubtletyRotation(sim *core.Simulation) { if rogue.ComboPoints() >= 1 && rogue.CurrentEnergy() > rogue.SliceAndDice.DefaultCast.Cost { return Cast } - if rogue.ComboPoints() < 1 && rogue.CurrentEnergy() > rogue.Builder.DefaultCast.Cost && rogue.getExpectedComboPointsPerSecond() >= 0.7 { + if rogue.ComboPoints() < 1 && rogue.CurrentEnergy() > x.builder.DefaultCast.Cost && comboPointsPerSecond() >= 0.7 { return Wait } - if rogue.ComboPoints() < 1 && rogue.CurrentEnergy() > rogue.Builder.DefaultCast.Cost { + if rogue.ComboPoints() < 1 && rogue.CurrentEnergy() > x.builder.DefaultCast.Cost { return Build } return Wait @@ -155,7 +110,7 @@ func (rogue *Rogue) setupSubtletyRotation(sim *core.Simulation) { if rogue.Rotation.ExposeArmorFrequency == proto.Rogue_Rotation_Once || rogue.Rotation.ExposeArmorFrequency == proto.Rogue_Rotation_Maintain { hasCastExpose := false - rogue.subtletyPrios = append(rogue.subtletyPrios, subtletyPrio{ + x.prios = append(x.prios, prio{ func(sim *core.Simulation, rogue *Rogue) PriorityAction { if hasCastExpose && rogue.Rotation.ExposeArmorFrequency == proto.Rogue_Rotation_Once { return Skip @@ -167,7 +122,7 @@ func (rogue *Rogue) setupSubtletyRotation(sim *core.Simulation) { } if timeLeft <= 0 { if rogue.ComboPoints() < minPoints { - if rogue.CurrentEnergy() >= rogue.Builder.DefaultCast.Cost && rogue.getExpectedComboPointsPerSecond() < 1 { + if rogue.CurrentEnergy() >= x.builder.DefaultCast.Cost && comboPointsPerSecond() < 1 { return Build } else { return Wait @@ -180,15 +135,15 @@ func (rogue *Rogue) setupSubtletyRotation(sim *core.Simulation) { } } } else { - energyGained := rogue.getExpectedEnergyPerSecond() * timeLeft.Seconds() - comboGained := rogue.getExpectedComboPointsPerSecond() * timeLeft.Seconds() - cpGenerated := energyGained/rogue.Builder.DefaultCast.Cost + comboGained + 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() >= rogue.Builder.DefaultCast.Cost { + if rogue.CurrentEnergy() >= x.builder.DefaultCast.Cost { return Build } } @@ -208,25 +163,22 @@ func (rogue *Rogue) setupSubtletyRotation(sim *core.Simulation) { } // Enable CDS - rogue.subtletyPrios = append(rogue.subtletyPrios, subtletyPrio{ + x.prios = append(x.prios, prio{ func(sim *core.Simulation, rogue *Rogue) PriorityAction { - if rogue.allMCDsDisabled { - for _, mcd := range rogue.GetMajorCooldowns() { - mcd.Enable() - } - rogue.allMCDsDisabled = false + for _, mcd := range rogue.GetMajorCooldowns() { + mcd.Enable() } - return Skip + return Once }, func(_ *core.Simulation, _ *Rogue) bool { - return false + return true }, 0, }) //Shadowstep if rogue.Talents.Shadowstep { - rogue.subtletyPrios = append(rogue.subtletyPrios, subtletyPrio{ + x.prios = append(x.prios, prio{ func(sim *core.Simulation, rogue *Rogue) PriorityAction { if rogue.Shadowstep.IsReady(sim) { // Can we cast Rupture now? @@ -248,7 +200,7 @@ func (rogue *Rogue) setupSubtletyRotation(sim *core.Simulation) { const ruptureMinDuration = time.Second * 8 // heuristically, 3-4 Rupture ticks are better DPE than Eviscerate or Envenom // Rupture - rogue.subtletyPrios = append(rogue.subtletyPrios, subtletyPrio{ + x.prios = append(x.prios, prio{ func(sim *core.Simulation, rogue *Rogue) PriorityAction { if rogue.Rupture.CurDot().IsActive() || sim.GetRemainingDuration() < ruptureMinDuration { return Skip @@ -257,7 +209,7 @@ func (rogue *Rogue) setupSubtletyRotation(sim *core.Simulation) { return Cast } // don't explicitly wait here, to shorten downtime - if rogue.ComboPoints() < 5 && rogue.CurrentEnergy() >= rogue.Builder.DefaultCast.Cost+rogue.Rupture.DefaultCast.Cost { + if rogue.ComboPoints() < 5 && rogue.CurrentEnergy() >= x.builder.DefaultCast.Cost+rogue.Rupture.DefaultCast.Cost { return Build } return Wait @@ -270,7 +222,7 @@ func (rogue *Rogue) setupSubtletyRotation(sim *core.Simulation) { //Envenom if rogue.Rotation.SubtletyFinisherPriority == proto.Rogue_Rotation_SubtletyEnvenom { - rogue.subtletyPrios = append(rogue.subtletyPrios, subtletyPrio{ + x.prios = append(x.prios, prio{ func(sim *core.Simulation, rogue *Rogue) PriorityAction { if !rogue.DeadlyPoison.CurDot().Aura.IsActive() { return Skip @@ -281,7 +233,7 @@ func (rogue *Rogue) setupSubtletyRotation(sim *core.Simulation) { if rogue.ComboPoints() >= 5 && rogue.CurrentEnergy() >= rogue.Envenom.DefaultCast.Cost { return Cast } - if rogue.ComboPoints() < 5 && rogue.CurrentEnergy() >= rogue.Builder.DefaultCast.Cost+rogue.Envenom.DefaultCast.Cost { + if rogue.ComboPoints() < 5 && rogue.CurrentEnergy() >= x.builder.DefaultCast.Cost+rogue.Envenom.DefaultCast.Cost { return Build } return Wait @@ -294,16 +246,16 @@ func (rogue *Rogue) setupSubtletyRotation(sim *core.Simulation) { } // Eviscerate - rogue.subtletyPrios = append(rogue.subtletyPrios, subtletyPrio{ + 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() < rogue.getExpectedSecondsPerComboPoint() && rogue.ComboPoints() >= 2 && rogue.CurrentEnergy() >= rogue.Eviscerate.DefaultCast.Cost { + 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() >= rogue.Builder.DefaultCast.Cost+rogue.Eviscerate.DefaultCast.Cost { + if rogue.ComboPoints() < 5 && rogue.CurrentEnergy() >= x.builder.DefaultCast.Cost+rogue.Eviscerate.DefaultCast.Cost { return Build } return Wait @@ -315,13 +267,11 @@ func (rogue *Rogue) setupSubtletyRotation(sim *core.Simulation) { }) } -func (rogue *Rogue) doSubtletyRotation(sim *core.Simulation) { - prioIndex := 0 - for prioIndex < len(rogue.subtletyPrios) { - prio := rogue.subtletyPrios[prioIndex] - switch prio.check(sim, rogue) { +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: - prioIndex += 1 + 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, @@ -330,34 +280,62 @@ func (rogue *Rogue) doSubtletyRotation(sim *core.Simulation) { return } - if rogue.GCD.IsReady(sim) { - rogue.setSubtletyBuilder(sim) - if !rogue.Builder.Cast(sim, rogue.CurrentTarget) { - rogue.WaitForEnergy(sim, rogue.Builder.DefaultCast.Cost) - return - } + x.setSubtletyBuilder(sim, rogue) + if !x.builder.Cast(sim, rogue.CurrentTarget) { + rogue.WaitForEnergy(sim, x.builder.DefaultCast.Cost) + return } - rogue.DoNothing() - return case Cast: - if rogue.GCD.IsReady(sim) { - if !prio.cast(sim, rogue) { - rogue.WaitForEnergy(sim, prio.cost) - return - } + if !p.cast(sim, rogue) { + rogue.WaitForEnergy(sim, p.cost) + return } - rogue.DoNothing() - 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 + } } - rogue.DoNothing() + log.Panic("skipped all prios") } -func (rogue *Rogue) OnCanActSubtlety(sim *core.Simulation) { - if rogue.GCD.IsReady(sim) { - rogue.doSubtletyRotation(sim) +func (x *rotation_subtlety) setSubtletyBuilder(sim *core.Simulation, rogue *Rogue) { + // Garrote + if !rogue.Garrote.CurDot().Aura.IsActive() && rogue.ShadowDanceAura.IsActive() && !rogue.PseudoStats.InFrontOfTarget { + x.builder = rogue.Garrote + return + } + // Ambush + if rogue.ShadowDanceAura.IsActive() && !rogue.PseudoStats.InFrontOfTarget && rogue.HasDagger(core.MainHand) { + x.builder = rogue.Ambush + return + } + // Backstab + if !rogue.Rotation.HemoWithDagger && !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/rupture.go b/sim/rogue/rupture.go index bcc1bdca25..b8dbecdd5c 100644 --- a/sim/rogue/rupture.go +++ b/sim/rogue/rupture.go @@ -12,6 +12,7 @@ const RuptureSpellID = 48672 func (rogue *Rogue) registerRupture() { glyphTicks := core.TernaryInt32(rogue.HasMajorGlyph(proto.RogueMajorGlyph_GlyphOfRupture), 2, 0) + rogue.Rupture = rogue.RegisterSpell(core.SpellConfig{ ActionID: core.ActionID{SpellID: RuptureSpellID}, SpellSchool: core.SpellSchoolPhysical, @@ -55,11 +56,7 @@ func (rogue *Rogue) registerRupture() { TickLength: time.Second * 2, OnSnapshot: func(sim *core.Simulation, target *core.Unit, dot *core.Dot, _ bool) { - comboPoints := rogue.ComboPoints() - dot.SnapshotBaseDamage = 127 + - 18*float64(comboPoints) + - []float64{0, 0.06 / 4, 0.12 / 5, 0.18 / 6, 0.24 / 7, 0.30 / 8}[comboPoints]*dot.Spell.MeleeAttackPower() - + dot.SnapshotBaseDamage = rogue.RuptureDamage(rogue.ComboPoints()) attackTable := dot.Spell.Unit.AttackTables[target.UnitIndex] dot.SnapshotCritChance = dot.Spell.PhysicalCritChance(target, attackTable) dot.SnapshotAttackerMultiplier = dot.Spell.AttackerDamageMultiplier(attackTable) @@ -72,11 +69,11 @@ func (rogue *Rogue) registerRupture() { ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { result := spell.CalcOutcome(sim, target, spell.OutcomeMeleeSpecialHit) if result.Landed() { - comboPoints := rogue.ComboPoints() + numberOfTicks := 3 + rogue.ComboPoints() + glyphTicks dot := spell.Dot(target) dot.Spell = spell - dot.NumberOfTicks = 3 + comboPoints + glyphTicks - dot.RecomputeAuraDuration() + dot.NumberOfTicks = numberOfTicks + dot.MaxStacks = numberOfTicks // slightly hacky; used to determine max extra ticks from Glyph of Backstab dot.Apply(sim) rogue.ApplyFinisher(sim, spell) } else { @@ -87,8 +84,16 @@ func (rogue *Rogue) registerRupture() { }) } +func (rogue *Rogue) RuptureDamage(comboPoints int32) float64 { + return 127 + + 18*float64(comboPoints) + + []float64{0, 0.06 / 4, 0.12 / 5, 0.18 / 6, 0.24 / 7, 0.30 / 8}[comboPoints]*rogue.Rupture.MeleeAttackPower() +} + +func (rogue *Rogue) RuptureTicks(comboPoints int32) int32 { + return 3 + comboPoints + core.TernaryInt32(rogue.HasMajorGlyph(proto.RogueMajorGlyph_GlyphOfRupture), 2, 0) +} + func (rogue *Rogue) RuptureDuration(comboPoints int32) time.Duration { - return time.Second*6 + - time.Second*2*time.Duration(comboPoints) + - core.TernaryDuration(rogue.HasMajorGlyph(proto.RogueMajorGlyph_GlyphOfRupture), time.Second*4, 0) + return time.Duration(rogue.RuptureTicks(comboPoints)) * time.Second * 2 } diff --git a/sim/rogue/talents.go b/sim/rogue/talents.go index e082524b9d..4b8fde39b7 100644 --- a/sim/rogue/talents.go +++ b/sim/rogue/talents.go @@ -71,6 +71,10 @@ func getRelentlessStrikesSpellID(talentPoints int32) int32 { func (rogue *Rogue) makeFinishingMoveEffectApplier() func(sim *core.Simulation, numPoints int32) { ruthlessnessMetrics := rogue.NewComboPointMetrics(core.ActionID{SpellID: 14161}) relentlessStrikesMetrics := rogue.NewEnergyMetrics(core.ActionID{SpellID: getRelentlessStrikesSpellID(rogue.Talents.RelentlessStrikes)}) + var mayhemMetrics *core.ResourceMetrics + if rogue.HasSetBonus(ItemSetShadowblades, 4) { + mayhemMetrics = rogue.NewComboPointMetrics(core.ActionID{SpellID: 70802}) + } return func(sim *core.Simulation, numPoints int32) { if t := rogue.Talents.Ruthlessness; t > 0 { @@ -83,6 +87,11 @@ func (rogue *Rogue) makeFinishingMoveEffectApplier() func(sim *core.Simulation, rogue.AddEnergy(sim, 25, relentlessStrikesMetrics) } } + if mayhemMetrics != nil { + if sim.RandomFloat("Mayhem") < 0.13 { + rogue.AddComboPoints(sim, 3, mayhemMetrics) + } + } } } @@ -534,12 +543,16 @@ func (rogue *Rogue) registerAdrenalineRushCD() { OnGain: func(aura *core.Aura, sim *core.Simulation) { rogue.ResetEnergyTick(sim) rogue.ApplyEnergyTickMultiplier(1.0) - rogue.rotationItems = rogue.planRotation(sim) + if r, ok := rogue.rotation.(*rotation_generic); ok { + r.planRotation(sim, rogue) + } }, OnExpire: func(aura *core.Aura, sim *core.Simulation) { rogue.ResetEnergyTick(sim) rogue.ApplyEnergyTickMultiplier(-1.0) - rogue.rotationItems = rogue.planRotation(sim) + if r, ok := rogue.rotation.(*rotation_generic); ok { + r.planRotation(sim, rogue) + } }, })