diff --git a/sim/core/item_swaps.go b/sim/core/item_swaps.go index b81cf12f95..bf41824421 100644 --- a/sim/core/item_swaps.go +++ b/sim/core/item_swaps.go @@ -25,6 +25,7 @@ type ItemSwap struct { // Holds items that are currently not equipped unEquippedItems [3]Item + swapped bool } /* @@ -41,6 +42,7 @@ func (character *Character) EnableItemSwap(itemSwap *proto.ItemSwap, mhCritMulti ohCritMultiplier: ohCritMultiplier, rangedCritMultiplier: rangedCritMultiplier, unEquippedItems: items, + swapped: false, } } @@ -90,6 +92,10 @@ func (swap *ItemSwap) IsEnabled() bool { return swap.character != nil } +func (swap *ItemSwap) IsSwapped() bool { + return swap.swapped +} + func (swap *ItemSwap) GetItem(slot proto.ItemSlot) *Item { if slot-offset < 0 { panic("Not able to swap Item " + slot.String() + " not supported") @@ -148,6 +154,7 @@ func (swap *ItemSwap) SwapItems(sim *Simulation, slots []proto.ItemSlot, useGCD if useGCD { character.SetGCDTimer(sim, 1500*time.Millisecond+sim.CurrentTime) } + swap.swapped = !swap.swapped } func (swap *ItemSwap) swapItem(slot proto.ItemSlot, has2H bool) (bool, stats.Stats) { @@ -231,6 +238,7 @@ func (swap *ItemSwap) reset(sim *Simulation) { } swap.unEquippedItems = swap.initialUnequippedItems + swap.swapped = false for _, onSwap := range swap.onSwapCallbacks { onSwap(sim) diff --git a/sim/warlock/TestAffliction.results b/sim/warlock/TestAffliction.results index f7dc7013b2..bd3a20b9df 100644 --- a/sim/warlock/TestAffliction.results +++ b/sim/warlock/TestAffliction.results @@ -46,357 +46,357 @@ character_stats_results: { dps_results: { key: "TestAffliction-AllItems-AshtongueTalismanofShadows-32493" value: { - dps: 9812.392 - tps: 8820.99728 + dps: 9813.97598 + tps: 8824.21102 } } dps_results: { key: "TestAffliction-AllItems-AustereEarthsiegeDiamond" value: { - dps: 9697.75019 - tps: 8701.80254 + dps: 9702.42883 + tps: 8707.37197 } } dps_results: { key: "TestAffliction-AllItems-BeamingEarthsiegeDiamond" value: { - dps: 9714.07539 - tps: 8718.12774 + dps: 9719.15949 + tps: 8724.10262 } } dps_results: { key: "TestAffliction-AllItems-BracingEarthsiegeDiamond" value: { - dps: 9745.03511 - tps: 8747.05063 + dps: 9750.98253 + tps: 8753.53236 } } dps_results: { key: "TestAffliction-AllItems-ChaoticSkyflareDiamond" value: { - dps: 9892.67473 - tps: 8897.00094 + dps: 9890.52389 + tps: 8895.46703 } } dps_results: { key: "TestAffliction-AllItems-DarkCoven'sRegalia" value: { - dps: 9639.43337 - tps: 8646.07634 + dps: 9618.32226 + tps: 8619.70031 } } dps_results: { key: "TestAffliction-AllItems-DeathbringerGarb" value: { - dps: 8905.91174 - tps: 7948.5684 + dps: 8905.59388 + tps: 7950.16543 } } dps_results: { key: "TestAffliction-AllItems-DestructiveSkyflareDiamond" value: { - dps: 9717.02416 - tps: 8721.0765 + dps: 9722.28531 + tps: 8727.22844 } } dps_results: { key: "TestAffliction-AllItems-EffulgentSkyflareDiamond" value: { - dps: 9697.75019 - tps: 8701.80254 + dps: 9702.42883 + tps: 8707.37197 } } dps_results: { key: "TestAffliction-AllItems-EmberSkyflareDiamond" value: { - dps: 9745.03511 - tps: 8747.05063 + dps: 9750.98253 + tps: 8753.53236 } } dps_results: { key: "TestAffliction-AllItems-EnigmaticSkyflareDiamond" value: { - dps: 9714.07539 - tps: 8718.12774 + dps: 9719.15949 + tps: 8724.10262 } } dps_results: { key: "TestAffliction-AllItems-EnigmaticStarflareDiamond" value: { - dps: 9707.99709 - tps: 8712.04944 + dps: 9713.19237 + tps: 8718.1355 } } dps_results: { key: "TestAffliction-AllItems-EternalEarthsiegeDiamond" value: { - dps: 9697.75019 - tps: 8701.80254 + dps: 9702.42883 + tps: 8707.37197 } } dps_results: { key: "TestAffliction-AllItems-ForlornSkyflareDiamond" value: { - dps: 9745.03511 - tps: 8747.05063 + dps: 9750.98253 + tps: 8753.53236 } } dps_results: { key: "TestAffliction-AllItems-ForlornStarflareDiamond" value: { - dps: 9735.63754 - tps: 8738.383 + dps: 9740.15716 + tps: 8743.57751 } } dps_results: { key: "TestAffliction-AllItems-Gladiator'sFelshroud" value: { - dps: 8883.74836 - tps: 7913.11722 + dps: 8887.23711 + tps: 7917.11061 } } dps_results: { key: "TestAffliction-AllItems-Gul'dan'sRegalia" value: { - dps: 8857.06226 - tps: 7805.03363 + dps: 8870.47722 + tps: 7819.88928 } } dps_results: { key: "TestAffliction-AllItems-ImpassiveSkyflareDiamond" value: { - dps: 9714.07539 - tps: 8718.12774 + dps: 9719.15949 + tps: 8724.10262 } } dps_results: { key: "TestAffliction-AllItems-ImpassiveStarflareDiamond" value: { - dps: 9707.99709 - tps: 8712.04944 + dps: 9713.19237 + tps: 8718.1355 } } dps_results: { key: "TestAffliction-AllItems-InsightfulEarthsiegeDiamond" value: { - dps: 9692.97238 - tps: 8696.17047 + dps: 9706.07967 + tps: 8707.49943 } } dps_results: { key: "TestAffliction-AllItems-InvigoratingEarthsiegeDiamond" value: { - dps: 9697.75019 - tps: 8701.80254 + dps: 9702.42883 + tps: 8707.37197 } } dps_results: { key: "TestAffliction-AllItems-MaleficRaiment" value: { - dps: 6826.91812 - tps: 5974.17604 + dps: 6826.10567 + tps: 5974.16967 } } dps_results: { key: "TestAffliction-AllItems-PersistentEarthshatterDiamond" value: { - dps: 9697.75019 - tps: 8701.80254 + dps: 9702.42883 + tps: 8707.37197 } } dps_results: { key: "TestAffliction-AllItems-PersistentEarthsiegeDiamond" value: { - dps: 9697.75019 - tps: 8701.80254 + dps: 9702.42883 + tps: 8707.37197 } } dps_results: { key: "TestAffliction-AllItems-PlagueheartGarb" value: { - dps: 8498.55099 - tps: 7564.35381 + dps: 8485.46692 + tps: 7550.66456 } } dps_results: { key: "TestAffliction-AllItems-PowerfulEarthshatterDiamond" value: { - dps: 9697.75019 - tps: 8701.80254 + dps: 9702.42883 + tps: 8707.37197 } } dps_results: { key: "TestAffliction-AllItems-PowerfulEarthsiegeDiamond" value: { - dps: 9697.75019 - tps: 8701.80254 + dps: 9702.42883 + tps: 8707.37197 } } dps_results: { key: "TestAffliction-AllItems-RelentlessEarthsiegeDiamond" value: { - dps: 9874.88026 - tps: 8879.20647 + dps: 9872.28748 + tps: 8877.23061 } } dps_results: { key: "TestAffliction-AllItems-RevitalizingSkyflareDiamond" value: { - dps: 9691.26345 - tps: 8691.66144 + dps: 9706.95052 + tps: 8707.53505 } } dps_results: { key: "TestAffliction-AllItems-SwiftSkyflareDiamond" value: { - dps: 9697.75019 - tps: 8701.80254 + dps: 9702.42883 + tps: 8707.37197 } } dps_results: { key: "TestAffliction-AllItems-SwiftStarflareDiamond" value: { - dps: 9697.75019 - tps: 8701.80254 + dps: 9702.42883 + tps: 8707.37197 } } dps_results: { key: "TestAffliction-AllItems-SwiftWindfireDiamond" value: { - dps: 9697.75019 - tps: 8701.80254 + dps: 9702.42883 + tps: 8707.37197 } } dps_results: { key: "TestAffliction-AllItems-ThunderingSkyflareDiamond" value: { - dps: 9697.75019 - tps: 8701.80254 + dps: 9702.42883 + tps: 8707.37197 } } dps_results: { key: "TestAffliction-AllItems-TirelessSkyflareDiamond" value: { - dps: 9745.03511 - tps: 8747.05063 + dps: 9750.98253 + tps: 8753.53236 } } dps_results: { key: "TestAffliction-AllItems-TirelessStarflareDiamond" value: { - dps: 9735.63754 - tps: 8738.383 + dps: 9740.15716 + tps: 8743.57751 } } dps_results: { key: "TestAffliction-AllItems-TrenchantEarthshatterDiamond" value: { - dps: 9735.63754 - tps: 8738.383 + dps: 9740.15716 + tps: 8743.57751 } } dps_results: { key: "TestAffliction-AllItems-TrenchantEarthsiegeDiamond" value: { - dps: 9745.03511 - tps: 8747.05063 + dps: 9750.98253 + tps: 8753.53236 } } dps_results: { key: "TestAffliction-Average-Default" value: { - dps: 9981.04862 - tps: 8984.43374 + dps: 9988.36348 + tps: 8991.30281 } } dps_results: { key: "TestAffliction-Settings-Orc-P2-AffItemSwap-FullBuffs-LongMultiTarget" value: { - dps: 9897.93489 - tps: 10584.35925 + dps: 26828.65053 + tps: 31672.76187 } } dps_results: { key: "TestAffliction-Settings-Orc-P2-AffItemSwap-FullBuffs-LongSingleTarget" value: { - dps: 9897.93489 - tps: 8900.47784 + dps: 9913.28273 + tps: 8916.52744 } } dps_results: { key: "TestAffliction-Settings-Orc-P2-AffItemSwap-FullBuffs-ShortSingleTarget" value: { - dps: 10579.33103 - tps: 9536.98224 + dps: 10621.15801 + tps: 9575.28576 } } dps_results: { key: "TestAffliction-Settings-Orc-P2-AffItemSwap-NoBuffs-LongMultiTarget" value: { - dps: 5681.69666 - tps: 7350.03451 + dps: 16048.19022 + tps: 21047.37708 } } dps_results: { key: "TestAffliction-Settings-Orc-P2-AffItemSwap-NoBuffs-LongSingleTarget" value: { - dps: 5681.69666 - tps: 5345.70269 + dps: 5680.67208 + tps: 5344.29601 } } dps_results: { key: "TestAffliction-Settings-Orc-P2-AffItemSwap-NoBuffs-ShortSingleTarget" value: { - dps: 5495.32631 - tps: 5085.13094 + dps: 5505.14837 + tps: 5094.96055 } } dps_results: { key: "TestAffliction-Settings-Orc-P2-Affliction Warlock-FullBuffs-LongMultiTarget" value: { - dps: 9875.79266 - tps: 10586.48959 + dps: 29448.56456 + tps: 34224.64373 } } dps_results: { key: "TestAffliction-Settings-Orc-P2-Affliction Warlock-FullBuffs-LongSingleTarget" value: { - dps: 9875.79266 - tps: 8883.14689 + dps: 9896.03229 + tps: 8903.19526 } } dps_results: { key: "TestAffliction-Settings-Orc-P2-Affliction Warlock-FullBuffs-ShortSingleTarget" value: { - dps: 10602.6343 - tps: 9561.44493 + dps: 10641.40514 + tps: 9599.45374 } } dps_results: { key: "TestAffliction-Settings-Orc-P2-Affliction Warlock-NoBuffs-LongMultiTarget" value: { - dps: 5674.98944 - tps: 7333.85014 + dps: 17514.05273 + tps: 22592.62225 } } dps_results: { key: "TestAffliction-Settings-Orc-P2-Affliction Warlock-NoBuffs-LongSingleTarget" value: { - dps: 5674.98944 - tps: 5339.90185 + dps: 5682.31203 + tps: 5346.9495 } } dps_results: { key: "TestAffliction-Settings-Orc-P2-Affliction Warlock-NoBuffs-ShortSingleTarget" value: { - dps: 5529.59757 - tps: 5116.83289 + dps: 5531.17129 + tps: 5117.91325 } } dps_results: { key: "TestAffliction-SwitchInFrontOfTarget-Default" value: { - dps: 9851.51572 - tps: 8897.00094 + dps: 9849.36768 + tps: 8895.46703 } } diff --git a/sim/warlock/TestDemonology.results b/sim/warlock/TestDemonology.results index 92d07bb909..e54b853331 100644 --- a/sim/warlock/TestDemonology.results +++ b/sim/warlock/TestDemonology.results @@ -46,315 +46,315 @@ character_stats_results: { dps_results: { key: "TestDemonology-AllItems-AshtongueTalismanofShadows-32493" value: { - dps: 8913.01648 - tps: 7477.54272 + dps: 8923.32281 + tps: 7491.72168 } } dps_results: { key: "TestDemonology-AllItems-AustereEarthsiegeDiamond" value: { - dps: 8922.64636 - tps: 7480.53791 + dps: 8938.86967 + tps: 7500.79184 } } dps_results: { key: "TestDemonology-AllItems-BeamingEarthsiegeDiamond" value: { - dps: 8929.4025 - tps: 7487.42755 + dps: 8942.57409 + tps: 7505.31591 } } dps_results: { key: "TestDemonology-AllItems-BracingEarthsiegeDiamond" value: { - dps: 8956.41711 - tps: 7509.72066 + dps: 8968.35388 + tps: 7526.5379 } } dps_results: { key: "TestDemonology-AllItems-ChaoticSkyflareDiamond" value: { - dps: 9100.79619 - tps: 7658.82123 + dps: 9114.79776 + tps: 7677.53959 } } dps_results: { key: "TestDemonology-AllItems-DarkCoven'sRegalia" value: { - dps: 9051.07359 - tps: 7613.05011 + dps: 9034.78411 + tps: 7597.23994 } } dps_results: { key: "TestDemonology-AllItems-DeathbringerGarb" value: { - dps: 8304.23639 - tps: 6922.53709 + dps: 8328.12096 + tps: 6948.52238 } } dps_results: { key: "TestDemonology-AllItems-DestructiveSkyflareDiamond" value: { - dps: 8931.52013 - tps: 7489.54517 + dps: 8944.47455 + tps: 7507.21638 } } dps_results: { key: "TestDemonology-AllItems-EffulgentSkyflareDiamond" value: { - dps: 8922.64636 - tps: 7480.53791 + dps: 8938.86967 + tps: 7500.79184 } } dps_results: { key: "TestDemonology-AllItems-EmberSkyflareDiamond" value: { - dps: 8956.41711 - tps: 7509.72066 + dps: 8968.35388 + tps: 7526.5379 } } dps_results: { key: "TestDemonology-AllItems-EnigmaticSkyflareDiamond" value: { - dps: 8929.4025 - tps: 7487.42755 + dps: 8942.57409 + tps: 7505.31591 } } dps_results: { key: "TestDemonology-AllItems-EnigmaticStarflareDiamond" value: { - dps: 8926.18896 - tps: 7484.214 + dps: 8940.81882 + tps: 7503.56064 } } dps_results: { key: "TestDemonology-AllItems-EternalEarthsiegeDiamond" value: { - dps: 8915.95619 - tps: 7473.98123 + dps: 8931.83709 + tps: 7494.57892 } } dps_results: { key: "TestDemonology-AllItems-ForlornSkyflareDiamond" value: { - dps: 8956.41711 - tps: 7509.72066 + dps: 8968.35388 + tps: 7526.5379 } } dps_results: { key: "TestDemonology-AllItems-ForlornStarflareDiamond" value: { - dps: 8943.72284 - tps: 7497.80175 + dps: 8960.31751 + tps: 7519.93527 } } dps_results: { key: "TestDemonology-AllItems-Gladiator'sFelshroud" value: { - dps: 8389.73834 - tps: 6974.88711 + dps: 8408.69504 + tps: 6996.82292 } } dps_results: { key: "TestDemonology-AllItems-Gul'dan'sRegalia" value: { - dps: 8395.16168 - tps: 6885.91594 + dps: 8397.36608 + tps: 6892.98957 } } dps_results: { key: "TestDemonology-AllItems-ImpassiveSkyflareDiamond" value: { - dps: 8929.4025 - tps: 7487.42755 + dps: 8942.57409 + tps: 7505.31591 } } dps_results: { key: "TestDemonology-AllItems-ImpassiveStarflareDiamond" value: { - dps: 8926.18896 - tps: 7484.214 + dps: 8940.81882 + tps: 7503.56064 } } dps_results: { key: "TestDemonology-AllItems-InsightfulEarthsiegeDiamond" value: { - dps: 8927.09872 - tps: 7483.59651 + dps: 8937.53529 + tps: 7499.83689 } } dps_results: { key: "TestDemonology-AllItems-InvigoratingEarthsiegeDiamond" value: { - dps: 8915.95619 - tps: 7473.98123 + dps: 8931.83709 + tps: 7494.57892 } } dps_results: { key: "TestDemonology-AllItems-MaleficRaiment" value: { - dps: 6623.94764 - tps: 5409.5947 + dps: 6622.31296 + tps: 5413.84104 } } dps_results: { key: "TestDemonology-AllItems-PersistentEarthshatterDiamond" value: { - dps: 8915.95619 - tps: 7473.98123 + dps: 8931.83709 + tps: 7494.57892 } } dps_results: { key: "TestDemonology-AllItems-PersistentEarthsiegeDiamond" value: { - dps: 8915.95619 - tps: 7473.98123 + dps: 8931.83709 + tps: 7494.57892 } } dps_results: { key: "TestDemonology-AllItems-PlagueheartGarb" value: { - dps: 8117.45883 - tps: 6763.2402 + dps: 8106.38586 + tps: 6759.00306 } } dps_results: { key: "TestDemonology-AllItems-PowerfulEarthshatterDiamond" value: { - dps: 8921.47283 - tps: 7479.50318 + dps: 8937.69379 + tps: 7499.75437 } } dps_results: { key: "TestDemonology-AllItems-PowerfulEarthsiegeDiamond" value: { - dps: 8922.64636 - tps: 7480.53791 + dps: 8938.86967 + tps: 7500.79184 } } dps_results: { key: "TestDemonology-AllItems-RelentlessEarthsiegeDiamond" value: { - dps: 9086.13971 - tps: 7644.16475 + dps: 9103.09444 + tps: 7665.83627 } } dps_results: { key: "TestDemonology-AllItems-RevitalizingSkyflareDiamond" value: { - dps: 8919.27934 - tps: 7475.832 + dps: 8934.56888 + tps: 7496.56059 } } dps_results: { key: "TestDemonology-AllItems-SwiftSkyflareDiamond" value: { - dps: 8915.95619 - tps: 7473.98123 + dps: 8931.83709 + tps: 7494.57892 } } dps_results: { key: "TestDemonology-AllItems-SwiftStarflareDiamond" value: { - dps: 8915.95619 - tps: 7473.98123 + dps: 8931.83709 + tps: 7494.57892 } } dps_results: { key: "TestDemonology-AllItems-SwiftWindfireDiamond" value: { - dps: 8915.95619 - tps: 7473.98123 + dps: 8931.83709 + tps: 7494.57892 } } dps_results: { key: "TestDemonology-AllItems-ThunderingSkyflareDiamond" value: { - dps: 8915.95619 - tps: 7473.98123 + dps: 8931.83709 + tps: 7494.57892 } } dps_results: { key: "TestDemonology-AllItems-TirelessSkyflareDiamond" value: { - dps: 8956.41711 - tps: 7509.72066 + dps: 8968.35388 + tps: 7526.5379 } } dps_results: { key: "TestDemonology-AllItems-TirelessStarflareDiamond" value: { - dps: 8943.72284 - tps: 7497.80175 + dps: 8960.31751 + tps: 7519.93527 } } dps_results: { key: "TestDemonology-AllItems-TrenchantEarthshatterDiamond" value: { - dps: 8943.72284 - tps: 7497.80175 + dps: 8960.31751 + tps: 7519.93527 } } dps_results: { key: "TestDemonology-AllItems-TrenchantEarthsiegeDiamond" value: { - dps: 8956.41711 - tps: 7509.72066 + dps: 8968.35388 + tps: 7526.5379 } } dps_results: { key: "TestDemonology-Average-Default" value: { - dps: 9207.22361 - tps: 7757.25927 + dps: 9204.57224 + tps: 7758.67475 } } dps_results: { key: "TestDemonology-Settings-Orc-P2-Demonology Warlock-FullBuffs-LongMultiTarget" value: { - dps: 11872.02872 - tps: 12074.30329 + dps: 32035.30776 + tps: 36434.98947 } } dps_results: { key: "TestDemonology-Settings-Orc-P2-Demonology Warlock-FullBuffs-LongSingleTarget" value: { - dps: 9103.13894 - tps: 7662.43695 + dps: 9121.70725 + tps: 7685.41695 } } dps_results: { key: "TestDemonology-Settings-Orc-P2-Demonology Warlock-FullBuffs-ShortSingleTarget" value: { - dps: 10536.01227 - tps: 8891.77115 + dps: 10543.86868 + tps: 8899.75437 } } dps_results: { key: "TestDemonology-Settings-Orc-P2-Demonology Warlock-NoBuffs-LongMultiTarget" value: { - dps: 7278.11605 - tps: 8970.89457 + dps: 19734.6907 + tps: 25031.83445 } } dps_results: { key: "TestDemonology-Settings-Orc-P2-Demonology Warlock-NoBuffs-LongSingleTarget" value: { - dps: 5179.93799 - tps: 4716.10509 + dps: 5197.29359 + tps: 4737.88837 } } dps_results: { key: "TestDemonology-Settings-Orc-P2-Demonology Warlock-NoBuffs-ShortSingleTarget" value: { - dps: 5498.96438 - tps: 4918.29555 + dps: 5468.12905 + tps: 4888.56788 } } dps_results: { key: "TestDemonology-SwitchInFrontOfTarget-Default" value: { - dps: 8973.15382 - tps: 7658.82123 + dps: 8988.61717 + tps: 7677.53959 } } diff --git a/sim/warlock/TestDestruction.results b/sim/warlock/TestDestruction.results index 57890408f1..70278d6228 100644 --- a/sim/warlock/TestDestruction.results +++ b/sim/warlock/TestDestruction.results @@ -46,36 +46,36 @@ character_stats_results: { dps_results: { key: "TestDestruction-AllItems-AshtongueTalismanofShadows-32493" value: { - dps: 9045.8536 - tps: 7312.21499 + dps: 9111.00396 + tps: 7369.58879 } } dps_results: { key: "TestDestruction-AllItems-AustereEarthsiegeDiamond" value: { - dps: 9039.20912 - tps: 7303.9201 + dps: 9126.76565 + tps: 7382.72098 } } dps_results: { key: "TestDestruction-AllItems-BeamingEarthsiegeDiamond" value: { - dps: 9056.68146 - tps: 7319.6524 + dps: 9144.4717 + tps: 7398.66362 } } dps_results: { key: "TestDestruction-AllItems-BracingEarthsiegeDiamond" value: { - dps: 9079.33864 - tps: 7337.81174 + dps: 9167.33869 + tps: 7417.01178 } } dps_results: { key: "TestDestruction-AllItems-ChaoticSkyflareDiamond" value: { - dps: 9301.55099 - tps: 7540.03497 + dps: 9392.52265 + tps: 7621.90947 } } dps_results: { @@ -88,127 +88,127 @@ dps_results: { dps_results: { key: "TestDestruction-AllItems-DeathbringerGarb" value: { - dps: 8484.75064 - tps: 6838.40605 + dps: 8566.41295 + tps: 6911.90213 } } dps_results: { key: "TestDestruction-AllItems-DestructiveSkyflareDiamond" value: { - dps: 9063.54242 - tps: 7325.82727 + dps: 9151.5324 + tps: 7405.01824 } } dps_results: { key: "TestDestruction-AllItems-EffulgentSkyflareDiamond" value: { - dps: 9039.20912 - tps: 7303.9201 + dps: 9126.76565 + tps: 7382.72098 } } dps_results: { key: "TestDestruction-AllItems-EmberSkyflareDiamond" value: { - dps: 9079.33864 - tps: 7337.81174 + dps: 9167.33869 + tps: 7417.01178 } } dps_results: { key: "TestDestruction-AllItems-EnigmaticSkyflareDiamond" value: { - dps: 9056.68146 - tps: 7319.6524 + dps: 9144.4717 + tps: 7398.66362 } } dps_results: { key: "TestDestruction-AllItems-EnigmaticStarflareDiamond" value: { - dps: 9053.9498 - tps: 7317.1939 + dps: 9141.71448 + tps: 7396.18212 } } dps_results: { key: "TestDestruction-AllItems-EternalEarthsiegeDiamond" value: { - dps: 9039.20912 - tps: 7303.9201 + dps: 9126.76565 + tps: 7382.72098 } } dps_results: { key: "TestDestruction-AllItems-ForlornSkyflareDiamond" value: { - dps: 9079.33864 - tps: 7337.81174 + dps: 9167.33869 + tps: 7417.01178 } } dps_results: { key: "TestDestruction-AllItems-ForlornStarflareDiamond" value: { - dps: 9071.37611 - tps: 7331.08579 + dps: 9159.29022 + tps: 7410.20849 } } dps_results: { key: "TestDestruction-AllItems-Gladiator'sFelshroud" value: { - dps: 8518.64487 - tps: 6847.86149 + dps: 8504.28635 + tps: 6832.98717 } } dps_results: { key: "TestDestruction-AllItems-Gul'dan'sRegalia" value: { - dps: 8629.66765 - tps: 6899.66786 + dps: 8710.91438 + tps: 6972.78991 } } dps_results: { key: "TestDestruction-AllItems-ImpassiveSkyflareDiamond" value: { - dps: 9056.68146 - tps: 7319.6524 + dps: 9144.4717 + tps: 7398.66362 } } dps_results: { key: "TestDestruction-AllItems-ImpassiveStarflareDiamond" value: { - dps: 9053.9498 - tps: 7317.1939 + dps: 9141.71448 + tps: 7396.18212 } } dps_results: { key: "TestDestruction-AllItems-InsightfulEarthsiegeDiamond" value: { - dps: 9047.64512 - tps: 7311.01298 + dps: 9135.28735 + tps: 7389.89099 } } dps_results: { key: "TestDestruction-AllItems-InvigoratingEarthsiegeDiamond" value: { - dps: 9039.20912 - tps: 7303.9201 + dps: 9126.76565 + tps: 7382.72098 } } dps_results: { key: "TestDestruction-AllItems-MaleficRaiment" value: { - dps: 6724.58837 - tps: 5342.84693 + dps: 6730.18524 + tps: 5347.11442 } } dps_results: { key: "TestDestruction-AllItems-PersistentEarthshatterDiamond" value: { - dps: 9039.20912 - tps: 7303.9201 + dps: 9126.76565 + tps: 7382.72098 } } dps_results: { key: "TestDestruction-AllItems-PersistentEarthsiegeDiamond" value: { - dps: 9039.20912 - tps: 7303.9201 + dps: 9126.76565 + tps: 7382.72098 } } dps_results: { @@ -221,140 +221,140 @@ dps_results: { dps_results: { key: "TestDestruction-AllItems-PowerfulEarthshatterDiamond" value: { - dps: 9039.20912 - tps: 7303.9201 + dps: 9126.76565 + tps: 7382.72098 } } dps_results: { key: "TestDestruction-AllItems-PowerfulEarthsiegeDiamond" value: { - dps: 9039.20912 - tps: 7303.9201 + dps: 9126.76565 + tps: 7382.72098 } } dps_results: { key: "TestDestruction-AllItems-RelentlessEarthsiegeDiamond" value: { - dps: 9282.61683 - tps: 7522.98704 + dps: 9373.33434 + tps: 7604.6328 } } dps_results: { key: "TestDestruction-AllItems-RevitalizingSkyflareDiamond" value: { - dps: 9040.31152 - tps: 7304.62203 + dps: 9127.89453 + tps: 7383.44673 } } dps_results: { key: "TestDestruction-AllItems-SwiftSkyflareDiamond" value: { - dps: 9039.20912 - tps: 7303.9201 + dps: 9126.76565 + tps: 7382.72098 } } dps_results: { key: "TestDestruction-AllItems-SwiftStarflareDiamond" value: { - dps: 9039.20912 - tps: 7303.9201 + dps: 9126.76565 + tps: 7382.72098 } } dps_results: { key: "TestDestruction-AllItems-SwiftWindfireDiamond" value: { - dps: 9039.20912 - tps: 7303.9201 + dps: 9126.76565 + tps: 7382.72098 } } dps_results: { key: "TestDestruction-AllItems-ThunderingSkyflareDiamond" value: { - dps: 9039.20912 - tps: 7303.9201 + dps: 9126.76565 + tps: 7382.72098 } } dps_results: { key: "TestDestruction-AllItems-TirelessSkyflareDiamond" value: { - dps: 9079.33864 - tps: 7337.81174 + dps: 9167.33869 + tps: 7417.01178 } } dps_results: { key: "TestDestruction-AllItems-TirelessStarflareDiamond" value: { - dps: 9071.37611 - tps: 7331.08579 + dps: 9159.29022 + tps: 7410.20849 } } dps_results: { key: "TestDestruction-AllItems-TrenchantEarthshatterDiamond" value: { - dps: 9071.37611 - tps: 7331.08579 + dps: 9159.29022 + tps: 7410.20849 } } dps_results: { key: "TestDestruction-AllItems-TrenchantEarthsiegeDiamond" value: { - dps: 9079.33864 - tps: 7337.81174 + dps: 9167.33869 + tps: 7417.01178 } } dps_results: { key: "TestDestruction-Average-Default" value: { - dps: 9423.96186 - tps: 7643.30708 + dps: 9515.82422 + tps: 7725.9832 } } dps_results: { key: "TestDestruction-Settings-Orc-P2-Destruction Warlock-FullBuffs-LongMultiTarget" value: { - dps: 9335.2174 - tps: 9610.77145 + dps: 22376.51722 + tps: 26905.6487 } } dps_results: { key: "TestDestruction-Settings-Orc-P2-Destruction Warlock-FullBuffs-LongSingleTarget" value: { - dps: 9335.2174 - tps: 7571.57895 + dps: 9426.175 + tps: 7653.44079 } } dps_results: { key: "TestDestruction-Settings-Orc-P2-Destruction Warlock-FullBuffs-ShortSingleTarget" value: { - dps: 10582.87467 - tps: 8604.29011 + dps: 10678.5304 + tps: 8690.38027 } } dps_results: { key: "TestDestruction-Settings-Orc-P2-Destruction Warlock-NoBuffs-LongMultiTarget" value: { - dps: 5061.0771 - tps: 6150.64929 + dps: 13081.16517 + tps: 18081.52664 } } dps_results: { key: "TestDestruction-Settings-Orc-P2-Destruction Warlock-NoBuffs-LongSingleTarget" value: { - dps: 5061.0771 - tps: 4274.50192 + dps: 5132.70587 + tps: 4336.74128 } } dps_results: { key: "TestDestruction-Settings-Orc-P2-Destruction Warlock-NoBuffs-ShortSingleTarget" value: { - dps: 5170.84611 - tps: 4311.90075 + dps: 5211.21887 + tps: 4347.13208 } } dps_results: { key: "TestDestruction-SwitchInFrontOfTarget-Default" value: { - dps: 9301.55099 - tps: 7540.03497 + dps: 9392.52265 + tps: 7621.90947 } } diff --git a/sim/warlock/conflagrate.go b/sim/warlock/conflagrate.go index 2fc0139d9b..464d732798 100644 --- a/sim/warlock/conflagrate.go +++ b/sim/warlock/conflagrate.go @@ -9,16 +9,6 @@ import ( func (warlock *Warlock) registerConflagrateSpell() { hasGlyphOfConflag := warlock.HasMajorGlyph(proto.WarlockMajorGlyph_GlyphOfConflagrate) - - directFlatDamage := 0.6 * 785 / 5 * float64(warlock.Immolate.CurDot().NumberOfTicks) - directSpellCoeff := 0.6 * 0.2 * float64(warlock.Immolate.CurDot().NumberOfTicks) - dotFlatDamage := 0.4 / 3 * 785 / 5 * float64(warlock.Immolate.CurDot().NumberOfTicks) - dotSpellCoeff := 0.4 / 3 * 0.2 * float64(warlock.Immolate.CurDot().NumberOfTicks) - - bonusPeriodicDamageMultiplier := 0 + - warlock.GrandSpellstoneBonus() - - warlock.GrandFirestoneBonus() - warlock.Conflagrate = warlock.RegisterSpell(core.SpellConfig{ ActionID: core.ActionID{SpellID: 17962}, SpellSchool: core.SpellSchoolFire, @@ -49,7 +39,9 @@ func (warlock *Warlock) registerConflagrateSpell() { 0.03*float64(warlock.Talents.Emberstorm) + 0.03*float64(warlock.Talents.Aftermath) + 0.1*float64(warlock.Talents.ImprovedImmolate) + - core.TernaryFloat64(warlock.HasMajorGlyph(proto.WarlockMajorGlyph_GlyphOfImmolate), 0.1, 0), + core.TernaryFloat64(warlock.HasMajorGlyph(proto.WarlockMajorGlyph_GlyphOfImmolate), 0.1, 0) + + core.TernaryFloat64(warlock.HasSetBonus(ItemSetDeathbringerGarb, 2), 0.1, 0) + + core.TernaryFloat64(warlock.HasSetBonus(ItemSetGuldansRegalia, 4), 0.1, 0), CritMultiplier: warlock.SpellCritMultiplier(1, float64(warlock.Talents.Ruin)/5), ThreatMultiplier: 1 - 0.1*float64(warlock.Talents.DestructiveReach), @@ -61,13 +53,14 @@ func (warlock *Warlock) registerConflagrateSpell() { TickLength: time.Second * 2, OnSnapshot: func(sim *core.Simulation, target *core.Unit, dot *core.Dot, isRollover bool) { - dot.SnapshotBaseDamage = dotFlatDamage + dotSpellCoeff*dot.Spell.SpellPower() + dot.SnapshotBaseDamage = (314.0 / 3) + (0.4/3)*dot.Spell.SpellPower() attackTable := dot.Spell.Unit.AttackTables[target.UnitIndex] dot.SnapshotCritChance = dot.Spell.SpellCritChance(target) - dot.Spell.DamageMultiplierAdditive += bonusPeriodicDamageMultiplier + // DoT does not benefit from firestone and also not from spellstone + dot.Spell.DamageMultiplierAdditive -= warlock.GrandFirestoneBonus() dot.SnapshotAttackerMultiplier = dot.Spell.AttackerDamageMultiplier(attackTable) - dot.Spell.DamageMultiplierAdditive -= bonusPeriodicDamageMultiplier + dot.Spell.DamageMultiplierAdditive += warlock.GrandFirestoneBonus() }, OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeSnapshotCrit) @@ -75,7 +68,8 @@ func (warlock *Warlock) registerConflagrateSpell() { }, ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { - baseDamage := directFlatDamage + directSpellCoeff*spell.SpellPower() + // takes the SP of the immolate (or shadowflame) dot on the target + baseDamage := 471.0 + 0.6*warlock.Immolate.Dot(target).Spell.SpellPower() result := spell.CalcAndDealDamage(sim, target, baseDamage, spell.OutcomeMagicHitAndCrit) if !result.Landed() { return diff --git a/sim/warlock/rotation.go b/sim/warlock/rotation.go index a411d4b027..95a69f52c9 100644 --- a/sim/warlock/rotation.go +++ b/sim/warlock/rotation.go @@ -1,7 +1,9 @@ package warlock import ( + "fmt" "math" + "sort" "time" "github.com/wowsims/wotlk/sim/core" @@ -114,14 +116,15 @@ func (warlock *Warlock) calcRelativeCorruptionInc(target *core.Unit) float64 { return curDmg / snapshotDmg } -func aclAppendSimple(acl []ActionCondition, spell *core.Spell, cond func(sim *core.Simulation) (bool, *core.Unit)) []ActionCondition { +func aclAppendSimple(acl []ActionCondition, spell *core.Spell, cond func(sim *core.Simulation) ( + bool, *core.Unit, string)) []ActionCondition { return append(acl, ActionCondition{ Spell: spell, - Condition: func(sim *core.Simulation) (ACLaction, *core.Unit) { - if cond, target := cond(sim); cond { - return ACLCast, target + Condition: func(sim *core.Simulation) (ACLaction, *core.Unit, string) { + if cond, target, reason := cond(sim); cond { + return ACLCast, target, reason } else { - return ACLNext, target + return ACLNext, nil, reason } }, }) @@ -129,69 +132,100 @@ func aclAppendSimple(acl []ActionCondition, spell *core.Spell, cond func(sim *co func (warlock *Warlock) defineRotation() { acl := warlock.acl - warlock.skipList = make(map[int]struct{}) - mainTarget := warlock.CurrentTarget + mainTarget := warlock.CurrentTarget // assumed to be the first element in the target list hauntTravel := time.Duration(float64(time.Second) * warlock.DistanceFromTarget / warlock.Haunt.MissileSpeed) - critDebuffCat := warlock.GetEnemyExclusiveCategories(core.SpellCritEffectCategory)[0] + critDebuffCat := warlock.GetEnemyExclusiveCategories(core.SpellCritEffectCategory).Get(mainTarget) + + allUnits := warlock.Env.Encounter.TargetUnits + if mainTarget != allUnits[0] { + panic("CurrentTarget assumption violated") + } + + var multidotTargets, uaDotTargets []*core.Unit + multidotCount := core.MinInt(len(allUnits), 3) + if warlock.Rotation.Type == proto.Warlock_Rotation_Affliction { + // up to 3 targets: multidot, no seed + // 4 targets: corruption+UA 3x, seed on 4th; possibly only 1x UA since it's close in value + // 5 targets: corruption x3, UA 1x, seed + // 6 targets: corruption x2, UA 1x, seed; only 1x corruption + UA is close in value + // 7-9 targets: corruption x1, no UA, seed + // 10+ targets: no corruption anymore probably + uaCount := core.MinInt(len(allUnits), 3) + + if len(allUnits) > 4 { + uaCount = 1 + } + if len(allUnits) == 6 { + multidotCount = 2 + } else if len(allUnits) > 6 { + uaCount = 0 + multidotCount = core.TernaryInt(len(allUnits) > 9, 0, 1) + } + + uaDotTargets = allUnits[:uaCount] + } else if warlock.Rotation.Type == proto.Warlock_Rotation_Destruction { + multidotCount = core.MinInt(len(allUnits), 4) + } + multidotTargets = allUnits[:multidotCount] if warlock.Talents.DemonicEmpowerment && warlock.Options.Summon != proto.Warlock_Options_NoSummon { - acl = aclAppendSimple(acl, warlock.DemonicEmpowerment, func(sim *core.Simulation) (bool, *core.Unit) { - return !warlock.Rotation.UseInfernal || warlock.Inferno.IsReady(sim), mainTarget + acl = aclAppendSimple(acl, warlock.DemonicEmpowerment, func(sim *core.Simulation) (bool, *core.Unit, string) { + return !warlock.Rotation.UseInfernal || warlock.Inferno.IsReady(sim), mainTarget, "" }) } if warlock.Talents.Metamorphosis { - acl = aclAppendSimple(acl, warlock.ImmolationAura, func(sim *core.Simulation) (bool, *core.Unit) { + acl = aclAppendSimple(acl, warlock.ImmolationAura, func(sim *core.Simulation) (bool, *core.Unit, string) { // TODO: potentially wait for procs - return true, nil + return true, nil, "" }) } - // TODO: the real AoE rotation is way more complicated than this, this is really just a stub + // only handles deliberate overrides of the primary spell if warlock.Rotation.PrimarySpell == proto.Warlock_Rotation_Seed { - acl = aclAppendSimple(acl, warlock.Seed, func(sim *core.Simulation) (bool, *core.Unit) { - return warlock.Rotation.DetonateSeed || !warlock.Seed.Dot(mainTarget).IsActive(), mainTarget + acl = aclAppendSimple(acl, warlock.Seed, func(sim *core.Simulation) (bool, *core.Unit, string) { + return warlock.Rotation.DetonateSeed || !warlock.Seed.Dot(mainTarget).IsActive(), mainTarget, "" }) } if warlock.Talents.Conflagrate { - acl = aclAppendSimple(acl, warlock.Conflagrate, func(sim *core.Simulation) (bool, *core.Unit) { - return warlock.Immolate.Dot(mainTarget).IsActive(), mainTarget + acl = aclAppendSimple(acl, warlock.Conflagrate, func(sim *core.Simulation) (bool, *core.Unit, string) { + return warlock.Immolate.Dot(mainTarget).IsActive(), mainTarget, "" }) } if warlock.Talents.Haunt && warlock.Rotation.SpecSpell == proto.Warlock_Rotation_Haunt { curIndex := len(acl) - acl = aclAppendSimple(acl, warlock.Haunt, func(sim *core.Simulation) (bool, *core.Unit) { + acl = aclAppendSimple(acl, warlock.Haunt, func(sim *core.Simulation) (bool, *core.Unit, string) { // no need for haunt until dots are up, mostly relevant in the opener if !warlock.Corruption.Dot(mainTarget).IsActive() && !warlock.UnstableAffliction.Dot(mainTarget).IsActive() { - return false, nil + return false, nil, "" } if !warlock.Haunt.CD.IsReady(sim) { - return false, nil + return false, nil, "" } if sim.GetRemainingDuration() < 5*time.Second { - return false, nil + return false, nil, "" } castTime := warlock.Haunt.CastTime() - nextActionTime := warlock.getNextActionTime(sim, curIndex) + _, nextActionTime := warlock.getAlternativeAction(sim, curIndex) hauntRem := warlock.HauntDebuffAuras.Get(mainTarget).RemainingDuration(sim) // 250ms of leeway in case haste buffs run out - return hauntRem-castTime-hauntTravel < nextActionTime+250*time.Millisecond, mainTarget + return hauntRem-castTime-hauntTravel < nextActionTime+250*time.Millisecond, mainTarget, "" }) - acl = aclAppendSimple(acl, warlock.LifeTap, func(sim *core.Simulation) (bool, *core.Unit) { + acl = aclAppendSimple(acl, warlock.LifeTap, func(sim *core.Simulation) (bool, *core.Unit, string) { val := warlock.ShadowBolt.DefaultCast.Cost if sim.IsExecutePhase25() { dsDot := warlock.DrainSoul.CurDot() if dsDot.IsActive() && dsDot.NumTicksRemaining(sim) >= 1 { - return false, nil // continuing to channel drain soul doesn't cost us any mana + return false, nil, "" // continuing to channel drain soul doesn't cost us any mana } val = warlock.UnstableAffliction.DefaultCast.Cost // highest mana cost spell outside SB @@ -199,44 +233,126 @@ func (warlock *Warlock) defineRotation() { val += warlock.Haunt.DefaultCast.Cost if warlock.CurrentMana() > val || sim.GetRemainingDuration() > 5*time.Second { - return false, nil + return false, nil, "" + } + + return true, nil, "Casting life tap to not drop haunt" + }) + } + + // refresh corruption with shadow bolt if it's running out + if warlock.Talents.EverlastingAffliction == 5 && len(allUnits) > 1 { + travel := time.Duration(float64(time.Second) * warlock.DistanceFromTarget / warlock.ShadowBolt.MissileSpeed) + curIndex := len(acl) + + acl = aclAppendSimple(acl, warlock.ShadowBolt, func(sim *core.Simulation) (bool, *core.Unit, string) { + type targetRem struct { + target *core.Unit + rem time.Duration + } + targets := make([]targetRem, 0, len(sim.Encounter.TargetUnits)) + for _, target := range sim.Encounter.TargetUnits { + // if there's already an shadowbolt on the way then skip + if warlock.corrRefreshList[target.UnitIndex] >= sim.CurrentTime-travel { + continue + } + + // same when we can't refresh in time + if warlock.Corruption.Dot(target).RemainingDuration(sim) < travel+warlock.ShadowBolt.CastTime() { + continue + } + + // assuming haunt doesn't drop, which it shouldn't, corruption will already be refreshed + if target == mainTarget && warlock.HauntDebuffAuras.Get(target).RemainingDuration(sim) < + warlock.Corruption.Dot(target).RemainingDuration(sim) { + continue + } + + rem := core.MinDuration(warlock.Corruption.Dot(target).RemainingDuration(sim), + warlock.ShadowEmbraceAuras.Get(target).RemainingDuration(sim)) + targets = append(targets, targetRem{rem: rem, target: target}) + } + sort.Slice(targets, func(i, j int) bool { return targets[i].rem < targets[j].rem }) + + // we know that the only higher priority action is haunt, thus the only 2 things we need to + // consider outside of shadow bolts is haunt and mana + nextSpell, timeAdvance := warlock.getAlternativeAction(sim, curIndex) + sbCastTime := warlock.ShadowBolt.EffectiveCastTime() + timeAdvance += sbCastTime + recast := false + // shadow trance proc will only speed up one cast + if warlock.ShadowBolt.CastTimeMultiplier == 0 { + // somewhat hacky, breaks if CastTimeMultiplier is ever changed by anything else + sbCastTime = time.Duration(float64(warlock.ShadowBolt.DefaultCast.CastTime) * warlock.CastSpeed) + sbCastTime = core.MaxDuration(sbCastTime, warlock.SpellGCD()) + + if nextSpell == warlock.ShadowBolt { + timeAdvance += sbCastTime - warlock.ShadowBolt.EffectiveCastTime() + } } + mana := warlock.CurrentMana() - warlock.ShadowBolt.DefaultCast.Cost + consideredHaunt := false + for _, ele := range targets { + if mana < warlock.ShadowBolt.DefaultCast.Cost { + timeAdvance += warlock.LifeTap.EffectiveCastTime() + mana += 10000.0 // we only need 1 life tap, so the exact value doesn't matter + } + mana -= warlock.ShadowBolt.DefaultCast.Cost + + if !consideredHaunt && timeAdvance+warlock.Haunt.CastTime()+hauntTravel >= + warlock.HauntDebuffAuras.Get(mainTarget).RemainingDuration(sim) { + timeAdvance += warlock.Haunt.EffectiveCastTime() + mana -= warlock.Haunt.DefaultCast.Cost + consideredHaunt = true + } + + // some extra time to accommodate haste buffs running out + if timeAdvance+travel+250*time.Millisecond >= ele.rem { + recast = true + break + } - if sim.Log != nil && len(warlock.skipList) == 0 { - warlock.Log(sim, "[Info] Casting life tap to not drop haunt") + timeAdvance += sbCastTime + 50*time.Millisecond } - return true, nil + if recast { + return true, targets[0].target, "" + } else { + return false, nil, "" + } }) } if warlock.Rotation.Corruption && warlock.Talents.EverlastingAffliction > 0 { - acl = aclAppendSimple(acl, warlock.Corruption, func(sim *core.Simulation) (bool, *core.Unit) { + acl = aclAppendSimple(acl, warlock.Corruption, func(sim *core.Simulation) (bool, *core.Unit, string) { + // TODO: wait for all targets SB debuff? if !critDebuffCat.AnyActive() && warlock.Talents.ImprovedShadowBolt > 0 && sim.CurrentTime < 25 { - return false, nil + return false, nil, "" } - if !warlock.Corruption.Dot(mainTarget).IsActive() { - return true, mainTarget - } + reason := "" + for _, target := range multidotTargets { + if !warlock.Corruption.Dot(target).IsActive() { + return true, target, "" + } - // check if reapplying corruption is a worthwhile - relDmgInc := warlock.calcRelativeCorruptionInc(mainTarget) - snapshotDmg := warlock.Corruption.ExpectedDamageFromCurrentSnapshot(sim, mainTarget) - snapshotDmg *= float64(sim.GetRemainingDuration()) / float64(warlock.Corruption.Dot(mainTarget).TickPeriod()) - snapshotDmg *= (relDmgInc - 1) + // check if reapplying corruption is worthwhile + relDmgInc := warlock.calcRelativeCorruptionInc(target) + snapshotDmg := warlock.Corruption.ExpectedDamageFromCurrentSnapshot(sim, target) + snapshotDmg *= float64(sim.GetRemainingDuration()) / float64(warlock.Corruption.Dot(target).TickPeriod()) + snapshotDmg *= (relDmgInc - 1) + snapshotDmg -= warlock.Corruption.ExpectedDamageFromCurrentSnapshot(sim, target) - if sim.Log != nil && len(warlock.skipList) == 0 { - warlock.Log(sim, "[Info] Relative Corruption Inc: [%.2f], expected dmg gain: [%.2f]", + reason = fmt.Sprintf("Relative Corruption Inc: [%.2f], expected dmg gain: [%.2f]", relDmgInc, snapshotDmg) - } - if relDmgInc > 1.15 || snapshotDmg > 6000 { - return true, mainTarget + if relDmgInc > 1.15 || snapshotDmg > 10000 { + return true, target, reason + } } - return false, nil + return false, nil, reason }) } @@ -244,131 +360,146 @@ func (warlock *Warlock) defineRotation() { switch warlock.Rotation.Curse { case proto.Warlock_Rotation_Elements: prefCurse = warlock.CurseOfElementsAuras.Get(mainTarget) - acl = aclAppendSimple(acl, warlock.CurseOfElements, func(sim *core.Simulation) (bool, *core.Unit) { - return warlock.CurseOfElementsAuras.Get(mainTarget).RemainingDuration(sim) < 3*time.Second, mainTarget + acl = aclAppendSimple(acl, warlock.CurseOfElements, func(sim *core.Simulation) (bool, *core.Unit, string) { + return warlock.CurseOfElementsAuras.Get(mainTarget).RemainingDuration(sim) < 3*time.Second, mainTarget, "" }) case proto.Warlock_Rotation_Weakness: prefCurse = warlock.CurseOfWeaknessAuras.Get(mainTarget) - acl = aclAppendSimple(acl, warlock.CurseOfWeakness, func(sim *core.Simulation) (bool, *core.Unit) { - return warlock.CurseOfWeaknessAuras.Get(mainTarget).RemainingDuration(sim) < 3*time.Second, mainTarget + acl = aclAppendSimple(acl, warlock.CurseOfWeakness, func(sim *core.Simulation) (bool, *core.Unit, string) { + return warlock.CurseOfWeaknessAuras.Get(mainTarget).RemainingDuration(sim) < 3*time.Second, mainTarget, "" }) case proto.Warlock_Rotation_Tongues: prefCurse = warlock.CurseOfTonguesAuras.Get(mainTarget) - acl = aclAppendSimple(acl, warlock.CurseOfTongues, func(sim *core.Simulation) (bool, *core.Unit) { - return warlock.CurseOfTonguesAuras.Get(mainTarget).RemainingDuration(sim) < 3*time.Second, mainTarget + acl = aclAppendSimple(acl, warlock.CurseOfTongues, func(sim *core.Simulation) (bool, *core.Unit, string) { + return warlock.CurseOfTonguesAuras.Get(mainTarget).RemainingDuration(sim) < 3*time.Second, mainTarget, "" }) } if warlock.HasMajorGlyph(proto.WarlockMajorGlyph_GlyphOfLifeTap) { - acl = aclAppendSimple(acl, warlock.LifeTap, func(sim *core.Simulation) (bool, *core.Unit) { + acl = aclAppendSimple(acl, warlock.LifeTap, func(sim *core.Simulation) (bool, *core.Unit, string) { // try to keep up the buff for the entire execute phase if possible expiresAt := core.MaxDuration(0, warlock.GlyphOfLifeTapAura.RemainingDuration(sim)) if sim.GetRemainingDuration() <= 40*time.Second && expiresAt+10*time.Second < sim.GetRemainingDuration() && warlock.CurrentManaPercent() < 0.35 { - if sim.Log != nil && len(warlock.skipList) == 0 { - warlock.Log(sim, "[Info] Casting life tap to keep up GoLT (40s till EOF)") - } - - return true, nil + return true, nil, "Casting life tap to keep up GoLT (40s till EOF)" } else if sim.GetRemainingDuration() <= 55*time.Second { - return false, nil + return false, nil, "" } if warlock.GlyphOfLifeTapAura.RemainingDuration(sim) > 1*time.Second || sim.GetRemainingDuration() <= 10*time.Second { - return false, nil + return false, nil, "" } - if sim.Log != nil && len(warlock.skipList) == 0 { - warlock.Log(sim, "[Info] Casting life tap to keep up GoLT") - } - - return true, nil + return true, nil, "Casting life tap to keep up GoLT" }) } if warlock.Talents.UnstableAffliction && warlock.Rotation.SecondaryDot == proto.Warlock_Rotation_UnstableAffliction { - acl = aclAppendSimple(acl, warlock.UnstableAffliction, func(sim *core.Simulation) (bool, *core.Unit) { + acl = aclAppendSimple(acl, warlock.UnstableAffliction, func(sim *core.Simulation) (bool, *core.Unit, string) { castTime := warlock.UnstableAffliction.CastTime() - if warlock.UnstableAffliction.Dot(mainTarget).RemainingDuration(sim)-castTime <= 0 && - sim.GetRemainingDuration() >= 9*time.Second+castTime { - return true, mainTarget + for _, target := range uaDotTargets { + if warlock.UnstableAffliction.Dot(target).RemainingDuration(sim)-castTime <= 0 && + sim.GetRemainingDuration() >= 9*time.Second+castTime { + return true, target, "" + } } - return false, nil + return false, nil, "" + }) + } + + if len(allUnits) > len(multidotTargets) { + acl = aclAppendSimple(acl, warlock.Seed, func(sim *core.Simulation) (bool, *core.Unit, string) { + for _, target := range sim.Encounter.TargetUnits { + // avoid mainTarget as we may want to corruption that later + if !warlock.Corruption.Dot(target).IsActive() && target != mainTarget { + return true, target, "" + } + } + panic("No viable seed target found") }) } // TODO: automatically determine based on haunt/SE? if warlock.Rotation.Curse == proto.Warlock_Rotation_Doom { - acl = aclAppendSimple(acl, warlock.CurseOfDoom, func(sim *core.Simulation) (bool, *core.Unit) { + acl = aclAppendSimple(acl, warlock.CurseOfDoom, func(sim *core.Simulation) (bool, *core.Unit, string) { return warlock.CurseOfDoom.Dot(mainTarget).RemainingDuration(sim) <= 0 && - sim.GetRemainingDuration() >= 60*time.Second, mainTarget + sim.GetRemainingDuration() >= 60*time.Second, mainTarget, "" }) } - if warlock.Rotation.Curse == proto.Warlock_Rotation_Agony || warlock.Rotation.Curse == proto.Warlock_Rotation_Doom { - tickHeuristic := core.TernaryDuration(warlock.Talents.Haunt, 16*time.Second, 22*time.Second) + if warlock.Rotation.Corruption && warlock.Talents.EverlastingAffliction <= 0 { + acl = aclAppendSimple(acl, warlock.Corruption, func(sim *core.Simulation) (bool, *core.Unit, string) { + for _, target := range multidotTargets { + dot := warlock.Corruption.Dot(target) + if dot.IsActive() { + continue + } - acl = aclAppendSimple(acl, warlock.CurseOfAgony, func(sim *core.Simulation) (bool, *core.Unit) { - if !warlock.CurseOfDoom.Dot(mainTarget).IsActive() && !warlock.CurseOfAgony. - Dot(mainTarget).IsActive() && sim.GetRemainingDuration() >= tickHeuristic { - return true, mainTarget - } + tickLen := dot.TickLength + if dot.AffectedByCastSpeed { + tickLen = warlock.ApplyCastSpeed(tickLen) + } - return false, nil + if sim.GetRemainingDuration() >= 4*tickLen { + return true, target, "" + } + } + return false, nil, "" }) } - if warlock.Rotation.Corruption && warlock.Talents.EverlastingAffliction <= 0 { - acl = aclAppendSimple(acl, warlock.Corruption, func(sim *core.Simulation) (bool, *core.Unit) { - dot := warlock.Corruption.Dot(mainTarget) - if dot.IsActive() { - return false, nil - } + if warlock.Rotation.Curse == proto.Warlock_Rotation_Agony || warlock.Rotation.Curse == proto.Warlock_Rotation_Doom { + tickHeuristic := core.TernaryDuration(warlock.Talents.Haunt, 16*time.Second, 22*time.Second) - tickLen := dot.TickLength - if dot.AffectedByCastSpeed { - tickLen = warlock.ApplyCastSpeed(tickLen) + acl = aclAppendSimple(acl, warlock.CurseOfAgony, func(sim *core.Simulation) (bool, *core.Unit, string) { + for _, target := range multidotTargets { + if !warlock.CurseOfDoom.Dot(target).IsActive() && !warlock.CurseOfAgony. + Dot(target).IsActive() && sim.GetRemainingDuration() >= tickHeuristic { + return true, target, "" + } } - return sim.GetRemainingDuration() >= 4*tickLen, mainTarget + return false, nil, "" }) } if !warlock.Talents.UnstableAffliction && warlock.Rotation.SecondaryDot == proto.Warlock_Rotation_Immolate { tickHeuristic := core.TernaryDuration(warlock.Talents.Conflagrate, 6*time.Second, 12*time.Second) - acl = aclAppendSimple(acl, warlock.Immolate, func(sim *core.Simulation) (bool, *core.Unit) { + acl = aclAppendSimple(acl, warlock.Immolate, func(sim *core.Simulation) (bool, *core.Unit, string) { castTime := warlock.Immolate.CastTime() - return warlock.Immolate.Dot(mainTarget).RemainingDuration(sim)-castTime <= 0 && - sim.GetRemainingDuration() >= tickHeuristic+castTime, mainTarget + for _, target := range multidotTargets { + if warlock.Immolate.Dot(target).RemainingDuration(sim)-castTime <= 0 && + sim.GetRemainingDuration() >= tickHeuristic+castTime { + return true, target, "" + } + } + return false, nil, "" }) } if warlock.Talents.ChaosBolt { - acl = aclAppendSimple(acl, warlock.ChaosBolt, func(sim *core.Simulation) (bool, *core.Unit) { - return true, mainTarget + acl = aclAppendSimple(acl, warlock.ChaosBolt, func(sim *core.Simulation) (bool, *core.Unit, string) { + return true, mainTarget, "" }) } if warlock.Talents.Haunt { - function := func(sim *core.Simulation) (ACLaction, *core.Unit) { + function := func(sim *core.Simulation) (ACLaction, *core.Unit, string) { dsDot := warlock.DrainSoul.CurDot() if !sim.IsExecutePhase25() { - return ACLNext, nil + return ACLNext, nil, "" } if !dsDot.IsActive() || dsDot.TimeUntilNextTick(sim) < dsDot.TickPeriod()-humanReactionTime { - return ACLCast, mainTarget + return ACLCast, mainTarget, "" } if warlock.Corruption.CurDot().RemainingDuration(sim) < dsDot.TickPeriod() { - if sim.Log != nil && len(warlock.skipList) == 0 { - warlock.Log(sim, "[Info] Recasting drain soul to not let corruption drop") - } - return ACLRecast, mainTarget // recast to not let corruption drop + return ACLRecast, mainTarget, "Recasting drain soul to not let corruption drop" } // check if recasting drain soul is worthwhile @@ -396,7 +527,7 @@ func (warlock *Warlock) defineRotation() { recastTicks = core.MinInt(recastTicks, int(dsDot.NumberOfTicks)) if ticksLeft <= 0 || recastTicks <= 0 { - return ACLCast, mainTarget + return ACLCast, mainTarget, "" } snapshotDmg := warlock.DrainSoul.ExpectedDamageFromCurrentSnapshot(sim, mainTarget) * float64(ticksLeft) @@ -406,18 +537,14 @@ func (warlock *Warlock) defineRotation() { humanReactionTime.Seconds()) if recastDps > snapshotDPS { - if sim.Log != nil && len(warlock.skipList) == 0 { - warlock.Log(sim, "[Info] Recasting drain soul, %.2f (%d) > %.2f (%d)\n", - recastDps, recastTicks, snapshotDPS, ticksLeft) - } - - return ACLRecast, mainTarget + return ACLRecast, mainTarget, fmt.Sprintf("Recasting drain soul, %.2f (%d) > %.2f (%d)", + recastDps, recastTicks, snapshotDPS, ticksLeft) } // TODO: if number of ticks left < number of ticks until we need to recast dots/haunt // and some proc effect falls off before the next tick, check if recasting is a DPS gain - return ACLCast, mainTarget + return ACLCast, mainTarget, "" } acl = append(acl, ActionCondition{ @@ -427,48 +554,44 @@ func (warlock *Warlock) defineRotation() { } if warlock.Talents.Decimation > 0 { - acl = aclAppendSimple(acl, warlock.SoulFire, func(sim *core.Simulation) (bool, *core.Unit) { - return warlock.DecimationAura.IsActive(), mainTarget + acl = aclAppendSimple(acl, warlock.SoulFire, func(sim *core.Simulation) (bool, *core.Unit, string) { + return warlock.DecimationAura.IsActive(), mainTarget, "" }) } if warlock.Talents.MoltenCore > 0 { - acl = aclAppendSimple(acl, warlock.Incinerate, func(sim *core.Simulation) (bool, *core.Unit) { - return warlock.MoltenCoreAura.IsActive(), mainTarget + acl = aclAppendSimple(acl, warlock.Incinerate, func(sim *core.Simulation) (bool, *core.Unit, string) { + return warlock.MoltenCoreAura.IsActive(), mainTarget, "" }) } if warlock.Rotation.PrimarySpell == proto.Warlock_Rotation_Incinerate { - acl = aclAppendSimple(acl, warlock.Incinerate, func(sim *core.Simulation) (bool, *core.Unit) { - return true, mainTarget + acl = aclAppendSimple(acl, warlock.Incinerate, func(sim *core.Simulation) (bool, *core.Unit, string) { + return true, mainTarget, "" }) } - acl = aclAppendSimple(acl, warlock.ShadowBolt, func(sim *core.Simulation) (bool, *core.Unit) { - return true, mainTarget + acl = aclAppendSimple(acl, warlock.ShadowBolt, func(sim *core.Simulation) (bool, *core.Unit, string) { + return true, mainTarget, "" }) if warlock.Talents.DarkPact { - acl = aclAppendSimple(acl, warlock.DarkPact, func(sim *core.Simulation) (bool, *core.Unit) { + acl = aclAppendSimple(acl, warlock.DarkPact, func(sim *core.Simulation) (bool, *core.Unit, string) { // if pet has enough mana, prefer dark pact over life tap - return warlock.Pet.CurrentMana() > warlock.GetStat(stats.SpellPower)+1200+131, nil + return warlock.Pet.CurrentMana() > warlock.GetStat(stats.SpellPower)+1200+131, nil, "" }) } - acl = aclAppendSimple(acl, warlock.LifeTap, func(sim *core.Simulation) (bool, *core.Unit) { return true, nil }) + acl = aclAppendSimple(acl, warlock.LifeTap, func(sim *core.Simulation) (bool, *core.Unit, string) { + return true, nil, "" + }) warlock.acl = acl } -func aclNextAction(sim *core.Simulation, acl []ActionCondition, skipList map[int]struct{}, skipIndex int) (*core.Spell, bool) { - skipList[skipIndex] = struct{}{} - for i, ac := range acl { - if _, contains := skipList[i]; contains { - continue - } - - if action, _ := ac.Condition(sim); action != ACLNext && ac.Spell.IsReady(sim) { - delete(skipList, skipIndex) +func aclNextAction(sim *core.Simulation, acl []ActionCondition, skipIndex int) (*core.Spell, bool) { + for _, ac := range acl[skipIndex+1:] { + if action, _, _ := ac.Condition(sim); action != ACLNext && ac.Spell.IsReady(sim) { return ac.Spell, action == ACLRecast } } @@ -476,10 +599,10 @@ func aclNextAction(sim *core.Simulation, acl []ActionCondition, skipList map[int panic("ACL list exhausted but no match found") } -// time the next action will take, until we are ready to cast something else again -func (warlock *Warlock) getNextActionTime(sim *core.Simulation, skipIndex int) time.Duration { +// Returns the spell and casttime of the alternative action we'd take, if we skip skipIndex +func (warlock *Warlock) getAlternativeAction(sim *core.Simulation, skipIndex int) (*core.Spell, time.Duration) { var nextSpellTime time.Duration - nextSpell, recast := aclNextAction(sim, warlock.acl, warlock.skipList, skipIndex) + nextSpell, recast := aclNextAction(sim, warlock.acl, skipIndex) if nextSpell == warlock.DrainSoul { if recast || !nextSpell.CurDot().IsActive() { @@ -491,7 +614,7 @@ func (warlock *Warlock) getNextActionTime(sim *core.Simulation, skipIndex int) t nextSpellTime = nextSpell.EffectiveCastTime() } - return core.MaxDuration(core.GCDMin, nextSpellTime) + return nextSpell, core.MaxDuration(core.GCDMin, nextSpellTime) } func (warlock *Warlock) OnGCDReady(sim *core.Simulation) { @@ -521,7 +644,10 @@ func (warlock *Warlock) OnGCDReady(sim *core.Simulation) { } for _, ac := range warlock.acl { - action, target := ac.Condition(sim) + action, target, reason := ac.Condition(sim) + if reason != "" && sim.Log != nil { + warlock.Log(sim, "[Info] %s\n", reason) + } if action == ACLNext || !ac.Spell.IsReady(sim) { continue } @@ -544,22 +670,28 @@ func (warlock *Warlock) OnGCDReady(sim *core.Simulation) { } } + castTime := ac.Spell.CastTime() if success := ac.Spell.Cast(sim, target); success { + // track shadowbolts "in the air" that haven't refreshed corruption yet + if ac.Spell == warlock.ShadowBolt || ac.Spell == warlock.Haunt { + warlock.corrRefreshList[target.UnitIndex] = sim.CurrentTime + castTime + } + if !warlock.GCD.IsReady(sim) { // after-GCD actions - - if ac.Spell == warlock.Corruption && warlock.ItemSwap.IsEnabled() && warlock.swapped { + if ac.Spell == warlock.Corruption && warlock.ItemSwap.IsEnabled() && warlock.ItemSwap.IsSwapped() { warlock.ItemSwap.SwapItems(sim, []proto.ItemSlot{proto.ItemSlot_ItemSlotMainHand, proto.ItemSlot_ItemSlotOffHand, proto.ItemSlot_ItemSlotRanged}, true) - warlock.swapped = false } return } - - // TODO: if the reason we failed to cast something is that we have not enough mana, we may want - // to just tap. On the other hand maybe falling through and casting the next best thing - // sometimes has value? + } else if warlock.CurrentMana() < ac.Spell.DefaultCast.Cost { + // TODO: this will only cast life tap right now + if success := warlock.acl[len(warlock.acl)-1].Spell.Cast(sim, nil); !success { + panic("Failed to cast life tap / dark pact") + } + return } } diff --git a/sim/warlock/seed.go b/sim/warlock/seed.go index 0254aa3051..ce1773889d 100644 --- a/sim/warlock/seed.go +++ b/sim/warlock/seed.go @@ -10,11 +10,10 @@ func (warlock *Warlock) registerSeedSpell() { actionID := core.ActionID{SpellID: 47836} seedExplosion := warlock.RegisterSpell(core.SpellConfig{ - ActionID: actionID.WithTag(1), // actually 47834 - SpellSchool: core.SpellSchoolShadow, - ProcMask: core.ProcMaskSpellDamage, - Flags: core.SpellFlagHauntSE, - MissileSpeed: 28, + ActionID: actionID.WithTag(1), // actually 47834 + SpellSchool: core.SpellSchoolShadow, + ProcMask: core.ProcMaskSpellDamage, + Flags: core.SpellFlagHauntSE | core.SpellFlagNoLogs, BonusCritRating: 0 + float64(warlock.Talents.ImprovedCorruption)*core.CritRatingPerCritChance, @@ -43,10 +42,11 @@ func (warlock *Warlock) registerSeedSpell() { } warlock.Seed = warlock.RegisterSpell(core.SpellConfig{ - ActionID: actionID, - SpellSchool: core.SpellSchoolShadow, - ProcMask: core.ProcMaskEmpty, - Flags: core.SpellFlagHauntSE, + ActionID: actionID, + SpellSchool: core.SpellSchoolShadow, + ProcMask: core.ProcMaskEmpty, + Flags: core.SpellFlagHauntSE, + MissileSpeed: 28, ManaCost: core.ManaCostOptions{ BaseCost: 0.34, @@ -105,6 +105,9 @@ func (warlock *Warlock) registerSeedSpell() { result := spell.CalcOutcome(sim, target, spell.OutcomeMagicHit) spell.WaitTravelTime(sim, func(sim *core.Simulation) { if result.Landed() { + // seed is mutually exclusive with corruption + warlock.Corruption.Dot(target).Deactivate(sim) + if warlock.Rotation.DetonateSeed { seedExplosion.Cast(sim, target) } else { diff --git a/sim/warlock/talents.go b/sim/warlock/talents.go index 7574d65b6e..55d8bcdaf1 100644 --- a/sim/warlock/talents.go +++ b/sim/warlock/talents.go @@ -292,7 +292,7 @@ func (warlock *Warlock) setupShadowEmbrace() { return } - shadowEmbraceAuras := warlock.NewEnemyAuraArray(warlock.ShadowEmbraceDebuffAura) + warlock.ShadowEmbraceAuras = warlock.NewEnemyAuraArray(warlock.ShadowEmbraceDebuffAura) warlock.RegisterAura(core.Aura{ Label: "Shadow Embrace Talent Hidden Aura", @@ -301,8 +301,8 @@ func (warlock *Warlock) setupShadowEmbrace() { aura.Activate(sim) }, OnSpellHitDealt: func(aura *core.Aura, sim *core.Simulation, spell *core.Spell, result *core.SpellResult) { - if spell == warlock.ShadowBolt || spell == warlock.Haunt { - aura := shadowEmbraceAuras.Get(result.Target) + if (spell == warlock.ShadowBolt || spell == warlock.Haunt) && result.Landed() { + aura := warlock.ShadowEmbraceAuras.Get(result.Target) aura.Activate(sim) aura.AddStack(sim) } @@ -453,7 +453,7 @@ func (warlock *Warlock) setupBackdraft() { aura.Activate(sim) }, OnSpellHitDealt: func(aura *core.Aura, sim *core.Simulation, spell *core.Spell, result *core.SpellResult) { - if spell == warlock.Conflagrate { + if spell == warlock.Conflagrate && result.Landed() { warlock.BackdraftAura.Activate(sim) warlock.BackdraftAura.SetStacks(sim, 3) } @@ -511,8 +511,8 @@ func (warlock *Warlock) setupImprovedSoulLeech() { aura.Activate(sim) }, OnSpellHitDealt: func(aura *core.Aura, sim *core.Simulation, spell *core.Spell, result *core.SpellResult) { - if spell == warlock.Conflagrate || spell == warlock.ShadowBolt || spell == warlock.ChaosBolt || - spell == warlock.SoulFire || spell == warlock.Incinerate { + if (spell == warlock.Conflagrate || spell == warlock.ShadowBolt || spell == warlock.ChaosBolt || + spell == warlock.SoulFire || spell == warlock.Incinerate) && result.Landed() { if !sim.Proc(soulLeechProcChance, "SoulLeech") { return } diff --git a/sim/warlock/warlock.go b/sim/warlock/warlock.go index be9525d97e..ce7bca9aa2 100644 --- a/sim/warlock/warlock.go +++ b/sim/warlock/warlock.go @@ -43,6 +43,7 @@ type Warlock struct { Seed *core.Spell SeedDamageTracker []float64 + ShadowEmbraceAuras core.AuraArray NightfallProcAura *core.Aura EradicationAura *core.Aura DemonicEmpowerment *core.Spell @@ -70,8 +71,9 @@ type Warlock struct { petStmBonusSP float64 acl []ActionCondition - skipList map[int]struct{} - swapped bool + + // contains for each target the time the last shadowbolt was casted onto them + corrRefreshList []time.Duration } type ACLaction int @@ -84,7 +86,7 @@ const ( type ActionCondition struct { Spell *core.Spell - Condition func(*core.Simulation) (ACLaction, *core.Unit) + Condition func(*core.Simulation) (ACLaction, *core.Unit, string) } func (warlock *Warlock) GetCharacter() *core.Character { @@ -180,7 +182,7 @@ func (warlock *Warlock) Reset(sim *core.Simulation) { warlock.ItemSwap.SwapItems(sim, []proto.ItemSlot{proto.ItemSlot_ItemSlotMainHand, proto.ItemSlot_ItemSlotOffHand, proto.ItemSlot_ItemSlotRanged}, false) - warlock.swapped = true + warlock.corrRefreshList = make([]time.Duration, len(warlock.Env.Encounter.TargetUnits)) warlock.setupCooldowns(sim) } diff --git a/sim/warlock/warlock_test.go b/sim/warlock/warlock_test.go index 420d4d18ef..78b7469f6e 100644 --- a/sim/warlock/warlock_test.go +++ b/sim/warlock/warlock_test.go @@ -99,6 +99,7 @@ var defaultDestroRotation = &proto.Warlock_Rotation{ SpecSpell: proto.Warlock_Rotation_ChaosBolt, Curse: proto.Warlock_Rotation_Doom, Corruption: false, + DetonateSeed: true, } var defaultDestroOptions = &proto.Warlock_Options{