diff --git a/proto/hunter.proto b/proto/hunter.proto index 592f2136c5..f470dd0c4d 100644 --- a/proto/hunter.proto +++ b/proto/hunter.proto @@ -217,7 +217,7 @@ message Hunter { ExplosiveTrap = 11; Volley = 12; } - CustomRotation custom_rotation = 8; + CustomRotation custom_rotation = 8 [deprecated = true]; // Switch to Aspect of the Viper when mana goes below this percent. double viper_start_mana_percent = 6; @@ -228,7 +228,7 @@ message Hunter { bool allow_explosive_shot_downrank = 10; bool multi_dot_serpent_sting = 11; - double steady_shot_max_delay = 12; + double steady_shot_max_delay = 12 [deprecated = true]; } Rotation rotation = 1; diff --git a/ui/core/components/individual_sim_ui/rotation_tab.ts b/ui/core/components/individual_sim_ui/rotation_tab.ts index 10f6745ab8..9a6a36ee3b 100644 --- a/ui/core/components/individual_sim_ui/rotation_tab.ts +++ b/ui/core/components/individual_sim_ui/rotation_tab.ts @@ -6,6 +6,7 @@ import { import { APLRotation, APLRotation_Type as APLRotationType, + SimpleRotation, } from "../../proto/apl"; import { SavedRotation, @@ -196,7 +197,7 @@ export class RotationTab extends SimTab { } private buildSavedDataPickers() { - const launchStatus = aplLaunchStatuses[this.simUI.player.spec]; + const aplIsLaunched = aplLaunchStatuses[this.simUI.player.spec] == LaunchStatus.Launched; const savedRotationsManager = new SavedDataManager, SavedRotation>(this.rightPanel, this.simUI, this.simUI.player, { label: 'Rotation', @@ -204,13 +205,13 @@ export class RotationTab extends SimTab { storageKey: this.simUI.getSavedRotationStorageKey(), getData: (player: Player) => SavedRotation.create({ rotation: APLRotation.clone(player.aplRotation), - specRotationOptionsJson: launchStatus == LaunchStatus.Launched ? '' : JSON.stringify(player.specTypeFunctions.rotationToJson(player.getRotation())), - cooldowns: LaunchStatus.Launched ? undefined : player.getCooldowns(), + specRotationOptionsJson: aplIsLaunched ? '{}' : JSON.stringify(player.specTypeFunctions.rotationToJson(player.getRotation())), + cooldowns: aplIsLaunched ? Cooldowns.create() : player.getCooldowns(), }), setData: (eventID: EventID, player: Player, newRotation: SavedRotation) => { TypedEvent.freezeAllAndDo(() => { player.setAplRotation(eventID, newRotation.rotation || APLRotation.create()); - if (launchStatus != LaunchStatus.Launched) { + if (!aplIsLaunched) { if (newRotation.specRotationOptionsJson) { try { const json = JSON.parse(newRotation.specRotationOptionsJson); @@ -225,7 +226,11 @@ export class RotationTab extends SimTab { }); }, changeEmitters: [this.simUI.player.rotationChangeEmitter, this.simUI.player.cooldownsChangeEmitter], - equals: (a: SavedRotation, b: SavedRotation) => SavedRotation.equals(a, b), + equals: (a: SavedRotation, b: SavedRotation) => { + // Uncomment this to debug equivalence checks with preset rotations (e.g. the chip doesn't highlight) + //console.log(`Rot A: ${SavedRotation.toJsonString(a, {prettySpaces: 2})}\n\nRot B: ${SavedRotation.toJsonString(b, {prettySpaces: 2})}`); + return SavedRotation.equals(a, b); + }, toJson: (a: SavedRotation) => SavedRotation.toJson(a), fromJson: (obj: any) => SavedRotation.fromJson(obj), }); @@ -236,6 +241,7 @@ export class RotationTab extends SimTab { const rotData = presetRotation.rotation; // Fill default values so the equality checks always work. if (!rotData.cooldowns) rotData.cooldowns = Cooldowns.create(); + if (!rotData.specRotationOptionsJson) rotData.specRotationOptionsJson = '{}'; if (!rotData.rotation) rotData.rotation = APLRotation.create(); savedRotationsManager.addSavedData({ diff --git a/ui/core/launched_sims.ts b/ui/core/launched_sims.ts index 0e58d7be66..e74f730b6f 100644 --- a/ui/core/launched_sims.ts +++ b/ui/core/launched_sims.ts @@ -47,7 +47,7 @@ export const aplLaunchStatuses: Record = { [Spec.SpecElementalShaman]: LaunchStatus.Alpha, [Spec.SpecEnhancementShaman]: LaunchStatus.Alpha, [Spec.SpecRestorationShaman]: LaunchStatus.Alpha, - [Spec.SpecHunter]: LaunchStatus.Beta, + [Spec.SpecHunter]: LaunchStatus.Launched, [Spec.SpecMage]: LaunchStatus.Alpha, [Spec.SpecRogue]: LaunchStatus.Alpha, [Spec.SpecHolyPaladin]: LaunchStatus.Alpha, diff --git a/ui/core/player.ts b/ui/core/player.ts index 134b2ab055..4074ae23a4 100644 --- a/ui/core/player.ts +++ b/ui/core/player.ts @@ -188,7 +188,7 @@ export class UnitMetadataList { } export type AutoRotationGenerator = (player: Player) => APLRotation; -export type SimpleRotationGenerator = (player: Player, simpleRotation: SpecRotation) => APLRotation; +export type SimpleRotationGenerator = (player: Player, simpleRotation: SpecRotation, cooldowns: Cooldowns) => APLRotation; // Manages all the gear / consumes / other settings for a single Player. export class Player { @@ -656,11 +656,16 @@ export class Player { getRotation(): SpecRotation { if (aplLaunchStatuses[this.spec] == LaunchStatus.Launched) { + const jsonStr = this.aplRotation.simple?.specRotationJson || ''; + if (!jsonStr) { + return this.specTypeFunctions.rotationCreate(); + } + try { - const json = JSON.parse(this.aplRotation.simple?.specRotationJson || ''); + const json = JSON.parse(jsonStr); return this.specTypeFunctions.rotationFromJson(json); } catch (e) { - console.warn('Error parsing rotation spec options: ' + e); + console.warn(`Error parsing rotation spec options: ${e}\n\nSpec options: '${jsonStr}'`); return this.specTypeFunctions.rotationCreate(); } } else { @@ -676,7 +681,7 @@ export class Player { if (!this.aplRotation.simple) { this.aplRotation.simple = SimpleRotation.create(); } - this.aplRotation.simple.specRotationJson = this.specTypeFunctions.rotationToJson(newRotation); + this.aplRotation.simple.specRotationJson = JSON.stringify(this.specTypeFunctions.rotationToJson(newRotation)); } else { this.rotation = this.specTypeFunctions.rotationCopy(newRotation); } @@ -721,7 +726,7 @@ export class Player { return APLRotation.clone(this.autoRotationGenerator(this)); } else if (type == APLRotationType.TypeSimple && this.simpleRotationGenerator) { // Clone to avoid modifying preset rotations, which are often returned directly. - const rot = APLRotation.clone(this.simpleRotationGenerator(this, this.getRotation())); + const rot = APLRotation.clone(this.simpleRotationGenerator(this, this.getRotation(), this.getCooldowns())); rot.type = APLRotationType.TypeAPL; // Set this here for convenience, so the generator functions don't need to. return rot; } else { @@ -1200,6 +1205,7 @@ export class Player { } toProto(forExport?: boolean, forSimming?: boolean): PlayerProto { + const aplIsLaunched = aplLaunchStatuses[this.spec] == LaunchStatus.Launched; const gear = this.getGear(); return withSpecProto( this.spec, @@ -1211,7 +1217,7 @@ export class Player { consumes: this.getConsumes(), bonusStats: this.getBonusStats().toProto(), buffs: this.getBuffs(), - cooldowns: this.getCooldowns(), + cooldowns: aplIsLaunched ? Cooldowns.create({ hpPercentForDefensives: this.getCooldowns().hpPercentForDefensives }) : this.getCooldowns(), talentsString: this.getTalentsString(), glyphs: this.getGlyphs(), rotation: forSimming ? this.getResolvedAplRotation() : this.aplRotation, @@ -1223,7 +1229,7 @@ export class Player { healingModel: this.getHealingModel(), database: forExport ? SimDatabase.create() : this.toDatabase(), }), - this.getRotation(), + aplIsLaunched ? this.specTypeFunctions.rotationCreate() : this.getRotation(), this.getSpecOptions()); } diff --git a/ui/core/proto_utils/apl_utils.ts b/ui/core/proto_utils/apl_utils.ts new file mode 100644 index 0000000000..718c9e6c89 --- /dev/null +++ b/ui/core/proto_utils/apl_utils.ts @@ -0,0 +1,44 @@ +import { + ActionID as ActionIdProto, + Cooldowns, +} from '../proto/common.js'; + +import { + APLAction, + APLPrepullAction, +} from '../proto/apl.js'; + +export function prepullPotionAction(doAt?: string): APLPrepullAction { + return APLPrepullAction.fromJsonString(`{"action":{"castSpell":{"spellId":{"otherId":"OtherActionPotion"}}},"doAtValue":{"const":{"val":"${doAt || '-1s'}"}}}`); +} + +export function autocastCooldownsAction(startAt?: string): APLAction { + if (startAt) { + return APLAction.fromJsonString(`{"condition":{"cmp":{"op":"OpGt","lhs":{"currentTime":{}},"rhs":{"const":{"val":"${startAt}"}}}},"autocastOtherCooldowns":{}}`); + } else { + return APLAction.fromJsonString(`{"autocastOtherCooldowns":{}}`); + } +} + +export function scheduledCooldownAction(schedule: string, actionId: ActionIdProto): APLAction { + return APLAction.fromJsonString(`{"schedule":{"schedule":"${schedule}","innerAction":{"castSpell":{"spellId":${ActionIdProto.toJson(actionId)}}}}}`); +} + +export function simpleCooldownActions(cooldowns: Cooldowns): Array { + return cooldowns.cooldowns + .filter(cd => cd.id) + .map(cd => { + const schedule = cd.timings.map(timing => timing.toFixed(1) + 's').join(', '); + return scheduledCooldownAction(schedule, cd.id!); + }); +} + +export function standardCooldownDefaults(cooldowns: Cooldowns, prepotAt?: string, startAutocastCDsAt?: string): [Array, Array] { + return [ + [prepullPotionAction(prepotAt)], + [ + autocastCooldownsAction(startAutocastCDsAt), + simpleCooldownActions(cooldowns), + ].flat(), + ]; +} diff --git a/ui/hunter/inputs.ts b/ui/hunter/inputs.ts index b5af555c37..377348b303 100644 --- a/ui/hunter/inputs.ts +++ b/ui/hunter/inputs.ts @@ -72,7 +72,6 @@ export const HunterRotationConfig = { values: [ { name: 'Single Target', value: RotationType.SingleTarget }, { name: 'AOE', value: RotationType.Aoe }, - { name: 'Custom', value: RotationType.Custom }, ], }), InputHelpers.makeRotationEnumInput({ @@ -90,12 +89,6 @@ export const HunterRotationConfig = { fieldName: 'trapWeave', label: 'Trap Weave', labelTooltip: 'Uses Explosive Trap at appropriate times. Note that selecting this will disable Black Arrow because they share a CD.', - showWhen: (player: Player) => player.getRotation().type != RotationType.Custom, - }), - InputHelpers.makeRotationNumberInput({ - fieldName: 'steadyShotMaxDelay', - label: 'Steady Shot Max Delay (ms)', - labelTooltip: 'If another higher-priority spell comes off cooldown in the specified time then steady shot is not cast and the rotation waits', }), InputHelpers.makeRotationBooleanInput({ fieldName: 'allowExplosiveShotDownrank', @@ -110,26 +103,6 @@ export const HunterRotationConfig = { labelTooltip: 'Casts Serpent Sting on multiple targets', changeEmitter: (player: Player) => TypedEvent.onAny([player.rotationChangeEmitter, player.talentsChangeEmitter]), }), - InputHelpers.makeCustomRotationInput({ - fieldName: 'customRotation', - numColumns: 2, - values: [ - { actionId: ActionId.fromSpellId(49052), value: SpellOption.SteadyShot }, - { actionId: ActionId.fromSpellId(49045), value: SpellOption.ArcaneShot }, - { actionId: ActionId.fromSpellId(49050), value: SpellOption.AimedShot }, - { actionId: ActionId.fromSpellId(49048), value: SpellOption.MultiShot }, - { actionId: ActionId.fromSpellId(49001), value: SpellOption.SerpentStingSpell }, - { actionId: ActionId.fromSpellId(3043), value: SpellOption.ScorpidStingSpell }, - { actionId: ActionId.fromSpellId(61006), value: SpellOption.KillShot }, - { actionId: ActionId.fromSpellId(63672), value: SpellOption.BlackArrow }, - { actionId: ActionId.fromSpellId(53209), value: SpellOption.ChimeraShot }, - { actionId: ActionId.fromSpellId(60053), value: SpellOption.ExplosiveShot, text: 'R4' }, - { actionId: ActionId.fromSpellId(60052), value: SpellOption.ExplosiveShotDownrank, text: 'R3' }, - { actionId: ActionId.fromSpellId(49067), value: SpellOption.ExplosiveTrap }, - { actionId: ActionId.fromSpellId(58434), value: SpellOption.Volley }, - ], - showWhen: (player: Player) => player.getRotation().type == RotationType.Custom, - }), InputHelpers.makeRotationNumberInput({ fieldName: 'viperStartManaPercent', label: 'Viper Start Mana %', diff --git a/ui/hunter/presets.ts b/ui/hunter/presets.ts index 8d41360872..bc6abcfeaa 100644 --- a/ui/hunter/presets.ts +++ b/ui/hunter/presets.ts @@ -7,7 +7,7 @@ import { Glyphs } from '../core/proto/common.js'; import { PetFood } from '../core/proto/common.js'; import { Potions } from '../core/proto/common.js'; import { SavedRotation, SavedTalents } from '../core/proto/ui.js'; -import { APLRotation } from '../core/proto/apl.js'; +import { APLRotation, APLRotation_Type } from '../core/proto/apl.js'; import { ferocityDefault, ferocityBMDefault } from '../core/talents/hunter_pet.js'; import { Player } from '../core/player.js'; @@ -84,25 +84,17 @@ export const DefaultRotation = HunterRotation.create({ viperStopManaPercent: 0.3, multiDotSerpentSting: true, allowExplosiveShotDownrank: true, - steadyShotMaxDelay: 300, - customRotation: CustomRotation.create({ - spells: [ - CustomSpell.create({ spell: SpellOption.SerpentStingSpell }), - CustomSpell.create({ spell: SpellOption.KillShot }), - CustomSpell.create({ spell: SpellOption.ChimeraShot }), - CustomSpell.create({ spell: SpellOption.BlackArrow }), - CustomSpell.create({ spell: SpellOption.ExplosiveShot }), - CustomSpell.create({ spell: SpellOption.AimedShot }), - CustomSpell.create({ spell: SpellOption.ArcaneShot }), - CustomSpell.create({ spell: SpellOption.SteadyShot }), - ], - }), }); export const ROTATION_PRESET_LEGACY_DEFAULT = { - name: 'Legacy Default', + name: 'Simple Default', rotation: SavedRotation.create({ - specRotationOptionsJson: HunterRotation.toJsonString(DefaultRotation), + rotation: { + type: APLRotation_Type.TypeSimple, + simple: { + specRotationJson: HunterRotation.toJsonString(DefaultRotation), + }, + }, }), } export const ROTATION_PRESET_BM = { diff --git a/ui/hunter/sim.ts b/ui/hunter/sim.ts index f182519fee..d656518b97 100644 --- a/ui/hunter/sim.ts +++ b/ui/hunter/sim.ts @@ -1,13 +1,22 @@ -import { RaidBuffs } from '../core/proto/common.js'; -import { PartyBuffs } from '../core/proto/common.js'; -import { IndividualBuffs } from '../core/proto/common.js'; -import { Debuffs } from '../core/proto/common.js'; -import { ItemSlot } from '../core/proto/common.js'; -import { Race } from '../core/proto/common.js'; -import { RangedWeaponType } from '../core/proto/common.js'; -import { Spec } from '../core/proto/common.js'; -import { Stat, PseudoStat } from '../core/proto/common.js'; -import { TristateEffect } from '../core/proto/common.js' +import { + Cooldowns, + Debuffs, + IndividualBuffs, + ItemSlot, + PartyBuffs, + Race, + RaidBuffs, + RangedWeaponType, + Spec, + Stat, PseudoStat, + TristateEffect, +} from '../core/proto/common.js'; +import { + APLAction, + APLListItem, + APLPrepullAction, + APLRotation, +} from '../core/proto/apl.js'; import { Player } from '../core/player.js'; import { Stats } from '../core/proto_utils/stats.js'; import { getTalentPoints } from '../core/proto_utils/utils.js'; @@ -19,15 +28,18 @@ import { protoToTalentString } from '../core/talents/factory.js'; import { Hunter, Hunter_Rotation as HunterRotation, + Hunter_Rotation_StingType as StingType, Hunter_Options as HunterOptions, Hunter_Options_PetType as PetType, HunterPetTalents, + Hunter_Rotation_RotationType, } from '../core/proto/hunter.js'; import * as IconInputs from '../core/components/icon_inputs.js'; import * as OtherInputs from '../core/components/other_inputs.js'; import * as Mechanics from '../core/constants/mechanics.js'; import * as Tooltips from '../core/constants/tooltips.js'; +import * as AplUtils from '../core/proto_utils/apl_utils.js'; import * as HunterInputs from './inputs.js'; import * as Presets from './presets.js'; @@ -262,7 +274,7 @@ export class HunterSimUI extends IndividualSimUI { ], }, - autoRotation: (player: Player) => { + autoRotation: (player: Player): APLRotation => { const talentTree = player.getTalentTree(); if (talentTree == 0) { return Presets.ROTATION_PRESET_BM.rotation.rotation!; @@ -272,6 +284,84 @@ export class HunterSimUI extends IndividualSimUI { return Presets.ROTATION_PRESET_SV.rotation.rotation!; } }, + + simpleRotation: (player: Player, simple: HunterRotation, cooldowns: Cooldowns): APLRotation => { + let [prepullActions, actions] = AplUtils.standardCooldownDefaults(cooldowns); + + const multiDotSerpentSting = (numTargets: number) => APLAction.fromJsonString(`{"condition":{"cmp":{"op":"OpGt","lhs":{"remainingTime":{}},"rhs":{"const":{"val":"6s"}}}},"multidot":{"spellId":{"spellId":49001},"maxDots":${numTargets},"maxOverlap":{"const":{"val":"0ms"}}}}`); + const scorpidSting = APLAction.fromJsonString(`{"condition":{"auraShouldRefresh":{"auraId":{"spellId":3043},"maxOverlap":{"const":{"val":"0ms"}}}},"castSpell":{"spellId":{"spellId":3043}}}`); + const trapWeave = APLAction.fromJsonString(`{"condition":{"not":{"val":{"dotIsActive":{"spellId":{"spellId":49067}}}}},"castSpell":{"spellId":{"tag":1,"spellId":49067}}}`); + const volley = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":58434}}}`); + const killShot = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":61006}}}`); + const aimedShot = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":49050}}}`); + const multiShot = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":49048}}}`); + const steadyShot = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":49052}}}`); + const silencingShot = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":34490}}}`); + const chimeraShot = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":53209}}}`); + const blackArrow = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":63672}}}`); + const explosiveShot4 = APLAction.fromJsonString(`{"condition":{"not":{"val":{"dotIsActive":{"spellId":{"spellId":60053}}}}},"castSpell":{"spellId":{"spellId":60053}}}`); + const explosiveShot3 = APLAction.fromJsonString(`{"condition":{"dotIsActive":{"spellId":{"spellId":60053}}},"castSpell":{"spellId":{"spellId":60052}}}`); + //const arcaneShot = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":49045}}}`); + + if (simple.viperStartManaPercent != 0) { + actions.push(APLAction.fromJsonString(`{"condition":{"and":{"vals":[{"not":{"val":{"auraIsActive":{"auraId":{"spellId":34074}}}}},{"cmp":{"op":"OpLt","lhs":{"currentManaPercent":{}},"rhs":{"const":{"val":"${(simple.viperStartManaPercent * 100).toFixed(0)}%"}}}}]}},"castSpell":{"spellId":{"spellId":34074}}}`)); + } + if (simple.viperStopManaPercent != 0) { + actions.push(APLAction.fromJsonString(`{"condition":{"and":{"vals":[{"not":{"val":{"auraIsActive":{"auraId":{"spellId":61847}}}}},{"cmp":{"op":"OpGt","lhs":{"currentManaPercent":{}},"rhs":{"const":{"val":"${(simple.viperStopManaPercent * 100).toFixed(0)}%"}}}}]}},"castSpell":{"spellId":{"spellId":61847}}}`)); + } + + const talentTree = player.getTalentTree(); + if (simple.type == Hunter_Rotation_RotationType.Aoe) { + actions.push(...[ + simple.sting == StingType.ScorpidSting ? scorpidSting : null, + simple.sting == StingType.SerpentSting ? (simple.multiDotSerpentSting ? multiDotSerpentSting(3) : multiDotSerpentSting(1)) : null, + simple.trapWeave ? trapWeave : null, + volley, + ].filter(a => a) as Array) + } else if (talentTree == 0) { // BM + actions.push(...[ + killShot, + simple.trapWeave ? trapWeave : null, + simple.sting == StingType.ScorpidSting ? scorpidSting : null, + simple.sting == StingType.SerpentSting ? (simple.multiDotSerpentSting ? multiDotSerpentSting(3) : multiDotSerpentSting(1)) : null, + aimedShot, + multiShot, + steadyShot, + ].filter(a => a) as Array) + } else if (talentTree == 1) { // MM + actions.push(...[ + silencingShot, + killShot, + simple.sting == StingType.ScorpidSting ? scorpidSting : null, + simple.sting == StingType.SerpentSting ? (simple.multiDotSerpentSting ? multiDotSerpentSting(3) : multiDotSerpentSting(1)) : null, + simple.trapWeave ? trapWeave : null, + chimeraShot, + aimedShot, + multiShot, + steadyShot, + ].filter(a => a) as Array) + } else if (talentTree == 2) { // SV + actions.push(...[ + killShot, + explosiveShot4, + simple.allowExplosiveShotDownrank ? explosiveShot3 : null, + simple.trapWeave ? trapWeave : null, + simple.sting == StingType.ScorpidSting ? scorpidSting : null, + simple.sting == StingType.SerpentSting ? (simple.multiDotSerpentSting ? multiDotSerpentSting(3) : multiDotSerpentSting(1)) : null, + blackArrow, + aimedShot, + multiShot, + steadyShot, + ].filter(a => a) as Array) + } + + return APLRotation.create({ + prepullActions: prepullActions, + priorityList: actions.map(action => APLListItem.create({ + action: action, + })) + }); + }, }); } } diff --git a/ui/scss/core/components/individual_sim_ui/_rotation_tab.scss b/ui/scss/core/components/individual_sim_ui/_rotation_tab.scss index 1d63c3f4a7..1210987874 100644 --- a/ui/scss/core/components/individual_sim_ui/_rotation_tab.scss +++ b/ui/scss/core/components/individual_sim_ui/_rotation_tab.scss @@ -28,6 +28,12 @@ } &.rotation-type-apl { + .rotation-tab-auto { + display: none; + } + .rotation-tab-simple { + display: none; + } .rotation-tab-legacy { display: none; }