diff --git a/proto/apl.proto b/proto/apl.proto index 760fe3e4ac..9a567b3951 100644 --- a/proto/apl.proto +++ b/proto/apl.proto @@ -124,7 +124,7 @@ message APLValue { APLValueCurrentRunicPower current_runic_power = 25; APLValueCurrentSolarEnergy current_solar_energy = 68; APLValueCurrentLunarEnergy current_lunar_energy = 69; - APLValueCurrentHolyPower current_holy_power = 75; + APLValueCurrentGenericResource current_generic_resource = 75; APLValueMaxComboPoints max_combo_points = 94; APLValueMaxEnergy max_energy = 88; APLValueMaxFocus max_focus = 89; @@ -470,7 +470,7 @@ message APLValueCurrentComboPoints {} message APLValueCurrentRunicPower {} message APLValueCurrentSolarEnergy {} message APLValueCurrentLunarEnergy {} -message APLValueCurrentHolyPower {} +message APLValueCurrentGenericResource {} message APLValueMaxComboPoints {} message APLValueMaxEnergy {} message APLValueMaxFocus {} diff --git a/proto/common.proto b/proto/common.proto index e4cddb99f6..0d7f300c17 100644 --- a/proto/common.proto +++ b/proto/common.proto @@ -872,5 +872,4 @@ enum LogLevel { Error = 2; Undefined = -1; -} - +} \ No newline at end of file diff --git a/proto/spell.proto b/proto/spell.proto index a53ae8e6e1..b5642ff429 100644 --- a/proto/spell.proto +++ b/proto/spell.proto @@ -40,6 +40,16 @@ enum ResourceType { ResourceTypeDeathRune = 11; ResourceTypeSolarEnergy = 12; ResourceTypeLunarEnergy = 13; - ResourceTypeHolyPower = 14; - ResourceTypeChi = 15; + ResourceTypeChi = 14; + ResourceTypeGenericResource = 15; } + +enum SecondaryResourceType { + SecondaryResourceTypeNone = 0; + SecondaryResourceTypeArcaneCharges = 36032; + SecondaryResourceTypeShadowOrbs = 95740; + SecondaryResourceTypeDemonicFury = 104315; + SecondaryResourceTypeBurningEmbers = 108647; + SecondaryResourceTypeSoulShards = 117198; + SecondaryResourceTypeHolyPower = 138248; +} \ No newline at end of file diff --git a/sim/core/proto_test.go b/sim/core/_proto_test.go similarity index 100% rename from sim/core/proto_test.go rename to sim/core/_proto_test.go diff --git a/sim/core/apl_value.go b/sim/core/apl_value.go index 106e2a7c42..cc9e904694 100644 --- a/sim/core/apl_value.go +++ b/sim/core/apl_value.go @@ -138,6 +138,8 @@ func (rot *APLRotation) newAPLValue(config *proto.APLValue) APLValue { value = rot.newValueEnergyTimeToTarget(config.GetEnergyTimeToTarget(), config.Uuid) case *proto.APLValue_FocusTimeToTarget: value = rot.newValueFocusTimeToTarget(config.GetFocusTimeToTarget(), config.Uuid) + case *proto.APLValue_CurrentGenericResource: + value = rot.newValueCurrentGenericResource(config.GetCurrentGenericResource(), config.Uuid) // Resources Runes case *proto.APLValue_CurrentRuneCount: diff --git a/sim/core/apl_values_resources.go b/sim/core/apl_values_resources.go index be4483d793..bef219a6ef 100644 --- a/sim/core/apl_values_resources.go +++ b/sim/core/apl_values_resources.go @@ -462,3 +462,28 @@ func (value *APLValueMaxRunicPower) GetInt(sim *Simulation) int32 { func (value *APLValueMaxRunicPower) String() string { return fmt.Sprintf("Max Runic Power(%d)", value.maxRunicPower) } + +type APLValueCurrentGenericResource struct { + DefaultAPLValueImpl + unit *Unit +} + +func (rot *APLRotation) newValueCurrentGenericResource(_ *proto.APLValueCurrentGenericResource, uuid *proto.UUID) APLValue { + unit := rot.unit + if unit.secondaryResourceBar == nil { + rot.ValidationMessageByUUID(uuid, proto.LogLevel_Warning, "%s does not have secondary resource", unit.Label) + return nil + } + return &APLValueCurrentGenericResource{ + unit: unit, + } +} +func (value *APLValueCurrentGenericResource) Type() proto.APLValueType { + return proto.APLValueType_ValueTypeInt +} +func (value *APLValueCurrentGenericResource) GetInt(sim *Simulation) int32 { + return value.unit.secondaryResourceBar.Value() +} +func (value *APLValueCurrentGenericResource) String() string { + return "Current {GENERIC_RESOURCE}" +} diff --git a/sim/core/metrics_aggregator.go b/sim/core/metrics_aggregator.go index 70fcd3a97f..39dddbd53a 100644 --- a/sim/core/metrics_aggregator.go +++ b/sim/core/metrics_aggregator.go @@ -333,6 +333,10 @@ func (unit *Unit) NewFocusMetrics(actionID ActionID) *ResourceMetrics { return unit.Metrics.NewResourceMetrics(actionID, proto.ResourceType_ResourceTypeFocus) } +func (unit *Unit) NewGenericMetric(actionID ActionID) *ResourceMetrics { + return unit.Metrics.NewResourceMetrics(actionID, proto.ResourceType_ResourceTypeGenericResource) +} + // Adds the results of a spell to the character metrics. func (unitMetrics *UnitMetrics) addSpellMetrics(spell *Spell, actionID ActionID, spellMetrics []SpellMetrics) { actionMetrics, ok := unitMetrics.actions[actionID] diff --git a/sim/core/secondary_resource_bar.go b/sim/core/secondary_resource_bar.go new file mode 100644 index 0000000000..f3358af37b --- /dev/null +++ b/sim/core/secondary_resource_bar.go @@ -0,0 +1,201 @@ +// Implements a generic resource bar that can be used to implement secondary resources +// TODO: Check whether pre-pull OOC resource loss needs to be supported for DemonicFury +package core + +import ( + "github.com/wowsims/mop/sim/core/proto" +) + +type OnGainCallback func(gain int32, realGain int32) +type OnSpendCallback func(amount int32) + +type SecondaryResourceBar interface { + CanSpend(limit int32) bool // Check whether the current resource is available or not + Spend(amount int32, action ActionID, sim *Simulation) // Spend the specified amount of resource + SpendUpTo(limit int32, action ActionID, sim *Simulation) int32 // Spends as much resource as possible up to the speciefied limit; Returns the amount of resource spent + Gain(amount int32, action ActionID, sim *Simulation) // Gain the amount specified from the action + Reset(sim *Simulation) // Resets the current resource bar + Value() int32 // Returns the current amount of resource + RegisterOnGain(callback OnGainCallback) // Registers a callback that will be called. Gain = amount gained, realGain = actual amount gained due to caps + RegisterOnSpend(callback OnSpendCallback) // Registers a callback that will be called when the resource was spend +} + +type SecondaryResourceConfig struct { + Type proto.SecondaryResourceType // The type of resource the bar tracks + Max int32 // The maximum amount the bar tracks + Default int32 // The default value this bar should be initialized with +} + +// Default implementation of SecondaryResourceBar +// Use RegisterSecondaryResourceBar to intantiate the resource bar +type DefaultSecondaryResourceBarImpl struct { + config SecondaryResourceConfig + value int32 + unit *Unit + metrics map[ActionID]*ResourceMetrics + onGain []OnGainCallback + onSpend []OnSpendCallback +} + +// CanSpend implements SecondaryResourceBar. +func (bar *DefaultSecondaryResourceBarImpl) CanSpend(limit int32) bool { + return bar.value >= limit +} + +// Gain implements SecondaryResourceBar. +func (bar *DefaultSecondaryResourceBarImpl) Gain(amount int32, action ActionID, sim *Simulation) { + if amount < 0 { + panic("Can not gain negative amount") + } + + oldValue := bar.value + bar.value = min(bar.value+amount, bar.config.Max) + amountGained := bar.value - oldValue + metrics := bar.GetMetric(action) + metrics.AddEvent(float64(amount), float64(amountGained)) + if sim.Log != nil { + bar.unit.Log( + sim, + "Gained %d %s from %s (%d --> %d) of %d total.", + amountGained, + proto.SecondaryResourceType_name[int32(bar.config.Type)], + action, + oldValue, + bar.value, + bar.config.Max, + ) + } + + bar.invokeOnGain(amount, amountGained) +} + +// Reset implements SecondaryResourceBar. +func (bar *DefaultSecondaryResourceBarImpl) Reset(sim *Simulation) { + bar.value = 0 + if bar.config.Default > 0 { + bar.Gain(bar.config.Default, ActionID{SpellID: int32(bar.config.Type)}, sim) + } +} + +// Spend implements SecondaryResourceBar. +func (bar *DefaultSecondaryResourceBarImpl) Spend(amount int32, action ActionID, sim *Simulation) { + if amount > bar.value { + panic("Trying to spend more resource than is available.") + } + + if amount < 0 { + panic("Trying to spend negative amount.") + } + + metrics := bar.GetMetric(action) + if sim.Log != nil { + bar.unit.Log( + sim, + "Spent %d %s from %s (%d --> %d) of %d total.", + amount, + proto.SecondaryResourceType_name[int32(bar.config.Type)], + metrics.ActionID, + bar.value, + bar.value-amount, + bar.config.Max, + ) + } + + metrics.AddEvent(float64(-amount), float64(-amount)) + bar.invokeOnSpend(amount) + bar.value -= amount +} + +// SpendUpTo implements SecondaryResourceBar. +func (bar *DefaultSecondaryResourceBarImpl) SpendUpTo(limit int32, action ActionID, sim *Simulation) int32 { + if bar.value > limit { + bar.Spend(limit, action, sim) + return limit + } + + bar.Spend(bar.value, action, sim) + return bar.value +} + +// Value implements SecondaryResourceBar. +func (bar *DefaultSecondaryResourceBarImpl) Value() int32 { + return bar.value +} + +func (bar *DefaultSecondaryResourceBarImpl) GetMetric(action ActionID) *ResourceMetrics { + metric, ok := bar.metrics[action] + if !ok { + metric = bar.unit.NewGenericMetric(action) + bar.metrics[action] = metric + } + + return metric +} + +func (bar *DefaultSecondaryResourceBarImpl) RegisterOnGain(callback OnGainCallback) { + if callback == nil { + panic("Can not register nil callback") + } + + bar.onGain = append(bar.onGain, callback) +} + +func (bar *DefaultSecondaryResourceBarImpl) RegisterOnSpend(callback OnSpendCallback) { + if callback == nil { + panic("Can not register nil callback") + } + + bar.onSpend = append(bar.onSpend, callback) +} + +func (bar *DefaultSecondaryResourceBarImpl) invokeOnGain(gain int32, realGain int32) { + for _, callback := range bar.onGain { + callback(gain, realGain) + } +} + +func (bar *DefaultSecondaryResourceBarImpl) invokeOnSpend(amount int32) { + for _, callback := range bar.onSpend { + callback(amount) + } +} + +func (unit *Unit) NewDefaultSecondaryResourceBar(config SecondaryResourceConfig) *DefaultSecondaryResourceBarImpl { + if config.Type <= 0 { + panic("Invalid SecondaryResourceType given.") + } + + if config.Max <= 0 { + panic("Invalid maximum resource value given.") + } + + if config.Default < 0 || config.Default > config.Max { + panic("Invalid default value given for resource bar") + } + + return &DefaultSecondaryResourceBarImpl{ + config: config, + unit: unit, + metrics: make(map[ActionID]*ResourceMetrics), + onGain: []OnGainCallback{}, + onSpend: []OnSpendCallback{}, + } +} + +func (unit *Unit) RegisterSecondaryResourceBar(config SecondaryResourceBar) { + if unit.secondaryResourceBar != nil { + panic("A secondary resource bar has already been registered.") + } + + unit.secondaryResourceBar = config +} + +func (unit *Unit) RegisterNewDefaultSecondaryResourceBar(config SecondaryResourceConfig) SecondaryResourceBar { + bar := unit.NewDefaultSecondaryResourceBar(config) + unit.RegisterSecondaryResourceBar(bar) + return bar +} + +func (unit *Unit) GetSecondaryResourceBar() SecondaryResourceBar { + return unit.secondaryResourceBar +} diff --git a/sim/core/unit.go b/sim/core/unit.go index bbeddb773e..089743b574 100644 --- a/sim/core/unit.go +++ b/sim/core/unit.go @@ -119,6 +119,8 @@ type Unit struct { focusBar runicPowerBar + secondaryResourceBar SecondaryResourceBar + // All spells that can be cast by this unit. Spellbook []*Spell spellRegistrationHandlers []SpellRegisteredHandler @@ -585,6 +587,10 @@ func (unit *Unit) reset(sim *Simulation, _ Agent) { unit.rageBar.reset(sim) unit.runicPowerBar.reset(sim) + if unit.secondaryResourceBar != nil { + unit.secondaryResourceBar.Reset(sim) + } + unit.AutoAttacks.reset(sim) if unit.Rotation != nil { diff --git a/sim/paladin/apl_values.go b/sim/paladin/apl_values.go deleted file mode 100644 index 6a2b5ce2f3..0000000000 --- a/sim/paladin/apl_values.go +++ /dev/null @@ -1,39 +0,0 @@ -package paladin - -import ( - "github.com/wowsims/mop/sim/core" - "github.com/wowsims/mop/sim/core/proto" -) - -func (paladin *Paladin) NewAPLValue(rot *core.APLRotation, config *proto.APLValue) core.APLValue { - switch config.Value.(type) { - case *proto.APLValue_CurrentHolyPower: - return paladin.newValueCurrentHolyPower(config.GetCurrentHolyPower(), config.Uuid) - default: - return nil - } -} - -type APLValueCurrentHolyPower struct { - core.DefaultAPLValueImpl - paladin *Paladin -} - -func (paladin *Paladin) newValueCurrentHolyPower(_ *proto.APLValueCurrentHolyPower, uuid *proto.UUID) core.APLValue { - if !paladin.HasHolyPowerBar() { - return nil - } - - return &APLValueCurrentHolyPower{ - paladin: paladin, - } -} -func (value *APLValueCurrentHolyPower) Type() proto.APLValueType { - return proto.APLValueType_ValueTypeInt -} -func (value *APLValueCurrentHolyPower) GetInt(sim *core.Simulation) int32 { - return value.paladin.CurrentHolyPower() -} -func (value *APLValueCurrentHolyPower) String() string { - return "Current Holy Power" -} diff --git a/sim/paladin/crusader_strike.go b/sim/paladin/crusader_strike.go index e08f7b1cd8..dc7e980d07 100644 --- a/sim/paladin/crusader_strike.go +++ b/sim/paladin/crusader_strike.go @@ -6,8 +6,6 @@ import ( func (paladin *Paladin) registerCrusaderStrike() { actionId := core.ActionID{SpellID: 35395} - hpMetrics := paladin.NewHolyPowerMetrics(actionId) - paladin.CrusaderStrike = paladin.RegisterSpell(core.SpellConfig{ ActionID: actionId, SpellSchool: core.SpellSchoolPhysical, @@ -41,7 +39,7 @@ func (paladin *Paladin) registerCrusaderStrike() { if result.Landed() { holyPowerGain := core.TernaryInt32(paladin.ZealotryAura.IsActive(), 3, 1) - paladin.GainHolyPower(sim, holyPowerGain, hpMetrics) + paladin.HolyPower.Gain(holyPowerGain, actionId, sim) } spell.DealOutcome(sim, result) diff --git a/sim/paladin/holy_power.go b/sim/paladin/holy_power.go deleted file mode 100644 index ead4050cc7..0000000000 --- a/sim/paladin/holy_power.go +++ /dev/null @@ -1,82 +0,0 @@ -package paladin - -import ( - "github.com/wowsims/mop/sim/core" - "github.com/wowsims/mop/sim/core/proto" -) - -type HolyPowerBar struct { - paladin *Paladin - - holyPower int32 -} - -// CurrentHolyPower returns the actual amount of holy power the paladin has, not counting the Divine Purpose proc. -func (paladin *Paladin) CurrentHolyPower() int32 { - return paladin.holyPower -} - -// GetHolyPowerValue returns the amount of holy power used for calculating the damage done by Templar's Verdict and duration of Inquisition. -func (paladin *Paladin) GetHolyPowerValue() int32 { - if paladin.DivinePurposeAura.IsActive() { - return 3 - } - - return paladin.CurrentHolyPower() -} - -func (paladin *Paladin) initializeHolyPowerBar() { - paladin.HolyPowerBar = HolyPowerBar{ - paladin: paladin, - holyPower: paladin.StartingHolyPower, - } -} - -func (pb *HolyPowerBar) Reset() { - if pb.paladin == nil { - return - } - - pb.holyPower = pb.paladin.StartingHolyPower -} - -func (paladin *Paladin) HasHolyPowerBar() bool { - return paladin.HolyPowerBar.paladin != nil -} - -func (pb *HolyPowerBar) GainHolyPower(sim *core.Simulation, amountToAdd int32, metrics *core.ResourceMetrics) { - if pb.paladin == nil { - return - } - - newHolyPower := min(pb.holyPower+amountToAdd, 3) - metrics.AddEvent(float64(amountToAdd), float64(newHolyPower-pb.holyPower)) - - if sim.Log != nil { - pb.paladin.Log(sim, "Gained %d holy power from %s (%d --> %d) of %0.0f total.", amountToAdd, metrics.ActionID, pb.holyPower, newHolyPower, 3.0) - } - - pb.holyPower = newHolyPower -} - -func (pb *HolyPowerBar) SpendHolyPower(sim *core.Simulation, metrics *core.ResourceMetrics) { - if pb.paladin == nil { - return - } - - if pb.paladin.DivinePurposeAura.IsActive() { - // Aura deactivation handled in talents_retribution.go:applyDivinePurpose() - return - } - - if sim.Log != nil { - pb.paladin.Log(sim, "Spent %d holy power from %s (%d --> %d) of %0.0f total.", pb.holyPower, metrics.ActionID, pb.holyPower, 0, 3.0) - } - - metrics.AddEvent(float64(-pb.holyPower), float64(-pb.holyPower)) - pb.holyPower = 0 -} - -func (unit *Paladin) NewHolyPowerMetrics(actionID core.ActionID) *core.ResourceMetrics { - return unit.Metrics.NewResourceMetrics(actionID, proto.ResourceType_ResourceTypeHolyPower) -} diff --git a/sim/paladin/holy_power_bar.go b/sim/paladin/holy_power_bar.go new file mode 100644 index 0000000000..8dd457ba1b --- /dev/null +++ b/sim/paladin/holy_power_bar.go @@ -0,0 +1,35 @@ +package paladin + +import "github.com/wowsims/mop/sim/core" + +type HolyPowerBar struct { + *core.DefaultSecondaryResourceBarImpl + paladin *Paladin +} + +// Spend implements core.SecondaryResourceBar. +func (h HolyPowerBar) Spend(amount int32, action core.ActionID, sim *core.Simulation) { + if h.paladin.DivinePurposeAura.IsActive() { + return + } + + h.DefaultSecondaryResourceBarImpl.Spend(amount, action, sim) +} + +// SpendUpTo implements core.SecondaryResourceBar. +func (h HolyPowerBar) SpendUpTo(limit int32, action core.ActionID, sim *core.Simulation) int32 { + if h.paladin.DivinePurposeAura.IsActive() { + return 3 + } + + return h.DefaultSecondaryResourceBarImpl.SpendUpTo(limit, action, sim) +} + +// Value implements core.SecondaryResourceBar. +func (h HolyPowerBar) Value() int32 { + if h.paladin.DivinePurposeAura.IsActive() { + return 3 + } + + return h.DefaultSecondaryResourceBarImpl.Value() +} diff --git a/sim/paladin/items.go b/sim/paladin/items.go index 8689bcb790..8173a47443 100644 --- a/sim/paladin/items.go +++ b/sim/paladin/items.go @@ -73,8 +73,6 @@ var ItemSetBattleplateOfRadiantGlory = core.NewItemSet(core.ItemSet{ Bonuses: map[int32]core.ApplySetBonus{ 2: func(agent core.Agent, setBonusAura *core.Aura) { paladin := agent.(PaladinAgent).GetPaladin() - // Actual buff credited with the Holy Power gain is Virtuous Empowerment - hpMetrics := paladin.NewHolyPowerMetrics(core.ActionID{SpellID: 105767}) // Used for checking "Is Aura Known" in the APL paladin.GetOrRegisterAura(core.Aura{ @@ -90,7 +88,7 @@ var ItemSetBattleplateOfRadiantGlory = core.NewItemSet(core.ItemSet{ ProcChance: 1, Handler: func(sim *core.Simulation, spell *core.Spell, result *core.SpellResult) { - paladin.GainHolyPower(sim, 1, hpMetrics) + paladin.HolyPower.Gain(1, core.ActionID{SpellID: 105765}, sim) }, }) }, diff --git a/sim/paladin/judgement.go b/sim/paladin/judgement.go index c0e00ec1f0..d6e4647aa4 100644 --- a/sim/paladin/judgement.go +++ b/sim/paladin/judgement.go @@ -22,7 +22,7 @@ func (paladin *Paladin) registerJudgement() { }, IgnoreHaste: true, CD: core.Cooldown{ - Timer: paladin.paladin.NewTimer(), + Timer: paladin.NewTimer(), Duration: time.Second * 10, }, }, diff --git a/sim/paladin/paladin.go b/sim/paladin/paladin.go index 186b9b8ac4..1a3bbfeb3b 100644 --- a/sim/paladin/paladin.go +++ b/sim/paladin/paladin.go @@ -124,10 +124,10 @@ const SpellMaskModifiedByZealOfTheCrusader = SpellMaskTemplarsVerdict | type Paladin struct { core.Character - HolyPowerBar PaladinAura proto.PaladinAura Seal proto.PaladinSeal + HolyPower core.SecondaryResourceBar Talents *proto.PaladinTalents @@ -266,26 +266,24 @@ func (paladin *Paladin) registerSpells() { } func (paladin *Paladin) Reset(sim *core.Simulation) { - switch paladin.Seal { - case proto.PaladinSeal_Truth: - paladin.CurrentJudgement = paladin.JudgementOfTruth - paladin.CurrentSeal = paladin.SealOfTruthAura - paladin.SealOfTruthAura.Activate(sim) - case proto.PaladinSeal_Insight: - paladin.CurrentJudgement = paladin.JudgementOfInsight - paladin.CurrentSeal = paladin.SealOfInsightAura - paladin.SealOfInsightAura.Activate(sim) - case proto.PaladinSeal_Righteousness: - paladin.CurrentJudgement = paladin.JudgementOfRighteousness - paladin.CurrentSeal = paladin.SealOfRighteousnessAura - paladin.SealOfRighteousnessAura.Activate(sim) - case proto.PaladinSeal_Justice: - paladin.CurrentJudgement = paladin.JudgementOfJustice - paladin.CurrentSeal = paladin.SealOfJusticeAura - paladin.SealOfJusticeAura.Activate(sim) - } - - paladin.HolyPowerBar.Reset() + // switch paladin.Seal { + // case proto.PaladinSeal_Truth: + // paladin.CurrentJudgement = paladin.JudgementOfTruth + // paladin.CurrentSeal = paladin.SealOfTruthAura + // paladin.SealOfTruthAura.Activate(sim) + // case proto.PaladinSeal_Insight: + // paladin.CurrentJudgement = paladin.JudgementOfInsight + // paladin.CurrentSeal = paladin.SealOfInsightAura + // paladin.SealOfInsightAura.Activate(sim) + // case proto.PaladinSeal_Righteousness: + // paladin.CurrentJudgement = paladin.JudgementOfRighteousness + // paladin.CurrentSeal = paladin.SealOfRighteousnessAura + // paladin.SealOfRighteousnessAura.Activate(sim) + // case proto.PaladinSeal_Justice: + // paladin.CurrentJudgement = paladin.JudgementOfJustice + // paladin.CurrentSeal = paladin.SealOfJusticeAura + // paladin.SealOfJusticeAura.Activate(sim) + // } } func NewPaladin(character *core.Character, talentsStr string, options *proto.PaladinOptions) *Paladin { @@ -302,7 +300,15 @@ func NewPaladin(character *core.Character, talentsStr string, options *proto.Pal paladin.PseudoStats.CanParry = true paladin.EnableManaBar() - paladin.initializeHolyPowerBar() + paladin.HolyPower = HolyPowerBar{ + DefaultSecondaryResourceBarImpl: paladin.NewDefaultSecondaryResourceBar(core.SecondaryResourceConfig{ + Type: proto.SecondaryResourceType_SecondaryResourceTypeHolyPower, + Max: 3, + Default: paladin.StartingHolyPower, + }), + paladin: paladin, + } + paladin.RegisterSecondaryResourceBar(paladin.HolyPower) // Only retribution and holy are actually pets performing some kind of action if paladin.Spec != proto.Spec_SpecProtectionPaladin { diff --git a/sim/paladin/protection/avengers_shield.go b/sim/paladin/protection/avengers_shield.go index 16f39b036f..49cdae4e28 100644 --- a/sim/paladin/protection/avengers_shield.go +++ b/sim/paladin/protection/avengers_shield.go @@ -9,7 +9,6 @@ import ( func (prot *ProtectionPaladin) registerAvengersShieldSpell() { actionId := core.ActionID{SpellID: 31935} - hpMetrics := prot.NewHolyPowerMetrics(actionId) asMinDamage, asMaxDamage := core.CalcScalingSpellEffectVarianceMinMax(proto.Class_ClassPaladin, 3.02399992943, 0.20000000298) glyphedSingleTargetAS := prot.HasMajorGlyph(proto.PaladinMajorGlyph_GlyphOfFocusedShield) @@ -59,7 +58,7 @@ func (prot *ProtectionPaladin) registerAvengersShieldSpell() { } if prot.GrandCrusaderAura.IsActive() { - prot.GainHolyPower(sim, 1, hpMetrics) + prot.HolyPower.Gain(1, actionId, sim) } }, diff --git a/sim/paladin/retribution/templars_verdict.go b/sim/paladin/retribution/templars_verdict.go index 8df6dbc352..ec05736310 100644 --- a/sim/paladin/retribution/templars_verdict.go +++ b/sim/paladin/retribution/templars_verdict.go @@ -7,7 +7,6 @@ import ( func (retPaladin *RetributionPaladin) RegisterTemplarsVerdict() { actionId := core.ActionID{SpellID: 85256} - hpMetrics := retPaladin.NewHolyPowerMetrics(actionId) retPaladin.TemplarsVerdict = retPaladin.RegisterSpell(core.SpellConfig{ ActionID: actionId, @@ -23,7 +22,7 @@ func (retPaladin *RetributionPaladin) RegisterTemplarsVerdict() { IgnoreHaste: true, }, ExtraCastCondition: func(sim *core.Simulation, target *core.Unit) bool { - return retPaladin.GetHolyPowerValue() > 0 + return retPaladin.HolyPower.CanSpend(1) }, DamageMultiplier: 1, @@ -31,7 +30,7 @@ func (retPaladin *RetributionPaladin) RegisterTemplarsVerdict() { ThreatMultiplier: 1, ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { - holyPower := retPaladin.GetHolyPowerValue() + holyPower := int32(retPaladin.HolyPower.Value()) multiplier := []float64{0, 0.3, 0.9, 2.35}[holyPower] @@ -42,7 +41,7 @@ func (retPaladin *RetributionPaladin) RegisterTemplarsVerdict() { spell.DamageMultiplier /= multiplier if result.Landed() { - retPaladin.SpendHolyPower(sim, hpMetrics) + retPaladin.HolyPower.SpendUpTo(3, actionId, sim) } spell.DealOutcome(sim, result) diff --git a/tools/database/dbc/maps.go b/tools/database/dbc/maps.go index d66d6cdc4f..e220b568ff 100644 --- a/tools/database/dbc/maps.go +++ b/tools/database/dbc/maps.go @@ -284,7 +284,7 @@ var MapPowerTypeEnumToResourceType = map[int32]proto.ResourceType{ 6: proto.ResourceType_ResourceTypeRunicPower, 7: proto.ResourceType_ResourceTypeNone, // Soulshards 8: proto.ResourceType_ResourceTypeLunarEnergy, - 9: proto.ResourceType_ResourceTypeHolyPower, + 9: proto.ResourceType_ResourceTypeNone, // Holy Power 12: proto.ResourceType_ResourceTypeChi, 20: proto.ResourceType_ResourceTypeBloodRune, 21: proto.ResourceType_ResourceTypeFrostRune, diff --git a/ui/core/components/detailed_results.tsx b/ui/core/components/detailed_results.tsx index 3b711dd8b0..f2575d665a 100644 --- a/ui/core/components/detailed_results.tsx +++ b/ui/core/components/detailed_results.tsx @@ -1,4 +1,5 @@ import { REPO_NAME } from '../constants/other'; +import { IndividualSimUI } from '../individual_sim_ui'; import { DetailedResultsUpdate, SimRun, SimRunData } from '../proto/ui'; import { SimResult } from '../proto_utils/sim_result'; import { SimUI } from '../sim_ui'; @@ -222,6 +223,7 @@ export abstract class DetailedResults extends Component { new ResourceMetricsTable({ parent: this.rootElem.querySelector('.resource-metrics')!, resultsEmitter: this.resultsEmitter, + secondaryResource: (simUI as IndividualSimUI)?.player?.secondaryResource, }); new PlayerDamageMetricsTable( { parent: this.rootElem.querySelector('.player-damage-metrics')!, resultsEmitter: this.resultsEmitter }, @@ -260,6 +262,7 @@ export abstract class DetailedResults extends Component { parent: this.rootElem.querySelector('.timeline')!, cssScheme: cssScheme, resultsEmitter: this.resultsEmitter, + secondaryResource: (simUI as IndividualSimUI)?.player?.secondaryResource, }); const tabEl = document.querySelector('button[data-bs-target="#timelineTab"]'); diff --git a/ui/core/components/detailed_results/resource_metrics.tsx b/ui/core/components/detailed_results/resource_metrics.tsx index 3673d8f522..e2aecce024 100644 --- a/ui/core/components/detailed_results/resource_metrics.tsx +++ b/ui/core/components/detailed_results/resource_metrics.tsx @@ -1,19 +1,29 @@ import { ResourceType } from '../../proto/spell'; import { resourceNames } from '../../proto_utils/names'; +import SecondaryResource from '../../proto_utils/secondary_resource'; import { ResourceMetrics } from '../../proto_utils/sim_result'; import { orderedResourceTypes } from '../../proto_utils/utils'; import { ColumnSortType, MetricsTable } from './metrics_table/metrics_table'; import { ResultComponent, ResultComponentConfig, SimResultData } from './result_component'; +interface ResourceMetricsTableConfig extends ResultComponentConfig { + secondaryResource?: SecondaryResource | null; +} + export class ResourceMetricsTable extends ResultComponent { - constructor(config: ResultComponentConfig) { + constructor(config: ResourceMetricsTableConfig) { config.rootCssClass = 'resource-metrics-root'; super(config); orderedResourceTypes.forEach(resourceType => { + let resourceName = resourceNames.get(resourceType); + if (resourceType == ResourceType.ResourceTypeGenericResource && !!config.secondaryResource) { + resourceName = config.secondaryResource.name; + } + const containerElem = (
- {resourceNames.get(resourceType)} + {resourceName}
) as HTMLElement; this.rootElem.appendChild(containerElem); diff --git a/ui/core/components/detailed_results/timeline.tsx b/ui/core/components/detailed_results/timeline.tsx index a95a110ea8..daf9f92fb2 100644 --- a/ui/core/components/detailed_results/timeline.tsx +++ b/ui/core/components/detailed_results/timeline.tsx @@ -8,6 +8,7 @@ import { ResourceType } from '../../proto/spell'; import { ActionId, buffAuraToSpellIdMap, resourceTypeToIcon } from '../../proto_utils/action_id'; import { AuraUptimeLog, CastLog, DpsLog, ResourceChangedLogGroup, SimLog, ThreatLogGroup } from '../../proto_utils/logs_parser'; import { resourceNames } from '../../proto_utils/names'; +import SecondaryResource from '../../proto_utils/secondary_resource'; import { UnitMetrics } from '../../proto_utils/sim_result'; import { orderedResourceTypes } from '../../proto_utils/utils'; import { TypedEvent } from '../../typed_event'; @@ -23,6 +24,10 @@ const threatColor = '#b56d07'; const cachedSpellCastIcon = new CacheHandler(); +interface TimelineConfig extends ResultComponentConfig { + secondaryResource?: SecondaryResource | null; +} + export class Timeline extends ResultComponent { private readonly dpsResourcesPlotElem: HTMLElement; private dpsResourcesPlot: any; @@ -51,7 +56,9 @@ export class Timeline extends ResultComponent { keysToKeep: 2, }); - constructor(config: ResultComponentConfig) { + private secondaryResource?: SecondaryResource | null; + + constructor(config: TimelineConfig) { config.rootCssClass = 'timeline-root'; super(config); this.resultData = null; @@ -59,6 +66,7 @@ export class Timeline extends ResultComponent { this.rendered = false; this.hiddenIds = []; this.hiddenIdsChangeEmitter = new TypedEvent(); + this.secondaryResource = config.secondaryResource; this.rootElem.appendChild(
@@ -309,7 +317,7 @@ export class Timeline extends ResultComponent { this.dpsResourcesPlot.updateOptions(options); - this.rotationTimelineTimeRulerElem?.toBlob(blob => { + this.rotationTimelineTimeRulerElem?.toBlob(() => { this.cacheHandler.set(this.resultData!.result.request.requestId, { dpsResourcesPlotOptions: options, rotationLabels: this.rotationLabels.cloneNode(true) as HTMLElement, @@ -770,14 +778,22 @@ export class Timeline extends ResultComponent { return group.maxValue; }; + + let resourceName = resourceNames.get(resourceType); + let resourceIcon = resourceTypeToIcon[resourceType]; + if (resourceType == ResourceType.ResourceTypeGenericResource && !!this.secondaryResource) { + resourceName = this.secondaryResource.name; + resourceIcon = this.secondaryResource.icon || ''; + } + const labelElem = (
- {resourceNames.get(resourceType)} + {resourceName}
); diff --git a/ui/core/components/individual_sim_ui/apl_values.ts b/ui/core/components/individual_sim_ui/apl_values.ts index 43e57556ac..2dbeded285 100644 --- a/ui/core/components/individual_sim_ui/apl_values.ts +++ b/ui/core/components/individual_sim_ui/apl_values.ts @@ -26,9 +26,9 @@ import { APLValueCurrentEclipsePhase, APLValueCurrentEnergy, APLValueCurrentFocus, + APLValueCurrentGenericResource, APLValueCurrentHealth, APLValueCurrentHealthPercent, - APLValueCurrentHolyPower, APLValueCurrentLunarEnergy, APLValueCurrentMana, APLValueCurrentManaPercent, @@ -100,6 +100,7 @@ import { } from '../../proto/apl.js'; import { Class, Spec } from '../../proto/common.js'; import { ShamanTotems_TotemType as TotemType } from '../../proto/shaman.js'; +import SecondaryResource from '../../proto_utils/secondary_resource'; import { EventID } from '../../typed_event.js'; import { randomUUID } from '../../utils'; import { Input, InputConfig } from '../input.js'; @@ -131,7 +132,6 @@ export class APLValuePicker extends Input, APLValue | undefined> { valueKind => valueKindFactories[valueKind].includeIf?.(player, isPrepull) ?? true, ); - if (this.rootElem.parentElement!.classList.contains('list-picker-item')) { const itemHeaderElem = ListPicker.getItemHeaderElem(this) || this.rootElem; ListPicker.makeListItemValidations( @@ -152,11 +152,14 @@ export class APLValuePicker extends Input, APLValue | undefined> { ].concat( allValueKinds.map(kind => { const factory = valueKindFactories[kind]; + const resolveString = factory.dynamicStringResolver || ((value: string) => value); return { value: kind, - label: factory.label, + label: resolveString(factory.label, player), submenu: factory.submenu, - tooltip: factory.fullDescription ? `

${factory.shortDescription}

${factory.fullDescription}` : factory.shortDescription, + tooltip: factory.fullDescription + ? `

${resolveString(factory.shortDescription, player)}

${resolveString(factory.fullDescription, player)}` + : resolveString(factory.shortDescription, player), }; }), ), @@ -275,10 +278,10 @@ export class APLValuePicker extends Input, APLValue | undefined> { } if (newValue) { - if (!newValue.uuid || newValue.uuid.value == "") { + if (!newValue.uuid || newValue.uuid.value == '') { newValue.uuid = { - value: randomUUID() - } + value: randomUUID(), + }; } this.rootElem.id = newValue.uuid!.value; } @@ -343,6 +346,7 @@ type ValueKindConfig = { newValue: () => T; includeIf?: (player: Player, isPrepull: boolean) => boolean; factory: (parent: HTMLElement, player: Player, config: InputConfig, T>) => Input, T>; + dynamicStringResolver?: (value: string, player: Player) => string; }; function comparisonOperatorFieldConfig(field: string): AplHelpers.APLPickerBuilderFieldConfig { @@ -435,8 +439,8 @@ export function valueFieldConfig( field: field, newValue: () => APLValue.create({ - uuid: { value: randomUUID() }, - }), + uuid: { value: randomUUID() }, + }), factory: (parent, player, config) => new APLValuePicker(parent, player, config), ...(options || {}), }; @@ -455,10 +459,12 @@ export function valueListFieldConfig(field: string): AplHelpers.APLPickerBuilder eventID, player, newValue.map(val => { - return val || - APLValue.create({ - uuid: { value: randomUUID() }, - }) + return ( + val || + APLValue.create({ + uuid: { value: randomUUID() }, + }) + ); }), ); }, @@ -466,7 +472,7 @@ export function valueListFieldConfig(field: string): AplHelpers.APLPickerBuilder newItem: () => { return APLValue.create({ uuid: { value: randomUUID() }, - }) + }); }, copyItem: (oldValue: APLValue | undefined) => (oldValue ? APLValue.clone(oldValue) : oldValue), newItemPicker: ( @@ -485,15 +491,11 @@ export function valueListFieldConfig(field: string): AplHelpers.APLPickerBuilder }; } -function inputBuilder(config: { - label: string; - submenu?: Array; - shortDescription: string; - fullDescription?: string; - newValue: () => T; - includeIf?: (player: Player, isPrepull: boolean) => boolean; - fields: Array>; -}): ValueKindConfig { +function inputBuilder( + config: { + fields: Array>; + } & Omit, 'factory'>, +): ValueKindConfig { return { label: config.label, submenu: config.submenu, @@ -502,6 +504,7 @@ function inputBuilder(config: { newValue: config.newValue, includeIf: config.includeIf, factory: AplHelpers.aplInputBuilder(config.newValue, config.fields), + dynamicStringResolver: config.dynamicStringResolver, }; } @@ -862,13 +865,14 @@ const valueKindFactories: { [f in NonNullable]: ValueKindConfig, _isPrepull: boolean) => player.getSpec() == Spec.SpecBalanceDruid, fields: [AplHelpers.eclipseTypeFieldConfig('eclipsePhase')], }), - currentHolyPower: inputBuilder({ - label: 'Holy Power', + currentGenericResource: inputBuilder({ + label: '{GENERIC_RESOURCE}', submenu: ['Resources'], - shortDescription: 'Amount of currently available Holy Power.', - newValue: APLValueCurrentHolyPower.create, - includeIf: (player: Player, _isPrepull: boolean) => player.getClass() == Class.ClassPaladin, + shortDescription: 'Amount of currently available {GENERIC_RESOURCE}.', + newValue: APLValueCurrentGenericResource.create, + includeIf: (player: Player, _isPrepull: boolean) => SecondaryResource.hasSecondaryResource(player.getSpec()), fields: [], + dynamicStringResolver: (value: string, player: Player) => player.secondaryResource?.replaceResourceName(value) || '', }), // Resources Rune @@ -1166,7 +1170,12 @@ const valueKindFactories: { [f in NonNullable]: ValueKindConfig]: ValueKindConfig]: ValueKindConfig APLValueTrinketProcsMaxRemainingICD.create({ statType1: -1, statType2: -1, statType3: -1, }), - fields: [AplHelpers.statTypeFieldConfig('statType1'), AplHelpers.statTypeFieldConfig('statType2'), AplHelpers.statTypeFieldConfig('statType3'), AplHelpers.minIcdInput], + fields: [ + AplHelpers.statTypeFieldConfig('statType1'), + AplHelpers.statTypeFieldConfig('statType2'), + AplHelpers.statTypeFieldConfig('statType3'), + AplHelpers.minIcdInput, + ], }), numEquippedStatProcTrinkets: inputBuilder({ label: 'Num Equipped Stat Proc Effects', @@ -1219,7 +1242,12 @@ const valueKindFactories: { [f in NonNullable]: ValueKindConfig extends PlayerConf raidSimPresets: Array>; } + + export function registerSpecConfig(spec: SpecType, config: IndividualSimUIConfig): IndividualSimUIConfig { registerPlayerConfig(spec, config); return config; diff --git a/ui/core/player.ts b/ui/core/player.ts index 6f2cafef96..061abc975d 100644 --- a/ui/core/player.ts +++ b/ui/core/player.ts @@ -54,6 +54,7 @@ import { Database } from './proto_utils/database'; import { EquippedItem, ReforgeData } from './proto_utils/equipped_item'; import { Gear, ItemSwapGear } from './proto_utils/gear'; import { gemMatchesSocket, isUnrestrictedGem } from './proto_utils/gems'; +import SecondaryResource from './proto_utils/secondary_resource'; import { StatCap, Stats } from './proto_utils/stats'; import { AL_CATEGORY_HARD_MODE, @@ -208,6 +209,7 @@ export interface PlayerConfig { autoRotation: AutoRotationGenerator; simpleRotation?: SimpleRotationGenerator; hiddenMCDs?: Array; // spell IDs for any MCDs that should be omitted from the Simple Cooldowns UI + secondaryResource?: SecondaryResource | null; } const SPEC_CONFIGS: Partial>> = {}; @@ -218,6 +220,7 @@ export function registerSpecConfig(spec: SpecType, config export function getSpecConfig(spec: SpecType): PlayerConfig { const config = SPEC_CONFIGS[spec] as PlayerConfig; + config.secondaryResource = SecondaryResource.create(spec); if (!config) { throw new Error('No config registered for Spec: ' + spec); } @@ -232,6 +235,7 @@ export class Player { readonly playerSpec: PlayerSpec; readonly playerClass: PlayerClass>; + readonly secondaryResource?: SecondaryResource | null; private name = ''; private buffs: IndividualBuffs = IndividualBuffs.create(); @@ -320,6 +324,8 @@ export class Player { const specConfig = getSpecConfig(this.getSpec()); + this.secondaryResource = specConfig.secondaryResource; + this.autoRotationGenerator = specConfig.autoRotation; if (specConfig.simpleRotation) { this.simpleRotationGenerator = specConfig.simpleRotation; diff --git a/ui/core/proto_utils/action_id.ts b/ui/core/proto_utils/action_id.ts index 46117befb8..2574bd6a57 100644 --- a/ui/core/proto_utils/action_id.ts +++ b/ui/core/proto_utils/action_id.ts @@ -1106,7 +1106,7 @@ export const resourceTypeToIcon: Record = { [ResourceType.ResourceTypeDeathRune]: '/mop/assets/img/death_rune.png', [ResourceType.ResourceTypeSolarEnergy]: 'https://wow.zamimg.com/images/wow/icons/large/ability_druid_eclipseorange.jpg', [ResourceType.ResourceTypeLunarEnergy]: 'https://wow.zamimg.com/images/wow/icons/large/ability_druid_eclipse.jpg', - [ResourceType.ResourceTypeHolyPower]: 'https://wow.zamimg.com/images/wow/icons/medium/spell_holy_holybolt.jpg', + [ResourceType.ResourceTypeGenericResource]: 'https://wow.zamimg.com/images/wow/icons/medium/spell_holy_holybolt.jpg', }; // Use this to connect a buff row to a cast row in the timeline view diff --git a/ui/core/proto_utils/logs_parser.tsx b/ui/core/proto_utils/logs_parser.tsx index 3744e26d09..c544e52427 100644 --- a/ui/core/proto_utils/logs_parser.tsx +++ b/ui/core/proto_utils/logs_parser.tsx @@ -3,10 +3,11 @@ import clsx from 'clsx'; import { CacheHandler } from '../cache_handler'; import { RaidSimResult } from '../proto/api.js'; import { SpellSchool } from '../proto/common'; -import { ResourceType } from '../proto/spell'; +import { ResourceType, SecondaryResourceType } from '../proto/spell'; import { bucket, getEnumValues, stringComparator, sum } from '../utils.js'; import { ActionId } from './action_id.js'; import { resourceNames, spellSchoolNames, stringToResourceType } from './names.js'; +import { SECONDARY_RESOURCES } from './secondary_resource'; export class Entity { readonly name: string; @@ -779,14 +780,16 @@ export class ResourceChangedLog extends SimLog { readonly valueAfter: number; readonly isSpend: boolean; readonly total: number; + readonly secondaryResourceType?: SecondaryResourceType; - constructor(params: SimLogParams, resourceType: ResourceType, valueBefore: number, valueAfter: number, isSpend: boolean, total: number) { + constructor(params: SimLogParams, resourceType: ResourceType, valueBefore: number, valueAfter: number, isSpend: boolean, total: number, secondaryType?: SecondaryResourceType) { super(params); this.resourceType = resourceType; this.valueBefore = valueBefore; this.valueAfter = valueAfter; this.isSpend = isSpend; this.total = total; + this.secondaryResourceType = secondaryType; } toHTML(includeTimestamp = true) { @@ -794,7 +797,7 @@ export class ResourceChangedLog extends SimLog { const signedDiff = (this.valueAfter - this.valueBefore) * (this.isSpend ? -1 : 1); const isHealth = this.resourceType == ResourceType.ResourceTypeHealth; const verb = isHealth ? (this.isSpend ? 'Lost' : 'Recovered') : this.isSpend ? 'Spent' : 'Gained'; - const resourceName = resourceNames.get(this.resourceType)!; + const resourceName = this.secondaryResourceType !== undefined ? SECONDARY_RESOURCES.get(this.secondaryResourceType)!.name : resourceNames.get(this.resourceType)!; const resourceClass = `resource-${resourceName.replace(/\s/g, '-').toLowerCase()}`; return ( @@ -821,16 +824,16 @@ export class ResourceChangedLog extends SimLog { static parse(params: SimLogParams): Promise | null { const match = params.raw.match( - /(Gained|Spent) (\d+\.?\d*) (health|mana|energy|focus|rage|chi|combo points|runic power|blood rune|frost rune|unholy rune|death rune|solar energy|lunar energy|holy power) from (.*?) \((\d+\.?\d*) --> (\d+\.?\d*)\)( of (\d+\.?\d*) total)?/, + /(Gained|Spent) (\d+\.?\d*) (\S.+?\S) from (.*?) \((\d+\.?\d*) --> (\d+\.?\d*)\)( of (\d+\.?\d*) total)?/, ); if (match) { - const resourceType = stringToResourceType(match[3]); + const [resourceType, secondaryType] = stringToResourceType(match[3]); const total = match[8] !== undefined ? parseFloat(match[8]) : 0; return ActionId.fromLogString(match[4]) .fill(params.source?.index) .then(cause => { params.actionId = cause; - return new ResourceChangedLog(params, resourceType, parseFloat(match[5]), parseFloat(match[6]), match[1] == 'Spent', total); + return new ResourceChangedLog(params, resourceType, parseFloat(match[5]), parseFloat(match[6]), match[1] == 'Spent', total, secondaryType); }); } else { return null; diff --git a/ui/core/proto_utils/names.ts b/ui/core/proto_utils/names.ts index 3833540833..856fd9b251 100644 --- a/ui/core/proto_utils/names.ts +++ b/ui/core/proto_utils/names.ts @@ -1,5 +1,5 @@ import { ArmorType, Class, ItemSlot, Profession, PseudoStat, Race, RangedWeaponType, Spec, Stat, WeaponType } from '../proto/common'; -import { ResourceType } from '../proto/spell'; +import { ResourceType, SecondaryResourceType } from '../proto/spell'; import { DungeonDifficulty, RaidFilterOption, RepFaction, RepLevel, SourceFilterOption, StatCapType } from '../proto/ui'; export const armorTypeNames: Map = new Map([ @@ -212,7 +212,7 @@ export const resourceNames: Map = new Map([ [ResourceType.ResourceTypeDeathRune, 'Death Rune'], [ResourceType.ResourceTypeSolarEnergy, 'Solar Energy'], [ResourceType.ResourceTypeLunarEnergy, 'Lunar Energy'], - [ResourceType.ResourceTypeHolyPower, 'Holy Power'], + [ResourceType.ResourceTypeGenericResource, 'Generic Resource'], ]); export const resourceColors: Map = new Map([ @@ -231,16 +231,23 @@ export const resourceColors: Map = new Map([ [ResourceType.ResourceTypeDeathRune, '#8b008b'], [ResourceType.ResourceTypeSolarEnergy, '#d2952b'], [ResourceType.ResourceTypeLunarEnergy, '#2c4f8f'], - [ResourceType.ResourceTypeHolyPower, '#ffa07b'], + [ResourceType.ResourceTypeGenericResource, '#ffffff'], ]); -export function stringToResourceType(str: string): ResourceType { +export function stringToResourceType(str: string): [ResourceType, SecondaryResourceType | undefined] { for (const [key, val] of resourceNames) { if (val.toLowerCase() == str.toLowerCase()) { - return key; + return [key, undefined]; + } + } + + for (const val of Object.keys(SecondaryResourceType).filter(key=> isNaN(Number(key)))) { + if (val.toLowerCase() == str.toLowerCase()) { + return [ResourceType.ResourceTypeGenericResource, (SecondaryResourceType)[val]]; } } - return ResourceType.ResourceTypeNone; + + return [ResourceType.ResourceTypeNone, undefined]; } export const sourceNames: Map = new Map([ diff --git a/ui/core/proto_utils/secondary_resource.ts b/ui/core/proto_utils/secondary_resource.ts new file mode 100644 index 0000000000..9d06f84e0f --- /dev/null +++ b/ui/core/proto_utils/secondary_resource.ts @@ -0,0 +1,103 @@ +import { Spec } from '../proto/common'; +import { SecondaryResourceType } from '../proto/spell'; + +export interface SecondaryResourceConfig { + name: string; + icon: string; +} + +export const SECONDARY_RESOURCES = new Map([ + [ + SecondaryResourceType.SecondaryResourceTypeArcaneCharges, + { + name: 'Arcane Charges', + icon: 'https://wow.zamimg.com/images/wow/icons/medium/spell_arcane_arcane01.jpg', + }, + ], + [ + SecondaryResourceType.SecondaryResourceTypeShadowOrbs, + { + name: 'Shadow Orbs', + icon: 'https://wow.zamimg.com/images/wow/icons/medium/spell_priest_shadoworbs.jpg', + }, + ], + [ + SecondaryResourceType.SecondaryResourceTypeDemonicFury, + { + name: 'Demonic Fury', + icon: 'https://wow.zamimg.com/images/wow/icons/medium/ability_warlock_eradication.jpg', + }, + ], + [ + SecondaryResourceType.SecondaryResourceTypeHolyPower, + { + name: 'Holy Power', + icon: 'https://wow.zamimg.com/images/wow/icons/medium/spell_holy_holybolt.jpg', + }, + ], + [ + SecondaryResourceType.SecondaryResourceTypeBurningEmbers, + { + name: 'Burning Embers', + icon: 'https://wow.zamimg.com/images/wow/icons/medium/inv_mace_2h_pvp410_c_01.jpg', + }, + ], + [ + SecondaryResourceType.SecondaryResourceTypeSoulShards, + { + name: 'Soul Shards', + icon: 'https://wow.zamimg.com/images/wow/icons/large/inv_misc_gem_amethyst_02.jpg', + }, + ], +]); + +const RESOURCE_TYPE_PER_SPEC = new Map([ + // Paladin + [Spec.SpecRetributionPaladin, SecondaryResourceType.SecondaryResourceTypeHolyPower], + [Spec.SpecProtectionPaladin, SecondaryResourceType.SecondaryResourceTypeHolyPower], + [Spec.SpecHolyPaladin, SecondaryResourceType.SecondaryResourceTypeHolyPower], + // Warlock + [Spec.SpecAfflictionWarlock, SecondaryResourceType.SecondaryResourceTypeSoulShards], + [Spec.SpecDemonologyWarlock, SecondaryResourceType.SecondaryResourceTypeDemonicFury], + [Spec.SpecDestructionWarlock, SecondaryResourceType.SecondaryResourceTypeBurningEmbers], + // Priest + [Spec.SpecShadowPriest, SecondaryResourceType.SecondaryResourceTypeShadowOrbs], + // Mage + [Spec.SpecArcaneMage, SecondaryResourceType.SecondaryResourceTypeArcaneCharges], +]); + +class SecondaryResource { + private readonly config: SecondaryResourceConfig | null; + constructor(spec: Spec) { + const type = SecondaryResource.getGenericResourcesForSpec(spec); + this.config = type || null; + } + + get name() { + return this.config?.name; + } + get icon() { + return this.config?.icon; + } + + static hasSecondaryResource(spec: Spec): boolean { + return RESOURCE_TYPE_PER_SPEC.has(spec); + } + + static getGenericResourcesForSpec(spec: Spec) { + const type = RESOURCE_TYPE_PER_SPEC.get(spec); + if (!type) return null; + return SECONDARY_RESOURCES.get(type); + } + + static create(spec: Spec): SecondaryResource | null { + if (!SecondaryResource.hasSecondaryResource(spec)) return null; + return new SecondaryResource(spec); + } + + replaceResourceName(value: string) { + return value.replaceAll(/{GENERIC_RESOURCE}/g, this.name || ''); + } +} + +export default SecondaryResource; diff --git a/ui/core/proto_utils/utils.ts b/ui/core/proto_utils/utils.ts index 1915c82d57..6ccc4813c1 100644 --- a/ui/core/proto_utils/utils.ts +++ b/ui/core/proto_utils/utils.ts @@ -2033,7 +2033,7 @@ export const orderedResourceTypes: Array = [ ResourceType.ResourceTypeDeathRune, ResourceType.ResourceTypeLunarEnergy, ResourceType.ResourceTypeSolarEnergy, - ResourceType.ResourceTypeHolyPower, + ResourceType.ResourceTypeGenericResource, ]; export const AL_CATEGORY_HARD_MODE = 'Hard Mode'; diff --git a/ui/core/sim_ui.tsx b/ui/core/sim_ui.tsx index 201607470b..92d71a96d5 100644 --- a/ui/core/sim_ui.tsx +++ b/ui/core/sim_ui.tsx @@ -68,6 +68,7 @@ export abstract class SimUI extends Component { this.cssClass = config.cssClass; this.cssScheme = config.cssScheme; this.isWithinRaidSim = this.rootElem.closest('.within-raid-sim') != null; + const container = ( <>
diff --git a/ui/paladin/retribution/apls/default.apl.json b/ui/paladin/retribution/apls/default.apl.json index f8b93c6318..2ced823c5a 100644 --- a/ui/paladin/retribution/apls/default.apl.json +++ b/ui/paladin/retribution/apls/default.apl.json @@ -13,7 +13,7 @@ "and": { "vals": [ { "cmp": { "op": "OpEq", "lhs": { "currentTime": {} }, "rhs": { "const": { "val": "0s" } } } }, - { "cmp": { "op": "OpEq", "lhs": { "currentHolyPower": {} }, "rhs": { "const": { "val": "3" } } } } + { "cmp": { "op": "OpEq", "lhs": { "currentGenericResource": {} }, "rhs": { "const": { "val": "3" } } } } ] } }, @@ -77,7 +77,7 @@ ] } }, - { "cmp": { "op": "OpLt", "lhs": { "currentHolyPower": {} }, "rhs": { "const": { "val": "3" } } } } + { "cmp": { "op": "OpLt", "lhs": { "currentGenericResource": {} }, "rhs": { "const": { "val": "3" } } } } ] } }, @@ -118,7 +118,7 @@ { "or": { "vals": [ - { "cmp": { "op": "OpEq", "lhs": { "currentHolyPower": {} }, "rhs": { "const": { "val": "3" } } } }, + { "cmp": { "op": "OpEq", "lhs": { "currentGenericResource": {} }, "rhs": { "const": { "val": "3" } } } }, { "auraIsActiveWithReactionTime": { "auraId": { "spellId": 90174 } } } ] } @@ -917,7 +917,7 @@ { "cmp": { "op": "OpGe", - "lhs": { "currentHolyPower": {} }, + "lhs": { "currentGenericResource": {} }, "rhs": { "const": { "val": "2" } } } } @@ -1044,7 +1044,7 @@ { "cmp": { "op": "OpLt", - "lhs": { "currentHolyPower": {} }, + "lhs": { "currentGenericResource": {} }, "rhs": { "const": { "val": "3" } } } }, @@ -1058,7 +1058,7 @@ { "cmp": { "op": "OpEq", - "lhs": { "currentHolyPower": {} }, + "lhs": { "currentGenericResource": {} }, "rhs": { "const": { "val": "3" } } } }, @@ -1097,7 +1097,7 @@ "and": { "vals": [ { "auraIsInactiveWithReactionTime": { "auraId": { "spellId": 90174 } } }, - { "cmp": { "op": "OpLt", "lhs": { "currentHolyPower": {} }, "rhs": { "const": { "val": "2" } } } } + { "cmp": { "op": "OpLt", "lhs": { "currentGenericResource": {} }, "rhs": { "const": { "val": "2" } } } } ] } }, @@ -1105,7 +1105,7 @@ "and": { "vals": [ { "auraIsActiveWithReactionTime": { "auraId": { "spellId": 90174 } } }, - { "cmp": { "op": "OpEq", "lhs": { "currentHolyPower": {} }, "rhs": { "const": { "val": "2" } } } } + { "cmp": { "op": "OpEq", "lhs": { "currentGenericResource": {} }, "rhs": { "const": { "val": "2" } } } } ] } } @@ -1133,7 +1133,7 @@ "and": { "vals": [ { "auraIsInactiveWithReactionTime": { "auraId": { "spellId": 90174 } } }, - { "cmp": { "op": "OpLt", "lhs": { "currentHolyPower": {} }, "rhs": { "const": { "val": "2" } } } } + { "cmp": { "op": "OpLt", "lhs": { "currentGenericResource": {} }, "rhs": { "const": { "val": "2" } } } } ] } }, @@ -1141,7 +1141,7 @@ "and": { "vals": [ { "auraIsActiveWithReactionTime": { "auraId": { "spellId": 90174 } } }, - { "cmp": { "op": "OpEq", "lhs": { "currentHolyPower": {} }, "rhs": { "const": { "val": "2" } } } } + { "cmp": { "op": "OpEq", "lhs": { "currentGenericResource": {} }, "rhs": { "const": { "val": "2" } } } } ] } } @@ -1169,7 +1169,7 @@ "vals": [ { "auraIsKnown": { "auraId": { "spellId": 105767 } } }, { "auraIsInactiveWithReactionTime": { "auraId": { "spellId": 90174 } } }, - { "cmp": { "op": "OpLt", "lhs": { "currentHolyPower": {} }, "rhs": { "const": { "val": "3" } } } } + { "cmp": { "op": "OpLt", "lhs": { "currentGenericResource": {} }, "rhs": { "const": { "val": "3" } } } } ] } } @@ -1295,7 +1295,7 @@ "condition": { "and": { "vals": [ - { "cmp": { "op": "OpLt", "lhs": { "currentHolyPower": {} }, "rhs": { "const": { "val": "3" } } } }, + { "cmp": { "op": "OpLt", "lhs": { "currentGenericResource": {} }, "rhs": { "const": { "val": "3" } } } }, { "or": { "vals": [ @@ -1316,7 +1316,7 @@ "and": { "vals": [ { "cmp": { "op": "OpGe", "lhs": { "numberTargets": {} }, "rhs": { "const": { "val": "4" } } } }, - { "cmp": { "op": "OpLt", "lhs": { "currentHolyPower": {} }, "rhs": { "const": { "val": "3" } } } }, + { "cmp": { "op": "OpLt", "lhs": { "currentGenericResource": {} }, "rhs": { "const": { "val": "3" } } } }, { "auraIsInactiveWithReactionTime": { "auraId": { "spellId": 85696 } } } ] } @@ -1380,7 +1380,7 @@ { "auraIsKnown": { "auraId": { "spellId": 105767 } } }, { "not": { "val": { "auraIsActive": { "auraId": { "spellId": 85696 } } } } }, { "auraIsInactiveWithReactionTime": { "auraId": { "spellId": 90174 } } }, - { "cmp": { "op": "OpLt", "lhs": { "currentHolyPower": {} }, "rhs": { "const": { "val": "3" } } } } + { "cmp": { "op": "OpLt", "lhs": { "currentGenericResource": {} }, "rhs": { "const": { "val": "3" } } } } ] } }, @@ -1396,7 +1396,7 @@ { "and": { "vals": [ - { "cmp": { "op": "OpEq", "lhs": { "currentHolyPower": {} }, "rhs": { "const": { "val": "3" } } } }, + { "cmp": { "op": "OpEq", "lhs": { "currentGenericResource": {} }, "rhs": { "const": { "val": "3" } } } }, { "or": { "vals": [ @@ -1435,7 +1435,7 @@ "vals": [ { "auraIsKnown": { "auraId": { "spellId": 105767 } } }, { "not": { "val": { "auraIsActive": { "auraId": { "spellId": 85696 } } } } }, - { "cmp": { "op": "OpLt", "lhs": { "currentHolyPower": {} }, "rhs": { "const": { "val": "3" } } } } + { "cmp": { "op": "OpLt", "lhs": { "currentGenericResource": {} }, "rhs": { "const": { "val": "3" } } } } ] } }, @@ -1448,7 +1448,7 @@ "condition": { "and": { "vals": [ - { "cmp": { "op": "OpEq", "lhs": { "currentHolyPower": {} }, "rhs": { "const": { "val": "3" } } } }, + { "cmp": { "op": "OpEq", "lhs": { "currentGenericResource": {} }, "rhs": { "const": { "val": "3" } } } }, { "cmp": { "op": "OpGt", @@ -1474,7 +1474,7 @@ "vals": [ { "auraIsKnown": { "auraId": { "spellId": 105767 } } }, { "auraIsActive": { "auraId": { "spellId": 85696 } } }, - { "cmp": { "op": "OpLt", "lhs": { "currentHolyPower": {} }, "rhs": { "const": { "val": "3" } } } } + { "cmp": { "op": "OpLt", "lhs": { "currentGenericResource": {} }, "rhs": { "const": { "val": "3" } } } } ] } }