Skip to content

Commit

Permalink
Implement TMI metric for tank specs
Browse files Browse the repository at this point in the history
  • Loading branch information
psiven committed Dec 19, 2022
1 parent c3322c4 commit 911eb80
Show file tree
Hide file tree
Showing 14 changed files with 167 additions and 11 deletions.
1 change: 1 addition & 0 deletions proto/api.proto
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ message UnitMetrics {
DistributionMetrics dpasp = 16;
DistributionMetrics threat = 8;
DistributionMetrics dtps = 11;
DistributionMetrics tmi = 17;
DistributionMetrics hps = 14;
DistributionMetrics tto = 15; // Time To OOM, in seconds.

Expand Down
3 changes: 3 additions & 0 deletions proto/common.proto
Original file line number Diff line number Diff line change
Expand Up @@ -792,7 +792,10 @@ message HealingModel {
double hps = 1;
// How often healing is applied.
double cadence_seconds = 2;
// % Inspiration buff uptime
double inspiration_uptime = 3;
// TMI burst window bin size
int32 burst_window = 4;
}

message CustomRotation {
Expand Down
1 change: 1 addition & 0 deletions proto/test.proto
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ message DpsTestResult {
double tps = 2;
double dtps = 3;
double hps = 4;
double tmi = 5;
}

message CastsTestResult {
Expand Down
27 changes: 19 additions & 8 deletions sim/core/health.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package core

import (
"time"

"github.com/wowsims/wotlk/sim/core/proto"
"github.com/wowsims/wotlk/sim/core/stats"
)
Expand All @@ -18,7 +18,6 @@ type healthBar struct {
func (unit *Unit) EnableHealthBar() {
unit.healthBar = healthBar{
unit: unit,

DamageTakenHealthMetrics: unit.NewHealthMetrics(ActionID{OtherID: proto.OtherAction_OtherActionDamageTaken}),
}
}
Expand Down Expand Up @@ -71,6 +70,15 @@ func (hb *healthBar) RemoveHealth(sim *Simulation, amount float64) {
newHealth := MaxFloat(oldHealth-amount, 0)
metrics := hb.DamageTakenHealthMetrics
metrics.AddEvent(-amount, newHealth-oldHealth)

// TMI calculations need timestamps and Max HP information for each damage taken event
if hb.unit.Metrics.isTanking {
entry := tmiListItem{
Timestamp: sim.CurrentTime,
WeightedDamage: amount / hb.MaxHealth(),
}
hb.unit.Metrics.tmiList = append(hb.unit.Metrics.tmiList, entry)
}

if sim.Log != nil {
hb.unit.Log(sim, "Spent %0.3f health from %s (%0.3f --> %0.3f).", amount, metrics.ActionID, oldHealth, newHealth)
Expand All @@ -82,20 +90,23 @@ func (hb *healthBar) RemoveHealth(sim *Simulation, amount float64) {
var ChanceOfDeathAuraLabel = "Chance of Death"

func (character *Character) trackChanceOfDeath(healingModel *proto.HealingModel) {
if healingModel == nil {
return
}

isTanking := false
character.Unit.Metrics.isTanking = false
for _, target := range character.Env.Encounter.Targets {
if target.CurrentTarget == &character.Unit {
isTanking = true
character.Unit.Metrics.isTanking = true
}
}
if !isTanking {
if !character.Unit.Metrics.isTanking {
return
}

if healingModel == nil {
return
}

character.Unit.Metrics.tmiBin = healingModel.BurstWindow

character.RegisterAura(Aura{
Label: ChanceOfDeathAuraLabel,
Duration: NeverExpires,
Expand Down
97 changes: 96 additions & 1 deletion sim/core/metrics_aggregator.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package core
import (
"math"
"time"

"github.com/wowsims/wotlk/sim/core/proto"
)

Expand Down Expand Up @@ -85,8 +85,13 @@ type UnitMetrics struct {
dpasp DistributionMetrics
threat DistributionMetrics
dtps DistributionMetrics
tmi DistributionMetrics
hps DistributionMetrics
tto DistributionMetrics

tmiList []tmiListItem
isTanking bool
tmiBin int32

CharacterIterationMetrics

Expand Down Expand Up @@ -119,6 +124,11 @@ type ActionMetrics struct {
Targets []TargetedActionMetrics
}

type tmiListItem struct {
Timestamp time.Duration
WeightedDamage float64
}

func (actionMetrics *ActionMetrics) ToProto(actionID ActionID) *proto.ActionMetrics {
var targetMetrics []*proto.TargetedActionMetrics
for _, tam := range actionMetrics.Targets {
Expand Down Expand Up @@ -198,6 +208,7 @@ func NewUnitMetrics() UnitMetrics {
dpasp: NewDistributionMetrics(),
threat: NewDistributionMetrics(),
dtps: NewDistributionMetrics(),
tmi: NewDistributionMetrics(),
hps: NewDistributionMetrics(),
tto: NewDistributionMetrics(),
actions: make(map[ActionID]*ActionMetrics),
Expand Down Expand Up @@ -361,6 +372,8 @@ func (unitMetrics *UnitMetrics) reset() {
unitMetrics.dpasp.reset()
unitMetrics.threat.reset()
unitMetrics.dtps.reset()
unitMetrics.tmi.reset()
unitMetrics.tmiList = nil
unitMetrics.hps.reset()
unitMetrics.tto.reset()
unitMetrics.CharacterIterationMetrics = CharacterIterationMetrics{}
Expand Down Expand Up @@ -393,10 +406,18 @@ func (unitMetrics *UnitMetrics) doneIteration(unit *Unit, sim *Simulation) {
unitMetrics.tto.Total *= encounterDurationSeconds
}

if unitMetrics.isTanking {
unitMetrics.tmi.Total = unitMetrics.calculateTMI(unit, sim);

// Hack because of the way DistributionMetrics does its calculations.
unitMetrics.tmi.Total *= sim.Duration.Seconds()
}

unitMetrics.dps.doneIteration(sim)
unitMetrics.dpasp.doneIteration(sim)
unitMetrics.threat.doneIteration(sim)
unitMetrics.dtps.doneIteration(sim)
unitMetrics.tmi.doneIteration(sim)
unitMetrics.hps.doneIteration(sim)
unitMetrics.tto.doneIteration(sim)

Expand All @@ -406,13 +427,87 @@ func (unitMetrics *UnitMetrics) doneIteration(unit *Unit, sim *Simulation) {
}
}

func (unitMetrics *UnitMetrics) calculateTMI(unit *Unit, sim *Simulation) float64 {

if unit.Metrics.tmiList == nil || unitMetrics.tmiBin == 0 {
return 0
}

bin := int(unitMetrics.tmiBin) // Seconds width for bin, default = 6
firstEvent := 0 // Marks event at start of current bin
ev := 0 // Marks event at end of current bin
lastEvent := len(unit.Metrics.tmiList)
var buckets []float64 = nil

// Traverse event array via marching time bins
for tStep:=0; float64(tStep) < float64(sim.Duration.Seconds()) - float64(bin); tStep++ {

// Increment event counter until we exceed the bin start
for ; firstEvent < lastEvent && unit.Metrics.tmiList[firstEvent].Timestamp.Seconds() < float64(tStep) ; firstEvent++ {
}

// Increment event counter until we exceed the bin end
for ; ev < lastEvent && unit.Metrics.tmiList[ev].Timestamp.Seconds() < float64(tStep+bin) ; ev++ {
}

if ev - firstEvent > 0 {
sum := float64(0);

// Add up everything in the bin
for j:=firstEvent; j < ev; j++ {
sum += unit.Metrics.tmiList[j].WeightedDamage
}

//if sim.Log != nil {
// unit.Log(sim, "Bucket from %ds to %ds with events %d to %d totaled %f", tStep, tStep+bin, firstEvent, ev-1, sum)
//}
buckets = append(buckets, sum)
} else { // an entire window with zero damage midfight still needs to be included
if firstEvent < lastEvent {
buckets = append(buckets, 0)
}
}

}

if buckets == nil {
return 0
}

sum := float64(0);

for i:=0; i < len(buckets); i++ {
sum += math.Pow(math.E, buckets[i] * float64(10))
}

/* DEBUG LOGS
if sim.Log != nil {
raw_avg := float64(0)
for i:=0; i < len(buckets); i++ {
raw_avg += buckets[i]
}
raw_avg = raw_avg / float64(len(buckets))
unit.Log(sim, "Sum of %d buckets was %f and raw mean bucket was %f", len(buckets), sum, raw_avg)
unit.Log(sim, "TMI should be reported as %f", float64(10000) * math.Log(float64(1)/float64(len(buckets)) * sum))
}
*/

return float64(10) * math.Log(float64(1)/float64(len(buckets)) * sum)

// 100000 / factor * ln ( Sum( p(window) * e ^ (factor * dmg(window) / hp ) ) )
// factor = 10, multiplier of 100000 equivalent to 100% HP
// Rescale to 100 = 100%

}

func (unitMetrics *UnitMetrics) ToProto() *proto.UnitMetrics {
n := float64(unitMetrics.dps.n)
protoMetrics := &proto.UnitMetrics{
Dps: unitMetrics.dps.ToProto(),
Dpasp: unitMetrics.dpasp.ToProto(),
Threat: unitMetrics.threat.ToProto(),
Dtps: unitMetrics.dtps.ToProto(),
Tmi: unitMetrics.tmi.ToProto(),
Hps: unitMetrics.hps.ToProto(),
Tto: unitMetrics.tto.ToProto(),
SecondsOomAvg: unitMetrics.oomTimeSum / n,
Expand Down
18 changes: 18 additions & 0 deletions ui/core/components/other_inputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,24 @@ export const HealingCadence = {
enableWhen: (player: Player<any>) => player.getRaid()!.getTanks().find(tank => RaidTarget.equals(tank, player.makeRaidTarget())) != null,
};

export const BurstWindow = {
type: 'number' as const,
float: false,
label: 'TMI Burst Window',
labelTooltip: `
<p>Size in whole seconds of the burst window for calculating TMI. It is important to use a consistent setting when comparing this metric.</p>
<p>Default is 6 seconds. If set to 0, TMI calculations are disabled.</p>
`,
changedEvent: (player: Player<any>) => player.getRaid()!.changeEmitter,
getValue: (player: Player<any>) => player.getHealingModel().burstWindow,
setValue: (eventID: EventID, player: Player<any>, newValue: number) => {
const healingModel = player.getHealingModel();
healingModel.burstWindow = newValue;
player.setHealingModel(eventID, healingModel);
},
enableWhen: (player: Player<any>) => player.getRaid()!.getTanks().find(tank => RaidTarget.equals(tank, player.makeRaidTarget())) != null,
};

export const HpPercentForDefensives = {
type: 'number' as const,
float: true,
Expand Down
17 changes: 17 additions & 0 deletions ui/core/components/raid_sim_action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export interface ResultMetrics {
dps: string,
dpasp: string,
dtps: string,
tmi: string,
dur: string,
hps: string,
tps: string,
Expand All @@ -60,6 +61,7 @@ export class RaidSimResultsManager {
dpasp: 'demo',
tps: 'threat',
dtps: 'threat',
tmi: 'threat',
cod: 'threat',
tto: 'healing',
hps: 'healing',
Expand All @@ -70,6 +72,7 @@ export class RaidSimResultsManager {
dps: 'results-sim-dps',
dpasp: 'results-sim-dpasp',
dtps: 'results-sim-dtps',
tmi: 'results-sim-tmi',
dur: 'results-sim-dur',
hps: 'results-sim-hps',
tps: 'results-sim-tps',
Expand Down Expand Up @@ -188,6 +191,11 @@ export class RaidSimResultsManager {
setResultTooltip('results-sim-hps', 'Healing+Shielding Per Second, including overhealing.');
setResultTooltip('results-sim-tps', 'Threat Per Second');
setResultTooltip('results-sim-dtps', 'Damage Taken Per Second');
setResultTooltip('results-sim-tmi', `
<p>Theck-Meloree Index (TMI)</p>
<p>A measure of incoming damage smoothness which combines the benefits of avoidance with effective health.</p>
<p><b>Lower is better.</b> This represents the % of your HP to expect in a 6-second burst window based on the encounter settings.</p>
`);
setResultTooltip('results-sim-cod', `
<p>Chance of Death</p>
<p>The percentage of iterations in which the player died, based on incoming damage from the enemies and incoming healing (see the <b>Incoming HPS</b> and <b>Healing Cadence</b> options).</p>
Expand All @@ -199,6 +207,7 @@ export class RaidSimResultsManager {
Array.from(this.simUI.resultsViewer.contentElem.getElementsByClassName('results-sim-reference-dpasp-diff')).forEach(e => e.remove());
Array.from(this.simUI.resultsViewer.contentElem.getElementsByClassName('results-sim-reference-tps-diff')).forEach(e => e.remove());
Array.from(this.simUI.resultsViewer.contentElem.getElementsByClassName('results-sim-reference-dtps-diff')).forEach(e => e.remove());
Array.from(this.simUI.resultsViewer.contentElem.getElementsByClassName('results-sim-reference-tmi-diff')).forEach(e => e.remove());
Array.from(this.simUI.resultsViewer.contentElem.getElementsByClassName('results-sim-reference-cod-diff')).forEach(e => e.remove());
}

Expand Down Expand Up @@ -269,6 +278,7 @@ export class RaidSimResultsManager {
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['dtps']} .results-reference-diff`, res => res.getPlayers()[0]!.dtps, 2, true);
this.formatToplineResult(`.${RaidSimResultsManager.resultMetricClasses['tmi']} .results-reference-diff`, res => res.getPlayers()[0]!.tmi, 2, true);
this.formatToplineResult(`.${RaidSimResultsManager.resultMetricClasses['cod']} .results-reference-diff`, res => res.getPlayers()[0]!.chanceOfDeath, 1, true);
}
}
Expand Down Expand Up @@ -363,6 +373,7 @@ export class RaidSimResultsManager {
const dpaspMetrics = playerMetrics.dpasp;
const tpsMetrics = playerMetrics.tps;
const dtpsMetrics = playerMetrics.dtps;
const tmiMetrics = playerMetrics.tmi;
content += this.buildResultsLine({
average: dpsMetrics.avg,
stdev: dpsMetrics.stdev,
Expand Down Expand Up @@ -390,6 +401,11 @@ export class RaidSimResultsManager {
stdev: dtpsMetrics.stdev,
classes: this.getResultsLineClasses('dtps'),
}).outerHTML;
content += this.buildResultsLine({
average: tmiMetrics.avg,
stdev: tmiMetrics.stdev,
classes: this.getResultsLineClasses('tmi'),
}).outerHTML;
content += this.buildResultsLine({
average: playerMetrics.chanceOfDeath,
classes: this.getResultsLineClasses('cod'),
Expand Down Expand Up @@ -479,4 +495,5 @@ export class RaidSimResultsManager {

return resultsFragment.children[0] as HTMLElement;
}

}
4 changes: 3 additions & 1 deletion ui/core/player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -851,7 +851,9 @@ export class Player<SpecType extends Spec> {
applySharedDefaults(eventID: EventID) {
TypedEvent.freezeAllAndDo(() => {
this.setInFrontOfTarget(eventID, isTankSpec(this.spec));
this.setHealingModel(eventID, HealingModel.create());
this.setHealingModel(eventID, HealingModel.create({
burstWindow: isTankSpec(this.spec) ? 6 : 0,
}));
this.setCooldowns(eventID, Cooldowns.create({
hpPercentForDefensives: isTankSpec(this.spec) ? 0.35 : 0,
}));
Expand Down
2 changes: 2 additions & 0 deletions ui/core/proto_utils/sim_result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ export class UnitMetrics {
readonly hps: DistributionMetricsProto;
readonly tps: DistributionMetricsProto;
readonly dtps: DistributionMetricsProto;
readonly tmi: DistributionMetricsProto;
readonly tto: DistributionMetricsProto;
readonly actions: Array<ActionMetrics>;
readonly auras: Array<AuraMetrics>;
Expand Down Expand Up @@ -332,6 +333,7 @@ export class UnitMetrics {
this.hps = this.metrics.hps!;
this.tps = this.metrics.threat!;
this.dtps = this.metrics.dtps!;
this.tmi = this.metrics.tmi!;
this.tto = this.metrics.tto!;
this.actions = actions;
this.auras = auras;
Expand Down
1 change: 1 addition & 0 deletions ui/feral_tank_druid/sim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ export class FeralTankDruidSimUI extends IndividualSimUI<Spec.SpecFeralTankDruid
OtherInputs.TankAssignment,
OtherInputs.IncomingHps,
OtherInputs.HealingCadence,
OtherInputs.BurstWindow,
OtherInputs.HpPercentForDefensives,
DruidInputs.StartingRage,
OtherInputs.InFrontOfTarget,
Expand Down
1 change: 1 addition & 0 deletions ui/protection_paladin/sim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ export class ProtectionPaladinSimUI extends IndividualSimUI<Spec.SpecProtectionP
OtherInputs.TankAssignment,
OtherInputs.IncomingHps,
OtherInputs.HealingCadence,
OtherInputs.BurstWindow,
OtherInputs.HpPercentForDefensives,
OtherInputs.InspirationUptime,
ProtectionPaladinInputs.AuraSelection,
Expand Down
Loading

0 comments on commit 911eb80

Please sign in to comment.