From ea9ad0753fed577f89a01f19f59e896295ee10e8 Mon Sep 17 00:00:00 2001 From: James Tanner Date: Sat, 26 Aug 2023 19:24:27 -0700 Subject: [PATCH] Add apl value for aura active with reaction time --- proto/api.proto | 1 + proto/apl.proto | 7 ++++- proto/ui.proto | 5 ++++ sim/core/apl_value.go | 2 ++ sim/core/apl_values_aura.go | 27 +++++++++++++++++++ sim/core/character.go | 1 + sim/core/unit.go | 4 +++ ui/balance_druid/sim.ts | 1 + .../individual_sim_ui/apl_values.ts | 11 ++++++++ .../individual_sim_ui/settings_tab.ts | 13 +++++++++ ui/core/components/other_inputs.ts | 12 +++++++++ ui/core/player.ts | 18 +++++++++++++ 12 files changed, 101 insertions(+), 1 deletion(-) diff --git a/proto/api.proto b/proto/api.proto index 9ce53245ba..5e4cf5c1c4 100644 --- a/proto/api.proto +++ b/proto/api.proto @@ -62,6 +62,7 @@ message Player { APLRotation rotation = 40; + int32 reaction_time_ms = 41; bool in_front_of_target = 23; double distance_from_target = 33; diff --git a/proto/apl.proto b/proto/apl.proto index f7cb156ace..88b34d0512 100644 --- a/proto/apl.proto +++ b/proto/apl.proto @@ -52,7 +52,7 @@ message APLAction { } } -// NextIndex: 50 +// NextIndex: 51 message APLValue { oneof value { // Operators @@ -109,6 +109,7 @@ message APLValue { // Aura values APLValueAuraIsActive aura_is_active = 22; + APLValueAuraIsActiveWithReactionTime aura_is_active_with_reaction_time = 50; APLValueAuraRemainingTime aura_remaining_time = 23; APLValueAuraNumStacks aura_num_stacks = 24; APLValueAuraInternalCooldown aura_internal_cooldown = 39; @@ -346,6 +347,10 @@ message APLValueAuraIsActive { UnitReference source_unit = 2; ActionID aura_id = 1; } +message APLValueAuraIsActiveWithReactionTime { + UnitReference source_unit = 2; + ActionID aura_id = 1; +} message APLValueAuraRemainingTime { UnitReference source_unit = 2; ActionID aura_id = 1; diff --git a/proto/ui.proto b/proto/ui.proto index cd3894f03f..16f3a07e44 100644 --- a/proto/ui.proto +++ b/proto/ui.proto @@ -293,6 +293,11 @@ message SavedSettings { Cooldowns cooldowns = 6; string rotation_json = 8; repeated Profession professions = 9; + + int32 reaction_time_ms = 10; + bool in_front_of_target = 11; + double distance_from_target = 12; + HealingModel healing_model = 13; } message SavedTalents { diff --git a/sim/core/apl_value.go b/sim/core/apl_value.go index 47e3996233..3ac1ea03b3 100644 --- a/sim/core/apl_value.go +++ b/sim/core/apl_value.go @@ -155,6 +155,8 @@ func (rot *APLRotation) newAPLValue(config *proto.APLValue) APLValue { // Auras case *proto.APLValue_AuraIsActive: return rot.newValueAuraIsActive(config.GetAuraIsActive()) + case *proto.APLValue_AuraIsActiveWithReactionTime: + return rot.newValueAuraIsActiveWithReactionTime(config.GetAuraIsActiveWithReactionTime()) case *proto.APLValue_AuraRemainingTime: return rot.newValueAuraRemainingTime(config.GetAuraRemainingTime()) case *proto.APLValue_AuraNumStacks: diff --git a/sim/core/apl_values_aura.go b/sim/core/apl_values_aura.go index 07d23113b6..1f42ef4720 100644 --- a/sim/core/apl_values_aura.go +++ b/sim/core/apl_values_aura.go @@ -31,6 +31,33 @@ func (value *APLValueAuraIsActive) String() string { return fmt.Sprintf("Aura Active(%s)", value.aura.String()) } +type APLValueAuraIsActiveWithReactionTime struct { + DefaultAPLValueImpl + aura AuraReference + reactionTime time.Duration +} + +func (rot *APLRotation) newValueAuraIsActiveWithReactionTime(config *proto.APLValueAuraIsActiveWithReactionTime) APLValue { + aura := rot.GetAPLAura(rot.GetSourceUnit(config.SourceUnit), config.AuraId) + if aura.Get() == nil { + return nil + } + return &APLValueAuraIsActiveWithReactionTime{ + aura: aura, + reactionTime: rot.unit.ReactionTime, + } +} +func (value *APLValueAuraIsActiveWithReactionTime) Type() proto.APLValueType { + return proto.APLValueType_ValueTypeBool +} +func (value *APLValueAuraIsActiveWithReactionTime) GetBool(sim *Simulation) bool { + aura := value.aura.Get() + return aura.IsActive() && aura.TimeActive(sim) >= value.reactionTime +} +func (value *APLValueAuraIsActiveWithReactionTime) String() string { + return fmt.Sprintf("Aura Active With Reaction Time(%s)", value.aura.String()) +} + type APLValueAuraRemainingTime struct { DefaultAPLValueImpl aura AuraReference diff --git a/sim/core/character.go b/sim/core/character.go index 290c34509f..bb30fc7769 100644 --- a/sim/core/character.go +++ b/sim/core/character.go @@ -98,6 +98,7 @@ func NewCharacter(party *Party, partyIndex int, player *proto.Player) Character StatDependencyManager: stats.NewStatDependencyManager(), + ReactionTime: time.Duration(player.ReactionTimeMs) * time.Millisecond, DistanceFromTarget: player.DistanceFromTarget, IsUsingAPL: player.Rotation != nil && player.Rotation.Enabled, }, diff --git a/sim/core/unit.go b/sim/core/unit.go index e444e9001a..e26092fbf2 100644 --- a/sim/core/unit.go +++ b/sim/core/unit.go @@ -49,6 +49,10 @@ type Unit struct { MobType proto.MobType + // Amount of time it takes for the human agent to react to in-game events. + // Used by certain APL values and actions. + ReactionTime time.Duration + // How far this unit is from its target(s). Measured in yards, this is used // for calculating spell travel time for certain spells. DistanceFromTarget float64 diff --git a/ui/balance_druid/sim.ts b/ui/balance_druid/sim.ts index 7fbf4f8ff7..81e77f560c 100644 --- a/ui/balance_druid/sim.ts +++ b/ui/balance_druid/sim.ts @@ -95,6 +95,7 @@ export class BalanceDruidSimUI extends IndividualSimUI { otherInputs: { inputs: [ OtherInputs.TankAssignment, + OtherInputs.ReactionTime, OtherInputs.DistanceFromTarget, ], }, diff --git a/ui/core/components/individual_sim_ui/apl_values.ts b/ui/core/components/individual_sim_ui/apl_values.ts index 49d179ae00..3f1ed0c2bd 100644 --- a/ui/core/components/individual_sim_ui/apl_values.ts +++ b/ui/core/components/individual_sim_ui/apl_values.ts @@ -47,6 +47,7 @@ import { APLValueSpellChannelTime, APLValueSpellCPM, APLValueAuraIsActive, + APLValueAuraIsActiveWithReactionTime, APLValueAuraRemainingTime, APLValueAuraNumStacks, APLValueAuraInternalCooldown, @@ -754,6 +755,16 @@ const valueKindFactories: {[f in NonNullable]: ValueKindConfigTrue if the aura is currently active on self AND it has been active for at least as long as the player reaction time (configured in Settings), otherwise False.', + newValue: APLValueAuraIsActiveWithReactionTime.create, + fields: [ + AplHelpers.unitFieldConfig('sourceUnit', 'aura_sources'), + AplHelpers.actionIdFieldConfig('auraId', 'auras', 'sourceUnit'), + ], + }), 'auraRemainingTime': inputBuilder({ label: 'Aura Remaining Time', submenu: ['Aura'], diff --git a/ui/core/components/individual_sim_ui/settings_tab.ts b/ui/core/components/individual_sim_ui/settings_tab.ts index 0354d648de..3dfc9c5d51 100644 --- a/ui/core/components/individual_sim_ui/settings_tab.ts +++ b/ui/core/components/individual_sim_ui/settings_tab.ts @@ -3,6 +3,7 @@ import { Consumes, Cooldowns, Debuffs, + HealingModel, IndividualBuffs, PartyBuffs, Profession, @@ -394,6 +395,10 @@ export class SettingsTab extends SimTab { consumes: player.getConsumes(), race: player.getRace(), professions: player.getProfessions(), + reactionTimeMs: player.getReactionTime(), + inFrontOfTarget: player.getInFrontOfTarget(), + distanceFromTarget: player.getDistanceFromTarget(), + healingModel: player.getHealingModel(), cooldowns: aplLaunchStatuses[simUI.player.spec] == LaunchStatus.Unlaunched ? player.getCooldowns() : undefined, rotationJson: aplLaunchStatuses[simUI.player.spec] == LaunchStatus.Unlaunched ? JSON.stringify(player.specTypeFunctions.rotationToJson(player.getRotation())) : undefined, }); @@ -410,6 +415,10 @@ export class SettingsTab extends SimTab { simUI.player.setConsumes(eventID, newSettings.consumes || Consumes.create()); simUI.player.setRace(eventID, newSettings.race); simUI.player.setProfessions(eventID, newSettings.professions); + simUI.player.setReactionTime(eventID, newSettings.reactionTimeMs); + simUI.player.setInFrontOfTarget(eventID, newSettings.inFrontOfTarget); + simUI.player.setDistanceFromTarget(eventID, newSettings.distanceFromTarget); + simUI.player.setHealingModel(eventID, newSettings.healingModel || HealingModel.create()); if (aplLaunchStatuses[simUI.player.spec] == LaunchStatus.Unlaunched) { simUI.player.setCooldowns(eventID, newSettings.cooldowns || Cooldowns.create()); if (newSettings.rotationJson) { @@ -426,6 +435,10 @@ export class SettingsTab extends SimTab { this.simUI.player.consumesChangeEmitter, this.simUI.player.raceChangeEmitter, this.simUI.player.professionChangeEmitter, + this.simUI.player.reactionTimeChangeEmitter, + this.simUI.player.inFrontOfTargetChangeEmitter, + this.simUI.player.distanceFromTargetChangeEmitter, + this.simUI.player.healingModelChangeEmitter, ].concat(aplLaunchStatuses[this.simUI.player.spec] == LaunchStatus.Unlaunched ? [ this.simUI.player.cooldownsChangeEmitter, this.simUI.player.rotationChangeEmitter, diff --git a/ui/core/components/other_inputs.ts b/ui/core/components/other_inputs.ts index cc262704c7..cc3e53d508 100644 --- a/ui/core/components/other_inputs.ts +++ b/ui/core/components/other_inputs.ts @@ -82,6 +82,18 @@ export function makePhaseSelector(parent: HTMLElement, sim: Sim): EnumPicker) => TypedEvent.onAny([player.reactionTimeChangeEmitter, player.rotationChangeEmitter]), + getValue: (player: Player) => player.getReactionTime(), + setValue: (eventID: EventID, player: Player, newValue: number) => { + player.setReactionTime(eventID, newValue); + }, + enableWhen: (player: Player) => player.aplRotation.enabled, +}; + export const InFrontOfTarget = { type: 'boolean' as const, label: 'In Front of Target', diff --git a/ui/core/player.ts b/ui/core/player.ts index 65fa7fa84f..079a3dc65f 100644 --- a/ui/core/player.ts +++ b/ui/core/player.ts @@ -205,6 +205,7 @@ export class Player { private glyphs: Glyphs = Glyphs.create(); private specOptions: SpecOptions; private cooldowns: Cooldowns = Cooldowns.create(); + private reactionTime: number = 0; private inFrontOfTarget: boolean = false; private distanceFromTarget: number = 0; private healingModel: HealingModel = HealingModel.create(); @@ -236,6 +237,7 @@ export class Player { readonly glyphsChangeEmitter = new TypedEvent('PlayerGlyphs'); readonly specOptionsChangeEmitter = new TypedEvent('PlayerSpecOptions'); readonly cooldownsChangeEmitter = new TypedEvent('PlayerCooldowns'); + readonly reactionTimeChangeEmitter = new TypedEvent('PlayerReactionTime'); readonly inFrontOfTargetChangeEmitter = new TypedEvent('PlayerInFrontOfTarget'); readonly distanceFromTargetChangeEmitter = new TypedEvent('PlayerDistanceFromTarget'); readonly healingModelChangeEmitter = new TypedEvent('PlayerHealingModel'); @@ -272,6 +274,7 @@ export class Player { this.glyphsChangeEmitter, this.specOptionsChangeEmitter, this.cooldownsChangeEmitter, + this.reactionTimeChangeEmitter, this.inFrontOfTargetChangeEmitter, this.distanceFromTargetChangeEmitter, this.healingModelChangeEmitter, @@ -715,6 +718,18 @@ export class Player { this.specOptionsChangeEmitter.emit(eventID); } + getReactionTime(): number { + return this.reactionTime; + } + + setReactionTime(eventID: EventID, newReactionTime: number) { + if (newReactionTime == this.reactionTime) + return; + + this.reactionTime = newReactionTime; + this.reactionTimeChangeEmitter.emit(eventID); + } + getInFrontOfTarget(): boolean { return this.inFrontOfTarget; } @@ -1104,6 +1119,7 @@ export class Player { rotation: this.aplRotation, profession1: this.getProfession1(), profession2: this.getProfession2(), + reactionTimeMs: this.getReactionTime(), inFrontOfTarget: this.getInFrontOfTarget(), distanceFromTarget: this.getDistanceFromTarget(), healingModel: this.getHealingModel(), @@ -1138,6 +1154,7 @@ export class Player { this.setGlyphs(eventID, proto.glyphs || Glyphs.create()); this.setProfession1(eventID, proto.profession1); this.setProfession2(eventID, proto.profession2); + this.setReactionTime(eventID, proto.reactionTimeMs); this.setInFrontOfTarget(eventID, proto.inFrontOfTarget); this.setDistanceFromTarget(eventID, proto.distanceFromTarget); this.setHealingModel(eventID, proto.healingModel || HealingModel.create()); @@ -1183,6 +1200,7 @@ export class Player { applySharedDefaults(eventID: EventID) { TypedEvent.freezeAllAndDo(() => { + this.setReactionTime(eventID, 200); this.setInFrontOfTarget(eventID, isTankSpec(this.spec)); this.setHealingModel(eventID, HealingModel.create({ burstWindow: isTankSpec(this.spec) ? 6 : 0,