From 2042b626b08d3d9553cab078293ed42c6a34fb3f Mon Sep 17 00:00:00 2001 From: rosenrusinov Date: Mon, 13 Nov 2023 12:26:54 +0100 Subject: [PATCH] add boss spell related apl values for tank sims --- proto/api.proto | 1 + proto/apl.proto | 16 +++++- sim/core/apl_helpers.go | 11 ++++ sim/core/apl_value.go | 6 ++ sim/core/apl_values_boss.go | 56 +++++++++++++++++++ sim/core/unit.go | 1 + sim/encounters/icc/lichking25h_ai.go | 5 +- sim/encounters/icc/sindragosa25h_ai.go | 9 +-- .../individual_sim_ui/apl_helpers.ts | 29 +++++++++- .../individual_sim_ui/apl_values.ts | 24 ++++++++ 10 files changed, 150 insertions(+), 8 deletions(-) create mode 100644 sim/core/apl_values_boss.go diff --git a/proto/api.proto b/proto/api.proto index 1be1bf8877..df2d9d3019 100644 --- a/proto/api.proto +++ b/proto/api.proto @@ -316,6 +316,7 @@ message SpellStats { bool has_shield = 6; // Whether this spell applies a shield effect. bool prepull_only = 5; // Whether this spell may only be cast during prepull. bool encounter_only = 8; // Whether this spell may only be cast during the encounter (not prepull). + bool has_cast_time = 9; // Whether this spell has a cast time or not. } message APLActionStats { repeated string warnings = 1; diff --git a/proto/apl.proto b/proto/apl.proto index d17be22e16..9c2f4087e8 100644 --- a/proto/apl.proto +++ b/proto/apl.proto @@ -76,7 +76,7 @@ message APLAction { } } -// NextIndex: 64 +// NextIndex: 66 message APLValue { oneof value { // Operators @@ -97,6 +97,10 @@ message APLValue { APLValueIsExecutePhase is_execute_phase = 41; APLValueNumberTargets number_targets = 28; + // Boss values + APLValueBossSpellTimeToReady boss_spell_time_to_ready = 64; + APLValueBossSpellIsCasting boss_spell_is_casting = 65; + // Resource values APLValueCurrentHealth current_health = 26; APLValueCurrentHealthPercent current_health_percent = 27; @@ -321,6 +325,16 @@ message APLValueIsExecutePhase { ExecutePhaseThreshold threshold = 1; } +message APLValueBossSpellTimeToReady { + UnitReference target_unit = 1; + ActionID spell_id = 2; +} + +message APLValueBossSpellIsCasting { + UnitReference target_unit = 1; + ActionID spell_id = 2; +} + message APLValueCurrentHealth { UnitReference source_unit = 1; } diff --git a/sim/core/apl_helpers.go b/sim/core/apl_helpers.go index fd3bf3f36e..faff038232 100644 --- a/sim/core/apl_helpers.go +++ b/sim/core/apl_helpers.go @@ -157,6 +157,17 @@ func (rot *APLRotation) GetAPLSpell(spellId *proto.ActionID) *Spell { return spell } +func (rot *APLRotation) GetTargetAPLSpell(spellId *proto.ActionID, targetUnit UnitReference) *Spell { + actionID := ProtoToActionID(spellId) + target := targetUnit.Get() + spell := target.GetSpell(actionID) + + if spell == nil { + rot.ValidationWarning("%s does not know spell %s", target.Label, actionID) + } + return spell +} + func (rot *APLRotation) GetAPLDot(targetUnit UnitReference, spellId *proto.ActionID) *Dot { spell := rot.GetAPLSpell(spellId) diff --git a/sim/core/apl_value.go b/sim/core/apl_value.go index 7d0646f499..25381b1f78 100644 --- a/sim/core/apl_value.go +++ b/sim/core/apl_value.go @@ -94,6 +94,12 @@ func (rot *APLRotation) newAPLValue(config *proto.APLValue) APLValue { case *proto.APLValue_NumberTargets: return rot.newValueNumberTargets(config.GetNumberTargets()) + // Boss + case *proto.APLValue_BossSpellIsCasting: + return rot.newValueBossSpellIsCasting(config.GetBossSpellIsCasting()) + case *proto.APLValue_BossSpellTimeToReady: + return rot.newValueBossSpellTimeToReady(config.GetBossSpellTimeToReady()) + // Resources case *proto.APLValue_CurrentHealth: return rot.newValueCurrentHealth(config.GetCurrentHealth()) diff --git a/sim/core/apl_values_boss.go b/sim/core/apl_values_boss.go new file mode 100644 index 0000000000..e44227aa88 --- /dev/null +++ b/sim/core/apl_values_boss.go @@ -0,0 +1,56 @@ +package core + +import ( + "fmt" + "time" + + "github.com/wowsims/wotlk/sim/core/proto" +) + +type APLValueBossSpellIsCasting struct { + DefaultAPLValueImpl + spell *Spell +} + +func (rot *APLRotation) newValueBossSpellIsCasting(config *proto.APLValueBossSpellIsCasting) APLValue { + spell := rot.GetTargetAPLSpell(config.SpellId, rot.GetTargetUnit(config.TargetUnit)) + if spell == nil { + return nil + } + return &APLValueBossSpellIsCasting{ + spell: spell, + } +} +func (value *APLValueBossSpellIsCasting) Type() proto.APLValueType { + return proto.APLValueType_ValueTypeBool +} +func (value *APLValueBossSpellIsCasting) GetBool(sim *Simulation) bool { + return value.spell.Unit.Hardcast.ActionID == value.spell.ActionID && value.spell.Unit.Hardcast.Expires > sim.CurrentTime +} +func (value *APLValueBossSpellIsCasting) String() string { + return fmt.Sprintf("Boss is Casting(%s)", value.spell.ActionID) +} + +type APLValueBossSpellTimeToReady struct { + DefaultAPLValueImpl + spell *Spell +} + +func (rot *APLRotation) newValueBossSpellTimeToReady(config *proto.APLValueBossSpellTimeToReady) APLValue { + spell := rot.GetTargetAPLSpell(config.SpellId, rot.GetTargetUnit(config.TargetUnit)) + if spell == nil { + return nil + } + return &APLValueBossSpellTimeToReady{ + spell: spell, + } +} +func (value *APLValueBossSpellTimeToReady) Type() proto.APLValueType { + return proto.APLValueType_ValueTypeDuration +} +func (value *APLValueBossSpellTimeToReady) GetDuration(sim *Simulation) time.Duration { + return value.spell.TimeToReady(sim) +} +func (value *APLValueBossSpellTimeToReady) String() string { + return fmt.Sprintf("Boss Spell Time to Ready(%s)", value.spell.ActionID) +} diff --git a/sim/core/unit.go b/sim/core/unit.go index ddf75c0665..cdec1ee4a1 100644 --- a/sim/core/unit.go +++ b/sim/core/unit.go @@ -536,6 +536,7 @@ func (unit *Unit) GetMetadata() *proto.UnitMetadata { HasShield: spell.shields != nil || spell.selfShield != nil, PrepullOnly: spell.Flags.Matches(SpellFlagPrepullOnly), EncounterOnly: spell.Flags.Matches(SpellFlagEncounterOnly), + HasCastTime: spell.DefaultCast.CastTime > 0, } }) diff --git a/sim/encounters/icc/lichking25h_ai.go b/sim/encounters/icc/lichking25h_ai.go index 59e10ebb41..945f8aa81f 100644 --- a/sim/encounters/icc/lichking25h_ai.go +++ b/sim/encounters/icc/lichking25h_ai.go @@ -61,6 +61,7 @@ func (ai *LichKing25HAI) Initialize(target *core.Target, _ *proto.Target) { } func (ai *LichKing25HAI) Reset(*core.Simulation) { + ai.SoulReaper.CD.Set(ai.SoulReaper.CD.Duration) } func (ai *LichKing25HAI) registerSoulReaperSpell(target *core.Target) { @@ -80,7 +81,7 @@ func (ai *LichKing25HAI) registerSoulReaperSpell(target *core.Target) { ActionID: core.ActionID{SpellID: 69409}, SpellSchool: core.SpellSchoolShadow, ProcMask: core.ProcMaskMeleeMHSpecial, - Flags: core.SpellFlagNone, + Flags: core.SpellFlagAPL, Cast: core.CastConfig{ CD: core.Cooldown{ @@ -135,7 +136,7 @@ func (ai *LichKing25HAI) registerSoulReaperSpell(target *core.Target) { func (ai *LichKing25HAI) DoAction(sim *core.Simulation) { if ai.Target.GCD.IsReady(sim) { if ai.Target.CurrentTarget != nil { - if ai.SoulReaper.IsReady(sim) && sim.CurrentTime >= ai.SoulReaper.CD.Duration { + if ai.SoulReaper.IsReady(sim) { // Based on log analysis, Soul Reaper appears to have a ~75% chance to "proc" on every 1.62 second server tick once it is off cooldown. // Note that analysis based only on the cast intervals supported a ~40% proc chance fit. However, many of the apparent delays in Soul Reaper casts are // due to Defile and Infest casts that take priority when the cooldowns overlap. Once these CD conflicts are corrected for, the variance in Soul Reaper diff --git a/sim/encounters/icc/sindragosa25h_ai.go b/sim/encounters/icc/sindragosa25h_ai.go index 01f9ac8f5b..bae19b56bd 100644 --- a/sim/encounters/icc/sindragosa25h_ai.go +++ b/sim/encounters/icc/sindragosa25h_ai.go @@ -50,7 +50,6 @@ type Sindragosa25HAI struct { FrostAura *core.Spell FrostBreath *core.Spell FrostBreathDebuff *core.Aura - FirstBreath time.Duration IncludeMysticBuffet bool MysticBuffetAuras []*core.Aura @@ -88,7 +87,9 @@ func (ai *Sindragosa25HAI) Reset(sim *core.Simulation) { breathPeriod := time.Millisecond * 22680 maxBreathsPossible := (sim.Duration - time.Millisecond*1500) / breathPeriod latestAllowedBreath := sim.Duration - time.Millisecond*1500 - breathPeriod*maxBreathsPossible - time.Millisecond*1620 - ai.FirstBreath = core.DurationFromSeconds(sim.RandomFloat("Frost Breath Timing") * latestAllowedBreath.Seconds()) + firstBreath := core.DurationFromSeconds(sim.RandomFloat("Frost Breath Timing") * latestAllowedBreath.Seconds()) + + ai.FrostBreath.CD.Set(firstBreath) } func (ai *Sindragosa25HAI) registerPermeatingChillAura(target *core.Target) { @@ -290,7 +291,7 @@ func (ai *Sindragosa25HAI) registerFrostBreathSpell(target *core.Target) { ActionID: actionID, SpellSchool: core.SpellSchoolFrost, ProcMask: core.ProcMaskSpellDamage, - Flags: core.SpellFlagNone, + Flags: core.SpellFlagAPL, Cast: core.CastConfig{ CD: core.Cooldown{ @@ -324,7 +325,7 @@ func (ai *Sindragosa25HAI) DoAction(sim *core.Simulation) { } if ai.Target.CurrentTarget != nil { - if ai.FrostBreath.IsReady(sim) && sim.CurrentTime >= ai.FirstBreath { + if ai.FrostBreath.IsReady(sim) { ai.Target.Unit.AutoAttacks.StopMeleeUntil(sim, sim.CurrentTime+time.Millisecond*1500, false) ai.FrostBreath.Cast(sim, ai.Target.CurrentTarget) return diff --git a/ui/core/components/individual_sim_ui/apl_helpers.ts b/ui/core/components/individual_sim_ui/apl_helpers.ts index 0664a21a38..fbed95b667 100644 --- a/ui/core/components/individual_sim_ui/apl_helpers.ts +++ b/ui/core/components/individual_sim_ui/apl_helpers.ts @@ -12,7 +12,7 @@ import { ActionID } from '../../proto/common.js'; import { BooleanPicker } from '../boolean_picker.js'; import { APLValueRuneSlot, APLValueRuneType } from '../../proto/apl.js'; -export type ACTION_ID_SET = 'auras' | 'stackable_auras' | 'icd_auras' | 'exclusive_effect_auras' | 'castable_spells' | 'channel_spells' | 'dot_spells' | 'shield_spells'; +export type ACTION_ID_SET = 'auras' | 'stackable_auras' | 'icd_auras' | 'exclusive_effect_auras' | 'spells' | 'castable_spells' | 'channel_spells' | 'dot_spells' | 'shield_spells' | 'non_instant_spells'; const actionIdSets: Record { + return metadata.getSpells().filter(spell => spell.data.isCastable).map(actionId => { + return { + value: actionId.id, + }; + }); + }, + }, 'castable_spells': { defaultLabel: 'Spell', getActionIDs: async (metadata) => { @@ -74,10 +85,12 @@ const actionIdSets: Record { return { value: actionId.id, + submenu: ['Spells'], extraCssClasses: (actionId.data.prepullOnly ? ['apl-prepull-actions-only'] : (actionId.data.encounterOnly @@ -88,10 +101,12 @@ const actionIdSets: Record { return { value: actionId.id, + submenu: ['Cooldowns'], extraCssClasses: (actionId.data.prepullOnly ? ['apl-prepull-actions-only'] : (actionId.data.encounterOnly @@ -102,16 +117,28 @@ const actionIdSets: Record { return { value: actionId, + submenu: ['Placeholders'], tooltip: 'The Prepull Potion if CurrentTime < 0, or the Combat Potion if combat has started.', }; }), ].flat(); }, }, + 'non_instant_spells': { + defaultLabel: 'Non-instant Spell', + getActionIDs: async (metadata) => { + return metadata.getSpells().filter(spell => spell.data.isCastable && spell.data.hasCastTime).map(actionId => { + return { + value: actionId.id, + }; + }); + }, + }, 'channel_spells': { defaultLabel: 'Channeled Spell', getActionIDs: async (metadata) => { diff --git a/ui/core/components/individual_sim_ui/apl_values.ts b/ui/core/components/individual_sim_ui/apl_values.ts index 1143277884..a74c7a6aca 100644 --- a/ui/core/components/individual_sim_ui/apl_values.ts +++ b/ui/core/components/individual_sim_ui/apl_values.ts @@ -75,6 +75,8 @@ import { APLValueWarlockShouldRecastDrainSoul, APLValueWarlockShouldRefreshCorruption, APLValueCatNewSavageRoarDuration, + APLValueBossSpellTimeToReady, + APLValueBossSpellIsCasting, } from '../../proto/apl.js'; import { EventID } from '../../typed_event.js'; @@ -554,6 +556,28 @@ const valueKindFactories: {[f in NonNullable]: ValueKindConfig