Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prot Paladin: Righteous fury + Hand of Reckoning #1064

Merged
merged 6 commits into from
Sep 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions proto/paladin.proto
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ message PaladinOptions {
bool IsUsingDivineStormStopAttack = 4;
bool IsUsingJudgementStopAttack = 5;
bool IsUsingCrusaderStrikeStopAttack = 6;
bool righteousFury = 8;
}

message RetributionPaladin {
Expand Down
25 changes: 12 additions & 13 deletions sim/core/health.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,10 +156,12 @@ func (character *Character) applyHealingModel(healingModel *proto.HealingModel)

healthMetrics := character.NewHealthMetrics(ActionID{OtherID: proto.OtherAction_OtherActionHealingModel})

// Dummy spell for healing callback
healingModelSpell := character.RegisterSpell(SpellConfig{
ActionID: ActionID{OtherID: proto.OtherAction_OtherActionHealingModel},
})

character.RegisterResetEffect(func(sim *Simulation) {
// Hack since we don't have OnHealingReceived aura handlers yet.
//ardentDefenderAura := character.GetAura("Ardent Defender")
willOfTheNecropolisAura := character.GetAura("Will of The Necropolis")

// Initialize randomized cadence model
timeToNextHeal := DurationFromSeconds(0.0)
Expand All @@ -171,18 +173,15 @@ func (character *Character) applyHealingModel(healingModel *proto.HealingModel)
pa.OnAction = func(sim *Simulation) {
// Use modeled HPS to scale heal per tick based on random cadence
healPerTick = healingModel.Hps * (float64(timeToNextHeal) / float64(time.Second))

totalHeal := healPerTick * character.PseudoStats.HealingTakenMultiplier
// Execute the heal
character.GainHealth(sim, healPerTick*character.PseudoStats.HealingTakenMultiplier, healthMetrics)
character.GainHealth(sim, totalHeal, healthMetrics)

// Might use this again in the future to track "absorb" metrics but currently disabled
//if ardentDefenderAura != nil && character.CurrentHealthPercent() >= 0.35 {
// ardentDefenderAura.Deactivate(sim)
//}

if willOfTheNecropolisAura != nil && character.CurrentHealthPercent() > 0.35 {
willOfTheNecropolisAura.Deactivate(sim)
}
// Callback that can be used by tank specs
result := healingModelSpell.NewResult(&character.Unit)
result.Damage = totalHeal
character.OnHealTaken(sim, healingModelSpell, result)
healingModelSpell.DisposeResult(result)

// Random roll for time to next heal. In the case where CadenceVariation exceeds CadenceSeconds, then
// CadenceSeconds is treated as the median, with two separate uniform distributions to the left and right
Expand Down
1 change: 1 addition & 0 deletions sim/paladin/paladin.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ func (paladin *Paladin) AddPartyBuffs(_ *proto.PartyBuffs) {
}

func (paladin *Paladin) Initialize() {
paladin.registerRighteousFury()
// Judgement and Seals
paladin.registerJudgement()

Expand Down
48 changes: 24 additions & 24 deletions sim/paladin/protection/TestProtection.results
Original file line number Diff line number Diff line change
Expand Up @@ -100,167 +100,167 @@ dps_results: {
key: "TestProtection-Lvl60-AllItems-EmeraldEncrustedBattleplate"
value: {
dps: 1449.05975
tps: 1702.73656
tps: 2731.34101
}
}
dps_results: {
key: "TestProtection-Lvl60-AllItems-Hero'sBrand-231328"
value: {
dps: 1818.00479
tps: 2095.2467
tps: 3835.15827
}
}
dps_results: {
key: "TestProtection-Lvl60-AllItems-Knight-Lieutenant'sImbuedPlate"
value: {
dps: 1449.12088
tps: 1703.41348
tps: 2732.01793
}
}
dps_results: {
key: "TestProtection-Lvl60-AllItems-Knight-Lieutenant'sLamellarPlate"
value: {
dps: 1531.46579
tps: 1798.72722
tps: 2882.3037
}
}
dps_results: {
key: "TestProtection-Lvl60-AllItems-LibramofDraconicDestruction-221457"
value: {
dps: 1880.89213
tps: 2160.81905
tps: 3960.92022
}
}
dps_results: {
key: "TestProtection-Lvl60-AllItems-ObsessedProphet'sPlate"
value: {
dps: 1652.06632
tps: 1934.08068
tps: 3491.09294
}
}
dps_results: {
key: "TestProtection-Lvl60-AllItems-SanctifiedOrb-20512"
value: {
dps: 1889.23354
tps: 2169.65251
tps: 3973.15985
}
}
dps_results: {
key: "TestProtection-Lvl60-AllItems-SoulforgeArmor"
value: {
dps: 1154.40755
tps: 1184.21575
tps: 1782.81781
}
}
dps_results: {
key: "TestProtection-Lvl60-AllItems-ZandalarFreethinker'sBelt-231330"
value: {
dps: 1567.29072
tps: 1794.85004
tps: 3303.06084
}
}
dps_results: {
key: "TestProtection-Lvl60-AllItems-ZandalarFreethinker'sBreastplate-231329"
value: {
dps: 1842.8433
tps: 2119.14986
tps: 3885.16513
}
}
dps_results: {
key: "TestProtection-Lvl60-Average-Default"
value: {
dps: 1864.69391
tps: 2142.97601
tps: 3925.90289
}
}
dps_results: {
key: "TestProtection-Lvl60-Settings-Dwarf-p4prot-P4 Prot-p4prot-FullBuffs-Phase 4 Consumes-LongMultiTarget"
value: {
dps: 1194.6808
tps: 2113.07899
tps: 3931.29347
}
}
dps_results: {
key: "TestProtection-Lvl60-Settings-Dwarf-p4prot-P4 Prot-p4prot-FullBuffs-Phase 4 Consumes-LongSingleTarget"
value: {
dps: 457.62392
tps: 686.68623
tps: 1481.71013
}
}
dps_results: {
key: "TestProtection-Lvl60-Settings-Dwarf-p4prot-P4 Prot-p4prot-FullBuffs-Phase 4 Consumes-ShortSingleTarget"
value: {
dps: 609.5157
tps: 901.48453
tps: 1938.74277
}
}
dps_results: {
key: "TestProtection-Lvl60-Settings-Dwarf-p4prot-P4 Prot-p4prot-NoBuffs-Phase 4 Consumes-LongMultiTarget"
value: {
dps: 409.47519
tps: 765.91203
tps: 1283.12503
}
}
dps_results: {
key: "TestProtection-Lvl60-Settings-Dwarf-p4prot-P4 Prot-p4prot-NoBuffs-Phase 4 Consumes-LongSingleTarget"
value: {
dps: 138.97267
tps: 204.40849
tps: 433.81686
}
}
dps_results: {
key: "TestProtection-Lvl60-Settings-Dwarf-p4prot-P4 Prot-p4prot-NoBuffs-Phase 4 Consumes-ShortSingleTarget"
value: {
dps: 281.31093
tps: 395.08441
tps: 839.09115
}
}
dps_results: {
key: "TestProtection-Lvl60-Settings-Human-p4prot-P4 Prot-p4prot-FullBuffs-Phase 4 Consumes-LongMultiTarget"
value: {
dps: 1198.66072
tps: 2120.65531
tps: 3950.54918
}
}
dps_results: {
key: "TestProtection-Lvl60-Settings-Human-p4prot-P4 Prot-p4prot-FullBuffs-Phase 4 Consumes-LongSingleTarget"
value: {
dps: 463.49911
tps: 694.25313
tps: 1496.79729
}
}
dps_results: {
key: "TestProtection-Lvl60-Settings-Human-p4prot-P4 Prot-p4prot-FullBuffs-Phase 4 Consumes-ShortSingleTarget"
value: {
dps: 612.37803
tps: 905.99254
tps: 1948.62369
}
}
dps_results: {
key: "TestProtection-Lvl60-Settings-Human-p4prot-P4 Prot-p4prot-NoBuffs-Phase 4 Consumes-LongMultiTarget"
value: {
dps: 404.46316
tps: 746.67678
tps: 1246.34729
}
}
dps_results: {
key: "TestProtection-Lvl60-Settings-Human-p4prot-P4 Prot-p4prot-NoBuffs-Phase 4 Consumes-LongSingleTarget"
value: {
dps: 149.02561
tps: 211.11276
tps: 448.28721
}
}
dps_results: {
key: "TestProtection-Lvl60-Settings-Human-p4prot-P4 Prot-p4prot-NoBuffs-Phase 4 Consumes-ShortSingleTarget"
value: {
dps: 282.58463
tps: 397.02423
tps: 843.34946
}
}
dps_results: {
key: "TestProtection-Lvl60-SwitchInFrontOfTarget-Default"
value: {
dps: 1569.54059
tps: 1845.34584
tps: 3378.44378
}
}
2 changes: 2 additions & 0 deletions sim/paladin/protection/protection.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ func NewProtectionPaladin(character *core.Character, options *proto.Player) *Pro
prot := &ProtectionPaladin{
Paladin: pal,
primarySeal: protOptions.PrimarySeal,
righteousFury: protOptions.RighteousFury,
IsUsingDivineStormStopAttack: protOptions.IsUsingDivineStormStopAttack,
IsUsingJudgementStopAttack: protOptions.IsUsingJudgementStopAttack,
IsUsingCrusaderStrikeStopAttack: protOptions.IsUsingCrusaderStrikeStopAttack,
Expand All @@ -48,6 +49,7 @@ type ProtectionPaladin struct {
*paladin.Paladin

primarySeal proto.PaladinSeal
righteousFury bool
IsUsingDivineStormStopAttack bool
IsUsingJudgementStopAttack bool
IsUsingCrusaderStrikeStopAttack bool
Expand Down
9 changes: 6 additions & 3 deletions sim/paladin/protection/protection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,15 +99,18 @@ var PlayerOptionsSealofRighteousness = &proto.Player_ProtectionPaladin{
}

var optionsSealOfCommand = &proto.PaladinOptions{
PrimarySeal: proto.PaladinSeal_Command,
PrimarySeal: proto.PaladinSeal_Command,
RighteousFury: true,
}

var optionsSealOfMartyrdom = &proto.PaladinOptions{
PrimarySeal: proto.PaladinSeal_Martyrdom,
PrimarySeal: proto.PaladinSeal_Martyrdom,
RighteousFury: true,
}

var optionsSealOfRighteousness = &proto.PaladinOptions{
PrimarySeal: proto.PaladinSeal_Righteousness,
PrimarySeal: proto.PaladinSeal_Righteousness,
RighteousFury: true,
}

var ItemFilters = core.ItemFilter{
Expand Down
58 changes: 58 additions & 0 deletions sim/paladin/righteous_fury.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package paladin

import (
"github.com/wowsims/sod/sim/core"
"github.com/wowsims/sod/sim/core/proto"
)

func (paladin *Paladin) registerRighteousFury() {
if !paladin.Options.RighteousFury {
return
}
horRune := proto.PaladinRune_RuneHandsHandOfReckoning
hasHoR := paladin.hasRune(horRune)

actionID := core.ActionID{SpellID: core.TernaryInt32(hasHoR, int32(horRune), 25780)}

rfThreatMultiplier := 0.6 + core.TernaryFloat64(hasHoR, 0.2, 0.0)
// Improved Righteous Fury is multiplicative.
rfThreatMultiplier *= 1.0 + []float64{0.0, 0.16, 0.33, 0.5}[paladin.Talents.ImprovedRighteousFury]

paladin.OnSpellRegistered(func(spell *core.Spell) {
if spell.SpellSchool.Matches(core.SpellSchoolHoly) {
spell.ThreatMultiplier *= 1.0 + rfThreatMultiplier
}
Comment on lines +21 to +24
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't the bonus threat only apply if RF is active?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or are we just assuming it's always active for prot I guess?

Copy link
Contributor Author

@wsphillips wsphillips Sep 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function starts with a check to see if RF was enabled via the button in the UI spec options. Since Rets won't have the option on their UI, the OnSpellRegistered won't get ever get evaluated for them (it will default to false in the options struct). For prot, it also works correctly if you turn it off in the UI.

Also note: In game, the passives for Hand of Reckoning are only active when you have Righteous fury on.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An alternative here is I could have the OnSpellRegistered just append each spell to an array (e.g. paladin.holySpells) and have the aura's OnGain and OnExpire apply/remove the threat multiplier on the list of spells. The disadvantage to that is you're doing a multiply/divide for each spell at the beginning and end of each iteration versus doing it once during the build phase. Putting the threat mod onto the aura itself would only be advantageous if some other future item or aura for paladins modified threat for specific Holy spells, in which case I think we can cross that bridge if we ever get there.

})

rfAura := core.MakePermanent(&core.Aura{Label: "Righteous Fury", ActionID: actionID})

// Passive effects granted by Hand of Reckoning rune; only active if Righteous Fury is on.
if hasHoR {

// Damage which takes you below 35% health is reduced by 20% (DR component of WotLK's Ardent Defender)
rfDamageReduction := 0.2

handler := func(sim *core.Simulation, spell *core.Spell, result *core.SpellResult) {
incomingDamage := result.Damage
if (paladin.CurrentHealth()-incomingDamage)/paladin.MaxHealth() <= 0.35 {
result.Damage -= (paladin.MaxHealth()*0.35 - (paladin.CurrentHealth() - incomingDamage)) * rfDamageReduction
if sim.Log != nil {
paladin.Log(sim, "Righteous Fury absorbs %d damage", int32(incomingDamage-result.Damage))
}
}
}

paladin.AddDynamicDamageTakenModifier(handler)

// Gives you mana when healed by other friendly targets' spells equal to 25% of the amount healed.
horManaMetrics := paladin.NewManaMetrics(actionID)

rfAura.OnHealTaken = func(aura *core.Aura, sim *core.Simulation, spell *core.Spell, result *core.SpellResult) {
if spell.IsOtherAction(proto.OtherAction_OtherActionHealingModel) {
manaGained := result.Damage * 0.25
paladin.AddMana(sim, manaGained, horManaMetrics)
}
}
}
paladin.RegisterAura(*rfAura)
}
14 changes: 11 additions & 3 deletions ui/protection_paladin/inputs.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import * as InputHelpers from '../core/components/input_helpers.js';
import { Player } from '../core/player.js';
import { Spec } from '../core/proto/common.js';
import { PaladinSeal, PaladinAura } from '../core/proto/paladin.js';
import { ItemSlot, Spec } from '../core/proto/common.js';
import { PaladinRune, PaladinSeal, PaladinAura } from '../core/proto/paladin.js';
import { ActionId } from '../core/proto_utils/action_id.js';
import { TypedEvent } from '../core/typed_event.js';

// Configuration for spec-specific UI elements on the settings tab.
// These don't need to be in a separate file but it keeps things cleaner.

Expand All @@ -22,6 +21,15 @@ export const AuraSelection = InputHelpers.makeSpecOptionsEnumIconInput<Spec.Spec
],
});

export const RighteousFuryToggle = InputHelpers.makeSpecOptionsBooleanIconInput<Spec.SpecProtectionPaladin>({
fieldName: 'righteousFury',
actionId: (player: Player<Spec.SpecProtectionPaladin>) =>
player.hasRune(ItemSlot.ItemSlotHands, PaladinRune.RuneHandsHandOfReckoning) ?
ActionId.fromSpellId(407627) : ActionId.fromSpellId(25780),
changeEmitter: (player: Player<Spec.SpecProtectionPaladin>) =>
TypedEvent.onAny([player.gearChangeEmitter, player.specOptionsChangeEmitter]),
});

// The below is used in the custom APL action "Cast Primary Seal".
// Only shows SoC if it's talented.
export const PrimarySealSelection = InputHelpers.makeSpecOptionsEnumIconInput<Spec.SpecProtectionPaladin, PaladinSeal>({
Expand Down
1 change: 1 addition & 0 deletions ui/protection_paladin/presets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export const DefaultTalents = TalentPresets[Phase.Phase4][0];
export const DefaultOptions = ProtectionPaladinOptions.create({
aura: PaladinAura.SanctityAura,
primarySeal: PaladinSeal.Martyrdom,
righteousFury: true,
});

export const DefaultConsumes = Consumes.create({
Expand Down
2 changes: 1 addition & 1 deletion ui/protection_paladin/sim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ const SPEC_CONFIG = registerSpecConfig(Spec.SpecProtectionPaladin, {
},

// IconInputs to include in the 'Player' section on the settings tab.
playerIconInputs: [ProtectionPaladinInputs.PrimarySealSelection, ProtectionPaladinInputs.AuraSelection],
playerIconInputs: [ProtectionPaladinInputs.PrimarySealSelection, ProtectionPaladinInputs.RighteousFuryToggle, ProtectionPaladinInputs.AuraSelection],
// Buff and Debuff inputs to include/exclude, overriding the EP-based defaults.
includeBuffDebuffInputs: [BuffDebuffInputs.SpellScorchDebuff],
excludeBuffDebuffInputs: [],
Expand Down
Loading