Skip to content

Commit

Permalink
Merge pull request #1330 from wowsims/feral
Browse files Browse the repository at this point in the history
Refactor equip scaling code for higher performance and item swap compatibility
  • Loading branch information
NerdEgghead authored Jan 29, 2025
2 parents 72afc4a + 3e21d6e commit 306b6b4
Show file tree
Hide file tree
Showing 5 changed files with 61 additions and 45 deletions.
88 changes: 48 additions & 40 deletions sim/core/character.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,6 @@ type Character struct {
// Base stats for this Character.
baseStats stats.Stats

// Handles scaling that only affects stats from items
itemStatMultipliers stats.Stats

// Bonus stats for this Character, specified in the UI and/or EP
// calculator
bonusStats stats.Stats
Expand All @@ -73,9 +70,8 @@ type Character struct {
glyphs [9]int32
PrimaryTalentTree uint8

// Used to track if we need to separately apply multipliers, because
// equipment was already applied
equipStatsApplied bool
// Used for effects like "Increased Armor Value from Items"
*EquipScalingManager

// Provides major cooldown management behavior.
majorCooldownManager
Expand Down Expand Up @@ -163,9 +159,6 @@ func NewCharacter(party *Party, partyIndex int, player *proto.Player) Character

character.AddStats(character.baseStats)
character.addUniversalStatDependencies()
for i := range character.itemStatMultipliers {
character.itemStatMultipliers[i] = 1
}

if player.BonusStats != nil {
if player.BonusStats.Stats != nil {
Expand Down Expand Up @@ -193,66 +186,81 @@ func NewCharacter(party *Party, partyIndex int, player *proto.Player) Character
character.enableItemSwap(player.ItemSwap, character.DefaultMeleeCritMultiplier(), character.DefaultMeleeCritMultiplier(), 0)
}

character.EquipScalingManager = character.NewEquipScalingManager()

return character
}

func (character *Character) applyEquipScaling(stat stats.Stat, multiplier float64) float64 {
var oldValue = character.EquipStats()[stat]
character.itemStatMultipliers[stat] *= multiplier
var newValue = character.EquipStats()[stat]
return newValue - oldValue
type EquipScalingManager struct {
itemStatMultipliers map[stats.Stat]float64
cachedEquipStats stats.Stats
equipStatsApplied bool
equipCacheValid bool
}

func (character *Character) NewEquipScalingManager() *EquipScalingManager {
return &EquipScalingManager{
itemStatMultipliers: make(map[stats.Stat]float64),
cachedEquipStats: character.Equipment.Stats().Add(character.bonusStats),
equipCacheValid: true,
}
}

func (character *Character) AddDynamicEquipStats(sim *Simulation, equipStats stats.Stats) {
character.AddStatsDynamic(sim, equipStats.ApplyMultipliers(character.itemStatMultipliers))
character.equipCacheValid = false
}

func (character *Character) applyEquipScalingInternal(stat stats.Stat, multiplier float64) float64 {
character.updateCachedEquipStats()
oldMultiplier, exists := character.itemStatMultipliers[stat]

if !exists {
oldMultiplier = 1.0
}

newMultiplier := oldMultiplier * multiplier
character.itemStatMultipliers[stat] = newMultiplier

return character.cachedEquipStats[stat] * (newMultiplier - oldMultiplier)
}

func (character *Character) ApplyEquipScaling(stat stats.Stat, multiplier float64) {
var statDiff stats.Stats
statDiff[stat] = character.applyEquipScaling(stat, multiplier)
statDiff := character.applyEquipScalingInternal(stat, multiplier)
// Equipment stats already applied, so need to manually at the bonus to
// the character now to ensure correct values
if character.equipStatsApplied {
character.AddStats(statDiff)
character.AddStat(stat, statDiff)
}
}

func (character *Character) ApplyDynamicEquipScaling(sim *Simulation, stat stats.Stat, multiplier float64) {
statDiff := character.applyEquipScaling(stat, multiplier)
character.AddStatDynamic(sim, stat, statDiff)
}

func (character *Character) ApplyBuildPhaseEquipScaling(sim *Simulation, stat stats.Stat, multiplier float64) {
if character.Env.MeasuringStats && (character.Env.State != Finalized) {
character.ApplyEquipScaling(stat, multiplier)
} else {
character.ApplyDynamicEquipScaling(sim, stat, multiplier)
statDiff := character.applyEquipScalingInternal(stat, multiplier)
character.AddStatDynamic(sim, stat, statDiff)
}
}

func (character *Character) RemoveEquipScaling(stat stats.Stat, multiplier float64) {
var statDiff stats.Stats
statDiff[stat] = character.applyEquipScaling(stat, 1/multiplier)
// Equipment stats already applied, so need to manually at the bonus to
// the character now to ensure correct values
if character.equipStatsApplied {
character.AddStats(statDiff)
}
character.ApplyEquipScaling(stat, 1/multiplier)
}

func (character *Character) RemoveDynamicEquipScaling(sim *Simulation, stat stats.Stat, multiplier float64) {
statDiff := character.applyEquipScaling(stat, 1/multiplier)
character.AddStatDynamic(sim, stat, statDiff)
character.ApplyDynamicEquipScaling(sim, stat, 1/multiplier)
}

func (character *Character) RemoveBuildPhaseEquipScaling(sim *Simulation, stat stats.Stat, multiplier float64) {
if character.Env.MeasuringStats && (character.Env.State != Finalized) {
character.RemoveEquipScaling(stat, multiplier)
} else {
character.RemoveDynamicEquipScaling(sim, stat, multiplier)
func (character *Character) updateCachedEquipStats() {
if !character.equipCacheValid {
character.cachedEquipStats = character.Equipment.Stats().Add(character.bonusStats)
character.equipCacheValid = true
}
}

func (character *Character) EquipStats() stats.Stats {
var baseEquipStats = character.Equipment.Stats()
var bonusEquipStats = baseEquipStats.Add(character.bonusStats)
return bonusEquipStats.DotProduct(character.itemStatMultipliers)
character.updateCachedEquipStats()
return character.cachedEquipStats.ApplyMultipliers(character.itemStatMultipliers)
}

func (character *Character) applyEquipment() {
Expand Down
2 changes: 1 addition & 1 deletion sim/core/item_swaps.go
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ func (swap *ItemSwap) SwapItems(sim *Simulation, swapSet proto.APLActionItemSwap
if sim.Log != nil {
sim.Log("Item Swap - Stats Change: %v", statsToSwap.FlatString())
}
character.AddStatsDynamic(sim, statsToSwap)
character.AddDynamicEquipStats(sim, statsToSwap)

if !isPrepull && !isReset && weaponSlotSwapped {
character.AutoAttacks.StopMeleeUntil(sim, sim.CurrentTime, false)
Expand Down
8 changes: 8 additions & 0 deletions sim/core/stats/stats.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,14 @@ func (stats Stats) DotProduct(other Stats) Stats {
return stats
}

// Higher performance variant of the above.
func (stats Stats) ApplyMultipliers(multipliers map[Stat]float64) Stats {
for k, v := range multipliers {
stats[k] *= v
}
return stats
}

func (stats Stats) Equals(other Stats) bool {
return stats == other
}
Expand Down
4 changes: 2 additions & 2 deletions sim/druid/forms.go
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ func (druid *Druid) RegisterBearFormAura() {
druid.PseudoStats.SpiritRegenMultiplier *= AnimalSpiritRegenSuppression

druid.AddStatsDynamic(sim, statBonus)
druid.ApplyBuildPhaseEquipScaling(sim, stats.Armor, baseBearArmorMulti)
druid.ApplyDynamicEquipScaling(sim, stats.Armor, baseBearArmorMulti)
druid.EnableBuildPhaseStatDep(sim, agiApDep)

// Preserve fraction of max health when shifting
Expand All @@ -288,7 +288,7 @@ func (druid *Druid) RegisterBearFormAura() {
druid.PseudoStats.SpiritRegenMultiplier /= AnimalSpiritRegenSuppression

druid.AddStatsDynamic(sim, statBonus.Invert())
druid.RemoveBuildPhaseEquipScaling(sim, stats.Armor, baseBearArmorMulti)
druid.RemoveDynamicEquipScaling(sim, stats.Armor, baseBearArmorMulti)
druid.DisableBuildPhaseStatDep(sim, agiApDep)

healthFrac := druid.CurrentHealth() / druid.MaxHealth()
Expand Down
4 changes: 2 additions & 2 deletions sim/druid/talents.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@ func (druid *Druid) applyThickHide() {
thickHideBearMulti := 1.0 + 0.26*float64(numPoints)

druid.BearFormAura.ApplyOnGain(func(_ *core.Aura, sim *core.Simulation) {
druid.ApplyBuildPhaseEquipScaling(sim, stats.Armor, thickHideBearMulti)
druid.ApplyDynamicEquipScaling(sim, stats.Armor, thickHideBearMulti)
})

druid.BearFormAura.ApplyOnExpire(func(_ *core.Aura, sim *core.Simulation) {
druid.RemoveBuildPhaseEquipScaling(sim, stats.Armor, thickHideBearMulti)
druid.RemoveDynamicEquipScaling(sim, stats.Armor, thickHideBearMulti)
})

druid.ApplyEquipScaling(stats.Armor, thickHideBearMulti)
Expand Down

0 comments on commit 306b6b4

Please sign in to comment.