Skip to content

Commit

Permalink
Merge pull request #3322 from wowsims/apl
Browse files Browse the repository at this point in the history
Fix some bugs in apl autofill logic
  • Loading branch information
jimmyt857 authored Jul 15, 2023
2 parents ccb389d + b227f54 commit 414b1d4
Show file tree
Hide file tree
Showing 5 changed files with 230 additions and 182 deletions.
35 changes: 18 additions & 17 deletions ui/core/components/dropdown_picker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,19 @@ import { EventID, TypedEvent } from '../typed_event.js';

import { Input, InputConfig } from './input.js';

export interface DropdownValueConfig<T> {
value: T,
export interface DropdownValueConfig<V> {
value: V,
submenu?: Array<string>,
headerText?: string,
tooltip?: string,
extraCssClasses?: Array<string>,
}

export interface DropdownPickerConfig<ModObject, T> extends InputConfig<ModObject, T> {
values: Array<DropdownValueConfig<T>>;
equals: (a: T | undefined, b: T | undefined) => boolean,
setOptionContent: (button: HTMLButtonElement, valueConfig: DropdownValueConfig<T>) => void,
createMissingValue?: (val: T) => Promise<DropdownValueConfig<T>>,
export interface DropdownPickerConfig<ModObject, T, V = T> extends InputConfig<ModObject, T, V> {
values: Array<DropdownValueConfig<V>>;
equals: (a: V | undefined, b: V | undefined) => boolean,
setOptionContent: (button: HTMLButtonElement, valueConfig: DropdownValueConfig<V>) => void,
createMissingValue?: (val: V) => Promise<DropdownValueConfig<V>>,
defaultLabel: string,
}

Expand All @@ -27,17 +27,17 @@ interface DropdownSubmenu {
}

/** UI Input that uses a dropdown menu. */
export class DropdownPicker<ModObject, T> extends Input<ModObject, T> {
private readonly config: DropdownPickerConfig<ModObject, T>;
private valueConfigs: Array<DropdownValueConfig<T>>;
export class DropdownPicker<ModObject, T, V = T> extends Input<ModObject, T, V> {
private readonly config: DropdownPickerConfig<ModObject, T, V>;
private valueConfigs: Array<DropdownValueConfig<V>>;

private readonly buttonElem: HTMLButtonElement;
private readonly listElem: HTMLUListElement;

private currentSelection: DropdownValueConfig<T> | null;
private currentSelection: DropdownValueConfig<V> | null;
private submenus: Array<DropdownSubmenu>;

constructor(parent: HTMLElement, modObject: ModObject, config: DropdownPickerConfig<ModObject, T>) {
constructor(parent: HTMLElement, modObject: ModObject, config: DropdownPickerConfig<ModObject, T, V>) {
super(parent, 'dropdown-picker-root', modObject, config);
this.config = config;
this.valueConfigs = this.config.values.filter(vc => !vc.headerText);
Expand All @@ -62,13 +62,13 @@ export class DropdownPicker<ModObject, T> extends Input<ModObject, T> {
this.init();
}

setOptions(newValueConfigs: Array<DropdownValueConfig<T>>) {
setOptions(newValueConfigs: Array<DropdownValueConfig<V>>) {
this.buildDropdown(newValueConfigs);
this.valueConfigs = newValueConfigs.filter(vc => !vc.headerText);
this.setInputValue(this.getSourceValue());
}

private buildDropdown(valueConfigs: Array<DropdownValueConfig<T>>) {
private buildDropdown(valueConfigs: Array<DropdownValueConfig<V>>) {
this.listElem.innerHTML = '';
this.submenus = [];
valueConfigs.forEach(valueConfig => {
Expand Down Expand Up @@ -183,10 +183,11 @@ export class DropdownPicker<ModObject, T> extends Input<ModObject, T> {
}

getInputValue(): T {
return this.currentSelection?.value as T;
return this.valueToSource(this.currentSelection?.value as V);
}

setInputValue(newValue: T) {
setInputValue(newSrcValue: T) {
const newValue = this.sourceToValue(newSrcValue);
const newSelection = this.valueConfigs.find(v => this.config.equals(v.value, newValue))!;
if (newSelection) {
this.updateValue(newSelection);
Expand All @@ -199,7 +200,7 @@ export class DropdownPicker<ModObject, T> extends Input<ModObject, T> {
}
}

private updateValue(newValue: DropdownValueConfig<T> | null) {
private updateValue(newValue: DropdownValueConfig<V> | null) {
this.currentSelection = newValue;

// Update button
Expand Down
121 changes: 79 additions & 42 deletions ui/core/components/individual_sim_ui/apl_actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,19 @@ import * as AplValues from './apl_values.js';
export interface APLActionPickerConfig extends InputConfig<Player<any>, APLAction> {
}

export type APLActionType = APLAction['action']['oneofKind'];
export type APLActionKind = APLAction['action']['oneofKind'];
type APLActionImplStruct<F extends APLActionKind> = Extract<APLAction['action'], {oneofKind: F}>;
type APLActionImplTypesUnion = {
[f in NonNullable<APLActionKind>]: f extends keyof APLActionImplStruct<f> ? APLActionImplStruct<f>[f] : never;
};
export type APLActionImplType = APLActionImplTypesUnion[NonNullable<APLActionKind>]|undefined;

export class APLActionPicker extends Input<Player<any>, APLAction> {

private typePicker: TextDropdownPicker<Player<any>, APLActionType>;
private kindPicker: TextDropdownPicker<Player<any>, APLActionKind>;

private readonly actionDiv: HTMLElement;
private currentType: APLActionType;
private currentKind: APLActionKind;
private actionPicker: Input<Player<any>, any> | null;

private readonly conditionPicker: AplValues.APLValuePicker;
Expand All @@ -54,15 +59,15 @@ export class APLActionPicker extends Input<Player<any>, APLAction> {

const isPrepull = this.rootElem.closest('.apl-prepull-action-picker') != null;

const allActionTypes = Object.keys(actionTypeFactories) as Array<NonNullable<APLActionType>>;
this.typePicker = new TextDropdownPicker(this.actionDiv, player, {
const allActionKinds = Object.keys(actionKindFactories) as Array<NonNullable<APLActionKind>>;
this.kindPicker = new TextDropdownPicker(this.actionDiv, player, {
defaultLabel: 'Action',
values: allActionTypes
.filter(actionType => actionTypeFactories[actionType].isPrepull == undefined || actionTypeFactories[actionType].isPrepull === isPrepull)
.map(actionType => {
const factory = actionTypeFactories[actionType];
values: allActionKinds
.filter(actionKind => actionKindFactories[actionKind].isPrepull == undefined || actionKindFactories[actionKind].isPrepull === isPrepull)
.map(actionKind => {
const factory = actionKindFactories[actionKind];
return {
value: actionType,
value: actionKind,
label: factory.label,
submenu: factory.submenu,
tooltip: factory.fullDescription ? `<p>${factory.shortDescription}</p> ${factory.fullDescription}` : factory.shortDescription,
Expand All @@ -71,26 +76,49 @@ export class APLActionPicker extends Input<Player<any>, APLAction> {
equals: (a, b) => a == b,
changedEvent: (player: Player<any>) => player.rotationChangeEmitter,
getValue: (player: Player<any>) => this.getSourceValue().action.oneofKind,
setValue: (eventID: EventID, player: Player<any>, newValue: APLActionType) => {
const action = this.getSourceValue();
if (action.action.oneofKind == newValue) {
setValue: (eventID: EventID, player: Player<any>, newKind: APLActionKind) => {
const sourceValue = this.getSourceValue();
const oldKind = sourceValue.action.oneofKind;
if (oldKind == newKind) {
return;
}
if (newValue) {
const factory = actionTypeFactories[newValue];
const obj: any = { oneofKind: newValue };
obj[newValue] = factory.newValue();
action.action = obj;

if (newKind) {
const factory = actionKindFactories[newKind];
let newSourceValue = this.makeAPLAction(newKind, factory.newValue());
if (sourceValue) {
// Some pre-fill logic when swapping kinds.
if (oldKind && this.actionPicker) {
if (newKind == 'sequence') {
if (sourceValue.action.oneofKind == 'strictSequence') {
(newSourceValue.action as APLActionImplStruct<'sequence'>).sequence.actions = sourceValue.action.strictSequence.actions;
} else {
(newSourceValue.action as APLActionImplStruct<'sequence'>).sequence.actions = [this.makeAPLAction(oldKind, this.actionPicker.getInputValue())];
}
} else if (newKind == 'strictSequence') {
if (sourceValue.action.oneofKind == 'sequence') {
(newSourceValue.action as APLActionImplStruct<'strictSequence'>).strictSequence.actions = sourceValue.action.sequence.actions;
} else {
(newSourceValue.action as APLActionImplStruct<'strictSequence'>).strictSequence.actions = [this.makeAPLAction(oldKind, this.actionPicker.getInputValue())];
}
} else if (sourceValue.action.oneofKind == 'sequence' && sourceValue.action.sequence.actions?.[0]?.action.oneofKind == newKind) {
newSourceValue = sourceValue.action.sequence.actions[0];
} else if (sourceValue.action.oneofKind == 'strictSequence' && sourceValue.action.strictSequence.actions?.[0]?.action.oneofKind == newKind) {
newSourceValue = sourceValue.action.strictSequence.actions[0];
}
}
}
this.setSourceValue(eventID, newSourceValue);
} else {
action.action = {
oneofKind: newValue,
sourceValue.action = {
oneofKind: newKind,
};
}
player.rotationChangeEmitter.emit(eventID);
},
});

this.currentType = undefined;
this.currentKind = undefined;
this.actionPicker = null;

this.init();
Expand All @@ -101,15 +129,15 @@ export class APLActionPicker extends Input<Player<any>, APLAction> {
}

getInputValue(): APLAction {
const actionType = this.typePicker.getInputValue();
const actionKind = this.kindPicker.getInputValue();
return APLAction.create({
condition: this.conditionPicker.getInputValue(),
action: {
oneofKind: actionType,
oneofKind: actionKind,
...((() => {
const val: any = {};
if (actionType && this.actionPicker) {
val[actionType] = this.actionPicker.getInputValue();
if (actionKind && this.actionPicker) {
val[actionKind] = this.actionPicker.getInputValue();
}
return val;
})()),
Expand All @@ -124,46 +152,55 @@ export class APLActionPicker extends Input<Player<any>, APLAction> {

this.conditionPicker.setInputValue(newValue.condition || APLValue.create());

const newActionType = newValue.action.oneofKind;
this.updateActionPicker(newActionType);
const newActionKind = newValue.action.oneofKind;
this.updateActionPicker(newActionKind);

if (newActionKind) {
this.actionPicker!.setInputValue((newValue.action as any)[newActionKind]);
}
}

if (newActionType) {
this.actionPicker!.setInputValue((newValue.action as any)[newActionType]);
private makeAPLAction<K extends NonNullable<APLActionKind>>(kind: K, implVal: APLActionImplTypesUnion[K]): APLAction {
if (!kind) {
return APLAction.create();
}
const obj: any = { oneofKind: kind };
obj[kind] = implVal;
return APLAction.create({action: obj});
}

private updateActionPicker(newActionType: APLActionType) {
const actionType = this.currentType;
if (newActionType == actionType) {
private updateActionPicker(newActionKind: APLActionKind) {
const actionKind = this.currentKind;
if (newActionKind == actionKind) {
return;
}
this.currentType = newActionType;
this.currentKind = newActionKind;

if (this.actionPicker) {
this.actionPicker.rootElem.remove();
this.actionPicker = null;
}

if (!newActionType) {
if (!newActionKind) {
return;
}

this.typePicker.setInputValue(newActionType);
this.kindPicker.setInputValue(newActionKind);

const factory = actionTypeFactories[newActionType];
const factory = actionKindFactories[newActionKind];
this.actionPicker = factory.factory(this.actionDiv, this.modObject, {
changedEvent: (player: Player<any>) => player.rotationChangeEmitter,
getValue: () => (this.getSourceValue().action as any)[newActionType] || factory.newValue(),
getValue: () => (this.getSourceValue().action as any)[newActionKind] || factory.newValue(),
setValue: (eventID: EventID, player: Player<any>, newValue: any) => {
(this.getSourceValue().action as any)[newActionType] = newValue;
(this.getSourceValue().action as any)[newActionKind] = newValue;
player.rotationChangeEmitter.emit(eventID);
},
});
this.actionPicker.rootElem.classList.add('apl-action-' + newActionType);
this.actionPicker.rootElem.classList.add('apl-action-' + newActionKind);
}
}

type ActionTypeConfig<T> = {
type ActionKindConfig<T> = {
label: string,
submenu?: Array<string>,
shortDescription: string,
Expand Down Expand Up @@ -210,7 +247,7 @@ function inputBuilder<T>(config: {
isPrepull?: boolean,
newValue: () => T,
fields: Array<AplHelpers.APLPickerBuilderFieldConfig<T, any>>,
}): ActionTypeConfig<T> {
}): ActionKindConfig<T> {
return {
label: config.label,
submenu: config.submenu,
Expand All @@ -222,7 +259,7 @@ function inputBuilder<T>(config: {
};
}

export const actionTypeFactories: Record<NonNullable<APLActionType>, ActionTypeConfig<any>> = {
const actionKindFactories: {[f in NonNullable<APLActionKind>]: ActionKindConfig<APLActionImplTypesUnion[f]>} = {
['castSpell']: inputBuilder({
label: 'Cast',
shortDescription: 'Casts the spell if possible, i.e. resource/cooldown/GCD/etc requirements are all met.',
Expand Down
14 changes: 8 additions & 6 deletions ui/core/components/individual_sim_ui/apl_helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,19 +105,19 @@ const actionIdSets: Record<ACTION_ID_SET, {
},
};

export interface APLActionIDPickerConfig<ModObject> extends Omit<DropdownPickerConfig<ModObject, ActionId>, 'defaultLabel' | 'equals' | 'setOptionContent' | 'values' | 'getValue' | 'setValue'> {
export interface APLActionIDPickerConfig<ModObject> extends Omit<DropdownPickerConfig<ModObject, ActionID, ActionId>, 'defaultLabel' | 'equals' | 'setOptionContent' | 'values' | 'getValue' | 'setValue'> {
actionIdSet: ACTION_ID_SET,
getValue: (obj: ModObject) => ActionID,
setValue: (eventID: EventID, obj: ModObject, newValue: ActionID) => void,
}

export class APLActionIDPicker extends DropdownPicker<Player<any>, ActionId> {
export class APLActionIDPicker extends DropdownPicker<Player<any>, ActionID, ActionId> {
constructor(parent: HTMLElement, player: Player<any>, config: APLActionIDPickerConfig<Player<any>>) {
const actionIdSet = actionIdSets[config.actionIdSet];
super(parent, player, {
...config,
getValue: (player) => ActionId.fromProto(config.getValue(player)),
setValue: (eventID: EventID, player: Player<any>, newValue: ActionId) => config.setValue(eventID, player, newValue.toProto()),
sourceToValue: (src: ActionID) => ActionId.fromProto(src),
valueToSource: (val: ActionId) => val.toProto(),
defaultLabel: actionIdSet.defaultLabel,
equals: (a, b) => ((a == null) == (b == null)) && (!a || a.equals(b!)),
setOptionContent: (button, valueConfig) => {
Expand Down Expand Up @@ -149,10 +149,12 @@ export class APLActionIDPicker extends DropdownPicker<Player<any>, ActionId> {
}
}

type APLPickerBuilderFieldFactory<F> = (parent: HTMLElement, player: Player<any>, config: InputConfig<Player<any>, F>) => Input<Player<any>, F>;

export interface APLPickerBuilderFieldConfig<T, F extends keyof T> {
field: F,
newValue: () => T[F],
factory: (parent: HTMLElement, player: Player<any>, config: InputConfig<Player<any>, T[F]>) => Input<Player<any>, T[F]>
factory: APLPickerBuilderFieldFactory<T[F]>,

label?: string,
labelTooltip?: string,
Expand Down Expand Up @@ -318,7 +320,7 @@ export function runeSlotFieldConfig(field: string): APLPickerBuilderFieldConfig<
};
}

export function aplInputBuilder<T>(newValue: () => T, fields: Array<APLPickerBuilderFieldConfig<T, any>>): (parent: HTMLElement, player: Player<any>, config: InputConfig<Player<any>, any>) => Input<Player<any>, any> {
export function aplInputBuilder<T>(newValue: () => T, fields: Array<APLPickerBuilderFieldConfig<T, keyof T>>): (parent: HTMLElement, player: Player<any>, config: InputConfig<Player<any>, T>) => Input<Player<any>, T> {
return (parent, player, config) => {
return new APLPickerBuilder(parent, player, {
...config,
Expand Down
Loading

0 comments on commit 414b1d4

Please sign in to comment.