Skip to content

Commit

Permalink
Merge pull request #3702 from wowsims/apl
Browse files Browse the repository at this point in the history
Implement APL Channel action
  • Loading branch information
jimmyt857 committed Sep 19, 2023
2 parents e4268b7 + 0107c49 commit 66f987a
Show file tree
Hide file tree
Showing 22 changed files with 1,238 additions and 972 deletions.
1 change: 1 addition & 0 deletions proto/api.proto
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,7 @@ message SpellStats {
ActionID id = 1;

bool is_castable = 2; // Whether this spell may be cast by the APL logic.
bool is_channeled = 7; // Whether this spell is a channeled spell (Mind Flay, Drain Soul, etc).
bool is_major_cooldown = 3; // Whether this spell is a major cooldown.
bool has_dot = 4; // Whether this spell applies a DoT effect.
bool has_shield = 6; // Whether this spell applies a shield effect.
Expand Down
20 changes: 18 additions & 2 deletions proto/apl.proto
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,14 @@ message APLListItem {
APLAction action = 3; // The action to be performed.
}

// NextIndex: 16
// NextIndex: 17
message APLAction {
APLValue condition = 1; // If set, action will only execute if value is true or != 0.

oneof action {
// Casting
APLActionCastSpell cast_spell = 3;
APLActionChannelSpell channel_spell = 16;
APLActionMultidot multidot = 8;
APLActionMultishield multishield = 12;
APLActionAutocastOtherCooldowns autocast_other_cooldowns = 7;
Expand All @@ -75,7 +76,7 @@ message APLAction {
}
}

// NextIndex: 56
// NextIndex: 58
message APLValue {
oneof value {
// Operators
Expand Down Expand Up @@ -132,6 +133,8 @@ message APLValue {
APLValueSpellChannelTime spell_channel_time = 36;
APLValueSpellTravelTime spell_travel_time = 37;
APLValueSpellCPM spell_cpm = 42;
APLValueSpellIsChanneling spell_is_channeling = 56;
APLValueSpellChanneledTicks spell_channeled_ticks = 57;

// Aura values
APLValueAuraIsActive aura_is_active = 22;
Expand Down Expand Up @@ -166,6 +169,13 @@ message APLActionCastSpell {
UnitReference target = 2;
}

message APLActionChannelSpell {
ActionID spell_id = 1;
UnitReference target = 2;

APLValue interrupt_if = 3;
}

message APLActionMultidot {
ActionID spell_id = 1;
int32 max_dots = 2;
Expand Down Expand Up @@ -390,6 +400,12 @@ message APLValueSpellTravelTime {
message APLValueSpellCPM {
ActionID spell_id = 1;
}
message APLValueSpellIsChanneling {
ActionID spell_id = 1;
}
message APLValueSpellChanneledTicks {
ActionID spell_id = 1;
}

message APLValueAuraIsActive {
UnitReference source_unit = 2;
Expand Down
34 changes: 34 additions & 0 deletions sim/core/apl.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ type APLRotation struct {
// Action currently controlling this rotation (only used for certain actions, such as StrictSequence).
controllingAction APLActionImpl

// Value that should evaluate to 'true' if the current channel is to be interrupted.
// Will be nil when there is no active channel.
interruptChannelIf APLValue

// Used inside of actions/value to determine whether they will occur during the prepull or regular rotation.
parsingPrepull bool

Expand Down Expand Up @@ -159,6 +163,36 @@ func (apl *APLRotation) DoNextAction(sim *Simulation) {
}

i := 0

channeledDot := apl.unit.ChanneledDot
if channeledDot != nil {
if channeledDot.MaxTicksRemaining() == 0 {
// Channel has ended, but apl.unit.ChanneledDot hasn't been cleared yet meaning the aura is still active.
return
}
if apl.unit.ChanneledDot.lastTickTime != sim.CurrentTime {
// Don't allow interupts between ticks, just continue channeling until next tick.
return
}
if !apl.unit.GCD.IsReady(sim) || apl.interruptChannelIf == nil || !apl.interruptChannelIf.GetBool(sim) {
// Continue the channel.
return
}

// Allow next action to interrupt the channel, but if the action is the same action then it still needs to continue.
nextAction := apl.getNextAction(sim)
if nextAction == nil {
return
}
if channelAction, ok := nextAction.impl.(*APLActionChannelSpell); ok && channelAction.spell == channeledDot.Spell {
// Newly selected action is channeling the same spell, so continue the channel.
return
}
channeledDot.Cancel(sim)
nextAction.Execute(sim)
i++
}

apl.inLoop = true
for nextAction := apl.getNextAction(sim); nextAction != nil; i, nextAction = i+1, apl.getNextAction(sim) {
if i > 1000 {
Expand Down
2 changes: 2 additions & 0 deletions sim/core/apl_action.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ func (rot *APLRotation) newAPLActionImpl(config *proto.APLAction) APLActionImpl
// Casting
case *proto.APLAction_CastSpell:
return rot.newActionCastSpell(config.GetCastSpell())
case *proto.APLAction_ChannelSpell:
return rot.newActionChannelSpell(config.GetChannelSpell())
case *proto.APLAction_Multidot:
return rot.newActionMultidot(config.GetMultidot())
case *proto.APLAction_Multishield:
Expand Down
46 changes: 46 additions & 0 deletions sim/core/apl_actions_casting.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,52 @@ func (action *APLActionCastSpell) String() string {
return fmt.Sprintf("Cast Spell(%s)", action.spell.ActionID)
}

type APLActionChannelSpell struct {
defaultAPLActionImpl
spell *Spell
target UnitReference
interruptIf APLValue
}

func (rot *APLRotation) newActionChannelSpell(config *proto.APLActionChannelSpell) APLActionImpl {
interruptIf := rot.coerceTo(rot.newAPLValue(config.InterruptIf), proto.APLValueType_ValueTypeBool)
if interruptIf == nil {
return rot.newActionCastSpell(&proto.APLActionCastSpell{
SpellId: config.SpellId,
Target: config.Target,
})
}

spell := rot.GetAPLSpell(config.SpellId)
if spell == nil {
return nil
}
if !spell.Flags.Matches(SpellFlagChanneled) {
return nil
}

target := rot.GetTargetUnit(config.Target)
if target.Get() == nil {
return nil
}

return &APLActionChannelSpell{
spell: spell,
target: target,
interruptIf: interruptIf,
}
}
func (action *APLActionChannelSpell) IsReady(sim *Simulation) bool {
return action.spell.CanCast(sim, action.target.Get())
}
func (action *APLActionChannelSpell) Execute(sim *Simulation) {
action.spell.Cast(sim, action.target.Get())
action.spell.Unit.Rotation.interruptChannelIf = action.interruptIf
}
func (action *APLActionChannelSpell) String() string {
return fmt.Sprintf("Channel Spell(%s, interruptIf=%s)", action.spell.ActionID, action.interruptIf)
}

type APLActionMultidot struct {
defaultAPLActionImpl
spell *Spell
Expand Down
4 changes: 4 additions & 0 deletions sim/core/apl_value.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,10 @@ func (rot *APLRotation) newAPLValue(config *proto.APLValue) APLValue {
return rot.newValueSpellTravelTime(config.GetSpellTravelTime())
case *proto.APLValue_SpellCpm:
return rot.newValueSpellCPM(config.GetSpellCpm())
case *proto.APLValue_SpellIsChanneling:
return rot.newValueSpellIsChanneling(config.GetSpellIsChanneling())
case *proto.APLValue_SpellChanneledTicks:
return rot.newValueSpellChanneledTicks(config.GetSpellChanneledTicks())

// Auras
case *proto.APLValue_AuraIsActive:
Expand Down
53 changes: 53 additions & 0 deletions sim/core/apl_values_spell.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,3 +174,56 @@ func (value *APLValueSpellCPM) GetFloat(sim *Simulation) float64 {
func (value *APLValueSpellCPM) String() string {
return fmt.Sprintf("CPM(%s)", value.spell.ActionID)
}

type APLValueSpellIsChanneling struct {
DefaultAPLValueImpl
spell *Spell
}

func (rot *APLRotation) newValueSpellIsChanneling(config *proto.APLValueSpellIsChanneling) APLValue {
spell := rot.GetAPLSpell(config.SpellId)
if spell == nil {
return nil
}
return &APLValueSpellIsChanneling{
spell: spell,
}
}
func (value *APLValueSpellIsChanneling) Type() proto.APLValueType {
return proto.APLValueType_ValueTypeBool
}
func (value *APLValueSpellIsChanneling) GetBool(sim *Simulation) bool {
return value.spell.Unit.ChanneledDot != nil && value.spell.Unit.ChanneledDot.Spell == value.spell
}
func (value *APLValueSpellIsChanneling) String() string {
return fmt.Sprintf("IsChanneling(%s)", value.spell.ActionID)
}

type APLValueSpellChanneledTicks struct {
DefaultAPLValueImpl
spell *Spell
}

func (rot *APLRotation) newValueSpellChanneledTicks(config *proto.APLValueSpellChanneledTicks) APLValue {
spell := rot.GetAPLSpell(config.SpellId)
if spell == nil {
return nil
}
return &APLValueSpellChanneledTicks{
spell: spell,
}
}
func (value *APLValueSpellChanneledTicks) Type() proto.APLValueType {
return proto.APLValueType_ValueTypeBool
}
func (value *APLValueSpellChanneledTicks) GetInt(sim *Simulation) int32 {
channeledDot := value.spell.Unit.ChanneledDot
if channeledDot == nil {
return 0
} else {
return channeledDot.TickCount
}
}
func (value *APLValueSpellChanneledTicks) String() string {
return fmt.Sprintf("ChanneledTicks(%s)", value.spell.ActionID)
}
34 changes: 32 additions & 2 deletions sim/core/dot.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,19 +53,28 @@ type Dot struct {
TickCount int32

lastTickTime time.Duration
isChanneled bool
}

// TickPeriod is how fast the snapshot dot ticks.
func (dot *Dot) TickPeriod() time.Duration {
return dot.tickPeriod
}

func (dot *Dot) NextTickAt() time.Duration {
return dot.lastTickTime + dot.tickPeriod
}

func (dot *Dot) TimeUntilNextTick(sim *Simulation) time.Duration {
return dot.lastTickTime + dot.tickPeriod - sim.CurrentTime
return dot.NextTickAt() - sim.CurrentTime
}

func (dot *Dot) MaxTicksRemaining() int32 {
return dot.NumberOfTicks - dot.TickCount
}

func (dot *Dot) NumTicksRemaining(sim *Simulation) int {
maxTicksRemaining := dot.NumberOfTicks - dot.TickCount
maxTicksRemaining := dot.MaxTicksRemaining()
finalTickAt := dot.lastTickTime + dot.tickPeriod*time.Duration(maxTicksRemaining)
return MaxInt(0, int((finalTickAt-sim.CurrentTime)/dot.tickPeriod)+1)
}
Expand Down Expand Up @@ -181,6 +190,16 @@ func (dot *Dot) TakeSnapshot(sim *Simulation, doRollover bool) {
func (dot *Dot) TickOnce(sim *Simulation) {
dot.lastTickTime = sim.CurrentTime
dot.OnTick(sim, dot.Unit, dot)

if dot.isChanneled && dot.Spell.Unit.IsUsingAPL {
if dot.MaxTicksRemaining() == 0 {
// If this was the last tick, wait 0ms to call the APL after the channel aura fully fades.
dot.Spell.Unit.WaitUntil(sim, sim.CurrentTime)
} else {
// Give the APL settings a chance to interrupt the channel.
dot.Spell.Unit.Rotation.DoNextAction(sim)
}
}
}

// ManualTick forces the dot forward one tick
Expand Down Expand Up @@ -231,12 +250,21 @@ func newDot(config Dot) *Dot {
periodicOptions.Period = dot.tickPeriod
dot.tickAction = NewPeriodicAction(sim, periodicOptions)
sim.AddPendingAction(dot.tickAction)
if dot.isChanneled {
dot.Spell.Unit.ChanneledDot = dot
}
})
dot.Aura.ApplyOnExpire(func(aura *Aura, sim *Simulation) {
if dot.tickAction != nil {
dot.tickAction.Cancel(sim)
dot.tickAction = nil
}
if dot.isChanneled {
dot.Spell.Unit.ChanneledDot = nil
if dot.Spell.Unit.IsUsingAPL {
dot.Spell.Unit.Rotation.interruptChannelIf = nil
}
}
})

return dot
Expand Down Expand Up @@ -265,6 +293,8 @@ func (spell *Spell) createDots(config DotConfig, isHot bool) {

OnSnapshot: config.OnSnapshot,
OnTick: config.OnTick,

isChanneled: config.Spell.Flags.Matches(SpellFlagChanneled),
}

auraConfig := config.Aura
Expand Down
2 changes: 1 addition & 1 deletion sim/core/gcd.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ func (unit *Unit) WaitUntil(sim *Simulation, readyTime time.Duration) {
}
unit.waitStartTime = sim.CurrentTime
unit.SetGCDTimer(sim, readyTime)
if sim.Log != nil {
if sim.Log != nil && readyTime > sim.CurrentTime {
unit.Log(sim, "Pausing GCD for %s due to rotation / CDs.", readyTime-sim.CurrentTime)
}
}
Expand Down
5 changes: 5 additions & 0 deletions sim/core/mana.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,11 @@ func (unit *Unit) UpdateManaRegenRates() {

// Applies 1 'tick' of mana regen, which worth 2s of regeneration based on mp5/int/spirit/etc.
func (unit *Unit) ManaTick(sim *Simulation) {
if unit.ChanneledDot != nil {
// Mana is not regenerated during channels
return
}

if sim.CurrentTime < unit.PseudoStats.FiveSecondRuleRefreshTime {
regen := unit.manaTickWhileCasting
unit.AddMana(sim, MaxFloat(0, regen), unit.manaCastingMetrics)
Expand Down
8 changes: 6 additions & 2 deletions sim/core/spell.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,10 @@ func (unit *Unit) RegisterSpell(config SpellConfig) *Spell {
config.DamageMultiplier = 1
}

if unit.IsUsingAPL {
config.Cast.DefaultCast.ChannelTime = 0
}

spell := &Spell{
ActionID: config.ActionID,
Unit: unit,
Expand Down Expand Up @@ -445,10 +449,10 @@ func (spell *Spell) CanCast(sim *Simulation, target *Unit) bool {
return false
}

// While casting or channeling, no other action is possible
// While casting, no other action is possible
if spell.Unit.Hardcast.Expires > sim.CurrentTime {
//if sim.Log != nil {
// sim.Log("Cant cast because already casting/channeling")
// sim.Log("Cant cast because already casting")
//}
return false
}
Expand Down
5 changes: 5 additions & 0 deletions sim/core/unit.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,9 @@ type Unit struct {
CurrentTarget *Unit
defaultTarget *Unit

// The currently-channeled DOT spell, otherwise nil.
ChanneledDot *Dot

DummyProcSpell *Spell
}

Expand Down Expand Up @@ -444,6 +447,7 @@ func (unit *Unit) reset(sim *Simulation, _ Agent) {
unit.enabled = true
unit.resetCDs(sim)
unit.Hardcast.Expires = startingCDTime
unit.ChanneledDot = nil
unit.Metrics.reset()
unit.ResetStatDeps()
unit.statsWithoutDeps = unit.initialStatsWithoutDeps
Expand Down Expand Up @@ -531,6 +535,7 @@ func (unit *Unit) GetMetadata() *proto.UnitMetadata {
Id: spell.ActionID.ToProto(),

IsCastable: spell.Flags.Matches(SpellFlagAPL),
IsChanneled: spell.Flags.Matches(SpellFlagChanneled),
IsMajorCooldown: spell.Flags.Matches(SpellFlagMCD),
HasDot: spell.dots != nil || spell.aoeDot != nil,
HasShield: spell.shields != nil || spell.selfShield != nil,
Expand Down
Loading

0 comments on commit 66f987a

Please sign in to comment.