From a281c3fb2050d6ec0f58aa5d8c233412a37ae0e1 Mon Sep 17 00:00:00 2001 From: vigo Date: Wed, 20 Sep 2023 18:44:40 +0200 Subject: [PATCH 1/2] [dk] slight RuneCost and RunicPowerBar overhaul --- sim/core/apl_values_runes.go | 16 +- sim/core/runic_power.go | 885 ++++++------------ sim/core/runic_power_helper.go | 103 +- sim/core/sim.go | 7 + sim/deathknight/blood_tap.go | 5 +- sim/deathknight/dps/rotation_blood_helper.go | 8 +- .../dps/rotation_frost_sub_blood.go | 12 +- .../dps/rotation_frost_sub_unholy.go | 16 +- sim/deathknight/dps/rotation_unholy_helper.go | 10 +- sim/deathknight/items.go | 2 +- sim/deathknight/tank/rotation_shared.go | 8 +- 11 files changed, 334 insertions(+), 738 deletions(-) diff --git a/sim/core/apl_values_runes.go b/sim/core/apl_values_runes.go index aa1ce0d164..15b1542868 100644 --- a/sim/core/apl_values_runes.go +++ b/sim/core/apl_values_runes.go @@ -27,10 +27,10 @@ func (rot *APLRotation) newValueCurrentRuneCount(config *proto.APLValueCurrentRu func (value *APLValueCurrentRuneCount) Type() proto.APLValueType { return proto.APLValueType_ValueTypeInt } -func (value *APLValueCurrentRuneCount) GetInt(sim *Simulation) int32 { +func (value *APLValueCurrentRuneCount) GetInt(_ *Simulation) int32 { switch value.runeType { case proto.APLValueRuneType_RuneBlood: - return int32(value.unit.CurrentBloodRunes()) + return int32(value.unit.CurrentBloodRunes()) // TODO these only count non-death runes of the specified kind case proto.APLValueRuneType_RuneFrost: return int32(value.unit.CurrentFrostRunes()) case proto.APLValueRuneType_RuneUnholy: @@ -64,14 +64,14 @@ func (rot *APLRotation) newValueCurrentNonDeathRuneCount(config *proto.APLValueC func (value *APLValueCurrentNonDeathRuneCount) Type() proto.APLValueType { return proto.APLValueType_ValueTypeInt } -func (value *APLValueCurrentNonDeathRuneCount) GetInt(sim *Simulation) int32 { +func (value *APLValueCurrentNonDeathRuneCount) GetInt(_ *Simulation) int32 { switch value.runeType { case proto.APLValueRuneType_RuneBlood: - return int32(value.unit.NormalCurrentBloodRunes()) + return int32(value.unit.CurrentBloodRunes()) case proto.APLValueRuneType_RuneFrost: - return int32(value.unit.NormalCurrentFrostRunes()) + return int32(value.unit.CurrentFrostRunes()) case proto.APLValueRuneType_RuneUnholy: - return int32(value.unit.NormalCurrentUnholyRunes()) + return int32(value.unit.CurrentUnholyRunes()) } return 0 } @@ -99,7 +99,7 @@ func (rot *APLRotation) newValueCurrentRuneActive(config *proto.APLValueCurrentR func (value *APLValueCurrentRuneActive) Type() proto.APLValueType { return proto.APLValueType_ValueTypeBool } -func (value *APLValueCurrentRuneActive) GetBool(sim *Simulation) bool { +func (value *APLValueCurrentRuneActive) GetBool(_ *Simulation) bool { return value.unit.RuneIsActive(value.runeSlot) } func (value *APLValueCurrentRuneActive) String() string { @@ -126,7 +126,7 @@ func (rot *APLRotation) newValueCurrentRuneDeath(config *proto.APLValueCurrentRu func (value *APLValueCurrentRuneDeath) Type() proto.APLValueType { return proto.APLValueType_ValueTypeBool } -func (value *APLValueCurrentRuneDeath) GetBool(sim *Simulation) bool { +func (value *APLValueCurrentRuneDeath) GetBool(_ *Simulation) bool { return value.unit.RuneIsDeath(int8(value.runeSlot)) } func (value *APLValueCurrentRuneDeath) String() string { diff --git a/sim/core/runic_power.go b/sim/core/runic_power.go index 4dff354435..cfa16435b0 100644 --- a/sim/core/runic_power.go +++ b/sim/core/runic_power.go @@ -2,6 +2,7 @@ package core import ( "fmt" + "strings" "time" "github.com/wowsims/wotlk/sim/core/proto" @@ -10,16 +11,6 @@ import ( type OnRune func(sim *Simulation) type OnRunicPowerGain func(sim *Simulation) -type RuneKind uint8 - -const ( - RuneKind_Undef RuneKind = iota - RuneKind_Blood - RuneKind_Frost - RuneKind_Unholy - RuneKind_Death -) - type RuneMeta struct { lastRegenTime time.Duration // last time the rune regenerated. lastSpendTime time.Duration // last time the rune was spent. @@ -58,20 +49,34 @@ type RunicPowerBar struct { clone *RunicPowerBar } -func (rp *RunicPowerBar) Print() { - fmt.Print(rp.DebugString()) -} +// Constants for finding runes +// |DS|DS|DS|DS|DS|DS| +const ( + baseRuneState = 0 // unspent, no death + + allDeath = 0b101010101010 + allSpent = 0b010101010101 + + anyBloodSpent = 0b0101 << 0 + anyFrostSpent = 0b0101 << 4 + anyUnholySpent = 0b0101 << 8 +) + +var ( + isDeaths = [6]int16{0b10 << 0, 0b10 << 2, 0b10 << 4, 0b10 << 6, 0b10 << 8, 0b10 << 10} + isSpents = [6]int16{0b01 << 0, 0b01 << 2, 0b01 << 4, 0b01 << 6, 0b01 << 8, 0b01 << 10} + isSpentDeath = [6]int16{0b11 << 0, 0b11 << 2, 0b11 << 4, 0b11 << 6, 0b11 << 8, 0b11 << 10} +) func (rp *RunicPowerBar) DebugString() string { - data := "" - for i := int32(0); i < 6; i++ { - data += fmt.Sprintf("Rune %d - D: %v S: %v\n\tRegenAt: %0.1f, RevertAt: %0.1f\n", i, rp.runeStates&isDeaths[i] != 0, rp.runeStates&isSpents[i] != 0, rp.runeMeta[i].regenAt.Seconds(), rp.runeMeta[i].revertAt.Seconds()) + ss := make([]string, len(rp.runeMeta)) + for i := range rp.runeMeta { + ss[i] += fmt.Sprintf("Rune %d - D: %v S: %v\n\tRegenAt: %0.1f, RevertAt: %0.1f", i, rp.runeStates&isDeaths[i] != 0, rp.runeStates&isSpents[i] != 0, rp.runeMeta[i].regenAt.Seconds(), rp.runeMeta[i].revertAt.Seconds()) } - - return data + return strings.Join(ss, "\n") } -// CopyRunicPowerBar will create a clone of the bar with the same +// CopyRunicPowerBar will create a clone of the bar with the same rune state func (rp *RunicPowerBar) CopyRunicPowerBar() *RunicPowerBar { if rp.clone == nil { rp.clone = &RunicPowerBar{isACopy: true} @@ -86,7 +91,7 @@ func (rp *RunicPowerBar) CopyRunicPowerBar() *RunicPowerBar { return rp.clone } -func ResetRunes(runeMeta *RuneMeta) { +func resetRunes(runeMeta *RuneMeta) { runeMeta.regenAt = NeverExpires runeMeta.revertAt = NeverExpires runeMeta.lastRegenTime = -1 @@ -101,17 +106,15 @@ func (rp *RunicPowerBar) reset(sim *Simulation) { rp.pa.Cancel(sim) } - ResetRunes(&rp.runeMeta[0]) - ResetRunes(&rp.runeMeta[1]) - ResetRunes(&rp.runeMeta[2]) - ResetRunes(&rp.runeMeta[3]) - ResetRunes(&rp.runeMeta[4]) - ResetRunes(&rp.runeMeta[5]) - rp.runeStates = baseRuneState // unspent, no death + resetRunes(&rp.runeMeta[0]) + resetRunes(&rp.runeMeta[1]) + resetRunes(&rp.runeMeta[2]) + resetRunes(&rp.runeMeta[3]) + resetRunes(&rp.runeMeta[4]) + resetRunes(&rp.runeMeta[5]) + rp.runeStates = baseRuneState } -const baseRuneState = int16(0) - func (unit *Unit) EnableRunicPowerBar(currentRunicPower float64, maxRunicPower float64, runeCD time.Duration, onRuneSpend OnRune, onBloodRuneGain OnRune, @@ -153,34 +156,10 @@ func (rp *RunicPowerBar) SetRuneCd(runeCd time.Duration) { rp.runeCD = runeCd } -func (rp *RunicPowerBar) BloodRuneGainMetrics() *ResourceMetrics { - return rp.bloodRuneGainMetrics -} - -func (rp *RunicPowerBar) FrostRuneGainMetrics() *ResourceMetrics { - return rp.frostRuneGainMetrics -} - -func (rp *RunicPowerBar) UnholyRuneGainMetrics() *ResourceMetrics { - return rp.unholyRuneGainMetrics -} - -func (rp *RunicPowerBar) DeathRuneGainMetrics() *ResourceMetrics { - return rp.deathRuneGainMetrics -} - func (rp *RunicPowerBar) CurrentRunicPower() float64 { return rp.currentRunicPower } -func (rp *RunicPowerBar) MaxRunicPower() float64 { - return rp.maxRunicPower -} - -func (rp *RunicPowerBar) PercentRunicPower() float64 { - return rp.currentRunicPower / rp.maxRunicPower -} - func (rp *RunicPowerBar) addRunicPowerInterval(sim *Simulation, amount float64, metrics *ResourceMetrics) { if amount < 0 { panic("Trying to add negative runic power!") @@ -226,8 +205,7 @@ func (rp *RunicPowerBar) spendRunicPower(sim *Simulation, amount float64, metric } // DeathRuneRegenAt returns the time the given death rune will regen at. -// -// If the rune is not death or not spent it returns NeverExpires +// If the rune is not death or not spent it returns NeverExpires func (rp *RunicPowerBar) DeathRuneRegenAt(slot int32) time.Duration { // If not death or not spent, no regen time if isSpentDeath[slot]&rp.runeStates != isSpentDeath[slot] { @@ -237,8 +215,7 @@ func (rp *RunicPowerBar) DeathRuneRegenAt(slot int32) time.Duration { } // DeathRuneRevertAt returns the next time that a death rune will revert. -// -// If there is no deathrune that needs to revert it returns `NeverExpires`. +// If there is no death rune that needs to revert it returns `NeverExpires`. func (rp *RunicPowerBar) DeathRuneRevertAt() time.Duration { readyAt := NeverExpires for i := int32(0); i < 6; i++ { @@ -249,14 +226,6 @@ func (rp *RunicPowerBar) DeathRuneRevertAt() time.Duration { return readyAt } -func (rp *RunicPowerBar) SpentDeathRuneReadyAt() time.Duration { - readyAt := NeverExpires - for i := int32(0); i < 6; i++ { - readyAt = MinDuration(readyAt, rp.DeathRuneRegenAt(i)) - } - return readyAt -} - func (rp *RunicPowerBar) RuneGraceRemaining(sim *Simulation, slot int8) time.Duration { lastRegenTime := rp.runeMeta[slot].lastRegenTime @@ -297,10 +266,6 @@ func (rp *RunicPowerBar) CurrentUnholyRuneGrace(sim *Simulation) time.Duration { return MaxDuration(rp.CurrentRuneGrace(sim, 4), rp.CurrentRuneGrace(sim, 5)) } -func (rp *RunicPowerBar) BloodRuneGraceRemaining(sim *Simulation) time.Duration { - return MaxDuration(rp.RuneGraceRemaining(sim, 0), rp.RuneGraceRemaining(sim, 1)) -} - func (rp *RunicPowerBar) FrostRuneGraceRemaining(sim *Simulation) time.Duration { return MaxDuration(rp.RuneGraceRemaining(sim, 2), rp.RuneGraceRemaining(sim, 3)) } @@ -309,69 +274,36 @@ func (rp *RunicPowerBar) UnholyRuneGraceRemaining(sim *Simulation) time.Duration return MaxDuration(rp.RuneGraceRemaining(sim, 4), rp.RuneGraceRemaining(sim, 5)) } -const anyBloodSpent = 0b0101 -const anyFrostSpent = 0b0101 << 4 -const anyUnholySpent = 0b0101 << 8 - -func (rp *RunicPowerBar) NormalSpentBloodRuneReadyAt(sim *Simulation) time.Duration { +func (rp *RunicPowerBar) normalSpentRuneReadyAt(slot int8) time.Duration { readyAt := NeverExpires - if rp.runeStates&isDeaths[0] == 0 && rp.runeStates&isSpents[0] != 0 { - readyAt = rp.runeMeta[0].regenAt + if t := rp.runeMeta[slot].regenAt; t < readyAt && rp.runeStates&isSpentDeath[slot] == isSpents[slot] { + readyAt = t } - if rp.runeStates&isDeaths[1] == 0 && rp.runeStates&isSpents[1] != 0 { - readyAt = MinDuration(readyAt, rp.runeMeta[1].regenAt) + if t := rp.runeMeta[slot+1].regenAt; t < readyAt && rp.runeStates&isSpentDeath[slot+1] == isSpents[slot+1] { + readyAt = t } return readyAt } -func (rp *RunicPowerBar) NormalSpentFrostRuneReadyAt(sim *Simulation) time.Duration { - readyAt := NeverExpires - if rp.runeStates&isDeaths[2] == 0 && rp.runeStates&isSpents[2] != 0 { - readyAt = rp.runeMeta[2].regenAt - } - if rp.runeStates&isDeaths[3] == 0 && rp.runeStates&isSpents[3] != 0 { - readyAt = MinDuration(readyAt, rp.runeMeta[3].regenAt) - } - return readyAt +// NormalSpentBloodRuneReadyAt returns the earliest time a spent non-death blood rune is ready. +func (rp *RunicPowerBar) NormalSpentBloodRuneReadyAt(_ *Simulation) time.Duration { + return rp.normalSpentRuneReadyAt(0) } -func (rp *RunicPowerBar) NormalFrostRuneReadyAt(sim *Simulation) time.Duration { - readyAt := NeverExpires - if rp.runeStates&isDeaths[2] == 0 && rp.runeStates&isSpents[2] != 0 { - readyAt = rp.runeMeta[2].regenAt - } - if rp.runeStates&isDeaths[3] == 0 && rp.runeStates&isSpents[3] != 0 { - readyAt = MinDuration(readyAt, rp.runeMeta[3].regenAt) - } - if (rp.runeStates&isDeaths[2] == 0 && rp.runeStates&isSpents[2] == 0) || (rp.runeStates&isDeaths[3] == 0 && rp.runeStates&isSpents[3] == 0) { - readyAt = sim.CurrentTime +func (rp *RunicPowerBar) normalRuneReadyAt(sim *Simulation, slot int8) time.Duration { + if rp.runeStates&isSpentDeath[slot] == 0 || rp.runeStates&isSpentDeath[slot+1] == 0 { + return sim.CurrentTime } - return readyAt + return rp.normalSpentRuneReadyAt(slot) } -func (rp *RunicPowerBar) NormalSpentUnholyRuneReadyAt(sim *Simulation) time.Duration { - readyAt := NeverExpires - if rp.runeStates&isDeaths[4] == 0 && rp.runeStates&isSpents[4] != 0 { - readyAt = rp.runeMeta[4].regenAt - } - if rp.runeStates&isDeaths[5] == 0 && rp.runeStates&isSpents[5] != 0 { - readyAt = MinDuration(readyAt, rp.runeMeta[5].regenAt) - } - return readyAt +// NormalFrostRuneReadyAt returns the earliest time a non-death frost rune is ready. +func (rp *RunicPowerBar) NormalFrostRuneReadyAt(sim *Simulation) time.Duration { + return rp.normalRuneReadyAt(sim, 2) } func (rp *RunicPowerBar) NormalUnholyRuneReadyAt(sim *Simulation) time.Duration { - readyAt := NeverExpires - if rp.runeStates&isDeaths[4] == 0 && rp.runeStates&isSpents[4] != 0 { - readyAt = rp.runeMeta[4].regenAt - } - if rp.runeStates&isDeaths[5] == 0 && rp.runeStates&isSpents[5] != 0 { - readyAt = MinDuration(readyAt, rp.runeMeta[5].regenAt) - } - if (rp.runeStates&isDeaths[4] == 0 && rp.runeStates&isSpents[4] == 0) || (rp.runeStates&isDeaths[5] == 0 && rp.runeStates&isSpents[5] == 0) { - readyAt = sim.CurrentTime - } - return readyAt + return rp.normalRuneReadyAt(sim, 4) } func (rp *RunicPowerBar) SpentBloodRuneReadyAt() time.Duration { @@ -410,6 +342,7 @@ func (rp *RunicPowerBar) SpendRuneReadyAt(slot int8, spendAt time.Duration) time return spendAt + (rp.runeCD - runeGraceDuration) } +// BloodRuneReadyAt returns the earliest time a (possibly death-converted) blood rune is ready. func (rp *RunicPowerBar) BloodRuneReadyAt(sim *Simulation) time.Duration { if rp.runeStates&anyBloodSpent != anyBloodSpent { // if any are not spent return sim.CurrentTime @@ -443,6 +376,7 @@ func (rp *RunicPowerBar) NextRuneTypeReadyAt(sim *Simulation, left int8, right i return MaxDuration(rp.runeMeta[left].regenAt, rp.runeMeta[right].regenAt) } +// TODO Can possibly replaced by either BloodRuneReadyAt() or SpentBloodRuneReadyAt() variants, depending on semantics. func (rp *RunicPowerBar) NextBloodRuneReadyAt(sim *Simulation) time.Duration { return rp.NextRuneTypeReadyAt(sim, 0, 1) } @@ -456,8 +390,7 @@ func (rp *RunicPowerBar) NextUnholyRuneReadyAt(sim *Simulation) time.Duration { } // AnySpentRuneReadyAt returns the next time that a rune will regenerate. -// -// It will be NeverExpires if there is no rune pending regeneration. +// It will be NeverExpires if there is no rune pending regeneration. func (rp *RunicPowerBar) AnySpentRuneReadyAt() time.Duration { return MinDuration(MinDuration(rp.SpentBloodRuneReadyAt(), rp.SpentFrostRuneReadyAt()), rp.SpentUnholyRuneReadyAt()) } @@ -468,7 +401,7 @@ func (rp *RunicPowerBar) AnyRuneReadyAt(sim *Simulation) time.Duration { // ConvertFromDeath reverts the rune to its original type. func (rp *RunicPowerBar) ConvertFromDeath(sim *Simulation, slot int8) { - rp.runeStates = ^isDeaths[slot] & rp.runeStates + rp.runeStates ^= isDeaths[slot] rp.runeMeta[slot].revertAt = NeverExpires if !rp.isACopy && rp.runeStates&isSpents[slot] == 0 { @@ -481,13 +414,13 @@ func (rp *RunicPowerBar) ConvertFromDeath(sim *Simulation, slot int8) { metrics = rp.unholyRuneGainMetrics onGain = rp.onUnholyRuneGain } - rp.SpendRuneMetrics(sim, rp.deathRuneGainMetrics, 1) - rp.GainRuneMetrics(sim, metrics, 1) + rp.spendRuneMetrics(sim, rp.deathRuneGainMetrics, 1) + rp.gainRuneMetrics(sim, metrics, 1) onGain(sim) } } -// ConvertToDeath converts the given slot to death and sets up the revertion conditions +// ConvertToDeath converts the given slot to death and sets up the reversion conditions func (rp *RunicPowerBar) ConvertToDeath(sim *Simulation, slot int8, revertAt time.Duration) { if slot == -1 { return @@ -514,451 +447,236 @@ func (rp *RunicPowerBar) ConvertToDeath(sim *Simulation, slot int8, revertAt tim } if rp.runeStates&isSpents[slot] == 0 { // Only lose/gain if it wasn't spent (which it should be at this point) - rp.SpendRuneMetrics(sim, metrics, 1) - rp.GainRuneMetrics(sim, rp.deathRuneGainMetrics, 1) + rp.spendRuneMetrics(sim, metrics, 1) + rp.gainRuneMetrics(sim, rp.deathRuneGainMetrics, 1) rp.onDeathRuneGain(sim) } } } func (rp *RunicPowerBar) LeftBloodRuneReady() bool { - const unspentBlood1 = isSpent - if rp.runeStates&unspentBlood1 == 0 { - return true - } else { - return false - } + return rp.runeStates&isSpents[0] == 0 } func (rp *RunicPowerBar) RightBloodRuneReady() bool { - const unspentBlood1 = isSpent - const unspentBlood2 = unspentBlood1 << 2 - if rp.runeStates&unspentBlood2 == 0 { - return true - } else { - return false - } + return rp.runeStates&isSpents[1] == 0 } func (rp *RunicPowerBar) RuneIsActive(slot int8) bool { - return (rp.runeStates & isSpents[slot]) == 0 + return rp.runeStates&isSpents[slot] == 0 } func (rp *RunicPowerBar) RuneIsDeath(slot int8) bool { - return (rp.runeStates & isDeaths[slot]) != 0 + return rp.runeStates&isDeaths[slot] != 0 } -func (rp *RunicPowerBar) CurrentBloodRunes() int8 { - const unspentBlood1 = isDeath | isSpent - const unspentBlood2 = unspentBlood1 << 2 +// rune state to count of non-death, non-spent runes (0b00) +var rs2c = []int8{ + 0b0000: 2, 0b0001: 1, 0b0010: 1, 0b0011: 1, 0b0100: 1, 0b0101: 0, 0b0110: 0, 0b0111: 0, + 0b1000: 1, 0b1001: 0, 0b1010: 0, 0b1011: 0, 0b1100: 1, 0b1101: 0, 0b1110: 0, 0b1111: 0, +} - var count int8 - if rp.runeStates&unspentBlood1 == 0 { - count++ - } - if rp.runeStates&unspentBlood2 == 0 { - count++ - } - return count +func (rp *RunicPowerBar) CurrentBloodRunes() int8 { + return rs2c[rp.runeStates&0b1111] } func (rp *RunicPowerBar) CurrentFrostRunes() int8 { - const unspentFrost1 = (isDeath | isSpent) << 4 - const unspentFrost2 = unspentFrost1 << 2 - - var count int8 - if rp.runeStates&unspentFrost1 == 0 { - count++ - } - if rp.runeStates&unspentFrost2 == 0 { - count++ - } - return count + return rs2c[(rp.runeStates>>4)&0b1111] } func (rp *RunicPowerBar) CurrentUnholyRunes() int8 { - const unspentUnholy1 = (isDeath | isSpent) << 8 - const unspentUnholy2 = unspentUnholy1 << 2 - - var count int8 - if rp.runeStates&unspentUnholy1 == 0 { - count++ - } - if rp.runeStates&unspentUnholy2 == 0 { - count++ - } + return rs2c[(rp.runeStates>>8)&0b1111] +} - return count +// rune state to count of death, non-spent runes (0b10) +var rs2d = []int8{ + 0b0000: 0, 0b0001: 0, 0b0010: 1, 0b0011: 0, 0b0100: 0, 0b0101: 0, 0b0110: 1, 0b0111: 0, + 0b1000: 1, 0b1001: 1, 0b1010: 2, 0b1011: 1, 0b1100: 0, 0b1101: 0, 0b1110: 1, 0b1111: 0, } func (rp *RunicPowerBar) CurrentDeathRunes() int8 { - var count int8 - for i := range rp.runeMeta { - if rp.runeStates&isDeaths[i] != 0 && rp.runeStates&isSpents[i] == 0 { - count++ - } - } - return count + return rs2d[rp.runeStates&0b1111] + rs2d[(rp.runeStates>>4)&0b1111] + rs2d[(rp.runeStates>>8)&0b1111] } func (rp *RunicPowerBar) DeathRunesInFU() int8 { - var count int8 - for i := 2; i < len(rp.runeMeta); i++ { - if rp.runeStates&isDeaths[i] != 0 { - count++ - } - } - return count + return rs2d[(rp.runeStates>>4)&0b1111] + rs2d[(rp.runeStates>>8)&0b1111] } -func (rp *RunicPowerBar) BloodRunesBTSync() bool { - const unspentBlood1 = isSpent - const unspentBlood2 = unspentBlood1 << 2 - - if rp.runeStates&unspentBlood1 != 0 && rp.runeStates&unspentBlood2 == 0 { - return true - } - - return false -} - -func (rp *RunicPowerBar) NormalCurrentBloodRunes() int32 { - const unspentBlood1 = isSpent - const unspentBlood2 = unspentBlood1 << 2 - const deathBlood1 = isDeath - const deathBlood2 = deathBlood1 << 2 - - var count int32 - if rp.runeStates&unspentBlood1 == 0 && rp.runeStates&deathBlood1 == 0 { - count++ - } - if rp.runeStates&unspentBlood2 == 0 && rp.runeStates&deathBlood2 == 0 { - count++ - } - - return count -} - -func (rp *RunicPowerBar) NormalCurrentFrostRunes() int32 { - const unspentFrost1 = isSpent << 4 - const unspentFrost2 = unspentFrost1 << 2 - const deathFrost1 = isDeath << 4 - const deathFrost2 = deathFrost1 << 2 - - var count int32 - if rp.runeStates&unspentFrost1 == 0 && rp.runeStates&deathFrost1 == 0 { - count++ - } - if rp.runeStates&unspentFrost2 == 0 && rp.runeStates&deathFrost2 == 0 { - count++ - } - - return count -} - -func (rp *RunicPowerBar) NormalCurrentUnholyRunes() int32 { - const unspentUnholy1 = isSpent << 8 - const unspentUnholy2 = unspentUnholy1 << 2 - const deathUnholy1 = isDeath << 8 - const deathUnholy2 = deathUnholy1 << 2 - - var count int32 - if rp.runeStates&unspentUnholy1 == 0 && rp.runeStates&deathUnholy1 == 0 { - count++ - } - if rp.runeStates&unspentUnholy2 == 0 && rp.runeStates&deathUnholy2 == 0 { - count++ - } - - return count -} - -func (rp *RunicPowerBar) NormalCurrentRunes() (int32, int32, int32) { - return rp.NormalCurrentBloodRunes(), rp.NormalCurrentFrostRunes(), rp.NormalCurrentUnholyRunes() -} func (rp *RunicPowerBar) AllRunesSpent() bool { - const allSpent = isSpent | (isSpent << 2) | (isSpent << 4) | (isSpent << 6) | (isSpent << 8) | (isSpent << 10) return rp.runeStates&allSpent == allSpent } -func (rp *RunicPowerBar) AllBloodRunesSpent() bool { - const checkBloodSpent = isSpent | (isSpent << 2) - return rp.runeStates&checkBloodSpent == checkBloodSpent -} - -func (rp *RunicPowerBar) AllFrostSpent() bool { - const checkFrostSpent = (isSpent << 4) | (isSpent << 6) - return rp.runeStates&checkFrostSpent == checkFrostSpent -} - -func (rp *RunicPowerBar) AllUnholySpent() bool { - const checkUnholySpent = (isSpent << 8) | (isSpent << 10) - return rp.runeStates&checkUnholySpent == checkUnholySpent -} - -func (rp *RunicPowerBar) CastCostPossible(sim *Simulation, runicPowerAmount float64, bloodAmount int8, frostAmount int8, unholyAmount int8) bool { - if rp.CurrentRunicPower() < runicPowerAmount { - return false - } +func (rp *RunicPowerBar) OptimalRuneCost(cost RuneCost) RuneCost { + var b, f, u, d int8 - var deficit int8 - if d := bloodAmount - rp.CurrentBloodRunes(); d > 0 { - deficit += d - } - if d := frostAmount - rp.CurrentFrostRunes(); d > 0 { - deficit += d - } - if d := unholyAmount - rp.CurrentUnholyRunes(); d > 0 { - deficit += d + if b = cost.Blood(); b > 0 { + if cb := rp.CurrentBloodRunes(); cb < b { + d += b - cb + b = cb + } } - return deficit <= rp.CurrentDeathRunes() -} -func (rp *RunicPowerBar) OptimalRuneCost(cost RuneCost) RuneCost { - bh := uint8(rp.CurrentBloodRunes()) - fh := uint8(rp.CurrentFrostRunes()) - uh := uint8(rp.CurrentUnholyRunes()) - dh := uint8(rp.CurrentDeathRunes()) - current := NewRuneCost(cost.RunicPower(), bh, fh, uh, dh) - if current&cost == cost { - return cost // if we match the cost then we dont need deathrunes + if f = cost.Frost(); f > 0 { + if cf := rp.CurrentFrostRunes(); cf < f { + d += f - cf + f = cf + } } - neededDeath := cost.Death() // just in case death was passed in as a cost. - - newCost := NewRuneCost(cost.RunicPower(), 0, 0, 0, 0) - - if c := cost.Blood(); bh < c { - neededDeath += c - bh - } else if c == 1 { - newCost |= 0b01 - } else if c == 2 { - newCost |= 0b11 + if u = cost.Unholy(); u > 0 { + if cu := rp.CurrentUnholyRunes(); cu < u { + d += u - cu + u = cu + } } - if c := cost.Frost(); fh < c { - neededDeath += c - fh - } else if c == 1 { - newCost |= 0b0100 - } else if c == 2 { - newCost |= 0b1100 + if d == 0 { + return cost } - if c := cost.Unholy(); uh < c { - neededDeath += c - uh - } else if c == 1 { - newCost |= 0b010000 - } else if c == 2 { - newCost |= 0b110000 - } + d += cost.Death() - if neededDeath > dh { - return 0 // can't cast - } else if neededDeath == 1 { - newCost |= 0b01000000 - } else if neededDeath == 2 { - newCost |= 0b11000000 + if cd := rp.CurrentDeathRunes(); cd >= d { + return NewRuneCost(cost.RunicPower(), b, f, u, d) } - return newCost + return 0 } func (rp *RunicPowerBar) SpendRuneCost(sim *Simulation, spell *Spell, cost RuneCost) (int8, int8, int8) { - r := [3]int8{-1, -1, -1} - idx := 0 + if !cost.HasRune() { + if rpc := cost.RunicPower(); rpc > 0 { + rp.spendRunicPower(sim, float64(cost.RunicPower()), spell.RunicPowerMetrics()) + } + return -1, -1, -1 + } - for i := uint8(0); i < cost.Blood(); i++ { - r[idx] = rp.SpendBloodRune(sim, spell.BloodRuneMetrics()) + slots := [3]int8{-1, -1, -1} + idx := 0 + for i := int8(0); i < cost.Blood(); i++ { + slots[idx] = rp.spendRune(sim, 0, spell.BloodRuneMetrics()) idx++ } - for i := uint8(0); i < cost.Frost(); i++ { - r[idx] = rp.SpendFrostRune(sim, spell.FrostRuneMetrics()) + for i := int8(0); i < cost.Frost(); i++ { + slots[idx] = rp.spendRune(sim, 2, spell.FrostRuneMetrics()) idx++ } - for i := uint8(0); i < cost.Unholy(); i++ { - r[idx] = rp.SpendUnholyRune(sim, spell.UnholyRuneMetrics()) + for i := int8(0); i < cost.Unholy(); i++ { + slots[idx] = rp.spendRune(sim, 4, spell.UnholyRuneMetrics()) idx++ } - for i := uint8(0); i < cost.Death(); i++ { - r[idx] = rp.SpendDeathRune(sim, spell.DeathRuneMetrics()) + for i := int8(0); i < cost.Death(); i++ { + slots[idx] = rp.spendDeathRune(sim, spell.DeathRuneMetrics()) idx++ } - rpc := cost.RunicPower() - hasRune := cost.HasRune() - if rpc <= 0 { - return r[0], r[1], r[2] - } - if !hasRune { - rp.spendRunicPower(sim, float64(rpc), spell.RunicPowerMetrics()) - } else { + + if rpc := cost.RunicPower(); rpc > 0 { rp.AddRunicPower(sim, float64(rpc), spell.RunicPowerMetrics()) } - return r[0], r[1], r[2] + return slots[0], slots[1], slots[2] } -// GainRuneMetrics should be called after gaining the rune -func (rp *RunicPowerBar) GainRuneMetrics(sim *Simulation, metrics *ResourceMetrics, gainAmount int8) { - if !rp.isACopy { - metrics.AddEvent(float64(gainAmount), float64(gainAmount)) - - if sim.Log != nil { - var name string - var currRunes int8 - - switch metrics.Type { - case proto.ResourceType_ResourceTypeDeathRune: - name = "death" - currRunes = rp.CurrentDeathRunes() - case proto.ResourceType_ResourceTypeBloodRune: - name = "blood" - currRunes = rp.CurrentBloodRunes() - case proto.ResourceType_ResourceTypeFrostRune: - name = "frost" - currRunes = rp.CurrentFrostRunes() - case proto.ResourceType_ResourceTypeUnholyRune: - name = "unholy" - currRunes = rp.CurrentUnholyRunes() - default: - panic("invalid metrics for rune gaining") - } - - rp.unit.Log(sim, "Gained %0.3f %s rune from %s (%d --> %d).", float64(gainAmount), name, metrics.ActionID, currRunes-gainAmount, currRunes) - } +// gainRuneMetrics should be called after gaining the rune +func (rp *RunicPowerBar) gainRuneMetrics(sim *Simulation, metrics *ResourceMetrics, gainAmount int8) { + if rp.isACopy { + return } -} - -// SpendRuneMetrics should be called after spending the rune -func (rp *RunicPowerBar) SpendRuneMetrics(sim *Simulation, metrics *ResourceMetrics, spendAmount int8) { - if !rp.isACopy { - metrics.AddEvent(-float64(spendAmount), -float64(spendAmount)) - - if sim.Log != nil { - var name string - var currRunes int8 - - switch metrics.Type { - case proto.ResourceType_ResourceTypeDeathRune: - name = "death" - currRunes = rp.CurrentDeathRunes() - case proto.ResourceType_ResourceTypeBloodRune: - name = "blood" - currRunes = rp.CurrentBloodRunes() - case proto.ResourceType_ResourceTypeFrostRune: - name = "frost" - currRunes = rp.CurrentFrostRunes() - case proto.ResourceType_ResourceTypeUnholyRune: - name = "unholy" - currRunes = rp.CurrentUnholyRunes() - default: - panic("invalid metrics for rune spending") - } - rp.unit.Log(sim, "Spent 1.000 %s rune from %s (%d --> %d).", name, metrics.ActionID, currRunes+spendAmount, currRunes) + metrics.AddEvent(float64(gainAmount), float64(gainAmount)) + + if sim.Log != nil { + var name string + var currRunes int8 + + switch metrics.Type { + case proto.ResourceType_ResourceTypeDeathRune: + name = "death" + currRunes = rp.CurrentDeathRunes() + case proto.ResourceType_ResourceTypeBloodRune: + name = "blood" + currRunes = rp.CurrentBloodRunes() + case proto.ResourceType_ResourceTypeFrostRune: + name = "frost" + currRunes = rp.CurrentFrostRunes() + case proto.ResourceType_ResourceTypeUnholyRune: + name = "unholy" + currRunes = rp.CurrentUnholyRunes() + default: + panic("invalid metrics for rune gaining") } - } -} -func (rp *RunicPowerBar) BloodRuneSpentAt(dur time.Duration) int32 { - if rp.runeMeta[0].lastSpendTime == dur { - return 0 + rp.unit.Log(sim, "Gained %0.3f %s rune from %s (%d --> %d).", float64(gainAmount), name, metrics.ActionID, currRunes-gainAmount, currRunes) } - if rp.runeMeta[1].lastSpendTime == dur { - return 1 - } - return -1 } -func (rp *RunicPowerBar) FrostRuneSpentAt(dur time.Duration) int32 { - if rp.runeMeta[2].lastSpendTime == dur { - return 2 - } - if rp.runeMeta[3].lastSpendTime == dur { - return 3 +// spendRuneMetrics should be called after spending the rune +func (rp *RunicPowerBar) spendRuneMetrics(sim *Simulation, metrics *ResourceMetrics, spendAmount int8) { + if rp.isACopy { + return } - return -1 -} -func (rp *RunicPowerBar) UnholyRuneSpentAt(dur time.Duration) int32 { - if rp.runeMeta[4].lastSpendTime == dur { - return 4 - } - if rp.runeMeta[5].lastSpendTime == dur { - return 5 + metrics.AddEvent(-float64(spendAmount), -float64(spendAmount)) + + if sim.Log != nil { + var name string + var currRunes int8 + + switch metrics.Type { + case proto.ResourceType_ResourceTypeDeathRune: + name = "death" + currRunes = rp.CurrentDeathRunes() + case proto.ResourceType_ResourceTypeBloodRune: + name = "blood" + currRunes = rp.CurrentBloodRunes() + case proto.ResourceType_ResourceTypeFrostRune: + name = "frost" + currRunes = rp.CurrentFrostRunes() + case proto.ResourceType_ResourceTypeUnholyRune: + name = "unholy" + currRunes = rp.CurrentUnholyRunes() + default: + panic("invalid metrics for rune spending") + } + + rp.unit.Log(sim, "Spent 1.000 %s rune from %s (%d --> %d).", name, metrics.ActionID, currRunes+spendAmount, currRunes) } - return -1 } -func (rp *RunicPowerBar) RegenRune(sim *Simulation, regenAt time.Duration, slot int8) { - checkSpent := isSpents[slot] - if checkSpent&rp.runeStates > 0 { - rp.runeStates = ^checkSpent & rp.runeStates // unset spent flag for this rune. - rp.runeMeta[slot].lastRegenTime = regenAt - rp.runeMeta[slot].regenAt = NeverExpires +func (rp *RunicPowerBar) regenRune(sim *Simulation, regenAt time.Duration, slot int8) { + if rp.runeStates&isSpents[slot] == 0 { + return + } - if !rp.isACopy { - metrics := rp.bloodRuneGainMetrics - onGain := rp.onBloodRuneGain - if rp.runeStates&(isDeaths[slot]) > 0 { - metrics = rp.deathRuneGainMetrics - onGain = rp.onDeathRuneGain - } else if slot == 2 || slot == 3 { - metrics = rp.frostRuneGainMetrics - onGain = rp.onFrostRuneGain - } else if slot == 4 || slot == 5 { - metrics = rp.unholyRuneGainMetrics - onGain = rp.onUnholyRuneGain - } + rp.runeStates ^= isSpents[slot] // unset spent flag for this rune. + rp.runeMeta[slot].lastRegenTime = regenAt + rp.runeMeta[slot].regenAt = NeverExpires - rp.GainRuneMetrics(sim, metrics, 1) - onGain(sim) + if !rp.isACopy { + metrics := rp.bloodRuneGainMetrics + onGain := rp.onBloodRuneGain + if rp.runeStates&isDeaths[slot] > 0 { + metrics = rp.deathRuneGainMetrics + onGain = rp.onDeathRuneGain + } else if slot == 2 || slot == 3 { + metrics = rp.frostRuneGainMetrics + onGain = rp.onFrostRuneGain + } else if slot == 4 || slot == 5 { + metrics = rp.unholyRuneGainMetrics + onGain = rp.onUnholyRuneGain } + + rp.gainRuneMetrics(sim, metrics, 1) + onGain(sim) } } func (rp *RunicPowerBar) RegenAllRunes(sim *Simulation) { - rp.RegenRune(sim, sim.CurrentTime, 0) - rp.RegenRune(sim, sim.CurrentTime, 1) - rp.RegenRune(sim, sim.CurrentTime, 2) - rp.RegenRune(sim, sim.CurrentTime, 3) - rp.RegenRune(sim, sim.CurrentTime, 4) - rp.RegenRune(sim, sim.CurrentTime, 5) -} - -func (rp *RunicPowerBar) SpendRuneFromKind(sim *Simulation, rkind RuneKind) int8 { - var rb int8 - if rkind == RuneKind_Frost { - rb = 2 - } else if rkind == RuneKind_Unholy { - rb = 4 - } else if rkind == RuneKind_Death { - panic("use 'ReadyRuneByKind' to find death rune") - } - spent1 := isSpents[rb] - spent2 := isSpents[rb+1] - - spendable1 := spent1 | isDeaths[rb] // verify rune is not spent and not death - spendable2 := spent2 | isDeaths[rb+1] - - slot := int8(-1) - // Figure out which rune is spendable (not death and not spent) - // Then mark the spend bit for that rune. - if rp.runeStates&spendable1 == 0 { - rp.runeStates |= spent1 - slot = rb - } else if rp.runeStates&spendable2 == 0 { - rp.runeStates |= spent2 - slot = rb + 1 - } else { - panic("Trying to spend rune that does not exist!") - } - - rp.runeMeta[slot].lastSpendTime = sim.CurrentTime - - if rp.onRuneSpend != nil { - rp.onRuneSpend(sim) - } - - return slot + rp.regenRune(sim, sim.CurrentTime, 0) + rp.regenRune(sim, sim.CurrentTime, 1) + rp.regenRune(sim, sim.CurrentTime, 2) + rp.regenRune(sim, sim.CurrentTime, 3) + rp.regenRune(sim, sim.CurrentTime, 4) + rp.regenRune(sim, sim.CurrentTime, 5) } func (rp *RunicPowerBar) RuneGraceAt(slot int8, at time.Duration) (runeGraceDuration time.Duration) { @@ -973,7 +691,7 @@ func (rp *RunicPowerBar) RuneGraceAt(slot int8, at time.Duration) (runeGraceDura return runeGraceDuration } -func (rp *RunicPowerBar) LaunchRuneRegen(sim *Simulation, slot int8) { +func (rp *RunicPowerBar) launchRuneRegen(sim *Simulation, slot int8) { runeGracePeriod := rp.RuneGraceAt(slot, sim.CurrentTime) rp.runeMeta[slot].regenAt = sim.CurrentTime + (rp.runeCD - runeGracePeriod) @@ -984,6 +702,7 @@ func (rp *RunicPowerBar) launchPA(sim *Simulation, at time.Duration) { if rp.isACopy { return } + if rp.pa != nil { // If this new regen is before currently scheduled one, we must cancel old regen and start a new one. if rp.pa.NextActionAt > at { @@ -993,6 +712,7 @@ func (rp *RunicPowerBar) launchPA(sim *Simulation, at time.Duration) { return } } + pa := &PendingAction{ NextActionAt: at, Priority: ActionPriorityRegen, @@ -1014,45 +734,9 @@ func (rp *RunicPowerBar) launchPA(sim *Simulation, at time.Duration) { } -// Constants for finding runes - -// |DS|DS|DS|DS|DS|DS| -const checkDeath = int16(0b101010101010) -const checkSpent = int16(0b010101010101) - -const isDeath = int16(0b10) -const isSpent = int16(0b01) - -var isDeaths = [6]int16{ - isDeath, - isDeath << 2, - isDeath << 4, - isDeath << 6, - isDeath << 8, - isDeath << 10, -} - -var isSpents = [6]int16{ - isSpent, - isSpent << 2, - isSpent << 4, - isSpent << 6, - isSpent << 8, - isSpent << 10, -} - -var isSpentDeath = [6]int16{ - (isDeath | isSpent), - (isDeath | isSpent) << 2, - (isDeath | isSpent) << 4, - (isDeath | isSpent) << 6, - (isDeath | isSpent) << 8, - (isDeath | isSpent) << 10, -} - func (rp *RunicPowerBar) Advance(sim *Simulation, newTime time.Duration) { - if rp.runeStates&checkDeath > 0 { - for i := int8(0); i < 6; i++ { + if rp.runeStates&allDeath > 0 { + for i := int8(0); i < int8(len(rp.runeMeta)); i++ { if rp.runeMeta[i].revertAt <= newTime { if rp.btslot == i { rp.btslot = -1 // this was the BT slot. @@ -1062,116 +746,84 @@ func (rp *RunicPowerBar) Advance(sim *Simulation, newTime time.Duration) { } } - if rp.runeStates&checkSpent > 0 { + if rp.runeStates&allSpent > 0 { rp.findAndRegen(sim, newTime) } } -func (rp *RunicPowerBar) TryRegenRune(sim *Simulation, newTime time.Duration, slot int8) { +func (rp *RunicPowerBar) tryRegenRune(sim *Simulation, newTime time.Duration, slot int8) { if rp.runeMeta[slot].regenAt > newTime { return } if rp.runeStates&isSpents[slot] == 0 { return } - - rp.RegenRune(sim, newTime, slot) + rp.regenRune(sim, newTime, slot) } func (rp *RunicPowerBar) findAndRegen(sim *Simulation, newTime time.Duration) { - rp.TryRegenRune(sim, newTime, 0) - rp.TryRegenRune(sim, newTime, 1) - rp.TryRegenRune(sim, newTime, 2) - rp.TryRegenRune(sim, newTime, 3) - rp.TryRegenRune(sim, newTime, 4) - rp.TryRegenRune(sim, newTime, 5) + rp.tryRegenRune(sim, newTime, 0) + rp.tryRegenRune(sim, newTime, 1) + rp.tryRegenRune(sim, newTime, 2) + rp.tryRegenRune(sim, newTime, 3) + rp.tryRegenRune(sim, newTime, 4) + rp.tryRegenRune(sim, newTime, 5) } -func (rp *RunicPowerBar) SpendBloodRune(sim *Simulation, metrics *ResourceMetrics) int8 { - currRunes := rp.CurrentBloodRunes() - if currRunes <= 0 { - panic("Trying to spend blood runes that don't exist!") - } +func (rp *RunicPowerBar) spendRune(sim *Simulation, firstSlot int8, metrics *ResourceMetrics) int8 { + slot := rp.findReadyRune(firstSlot) + rp.runeStates |= isSpents[slot] - spendSlot := rp.SpendRuneFromKind(sim, RuneKind_Blood) - rp.SpendRuneMetrics(sim, metrics, 1) - rp.LaunchRuneRegen(sim, spendSlot) - return spendSlot -} + rp.runeMeta[slot].lastSpendTime = sim.CurrentTime -func (rp *RunicPowerBar) SpendFrostRune(sim *Simulation, metrics *ResourceMetrics) int8 { - currRunes := rp.CurrentFrostRunes() - if currRunes <= 0 { - panic("Trying to spend frost runes that don't exist!") + if rp.onRuneSpend != nil { + rp.onRuneSpend(sim) } - spendSlot := rp.SpendRuneFromKind(sim, RuneKind_Frost) - rp.SpendRuneMetrics(sim, metrics, 1) - rp.LaunchRuneRegen(sim, spendSlot) - return spendSlot + rp.spendRuneMetrics(sim, metrics, 1) + rp.launchRuneRegen(sim, slot) + return slot } -func (rp *RunicPowerBar) SpendUnholyRune(sim *Simulation, metrics *ResourceMetrics) int8 { - currRunes := rp.CurrentUnholyRunes() - if currRunes <= 0 { - panic("Trying to spend unholy runes that don't exist!") +func (rp *RunicPowerBar) findReadyRune(slot int8) int8 { + if rp.runeStates&isSpentDeath[slot] == 0 { + return slot } - - spendSlot := rp.SpendRuneFromKind(sim, RuneKind_Unholy) - rp.SpendRuneMetrics(sim, metrics, 1) - rp.LaunchRuneRegen(sim, spendSlot) - return spendSlot -} - -// ReadyDeathRune returns the slot of first available death rune. -// -// Returns -1 if there are no ready death runes -func (rp *RunicPowerBar) ReadyDeathRune() int8 { - // Death runes are spent in the order Unholy -> Frost -> Blood in-game... - for runeType := int8(2); runeType >= 0; runeType-- { - for i := runeType * 2; i < (runeType+1)*2; i++ { - if rp.runeStates&isDeaths[i] != 0 && rp.runeStates&isSpents[i] == 0 { - return i - } - } + if rp.runeStates&isSpentDeath[slot+1] == 0 { + return slot + 1 } - return -1 -} - -func (rp *RunicPowerBar) IsBloodTappedRune(slot int8) bool { - return slot == rp.btslot + panic(fmt.Sprintf("findReadyRune(%d) - no slot found (runeStates = %12b)", slot, rp.runeStates)) } -func (rp *RunicPowerBar) SpendDeathRune(sim *Simulation, metrics *ResourceMetrics) int8 { - if rp.runeStates&checkDeath == 0 { - panic("Trying to spend death runes that don't exist!") - } - - slot := rp.ReadyDeathRune() +func (rp *RunicPowerBar) spendDeathRune(sim *Simulation, metrics *ResourceMetrics) int8 { + slot := rp.findReadyDeathRune() if rp.btslot != slot { - // disable revert at - rp.runeMeta[slot].revertAt = NeverExpires - // clear death bit to revert. - rp.runeStates = ^isDeaths[slot] & rp.runeStates + rp.runeMeta[slot].revertAt = NeverExpires // disable revert at + rp.runeStates ^= isDeaths[slot] // clear death bit to revert. } // mark spent bit to spend rp.runeStates |= isSpents[slot] rp.runeMeta[slot].lastSpendTime = sim.CurrentTime - rp.SpendRuneMetrics(sim, metrics, 1) - rp.LaunchRuneRegen(sim, slot) + rp.spendRuneMetrics(sim, metrics, 1) + rp.launchRuneRegen(sim, slot) return slot } -type RuneConvertType int8 +// findReadyDeathRune returns the slot of first available death rune. +func (rp *RunicPowerBar) findReadyDeathRune() int8 { + for _, slot := range []int8{4, 5, 2, 3, 0, 1} { // Death runes are spent in the order Unholy -> Frost -> Blood in-game... + if rp.runeStates&isSpentDeath[slot] == isDeaths[slot] { + return slot + } + } + panic(fmt.Sprintf("findReadyDeathRune() - no slot found (runeStates = %12b)", rp.runeStates)) +} -const ( - RuneConvertTypeNone RuneConvertType = 1 << iota - RuneConvertTypeBlood - RuneConvertTypeFrost - RuneConvertTypeUnholy -) +func (rp *RunicPowerBar) IsBloodTappedRune(slot int8) bool { + return slot == rp.btslot +} type RuneCostOptions struct { BloodRuneCost int8 @@ -1181,6 +833,7 @@ type RuneCostOptions struct { RunicPowerGain float64 Refundable bool } + type RuneCostImpl struct { BloodRuneCost int8 FrostRuneCost int8 @@ -1197,7 +850,7 @@ type RuneCostImpl struct { } func newRuneCost(spell *Spell, options RuneCostOptions) *RuneCostImpl { - baseCost := float64(NewRuneCost(uint8(options.RunicPowerCost), uint8(options.BloodRuneCost), uint8(options.FrostRuneCost), uint8(options.UnholyRuneCost), 0)) + baseCost := float64(NewRuneCost(int8(options.RunicPowerCost), options.BloodRuneCost, options.FrostRuneCost, options.UnholyRuneCost, 0)) spell.DefaultCast.Cost = baseCost spell.CurCast.Cost = baseCost @@ -1218,8 +871,8 @@ func newRuneCost(spell *Spell, options RuneCostOptions) *RuneCostImpl { } func (rc *RuneCostImpl) MeetsRequirement(spell *Spell) bool { - //rp := &spell.Unit.RunicPowerBar - spell.CurCast.Cost *= spell.CostMultiplier + spell.CurCast.Cost *= spell.CostMultiplier // TODO this looks fishy - multiplying and rune costs don't go well together + cost := RuneCost(spell.CurCast.Cost) if cost == 0 { return true @@ -1238,20 +891,22 @@ func (rc *RuneCostImpl) MeetsRequirement(spell *Spell) bool { spell.CurCast.Cost = float64(optCost) // assign chosen runes to the cost return true } + func (rc *RuneCostImpl) LogCostFailure(sim *Simulation, spell *Spell) { spell.Unit.Log(sim, "Failed casting %s, not enough RP or runes.", spell.ActionID) } + func (rc *RuneCostImpl) SpendCost(sim *Simulation, spell *Spell) { // Spend now if there is no way to refund the spell if !rc.Refundable { - cost := RuneCost(spell.CurCast.Cost) - spell.Unit.SpendRuneCost(sim, spell, cost) + spell.Unit.SpendRuneCost(sim, spell, RuneCost(spell.CurCast.Cost)) } if rc.RunicPowerGain > 0 && spell.CurCast.Cost > 0 { spell.Unit.AddRunicPower(sim, rc.RunicPowerGain, spell.RunicPowerMetrics()) } } -func (rc *RuneCostImpl) SpendRefundableCost(sim *Simulation, spell *Spell, result *SpellResult) { + +func (rc *RuneCostImpl) spendRefundableCost(sim *Simulation, spell *Spell, result *SpellResult) { cost := RuneCost(spell.CurCast.Cost) // cost was already optimized in RuneSpell.Cast if cost == 0 { return // it was free this time. we don't care @@ -1260,10 +915,12 @@ func (rc *RuneCostImpl) SpendRefundableCost(sim *Simulation, spell *Spell, resul spell.Unit.SpendRuneCost(sim, spell, cost) } } + func (spell *Spell) SpendRefundableCost(sim *Simulation, result *SpellResult) { - spell.Cost.(*RuneCostImpl).SpendRefundableCost(sim, spell, result) + spell.Cost.(*RuneCostImpl).spendRefundableCost(sim, spell, result) } -func (rc *RuneCostImpl) SpendRefundableCostAndConvertBloodRune(sim *Simulation, spell *Spell, result *SpellResult, convertChance float64) { + +func (rc *RuneCostImpl) spendRefundableCostAndConvertBloodRune(sim *Simulation, spell *Spell, result *SpellResult, convertChance float64) { cost := RuneCost(spell.CurCast.Cost) // cost was already optimized in RuneSpell.Cast if cost == 0 { return // it was free this time. we don't care @@ -1277,7 +934,7 @@ func (rc *RuneCostImpl) SpendRefundableCostAndConvertBloodRune(sim *Simulation, return } - for _, slot := range [2]int8{slot1, slot2} { + for _, slot := range []int8{slot1, slot2} { if slot == 0 || slot == 1 { // If the slot to be converted is already blood-tapped, then we convert the other blood rune if spell.Unit.IsBloodTappedRune(slot) { @@ -1289,10 +946,12 @@ func (rc *RuneCostImpl) SpendRefundableCostAndConvertBloodRune(sim *Simulation, } } } + func (spell *Spell) SpendRefundableCostAndConvertBloodRune(sim *Simulation, result *SpellResult, convertChance float64) { - spell.Cost.(*RuneCostImpl).SpendRefundableCostAndConvertBloodRune(sim, spell, result, convertChance) + spell.Cost.(*RuneCostImpl).spendRefundableCostAndConvertBloodRune(sim, spell, result, convertChance) } -func (rc *RuneCostImpl) SpendRefundableCostAndConvertFrostOrUnholyRune(sim *Simulation, spell *Spell, result *SpellResult, convertChance float64) { + +func (rc *RuneCostImpl) spendRefundableCostAndConvertFrostOrUnholyRune(sim *Simulation, spell *Spell, result *SpellResult, convertChance float64) { cost := RuneCost(spell.CurCast.Cost) // cost was already optimized in RuneSpell.Cast if cost == 0 { return // it was free this time. we don't care @@ -1306,16 +965,18 @@ func (rc *RuneCostImpl) SpendRefundableCostAndConvertFrostOrUnholyRune(sim *Simu return } - for _, slot := range [3]int8{slot1, slot2, slot3} { + for _, slot := range []int8{slot1, slot2, slot3} { if slot == 2 || slot == 3 || slot == 4 || slot == 5 { spell.Unit.ConvertToDeath(sim, slot, NeverExpires) } } } + func (spell *Spell) SpendRefundableCostAndConvertFrostOrUnholyRune(sim *Simulation, result *SpellResult, convertChance float64) { - spell.Cost.(*RuneCostImpl).SpendRefundableCostAndConvertFrostOrUnholyRune(sim, spell, result, convertChance) + spell.Cost.(*RuneCostImpl).spendRefundableCostAndConvertFrostOrUnholyRune(sim, spell, result, convertChance) } -func (rc *RuneCostImpl) IssueRefund(sim *Simulation, spell *Spell) { + +func (rc *RuneCostImpl) IssueRefund(_ *Simulation, _ *Spell) { // Instead of issuing refunds we just don't charge the cost of spells which // miss; this is better for perf since we'd have to cancel the regen actions. } diff --git a/sim/core/runic_power_helper.go b/sim/core/runic_power_helper.go index d711c18252..c29caf91d7 100644 --- a/sim/core/runic_power_helper.go +++ b/sim/core/runic_power_helper.go @@ -5,39 +5,11 @@ import ( "time" ) +// RuneCost's bit layout is: . Each part is just a count now (0..3 for runes). type RuneCost uint16 -func NewRuneCost(rp, blood, frost, unholy, death uint8) RuneCost { - value := int16(0) - if blood == 1 { - value = 1 - } else if blood == 2 { - value = 3 - } - - if frost == 1 { - value += 1 << 2 - } else if frost == 2 { - value += 3 << 2 - } - - if unholy == 1 { - value += 1 << 4 - } else if unholy == 2 { - value += 3 << 4 - } - - if death == 1 { - value += 1 << 6 - } else if death == 2 { - value += 3 << 6 - } else if death > 2 { - value += 3 << 6 // we cant represent more than 2 death runes - } - - value += int16(rp) << 8 - - return RuneCost(value) +func NewRuneCost(rp, blood, frost, unholy, death int8) RuneCost { + return RuneCost(rp)<<8 | RuneCost((death&0b11)<<6|(unholy&0b11)<<4|(frost&0b11)<<2|blood&0b11) } func (rc RuneCost) String() string { @@ -45,71 +17,31 @@ func (rc RuneCost) String() string { } // HasRune returns if this cost includes a rune portion. -// -// If any bit is set in the rune bits it means that there is a rune cost. func (rc RuneCost) HasRune() bool { - const runebits = int16(0b11111111) - return runebits&int16(rc) > 0 + return rc&0b1111_1111 > 0 } -func (rc RuneCost) RunicPower() uint8 { - const rpbits = uint16(0b1111111100000000) - return uint8((uint16(rc) & rpbits) >> 8) +func (rc RuneCost) RunicPower() int8 { + return int8(rc >> 8) } -func (rc RuneCost) Blood() uint8 { - runes := uint16(rc) & 0b11 - switch runes { - case 0b00: - return 0 - case 0b01: - return 1 - case 0b11: - return 2 - } - return 0 +func (rc RuneCost) Blood() int8 { + return int8(rc & 0b11) } -func (rc RuneCost) Frost() uint8 { - runes := uint16(rc) & 0b1100 - switch runes { - case 0: - return 0 - case 0b0100: - return 1 - case 0b1100: - return 2 - } - return 0 +func (rc RuneCost) Frost() int8 { + return int8((rc >> 2) & 0b11) } -func (rc RuneCost) Unholy() uint8 { - runes := uint16(rc) & 0b110000 - switch runes { - case 0: - return 0 - case 0b010000: - return 1 - case 0b110000: - return 2 - } - return 0 +func (rc RuneCost) Unholy() int8 { + return int8((rc >> 4) & 0b11) } -func (rc RuneCost) Death() uint8 { - runes := uint16(rc) & 0b11000000 - switch runes { - case 0: - return 0 - case 0b01000000: - return 1 - case 0b11000000: - return 2 - } - return 0 +func (rc RuneCost) Death() int8 { + return int8((rc >> 6) & 0b11) } -func (rp *RunicPowerBar) GainDeathRuneMetrics(sim *Simulation, spell *Spell, currRunes int32, newRunes int32) { +func (rp *RunicPowerBar) GainDeathRuneMetrics(sim *Simulation, _ *Spell, currRunes int32, newRunes int32) { if !rp.isACopy { metrics := rp.deathRuneGainMetrics metrics.AddEvent(1, float64(newRunes)-float64(currRunes)) @@ -130,10 +62,9 @@ func (rp *RunicPowerBar) CancelBloodTap(sim *Simulation) { rp.btslot = -1 } -func (rp *RunicPowerBar) CorrectBloodTapConversion(sim *Simulation, bloodGainMetrics *ResourceMetrics, deathGainMetrics *ResourceMetrics, spell *Spell) { +func (rp *RunicPowerBar) CorrectBloodTapConversion(sim *Simulation) { // 1. converts a blood rune -> death rune // 2. then convert one inactive blood or death rune -> active - slot := int8(-1) if rp.runeStates&isDeaths[0] == 0 { slot = 0 @@ -152,7 +83,7 @@ func (rp *RunicPowerBar) CorrectBloodTapConversion(sim *Simulation, bloodGainMet slot = 1 } if slot > -1 { - rp.RegenRune(sim, sim.CurrentTime, slot) + rp.regenRune(sim, sim.CurrentTime, slot) } // if PA isn't running, make it run 20s from now to disable BT diff --git a/sim/core/sim.go b/sim/core/sim.go index 4a46cb75da..ef87ea7c9d 100644 --- a/sim/core/sim.go +++ b/sim/core/sim.go @@ -2,6 +2,7 @@ package core import ( "fmt" + "log" "math" "math/rand" "runtime" @@ -230,6 +231,8 @@ func (sim *Simulation) Init() { // Run runs the simulation for the configured number of iterations, and // collects all the metrics together. func (sim *Simulation) run() *proto.RaidSimResult { + t0 := time.Now() + logsBuffer := &strings.Builder{} if sim.Options.Debug || sim.Options.DebugFirstIteration { sim.Log = func(message string, vals ...interface{}) { @@ -290,6 +293,10 @@ func (sim *Simulation) run() *proto.RaidSimResult { sim.ProgressReport(&proto.ProgressMetrics{TotalIterations: sim.Options.Iterations, CompletedIterations: sim.Options.Iterations, Dps: result.RaidMetrics.Dps.Avg, FinalRaidResult: result}) } + if d := sim.Options.Iterations; d > 3000 { + log.Printf("running %d iterations took %s", d, time.Since(t0)) + } + return result } diff --git a/sim/deathknight/blood_tap.go b/sim/deathknight/blood_tap.go index 15e39f0825..66cbe9d040 100644 --- a/sim/deathknight/blood_tap.go +++ b/sim/deathknight/blood_tap.go @@ -17,10 +17,7 @@ func (dk *Deathknight) registerBloodTapSpell() { ActionID: actionID, Duration: time.Second * 20, OnGain: func(aura *core.Aura, sim *core.Simulation) { - dk.CorrectBloodTapConversion(sim, - dk.BloodRuneGainMetrics(), - dk.DeathRuneGainMetrics(), - dk.BloodTap) + dk.CorrectBloodTapConversion(sim) // Gain at the end, to take into account previous effects for callback amountOfRunicPower := 10.0 diff --git a/sim/deathknight/dps/rotation_blood_helper.go b/sim/deathknight/dps/rotation_blood_helper.go index ee8d76e77a..f4ca579f95 100644 --- a/sim/deathknight/dps/rotation_blood_helper.go +++ b/sim/deathknight/dps/rotation_blood_helper.go @@ -20,7 +20,7 @@ type BloodRotation struct { dsGlyphed bool } -func (br *BloodRotation) Reset(sim *core.Simulation) { +func (br *BloodRotation) Reset(_ *core.Simulation) { br.activatingDrw = false if br.drwSnapshot != nil { br.drwSnapshot.ResetProcTrackers() @@ -114,7 +114,7 @@ func (dk *DpsDeathknight) blDiseaseCheck(sim *core.Simulation, target *core.Unit return true } -func (dk *DpsDeathknight) blSpreadDiseases(sim *core.Simulation, target *core.Unit, s *deathknight.Sequence) time.Duration { +func (dk *DpsDeathknight) blSpreadDiseases(sim *core.Simulation, target *core.Unit, _ *deathknight.Sequence) time.Duration { if dk.blDiseaseCheck(sim, target, dk.Pestilence, true, 1) { casted := dk.Pestilence.Cast(sim, target) landed := dk.LastOutcome.Matches(core.OutcomeLanded) @@ -133,7 +133,7 @@ func (dk *DpsDeathknight) blSpreadDiseases(sim *core.Simulation, target *core.Un func (dk *DpsDeathknight) blDeathCoilCheck(sim *core.Simulation) bool { canCastDrw := dk.Talents.DancingRuneWeapon && dk.DancingRuneWeapon != nil && (dk.DancingRuneWeapon.IsReady(sim) || dk.DancingRuneWeapon.CD.TimeToReady(sim) < 5*time.Second) currentRP := dk.CurrentRunicPower() - willCastDS := dk.NormalCurrentFrostRunes() > 0 && dk.NormalCurrentUnholyRunes() > 1 && dk.CurrentBloodRunes() == 0 + willCastDS := dk.CurrentFrostRunes() > 0 && dk.CurrentUnholyRunes() > 1 && dk.CurrentBloodRunes() == 0 return (!canCastDrw && currentRP >= float64(core.TernaryInt(dk.br.dsGlyphed && willCastDS, 65, 40))) || (canCastDrw && currentRP >= 100) } @@ -178,7 +178,7 @@ func (dk *DpsDeathknight) blDrwCanCast(sim *core.Simulation, castTime time.Durat if !dk.DancingRuneWeapon.IsReady(sim) { return false } - if !dk.CastCostPossible(sim, 60.0, 0, 0, 0) { + if dk.CurrentRunicPower() < 60 { return false } // Setup max delay possible diff --git a/sim/deathknight/dps/rotation_frost_sub_blood.go b/sim/deathknight/dps/rotation_frost_sub_blood.go index aefb0a2830..797cb5b4b1 100644 --- a/sim/deathknight/dps/rotation_frost_sub_blood.go +++ b/sim/deathknight/dps/rotation_frost_sub_blood.go @@ -154,8 +154,8 @@ func (dk *DpsDeathknight) RotationActionCallback_FrostSubBlood_FS_Dump(sim *core casted = dk.RotationActionCallback_LastSecondsCast(sim, target) if !casted { - fr := dk.NormalCurrentFrostRunes() - uh := dk.NormalCurrentUnholyRunes() + fr := dk.CurrentFrostRunes() + uh := dk.CurrentUnholyRunes() bAt := dk.BloodRuneReadyAt(sim) frAt := dk.NormalFrostRuneReadyAt(sim) uhAt := dk.NormalUnholyRuneReadyAt(sim) @@ -252,7 +252,7 @@ func (dk *DpsDeathknight) frCheckForUATime(sim *core.Simulation) FrostSubBloodUA } } -func (dk *DpsDeathknight) RotationActionCallback_FrostSubBlood_UA_Check(sim *core.Simulation, target *core.Unit, s *deathknight.Sequence) time.Duration { +func (dk *DpsDeathknight) RotationActionCallback_FrostSubBlood_UA_Check(sim *core.Simulation, _ *core.Unit, s *deathknight.Sequence) time.Duration { uaState := dk.frCheckForUATime(sim) if uaState == FrostSubBloodUAState_CD { @@ -269,7 +269,7 @@ func (dk *DpsDeathknight) RotationActionCallback_FrostSubBlood_UA_Check(sim *cor return sim.CurrentTime } -func (dk *DpsDeathknight) RotationActionCallback_FrostSubBlood_Obli_Check(sim *core.Simulation, target *core.Unit, s *deathknight.Sequence) time.Duration { +func (dk *DpsDeathknight) RotationActionCallback_FrostSubBlood_Obli_Check(sim *core.Simulation, _ *core.Unit, s *deathknight.Sequence) time.Duration { if dk.BloodTap.IsReady(sim) { s.Clear(). NewAction(dk.RotationActionCallback_FrostSubBlood_Dump_UntilBR). @@ -288,7 +288,7 @@ func (dk *DpsDeathknight) RotationActionCallback_FrostSubBlood_Obli_Check(sim *c return sim.CurrentTime } -func (dk *DpsDeathknight) RotationActionCallback_FrostSubBlood_SequenceRotation(sim *core.Simulation, target *core.Unit, s *deathknight.Sequence) time.Duration { +func (dk *DpsDeathknight) RotationActionCallback_FrostSubBlood_SequenceRotation(sim *core.Simulation, _ *core.Unit, s *deathknight.Sequence) time.Duration { s.Clear(). NewAction(dk.RotationActionCallback_FrostSubBlood_FS_Dump) @@ -351,7 +351,7 @@ func (dk *DpsDeathknight) setupFrostSubBloodNoERWOpener() { NewAction(dk.RotationActionCallback_FrostSubBlood_SequenceRotation) } -func (dk *DpsDeathknight) RotationActionCallback_FrostSubBlood_RecoverFromPestiMiss(sim *core.Simulation, target *core.Unit, s *deathknight.Sequence) time.Duration { +func (dk *DpsDeathknight) RotationActionCallback_FrostSubBlood_RecoverFromPestiMiss(sim *core.Simulation, _ *core.Unit, s *deathknight.Sequence) time.Duration { if dk.LastCast == dk.fr.bloodSpell { s.Clear(). NewAction(dk.RotationActionCallback_BS_Frost). diff --git a/sim/deathknight/dps/rotation_frost_sub_unholy.go b/sim/deathknight/dps/rotation_frost_sub_unholy.go index 3252a01378..19354d1ca6 100644 --- a/sim/deathknight/dps/rotation_frost_sub_unholy.go +++ b/sim/deathknight/dps/rotation_frost_sub_unholy.go @@ -119,7 +119,7 @@ func (dk *DpsDeathknight) RotationActionCallback_FrostSubUnholy_Dump_Until_Death return -1 } -func (dk *DpsDeathknight) RotationActionCallback_FrostSubUnholy_UA_Check1(sim *core.Simulation, target *core.Unit, s *deathknight.Sequence) time.Duration { +func (dk *DpsDeathknight) RotationActionCallback_FrostSubUnholy_UA_Check1(sim *core.Simulation, _ *core.Unit, s *deathknight.Sequence) time.Duration { if dk.UnbreakableArmor.CanCast(sim, nil) && dk.BloodTap.CanCast(sim, nil) { s.Clear(). NewAction(dk.RotationActionCallback_FrostSubUnholy_Dump_Until_Deaths). @@ -133,7 +133,7 @@ func (dk *DpsDeathknight) RotationActionCallback_FrostSubUnholy_UA_Check1(sim *c return sim.CurrentTime } -func (dk *DpsDeathknight) RotationActionCallback_FrostSubUnholy_UA_Check2(sim *core.Simulation, target *core.Unit, s *deathknight.Sequence) time.Duration { +func (dk *DpsDeathknight) RotationActionCallback_FrostSubUnholy_UA_Check2(sim *core.Simulation, _ *core.Unit, s *deathknight.Sequence) time.Duration { if dk.UnbreakableArmor.CanCast(sim, nil) && dk.BloodTap.CanCast(sim, nil) { s.Clear(). NewAction(dk.RotationActionCallback_UA_Frost). @@ -145,7 +145,7 @@ func (dk *DpsDeathknight) RotationActionCallback_FrostSubUnholy_UA_Check2(sim *c return sim.CurrentTime } -func (dk *DpsDeathknight) RotationActionCallback_FrostSubUnholy_UA_Check3(sim *core.Simulation, target *core.Unit, s *deathknight.Sequence) time.Duration { +func (dk *DpsDeathknight) RotationActionCallback_FrostSubUnholy_UA_Check3(sim *core.Simulation, _ *core.Unit, s *deathknight.Sequence) time.Duration { if (dk.UnbreakableArmor.TimeToReady(sim) < 2500*time.Millisecond+sim.CurrentTime) && (dk.BloodTap.TimeToReady(sim) < 2500*time.Millisecond+sim.CurrentTime) { s.Clear(). NewAction(dk.RotationActionCallback_BT). @@ -158,7 +158,7 @@ func (dk *DpsDeathknight) RotationActionCallback_FrostSubUnholy_UA_Check3(sim *c return sim.CurrentTime } -func (dk *DpsDeathknight) RotationActionCallback_FrostSubUnholy_Sequence1(sim *core.Simulation, target *core.Unit, s *deathknight.Sequence) time.Duration { +func (dk *DpsDeathknight) RotationActionCallback_FrostSubUnholy_Sequence1(sim *core.Simulation, _ *core.Unit, s *deathknight.Sequence) time.Duration { s.Clear(). NewAction(dk.RotationActionCallback_EndOfFightCheck). NewAction(dk.RotationActionCallback_FrostSubUnholy_FS_Dump). @@ -190,7 +190,7 @@ func (dk *DpsDeathknight) RotationActionCallback_FrostSubUnholy_Pesti(sim *core. return -1 } -func (dk *DpsDeathknight) RotationActionCallback_FrostSubUnholy_Sequence2(sim *core.Simulation, target *core.Unit, s *deathknight.Sequence) time.Duration { +func (dk *DpsDeathknight) RotationActionCallback_FrostSubUnholy_Sequence2(sim *core.Simulation, _ *core.Unit, s *deathknight.Sequence) time.Duration { s.Clear(). NewAction(dk.RotationAction_CancelBT). NewAction(dk.RotationActionCallback_EndOfFightCheck). @@ -206,7 +206,7 @@ func (dk *DpsDeathknight) RotationActionCallback_FrostSubUnholy_Sequence2(sim *c return sim.CurrentTime } -func (dk *DpsDeathknight) RotationActionCallback_FrostSubUnholy_RecoverFromPestiMiss(sim *core.Simulation, target *core.Unit, s *deathknight.Sequence) time.Duration { +func (dk *DpsDeathknight) RotationActionCallback_FrostSubUnholy_RecoverFromPestiMiss(sim *core.Simulation, _ *core.Unit, s *deathknight.Sequence) time.Duration { if dk.LastCast == dk.BloodStrike { s.Clear(). NewAction(dk.RotationActionCallback_BS). @@ -239,8 +239,8 @@ func (dk *DpsDeathknight) RotationActionCallback_FrostSubUnholy_FS_Dump(sim *cor casted := false waitUntil := time.Duration(-1) - fr := dk.NormalCurrentFrostRunes() - uh := dk.NormalCurrentUnholyRunes() + fr := dk.CurrentFrostRunes() + uh := dk.CurrentUnholyRunes() if fr > 0 && uh > 0 { s.Advance() diff --git a/sim/deathknight/dps/rotation_unholy_helper.go b/sim/deathknight/dps/rotation_unholy_helper.go index 4ed65a8d7a..d22f84fec5 100644 --- a/sim/deathknight/dps/rotation_unholy_helper.go +++ b/sim/deathknight/dps/rotation_unholy_helper.go @@ -55,7 +55,7 @@ type UnholyRotation struct { berserkingOh *core.Aura } -func (ur *UnholyRotation) Reset(sim *core.Simulation) { +func (ur *UnholyRotation) Reset(_ *core.Simulation) { ur.syncTimeFF = 0 ur.activatingGargoyle = false ur.gargoyleMaxDelay = -1 @@ -233,12 +233,12 @@ func (dk *DpsDeathknight) uhDiseaseCheck(sim *core.Simulation, target *core.Unit return dk.shDiseaseCheck(sim, target, spell, costRunes, casts, 0) } -func (dk *DpsDeathknight) uhSpreadDiseases(sim *core.Simulation, target *core.Unit, s *deathknight.Sequence) bool { +func (dk *DpsDeathknight) uhSpreadDiseases(sim *core.Simulation, target *core.Unit, _ *deathknight.Sequence) bool { if dk.uhDiseaseCheck(sim, target, dk.Pestilence, true, 1) { casted := dk.Pestilence.Cast(sim, target) landed := dk.LastOutcome.Matches(core.OutcomeLanded) - // Reset flags on succesfull cast + // Reset flags on successful cast dk.sr.recastedFF = !(casted && landed) dk.sr.recastedBP = !(casted && landed) return casted @@ -333,7 +333,7 @@ func (dk *DpsDeathknight) uhEmpoweredRuneWeapon(sim *core.Simulation, target *co return false } - if dk.CurrentBloodRunes() > 0 || dk.CurrentFrostRunes() > 0 || dk.CurrentUnholyRunes() > 0 || dk.CurrentDeathRunes() > 0 { + if !dk.AllRunesSpent() { return false } @@ -426,7 +426,7 @@ func (dk *DpsDeathknight) uhGargoyleCanCast(sim *core.Simulation, castTime time. if sim.CurrentTime < dk.ur.gargoyleMinTime { return false } - if !dk.CastCostPossible(sim, 60.0, 0, 0, 0) { + if dk.CurrentRunicPower() < 60 { return false } // Setup max delay possible diff --git a/sim/deathknight/items.go b/sim/deathknight/items.go index 67f5357b3f..bdfc3c983b 100644 --- a/sim/deathknight/items.go +++ b/sim/deathknight/items.go @@ -221,7 +221,7 @@ func (dk *Deathknight) registerScourgelordsBattlegearProc() { }) dk.onRuneSpendT10 = func(sim *core.Simulation) { - if dk.CurrentBloodRunes() == 0 && dk.CurrentFrostRunes() == 0 && dk.CurrentUnholyRunes() == 0 && dk.CurrentDeathRunes() == 0 { + if dk.AllRunesSpent() { damageAura.Activate(sim) } } diff --git a/sim/deathknight/tank/rotation_shared.go b/sim/deathknight/tank/rotation_shared.go index 28ed5488a2..0dd5778384 100644 --- a/sim/deathknight/tank/rotation_shared.go +++ b/sim/deathknight/tank/rotation_shared.go @@ -7,7 +7,7 @@ import ( "github.com/wowsims/wotlk/sim/deathknight" ) -func (dk *TankDeathknight) DoDiseaseChecks(sim *core.Simulation, target *core.Unit, s *deathknight.Sequence) bool { +func (dk *TankDeathknight) DoDiseaseChecks(sim *core.Simulation, target *core.Unit, _ *deathknight.Sequence) bool { t := sim.CurrentTime recast := 3 * time.Second // 2 GCDs for miss ff := dk.FrostFeverSpell.Dot(target).ExpiresAt() - t @@ -31,7 +31,7 @@ func (dk *TankDeathknight) DoDiseaseChecks(sim *core.Simulation, target *core.Un return false } -func (dk *TankDeathknight) DoFrostCast(sim *core.Simulation, target *core.Unit, s *deathknight.Sequence) bool { +func (dk *TankDeathknight) DoFrostCast(sim *core.Simulation, target *core.Unit, _ *deathknight.Sequence) bool { if dk.Talents.FrostStrike && dk.FrostStrike.CanCast(sim, target) { dk.FrostStrike.Cast(sim, target) return true @@ -45,12 +45,12 @@ func (dk *TankDeathknight) DoFrostCast(sim *core.Simulation, target *core.Unit, return false } -func (dk *TankDeathknight) DoBloodCast(sim *core.Simulation, target *core.Unit, s *deathknight.Sequence) bool { +func (dk *TankDeathknight) DoBloodCast(sim *core.Simulation, target *core.Unit, _ *deathknight.Sequence) bool { t := sim.CurrentTime recast := 3 * time.Second // 2 GCDs for miss ff := dk.FrostFeverSpell.Dot(target).ExpiresAt() - t bp := dk.BloodPlagueSpell.Dot(target).ExpiresAt() - t - b, _, _ := dk.NormalCurrentRunes() + b := dk.CurrentBloodRunes() if b >= 1 { if dk.NormalSpentBloodRuneReadyAt(sim)-t < ff-recast && dk.NormalSpentBloodRuneReadyAt(sim)-t < bp-recast { From 28a4add82719d8e14ef78975ba8ccbd42d7fc774 Mon Sep 17 00:00:00 2001 From: vigo Date: Fri, 22 Sep 2023 10:48:46 +0200 Subject: [PATCH 2/2] [dk] fixed NextBloodRuneReadyAt() and added CurrentXXXOrDeathRunes() for APL use. --- sim/core/apl_values_runes.go | 6 ++--- sim/core/runic_power.go | 45 +++++++++++++++++++++++++----------- 2 files changed, 34 insertions(+), 17 deletions(-) diff --git a/sim/core/apl_values_runes.go b/sim/core/apl_values_runes.go index 15b1542868..4e50a0addb 100644 --- a/sim/core/apl_values_runes.go +++ b/sim/core/apl_values_runes.go @@ -30,11 +30,11 @@ func (value *APLValueCurrentRuneCount) Type() proto.APLValueType { func (value *APLValueCurrentRuneCount) GetInt(_ *Simulation) int32 { switch value.runeType { case proto.APLValueRuneType_RuneBlood: - return int32(value.unit.CurrentBloodRunes()) // TODO these only count non-death runes of the specified kind + return int32(value.unit.CurrentBloodOrDeathRunes()) case proto.APLValueRuneType_RuneFrost: - return int32(value.unit.CurrentFrostRunes()) + return int32(value.unit.CurrentFrostOrDeathRunes()) case proto.APLValueRuneType_RuneUnholy: - return int32(value.unit.CurrentUnholyRunes()) + return int32(value.unit.CurrentUnholyOrDeathRunes()) case proto.APLValueRuneType_RuneDeath: return int32(value.unit.CurrentDeathRunes()) } diff --git a/sim/core/runic_power.go b/sim/core/runic_power.go index cfa16435b0..713543feeb 100644 --- a/sim/core/runic_power.go +++ b/sim/core/runic_power.go @@ -364,29 +364,29 @@ func (rp *RunicPowerBar) UnholyRuneReadyAt(sim *Simulation) time.Duration { return MinDuration(rp.runeMeta[4].regenAt, rp.runeMeta[5].regenAt) } -func (rp *RunicPowerBar) NextRuneTypeReadyAt(sim *Simulation, left int8, right int8) time.Duration { - if rp.runeStates&isSpents[left] != isSpents[left] && rp.runeStates&isSpents[right] != isSpents[right] { - // Both are ready so return current time +func (rp *RunicPowerBar) bothRunesReadyAt(sim *Simulation, slot int8) time.Duration { + switch (rp.runeStates >> (2 * slot)) & 0b0101 { + case 0b0000: return sim.CurrentTime - } else if rp.runeStates&isSpents[left] == isSpents[left] || rp.runeStates&isSpents[right] == isSpents[right] { - // One is spent so return the time it will regen at - return MinDuration(rp.runeMeta[left].regenAt, rp.runeMeta[right].regenAt) + case 0b0001: + return rp.runeMeta[slot].regenAt + case 0b0100: + return rp.runeMeta[slot+1].regenAt + default: + return MaxDuration(rp.runeMeta[slot].regenAt, rp.runeMeta[slot+1].regenAt) } - // Both are spent so return the last one to regen at - return MaxDuration(rp.runeMeta[left].regenAt, rp.runeMeta[right].regenAt) } -// TODO Can possibly replaced by either BloodRuneReadyAt() or SpentBloodRuneReadyAt() variants, depending on semantics. func (rp *RunicPowerBar) NextBloodRuneReadyAt(sim *Simulation) time.Duration { - return rp.NextRuneTypeReadyAt(sim, 0, 1) + return rp.bothRunesReadyAt(sim, 0) } func (rp *RunicPowerBar) NextFrostRuneReadyAt(sim *Simulation) time.Duration { - return rp.NextRuneTypeReadyAt(sim, 2, 3) + return rp.bothRunesReadyAt(sim, 2) } func (rp *RunicPowerBar) NextUnholyRuneReadyAt(sim *Simulation) time.Duration { - return rp.NextRuneTypeReadyAt(sim, 4, 5) + return rp.bothRunesReadyAt(sim, 4) } // AnySpentRuneReadyAt returns the next time that a rune will regenerate. @@ -477,7 +477,7 @@ var rs2c = []int8{ } func (rp *RunicPowerBar) CurrentBloodRunes() int8 { - return rs2c[rp.runeStates&0b1111] + return rs2c[(rp.runeStates>>0)&0b1111] } func (rp *RunicPowerBar) CurrentFrostRunes() int8 { @@ -495,13 +495,30 @@ var rs2d = []int8{ } func (rp *RunicPowerBar) CurrentDeathRunes() int8 { - return rs2d[rp.runeStates&0b1111] + rs2d[(rp.runeStates>>4)&0b1111] + rs2d[(rp.runeStates>>8)&0b1111] + return rs2d[(rp.runeStates>>0)&0b1111] + rs2d[(rp.runeStates>>4)&0b1111] + rs2d[(rp.runeStates>>8)&0b1111] } func (rp *RunicPowerBar) DeathRunesInFU() int8 { return rs2d[(rp.runeStates>>4)&0b1111] + rs2d[(rp.runeStates>>8)&0b1111] } +// rune state to count of non-spent runes (0bx0), masking death runes +var rs2cd = [16]int8{ + 0b0000: 2, 0b0001: 1, 0b0100: 1, +} + +func (rp *RunicPowerBar) CurrentBloodOrDeathRunes() int8 { + return rs2cd[(rp.runeStates>>0)&0b0101] +} + +func (rp *RunicPowerBar) CurrentFrostOrDeathRunes() int8 { + return rs2cd[(rp.runeStates>>4)&0b0101] +} + +func (rp *RunicPowerBar) CurrentUnholyOrDeathRunes() int8 { + return rs2cd[(rp.runeStates>>8)&0b0101] +} + func (rp *RunicPowerBar) AllRunesSpent() bool { return rp.runeStates&allSpent == allSpent }