From 3a31ca0fb46b075679e5c4b3a27e66fe1e9c3f35 Mon Sep 17 00:00:00 2001 From: James Tanner Date: Sat, 16 Sep 2023 14:02:38 -0700 Subject: [PATCH] Implement Scheduled Actions for APL --- proto/apl.proto | 72 +++++----- sim/core/apl_action.go | 29 ++-- sim/core/apl_actions_core.go | 60 +++++++++ .../individual_sim_ui/apl_actions.ts | 125 +++++++++++------- .../individual_sim_ui/apl_helpers.ts | 5 +- .../_apl_rotation_picker.scss | 5 +- 6 files changed, 205 insertions(+), 91 deletions(-) diff --git a/proto/apl.proto b/proto/apl.proto index 89d3b6c466..5242fb53bb 100644 --- a/proto/apl.proto +++ b/proto/apl.proto @@ -46,27 +46,32 @@ message APLListItem { APLAction action = 3; // The action to be performed. } -// NextIndex: 15 +// NextIndex: 16 message APLAction { APLValue condition = 1; // If set, action will only execute if value is true or != 0. oneof action { - APLActionSequence sequence = 2; - APLActionResetSequence reset_sequence = 5; - APLActionStrictSequence strict_sequence = 6; - + // Casting APLActionCastSpell cast_spell = 3; APLActionMultidot multidot = 8; APLActionMultishield multishield = 12; + APLActionAutocastOtherCooldowns autocast_other_cooldowns = 7; + + // Timing + APLActionWait wait = 4; + APLActionWaitUntil wait_until = 14; + APLActionSchedule schedule = 15; + + // Sequences + APLActionSequence sequence = 2; + APLActionResetSequence reset_sequence = 5; + APLActionStrictSequence strict_sequence = 6; // Misc - APLActionAutocastOtherCooldowns autocast_other_cooldowns = 7; APLActionChangeTarget change_target = 9; APLActionActivateAura activate_aura = 13; APLActionCancelAura cancel_aura = 10; APLActionTriggerICD trigger_icd = 11; - APLActionWait wait = 4; - APLActionWaitUntil wait_until = 14; } } @@ -153,20 +158,6 @@ message APLValue { // ACTIONS /////////////////////////////////////////////////////////////////////////// -message APLActionSequence { - string name = 1; - - repeated APLAction actions = 2; -} - -message APLActionResetSequence { - string sequence_name = 1; -} - -message APLActionStrictSequence { - repeated APLAction actions = 1; -} - message APLActionCastSpell { ActionID spell_id = 1; UnitReference target = 2; @@ -187,6 +178,35 @@ message APLActionMultishield { message APLActionAutocastOtherCooldowns { } +message APLActionWait { + APLValue duration = 1; +} + +message APLActionWaitUntil { + APLValue condition = 1; +} + +message APLActionSchedule { + // Comma-separated list of times, e.g. '0s, 30s, 60s' + string schedule = 1; + + APLAction inner_action = 2; +} + +message APLActionSequence { + string name = 1; + + repeated APLAction actions = 2; +} + +message APLActionResetSequence { + string sequence_name = 1; +} + +message APLActionStrictSequence { + repeated APLAction actions = 1; +} + message APLActionChangeTarget { UnitReference new_target = 1; } @@ -203,14 +223,6 @@ message APLActionTriggerICD { ActionID aura_id = 1; } -message APLActionWait { - APLValue duration = 1; -} - -message APLActionWaitUntil { - APLValue condition = 1; -} - /////////////////////////////////////////////////////////////////////////// // VALUES /////////////////////////////////////////////////////////////////////////// diff --git a/sim/core/apl_action.go b/sim/core/apl_action.go index 99220e26ac..d656a0b5a1 100644 --- a/sim/core/apl_action.go +++ b/sim/core/apl_action.go @@ -132,12 +132,7 @@ func (rot *APLRotation) newAPLActionImpl(config *proto.APLAction) APLActionImpl } switch config.Action.(type) { - case *proto.APLAction_Sequence: - return rot.newActionSequence(config.GetSequence()) - case *proto.APLAction_ResetSequence: - return rot.newActionResetSequence(config.GetResetSequence()) - case *proto.APLAction_StrictSequence: - return rot.newActionStrictSequence(config.GetStrictSequence()) + // Casting case *proto.APLAction_CastSpell: return rot.newActionCastSpell(config.GetCastSpell()) case *proto.APLAction_Multidot: @@ -146,6 +141,24 @@ func (rot *APLRotation) newAPLActionImpl(config *proto.APLAction) APLActionImpl return rot.newActionMultishield(config.GetMultishield()) case *proto.APLAction_AutocastOtherCooldowns: return rot.newActionAutocastOtherCooldowns(config.GetAutocastOtherCooldowns()) + + // Timing + case *proto.APLAction_Wait: + return rot.newActionWait(config.GetWait()) + case *proto.APLAction_WaitUntil: + return rot.newActionWaitUntil(config.GetWaitUntil()) + case *proto.APLAction_Schedule: + return rot.newActionSchedule(config.GetSchedule()) + + // Sequences + case *proto.APLAction_Sequence: + return rot.newActionSequence(config.GetSequence()) + case *proto.APLAction_ResetSequence: + return rot.newActionResetSequence(config.GetResetSequence()) + case *proto.APLAction_StrictSequence: + return rot.newActionStrictSequence(config.GetStrictSequence()) + + // Misc case *proto.APLAction_ChangeTarget: return rot.newActionChangeTarget(config.GetChangeTarget()) case *proto.APLAction_ActivateAura: @@ -154,10 +167,6 @@ func (rot *APLRotation) newAPLActionImpl(config *proto.APLAction) APLActionImpl return rot.newActionCancelAura(config.GetCancelAura()) case *proto.APLAction_TriggerIcd: return rot.newActionTriggerICD(config.GetTriggerIcd()) - case *proto.APLAction_Wait: - return rot.newActionWait(config.GetWait()) - case *proto.APLAction_WaitUntil: - return rot.newActionWaitUntil(config.GetWaitUntil()) default: return nil } diff --git a/sim/core/apl_actions_core.go b/sim/core/apl_actions_core.go index ca5cc2a92c..22e101d07d 100644 --- a/sim/core/apl_actions_core.go +++ b/sim/core/apl_actions_core.go @@ -2,6 +2,7 @@ package core import ( "fmt" + "strings" "time" "github.com/wowsims/wotlk/sim/core/proto" @@ -298,3 +299,62 @@ func (action *APLActionWaitUntil) GetNextAction(sim *Simulation) *APLAction { func (action *APLActionWaitUntil) String() string { return fmt.Sprintf("WaitUntil(%s)", action.condition) } + +type APLActionSchedule struct { + defaultAPLActionImpl + innerAction *APLAction + + timings []time.Duration + nextTimingIdx int +} + +func (rot *APLRotation) newActionSchedule(config *proto.APLActionSchedule) APLActionImpl { + innerAction := rot.newAPLAction(config.InnerAction) + if innerAction == nil { + return nil + } + + timingStrs := strings.Split(config.Schedule, ",") + if len(timingStrs) == 0 { + return nil + } + + timings := make([]time.Duration, len(timingStrs)) + valid := true + for i, timingStr := range timingStrs { + if durVal, err := time.ParseDuration(strings.TrimSpace(timingStr)); err == nil { + timings[i] = durVal + } else { + rot.ValidationWarning("Invalid duration value '%s'", strings.TrimSpace(timingStr)) + valid = false + } + } + if !valid { + return nil + } + + return &APLActionSchedule{ + innerAction: innerAction, + timings: timings, + } +} +func (action *APLActionSchedule) Reset(*Simulation) { + action.nextTimingIdx = 0 +} +func (action *APLActionSchedule) GetInnerActions() []*APLAction { + return []*APLAction{action.innerAction} +} +func (action *APLActionSchedule) IsReady(sim *Simulation) bool { + return action.nextTimingIdx < len(action.timings) && + sim.CurrentTime >= action.timings[action.nextTimingIdx] && + action.innerAction.IsReady(sim) +} + +func (action *APLActionSchedule) Execute(sim *Simulation) { + action.nextTimingIdx++ + action.innerAction.Execute(sim) +} + +func (action *APLActionSchedule) String() string { + return fmt.Sprintf("Schedule(%s, %s)", action.timings, action.innerAction) +} diff --git a/ui/core/components/individual_sim_ui/apl_actions.ts b/ui/core/components/individual_sim_ui/apl_actions.ts index 1f9645fc5c..a47e20b6a6 100644 --- a/ui/core/components/individual_sim_ui/apl_actions.ts +++ b/ui/core/components/individual_sim_ui/apl_actions.ts @@ -1,19 +1,25 @@ import { APLAction, + APLActionCastSpell, + APLActionMultidot, + APLActionMultishield, + APLActionAutocastOtherCooldowns, + + APLActionWait, + APLActionWaitUntil, + APLActionSchedule, + APLActionSequence, APLActionResetSequence, APLActionStrictSequence, - APLActionMultidot, - APLActionAutocastOtherCooldowns, + APLActionChangeTarget, APLActionActivateAura, APLActionCancelAura, APLActionTriggerICD, - APLActionWait, - APLActionWaitUntil, + APLValue, - APLActionMultishield, } from '../../proto/apl.js'; import { isHealingSpec } from '../../proto_utils/utils.js'; @@ -90,7 +96,7 @@ export class APLActionPicker extends Input, APLAction> { }), equals: (a, b) => a == b, changedEvent: (player: Player) => player.rotationChangeEmitter, - getValue: (player: Player) => this.getSourceValue().action.oneofKind, + getValue: (player: Player) => this.getSourceValue()?.action.oneofKind, setValue: (eventID: EventID, player: Player, newKind: APLActionKind) => { const sourceValue = this.getSourceValue(); const oldKind = sourceValue.action.oneofKind; @@ -293,6 +299,7 @@ const actionKindFactories: {[f in NonNullable]: ActionKindConfig< }), ['multidot']: inputBuilder({ label: 'Multi Dot', + submenu: ['Casting'], shortDescription: 'Keeps a DoT active on multiple targets by casting the specified spell.', includeIf: (player: Player, isPrepull: boolean) => !isPrepull, newValue: () => APLActionMultidot.create({ @@ -320,6 +327,7 @@ const actionKindFactories: {[f in NonNullable]: ActionKindConfig< }), ['multishield']: inputBuilder({ label: 'Multi Shield', + submenu: ['Casting'], shortDescription: 'Keeps a Shield active on multiple targets by casting the specified spell.', includeIf: (player: Player, isPrepull: boolean) => !isPrepull && isHealingSpec(player.spec), newValue: () => APLActionMultishield.create({ @@ -345,6 +353,68 @@ const actionKindFactories: {[f in NonNullable]: ActionKindConfig< }), ], }), + ['autocastOtherCooldowns']: inputBuilder({ + label: 'Autocast Other Cooldowns', + submenu: ['Casting'], + shortDescription: 'Auto-casts cooldowns as soon as they are ready.', + fullDescription: ` +
    +
  • Does not auto-cast cooldowns which are already controlled by other actions in the priority list.
  • +
  • Cooldowns are usually cast immediately upon becoming ready, but there are some basic smart checks in place, e.g. don't use Mana CDs when near full mana.
  • +
+ `, + includeIf: (player: Player, isPrepull: boolean) => !isPrepull, + newValue: APLActionAutocastOtherCooldowns.create, + fields: [], + }), + ['wait']: inputBuilder({ + label: 'Wait', + submenu: ['Timing'], + shortDescription: 'Pauses all APL actions for a specified amount of time.', + includeIf: (player: Player, isPrepull: boolean) => !isPrepull, + newValue: () => APLActionWait.create({ + duration: { + value: { + oneofKind: 'const', + const: { + val: '1000ms', + }, + }, + }, + }), + fields: [ + AplValues.valueFieldConfig('duration'), + ], + }), + ['waitUntil']: inputBuilder({ + label: 'Wait Until', + submenu: ['Timing'], + shortDescription: 'Pauses all APL actions until the specified condition is True.', + includeIf: (player: Player, isPrepull: boolean) => !isPrepull, + newValue: () => APLActionWaitUntil.create(), + fields: [ + AplValues.valueFieldConfig('condition'), + ], + }), + ['schedule']: inputBuilder({ + label: 'Scheduled Action', + submenu: ['Timing'], + shortDescription: 'Executes the inner action once at each specified timing.', + includeIf: (player: Player, isPrepull: boolean) => !isPrepull, + newValue: () => APLActionSchedule.create({ + schedule: '0s, 60s', + innerAction: { + action: {oneofKind: 'castSpell', castSpell: {}}, + }, + }), + fields: [ + AplHelpers.stringFieldConfig('schedule', { + label: 'Do At', + labelTooltip: 'Comma-separated list of timings. The inner action will be performed once at each timing.', + }), + actionFieldConfig('innerAction'), + ], + }), ['sequence']: inputBuilder({ label: 'Sequence', submenu: ['Sequences'], @@ -386,49 +456,6 @@ const actionKindFactories: {[f in NonNullable]: ActionKindConfig< actionListFieldConfig('actions'), ], }), - ['autocastOtherCooldowns']: inputBuilder({ - label: 'Autocast Other Cooldowns', - submenu: ['Misc'], - shortDescription: 'Auto-casts cooldowns as soon as they are ready.', - fullDescription: ` -
    -
  • Does not auto-cast cooldowns which are already controlled by other actions in the priority list.
  • -
  • Cooldowns are usually cast immediately upon becoming ready, but there are some basic smart checks in place, e.g. don't use Mana CDs when near full mana.
  • -
- `, - includeIf: (player: Player, isPrepull: boolean) => !isPrepull, - newValue: APLActionAutocastOtherCooldowns.create, - fields: [], - }), - ['wait']: inputBuilder({ - label: 'Wait', - submenu: ['Misc'], - shortDescription: 'Pauses all APL actions for a specified amount of time.', - includeIf: (player: Player, isPrepull: boolean) => !isPrepull, - newValue: () => APLActionWait.create({ - duration: { - value: { - oneofKind: 'const', - const: { - val: '1000ms', - }, - }, - }, - }), - fields: [ - AplValues.valueFieldConfig('duration'), - ], - }), - ['waitUntil']: inputBuilder({ - label: 'Wait Until', - submenu: ['Misc'], - shortDescription: 'Pauses all APL actions until the specified condition is True.', - includeIf: (player: Player, isPrepull: boolean) => !isPrepull, - newValue: () => APLActionWaitUntil.create(), - fields: [ - AplValues.valueFieldConfig('condition'), - ], - }), ['changeTarget']: inputBuilder({ label: 'Change Target', submenu: ['Misc'], diff --git a/ui/core/components/individual_sim_ui/apl_helpers.ts b/ui/core/components/individual_sim_ui/apl_helpers.ts index a80e2cc951..b43738c343 100644 --- a/ui/core/components/individual_sim_ui/apl_helpers.ts +++ b/ui/core/components/individual_sim_ui/apl_helpers.ts @@ -454,7 +454,10 @@ export function stringFieldConfig(field: string, options?: Partial '', - factory: (parent, player, config) => new AdaptiveStringPicker(parent, player, config), + factory: (parent, player, config) => { + config.extraCssClasses = ['input-inline'].concat(config.extraCssClasses || []); + return new AdaptiveStringPicker(parent, player, config); + }, ...(options || {}), }; } diff --git a/ui/scss/core/components/individual_sim_ui/_apl_rotation_picker.scss b/ui/scss/core/components/individual_sim_ui/_apl_rotation_picker.scss index 0998469ec7..9bfcc5caa8 100644 --- a/ui/scss/core/components/individual_sim_ui/_apl_rotation_picker.scss +++ b/ui/scss/core/components/individual_sim_ui/_apl_rotation_picker.scss @@ -172,4 +172,7 @@ } .apl-picker-builder-root.apl-action-strictSequence .apl-action-condition { display: none; -} \ No newline at end of file +} +.apl-action-schedule .apl-action-condition { + display: none; +}