Skip to content

Commit

Permalink
Merge pull request #1064 from wsphillips/righteous_fury
Browse files Browse the repository at this point in the history
Prot Paladin: Righteous fury + Hand of Reckoning
  • Loading branch information
kayla-glick authored Sep 21, 2024
2 parents 8b57a5a + 0f44583 commit 6765e09
Show file tree
Hide file tree
Showing 10 changed files with 117 additions and 44 deletions.
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
}
})

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

0 comments on commit 6765e09

Please sign in to comment.