diff --git a/.vscode/settings.json b/.vscode/settings.json index 6c23f9e065..7f94fc83f2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -99,4 +99,7 @@ "tinsert", "UIParent" ], + "[go]": { + "editor.defaultFormatter": "golang.go" + } } diff --git a/proto/api.proto b/proto/api.proto index 3565966b77..cbf6629dd7 100644 --- a/proto/api.proto +++ b/proto/api.proto @@ -138,6 +138,11 @@ message ActionMetrics { // Note that some spells are untargeted, these will always have a single // element in this array. repeated TargetedActionMetrics targets = 3; + + int32 spell_school = 4; + + // True if action is applied/cast as a result of another action + bool is_passive = 5; } // Metrics for a specific action, when cast at a particular target. @@ -151,9 +156,27 @@ message TargetedActionMetrics { // # of times this action hit a target. For cleave spells this can be larger than casts. int32 hits = 2; + // # of times this action resisted hit a target. + int32 resisted_hits = 25; + // # of times this action was a critical strike. int32 crits = 3; + // # of times this action resisted crit a target. + int32 resisted_crits = 26; + + // # of times this action ticked on a target. + int32 ticks = 21; + + // # of times this action resisted ticked a target. + int32 resisted_ticks = 27; + + // # of times this action was a critical tick on a target. + int32 crit_ticks = 22; + + // # of times this action was a resisted tick on a target. + int32 resisted_crit_ticks = 28; + // # of times this action was a Miss or Resist. int32 misses = 4; @@ -166,18 +189,54 @@ message TargetedActionMetrics { // # of times this action was a Block. int32 blocks = 7; + // # of times this action was a Critical Block. + int32 crit_blocks = 20; + // # of times this action was a Glance. int32 glances = 8; // Total damage done to this target by this action. double damage = 9; + // Total resisted damage done to this target by this action. + double resisted_damage = 29; + + // Total critical damage done to this target by this action. + double crit_damage = 15; + + // Total resisted critical damage done to this target by this action. + double resisted_crit_damage = 30; + + // Total tick damage done to this target by this action. + double tick_damage = 23; + + // Total resisted tick damage done to this target by this action. + double resisted_tick_damage = 31; + + // Total critical tick damage done to this target by this action. + double crit_tick_damage = 24; + + // Total resisted critical tick damage done to this target by this action. + double resisted_crit_tick_damage = 32; + + // Total glancing damage done to this target by this action. + double glance_damage = 17; + + // Total block damage done to this target by this action. + double block_damage = 18; + + // Total critical block damage done to this target by this action. + double crit_block_damage = 19; + // Total threat done to this target by this action. double threat = 10; // Total healing done to this target by this action. double healing = 11; + // Total critical healing done to this target by this action. + double crit_healing = 16; + // Total shielding done to this target by this action. double shielding = 13; diff --git a/sim/common/itemhelpers/weaponprocs.go b/sim/common/itemhelpers/weaponprocs.go index b1e82a0a80..13a5a3044b 100644 --- a/sim/common/itemhelpers/weaponprocs.go +++ b/sim/common/itemhelpers/weaponprocs.go @@ -18,6 +18,7 @@ func CreateWeaponProcDamage(itemId int32, itemName string, ppm float64, spellId SpellSchool: school, DefenseType: defType, ProcMask: core.ProcMaskEmpty, + Flags: core.SpellFlagNoOnCastComplete | core.SpellFlagPassiveSpell, DamageMultiplier: 1, ThreatMultiplier: 1, @@ -75,6 +76,7 @@ func CreateWeaponProcSpell(itemId int32, itemName string, ppm float64, procSpell character := agent.GetCharacter() procSpell := procSpellGenerator(character) + procSpell.Flags |= core.SpellFlagNoOnCastComplete | core.SpellFlagPassiveSpell procMask := character.GetProcMaskForItem(itemId) ppmm := character.AutoAttacks.NewPPMManager(ppm, procMask) diff --git a/sim/common/sod/crafted/phase_2.go b/sim/common/sod/crafted/phase_2.go index a5f113ba46..0697c5f40f 100644 --- a/sim/common/sod/crafted/phase_2.go +++ b/sim/common/sod/crafted/phase_2.go @@ -131,7 +131,7 @@ func init() { SpellSchool: core.SpellSchoolArcane, ProcMask: core.ProcMaskEmpty, // TODO: Verify if SP affects the damage - Flags: core.SpellFlagNoMetrics | core.SpellFlagIgnoreAttackerModifiers, + Flags: core.SpellFlagNoOnCastComplete | core.SpellFlagNoMetrics | core.SpellFlagIgnoreAttackerModifiers, Cast: core.CastConfig{ CD: core.Cooldown{ @@ -252,7 +252,7 @@ func init() { SpellSchool: core.SpellSchoolArcane, ProcMask: core.ProcMaskEmpty, // TODO: Verify if SP affects the damage - Flags: core.SpellFlagNoMetrics | core.SpellFlagIgnoreAttackerModifiers, + Flags: core.SpellFlagNoOnCastComplete | core.SpellFlagPassiveSpell | core.SpellFlagNoMetrics | core.SpellFlagIgnoreAttackerModifiers, Cast: core.CastConfig{ CD: core.Cooldown{ diff --git a/sim/common/sod/item_effects/phase_2.go b/sim/common/sod/item_effects/phase_2.go index daf721f1a0..0ec6d70ec3 100644 --- a/sim/common/sod/item_effects/phase_2.go +++ b/sim/common/sod/item_effects/phase_2.go @@ -38,7 +38,7 @@ func init() { SpellSchool: core.SpellSchoolNature, DefenseType: core.DefenseTypeMagic, ProcMask: core.ProcMaskEmpty, - Flags: core.SpellFlagNoOnCastComplete, + Flags: core.SpellFlagNoOnCastComplete | core.SpellFlagPassiveSpell, DamageMultiplier: 1, ThreatMultiplier: 1, @@ -147,7 +147,7 @@ func init() { regChannel := character.GetOrRegisterSpell(core.SpellConfig{ ActionID: actionID, - Flags: core.SpellFlagNoOnCastComplete | core.SpellFlagChanneled, + Flags: core.SpellFlagNoOnCastComplete | core.SpellFlagPassiveSpell | core.SpellFlagChanneled, Cast: core.CastConfig{ CD: core.Cooldown{ diff --git a/sim/common/sod/item_effects/phase_3.go b/sim/common/sod/item_effects/phase_3.go index 7ef21e76a3..08684ed323 100644 --- a/sim/common/sod/item_effects/phase_3.go +++ b/sim/common/sod/item_effects/phase_3.go @@ -242,6 +242,7 @@ func init() { SpellSchool: core.SpellSchoolShadow, DefenseType: core.DefenseTypeMagic, ProcMask: core.ProcMaskEmpty, + Flags: core.SpellFlagNoOnCastComplete | core.SpellFlagPassiveSpell, DamageMultiplier: 1, ThreatMultiplier: 1, @@ -256,7 +257,7 @@ func init() { SpellSchool: core.SpellSchoolShadow, DefenseType: core.DefenseTypeMagic, ProcMask: core.ProcMaskEmpty, - Flags: core.SpellFlagBinary, + Flags: core.SpellFlagNoOnCastComplete | core.SpellFlagPassiveSpell | core.SpellFlagBinary, DamageMultiplier: 1, ThreatMultiplier: 1, @@ -320,6 +321,7 @@ func init() { SpellSchool: core.SpellSchoolNature, DefenseType: core.DefenseTypeMagic, ProcMask: core.ProcMaskEmpty, + Flags: core.SpellFlagNoOnCastComplete | core.SpellFlagPassiveSpell, DamageMultiplier: 1, ThreatMultiplier: 1, @@ -468,7 +470,7 @@ func init() { } if ppmm.Proc(sim, procMask, "Cobra Fang Claw Extra Attack") { - character.AutoAttacks.ExtraMHAttackProc(sim , 1, core.ActionID{SpellID: 220588}, spell) + character.AutoAttacks.ExtraMHAttackProc(sim, 1, core.ActionID{SpellID: 220588}, spell) } }, }) @@ -482,7 +484,7 @@ func init() { SpellSchool: core.SpellSchoolNature, DefenseType: core.DefenseTypeMagic, ProcMask: core.ProcMaskEmpty, - Flags: core.SpellFlagPoison, + Flags: core.SpellFlagPoison | core.SpellFlagNoOnCastComplete | core.SpellFlagPassiveSpell, DamageMultiplier: 1, ThreatMultiplier: 1, diff --git a/sim/common/vanilla/item_effects.go b/sim/common/vanilla/item_effects.go index 394856cd91..5ca6c6cdf0 100644 --- a/sim/common/vanilla/item_effects.go +++ b/sim/common/vanilla/item_effects.go @@ -160,7 +160,7 @@ func init() { dot.Snapshot(target, 30, isRollover) }, OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { - result := dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTickCounted) + result := dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTick) enemyDamageTaken[target.UnitIndex] += result.Damage }, }, @@ -241,7 +241,7 @@ func init() { }, }) }) - + mightOfShahram := character.GetOrRegisterSpell(core.SpellConfig{ ActionID: core.ActionID{SpellID: 16600}, SpellSchool: core.SpellSchoolArcane, @@ -261,11 +261,11 @@ func init() { ProcMask: core.ProcMaskEmpty, ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { counter := 0 - + for counter < 10 { fistOfShahramAura := character.GetOrRegisterAura(core.Aura{ ActionID: core.ActionID{SpellID: 16601}, - Label: fmt.Sprintf("Fist of Shahram (%d)", counter), + Label: fmt.Sprintf("Fist of Shahram (%d)", counter), Duration: time.Second * 8, OnGain: func(aura *core.Aura, sim *core.Simulation) { character.MultiplyAttackSpeed(sim, 1.3) @@ -274,15 +274,15 @@ func init() { character.MultiplyAttackSpeed(sim, 1/(1.3)) }, }) - + if !fistOfShahramAura.IsActive() { - fistOfShahramAura.Activate(sim) - break + fistOfShahramAura.Activate(sim) + break } - + counter += 1 - - } + + } }, }) @@ -292,7 +292,7 @@ func init() { SpellSchool: core.SpellSchoolArcane, DefenseType: core.DefenseTypeMagic, ProcMask: core.ProcMaskEmpty, - Flags: core.SpellFlagIgnoreAttackerModifiers, + Flags: core.SpellFlagIgnoreAttackerModifiers | core.SpellFlagNoOnCastComplete | core.SpellFlagPassiveSpell, Hot: core.DotConfig{ Aura: core.Aura{ Label: "Blessing of Shahram", @@ -349,7 +349,7 @@ func init() { SpellSchool: core.SpellSchoolFire, DefenseType: core.DefenseTypeMagic, ProcMask: core.ProcMaskEmpty, - Flags: core.SpellFlagIgnoreAttackerModifiers, + Flags: core.SpellFlagIgnoreAttackerModifiers | core.SpellFlagNoOnCastComplete | core.SpellFlagPassiveSpell, DamageMultiplier: 1, ThreatMultiplier: 1, ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { @@ -484,7 +484,7 @@ func init() { }, OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { - dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTickCounted) + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTick) }, }, @@ -535,7 +535,7 @@ func init() { }, OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { - dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTickCounted) + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTick) }, }, DamageMultiplier: 1, @@ -543,7 +543,6 @@ func init() { ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { result := spell.CalcAndDealOutcome(sim, target, spell.OutcomeMeleeSpecialHit) if result.Landed() { - spell.SpellMetrics[result.Target.UnitIndex].Hits-- spell.Dot(target).Apply(sim) } }, @@ -735,7 +734,7 @@ func init() { DamageMultiplier: 1, ThreatMultiplier: 1, ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { - character.AutoAttacks.ExtraMHAttackProc(sim , 1, core.ActionID{SpellID: 18797}, spell) + character.AutoAttacks.ExtraMHAttackProc(sim, 1, core.ActionID{SpellID: 18797}, spell) }, }) }) @@ -813,7 +812,7 @@ func init() { dot.Snapshot(target, 8, isRollover) }, OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { - dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTickCounted) + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTick) }, }, @@ -881,7 +880,7 @@ func init() { dot.Snapshot(target, 55, isRollover) }, OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { - dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTickCounted) + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTick) }, }, @@ -926,7 +925,7 @@ func init() { dot.Snapshot(target, 8, isRollover) }, OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { - dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTickCounted) + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTick) }, }, }) @@ -1102,7 +1101,7 @@ func init() { DamageMultiplier: 1, ThreatMultiplier: 1, ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { - character.AutoAttacks.ExtraMHAttackProc(sim , 2, core.ActionID{SpellID: 15494}, spell) + character.AutoAttacks.ExtraMHAttackProc(sim, 2, core.ActionID{SpellID: 15494}, spell) }, }) }) @@ -1211,6 +1210,7 @@ func init() { SpellSchool: core.SpellSchoolPhysical, DefenseType: core.DefenseTypeMelee, ProcMask: core.ProcMaskMeleeMHSpecial, + Flags: core.SpellFlagNoOnCastComplete | core.SpellFlagPassiveSpell, DamageMultiplier: 1, BonusCoefficient: 1, @@ -1370,7 +1370,7 @@ func init() { debuffAuras.Get(target).Activate(sim) }, OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { - dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTickCounted) + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTick) }, }, DamageMultiplier: 1, @@ -1379,7 +1379,6 @@ func init() { for _, aoeTarget := range sim.Encounter.TargetUnits { result := spell.CalcAndDealOutcome(sim, aoeTarget, spell.OutcomeMagicHit) if result.Landed() { - spell.SpellMetrics[result.Target.UnitIndex].Hits-- spell.Dot(aoeTarget).Apply(sim) } } @@ -1412,7 +1411,7 @@ func init() { dot.Snapshot(target, 8, isRollover) }, OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { - dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTickCounted) + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTick) }, }, }) @@ -1466,7 +1465,7 @@ func init() { dot.Snapshot(target, 2, isRollover) }, OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { - result := dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTickCounted) + result := dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTick) character.GainHealth(sim, result.Damage, healthMetrics) }, }, @@ -1490,6 +1489,7 @@ func init() { SpellSchool: core.SpellSchoolNature, DefenseType: core.DefenseTypeMagic, ProcMask: core.ProcMaskEmpty, + Flags: core.SpellFlagNoOnCastComplete | core.SpellFlagPassiveSpell, BonusCoefficient: 0.1, DamageMultiplier: 1, ThreatMultiplier: 1, @@ -1544,6 +1544,7 @@ func init() { SpellSchool: core.SpellSchoolFire, DefenseType: core.DefenseTypeMagic, ProcMask: core.ProcMaskEmpty, + Flags: core.SpellFlagNoOnCastComplete | core.SpellFlagPassiveSpell, BonusCoefficient: .025, DamageMultiplier: 1, @@ -1588,6 +1589,7 @@ func init() { SpellSchool: core.SpellSchoolFire, DefenseType: core.DefenseTypeMagic, ProcMask: core.ProcMaskEmpty, + Flags: core.SpellFlagNoOnCastComplete | core.SpellFlagPassiveSpell, DamageMultiplier: 1, ThreatMultiplier: 1, @@ -1857,7 +1859,7 @@ func init() { DamageMultiplier: 1, ThreatMultiplier: 1, ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { - character.AutoAttacks.ExtraMHAttackProc(sim , 1, core.ActionID{SpellID: 21919}, spell) + character.AutoAttacks.ExtraMHAttackProc(sim, 1, core.ActionID{SpellID: 21919}, spell) }, }) }) @@ -1978,7 +1980,7 @@ func init() { SpellFlagsExclude: core.SpellFlagSuppressWeaponProcs, PPM: 1.0, Handler: func(sim *core.Simulation, spell *core.Spell, result *core.SpellResult) { - character.AutoAttacks.ExtraMHAttackProc(sim , 1, core.ActionID{SpellID: 461985}, spell) + character.AutoAttacks.ExtraMHAttackProc(sim, 1, core.ActionID{SpellID: 461985}, spell) }, }) }) @@ -2005,7 +2007,7 @@ func init() { }, OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { - dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTickCounted) + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTick) }, }, DamageMultiplier: 1, @@ -2013,7 +2015,6 @@ func init() { ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { result := spell.CalcAndDealOutcome(sim, target, spell.OutcomeMagicHit) if result.Landed() { - spell.SpellMetrics[result.Target.UnitIndex].Hits-- spell.Dot(target).Apply(sim) } }, @@ -2143,6 +2144,7 @@ func init() { ActionID: actionID, SpellSchool: core.SpellSchoolHoly, ProcMask: core.ProcMaskEmpty, + Flags: core.SpellFlagNoOnCastComplete, ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { character.GainHealth(sim, sim.Roll(120, 180), healthMetrics) }, @@ -2173,6 +2175,7 @@ func init() { SpellSchool: core.SpellSchoolNature, DefenseType: core.DefenseTypeMagic, ProcMask: core.ProcMaskEmpty, + Flags: core.SpellFlagNoOnCastComplete | core.SpellFlagPassiveSpell, DamageMultiplier: 1, ThreatMultiplier: 1, @@ -2205,6 +2208,7 @@ func init() { SpellSchool: core.SpellSchoolFire, DefenseType: core.DefenseTypeMagic, ProcMask: core.ProcMaskEmpty, + Flags: core.SpellFlagNoOnCastComplete | core.SpellFlagPassiveSpell, DamageMultiplier: 1, ThreatMultiplier: 1, @@ -2279,7 +2283,7 @@ func init() { } if result.Landed() && spell.ProcMask.Matches(core.ProcMaskMelee) && icd.IsReady(sim) && sim.Proc(0.02, "HandOfJustice") { icd.Use(sim) - aura.Unit.AutoAttacks.ExtraMHAttackProc(sim , 1, core.ActionID{SpellID: 15600}, spell) + aura.Unit.AutoAttacks.ExtraMHAttackProc(sim, 1, core.ActionID{SpellID: 15600}, spell) } }, }) @@ -2388,7 +2392,7 @@ func init() { ActionID: core.ActionID{SpellID: 26470}, SpellSchool: core.SpellSchoolNature, ProcMask: core.ProcMaskSpellHealing, - Flags: core.SpellFlagNoOnCastComplete | core.SpellFlagHelpful, + Flags: core.SpellFlagNoOnCastComplete | core.SpellFlagPassiveSpell | core.SpellFlagHelpful, DamageMultiplier: 1, ThreatMultiplier: 1, @@ -2474,7 +2478,7 @@ func init() { ActionID: core.ActionID{SpellID: 17330}, SpellSchool: core.SpellSchoolNature, ProcMask: core.ProcMaskEmpty, - Flags: core.SpellFlagPoison | core.SpellFlagPureDot, + Flags: core.SpellFlagPoison | core.SpellFlagPureDot | core.SpellFlagNoOnCastComplete | core.SpellFlagPassiveSpell, Cast: core.CastConfig{ CD: core.Cooldown{ Timer: character.NewTimer(), @@ -2491,7 +2495,7 @@ func init() { dot.Snapshot(target, 20, isRollover) }, OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { - dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTickCounted) + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTick) }, }, DamageMultiplier: 1, @@ -2536,6 +2540,7 @@ func init() { SpellSchool: core.SpellSchoolFire, DefenseType: core.DefenseTypeMagic, ProcMask: core.ProcMaskTriggerInstant, + Flags: core.SpellFlagNoOnCastComplete | core.SpellFlagPassiveSpell, DamageMultiplier: 1, ThreatMultiplier: 1, ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { diff --git a/sim/core/attack.go b/sim/core/attack.go index 527917f16f..3ad25941c1 100644 --- a/sim/core/attack.go +++ b/sim/core/attack.go @@ -240,12 +240,12 @@ type WeaponAttack struct { replaceSwing ReplaceMHSwing - swingAt time.Duration - lastSwingAt time.Duration - extraAttacks int32 // extra attacks that happen right away - extraAttacksStored int32 // extra attack that happen on next auto (e.g reckoning) + swingAt time.Duration + lastSwingAt time.Duration + extraAttacks int32 // extra attacks that happen right away + extraAttacksStored int32 // extra attack that happen on next auto (e.g reckoning) extraAttacksPending int32 // extraAttacks prior to previous ones resolving for spell metrics - extraAttacksAura *Aura + extraAttacksAura *Aura curSwingSpeed float64 curSwingDuration time.Duration @@ -270,7 +270,7 @@ func (wa *WeaponAttack) trySwing(sim *Simulation) time.Duration { func (wa *WeaponAttack) castExtraAttacksStored(sim *Simulation) { wa.castExtraAttacks(sim, wa.extraAttacksStored, 0) - wa.extraAttacksStored = 0; + wa.extraAttacksStored = 0 if wa.extraAttacksAura != nil { wa.extraAttacksAura.SetStacks(sim, 0) @@ -298,11 +298,10 @@ func (wa *WeaponAttack) castExtraAttacks(sim *Simulation, numExtraAttacks int32, return true } - return false + return false } - func (wa *WeaponAttack) swing(sim *Simulation) time.Duration { isExtraAttack := wa.extraAttacksPending > 0 @@ -328,7 +327,7 @@ func (wa *WeaponAttack) swing(sim *Simulation) time.Duration { if attackSpell.CanCast(sim, wa.unit.CurrentTarget) { // Update swing timer BEFORE the cast, so that APL checks for TimeToNextAuto behave correctly // if the attack causes APL evaluations (e.g. from rage gain). - + wa.swingAt = sim.CurrentTime + wa.curSwingDuration wa.lastSwingAt = sim.CurrentTime @@ -346,7 +345,7 @@ func (wa *WeaponAttack) swing(sim *Simulation) time.Duration { attackSpell.Cast(sim, wa.unit.CurrentTarget) moreAttacks := !isExtraAttack && wa.extraAttacksPending > 0 // True if above cast is a normal Auto attack that triggered an Extra Attack - wa.castExtraAttacksTriggered(sim, moreAttacks) // more attacks means we don't count the above cast + wa.castExtraAttacksTriggered(sim, moreAttacks) // more attacks means we don't count the above cast if isExtraAttack { // For ranged extra attacks, we have to wait for the spell to hit before resettings the cast time and metrics split @@ -717,9 +716,9 @@ func (aa *AutoAttacks) ExtraMHAttackProc(sim *Simulation, attacks int32, actionI // ExtraMHAttack should be used for all "extra attack" procs in Classic Era versions, including Wild Strikes and Hand of Justice. In vanilla, these procs don't actually grant a full extra attack, but instead just advance the MH swing timer. func (aa *AutoAttacks) ExtraMHAttack(sim *Simulation, attacks int32, actionID ActionID, triggerAction ActionID) { - if attacks == 0 { + if attacks == 0 { return - } + } if sim.Log != nil { aa.mh.unit.Log(sim, "gains %d extra attacks from %s triggered by %s", attacks, actionID, triggerAction) } @@ -730,25 +729,25 @@ func (aa *AutoAttacks) ExtraMHAttack(sim *Simulation, attacks int32, actionID Ac } func (aa *AutoAttacks) StoreExtraMHAttack(sim *Simulation, attacks int32, actionID ActionID, triggerAction ActionID) { - if attacks == 0 { - return - } + if attacks == 0 { + return + } if aa.mh.extraAttacksAura == nil { aa.mh.extraAttacksAura = aa.mh.unit.GetOrRegisterAura(Aura{ - Label: "Extra Attacks", // Tracks Stored Extra Attacks from all sources - ActionID: ActionID{SpellID: 21919}, // Thrash ID + Label: "Extra Attacks", // Tracks Stored Extra Attacks from all sources + ActionID: ActionID{SpellID: 21919}, // Thrash ID Duration: NeverExpires, MaxStacks: 4, // Max is 4 extra attacks stored - more can proc after }) } - if !aa.mh.extraAttacksAura.IsActive() { + if !aa.mh.extraAttacksAura.IsActive() { aa.mh.extraAttacksAura.Activate(sim) } - aa.mh.extraAttacksStored = min(aa.mh.extraAttacksStored + attacks, 4) // Max is 4 stored extra attacks - + aa.mh.extraAttacksStored = min(aa.mh.extraAttacksStored+attacks, 4) // Max is 4 stored extra attacks + aa.mh.extraAttacksAura.SetStacks(sim, aa.mh.extraAttacksStored) if sim.Log != nil { diff --git a/sim/core/aura.go b/sim/core/aura.go index 700cb12b50..6e280769a1 100644 --- a/sim/core/aura.go +++ b/sim/core/aura.go @@ -59,6 +59,7 @@ type Aura struct { startTime time.Duration // Time at which the aura was applied. expires time.Duration // Time at which aura will be removed. + fadeTime time.Duration // Time at which the aura was actually removed. // The unit this aura is attached to. Unit *Unit @@ -134,6 +135,7 @@ func (aura *Aura) reset(sim *Simulation) { panic("Aura nonzero stacks during reset: " + aura.Label) } aura.metrics.reset() + aura.fadeTime = -NeverExpires if aura.OnReset != nil { aura.OnReset(aura, sim) @@ -668,6 +670,7 @@ func (aura *Aura) Deactivate(sim *Simulation) { } aura.expires = 0 + aura.fadeTime = sim.CurrentTime if aura.activeIndex != Inactive { removeActiveIndex := aura.activeIndex aura.Unit.activeAuras = removeBySwappingToBack(aura.Unit.activeAuras, removeActiveIndex) diff --git a/sim/core/buffs.go b/sim/core/buffs.go index 0cb5857780..818b3e9050 100644 --- a/sim/core/buffs.go +++ b/sim/core/buffs.go @@ -1173,7 +1173,7 @@ func RetributionAura(character *Character, points int32) *Aura { ActionID: actionID, SpellSchool: SpellSchoolHoly, ProcMask: ProcMaskEmpty, - Flags: SpellFlagBinary, + Flags: SpellFlagBinary | SpellFlagNoOnCastComplete | SpellFlagPassiveSpell, DamageMultiplier: 1, ThreatMultiplier: 1, @@ -1221,7 +1221,7 @@ func ThornsAura(character *Character, points int32) *Aura { ActionID: actionID, SpellSchool: SpellSchoolNature, ProcMask: ProcMaskEmpty, - Flags: SpellFlagBinary, + Flags: SpellFlagBinary | SpellFlagNoOnCastComplete | SpellFlagPassiveSpell, DamageMultiplier: 1, ThreatMultiplier: 1, @@ -2211,7 +2211,7 @@ func ApplyWildStrikes(character *Character) *Aura { if wsBuffAura.GetStacks() == 2 { wsBuffAura.SetStacks(sim, 1) wsBuffAura.Duration = time.Millisecond * 100 // 100 ms might be generous - could anywhere from 50-150 ms potentially - wsBuffAura.Refresh(sim) // Apply New Duration + wsBuffAura.Refresh(sim) // Apply New Duration } } @@ -2222,8 +2222,8 @@ func ApplyWildStrikes(character *Character) *Aura { wsBuffAura.SetStacks(sim, 2) wsBuffAura.Duration = time.Millisecond * 1500 wsBuffAura.Refresh(sim) // Apply New Duration - aura.Unit.AutoAttacks.ExtraMHAttackProc(sim , 1, buffActionID, spell) - } + aura.Unit.AutoAttacks.ExtraMHAttackProc(sim, 1, buffActionID, spell) + } }, })) diff --git a/sim/core/cast.go b/sim/core/cast.go index 09f8e31748..4e99de6fc1 100644 --- a/sim/core/cast.go +++ b/sim/core/cast.go @@ -199,7 +199,11 @@ func (spell *Spell) makeCastFunc(config CastConfig) CastSuccessFunc { if spell.Flags.Matches(SpellFlagCastTimeNoGCD) { effectiveTime = max(effectiveTime, spell.Unit.GCD.TimeToReady(sim)) } - spell.SpellMetrics[target.UnitIndex].TotalCastTime += effectiveTime + // do not add channeled time here as they have variable cast length + // cast time for channels is handled in dot.OnExpire + if !spell.Flags.Matches(SpellFlagChanneled) { + spell.SpellMetrics[target.UnitIndex].TotalCastTime += effectiveTime + } spell.Unit.SetGCDTimer(sim, sim.CurrentTime+effectiveTime) } diff --git a/sim/core/consumes.go b/sim/core/consumes.go index b34f94871b..ef06a6993b 100644 --- a/sim/core/consumes.go +++ b/sim/core/consumes.go @@ -206,7 +206,8 @@ func registerShadowOil(character *Character, isMh bool, icd Cooldown) { ActionID: ActionID{SpellID: 1382}, SpellSchool: SpellSchoolShadow, DefenseType: DefenseTypeMagic, - ProcMask: ProcMaskSpellDamage, + ProcMask: ProcMaskEmpty, + Flags: SpellFlagNoOnCastComplete | SpellFlagPassiveSpell, DamageMultiplier: 1, ThreatMultiplier: 1, @@ -255,7 +256,8 @@ func registerFrostOil(character *Character, isMh bool) { ActionID: ActionID{SpellID: 1191}, SpellSchool: SpellSchoolFrost, DefenseType: DefenseTypeMagic, - ProcMask: ProcMaskSpellDamage, + ProcMask: ProcMaskEmpty, + Flags: SpellFlagNoOnCastComplete | SpellFlagPassiveSpell, DamageMultiplier: 1, ThreatMultiplier: 1, @@ -392,7 +394,7 @@ func DragonBreathChiliAura(character *Character) *Aura { SpellSchool: SpellSchoolFire, DefenseType: DefenseTypeMagic, ProcMask: ProcMaskEmpty, - Flags: SpellFlagNone, + Flags: SpellFlagNoOnCastComplete | SpellFlagPassiveSpell, DamageMultiplier: 1, ThreatMultiplier: 1, diff --git a/sim/core/dot.go b/sim/core/dot.go index 18fdbeed2e..c8d0e6f2ee 100644 --- a/sim/core/dot.go +++ b/sim/core/dot.go @@ -285,6 +285,8 @@ func newDot(config Dot) *Dot { dot.Spell.Unit.ChanneledDot = nil dot.Spell.Unit.Rotation.interruptChannelIf = nil dot.Spell.Unit.Rotation.allowChannelRecastOnInterrupt = false + // track time metrics for channels + dot.Spell.SpellMetrics[aura.Unit.UnitIndex].TotalCastTime += dot.fadeTime - dot.StartedAt() } }) diff --git a/sim/core/flags.go b/sim/core/flags.go index 95094f6833..f45441c3d4 100644 --- a/sim/core/flags.go +++ b/sim/core/flags.go @@ -188,12 +188,11 @@ const ( SpellFlagCastTimeNoGCD // Indicates this spell is off the GCD (e.g. hunter's Auto Shot) SpellFlagCastWhileCasting // Indicates this spell can be cast while another spell is being cast (e.g. mage's Fire Blast with Overheat rune) SpellFlagPureDot // Indicates this spell is a dot with no initial damage component - - SpellFlagSupressExtraAttack // Mask for Seal of Righteousness, it does not proc Wild Strikes - SpellFlagSuppressWeaponProcs // Indicates this spell cannot proc weapon chance on hits or enchants - SpellFlagSuppressEquipProcs // Indicates this spell cannot proc Equip procs - - SpellFlagBatchStopAttackMacro // Indicates this spell is being cast in a Macro with a stopattack following it + SpellFlagPassiveSpell // Indicates this spell is applied/cast as a result of another spell + SpellFlagSupressExtraAttack // Mask for Seal of Righteousness, it does not proc Wild Strikes + SpellFlagSuppressWeaponProcs // Indicates this spell cannot proc weapon chance on hits or enchants + SpellFlagSuppressEquipProcs // Indicates this spell cannot proc Equip procs + SpellFlagBatchStopAttackMacro // Indicates this spell is being cast in a Macro with a stopattack following it // Used to let agents categorize their spells. SpellFlagAgentReserved1 diff --git a/sim/core/metrics_aggregator.go b/sim/core/metrics_aggregator.go index 305aa58bd5..665f3ee996 100644 --- a/sim/core/metrics_aggregator.go +++ b/sim/core/metrics_aggregator.go @@ -114,7 +114,9 @@ type CharacterIterationMetrics struct { } type ActionMetrics struct { - IsMelee bool // True if melee action, false if spell action. + IsMelee bool // True if melee action, false if spell action. + IsPassive bool // True if action is applied/cast as a result of another action + SpellSchool SpellSchool // Metrics for this action, for each possible target. Targets []TargetedActionMetrics @@ -132,67 +134,122 @@ func (actionMetrics *ActionMetrics) ToProto(actionID ActionID) *proto.ActionMetr } return &proto.ActionMetrics{ - Id: actionID.ToProto(), - IsMelee: actionMetrics.IsMelee, - Targets: targetMetrics, + Id: actionID.ToProto(), + IsMelee: actionMetrics.IsMelee, + IsPassive: actionMetrics.IsPassive, + Targets: targetMetrics, + SpellSchool: int32(actionMetrics.SpellSchool), } } // Metric totals for a spell against a specific target, for the current iteration. type SpellMetrics struct { - Casts int32 - Misses int32 - Hits int32 - Crits int32 - Crushes int32 - Dodges int32 - Glances int32 - Parries int32 - Blocks int32 + Casts int32 + Misses int32 + Hits int32 + ResistedHits int32 + Crits int32 + ResistedCrits int32 + Ticks int32 + ResistedTicks int32 + CritTicks int32 + ResistedCritTicks int32 + Crushes int32 + Dodges int32 + Glances int32 + Parries int32 + Blocks int32 + CritBlocks int32 // Partial or full resists aren't tracked, at the moment, cp. applyResistances() - - TotalDamage float64 // Damage done by all casts of this spell. - TotalThreat float64 // Threat generated by all casts of this spell. - TotalHealing float64 // Healing done by all casts of this spell. - TotalShielding float64 // Shielding done by all casts of this spell. - TotalCastTime time.Duration + TotalDamage float64 // Damage done by all casts of this spell. + TotalResistedDamage float64 // Damage done by all resisted casts of this spell. + TotalCritDamage float64 // Damage done by all critical casts of this spell. + TotalResistedCritDamage float64 // Damage done by all resisted critical casts of this spell. + TotalTickDamage float64 // Damage done by all dots of this spell. + TotalResistedTickDamage float64 // Damage done by all resisted dots of this spell. + TotalCritTickDamage float64 // Damage done by all critical dots of this spell. + TotalResistedCritTickDamage float64 // Damage done by all resisted critical dots of this spell. + TotalGlanceDamage float64 // Damage done by all glance casts of this spell. + TotalBlockDamage float64 // Damage done by all block casts of this spell. + TotalCritBlockDamage float64 // Damage done by all critical block casts of this spell. + TotalThreat float64 // Threat generated by all casts of this spell. + TotalHealing float64 // Healing done by all casts of this spell. + TotalCritHealing float64 // Healing done by all critical casts of this spell. + TotalShielding float64 // Shielding done by all casts of this spell. + TotalCastTime time.Duration } type TargetedActionMetrics struct { - Casts int32 - Hits int32 - Crits int32 - Misses int32 - Dodges int32 - Parries int32 - Blocks int32 - Glances int32 - - Damage float64 - Threat float64 - Healing float64 - Shielding float64 - CastTime time.Duration + Casts int32 + Misses int32 + Hits int32 + ResistedHits int32 + Crits int32 + ResistedCrits int32 + Ticks int32 + ResistedTicks int32 + CritTicks int32 + ResistedCritTicks int32 + Dodges int32 + Glances int32 + Parries int32 + Blocks int32 + CritBlocks int32 + + Damage float64 + ResistedDamage float64 + CritDamage float64 + ResistedCritDamage float64 + TickDamage float64 + ResistedTickDamage float64 + CritTickDamage float64 + ResistedCritTickDamage float64 + GlanceDamage float64 + BlockDamage float64 + CritBlockDamage float64 + Threat float64 + Healing float64 + CritHealing float64 + Shielding float64 + CastTime time.Duration } func (tam *TargetedActionMetrics) ToProto(unitIndex int32) *proto.TargetedActionMetrics { return &proto.TargetedActionMetrics{ UnitIndex: unitIndex, - Casts: tam.Casts, - Hits: tam.Hits, - Crits: tam.Crits, - Misses: tam.Misses, - Dodges: tam.Dodges, - Parries: tam.Parries, - Blocks: tam.Blocks, - Glances: tam.Glances, - Damage: tam.Damage, - Threat: tam.Threat, - Healing: tam.Healing, - Shielding: tam.Shielding, - CastTimeMs: float64(tam.CastTime.Milliseconds()), + Casts: tam.Casts, + Misses: tam.Misses, + Hits: tam.Hits, + ResistedHits: tam.ResistedHits, + Crits: tam.Crits, + ResistedCrits: tam.ResistedCrits, + Ticks: tam.Ticks, + ResistedTicks: tam.ResistedTicks, + CritTicks: tam.CritTicks, + ResistedCritTicks: tam.ResistedCritTicks, + Dodges: tam.Dodges, + Glances: tam.Glances, + Parries: tam.Parries, + Blocks: tam.Blocks, + CritBlocks: tam.CritBlocks, + Damage: tam.Damage, + ResistedDamage: tam.ResistedDamage, + CritDamage: tam.CritDamage, + ResistedCritDamage: tam.ResistedCritDamage, + TickDamage: tam.TickDamage, + ResistedTickDamage: tam.ResistedTickDamage, + CritTickDamage: tam.CritTickDamage, + ResistedCritTickDamage: tam.ResistedCritTickDamage, + GlanceDamage: tam.GlanceDamage, + BlockDamage: tam.BlockDamage, + CritBlockDamage: tam.CritBlockDamage, + Threat: tam.Threat, + Healing: tam.Healing, + CritHealing: tam.CritHealing, + Shielding: tam.Shielding, + CastTimeMs: float64(tam.CastTime.Milliseconds()), } } @@ -308,27 +365,49 @@ func (unitMetrics *UnitMetrics) addSpellMetrics(spell *Spell, actionID ActionID, actionMetrics, ok := unitMetrics.actions[actionID] if !ok { actionMetrics = &ActionMetrics{ - IsMelee: spell.Flags.Matches(SpellFlagMeleeMetrics), - Targets: make([]TargetedActionMetrics, len(spellMetrics)), + IsMelee: spell.Flags.Matches(SpellFlagMeleeMetrics), + IsPassive: spell.Flags.Matches(SpellFlagPassiveSpell), + SpellSchool: spell.SpellSchool, + Targets: make([]TargetedActionMetrics, len(spellMetrics)), } unitMetrics.actions[actionID] = actionMetrics } for i, spellTargetMetrics := range spellMetrics { tam := &actionMetrics.Targets[i] - tam.Casts += spellTargetMetrics.Casts + if !spell.Flags.Matches(SpellFlagPassiveSpell) { + tam.Casts += spellTargetMetrics.Casts + } tam.Misses += spellTargetMetrics.Misses tam.Hits += spellTargetMetrics.Hits + tam.ResistedHits += spellTargetMetrics.ResistedHits tam.Crits += spellTargetMetrics.Crits + tam.ResistedCrits += spellTargetMetrics.ResistedCrits + tam.Ticks += spellTargetMetrics.Ticks + tam.ResistedTicks += spellTargetMetrics.ResistedTicks + tam.CritTicks += spellTargetMetrics.CritTicks + tam.ResistedCritTicks += spellTargetMetrics.ResistedCritTicks tam.Dodges += spellTargetMetrics.Dodges tam.Parries += spellTargetMetrics.Parries tam.Blocks += spellTargetMetrics.Blocks - tam.Glances += spellTargetMetrics.Glances tam.Damage += spellTargetMetrics.TotalDamage + tam.ResistedDamage += spellTargetMetrics.TotalResistedDamage + tam.CritDamage += spellTargetMetrics.TotalCritDamage + tam.ResistedCritDamage += spellTargetMetrics.TotalResistedCritDamage + tam.TickDamage += spellTargetMetrics.TotalTickDamage + tam.ResistedTickDamage += spellTargetMetrics.TotalResistedTickDamage + tam.CritTickDamage += spellTargetMetrics.TotalCritTickDamage + tam.ResistedCritTickDamage += spellTargetMetrics.TotalResistedCritTickDamage + tam.GlanceDamage += spellTargetMetrics.TotalGlanceDamage + tam.BlockDamage += spellTargetMetrics.TotalBlockDamage + tam.CritBlockDamage += spellTargetMetrics.TotalCritBlockDamage tam.Threat += spellTargetMetrics.TotalThreat tam.Healing += spellTargetMetrics.TotalHealing + tam.CritHealing += spellTargetMetrics.TotalCritHealing tam.Shielding += spellTargetMetrics.TotalShielding - tam.CastTime += spellTargetMetrics.TotalCastTime + if !spell.Flags.Matches(SpellFlagPassiveSpell) { + tam.CastTime += spellTargetMetrics.TotalCastTime + } target := spell.Unit.Env.AllUnits[i] target.Metrics.dtps.Total += spellTargetMetrics.TotalDamage diff --git a/sim/core/spell_outcome.go b/sim/core/spell_outcome.go index c067cbd1f4..cfa30a0da5 100644 --- a/sim/core/spell_outcome.go +++ b/sim/core/spell_outcome.go @@ -16,71 +16,81 @@ func (spell *Spell) OutcomeAlwaysHit(_ *Simulation, result *SpellResult, _ *Atta result.Outcome = OutcomeHit spell.SpellMetrics[result.Target.UnitIndex].Hits++ } + +// Hit without Hits++ counter +func (spell *Spell) OutcomeAlwaysHitNoHitCounter(_ *Simulation, result *SpellResult, _ *AttackTable) { + result.Outcome = OutcomeHit +} + func (spell *Spell) OutcomeAlwaysMiss(_ *Simulation, result *SpellResult, _ *AttackTable) { result.Outcome = OutcomeMiss result.Damage = 0 spell.SpellMetrics[result.Target.UnitIndex].Misses++ } -// A tick always hits, but we don't count them as hits in the metrics. func (dot *Dot) OutcomeTick(_ *Simulation, result *SpellResult, _ *AttackTable) { result.Outcome = OutcomeHit -} - -func (dot *Dot) OutcomeTickCounted(_ *Simulation, result *SpellResult, _ *AttackTable) { - result.Outcome = OutcomeHit - dot.Spell.SpellMetrics[result.Target.UnitIndex].Hits++ -} - -func (dot *Dot) OutcomeTickPhysicalCrit(sim *Simulation, result *SpellResult, attackTable *AttackTable) { - if dot.Spell.PhysicalCritCheck(sim, attackTable) { - result.Outcome = OutcomeCrit - result.Damage *= dot.Spell.CritMultiplier(attackTable) - } else { - result.Outcome = OutcomeHit + dot.Spell.SpellMetrics[result.Target.UnitIndex].Ticks++ + if result.DidResist() { + dot.Spell.SpellMetrics[result.Target.UnitIndex].ResistedTicks++ } } -func (dot *Dot) OutcomeTickSnapshotCrit(sim *Simulation, result *SpellResult, attackTable *AttackTable) { - if sim.RandomFloat("Snapshot Crit Roll") < dot.SnapshotCritChance { - result.Outcome = OutcomeCrit - result.Damage *= dot.Spell.CritMultiplier(attackTable) - } else { - result.Outcome = OutcomeHit - } -} +func (dot *Dot) OutcomeTickPhysicalCrit(sim *Simulation, result *SpellResult, attackTable *AttackTable) { + isPartialResist := result.DidResist() -func (dot *Dot) OutcomeTickSnapshotCritCounted(sim *Simulation, result *SpellResult, attackTable *AttackTable) { - if sim.RandomFloat("Snapshot Crit Roll") < dot.SnapshotCritChance { + if dot.Spell.PhysicalCritCheck(sim, attackTable) { result.Outcome = OutcomeCrit result.Damage *= dot.Spell.CritMultiplier(attackTable) - dot.Spell.SpellMetrics[result.Target.UnitIndex].Crits++ + dot.Spell.SpellMetrics[result.Target.UnitIndex].CritTicks++ + if isPartialResist { + dot.Spell.SpellMetrics[result.Target.UnitIndex].ResistedCritTicks++ + } } else { result.Outcome = OutcomeHit - dot.Spell.SpellMetrics[result.Target.UnitIndex].Hits++ + dot.Spell.SpellMetrics[result.Target.UnitIndex].Ticks++ + if isPartialResist { + dot.Spell.SpellMetrics[result.Target.UnitIndex].ResistedTicks++ + } } } func (dot *Dot) OutcomeSnapshotCrit(sim *Simulation, result *SpellResult, attackTable *AttackTable) { + isPartialResist := result.DidResist() + if sim.RandomFloat("Snapshot Crit Roll") < dot.SnapshotCritChance { result.Outcome = OutcomeCrit result.Damage *= dot.Spell.CritMultiplier(attackTable) - dot.Spell.SpellMetrics[result.Target.UnitIndex].Crits++ + dot.Spell.SpellMetrics[result.Target.UnitIndex].CritTicks++ + if isPartialResist { + dot.Spell.SpellMetrics[result.Target.UnitIndex].ResistedCritTicks++ + } } else { result.Outcome = OutcomeHit - dot.Spell.SpellMetrics[result.Target.UnitIndex].Hits++ + dot.Spell.SpellMetrics[result.Target.UnitIndex].Ticks++ + if isPartialResist { + dot.Spell.SpellMetrics[result.Target.UnitIndex].ResistedTicks++ + } } } func (dot *Dot) OutcomeMagicHitAndSnapshotCrit(sim *Simulation, result *SpellResult, attackTable *AttackTable) { if dot.Spell.MagicHitCheck(sim, attackTable) { + isPartialResist := result.DidResist() + if sim.RandomFloat("Snapshot Crit Roll") < dot.SnapshotCritChance { result.Outcome = OutcomeCrit result.Damage *= dot.Spell.CritMultiplier(attackTable) - dot.Spell.SpellMetrics[result.Target.UnitIndex].Crits++ + dot.Spell.SpellMetrics[result.Target.UnitIndex].CritTicks++ + if isPartialResist { + dot.Spell.SpellMetrics[result.Target.UnitIndex].ResistedCritTicks++ + } } else { result.Outcome = OutcomeHit - dot.Spell.SpellMetrics[result.Target.UnitIndex].Hits++ + dot.Spell.SpellMetrics[result.Target.UnitIndex].Ticks++ + if isPartialResist { + dot.Spell.SpellMetrics[result.Target.UnitIndex].ResistedTicks++ + } } } else { result.Outcome = OutcomeMiss @@ -90,14 +100,31 @@ func (dot *Dot) OutcomeMagicHitAndSnapshotCrit(sim *Simulation, result *SpellRes } func (spell *Spell) OutcomeMagicHitAndCrit(sim *Simulation, result *SpellResult, attackTable *AttackTable) { + spell.outcomeMagicHitAndCrit(sim, result, attackTable, true) +} +func (spell *Spell) OutcomeMagicHitAndCritNoHitCounter(sim *Simulation, result *SpellResult, attackTable *AttackTable) { + spell.outcomeMagicHitAndCrit(sim, result, attackTable, false) +} +func (spell *Spell) outcomeMagicHitAndCrit(sim *Simulation, result *SpellResult, attackTable *AttackTable, countHits bool) { + isPartialResist := result.DidResist() if spell.MagicHitCheck(sim, attackTable) { if spell.MagicCritCheck(sim, result.Target) { result.Outcome = OutcomeCrit result.Damage *= spell.CritMultiplier(attackTable) - spell.SpellMetrics[result.Target.UnitIndex].Crits++ + if countHits { + spell.SpellMetrics[result.Target.UnitIndex].Crits++ + if isPartialResist { + spell.SpellMetrics[result.Target.UnitIndex].ResistedCrits++ + } + } } else { result.Outcome = OutcomeHit - spell.SpellMetrics[result.Target.UnitIndex].Hits++ + if countHits { + spell.SpellMetrics[result.Target.UnitIndex].Hits++ + if isPartialResist { + spell.SpellMetrics[result.Target.UnitIndex].ResistedHits++ + } + } } } else { result.Outcome = OutcomeMiss @@ -107,29 +134,76 @@ func (spell *Spell) OutcomeMagicHitAndCrit(sim *Simulation, result *SpellResult, } func (spell *Spell) OutcomeMagicCrit(sim *Simulation, result *SpellResult, attackTable *AttackTable) { + spell.outcomeMagicCrit(sim, result, attackTable, true) +} +func (spell *Spell) OutcomeMagicCritNoHitCounter(sim *Simulation, result *SpellResult, attackTable *AttackTable) { + spell.outcomeMagicCrit(sim, result, attackTable, false) +} +func (spell *Spell) outcomeMagicCrit(sim *Simulation, result *SpellResult, attackTable *AttackTable, countHits bool) { + isPartialResist := result.DidResist() + if spell.MagicCritCheck(sim, result.Target) { result.Outcome = OutcomeCrit result.Damage *= spell.CritMultiplier(attackTable) - spell.SpellMetrics[result.Target.UnitIndex].Crits++ + if countHits { + spell.SpellMetrics[result.Target.UnitIndex].Crits++ + if isPartialResist { + spell.SpellMetrics[result.Target.UnitIndex].ResistedCrits++ + } + } } else { result.Outcome = OutcomeHit - spell.SpellMetrics[result.Target.UnitIndex].Hits++ + if countHits { + spell.SpellMetrics[result.Target.UnitIndex].Hits++ + if isPartialResist { + spell.SpellMetrics[result.Target.UnitIndex].ResistedHits++ + } + } } } func (spell *Spell) OutcomeHealing(_ *Simulation, result *SpellResult, _ *AttackTable) { + spell.outcomeHealing(nil, result, nil, true) +} +func (spell *Spell) OutcomeHealingNoHitCounter(_ *Simulation, result *SpellResult, _ *AttackTable) { + spell.outcomeHealing(nil, result, nil, false) +} +func (spell *Spell) outcomeHealing(_ *Simulation, result *SpellResult, _ *AttackTable, countHits bool) { result.Outcome = OutcomeHit - spell.SpellMetrics[result.Target.UnitIndex].Hits++ + if countHits { + spell.SpellMetrics[result.Target.UnitIndex].Hits++ + if result.DidResist() { + spell.SpellMetrics[result.Target.UnitIndex].ResistedHits++ + } + } } func (spell *Spell) OutcomeHealingCrit(sim *Simulation, result *SpellResult, attackTable *AttackTable) { + spell.outcomeHealingCrit(sim, result, attackTable, true) +} +func (spell *Spell) OutcomeHealingCritNoHitCounter(sim *Simulation, result *SpellResult, attackTable *AttackTable) { + spell.outcomeHealingCrit(sim, result, attackTable, false) +} +func (spell *Spell) outcomeHealingCrit(sim *Simulation, result *SpellResult, attackTable *AttackTable, countHits bool) { + isPartialResist := result.DidResist() + if spell.HealingCritCheck(sim) { result.Outcome = OutcomeCrit result.Damage *= spell.CritMultiplier(attackTable) - spell.SpellMetrics[result.Target.UnitIndex].Crits++ + if countHits { + spell.SpellMetrics[result.Target.UnitIndex].Crits++ + if isPartialResist { + spell.SpellMetrics[result.Target.UnitIndex].ResistedCrits++ + } + } } else { result.Outcome = OutcomeHit - spell.SpellMetrics[result.Target.UnitIndex].Hits++ + if countHits { + spell.SpellMetrics[result.Target.UnitIndex].Hits++ + if isPartialResist { + spell.SpellMetrics[result.Target.UnitIndex].ResistedHits++ + } + } } } @@ -141,10 +215,22 @@ func (spell *Spell) OutcomeTickMagicHit(sim *Simulation, result *SpellResult, at result.Damage = 0 } } + func (spell *Spell) OutcomeMagicHit(sim *Simulation, result *SpellResult, attackTable *AttackTable) { + spell.outcomeMagicHit(sim, result, attackTable, true) +} +func (spell *Spell) OutcomeMagicHitNoHitCounter(sim *Simulation, result *SpellResult, attackTable *AttackTable) { + spell.outcomeMagicHit(sim, result, attackTable, false) +} +func (spell *Spell) outcomeMagicHit(sim *Simulation, result *SpellResult, attackTable *AttackTable, countHits bool) { if spell.MagicHitCheck(sim, attackTable) { result.Outcome = OutcomeHit - spell.SpellMetrics[result.Target.UnitIndex].Hits++ + if countHits { + spell.SpellMetrics[result.Target.UnitIndex].Hits++ + if result.DidResist() { + spell.SpellMetrics[result.Target.UnitIndex].ResistedHits++ + } + } } else { result.Outcome = OutcomeMiss result.Damage = 0 @@ -153,6 +239,12 @@ func (spell *Spell) OutcomeMagicHit(sim *Simulation, result *SpellResult, attack } func (spell *Spell) OutcomeMeleeWhite(sim *Simulation, result *SpellResult, attackTable *AttackTable) { + spell.outcomeMeleeWhite(sim, result, attackTable, true) +} +func (spell *Spell) OutcomeMeleeWhiteNoHitCounter(sim *Simulation, result *SpellResult, attackTable *AttackTable) { + spell.outcomeMeleeWhite(sim, result, attackTable, false) +} +func (spell *Spell) outcomeMeleeWhite(sim *Simulation, result *SpellResult, attackTable *AttackTable, countHits bool) { unit := spell.Unit roll := sim.RandomFloat("White Hit Table") glanceRoll := sim.RandomFloat("White Hit Glancing Penalty") @@ -164,20 +256,26 @@ func (spell *Spell) OutcomeMeleeWhite(sim *Simulation, result *SpellResult, atta !result.applyAttackTableParry(spell, attackTable, roll, &chance) && !result.applyAttackTableGlance(spell, attackTable, roll, &chance, glanceRoll) && !result.applyAttackTableBlock(spell, attackTable, roll, &chance) && - !result.applyAttackTableCrit(spell, attackTable, roll, &chance) { - result.applyAttackTableHit(spell) + !result.applyAttackTableCrit(spell, attackTable, roll, &chance, countHits) { + result.applyAttackTableHit(spell, countHits) } } else { if !result.applyAttackTableMiss(spell, attackTable, roll, &chance) && !result.applyAttackTableDodge(spell, attackTable, roll, &chance) && !result.applyAttackTableGlance(spell, attackTable, roll, &chance, glanceRoll) && - !result.applyAttackTableCrit(spell, attackTable, roll, &chance) { - result.applyAttackTableHit(spell) + !result.applyAttackTableCrit(spell, attackTable, roll, &chance, countHits) { + result.applyAttackTableHit(spell, countHits) } } } func (spell *Spell) OutcomeMeleeSpecialHit(sim *Simulation, result *SpellResult, attackTable *AttackTable) { + spell.outcomeMeleeSpecialHit(sim, result, attackTable, true) +} +func (spell *Spell) OutcomeMeleeSpecialHitNoHitCounter(sim *Simulation, result *SpellResult, attackTable *AttackTable) { + spell.outcomeMeleeSpecialHit(sim, result, attackTable, false) +} +func (spell *Spell) outcomeMeleeSpecialHit(sim *Simulation, result *SpellResult, attackTable *AttackTable, countHits bool) { unit := spell.Unit roll := sim.RandomFloat("White Hit Table") chance := 0.0 @@ -186,17 +284,23 @@ func (spell *Spell) OutcomeMeleeSpecialHit(sim *Simulation, result *SpellResult, if !result.applyAttackTableMissNoDWPenalty(spell, attackTable, roll, &chance) && !result.applyAttackTableDodge(spell, attackTable, roll, &chance) && !result.applyAttackTableParry(spell, attackTable, roll, &chance) { - result.applyAttackTableHit(spell) + result.applyAttackTableHit(spell, countHits) } } else { if !result.applyAttackTableMissNoDWPenalty(spell, attackTable, roll, &chance) && !result.applyAttackTableDodge(spell, attackTable, roll, &chance) { - result.applyAttackTableHit(spell) + result.applyAttackTableHit(spell, countHits) } } } func (spell *Spell) OutcomeMeleeSpecialHitAndCrit(sim *Simulation, result *SpellResult, attackTable *AttackTable) { + spell.outcomeMeleeSpecialHitAndCrit(sim, result, attackTable, true) +} +func (spell *Spell) OutcomeMeleeSpecialHitAndCritNoHitCounter(sim *Simulation, result *SpellResult, attackTable *AttackTable) { + spell.outcomeMeleeSpecialHitAndCrit(sim, result, attackTable, false) +} +func (spell *Spell) outcomeMeleeSpecialHitAndCrit(sim *Simulation, result *SpellResult, attackTable *AttackTable, countHits bool) { unit := spell.Unit roll := sim.RandomFloat("White Hit Table") chance := 0.0 @@ -205,25 +309,32 @@ func (spell *Spell) OutcomeMeleeSpecialHitAndCrit(sim *Simulation, result *Spell if !result.applyAttackTableMissNoDWPenalty(spell, attackTable, roll, &chance) && !result.applyAttackTableDodge(spell, attackTable, roll, &chance) && !result.applyAttackTableParry(spell, attackTable, roll, &chance) { - if result.applyAttackTableCritSeparateRoll(sim, spell, attackTable) { + if result.applyAttackTableCritSeparateRoll(sim, spell, attackTable, countHits) { result.applyAttackTableBlock(spell, attackTable, roll, &chance) } else { if !result.applyAttackTableBlock(spell, attackTable, roll, &chance) { - result.applyAttackTableHit(spell) + result.applyAttackTableHit(spell, countHits) } } } } else { if !result.applyAttackTableMissNoDWPenalty(spell, attackTable, roll, &chance) && !result.applyAttackTableDodge(spell, attackTable, roll, &chance) && - !result.applyAttackTableCritSeparateRoll(sim, spell, attackTable) { - result.applyAttackTableHit(spell) + !result.applyAttackTableCritSeparateRoll(sim, spell, attackTable, countHits) { + result.applyAttackTableHit(spell, countHits) } } } -// Like OutcomeMeleeSpecialHitAndCrit, but blocks prevent crits (all weapon damage based attacks). func (spell *Spell) OutcomeMeleeWeaponSpecialHitAndCrit(sim *Simulation, result *SpellResult, attackTable *AttackTable) { + spell.outcomeMeleeWeaponSpecialHitAndCrit(sim, result, attackTable, true) +} +func (spell *Spell) OutcomeMeleeWeaponSpecialHitAndCritNoHitCounter(sim *Simulation, result *SpellResult, attackTable *AttackTable) { + spell.outcomeMeleeWeaponSpecialHitAndCrit(sim, result, attackTable, false) +} + +// Like OutcomeMeleeSpecialHitAndCrit, but blocks prevent crits (all weapon damage based attacks). +func (spell *Spell) outcomeMeleeWeaponSpecialHitAndCrit(sim *Simulation, result *SpellResult, attackTable *AttackTable, countHits bool) { if spell.Unit.PseudoStats.InFrontOfTarget { roll := sim.RandomFloat("White Hit Table") chance := 0.0 @@ -232,15 +343,21 @@ func (spell *Spell) OutcomeMeleeWeaponSpecialHitAndCrit(sim *Simulation, result !result.applyAttackTableDodge(spell, attackTable, roll, &chance) && !result.applyAttackTableParry(spell, attackTable, roll, &chance) && !result.applyAttackTableBlock(spell, attackTable, roll, &chance) && - !result.applyAttackTableCritSeparateRoll(sim, spell, attackTable) { - result.applyAttackTableHit(spell) + !result.applyAttackTableCritSeparateRoll(sim, spell, attackTable, countHits) { + result.applyAttackTableHit(spell, countHits) } } else { - spell.OutcomeMeleeSpecialHitAndCrit(sim, result, attackTable) + spell.outcomeMeleeSpecialHitAndCrit(sim, result, attackTable, countHits) } } func (spell *Spell) OutcomeMeleeWeaponSpecialNoCrit(sim *Simulation, result *SpellResult, attackTable *AttackTable) { + spell.outcomeMeleeWeaponSpecialNoCrit(sim, result, attackTable, true) +} +func (spell *Spell) OutcomeMeleeWeaponSpecialNoCritNoHitCounter(sim *Simulation, result *SpellResult, attackTable *AttackTable) { + spell.outcomeMeleeWeaponSpecialNoCrit(sim, result, attackTable, false) +} +func (spell *Spell) outcomeMeleeWeaponSpecialNoCrit(sim *Simulation, result *SpellResult, attackTable *AttackTable, countHits bool) { unit := spell.Unit roll := sim.RandomFloat("White Hit Table") chance := 0.0 @@ -250,83 +367,126 @@ func (spell *Spell) OutcomeMeleeWeaponSpecialNoCrit(sim *Simulation, result *Spe !result.applyAttackTableDodge(spell, attackTable, roll, &chance) && !result.applyAttackTableParry(spell, attackTable, roll, &chance) && !result.applyAttackTableBlock(spell, attackTable, roll, &chance) { - result.applyAttackTableHit(spell) + result.applyAttackTableHit(spell, countHits) } } else { if !result.applyAttackTableMissNoDWPenalty(spell, attackTable, roll, &chance) && !result.applyAttackTableDodge(spell, attackTable, roll, &chance) { - result.applyAttackTableHit(spell) + result.applyAttackTableHit(spell, countHits) } } } func (spell *Spell) OutcomeMeleeSpecialNoDodgeParry(sim *Simulation, result *SpellResult, attackTable *AttackTable) { + spell.outcomeMeleeSpecialNoDodgeParry(sim, result, attackTable, true) +} +func (spell *Spell) OutcomeMeleeSpecialNoDodgeParryNoHitCounter(sim *Simulation, result *SpellResult, attackTable *AttackTable) { + spell.outcomeMeleeSpecialNoDodgeParry(sim, result, attackTable, false) +} +func (spell *Spell) outcomeMeleeSpecialNoDodgeParry(sim *Simulation, result *SpellResult, attackTable *AttackTable, countHits bool) { roll := sim.RandomFloat("White Hit Table") chance := 0.0 if !result.applyAttackTableMissNoDWPenalty(spell, attackTable, roll, &chance) && !result.applyAttackTableBlock(spell, attackTable, roll, &chance) && - !result.applyAttackTableCritSeparateRoll(sim, spell, attackTable) { - result.applyAttackTableHit(spell) + !result.applyAttackTableCritSeparateRoll(sim, spell, attackTable, countHits) { + result.applyAttackTableHit(spell, countHits) } } func (spell *Spell) OutcomeMeleeSpecialNoBlockDodgeParry(sim *Simulation, result *SpellResult, attackTable *AttackTable) { + spell.outcomeMeleeSpecialNoBlockDodgeParry(sim, result, attackTable, true) +} +func (spell *Spell) OutcomeMeleeSpecialNoBlockDodgeParryNoHitCounter(sim *Simulation, result *SpellResult, attackTable *AttackTable) { + spell.outcomeMeleeSpecialNoBlockDodgeParry(sim, result, attackTable, false) +} +func (spell *Spell) outcomeMeleeSpecialNoBlockDodgeParry(sim *Simulation, result *SpellResult, attackTable *AttackTable, countHits bool) { roll := sim.RandomFloat("White Hit Table") chance := 0.0 if !result.applyAttackTableMissNoDWPenalty(spell, attackTable, roll, &chance) && - !result.applyAttackTableCritSeparateRoll(sim, spell, attackTable) { - result.applyAttackTableHit(spell) + !result.applyAttackTableCritSeparateRoll(sim, spell, attackTable, countHits) { + result.applyAttackTableHit(spell, countHits) } } func (spell *Spell) OutcomeMeleeSpecialNoBlockDodgeParryNoCrit(sim *Simulation, result *SpellResult, attackTable *AttackTable) { + spell.outcomeMeleeSpecialNoBlockDodgeParryNoCrit(sim, result, attackTable, true) +} +func (spell *Spell) OutcomeMeleeSpecialNoBlockDodgeParryNoCritNoHitCounter(sim *Simulation, result *SpellResult, attackTable *AttackTable) { + spell.outcomeMeleeSpecialNoBlockDodgeParryNoCrit(sim, result, attackTable, false) +} +func (spell *Spell) outcomeMeleeSpecialNoBlockDodgeParryNoCrit(sim *Simulation, result *SpellResult, attackTable *AttackTable, countHits bool) { roll := sim.RandomFloat("White Hit Table") chance := 0.0 if !result.applyAttackTableMissNoDWPenalty(spell, attackTable, roll, &chance) { - result.applyAttackTableHit(spell) + result.applyAttackTableHit(spell, countHits) } } func (spell *Spell) OutcomeMeleeSpecialCritOnly(sim *Simulation, result *SpellResult, attackTable *AttackTable) { - if !result.applyAttackTableCritSeparateRoll(sim, spell, attackTable) { - result.applyAttackTableHit(spell) + spell.outcomeMeleeSpecialCritOnly(sim, result, attackTable, true) +} +func (spell *Spell) OutcomeMeleeSpecialCritOnlyNoHitCounter(sim *Simulation, result *SpellResult, attackTable *AttackTable) { + spell.outcomeMeleeSpecialCritOnly(sim, result, attackTable, false) +} +func (spell *Spell) outcomeMeleeSpecialCritOnly(sim *Simulation, result *SpellResult, attackTable *AttackTable, countHits bool) { + if !result.applyAttackTableCritSeparateRoll(sim, spell, attackTable, countHits) { + result.applyAttackTableHit(spell, countHits) } } func (spell *Spell) OutcomeRangedHit(sim *Simulation, result *SpellResult, attackTable *AttackTable) { + spell.outcomeRangedHit(sim, result, attackTable, true) +} +func (spell *Spell) OutcomeRangedHitNoHitCounter(sim *Simulation, result *SpellResult, attackTable *AttackTable) { + spell.outcomeRangedHit(sim, result, attackTable, false) +} +func (spell *Spell) outcomeRangedHit(sim *Simulation, result *SpellResult, attackTable *AttackTable, countHits bool) { roll := sim.RandomFloat("White Hit Table") chance := 0.0 if !result.applyAttackTableMissNoDWPenalty(spell, attackTable, roll, &chance) { - result.applyAttackTableHit(spell) + result.applyAttackTableHit(spell, countHits) } } func (spell *Spell) OutcomeRangedHitAndCrit(sim *Simulation, result *SpellResult, attackTable *AttackTable) { + spell.outcomeRangedHitAndCrit(sim, result, attackTable, true) +} +func (spell *Spell) OutcomeRangedHitAndCritNoHitCounter(sim *Simulation, result *SpellResult, attackTable *AttackTable) { + spell.outcomeRangedHitAndCrit(sim, result, attackTable, false) +} +func (spell *Spell) outcomeRangedHitAndCrit(sim *Simulation, result *SpellResult, attackTable *AttackTable, countHits bool) { roll := sim.RandomFloat("White Hit Table") chance := 0.0 if spell.Unit.PseudoStats.InFrontOfTarget { if !result.applyAttackTableMissNoDWPenalty(spell, attackTable, roll, &chance) { - if result.applyAttackTableCritSeparateRoll(sim, spell, attackTable) { + if result.applyAttackTableCritSeparateRoll(sim, spell, attackTable, countHits) { result.applyAttackTableBlock(spell, attackTable, roll, &chance) } else { if !result.applyAttackTableBlock(spell, attackTable, roll, &chance) { - result.applyAttackTableHit(spell) + result.applyAttackTableHit(spell, countHits) } } } } else { if !result.applyAttackTableMissNoDWPenalty(spell, attackTable, roll, &chance) && - !result.applyAttackTableCritSeparateRoll(sim, spell, attackTable) { - result.applyAttackTableHit(spell) + !result.applyAttackTableCritSeparateRoll(sim, spell, attackTable, countHits) { + result.applyAttackTableHit(spell, countHits) } } } + func (dot *Dot) OutcomeRangedHitAndCritSnapshot(sim *Simulation, result *SpellResult, attackTable *AttackTable) { + dot.outcomeRangedHitAndCritSnapshot(sim, result, attackTable, true) +} +func (dot *Dot) OutcomeRangedHitAndCritSnapshotNoHitCounter(sim *Simulation, result *SpellResult, attackTable *AttackTable) { + dot.outcomeRangedHitAndCritSnapshot(sim, result, attackTable, false) +} +func (dot *Dot) outcomeRangedHitAndCritSnapshot(sim *Simulation, result *SpellResult, attackTable *AttackTable, countHits bool) { roll := sim.RandomFloat("White Hit Table") chance := 0.0 @@ -336,49 +496,67 @@ func (dot *Dot) OutcomeRangedHitAndCritSnapshot(sim *Simulation, result *SpellRe result.applyAttackTableBlock(dot.Spell, attackTable, roll, &chance) } else { if !result.applyAttackTableBlock(dot.Spell, attackTable, roll, &chance) { - result.applyAttackTableHit(dot.Spell) + result.applyAttackTableHit(dot.Spell, countHits) } } } } else { if !result.applyAttackTableMissNoDWPenalty(dot.Spell, attackTable, roll, &chance) && !result.applyAttackTableCritSeparateRollSnapshot(sim, dot, attackTable) { - result.applyAttackTableHit(dot.Spell) + result.applyAttackTableHit(dot.Spell, countHits) } } } func (spell *Spell) OutcomeRangedHitAndCritNoBlock(sim *Simulation, result *SpellResult, attackTable *AttackTable) { + spell.outcomeRangedHitAndCritNoBlock(sim, result, attackTable, true) +} +func (spell *Spell) OutcomeRangedHitAndCritNoBlockNoHitCounter(sim *Simulation, result *SpellResult, attackTable *AttackTable) { + spell.outcomeRangedHitAndCritNoBlock(sim, result, attackTable, false) +} +func (spell *Spell) outcomeRangedHitAndCritNoBlock(sim *Simulation, result *SpellResult, attackTable *AttackTable, countHits bool) { roll := sim.RandomFloat("White Hit Table") chance := 0.0 if !result.applyAttackTableMissNoDWPenalty(spell, attackTable, roll, &chance) && - !result.applyAttackTableCritSeparateRoll(sim, spell, attackTable) { - result.applyAttackTableHit(spell) + !result.applyAttackTableCritSeparateRoll(sim, spell, attackTable, countHits) { + result.applyAttackTableHit(spell, countHits) } } func (spell *Spell) OutcomeRangedCritOnly(sim *Simulation, result *SpellResult, attackTable *AttackTable) { + spell.outcomeRangedCritOnly(sim, result, attackTable, true) +} +func (spell *Spell) OutcomeRangedCritOnlyNoHitCounter(sim *Simulation, result *SpellResult, attackTable *AttackTable) { + spell.outcomeRangedCritOnly(sim, result, attackTable, false) +} +func (spell *Spell) outcomeRangedCritOnly(sim *Simulation, result *SpellResult, attackTable *AttackTable, countHits bool) { // Block already checks for this, but we can skip the RNG roll which is expensive. if spell.Unit.PseudoStats.InFrontOfTarget { roll := sim.RandomFloat("White Hit Table") chance := 0.0 - if result.applyAttackTableCritSeparateRoll(sim, spell, attackTable) { + if result.applyAttackTableCritSeparateRoll(sim, spell, attackTable, countHits) { result.applyAttackTableBlock(spell, attackTable, roll, &chance) } else { if !result.applyAttackTableBlock(spell, attackTable, roll, &chance) { - result.applyAttackTableHit(spell) + result.applyAttackTableHit(spell, countHits) } } } else { - if !result.applyAttackTableCritSeparateRoll(sim, spell, attackTable) { - result.applyAttackTableHit(spell) + if !result.applyAttackTableCritSeparateRoll(sim, spell, attackTable, countHits) { + result.applyAttackTableHit(spell, countHits) } } } func (spell *Spell) OutcomeEnemyMeleeWhite(sim *Simulation, result *SpellResult, attackTable *AttackTable) { + spell.outcomeEnemyMeleeWhite(sim, result, attackTable, true) +} +func (spell *Spell) OutcomeEnemyMeleeWhiteNoHitCounter(sim *Simulation, result *SpellResult, attackTable *AttackTable) { + spell.outcomeEnemyMeleeWhite(sim, result, attackTable, false) +} +func (spell *Spell) outcomeEnemyMeleeWhite(sim *Simulation, result *SpellResult, attackTable *AttackTable, countHits bool) { roll := sim.RandomFloat("Enemy White Hit Table") chance := 0.0 @@ -386,8 +564,8 @@ func (spell *Spell) OutcomeEnemyMeleeWhite(sim *Simulation, result *SpellResult, !result.applyEnemyAttackTableDodge(spell, attackTable, roll, &chance) && !result.applyEnemyAttackTableParry(spell, attackTable, roll, &chance) && !result.applyEnemyAttackTableBlock(spell, attackTable, roll, &chance) && - !result.applyEnemyAttackTableCrit(spell, attackTable, roll, &chance) { - result.applyAttackTableHit(spell) + !result.applyEnemyAttackTableCrit(spell, attackTable, roll, &chance, countHits) { + result.applyAttackTableHit(spell, countHits) } } @@ -477,22 +655,32 @@ func (result *SpellResult) applyAttackTableGlance(spell *Spell, attackTable *Att return false } -func (result *SpellResult) applyAttackTableCrit(spell *Spell, attackTable *AttackTable, roll float64, chance *float64) bool { +func (result *SpellResult) applyAttackTableCrit(spell *Spell, attackTable *AttackTable, roll float64, chance *float64, countHits bool) bool { *chance += spell.PhysicalCritChance(attackTable) if roll < *chance { result.Outcome = OutcomeCrit - spell.SpellMetrics[result.Target.UnitIndex].Crits++ + if countHits { + spell.SpellMetrics[result.Target.UnitIndex].Crits++ + if result.DidResist() { + spell.SpellMetrics[result.Target.UnitIndex].ResistedCrits++ + } + } result.Damage *= spell.CritMultiplier(attackTable) return true } return false } -func (result *SpellResult) applyAttackTableCritSeparateRoll(sim *Simulation, spell *Spell, attackTable *AttackTable) bool { +func (result *SpellResult) applyAttackTableCritSeparateRoll(sim *Simulation, spell *Spell, attackTable *AttackTable, countHits bool) bool { if spell.PhysicalCritCheck(sim, attackTable) { result.Outcome = OutcomeCrit - spell.SpellMetrics[result.Target.UnitIndex].Crits++ + if countHits { + spell.SpellMetrics[result.Target.UnitIndex].Crits++ + if result.DidResist() { + spell.SpellMetrics[result.Target.UnitIndex].ResistedCrits++ + } + } result.Damage *= spell.CritMultiplier(attackTable) return true } @@ -502,15 +690,23 @@ func (result *SpellResult) applyAttackTableCritSeparateRollSnapshot(sim *Simulat if sim.RandomFloat("Physical Crit Roll") < dot.SnapshotCritChance { result.Outcome = OutcomeCrit result.Damage *= dot.Spell.CritMultiplier(attackTable) - dot.Spell.SpellMetrics[result.Target.UnitIndex].Crits++ + dot.Spell.SpellMetrics[result.Target.UnitIndex].CritTicks++ + if result.DidResist() { + dot.Spell.SpellMetrics[result.Target.UnitIndex].ResistedCritTicks++ + } return true } return false } -func (result *SpellResult) applyAttackTableHit(spell *Spell) { +func (result *SpellResult) applyAttackTableHit(spell *Spell, countHits bool) { result.Outcome = OutcomeHit - spell.SpellMetrics[result.Target.UnitIndex].Hits++ + if countHits { + spell.SpellMetrics[result.Target.UnitIndex].Hits++ + if result.DidResist() { + spell.SpellMetrics[result.Target.UnitIndex].ResistedHits++ + } + } } func (result *SpellResult) applyEnemyAttackTableMiss(spell *Spell, attackTable *AttackTable, roll float64, chance *float64) bool { @@ -584,7 +780,7 @@ func (result *SpellResult) applyEnemyAttackTableParry(spell *Spell, attackTable return false } -func (result *SpellResult) applyEnemyAttackTableCrit(spell *Spell, at *AttackTable, roll float64, chance *float64) bool { +func (result *SpellResult) applyEnemyAttackTableCrit(spell *Spell, at *AttackTable, roll float64, chance *float64, countHits bool) bool { // "Base Melee Crit" is set as part of AttackTable critChance := at.BaseCritChance + spell.BonusCritRating/100 // Crit reduction from bonus Defense of target (Talent, Gear, etc) @@ -595,7 +791,12 @@ func (result *SpellResult) applyEnemyAttackTableCrit(spell *Spell, at *AttackTab if roll < *chance { result.Outcome = OutcomeCrit - spell.SpellMetrics[result.Target.UnitIndex].Crits++ + if countHits { + spell.SpellMetrics[result.Target.UnitIndex].Crits++ + if result.DidResist() { + spell.SpellMetrics[result.Target.UnitIndex].ResistedCrits++ + } + } result.Damage *= 2 return true } diff --git a/sim/core/spell_result.go b/sim/core/spell_result.go index 12eb0ac548..435a22d4f4 100644 --- a/sim/core/spell_result.go +++ b/sim/core/spell_result.go @@ -47,10 +47,18 @@ func (result *SpellResult) DidCrit() bool { return result.Outcome.Matches(OutcomeCrit) } +func (result *SpellResult) DidGlance() bool { + return result.Outcome.Matches(OutcomeGlance) +} + func (result *SpellResult) DidBlock() bool { return result.Outcome.Matches(OutcomeBlock) } +func (result *SpellResult) DidResist() bool { + return result.Outcome.Matches(OutcomePartial) +} + func (result *SpellResult) DidParry() bool { return result.Outcome.Matches(OutcomeParry) } @@ -354,8 +362,37 @@ func (spell *Spell) CalcAndDealOutcome(sim *Simulation, target *Unit, outcomeApp // Applies the fully computed spell result to the sim. func (spell *Spell) dealDamageInternal(sim *Simulation, isPeriodic bool, result *SpellResult) { + isPartialResist := result.DidResist() + if sim.CurrentTime >= 0 { spell.SpellMetrics[result.Target.UnitIndex].TotalDamage += result.Damage + if isPeriodic { + spell.SpellMetrics[result.Target.UnitIndex].TotalTickDamage += result.Damage + if isPartialResist { + spell.SpellMetrics[result.Target.UnitIndex].TotalResistedTickDamage += result.Damage + } + } + + if result.DidCrit() { + if result.DidBlock() { + spell.SpellMetrics[result.Target.UnitIndex].TotalCritBlockDamage += result.Damage + } else { + spell.SpellMetrics[result.Target.UnitIndex].TotalCritDamage += result.Damage + if isPartialResist { + spell.SpellMetrics[result.Target.UnitIndex].TotalResistedCritDamage += result.Damage + } + if isPeriodic { + spell.SpellMetrics[result.Target.UnitIndex].TotalCritTickDamage += result.Damage + if isPartialResist { + spell.SpellMetrics[result.Target.UnitIndex].TotalResistedCritTickDamage += result.Damage + } + } + } + } else if result.DidGlance() { + spell.SpellMetrics[result.Target.UnitIndex].TotalGlanceDamage += result.Damage + } else if result.DidBlock() { + spell.SpellMetrics[result.Target.UnitIndex].TotalBlockDamage += result.Damage + } spell.SpellMetrics[result.Target.UnitIndex].TotalThreat += result.Threat } @@ -367,9 +404,9 @@ func (spell *Spell) dealDamageInternal(sim *Simulation, isPeriodic bool, result if sim.Log != nil && !spell.Flags.Matches(SpellFlagNoLogs) { if isPeriodic { - spell.Unit.Log(sim, "%s %s tick %s. (Threat: %0.3f)", result.Target.LogLabel(), spell.ActionID, result.DamageString(), result.Threat) + spell.Unit.Log(sim, "%s %s tick %s (SpellSchool: %d). (Threat: %0.3f)", result.Target.LogLabel(), spell.ActionID, result.DamageString(), spell.SpellSchool, result.Threat) } else { - spell.Unit.Log(sim, "%s %s %s. (Threat: %0.3f)", result.Target.LogLabel(), spell.ActionID, result.DamageString(), result.Threat) + spell.Unit.Log(sim, "%s %s %s (SpellSchool: %d). (Threat: %0.3f)", result.Target.LogLabel(), spell.ActionID, result.DamageString(), spell.SpellSchool, result.Threat) } } @@ -461,6 +498,9 @@ func (dot *Dot) SnapshotHeal(target *Unit, baseHealing float64, isRollover bool) // Applies the fully computed spell result to the sim. func (spell *Spell) dealHealingInternal(sim *Simulation, isPeriodic bool, result *SpellResult) { + if result.DidCrit() { + spell.SpellMetrics[result.Target.UnitIndex].TotalCritHealing += result.Damage + } spell.SpellMetrics[result.Target.UnitIndex].TotalHealing += result.Damage spell.SpellMetrics[result.Target.UnitIndex].TotalThreat += result.Threat if result.Target.HasHealthBar() { diff --git a/sim/druid/hurricane.go b/sim/druid/hurricane.go index 3e85562d39..9cb8f97f8a 100644 --- a/sim/druid/hurricane.go +++ b/sim/druid/hurricane.go @@ -53,7 +53,7 @@ func (druid *Druid) registerHurricaneSpell() { Rank: i + 1, ManaCost: core.ManaCostOptions{ - FlatCost: rank.manaCost, + FlatCost: rank.manaCost, Multiplier: costMultiplier, }, Cast: core.CastConfig{ @@ -81,7 +81,7 @@ func (druid *Druid) registerHurricaneSpell() { }, OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { for _, aoeTarget := range sim.Encounter.TargetUnits { - dot.CalcAndDealPeriodicSnapshotDamage(sim, aoeTarget, dot.OutcomeTickCounted) + dot.CalcAndDealPeriodicSnapshotDamage(sim, aoeTarget, dot.OutcomeTick) } }, }, diff --git a/sim/druid/insect_swarm.go b/sim/druid/insect_swarm.go index 53ce35f3c4..813be6a280 100644 --- a/sim/druid/insect_swarm.go +++ b/sim/druid/insect_swarm.go @@ -75,7 +75,7 @@ func (druid *Druid) registerInsectSwarmSpell() { } }, OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { - dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTickSnapshotCritCounted) + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeSnapshotCrit) }, }, @@ -83,7 +83,6 @@ func (druid *Druid) registerInsectSwarmSpell() { result := spell.CalcOutcome(sim, target, spell.OutcomeMagicHit) if result.Landed() { spell.Dot(target).Apply(sim) - spell.SpellMetrics[result.Target.UnitIndex].Hits-- } spell.DealOutcome(sim, result) }, diff --git a/sim/druid/moonfire.go b/sim/druid/moonfire.go index 61bdeca375..b8b9d40661 100644 --- a/sim/druid/moonfire.go +++ b/sim/druid/moonfire.go @@ -81,7 +81,7 @@ func (druid *Druid) getMoonfireBaseConfig(rank int) core.SpellConfig { } }, OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { - dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTickSnapshotCrit) + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeSnapshotCrit) }, }, diff --git a/sim/druid/rake.go b/sim/druid/rake.go index bc3eb1c060..0681145dcb 100644 --- a/sim/druid/rake.go +++ b/sim/druid/rake.go @@ -106,9 +106,9 @@ func (druid *Druid) newRakeSpellConfig(rakeRank RakeRankInfo) core.SpellConfig { }, OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { if has4PCenarionCunning { - dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTickSnapshotCritCounted) + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeSnapshotCrit) } else { - dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTickCounted) + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTick) } }, }, diff --git a/sim/druid/rip.go b/sim/druid/rip.go index 8fb0e2ab3e..6f9a743553 100644 --- a/sim/druid/rip.go +++ b/sim/druid/rip.go @@ -122,9 +122,9 @@ func (druid *Druid) newRipSpellConfig(ripRank RipRankInfo) core.SpellConfig { }, OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { if has4PCenarionCunning { - dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTickSnapshotCritCounted) + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeSnapshotCrit) } else { - dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTickCounted) + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTick) } }, }, diff --git a/sim/druid/sunfire.go b/sim/druid/sunfire.go index d3372ceb90..7e2c96e75b 100644 --- a/sim/druid/sunfire.go +++ b/sim/druid/sunfire.go @@ -119,7 +119,7 @@ func (druid *Druid) getSunfireBaseSpellConfig( } }, OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { - dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTickSnapshotCrit) + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeSnapshotCrit) }, }, diff --git a/sim/hunter/explosive_shot.go b/sim/hunter/explosive_shot.go index 5301728297..293f4d7beb 100644 --- a/sim/hunter/explosive_shot.go +++ b/sim/hunter/explosive_shot.go @@ -61,7 +61,7 @@ func (hunter *Hunter) registerExplosiveShotSpell() { dot.Snapshot(target, baseDamage, isRollover) }, OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { - dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTickSnapshotCrit) + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeSnapshotCrit) }, }, diff --git a/sim/hunter/explosive_trap.go b/sim/hunter/explosive_trap.go index b4e930ddf3..a71f298559 100644 --- a/sim/hunter/explosive_trap.go +++ b/sim/hunter/explosive_trap.go @@ -24,7 +24,7 @@ func (hunter *Hunter) getExplosiveTrapConfig(rank int, timer *core.Timer) core.S SpellSchool: core.SpellSchoolFire, DefenseType: core.DefenseTypeMagic, ProcMask: core.ProcMaskSpellDamage, - Flags: core.SpellFlagAPL | SpellFlagTrap, + Flags: core.SpellFlagAPL | core.SpellFlagPassiveSpell | SpellFlagTrap, Rank: rank, RequiredLevel: level, MissileSpeed: 24, diff --git a/sim/hunter/immolation_trap.go b/sim/hunter/immolation_trap.go index 44ba34082d..ffe8a919f0 100644 --- a/sim/hunter/immolation_trap.go +++ b/sim/hunter/immolation_trap.go @@ -21,7 +21,7 @@ func (hunter *Hunter) getImmolationTrapConfig(rank int, timer *core.Timer) core. SpellSchool: core.SpellSchoolFire, DefenseType: core.DefenseTypeMagic, ProcMask: core.ProcMaskSpellDamage, - Flags: core.SpellFlagAPL | SpellFlagTrap, + Flags: core.SpellFlagAPL | core.SpellFlagPassiveSpell | SpellFlagTrap, Rank: rank, RequiredLevel: level, MissileSpeed: 24, @@ -61,7 +61,7 @@ func (hunter *Hunter) getImmolationTrapConfig(rank int, timer *core.Timer) core. dot.Snapshot(target, tickDamage, isRollover) }, OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { - dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTickCounted) + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTick) }, }, @@ -70,7 +70,6 @@ func (hunter *Hunter) getImmolationTrapConfig(rank int, timer *core.Timer) core. spell.WaitTravelTime(sim, func(s *core.Simulation) { spell.DealOutcome(sim, result) if result.Landed() { - spell.SpellMetrics[target.UnitIndex].Hits-- spell.Dot(target).Apply(sim) } diff --git a/sim/hunter/serpent_sting.go b/sim/hunter/serpent_sting.go index 54fb061a87..1d2ab50472 100644 --- a/sim/hunter/serpent_sting.go +++ b/sim/hunter/serpent_sting.go @@ -55,7 +55,7 @@ func (hunter *Hunter) getSerpentStingConfig(rank int) core.SpellConfig { dot.Snapshot(target, baseDamage, isRollover) }, OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { - dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTickCounted) + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTick) }, }, @@ -66,7 +66,6 @@ func (hunter *Hunter) getSerpentStingConfig(rank int) core.SpellConfig { spell.DealOutcome(sim, result) if result.Landed() { - spell.SpellMetrics[target.UnitIndex].Hits-- spell.Dot(target).Apply(sim) } }) @@ -81,7 +80,7 @@ func (hunter *Hunter) chimeraShotSerpentStingSpell(rank int) *core.Spell { SpellSchool: core.SpellSchoolNature, DefenseType: core.DefenseTypeRanged, ProcMask: core.ProcMaskEmpty, - Flags: core.SpellFlagMeleeMetrics, + Flags: core.SpellFlagMeleeMetrics | core.SpellFlagPassiveSpell, BonusCritRating: 1 * float64(hunter.Talents.LethalShots) * core.CritRatingPerCritChance, // This is added manually here because spell uses ProcMaskEmpty diff --git a/sim/hunter/wyvern_strike.go b/sim/hunter/wyvern_strike.go index c6f5f6b147..6761403242 100644 --- a/sim/hunter/wyvern_strike.go +++ b/sim/hunter/wyvern_strike.go @@ -58,7 +58,7 @@ func (hunter *Hunter) getWyvernStrikeConfig(rank int) core.SpellConfig { dot.Snapshot(target, tickDamage, isRollover) }, OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { - dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTickCounted) + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTick) }, }, diff --git a/sim/mage/TestArcane.results b/sim/mage/TestArcane.results index 8db065e361..bba80388fb 100644 --- a/sim/mage/TestArcane.results +++ b/sim/mage/TestArcane.results @@ -53,18 +53,18 @@ stat_weights_results: { weights: 0 weights: 0 weights: 0 - weights: 3.04655 + weights: -1.23816 weights: 0 - weights: 1.22913 - weights: 1.12404 - weights: 0.1051 + weights: 1.34405 + weights: 1.22961 + weights: 0.11444 weights: 0 weights: 0 weights: 0 weights: 0 weights: 0 weights: 0 - weights: 14.97247 + weights: 16.86698 weights: 0 weights: 0 weights: 0 @@ -99,57 +99,57 @@ stat_weights_results: { dps_results: { key: "TestArcane-Lvl60-AllItems-BloodGuard'sDreadweave" value: { - dps: 997.43225 - tps: 1011.97656 + dps: 997.98022 + tps: 1009.35147 } } dps_results: { key: "TestArcane-Lvl60-AllItems-BloodGuard'sSatin" value: { - dps: 901.9103 - tps: 916.11191 + dps: 902.07709 + tps: 912.78413 } } dps_results: { key: "TestArcane-Lvl60-AllItems-EmeraldEnchantedVestments" value: { - dps: 992.52706 - tps: 1006.88677 + dps: 993.04678 + tps: 1004.3609 } } dps_results: { key: "TestArcane-Lvl60-AllItems-EmeraldWovenGarb" value: { - dps: 901.9103 - tps: 916.12257 + dps: 901.73152 + tps: 912.79788 } } dps_results: { key: "TestArcane-Lvl60-AllItems-IronweaveBattlesuit" value: { - dps: 467.63169 - tps: 481.78665 + dps: 467.79054 + tps: 481.94289 } } dps_results: { key: "TestArcane-Lvl60-AllItems-Knight-Lieutenant'sDreadweave" value: { - dps: 997.43225 - tps: 1011.97656 + dps: 997.98022 + tps: 1009.35147 } } dps_results: { key: "TestArcane-Lvl60-AllItems-KnightLieutenant'sSatin" value: { - dps: 901.9103 - tps: 916.11191 + dps: 902.07709 + tps: 912.78413 } } dps_results: { key: "TestArcane-Lvl60-AllItems-MalevolentProphet'sVestments" value: { - dps: 1633.19535 - tps: 1651.21175 + dps: 1709.48261 + tps: 1726.11918 } } dps_results: { @@ -162,98 +162,98 @@ dps_results: { dps_results: { key: "TestArcane-Lvl60-Average-Default" value: { - dps: 2023.46193 - tps: 2042.17397 + dps: 2211.06874 + tps: 2229.04046 } } dps_results: { key: "TestArcane-Lvl60-Settings-Gnome-p4_arcane-Arcane-p4_arcane-FullBuffs-Phase 3 Consumes-LongMultiTarget" value: { - dps: 2158.88636 - tps: 2532.34505 + dps: 2218.58429 + tps: 2526.25182 } } dps_results: { key: "TestArcane-Lvl60-Settings-Gnome-p4_arcane-Arcane-p4_arcane-FullBuffs-Phase 3 Consumes-LongSingleTarget" value: { - dps: 2158.88636 - tps: 2177.55929 + dps: 2218.58429 + tps: 2233.96766 } } dps_results: { key: "TestArcane-Lvl60-Settings-Gnome-p4_arcane-Arcane-p4_arcane-FullBuffs-Phase 3 Consumes-ShortSingleTarget" value: { - dps: 2425.82541 - tps: 2450.65431 + dps: 2428.95391 + tps: 2445.77192 } } dps_results: { key: "TestArcane-Lvl60-Settings-Gnome-p4_arcane-Arcane-p4_arcane-NoBuffs-Phase 3 Consumes-LongMultiTarget" value: { - dps: 909.31002 - tps: 1170.56155 + dps: 1070.23871 + tps: 1314.87534 } } dps_results: { key: "TestArcane-Lvl60-Settings-Gnome-p4_arcane-Arcane-p4_arcane-NoBuffs-Phase 3 Consumes-LongSingleTarget" value: { - dps: 909.31002 - tps: 922.3726 + dps: 1070.23871 + tps: 1082.47054 } } dps_results: { key: "TestArcane-Lvl60-Settings-Gnome-p4_arcane-Arcane-p4_arcane-NoBuffs-Phase 3 Consumes-ShortSingleTarget" value: { - dps: 1289.70878 - tps: 1309.01703 + dps: 1290.4412 + tps: 1305.85215 } } dps_results: { key: "TestArcane-Lvl60-Settings-Troll-p4_arcane-Arcane-p4_arcane-FullBuffs-Phase 3 Consumes-LongMultiTarget" value: { - dps: 2073.92183 - tps: 2447.65742 + dps: 2210.02502 + tps: 2527.93221 } } dps_results: { key: "TestArcane-Lvl60-Settings-Troll-p4_arcane-Arcane-p4_arcane-FullBuffs-Phase 3 Consumes-LongSingleTarget" value: { - dps: 2073.92183 - tps: 2092.60861 + dps: 2210.02502 + tps: 2225.92038 } } dps_results: { key: "TestArcane-Lvl60-Settings-Troll-p4_arcane-Arcane-p4_arcane-FullBuffs-Phase 3 Consumes-ShortSingleTarget" value: { - dps: 2447.28075 - tps: 2472.96788 + dps: 2450.39072 + tps: 2467.35281 } } dps_results: { key: "TestArcane-Lvl60-Settings-Troll-p4_arcane-Arcane-p4_arcane-NoBuffs-Phase 3 Consumes-LongMultiTarget" value: { - dps: 904.72601 - tps: 1169.61679 + dps: 1065.82679 + tps: 1310.99517 } } dps_results: { key: "TestArcane-Lvl60-Settings-Troll-p4_arcane-Arcane-p4_arcane-NoBuffs-Phase 3 Consumes-LongSingleTarget" value: { - dps: 904.72601 - tps: 917.97055 + dps: 1065.82679 + tps: 1078.08521 } } dps_results: { key: "TestArcane-Lvl60-Settings-Troll-p4_arcane-Arcane-p4_arcane-NoBuffs-Phase 3 Consumes-ShortSingleTarget" value: { - dps: 1302.99223 - tps: 1323.00729 + dps: 1303.85396 + tps: 1319.36998 } } dps_results: { key: "TestArcane-Lvl60-SwitchInFrontOfTarget-Default" value: { - dps: 2035.23085 - tps: 2053.74553 + dps: 2201.5491 + tps: 2218.71932 } } diff --git a/sim/mage/TestFire.results b/sim/mage/TestFire.results index 4b4fd0dc48..25cdec8a39 100644 --- a/sim/mage/TestFire.results +++ b/sim/mage/TestFire.results @@ -200,7 +200,7 @@ stat_weights_results: { weights: 0 weights: 0 weights: 0 - weights: 0.53133 + weights: 0.59166 weights: 0 weights: 1.33659 weights: 0 @@ -211,7 +211,7 @@ stat_weights_results: { weights: 0 weights: 0 weights: 0 - weights: 10.87853 + weights: 10.84172 weights: 0 weights: 0 weights: 0 @@ -260,7 +260,7 @@ stat_weights_results: { weights: 0 weights: 0 weights: 0 - weights: 43.92047 + weights: 43.91938 weights: 0 weights: 0 weights: 0 @@ -393,99 +393,99 @@ dps_results: { dps_results: { key: "TestFire-Lvl50-Average-Default" value: { - dps: 1274.07355 - tps: 910.26347 + dps: 1267.10515 + tps: 905.38558 } } dps_results: { key: "TestFire-Lvl50-Settings-Gnome-p3_fire-Fire-p3_fire-FullBuffs-Phase 3 Consumes-LongMultiTarget" value: { - dps: 2720.84173 - tps: 2280.38103 + dps: 2698.01907 + tps: 2264.40517 } } dps_results: { key: "TestFire-Lvl50-Settings-Gnome-p3_fire-Fire-p3_fire-FullBuffs-Phase 3 Consumes-LongSingleTarget" value: { - dps: 1287.87093 - tps: 920.0121 + dps: 1279.44523 + tps: 914.11411 } } dps_results: { key: "TestFire-Lvl50-Settings-Gnome-p3_fire-Fire-p3_fire-FullBuffs-Phase 3 Consumes-ShortSingleTarget" value: { - dps: 1312.93559 - tps: 936.61549 + dps: 1307.34326 + tps: 932.70085 } } dps_results: { key: "TestFire-Lvl50-Settings-Gnome-p3_fire-Fire-p3_fire-NoBuffs-Phase 3 Consumes-LongMultiTarget" value: { - dps: 1118.58326 - tps: 1057.28275 + dps: 1112.73616 + tps: 1053.18978 } } dps_results: { key: "TestFire-Lvl50-Settings-Gnome-p3_fire-Fire-p3_fire-NoBuffs-Phase 3 Consumes-LongSingleTarget" value: { - dps: 580.33121 - tps: 420.383 + dps: 577.02059 + tps: 418.06556 } } dps_results: { key: "TestFire-Lvl50-Settings-Gnome-p3_fire-Fire-p3_fire-NoBuffs-Phase 3 Consumes-ShortSingleTarget" value: { - dps: 770.24435 - tps: 564.42241 + dps: 766.74351 + tps: 561.97182 } } dps_results: { key: "TestFire-Lvl50-Settings-Troll-p3_fire-Fire-p3_fire-FullBuffs-Phase 3 Consumes-LongMultiTarget" value: { - dps: 2681.17535 - tps: 2247.91101 + dps: 2659.89623 + tps: 2233.01562 } } dps_results: { key: "TestFire-Lvl50-Settings-Troll-p3_fire-Fire-p3_fire-FullBuffs-Phase 3 Consumes-LongSingleTarget" value: { - dps: 1275.42603 - tps: 911.28122 + dps: 1267.73597 + tps: 905.89818 } } dps_results: { key: "TestFire-Lvl50-Settings-Troll-p3_fire-Fire-p3_fire-FullBuffs-Phase 3 Consumes-ShortSingleTarget" value: { - dps: 1351.11503 - tps: 968.76607 + dps: 1342.70288 + tps: 962.87757 } } dps_results: { key: "TestFire-Lvl50-Settings-Troll-p3_fire-Fire-p3_fire-NoBuffs-Phase 3 Consumes-LongMultiTarget" value: { - dps: 1107.42871 - tps: 1049.22869 + dps: 1101.57696 + tps: 1045.13247 } } dps_results: { key: "TestFire-Lvl50-Settings-Troll-p3_fire-Fire-p3_fire-NoBuffs-Phase 3 Consumes-LongSingleTarget" value: { - dps: 558.8104 - tps: 405.24518 + dps: 555.81695 + tps: 403.14976 } } dps_results: { key: "TestFire-Lvl50-Settings-Troll-p3_fire-Fire-p3_fire-NoBuffs-Phase 3 Consumes-ShortSingleTarget" value: { - dps: 770.92829 - tps: 564.85804 + dps: 767.37202 + tps: 562.36865 } } dps_results: { key: "TestFire-Lvl50-SwitchInFrontOfTarget-Default" value: { - dps: 1277.04486 - tps: 912.55635 + dps: 1270.1017 + tps: 907.69614 } } dps_results: { @@ -554,8 +554,8 @@ dps_results: { dps_results: { key: "TestFire-Lvl60-Average-Default" value: { - dps: 2851.43101 - tps: 1829.2497 + dps: 2851.43035 + tps: 1829.24924 } } dps_results: { @@ -603,8 +603,8 @@ dps_results: { dps_results: { key: "TestFire-Lvl60-Settings-Troll-p4_fire-Fire-p4_fire-FullBuffs-Phase 3 Consumes-LongMultiTarget" value: { - dps: 4046.39192 - tps: 3198.11391 + dps: 4046.38719 + tps: 3198.11061 } } dps_results: { @@ -624,8 +624,8 @@ dps_results: { dps_results: { key: "TestFire-Lvl60-Settings-Troll-p4_fire-Fire-p4_fire-NoBuffs-Phase 3 Consumes-LongMultiTarget" value: { - dps: 1352.79013 - tps: 1225.64001 + dps: 1352.78731 + tps: 1225.63803 } } dps_results: { diff --git a/sim/mage/TestFrost.results b/sim/mage/TestFrost.results index 37c24cd5d7..6c0de7f93b 100644 --- a/sim/mage/TestFrost.results +++ b/sim/mage/TestFrost.results @@ -102,7 +102,7 @@ stat_weights_results: { weights: 0 weights: 0 weights: 0 - weights: 0.20896 + weights: 0.21856 weights: 0 weights: 1.17675 weights: 0 @@ -113,7 +113,7 @@ stat_weights_results: { weights: 0 weights: 0 weights: 0 - weights: 11.60677 + weights: 11.41427 weights: 0 weights: 0 weights: 0 @@ -197,99 +197,99 @@ stat_weights_results: { dps_results: { key: "TestFrost-Lvl50-Average-Default" value: { - dps: 1083.71577 - tps: 851.6141 + dps: 1079.19759 + tps: 848.13972 } } dps_results: { key: "TestFrost-Lvl50-Settings-Gnome-p3_frost_ffb-Frost-p3_frost-FullBuffs-Phase 3 Consumes-LongMultiTarget" value: { - dps: 1069.28377 - tps: 1138.87995 + dps: 1063.64716 + tps: 1134.59901 } } dps_results: { key: "TestFrost-Lvl50-Settings-Gnome-p3_frost_ffb-Frost-p3_frost-FullBuffs-Phase 3 Consumes-LongSingleTarget" value: { - dps: 1069.28377 - tps: 840.46361 + dps: 1063.64716 + tps: 836.18267 } } dps_results: { key: "TestFrost-Lvl50-Settings-Gnome-p3_frost_ffb-Frost-p3_frost-FullBuffs-Phase 3 Consumes-ShortSingleTarget" value: { - dps: 1131.94695 - tps: 878.48652 + dps: 1128.88105 + tps: 876.07045 } } dps_results: { key: "TestFrost-Lvl50-Settings-Gnome-p3_frost_ffb-Frost-p3_frost-NoBuffs-Phase 3 Consumes-LongMultiTarget" value: { - dps: 569.47013 - tps: 650.25415 + dps: 567.02766 + tps: 648.41377 } } dps_results: { key: "TestFrost-Lvl50-Settings-Gnome-p3_frost_ffb-Frost-p3_frost-NoBuffs-Phase 3 Consumes-LongSingleTarget" value: { - dps: 569.47013 - tps: 435.57758 + dps: 567.02766 + tps: 433.7372 } } dps_results: { key: "TestFrost-Lvl50-Settings-Gnome-p3_frost_ffb-Frost-p3_frost-NoBuffs-Phase 3 Consumes-ShortSingleTarget" value: { - dps: 623.20164 - tps: 476.10731 + dps: 621.75726 + tps: 474.98212 } } dps_results: { key: "TestFrost-Lvl50-Settings-Troll-p3_frost_ffb-Frost-p3_frost-FullBuffs-Phase 3 Consumes-LongMultiTarget" value: { - dps: 1067.20328 - tps: 1136.92578 + dps: 1061.78939 + tps: 1132.80754 } } dps_results: { key: "TestFrost-Lvl50-Settings-Troll-p3_frost_ffb-Frost-p3_frost-FullBuffs-Phase 3 Consumes-LongSingleTarget" value: { - dps: 1067.20328 - tps: 838.53639 + dps: 1061.78939 + tps: 834.41814 } } dps_results: { key: "TestFrost-Lvl50-Settings-Troll-p3_frost_ffb-Frost-p3_frost-FullBuffs-Phase 3 Consumes-ShortSingleTarget" value: { - dps: 1135.47222 - tps: 881.1592 + dps: 1131.97445 + tps: 878.41879 } } dps_results: { key: "TestFrost-Lvl50-Settings-Troll-p3_frost_ffb-Frost-p3_frost-NoBuffs-Phase 3 Consumes-LongMultiTarget" value: { - dps: 569.98338 - tps: 641.07171 + dps: 567.51744 + tps: 639.21726 } } dps_results: { key: "TestFrost-Lvl50-Settings-Troll-p3_frost_ffb-Frost-p3_frost-NoBuffs-Phase 3 Consumes-LongSingleTarget" value: { - dps: 569.98338 - tps: 435.18302 + dps: 567.51744 + tps: 433.32857 } } dps_results: { key: "TestFrost-Lvl50-Settings-Troll-p3_frost_ffb-Frost-p3_frost-NoBuffs-Phase 3 Consumes-ShortSingleTarget" value: { - dps: 627.83586 - tps: 484.19052 + dps: 626.27342 + tps: 482.98662 } } dps_results: { key: "TestFrost-Lvl50-SwitchInFrontOfTarget-Default" value: { - dps: 1070.52558 - tps: 841.11536 + dps: 1065.34806 + tps: 837.17892 } } dps_results: { @@ -386,15 +386,15 @@ dps_results: { dps_results: { key: "TestFrost-Lvl60-Settings-Gnome-p4_frost-Frost-p4_frost-NoBuffs-Phase 3 Consumes-LongMultiTarget" value: { - dps: 972.19785 - tps: 1115.352 + dps: 972.19514 + tps: 1115.35011 } } dps_results: { key: "TestFrost-Lvl60-Settings-Gnome-p4_frost-Frost-p4_frost-NoBuffs-Phase 3 Consumes-LongSingleTarget" value: { - dps: 972.19785 - tps: 790.3881 + dps: 972.19514 + tps: 790.3862 } } dps_results: { diff --git a/sim/mage/ignite.go b/sim/mage/ignite.go index 61533e6676..7904897583 100644 --- a/sim/mage/ignite.go +++ b/sim/mage/ignite.go @@ -45,7 +45,7 @@ func (mage *Mage) applyIgnite() { SpellSchool: core.SpellSchoolFire, DefenseType: core.DefenseTypeMagic, ProcMask: core.ProcMaskProc, - Flags: SpellFlagMage, + Flags: core.SpellFlagNoOnCastComplete | core.SpellFlagPassiveSpell | SpellFlagMage, DamageMultiplier: 1, ThreatMultiplier: 1, @@ -61,12 +61,11 @@ func (mage *Mage) applyIgnite() { NumberOfTicks: IgniteTicks, TickLength: time.Second * 2, OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { - dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTickCounted) + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTick) }, }, ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { - spell.SpellMetrics[target.UnitIndex].Hits++ spell.Dot(target).ApplyOrReset(sim) }, }) diff --git a/sim/mage/living_bomb.go b/sim/mage/living_bomb.go index 793e1037ce..d19a485103 100644 --- a/sim/mage/living_bomb.go +++ b/sim/mage/living_bomb.go @@ -32,7 +32,7 @@ func (mage *Mage) registerLivingBombSpell() { SpellSchool: core.SpellSchoolFire, DefenseType: core.DefenseTypeMagic, ProcMask: core.ProcMaskSpellDamage, - Flags: SpellFlagMage, + Flags: SpellFlagMage | core.SpellFlagPassiveSpell, DamageMultiplier: 1, ThreatMultiplier: 1, @@ -79,7 +79,7 @@ func (mage *Mage) registerLivingBombSpell() { dot.Snapshot(target, baseDotDamage, isRollover) }, OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { - dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTickCounted) + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTick) }, }, @@ -88,7 +88,6 @@ func (mage *Mage) registerLivingBombSpell() { if result.Landed() { spell.Dot(target).Apply(sim) } - spell.SpellMetrics[target.UnitIndex].Hits -= 1 }, }) } diff --git a/sim/mage/living_flame.go b/sim/mage/living_flame.go index 3c9ace9059..1122a95714 100644 --- a/sim/mage/living_flame.go +++ b/sim/mage/living_flame.go @@ -70,7 +70,7 @@ func (mage *Mage) registerLivingFlameSpell() { }, OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { for _, aoeTarget := range sim.Encounter.TargetUnits { - dot.CalcAndDealPeriodicSnapshotDamage(sim, aoeTarget, dot.OutcomeTickCounted) + dot.CalcAndDealPeriodicSnapshotDamage(sim, aoeTarget, dot.OutcomeTick) } }, }, diff --git a/sim/paladin/judgement.go b/sim/paladin/judgement.go index 4f5aa17614..6601687513 100644 --- a/sim/paladin/judgement.go +++ b/sim/paladin/judgement.go @@ -15,10 +15,10 @@ func (paladin *Paladin) registerJudgement() { ActionID: core.ActionID{SpellID: 20271}, SpellSchool: core.SpellSchoolHoly, ProcMask: core.ProcMaskEmpty, - Flags: core.SpellFlagMeleeMetrics | core.SpellFlagAPL, + Flags: core.SpellFlagMeleeMetrics | core.SpellFlagAPL | core.SpellFlagNoOnCastComplete | core.SpellFlagPassiveSpell, ManaCost: core.ManaCostOptions{ - BaseCost: 0.06, + BaseCost: 0.06, Multiplier: paladin.benediction(), }, Cast: core.CastConfig{ diff --git a/sim/paladin/righteous_vengeance.go b/sim/paladin/righteous_vengeance.go index e82403d8f6..3baf19c56a 100644 --- a/sim/paladin/righteous_vengeance.go +++ b/sim/paladin/righteous_vengeance.go @@ -33,7 +33,7 @@ func (paladin *Paladin) registerRV() { SpellSchool: core.SpellSchoolHoly, DefenseType: core.DefenseTypeMelee, ProcMask: core.ProcMaskSpellDamage, - Flags: core.SpellFlagPureDot | core.SpellFlagIgnoreAttackerModifiers, + Flags: core.SpellFlagPureDot | core.SpellFlagIgnoreAttackerModifiers | core.SpellFlagNoOnCastComplete | core.SpellFlagPassiveSpell, // SpellFlagIgnoreTargetModifiers was thought to be used based on wowhead flags // WCL parses show that this is not the case @@ -52,12 +52,11 @@ func (paladin *Paladin) registerRV() { NumberOfTicks: RVTicks, TickLength: time.Second * 2, OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { - dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTickCounted) + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTick) }, }, ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { - spell.SpellMetrics[target.UnitIndex].Hits++ spell.Dot(target).ApplyOrReset(sim) }, }) diff --git a/sim/paladin/soc.go b/sim/paladin/soc.go index bfcb2b1162..a1ae7dc65f 100644 --- a/sim/paladin/soc.go +++ b/sim/paladin/soc.go @@ -70,7 +70,7 @@ func (paladin *Paladin) registerSealOfCommand() { SpellSchool: core.SpellSchoolHoly, DefenseType: core.DefenseTypeMelee, ProcMask: core.ProcMaskMeleeMHSpecial, - Flags: core.SpellFlagMeleeMetrics | SpellFlag_RV, + Flags: core.SpellFlagMeleeMetrics | core.SpellFlagNoOnCastComplete | core.SpellFlagPassiveSpell | SpellFlag_RV, SpellCode: SpellCode_PaladinJudgementOfCommand, // used in judgement.go @@ -137,7 +137,7 @@ func (paladin *Paladin) registerSealOfCommand() { Rank: i + 1, ManaCost: core.ManaCostOptions{ - FlatCost: rank.manaCost - paladin.getLibramSealCostReduction(), + FlatCost: rank.manaCost - paladin.getLibramSealCostReduction(), Multiplier: paladin.benediction(), }, Cast: core.CastConfig{ diff --git a/sim/paladin/som.go b/sim/paladin/som.go index aaa8f81bd3..d5ecd1f830 100644 --- a/sim/paladin/som.go +++ b/sim/paladin/som.go @@ -21,7 +21,7 @@ func (paladin *Paladin) registerSealOfMartyrdom() { SpellSchool: core.SpellSchoolHoly, DefenseType: core.DefenseTypeMelee, ProcMask: core.ProcMaskMeleeMHSpecial, - Flags: core.SpellFlagMeleeMetrics | SpellFlag_RV, + Flags: core.SpellFlagMeleeMetrics | core.SpellFlagNoOnCastComplete | core.SpellFlagPassiveSpell | SpellFlag_RV, DamageMultiplier: 0.85 * paladin.getWeaponSpecializationModifier() * paladin.improvedSoR(), ThreatMultiplier: 1, @@ -76,7 +76,7 @@ func (paladin *Paladin) registerSealOfMartyrdom() { Flags: core.SpellFlagAPL, ManaCost: core.ManaCostOptions{ - FlatCost: paladin.BaseMana*0.04 - paladin.getLibramSealCostReduction(), + FlatCost: paladin.BaseMana*0.04 - paladin.getLibramSealCostReduction(), Multiplier: paladin.benediction(), }, Cast: core.CastConfig{ diff --git a/sim/priest/devouring_plague.go b/sim/priest/devouring_plague.go index 7d8ae052bd..2287f15736 100644 --- a/sim/priest/devouring_plague.go +++ b/sim/priest/devouring_plague.go @@ -86,9 +86,9 @@ func (priest *Priest) getDevouringPlagueConfig(rank int, cdTimer *core.Timer) co }, OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { if hasDespairRune { - dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTickSnapshotCritCounted) + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeSnapshotCrit) } else { - dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTickCounted) + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTick) } }, }, @@ -96,7 +96,6 @@ func (priest *Priest) getDevouringPlagueConfig(rank int, cdTimer *core.Timer) co ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { result := spell.CalcOutcome(sim, target, spell.OutcomeMagicHit) if result.Landed() { - spell.SpellMetrics[target.UnitIndex].Hits-- priest.AddShadowWeavingStack(sim, target) spell.Dot(target).Apply(sim) } diff --git a/sim/priest/holy_fire.go b/sim/priest/holy_fire.go index 95d0773514..68f24bd506 100644 --- a/sim/priest/holy_fire.go +++ b/sim/priest/holy_fire.go @@ -87,7 +87,7 @@ func (priest *Priest) getHolyFireConfig(rank int) core.SpellConfig { OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { if hasDespairRune { - dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTickSnapshotCrit) + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeSnapshotCrit) } else { dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTick) } diff --git a/sim/priest/mind_flay.go b/sim/priest/mind_flay.go index 77994ed8a3..37c1e6aeb1 100644 --- a/sim/priest/mind_flay.go +++ b/sim/priest/mind_flay.go @@ -99,9 +99,9 @@ func (priest *Priest) newMindFlaySpellConfig(rank int, tickIdx int32) core.Spell }, OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { if hasDespairRune { - dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTickSnapshotCritCounted) + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeSnapshotCrit) } else { - dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTickCounted) + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTick) } }, }, @@ -109,7 +109,6 @@ func (priest *Priest) newMindFlaySpellConfig(rank int, tickIdx int32) core.Spell ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { result := spell.CalcOutcome(sim, target, spell.OutcomeMagicHit) if result.Landed() { - spell.SpellMetrics[target.UnitIndex].Hits-- priest.AddShadowWeavingStack(sim, target) spell.Dot(target).Apply(sim) } diff --git a/sim/priest/penance.go b/sim/priest/penance.go index fdb7050c5d..328c97eb9c 100644 --- a/sim/priest/penance.go +++ b/sim/priest/penance.go @@ -86,7 +86,6 @@ func (priest *Priest) makePenanceSpell(isHeal bool) *core.Spell { ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { if isHeal { - spell.SpellMetrics[target.UnitIndex].Hits-- hot := spell.Hot(target) hot.Apply(sim) // Do immediate tick @@ -94,7 +93,6 @@ func (priest *Priest) makePenanceSpell(isHeal bool) *core.Spell { } else { result := spell.CalcOutcome(sim, target, spell.OutcomeMagicHit) if result.Landed() { - spell.SpellMetrics[target.UnitIndex].Hits-- dot := spell.Dot(target) dot.Apply(sim) // Do immediate tick diff --git a/sim/priest/renew.go b/sim/priest/renew.go index 9d60a23f0d..fbe2c9bddf 100644 --- a/sim/priest/renew.go +++ b/sim/priest/renew.go @@ -38,7 +38,6 @@ package priest // }, // ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { -// spell.SpellMetrics[target.UnitIndex].Hits++ // spell.Hot(target).Apply(sim) // if priest.EmpoweredRenew != nil { diff --git a/sim/priest/shadow_word_pain.go b/sim/priest/shadow_word_pain.go index a9134c5133..58230dcda4 100644 --- a/sim/priest/shadow_word_pain.go +++ b/sim/priest/shadow_word_pain.go @@ -89,9 +89,9 @@ func (priest *Priest) getShadowWordPainConfig(rank int) core.SpellConfig { }, OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { if hasDespairRune { - dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTickSnapshotCritCounted) + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeSnapshotCrit) } else { - dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTickCounted) + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTick) } }, }, @@ -103,7 +103,6 @@ func (priest *Priest) getShadowWordPainConfig(rank int) core.SpellConfig { } for _, result := range results { if result.Landed() { - spell.SpellMetrics[result.Target.UnitIndex].Hits-- priest.AddShadowWeavingStack(sim, result.Target) spell.Dot(result.Target).Apply(sim) } diff --git a/sim/priest/vampiric_touch.go b/sim/priest/vampiric_touch.go index 032a878f6e..09a77143e6 100644 --- a/sim/priest/vampiric_touch.go +++ b/sim/priest/vampiric_touch.go @@ -86,9 +86,9 @@ func (priest *Priest) registerVampiricTouchSpell() { }, OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { if hasDespairRune { - dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTickSnapshotCritCounted) + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeSnapshotCrit) } else { - dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTickCounted) + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTick) } }, }, @@ -96,7 +96,6 @@ func (priest *Priest) registerVampiricTouchSpell() { ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { result := spell.CalcOutcome(sim, target, spell.OutcomeMagicHit) if result.Landed() { - spell.SpellMetrics[target.UnitIndex].Hits-- priest.AddShadowWeavingStack(sim, target) spell.Dot(target).Apply(sim) } diff --git a/sim/priest/void_plague.go b/sim/priest/void_plague.go index 9b025f6a2b..6fd83f7aba 100644 --- a/sim/priest/void_plague.go +++ b/sim/priest/void_plague.go @@ -65,9 +65,9 @@ func (priest *Priest) registerVoidPlagueSpell() { }, OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { if hasDespairRune { - dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTickSnapshotCritCounted) + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeSnapshotCrit) } else { - dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTickCounted) + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTick) } }, }, @@ -75,7 +75,6 @@ func (priest *Priest) registerVoidPlagueSpell() { ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { result := spell.CalcOutcome(sim, target, spell.OutcomeMagicHit) if result.Landed() { - spell.SpellMetrics[target.UnitIndex].Hits-- priest.AddShadowWeavingStack(sim, target) spell.Dot(target).Apply(sim) } diff --git a/sim/priest/void_zone.go b/sim/priest/void_zone.go index 60c0d12466..ea032e25a0 100644 --- a/sim/priest/void_zone.go +++ b/sim/priest/void_zone.go @@ -62,9 +62,9 @@ func (priest *Priest) registerVoidZoneSpell() { OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { for _, aoeTarget := range sim.Encounter.TargetUnits { if hasDespairRune { - dot.CalcAndDealPeriodicSnapshotDamage(sim, aoeTarget, dot.OutcomeTickSnapshotCritCounted) + dot.CalcAndDealPeriodicSnapshotDamage(sim, aoeTarget, dot.OutcomeSnapshotCrit) } else { - dot.CalcAndDealPeriodicSnapshotDamage(sim, aoeTarget, dot.OutcomeTickCounted) + dot.CalcAndDealPeriodicSnapshotDamage(sim, aoeTarget, dot.OutcomeTick) } } }, diff --git a/sim/rogue/crimson_tempest.go b/sim/rogue/crimson_tempest.go index 2e3cd86a0b..aabf9418a7 100644 --- a/sim/rogue/crimson_tempest.go +++ b/sim/rogue/crimson_tempest.go @@ -2,7 +2,7 @@ package rogue import ( "time" - + "github.com/wowsims/sod/sim/core" "github.com/wowsims/sod/sim/core/proto" "github.com/wowsims/sod/sim/core/stats" @@ -11,7 +11,7 @@ import ( func (rogue *Rogue) makeCrimsonTempestHitSpell() *core.Spell { actionID := core.ActionID{SpellID: 436611} procMask := core.ProcMaskMeleeMHSpecial - activate2PcBonuses := rogue.HasSetBonus(ItemSetNightSlayerBattlearmor, 2) && rogue.HasAura("Blade Dance") && rogue.HasRune(proto.RogueRune_RuneJustAFleshWound) + activate2PcBonuses := rogue.HasSetBonus(ItemSetNightSlayerBattlearmor, 2) && rogue.HasAura("Blade Dance") && rogue.HasRune(proto.RogueRune_RuneJustAFleshWound) return rogue.RegisterSpell(core.SpellConfig{ ActionID: actionID, @@ -60,7 +60,7 @@ func (rogue *Rogue) registerCrimsonTempestSpell() { // Must be updated to match combo points spent rogue.CrimsonTempestBleed = rogue.makeCrimsonTempestHitSpell() - activate2PcBonuses := rogue.HasSetBonus(ItemSetNightSlayerBattlearmor, 2) && rogue.HasAura("Blade Dance") && rogue.HasRune(proto.RogueRune_RuneJustAFleshWound) + activate2PcBonuses := rogue.HasSetBonus(ItemSetNightSlayerBattlearmor, 2) && rogue.HasAura("Blade Dance") && rogue.HasRune(proto.RogueRune_RuneJustAFleshWound) rogue.CrimsonTempest = rogue.RegisterSpell(core.SpellConfig{ ActionID: core.ActionID{SpellID: 412096}, @@ -94,8 +94,7 @@ func (rogue *Rogue) registerCrimsonTempestSpell() { } func (rogue *Rogue) CrimsonTempestDamage(comboPoints int32) float64 { - tickDamageValues := []float64{0, 0.3, 0.45, 0.6, 0.75, 0.9} - tickDamage := tickDamageValues[comboPoints] * rogue.GetStat(stats.AttackPower)/float64(comboPoints+1) - return tickDamage + tickDamageValues := []float64{0, 0.3, 0.45, 0.6, 0.75, 0.9} + tickDamage := tickDamageValues[comboPoints] * rogue.GetStat(stats.AttackPower) / float64(comboPoints+1) + return tickDamage } - diff --git a/sim/rogue/fan_of_knives.go b/sim/rogue/fan_of_knives.go index 8db94a55ad..a7dd4b79a2 100644 --- a/sim/rogue/fan_of_knives.go +++ b/sim/rogue/fan_of_knives.go @@ -10,19 +10,17 @@ import ( const FanOfKnivesSpellID int32 = 409240 func (rogue *Rogue) makeFanOfKnivesWeaponHitSpell(isMH bool) *core.Spell { - var procMask core.ProcMask - var weaponMultiplier float64 - var actionID core.ActionID - activate2PcBonuses := rogue.HasSetBonus(ItemSetNightSlayerBattlearmor, 2) && rogue.HasAura("Blade Dance") && rogue.HasRune(proto.RogueRune_RuneJustAFleshWound) - if isMH { - actionID = core.ActionID{SpellID: FanOfKnivesSpellID}.WithTag(1) - weaponMultiplier = core.TernaryFloat64(rogue.HasDagger(core.MainHand), 0.75, 0.5) - procMask = core.ProcMaskMeleeMHSpecial - } else { - actionID = core.ActionID{SpellID: FanOfKnivesSpellID}.WithTag(2) - weaponMultiplier = core.TernaryFloat64(rogue.HasDagger(core.OffHand), 0.75, 0.5) - weaponMultiplier *= rogue.dwsMultiplier() + actionID := core.ActionID{SpellID: FanOfKnivesSpellID}.WithTag(1) + procMask := core.ProcMaskMeleeMHSpecial + flags := core.SpellFlagMeleeMetrics | SpellFlagColdBlooded + weaponMultiplier := core.TernaryFloat64(rogue.HasDagger(core.MainHand), 0.75, 0.5) + activate2PcBonuses := rogue.HasSetBonus(ItemSetNightSlayerBattlearmor, 2) && rogue.HasAura("Blade Dance") && rogue.HasRune(proto.RogueRune_RuneJustAFleshWound) + + if !isMH { + actionID.Tag = 2 procMask = core.ProcMaskMeleeOHSpecial + flags |= core.SpellFlagNoOnCastComplete | core.SpellFlagPassiveSpell + weaponMultiplier = core.TernaryFloat64(rogue.HasDagger(core.OffHand), 0.75, 0.5) * rogue.dwsMultiplier() } return rogue.RegisterSpell(core.SpellConfig{ @@ -30,7 +28,7 @@ func (rogue *Rogue) makeFanOfKnivesWeaponHitSpell(isMH bool) *core.Spell { SpellSchool: core.SpellSchoolPhysical, DefenseType: core.DefenseTypeMelee, ProcMask: procMask, - Flags: core.SpellFlagMeleeMetrics | SpellFlagColdBlooded, + Flags: flags, DamageMultiplier: weaponMultiplier, ThreatMultiplier: core.TernaryFloat64(activate2PcBonuses, 2, 1), @@ -43,7 +41,7 @@ func (rogue *Rogue) registerFanOfKnives() { return } - activate2PcBonuses := rogue.HasSetBonus(ItemSetNightSlayerBattlearmor, 2) && rogue.HasAura("Blade Dance") && rogue.HasRune(proto.RogueRune_RuneJustAFleshWound) + activate2PcBonuses := rogue.HasSetBonus(ItemSetNightSlayerBattlearmor, 2) && rogue.HasAura("Blade Dance") && rogue.HasRune(proto.RogueRune_RuneJustAFleshWound) mhSpell := rogue.makeFanOfKnivesWeaponHitSpell(true) ohSpell := rogue.makeFanOfKnivesWeaponHitSpell(false) results := make([]*core.SpellResult, len(rogue.Env.Encounter.TargetUnits)) @@ -69,7 +67,7 @@ func (rogue *Rogue) registerFanOfKnives() { for i, aoeTarget := range sim.Encounter.TargetUnits { baseDamage := ohSpell.Unit.OHWeaponDamage(sim, ohSpell.MeleeAttackPower()) baseDamage *= sim.Encounter.AOECapMultiplier() - results[i] = ohSpell.CalcDamage(sim, aoeTarget, baseDamage, ohSpell.OutcomeMeleeSpecialHit) + results[i] = ohSpell.CalcDamage(sim, aoeTarget, baseDamage, ohSpell.OutcomeMeleeSpecialHitAndCrit) } for i := range sim.Encounter.TargetUnits { ohSpell.DealDamage(sim, results[i]) @@ -78,7 +76,7 @@ func (rogue *Rogue) registerFanOfKnives() { for i, aoeTarget := range sim.Encounter.TargetUnits { baseDamage := mhSpell.Unit.MHWeaponDamage(sim, mhSpell.MeleeAttackPower()) baseDamage *= sim.Encounter.AOECapMultiplier() - results[i] = mhSpell.CalcDamage(sim, aoeTarget, baseDamage, mhSpell.OutcomeMeleeSpecialHit) + results[i] = mhSpell.CalcDamage(sim, aoeTarget, baseDamage, mhSpell.OutcomeMeleeSpecialHitAndCrit) } for i := range sim.Encounter.TargetUnits { mhSpell.DealDamage(sim, results[i]) diff --git a/sim/rogue/mutilate.go b/sim/rogue/mutilate.go index e17c46dcdf..a469662b11 100644 --- a/sim/rogue/mutilate.go +++ b/sim/rogue/mutilate.go @@ -23,7 +23,7 @@ func (rogue *Rogue) newMutilateHitSpell(isMH bool) *core.Spell { SpellSchool: core.SpellSchoolPhysical, DefenseType: core.DefenseTypeMelee, ProcMask: procMask, - Flags: SpellFlagBuilder | SpellFlagColdBlooded | SpellFlagCarnage | core.SpellFlagMeleeMetrics, + Flags: core.SpellFlagMeleeMetrics | core.SpellFlagNoOnCastComplete | core.SpellFlagPassiveSpell | SpellFlagBuilder | SpellFlagColdBlooded | SpellFlagCarnage, BonusCritRating: 10 * core.CritRatingPerCritChance * float64(rogue.Talents.ImprovedBackstab), diff --git a/sim/rogue/poisons.go b/sim/rogue/poisons.go index 5c3d986c67..e6725c9930 100644 --- a/sim/rogue/poisons.go +++ b/sim/rogue/poisons.go @@ -278,7 +278,7 @@ func (rogue *Rogue) registerDeadlyPoisonSpell() { SpellSchool: core.SpellSchoolNature, DefenseType: core.DefenseTypeMagic, ProcMask: core.ProcMaskWeaponProc, - Flags: SpellFlagCarnage | core.SpellFlagPoison | SpellFlagRoguePoison, + Flags: core.SpellFlagPoison | core.SpellFlagPassiveSpell | SpellFlagRoguePoison | SpellFlagCarnage, DamageMultiplier: rogue.getPoisonDamageMultiplier(), ThreatMultiplier: 1, @@ -310,7 +310,7 @@ func (rogue *Rogue) registerDeadlyPoisonSpell() { }, OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { - dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTickCounted) + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTick) }, }, }) @@ -396,7 +396,7 @@ func (rogue *Rogue) registerOccultPoisonSpell() { // }, // OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { - // dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTickCounted) + // dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTick) // }, // }, // }) @@ -444,7 +444,7 @@ func (rogue *Rogue) makeInstantPoison(procSource PoisonProcSource) *core.Spell { SpellSchool: core.SpellSchoolNature, DefenseType: core.DefenseTypeMagic, ProcMask: core.ProcMaskWeaponProc, - Flags: SpellFlagDeadlyBrewed | SpellFlagCarnage | core.SpellFlagPoison | SpellFlagRoguePoison, + Flags: core.SpellFlagPoison | core.SpellFlagPassiveSpell | SpellFlagDeadlyBrewed | SpellFlagCarnage | SpellFlagRoguePoison, DamageMultiplier: rogue.getPoisonDamageMultiplier(), ThreatMultiplier: 1, @@ -491,7 +491,7 @@ func (rogue *Rogue) makeWoundPoison(procSource PoisonProcSource) *core.Spell { SpellSchool: core.SpellSchoolNature, DefenseType: core.DefenseTypeMagic, ProcMask: core.ProcMaskWeaponProc, - Flags: SpellFlagDeadlyBrewed | core.SpellFlagPoison | SpellFlagRoguePoison, + Flags: core.SpellFlagPoison | core.SpellFlagPassiveSpell | SpellFlagDeadlyBrewed | SpellFlagRoguePoison, DamageMultiplier: rogue.getPoisonDamageMultiplier(), ThreatMultiplier: 1, diff --git a/sim/rogue/rupture.go b/sim/rogue/rupture.go index 84276bfdcd..f16b69683c 100644 --- a/sim/rogue/rupture.go +++ b/sim/rogue/rupture.go @@ -19,7 +19,7 @@ func (rogue *Rogue) registerRupture() { SpellSchool: core.SpellSchoolPhysical, DefenseType: core.DefenseTypeMelee, ProcMask: core.ProcMaskMeleeMHSpecial, - Flags: rogue.finisherFlags(), + Flags: core.SpellFlagPassiveSpell | rogue.finisherFlags(), MetricSplits: 6, EnergyCost: core.EnergyCostOptions{ diff --git a/sim/rogue/saber_slash.go b/sim/rogue/saber_slash.go index 4d889ceb9f..f156c8654f 100644 --- a/sim/rogue/saber_slash.go +++ b/sim/rogue/saber_slash.go @@ -50,7 +50,7 @@ func (rogue *Rogue) registerSaberSlashSpell() { dot.SnapshotBaseDamage += 0.05 * dot.Spell.MeleeAttackPower() }, OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { - dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTickCounted) + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTick) }, }, }) diff --git a/sim/rogue/unfair_advantage.go b/sim/rogue/unfair_advantage.go index c965daa6b1..5a4da14ee7 100644 --- a/sim/rogue/unfair_advantage.go +++ b/sim/rogue/unfair_advantage.go @@ -18,7 +18,7 @@ func (rogue *Rogue) applyUnfairAdvantage() { SpellSchool: core.SpellSchoolPhysical, DefenseType: core.DefenseTypeMelee, ProcMask: core.ProcMaskMeleeMHSpecial, - Flags: core.SpellFlagMeleeMetrics, + Flags: core.SpellFlagMeleeMetrics | core.SpellFlagPassiveSpell, DamageMultiplier: 1, ThreatMultiplier: 1, @@ -46,7 +46,7 @@ func (rogue *Rogue) applyUnfairAdvantage() { }, OnSpellHitTaken: func(aura *core.Aura, sim *core.Simulation, spell *core.Spell, result *core.SpellResult) { // need to add parry - if result.Outcome.Matches(core.OutcomeDodge | core.OutcomeParry) && icd.IsReady(sim) { + if result.Outcome.Matches(core.OutcomeDodge|core.OutcomeParry) && icd.IsReady(sim) { unfairAdvantage.Cast(sim, spell.Unit) rogue.AddComboPoints(sim, 1, comboMetrics) icd.Use(sim) diff --git a/sim/shaman/flametongue_weapon.go b/sim/shaman/flametongue_weapon.go index c308bfc598..fd4999f9ce 100644 --- a/sim/shaman/flametongue_weapon.go +++ b/sim/shaman/flametongue_weapon.go @@ -30,6 +30,7 @@ func (shaman *Shaman) newFlametongueImbueSpell(weapon *core.Item) *core.Spell { SpellSchool: core.SpellSchoolFire, DefenseType: core.DefenseTypeMagic, ProcMask: core.ProcMaskWeaponProc, + Flags: core.SpellFlagNoOnCastComplete | core.SpellFlagPassiveSpell, DamageMultiplier: []float64{1, 1.05, 1.1, 1.15}[shaman.Talents.ElementalWeapons], ThreatMultiplier: 1, diff --git a/sim/shaman/lightning_shield.go b/sim/shaman/lightning_shield.go index 6c6154808c..82de25ae1f 100644 --- a/sim/shaman/lightning_shield.go +++ b/sim/shaman/lightning_shield.go @@ -58,7 +58,7 @@ func (shaman *Shaman) registerNewLightningShieldSpell(rank int) { ActionID: core.ActionID{SpellID: procSpellId}, SpellSchool: core.SpellSchoolNature, ProcMask: core.ProcMaskEmpty, - Flags: SpellFlagShaman | SpellFlagLightning, + Flags: core.SpellFlagNoOnCastComplete | core.SpellFlagPassiveSpell | SpellFlagShaman | SpellFlagLightning, DamageMultiplier: 1, ThreatMultiplier: 1, diff --git a/sim/shaman/overload.go b/sim/shaman/overload.go index 50b9f5aef2..3cc7d676f6 100644 --- a/sim/shaman/overload.go +++ b/sim/shaman/overload.go @@ -12,7 +12,7 @@ const ShamanOverloadChance = .50 func (shaman *Shaman) applyOverloadModifiers(spell *core.SpellConfig) { spell.ActionID.Tag = int32(CastTagOverload) - spell.Flags |= core.SpellFlagNoOnCastComplete + spell.Flags |= core.SpellFlagNoOnCastComplete | core.SpellFlagPassiveSpell spell.Cast.DefaultCast.CastTime = 0 spell.Cast.DefaultCast.GCD = 0 spell.Cast.DefaultCast.Cost = 0 diff --git a/sim/shaman/riptide.go b/sim/shaman/riptide.go index 1f8f4dbe75..2abca9ec34 100644 --- a/sim/shaman/riptide.go +++ b/sim/shaman/riptide.go @@ -52,7 +52,7 @@ func (shaman *Shaman) registerRiptideSpell() { dot.Snapshot(target, baseHotHealing, isRollover) }, OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { - dot.CalcAndDealPeriodicSnapshotHealing(sim, target, dot.OutcomeTickCounted) + dot.CalcAndDealPeriodicSnapshotHealing(sim, target, dot.OutcomeTick) if hasPowerSurgeRune && sim.Proc(shaman.powerSurgeProcChance, "Power Surge Proc") { shaman.PowerSurgeHealAura.Activate(sim) diff --git a/sim/shaman/runes.go b/sim/shaman/runes.go index d101d4cec7..f061cfa128 100644 --- a/sim/shaman/runes.go +++ b/sim/shaman/runes.go @@ -263,7 +263,7 @@ func (shaman *Shaman) applyRollingThunder() { SpellSchool: core.SpellSchoolNature, DefenseType: core.DefenseTypeMagic, ProcMask: core.ProcMaskEmpty, - Flags: SpellFlagShaman | SpellFlagLightning, + Flags: SpellFlagShaman | SpellFlagLightning | core.SpellFlagPassiveSpell, DamageMultiplier: 1, ThreatMultiplier: 1, diff --git a/sim/shaman/windfury_weapon.go b/sim/shaman/windfury_weapon.go index ba40140cc2..c41c7f4efe 100644 --- a/sim/shaman/windfury_weapon.go +++ b/sim/shaman/windfury_weapon.go @@ -41,7 +41,7 @@ func (shaman *Shaman) newWindfuryImbueSpell(isMH bool) *core.Spell { SpellSchool: core.SpellSchoolPhysical, DefenseType: core.DefenseTypeMelee, ProcMask: procMask | core.ProcMaskWeaponProc, - Flags: core.SpellFlagMeleeMetrics, + Flags: core.SpellFlagMeleeMetrics | core.SpellFlagNoOnCastComplete | core.SpellFlagPassiveSpell, DamageMultiplier: damageMultiplier, ThreatMultiplier: 1, diff --git a/sim/warlock/corruption.go b/sim/warlock/corruption.go index cb994f1190..e3889e75c9 100644 --- a/sim/warlock/corruption.go +++ b/sim/warlock/corruption.go @@ -67,9 +67,9 @@ func (warlock *Warlock) getCorruptionConfig(rank int) core.SpellConfig { }, OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { if hasPandemicRune { - dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTickSnapshotCritCounted) + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeSnapshotCrit) } else { - dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTickCounted) + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTick) } }, }, @@ -77,8 +77,6 @@ func (warlock *Warlock) getCorruptionConfig(rank int) core.SpellConfig { ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { result := spell.CalcOutcome(sim, target, spell.OutcomeMagicHit) if result.Landed() { - spell.SpellMetrics[target.UnitIndex].Hits-- - if hasInvocationRune && spell.Dot(target).IsActive() { warlock.InvocationRefresh(sim, spell.Dot(target)) } diff --git a/sim/warlock/curses.go b/sim/warlock/curses.go index 023d9a3c46..7961347478 100644 --- a/sim/warlock/curses.go +++ b/sim/warlock/curses.go @@ -83,9 +83,9 @@ func (warlock *Warlock) getCurseOfAgonyBaseConfig(rank int) core.SpellConfig { }, OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { if hasPandemicRune { - dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTickSnapshotCritCounted) + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeSnapshotCrit) } else { - dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTickCounted) + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTick) } if dot.TickCount%4 == 0 { // CoA ramp up dot.SnapshotBaseDamage += snapshotBaseDmgNoBonus @@ -96,7 +96,6 @@ func (warlock *Warlock) getCurseOfAgonyBaseConfig(rank int) core.SpellConfig { ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { result := spell.CalcOutcome(sim, target, spell.OutcomeMagicHit) if result.Landed() { - spell.SpellMetrics[target.UnitIndex].Hits-- // If the spell's DoT is already applied, do a refresh if the Invocation rune is also being used // Else deactivate the existing curse and apply this one instead @@ -394,9 +393,9 @@ func (warlock *Warlock) registerCurseOfDoomSpell() { }, OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { if hasPandemicRune { - dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTickSnapshotCritCounted) + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeSnapshotCrit) } else { - dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTickCounted) + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTick) } }, }, diff --git a/sim/warlock/drain_life.go b/sim/warlock/drain_life.go index 135b330deb..397d0889ff 100644 --- a/sim/warlock/drain_life.go +++ b/sim/warlock/drain_life.go @@ -75,7 +75,7 @@ func (warlock *Warlock) getDrainLifeBaseConfig(rank int) core.SpellConfig { // dot.SnapshotAttackerMultiplier *= dot.Spell.TargetDamageMultiplier(dot.Spell.Unit.AttackTables[target.UnitIndex][dot.Spell.CastType], true) }, OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { - result := dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTickCounted) + result := dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTick) health := result.Damage if hasMasterChannelerRune { @@ -88,7 +88,6 @@ func (warlock *Warlock) getDrainLifeBaseConfig(rank int) core.SpellConfig { ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { result := spell.CalcOutcome(sim, target, spell.OutcomeMagicHit) if result.Landed() { - spell.SpellMetrics[target.UnitIndex].Hits-- dot := spell.Dot(target) dot.Apply(sim) diff --git a/sim/warlock/drain_soul.go b/sim/warlock/drain_soul.go index d01731042d..56e0329b11 100644 --- a/sim/warlock/drain_soul.go +++ b/sim/warlock/drain_soul.go @@ -62,14 +62,13 @@ func (warlock *Warlock) getDrainSoulBaseConfig(rank int) core.SpellConfig { } }, OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { - dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTickCounted) + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTick) }, }, ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { result := spell.CalcOutcome(sim, target, spell.OutcomeMagicHit) if result.Landed() { - spell.SpellMetrics[target.UnitIndex].Hits-- dot := spell.Dot(target) dot.Apply(sim) diff --git a/sim/warlock/immolate.go b/sim/warlock/immolate.go index 2981b324c0..12dcafe3f3 100644 --- a/sim/warlock/immolate.go +++ b/sim/warlock/immolate.go @@ -77,7 +77,7 @@ func (warlock *Warlock) getImmolateConfig(rank int) core.SpellConfig { if hasPandemicRune { // We add the crit damage bonus and remove it after the call to not affect the initial damage portion of the spell dot.Spell.CritDamageBonus += 1 - result = dot.CalcSnapshotDamage(sim, target, dot.OutcomeTickSnapshotCrit) + result = dot.CalcSnapshotDamage(sim, target, dot.OutcomeSnapshotCrit) dot.Spell.CritDamageBonus -= 1 } else { result = dot.CalcSnapshotDamage(sim, target, dot.OutcomeTick) diff --git a/sim/warlock/immolation_aura.go b/sim/warlock/immolation_aura.go index 65d07a9ea4..fb537118cb 100644 --- a/sim/warlock/immolation_aura.go +++ b/sim/warlock/immolation_aura.go @@ -23,6 +23,7 @@ func (warlock *Warlock) registerImmolationAuraSpell() { SpellSchool: core.SpellSchoolFire, DefenseType: core.DefenseTypeMagic, ProcMask: core.ProcMaskEmpty, + Flags: core.SpellFlagNoOnCastComplete | core.SpellFlagPassiveSpell, DamageMultiplier: 1, ThreatMultiplier: 1, diff --git a/sim/warlock/shadowflame.go b/sim/warlock/shadowflame.go index 52a43705dc..e6770e24d3 100644 --- a/sim/warlock/shadowflame.go +++ b/sim/warlock/shadowflame.go @@ -60,7 +60,7 @@ func (warlock *Warlock) registerShadowflameSpell() { if hasPandemicRune { // We add the crit damage bonus and remove it after the call to not affect the initial damage portion of the spell dot.Spell.CritDamageBonus += 1 - result = dot.CalcSnapshotDamage(sim, target, dot.OutcomeTickSnapshotCrit) + result = dot.CalcSnapshotDamage(sim, target, dot.OutcomeSnapshotCrit) dot.Spell.CritDamageBonus -= 1 } else { result = dot.CalcSnapshotDamage(sim, target, dot.OutcomeTick) diff --git a/sim/warlock/siphon_life.go b/sim/warlock/siphon_life.go index 5a17e4cf2a..914629263e 100644 --- a/sim/warlock/siphon_life.go +++ b/sim/warlock/siphon_life.go @@ -73,9 +73,9 @@ func (warlock *Warlock) getSiphonLifeBaseConfig(rank int) core.SpellConfig { var result *core.SpellResult if hasPandemicRune { - result = dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTickSnapshotCritCounted) + result = dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeSnapshotCrit) } else { - result = dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTickCounted) + result = dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTick) } // revert flag changes @@ -89,7 +89,6 @@ func (warlock *Warlock) getSiphonLifeBaseConfig(rank int) core.SpellConfig { ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { result := spell.CalcOutcome(sim, target, spell.OutcomeMagicHit) if result.Landed() { - spell.SpellMetrics[target.UnitIndex].Hits-- if hasInvocationRune && spell.Dot(target).IsActive() { warlock.InvocationRefresh(sim, spell.Dot(target)) diff --git a/sim/warlock/unstable_affliction.go b/sim/warlock/unstable_affliction.go index 6befa52b23..73e2894845 100644 --- a/sim/warlock/unstable_affliction.go +++ b/sim/warlock/unstable_affliction.go @@ -53,9 +53,9 @@ func (warlock *Warlock) registerUnstableAfflictionSpell() { }, OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { if hasPandemicRune { - dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTickSnapshotCritCounted) + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeSnapshotCrit) } else { - dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTickCounted) + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTick) } }, }, @@ -63,7 +63,6 @@ func (warlock *Warlock) registerUnstableAfflictionSpell() { ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { result := spell.CalcOutcome(sim, target, spell.OutcomeMagicHit) if result.Landed() { - spell.SpellMetrics[target.UnitIndex].Hits-- if hasInvocationRune && spell.Dot(target).IsActive() { warlock.InvocationRefresh(sim, spell.Dot(target)) diff --git a/sim/warrior/deep_wounds.go b/sim/warrior/deep_wounds.go index c3eb968e22..c46102a79b 100644 --- a/sim/warrior/deep_wounds.go +++ b/sim/warrior/deep_wounds.go @@ -22,7 +22,7 @@ func (warrior *Warrior) applyDeepWounds() { ActionID: core.ActionID{SpellID: spellID}, SpellSchool: core.SpellSchoolPhysical, ProcMask: core.ProcMaskEmpty, - Flags: core.SpellFlagNoOnCastComplete, + Flags: core.SpellFlagNoOnCastComplete | core.SpellFlagPassiveSpell, DamageMultiplier: 1, ThreatMultiplier: 1, diff --git a/sim/warrior/execute.go b/sim/warrior/execute.go index 88f7483ef3..cc927c88c4 100644 --- a/sim/warrior/execute.go +++ b/sim/warrior/execute.go @@ -36,7 +36,7 @@ func (warrior *Warrior) registerExecuteSpell() { SpellSchool: core.SpellSchoolPhysical, DefenseType: core.DefenseTypeMelee, ProcMask: core.ProcMaskMeleeMHSpecial, - Flags: core.SpellFlagMeleeMetrics | core.SpellFlagAPL, + Flags: core.SpellFlagMeleeMetrics | core.SpellFlagAPL | core.SpellFlagPassiveSpell, RageCost: core.RageCostOptions{ Cost: 15 - []float64{0, 2, 5}[warrior.Talents.ImprovedExecute] - warrior.FocusedRageDiscount, diff --git a/sim/warrior/rend.go b/sim/warrior/rend.go index c457932d4b..e5e3cd820c 100644 --- a/sim/warrior/rend.go +++ b/sim/warrior/rend.go @@ -35,7 +35,7 @@ func (warrior *Warrior) registerRendSpell() { ActionID: core.ActionID{SpellID: rend.spellID}, SpellSchool: core.SpellSchoolPhysical, ProcMask: core.ProcMaskMeleeMHSpecial, - Flags: core.SpellFlagNoOnCastComplete | core.SpellFlagAPL, + Flags: core.SpellFlagAPL | core.SpellFlagNoOnCastComplete | core.SpellFlagPassiveSpell, RageCost: core.RageCostOptions{ Cost: 10 - warrior.FocusedRageDiscount, diff --git a/sim/warrior/slam.go b/sim/warrior/slam.go index 11a8e958cc..9be30bfcce 100644 --- a/sim/warrior/slam.go +++ b/sim/warrior/slam.go @@ -99,10 +99,12 @@ func (warrior *Warrior) newSlamHitSpell(isMH bool) *WarriorSpell { }[warrior.Level] procMask := core.ProcMaskMeleeMHSpecial + flags := core.SpellFlagMeleeMetrics | core.SpellFlagNoOnCastComplete damageFunc := warrior.MHWeaponDamage if !isMH { flatDamageBonus /= 2 procMask = core.ProcMaskMeleeOHSpecial + flags |= core.SpellFlagPassiveSpell damageFunc = warrior.OHWeaponDamage } @@ -112,7 +114,7 @@ func (warrior *Warrior) newSlamHitSpell(isMH bool) *WarriorSpell { SpellSchool: core.SpellSchoolPhysical, DefenseType: core.DefenseTypeMelee, ProcMask: procMask, - Flags: core.SpellFlagMeleeMetrics | core.SpellFlagNoOnCastComplete, + Flags: flags, CritDamageBonus: warrior.impale(), FlatThreatBonus: 1 * requiredLevel, diff --git a/sim/warrior/whirlwind.go b/sim/warrior/whirlwind.go index 4977e46ac1..b7861f6ff1 100644 --- a/sim/warrior/whirlwind.go +++ b/sim/warrior/whirlwind.go @@ -66,7 +66,7 @@ func (warrior *Warrior) newWhirlwindHitSpell(isMH bool) *WarriorSpell { SpellSchool: core.SpellSchoolPhysical, DefenseType: core.DefenseTypeMelee, ProcMask: procMask, - Flags: core.SpellFlagMeleeMetrics | core.SpellFlagNoOnCastComplete, + Flags: core.SpellFlagMeleeMetrics | core.SpellFlagNoOnCastComplete | core.SpellFlagPassiveSpell, CritDamageBonus: warrior.impale(), diff --git a/ui/core/components/detailed_results.tsx b/ui/core/components/detailed_results.tsx index 4a3edef8e0..a26f38e6a3 100644 --- a/ui/core/components/detailed_results.tsx +++ b/ui/core/components/detailed_results.tsx @@ -1,5 +1,3 @@ -import { ref } from 'tsx-vanilla'; - import { REPO_NAME } from '../constants/other'; import { DetailedResultsUpdate, SimRun, SimRunData } from '../proto/ui'; import { SimResult } from '../proto_utils/sim_result'; @@ -8,17 +6,16 @@ import { TypedEvent } from '../typed_event'; import { Component } from './component'; import { AuraMetricsTable } from './detailed_results/aura_metrics'; import { CastMetricsTable } from './detailed_results/cast_metrics'; +import { DamageMetricsTable } from './detailed_results/damage_metrics'; import { DpsHistogram } from './detailed_results/dps_histogram'; -import { DtpsMeleeMetricsTable } from './detailed_results/dtps_melee_metrics'; -import { DtpsSpellMetricsTable } from './detailed_results/dtps_spell_metrics'; +import { DtpsMetricsTable } from './detailed_results/dtps_metrics'; import { HealingMetricsTable } from './detailed_results/healing_metrics'; import { LogRunner } from './detailed_results/log_runner'; -import { MeleeMetricsTable } from './detailed_results/melee_metrics'; import { PlayerDamageMetricsTable } from './detailed_results/player_damage'; +import { PlayerDamageTakenMetricsTable } from './detailed_results/player_damage_taken'; import { ResourceMetricsTable } from './detailed_results/resource_metrics'; import { SimResultData } from './detailed_results/result_component'; import { ResultsFilter } from './detailed_results/results_filter'; -import { SpellMetricsTable } from './detailed_results/spell_metrics'; import { Timeline } from './detailed_results/timeline'; import { ToplineResults } from './detailed_results/topline_results'; import { RaidSimResultsManager } from './raid_sim_action'; @@ -37,17 +34,17 @@ const tabs: Tab[] = [ isActive: true, targetId: 'damageTab', label: 'Damage', - classes: ['damage-metrics'], + classes: ['damage-metrics-tab'], }, { targetId: 'healingTab', label: 'Healing', - classes: ['healing-metrics'], + classes: ['healing-metrics-tab'], }, { targetId: 'damageTakenTab', label: 'Damage Taken', - classes: ['threat-metrics'], + classes: ['threat-metrics-tab'], }, { targetId: 'buffsTab', @@ -75,104 +72,6 @@ const tabs: Tab[] = [ }, ]; -const layoutHTML = ( -
-
-
-
- -
-
-
- Run a simulation to view results -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-); - export abstract class DetailedResults extends Component { protected readonly simUI: SimUI | null; protected latestRun: SimRunData | null = null; @@ -184,7 +83,104 @@ export abstract class DetailedResults extends Component { constructor(parent: HTMLElement, simUI: SimUI | null, cssScheme: string) { super(parent, 'detailed-results-manager-root'); - this.rootElem.appendChild(layoutHTML); + + this.rootElem.appendChild( +
+
+
+
+ +
+
+
+ Run a simulation to view results +
+
+
+
+
+
+
+
+
+ {/*
+
+
+
+
+
*/} +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
, + ); this.rootDiv = this.rootElem.querySelector('.dr-root')!; this.simUI = simUI; @@ -218,14 +214,11 @@ export abstract class DetailedResults extends Component { parent: this.rootElem.querySelector('.cast-metrics')!, resultsEmitter: this.resultsEmitter, }); - new MeleeMetricsTable({ - parent: this.rootElem.querySelector('.melee-metrics')!, - resultsEmitter: this.resultsEmitter, - }); - new SpellMetricsTable({ - parent: this.rootElem.querySelector('.spell-metrics')!, + new DamageMetricsTable({ + parent: this.rootElem.querySelector('.damage-metrics')!, resultsEmitter: this.resultsEmitter, }); + new HealingMetricsTable({ parent: this.rootElem.querySelector('.healing-spell-metrics')!, resultsEmitter: this.resultsEmitter, @@ -238,7 +231,10 @@ export abstract class DetailedResults extends Component { { parent: this.rootElem.querySelector('.player-damage-metrics')!, resultsEmitter: this.resultsEmitter }, this.resultsFilter, ); - + new PlayerDamageTakenMetricsTable( + { parent: this.rootElem.querySelector('.player-damage-taken-metrics')!, resultsEmitter: this.resultsEmitter }, + this.resultsFilter, + ); new AuraMetricsTable( { parent: this.rootElem.querySelector('.buff-aura-metrics')!, @@ -259,12 +255,8 @@ export abstract class DetailedResults extends Component { resultsEmitter: this.resultsEmitter, }); - new DtpsMeleeMetricsTable({ - parent: this.rootElem.querySelector('.dtps-melee-metrics')!, - resultsEmitter: this.resultsEmitter, - }); - new DtpsSpellMetricsTable({ - parent: this.rootElem.querySelector('.dtps-spell-metrics')!, + new DtpsMetricsTable({ + parent: this.rootElem.querySelector('.dtps-metrics')!, resultsEmitter: this.resultsEmitter, }); @@ -274,7 +266,7 @@ export abstract class DetailedResults extends Component { resultsEmitter: this.resultsEmitter, }); - const tabEl = document.querySelector('a[data-bs-target="#timelineTab"]'); + const tabEl = document.querySelector('button[data-bs-target="#timelineTab"]'); tabEl?.addEventListener('shown.bs.tab', () => { timeline.render(); }); diff --git a/ui/core/components/detailed_results/aura_metrics.ts b/ui/core/components/detailed_results/aura_metrics.ts index 579ba2bcef..d180de87c6 100644 --- a/ui/core/components/detailed_results/aura_metrics.ts +++ b/ui/core/components/detailed_results/aura_metrics.ts @@ -1,8 +1,7 @@ -import { ActionId } from '../../proto_utils/action_id'; -import { AuraMetrics, SimResult, SimResultFilter } from '../../proto_utils/sim_result'; - -import { ColumnSortType, MetricsTable } from './metrics_table'; -import { ResultComponent, ResultComponentConfig, SimResultData } from './result_component'; +import { OtherAction } from '../../proto/common'; +import { AuraMetrics } from '../../proto_utils/sim_result'; +import { ColumnSortType, MetricsTable } from './metrics_table/metrics_table'; +import { ResultComponentConfig, SimResultData } from './result_component'; export class AuraMetricsTable extends MetricsTable { private readonly useDebuffs: boolean; @@ -18,23 +17,21 @@ export class AuraMetricsTable extends MetricsTable { return { name: metric.name, actionId: metric.actionId, + metricType: metric?.constructor?.name, }; }), { name: 'Procs', - tooltip: 'Procs', getValue: (metric: AuraMetrics) => metric.averageProcs, getDisplayString: (metric: AuraMetrics) => metric.averageProcs.toFixed(2), }, { name: 'PPM', - tooltip: 'Procs Per Minute', getValue: (metric: AuraMetrics) => metric.ppm, getDisplayString: (metric: AuraMetrics) => metric.ppm.toFixed(2), }, { name: 'Uptime', - tooltip: 'Uptime / Encounter Duration', sort: ColumnSortType.Descending, getValue: (metric: AuraMetrics) => metric.uptimePercent, getDisplayString: (metric: AuraMetrics) => metric.uptimePercent.toFixed(2) + '%', @@ -47,25 +44,31 @@ export class AuraMetricsTable extends MetricsTable { if (this.useDebuffs) { return AuraMetrics.groupById(resultData.result.getDebuffMetrics(resultData.filter)); } else { - const players = resultData.result.getPlayers(resultData.filter); + const players = resultData.result.getRaidIndexedPlayers(resultData.filter); if (players.length != 1) { return []; } const player = players[0]; - const auras = player.auras; + const auras = this.filterMetrics(player.auras); const actionGroups = AuraMetrics.groupById(auras); - const petGroups = player.pets.map(pet => pet.auras); - + const petGroups = player.pets.map(pet => this.filterMetrics(pet.auras)); return actionGroups.concat(petGroups); } } mergeMetrics(metrics: Array): AuraMetrics { - return AuraMetrics.merge(metrics, true, metrics[0].unit?.petActionId || undefined); + return AuraMetrics.merge(metrics, { + removeTag: true, + actionIdOverride: metrics[0].unit?.petActionId || undefined, + }); } shouldCollapse(metric: AuraMetrics): boolean { return !metric.unit?.isPet; } + + filterMetrics(metrics: Array): Array { + return metrics.filter(aura => !(aura.unit?.isPet && aura.actionId.otherId === OtherAction.OtherActionMove)); + } } diff --git a/ui/core/components/detailed_results/cast_metrics.ts b/ui/core/components/detailed_results/cast_metrics.ts index e0f8e14f44..a2a30d2dc3 100644 --- a/ui/core/components/detailed_results/cast_metrics.ts +++ b/ui/core/components/detailed_results/cast_metrics.ts @@ -1,7 +1,6 @@ -import { ActionMetrics, SimResult, SimResultFilter } from '../../proto_utils/sim_result.js'; - -import { ColumnSortType, MetricsTable } from './metrics_table.js'; -import { ResultComponent, ResultComponentConfig, SimResultData } from './result_component.js'; +import { ActionMetrics } from '../../proto_utils/sim_result'; +import { ColumnSortType, MetricsTable } from './metrics_table/metrics_table'; +import { ResultComponentConfig, SimResultData } from './result_component'; export class CastMetricsTable extends MetricsTable { constructor(config: ResultComponentConfig) { @@ -11,18 +10,17 @@ export class CastMetricsTable extends MetricsTable { return { name: metric.name, actionId: metric.actionId, + metricType: metric.constructor?.name, }; }), { name: 'Casts', - tooltip: 'Casts', sort: ColumnSortType.Descending, getValue: (metric: ActionMetrics) => metric.casts, getDisplayString: (metric: ActionMetrics) => metric.casts.toFixed(1), }, { name: 'CPM', - tooltip: 'Casts / (Encounter Duration / 60 Seconds)', getValue: (metric: ActionMetrics) => metric.castsPerMinute, getDisplayString: (metric: ActionMetrics) => metric.castsPerMinute.toFixed(1), }, @@ -31,7 +29,7 @@ export class CastMetricsTable extends MetricsTable { getGroupedMetrics(resultData: SimResultData): Array> { //const actionMetrics = resultData.result.getActionMetrics(resultData.filter); - const players = resultData.result.getPlayers(resultData.filter); + const players = resultData.result.getRaidIndexedPlayers(resultData.filter); if (players.length != 1) { return []; } @@ -45,7 +43,10 @@ export class CastMetricsTable extends MetricsTable { } mergeMetrics(metrics: Array): ActionMetrics { - return ActionMetrics.merge(metrics, true, metrics[0].unit?.petActionId || undefined); + return ActionMetrics.merge(metrics, { + removeTag: true, + actionIdOverride: metrics[0].unit?.petActionId || undefined, + }); } shouldCollapse(metric: ActionMetrics): boolean { diff --git a/ui/core/components/detailed_results/damage_metrics.tsx b/ui/core/components/detailed_results/damage_metrics.tsx new file mode 100644 index 0000000000..4216260a0d --- /dev/null +++ b/ui/core/components/detailed_results/damage_metrics.tsx @@ -0,0 +1,490 @@ +import { relative } from 'node:path'; + +import { TOOLTIP_METRIC_LABELS } from '../../constants/tooltips'; +import { ActionMetrics } from '../../proto_utils/sim_result'; +import { bucket, formatToCompactNumber, formatToNumber, formatToPercent } from '../../utils'; +import { MetricsCombinedTooltipTable } from './metrics_table/metrics_combined_tooltip_table'; +import { ColumnSortType, MetricsTable } from './metrics_table/metrics_table'; +import { MetricsTotalBar } from './metrics_table/metrics_total_bar'; +import { ResultComponentConfig, SimResultData } from './result_component'; + +export class DamageMetricsTable extends MetricsTable { + maxDamageAmount: number | null = null; + constructor(config: ResultComponentConfig) { + config.rootCssClass = 'damage-metrics-root'; + config.resultsEmitter.on((_, resultData) => { + const lastResult = resultData + ? this.getGroupedMetrics(resultData) + .filter(g => g.length) + .map(groups => this.mergeMetrics(groups)) + : undefined; + this.maxDamageAmount = Math.max(...(lastResult || []).map(a => a.damage)); + }); + super(config, [ + MetricsTable.nameCellConfig((metric: ActionMetrics) => { + return { + name: metric.name, + actionId: metric.actionId, + metricType: metric.constructor?.name, + }; + }), + { + name: 'Damage done', + headerCellClass: 'text-center metrics-table-cell--primary-metric', + columnClass: 'metrics-table-cell--primary-metric', + getValue: (metric: ActionMetrics) => metric.avgDamage, + fillCell: (metric: ActionMetrics, cellElem: HTMLElement) => { + cellElem.appendChild( + , + ); + + const hitValues = metric.damageDone.hit; + const resistedHitValues = metric.damageDone.resistedHit; + const critHitValues = metric.damageDone.critHit; + const resistedCritHitValues = metric.damageDone.resistedCritHit; + const tickValues = metric.damageDone.tick; + const resistedTickValues = metric.damageDone.resistedTick; + const critTickValues = metric.damageDone.critTick; + const resistedCritTickValues = metric.damageDone.resistedCritTick; + const glanceValues = metric.damageDone.glance; + const blockValues = metric.damageDone.block; + const critBlockValues = metric.damageDone.critBlock; + + cellElem.appendChild( + , + ); + }, + }, + { + name: 'Casts', + getValue: (metric: ActionMetrics) => metric.casts, + fillCell: (metric: ActionMetrics, cellElem: HTMLElement) => { + cellElem.appendChild(<>{formatToNumber(metric.casts, { fallbackString: '-' })}); + + if (metric.isPassiveAction || (!metric.landedHits && !metric.totalMisses)) return; + const relativeHitPercent = ((metric.landedHits || metric.casts) / ((metric.landedHits || metric.casts) + metric.totalMisses)) * 100; + + cellElem.appendChild( + , + ); + }, + }, + { + name: 'Avg Cast', + tooltip: TOOLTIP_METRIC_LABELS['Damage Avg Cast'], + getValue: (metric: ActionMetrics) => { + if (metric.isPassiveAction) return 0; + return metric.avgCastHit || metric.avgCastTick; + }, + fillCell: (metric: ActionMetrics, cellElem: HTMLElement) => { + cellElem.appendChild( + <> + {metric.isPassiveAction ? ( + '-' + ) : ( + <> + {formatToCompactNumber(metric.avgCastHit || metric.avgCastTick, { fallbackString: '-' })} + {metric.avgCastHit && metric.avgCastTick ? ( + <> ({formatToCompactNumber(metric.avgCastTick, { fallbackString: '-' })}) + ) : undefined} + + )} + , + ); + + if (!metric.avgCastHit && !metric.avgCastTick) return; + + cellElem.appendChild( + { + const hideThreatMetrics = !!document.querySelector('.hide-threat-metrics'); + if (hideThreatMetrics) return false; + }, + }} + headerValues={[, 'Amount']} + groups={[ + { + spellSchool: metric.spellSchool, + total: metric.avgCastThreat, + totalPercentage: 100, + data: [ + { + name: 'Threat', + value: metric.avgCastThreat, + percentage: 100, + }, + ], + }, + ]} + />, + ); + }, + }, + { + name: 'Hits', + getValue: (metric: ActionMetrics) => metric.landedHits || metric.landedTicks, + fillCell: (metric: ActionMetrics, cellElem: HTMLElement) => { + cellElem.appendChild( + <> + {formatToNumber(metric.landedHits || metric.landedTicks, { fallbackString: '-' })} + {metric.landedHits && metric.landedTicks ? <> ({formatToNumber(metric.landedTicks, { fallbackString: '-' })}) : undefined} + , + ); + if (!metric.landedHits && !metric.landedTicks) return; + + const relativeHitPercent = ((metric.hits - metric.resistedHits) / metric.landedHits) * 100; + const relativeResistedHitPercent = (metric.resistedHits / metric.landedHits) * 100; + const relativeCritPercent = ((metric.crits - metric.resistedCrits) / metric.landedHits) * 100; + const relativeResistedCritPercent = (metric.resistedCrits / metric.landedHits) * 100; + const relativeTickPercent = ((metric.ticks - metric.resistedTicks) / metric.landedTicks) * 100; + const relativeResistedTickPercent = (metric.resistedTicks / metric.landedTicks) * 100; + const relativeCritTickPercent = ((metric.critTicks - metric.resistedCritTicks) / metric.landedTicks) * 100; + const relativeResistedCritTickPercent = (metric.resistedCritTicks / metric.landedTicks) * 100; + const relativeGlancePercent = (metric.glances / metric.landedHits) * 100; + const relativeBlockPercent = (metric.blocks / metric.landedHits) * 100; + const relativeCritBlockPercent = (metric.critBlocks / metric.landedHits) * 100; + + cellElem.appendChild( + , + ); + }, + }, + { + name: 'Avg Hit', + getValue: (metric: ActionMetrics) => metric.avgHit || metric.avgTick, + fillCell: (metric: ActionMetrics, cellElem: HTMLElement) => { + cellElem.appendChild( + <> + {formatToCompactNumber(metric.avgHit || metric.avgTick, { fallbackString: '-' })} + {metric.avgHit && metric.avgTick ? <> ({formatToCompactNumber(metric.avgTick, { fallbackString: '-' })}) : undefined} + , + ); + + if (!metric.avgHitThreat) return; + + cellElem.appendChild( + { + const hideThreatMetrics = !!document.querySelector('.hide-threat-metrics'); + if (hideThreatMetrics) return false; + }, + }} + headerValues={[, 'Amount']} + groups={[ + { + spellSchool: metric.spellSchool, + total: metric.avgHitThreat, + totalPercentage: 100, + data: [ + { + name: 'Threat', + value: metric.avgHitThreat, + percentage: 100, + }, + ], + }, + ]} + />, + ); + }, + }, + { + name: 'Crit %', + getValue: (metric: ActionMetrics) => metric.critPercent || metric.critTickPercent, + getDisplayString: (metric: ActionMetrics) => + `${formatToPercent(metric.critPercent || metric.critTickPercent, { fallbackString: '-' })}${ + metric.critPercent && metric.critTickPercent ? ` (${formatToPercent(metric.critTickPercent, { fallbackString: '-' })})` : '' + }`, + }, + { + name: 'Miss %', + getValue: (metric: ActionMetrics) => metric.totalMissesPercent, + fillCell: (metric: ActionMetrics, cellElem: HTMLElement) => { + cellElem.appendChild(<>{formatToPercent(metric.totalMissesPercent, { fallbackString: '-' })}); + if (!metric.totalMissesPercent) return; + + cellElem.appendChild( + , + ); + }, + }, + { + name: 'DPET', + getValue: (metric: ActionMetrics) => metric.damageThroughput, + getDisplayString: (metric: ActionMetrics) => formatToCompactNumber(metric.damageThroughput, { fallbackString: '-' }), + }, + { + name: 'DPS', + headerCellClass: 'text-body', + columnClass: 'text-success', + sort: ColumnSortType.Descending, + getValue: (metric: ActionMetrics) => metric.dps, + fillCell: (metric: ActionMetrics, cellElem: HTMLElement) => { + cellElem.appendChild(<>{formatToNumber(metric.dps, { minimumFractionDigits: 2, fallbackString: '-' })}); + if (!metric.dps) return; + + cellElem.appendChild( + { + const hideThreatMetrics = !!document.querySelector('.hide-threat-metrics'); + if (hideThreatMetrics) return false; + }, + }} + headerValues={[, 'Amount']} + groups={[ + { + spellSchool: metric.spellSchool, + total: metric.tps, + totalPercentage: 100, + data: [ + { + name: 'Threat', + value: metric.tps, + percentage: 100, + }, + ], + }, + ]} + />, + ); + }, + }, + ]); + } + + customizeRowElem(action: ActionMetrics, rowElem: HTMLElement) { + if (action.hitAttempts == 0 && action.dps == 0) { + rowElem.classList.add('threat-metrics'); + } + } + + getGroupedMetrics(resultData: SimResultData): Array> { + const players = resultData.result.getRaidIndexedPlayers(resultData.filter); + if (players.length != 1) { + return []; + } + const player = players[0]; + + const actions = player.getDamageActions().map(action => action.forTarget(resultData.filter)); + const actionGroups = ActionMetrics.groupById(actions); + const petsByName = bucket(player.pets, pet => pet.name); + + const petGroups = Object.values(petsByName).map(pets => + ActionMetrics.joinById( + pets.flatMap(pet => pet.getDamageActions().map(action => action.forTarget(resultData.filter))), + true, + ), + ); + + return actionGroups.concat(petGroups); + } + + mergeMetrics(metrics: Array): ActionMetrics { + return ActionMetrics.merge(metrics, { + removeTag: true, + actionIdOverride: metrics[0]?.unit?.petActionId || undefined, + }); + } + + shouldCollapse(metric: ActionMetrics): boolean { + return !metric.unit?.isPet; + } +} diff --git a/ui/core/components/detailed_results/dtps_melee_metrics.ts b/ui/core/components/detailed_results/dtps_melee_metrics.ts deleted file mode 100644 index 2e5a31b512..0000000000 --- a/ui/core/components/detailed_results/dtps_melee_metrics.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { ActionMetrics, SimResult, SimResultFilter } from '../../proto_utils/sim_result.js'; - -import { ColumnSortType, MetricsTable } from './metrics_table.js'; -import { ResultComponent, ResultComponentConfig, SimResultData } from './result_component.js'; - -export class DtpsMeleeMetricsTable extends MetricsTable { - constructor(config: ResultComponentConfig) { - config.rootCssClass = 'dtps-melee-metrics-root'; - super(config, [ - MetricsTable.nameCellConfig((metric: ActionMetrics) => { - return { - name: metric.name, - actionId: metric.actionId, - }; - }), - { - name: 'DPS', - tooltip: 'Damage / Encounter Duration', - sort: ColumnSortType.Descending, - getValue: (metric: ActionMetrics) => metric.dps, - getDisplayString: (metric: ActionMetrics) => metric.dps.toFixed(1), - }, - { - name: 'Avg Cast', - tooltip: 'Damage / Casts', - getValue: (metric: ActionMetrics) => metric.avgCast, - getDisplayString: (metric: ActionMetrics) => metric.avgCast.toFixed(1), - }, - { - name: 'Avg Hit', - tooltip: 'Damage / (Hits + Crits + Glances + Blocks)', - getValue: (metric: ActionMetrics) => metric.avgHit, - getDisplayString: (metric: ActionMetrics) => metric.avgHit.toFixed(1), - }, - { - name: 'Casts', - tooltip: 'Casts', - getValue: (metric: ActionMetrics) => metric.casts, - getDisplayString: (metric: ActionMetrics) => metric.casts.toFixed(1), - }, - { - name: 'Hits', - tooltip: 'Hits + Crits + Glances + Blocks', - getValue: (metric: ActionMetrics) => metric.landedHits, - getDisplayString: (metric: ActionMetrics) => metric.landedHits.toFixed(1), - }, - { - name: 'Miss %', - tooltip: 'Misses / Swings', - getValue: (metric: ActionMetrics) => metric.missPercent, - getDisplayString: (metric: ActionMetrics) => metric.missPercent.toFixed(2) + '%', - }, - { - name: 'Dodge %', - tooltip: 'Dodges / Swings', - getValue: (metric: ActionMetrics) => metric.dodgePercent, - getDisplayString: (metric: ActionMetrics) => metric.dodgePercent.toFixed(2) + '%', - }, - { - name: 'Parry %', - tooltip: 'Parries / Swings', - getValue: (metric: ActionMetrics) => metric.parryPercent, - getDisplayString: (metric: ActionMetrics) => metric.parryPercent.toFixed(2) + '%', - }, - { - name: 'Block %', - tooltip: 'Blocks / Swings', - getValue: (metric: ActionMetrics) => metric.blockPercent, - getDisplayString: (metric: ActionMetrics) => metric.blockPercent.toFixed(2) + '%', - }, - { - name: 'Crit %', - tooltip: 'Crits / Swings', - getValue: (metric: ActionMetrics) => metric.critPercent, - getDisplayString: (metric: ActionMetrics) => metric.critPercent.toFixed(2) + '%', - }, - ]); - } - - getGroupedMetrics(resultData: SimResultData): Array> { - const players = resultData.result.getPlayers(resultData.filter); - if (players.length != 1) { - return []; - } - const player = players[0]; - - const targets = resultData.result.getTargets(resultData.filter); - const targetActions = targets.map(target => target.getMeleeActions().map(action => action.forTarget(resultData.filter))).flat(); - const actionGroups = ActionMetrics.groupById(targetActions); - - return actionGroups; - } - - mergeMetrics(metrics: Array): ActionMetrics { - // TODO: Use NPC ID here instead of pet ID. - return ActionMetrics.merge(metrics, true, metrics[0].unit?.petActionId || undefined); - } -} diff --git a/ui/core/components/detailed_results/dtps_metrics.tsx b/ui/core/components/detailed_results/dtps_metrics.tsx new file mode 100644 index 0000000000..b252d94030 --- /dev/null +++ b/ui/core/components/detailed_results/dtps_metrics.tsx @@ -0,0 +1,327 @@ +import { TOOLTIP_METRIC_LABELS } from '../../constants/tooltips'; +import { ActionMetrics } from '../../proto_utils/sim_result'; +import { formatToCompactNumber, formatToNumber, formatToPercent } from '../../utils'; +import { MetricsCombinedTooltipTable } from './metrics_table/metrics_combined_tooltip_table'; +import { ColumnSortType, MetricsTable } from './metrics_table/metrics_table'; +import { MetricsTotalBar } from './metrics_table/metrics_total_bar'; +import { ResultComponentConfig, SimResultData } from './result_component'; + +export class DtpsMetricsTable extends MetricsTable { + maxDtpsAmount: number | null = null; + constructor(config: ResultComponentConfig) { + config.rootCssClass = 'dtps-metrics-root'; + config.resultsEmitter.on((_, resultData) => { + const lastResult = resultData + ? this.getGroupedMetrics(resultData) + .filter(g => g.length) + .map(groups => this.mergeMetrics(groups)) + : undefined; + this.maxDtpsAmount = Math.max(...(lastResult || []).map(a => a.damage)); + }); + super(config, [ + MetricsTable.nameCellConfig((metric: ActionMetrics) => { + return { + name: metric.name, + actionId: metric.actionId, + metricType: metric.constructor?.name, + }; + }), + { + name: 'Damage Taken', + headerCellClass: 'text-center metrics-table-cell--primary-metric', + columnClass: 'metrics-table-cell--primary-metric', + getValue: (metric: ActionMetrics) => metric.avgDamage, + fillCell: (metric: ActionMetrics, cellElem: HTMLElement) => { + cellElem.appendChild( + , + ); + + const hitValues = metric.damageDone.hit; + const critHitValues = metric.damageDone.critHit; + const tickValues = metric.damageDone.tick; + const critTickValues = metric.damageDone.critTick; + const glanceValues = metric.damageDone.glance; + const blockValues = metric.damageDone.block; + const critBlockValues = metric.damageDone.critBlock; + + cellElem.appendChild( + , + ); + }, + }, + { + name: 'Casts', + getValue: (metric: ActionMetrics) => metric.casts, + fillCell: (metric: ActionMetrics, cellElem: HTMLElement) => { + cellElem.appendChild(<>{formatToNumber(metric.casts, { fallbackString: '-' })}); + + if (!metric.landedHits && !metric.totalMisses) return; + const relativeHitPercent = ((metric.landedHits || metric.casts) / ((metric.landedHits || metric.casts) + metric.totalMisses)) * 100; + cellElem.appendChild( + , + ); + }, + }, + { + name: 'Avg Cast', + tooltip: TOOLTIP_METRIC_LABELS['Damage Avg Cast'], + getValue: (metric: ActionMetrics) => { + if (metric.isPassiveAction) return 0; + return metric.avgCastHit || metric.avgCastTick; + }, + fillCell: (metric: ActionMetrics, cellElem: HTMLElement) => { + cellElem.appendChild( + <> + {formatToCompactNumber(metric.avgCastHit || metric.avgCastTick, { fallbackString: '-' })} + {metric.avgCastHit && metric.avgCastTick ? <> ({formatToCompactNumber(metric.avgCastTick, { fallbackString: '-' })}) : undefined} + , + ); + }, + }, + { + name: 'Hits', + getValue: (metric: ActionMetrics) => metric.landedHits || metric.landedTicks, + fillCell: (metric: ActionMetrics, cellElem: HTMLElement) => { + cellElem.appendChild( + <> + {formatToNumber(metric.landedHits || metric.landedTicks, { fallbackString: '-' })} + {metric.landedHits && metric.landedTicks ? <> ({formatToNumber(metric.landedTicks, { fallbackString: '-' })}) : undefined} + , + ); + if (!metric.landedHits && !metric.landedTicks) return; + + const relativeHitPercent = (metric.hits / metric.landedHits) * 100; + const relativeCritPercent = (metric.crits / metric.landedHits) * 100; + const relativeTickPercent = (metric.ticks / metric.landedTicks) * 100; + const relativeCritTickPercent = (metric.critTicks / metric.landedTicks) * 100; + const relativeGlancePercent = (metric.glances / metric.landedHits) * 100; + const relativeBlockPercent = (metric.blocks / metric.landedHits) * 100; + const relativeCritBlockPercent = (metric.critBlocks / metric.landedHits) * 100; + + cellElem.appendChild( + , + ); + }, + }, + { + name: 'Avg Hit', + getValue: (metric: ActionMetrics) => metric.avgHit || metric.avgTick, + fillCell: (metric: ActionMetrics, cellElem: HTMLElement) => { + cellElem.appendChild( + <> + {formatToCompactNumber(metric.avgHit || metric.avgTick, { fallbackString: '-' })} + {metric.avgHit && metric.avgTick ? <> ({formatToCompactNumber(metric.avgTick, { fallbackString: '-' })}) : undefined} + , + ); + }, + }, + { + name: 'Miss %', + tooltip: TOOLTIP_METRIC_LABELS['Hit Miss %'], + getValue: (metric: ActionMetrics) => metric.totalMissesPercent, + fillCell: (metric: ActionMetrics, cellElem: HTMLElement) => { + cellElem.appendChild(<>{formatToPercent(metric.totalMissesPercent, { fallbackString: '-' })}); + if (!metric.totalMissesPercent) return; + + cellElem.appendChild( + , + ); + }, + }, + { + name: 'Crit %', + getValue: (metric: ActionMetrics) => metric.critPercent || metric.critTickPercent, + getDisplayString: (metric: ActionMetrics) => + `${formatToPercent(metric.critPercent || metric.critTickPercent, { fallbackString: '-' })}${ + metric.critPercent && metric.critTickPercent ? ` (${formatToPercent(metric.critTickPercent, { fallbackString: '-' })})` : '' + }`, + }, + { + name: 'DTPS', + sort: ColumnSortType.Descending, + headerCellClass: 'text-body', + columnClass: 'text-success', + getValue: (metric: ActionMetrics) => metric.dps, + getDisplayString: (metric: ActionMetrics) => formatToNumber(metric.dps, { minimumFractionDigits: 2, fallbackString: '-' }), + }, + ]); + } + + getGroupedMetrics(resultData: SimResultData): Array> { + const players = resultData.result.getRaidIndexedPlayers(resultData.filter); + if (players.length != 1) { + return []; + } + const player = players[0]; + + const targets = resultData.result.getTargets(resultData.filter); + const targetActions = targets.map(target => target.getDamageActions().map(action => action.forTarget({ player: player.unitIndex }))).flat(); + const actionGroups = ActionMetrics.groupById(targetActions); + + return actionGroups; + } + + mergeMetrics(metrics: Array): ActionMetrics { + // TODO: Use NPC ID here instead of pet ID. + return ActionMetrics.merge(metrics, { + removeTag: true, + actionIdOverride: metrics[0].unit?.petActionId || undefined, + }); + } +} diff --git a/ui/core/components/detailed_results/dtps_spell_metrics.ts b/ui/core/components/detailed_results/dtps_spell_metrics.ts deleted file mode 100644 index 8e5a05d417..0000000000 --- a/ui/core/components/detailed_results/dtps_spell_metrics.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { ActionMetrics, SimResult, SimResultFilter } from '../../proto_utils/sim_result.js'; - -import { ColumnSortType, MetricsTable } from './metrics_table.js'; -import { ResultComponent, ResultComponentConfig, SimResultData } from './result_component.js'; - -export class DtpsSpellMetricsTable extends MetricsTable { - constructor(config: ResultComponentConfig) { - config.rootCssClass = 'dtps-spell-metrics-root'; - super(config, [ - MetricsTable.nameCellConfig((metric: ActionMetrics) => { - return { - name: metric.name, - actionId: metric.actionId, - }; - }), - { - name: 'DPS', - tooltip: 'Damage / Encounter Duration', - sort: ColumnSortType.Descending, - getValue: (metric: ActionMetrics) => metric.dps, - getDisplayString: (metric: ActionMetrics) => metric.dps.toFixed(1), - }, - { - name: 'Avg Cast', - tooltip: 'Damage / Casts', - getValue: (metric: ActionMetrics) => metric.avgCast, - getDisplayString: (metric: ActionMetrics) => metric.avgCast.toFixed(1), - }, - { - name: 'Avg Hit', - tooltip: 'Damage / (Hits + Crits)', - getValue: (metric: ActionMetrics) => metric.avgHit, - getDisplayString: (metric: ActionMetrics) => metric.avgHit.toFixed(1), - }, - { - name: 'Casts', - tooltip: 'Casts', - getValue: (metric: ActionMetrics) => metric.casts, - getDisplayString: (metric: ActionMetrics) => metric.casts.toFixed(1), - }, - { - name: 'Hits', - tooltip: 'Hits + Crits', - getValue: (metric: ActionMetrics) => metric.landedHits, - getDisplayString: (metric: ActionMetrics) => metric.landedHits.toFixed(1), - }, - { - name: 'Miss %', - tooltip: 'Misses / Swings', - getValue: (metric: ActionMetrics) => metric.missPercent, - getDisplayString: (metric: ActionMetrics) => metric.missPercent.toFixed(2) + '%', - }, - { - name: 'Crit %', - tooltip: 'Crits / Swings', - getValue: (metric: ActionMetrics) => metric.critPercent, - getDisplayString: (metric: ActionMetrics) => metric.critPercent.toFixed(2) + '%', - }, - ]); - } - - getGroupedMetrics(resultData: SimResultData): Array> { - const players = resultData.result.getPlayers(resultData.filter); - if (players.length != 1) { - return []; - } - const player = players[0]; - - const targets = resultData.result.getTargets(resultData.filter); - const targetActions = targets.map(target => target.getSpellActions().map(action => action.forTarget(resultData.filter))).flat(); - const actionGroups = ActionMetrics.groupById(targetActions); - - return actionGroups; - } - - mergeMetrics(metrics: Array): ActionMetrics { - // TODO: Use NPC ID here instead of pet ID. - return ActionMetrics.merge(metrics, true, metrics[0].unit?.petActionId || undefined); - } -} diff --git a/ui/core/components/detailed_results/healing_metrics.ts b/ui/core/components/detailed_results/healing_metrics.ts deleted file mode 100644 index d27306b0e2..0000000000 --- a/ui/core/components/detailed_results/healing_metrics.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { ActionMetrics, SimResult, SimResultFilter } from '../../proto_utils/sim_result.js'; - -import { ColumnSortType, MetricsTable } from './metrics_table.js'; -import { ResultComponent, ResultComponentConfig, SimResultData } from './result_component.js'; - -export class HealingMetricsTable extends MetricsTable { - constructor(config: ResultComponentConfig) { - config.rootCssClass = 'healing-metrics-root'; - super(config, [ - MetricsTable.nameCellConfig((metric: ActionMetrics) => { - return { - name: metric.name, - actionId: metric.actionId, - }; - }), - { - name: 'Casts', - tooltip: 'Casts', - getValue: (metric: ActionMetrics) => metric.casts, - getDisplayString: (metric: ActionMetrics) => metric.casts.toFixed(1), - }, - { - name: 'CPM', - tooltip: 'Casts / (Encounter Duration / 60 Seconds)', - getValue: (metric: ActionMetrics) => metric.castsPerMinute, - getDisplayString: (metric: ActionMetrics) => metric.castsPerMinute.toFixed(1), - }, - { - name: 'Cast Time', - tooltip: 'Average cast time in seconds', - getValue: (metric: ActionMetrics) => metric.avgCastTimeMs, - getDisplayString: (metric: ActionMetrics) => (metric.avgCastTimeMs / 1000).toFixed(2), - }, - { - name: 'HPM', - tooltip: 'Healing / Mana', - getValue: (metric: ActionMetrics) => metric.hpm, - getDisplayString: (metric: ActionMetrics) => metric.hpm.toFixed(1), - }, - { - name: 'HPET', - tooltip: 'Healing / Avg Cast Time', - getValue: (metric: ActionMetrics) => metric.healingThroughput, - getDisplayString: (metric: ActionMetrics) => metric.healingThroughput.toFixed(1), - }, - { - name: 'HPS', - tooltip: 'Healing / Encounter Duration', - sort: ColumnSortType.Descending, - getValue: (metric: ActionMetrics) => metric.hps, - getDisplayString: (metric: ActionMetrics) => metric.hps.toFixed(1), - }, - { - name: 'Avg Cast', - tooltip: 'Healing / Casts', - getValue: (metric: ActionMetrics) => metric.avgCastHealing, - getDisplayString: (metric: ActionMetrics) => metric.avgCastHealing.toFixed(1), - }, - { - name: 'TPS', - tooltip: 'Threat / Encounter Duration', - columnClass: 'threat-metrics', - getValue: (metric: ActionMetrics) => metric.tps, - getDisplayString: (metric: ActionMetrics) => metric.tps.toFixed(1), - }, - { - name: 'Avg Cast', - tooltip: 'Threat / Casts', - columnClass: 'threat-metrics', - getValue: (metric: ActionMetrics) => metric.avgCastThreat, - getDisplayString: (metric: ActionMetrics) => metric.avgCastThreat.toFixed(1), - }, - { - name: 'Crit %', - tooltip: 'Crits / Hits', - getValue: (metric: ActionMetrics) => metric.critPercent, - getDisplayString: (metric: ActionMetrics) => metric.critPercent.toFixed(2) + '%', - }, - ]); - } - - customizeRowElem(action: ActionMetrics, rowElem: HTMLElement) { - if (action.hitAttempts == 0 && action.hps == 0) { - rowElem.classList.add('threat-metrics'); - } - } - - getGroupedMetrics(resultData: SimResultData): Array> { - const players = resultData.result.getPlayers(resultData.filter); - if (players.length != 1) { - return []; - } - const player = players[0]; - - //const actions = player.getSpellActions().map(action => action.forTarget(resultData.filter)); - const actions = player.getHealingActions(); - const actionGroups = ActionMetrics.groupById(actions); - - return actionGroups; - } - - mergeMetrics(metrics: Array): ActionMetrics { - return ActionMetrics.merge(metrics, true, metrics[0].unit?.petActionId || undefined); - } - - shouldCollapse(metric: ActionMetrics): boolean { - return !metric.unit?.isPet; - } -} diff --git a/ui/core/components/detailed_results/healing_metrics.tsx b/ui/core/components/detailed_results/healing_metrics.tsx new file mode 100644 index 0000000000..1dfe56db89 --- /dev/null +++ b/ui/core/components/detailed_results/healing_metrics.tsx @@ -0,0 +1,330 @@ +import { TOOLTIP_METRIC_LABELS } from '../../constants/tooltips'; +import { ActionMetrics } from '../../proto_utils/sim_result.js'; +import { formatToCompactNumber, formatToNumber, formatToPercent } from '../../utils.js'; +import { MetricsCombinedTooltipTable } from './metrics_table/metrics_combined_tooltip_table'; +import { ColumnSortType, MetricsTable } from './metrics_table/metrics_table.jsx'; +import { MetricsTotalBar } from './metrics_table/metrics_total_bar'; +import { ResultComponentConfig, SimResultData } from './result_component.js'; + +export class HealingMetricsTable extends MetricsTable { + maxHealingAmount: number | null = null; + constructor(config: ResultComponentConfig) { + config.rootCssClass = 'healing-metrics-root'; + config.resultsEmitter.on((_, resultData) => { + const lastResult = resultData + ? this.getGroupedMetrics(resultData) + .filter(g => g.length) + .map(groups => this.mergeMetrics(groups)) + : undefined; + this.maxHealingAmount = Math.max(...(lastResult || []).map(a => a.healing)); + }); + super(config, [ + MetricsTable.nameCellConfig((metric: ActionMetrics) => { + return { + name: metric.name, + actionId: metric.actionId, + metricType: metric.constructor?.name, + }; + }), + { + name: 'Healing done', + headerCellClass: 'text-center metrics-table-cell--primary-metric', + columnClass: 'metrics-table-cell--primary-metric', + getValue: (metric: ActionMetrics) => metric.avgHealing, + fillCell: (metric: ActionMetrics, cellElem: HTMLElement) => { + cellElem.appendChild( + , + ); + + cellElem.appendChild( + , + ); + }, + }, + { + name: 'Casts', + getValue: (metric: ActionMetrics) => metric.casts, + getDisplayString: (metric: ActionMetrics) => formatToNumber(metric.casts, { fallbackString: '-' }), + }, + { + name: 'CPM', + getValue: (metric: ActionMetrics) => metric.castsPerMinute, + getDisplayString: (metric: ActionMetrics) => formatToNumber(metric.castsPerMinute, { fallbackString: '-' }), + }, + { + name: 'Cast Time', + getValue: (metric: ActionMetrics) => metric.avgCastTimeMs, + getDisplayString: (metric: ActionMetrics) => formatToNumber(metric.avgCastTimeMs / 1000, { minimumFractionDigits: 2, fallbackString: '-' }), + }, + { + name: 'Avg Cast', + tooltip: TOOLTIP_METRIC_LABELS['Healing Avg Cast'], + getValue: (metric: ActionMetrics) => metric.avgCastHealing, + fillCell: (metric: ActionMetrics, cellElem: HTMLElement) => { + cellElem.appendChild(<>{formatToCompactNumber(metric.avgCastHealing, { fallbackString: '-' })}); + if (!metric.avgCastHealing) return; + + cellElem.appendChild( + { + const hideThreatMetrics = !!document.querySelector('.hide-threat-metrics'); + if (hideThreatMetrics) return false; + }, + }} + headerValues={[, 'Amount']} + groups={[ + { + spellSchool: metric.spellSchool, + total: metric.avgCastThreat, + totalPercentage: 100, + data: [ + { + name: 'Threat', + value: metric.avgCastThreat, + percentage: 100, + }, + ], + }, + ]} + />, + ); + }, + }, + { + name: 'Hits', + tooltip: TOOLTIP_METRIC_LABELS['Healing Hits'], + getValue: (metric: ActionMetrics) => metric.landedHits, + fillCell: (metric: ActionMetrics, cellElem: HTMLElement) => { + cellElem.appendChild( + <> + {formatToNumber(metric.landedHits, { fallbackString: '-' })} + {metric.landedTicks ? <> ({formatToNumber(metric.landedTicks, { fallbackString: '-' })}) : undefined}{' '} + , + ); + if (!metric.landedHits && !metric.landedTicks) return; + + const relativeHitPercent = (metric.hits / metric.landedHits) * 100; + const relativeCritPercent = (metric.crits / metric.landedHits) * 100; + const relativeTickPercent = (metric.ticks / metric.landedTicks) * 100; + const relativeCritTickPercent = (metric.critTicks / metric.landedTicks) * 100; + const relativeGlancePercent = (metric.glances / metric.landedHits) * 100; + const relativeBlockPercent = (metric.blocks / metric.landedHits) * 100; + const relativeCritBlockPercent = (metric.critBlocks / metric.landedHits) * 100; + + cellElem.appendChild( + , + ); + }, + }, + { + name: 'Avg Hit', + tooltip: TOOLTIP_METRIC_LABELS['Healing Avg Hit'], + getValue: (metric: ActionMetrics) => metric.avgHitHealing, + fillCell: (metric: ActionMetrics, cellElem: HTMLElement) => { + cellElem.appendChild(<>{formatToCompactNumber(metric.avgHitHealing, { fallbackString: '-' })}); + if (!metric.avgHitHealing) return; + + cellElem.appendChild( + { + const hideThreatMetrics = !!document.querySelector('.hide-threat-metrics'); + if (hideThreatMetrics) return false; + }, + }} + headerValues={[, 'Amount']} + groups={[ + { + spellSchool: metric.spellSchool, + total: metric.avgHitThreat, + totalPercentage: 100, + data: [ + { + name: 'Threat', + value: metric.avgHitThreat, + percentage: 100, + }, + ], + }, + ]} + />, + ); + }, + }, + { + name: 'HPM', + getValue: (metric: ActionMetrics) => metric.hpm, + getDisplayString: (metric: ActionMetrics) => formatToCompactNumber(metric.hpm, { fallbackString: '-' }), + }, + + { + name: 'Crit %', + getValue: (metric: ActionMetrics) => metric.healingCritPercent, + getDisplayString: (metric: ActionMetrics) => formatToPercent(metric.healingCritPercent, { fallbackString: '-' }), + }, + { + name: 'HPET', + getValue: (metric: ActionMetrics) => metric.healingThroughput, + getDisplayString: (metric: ActionMetrics) => formatToCompactNumber(metric.healingThroughput, { fallbackString: '-' }), + }, + { + name: 'HPS', + sort: ColumnSortType.Descending, + headerCellClass: 'text-body', + columnClass: 'text-success', + getValue: (metric: ActionMetrics) => metric.hps, + fillCell: (metric: ActionMetrics, cellElem: HTMLElement) => { + cellElem.appendChild(<>{formatToNumber(metric.hps, { minimumFractionDigits: 2 })}); + + cellElem.appendChild( + { + const hideThreatMetrics = !!document.querySelector('.hide-threat-metrics'); + if (hideThreatMetrics) return false; + }, + }} + headerValues={[, 'Amount']} + groups={[ + { + spellSchool: metric.spellSchool, + total: metric.tps, + totalPercentage: 100, + data: [ + { + name: 'Threat', + value: metric.tps, + percentage: 100, + }, + ], + }, + ]} + />, + ); + }, + }, + ]); + } + + customizeRowElem(action: ActionMetrics, rowElem: HTMLElement) { + if (action.hitAttempts == 0 && action.hps == 0) { + rowElem.classList.add('threat-metrics'); + } + } + + getGroupedMetrics(resultData: SimResultData): Array> { + const players = resultData.result.getRaidIndexedPlayers(resultData.filter); + if (players.length != 1) { + return []; + } + const player = players[0]; + + //const actions = player.getSpellActions().map(action => action.forTarget(resultData.filter)); + // TODO: Do we want to show 0 hps metrics in here? Make it conditional for healing sims + // in case they want to show the threat for non healing spells + const actions = player.getHealingActions().filter(action => action.hps > 0); + const actionGroups = ActionMetrics.groupById(actions); + + return actionGroups; + } + + mergeMetrics(metrics: Array): ActionMetrics { + return ActionMetrics.merge(metrics, { + removeTag: true, + actionIdOverride: metrics[0]?.unit?.petActionId || undefined, + }); + } + + shouldCollapse(metric: ActionMetrics): boolean { + return !metric.unit?.isPet; + } +} diff --git a/ui/core/components/detailed_results/melee_metrics.ts b/ui/core/components/detailed_results/melee_metrics.ts deleted file mode 100644 index d73fa3ad46..0000000000 --- a/ui/core/components/detailed_results/melee_metrics.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { ActionMetrics, SimResult, SimResultFilter } from '../../proto_utils/sim_result.js'; -import { bucket } from '../../utils.js'; - -import { ColumnSortType, MetricsTable } from './metrics_table.js'; -import { ResultComponent, ResultComponentConfig, SimResultData } from './result_component.js'; - -export class MeleeMetricsTable extends MetricsTable { - constructor(config: ResultComponentConfig) { - config.rootCssClass = 'melee-metrics-root'; - super(config, [ - MetricsTable.nameCellConfig((metric: ActionMetrics) => { - return { - name: metric.name, - actionId: metric.actionId, - }; - }), - { - name: 'DPS', - tooltip: 'Damage / Encounter Duration', - sort: ColumnSortType.Descending, - getValue: (metric: ActionMetrics) => metric.dps, - getDisplayString: (metric: ActionMetrics) => metric.dps.toFixed(1), - }, - { - name: 'Avg Cast', - tooltip: 'Damage / Casts', - getValue: (metric: ActionMetrics) => metric.avgCast, - getDisplayString: (metric: ActionMetrics) => metric.avgCast.toFixed(1), - }, - { - name: 'Avg Hit', - tooltip: 'Damage / (Hits + Crits + Glances + Blocks)', - getValue: (metric: ActionMetrics) => metric.avgHit, - getDisplayString: (metric: ActionMetrics) => metric.avgHit.toFixed(1), - }, - { - name: 'TPS', - tooltip: 'Threat / Encounter Duration', - columnClass: 'threat-metrics', - getValue: (metric: ActionMetrics) => metric.tps, - getDisplayString: (metric: ActionMetrics) => metric.tps.toFixed(1), - }, - { - name: 'Avg Cast', - tooltip: 'Threat / Casts', - columnClass: 'threat-metrics', - getValue: (metric: ActionMetrics) => metric.avgCastThreat, - getDisplayString: (metric: ActionMetrics) => metric.avgCastThreat.toFixed(1), - }, - { - name: 'Avg Hit', - tooltip: 'Threat / (Hits + Crits + Glances + Blocks)', - columnClass: 'threat-metrics', - getValue: (metric: ActionMetrics) => metric.avgHitThreat, - getDisplayString: (metric: ActionMetrics) => metric.avgHitThreat.toFixed(1), - }, - { - name: 'Casts', - tooltip: 'Casts', - getValue: (metric: ActionMetrics) => metric.casts, - getDisplayString: (metric: ActionMetrics) => metric.casts.toFixed(1), - }, - { - name: 'Hits', - tooltip: 'Hits + Crits + Glances + Blocks', - getValue: (metric: ActionMetrics) => metric.landedHits, - getDisplayString: (metric: ActionMetrics) => metric.landedHits.toFixed(1), - }, - { - name: 'Miss %', - tooltip: 'Misses / Swings', - getValue: (metric: ActionMetrics) => metric.missPercent, - getDisplayString: (metric: ActionMetrics) => metric.missPercent.toFixed(2) + '%', - }, - { - name: 'Dodge %', - tooltip: 'Dodges / Swings', - getValue: (metric: ActionMetrics) => metric.dodgePercent, - getDisplayString: (metric: ActionMetrics) => metric.dodgePercent.toFixed(2) + '%', - }, - { - name: 'Parry %', - tooltip: 'Parries / Swings', - columnClass: 'in-front-of-target', - getValue: (metric: ActionMetrics) => metric.parryPercent, - getDisplayString: (metric: ActionMetrics) => metric.parryPercent.toFixed(2) + '%', - }, - { - name: 'Block %', - tooltip: 'Blocks / Swings', - columnClass: 'in-front-of-target', - getValue: (metric: ActionMetrics) => metric.blockPercent, - getDisplayString: (metric: ActionMetrics) => metric.blockPercent.toFixed(2) + '%', - }, - { - name: 'Glance %', - tooltip: 'Glances / Swings', - getValue: (metric: ActionMetrics) => metric.glancePercent, - getDisplayString: (metric: ActionMetrics) => metric.glancePercent.toFixed(2) + '%', - }, - { - name: 'Crit %', - tooltip: 'Crits / Swings', - getValue: (metric: ActionMetrics) => metric.critPercent, - getDisplayString: (metric: ActionMetrics) => metric.critPercent.toFixed(2) + '%', - }, - ]); - } - - getGroupedMetrics(resultData: SimResultData): Array> { - const players = resultData.result.getPlayers(resultData.filter); - if (players.length != 1) { - return []; - } - const player = players[0]; - - if (player.inFrontOfTarget) { - this.rootElem.classList.remove('hide-in-front-of-target'); - } else { - this.rootElem.classList.add('hide-in-front-of-target'); - } - - const actions = player.getMeleeActions().map(action => action.forTarget(resultData.filter)); - const actionGroups = ActionMetrics.groupById(actions); - - const petsByName = bucket(player.pets, pet => pet.name); - const petGroups = Object.values(petsByName).map(pets => ActionMetrics.joinById(pets.map(pet => pet.getMeleeActions().map(action => action.forTarget(resultData.filter))).flat(), true)); - - return actionGroups.concat(petGroups); - } - - mergeMetrics(metrics: Array): ActionMetrics { - return ActionMetrics.merge(metrics, true, metrics[0].unit?.petActionId || undefined); - } - - shouldCollapse(metric: ActionMetrics): boolean { - return !metric.unit?.isPet; - } -} diff --git a/ui/core/components/detailed_results/metrics_table/metrics_combined_tooltip_table.tsx b/ui/core/components/detailed_results/metrics_table/metrics_combined_tooltip_table.tsx new file mode 100644 index 0000000000..88fd9d9859 --- /dev/null +++ b/ui/core/components/detailed_results/metrics_table/metrics_combined_tooltip_table.tsx @@ -0,0 +1,101 @@ +import clsx from 'clsx'; +import tippy, { Props as TippyProps } from 'tippy.js'; + +import { SpellSchool } from '../../../proto/common'; +import { formatToCompactNumber } from '../../../utils'; +import { MetricsTotalBar, MetricsTotalBarProps } from './metrics_total_bar'; + +type MetricsCombinedGroup = { + name?: string; + cssClass?: string; + total?: number; + totalPercentage: number; + spellSchool: SpellSchool | undefined | null; + data: MetricsCombinedTableEntry[]; +}; +type MetricsCombinedTableEntry = { + name: string; + min?: number; + max?: number; + average?: number; + percentage: number; +} & Pick; + +type MetricsCombinedTooltipTableProps = { + tooltipElement: HTMLElement; + tooltipConfig?: Partial; + groups: MetricsCombinedGroup[]; + hasMetricBars?: boolean; + headerValues?: (MetricsCombinedGroup['name'] | undefined)[]; +}; +export const MetricsCombinedTooltipTable = ({ + tooltipElement, + tooltipConfig, + headerValues, + groups, + hasMetricBars = true, +}: MetricsCombinedTooltipTableProps) => { + const displayGroups = groups.filter(group => group.data.some(d => d.value)).map(group => ({ ...group, data: group.data.filter(v => v.value) })); + const hasAverageColumn = displayGroups.some(group => group.data.find(d => typeof d.average === 'number')); + + if (groups.length) { + tippy(tooltipElement, { + placement: 'auto', + theme: 'metrics-table', + maxWidth: 'none', + ...tooltipConfig, + content: ( + + + + + + {hasAverageColumn ? : undefined} + + + + {displayGroups.map(({ name: groupName, cssClass, data, spellSchool, totalPercentage }) => { + const maxValue = Math.max(...data.map(a => a.value)); + const columnCount = data.some(d => typeof d.average === 'number') ? 3 : 2; + return ( + <> + {groupName && displayGroups.length > 1 ? ( + + + + ) : undefined} + {data + .sort((a, b) => b.value - a.value) + .map(({ name, value, percentage, average }) => ( + <> + + + + {typeof average === 'number' ? : undefined} + + + ))} + + ); + })} + +
{headerValues?.[0] || 'Type'}{headerValues?.[1] || 'Count'}{headerValues?.[2] || 'Average'}
+ {groupName} +
{name} + {hasMetricBars ? ( + + ) : ( + formatToCompactNumber(value) + )} + {formatToCompactNumber(average)}
+ ), + }); + } + return <>; +}; diff --git a/ui/core/components/detailed_results/metrics_table.tsx b/ui/core/components/detailed_results/metrics_table/metrics_table.tsx similarity index 73% rename from ui/core/components/detailed_results/metrics_table.tsx rename to ui/core/components/detailed_results/metrics_table/metrics_table.tsx index 79b84be1a9..8238202f02 100644 --- a/ui/core/components/detailed_results/metrics_table.tsx +++ b/ui/core/components/detailed_results/metrics_table/metrics_table.tsx @@ -1,10 +1,11 @@ import tippy from 'tippy.js'; import { ref } from 'tsx-vanilla'; -import { ActionId } from '../../proto_utils/action_id.js'; -import { ActionMetrics, SimResult, SimResultFilter,UnitMetrics } from '../../proto_utils/sim_result.js'; -import { EventID, TypedEvent } from '../../typed_event.js'; -import { ResultComponent, ResultComponentConfig, SimResultData } from './result_component.js'; +import { TOOLTIP_METRIC_LABELS } from '../../../constants/tooltips'; +import { ActionId } from '../../../proto_utils/action_id'; +import { ActionMetrics, AuraMetrics, ResourceMetrics, UnitMetrics } from '../../../proto_utils/sim_result'; +import { TypedEvent } from '../../../typed_event'; +import { ResultComponent, ResultComponentConfig, SimResultData } from '../result_component'; declare let $: any; @@ -21,14 +22,14 @@ export interface MetricsColumnConfig { columnClass?: string; sort?: ColumnSortType; - getValue?: (metric: T) => number; + getValue?: (metric: T, isChildRow?: boolean) => number; // Either getDisplayString or fillCell must be specified. - getDisplayString?: (metric: T) => string; - fillCell?: (metric: T, cellElem: HTMLElement, rowElem: HTMLElement) => void; + getDisplayString?: (metric: T, isChildRow?: boolean) => string; + fillCell?: (metric: T, cellElem: HTMLElement, rowElem: HTMLElement, isChildRow?: boolean) => void; } -export abstract class MetricsTable extends ResultComponent { +export abstract class MetricsTable extends ResultComponent { private readonly columnConfigs: Array>; protected readonly tableElem: HTMLElement; @@ -55,17 +56,18 @@ export abstract class MetricsTable extends ResultComponent { const headerRowElem = this.rootElem.getElementsByClassName('metrics-table-header-row')[0] as HTMLElement; this.columnConfigs.forEach(columnConfig => { const headerCell = document.createElement('th'); + const tooltip = columnConfig.tooltip || TOOLTIP_METRIC_LABELS[columnConfig.name as keyof typeof TOOLTIP_METRIC_LABELS]; headerCell.classList.add('metrics-table-header-cell'); - if (columnConfig.headerCellClass) { - headerCell.classList.add(columnConfig.headerCellClass); - } if (columnConfig.columnClass) { - headerCell.classList.add(columnConfig.columnClass); + headerCell.classList.add(...columnConfig.columnClass.split(' ')); + } + if (columnConfig.headerCellClass) { + headerCell.classList.add(...columnConfig.headerCellClass.split(' ')); } headerCell.appendChild({columnConfig.name}); - if (columnConfig.tooltip) { + if (tooltip) { tippy(headerCell, { - content: columnConfig.tooltip, + content: tooltip, ignoreAttributes: true, }); } @@ -75,6 +77,7 @@ export abstract class MetricsTable extends ResultComponent { const sortList = this.columnConfigs .map((config, i) => [i, config.sort == ColumnSortType.Ascending ? 0 : 1]) .filter(sortData => this.columnConfigs[sortData[0]].sort); + $(this.tableElem).tablesorter({ sortList: sortList, cssChildRow: 'child-metric', @@ -96,26 +99,29 @@ export abstract class MetricsTable extends ResultComponent { }); } - private addRow(metric: T): HTMLElement { + private addRow(metric: T, isChildRow = false): HTMLElement { const rowElem = document.createElement('tr'); this.bodyElem.appendChild(rowElem); this.columnConfigs.forEach(columnConfig => { const cellElem = document.createElement('td'); + if (columnConfig.getValue) { + cellElem.dataset.text = String(columnConfig.getValue(metric, isChildRow)); + } if (columnConfig.columnClass) { cellElem.classList.add(columnConfig.columnClass); } if (columnConfig.fillCell) { - columnConfig.fillCell(metric, cellElem, rowElem); + columnConfig.fillCell(metric, cellElem, rowElem, isChildRow); } else if (columnConfig.getDisplayString) { - cellElem.textContent = columnConfig.getDisplayString(metric); + cellElem.textContent = columnConfig.getDisplayString(metric, isChildRow); } else { throw new Error('Metrics column config does not provide content function: ' + columnConfig.name); } rowElem.appendChild(cellElem); }); - this.customizeRowElem(metric, rowElem); + this.customizeRowElem(metric, rowElem, isChildRow); return rowElem; } @@ -134,12 +140,12 @@ export abstract class MetricsTable extends ResultComponent { const mergedMetrics = this.mergeMetrics(metrics); const parentRow = this.addRow(mergedMetrics); - const childRows = metrics.map(metric => this.addRow(metric)); + const childRows = metrics.map(metric => this.addRow(metric, true)); childRows.forEach(childRow => childRow.classList.add('child-metric')); let expand = true; parentRow.classList.add('parent-metric', 'expand'); - parentRow.addEventListener('click', event => { + parentRow.addEventListener('click', () => { expand = !expand; if (expand) { childRows.forEach(row => row.classList.remove('hide')); @@ -161,7 +167,6 @@ export abstract class MetricsTable extends ResultComponent { } else { this.rootElem.classList.remove('hide'); } - groupedMetrics.forEach(group => this.addGroup(group)); $(this.tableElem).trigger('update'); this.onUpdate.emit(resultData.eventID); @@ -174,7 +179,9 @@ export abstract class MetricsTable extends ResultComponent { } // Override this to customize rowElem after it has been populated. - protected customizeRowElem(metric: T, rowElem: HTMLElement) {} + protected customizeRowElem(metric: T, rowElem: HTMLElement, isChildRow = false) { + return; + } // Override this to provide custom merge behavior. protected mergeMetrics(metrics: Array): T { @@ -184,21 +191,32 @@ export abstract class MetricsTable extends ResultComponent { // Returns grouped metrics to display. abstract getGroupedMetrics(resultData: SimResultData): Array>; - static nameCellConfig(getData: (metric: T) => { name: string; actionId: ActionId }): MetricsColumnConfig { + static nameCellConfig( + getData: (metric: T) => { + name: string; + actionId: ActionId; + metricType: string; + } & Pick, 'columnClass' | 'headerCellClass'>, + ): MetricsColumnConfig { return { name: 'Name', fillCell: (metric: T, cellElem: HTMLElement, rowElem: HTMLElement) => { const data = getData(metric); const iconElem = ref(); cellElem.appendChild( - <> +
- {data.name} - + {data.name} - , + +
, ); - data.actionId.setBackgroundAndHref(iconElem.value!); + if (iconElem.value) { + data.actionId.setBackgroundAndHref(iconElem.value); + data.actionId.setWowheadDataset(iconElem.value, { + useBuffAura: data.metricType === 'AuraMetrics', + }); + } }, }; } @@ -206,6 +224,7 @@ export abstract class MetricsTable extends ResultComponent { static playerNameCellConfig(): MetricsColumnConfig { return { name: 'Name', + columnClass: 'name-cell', fillCell: (player: UnitMetrics, cellElem: HTMLElement, rowElem: HTMLElement) => { cellElem.appendChild( <> diff --git a/ui/core/components/detailed_results/metrics_table/metrics_total_bar.tsx b/ui/core/components/detailed_results/metrics_table/metrics_total_bar.tsx new file mode 100644 index 0000000000..4984a0257a --- /dev/null +++ b/ui/core/components/detailed_results/metrics_table/metrics_total_bar.tsx @@ -0,0 +1,41 @@ +import clsx from 'clsx'; + +import { SpellSchool } from '../../../proto/common'; +import { spellSchoolNames } from '../../../proto_utils/names'; +import { formatToCompactNumber, formatToPercent } from '../../../utils'; + +export type MetricsTotalBarProps = { + percentage: number | undefined | null; + max: number | null; + total: number; + value: number; + // Used for overlayed value display, such as shielding. + // Will show as darkened bar on top of main bar. + overlayValue?: number; + spellSchool?: SpellSchool | undefined | null; + classColor?: string | undefined | null; +}; +export const MetricsTotalBar = ({ percentage, max, total, value, overlayValue, spellSchool, classColor }: MetricsTotalBarProps) => { + const spellSchoolString = typeof spellSchool === 'number' ? spellSchoolNames.get(spellSchool) : undefined; + return ( +
+
{formatToPercent(percentage || 0)}
+
+
+ {overlayValue ? ( +
+ ) : undefined} +
+
{formatToCompactNumber(total)}
+
+ ); +}; diff --git a/ui/core/components/detailed_results/player_damage.ts b/ui/core/components/detailed_results/player_damage.ts deleted file mode 100644 index 0922c9cb0e..0000000000 --- a/ui/core/components/detailed_results/player_damage.ts +++ /dev/null @@ -1,90 +0,0 @@ -import tippy from 'tippy.js'; - -import { SimResult, SimResultFilter,UnitMetrics } from '../../proto_utils/sim_result.js'; -import { maxIndex } from '../../utils.js'; -import { ColumnSortType, MetricsTable } from './metrics_table.js'; -import { ResultComponent, ResultComponentConfig, SimResultData } from './result_component.js'; -import { ResultsFilter } from './results_filter.js'; -import { SourceChart } from './source_chart.js'; - -export class PlayerDamageMetricsTable extends MetricsTable { - private readonly resultsFilter: ResultsFilter; - - // Cached values from most recent result. - private raidDps: number; - private maxDps: number; - - constructor(config: ResultComponentConfig, resultsFilter: ResultsFilter) { - config.rootCssClass = 'player-damage-metrics-root'; - super(config, [ - MetricsTable.playerNameCellConfig(), - { - name: 'Amount', - tooltip: 'Player Damage / Raid Damage', - headerCellClass: 'amount-header-cell', - fillCell: (player: UnitMetrics, cellElem: HTMLElement, rowElem: HTMLElement) => { - cellElem.classList.add('amount-cell'); - - let chart: HTMLElement | null = null; - const makeChart = () => { - const chartContainer = document.createElement('div'); - rowElem.appendChild(chartContainer); - const sourceChart = new SourceChart(chartContainer, player.actions); - return chartContainer; - }; - - tippy(rowElem, { - content: 'Loading...', - placement: 'bottom', - ignoreAttributes: true, - onShow(instance: any) { - if (!chart) { - chart = makeChart(); - instance.setContent(chart); - } - }, - }); - - cellElem.innerHTML = ` -
- ${(player.dps.avg / this.raidDps * 100).toFixed(2)}% -
-
-
-
-
- ${(player.totalDamage / 1000).toFixed(1)}k -
- `; - }, - }, - { - name: 'DPS', - tooltip: 'Damage / Encounter Duration', - sort: ColumnSortType.Descending, - getValue: (metric: UnitMetrics) => metric.dps.avg, - getDisplayString: (metric: UnitMetrics) => metric.dps.avg.toFixed(1), - }, - ]); - this.resultsFilter = resultsFilter; - this.raidDps = 0; - this.maxDps = 0; - } - - customizeRowElem(player: UnitMetrics, rowElem: HTMLElement) { - rowElem.classList.add('player-damage-row'); - rowElem.addEventListener('click', event => { - this.resultsFilter.setPlayer(this.getLastSimResult().eventID, player.unitIndex); - }); - } - - getGroupedMetrics(resultData: SimResultData): Array> { - const players = resultData.result.getPlayers(resultData.filter); - - this.raidDps = resultData.result.raidMetrics.dps.avg; - const maxDpsIndex = maxIndex(players.map(player => player.dps.avg))!; - this.maxDps = players[maxDpsIndex].dps.avg; - - return players.map(player => [player]); - } -} diff --git a/ui/core/components/detailed_results/player_damage.tsx b/ui/core/components/detailed_results/player_damage.tsx new file mode 100644 index 0000000000..f2f7bb3f94 --- /dev/null +++ b/ui/core/components/detailed_results/player_damage.tsx @@ -0,0 +1,120 @@ +import tippy from 'tippy.js'; + +import { SimResult, SimResultFilter, UnitMetrics } from '../../proto_utils/sim_result.js'; +import { maxIndex, sum } from '../../utils.js'; +import { ColumnSortType, MetricsTable } from './metrics_table/metrics_table.js'; +import { MetricsTotalBar } from './metrics_table/metrics_total_bar'; +import { ResultComponent, ResultComponentConfig, SimResultData } from './result_component.js'; +import { ResultsFilter } from './results_filter.js'; +import { SourceChart } from './source_chart.js'; + +export class PlayerDamageMetricsTable extends MetricsTable { + private readonly resultsFilter: ResultsFilter; + + // Cached values from most recent result. + private raidDps: number; + private maxDps: number; + + constructor(config: ResultComponentConfig, resultsFilter: ResultsFilter) { + config.rootCssClass = 'player-damage-metrics-root'; + super(config, [ + MetricsTable.playerNameCellConfig(), + { + name: 'Amount', + tooltip: 'Player Damage / Raid Damage', + headerCellClass: 'amount-header-cell text-center', + fillCell: (player: UnitMetrics, cellElem: HTMLElement, rowElem: HTMLElement) => { + cellElem.classList.add('amount-cell'); + + let chart: HTMLElement | null = null; + const makeChart = () => { + const chartContainer = document.createElement('div'); + rowElem.appendChild(chartContainer); + const playerActions = player + .getPlayerAndPetActions() + .map(action => action.forTarget(this.resultsFilter.getFilter())) + .flat(); + const sourceChart = new SourceChart(chartContainer, playerActions); + return chartContainer; + }; + + tippy(rowElem, { + content: 'Loading...', + placement: 'bottom', + ignoreAttributes: true, + onShow(instance: any) { + if (!chart) { + chart = makeChart(); + instance.setContent(chart); + } + }, + }); + + const playerDps = this.getPlayerDps(player); + + cellElem.appendChild( + , + ); + }, + }, + { + name: 'DPS', + tooltip: 'Damage / Encounter Duration', + columnClass: 'dps-cell', + sort: ColumnSortType.Descending, + getValue: (player: UnitMetrics) => this.getPlayerDps(player), + getDisplayString: (player: UnitMetrics) => this.getPlayerDps(player).toFixed(1), + }, + ]); + this.resultsFilter = resultsFilter; + this.raidDps = 0; + this.maxDps = 0; + } + + private getPlayerDps(player: UnitMetrics): number { + const playerActions = player + .getPlayerAndPetActions() + .map(action => action.forTarget(this.resultsFilter.getFilter())) + .flat(); + const playerDps = sum(playerActions.map(action => action.dps)); + return playerDps; + } + + customizeRowElem(player: UnitMetrics, rowElem: HTMLElement) { + rowElem.classList.add('player-damage-row'); + rowElem.addEventListener('click', event => { + this.resultsFilter.setPlayer(this.getLastSimResult().eventID, player.index); + }); + } + + getGroupedMetrics(resultData: SimResultData): Array> { + const players = resultData.result.getPlayers(resultData.filter); + + const targetActions = players.map(player => player.getPlayerAndPetActions().map(action => action.forTarget(resultData.filter))).flat(); + + this.raidDps = sum(targetActions.map(action => action.dps)); + const maxDpsIndex = maxIndex( + players.map(player => { + const targetActions = player + .getPlayerAndPetActions() + .map(action => action.forTarget(resultData.filter)) + .flat(); + return sum(targetActions.map(action => action.dps)); + }), + )!; + + const maxDpsTargetActions = players[maxDpsIndex] + .getPlayerAndPetActions() + .map(action => action.forTarget(resultData.filter)) + .flat(); + this.maxDps = sum(maxDpsTargetActions.map(action => action.dps)); + + return players.map(player => [player]); + } +} diff --git a/ui/core/components/detailed_results/player_damage_taken.tsx b/ui/core/components/detailed_results/player_damage_taken.tsx new file mode 100644 index 0000000000..845f1cc5f8 --- /dev/null +++ b/ui/core/components/detailed_results/player_damage_taken.tsx @@ -0,0 +1,122 @@ +import tippy from 'tippy.js'; + +import { SimResult, SimResultFilter, UnitMetrics } from '../../proto_utils/sim_result.js'; +import { maxIndex, sum } from '../../utils.js'; +import { ColumnSortType, MetricsTable } from './metrics_table/metrics_table.jsx'; +import { MetricsTotalBar } from './metrics_table/metrics_total_bar'; +import { ResultComponent, ResultComponentConfig, SimResultData } from './result_component.js'; +import { ResultsFilter } from './results_filter.js'; +import { SourceChart } from './source_chart.js'; + +export class PlayerDamageTakenMetricsTable extends MetricsTable { + private readonly resultsFilter: ResultsFilter; + + // Cached values from most recent result. + private resultData: SimResultData | undefined; + private raidDtps: number; + private maxDtps: number; + + constructor(config: ResultComponentConfig, resultsFilter: ResultsFilter) { + config.rootCssClass = 'player-damage-taken-metrics-root'; + super(config, [ + MetricsTable.playerNameCellConfig(), + { + name: 'Amount', + tooltip: 'Player Damage Taken / Raid Damage Taken', + headerCellClass: 'amount-header-cell text-center', + fillCell: (player: UnitMetrics, cellElem: HTMLElement, rowElem: HTMLElement) => { + cellElem.classList.add('amount-cell'); + + let chart: HTMLElement | null = null; + const makeChart = () => { + const chartContainer = document.createElement('div'); + rowElem.appendChild(chartContainer); + if (this.resultData) { + const targets = this.resultData.result.getTargets(this.resultData.filter); + const playerFilter = { + player: player.unitIndex, + }; + const targetActions = targets.map(target => target.getPlayerAndPetActions().map(action => action.forTarget(playerFilter))).flat(); + const sourceChart = new SourceChart(chartContainer, targetActions); + } + return chartContainer; + }; + + tippy(rowElem, { + content: 'Loading...', + placement: 'bottom', + ignoreAttributes: true, + onShow(instance: any) { + if (!chart) { + chart = makeChart(); + instance.setContent(chart); + } + }, + }); + + const playerDtps = this.getPlayerDtps(player); + + cellElem.appendChild( + , + ); + }, + }, + { + name: 'DTPS', + tooltip: 'Damage Taken / Encounter Duration', + columnClass: 'dps-cell', + sort: ColumnSortType.Descending, + getValue: (player: UnitMetrics) => this.getPlayerDtps(player), + getDisplayString: (player: UnitMetrics) => this.getPlayerDtps(player).toFixed(1), + }, + ]); + this.resultsFilter = resultsFilter; + this.raidDtps = 0; + this.maxDtps = 0; + } + + private getPlayerDtps(player: UnitMetrics): number { + const targets = this.resultData!.result.getTargets(this.resultData!.filter); + const targetActions = targets.map(target => target.getPlayerAndPetActions().map(action => action.forTarget({ player: player.unitIndex }))).flat(); + const playerDtps = sum(targetActions.map(action => action.dps)); + return playerDtps; + } + + customizeRowElem(player: UnitMetrics, rowElem: HTMLElement) { + rowElem.classList.add('player-damage-row'); + rowElem.addEventListener('click', event => { + this.resultsFilter.setPlayer(this.getLastSimResult().eventID, player.index); + }); + } + + getGroupedMetrics(resultData: SimResultData): Array> { + this.resultData = resultData; + const players = resultData.result.getPlayers(resultData.filter); + + const targets = resultData.result.getTargets(resultData.filter); + const targetActions = targets.map(target => target.getPlayerAndPetActions().map(action => action.forTarget(resultData.filter))).flat(); + + this.raidDtps = sum(targetActions.map(action => action.dps)); + const maxDpsIndex = maxIndex( + players.map(player => { + const targetActions = targets + .map(target => target.getPlayerAndPetActions().map(action => action.forTarget({ player: player.unitIndex }))) + .flat(); + return sum(targetActions.map(action => action.dps)); + }), + )!; + + const maxDtpsTargetActions = targets + .map(target => target.getPlayerAndPetActions().map(action => action.forTarget({ player: players[maxDpsIndex].unitIndex }))) + .flat(); + this.maxDtps = sum(maxDtpsTargetActions.map(action => action.dps)); + + return players.map(player => [player]); + } +} diff --git a/ui/core/components/detailed_results/resource_metrics.ts b/ui/core/components/detailed_results/resource_metrics.ts index 21401b260e..adaded2d45 100644 --- a/ui/core/components/detailed_results/resource_metrics.ts +++ b/ui/core/components/detailed_results/resource_metrics.ts @@ -1,10 +1,10 @@ -import { ResourceMetrics, SimResult, SimResultFilter } from '../../proto_utils/sim_result.js'; -import { ResourceType } from '../../proto/api.js'; -import { resourceNames } from '../../proto_utils/names.js'; -import { orderedResourceTypes } from '../../proto_utils/utils.js'; - -import { ColumnSortType, MetricsTable } from './metrics_table.js'; -import { ResultComponent, ResultComponentConfig, SimResultData } from './result_component.js'; +import { TOOLTIP_METRIC_LABELS } from '../../constants/tooltips'; +import { ResourceType } from '../../proto/api'; +import { resourceNames } from '../../proto_utils/names'; +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'; export class ResourceMetricsTable extends ResultComponent { constructor(config: ResultComponentConfig) { @@ -30,8 +30,8 @@ export class ResourceMetricsTable extends ResultComponent { }); } - onSimResult(resultData: SimResultData) { - } + // eslint-disable-next-line @typescript-eslint/no-empty-function + onSimResult() {} } export class TypedResourceMetricsTable extends MetricsTable { @@ -44,36 +44,32 @@ export class TypedResourceMetricsTable extends MetricsTable { return { name: metric.name, actionId: metric.actionId, + metricType: metric.constructor?.name, }; }), { name: 'Casts', - tooltip: 'Casts', getValue: (metric: ResourceMetrics) => metric.events, getDisplayString: (metric: ResourceMetrics) => metric.events.toFixed(1), }, { name: 'Gain', - tooltip: 'Gain', sort: ColumnSortType.Descending, getValue: (metric: ResourceMetrics) => metric.gain, getDisplayString: (metric: ResourceMetrics) => metric.gain.toFixed(1), }, { name: 'Gain / s', - tooltip: 'Gain / Second', getValue: (metric: ResourceMetrics) => metric.gainPerSecond, getDisplayString: (metric: ResourceMetrics) => metric.gainPerSecond.toFixed(1), }, { name: 'Avg Gain', - tooltip: 'Gain / Event', getValue: (metric: ResourceMetrics) => metric.avgGain, getDisplayString: (metric: ResourceMetrics) => metric.avgGain.toFixed(1), }, { name: 'Wasted Gain', - tooltip: 'Gain that was wasted because of resource cap.', getValue: (metric: ResourceMetrics) => metric.wastedGain, getDisplayString: (metric: ResourceMetrics) => metric.wastedGain.toFixed(1), }, @@ -82,7 +78,7 @@ export class TypedResourceMetricsTable extends MetricsTable { } getGroupedMetrics(resultData: SimResultData): Array> { - const players = resultData.result.getPlayers(resultData.filter); + const players = resultData.result.getRaidIndexedPlayers(resultData.filter); if (players.length != 1) { return []; } @@ -94,6 +90,9 @@ export class TypedResourceMetricsTable extends MetricsTable { } mergeMetrics(metrics: Array): ResourceMetrics { - return ResourceMetrics.merge(metrics, true, metrics[0].unit?.petActionId || undefined); + return ResourceMetrics.merge(metrics, { + removeTag: true, + actionIdOverride: metrics[0].unit?.petActionId || undefined, + }); } } diff --git a/ui/core/components/detailed_results/result_component.ts b/ui/core/components/detailed_results/result_component.ts index f32fb7a8db..f6f13538f9 100644 --- a/ui/core/components/detailed_results/result_component.ts +++ b/ui/core/components/detailed_results/result_component.ts @@ -1,30 +1,29 @@ -import { SimResult, SimResultFilter } from '../..//proto_utils/sim_result.js'; import { Component } from '../../components/component.js'; +import { SimResult, SimResultFilter } from '../../proto_utils/sim_result.js'; import { EventID, TypedEvent } from '../../typed_event.js'; export interface SimResultData { - eventID: EventID, - result: SimResult, - filter: SimResultFilter, -}; + eventID: EventID; + result: SimResult; + filter: SimResultFilter; +} export interface ResultComponentConfig { - parent: HTMLElement, - rootCssClass?: string, - cssScheme?: String | null, - resultsEmitter: TypedEvent, -}; + parent: HTMLElement; + rootCssClass?: string; + cssScheme?: string | null; + resultsEmitter: TypedEvent; +} export abstract class ResultComponent extends Component { - private lastSimResult: SimResultData | null; + lastSimResult: SimResultData | null; constructor(config: ResultComponentConfig) { super(config.parent, config.rootCssClass || 'result-component'); this.lastSimResult = null; - config.resultsEmitter.on((eventID, resultData) => { - if (!resultData) - return; + config.resultsEmitter.on((_, resultData) => { + if (!resultData) return; this.lastSimResult = resultData; this.onSimResult(resultData); diff --git a/ui/core/components/detailed_results/spell_metrics.ts b/ui/core/components/detailed_results/spell_metrics.ts deleted file mode 100644 index 6a31a5d76a..0000000000 --- a/ui/core/components/detailed_results/spell_metrics.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { ActionMetrics, SimResult, SimResultFilter } from '../../proto_utils/sim_result.js'; -import { bucket } from '../../utils.js'; - -import { ColumnSortType, MetricsTable } from './metrics_table.js'; -import { ResultComponent, ResultComponentConfig, SimResultData } from './result_component.js'; - -export class SpellMetricsTable extends MetricsTable { - constructor(config: ResultComponentConfig) { - config.rootCssClass = 'spell-metrics-root'; - super(config, [ - MetricsTable.nameCellConfig((metric: ActionMetrics) => { - return { - name: metric.name, - actionId: metric.actionId, - }; - }), - { - name: 'DPS', - tooltip: 'Damage / Encounter Duration', - sort: ColumnSortType.Descending, - getValue: (metric: ActionMetrics) => metric.dps, - getDisplayString: (metric: ActionMetrics) => metric.dps.toFixed(1), - }, - { - name: 'Avg Cast', - tooltip: 'Damage / Casts', - getValue: (metric: ActionMetrics) => metric.avgCast, - getDisplayString: (metric: ActionMetrics) => metric.avgCast.toFixed(1), - }, - { - name: 'Avg Hit', - tooltip: 'Damage / Hits', - getValue: (metric: ActionMetrics) => metric.avgHit, - getDisplayString: (metric: ActionMetrics) => metric.avgHit.toFixed(1), - }, - { - name: 'TPS', - tooltip: 'Threat / Encounter Duration', - columnClass: 'threat-metrics', - getValue: (metric: ActionMetrics) => metric.tps, - getDisplayString: (metric: ActionMetrics) => metric.tps.toFixed(1), - }, - { - name: 'Avg Cast', - tooltip: 'Threat / Casts', - columnClass: 'threat-metrics', - getValue: (metric: ActionMetrics) => metric.avgCastThreat, - getDisplayString: (metric: ActionMetrics) => metric.avgCastThreat.toFixed(1), - }, - { - name: 'Avg Hit', - tooltip: 'Threat / Hits', - columnClass: 'threat-metrics', - getValue: (metric: ActionMetrics) => metric.avgHitThreat, - getDisplayString: (metric: ActionMetrics) => metric.avgHitThreat.toFixed(1), - }, - { - name: 'Casts', - tooltip: 'Casts', - getValue: (metric: ActionMetrics) => metric.casts, - getDisplayString: (metric: ActionMetrics) => metric.casts.toFixed(1), - }, - { - name: 'Hits', - tooltip: 'Hits', - getValue: (metric: ActionMetrics) => metric.landedHits, - getDisplayString: (metric: ActionMetrics) => metric.landedHits.toFixed(1), - }, - { - name: 'Miss %', - tooltip: 'Misses / Casts', - getValue: (metric: ActionMetrics) => metric.missPercent, - getDisplayString: (metric: ActionMetrics) => metric.missPercent.toFixed(2) + '%', - }, - { - name: 'Crit %', - tooltip: 'Crits / Hits', - getValue: (metric: ActionMetrics) => metric.critPercent, - getDisplayString: (metric: ActionMetrics) => metric.critPercent.toFixed(2) + '%', - }, - ]); - } - - customizeRowElem(action: ActionMetrics, rowElem: HTMLElement) { - if (action.hitAttempts == 0 && action.dps == 0) { - rowElem.classList.add('threat-metrics'); - } - } - - getGroupedMetrics(resultData: SimResultData): Array> { - const players = resultData.result.getPlayers(resultData.filter); - if (players.length != 1) { - return []; - } - const player = players[0]; - - const actions = player.getSpellActions().map(action => action.forTarget(resultData.filter)); - const actionGroups = ActionMetrics.groupById(actions); - - const petsByName = bucket(player.pets, pet => pet.name); - const petGroups = Object.values(petsByName).map(pets => ActionMetrics.joinById(pets.map(pet => pet.getSpellActions().map(action => action.forTarget(resultData.filter))).flat(), true)); - - return actionGroups.concat(petGroups); - } - - mergeMetrics(metrics: Array): ActionMetrics { - return ActionMetrics.merge(metrics, true, metrics[0].unit?.petActionId || undefined); - } - - shouldCollapse(metric: ActionMetrics): boolean { - return !metric.unit?.isPet; - } -} diff --git a/ui/core/components/detailed_results/topline_results.tsx b/ui/core/components/detailed_results/topline_results.tsx index f06211380c..8d95471712 100644 --- a/ui/core/components/detailed_results/topline_results.tsx +++ b/ui/core/components/detailed_results/topline_results.tsx @@ -1,6 +1,6 @@ import { Spec } from '../../proto/common.js'; -import { RaidSimResultsManager } from '../raid_sim_action.jsx'; -import { ResultComponent, ResultComponentConfig, SimResultData } from './result_component.js'; +import { RaidSimResultsManager } from '../raid_sim_action'; +import { ResultComponent, ResultComponentConfig, SimResultData } from './result_component'; export class ToplineResults extends ResultComponent { constructor(config: ResultComponentConfig) { @@ -11,25 +11,13 @@ export class ToplineResults extends ResultComponent { } onSimResult(resultData: SimResultData) { - const content = RaidSimResultsManager.makeToplineResultsContent(resultData.result, resultData.filter); - const noManaSpecs = [Spec.SpecFeralTankDruid, Spec.SpecRogue, Spec.SpecWarrior, Spec.SpecTankWarrior]; + const players = resultData.result.getRaidIndexedPlayers(resultData.filter); - const players = resultData.result.getPlayers(resultData.filter); - if (players.length == 1 && !noManaSpecs.includes(players[0].spec)) { - const player = players[0]; - const secondsOOM = player.secondsOomAvg; - const percentOOM = secondsOOM / resultData.result.encounterMetrics.durationSeconds; - const dangerLevel = percentOOM < 0.01 ? 'safe' : percentOOM < 0.05 ? 'warning' : 'danger'; - - content.appendChild( -
- {secondsOOM.toFixed(1)}s -
, - ); - } + const content = RaidSimResultsManager.makeToplineResultsContent(resultData.result, resultData.filter, { + showOutOfMana: players.length === 1 && !noManaSpecs.includes(players[0].spec), + }); - this.rootElem.innerHTML = ''; - this.rootElem.appendChild(content); + this.rootElem.replaceChildren(content); } } diff --git a/ui/core/components/raid_sim_action.tsx b/ui/core/components/raid_sim_action.tsx index bbf92b3183..352d8d33ab 100644 --- a/ui/core/components/raid_sim_action.tsx +++ b/ui/core/components/raid_sim_action.tsx @@ -1,12 +1,14 @@ +import clsx from 'clsx'; import tippy from 'tippy.js'; -import { DistributionMetrics as DistributionMetricsProto, ProgressMetrics, Raid as RaidProto } from '../proto/api.js'; -import { Encounter as EncounterProto } from '../proto/common.js'; -import { SimRunData } from '../proto/ui.js'; -import { ActionMetrics, SimResult, SimResultFilter } from '../proto_utils/sim_result.js'; -import { SimUI } from '../sim_ui.js'; -import { EventID, TypedEvent } from '../typed_event.js'; -import { formatDeltaTextElem } from '../utils.js'; +import { TOOLTIP_METRIC_LABELS } from '../constants/tooltips'; +import { DistributionMetrics as DistributionMetricsProto, ProgressMetrics, Raid as RaidProto } from '../proto/api'; +import { Encounter as EncounterProto } from '../proto/common'; +import { SimRunData } from '../proto/ui'; +import { ActionMetrics, SimResult, SimResultFilter } from '../proto_utils/sim_result'; +import { SimUI } from '../sim_ui'; +import { EventID, TypedEvent } from '../typed_event'; +import { formatDeltaTextElem, formatToNumber, formatToPercent, sum } from '../utils'; export function addRaidSimAction(simUI: SimUI): RaidSimResultsManager { simUI.addAction('Simulate', 'dps-action', async () => @@ -39,6 +41,7 @@ export interface ResultMetrics { hps: string; tps: string; tto: string; + oom: string; } export interface ResultMetricCategories { @@ -76,6 +79,7 @@ export class RaidSimResultsManager { hps: 'results-sim-hps', tps: 'results-sim-tps', tto: 'results-sim-tto', + oom: 'results-sim-oom', }; static metricsClasses: { [ResultMetricCategories: string]: string } = { @@ -135,7 +139,7 @@ export class RaidSimResultsManager { this.simUI.resultsViewer.setContent(
- {RaidSimResultsManager.makeToplineResultsContent(simResult)} + {RaidSimResultsManager.makeToplineResultsContent(simResult, undefined, { asList: true })}
Save as Reference @@ -263,21 +267,28 @@ export class RaidSimResultsManager { if (this.simUI.isIndividualSim()) { this.formatToplineResult(`.${RaidSimResultsManager.resultMetricClasses['hps']} .results-reference-diff`, res => res.raidMetrics.hps, 2); this.formatToplineResult(`.${RaidSimResultsManager.resultMetricClasses['dpasp']} .results-reference-diff`, res => res.getPlayers()[0]!.dpasp, 2); - this.formatToplineResult(`.${RaidSimResultsManager.resultMetricClasses['tto']} .results-reference-diff`, res => res.getPlayers()[0]!.tto, 2); - this.formatToplineResult(`.${RaidSimResultsManager.resultMetricClasses['tps']} .results-reference-diff`, res => res.getPlayers()[0]!.tps, 2); + this.formatToplineResult(`.${RaidSimResultsManager.resultMetricClasses['tto']} .results-reference-diff`, res => res.getFirstPlayer()!.tto, 2); + this.formatToplineResult(`.${RaidSimResultsManager.resultMetricClasses['tps']} .results-reference-diff`, res => res.getFirstPlayer()!.tps, 2); this.formatToplineResult( `.${RaidSimResultsManager.resultMetricClasses['dtps']} .results-reference-diff`, - res => res.getPlayers()[0]!.dtps, + res => res.getFirstPlayer()!.dtps, 2, true, ); - this.formatToplineResult(`.${RaidSimResultsManager.resultMetricClasses['tmi']} .results-reference-diff`, res => res.getPlayers()[0]!.tmi, 2, true); + this.formatToplineResult(`.${RaidSimResultsManager.resultMetricClasses['tmi']} .results-reference-diff`, res => res.getFirstPlayer()!.tmi, 2, true); this.formatToplineResult( `.${RaidSimResultsManager.resultMetricClasses['cod']} .results-reference-diff`, - res => res.getPlayers()[0]!.chanceOfDeath, + res => res.getFirstPlayer()!.chanceOfDeath, 1, true, ); + } else { + this.formatToplineResult( + `.${RaidSimResultsManager.resultMetricClasses['dtps']} .results-reference-diff`, + res => sum(res.getPlayers()!.map(player => player.dtps.avg)) / res.getPlayers().length, + 2, + true, + ); } } @@ -369,82 +380,75 @@ export class RaidSimResultsManager { }; } - static makeToplineResultsContent(simResult: SimResult, filter?: SimResultFilter): HTMLElement { + static makeToplineResultsContent(simResult: SimResult, filter?: SimResultFilter, options: ToplineResultOptions = {}) { + const { showOutOfMana = false } = options; + const players = simResult.getRaidIndexedPlayers(filter); - const content = (<>) as HTMLElement; + + const resultColumns: ResultMetric[] = []; if (players.length === 1) { const playerMetrics = players[0]; if (playerMetrics.getTargetIndex(filter) === null) { - const dpsMetrics = playerMetrics.dps; - const dpaspMetrics = playerMetrics.dpasp; - const tpsMetrics = playerMetrics.tps; - const dtpsMetrics = playerMetrics.dtps; - const tmiMetrics = playerMetrics.tmi; - content.appendChild( - this.buildResultsLine({ - average: dpsMetrics.avg, - stdev: dpsMetrics.stdev, - classes: this.getResultsLineClasses('dps'), - }), - ); - - // Hide dpasp if it's zero. - const dpaspContent = this.buildResultsLine({ - average: dpaspMetrics.avg, - stdev: dpaspMetrics.stdev, - classes: this.getResultsLineClasses('dpasp'), + const { chanceOfDeath, dps: dpsMetrics, dpasp: dpaspMetrics, tps: tpsMetrics, dtps: dtpsMetrics, tmi: tmiMetrics } = playerMetrics; + + resultColumns.push({ + name: 'DPS', + average: dpsMetrics.avg, + stdev: dpsMetrics.stdev, + classes: this.getResultsLineClasses('dps'), }); - if (dpaspMetrics.avg === 0) { - dpaspContent.classList.add('hide'); + + if (dpaspMetrics.avg) { + resultColumns.push({ + name: 'DPASP', + average: dpaspMetrics.avg, + stdev: dpaspMetrics.stdev, + classes: this.getResultsLineClasses('dpasp'), + }); } - content.appendChild(dpaspContent); - content.appendChild( - this.buildResultsLine({ - average: tpsMetrics.avg, - stdev: tpsMetrics.stdev, - classes: this.getResultsLineClasses('tps'), - }), - ); - content.appendChild( - this.buildResultsLine({ - average: dtpsMetrics.avg, - stdev: dtpsMetrics.stdev, - classes: this.getResultsLineClasses('dtps'), - }), - ); - - content.appendChild( - this.buildResultsLine({ - average: tmiMetrics.avg, - stdev: tmiMetrics.stdev, - classes: this.getResultsLineClasses('tmi'), - }), - ); - - content.appendChild( - this.buildResultsLine({ - average: playerMetrics.chanceOfDeath, - classes: this.getResultsLineClasses('cod'), - }), - ); + resultColumns.push({ + name: 'TPS', + average: tpsMetrics.avg, + stdev: tpsMetrics.stdev, + classes: this.getResultsLineClasses('tps'), + }); + resultColumns.push({ + name: 'DTPS', + average: dtpsMetrics.avg, + stdev: dtpsMetrics.stdev, + classes: this.getResultsLineClasses('dtps'), + }); + + resultColumns.push({ + name: 'HPS', + average: tmiMetrics.avg, + stdev: tmiMetrics.stdev, + classes: this.getResultsLineClasses('tmi'), + }); + + resultColumns.push({ + name: 'COD', + average: chanceOfDeath, + classes: this.getResultsLineClasses('cod'), + unit: 'percentage', + }); } else { const actions = simResult.getRaidIndexedActionMetrics(filter); if (!!actions.length) { - const mergedActions = ActionMetrics.merge(actions); - content.appendChild( - this.buildResultsLine({ - average: mergedActions.dps, - classes: this.getResultsLineClasses('dps'), - }), - ); - content.appendChild( - this.buildResultsLine({ - average: mergedActions.tps, - classes: this.getResultsLineClasses('tps'), - }), - ); + const { dps, tps } = ActionMetrics.merge(actions); + resultColumns.push({ + name: 'DPS', + average: dps, + classes: this.getResultsLineClasses('dps'), + }); + + resultColumns.push({ + name: 'TPS', + average: tps, + classes: this.getResultsLineClasses('tps'), + }); } const targetActions = simResult @@ -453,24 +457,24 @@ export class RaidSimResultsManager { .flat() .map(action => action.forTarget({ player: playerMetrics.unitIndex })); if (!!targetActions.length) { - const mergedTargetActions = ActionMetrics.merge(targetActions); - content.appendChild( - this.buildResultsLine({ - average: mergedTargetActions.dps, - classes: this.getResultsLineClasses('dtps'), - }), - ); + const { dps: dtps } = ActionMetrics.merge(targetActions); + + resultColumns.push({ + name: 'DTPS', + average: dtps, + classes: this.getResultsLineClasses('dtps'), + }); } } } else { const dpsMetrics = simResult.raidMetrics.dps; - content.appendChild( - this.buildResultsLine({ - average: dpsMetrics.avg, - stdev: dpsMetrics.stdev, - classes: this.getResultsLineClasses('dps'), - }), - ); + + resultColumns.push({ + name: 'DPS', + average: dpsMetrics.avg, + stdev: dpsMetrics.stdev, + classes: this.getResultsLineClasses('dps'), + }); const targetActions = simResult .getTargets(filter) @@ -479,34 +483,47 @@ export class RaidSimResultsManager { .map(action => action.forTarget(filter)); if (!!targetActions.length) { const mergedTargetActions = ActionMetrics.merge(targetActions); - content.appendChild( - this.buildResultsLine({ - average: mergedTargetActions.dps, - classes: this.getResultsLineClasses('dtps'), - }), - ); + resultColumns.push({ + name: 'DTPS', + average: mergedTargetActions.dps, + classes: this.getResultsLineClasses('dtps'), + }); } const hpsMetrics = simResult.raidMetrics.hps; - content.appendChild( - this.buildResultsLine({ - average: hpsMetrics.avg, - stdev: hpsMetrics.stdev, - classes: this.getResultsLineClasses('hps'), - }), - ); + resultColumns.push({ + name: 'HPS', + average: hpsMetrics.avg, + stdev: hpsMetrics.stdev, + classes: this.getResultsLineClasses('hps'), + }); } if (simResult.request.encounter?.useHealth) { - content.appendChild( - this.buildResultsLine({ - average: simResult.result.avgIterationDuration, - classes: this.getResultsLineClasses('dur'), - }), - ); + resultColumns.push({ + name: 'DUR', + average: simResult.result.avgIterationDuration, + classes: this.getResultsLineClasses('dur'), + unit: 'seconds', + }); } - return content; + if (showOutOfMana) { + const player = players[0]; + const secondsOOM = player.secondsOomAvg; + const percentOOM = secondsOOM / simResult.encounterMetrics.durationSeconds; + const dangerLevel = percentOOM < 0.01 ? 'safe' : percentOOM < 0.05 ? 'warning' : 'danger'; + + resultColumns.push({ + name: 'OOM', + average: secondsOOM, + classes: [this.getResultsLineClasses('oom'), dangerLevel].join(' '), + unit: 'seconds', + }); + } + + if (options.asList) return this.buildResultsList(resultColumns); + return this.buildResultsTable(resultColumns); } private static getResultsLineClasses(metric: keyof ResultMetrics): string { @@ -516,20 +533,91 @@ export class RaidSimResultsManager { return classes.join(' '); } - private static buildResultsLine(args: ResultsLineArgs): Element { + private static buildResultsTable(data: ResultMetric[]): Element { return ( -
- {args.average.toFixed(2)} - {args.stdev && ( - - ( - {args.stdev.toFixed()}) - - )} -
- vs reference -
-
+ <> + + + + {data.map(({ name, classes }) => { + const cell = ; + + tippy(cell, { + content: TOOLTIP_METRIC_LABELS[name], + ignoreAttributes: true, + }); + + return cell; + })} + + + + + {data.map(({ average, stdev, classes, unit }) => { + let value = ''; + switch (unit) { + case 'percentage': + value = formatToPercent(average); + break; + case 'seconds': + value = formatToNumber(average, { style: 'unit', unit: 'second', unitDisplay: 'narrow' }); + break; + default: + value = formatToNumber(average); + break; + } + return ( + + ); + })} + + +
{name}
+
{value}
+ {stdev ? ( +
+ {formatToNumber(stdev, { maximumFractionDigits: 0 })} +
+ ) : undefined} +
+ vs reference +
+
+ + ); + } + + private static buildResultsList(data: ResultMetric[]): Element { + return ( + <> + {data.map(column => ( +
+ {column.average.toFixed(2)} + {column.stdev && ( + + ( + {column.stdev.toFixed()}) + + )} +
+ vs reference +
+
+ ))} + ); } } + +type ToplineResultOptions = { + showOutOfMana?: boolean; + asList?: boolean; +}; + +type ResultMetric = { + name: keyof typeof TOOLTIP_METRIC_LABELS; + average: number; + stdev?: number; + classes?: string; + unit?: 'percentage' | 'number' | 'seconds' | undefined; +}; diff --git a/ui/core/constants/tooltips.ts b/ui/core/constants/tooltips.ts index 343caf99e9..66eb8ad671 100644 --- a/ui/core/constants/tooltips.ts +++ b/ui/core/constants/tooltips.ts @@ -22,3 +22,48 @@ export const TOO_MANY_TALENT_POINTS_WARNING = 'More talent points spent than cur export const TITANS_GRIP_WARNING = "Dual wielding two-handed weapon(s) without Titan's Grip spec."; export const GEAR_MIN_LEVEL_WARNING = (playerLevel: number) => `Wearing gear with a minumum level requirement above level ${playerLevel}.`; + +export const TOOLTIP_METRIC_LABELS = { + // Damage metrics + Damage: 'Total Damage done', + DPS: 'Damage / Encounter Duration', + DPASP: 'Dark Pact Average Spellpower', + TPS: 'Threat / Encounter Duration', + DPET: 'Damage / Avg Cast Time', + 'Damage Avg Cast': 'Damage / Casts and/or Damage / Ticks', + 'Avg Hit': 'Damage / (Hits + Crits + Glances + Blocks) and/or Damage / (Ticks + Critical Ticks)', + // Healing metrics + Healing: 'Total Healing done', + 'Healing Avg Cast': 'Healing / Casts', + 'Healing Avg Hit': 'Healing / Hits and/or Healing / (Ticks + Critical Ticks)', + 'Healing Hits': 'Healing / (Hits + Crits + Glances + Blocks) and/or Healing / Ticks + Critical Ticks', + HPM: 'Healing / Mana', + HPET: 'Healing / Avg Cast Time', + HPS: 'Healing / Encounter Duration', + // Damage taken metrics + 'Damage Taken': 'Total Damage taken', + DTPS: 'Damage Taken / Encounter Duration', + COD: 'Chance of Death', + // Cast metrics + Casts: 'Casts', + CPM: 'Casts / (Encounter Duration / 60 Seconds)', + 'Cast Time': 'Average cast time in seconds', + // Hit metrics + Hits: 'Hits + Crits + Glances + Blocks and/or Ticks + Critical Ticks', + 'Crit %': 'Crits / Hits', + 'Hit Miss %': 'Misses / (Hits + Crits + Glances + Blocks)', + 'Cast Miss %': 'Misses / Casts', + // Encounter + DUR: 'Encounter Duration', + OOM: 'Spent Out of Mana', + TTO: 'Time to Out of Mana in seconds', + // Aura metrcis + Procs: 'Procs', + PPM: 'Procs Per Minute', + Uptime: 'Uptime / Encounter Duration', + // Resource Metrics + Gain: 'Gain', + 'Gain / s': 'Gain / Second', + 'Avg Gain': 'Gain / Event', + 'Wasted Gain': 'Gain that was wasted because of resource cap.', +} as const; diff --git a/ui/core/proto_utils/action_id.ts b/ui/core/proto_utils/action_id.ts index 4f682888d7..ab1833516d 100644 --- a/ui/core/proto_utils/action_id.ts +++ b/ui/core/proto_utils/action_id.ts @@ -25,6 +25,7 @@ export class ActionId { readonly baseName: string; // The name without any tag additions. readonly name: string; readonly iconUrl: string; + readonly spellIdTooltipOverride: number | null; private constructor( itemId: number, @@ -135,6 +136,7 @@ export class ActionId { this.baseName = baseName; this.name = name || baseName; this.iconUrl = iconUrl; + this.spellIdTooltipOverride = this.spellTooltipOverride?.spellId || null; if (this.name) this.name += rank ? ` (Rank ${rank})` : ''; } @@ -195,12 +197,15 @@ export class ActionId { if (this.itemId) { elem.href = ActionId.makeItemUrl(this.itemId, this.randomSuffixId); } else if (this.spellId) { - elem.href = ActionId.makeSpellUrl(this.spellId); + elem.href = ActionId.makeSpellUrl(this.spellIdTooltipOverride || this.spellId); } } async setWowheadDataset(elem: HTMLElement, params?: Omit | Omit) { - (this.itemId ? ActionId.makeItemTooltipData(this.itemId, params) : ActionId.makeSpellTooltipData(this.spellId, params)).then(url => { + (this.itemId + ? ActionId.makeItemTooltipData(this.itemId, params) + : ActionId.makeSpellTooltipData(this.spellIdTooltipOverride || this.spellId, params) + ).then(url => { if (elem) elem.dataset.wowhead = url; }); } @@ -342,7 +347,7 @@ export class ActionId { case 'Lesser Healing Wave': case 'Chain Heal': if (this.tag === 6) { - name = `${name} (Overload)`; + name = `${name} OL`; } else if (this.tag) { name = `${name} (${this.tag} MSW)`; } @@ -463,8 +468,7 @@ export class ActionId { break; } - const idString = this.toProtoString(); - let iconOverrideId = idOverrides[idString] || null; + let iconOverrideId = this.spellIconOverride; // Icon Overrides based on tags switch (this.spellId) { @@ -482,7 +486,7 @@ export class ActionId { iconUrl = ActionId.makeIconUrl(overrideTooltipData['icon']); } - return new ActionId(this.itemId, this.spellId, this.otherId, this.tag, baseName, name, iconUrl, this.rank || tooltipData['rank'], this.randomSuffixId); + return new ActionId(this.itemId, this.spellId, this.otherId, this.tag, baseName, name, iconUrl, this.rank || tooltipData.rank, this.randomSuffixId); } toString(): string { @@ -636,13 +640,29 @@ export class ActionId { return await Database.getSpellIconData(actionId.spellId); } } + get spellIconOverride(): ActionId | null { + const override = spellIdIconOverrides.get(JSON.stringify({ spellId: this.spellId })); + if (!override) return null; + return override.itemId ? ActionId.fromItemId(override.itemId) : ActionId.fromItemId(override.spellId!); + } + + get spellTooltipOverride(): ActionId | null { + const override = spellIdTooltipOverrides.get(JSON.stringify({ spellId: this.spellId, tag: this.tag })); + if (!override) return null; + return override.itemId ? ActionId.fromItemId(override.itemId) : ActionId.fromSpellId(override.spellId!); + } } +type ActionIdOverride = { itemId?: number; spellId?: number }; + // Some items/spells have weird icons, so use this to show a different icon instead. -const idOverrides: Record = {}; -idOverrides[ActionId.fromSpellId(449288).toProtoString()] = ActionId.fromItemId(221309); // Darkmoon Card: Sandstorm -idOverrides[ActionId.fromSpellId(455864).toProtoString()] = ActionId.fromSpellId(9907); // Tier 1 Balance Druid "Improved Faerie Fire" -idOverrides[ActionId.fromSpellId(457544).toProtoString()] = ActionId.fromSpellId(10408); // Tier 1 Shaman Tank "Improved Stoneskin / Windwall Totem" +const spellIdIconOverrides: Map = new Map([ + [JSON.stringify({ spellId: 449288 }), { itemId: 221309 }], // Darkmoon Card: Sandstorm + [JSON.stringify({ spellId: 455864 }), { spellId: 9907 }], // Tier 1 Balance Druid "Improved Faerie Fire" + [JSON.stringify({ spellId: 457544 }), { spellId: 10408 }], // Tier 1 Shaman Tank "Improved Stoneskin / Windwall Totem" +]); + +const spellIdTooltipOverrides: Map = new Map([]); const spellIDsToShowBuffs = new Set([ 702, // https://www.wowhead.com/classic/spell=702/curse-of-weakness diff --git a/ui/core/proto_utils/names.ts b/ui/core/proto_utils/names.ts index 304c1d2c5b..a3b0935ddc 100644 --- a/ui/core/proto_utils/names.ts +++ b/ui/core/proto_utils/names.ts @@ -219,6 +219,35 @@ export function getClassStatName(stat: Stat, playerClass: Class): string { } } +// TODO: Make sure BE exports the spell schools properly +export enum SpellSchool { + None = 0, + Physical = 1 << 1, + Arcane = 1 << 2, + Fire = 1 << 3, + Frost = 1 << 4, + Holy = 1 << 5, + Nature = 1 << 6, + Shadow = 1 << 7, +} + +export const spellSchoolNames: Map = new Map([ + [SpellSchool.Physical, 'Physical'], + [SpellSchool.Arcane, 'Arcane'], + [SpellSchool.Fire, 'Fire'], + [SpellSchool.Frost, 'Frost'], + [SpellSchool.Holy, 'Holy'], + [SpellSchool.Nature, 'Nature'], + [SpellSchool.Shadow, 'Shadow'], + [SpellSchool.Nature + SpellSchool.Arcane, 'Astral'], + [SpellSchool.Shadow + SpellSchool.Fire, 'Shadowflame'], + [SpellSchool.Fire + SpellSchool.Arcane, 'Spellfire'], + [SpellSchool.Arcane + SpellSchool.Frost, 'Spellfrost'], + [SpellSchool.Frost + SpellSchool.Fire, 'Frostfire'], + [SpellSchool.Shadow + SpellSchool.Frost, 'Shadowfrost'], + [SpellSchool.Arcane + SpellSchool.Fire + SpellSchool.Frost, 'Chimeric'], +]); + export const itemTypeNames: Map = new Map([ [ItemType.ItemTypeHead, 'Helm'], [ItemType.ItemTypeNeck, 'Neck'], diff --git a/ui/core/proto_utils/sim_result.ts b/ui/core/proto_utils/sim_result.ts index 399cb3562d..bf7c426871 100644 --- a/ui/core/proto_utils/sim_result.ts +++ b/ui/core/proto_utils/sim_result.ts @@ -15,7 +15,7 @@ import { TargetedActionMetrics as TargetedActionMetricsProto, UnitMetrics as UnitMetricsProto, } from '../proto/api.js'; -import { Class, Encounter as EncounterProto, Spec, Target as TargetProto } from '../proto/common.js'; +import { Class, Encounter as EncounterProto, Spec, SpellSchool, Target as TargetProto } from '../proto/common.js'; import { SimRun } from '../proto/ui.js'; import { ActionId, defaultTargetIcon } from '../proto_utils/action_id.js'; import { bucket, sum } from '../utils.js'; @@ -30,7 +30,7 @@ import { SimLog, ThreatLogGroup, } from './logs_parser.js'; -import { cssClassForClass, getTalentTreeIcon, playerToSpec, specToClass } from './utils.js'; +import { cssClassForClass, getTalentTreeIcon, playerToSpec, specToClass } from './utils'; export interface SimResultFilter { // Raid index of the player to display, or null for all players. @@ -217,7 +217,6 @@ export class SimResult { static async makeNew(request: RaidSimRequest, result: RaidSimResult): Promise { const resultData = new SimResultData(request, result); const logs = await SimLog.parseAll(result); - const raidPromise = RaidMetrics.makeNew(resultData, request.raid!, result.raidMetrics!, logs); const encounterPromise = EncounterMetrics.makeNew(resultData, request.encounter!, result.encounterMetrics!, logs); @@ -467,10 +466,22 @@ export class UnitMetrics { return this.getActionsForDisplay().filter(e => e.isMeleeAction); } + getMeleeDamageActions(): Array { + return this.getMeleeActions().filter(e => e.dps !== 0 && e.hps === 0); + } + getSpellActions(): Array { return this.getActionsForDisplay().filter(e => !e.isMeleeAction); } + getSpellDamageActions(): Array { + return this.getSpellActions().filter(e => e.dps !== 0 && e.hps === 0); + } + + getDamageActions(): Array { + return this.getActionsForDisplay().filter(e => e.dps !== 0 && e.hps === 0); + } + getHealingActions(): Array { return this.getActionsForDisplay(); } @@ -605,7 +616,7 @@ export class AuraMetrics { } // Merges an array of metrics into a single metrics. - static merge(auras: Array, removeTag?: boolean, actionIdOverride?: ActionId): AuraMetrics { + static merge(auras: Array, { removeTag, actionIdOverride }: { removeTag?: boolean; actionIdOverride?: ActionId } = {}): AuraMetrics { const firstAura = auras[0]; const unit = auras.every(aura => aura.unit == firstAura.unit) ? firstAura.unit : null; let actionId = actionIdOverride || firstAura.actionId; @@ -637,7 +648,6 @@ export class AuraMetrics { return AuraMetrics.groupById(auras, useTag).map(aurasToJoin => AuraMetrics.merge(aurasToJoin)); } } - export class ResourceMetrics { unit: UnitMetrics | null; readonly actionId: ActionId; @@ -692,7 +702,10 @@ export class ResourceMetrics { } // Merges an array of metrics into a single metrics. - static merge(resources: Array, removeTag?: boolean, actionIdOverride?: ActionId): ResourceMetrics { + static merge( + resources: Array, + { removeTag, actionIdOverride }: { removeTag?: boolean; actionIdOverride?: ActionId } = {}, + ): ResourceMetrics { const firstResource = resources[0]; const unit = resources.every(resource => resource.unit == firstResource.unit) ? firstResource.unit : null; let actionId = actionIdOverride || firstResource.actionId; @@ -733,6 +746,7 @@ export class ActionMetrics { readonly actionId: ActionId; readonly name: string; readonly iconUrl: string; + readonly spellSchool: SpellSchool | null; readonly targets: Array; private readonly resultData: SimResultData; private readonly iterations: number; @@ -750,7 +764,14 @@ export class ActionMetrics { this.iterations = resultData.iterations; this.duration = resultData.duration; this.data = data; - this.targets = data.targets.map(tam => new TargetedActionMetrics(this.iterations, this.duration, tam)); + this.spellSchool = data.spellSchool; + this.targets = data.targets.map( + tam => + new TargetedActionMetrics(tam, { + iterations: this.iterations, + duration: this.duration, + }), + ); this.combinedMetrics = TargetedActionMetrics.merge(this.targets); this.resources = []; } @@ -759,14 +780,143 @@ export class ActionMetrics { return this.data.isMelee; } + get isPassiveAction() { + return this.data.isPassive; + } + + get totalDamagePercent() { + const totalAvgDps = this.resultData.result.raidMetrics?.dps?.avg; + if (!totalAvgDps) return undefined; + + return (this.avgDamage / (totalAvgDps * this.duration)) * 100; + } + get damage() { return this.combinedMetrics.damage; } + get avgDamage() { + return this.combinedMetrics.avgDamage; + } + + get avgHitDamage() { + return ( + this.avgDamage - + this.avgTickDamage - + this.avgCritDamage + + this.avgCritTickDamage - + this.avgGlanceDamage - + this.avgBlockDamage - + this.avgCritBlockDamage + ); + } + + get resistedDamage() { + return this.combinedMetrics.resistedDamage; + } + + get avgResistedDamage() { + return this.combinedMetrics.avgResistedDamage; + } + + get critDamage() { + return this.combinedMetrics.critDamage; + } + + get avgCritDamage() { + return this.combinedMetrics.avgCritDamage; + } + + get resistedCritDamage() { + return this.combinedMetrics.resistedCritDamage; + } + + get avgResistedCritDamage() { + return this.combinedMetrics.avgResistedCritDamage; + } + + get tickDamage() { + return this.combinedMetrics.tickDamage; + } + + get avgTickDamage() { + return this.combinedMetrics.avgTickDamage; + } + + get resistedTickDamage() { + return this.combinedMetrics.resistedTickDamage; + } + + get avgResistedTickDamage() { + return this.combinedMetrics.avgResistedTickDamage; + } + + get critTickDamage() { + return this.combinedMetrics.critTickDamage; + } + + get avgCritTickDamage() { + return this.combinedMetrics.avgCritTickDamage; + } + get resistedCritTickDamage() { + return this.combinedMetrics.resistedCritTickDamage; + } + + get avgResistedCritTickDamage() { + return this.combinedMetrics.avgResistedCritTickDamage; + } + + get glanceDamage() { + return this.combinedMetrics.glanceDamage; + } + + get avgGlanceDamage() { + return this.combinedMetrics.avgGlanceDamage; + } + + get blockDamage() { + return this.combinedMetrics.blockDamage; + } + + get avgBlockDamage() { + return this.combinedMetrics.avgBlockDamage; + } + + get critBlockDamage() { + return this.combinedMetrics.critBlockDamage; + } + + get avgCritBlockDamage() { + return this.combinedMetrics.avgCritBlockDamage; + } + get dps() { return this.combinedMetrics.dps; } + get totalHealingPercent() { + const totalAvgHps = this.resultData.result.raidMetrics?.hps?.avg; + if (!totalAvgHps) return undefined; + + return (this.avgHealing / (totalAvgHps * this.duration)) * 100; + } + + get healing() { + return this.combinedMetrics.healing; + } + + get avgHealing() { + return this.combinedMetrics.healing / this.iterations; + } + + get critHealing() { + return this.combinedMetrics.critHealing; + } + + get avgCritHealing() { + return this.combinedMetrics.critHealing / this.iterations; + } + get hps() { return this.combinedMetrics.hps; } @@ -776,14 +926,17 @@ export class ActionMetrics { } get casts() { + if (this.isPassiveAction) return 0; return this.combinedMetrics.casts; } get castsPerMinute() { + if (this.isPassiveAction) return 0; return this.combinedMetrics.castsPerMinute; } get avgCastTimeMs() { + if (this.isPassiveAction) return 0; return this.combinedMetrics.avgCastTimeMs; } @@ -797,19 +950,40 @@ export class ActionMetrics { return 0; } + get damageThroughput() { + if (this.unit?.isPet && !this.actionId.spellId) return 0; + return this.combinedMetrics.damageThroughput; + } + get healingThroughput() { return this.combinedMetrics.healingThroughput; } + get shielding() { + return this.combinedMetrics.shielding; + } + get avgCast() { + if (this.isPassiveAction) return 0; return this.combinedMetrics.avgCast; } + get avgCastHit() { + if (!this.combinedMetrics.avgCast) return 0; + return this.combinedMetrics.avgCast - this.avgCastTick; + } + + get avgCastTick() { + return this.combinedMetrics.avgCastTick; + } + get avgCastHealing() { + if (this.isPassiveAction) return 0; return this.combinedMetrics.avgCastHealing; } get avgCastThreat() { + if (this.isPassiveAction) return 0; return this.combinedMetrics.avgCastThreat; } @@ -817,6 +991,10 @@ export class ActionMetrics { return this.combinedMetrics.landedHits; } + get landedTicks() { + return this.combinedMetrics.landedTicks; + } + get hitAttempts() { return this.combinedMetrics.hitAttempts; } @@ -825,12 +1003,24 @@ export class ActionMetrics { return this.combinedMetrics.avgHit; } + get avgTick() { + return this.combinedMetrics.avgTick; + } + + get avgHitHealing() { + return this.combinedMetrics.avgHitHealing; + } + get avgHitThreat() { return this.combinedMetrics.avgHitThreat; } - get critPercent() { - return this.combinedMetrics.critPercent; + get totalMisses() { + return this.misses + this.dodges + this.parries; + } + + get totalMissesPercent() { + return this.missPercent + this.dodgePercent + this.parryPercent; } get misses() { @@ -857,6 +1047,50 @@ export class ActionMetrics { return this.combinedMetrics.parryPercent; } + get hits() { + return this.combinedMetrics.hits; + } + + get resistedHits() { + return this.combinedMetrics.resistedHits; + } + + get hitPercent() { + return this.combinedMetrics.hitPercent; + } + + get resistedHitPercent() { + return this.combinedMetrics.resistedHitPercent; + } + + get ticks() { + return this.combinedMetrics.ticks; + } + + get resistedTicks() { + return this.combinedMetrics.resistedTicks; + } + + get resistedTickPercent() { + return this.combinedMetrics.resistedTickPercent; + } + + get critTicks() { + return this.combinedMetrics.critTicks; + } + + get critTickPercent() { + return this.combinedMetrics.critTickPercent; + } + + get resistedCritTicks() { + return this.combinedMetrics.resistedCritTicks; + } + + get resistedCritTickPercent() { + return this.combinedMetrics.resistedCritTickPercent; + } + get blocks() { return this.combinedMetrics.blocks; } @@ -865,6 +1099,14 @@ export class ActionMetrics { return this.combinedMetrics.blockPercent; } + get critBlocks() { + return this.combinedMetrics.critBlocks; + } + + get critBlockPercent() { + return this.combinedMetrics.critBlockPercent; + } + get glances() { return this.combinedMetrics.glances; } @@ -873,6 +1115,102 @@ export class ActionMetrics { return this.combinedMetrics.glancePercent; } + get crits() { + return this.combinedMetrics.crits; + } + + get critPercent() { + return this.combinedMetrics.critPercent; + } + + get resistedCrits() { + return this.combinedMetrics.resistedCrits; + } + + get resistedCritPercent() { + return this.combinedMetrics.resistedCritPercent; + } + + get healingPercent() { + return this.combinedMetrics.healingPercent; + } + + get healingCritPercent() { + return this.combinedMetrics.healingCritPercent; + } + + get damageDone() { + const normalHitAvgDamage = + this.avgDamage - + this.avgTickDamage - + this.avgCritDamage + + this.avgCritTickDamage - + this.avgGlanceDamage - + this.avgBlockDamage - + this.avgCritBlockDamage; + + const normalTickAvgDamage = this.avgTickDamage - this.avgResistedTickDamage - this.avgCritTickDamage - this.avgResistedCritTickDamage; + const critHitAvgDamage = this.avgCritDamage - this.avgResistedCritDamage - this.avgCritTickDamage; + + return { + hit: { + value: normalHitAvgDamage, + percentage: (normalHitAvgDamage / this.avgDamage) * 100, + average: normalHitAvgDamage / this.hits, + }, + resistedHit: { + value: this.avgResistedDamage, + percentage: (this.avgResistedDamage / this.avgDamage) * 100, + average: this.avgResistedDamage / this.hits, + }, + critHit: { + value: critHitAvgDamage, + percentage: (critHitAvgDamage / this.avgDamage) * 100, + average: critHitAvgDamage / this.crits, + }, + resistedCritHit: { + value: this.avgResistedCritDamage, + percentage: (this.avgResistedCritDamage / this.avgDamage) * 100, + average: this.avgResistedCritDamage / this.crits, + }, + tick: { + value: normalTickAvgDamage, + percentage: (normalTickAvgDamage / this.avgDamage) * 100, + average: normalTickAvgDamage / this.ticks, + }, + resistedTick: { + value: this.avgResistedTickDamage, + percentage: (this.avgResistedTickDamage / this.avgDamage) * 100, + average: this.avgResistedTickDamage / this.ticks, + }, + critTick: { + value: this.avgCritTickDamage, + percentage: (this.avgCritTickDamage / this.avgDamage) * 100, + average: this.avgCritTickDamage / this.critTicks, + }, + resistedCritTick: { + value: this.avgResistedCritTickDamage, + percentage: (this.avgResistedCritTickDamage / this.avgDamage) * 100, + average: this.avgResistedCritTickDamage / this.critTicks, + }, + glance: { + value: this.avgGlanceDamage, + percentage: (this.avgGlanceDamage / this.avgDamage) * 100, + average: this.avgGlanceDamage / this.hits, + }, + block: { + value: this.avgBlockDamage, + percentage: (this.avgBlockDamage / this.avgDamage) * 100, + average: this.avgBlockDamage / this.hits, + }, + critBlock: { + value: this.avgCritBlockDamage, + percentage: (this.avgCritBlockDamage / this.avgDamage) * 100, + average: this.avgCritBlockDamage / this.crits, + }, + }; + } + forTarget(filter?: SimResultFilter): ActionMetrics { const unitIndex = this.unit!.getTargetIndex(filter); if (unitIndex == null) { @@ -895,7 +1233,7 @@ export class ActionMetrics { } // Merges an array of metrics into a single metric. - static merge(actions: Array, removeTag?: boolean, actionIdOverride?: ActionId): ActionMetrics { + static merge(actions: Array, { removeTag, actionIdOverride }: { removeTag?: boolean; actionIdOverride?: ActionId } = {}): ActionMetrics { const firstAction = actions[0]; const unit = firstAction.unit; let actionId = actionIdOverride || firstAction.actionId; @@ -905,13 +1243,16 @@ export class ActionMetrics { const maxTargets = Math.max(...actions.map(action => action.targets.length)); const mergedTargets = [...Array(maxTargets).keys()].map(i => TargetedActionMetrics.merge(actions.map(action => action.targets[i]))); + const isAllPassiveSpells = actions.every(action => action.isPassiveAction); return new ActionMetrics( unit, actionId, ActionMetricsProto.create({ isMelee: firstAction.isMeleeAction, + isPassive: isAllPassiveSpells, targets: mergedTargets.map(t => t.data), + spellSchool: firstAction.spellSchool || undefined, }), firstAction.resultData, ); @@ -933,6 +1274,11 @@ export class ActionMetrics { } } +type TargetedActionMetricsOptions = { + iterations: number; + duration: number; +}; + // Manages the metrics for a single action applied to a specific target. export class TargetedActionMetrics { private readonly iterations: number; @@ -940,26 +1286,140 @@ export class TargetedActionMetrics { readonly data: TargetedActionMetricsProto; readonly landedHitsRaw: number; + readonly landedTicksRaw: number; readonly hitAttempts: number; - constructor(iterations: number, duration: number, data: TargetedActionMetricsProto) { + constructor(data: TargetedActionMetricsProto, { iterations, duration }: TargetedActionMetricsOptions) { this.iterations = iterations; this.duration = duration; this.data = data; - this.landedHitsRaw = this.data.hits + this.data.crits + this.data.blocks + this.data.glances; + this.landedHitsRaw = this.data.hits + this.data.crits + this.data.blocks + this.data.critBlocks + this.data.glances; + this.landedTicksRaw = this.data.ticks + this.data.critTicks; - this.hitAttempts = this.data.misses + this.data.dodges + this.data.parries + this.data.blocks + this.data.glances + this.data.crits + this.data.hits; + this.hitAttempts = + this.data.misses + + this.data.dodges + + this.data.parries + + this.data.critBlocks + + this.data.blocks + + this.data.glances + + this.data.crits + + (this.data.hits || this.data.casts); } get damage() { return this.data.damage; } + get avgDamage() { + return this.data.damage / this.iterations; + } + + get resistedDamage() { + return this.data.resistedDamage; + } + + get avgResistedDamage() { + return this.data.resistedDamage / this.iterations; + } + + get critDamage() { + return this.data.critDamage; + } + + get avgCritDamage() { + return this.data.critDamage / this.iterations; + } + + get resistedCritDamage() { + return this.data.resistedCritDamage; + } + + get avgResistedCritDamage() { + return this.data.resistedCritDamage / this.iterations; + } + + get tickDamage() { + return this.data.tickDamage; + } + + get avgTickDamage() { + return this.data.tickDamage / this.iterations; + } + + get resistedTickDamage() { + return this.data.resistedTickDamage; + } + + get avgResistedTickDamage() { + return this.data.resistedTickDamage / this.iterations; + } + + get critTickDamage() { + return this.data.critTickDamage; + } + + get avgCritTickDamage() { + return this.data.critTickDamage / this.iterations; + } + + get resistedCritTickDamage() { + return this.data.resistedCritTickDamage; + } + + get avgResistedCritTickDamage() { + return this.data.resistedCritTickDamage / this.iterations; + } + + get glanceDamage() { + return this.data.glanceDamage; + } + + get avgGlanceDamage() { + return this.data.glanceDamage / this.iterations; + } + + get blockDamage() { + return this.data.blockDamage; + } + + get avgBlockDamage() { + return this.data.blockDamage / this.iterations; + } + + get critBlockDamage() { + return this.data.critBlockDamage; + } + + get avgCritBlockDamage() { + return this.data.critBlockDamage / this.iterations; + } + get dps() { return this.data.damage / this.iterations / this.duration; } + get healing() { + return this.data.healing + this.data.shielding; + } + + get avgHealing() { + return (this.data.healing + this.data.shielding) / this.iterations; + } + + get critHealing() { + return this.data.critHealing; + } + + get avgCritHealing() { + return this.data.critHealing / this.iterations; + } + + get shielding() { + return this.data.shielding; + } + get hps() { return (this.data.healing + this.data.shielding) / this.iterations / this.duration; } @@ -969,7 +1429,7 @@ export class TargetedActionMetrics { } get casts() { - return (this.data.casts || this.hitAttempts) / this.iterations; + return this.data.casts / this.iterations; } get castsPerMinute() { @@ -980,6 +1440,14 @@ export class TargetedActionMetrics { return this.data.castTimeMs / this.iterations / this.casts; } + get damageThroughput() { + if (this.avgCastTimeMs) { + return this.avgCast / (this.avgCastTimeMs / 1000); + } else { + return 0; + } + } + get healingThroughput() { if (this.avgCastTimeMs) { return this.hps / (this.avgCastTimeMs / 1000); @@ -993,9 +1461,14 @@ export class TargetedActionMetrics { } get avgCast() { + if (!this.casts) return 0; return this.data.damage / this.iterations / (this.casts || 1); } + get avgCastTick() { + return this.data.tickDamage / this.iterations / (this.casts || 1); + } + get avgCastHealing() { return (this.data.healing + this.data.shielding) / this.iterations / (this.casts || 1); } @@ -1008,9 +1481,22 @@ export class TargetedActionMetrics { return this.landedHitsRaw / this.iterations; } + get landedTicks() { + return this.landedTicksRaw / this.iterations; + } + get avgHit() { const lhr = this.landedHitsRaw; - return lhr == 0 ? 0 : this.data.damage / lhr; + return lhr == 0 ? 0 : (this.data.damage - this.data.tickDamage) / lhr; + } + + get avgTick() { + const ltr = this.landedTicksRaw; + return ltr == 0 ? 0 : this.data.tickDamage / ltr; + } + + get avgHitHealing() { + return (this.data.healing + this.data.shielding) / this.iterations / this.landedHits; } get avgHitThreat() { @@ -1018,8 +1504,12 @@ export class TargetedActionMetrics { return lhr == 0 ? 0 : this.data.threat / lhr; } - get critPercent() { - return (this.data.crits / (this.hitAttempts || 1)) * 100; + get totalMisses() { + return this.misses + this.dodges + this.parries; + } + + get totalMissesPercent() { + return this.missPercent + this.dodgePercent + this.parryPercent; } get misses() { @@ -1027,7 +1517,7 @@ export class TargetedActionMetrics { } get missPercent() { - return (this.data.misses / (this.data.casts || 1)) * 100; + return (this.data.misses / this.hitAttempts) * 100; } get dodges() { @@ -1035,7 +1525,7 @@ export class TargetedActionMetrics { } get dodgePercent() { - return (this.data.dodges / (this.hitAttempts || 1)) * 100; + return (this.data.dodges / this.hitAttempts) * 100; } get parries() { @@ -1043,7 +1533,51 @@ export class TargetedActionMetrics { } get parryPercent() { - return (this.data.parries / (this.hitAttempts || 1)) * 100; + return (this.data.parries / this.hitAttempts) * 100; + } + + get hits() { + return this.data.hits / this.iterations; + } + + get hitPercent() { + return (this.data.hits / this.hitAttempts) * 100; + } + + get resistedHits() { + return this.data.resistedHits / this.iterations; + } + + get resistedHitPercent() { + return (this.data.resistedHits / this.hitAttempts) * 100; + } + + get ticks() { + return this.data.ticks / this.iterations; + } + + get resistedTicks() { + return this.data.resistedTicks / this.iterations; + } + + get resistedTickPercent() { + return (this.data.resistedTicks / (this.data.ticks + this.data.critTicks)) * 100; + } + + get critTicks() { + return this.data.critTicks / this.iterations; + } + + get critTickPercent() { + return (this.data.critTicks / (this.data.ticks + this.data.critTicks)) * 100; + } + + get resistedCritTicks() { + return this.data.resistedCritTicks / this.iterations; + } + + get resistedCritTickPercent() { + return (this.data.resistedCritTicks / (this.data.ticks + this.data.critTicks)) * 100; } get blocks() { @@ -1051,7 +1585,15 @@ export class TargetedActionMetrics { } get blockPercent() { - return (this.data.blocks / (this.hitAttempts || 1)) * 100; + return (this.data.blocks / this.hitAttempts) * 100; + } + + get critBlocks() { + return this.data.critBlocks / this.iterations; + } + + get critBlockPercent() { + return (this.data.critBlocks / this.hitAttempts) * 100; } get glances() { @@ -1059,29 +1601,75 @@ export class TargetedActionMetrics { } get glancePercent() { - return (this.data.glances / (this.hitAttempts || 1)) * 100; + return (this.data.glances / this.hitAttempts) * 100; + } + + get crits() { + return this.data.crits / this.iterations; + } + + get critPercent() { + return (this.data.crits / this.hitAttempts) * 100; + } + + get resistedCrits() { + return this.data.resistedCrits / this.iterations; + } + + get resistedCritPercent() { + return (this.data.resistedCrits / this.hitAttempts) * 100; + } + + get healingPercent() { + return ((this.healing - this.critHealing) / this.healing) * 100; + } + + get healingCritPercent() { + return (this.data.critHealing / this.healing) * 100; } // Merges an array of metrics into a single metric. static merge(actions: Array): TargetedActionMetrics { + const { iterations = 1, duration = 1 } = actions[0]; + return new TargetedActionMetrics( - actions[0]?.iterations || 1, - actions[0]?.duration || 1, TargetedActionMetricsProto.create({ casts: sum(actions.map(a => a.data.casts)), hits: sum(actions.map(a => a.data.hits)), + resistedHits: sum(actions.map(a => a.data.resistedHits)), crits: sum(actions.map(a => a.data.crits)), + resistedCrits: sum(actions.map(a => a.data.resistedCrits)), + ticks: sum(actions.map(a => a.data.ticks)), + resistedTicks: sum(actions.map(a => a.data.resistedTicks)), + critTicks: sum(actions.map(a => a.data.critTicks)), + resistedCritTicks: sum(actions.map(a => a.data.resistedCritTicks)), misses: sum(actions.map(a => a.data.misses)), dodges: sum(actions.map(a => a.data.dodges)), parries: sum(actions.map(a => a.data.parries)), blocks: sum(actions.map(a => a.data.blocks)), + critBlocks: sum(actions.map(a => a.data.critBlocks)), glances: sum(actions.map(a => a.data.glances)), damage: sum(actions.map(a => a.data.damage)), + resistedDamage: sum(actions.map(a => a.data.resistedDamage)), + critDamage: sum(actions.map(a => a.data.critDamage)), + resistedCritDamage: sum(actions.map(a => a.data.resistedCritDamage)), + tickDamage: sum(actions.map(a => a.data.tickDamage)), + resistedTickDamage: sum(actions.map(a => a.data.resistedTickDamage)), + critTickDamage: sum(actions.map(a => a.data.critTickDamage)), + resistedCritTickDamage: sum(actions.map(a => a.data.resistedCritTickDamage)), + glanceDamage: sum(actions.map(a => a.data.glanceDamage)), + blockDamage: sum(actions.map(a => a.data.blockDamage)), + critBlockDamage: sum(actions.map(a => a.data.critBlockDamage)), threat: sum(actions.map(a => a.data.threat)), healing: sum(actions.map(a => a.data.healing)), + critHealing: sum(actions.map(a => a.data.critHealing)), shielding: sum(actions.map(a => a.data.shielding)), castTimeMs: sum(actions.map(a => a.data.castTimeMs)), }), + { + iterations, + duration, + }, ); } } diff --git a/ui/core/utils.ts b/ui/core/utils.ts index 57d1089778..3d9d93752f 100644 --- a/ui/core/utils.ts +++ b/ui/core/utils.ts @@ -315,6 +315,15 @@ export const mod = (n: number, m: number): number => { return ((n % m) + m) % m; }; +export const formatToCompactNumber: typeof formatToNumber = (number, options) => formatToNumber(number, { notation: 'compact', ...options }); + +export const formatToPercent: typeof formatToNumber = (number, options) => formatToNumber(number / 100, { style: 'percent', ...options }); + +export const formatToNumber = (number: number, options?: Intl.NumberFormatOptions & { fallbackString?: string }) => { + if (!number && options?.fallbackString) return options.fallbackString; + return new Intl.NumberFormat('en-US', { maximumFractionDigits: 2, ...options }).format(number); +}; + type Environments = 'local' | 'external'; const hostname = window.location.hostname; @@ -325,3 +334,6 @@ export const getEnvironment = (): Environments => { export const isLocal = () => getEnvironment() === 'local'; export const isExternal = () => getEnvironment() === 'external'; +export const isDevMode = () => { + return import.meta.env.DEV; +}; diff --git a/ui/index.ts b/ui/index.ts index 36e87eec79..da9ab6e71b 100644 --- a/ui/index.ts +++ b/ui/index.ts @@ -1,3 +1,4 @@ +/// import './shared/bootstrap_overrides'; import * as Popper from '@popperjs/core'; diff --git a/ui/scss/core/components/_character_stats.scss b/ui/scss/core/components/_character_stats.scss index b427bb6b6d..735fc202cd 100644 --- a/ui/scss/core/components/_character_stats.scss +++ b/ui/scss/core/components/_character_stats.scss @@ -4,7 +4,7 @@ width: 100%; .character-stats-label { - margin-bottom: map.get($spacers, 2); + margin-bottom: var(--spacer-2); display: flex; } @@ -17,17 +17,17 @@ .character-stats-table-label, .character-stats-table-value { - padding-top: map.get($spacers, 2); - padding-bottom: map.get($spacers, 2); + padding-top: var(--spacer-2); + padding-bottom: var(--spacer-2); } .character-stats-table-label { - padding-right: map.get($spacers, 2); + padding-right: var(--spacer-2); text-align: left; } .character-stats-table-value { - padding-left: map.get($spacers, 2); + padding-left: var(--spacer-2); font-weight: bold; text-align: right; @@ -57,6 +57,6 @@ justify-content: space-between; span:first-child { - margin-right: map.get($spacers, 2); + margin-right: var(--spacer-2); } } diff --git a/ui/scss/core/components/_content_block.scss b/ui/scss/core/components/_content_block.scss index d78a1339b9..616ea7a387 100644 --- a/ui/scss/core/components/_content_block.scss +++ b/ui/scss/core/components/_content_block.scss @@ -5,7 +5,7 @@ flex-direction: column; &:not(:last-child) { - margin-bottom: 2 * map.get($spacers, 3); + margin-bottom: calc(2 * var(--spacer-3)); } .content-block-header { diff --git a/ui/scss/core/components/_detailed_results.scss b/ui/scss/core/components/_detailed_results.scss index 21294fa35f..29aadf7125 100644 --- a/ui/scss/core/components/_detailed_results.scss +++ b/ui/scss/core/components/_detailed_results.scss @@ -39,10 +39,6 @@ display: none; } -.dr-row { - padding-right: 10px; -} - .dr-root { display: flex; flex-direction: column; @@ -61,7 +57,7 @@ > { .tab-content { - padding-top: 1rem; + padding-top: var(--gap-width); } .tab-content > .tab-pane.dr-tab-content { @@ -93,7 +89,7 @@ height: 1px; margin-left: auto; margin-right: auto; - background-color: $border-color; + background-color: var(--bs-border-color); transition: width 0.15s ease-in-out; } @@ -122,7 +118,7 @@ display: flex; justify-content: center; align-items: center; - padding: calc(map-get($spacers, 3) * 2); + padding: var(--gap-width); font-size: 1rem; } @@ -145,47 +141,109 @@ } .metrics-table { + table-layout: auto; + + td, + th { + width: 0; + } + + th { + white-space: nowrap; + } + td { + @include media-breakpoint-up(1080p) { + white-space: nowrap; + } + } + + .metrics-table-cell--primary-metric { + width: 400px; + } +} + +.damage-content .metrics-table, +.threat-content .metrics-table, +.healing-content .metrics-table, +.tippy-box[data-theme='metrics-table'] { + font-size: 12px; +} + +.tippy-box[data-theme='metrics-table'], +.metrics-table { + --bs-border-default: rgba(var(--bs-white-rgb), 0.2); width: 100%; + + th, + td { + border-left: 1px solid var(--bs-border-default); + border-right: 1px solid var(--bs-border-default); + &:first-child { + border-left: 0; + } + &:last-child { + border-right: 0; + } + } } .metrics-table-header-row { - border-bottom: 2px solid white; + border-bottom: 2px solid rgba(var(--bs-white-rgb), 0.8); +} + +.metrics-table-footer-row { + border-top: 2px solid rgba(var(--bs-white-rgb), 0.8); } .metrics-table-header-cell { - padding: 0.25rem; + padding: var(--spacer-1) var(--spacer-2); cursor: pointer; + text-align: center; } .metrics-table-body tr { - border-bottom: $border-default; + border-bottom: 1px solid var(--bs-border-default); - td:first-child { - width: 50%; + &:nth-child(even) { + background-color: rgba(var(--bs-white-rgb), 0.05); } } .metrics-table-body tr:hover { - background-color: rgba(0, 0, 0, 0.5); + background-color: var(--bs-black-alpha-50); } -.metrics-table-body td { - padding: 0.25rem; +.metrics-table-body, +.metrics-table-footer { + td { + padding: var(--spacer-1) var(--spacer-2); + } } -.metrics-table-header-cell:first-child, -.metrics-table-body td:first-child { +.metrics-table-body td:first-child, +.metrics-table-footer td:first-child { text-align: left; } -.metrics-table-header-cell:not(:first-child), -.metrics-table-body td:not(:first-child) { +.metrics-table-body td:not(:first-child), +.metrics-table-footer td:not(:first-child) { text-align: right; } +.metrics-action { + display: flex; + align-items: center; + gap: var(--spacer-2); + white-space: initial; + + .expand-toggle { + margin-left: auto; + } +} + .metrics-action-icon { @include wowhead-background-icon; - height: 30px; - width: 30px; + height: 24px; + width: 24px; vertical-align: middle; margin-right: 4px; } @@ -197,16 +255,70 @@ padding-left: 20px; } -.expand-toggle { - margin-left: var(--spacer-1); -} - tr:not(.parent-metric) .expand-toggle { display: none; } -.parent-metric.expand .fa-caret-down { +.parent-metric.expand .fa-caret-right, +.parent-metric:not(.expand) .fa-caret-down { display: none; } -.parent-metric:not(.expand) .fa-caret-right { - display: none; + +.metrics-total { + --total-percentage-width: 7ch; + --total-amount-width: 7ch; + --total-bar-min-width: 50px; + min-width: calc(var(--total-percentage-width) + var(--total-amount-width) + var(--total-bar-min-width)); +} + +.metrics-total-percentage, +.metrics-total-damage { + flex-shrink: 0; +} +.metrics-total-percentage { + width: var(--total-percentage-width); +} + +.metrics-total-amount { + width: var(--total-amount-width); +} + +.metrics-total-bar { + position: absolute; + left: var(--total-percentage-width); + right: var(--total-amount-width); + height: 100%; + flex-shrink: 1; + flex-grow: 1; + background-color: color-mix(in srgb, var(--bs-white) 5%, transparent); +} + +.metrics-total-bar-fill { + position: absolute; + top: 0; + left: 0; + width: var(--percentage); + height: 100%; + background-color: currentColor; +} + +.tippy-box[data-theme='metrics-table'] { + @include media-breakpoint-down(sm) { + max-width: 300px; + } + + thead { + th { + padding-top: 0; + text-align: center; + } + } + + tfoot { + tr:last-child { + td { + font-weight: var(--font-weight-medium); + padding-bottom: 0; + } + } + } } diff --git a/ui/scss/core/components/_icon_picker.scss b/ui/scss/core/components/_icon_picker.scss index d1a390cf93..f993a10d9c 100644 --- a/ui/scss/core/components/_icon_picker.scss +++ b/ui/scss/core/components/_icon_picker.scss @@ -63,7 +63,7 @@ } label { - margin-left: map.get($spacers, 2); + margin-left: var(--spacer-2); margin-bottom: 0; } diff --git a/ui/scss/core/components/_input.scss b/ui/scss/core/components/_input.scss index db8fedae34..dec7980d15 100644 --- a/ui/scss/core/components/_input.scss +++ b/ui/scss/core/components/_input.scss @@ -24,7 +24,7 @@ align-items: center; label { - margin-right: map.get($spacers, 2); + margin-right: var(--spacer-2); } input:not(.form-check-input), @@ -41,7 +41,7 @@ flex: 1; &:not(:last-child) { - margin-right: map.get($spacers, 3); + margin-right: var(--spacer-3); } } } diff --git a/ui/scss/core/components/_item_swap_picker.scss b/ui/scss/core/components/_item_swap_picker.scss index a14af32e48..63f537a74e 100644 --- a/ui/scss/core/components/_item_swap_picker.scss +++ b/ui/scss/core/components/_item_swap_picker.scss @@ -16,7 +16,7 @@ margin-right: 0 !important; &:not(:first-child) { - margin-left: map-get($spacers, 1); + margin-left: var(--spacer-1); } } } diff --git a/ui/scss/core/components/_list_picker.scss b/ui/scss/core/components/_list_picker.scss index 8eb74ef5b9..9522f2cef8 100644 --- a/ui/scss/core/components/_list_picker.scss +++ b/ui/scss/core/components/_list_picker.scss @@ -5,12 +5,12 @@ align-items: center; &:not(:last-child) { - margin-bottom: 2 * map.get($spacers, 3); + margin-bottom: calc(2 * var(--spacer-3)); } .list-picker-title { width: 100%; - padding-bottom: map-get($spacers, 2); + padding-bottom: var(--spacer-2); border-bottom: $border-default; margin-bottom: var(--block-spacer); font-size: 1rem; @@ -21,7 +21,7 @@ width: 100%; .list-picker-item-container { - margin-bottom: map.get($spacers, 3); + margin-bottom: var(--spacer-3); &.inline { border: 0; @@ -36,7 +36,7 @@ } &:not(.inline) { - padding: map.get($spacers, 3) map.get($spacers, 3) 0 map.get($spacers, 3); + padding: var(--spacer-3) var(--spacer-3) 0 var(--spacer-3); border: 1px solid $link-color; .list-picker-item-header { @@ -59,7 +59,7 @@ } .list-picker-item-action { - margin-left: map.get($spacers, 2); + margin-left: var(--spacer-2); } } } diff --git a/ui/scss/core/components/_raid_sim_action.scss b/ui/scss/core/components/_raid_sim_action.scss index 96ba2ed2b1..a5c6b11d90 100644 --- a/ui/scss/core/components/_raid_sim_action.scss +++ b/ui/scss/core/components/_raid_sim_action.scss @@ -1,5 +1,3 @@ -@use 'sass:map'; - .results-pending .loader { margin: auto; } @@ -10,34 +8,35 @@ .results-metric { text-align: left; } + .sim-sidebar & { + .results-sim-dps .topline-result-avg:after { + content: ' DPS'; + } + .results-sim-tto .topline-result-avg:after { + content: ' TTO'; + } + .results-sim-hps .topline-result-avg:after { + content: ' HPS'; + } + .results-sim-tps .topline-result-avg:after { + content: ' TPS'; + } + .results-sim-dtps .topline-result-avg:after { + content: ' DTPS'; + } + .results-sim-tmi .topline-result-avg:after { + content: '% TMI'; + } + .results-sim-cod .topline-result-avg:after { + content: '% Chance of Death'; + } + .results-sim-dur .topline-result-avg:after { + content: ' Duration'; + } - .results-sim-dps .topline-result-avg:after { - content: ' DPS'; - } - .results-sim-dpasp .topline-result-avg:after { - content: 'DPASP'; - } - .results-sim-tto .topline-result-avg:after { - content: ' TTO'; - } - .results-sim-hps .topline-result-avg:after { - content: ' HPS'; - } - .results-sim-tps .topline-result-avg:after { - content: ' TPS'; - } - .results-sim-dtps .topline-result-avg:after { - content: ' DTPS'; - } - .results-sim-tmi .topline-result-avg:after { - content: '% TMI'; - } - .results-sim-cod .topline-result-avg:after { - content: '% Chance of Death'; - } - - .results-sim-percent-oom .topline-result-avg:after { - content: ' spent OOM'; + .results-sim-percent-oom .topline-result-avg:after { + content: ' spent OOM'; + } } [class^='results-sim-'], @@ -59,7 +58,7 @@ } .results-reference { - margin-bottom: map.get($spacers, 2); + margin-bottom: var(--spacer-2); font-weight: normal; .results-reference-diff { @@ -69,7 +68,7 @@ } .results-sim-reference { - margin-top: map.get($spacers, 2); + margin-top: var(--spacer-2); font-weight: normal; &.has-reference { diff --git a/ui/scss/core/components/_saved_data_manager.scss b/ui/scss/core/components/_saved_data_manager.scss index c7f9b2f14f..82eb1e2bff 100644 --- a/ui/scss/core/components/_saved_data_manager.scss +++ b/ui/scss/core/components/_saved_data_manager.scss @@ -59,13 +59,13 @@ } .saved-data-set-name { - padding: map.get($spacers, 2); + padding: var(--spacer-2); color: $body-color; } .saved-data-set-delete { - padding: map.get($spacers, 2) 0; - margin-right: map.get($spacers, 2); + padding: var(--spacer-2) 0; + margin-right: var(--spacer-2); color: $body-color; } } diff --git a/ui/scss/core/components/_unit_picker.scss b/ui/scss/core/components/_unit_picker.scss index dfd7f1e95d..9f7ea953bc 100644 --- a/ui/scss/core/components/_unit_picker.scss +++ b/ui/scss/core/components/_unit_picker.scss @@ -6,7 +6,7 @@ .unit-picker-item-icon { width: var(--icon-size-sm); height: var(--icon-size-sm); - margin-right: map-get($spacers, 1); + margin-right: var(--spacer-1); display: flex; justify-content: center; align-items: center; diff --git a/ui/scss/core/components/detailed_results/_log_runner.scss b/ui/scss/core/components/detailed_results/_log_runner.scss index a1bcec89f7..387f801003 100644 --- a/ui/scss/core/components/detailed_results/_log_runner.scss +++ b/ui/scss/core/components/detailed_results/_log_runner.scss @@ -38,11 +38,11 @@ vertical-align: bottom; &:first-child { - width: 6.5rem; + width: 10ch; } &:not(:first-child) { - padding-left: 0; + width: auto; } } .log-runner-logs { @@ -56,6 +56,7 @@ td { padding-top: var(--spacer-2); padding-bottom: var(--spacer-2); + padding-left: var(--spacer-2); } .log-timestamp { diff --git a/ui/scss/core/components/detailed_results/_player_damage.scss b/ui/scss/core/components/detailed_results/_player_damage.scss index f6fdd4f3b0..44e6ef9154 100644 --- a/ui/scss/core/components/detailed_results/_player_damage.scss +++ b/ui/scss/core/components/detailed_results/_player_damage.scss @@ -2,8 +2,13 @@ cursor: pointer; } -.amount-header-cell, .amount-cell { - text-align: center !important; +.name-cell { + width: 250px; +} + +.dps-cell { + max-width: 250px; + min-width: 80px; } .amount-cell { @@ -11,7 +16,7 @@ align-items: center; justify-content: space-between; height: 35px; - width: 600px; + width: auto; margin: auto; } diff --git a/ui/scss/core/components/detailed_results/_topline_results.scss b/ui/scss/core/components/detailed_results/_topline_results.scss index 846b312919..1a05478ff2 100644 --- a/ui/scss/core/components/detailed_results/_topline_results.scss +++ b/ui/scss/core/components/detailed_results/_topline_results.scss @@ -1,7 +1,21 @@ -@import "../raid_sim_action"; +@import '../raid_sim_action'; .topline-results-root { - padding-bottom: 1rem; - display: flex; - justify-content: space-around; + padding-bottom: var(--gap-width); + + .metrics-table { + width: max-content; + max-width: 100%; + width: 100%; + table-layout: fixed; + } + .metrics-table-body { + tr { + border-bottom: 0; + + &:hover { + background-color: transparent; + } + } + } } diff --git a/ui/scss/core/components/individual_sim_ui/_apl_helpers.scss b/ui/scss/core/components/individual_sim_ui/_apl_helpers.scss index 35d3ff2023..e8bc7f603e 100644 --- a/ui/scss/core/components/individual_sim_ui/_apl_helpers.scss +++ b/ui/scss/core/components/individual_sim_ui/_apl_helpers.scss @@ -2,7 +2,7 @@ display: inline-block; width: var(--icon-size-sm); height: var(--icon-size-sm); - margin-right: map-get($spacers, 1); + margin-right: var(--spacer-1); background-size: cover; } diff --git a/ui/scss/core/components/individual_sim_ui/_apl_rotation_picker.scss b/ui/scss/core/components/individual_sim_ui/_apl_rotation_picker.scss index 30cb008dff..e2b56ce450 100644 --- a/ui/scss/core/components/individual_sim_ui/_apl_rotation_picker.scss +++ b/ui/scss/core/components/individual_sim_ui/_apl_rotation_picker.scss @@ -21,8 +21,8 @@ > * > .list-picker-item-container { background-color: rgba(21, 23, 30, 0.8); border: 1px solid var(--bs-primary) !important; - padding: map-get($spacers, 2); - margin-bottom: map-get($spacers, 2); + padding: var(--spacer-2); + margin-bottom: var(--spacer-2); .list-picker-item-header { align-items: flex-start; @@ -34,7 +34,7 @@ } .form-label { - margin-right: map-get($spacers, 2); + margin-right: var(--spacer-2); margin-bottom: 0; } } @@ -71,7 +71,7 @@ margin: 0; & > :not(:last-child) { - margin-right: map-get($spacers, 2); + margin-right: var(--spacer-2); } .form-label { @@ -103,7 +103,7 @@ margin: 0; .apl-action-condition { - margin-bottom: map-get($spacers, 2) !important; + margin-bottom: var(--spacer-2) !important; } .input-root { @@ -112,7 +112,7 @@ border: none; &:not(:last-child) { - margin-right: map-get($spacers, 2); + margin-right: var(--spacer-2); } } @@ -131,7 +131,7 @@ flex-wrap: wrap; .apl-picker-builder-multi { - padding-left: map-get($spacers, 2); + padding-left: var(--spacer-2); border-left: 1px solid $border-color; } @@ -156,8 +156,8 @@ .list-picker-item-header { order: 1; - margin-left: map-get($spacers, 2) !important; - line-height: map-get($spacers, 4); + margin-left: var(--spacer-2) !important; + line-height: var(--spacer-4); } } } diff --git a/ui/scss/core/components/individual_sim_ui/_cooldowns_picker.scss b/ui/scss/core/components/individual_sim_ui/_cooldowns_picker.scss index c82bbd5c1d..43607f2e66 100644 --- a/ui/scss/core/components/individual_sim_ui/_cooldowns_picker.scss +++ b/ui/scss/core/components/individual_sim_ui/_cooldowns_picker.scss @@ -18,7 +18,7 @@ } & > :not(:last-child) { - margin-right: map.get($spacers, 2); + margin-right: var(--spacer-2); } .cooldown-picker-label { diff --git a/ui/scss/core/components/individual_sim_ui/_rotation_tab.scss b/ui/scss/core/components/individual_sim_ui/_rotation_tab.scss index 1210987874..c0141b5bbc 100644 --- a/ui/scss/core/components/individual_sim_ui/_rotation_tab.scss +++ b/ui/scss/core/components/individual_sim_ui/_rotation_tab.scss @@ -1,6 +1,6 @@ -@use "sass:map"; +@use 'sass:map'; -@import "./apl_rotation_picker"; +@import './apl_rotation_picker'; #rotation-tab { &.rotation-type-auto { @@ -49,7 +49,7 @@ .rotation-tab-apl { display: none; } - + .enum-picker-selector, .number-picker-input { width: 8rem; @@ -74,7 +74,7 @@ .rotation-settings { flex: 1; - margin-right: 2 * map.get($spacers, 3); + margin-right: calc(2 * var(--spacer-3)); } .cooldown-settings { @@ -87,5 +87,5 @@ .rotation-settings { margin-right: 0; } - } + } } diff --git a/ui/scss/core/sim_ui/_shared.scss b/ui/scss/core/sim_ui/_shared.scss index b791fba114..1aab6e0ab1 100644 --- a/ui/scss/core/sim_ui/_shared.scss +++ b/ui/scss/core/sim_ui/_shared.scss @@ -68,6 +68,9 @@ th { } .sim-ui--is-unlaunched { + .import-export { + display: none !important; + } .sim-sidebar { .sim-sidebar-actions > *:not(.sim-ui-unlaunched-container), .sim-sidebar-results { @@ -77,6 +80,8 @@ th { } .sim-ui-unlaunched-container { + max-width: 400px; + i { color: var(--bs-danger); } @@ -109,27 +114,42 @@ th { } // TODO: Move these to an organized partial -.hide-damage-metrics .damage-metrics { - display: none !important; +.hide-damage-metrics { + .damage-metrics-tab, + .damage-metrics { + display: none !important; + } } -.hide-threat-metrics .threat-metrics { - display: none !important; +.hide-threat-metrics { + .threat-metrics-tab, + .threat-metrics { + display: none !important; + } } -.hide-healing-metrics .healing-metrics { - display: none !important; +.hide-healing-metrics { + .healing-metrics-tab, + .healing-metrics { + display: none !important; + } } -.hide-experimental .experimental { - display: none !important; +.hide-experimental { + .experimental { + display: none !important; + } } -.hide-in-front-of-target .in-front-of-target { - display: none !important; +.hide-in-front-of-target { + .in-front-of-target { + display: none !important; + } } -.hide-ep-ratios .ep-ratios { - display: none !important; +.hide-ep-ratios { + .ep-ratios { + display: none !important; + } } // END TODO diff --git a/ui/scss/homepage/_homepage.scss b/ui/scss/homepage/_homepage.scss index ef78a522e8..fbc375df05 100644 --- a/ui/scss/homepage/_homepage.scss +++ b/ui/scss/homepage/_homepage.scss @@ -35,7 +35,7 @@ .wowsims-logo { width: 6rem; - margin-right: map.get($spacers, 3); + margin-right: var(--spacer-3); } .wowsims-title { @@ -48,8 +48,8 @@ } .homepage-header-collapse { - padding-top: map.get($spacers, 3); - padding-bottom: map.get($spacers, 3); + padding-top: var(--spacer-3); + padding-bottom: var(--spacer-3); align-items: flex-end; justify-content: flex-end; } @@ -125,8 +125,8 @@ } .sim-links-container { - margin-left: map.get($spacers, 3) * -1; - margin-right: map.get($spacers, 3) * -1; + margin-left: calc(var(--spacer-3) * -1); + margin-right: calc(var(--spacer-3) * -1); .sim-links { margin-bottom: 0 !important; @@ -156,8 +156,8 @@ .homepage-header-container, .homepage-content-container, .homepage-footer-container { - padding-top: map.get($spacers, 3); - padding-bottom: map.get($spacers, 3); + padding-top: var(--spacer-3); + padding-bottom: var(--spacer-3); } .homepage-header { @@ -178,7 +178,7 @@ .homepage-content-container { .info-container { - margin-bottom: map.get($spacers, 3); + margin-bottom: var(--spacer-3); } .sim-links-container { diff --git a/ui/scss/shared/_global.scss b/ui/scss/shared/_global.scss index 71fa04839e..9f51283c2e 100644 --- a/ui/scss/shared/_global.scss +++ b/ui/scss/shared/_global.scss @@ -7,13 +7,13 @@ } // We want to apply only to 1440p monitors, NOT 1080p Ultrawide - @media (min-width: map-get($grid-breakpoints, 1440p)) and (max-aspect-ratio: 16/9) { - --bs-body-font-size: 24px; - } + // @media (min-width: map-get($grid-breakpoints, 1440p)) and (max-aspect-ratio: 16/9) { + // --bs-body-font-size: 20px; + // } - @include media-breakpoint-up(4k) { - --bs-body-font-size: 32px; - } + // @include media-breakpoint-up(4k) { + // --bs-body-font-size: 24px; + // } @include media-breakpoint-up(lg) { --section-spacer: var(--section-spacer-lg) !important; @@ -116,6 +116,12 @@ kbd { color: var(--bs-white); } +.list-reset { + list-style: none; + padding: 0; + margin: 0; +} + .dragto:not(.dragfrom) { filter: brightness(0.75); } @@ -145,12 +151,36 @@ kbd { background-size: cover; } +.p-gap { + @extend .px-gap; + @extend .py-gap; +} +.pl-gap { + padding-left: var(--gap-width); +} +.pr-gap { + padding-right: var(--gap-width); +} +.pt-gap { + padding-top: var(--gap-width); +} +.pb-gap { + padding-bottom: var(--gap-width); +} +.px-gap { + @extend .pl-gap; + @extend .pr-gap; +} +.py-gap { + @extend .pt-gap; + @extend .pb-gap; +} + @each $label, $value in $item-quality-colors { .item-quality-#{$label} { color: var(--bs-#{$label}) !important; } } - @each $label, $value in $resource-colors { .resource-#{$label} { color: var(--bs-#{$label}) !important; @@ -166,6 +196,27 @@ kbd { color: var(--bs-#{$label}) !important; } } +@each $label, $value in $spell-school-colors { + .spell-school-#{$label} { + color: var(--bs-#{$label}) !important; + } + .bg-spell-school-#{$label} { + background: var(--bs-#{$label}) !important; + } +} + +@each $label, $value in $multi-spell-school-colors { + .spell-school-#{$label} { + color: var(--bs-primary); + background-image: $value; + background-clip: text; + -webkit-text-fill-color: transparent; + } + + .bg-spell-school-#{$label} { + background: $value; + } +} [contenteditable='true']:active, [contenteditable='true']:focus { @@ -181,3 +232,9 @@ kbd { width: 100%; background: color-mix(in srgb, var(--bs-body-bg), var(--bs-white) 5%); } + +.blurred { + filter: blur(2px); + overflow: hidden; + pointer-events: none; +} diff --git a/ui/scss/shared/_global_old.scss b/ui/scss/shared/_global_old.scss index 76e99c07ae..8a0a666850 100644 --- a/ui/scss/shared/_global_old.scss +++ b/ui/scss/shared/_global_old.scss @@ -84,9 +84,9 @@ $sim-transitions: 0.2s ease-in; .warning { color: yellow; text-shadow: - 0 0 map-get($spacers, 2) $danger, - 0 0 map-get($spacers, 2) $danger, - 0 0 map-get($spacers, 2) $danger; + 0 0 var(--spacer-2) $danger, + 0 0 var(--spacer-2) $danger, + 0 0 var(--spacer-2) $danger; } .danger { diff --git a/ui/scss/shared/_variables.scss b/ui/scss/shared/_variables.scss index 2220706562..0e7640818c 100644 --- a/ui/scss/shared/_variables.scss +++ b/ui/scss/shared/_variables.scss @@ -39,6 +39,29 @@ $class-colors: ( ); $theme-colors: map-merge($theme-colors, $class-colors); +$spell-school-colors: ( + physical: #e5cc80, + arcane: #8ff2ff, + fire: #eb4561, + frost: #4a80ff, + holy: #ffff8f, + nature: #d1fa99, + shadow: #b8a8f0, +); + +$multi-spell-school-colors: ( + astral: linear-gradient(90deg, map-get($spell-school-colors, nature), map-get($spell-school-colors, arcane)), + shadowflame: linear-gradient(90deg, map-get($spell-school-colors, shadow), map-get($spell-school-colors, fire)), + spellfire: linear-gradient(90deg, map-get($spell-school-colors, fire), map-get($spell-school-colors, arcane)), + spellfrost: linear-gradient(90deg, map-get($spell-school-colors, arcane), map-get($spell-school-colors, frost)), + frostfire: linear-gradient(90deg, map-get($spell-school-colors, frost), map-get($spell-school-colors, fire)), + shadowfrost: linear-gradient(90deg, map-get($spell-school-colors, shadow), map-get($spell-school-colors, frost)), + chimeric: linear-gradient(90deg, map-get($spell-school-colors, arcane), map-get($spell-school-colors, fire), map-get($spell-school-colors, frost)), +); + +$theme-colors: map-merge($theme-colors, $spell-school-colors); + + $custom-colors: ( expansion: $expansion, brand: $brand, diff --git a/ui/scss/sims/raid/_player.scss b/ui/scss/sims/raid/_player.scss index 4aff4d8d23..609f59744b 100644 --- a/ui/scss/sims/raid/_player.scss +++ b/ui/scss/sims/raid/_player.scss @@ -3,7 +3,7 @@ .player { width: 100%; height: 2.5rem; - padding: calc(map-get($spacers, 1) - 1px) map-get($spacers, 1); + padding: calc(var(--spacer-1) - 1px) var(--spacer-1); border: $border-default; display: flex; justify-content: space-between; @@ -15,7 +15,7 @@ } .player-label { - margin-right: map-get($spacers, 2); + margin-right: var(--spacer-2); display: flex; align-items: center; flex-grow: 1; diff --git a/ui/scss/sims/raid/_raid_sim_ui.scss b/ui/scss/sims/raid/_raid_sim_ui.scss index 41dd7022f9..0efa5e52c6 100644 --- a/ui/scss/sims/raid/_raid_sim_ui.scss +++ b/ui/scss/sims/raid/_raid_sim_ui.scss @@ -1,37 +1,37 @@ -@use "sass:map"; +@use 'sass:map'; -@import "./assignments_picker"; -@import "./blessings_picker"; +@import './assignments_picker'; +@import './blessings_picker'; @import './player'; @import './player_editor'; -@import "./raid_picker"; -@import "./raid_stats"; -@import "./tanks_picker"; +@import './raid_picker'; +@import './raid_stats'; +@import './tanks_picker'; -@import "./raid_tab"; -@import "./settings_tab"; +@import './raid_tab'; +@import './settings_tab'; .raid-settings-tab { overflow-y: auto; } .raid-settings-sections { - position: relative; + position: relative; - .settings-section-container { - position: absolute; - margin-bottom: 2 * map.get($spacers, 3); - margin-right: 2 * map.get($spacers, 3); + .settings-section-container { + position: absolute; + margin-bottom: calc(2 * var(--spacer-3)); + margin-right: calc(2 * var(--spacer-3)); - .raid-encounter-section { - .encounter-picker-root { - flex-direction: column; + .raid-encounter-section { + .encounter-picker-root { + flex-direction: column; - .picker-group { - flex-direction: column; - } - } - } + .picker-group { + flex-direction: column; + } + } + } .consumes-section { display: grid; @@ -41,5 +41,5 @@ margin: 3px; } } - } + } } diff --git a/ui/scss/sims/raid/_raid_stats.scss b/ui/scss/sims/raid/_raid_stats.scss index 128a05029c..b80fd0ed6a 100644 --- a/ui/scss/sims/raid/_raid_stats.scss +++ b/ui/scss/sims/raid/_raid_stats.scss @@ -9,7 +9,7 @@ } .raid-stats-section-content { - padding: map.get($spacers, 2); + padding: var(--spacer-2); border: $border-default; display: grid; grid-template-columns: repeat(4, 1fr);