Skip to content

Commit

Permalink
Merge pull request #3123 from wowsims/apl
Browse files Browse the repository at this point in the history
Refactor ListPicker to require less hacky code for nested lists, and start a Rotation UI tab for APL (still mostly empty).
  • Loading branch information
jimmyt857 authored May 28, 2023
2 parents 7f880c0 + 54ee6c4 commit f966a98
Show file tree
Hide file tree
Showing 33 changed files with 741 additions and 835 deletions.
493 changes: 294 additions & 199 deletions ui/core/components/encounter_picker.ts

Large diffs are not rendered by default.

3 changes: 0 additions & 3 deletions ui/core/components/icon_inputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
118 changes: 118 additions & 0 deletions ui/core/components/individual_sim_ui/apl_rotation_picker.ts
Original file line number Diff line number Diff line change
@@ -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<any>) {
super(parent, 'apl-rotation-picker-root');

new ListPicker<Player<any>, 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<any>) => player.rotationChangeEmitter,
getValue: (player: Player<any>) => player.aplRotation.priorityList,
setValue: (eventID: EventID, player: Player<any>, newValue: Array<APLListItem>) => {
player.aplRotation.priorityList = newValue;
player.rotationChangeEmitter.emit(eventID);
},
newItem: () => APLListItem.create(),
copyItem: (oldItem: APLListItem) => APLListItem.clone(oldItem),
newItemPicker: (parent: HTMLElement, listPicker: ListPicker<Player<any>, APLListItem>, index: number, config: ListItemPickerConfig<Player<any>, APLListItem>) => new APLListItemPicker(parent, modPlayer, listPicker, index, config),
//inlineMenuBar: true,
});
}
}

class APLListItemPicker extends Input<Player<any>, APLListItem> {
private readonly player: Player<any>;
private readonly listPicker: ListPicker<Player<any>, APLListItem>;
private readonly itemIndex: number;

private readonly hidePicker: Input<null, boolean>;
private readonly notesPicker: Input<null, string>;

private getItem(): APLListItem {
return this.player.aplRotation.priorityList[this.itemIndex] || APLListItem.create();
}

constructor(parent: HTMLElement, player: Player<any>, listPicker: ListPicker<Player<any>, APLListItem>, itemIndex: number, config: ListItemPickerConfig<Player<any>, 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<CustomSpell, number>(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<SpecType>) => boolean);
// }
// return value;
// }) as unknown as Array<IconEnumValueConfig<CustomSpell, number>>,
// 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);
}
}
83 changes: 57 additions & 26 deletions ui/core/components/individual_sim_ui/custom_rotation_picker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<SpecType extends Spec, T> {
Expand All @@ -28,7 +29,7 @@ export class CustomRotationPicker<SpecType extends Spec, T> extends Component {
if (config.extraCssClasses)
this.rootElem.classList.add(...config.extraCssClasses);

new ListPicker<Player<SpecType>, CustomSpell, CustomSpellPicker<SpecType, T>>(this.rootElem, simUI, modPlayer, {
new ListPicker<Player<SpecType>, 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.',
Expand All @@ -42,63 +43,93 @@ export class CustomRotationPicker<SpecType extends Spec, T> extends Component {
},
newItem: () => CustomSpell.create(),
copyItem: (oldItem: CustomSpell) => CustomSpell.clone(oldItem),
newItemPicker: (parent: HTMLElement, newItem: CustomSpell, listPicker: ListPicker<Player<SpecType>, CustomSpell, CustomSpellPicker<SpecType, T>>) => new CustomSpellPicker(parent, modPlayer, newItem, config, listPicker),
newItemPicker: (parent: HTMLElement, listPicker: ListPicker<Player<SpecType>, CustomSpell>, index: number, itemConfig: ListItemPickerConfig<Player<SpecType>, CustomSpell>) => new CustomSpellPicker(parent, modPlayer, index, itemConfig, listPicker, config),
inlineMenuBar: true,
showWhen: config.showWhen,
});
}
}

class CustomSpellPicker<SpecType extends Spec, T> extends Component {
class CustomSpellPicker<SpecType extends Spec, T> extends Input<Player<SpecType>, CustomSpell> {
private readonly player: Player<SpecType>;
private readonly config: CustomRotationPickerConfig<SpecType, T>;
private readonly listPicker: ListPicker<Player<SpecType>, CustomSpell, CustomSpellPicker<SpecType, T>>;
private readonly listPicker: ListPicker<Player<SpecType>, CustomSpell>;
private readonly spellIndex: number;

constructor(parent: HTMLElement, player: Player<SpecType>, modSpell: CustomSpell, config: CustomRotationPickerConfig<SpecType, T>, listPicker: ListPicker<Player<SpecType>, CustomSpell, CustomSpellPicker<SpecType, T>>) {
super(parent, 'custom-spell-picker-root');
private readonly spellPicker: Input<null, number>;
private readonly cpmPicker: Input<null, number>|null;

getSpell(): CustomSpell {
return this.listPicker.config.getValue(this.player)[this.spellIndex] || CustomSpell.create();
}

constructor(parent: HTMLElement, player: Player<SpecType>, spellIndex: number, config: ListItemPickerConfig<Player<SpecType>, CustomSpell>, listPicker: ListPicker<Player<SpecType>, CustomSpell>, crConfig: CustomRotationPickerConfig<SpecType, T>) {
super(parent, 'custom-spell-picker-root', player, config);
this.player = player;
this.config = config;
this.listPicker = listPicker;
this.spellIndex = spellIndex;

new IconEnumPicker<CustomSpell, number>(this.rootElem, modSpell, {
numColumns: config.numColumns,
values: config.values.map(value => {
this.spellPicker = new IconEnumPicker<null, number>(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<SpecType>) => boolean);
value.showWhen = (() => oldShowWhen(player)) as unknown as ((player: Player<SpecType>) => boolean);
}
return value;
}) as unknown as Array<IconEnumValueConfig<CustomSpell, number>>,
}) as unknown as Array<IconEnumValueConfig<null, number>>,
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<CustomSpell>(this.rootElem, modSpell, {
this.cpmPicker = null;
if (crConfig.showCastsPerMinute) {
this.cpmPicker = new NumberPicker<null>(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);
},
});
}
}

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);
}
}
82 changes: 82 additions & 0 deletions ui/core/components/individual_sim_ui/rotation_tab.ts
Original file line number Diff line number Diff line change
@@ -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<Spec>;

readonly leftPanel: HTMLElement;
readonly rightPanel: HTMLElement;

constructor(parentElem: HTMLElement, simUI: IndividualSimUI<Spec>) {
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<any>) => player.rotationChangeEmitter,
getValue: (player: Player<any>) => player.aplRotation.enabled,
setValue: (eventID: EventID, player: Player<any>, 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);
}
}
6 changes: 3 additions & 3 deletions ui/core/components/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,11 @@ export abstract class Input<ModObject, T> 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);
Expand All @@ -107,7 +107,7 @@ export abstract class Input<ModObject, T> extends Component {
this.update();
}

abstract getInputElem(): HTMLElement;
abstract getInputElem(): HTMLElement|null;

abstract getInputValue(): T;

Expand Down
Loading

0 comments on commit f966a98

Please sign in to comment.