diff --git a/ui/core/components/encounter_picker.ts b/ui/core/components/encounter_picker.ts index 86d44060ce..f1db436079 100644 --- a/ui/core/components/encounter_picker.ts +++ b/ui/core/components/encounter_picker.ts @@ -4,15 +4,17 @@ import { SpellSchool, Stat, Target as TargetProto, + TargetInput, + Target, } from '../proto/common.js'; import { Encounter } from '../encounter.js'; import { Raid } from '../raid.js'; -import { Target } from '../target.js'; import { EventID, TypedEvent } from '../typed_event.js'; import { BooleanPicker } from '../components/boolean_picker.js'; import { EnumPicker } from '../components/enum_picker.js'; -import { ListPicker } from '../components/list_picker.js'; +import { ListItemPickerConfig, ListPicker } from '../components/list_picker.js'; import { NumberPicker } from '../components/number_picker.js'; +import { Stats } from '../proto_utils/stats.js'; import { isHealingSpec, isTankSpec } from '../proto_utils/utils.js'; import { statNames } from '../proto_utils/names.js'; @@ -23,7 +25,6 @@ import { IndividualSimUI } from '../individual_sim_ui.js'; import { SimUI } from '../sim_ui.js'; import { Input } from './input.js'; import { BaseModal } from './base_modal.js'; -import { TargetInputs } from '../target_inputs.js'; export interface EncounterPickerConfig { showExecuteProportion: boolean, @@ -52,13 +53,11 @@ export class EncounterPicker extends Component { }; })), changedEvent: (encounter: Encounter) => encounter.changeEmitter, - getValue: (encounter: Encounter) => presetTargets.findIndex(pe => encounter.primaryTarget.matchesPreset(pe)), + getValue: (encounter: Encounter) => presetTargets.findIndex(pe => equalTargetsIgnoreInputs(encounter.primaryTarget, pe.target)), setValue: (eventID: EventID, encounter: Encounter, newValue: number) => { if (newValue != -1) { - encounter.primaryTarget.applyPreset(eventID, presetTargets[newValue]); + encounter.applyPresetTarget(eventID, presetTargets[newValue], 0); } - - EncounterPicker.updatePrimaryTargetInputs(encounter.primaryTarget); }, }); @@ -119,100 +118,33 @@ export class EncounterPicker extends Component { label: 'Min Base Damage', labelTooltip: 'Base damage for auto attacks, i.e. lowest roll with 0 AP against a 0-armor Player.', changedEvent: (encounter: Encounter) => encounter.changeEmitter, - getValue: (encounter: Encounter) => encounter.primaryTarget.getMinBaseDamage(), + getValue: (encounter: Encounter) => encounter.primaryTarget.minBaseDamage, setValue: (eventID: EventID, encounter: Encounter, newValue: number) => { - encounter.primaryTarget.setMinBaseDamage(eventID, newValue); + encounter.primaryTarget.minBaseDamage = newValue; + encounter.targetsChangeEmitter.emit(eventID); }, }); } + // Transfer Target Inputs from target Id if they dont match (possible when custom AI is selected) + let targetIndex = presetTargets.findIndex(pe => modEncounter.primaryTarget.id == pe.target?.id); + let targetInputs = presetTargets[targetIndex]?.target?.targetInputs || []; + + if (targetInputs.length != modEncounter.primaryTarget.targetInputs.length + || modEncounter.primaryTarget.targetInputs.some((ti, i) => ti.label != targetInputs[i].label)) { + modEncounter.primaryTarget.targetInputs = targetInputs; + modEncounter.targetsChangeEmitter.emit(TypedEvent.nextEventID()); + } + + makeTargetInputsPicker(this.rootElem, modEncounter, 0); + const advancedButton = document.createElement('button'); advancedButton.classList.add('advanced-button', 'btn', 'btn-primary'); advancedButton.textContent = 'Advanced'; advancedButton.addEventListener('click', () => new AdvancedEncounterModal(simUI.rootElem, simUI, modEncounter)); this.rootElem.appendChild(advancedButton); - - // Transfer Target Inputs from target Id if they dont match (possible when custom AI is selected) - let targetIndex = presetTargets.findIndex(pe => modEncounter.primaryTarget.getId() == pe.target?.id); - let targetInputs = new TargetInputs(presetTargets[targetIndex]?.target?.targetInputs); - - if (targetInputs.getLength() != modEncounter.primaryTarget.getTargetInputsLength()) { - modEncounter.primaryTarget.setTargetInputs(TypedEvent.nextEventID(), targetInputs); - } else { - let isDiff = false - for (let i = 0; i < modEncounter.primaryTarget.getTargetInputsLength(); i++) { - if (modEncounter.primaryTarget.getTargetInputs().getTargetInput(i).label != targetInputs.getTargetInput(i)?.label) { - isDiff = true - break; - } - } - - if (isDiff) { - modEncounter.primaryTarget.setTargetInputs(TypedEvent.nextEventID(), targetInputs); - } - } - - EncounterPicker.primaryRootElem = this.rootElem; - EncounterPicker.updatePrimaryTargetInputs(modEncounter.primaryTarget); }); } - - static primaryRootElem: HTMLElement; - static primaryPickers = new Array(); - - static clearTargetInputPickers(targetInputPickers: Array) { - targetInputPickers.forEach(picker => { - picker?.rootElem.remove() - picker?.dispose() - }) - } - - static updatePrimaryTargetInputs(target: Target) { - EncounterPicker.clearTargetInputPickers(EncounterPicker.primaryPickers) - EncounterPicker.primaryPickers = [] - EncounterPicker.rebuildTargetInputs(EncounterPicker.primaryRootElem, target, EncounterPicker.primaryPickers, true) - } - - static rebuildTargetInputs(rootElem: HTMLElement, target: Target, pickers: Array, beforeLast: boolean) { - if (target.hasTargetInputs()) { - for (let index = 0; index < target.getTargetInputsLength(); index++) { - let targetInput = target.getTargetInputs().getTargetInput(index) - if (targetInput.inputType == InputType.Number) { - let numberPicker = new NumberPicker(rootElem, target, { - label: targetInput.label, - labelTooltip: targetInput.tooltip, - changedEvent: (target: Target) => target.propChangeEmitter, - getValue: (target: Target) => target.getTargetInputNumberValue(index), - setValue: (eventID: EventID, target: Target, newValue: number) => { - target.setTargetInputNumberValue(eventID, index, newValue) - }, - }); - if (beforeLast) { - let parent = numberPicker.rootElem.parentElement; - parent?.removeChild(numberPicker.rootElem) - parent?.insertBefore(numberPicker.rootElem, parent.lastChild) - } - pickers.push(numberPicker) - } else if (targetInput.inputType == InputType.Bool) { - let booleanPicker = new BooleanPicker(rootElem, target, { - label: targetInput.label, - labelTooltip: targetInput.tooltip, - changedEvent: (target: Target) => target.propChangeEmitter, - getValue: (target: Target) => target.getTargetInputBooleanValue(index), - setValue: (eventID: EventID, target: Target, newValue: boolean) => { - target.setTargetInputBooleanValue(eventID, index, newValue); - }, - }); - if (beforeLast) { - let parent = booleanPicker.rootElem.parentElement; - parent?.removeChild(booleanPicker.rootElem) - parent?.insertBefore(booleanPicker.rootElem, parent.lastChild) - } - pickers.push(booleanPicker) - } - } - } - } } class AdvancedEncounterModal extends BaseModal { @@ -244,17 +176,18 @@ class AdvancedEncounterModal extends BaseModal { }, }); } - new ListPicker(targetsElem, simUI, this.encounter, { + new ListPicker(targetsElem, this.encounter, { extraCssClasses: ['targets-picker', 'mb-0'], itemLabel: 'Target', changedEvent: (encounter: Encounter) => encounter.targetsChangeEmitter, - getValue: (encounter: Encounter) => encounter.getTargets(), - setValue: (eventID: EventID, encounter: Encounter, newValue: Array) => { - encounter.setTargets(eventID, newValue); + getValue: (encounter: Encounter) => encounter.targets, + setValue: (eventID: EventID, encounter: Encounter, newValue: Array) => { + encounter.targets = newValue; + encounter.targetsChangeEmitter.emit(eventID); }, - newItem: () => Target.fromDefaults(TypedEvent.nextEventID(), this.encounter.sim), - copyItem: (oldItem: Target) => oldItem.clone(TypedEvent.nextEventID()), - newItemPicker: (parent: HTMLElement, target: Target) => new TargetPicker(parent, target), + newItem: () => Encounter.defaultTargetProto(), + copyItem: (oldItem: TargetProto) => TargetProto.clone(oldItem), + newItemPicker: (parent: HTMLElement, listPicker: ListPicker, index: number, config: ListItemPickerConfig) => new TargetPicker(parent, encounter, index, config), }); } @@ -278,30 +211,51 @@ class AdvancedEncounterModal extends BaseModal { if (newValue != -1) { encounter.applyPreset(eventID, presetEncounters[newValue]); } - EncounterPicker.updatePrimaryTargetInputs(encounter.primaryTarget); }, }); } } -class TargetPicker extends Component { - constructor(parent: HTMLElement, modTarget: Target) { - super(parent, 'target-picker-root'); +class TargetPicker extends Input { + private readonly encounter: Encounter; + private readonly targetIndex: number; + + private readonly aiPicker: Input; + private readonly levelPicker: Input; + private readonly mobTypePicker: Input; + private readonly tankIndexPicker: Input; + private readonly statPickers: Array>; + private readonly swingSpeedPicker: Input; + private readonly minBaseDamagePicker: Input; + private readonly dualWieldPicker: Input; + private readonly dwMissPenaltyPicker: Input; + private readonly parryHastePicker: Input; + private readonly spellSchoolPicker: Input; + private readonly suppressDodgePicker: Input; + private readonly tightDamageRangePicker: Input; + private readonly targetInputPickers: ListPicker; + + private getTarget(): TargetProto { + return this.encounter.targets[this.targetIndex] || Target.create(); + } + + constructor(parent: HTMLElement, encounter: Encounter, targetIndex: number, config: ListItemPickerConfig) { + super(parent, 'target-picker-root', encounter, config) + this.encounter = encounter; + this.targetIndex = targetIndex; + this.rootElem.innerHTML = `
`; - const encounter = modTarget.sim.encounter; const section1 = this.rootElem.getElementsByClassName('target-picker-section1')[0] as HTMLElement; const section2 = this.rootElem.getElementsByClassName('target-picker-section2')[0] as HTMLElement; const section3 = this.rootElem.getElementsByClassName('target-picker-section3')[0] as HTMLElement; - let targetInputPickers = new Array(); - - const presetTargets = modTarget.sim.db.getAllPresetTargets(); - new EnumPicker(section1, modTarget, { + const presetTargets = encounter.sim.db.getAllPresetTargets(); + new EnumPicker(section1, null, { extraCssClasses: ['npc-picker'], label: 'NPC', labelTooltip: 'Selects a preset NPC configuration.', @@ -313,24 +267,17 @@ class TargetPicker extends Component { value: i, }; })), - changedEvent: (target: Target) => target.changeEmitter, - getValue: (target: Target) => presetTargets.findIndex(pe => target.matchesPreset(pe)), - setValue: (eventID: EventID, target: Target, newValue: number) => { + changedEvent: () => encounter.targetsChangeEmitter, + getValue: () => presetTargets.findIndex(pe => equalTargetsIgnoreInputs(this.getTarget(), pe.target)), + setValue: (eventID: EventID, _: null, newValue: number) => { if (newValue != -1) { - target.applyPreset(eventID, presetTargets[newValue]); - } - - EncounterPicker.clearTargetInputPickers(targetInputPickers); - targetInputPickers = []; - EncounterPicker.rebuildTargetInputs(section1, target, targetInputPickers, false); - - if (target == encounter.primaryTarget) { - EncounterPicker.updatePrimaryTargetInputs(encounter.primaryTarget); + encounter.applyPresetTarget(eventID, presetTargets[newValue], this.targetIndex); + encounter.targetsChangeEmitter.emit(eventID); } }, }); - new EnumPicker(section1, modTarget, { + this.aiPicker = new EnumPicker(section1, null, { extraCssClasses: ['ai-picker'], label: 'AI', labelTooltip: ` @@ -345,28 +292,20 @@ class TargetPicker extends Component { value: pe.target!.id, }; })), - changedEvent: (target: Target) => target.changeEmitter, - getValue: (target: Target) => target.getId(), - setValue: (eventID: EventID, target: Target, newValue: number) => { - target.setId(eventID, newValue); + changedEvent: () => encounter.targetsChangeEmitter, + getValue: () => this.getTarget().id, + setValue: (eventID: EventID, _: null, newValue: number) => { + const target = this.getTarget(); + target.id = newValue; // Transfer Target Inputs from the AI of the selected target - let newTargetIndex = presetTargets.findIndex(pe => target.getId() == pe.target?.id); - let newTargetInputs = new TargetInputs(presetTargets[newTargetIndex]?.target?.targetInputs); - target.setTargetInputs(eventID, newTargetInputs); - - // Update picker elements - EncounterPicker.clearTargetInputPickers(targetInputPickers); - targetInputPickers = []; - EncounterPicker.rebuildTargetInputs(section1, target, targetInputPickers, false); - - if (target == encounter.primaryTarget) { - EncounterPicker.updatePrimaryTargetInputs(encounter.primaryTarget); - } + target.targetInputs = (presetTargets.find(pe => target.id == pe.target?.id)?.target?.targetInputs || []).map(ti => TargetInput.clone(ti)); + + encounter.targetsChangeEmitter.emit(eventID); }, }); - new EnumPicker(section1, modTarget, { + this.levelPicker = new EnumPicker(section1, null, { label: 'Level', values: [ { name: '83', value: 83 }, @@ -374,22 +313,24 @@ class TargetPicker extends Component { { name: '81', value: 81 }, { name: '80', value: 80 }, ], - changedEvent: (target: Target) => target.levelChangeEmitter, - getValue: (target: Target) => target.getLevel(), - setValue: (eventID: EventID, target: Target, newValue: number) => { - target.setLevel(eventID, newValue); + changedEvent: () => encounter.targetsChangeEmitter, + getValue: () => this.getTarget().level, + setValue: (eventID: EventID, _: null, newValue: number) => { + this.getTarget().level = newValue; + encounter.targetsChangeEmitter.emit(eventID); }, }); - new EnumPicker(section1, modTarget, { + this.mobTypePicker = new EnumPicker(section1, null, { label: 'Mob Type', values: mobTypeEnumValues, - changedEvent: (target: Target) => target.mobTypeChangeEmitter, - getValue: (target: Target) => target.getMobType(), - setValue: (eventID: EventID, target: Target, newValue: number) => { - target.setMobType(eventID, newValue); + changedEvent: () => encounter.targetsChangeEmitter, + getValue: () => this.getTarget().mobType, + setValue: (eventID: EventID, _: null, newValue: number) => { + this.getTarget().mobType = newValue; + encounter.targetsChangeEmitter.emit(eventID); }, }); - new EnumPicker(section1, modTarget, { + this.tankIndexPicker = new EnumPicker(section1, null, { extraCssClasses: ['threat-metrics'], label: 'Tanked By', labelTooltip: 'Determines which player in the raid this enemy will attack. If no player is assigned to the specified tank slot, this enemy will not attack.', @@ -400,81 +341,88 @@ class TargetPicker extends Component { { name: 'Tank 3', value: 2 }, { name: 'Tank 4', value: 3 }, ], - changedEvent: (target: Target) => target.propChangeEmitter, - getValue: (target: Target) => target.getTankIndex(), - setValue: (eventID: EventID, target: Target, newValue: number) => { - target.setTankIndex(eventID, newValue); + changedEvent: () => encounter.targetsChangeEmitter, + getValue: () => this.getTarget().tankIndex, + setValue: (eventID: EventID, _: null, newValue: number) => { + this.getTarget().tankIndex = newValue; + encounter.targetsChangeEmitter.emit(eventID); }, }); - EncounterPicker.rebuildTargetInputs(section1, modTarget, targetInputPickers, false); + this.targetInputPickers = makeTargetInputsPicker(section1, encounter, this.targetIndex); - ALL_TARGET_STATS.forEach(statData => { + this.statPickers = ALL_TARGET_STATS.map(statData => { const stat = statData.stat; - new NumberPicker(section2, modTarget, { + return new NumberPicker(section2, null, { inline: true, extraCssClasses: statData.extraCssClasses, label: statNames[stat], labelTooltip: statData.tooltip, - changedEvent: (target: Target) => target.statsChangeEmitter, - getValue: (target: Target) => target.getStats().getStat(stat), - setValue: (eventID: EventID, target: Target, newValue: number) => { - target.setStats(eventID, target.getStats().withStat(stat, newValue)); + changedEvent: () => encounter.targetsChangeEmitter, + getValue: () => this.getTarget().stats[stat], + setValue: (eventID: EventID, _: null, newValue: number) => { + this.getTarget().stats[stat] = newValue; + encounter.targetsChangeEmitter.emit(eventID); }, }); }); - new NumberPicker(section3, modTarget, { + this.swingSpeedPicker = new NumberPicker(section3, null, { label: 'Swing Speed', labelTooltip: 'Time in seconds between auto attacks. Set to 0 to disable auto attacks.', float: true, - changedEvent: (target: Target) => target.propChangeEmitter, - getValue: (target: Target) => target.getSwingSpeed(), - setValue: (eventID: EventID, target: Target, newValue: number) => { - target.setSwingSpeed(eventID, newValue); + changedEvent: () => encounter.targetsChangeEmitter, + getValue: () => this.getTarget().swingSpeed, + setValue: (eventID: EventID, _: null, newValue: number) => { + this.getTarget().swingSpeed = newValue; + encounter.targetsChangeEmitter.emit(eventID); }, }); - new NumberPicker(section3, modTarget, { + this.minBaseDamagePicker = new NumberPicker(section3, null, { label: 'Min Base Damage', labelTooltip: 'Base damage for auto attacks, i.e. lowest roll with 0 AP against a 0-armor Player.', - changedEvent: (target: Target) => target.propChangeEmitter, - getValue: (target: Target) => target.getMinBaseDamage(), - setValue: (eventID: EventID, target: Target, newValue: number) => { - target.setMinBaseDamage(eventID, newValue); + changedEvent: () => encounter.targetsChangeEmitter, + getValue: () => this.getTarget().minBaseDamage, + setValue: (eventID: EventID, _: null, newValue: number) => { + this.getTarget().minBaseDamage = newValue; + encounter.targetsChangeEmitter.emit(eventID); }, }); - new BooleanPicker(section3, modTarget, { + this.dualWieldPicker = new BooleanPicker(section3, null, { label: 'Dual Wield', labelTooltip: 'Uses 2 separate weapons to attack.', inline: true, - changedEvent: (target: Target) => target.propChangeEmitter, - getValue: (target: Target) => target.getDualWield(), - setValue: (eventID: EventID, target: Target, newValue: boolean) => { - target.setDualWield(eventID, newValue); + changedEvent: () => encounter.targetsChangeEmitter, + getValue: () => this.getTarget().dualWield, + setValue: (eventID: EventID, _: null, newValue: boolean) => { + this.getTarget().dualWield = newValue; + encounter.targetsChangeEmitter.emit(eventID); }, }); - new BooleanPicker(section3, modTarget, { + this.dwMissPenaltyPicker = new BooleanPicker(section3, null, { label: 'DW Miss Penalty', labelTooltip: 'Enables the Dual Wield Miss Penalty (+19% chance to miss) if dual wielding. Bosses in Hyjal/BT/SWP usually have this disabled to stop tanks from avoidance stacking.', inline: true, - changedEvent: (target: Target) => target.changeEmitter, - getValue: (target: Target) => target.getDualWieldPenalty(), - setValue: (eventID: EventID, target: Target, newValue: boolean) => { - target.setDualWieldPenalty(eventID, newValue); + changedEvent: () => encounter.targetsChangeEmitter, + getValue: () => this.getTarget().dualWieldPenalty, + setValue: (eventID: EventID, _: null, newValue: boolean) => { + this.getTarget().dualWieldPenalty = newValue; + encounter.targetsChangeEmitter.emit(eventID); }, - enableWhen: (target: Target) => target.getDualWield(), + enableWhen: () => this.getTarget().dualWield, }); - new BooleanPicker(section3, modTarget, { + this.parryHastePicker = new BooleanPicker(section3, null, { label: 'Parry Haste', labelTooltip: 'Whether this enemy will gain parry haste when parrying attacks.', inline: true, - changedEvent: (target: Target) => target.propChangeEmitter, - getValue: (target: Target) => target.getParryHaste(), - setValue: (eventID: EventID, target: Target, newValue: boolean) => { - target.setParryHaste(eventID, newValue); + changedEvent: () => encounter.targetsChangeEmitter, + getValue: () => this.getTarget().parryHaste, + setValue: (eventID: EventID, _: null, newValue: boolean) => { + this.getTarget().parryHaste = newValue; + encounter.targetsChangeEmitter.emit(eventID); }, }); - new EnumPicker(section3, modTarget, { + this.spellSchoolPicker = new EnumPicker(section3, null, { label: 'Spell School', labelTooltip: 'Type of damage caused by auto attacks. This is usually Physical, but some enemies have elemental attacks.', values: [ @@ -486,35 +434,154 @@ class TargetPicker extends Component { { name: 'Nature', value: SpellSchool.SpellSchoolNature }, { name: 'Shadow', value: SpellSchool.SpellSchoolShadow }, ], - changedEvent: (target: Target) => target.propChangeEmitter, - getValue: (target: Target) => target.getSpellSchool(), - setValue: (eventID: EventID, target: Target, newValue: number) => { - target.setSpellSchool(eventID, newValue); + changedEvent: () => encounter.targetsChangeEmitter, + getValue: () => this.getTarget().spellSchool, + setValue: (eventID: EventID, _: null, newValue: number) => { + this.getTarget().spellSchool = newValue; + encounter.targetsChangeEmitter.emit(eventID); }, }); - new BooleanPicker(section3, modTarget, { + this.suppressDodgePicker = new BooleanPicker(section3, null, { label: 'Chill of the Throne', labelTooltip: 'Reduces the chance for this enemy\'s attacks to be dodged by 20%. Active in Icecrown Citadel.', inline: true, - changedEvent: (target: Target) => target.changeEmitter, - getValue: (target: Target) => target.getSuppressDodge(), - setValue: (eventID: EventID, target: Target, newValue: boolean) => { - target.setSuppressDodge(eventID, newValue); + changedEvent: () => encounter.targetsChangeEmitter, + getValue: () => this.getTarget().suppressDodge, + setValue: (eventID: EventID, _: null, newValue: boolean) => { + this.getTarget().suppressDodge = newValue; + encounter.targetsChangeEmitter.emit(eventID); }, - enableWhen: (target: Target) => target.getLevel() == Mechanics.BOSS_LEVEL, + enableWhen: () => this.getTarget().level == Mechanics.BOSS_LEVEL, }); - new BooleanPicker(section3, modTarget, { + this.tightDamageRangePicker = new BooleanPicker(section3, null, { label: 'Tightened Damage Range', labelTooltip: 'Reduces the damage range of this enemy\'s auto-attacks. Observed behavior for Patchwerk.', inline: true, - changedEvent: (target: Target) => target.changeEmitter, - getValue: (target: Target) => target.getTightEnemyDamage(), - setValue: (eventID: EventID, target: Target, newValue: boolean) => { - target.setTightEnemyDamage(eventID, newValue); + changedEvent: () => encounter.targetsChangeEmitter, + getValue: () => this.getTarget().tightEnemyDamage, + setValue: (eventID: EventID, _: null, newValue: boolean) => { + this.getTarget().tightEnemyDamage = newValue; + encounter.targetsChangeEmitter.emit(eventID); }, - enableWhen: (target: Target) => target.getLevel() == Mechanics.BOSS_LEVEL, + enableWhen: () => this.getTarget().level == Mechanics.BOSS_LEVEL, + }); + + this.init(); + } + + getInputElem(): HTMLElement|null { + return null; + } + getInputValue(): TargetProto { + return TargetProto.create({ + id: this.aiPicker.getInputValue(), + level: this.levelPicker.getInputValue(), + mobType: this.mobTypePicker.getInputValue(), + tankIndex: this.tankIndexPicker.getInputValue(), + swingSpeed: this.swingSpeedPicker.getInputValue(), + minBaseDamage: this.minBaseDamagePicker.getInputValue(), + suppressDodge: this.suppressDodgePicker.getInputValue(), + dualWield: this.dualWieldPicker.getInputValue(), + dualWieldPenalty: this.dwMissPenaltyPicker.getInputValue(), + parryHaste: this.parryHastePicker.getInputValue(), + spellSchool: this.spellSchoolPicker.getInputValue(), + tightEnemyDamage: this.tightDamageRangePicker.getInputValue(), + stats: this.statPickers + .map(picker => picker.getInputValue()) + .map((statValue, i) => new Stats().withStat(ALL_TARGET_STATS[i].stat, statValue)) + .reduce((totalStats, curStats) => totalStats.add(curStats)).asArray(), + targetInputs: this.targetInputPickers.getInputValue(), }); } + setInputValue(newValue: TargetProto) { + if (!newValue) { + return; + } + this.aiPicker.setInputValue(newValue.id); + this.levelPicker.setInputValue(newValue.level); + this.mobTypePicker.setInputValue(newValue.mobType); + this.tankIndexPicker.setInputValue(newValue.tankIndex); + this.swingSpeedPicker.setInputValue(newValue.swingSpeed); + this.minBaseDamagePicker.setInputValue(newValue.minBaseDamage); + this.suppressDodgePicker.setInputValue(newValue.suppressDodge); + this.dualWieldPicker.setInputValue(newValue.dualWield); + this.dwMissPenaltyPicker.setInputValue(newValue.dualWieldPenalty); + this.parryHastePicker.setInputValue(newValue.parryHaste); + this.spellSchoolPicker.setInputValue(newValue.spellSchool); + this.tightDamageRangePicker.setInputValue(newValue.tightEnemyDamage); + ALL_TARGET_STATS.forEach((statData, i) => this.statPickers[i].setInputValue(newValue.stats[statData.stat])); + this.targetInputPickers.setInputValue(newValue.targetInputs); + } +} + +class TargetInputPicker extends Input { + private readonly encounter: Encounter; + private readonly targetIndex: number; + private readonly targetInputIndex: number; + + private boolPicker: Input|null; + private numberPicker: Input|null; + + private getTargetInput(): TargetInput { + return this.encounter.targets[this.targetIndex].targetInputs[this.targetInputIndex] || TargetInput.create(); + } + + constructor(parent: HTMLElement, encounter: Encounter, targetIndex: number, targetInputIndex: number, config: ListItemPickerConfig) { + super(parent, 'target-input-picker-root', encounter, config) + this.encounter = encounter; + this.targetIndex = targetIndex; + this.targetInputIndex = targetInputIndex; + + this.boolPicker = null; + this.numberPicker = null; + this.init(); + } + + getInputElem(): HTMLElement|null { + return this.rootElem; + } + getInputValue(): TargetInput { + return TargetInput.create({ + boolValue: this.boolPicker ? this.boolPicker.getInputValue() : undefined, + numberValue: this.numberPicker ? this.numberPicker.getInputValue() : undefined, + }); + } + setInputValue(newValue: TargetInput) { + if (!newValue) { + return; + } + if (newValue.inputType == InputType.Number && !this.numberPicker) { + if (this.boolPicker) { + this.boolPicker.rootElem.remove(); + this.boolPicker = null; + } + this.numberPicker = new NumberPicker(this.rootElem, null, { + label: newValue.label, + labelTooltip: newValue.tooltip, + changedEvent: () => this.encounter.targetsChangeEmitter, + getValue: () => this.getTargetInput().numberValue, + setValue: (eventID: EventID, _: null, newValue: number) => { + this.getTargetInput().numberValue = newValue; + this.encounter.targetsChangeEmitter.emit(eventID); + }, + }); + } else if (newValue.inputType == InputType.Bool && !this.boolPicker) { + if (this.numberPicker) { + this.numberPicker.rootElem.remove(); + this.numberPicker = null; + } + this.boolPicker = new BooleanPicker(this.rootElem, null, { + label: newValue.label, + labelTooltip: newValue.tooltip, + changedEvent: () => this.encounter.targetsChangeEmitter, + getValue: () => this.getTargetInput().boolValue, + setValue: (eventID: EventID, _: null, newValue: boolean) => { + this.getTargetInput().boolValue = newValue; + this.encounter.targetsChangeEmitter.emit(eventID); + }, + }); + } + } } function addEncounterFieldPickers(rootElem: HTMLElement, encounter: Encounter, showExecuteProportion: boolean) { @@ -580,6 +647,34 @@ function addEncounterFieldPickers(rootElem: HTMLElement, encounter: Encounter, s } } +function makeTargetInputsPicker(parent: HTMLElement, encounter: Encounter, targetIndex: number): ListPicker { + return new ListPicker(parent, encounter, { + itemLabel: 'Target Input', + changedEvent: (encounter: Encounter) => encounter.targetsChangeEmitter, + getValue: (encounter: Encounter) => encounter.targets[targetIndex].targetInputs, + setValue: (eventID: EventID, encounter: Encounter, newValue: Array) => { + encounter.targets[targetIndex].targetInputs = newValue; + encounter.targetsChangeEmitter.emit(eventID); + }, + newItem: () => TargetInput.create(), + copyItem: (oldItem: TargetInput) => TargetInput.clone(oldItem), + newItemPicker: (parent: HTMLElement, listPicker: ListPicker, index: number, config: ListItemPickerConfig) => new TargetInputPicker(parent, encounter, targetIndex, index, config), + hideUi: true, + }); +} + +function equalTargetsIgnoreInputs(target1: TargetProto|undefined, target2: TargetProto|undefined): boolean { + if ((target1 == null) != (target2 == null)) { + return false; + } + if (target1 == null) { + return true; + } + const modTarget2 = TargetProto.clone(target2!); + modTarget2.targetInputs = target1.targetInputs; + return TargetProto.equals(target1, modTarget2); +} + const ALL_TARGET_STATS: Array<{ stat: Stat, tooltip: string, extraCssClasses: Array }> = [ { stat: Stat.StatHealth, tooltip: '', extraCssClasses: [] }, { stat: Stat.StatArmor, tooltip: '', extraCssClasses: [] }, diff --git a/ui/core/components/icon_inputs.ts b/ui/core/components/icon_inputs.ts index 0e589be5ca..ec74f5e710 100644 --- a/ui/core/components/icon_inputs.ts +++ b/ui/core/components/icon_inputs.ts @@ -18,9 +18,6 @@ import { TristateEffect } from '../proto/common.js'; import { Party } from '../party.js'; import { Player } from '../player.js'; import { Raid } from '../raid.js'; -import { Sim } from '../sim.js'; -import { Target } from '../target.js'; -import { Encounter } from '../encounter.js'; import { EventID, TypedEvent } from '../typed_event.js'; import { IconPicker, IconPickerConfig } from './icon_picker.js'; diff --git a/ui/core/components/individual_sim_ui/apl_rotation_picker.ts b/ui/core/components/individual_sim_ui/apl_rotation_picker.ts new file mode 100644 index 0000000000..79944426f0 --- /dev/null +++ b/ui/core/components/individual_sim_ui/apl_rotation_picker.ts @@ -0,0 +1,118 @@ +import { Spec } from '../../proto/common.js'; +import { EventID } from '../../typed_event.js'; +import { Player } from '../../player.js'; +import { IconEnumPicker, IconEnumValueConfig } from '../icon_enum_picker.js'; +import { BooleanPicker } from '../boolean_picker.js'; +import { ListItemPickerConfig, ListPicker } from '../list_picker.js'; +import { NumberPicker } from '../number_picker.js'; +import { StringPicker } from '../string_picker.js'; +import { APLListItem, APLAction } from '../../proto/apl.js'; + +import { Component } from '../component.js'; +import { Input } from '../input.js'; +import { SimUI } from 'ui/core/sim_ui.js'; + +export class APLRotationPicker extends Component { + constructor(parent: HTMLElement, simUI: SimUI, modPlayer: Player) { + super(parent, 'apl-rotation-picker-root'); + + new ListPicker, APLListItem>(this.rootElem, modPlayer, { + extraCssClasses: ['apl-list-item-picker'], + title: 'Priority List', + titleTooltip: 'At each decision point, the simulation will perform the first valid action from this list.', + itemLabel: 'Action', + changedEvent: (player: Player) => player.rotationChangeEmitter, + getValue: (player: Player) => player.aplRotation.priorityList, + setValue: (eventID: EventID, player: Player, newValue: Array) => { + player.aplRotation.priorityList = newValue; + player.rotationChangeEmitter.emit(eventID); + }, + newItem: () => APLListItem.create(), + copyItem: (oldItem: APLListItem) => APLListItem.clone(oldItem), + newItemPicker: (parent: HTMLElement, listPicker: ListPicker, APLListItem>, index: number, config: ListItemPickerConfig, APLListItem>) => new APLListItemPicker(parent, modPlayer, listPicker, index, config), + //inlineMenuBar: true, + }); + } +} + +class APLListItemPicker extends Input, APLListItem> { + private readonly player: Player; + private readonly listPicker: ListPicker, APLListItem>; + private readonly itemIndex: number; + + private readonly hidePicker: Input; + private readonly notesPicker: Input; + + private getItem(): APLListItem { + return this.player.aplRotation.priorityList[this.itemIndex] || APLListItem.create(); + } + + constructor(parent: HTMLElement, player: Player, listPicker: ListPicker, APLListItem>, itemIndex: number, config: ListItemPickerConfig, APLListItem>) { + super(parent, 'apl-list-item-picker-root', player, config); + this.player = player; + this.listPicker = listPicker; + this.itemIndex = itemIndex; + + this.hidePicker = new BooleanPicker(this.rootElem, null, { + label: 'Hide', + labelTooltip: 'Ignores this APL action.', + inline: true, + changedEvent: () => this.player.rotationChangeEmitter, + getValue: () => this.getItem().hide, + setValue: (eventID: EventID, _: null, newValue: boolean) => { + this.getItem().hide = newValue; + this.player.rotationChangeEmitter.emit(eventID); + }, + }); + + this.notesPicker = new StringPicker(this.rootElem, null, { + label: 'Notes', + labelTooltip: 'Description for this action. The sim will ignore this value, it\'s just to allow self-documentation.', + inline: true, + changedEvent: () => this.player.rotationChangeEmitter, + getValue: () => this.getItem().notes, + setValue: (eventID: EventID, _: null, newValue: string) => { + this.getItem().notes = newValue; + this.player.rotationChangeEmitter.emit(eventID); + }, + }); + + //new IconEnumPicker(this.rootElem, modSpell, { + // numColumns: config.numColumns, + // values: config.values.map(value => { + // if (value.showWhen) { + // const oldShowWhen = value.showWhen; + // value.showWhen = ((spell: CustomSpell) => oldShowWhen(player)) as unknown as ((player: Player) => boolean); + // } + // return value; + // }) as unknown as Array>, + // equals: (a: number, b: number) => a == b, + // zeroValue: 0, + // changedEvent: (spell: CustomSpell) => player.changeEmitter, + // getValue: (spell: CustomSpell) => spell.spell, + // setValue: (eventID: EventID, spell: CustomSpell, newValue: number) => { + // spell.spell = newValue; + // this.setSpell(eventID, spell); + // }, + //}); + } + + getInputElem(): HTMLElement | null { + return this.rootElem; + } + + getInputValue(): APLListItem { + return APLListItem.create({ + hide: this.hidePicker.getInputValue(), + notes: this.notesPicker.getInputValue(), + }) + } + + setInputValue(newValue: APLListItem) { + if (!newValue) { + return; + } + this.hidePicker.setInputValue(newValue.hide); + this.notesPicker.setInputValue(newValue.notes); + } +} diff --git a/ui/core/components/individual_sim_ui/custom_rotation_picker.ts b/ui/core/components/individual_sim_ui/custom_rotation_picker.ts index bcf1232631..4654b0b3b9 100644 --- a/ui/core/components/individual_sim_ui/custom_rotation_picker.ts +++ b/ui/core/components/individual_sim_ui/custom_rotation_picker.ts @@ -3,10 +3,11 @@ import { CustomRotation, CustomSpell } from '../../proto/common.js'; import { EventID } from '../../typed_event.js'; import { Player } from '../../player.js'; import { IconEnumPicker, IconEnumValueConfig } from '../icon_enum_picker.js'; -import { ListPicker } from '../list_picker.js'; +import { ListItemPickerConfig, ListPicker } from '../list_picker.js'; import { NumberPicker } from '../number_picker.js'; import { Component } from '../component.js'; +import { Input } from '../input.js'; import { SimUI } from 'ui/core/sim_ui.js'; export interface CustomRotationPickerConfig { @@ -28,7 +29,7 @@ export class CustomRotationPicker extends Component { if (config.extraCssClasses) this.rootElem.classList.add(...config.extraCssClasses); - new ListPicker, CustomSpell, CustomSpellPicker>(this.rootElem, simUI, modPlayer, { + new ListPicker, CustomSpell>(this.rootElem, modPlayer, { extraCssClasses: ['custom-spells-picker'], title: 'Spell Priority', titleTooltip: 'Spells at the top of the list are prioritized first. Safely ignores untalented options.', @@ -42,52 +43,62 @@ export class CustomRotationPicker extends Component { }, newItem: () => CustomSpell.create(), copyItem: (oldItem: CustomSpell) => CustomSpell.clone(oldItem), - newItemPicker: (parent: HTMLElement, newItem: CustomSpell, listPicker: ListPicker, CustomSpell, CustomSpellPicker>) => new CustomSpellPicker(parent, modPlayer, newItem, config, listPicker), + newItemPicker: (parent: HTMLElement, listPicker: ListPicker, CustomSpell>, index: number, itemConfig: ListItemPickerConfig, CustomSpell>) => new CustomSpellPicker(parent, modPlayer, index, itemConfig, listPicker, config), inlineMenuBar: true, showWhen: config.showWhen, }); } } -class CustomSpellPicker extends Component { +class CustomSpellPicker extends Input, CustomSpell> { private readonly player: Player; - private readonly config: CustomRotationPickerConfig; - private readonly listPicker: ListPicker, CustomSpell, CustomSpellPicker>; + private readonly listPicker: ListPicker, CustomSpell>; + private readonly spellIndex: number; - constructor(parent: HTMLElement, player: Player, modSpell: CustomSpell, config: CustomRotationPickerConfig, listPicker: ListPicker, CustomSpell, CustomSpellPicker>) { - super(parent, 'custom-spell-picker-root'); + private readonly spellPicker: Input; + private readonly cpmPicker: Input|null; + + getSpell(): CustomSpell { + return this.listPicker.config.getValue(this.player)[this.spellIndex] || CustomSpell.create(); + } + + constructor(parent: HTMLElement, player: Player, spellIndex: number, config: ListItemPickerConfig, CustomSpell>, listPicker: ListPicker, CustomSpell>, crConfig: CustomRotationPickerConfig) { + super(parent, 'custom-spell-picker-root', player, config); this.player = player; - this.config = config; this.listPicker = listPicker; + this.spellIndex = spellIndex; - new IconEnumPicker(this.rootElem, modSpell, { - numColumns: config.numColumns, - values: config.values.map(value => { + this.spellPicker = new IconEnumPicker(this.rootElem, null, { + numColumns: crConfig.numColumns, + values: crConfig.values.map(value => { if (value.showWhen) { const oldShowWhen = value.showWhen; - value.showWhen = ((spell: CustomSpell) => oldShowWhen(player)) as unknown as ((player: Player) => boolean); + value.showWhen = (() => oldShowWhen(player)) as unknown as ((player: Player) => boolean); } return value; - }) as unknown as Array>, + }) as unknown as Array>, equals: (a: number, b: number) => a == b, zeroValue: 0, - changedEvent: (spell: CustomSpell) => player.changeEmitter, - getValue: (spell: CustomSpell) => spell.spell, - setValue: (eventID: EventID, spell: CustomSpell, newValue: number) => { + changedEvent: () => player.changeEmitter, + getValue: () => this.getSpell().spell, + setValue: (eventID: EventID, _: null, newValue: number) => { + const spell = this.getSpell(); spell.spell = newValue; this.setSpell(eventID, spell); }, }); - if (config.showCastsPerMinute) { - new NumberPicker(this.rootElem, modSpell, { + this.cpmPicker = null; + if (crConfig.showCastsPerMinute) { + this.cpmPicker = new NumberPicker(this.rootElem, null, { label: 'CPM', labelTooltip: 'Desired Casts-Per-Minute for this spell.', float: true, positive: true, - changedEvent: (spell: CustomSpell) => player.changeEmitter, - getValue: (spell: CustomSpell) => spell.castsPerMinute, - setValue: (eventID: EventID, spell: CustomSpell, newValue: number) => { + changedEvent: () => player.changeEmitter, + getValue: () => this.getSpell().castsPerMinute, + setValue: (eventID: EventID, _: null, newValue: number) => { + const spell = this.getSpell(); spell.castsPerMinute = newValue; this.setSpell(eventID, spell); }, @@ -95,10 +106,30 @@ class CustomSpellPicker extends Component { } } + getInputElem(): HTMLElement | null { + return this.rootElem; + } + + getInputValue(): CustomSpell { + return CustomSpell.create({ + spell: this.spellPicker.getInputValue(), + castsPerMinute: this.cpmPicker?.getInputValue(), + }); + } + + setInputValue(newValue: CustomSpell) { + if (!newValue) { + return; + } + this.spellPicker.setInputValue(newValue.spell); + if (this.cpmPicker) { + this.cpmPicker.setInputValue(newValue.castsPerMinute); + } + } + private setSpell(eventID: EventID, spell: CustomSpell) { - const index = this.listPicker.getPickerIndex(this); - const cr = this.config.getValue(this.player); - cr.spells[index] = CustomSpell.clone(spell); - this.config.setValue(eventID, this.player, cr); + const customSpells = this.listPicker.config.getValue(this.player); + customSpells[this.spellIndex] = CustomSpell.clone(spell); + this.listPicker.config.setValue(eventID, this.player, customSpells); } } diff --git a/ui/core/components/individual_sim_ui/rotation_tab.ts b/ui/core/components/individual_sim_ui/rotation_tab.ts new file mode 100644 index 0000000000..258fc567ab --- /dev/null +++ b/ui/core/components/individual_sim_ui/rotation_tab.ts @@ -0,0 +1,82 @@ +import { IndividualSimUI } from "../../individual_sim_ui"; +import { + Consumes, + Cooldowns, + Debuffs, + IndividualBuffs, + PartyBuffs, + Profession, + RaidBuffs, + Spec, + Stat +} from "../../proto/common"; +import { ActionId } from "../../proto_utils/action_id"; +import { EventID, TypedEvent } from "../../typed_event"; +import { getEnumValues } from "../../utils"; +import { Player } from "../../player"; + +import { SimTab } from "../sim_tab"; +import { NumberPicker } from "../number_picker"; +import { BooleanPicker } from "../boolean_picker"; +import { EnumPicker } from "../enum_picker"; +import { Input } from "../input"; +import { MultiIconPicker } from "../multi_icon_picker"; +import { IconPickerConfig } from "../icon_picker"; +import { TypedIconPickerConfig } from "../input_helpers"; + +import { APLRotationPicker } from "./apl_rotation_picker"; + +export class RotationTab extends SimTab { + protected simUI: IndividualSimUI; + + readonly leftPanel: HTMLElement; + readonly rightPanel: HTMLElement; + + constructor(parentElem: HTMLElement, simUI: IndividualSimUI) { + super(parentElem, simUI, {identifier: 'rotation-tab', title: 'Rotation'}); + this.rootElem.classList.add('experimental'); + this.simUI = simUI; + + this.leftPanel = document.createElement('div'); + this.leftPanel.classList.add('rotation-tab-left', 'tab-panel-left'); + + this.rightPanel = document.createElement('div'); + this.rightPanel.classList.add('rotation-tab-right', 'tab-panel-right'); + + this.contentContainer.appendChild(this.leftPanel); + this.contentContainer.appendChild(this.rightPanel); + + this.buildTabContent(); + } + + protected buildTabContent() { + this.buildHeader(); + this.buildContent(); + } + + private buildHeader() { + const header = document.createElement('div'); + header.classList.add('rotation-tab-header'); + this.leftPanel.appendChild(header); + + new BooleanPicker(header, this.simUI.player, { + label: 'Use APL Rotation', + labelTooltip: 'Enables the APL Rotation options.', + inline: true, + changedEvent: (player: Player) => player.rotationChangeEmitter, + getValue: (player: Player) => player.aplRotation.enabled, + setValue: (eventID: EventID, player: Player, newValue: boolean) => { + player.aplRotation.enabled = newValue; + player.rotationChangeEmitter.emit(eventID); + }, + }); + } + + private buildContent() { + const content = document.createElement('div'); + content.classList.add('rotation-tab-main'); + this.leftPanel.appendChild(content); + + new APLRotationPicker(content, this.simUI, this.simUI.player); + } +} diff --git a/ui/core/components/input.ts b/ui/core/components/input.ts index f478432e66..2397cf73e8 100644 --- a/ui/core/components/input.ts +++ b/ui/core/components/input.ts @@ -82,11 +82,11 @@ export abstract class Input extends Component { if (enable) { this.enabled = true; this.rootElem.classList.remove('disabled'); - this.getInputElem().removeAttribute('disabled'); + this.getInputElem()?.removeAttribute('disabled'); } else { this.enabled = false; this.rootElem.classList.add('disabled'); - this.getInputElem().setAttribute('disabled', ''); + this.getInputElem()?.setAttribute('disabled', ''); } const show = !this.inputConfig.showWhen || this.inputConfig.showWhen(this.modObject); @@ -107,7 +107,7 @@ export abstract class Input extends Component { this.update(); } - abstract getInputElem(): HTMLElement; + abstract getInputElem(): HTMLElement|null; abstract getInputValue(): T; diff --git a/ui/core/components/input_helpers.ts b/ui/core/components/input_helpers.ts index 8ee9be5a0d..2b6b012ccb 100644 --- a/ui/core/components/input_helpers.ts +++ b/ui/core/components/input_helpers.ts @@ -1,25 +1,17 @@ import { ActionId } from '../proto_utils/action_id.js'; import { CustomRotation, ItemSwap, ItemSlot } from '../proto/common.js'; import { Spec } from '../proto/common.js'; -import { TristateEffect } from '../proto/common.js'; -import { Party } from '../party.js'; import { Player } from '../player.js'; -import { Raid } from '../raid.js'; -import { Sim } from '../sim.js'; -import { Target } from '../target.js'; -import { Encounter } from '../encounter.js'; import { EventID, TypedEvent } from '../typed_event.js'; import { SpecOptions, SpecRotation } from '../proto_utils/utils.js'; import { ItemSwapPickerConfig } from './item_swap_picker.js' import { CustomRotationPickerConfig } from './individual_sim_ui/custom_rotation_picker.js'; -import { InputConfig } from './input.js' import { IconPickerConfig } from './icon_picker.js'; import { IconEnumPicker, IconEnumPickerConfig, IconEnumValueConfig } from './icon_enum_picker.js'; import { EnumPickerConfig, EnumValueConfig } from './enum_picker.js'; import { BooleanPickerConfig } from './boolean_picker.js'; import { NumberPickerConfig } from './number_picker.js'; import { MultiIconPickerConfig } from './multi_icon_picker.js'; -import { playerTalentStringToProto } from '../talents/factory.js'; export function makeMultiIconInput(inputs: Array>, label: string, numColumns?: number): MultiIconPickerConfig { return { diff --git a/ui/core/components/list_picker.ts b/ui/core/components/list_picker.ts index 501d382115..8f99c001ba 100644 --- a/ui/core/components/list_picker.ts +++ b/ui/core/components/list_picker.ts @@ -1,33 +1,35 @@ import { Tooltip } from 'bootstrap'; -import { SimUI } from '../sim_ui.js'; import { EventID, TypedEvent } from '../typed_event.js'; -import { arrayEquals, swap } from '../utils.js'; +import { swap } from '../utils.js'; import { Input, InputConfig } from './input.js'; -export interface ListPickerConfig extends InputConfig> { +export interface ListPickerConfig extends InputConfig> { title?: string, titleTooltip?: string, itemLabel: string, newItem: () => ItemType, copyItem: (oldItem: ItemType) => ItemType, - newItemPicker: (parent: HTMLElement, item: ItemType, listPicker: ListPicker) => ItemPicker, + newItemPicker: (parent: HTMLElement, listPicker: ListPicker, index: number, config: ListItemPickerConfig) => Input, inlineMenuBar?: boolean, + hideUi?: boolean, } -interface ItemPickerPair { - item: ItemType, +export interface ListItemPickerConfig extends InputConfig { +} + +interface ItemPickerPair { elem: HTMLElement, - picker: ItemPicker, + picker: Input, } -export class ListPicker extends Input> { - private readonly config: ListPickerConfig; +export class ListPicker extends Input> { + readonly config: ListPickerConfig; private readonly itemsDiv: HTMLElement; - private itemPickerPairs: Array>; + private itemPickerPairs: Array>; - constructor(parent: HTMLElement, simUI: SimUI, modObject: ModObject, config: ListPickerConfig) { + constructor(parent: HTMLElement, modObject: ModObject, config: ListPickerConfig) { super(parent, 'list-picker-root', modObject, config); this.config = config; this.itemPickerPairs = []; @@ -44,6 +46,10 @@ export class ListPicker extends InputNew ${config.itemLabel} `; + if (this.config.hideUi) { + this.rootElem.classList.add('hide-ui'); + } + if (this.config.titleTooltip) Tooltip.getOrCreateInstance(this.rootElem.querySelector('.list-picker-title') as HTMLElement); @@ -64,37 +70,27 @@ export class ListPicker extends Input { - return this.itemPickerPairs.map(pair => pair.item); + return this.itemPickerPairs.map(pair => pair.picker.getInputValue()); } setInputValue(newValue: Array): void { - // Remove items that are no longer in the list. - const removePairs = this.itemPickerPairs.filter(ipp => !newValue.includes(ipp.item)); - removePairs.forEach(ipp => ipp.elem.remove()); - this.itemPickerPairs = this.itemPickerPairs.filter(ipp => !removePairs.includes(ipp)); - - // Add items that were missing. - const curItems = this.getInputValue(); - newValue - .filter(newItem => !curItems.includes(newItem)) - .forEach(newItem => this.addNewPicker(newItem)); - - // Reorder to match the new list. - this.itemPickerPairs = newValue.map(item => this.itemPickerPairs.find(ipp => ipp.item == item)!); - - // Reorder item picker elements in the DOM if necessary. - const curPickerElems = Array.from(this.itemsDiv.children); - if (!curPickerElems.every((elem, i) => elem == this.itemPickerPairs[i].elem)) { - this.itemPickerPairs.forEach(ipp => ipp.elem.remove()); - this.itemPickerPairs.forEach(ipp => this.itemsDiv.appendChild(ipp.elem)); + // Add/remove pickers to make the lengths match. + if (newValue.length < this.itemPickerPairs.length) { + this.itemPickerPairs.slice(newValue.length).forEach(ipp => ipp.elem.remove()); + this.itemPickerPairs = this.itemPickerPairs.slice(0, newValue.length); + } else if (newValue.length > this.itemPickerPairs.length) { + const numToAdd = newValue.length - this.itemPickerPairs.length; + for (let i = 0; i < numToAdd; i++) { + this.addNewPicker(); + } } - } - getPickerIndex(picker: ItemPicker): number { - return this.itemPickerPairs.findIndex(ipp => ipp.picker == picker); + // Set all the values. + newValue.forEach((val, i) => this.itemPickerPairs[i].picker.setInputValue(val)) } - private addNewPicker(item: ItemType) { + private addNewPicker() { + const index = this.itemPickerPairs.length; const itemContainer = document.createElement('div'); itemContainer.classList.add('list-picker-item-container'); if (this.config.inlineMenuBar) { @@ -126,11 +122,6 @@ export class ListPicker extends Input { - const index = this.itemPickerPairs.findIndex(ipp => ipp.item == item); - if (index == -1) { - console.error('Could not find list picker item!'); - return; - } if (index == 0) { return; } @@ -145,11 +136,6 @@ export class ListPicker extends Input { - const index = this.itemPickerPairs.findIndex(ipp => ipp.item == item); - if (index == -1) { - console.error('Could not find list picker item!'); - return; - } if (index == this.itemPickerPairs.length - 1) { return; } @@ -164,14 +150,8 @@ export class ListPicker extends Input { - const index = this.itemPickerPairs.findIndex(ipp => ipp.item == item); - if (index == -1) { - console.error('Could not find list picker item!'); - return; - } - - const copiedItem = this.config.copyItem(item); - const newList = this.config.getValue(this.modObject).concat([copiedItem]); + const newList = this.config.getValue(this.modObject) + newList.push(this.config.copyItem(newList[index])); this.config.setValue(TypedEvent.nextEventID(), this.modObject, newList); copyButtonTooltip.hide(); }); @@ -180,12 +160,6 @@ export class ListPicker extends Input { - const index = this.itemPickerPairs.findIndex(ipp => ipp.item == item); - if (index == -1) { - console.error('Could not find list picker item!'); - return; - } - const newList = this.config.getValue(this.modObject); newList.splice(index, 1); this.config.setValue(TypedEvent.nextEventID(), this.modObject, newList); @@ -193,9 +167,17 @@ export class ListPicker extends Input this.config.getValue(modObj)[index], + setValue: (eventID: EventID, modObj: ModObject, newValue: ItemType) => { + const newList = this.config.getValue(modObj); + newList[index] = newValue; + this.config.setValue(eventID, modObj, newList); + }, + }); this.itemsDiv.appendChild(itemContainer); - this.itemPickerPairs.push({ item: item, elem: itemContainer, picker: itemPicker }); + this.itemPickerPairs.push({ elem: itemContainer, picker: itemPicker }); } } diff --git a/ui/core/components/other_inputs.ts b/ui/core/components/other_inputs.ts index 776b91cd3d..7426d99658 100644 --- a/ui/core/components/other_inputs.ts +++ b/ui/core/components/other_inputs.ts @@ -1,16 +1,8 @@ import { BooleanPicker } from '../components/boolean_picker.js'; -import { EnumPicker, EnumPickerConfig } from '../components/enum_picker.js'; -import { Conjured } from '../proto/common.js'; +import { EnumPicker } from '../components/enum_picker.js'; import { RaidTarget } from '../proto/common.js'; -import { TristateEffect } from '../proto/common.js'; -import { Party } from '../party.js'; import { Player } from '../player.js'; import { Sim } from '../sim.js'; -import { Target } from '../target.js'; -import { Encounter } from '../encounter.js'; -import { Raid } from '../raid.js'; -import { SimUI } from '../sim_ui.js'; -import { IndividualSimUI } from '../individual_sim_ui.js'; import { EventID, TypedEvent } from '../typed_event.js'; import { emptyRaidTarget } from '../proto_utils/utils.js'; diff --git a/ui/core/encounter.ts b/ui/core/encounter.ts index 5f88749481..ad4946d85d 100644 --- a/ui/core/encounter.ts +++ b/ui/core/encounter.ts @@ -1,11 +1,15 @@ -import { Encounter as EncounterProto } from './proto/common.js'; -import { MobType } from './proto/common.js'; -import { Stat } from './proto/common.js'; -import { Target as TargetProto } from './proto/common.js'; -import { PresetEncounter } from './proto/common.js'; -import { PresetTarget } from './proto/common.js'; -import { Target } from './target.js'; +import { + Encounter as EncounterProto, + MobType, + SpellSchool, + Stat, + Target as TargetProto, + TargetInput, + PresetEncounter, + PresetTarget, +} from './proto/common.js'; import { Stats } from './proto_utils/stats.js'; +import * as Mechanics from './constants/mechanics.js'; import { Sim } from './sim.js'; import { EventID, TypedEvent } from './typed_event.js'; @@ -20,7 +24,7 @@ export class Encounter { private executeProportion25: number = 0.25; private executeProportion35: number = 0.35; private useHealth: boolean = false; - private targets: Array; + targets: Array; readonly targetsChangeEmitter = new TypedEvent(); readonly durationChangeEmitter = new TypedEvent(); @@ -31,7 +35,7 @@ export class Encounter { constructor(sim: Sim) { this.sim = sim; - this.targets = [Target.fromDefaults(TypedEvent.nextEventID(), sim)]; + this.targets = [Encounter.defaultTargetProto()]; [ this.targetsChangeEmitter, @@ -40,8 +44,8 @@ export class Encounter { ].forEach(emitter => emitter.on(eventID => this.changeEmitter.emit(eventID))); } - get primaryTarget(): Target { - return this.targets[0]; + get primaryTarget(): TargetProto { + return TargetProto.clone(this.targets[0]); } getDurationVariation(): number { @@ -109,41 +113,18 @@ export class Encounter { this.executeProportionChangeEmitter.emit(eventID); } - getNumTargets(): number { - return this.targets.length; - } - - getTargets(): Array { - return this.targets.slice(); - } - setTargets(eventID: EventID, newTargets: Array) { - TypedEvent.freezeAllAndDo(() => { - if (newTargets.length == 0) { - newTargets = [Target.fromDefaults(eventID, this.sim)]; - } - if (newTargets.length == this.targets.length && newTargets.every((target, i) => TargetProto.equals(target.toProto(), this.targets[i].toProto()))) { - return; - } - - this.targets = newTargets; - this.targetsChangeEmitter.emit(eventID); - }); - } - matchesPreset(preset: PresetEncounter): boolean { - return preset.targets.length == this.targets.length && this.targets.every((t, i) => t.matchesPreset(preset.targets[i])); + return preset.targets.length == this.targets.length && this.targets.every((t, i) => TargetProto.equals(t, preset.targets[i].target)); } applyPreset(eventID: EventID, preset: PresetEncounter) { - TypedEvent.freezeAllAndDo(() => { - let newTargets = this.targets.slice(0, preset.targets.length); - while (newTargets.length < preset.targets.length) { - newTargets.push(new Target(this.sim)); - } + this.targets = preset.targets.map(presetTarget => presetTarget.target || TargetProto.create()); + this.targetsChangeEmitter.emit(eventID); + } - newTargets.forEach((nt, i) => nt.applyPreset(eventID, preset.targets[i])); - this.setTargets(eventID, newTargets); - }); + applyPresetTarget(eventID: EventID, preset: PresetTarget, index: number) { + this.targets[index] = preset.target || TargetProto.create(); + this.targetsChangeEmitter.emit(eventID); } toProto(): EncounterProto { @@ -154,7 +135,7 @@ export class Encounter { executeProportion25: this.executeProportion25, executeProportion35: this.executeProportion35, useHealth: this.useHealth, - targets: this.targets.map(target => target.toProto()), + targets: this.targets, }); } @@ -166,16 +147,8 @@ export class Encounter { this.setExecuteProportion25(eventID, proto.executeProportion25); this.setExecuteProportion35(eventID, proto.executeProportion35); this.setUseHealth(eventID, proto.useHealth); - - if (proto.targets.length > 0) { - this.setTargets(eventID, proto.targets.map(targetProto => { - const target = new Target(this.sim); - target.fromProto(eventID, targetProto); - return target; - })); - } else { - this.setTargets(eventID, [Target.fromDefaults(eventID, this.sim)]); - } + this.targets = proto.targets; + this.targetsChangeEmitter.emit(eventID); }); } @@ -186,7 +159,28 @@ export class Encounter { executeProportion20: 0.2, executeProportion25: 0.25, executeProportion35: 0.35, - targets: [Target.defaultProto()], + targets: [Encounter.defaultTargetProto()], })); } + + static defaultTargetProto(): TargetProto { + return TargetProto.create({ + level: Mechanics.BOSS_LEVEL, + mobType: MobType.MobTypeGiant, + tankIndex: 0, + swingSpeed: 1.5, + minBaseDamage: 65000, + dualWield: false, + dualWieldPenalty: false, + suppressDodge: false, + parryHaste: true, + spellSchool: SpellSchool.SpellSchoolPhysical, + stats: Stats.fromMap({ + [Stat.StatArmor]: 10643, + [Stat.StatAttackPower]: 805, + [Stat.StatBlockValue]: 76, + }).asArray(), + targetInputs: new Array(0), + }); + } } diff --git a/ui/core/individual_sim_ui.ts b/ui/core/individual_sim_ui.ts index a993b2972c..ef6e0c698c 100644 --- a/ui/core/individual_sim_ui.ts +++ b/ui/core/individual_sim_ui.ts @@ -17,6 +17,7 @@ import { addStatWeightsAction } from './components/stat_weights_action'; import { BulkTab } from './components/individual_sim_ui/bulk_tab'; import { GearTab } from './components/individual_sim_ui/gear_tab'; import { SettingsTab } from './components/individual_sim_ui/settings_tab'; +import { RotationTab } from './components/individual_sim_ui/rotation_tab'; import { Class, @@ -276,6 +277,7 @@ export abstract class IndividualSimUI extends SimUI { this.bt = this.addBulkTab(); this.addSettingsTab(); this.addTalentsTab(); + //this.addRotationTab(); if (!this.isWithinRaidSim) { this.addDetailedResultsTab(); @@ -448,6 +450,10 @@ export abstract class IndividualSimUI extends SimUI { }); } + private addRotationTab() { + new RotationTab(this.simTabContentsContainer, this); + } + private addDetailedResultsTab() { this.addTab('Results', 'detailed-results-tab', `
diff --git a/ui/core/player.ts b/ui/core/player.ts index da2f7db4ae..be4f653b3a 100644 --- a/ui/core/player.ts +++ b/ui/core/player.ts @@ -24,6 +24,9 @@ import { UnitStats, WeaponType, } from './proto/common.js'; +import { + APLRotation, +} from './proto/apl.js'; import { DungeonDifficulty, Expansion, @@ -83,9 +86,6 @@ import { Party, MAX_PARTY_SIZE } from './party.js'; import { Raid } from './raid.js'; import { Sim } from './sim.js'; import { sum } from './utils.js'; -import { wait } from './utils.js'; -import { WorkerPool } from './worker_pool.js'; -import { EnhancementShaman_Options } from './proto/shaman.js'; // Manages all the gear / consumes / other settings for a single Player. export class Player { @@ -105,6 +105,7 @@ export class Player { private profession1: Profession = 0; private profession2: Profession = 0; private rotation: SpecRotation; + aplRotation: APLRotation = APLRotation.create(); private talentsString: string = ''; private glyphs: Glyphs = Glyphs.create(); private specOptions: SpecOptions; @@ -530,11 +531,23 @@ export class Player { this.rotationChangeEmitter.emit(eventID); } + getAplRotation(): APLRotation { + return APLRotation.clone(this.aplRotation); + } + + setAplRotation(eventID: EventID, newRotation: APLRotation) { + if (APLRotation.equals(newRotation, this.aplRotation)) + return; + + this.aplRotation = APLRotation.clone(newRotation); + this.rotationChangeEmitter.emit(eventID); + } + getTalents(): SpecTalents { if (this.talents == null) { this.talents = playerTalentStringToProto(this.spec, this.talentsString) as SpecTalents; } - return this.talents; + return this.talents!; } getTalentsString(): string { @@ -627,18 +640,17 @@ export class Player { this.distanceFromTarget = newDistanceFromTarget; this.distanceFromTargetChangeEmitter.emit(eventID); } - setDefaultHealingParams(hm: HealingModel) { var boss = this.sim.encounter.primaryTarget; - var dualWield = boss.getDualWield(); + var dualWield = boss.dualWield; if (hm.cadenceSeconds == 0) { - hm.cadenceSeconds = 1.5 * boss.getSwingSpeed(); + hm.cadenceSeconds = 1.5 * boss.swingSpeed; if (dualWield) { hm.cadenceSeconds /= 2; } } if (hm.hps == 0) { - hm.hps = 0.175 * boss.getMinBaseDamage() / boss.getSwingSpeed(); + hm.hps = 0.175 * boss.minBaseDamage / boss.swingSpeed; if (dualWield) { hm.hps *= 1.5; } @@ -991,6 +1003,7 @@ export class Player { cooldowns: this.getCooldowns(), talentsString: this.getTalentsString(), glyphs: this.getGlyphs(), + rotation: this.aplRotation, profession1: this.getProfession1(), profession2: this.getProfession2(), inFrontOfTarget: this.getInFrontOfTarget(), @@ -1020,7 +1033,11 @@ export class Player { this.setDistanceFromTarget(eventID, proto.distanceFromTarget); this.setHealingModel(eventID, proto.healingModel || HealingModel.create()); this.setRotation(eventID, this.specTypeFunctions.rotationFromPlayer(proto)); + this.setAplRotation(eventID, proto.rotation || APLRotation.create()) this.setSpecOptions(eventID, this.specTypeFunctions.optionsFromPlayer(proto)); + + this.aplRotation = proto.rotation || APLRotation.create(); + this.rotationChangeEmitter.emit(eventID); }); } diff --git a/ui/core/sim.ts b/ui/core/sim.ts index a91533203f..2c9a0631e8 100644 --- a/ui/core/sim.ts +++ b/ui/core/sim.ts @@ -188,7 +188,7 @@ export class Sim { async runBulkSim(bulkSettings: BulkSettings, bulkItemsDb: SimDatabase, onProgress: Function): Promise { if (this.raid.isEmpty()) { throw new Error('Raid is empty! Try adding some players first.'); - } else if (this.encounter.getNumTargets() < 1) { + } else if (this.encounter.targets.length < 1) { throw new Error('Encounter has no targets! Try adding some targets first.'); } @@ -227,7 +227,7 @@ export class Sim { async runRaidSim(eventID: EventID, onProgress: Function): Promise { if (this.raid.isEmpty()) { throw new Error('Raid is empty! Try adding some players first.'); - } else if (this.encounter.getNumTargets() < 1) { + } else if (this.encounter.targets.length < 1) { throw new Error('Encounter has no targets! Try adding some targets first.'); } @@ -247,7 +247,7 @@ export class Sim { async runRaidSimWithLogs(eventID: EventID): Promise { if (this.raid.isEmpty()) { throw new Error('Raid is empty! Try adding some players first.'); - } else if (this.encounter.getNumTargets() < 1) { + } else if (this.encounter.targets.length < 1) { throw new Error('Encounter has no targets! Try adding some targets first.'); } @@ -296,7 +296,7 @@ export class Sim { async statWeights(player: Player, epStats: Array, epPseudoStats: Array, epReferenceStat: Stat, onProgress: Function): Promise { if (this.raid.isEmpty()) { throw new Error('Raid is empty! Try adding some players first.'); - } else if (this.encounter.getNumTargets() < 1) { + } else if (this.encounter.targets.length < 1) { throw new Error('Encounter has no targets! Try adding some targets first.'); } diff --git a/ui/core/sim_ui.ts b/ui/core/sim_ui.ts index 3c99d39c5f..10dd4c7178 100644 --- a/ui/core/sim_ui.ts +++ b/ui/core/sim_ui.ts @@ -4,15 +4,11 @@ import { ResultsViewer } from './components/results_viewer.js'; import { SimTitleDropdown } from './components/sim_title_dropdown.js'; import { SimHeader } from './components/sim_header'; import { Spec } from './proto/common.js'; -import { SimOptions } from './proto/api.js'; import { LaunchStatus } from './launched_sims.js'; -import { specToLocalStorageKey } from './proto_utils/utils.js'; import { Sim, SimError } from './sim.js'; -import { Target } from './target.js'; import { EventID, TypedEvent } from './typed_event.js'; -import { Tooltip } from 'bootstrap'; import { SimTab } from './components/sim_tab.js'; import { BaseModal } from './components/base_modal.js'; diff --git a/ui/core/target.ts b/ui/core/target.ts deleted file mode 100644 index 1369e2d875..0000000000 --- a/ui/core/target.ts +++ /dev/null @@ -1,352 +0,0 @@ -import { MobType, TargetInput } from './proto/common.js'; -import { SpellSchool } from './proto/common.js'; -import { Stat } from './proto/common.js'; -import { Target as TargetProto } from './proto/common.js'; -import { PresetTarget } from './proto/common.js'; -import { Stats } from './proto_utils/stats.js'; - -import * as Mechanics from './constants/mechanics.js'; - -import { Sim } from './sim.js'; -import { EventID, TypedEvent } from './typed_event.js'; -import { TargetInputs } from './target_inputs.js'; - -// Manages all the settings for a single Target. -export class Target { - readonly sim: Sim; - - private id: number = 0; - private name: string = ''; - private level: number = Mechanics.BOSS_LEVEL; - private mobType: MobType = MobType.MobTypeDemon; - private tankIndex: number = 0; - private stats: Stats = new Stats(); - - private swingSpeed: number = 0; - private minBaseDamage: number = 0; - private dualWield: boolean = false; - private dualWieldPenalty: boolean = false; - private suppressDodge: boolean = false; - private parryHaste: boolean = true; - private tightEnemyDamage: boolean = false; - private spellSchool: SpellSchool = SpellSchool.SpellSchoolPhysical; - private targetInputs: TargetInputs = new TargetInputs() - - readonly idChangeEmitter = new TypedEvent(); - readonly nameChangeEmitter = new TypedEvent(); - readonly levelChangeEmitter = new TypedEvent(); - readonly mobTypeChangeEmitter = new TypedEvent(); - readonly propChangeEmitter = new TypedEvent(); - readonly statsChangeEmitter = new TypedEvent(); - - // Emits when any of the above emitters emit. - readonly changeEmitter = new TypedEvent(); - - constructor(sim: Sim) { - this.sim = sim; - - [ - this.idChangeEmitter, - this.nameChangeEmitter, - this.levelChangeEmitter, - this.mobTypeChangeEmitter, - this.propChangeEmitter, - this.statsChangeEmitter, - ].forEach(emitter => emitter.on(eventID => this.changeEmitter.emit(eventID))); - - this.changeEmitter.on(eventID => this.sim.encounter?.changeEmitter.emit(eventID)); - } - - getId(): number { - return this.id; - } - - setId(eventID: EventID, newId: number) { - if (newId == this.id) - return; - - this.id = newId; - this.idChangeEmitter.emit(eventID); - } - - getName(): string { - return this.name; - } - - setName(eventID: EventID, newName: string) { - if (newName == this.name) - return; - - this.name = newName; - this.nameChangeEmitter.emit(eventID); - } - - getLevel(): number { - return this.level; - } - - setLevel(eventID: EventID, newLevel: number) { - if (newLevel == this.level) - return; - - this.level = newLevel; - this.levelChangeEmitter.emit(eventID); - } - - getMobType(): MobType { - return this.mobType; - } - - setMobType(eventID: EventID, newMobType: MobType) { - if (newMobType == this.mobType) - return; - - this.mobType = newMobType; - this.mobTypeChangeEmitter.emit(eventID); - } - - getTankIndex(): number { - return this.tankIndex; - } - - setTankIndex(eventID: EventID, newTankIndex: number) { - if (newTankIndex == this.tankIndex) - return; - - this.tankIndex = newTankIndex; - this.propChangeEmitter.emit(eventID); - } - - getSwingSpeed(): number { - return this.swingSpeed; - } - - setSwingSpeed(eventID: EventID, newSwingSpeed: number) { - if (newSwingSpeed == this.swingSpeed) - return; - - this.swingSpeed = newSwingSpeed; - this.propChangeEmitter.emit(eventID); - } - - getMinBaseDamage(): number { - return this.minBaseDamage; - } - - setMinBaseDamage(eventID: EventID, newMinBaseDamage: number) { - if (newMinBaseDamage == this.minBaseDamage) - return; - - this.minBaseDamage = newMinBaseDamage; - this.propChangeEmitter.emit(eventID); - } - - getDualWield(): boolean { - return this.dualWield; - } - - setDualWield(eventID: EventID, newDualWield: boolean) { - if (newDualWield == this.dualWield) - return; - - this.dualWield = newDualWield; - this.propChangeEmitter.emit(eventID); - } - - getDualWieldPenalty(): boolean { - return this.dualWieldPenalty; - } - - setDualWieldPenalty(eventID: EventID, newDualWieldPenalty: boolean) { - if (newDualWieldPenalty == this.dualWieldPenalty) - return; - - this.dualWieldPenalty = newDualWieldPenalty; - this.propChangeEmitter.emit(eventID); - } - - getSuppressDodge(): boolean { - return this.suppressDodge; - } - - setSuppressDodge(eventID: EventID, newSuppressDodge: boolean) { - if (newSuppressDodge == this.suppressDodge) - return; - - this.suppressDodge = newSuppressDodge; - this.propChangeEmitter.emit(eventID); - } - - getParryHaste(): boolean { - return this.parryHaste; - } - - setParryHaste(eventID: EventID, newParryHaste: boolean) { - if (newParryHaste == this.parryHaste) - return; - - this.parryHaste = newParryHaste; - this.propChangeEmitter.emit(eventID); - } - - getTightEnemyDamage(): boolean { - return this.tightEnemyDamage; - } - - setTightEnemyDamage(eventID: EventID, newTightEnemyDamage: boolean) { - if (newTightEnemyDamage == this.tightEnemyDamage) - return; - - this.tightEnemyDamage = newTightEnemyDamage; - this.propChangeEmitter.emit(eventID); - } - - getSpellSchool(): SpellSchool { - return this.spellSchool; - } - - setSpellSchool(eventID: EventID, newSpellSchool: SpellSchool) { - if (newSpellSchool == this.spellSchool) - return; - - this.spellSchool = newSpellSchool; - this.propChangeEmitter.emit(eventID); - } - - getTargetInputs(): TargetInputs { - return this.targetInputs; - } - - setTargetInputs(eventID: EventID, newTargetInputs?: TargetInputs) { - if (newTargetInputs?.equals(this.targetInputs)) - return; - - this.targetInputs = newTargetInputs ?? new TargetInputs(); - this.propChangeEmitter.emit(eventID); - } - - hasTargetInputs(): boolean { - return this.targetInputs.hasInputs(); - } - - getTargetInputsLength(): number { - return this.targetInputs.getLength(); - } - - getTargetInputNumberValue(index: number): number { - return this.targetInputs.getTargetInput(index)?.numberValue; - } - - setTargetInputNumberValue(eventID: EventID, index: number, newValue: number) { - if (this.getTargetInputNumberValue(index) == newValue) - return; - - this.targetInputs.getTargetInput(index).numberValue = newValue; - this.propChangeEmitter.emit(eventID); - } - - getTargetInputBooleanValue(index: number): boolean { - return this.targetInputs.getTargetInput(index)?.boolValue; - } - - setTargetInputBooleanValue(eventID: EventID, index: number, newValue: boolean) { - if (this.getTargetInputBooleanValue(index) == newValue) - return; - - this.targetInputs.getTargetInput(index).boolValue = newValue; - this.propChangeEmitter.emit(eventID); - } - - getStats(): Stats { - return this.stats; - } - - setStats(eventID: EventID, newStats: Stats) { - if (newStats.equals(this.stats)) - return; - - this.stats = newStats; - this.statsChangeEmitter.emit(eventID); - } - - matchesPreset(preset: PresetTarget): boolean { - return TargetProto.equals(this.toProto(), preset.target); - } - - applyPreset(eventID: EventID, preset: PresetTarget) { - this.fromProto(eventID, preset.target || TargetProto.create()); - } - - toProto(): TargetProto { - return TargetProto.create({ - id: this.getId(), - name: this.getName(), - level: this.getLevel(), - mobType: this.getMobType(), - tankIndex: this.getTankIndex(), - swingSpeed: this.getSwingSpeed(), - minBaseDamage: this.getMinBaseDamage(), - dualWield: this.getDualWield(), - dualWieldPenalty: this.getDualWieldPenalty(), - suppressDodge: this.getSuppressDodge(), - parryHaste: this.getParryHaste(), - tightEnemyDamage: this.getTightEnemyDamage(), - spellSchool: this.getSpellSchool(), - stats: this.stats.asArray(), - targetInputs: this.targetInputs.asArray(), - }); - } - - fromProto(eventID: EventID, proto: TargetProto) { - TypedEvent.freezeAllAndDo(() => { - this.setId(eventID, proto.id); - this.setName(eventID, proto.name); - this.setLevel(eventID, proto.level); - this.setMobType(eventID, proto.mobType); - this.setTankIndex(eventID, proto.tankIndex); - this.setSwingSpeed(eventID, proto.swingSpeed); - this.setMinBaseDamage(eventID, proto.minBaseDamage); - this.setDualWield(eventID, proto.dualWield); - this.setDualWieldPenalty(eventID, proto.dualWieldPenalty); - this.setSuppressDodge(eventID, proto.suppressDodge); - this.setParryHaste(eventID, proto.parryHaste); - this.setTightEnemyDamage(eventID, proto.tightEnemyDamage); - this.setSpellSchool(eventID, proto.spellSchool); - this.setTargetInputs(eventID, new TargetInputs(proto.targetInputs)); - this.setStats(eventID, new Stats(proto.stats)); - }); - } - - clone(eventID: EventID): Target { - const newTarget = new Target(this.sim); - newTarget.fromProto(eventID, this.toProto()); - return newTarget; - } - - static defaultProto(): TargetProto { - return TargetProto.create({ - level: Mechanics.BOSS_LEVEL, - mobType: MobType.MobTypeGiant, - tankIndex: 0, - swingSpeed: 1.5, - minBaseDamage: 65000, - dualWield: false, - dualWieldPenalty: false, - suppressDodge: false, - parryHaste: true, - spellSchool: SpellSchool.SpellSchoolPhysical, - stats: Stats.fromMap({ - [Stat.StatArmor]: 10643, - [Stat.StatAttackPower]: 805, - [Stat.StatBlockValue]: 76, - }).asArray(), - targetInputs: new Array(0), - }); - } - - static fromDefaults(eventID: EventID, sim: Sim): Target { - const target = new Target(sim); - target.fromProto(eventID, Target.defaultProto()); - return target; - } -} diff --git a/ui/core/target_inputs.ts b/ui/core/target_inputs.ts deleted file mode 100644 index b640749e74..0000000000 --- a/ui/core/target_inputs.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { TargetInput } from "./proto/common"; - -export class TargetInputs { - private readonly targetInputs: Array; - - constructor(targetInputs?: Array) { - this.targetInputs = TargetInputs.initTargetInputsArray(targetInputs); - } - - private static initTargetInputsArray(newTargetInputs?: Array): Array { - return newTargetInputs?.slice(0, newTargetInputs.length) || []; - } - - private static targetInputEqual(lhs: TargetInput, rhs: TargetInput): boolean { - return lhs?.label == rhs?.label && lhs?.inputType == rhs?.inputType && lhs?.boolValue == rhs?.boolValue && lhs?.numberValue == rhs?.numberValue; - } - - equals(other: TargetInputs): boolean { - if (this.targetInputs?.length != other.targetInputs?.length) { - return false; - } - return this.targetInputs.every((newTargetInput, inputIdx) => TargetInputs.targetInputEqual(newTargetInput, other.getTargetInput(inputIdx))); - } - - getLength(): number { - return this.targetInputs?.length; - } - - getTargetInput(index: number): TargetInput { - return this.targetInputs[index]; - } - - asArray(): Array { - return this.targetInputs.slice(); - } - - hasInputs(): boolean { - return this.targetInputs?.length > 0; - } -} \ No newline at end of file diff --git a/ui/elemental_shaman/inputs.ts b/ui/elemental_shaman/inputs.ts index fb4519c8af..492c740ccf 100644 --- a/ui/elemental_shaman/inputs.ts +++ b/ui/elemental_shaman/inputs.ts @@ -5,9 +5,6 @@ import { AirTotem } from '../core/proto/shaman.js'; import { Spec } from '../core/proto/common.js'; import { ActionId } from '../core/proto_utils/action_id.js'; import { Player } from '../core/player.js'; -import { Sim } from '../core/sim.js'; -import { Target } from '../core/target.js'; -import { EventID, TypedEvent } from '../core/typed_event.js'; import * as InputHelpers from '../core/components/input_helpers.js'; diff --git a/ui/enhancement_shaman/inputs.ts b/ui/enhancement_shaman/inputs.ts index 14447190ad..7f1083faf3 100644 --- a/ui/enhancement_shaman/inputs.ts +++ b/ui/enhancement_shaman/inputs.ts @@ -19,10 +19,6 @@ import { import { CustomSpell, Spec, ItemSwap, ItemSlot } from '../core/proto/common.js'; import { ActionId } from '../core/proto_utils/action_id.js'; import { Player } from '../core/player.js'; -import { Sim } from '../core/sim.js'; -import { IndividualSimUI } from '../core/individual_sim_ui.js'; -import { Target } from '../core/target.js'; -import { EventID, TypedEvent } from '../core/typed_event.js'; import * as InputHelpers from '../core/components/input_helpers.js'; diff --git a/ui/feral_tank_druid/inputs.ts b/ui/feral_tank_druid/inputs.ts index db51f236b8..f1680a5139 100644 --- a/ui/feral_tank_druid/inputs.ts +++ b/ui/feral_tank_druid/inputs.ts @@ -1,9 +1,7 @@ import { Spec } from '../core/proto/common.js'; import { ActionId } from '../core/proto_utils/action_id.js'; import { Player } from '../core/player.js'; -import { Sim } from '../core/sim.js'; import { EventID, TypedEvent } from '../core/typed_event.js'; -import { Target } from '../core/target.js'; import { getEnumValues } from '../core/utils.js'; import { ItemSlot } from '../core/proto/common.js'; diff --git a/ui/healing_priest/inputs.ts b/ui/healing_priest/inputs.ts index cd3b37e550..64e2f12cbb 100644 --- a/ui/healing_priest/inputs.ts +++ b/ui/healing_priest/inputs.ts @@ -1,12 +1,8 @@ -import { CustomRotation } from '../core/proto/common.js'; -import { Race, RaidTarget } from '../core/proto/common.js'; +import { RaidTarget } from '../core/proto/common.js'; import { Spec } from '../core/proto/common.js'; import { NO_TARGET } from '../core/proto_utils/utils.js'; import { ActionId } from '../core/proto_utils/action_id.js'; import { Player } from '../core/player.js'; -import { Sim } from '../core/sim.js'; -import { IndividualSimUI } from '../core/individual_sim_ui.js'; -import { Target } from '../core/target.js'; import { EventID, TypedEvent } from '../core/typed_event.js'; import { diff --git a/ui/hunter/inputs.ts b/ui/hunter/inputs.ts index 4f0bbba3ef..0df3ba0686 100644 --- a/ui/hunter/inputs.ts +++ b/ui/hunter/inputs.ts @@ -1,13 +1,6 @@ -import { BooleanPicker } from '../core/components/boolean_picker.js'; -import { EnumPicker } from '../core/components/enum_picker.js'; -import { IconEnumPicker, IconEnumPickerConfig } from '../core/components/icon_enum_picker.js'; -import { IconPickerConfig } from '../core/components/icon_picker.js'; -import { CustomRotation } from '../core/proto/common.js'; import { Spec } from '../core/proto/common.js'; import { ActionId } from '../core/proto_utils/action_id.js'; import { Player } from '../core/player.js'; -import { Sim } from '../core/sim.js'; -import { Target } from '../core/target.js'; import { EventID, TypedEvent } from '../core/typed_event.js'; import { makePetTypeInputConfig } from '../core/talents/hunter_pet.js'; diff --git a/ui/mage/inputs.ts b/ui/mage/inputs.ts index 1aa5e0088b..cee892779b 100644 --- a/ui/mage/inputs.ts +++ b/ui/mage/inputs.ts @@ -1,13 +1,7 @@ -import { IconPickerConfig } from '../core/components/icon_picker.js'; -import { RaidTarget } from '../core/proto/common.js'; import { Spec } from '../core/proto/common.js'; -import { NO_TARGET } from '../core/proto_utils/utils.js'; import { ActionId } from '../core/proto_utils/action_id.js'; import { Player } from '../core/player.js'; -import { Sim } from '../core/sim.js'; import { EventID, TypedEvent } from '../core/typed_event.js'; -import { IndividualSimUI } from '../core/individual_sim_ui.js'; -import { Target } from '../core/target.js'; import { Mage, diff --git a/ui/protection_warrior/inputs.ts b/ui/protection_warrior/inputs.ts index 9e35c508b7..2b4a495128 100644 --- a/ui/protection_warrior/inputs.ts +++ b/ui/protection_warrior/inputs.ts @@ -1,17 +1,5 @@ -import { IconPickerConfig } from '../core/components/icon_picker.js'; -import { RaidTarget } from '../core/proto/common.js'; import { Spec } from '../core/proto/common.js'; -import { NO_TARGET } from '../core/proto_utils/utils.js'; import { ActionId } from '../core/proto_utils/action_id.js'; -import { Player } from '../core/player.js'; -import { Sim } from '../core/sim.js'; -import { EventID, TypedEvent } from '../core/typed_event.js'; -import { IndividualSimUI } from '../core/individual_sim_ui.js'; -import { Target } from '../core/target.js'; -import { EnumPicker } from '../core/components/enum_picker.js'; -import { IconEnumPicker, IconEnumPickerConfig } from '../core/components/icon_enum_picker.js'; -import { CustomRotationPickerConfig } from '../core/components/individual_sim_ui/custom_rotation_picker.js'; -import { CustomRotation } from '../core/proto/common.js'; import { WarriorShout, diff --git a/ui/raid/import_export.ts b/ui/raid/import_export.ts index 447a9bbb0e..9c34a1188a 100644 --- a/ui/raid/import_export.ts +++ b/ui/raid/import_export.ts @@ -35,7 +35,7 @@ import { } from '../core/proto_utils/utils'; import { MAX_NUM_PARTIES } from '../core/raid'; import { Player } from '../core/player'; -import { Target } from '../core/target'; +import { Encounter } from '../core/encounter'; import { bucket, distinct, sortByProperty } from '../core/utils'; import { playerPresets, PresetSpecSettings } from './presets'; @@ -492,7 +492,7 @@ export class RaidWCLImporter extends Importer { // Build a manual target list if no preset encounter exists. if (encounter.targets.length === 0) { - encounter.targets.push(Target.defaultProto()); + encounter.targets.push(Encounter.defaultTargetProto()); } return encounter; diff --git a/ui/restoration_shaman/inputs.ts b/ui/restoration_shaman/inputs.ts index 8b8a3af6ee..6bde153e57 100644 --- a/ui/restoration_shaman/inputs.ts +++ b/ui/restoration_shaman/inputs.ts @@ -1,9 +1,6 @@ -import { IconPickerConfig } from '../core/components/icon_picker.js'; import { Spec } from '../core/proto/common.js'; -import { ActionId } from '../core/proto_utils/action_id.js'; import { Player } from '../core/player.js'; -import { Sim } from '../core/sim.js'; -import { Target } from '../core/target.js'; +import { ActionId } from '../core/proto_utils/action_id.js'; import { EventID, TypedEvent } from '../core/typed_event.js'; import { diff --git a/ui/rogue/sim.ts b/ui/rogue/sim.ts index 745b55b5c9..957fed646b 100644 --- a/ui/rogue/sim.ts +++ b/ui/rogue/sim.ts @@ -46,8 +46,8 @@ export class RogueSimUI extends IndividualSimUI { updateOn: simUI.sim.encounter.changeEmitter, getContent: () => { let hasNoArmor = false - for (const target of simUI.sim.encounter.getTargets()) { - if (target.getStats().getStat(Stat.StatArmor) <= 0) { + for (const target of simUI.sim.encounter.targets) { + if (new Stats(target.stats).getStat(Stat.StatArmor) <= 0) { hasNoArmor = true break } @@ -339,7 +339,7 @@ export class RogueSimUI extends IndividualSimUI { if (typeof mhWeaponSpeed == 'undefined' || typeof ohWeaponSpeed == 'undefined') { return } - if (encounter.getNumTargets() > 3) { + if (encounter.targets.length > 3) { options.mhImbue = Rogue_Options_PoisonImbue.InstantPoison options.ohImbue = Rogue_Options_PoisonImbue.InstantPoison } else { @@ -358,7 +358,7 @@ export class RogueSimUI extends IndividualSimUI { const rotation = this.player.getRotation() const options = this.player.getSpecOptions() const encounter = this.sim.encounter - if (this.sim.encounter.getNumTargets() > 3) { + if (this.sim.encounter.targets.length > 3) { if (rotation.multiTargetSliceFrequency == Frequency.FrequencyUnknown) { rotation.multiTargetSliceFrequency = Presets.DefaultRotation.multiTargetSliceFrequency; } @@ -372,7 +372,7 @@ export class RogueSimUI extends IndividualSimUI { if (typeof mhWeaponSpeed == 'undefined' || typeof ohWeaponSpeed == 'undefined') { return } - if (encounter.getNumTargets() > 3) { + if (encounter.targets.length > 3) { options.mhImbue = Rogue_Options_PoisonImbue.InstantPoison options.ohImbue = Rogue_Options_PoisonImbue.InstantPoison } else { diff --git a/ui/scss/core/components/_list_picker.scss b/ui/scss/core/components/_list_picker.scss index b873c1af08..9aedfa6b35 100644 --- a/ui/scss/core/components/_list_picker.scss +++ b/ui/scss/core/components/_list_picker.scss @@ -65,3 +65,17 @@ width: 33.33%; } } + +.list-picker-root.hide-ui { + .list-picker-item-container { + padding: 0 !important; + margin: 0 !important; + border: none !important; + } + .list-picker-item-header { + display: none !important; + } + .list-picker-new-button { + display: none !important; + } +} 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 new file mode 100644 index 0000000000..c988d2f26c --- /dev/null +++ b/ui/scss/core/components/individual_sim_ui/_apl_rotation_picker.scss @@ -0,0 +1,33 @@ +.apl-rotation-picker-root { + .input-root.apl-list-item-picker { + flex-wrap: wrap; + align-items: flex-start !important; + + .list-picker-title { + width: unset; + padding: 0; + border: 0; + } + + .list-picker-items { + width: unset; + } + + .list-picker-new-button { + width: 100%; + } + } +} + +.apl-list-item-picker-root { + display: flex; + align-items: center; + + &> :not(:last-child) { + margin-right: map.get($spacers, 2); + } + + .number-picker-input { + width: 100% !important; + } +} \ No newline at end of file diff --git a/ui/scss/core/components/individual_sim_ui/_rotation_tab.scss b/ui/scss/core/components/individual_sim_ui/_rotation_tab.scss new file mode 100644 index 0000000000..a99d75e8e6 --- /dev/null +++ b/ui/scss/core/components/individual_sim_ui/_rotation_tab.scss @@ -0,0 +1,3 @@ +@use "sass:map"; + +@import "./apl_rotation_picker"; \ No newline at end of file diff --git a/ui/scss/core/individual_sim_ui/index.scss b/ui/scss/core/individual_sim_ui/index.scss index 48ff1d7e71..b473fd1e11 100644 --- a/ui/scss/core/individual_sim_ui/index.scss +++ b/ui/scss/core/individual_sim_ui/index.scss @@ -27,6 +27,7 @@ @import "../components/stat_weights_action"; @import "../components/individual_sim_ui/settings_tab"; +@import "../components/individual_sim_ui/rotation_tab"; @import "../talents/hunter_pet"; @import "../talents/glyphs_picker"; diff --git a/ui/smite_priest/inputs.ts b/ui/smite_priest/inputs.ts index 4ff5001d3e..4018e0dee6 100644 --- a/ui/smite_priest/inputs.ts +++ b/ui/smite_priest/inputs.ts @@ -1,11 +1,8 @@ -import { Race, RaidTarget } from '../core/proto/common.js'; +import { RaidTarget } from '../core/proto/common.js'; import { Spec } from '../core/proto/common.js'; import { NO_TARGET } from '../core/proto_utils/utils.js'; import { ActionId } from '../core/proto_utils/action_id.js'; import { Player } from '../core/player.js'; -import { Sim } from '../core/sim.js'; -import { IndividualSimUI } from '../core/individual_sim_ui.js'; -import { Target } from '../core/target.js'; import { EventID, TypedEvent } from '../core/typed_event.js'; import * as InputHelpers from '../core/components/input_helpers.js'; diff --git a/ui/warlock/inputs.ts b/ui/warlock/inputs.ts index d06db00390..d67a270958 100644 --- a/ui/warlock/inputs.ts +++ b/ui/warlock/inputs.ts @@ -15,14 +15,8 @@ import { RaidTarget, Spec, Glyphs, Debuffs, IndividualBuffs, RaidBuffs, ItemSwap import { NO_TARGET } from '../core/proto_utils/utils.js'; import { ActionId } from '../core/proto_utils/action_id.js'; import { Player } from '../core/player.js'; -import { Sim } from '../core/sim.js'; import { EventID, TypedEvent } from '../core/typed_event.js'; -import { IndividualSimUI } from '../core/individual_sim_ui.js'; -import { Target } from '../core/target.js'; -import { SimUI, SimWarning } from '../core/sim_ui.js'; -import { IconPickerConfig } from '../core/components/icon_picker.js'; -import { IconEnumPicker, IconEnumPickerConfig, IconEnumValueConfig } from '../core/components/icon_enum_picker.js'; import * as Presets from './presets.js'; import * as InputHelpers from '../core/components/input_helpers.js'; diff --git a/ui/warrior/inputs.ts b/ui/warrior/inputs.ts index 1e1353bd81..73e638c8a1 100644 --- a/ui/warrior/inputs.ts +++ b/ui/warrior/inputs.ts @@ -1,12 +1,7 @@ -import { IconPickerConfig } from '../core/components/icon_picker.js'; -import { RaidTarget } from '../core/proto/common.js'; import { Spec } from '../core/proto/common.js'; -import { NO_TARGET } from '../core/proto_utils/utils.js'; import { ActionId } from '../core/proto_utils/action_id.js'; import { Player } from '../core/player.js'; -import { Sim } from '../core/sim.js'; import { EventID, TypedEvent } from '../core/typed_event.js'; -import { Target } from '../core/target.js'; import { WarriorShout,