From 829fde943ffdea420b54333824833d553947f49c Mon Sep 17 00:00:00 2001 From: James Tanner Date: Tue, 20 Jun 2023 20:40:38 -0700 Subject: [PATCH 1/5] Start work on adding apl values --- .../individual_sim_ui/apl_actions.ts | 127 ++++++++++++++- .../individual_sim_ui/apl_rotation_picker.ts | 144 +----------------- .../individual_sim_ui/apl_values.ts | 140 +++++++++++++++++ 3 files changed, 265 insertions(+), 146 deletions(-) create mode 100644 ui/core/components/individual_sim_ui/apl_values.ts diff --git a/ui/core/components/individual_sim_ui/apl_actions.ts b/ui/core/components/individual_sim_ui/apl_actions.ts index fd96b27e56..e7163688db 100644 --- a/ui/core/components/individual_sim_ui/apl_actions.ts +++ b/ui/core/components/individual_sim_ui/apl_actions.ts @@ -12,10 +12,11 @@ import { Input, InputConfig } from '../input.js'; import { ActionId } from '../../proto_utils/action_id.js'; import { Player } from '../../player.js'; import { stringComparator } from '../../utils.js'; +import { TextDropdownPicker } from '../dropdown_picker.js'; import * as AplHelpers from './apl_helpers.js'; -export class APLActionCastSpellPicker extends Input, APLActionCastSpell> { +class APLActionCastSpellPicker extends Input, APLActionCastSpell> { private readonly spellIdPicker: AplHelpers.APLActionIDPicker; constructor(parent: HTMLElement, player: Player, config: InputConfig, APLActionCastSpell>) { @@ -72,7 +73,7 @@ export class APLActionCastSpellPicker extends Input, APLActionCastSp } } -export class APLActionSequencePicker extends Input, APLActionSequence> { +class APLActionSequencePicker extends Input, APLActionSequence> { constructor(parent: HTMLElement, player: Player, config: InputConfig, APLActionSequence>) { super(parent, 'apl-action-sequence-picker-root', player, config); @@ -95,7 +96,7 @@ export class APLActionSequencePicker extends Input, APLActionSequenc } } -export class APLActionWaitPicker extends Input, APLActionWait> { +class APLActionWaitPicker extends Input, APLActionWait> { constructor(parent: HTMLElement, player: Player, config: InputConfig, APLActionWait>) { super(parent, 'apl-action-wait-picker-root', player, config); @@ -116,4 +117,124 @@ export class APLActionWaitPicker extends Input, APLActionWait> { return; } } +} + +export interface APLActionPickerConfig extends InputConfig, APLAction> { +} + +export type APLActionType = APLAction['action']['oneofKind']; + +export class APLActionPicker extends Input, APLAction> { + + private typePicker: TextDropdownPicker, APLActionType>; + + private currentType: APLActionType; + private actionPicker: Input, any>|null; + + constructor(parent: HTMLElement, player: Player, config: APLActionPickerConfig) { + super(parent, 'apl-action-picker-root', player, config); + + const allActionTypes = Object.keys(APLActionPicker.actionTypeFactories) as Array>; + this.typePicker = new TextDropdownPicker(this.rootElem, player, { + defaultLabel: 'Action', + values: allActionTypes.map(actionType => { + return { + value: actionType, + label: APLActionPicker.actionTypeFactories[actionType].label, + }; + }), + equals: (a, b) => a == b, + changedEvent: (player: Player) => player.rotationChangeEmitter, + getValue: (player: Player) => this.getSourceValue().action.oneofKind, + setValue: (eventID: EventID, player: Player, newValue: APLActionType) => { + const action = this.getSourceValue(); + if (action.action.oneofKind == newValue) { + return; + } + if (newValue) { + const factory = APLActionPicker.actionTypeFactories[newValue]; + const obj: any = { oneofKind: newValue }; + obj[newValue] = factory.newValue(); + action.action = obj; + } else { + action.action = { + oneofKind: newValue, + }; + } + player.rotationChangeEmitter.emit(eventID); + }, + }); + + this.currentType = undefined; + this.actionPicker = null; + + this.init(); + } + + getInputElem(): HTMLElement | null { + return this.rootElem; + } + + getInputValue(): APLAction { + const actionType = this.typePicker.getInputValue(); + return APLAction.create({ + action: { + oneofKind: actionType, + ...((() => { + if (!actionType || !this.actionPicker) return; + const val: any = {}; + val[actionType] = this.actionPicker.getInputValue(); + return val; + })()), + }, + }) + } + + setInputValue(newValue: APLAction) { + if (!newValue) { + return; + } + + const newActionType = newValue.action.oneofKind; + this.updateActionPicker(newActionType); + + if (newActionType) { + this.actionPicker!.setInputValue((newValue.action as any)[newActionType]); + } + } + + private updateActionPicker(newActionType: APLActionType) { + const actionType = this.currentType; + if (newActionType == actionType) { + return; + } + this.currentType = newActionType; + + if (this.actionPicker) { + this.actionPicker.rootElem.remove(); + this.actionPicker = null; + } + + if (!newActionType) { + return; + } + + this.typePicker.setInputValue(newActionType); + + const factory = APLActionPicker.actionTypeFactories[newActionType]; + this.actionPicker = new factory.factory(this.rootElem, this.modObject, { + changedEvent: (player: Player) => player.rotationChangeEmitter, + getValue: () => (this.getSourceValue().action as any)[newActionType] || factory.newValue(), + setValue: (eventID: EventID, player: Player, newValue: any) => { + (this.getSourceValue().action as any)[newActionType] = newValue; + player.rotationChangeEmitter.emit(eventID); + }, + }); + } + + private static actionTypeFactories: Record, { label: string, newValue: () => object, factory: new (parent: HTMLElement, player: Player, config: InputConfig, any>) => Input, any> }> = { + ['castSpell']: { label: 'Cast', newValue: APLActionCastSpell.create, factory: APLActionCastSpellPicker }, + ['sequence']: { label: 'Sequence', newValue: APLActionSequence.create, factory: APLActionSequencePicker }, + ['wait']: { label: 'Wait', newValue: APLActionWait.create, factory: APLActionWaitPicker }, + }; } \ No newline at end of file diff --git a/ui/core/components/individual_sim_ui/apl_rotation_picker.ts b/ui/core/components/individual_sim_ui/apl_rotation_picker.ts index cac6e5861b..01b33ec2fe 100644 --- a/ui/core/components/individual_sim_ui/apl_rotation_picker.ts +++ b/ui/core/components/individual_sim_ui/apl_rotation_picker.ts @@ -1,16 +1,10 @@ -import { ActionID } from '../../proto/common.js'; import { EventID } from '../../typed_event.js'; import { Player } from '../../player.js'; import { BooleanPicker } from '../boolean_picker.js'; -import { DropdownPicker, DropdownPickerConfig, TextDropdownPicker } from '../dropdown_picker.js'; import { ListItemPickerConfig, ListPicker } from '../list_picker.js'; -import { StringPicker } from '../string_picker.js'; import { APLListItem, APLAction, - APLActionCastSpell, - APLActionSequence, - APLActionWait, APLRotation, } from '../../proto/apl.js'; @@ -18,7 +12,7 @@ import { Component } from '../component.js'; import { Input, InputConfig } from '../input.js'; import { SimUI } from '../../sim_ui.js'; -import * as AplActions from './apl_actions.js'; +import { APLActionPicker } from './apl_actions.js'; export class APLRotationPicker extends Component { constructor(parent: HTMLElement, simUI: SimUI, modPlayer: Player) { @@ -52,7 +46,6 @@ class APLListItemPicker extends Input, APLListItem> { private readonly itemIndex: number; private readonly hidePicker: Input; - private readonly notesPicker: Input; private readonly actionPicker: APLActionPicker; private getItem(): APLListItem { @@ -78,18 +71,6 @@ class APLListItemPicker extends Input, APLListItem> { }, }); - 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); - }, - }); - this.actionPicker = new APLActionPicker(this.rootElem, this.player, { changedEvent: () => this.player.rotationChangeEmitter, getValue: () => this.getItem().action!, @@ -108,7 +89,6 @@ class APLListItemPicker extends Input, APLListItem> { getInputValue(): APLListItem { const item = APLListItem.create({ hide: this.hidePicker.getInputValue(), - notes: this.notesPicker.getInputValue(), action: this.actionPicker.getInputValue(), }); return item; @@ -119,128 +99,6 @@ class APLListItemPicker extends Input, APLListItem> { return; } this.hidePicker.setInputValue(newValue.hide); - this.notesPicker.setInputValue(newValue.notes); this.actionPicker.setInputValue(newValue.action || APLAction.create()); } } - -export interface APLActionPickerConfig extends InputConfig, APLAction> { -} - -type APLActionType = APLAction['action']['oneofKind']; - -class APLActionPicker extends Input, APLAction> { - - private typePicker: TextDropdownPicker, APLActionType>; - - private currentType: APLActionType; - private actionPicker: Input, any>|null; - - constructor(parent: HTMLElement, player: Player, config: APLActionPickerConfig) { - super(parent, 'apl-action-picker-root', player, config); - - const allActionTypes = Object.keys(APLActionPicker.actionTypeFactories) as Array>; - this.typePicker = new TextDropdownPicker(this.rootElem, player, { - defaultLabel: 'Action', - values: allActionTypes.map(actionType => { - return { - value: actionType, - label: APLActionPicker.actionTypeFactories[actionType].label, - }; - }), - equals: (a, b) => a == b, - changedEvent: (player: Player) => player.rotationChangeEmitter, - getValue: (player: Player) => this.getSourceValue().action.oneofKind, - setValue: (eventID: EventID, player: Player, newValue: APLActionType) => { - const action = this.getSourceValue(); - if (action.action.oneofKind == newValue) { - return; - } - if (newValue) { - const factory = APLActionPicker.actionTypeFactories[newValue]; - const obj: any = { oneofKind: newValue }; - obj[newValue] = factory.newValue(); - action.action = obj; - } else { - action.action = { - oneofKind: newValue, - }; - } - player.rotationChangeEmitter.emit(eventID); - }, - }); - - this.currentType = undefined; - this.actionPicker = null; - - this.init(); - //player.rotationChangeEmitter.on(() => this.updateActionPicker(this.typePicker.getInputValue())); - } - - getInputElem(): HTMLElement | null { - return this.rootElem; - } - - getInputValue(): APLAction { - const actionType = this.typePicker.getInputValue(); - return APLAction.create({ - action: { - oneofKind: actionType, - ...((() => { - if (!actionType || !this.actionPicker) return; - const val: any = {}; - val[actionType] = this.actionPicker.getInputValue(); - return val; - })()), - }, - }) - } - - setInputValue(newValue: APLAction) { - if (!newValue) { - return; - } - - const newActionType = newValue.action.oneofKind; - this.updateActionPicker(newActionType); - - if (newActionType) { - this.actionPicker!.setInputValue((newValue.action as any)[newActionType]); - } - } - - private updateActionPicker(newActionType: APLActionType) { - const actionType = this.currentType; - if (newActionType == actionType) { - return; - } - this.currentType = newActionType; - - if (this.actionPicker) { - this.actionPicker.rootElem.remove(); - this.actionPicker = null; - } - - if (!newActionType) { - return; - } - - this.typePicker.setInputValue(newActionType); - - const factory = APLActionPicker.actionTypeFactories[newActionType]; - this.actionPicker = new factory.factory(this.rootElem, this.modObject, { - changedEvent: (player: Player) => player.rotationChangeEmitter, - getValue: () => (this.getSourceValue().action as any)[newActionType] || factory.newValue(), - setValue: (eventID: EventID, player: Player, newValue: any) => { - (this.getSourceValue().action as any)[newActionType] = newValue; - player.rotationChangeEmitter.emit(eventID); - }, - }); - } - - private static actionTypeFactories: Record, { label: string, newValue: () => object, factory: new (parent: HTMLElement, player: Player, config: InputConfig, any>) => Input, any> }> = { - ['castSpell']: { label: 'Cast', newValue: APLActionCastSpell.create, factory: AplActions.APLActionCastSpellPicker }, - ['sequence']: { label: 'Sequence', newValue: APLActionSequence.create, factory: AplActions.APLActionSequencePicker }, - ['wait']: { label: 'Wait', newValue: APLActionWait.create, factory: AplActions.APLActionWaitPicker }, - }; -} diff --git a/ui/core/components/individual_sim_ui/apl_values.ts b/ui/core/components/individual_sim_ui/apl_values.ts new file mode 100644 index 0000000000..24893b203e --- /dev/null +++ b/ui/core/components/individual_sim_ui/apl_values.ts @@ -0,0 +1,140 @@ +import { + APLValue, + APLValueAnd, + APLValueOr, + APLValueNot, + APLValueCompare, + APLValueCompare_ComparisonOperator as ComparisonOperator, + APLValueConst, + APLValueDotIsActive, +} from '../../proto/apl.js'; + +import { ActionID, Spec } from '../../proto/common.js'; +import { EventID } from '../../typed_event.js'; +import { Input, InputConfig } from '../input.js'; +import { ActionId } from '../../proto_utils/action_id.js'; +import { Player } from '../../player.js'; +import { stringComparator } from '../../utils.js'; +import { TextDropdownPicker } from '../dropdown_picker.js'; + +import * as AplHelpers from './apl_helpers.js'; + +export interface APLValuePickerConfig extends InputConfig, APLValue> { +} + +export type APLValueType = APLValue['value']['oneofKind']; + +export class APLValuePicker extends Input, APLValue> { + + private typePicker: TextDropdownPicker, APLValueType>; + + private currentType: APLValueType; + private actionPicker: Input, any>|null; + + constructor(parent: HTMLElement, player: Player, config: APLValuePickerConfig) { + super(parent, 'apl-action-picker-root', player, config); + + const allValueTypes = Object.keys(APLValuePicker.valueTypeFactories) as Array>; + this.typePicker = new TextDropdownPicker(this.rootElem, player, { + defaultLabel: 'No Condition', + values: allValueTypes.map(actionType => { + return { + value: actionType, + label: APLValuePicker.valueTypeFactories[actionType].label, + }; + }), + equals: (a, b) => a == b, + changedEvent: (player: Player) => player.rotationChangeEmitter, + getValue: (player: Player) => this.getSourceValue().value.oneofKind, + setValue: (eventID: EventID, player: Player, newValue: APLValueType) => { + const action = this.getSourceValue(); + if (action.value.oneofKind == newValue) { + return; + } + if (newValue) { + const factory = APLValuePicker.valueTypeFactories[newValue]; + const obj: any = { oneofKind: newValue }; + obj[newValue] = factory.newValue(); + action.value = obj; + } else { + action.value = { + oneofKind: newValue, + }; + } + player.rotationChangeEmitter.emit(eventID); + }, + }); + + this.currentType = undefined; + this.actionPicker = null; + + this.init(); + } + + getInputElem(): HTMLElement | null { + return this.rootElem; + } + + getInputValue(): APLValue { + const actionType = this.typePicker.getInputValue(); + return APLValue.create({ + value: { + oneofKind: actionType, + ...((() => { + if (!actionType || !this.actionPicker) return; + const val: any = {}; + val[actionType] = this.actionPicker.getInputValue(); + return val; + })()), + }, + }) + } + + setInputValue(newValue: APLValue) { + if (!newValue) { + return; + } + + const newValueType = newValue.value.oneofKind; + this.updateValuePicker(newValueType); + + if (newValueType) { + this.actionPicker!.setInputValue((newValue.value as any)[newValueType]); + } + } + + private updateValuePicker(newValueType: APLValueType) { + const actionType = this.currentType; + if (newValueType == actionType) { + return; + } + this.currentType = newValueType; + + if (this.actionPicker) { + this.actionPicker.rootElem.remove(); + this.actionPicker = null; + } + + if (!newValueType) { + return; + } + + this.typePicker.setInputValue(newValueType); + + const factory = APLValuePicker.valueTypeFactories[newValueType]; + this.actionPicker = new factory.factory(this.rootElem, this.modObject, { + changedEvent: (player: Player) => player.rotationChangeEmitter, + getValue: () => (this.getSourceValue().value as any)[newValueType] || factory.newValue(), + setValue: (eventID: EventID, player: Player, newValue: any) => { + (this.getSourceValue().value as any)[newValueType] = newValue; + player.rotationChangeEmitter.emit(eventID); + }, + }); + } + + private static valueTypeFactories: Record, { label: string, newValue: () => object, factory: new (parent: HTMLElement, player: Player, config: InputConfig, any>) => Input, any> }> = { + ['and']: { label: 'All of', newValue: APLValueAnd.create, factory: APLValueCastSpellPicker }, + ['or']: { label: 'Any of', newValue: APLValueOr.create, factory: APLValueSequencePicker }, + ['not']: { label: 'Not', newValue: APLValueNot.create, factory: APLValueWaitPicker }, + }; +} \ No newline at end of file From ef3a1ec338cda5ecba3fa190dd9c27539066ff14 Mon Sep 17 00:00:00 2001 From: James Tanner Date: Sat, 24 Jun 2023 12:56:37 -0700 Subject: [PATCH 2/5] More work on apl --- .../individual_sim_ui/apl_values.ts | 74 ++++++++++++++++++- ui/core/individual_sim_ui.ts | 2 +- 2 files changed, 71 insertions(+), 5 deletions(-) diff --git a/ui/core/components/individual_sim_ui/apl_values.ts b/ui/core/components/individual_sim_ui/apl_values.ts index 24893b203e..412600b589 100644 --- a/ui/core/components/individual_sim_ui/apl_values.ts +++ b/ui/core/components/individual_sim_ui/apl_values.ts @@ -10,7 +10,7 @@ import { } from '../../proto/apl.js'; import { ActionID, Spec } from '../../proto/common.js'; -import { EventID } from '../../typed_event.js'; +import { EventID, TypedEvent } from '../../typed_event.js'; import { Input, InputConfig } from '../input.js'; import { ActionId } from '../../proto_utils/action_id.js'; import { Player } from '../../player.js'; @@ -19,6 +19,69 @@ import { TextDropdownPicker } from '../dropdown_picker.js'; import * as AplHelpers from './apl_helpers.js'; +export class APLValueConstPicker extends Input, APLValueConst> { + private readonly inputElem: HTMLInputElement; + + constructor(parent: HTMLElement, modObject: Player, config: InputConfig, APLValueConst>) { + super(parent, 'apl-value-const-picker-root', modObject, config); + + this.inputElem = document.createElement('input'); + this.inputElem.type = 'text'; + this.rootElem.appendChild(this.inputElem); + + this.init(); + + this.inputElem.addEventListener('change', event => { + this.inputChanged(TypedEvent.nextEventID()); + }); + } + + getInputElem(): HTMLElement { + return this.inputElem; + } + + getInputValue(): APLValueConst { + return APLValueConst.create({ val: this.inputElem.value }); + } + + setInputValue(newValue: APLValueConst) { + this.inputElem.value = newValue.val; + } +} + +export interface APLValuePickerBuilderConfig extends InputConfig, T> { +} + +class APLValuePickerBuilder extends Input, T> { + private readonly inputElem: HTMLInputElement; + + constructor(parent: HTMLElement, modObject: Player, config: APLValuePickerBuilderConfig) { + super(parent, 'apl-value-picker-builder-root', modObject, config); + + this.inputElem = document.createElement('input'); + this.inputElem.type = 'text'; + this.rootElem.appendChild(this.inputElem); + + this.init(); + + this.inputElem.addEventListener('change', event => { + this.inputChanged(TypedEvent.nextEventID()); + }); + } + + getInputElem(): HTMLElement { + return this.inputElem; + } + + getInputValue(): APLValueConst { + return APLValueConst.create({ val: this.inputElem.value }); + } + + setInputValue(newValue: APLValueConst) { + this.inputElem.value = newValue.val; + } +} + export interface APLValuePickerConfig extends InputConfig, APLValue> { } @@ -133,8 +196,11 @@ export class APLValuePicker extends Input, APLValue> { } private static valueTypeFactories: Record, { label: string, newValue: () => object, factory: new (parent: HTMLElement, player: Player, config: InputConfig, any>) => Input, any> }> = { - ['and']: { label: 'All of', newValue: APLValueAnd.create, factory: APLValueCastSpellPicker }, - ['or']: { label: 'Any of', newValue: APLValueOr.create, factory: APLValueSequencePicker }, - ['not']: { label: 'Not', newValue: APLValueNot.create, factory: APLValueWaitPicker }, + ['const']: { label: 'Const', newValue: APLValueConst.create, factory: APLValueConstPicker }, + ['and']: { label: 'All of', newValue: APLValueAnd.create, factory: APLValueConstPicker }, + ['or']: { label: 'Any of', newValue: APLValueOr.create, factory: APLValueConstPicker }, + ['not']: { label: 'Not', newValue: APLValueNot.create, factory: APLValueConstPicker }, + ['cmp']: { label: 'Compare', newValue: APLValueNot.create, factory: APLValueConstPicker }, + ['dotIsActive']: { label: 'Dot Is Active', newValue: APLValueNot.create, factory: APLValueConstPicker }, }; } \ No newline at end of file diff --git a/ui/core/individual_sim_ui.ts b/ui/core/individual_sim_ui.ts index ef6e0c698c..2c6c65ca37 100644 --- a/ui/core/individual_sim_ui.ts +++ b/ui/core/individual_sim_ui.ts @@ -277,7 +277,7 @@ export abstract class IndividualSimUI extends SimUI { this.bt = this.addBulkTab(); this.addSettingsTab(); this.addTalentsTab(); - //this.addRotationTab(); + this.addRotationTab(); if (!this.isWithinRaidSim) { this.addDetailedResultsTab(); From decf67b784a4f14b703ca7e237ba720fd3608e22 Mon Sep 17 00:00:00 2001 From: James Tanner Date: Mon, 26 Jun 2023 21:11:20 -0700 Subject: [PATCH 3/5] More work on apl --- .../individual_sim_ui/apl_actions.ts | 145 ++++------------- .../individual_sim_ui/apl_helpers.ts | 150 +++++++++++++++++- .../individual_sim_ui/apl_values.ts | 128 +++++++-------- ui/scss/core/components/_dropdown_picker.scss | 1 + .../individual_sim_ui/_apl_helpers.scss | 5 + .../_apl_rotation_picker.scss | 2 + 6 files changed, 243 insertions(+), 188 deletions(-) diff --git a/ui/core/components/individual_sim_ui/apl_actions.ts b/ui/core/components/individual_sim_ui/apl_actions.ts index e7163688db..d64428431a 100644 --- a/ui/core/components/individual_sim_ui/apl_actions.ts +++ b/ui/core/components/individual_sim_ui/apl_actions.ts @@ -1,124 +1,17 @@ import { - APLListItem, APLAction, APLActionCastSpell, APLActionSequence, APLActionWait, } from '../../proto/apl.js'; -import { ActionID, Spec } from '../../proto/common.js'; import { EventID } from '../../typed_event.js'; import { Input, InputConfig } from '../input.js'; -import { ActionId } from '../../proto_utils/action_id.js'; import { Player } from '../../player.js'; -import { stringComparator } from '../../utils.js'; import { TextDropdownPicker } from '../dropdown_picker.js'; import * as AplHelpers from './apl_helpers.js'; -class APLActionCastSpellPicker extends Input, APLActionCastSpell> { - private readonly spellIdPicker: AplHelpers.APLActionIDPicker; - - constructor(parent: HTMLElement, player: Player, config: InputConfig, APLActionCastSpell>) { - super(parent, 'apl-action-cast-spell-picker-root', player, config); - - this.spellIdPicker = new AplHelpers.APLActionIDPicker(this.rootElem, player, { - defaultLabel: 'Spell', - values: [], - changedEvent: (player: Player) => player.rotationChangeEmitter, - getValue: () => ActionId.fromProto(this.getSourceValue().spellId || ActionID.create()), - setValue: (eventID: EventID, player: Player, newValue: ActionId) => { - this.getSourceValue().spellId = newValue.toProto(); - player.rotationChangeEmitter.emit(eventID); - }, - }); - - this.init(); - - const updateValues = async () => { - const playerStats = player.getCurrentStats(); - const spellPromises = Promise.all(playerStats.spells.map(spell => ActionId.fromProto(spell).fill())); - const cooldownPromises = Promise.all(playerStats.cooldowns.map(cd => ActionId.fromProto(cd).fill())); - - let [spells, cooldowns] = await Promise.all([spellPromises, cooldownPromises]); - spells = spells.sort((a, b) => stringComparator(a.name, b.name)) - cooldowns = cooldowns.sort((a, b) => stringComparator(a.name, b.name)) - - const values = [...spells, ...cooldowns].map(actionId => { - return { - value: actionId, - }; - }); - this.spellIdPicker.setOptions(values); - }; - updateValues(); - player.currentStatsEmitter.on(updateValues); - } - - getInputElem(): HTMLElement | null { - return this.rootElem; - } - - getInputValue(): APLActionCastSpell { - return APLActionCastSpell.create({ - spellId: this.spellIdPicker.getInputValue(), - }) - } - - setInputValue(newValue: APLActionCastSpell) { - if (!newValue) { - return; - } - this.spellIdPicker.setInputValue(ActionId.fromProto(newValue.spellId || ActionID.create())); - } -} - -class APLActionSequencePicker extends Input, APLActionSequence> { - - constructor(parent: HTMLElement, player: Player, config: InputConfig, APLActionSequence>) { - super(parent, 'apl-action-sequence-picker-root', player, config); - this.init(); - } - - getInputElem(): HTMLElement | null { - return this.rootElem; - } - - getInputValue(): APLActionSequence { - return APLActionSequence.create({ - }) - } - - setInputValue(newValue: APLActionSequence) { - if (!newValue) { - return; - } - } -} - -class APLActionWaitPicker extends Input, APLActionWait> { - - constructor(parent: HTMLElement, player: Player, config: InputConfig, APLActionWait>) { - super(parent, 'apl-action-wait-picker-root', player, config); - this.init(); - } - - getInputElem(): HTMLElement | null { - return this.rootElem; - } - - getInputValue(): APLActionWait { - return APLActionWait.create({ - }) - } - - setInputValue(newValue: APLActionWait) { - if (!newValue) { - return; - } - } -} - export interface APLActionPickerConfig extends InputConfig, APLAction> { } @@ -134,13 +27,13 @@ export class APLActionPicker extends Input, APLAction> { constructor(parent: HTMLElement, player: Player, config: APLActionPickerConfig) { super(parent, 'apl-action-picker-root', player, config); - const allActionTypes = Object.keys(APLActionPicker.actionTypeFactories) as Array>; + const allActionTypes = Object.keys(actionTypeFactories) as Array>; this.typePicker = new TextDropdownPicker(this.rootElem, player, { defaultLabel: 'Action', values: allActionTypes.map(actionType => { return { value: actionType, - label: APLActionPicker.actionTypeFactories[actionType].label, + label: actionTypeFactories[actionType].label, }; }), equals: (a, b) => a == b, @@ -152,7 +45,7 @@ export class APLActionPicker extends Input, APLAction> { return; } if (newValue) { - const factory = APLActionPicker.actionTypeFactories[newValue]; + const factory = actionTypeFactories[newValue]; const obj: any = { oneofKind: newValue }; obj[newValue] = factory.newValue(); action.action = obj; @@ -221,8 +114,8 @@ export class APLActionPicker extends Input, APLAction> { this.typePicker.setInputValue(newActionType); - const factory = APLActionPicker.actionTypeFactories[newActionType]; - this.actionPicker = new factory.factory(this.rootElem, this.modObject, { + const factory = actionTypeFactories[newActionType]; + this.actionPicker = factory.factory(this.rootElem, this.modObject, { changedEvent: (player: Player) => player.rotationChangeEmitter, getValue: () => (this.getSourceValue().action as any)[newActionType] || factory.newValue(), setValue: (eventID: EventID, player: Player, newValue: any) => { @@ -231,10 +124,28 @@ export class APLActionPicker extends Input, APLAction> { }, }); } +} - private static actionTypeFactories: Record, { label: string, newValue: () => object, factory: new (parent: HTMLElement, player: Player, config: InputConfig, any>) => Input, any> }> = { - ['castSpell']: { label: 'Cast', newValue: APLActionCastSpell.create, factory: APLActionCastSpellPicker }, - ['sequence']: { label: 'Sequence', newValue: APLActionSequence.create, factory: APLActionSequencePicker }, - ['wait']: { label: 'Wait', newValue: APLActionWait.create, factory: APLActionWaitPicker }, +type ActionTypeConfig = { + label: string, + newValue: () => object, + factory: (parent: HTMLElement, player: Player, config: InputConfig, any>) => Input, any>, +}; + +function inputBuilder(label: string, newValue: () => T, fields: Array>): ActionTypeConfig { + return { + label: label, + newValue: newValue, + factory: AplHelpers.aplInputBuilder(newValue, fields), }; -} \ No newline at end of file +} + +export const actionTypeFactories: Record, ActionTypeConfig> = { + ['castSpell']: inputBuilder('Cast', APLActionCastSpell.create, [ + AplHelpers.actionIdFieldConfig('spellId', 'all_spells'), + ]), + ['sequence']: inputBuilder('Sequence', APLActionSequence.create, [ + ]), + ['wait']: inputBuilder('Wait', APLActionWait.create, [ + ]), +}; \ No newline at end of file diff --git a/ui/core/components/individual_sim_ui/apl_helpers.ts b/ui/core/components/individual_sim_ui/apl_helpers.ts index c0a7c3babe..edc1bbc5f9 100644 --- a/ui/core/components/individual_sim_ui/apl_helpers.ts +++ b/ui/core/components/individual_sim_ui/apl_helpers.ts @@ -1,15 +1,61 @@ import { ActionId } from '../../proto_utils/action_id.js'; import { Player } from '../../player.js'; -import { DropdownPicker, DropdownPickerConfig, TextDropdownPicker } from '../dropdown_picker.js'; +import { EventID, TypedEvent } from '../../typed_event.js'; +import { stringComparator } from '../../utils.js'; +import { DropdownPicker, DropdownPickerConfig, DropdownValueConfig, TextDropdownPicker } from '../dropdown_picker.js'; +import { Input, InputConfig } from '../input.js'; +import { ActionID } from 'ui/core/proto/common.js'; +export type ACTION_ID_SET = 'all_spells' | 'dots'; -export interface APLActionIDPickerConfig extends Omit, 'equals' | 'setOptionContent'> { +export interface APLActionIDPickerConfig extends Omit, 'defaultLabel' | 'equals' | 'setOptionContent' | 'values'> { + actionIdSet: ACTION_ID_SET, } +const actionIdSets: Record) => Promise>>, +}> = { + ['all_spells']: { + defaultLabel: 'Spell', + getActionIDs: async (player) => { + const playerStats = player.getCurrentStats(); + const spellPromises = Promise.all(playerStats.spells.map(spell => ActionId.fromProto(spell).fill())); + const cooldownPromises = Promise.all(playerStats.cooldowns.map(cd => ActionId.fromProto(cd).fill())); + + let [spells, cooldowns] = await Promise.all([spellPromises, cooldownPromises]); + spells = spells.sort((a, b) => stringComparator(a.name, b.name)) + cooldowns = cooldowns.sort((a, b) => stringComparator(a.name, b.name)) + + return [...spells, ...cooldowns].map(actionId => { + return { + value: actionId, + }; + }); + }, + }, + ['dots']: { + defaultLabel: 'DoT Spell', + getActionIDs: async (player) => { + const playerStats = player.getCurrentStats(); + let spells = await Promise.all(playerStats.spells.map(spell => ActionId.fromProto(spell).fill())); + spells = spells.sort((a, b) => stringComparator(a.name, b.name)) + + return spells.map(actionId => { + return { + value: actionId, + }; + }); + }, + }, +}; + export class APLActionIDPicker extends DropdownPicker, ActionId> { constructor(parent: HTMLElement, player: Player, config: APLActionIDPickerConfig>) { + const actionIdSet = actionIdSets[config.actionIdSet]; super(parent, player, { ...config, + defaultLabel: actionIdSet.defaultLabel, equals: (a, b) => ((a == null) == (b == null)) && (!a || a.equals(b!)), setOptionContent: (button, valueConfig) => { const actionId = valueConfig.value; @@ -22,6 +68,106 @@ export class APLActionIDPicker extends DropdownPicker, ActionId> { const textElem = document.createTextNode(actionId.name); button.appendChild(textElem); }, + values: [], + }); + + const getActionIDs = actionIdSet.getActionIDs; + const updateValues = async () => { + const values = await getActionIDs(player); + this.setOptions(values); + }; + updateValues(); + player.currentStatsEmitter.on(updateValues); + } +} + +export interface APLPickerBuilderFieldConfig { + field: F, + newValue: () => T[F], + factory: (parent: HTMLElement, player: Player, config: InputConfig, T[F]>) => Input, T[F]> +} + +export interface APLPickerBuilderConfig extends InputConfig, T> { + newValue: () => T, + fields: Array>, +} + +export interface APLPickerBuilderField extends APLPickerBuilderFieldConfig { + picker: Input, T[F]>, +} + +export class APLPickerBuilder extends Input, T> { + private readonly config: APLPickerBuilderConfig; + private readonly fieldPickers: Array>; + + constructor(parent: HTMLElement, modObject: Player, config: APLPickerBuilderConfig) { + super(parent, 'apl-picker-builder-root', modObject, config); + this.config = config; + + const openSpan = document.createElement('span'); + openSpan.textContent = '('; + this.rootElem.appendChild(openSpan); + + this.fieldPickers = config.fields.map(fieldConfig => APLPickerBuilder.makeFieldPicker(this, fieldConfig)); + + const closeSpan = document.createElement('span'); + closeSpan.textContent = ')'; + this.rootElem.appendChild(closeSpan); + + this.init(); + } + + private static makeFieldPicker(builder: APLPickerBuilder, fieldConfig: APLPickerBuilderFieldConfig): APLPickerBuilderField { + const field: F = fieldConfig.field; + return { + ...fieldConfig, + picker: fieldConfig.factory(builder.rootElem, builder.modObject, { + changedEvent: (player: Player) => player.rotationChangeEmitter, + getValue: () => builder.getSourceValue()[field] || fieldConfig.newValue(), + setValue: (eventID: EventID, player: Player, newValue: any) => { + builder.getSourceValue()[field] = newValue; + player.rotationChangeEmitter.emit(eventID); + }, + }), + }; + } + + getInputElem(): HTMLElement { + return this.rootElem; + } + + getInputValue(): T { + const val = this.config.newValue(); + this.fieldPickers.forEach(pickerData => { + val[pickerData.field as keyof T] = pickerData.picker.getInputValue(); + }); + return val; + } + + setInputValue(newValue: T) { + this.fieldPickers.forEach(pickerData => { + pickerData.picker.setInputValue(newValue[pickerData.field as keyof T]); }); } +} + +export function actionIdFieldConfig(field: string, actionIdSet: ACTION_ID_SET): APLPickerBuilderFieldConfig { + return { + field: field, + newValue: () => ActionID.create(), + factory: (parent, player, config) => new APLActionIDPicker(parent, player, { + ...config, + actionIdSet: actionIdSet, + }), + }; +} + +export function aplInputBuilder(newValue: () => T, fields: Array>): (parent: HTMLElement, player: Player, config: InputConfig, any>) => Input, any> { + return (parent, player, config) => { + return new APLPickerBuilder(parent, player, { + ...config, + newValue: newValue, + fields: fields, + }) + } } \ No newline at end of file diff --git a/ui/core/components/individual_sim_ui/apl_values.ts b/ui/core/components/individual_sim_ui/apl_values.ts index 412600b589..75f8b71975 100644 --- a/ui/core/components/individual_sim_ui/apl_values.ts +++ b/ui/core/components/individual_sim_ui/apl_values.ts @@ -9,17 +9,14 @@ import { APLValueDotIsActive, } from '../../proto/apl.js'; -import { ActionID, Spec } from '../../proto/common.js'; import { EventID, TypedEvent } from '../../typed_event.js'; import { Input, InputConfig } from '../input.js'; -import { ActionId } from '../../proto_utils/action_id.js'; import { Player } from '../../player.js'; -import { stringComparator } from '../../utils.js'; import { TextDropdownPicker } from '../dropdown_picker.js'; import * as AplHelpers from './apl_helpers.js'; -export class APLValueConstPicker extends Input, APLValueConst> { +export class APLValueConstValuePicker extends Input, APLValueConst> { private readonly inputElem: HTMLInputElement; constructor(parent: HTMLElement, modObject: Player, config: InputConfig, APLValueConst>) { @@ -49,39 +46,6 @@ export class APLValueConstPicker extends Input, APLValueConst> { } } -export interface APLValuePickerBuilderConfig extends InputConfig, T> { -} - -class APLValuePickerBuilder extends Input, T> { - private readonly inputElem: HTMLInputElement; - - constructor(parent: HTMLElement, modObject: Player, config: APLValuePickerBuilderConfig) { - super(parent, 'apl-value-picker-builder-root', modObject, config); - - this.inputElem = document.createElement('input'); - this.inputElem.type = 'text'; - this.rootElem.appendChild(this.inputElem); - - this.init(); - - this.inputElem.addEventListener('change', event => { - this.inputChanged(TypedEvent.nextEventID()); - }); - } - - getInputElem(): HTMLElement { - return this.inputElem; - } - - getInputValue(): APLValueConst { - return APLValueConst.create({ val: this.inputElem.value }); - } - - setInputValue(newValue: APLValueConst) { - this.inputElem.value = newValue.val; - } -} - export interface APLValuePickerConfig extends InputConfig, APLValue> { } @@ -92,35 +56,35 @@ export class APLValuePicker extends Input, APLValue> { private typePicker: TextDropdownPicker, APLValueType>; private currentType: APLValueType; - private actionPicker: Input, any>|null; + private valuePicker: Input, any>|null; constructor(parent: HTMLElement, player: Player, config: APLValuePickerConfig) { - super(parent, 'apl-action-picker-root', player, config); + super(parent, 'apl-value-picker-root', player, config); - const allValueTypes = Object.keys(APLValuePicker.valueTypeFactories) as Array>; + const allValueTypes = Object.keys(valueTypeFactories) as Array>; this.typePicker = new TextDropdownPicker(this.rootElem, player, { defaultLabel: 'No Condition', - values: allValueTypes.map(actionType => { + values: allValueTypes.map(valueType => { return { - value: actionType, - label: APLValuePicker.valueTypeFactories[actionType].label, + value: valueType, + label: valueTypeFactories[valueType].label, }; }), equals: (a, b) => a == b, changedEvent: (player: Player) => player.rotationChangeEmitter, getValue: (player: Player) => this.getSourceValue().value.oneofKind, setValue: (eventID: EventID, player: Player, newValue: APLValueType) => { - const action = this.getSourceValue(); - if (action.value.oneofKind == newValue) { + const value = this.getSourceValue(); + if (value.value.oneofKind == newValue) { return; } if (newValue) { - const factory = APLValuePicker.valueTypeFactories[newValue]; + const factory = valueTypeFactories[newValue]; const obj: any = { oneofKind: newValue }; obj[newValue] = factory.newValue(); - action.value = obj; + value.value = obj; } else { - action.value = { + value.value = { oneofKind: newValue, }; } @@ -129,7 +93,7 @@ export class APLValuePicker extends Input, APLValue> { }); this.currentType = undefined; - this.actionPicker = null; + this.valuePicker = null; this.init(); } @@ -139,14 +103,14 @@ export class APLValuePicker extends Input, APLValue> { } getInputValue(): APLValue { - const actionType = this.typePicker.getInputValue(); + const valueType = this.typePicker.getInputValue(); return APLValue.create({ value: { - oneofKind: actionType, + oneofKind: valueType, ...((() => { - if (!actionType || !this.actionPicker) return; + if (!valueType || !this.valuePicker) return; const val: any = {}; - val[actionType] = this.actionPicker.getInputValue(); + val[valueType] = this.valuePicker.getInputValue(); return val; })()), }, @@ -162,20 +126,20 @@ export class APLValuePicker extends Input, APLValue> { this.updateValuePicker(newValueType); if (newValueType) { - this.actionPicker!.setInputValue((newValue.value as any)[newValueType]); + this.valuePicker!.setInputValue((newValue.value as any)[newValueType]); } } private updateValuePicker(newValueType: APLValueType) { - const actionType = this.currentType; - if (newValueType == actionType) { + const valueType = this.currentType; + if (newValueType == valueType) { return; } this.currentType = newValueType; - if (this.actionPicker) { - this.actionPicker.rootElem.remove(); - this.actionPicker = null; + if (this.valuePicker) { + this.valuePicker.rootElem.remove(); + this.valuePicker = null; } if (!newValueType) { @@ -184,8 +148,8 @@ export class APLValuePicker extends Input, APLValue> { this.typePicker.setInputValue(newValueType); - const factory = APLValuePicker.valueTypeFactories[newValueType]; - this.actionPicker = new factory.factory(this.rootElem, this.modObject, { + const factory = valueTypeFactories[newValueType]; + this.valuePicker = factory.factory(this.rootElem, this.modObject, { changedEvent: (player: Player) => player.rotationChangeEmitter, getValue: () => (this.getSourceValue().value as any)[newValueType] || factory.newValue(), setValue: (eventID: EventID, player: Player, newValue: any) => { @@ -194,13 +158,39 @@ export class APLValuePicker extends Input, APLValue> { }, }); } +} - private static valueTypeFactories: Record, { label: string, newValue: () => object, factory: new (parent: HTMLElement, player: Player, config: InputConfig, any>) => Input, any> }> = { - ['const']: { label: 'Const', newValue: APLValueConst.create, factory: APLValueConstPicker }, - ['and']: { label: 'All of', newValue: APLValueAnd.create, factory: APLValueConstPicker }, - ['or']: { label: 'Any of', newValue: APLValueOr.create, factory: APLValueConstPicker }, - ['not']: { label: 'Not', newValue: APLValueNot.create, factory: APLValueConstPicker }, - ['cmp']: { label: 'Compare', newValue: APLValueNot.create, factory: APLValueConstPicker }, - ['dotIsActive']: { label: 'Dot Is Active', newValue: APLValueNot.create, factory: APLValueConstPicker }, +type ValueTypeConfig = { + label: string, + newValue: () => object, + factory: (parent: HTMLElement, player: Player, config: InputConfig, any>) => Input, any>, +}; + +function inputBuilder(label: string, newValue: () => T, fields: Array>): ValueTypeConfig { + return { + label: label, + newValue: newValue, + factory: AplHelpers.aplInputBuilder(newValue, fields), }; -} \ No newline at end of file +} + +const valueTypeFactories: Record, ValueTypeConfig> = { + ['const']: inputBuilder('Const', APLValueConst.create, [ + { + field: 'value', + newValue: () => '', + factory: (parent, player, config) => new APLValueConstValuePicker(parent, player, config), + }, + ]), + ['and']: inputBuilder('All of', APLValueAnd.create, [ + ]), + ['or']: inputBuilder('Any of', APLValueOr.create, [ + ]), + ['not']: inputBuilder('Not', APLValueNot.create, [ + ]), + ['cmp']: inputBuilder('Compare', APLValueCompare.create, [ + ]), + ['dotIsActive']: inputBuilder('Dot Is Active', APLValueDotIsActive.create, [ + AplHelpers.actionIdFieldConfig('value', 'dots'), + ]), +}; \ No newline at end of file diff --git a/ui/scss/core/components/_dropdown_picker.scss b/ui/scss/core/components/_dropdown_picker.scss index f75ce94b58..0ba58ebc26 100644 --- a/ui/scss/core/components/_dropdown_picker.scss +++ b/ui/scss/core/components/_dropdown_picker.scss @@ -6,4 +6,5 @@ .dropdown-picker-button, .dropdown-picker-item>.dropdown-item { display: flex; align-items: center; + padding: 2px; } \ No newline at end of file diff --git a/ui/scss/core/components/individual_sim_ui/_apl_helpers.scss b/ui/scss/core/components/individual_sim_ui/_apl_helpers.scss index 48782712d2..8fbd5fd00c 100644 --- a/ui/scss/core/components/individual_sim_ui/_apl_helpers.scss +++ b/ui/scss/core/components/individual_sim_ui/_apl_helpers.scss @@ -5,4 +5,9 @@ height: 1rem; margin: 2px; background-size: cover; +} + +.apl-picker-builder-root { + flex-direction: row; + align-items: center; } \ No newline at end of file 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 index a830d10d80..75c3403418 100644 --- a/ui/scss/core/components/individual_sim_ui/_apl_rotation_picker.scss +++ b/ui/scss/core/components/individual_sim_ui/_apl_rotation_picker.scss @@ -26,6 +26,8 @@ .apl-list-item-picker-root { display: flex; align-items: center; + flex-direction: row; + margin: 5px; &> :not(:last-child) { margin-right: map.get($spacers, 2); From 4a8ce758d178659085e6670c065b0eb640ebfef2 Mon Sep 17 00:00:00 2001 From: James Tanner Date: Wed, 28 Jun 2023 22:11:05 -0700 Subject: [PATCH 4/5] More work on apl --- .../individual_sim_ui/apl_actions.ts | 36 ++++++++++++++++--- .../individual_sim_ui/apl_helpers.ts | 16 +++++++-- .../individual_sim_ui/apl_rotation_picker.ts | 1 + .../individual_sim_ui/apl_values.ts | 9 ++--- .../_apl_rotation_picker.scss | 11 +++++- 5 files changed, 61 insertions(+), 12 deletions(-) diff --git a/ui/core/components/individual_sim_ui/apl_actions.ts b/ui/core/components/individual_sim_ui/apl_actions.ts index d64428431a..a538a6f717 100644 --- a/ui/core/components/individual_sim_ui/apl_actions.ts +++ b/ui/core/components/individual_sim_ui/apl_actions.ts @@ -3,6 +3,7 @@ import { APLActionCastSpell, APLActionSequence, APLActionWait, + APLValue, } from '../../proto/apl.js'; import { EventID } from '../../typed_event.js'; @@ -11,6 +12,7 @@ import { Player } from '../../player.js'; import { TextDropdownPicker } from '../dropdown_picker.js'; import * as AplHelpers from './apl_helpers.js'; +import * as AplValues from './apl_values.js'; export interface APLActionPickerConfig extends InputConfig, APLAction> { } @@ -21,14 +23,36 @@ export class APLActionPicker extends Input, APLAction> { private typePicker: TextDropdownPicker, APLActionType>; + private readonly actionDiv: HTMLElement; private currentType: APLActionType; private actionPicker: Input, any>|null; + private readonly conditionPicker: Input, APLValue>; + constructor(parent: HTMLElement, player: Player, config: APLActionPickerConfig) { super(parent, 'apl-action-picker-root', player, config); + this.conditionPicker = new AplValues.APLValuePicker(this.rootElem, this.modObject, { + changedEvent: (player: Player) => player.rotationChangeEmitter, + getValue: (player: Player) => { + const action = this.getSourceValue(); + if (!action.condition) { + action.condition = APLValue.create(); + } + return action.condition; + }, + setValue: (eventID: EventID, player: Player, newValue: APLValue) => { + this.getSourceValue().condition = newValue; + player.rotationChangeEmitter.emit(eventID); + }, + }); + + this.actionDiv = document.createElement('div'); + this.actionDiv.classList.add('apl-action-picker-action'); + this.rootElem.appendChild(this.actionDiv); + const allActionTypes = Object.keys(actionTypeFactories) as Array>; - this.typePicker = new TextDropdownPicker(this.rootElem, player, { + this.typePicker = new TextDropdownPicker(this.actionDiv, player, { defaultLabel: 'Action', values: allActionTypes.map(actionType => { return { @@ -71,12 +95,14 @@ export class APLActionPicker extends Input, APLAction> { getInputValue(): APLAction { const actionType = this.typePicker.getInputValue(); return APLAction.create({ + condition: this.conditionPicker.getInputValue(), action: { oneofKind: actionType, ...((() => { - if (!actionType || !this.actionPicker) return; const val: any = {}; - val[actionType] = this.actionPicker.getInputValue(); + if (actionType && this.actionPicker) { + val[actionType] = this.actionPicker.getInputValue(); + } return val; })()), }, @@ -88,6 +114,8 @@ export class APLActionPicker extends Input, APLAction> { return; } + this.conditionPicker.setInputValue(newValue.condition || APLValue.create()); + const newActionType = newValue.action.oneofKind; this.updateActionPicker(newActionType); @@ -115,7 +143,7 @@ export class APLActionPicker extends Input, APLAction> { this.typePicker.setInputValue(newActionType); const factory = actionTypeFactories[newActionType]; - this.actionPicker = factory.factory(this.rootElem, this.modObject, { + this.actionPicker = factory.factory(this.actionDiv, this.modObject, { changedEvent: (player: Player) => player.rotationChangeEmitter, getValue: () => (this.getSourceValue().action as any)[newActionType] || factory.newValue(), setValue: (eventID: EventID, player: Player, newValue: any) => { diff --git a/ui/core/components/individual_sim_ui/apl_helpers.ts b/ui/core/components/individual_sim_ui/apl_helpers.ts index edc1bbc5f9..e06f1c22c0 100644 --- a/ui/core/components/individual_sim_ui/apl_helpers.ts +++ b/ui/core/components/individual_sim_ui/apl_helpers.ts @@ -4,12 +4,14 @@ import { EventID, TypedEvent } from '../../typed_event.js'; import { stringComparator } from '../../utils.js'; import { DropdownPicker, DropdownPickerConfig, DropdownValueConfig, TextDropdownPicker } from '../dropdown_picker.js'; import { Input, InputConfig } from '../input.js'; -import { ActionID } from 'ui/core/proto/common.js'; +import { ActionID } from '../../proto/common.js'; export type ACTION_ID_SET = 'all_spells' | 'dots'; -export interface APLActionIDPickerConfig extends Omit, 'defaultLabel' | 'equals' | 'setOptionContent' | 'values'> { +export interface APLActionIDPickerConfig extends Omit, 'defaultLabel' | 'equals' | 'setOptionContent' | 'values' | 'getValue' | 'setValue'> { actionIdSet: ACTION_ID_SET, + getValue: (obj: ModObject) => ActionID, + setValue: (eventID: EventID, obj: ModObject, newValue: ActionID) => void, } const actionIdSets: Record, ActionId> { const actionIdSet = actionIdSets[config.actionIdSet]; super(parent, player, { ...config, + getValue: (player) => ActionId.fromProto(config.getValue(player)), + setValue: (eventID: EventID, player: Player, newValue: ActionId) => config.setValue(eventID, player, newValue.toProto()), defaultLabel: actionIdSet.defaultLabel, equals: (a, b) => ((a == null) == (b == null)) && (!a || a.equals(b!)), setOptionContent: (button, valueConfig) => { @@ -123,7 +127,13 @@ export class APLPickerBuilder extends Input, T> { ...fieldConfig, picker: fieldConfig.factory(builder.rootElem, builder.modObject, { changedEvent: (player: Player) => player.rotationChangeEmitter, - getValue: () => builder.getSourceValue()[field] || fieldConfig.newValue(), + getValue: () => { + const source = builder.getSourceValue(); + if (!source[field]) { + source[field] = fieldConfig.newValue(); + } + return source[field]; + }, setValue: (eventID: EventID, player: Player, newValue: any) => { builder.getSourceValue()[field] = newValue; player.rotationChangeEmitter.emit(eventID); diff --git a/ui/core/components/individual_sim_ui/apl_rotation_picker.ts b/ui/core/components/individual_sim_ui/apl_rotation_picker.ts index 01b33ec2fe..d4266932d4 100644 --- a/ui/core/components/individual_sim_ui/apl_rotation_picker.ts +++ b/ui/core/components/individual_sim_ui/apl_rotation_picker.ts @@ -6,6 +6,7 @@ import { APLListItem, APLAction, APLRotation, + APLValue, } from '../../proto/apl.js'; import { Component } from '../component.js'; diff --git a/ui/core/components/individual_sim_ui/apl_values.ts b/ui/core/components/individual_sim_ui/apl_values.ts index 75f8b71975..7ef3f66d44 100644 --- a/ui/core/components/individual_sim_ui/apl_values.ts +++ b/ui/core/components/individual_sim_ui/apl_values.ts @@ -42,7 +42,7 @@ export class APLValueConstValuePicker extends Input, APLValueConst> } setInputValue(newValue: APLValueConst) { - this.inputElem.value = newValue.val; + this.inputElem.value = newValue ? newValue.val : ''; } } @@ -108,9 +108,10 @@ export class APLValuePicker extends Input, APLValue> { value: { oneofKind: valueType, ...((() => { - if (!valueType || !this.valuePicker) return; const val: any = {}; - val[valueType] = this.valuePicker.getInputValue(); + if (valueType && this.valuePicker) { + val[valueType] = this.valuePicker.getInputValue(); + } return val; })()), }, @@ -191,6 +192,6 @@ const valueTypeFactories: Record, ValueTypeConfig> = ['cmp']: inputBuilder('Compare', APLValueCompare.create, [ ]), ['dotIsActive']: inputBuilder('Dot Is Active', APLValueDotIsActive.create, [ - AplHelpers.actionIdFieldConfig('value', 'dots'), + AplHelpers.actionIdFieldConfig('spellId', 'dots'), ]), }; \ No newline at end of file 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 index 75c3403418..2aead1274b 100644 --- a/ui/scss/core/components/individual_sim_ui/_apl_rotation_picker.scss +++ b/ui/scss/core/components/individual_sim_ui/_apl_rotation_picker.scss @@ -38,11 +38,20 @@ } } -.apl-action-picker-root { +.apl-action-picker-root, .apl-value-picker-root { flex-direction: row; margin: 2px; .input-root { margin: 0; } +} +.apl-list-item-picker-root > .apl-action-picker-root { + flex-direction: column; +} + +.apl-action-picker-action { + display: flex; + flex-direction: row; + align-items: center; } \ No newline at end of file From 09fd92478fe0af223ca6ac153ac40271366c1a56 Mon Sep 17 00:00:00 2001 From: James Tanner Date: Sat, 1 Jul 2023 15:19:37 -0700 Subject: [PATCH 5/5] Lots of UI work for APL, all existing apl values implemented now --- .../individual_sim_ui/apl_actions.ts | 12 +- .../individual_sim_ui/apl_rotation_picker.ts | 68 ++++-- .../individual_sim_ui/apl_values.ts | 186 +++++++++++---- ui/core/components/input.ts | 4 + ui/core/components/list_picker.ts | 211 ++++++++++++------ ui/core/individual_sim_ui.ts | 2 +- ui/scss/core/components/_dropdown_picker.scss | 4 + ui/scss/core/components/_list_picker.scss | 11 + .../individual_sim_ui/_apl_helpers.scss | 1 + .../_apl_rotation_picker.scss | 18 ++ .../shared/_bootstrap_style_overrides.scss | 4 + 11 files changed, 378 insertions(+), 143 deletions(-) diff --git a/ui/core/components/individual_sim_ui/apl_actions.ts b/ui/core/components/individual_sim_ui/apl_actions.ts index a538a6f717..e3aae8e38d 100644 --- a/ui/core/components/individual_sim_ui/apl_actions.ts +++ b/ui/core/components/individual_sim_ui/apl_actions.ts @@ -27,21 +27,15 @@ export class APLActionPicker extends Input, APLAction> { private currentType: APLActionType; private actionPicker: Input, any>|null; - private readonly conditionPicker: Input, APLValue>; + private readonly conditionPicker: AplValues.APLValuePicker; constructor(parent: HTMLElement, player: Player, config: APLActionPickerConfig) { super(parent, 'apl-action-picker-root', player, config); this.conditionPicker = new AplValues.APLValuePicker(this.rootElem, this.modObject, { changedEvent: (player: Player) => player.rotationChangeEmitter, - getValue: (player: Player) => { - const action = this.getSourceValue(); - if (!action.condition) { - action.condition = APLValue.create(); - } - return action.condition; - }, - setValue: (eventID: EventID, player: Player, newValue: APLValue) => { + getValue: (player: Player) => this.getSourceValue().condition, + setValue: (eventID: EventID, player: Player, newValue: APLValue|undefined) => { this.getSourceValue().condition = newValue; player.rotationChangeEmitter.emit(eventID); }, diff --git a/ui/core/components/individual_sim_ui/apl_rotation_picker.ts b/ui/core/components/individual_sim_ui/apl_rotation_picker.ts index d4266932d4..d81e13b8c7 100644 --- a/ui/core/components/individual_sim_ui/apl_rotation_picker.ts +++ b/ui/core/components/individual_sim_ui/apl_rotation_picker.ts @@ -1,4 +1,6 @@ -import { EventID } from '../../typed_event.js'; +import { Tooltip } from 'bootstrap'; + +import { EventID, TypedEvent } from '../../typed_event.js'; import { Player } from '../../player.js'; import { BooleanPicker } from '../boolean_picker.js'; import { ListItemPickerConfig, ListPicker } from '../list_picker.js'; @@ -6,7 +8,6 @@ import { APLListItem, APLAction, APLRotation, - APLValue, } from '../../proto/apl.js'; import { Component } from '../component.js'; @@ -34,7 +35,7 @@ export class APLRotationPicker extends Component { action: {}, }), copyItem: (oldItem: APLListItem) => APLListItem.clone(oldItem), - newItemPicker: (parent: HTMLElement, listPicker: ListPicker, APLListItem>, index: number, config: ListItemPickerConfig, APLListItem>) => new APLListItemPicker(parent, modPlayer, index, config), + newItemPicker: (parent: HTMLElement, listPicker: ListPicker, APLListItem>, index: number, config: ListItemPickerConfig, APLListItem>) => new APLListItemPicker(parent, modPlayer, config), inlineMenuBar: true, }); @@ -44,9 +45,8 @@ export class APLRotationPicker extends Component { class APLListItemPicker extends Input, APLListItem> { private readonly player: Player; - private readonly itemIndex: number; - private readonly hidePicker: Input; + private readonly hidePicker: Input, boolean>; private readonly actionPicker: APLActionPicker; private getItem(): APLListItem { @@ -55,18 +55,17 @@ class APLListItemPicker extends Input, APLListItem> { }); } - constructor(parent: HTMLElement, player: Player, itemIndex: number, config: ListItemPickerConfig, APLListItem>) { - super(parent, 'apl-list-item-picker-root', player, config); + constructor(parent: HTMLElement, player: Player, config: ListItemPickerConfig, APLListItem>) { + super(parent, 'apl-list-item-picker-root', player, { + ...config, + enableWhen: () => !this.getItem().hide, + }); this.player = player; - this.itemIndex = itemIndex; - this.hidePicker = new BooleanPicker(this.rootElem, null, { - label: 'Hide', - labelTooltip: 'Ignores this APL action.', - inline: true, + this.hidePicker = new HidePicker(ListPicker.getItemHeaderElem(this), player, { changedEvent: () => this.player.rotationChangeEmitter, getValue: () => this.getItem().hide, - setValue: (eventID: EventID, _: null, newValue: boolean) => { + setValue: (eventID: EventID, player: Player, newValue: boolean) => { this.getItem().hide = newValue; this.player.rotationChangeEmitter.emit(eventID); }, @@ -103,3 +102,46 @@ class APLListItemPicker extends Input, APLListItem> { this.actionPicker.setInputValue(newValue.action || APLAction.create()); } } + +class HidePicker extends Input, boolean> { + private readonly inputElem: HTMLElement; + private readonly iconElem: HTMLElement; + private tooltip: Tooltip; + + constructor(parent: HTMLElement, modObject: Player, config: InputConfig, boolean>) { + super(parent, 'hide-picker-root', modObject, config); + + this.inputElem = ListPicker.makeActionElem('hide-picker-button', 'Enable/Disable', 'fa-eye'); + this.iconElem = this.inputElem.childNodes[0] as HTMLElement; + this.rootElem.appendChild(this.inputElem); + this.tooltip = Tooltip.getOrCreateInstance(this.inputElem); + + this.init(); + + this.inputElem.addEventListener('click', event => { + this.setInputValue(!this.getInputValue()); + this.inputChanged(TypedEvent.nextEventID()); + }); + } + + getInputElem(): HTMLElement { + return this.inputElem; + } + + getInputValue(): boolean { + return this.iconElem.classList.contains('fa-eye-slash'); + } + + setInputValue(newValue: boolean) { + if (newValue) { + this.iconElem.classList.add('fa-eye-slash'); + this.iconElem.classList.remove('fa-eye'); + this.tooltip.setContent({'.tooltip-inner': 'Enable Action'}); + } else { + this.iconElem.classList.add('fa-eye'); + this.iconElem.classList.remove('fa-eye-slash'); + //this.inputElem.setAttribute('data-bs-title', 'Disable Action'); + this.tooltip.setContent({'.tooltip-inner': 'Disable Action'}); + } + } +} \ No newline at end of file diff --git a/ui/core/components/individual_sim_ui/apl_values.ts b/ui/core/components/individual_sim_ui/apl_values.ts index 7ef3f66d44..1ea9e2693f 100644 --- a/ui/core/components/individual_sim_ui/apl_values.ts +++ b/ui/core/components/individual_sim_ui/apl_values.ts @@ -12,14 +12,15 @@ import { import { EventID, TypedEvent } from '../../typed_event.js'; import { Input, InputConfig } from '../input.js'; import { Player } from '../../player.js'; -import { TextDropdownPicker } from '../dropdown_picker.js'; +import { TextDropdownPicker, TextDropdownValueConfig } from '../dropdown_picker.js'; +import { ListItemPickerConfig, ListPicker, ListItemAction } from '../list_picker.js'; import * as AplHelpers from './apl_helpers.js'; -export class APLValueConstValuePicker extends Input, APLValueConst> { +export class APLValueConstValuePicker extends Input, string> { private readonly inputElem: HTMLInputElement; - constructor(parent: HTMLElement, modObject: Player, config: InputConfig, APLValueConst>) { + constructor(parent: HTMLElement, modObject: Player, config: InputConfig, string>) { super(parent, 'apl-value-const-picker-root', modObject, config); this.inputElem = document.createElement('input'); @@ -31,27 +32,38 @@ export class APLValueConstValuePicker extends Input, APLValueConst> this.inputElem.addEventListener('change', event => { this.inputChanged(TypedEvent.nextEventID()); }); + this.inputElem.addEventListener('input', event => { + this.updateSize(); + }); + this.updateSize(); } getInputElem(): HTMLElement { return this.inputElem; } - getInputValue(): APLValueConst { - return APLValueConst.create({ val: this.inputElem.value }); + getInputValue(): string { + return this.inputElem.value; + } + + setInputValue(newValue: string) { + this.inputElem.value = newValue; + this.updateSize(); } - setInputValue(newValue: APLValueConst) { - this.inputElem.value = newValue ? newValue.val : ''; + private updateSize() { + const newSize = Math.max(3, this.inputElem.value.length); + if (this.inputElem.size != newSize) + this.inputElem.size = newSize; } } -export interface APLValuePickerConfig extends InputConfig, APLValue> { +export interface APLValuePickerConfig extends InputConfig, APLValue|undefined> { } export type APLValueType = APLValue['value']['oneofKind']; -export class APLValuePicker extends Input, APLValue> { +export class APLValuePicker extends Input, APLValue|undefined> { private typePicker: TextDropdownPicker, APLValueType>; @@ -64,29 +76,37 @@ export class APLValuePicker extends Input, APLValue> { const allValueTypes = Object.keys(valueTypeFactories) as Array>; this.typePicker = new TextDropdownPicker(this.rootElem, player, { defaultLabel: 'No Condition', - values: allValueTypes.map(valueType => { + values: [{ + value: undefined, + label: 'None', + } as TextDropdownValueConfig].concat(allValueTypes.map(valueType => { return { value: valueType, label: valueTypeFactories[valueType].label, }; - }), + })), equals: (a, b) => a == b, changedEvent: (player: Player) => player.rotationChangeEmitter, - getValue: (player: Player) => this.getSourceValue().value.oneofKind, + getValue: (player: Player) => this.getSourceValue()?.value.oneofKind, setValue: (eventID: EventID, player: Player, newValue: APLValueType) => { - const value = this.getSourceValue(); - if (value.value.oneofKind == newValue) { + const sourceValue = this.getSourceValue(); + if (sourceValue?.value.oneofKind == newValue) { return; } + if (newValue) { const factory = valueTypeFactories[newValue]; const obj: any = { oneofKind: newValue }; obj[newValue] = factory.newValue(); - value.value = obj; + if (sourceValue) { + sourceValue.value = obj; + } else { + const newSourceValue = APLValue.create(); + newSourceValue.value = obj; + this.setSourceValue(eventID, newSourceValue); + } } else { - value.value = { - oneofKind: newValue, - }; + this.setSourceValue(eventID, newValue); } player.rotationChangeEmitter.emit(eventID); }, @@ -102,31 +122,31 @@ export class APLValuePicker extends Input, APLValue> { return this.rootElem; } - getInputValue(): APLValue { + getInputValue(): APLValue|undefined { const valueType = this.typePicker.getInputValue(); - return APLValue.create({ - value: { - oneofKind: valueType, - ...((() => { - const val: any = {}; - if (valueType && this.valuePicker) { - val[valueType] = this.valuePicker.getInputValue(); - } - return val; - })()), - }, - }) - } - - setInputValue(newValue: APLValue) { - if (!newValue) { - return; + if (!valueType) { + return undefined; + } else { + return APLValue.create({ + value: { + oneofKind: valueType, + ...((() => { + const val: any = {}; + if (valueType && this.valuePicker) { + val[valueType] = this.valuePicker.getInputValue(); + } + return val; + })()), + }, + }) } + } - const newValueType = newValue.value.oneofKind; + setInputValue(newValue: APLValue|undefined) { + const newValueType = newValue?.value.oneofKind; this.updateValuePicker(newValueType); - if (newValueType) { + if (newValueType && newValue) { this.valuePicker!.setInputValue((newValue.value as any)[newValueType]); } } @@ -152,22 +172,28 @@ export class APLValuePicker extends Input, APLValue> { const factory = valueTypeFactories[newValueType]; this.valuePicker = factory.factory(this.rootElem, this.modObject, { changedEvent: (player: Player) => player.rotationChangeEmitter, - getValue: () => (this.getSourceValue().value as any)[newValueType] || factory.newValue(), + getValue: () => { + const sourceVal = this.getSourceValue(); + return sourceVal ? (sourceVal.value as any)[newValueType] || factory.newValue() : factory.newValue(); + }, setValue: (eventID: EventID, player: Player, newValue: any) => { - (this.getSourceValue().value as any)[newValueType] = newValue; + const sourceVal = this.getSourceValue(); + if (sourceVal) { + (sourceVal.value as any)[newValueType] = newValue; + } player.rotationChangeEmitter.emit(eventID); }, }); } } -type ValueTypeConfig = { +type ValueTypeConfig = { label: string, - newValue: () => object, - factory: (parent: HTMLElement, player: Player, config: InputConfig, any>) => Input, any>, + newValue: () => T, + factory: (parent: HTMLElement, player: Player, config: InputConfig, T>) => Input, T>, }; -function inputBuilder(label: string, newValue: () => T, fields: Array>): ValueTypeConfig { +function inputBuilder(label: string, newValue: () => T, fields: Array>): ValueTypeConfig { return { label: label, newValue: newValue, @@ -175,21 +201,89 @@ function inputBuilder(label: string, newValue: () => T, fields }; } -const valueTypeFactories: Record, ValueTypeConfig> = { +const valueTypeFactories: Record, ValueTypeConfig> = { ['const']: inputBuilder('Const', APLValueConst.create, [ { - field: 'value', + field: 'val', newValue: () => '', factory: (parent, player, config) => new APLValueConstValuePicker(parent, player, config), }, ]), ['and']: inputBuilder('All of', APLValueAnd.create, [ + { + field: 'vals', + newValue: () => [], + factory: (parent, player, config) => new ListPicker, APLValue|undefined>(parent, player, { + ...config, + // Override setValue to replace undefined elements with default messages. + setValue: (eventID: EventID, player: Player, newValue: Array) => { + config.setValue(eventID, player, newValue.map(val => val || APLValue.create())); + }, + + itemLabel: 'Value', + newItem: APLValue.create, + copyItem: (oldValue: APLValue|undefined) => oldValue ? APLValue.clone(oldValue) : oldValue, + newItemPicker: (parent: HTMLElement, listPicker: ListPicker, APLValue|undefined>, index: number, config: ListItemPickerConfig, APLValue|undefined>) => new APLValuePicker(parent, player, config), + horizontalLayout: true, + allowedActions: ['create', 'delete'], + }), + }, ]), ['or']: inputBuilder('Any of', APLValueOr.create, [ + { + field: 'vals', + newValue: () => [], + factory: (parent, player, config) => new ListPicker, APLValue|undefined>(parent, player, { + ...config, + // Override setValue to replace undefined elements with default messages. + setValue: (eventID: EventID, player: Player, newValue: Array) => { + config.setValue(eventID, player, newValue.map(val => val || APLValue.create())); + }, + + itemLabel: 'Value', + newItem: APLValue.create, + copyItem: (oldValue: APLValue|undefined) => oldValue ? APLValue.clone(oldValue) : oldValue, + newItemPicker: (parent: HTMLElement, listPicker: ListPicker, APLValue|undefined>, index: number, config: ListItemPickerConfig, APLValue|undefined>) => new APLValuePicker(parent, player, config), + horizontalLayout: true, + allowedActions: ['create', 'delete'], + }), + }, ]), ['not']: inputBuilder('Not', APLValueNot.create, [ + { + field: 'val', + newValue: APLValue.create, + factory: (parent, player, config) => new APLValuePicker(parent, player, config), + }, ]), ['cmp']: inputBuilder('Compare', APLValueCompare.create, [ + { + field: 'lhs', + newValue: APLValue.create, + factory: (parent, player, config) => new APLValuePicker(parent, player, config), + }, + { + field: 'op', + newValue: () => ComparisonOperator.OpEq, + factory: (parent, player, config) => new TextDropdownPicker(parent, player, { + ...config, + defaultLabel: 'None', + equals: (a, b) => a == b, + values: [ + { value: ComparisonOperator.OpEq, label: '==' }, + { value: ComparisonOperator.OpNe, label: '!=' }, + { value: ComparisonOperator.OpGe, label: '>=' }, + { value: ComparisonOperator.OpGt, label: '>' }, + { value: ComparisonOperator.OpLe, label: '<=' }, + { value: ComparisonOperator.OpLt, label: '<' }, + ], + }), + }, + { + field: 'rhs', + newValue: APLValue.create, + factory: (parent, player, config) => new APLValuePicker(parent, player, config), + }, ]), ['dotIsActive']: inputBuilder('Dot Is Active', APLValueDotIsActive.create, [ AplHelpers.actionIdFieldConfig('spellId', 'dots'), diff --git a/ui/core/components/input.ts b/ui/core/components/input.ts index 9ee92aca3d..00c101d150 100644 --- a/ui/core/components/input.ts +++ b/ui/core/components/input.ts @@ -117,6 +117,10 @@ export abstract class Input extends Component { return this.inputConfig.getValue(this.modObject); } + protected setSourceValue(eventID: EventID, newValue: T) { + this.inputConfig.setValue(eventID, this.modObject, newValue); + } + // Child classes should call this method when the value in the input element changes. inputChanged(eventID: EventID) { this.inputConfig.setValue(eventID, this.modObject, this.getInputValue()); diff --git a/ui/core/components/list_picker.ts b/ui/core/components/list_picker.ts index a4548d29de..b2e7d776c7 100644 --- a/ui/core/components/list_picker.ts +++ b/ui/core/components/list_picker.ts @@ -4,6 +4,8 @@ import { swap } from '../utils.js'; import { Input, InputConfig } from './input.js'; +export type ListItemAction = 'create' | 'delete' | 'move' | 'copy'; + export interface ListPickerConfig extends InputConfig> { title?: string, titleTooltip?: string, @@ -13,6 +15,10 @@ export interface ListPickerConfig extends InputConfig, index: number, config: ListItemPickerConfig) => Input, inlineMenuBar?: boolean, hideUi?: boolean, + horizontalLayout?: boolean, + + // If set, only actions included in the list are allowed. Otherwise, all actions are allowed. + allowedActions?: Array, } export interface ListItemPickerConfig extends InputConfig { @@ -43,24 +49,43 @@ export class ListPicker extends Input${config.title}` : '' }
- `; if (this.config.hideUi) { this.rootElem.classList.add('hide-ui'); } + if (this.config.horizontalLayout) { + this.config.inlineMenuBar = true; + this.rootElem.classList.add('horizontal'); + } if (this.config.titleTooltip) Tooltip.getOrCreateInstance(this.rootElem.querySelector('.list-picker-title') as HTMLElement); this.itemsDiv = this.rootElem.getElementsByClassName('list-picker-items')[0] as HTMLElement; - const newItemButton = this.rootElem.getElementsByClassName('list-picker-new-button')[0] as HTMLElement; - newItemButton.addEventListener('click', event => { - const newItem = this.config.newItem(); - const newList = this.config.getValue(this.modObject).concat([newItem]); - this.config.setValue(TypedEvent.nextEventID(), this.modObject, newList); - }); + if (this.actionEnabled('create')) { + let newItemButton = null; + let newButtonTooltip: Tooltip|null = null; + if (this.config.horizontalLayout) { + newItemButton = ListPicker.makeActionElem('link-success', `New ${config.itemLabel}`, 'fa-plus') + newButtonTooltip = Tooltip.getOrCreateInstance(newItemButton); + } else { + newItemButton = document.createElement('button'); + newItemButton.classList.add('btn', 'btn-primary'); + newItemButton.textContent = `New ${config.itemLabel}`; + } + newItemButton.classList.add('list-picker-new-button'); + newItemButton.addEventListener('click', event => { + const newItem = this.config.newItem(); + const newList = this.config.getValue(this.modObject).concat([newItem]); + this.config.setValue(TypedEvent.nextEventID(), this.modObject, newList); + if (newButtonTooltip) { + newButtonTooltip.hide(); + } + }); + this.rootElem.appendChild(newItemButton); + } this.init(); } @@ -89,6 +114,10 @@ export class ListPicker extends Input this.itemPickerPairs[i].picker.setInputValue(val)) } + private actionEnabled(action: ListItemAction): boolean { + return !this.config.allowedActions || this.config.allowedActions.includes(action); + } + private addNewPicker() { const index = this.itemPickerPairs.length; const itemContainer = document.createElement('div'); @@ -96,77 +125,29 @@ export class ListPicker extends Input - ${this.config.itemLabel && !this.config.inlineMenuBar ? `
${this.config.itemLabel} ${this.itemPickerPairs.length + 1}
` : ''} - - - - - - - - - - - - - - ${!this.config.inlineMenuBar ? itemHTML : ''} - `; - - const upButton = itemContainer.getElementsByClassName('list-picker-item-up')[0] as HTMLElement; - const upButtonTooltip = Tooltip.getOrCreateInstance(upButton); - - upButton.addEventListener('click', event => { - if (index == 0) { - return; - } - - const newList = this.config.getValue(this.modObject); - swap(newList, index, index - 1); - this.config.setValue(TypedEvent.nextEventID(), this.modObject, newList); - upButtonTooltip.hide(); - }); + const itemElem = document.createElement('div'); + itemElem.classList.add('list-picker-item'); - const downButton = itemContainer.getElementsByClassName('list-picker-item-down')[0] as HTMLElement; - const downButtonTooltip = Tooltip.getOrCreateInstance(downButton); + const itemHeader = document.createElement('div'); + itemHeader.classList.add('list-picker-item-header'); + const itemHTML = '
'; - downButton.addEventListener('click', event => { - if (index == this.itemPickerPairs.length - 1) { - return; + if (this.config.inlineMenuBar) { + itemContainer.appendChild(itemElem); + itemContainer.appendChild(itemHeader); + } else { + itemContainer.appendChild(itemHeader); + itemContainer.appendChild(itemElem); + if (this.config.itemLabel) { + const itemLabel = document.createElement('h6'); + itemLabel.classList.add('list-picker-item-title'); + itemLabel.textContent = `${this.config.itemLabel} ${this.itemPickerPairs.length + 1}`; + itemHeader.appendChild(itemLabel); } + } - const newList = this.config.getValue(this.modObject); - swap(newList, index, index + 1); - this.config.setValue(TypedEvent.nextEventID(), this.modObject, newList); - downButtonTooltip.hide(); - }); - - const copyButton = itemContainer.getElementsByClassName('list-picker-item-copy')[0] as HTMLElement; - const copyButtonTooltip = Tooltip.getOrCreateInstance(copyButton); - - copyButton.addEventListener('click', event => { - const newList = this.config.getValue(this.modObject) - newList.push(this.config.copyItem(newList[index])); - this.config.setValue(TypedEvent.nextEventID(), this.modObject, newList); - copyButtonTooltip.hide(); - }); - - const deleteButton = itemContainer.getElementsByClassName('list-picker-item-delete')[0] as HTMLElement; - const deleteButtonTooltip = Tooltip.getOrCreateInstance(deleteButton); - - deleteButton.addEventListener('click', event => { - const newList = this.config.getValue(this.modObject); - newList.splice(index, 1); - this.config.setValue(TypedEvent.nextEventID(), this.modObject, newList); - deleteButtonTooltip.hide(); - }); - - const itemElem = itemContainer.getElementsByClassName('list-picker-item')[0] as HTMLElement; const itemPicker = this.config.newItemPicker(itemElem, this, index, { changedEvent: this.config.changedEvent, getValue: (modObj: ModObject) => this.getSourceValue()[index], @@ -176,8 +157,90 @@ export class ListPicker extends Input { + if (index == 0) { + return; + } + + const newList = this.config.getValue(this.modObject); + swap(newList, index, index - 1); + this.config.setValue(TypedEvent.nextEventID(), this.modObject, newList); + upButtonTooltip.hide(); + }); + + const downButton = ListPicker.makeActionElem('list-picker-item-down', 'Move Down', 'fa-angle-down'); + itemHeader.appendChild(downButton); + const downButtonTooltip = Tooltip.getOrCreateInstance(downButton); + + downButton.addEventListener('click', event => { + if (index == this.itemPickerPairs.length - 1) { + return; + } + + const newList = this.config.getValue(this.modObject); + swap(newList, index, index + 1); + this.config.setValue(TypedEvent.nextEventID(), this.modObject, newList); + downButtonTooltip.hide(); + }); + } + + if (this.actionEnabled('copy')) { + const copyButton = ListPicker.makeActionElem('list-picker-item-copy', `Copy to New ${this.config.itemLabel}`, 'fa-copy'); + itemHeader.appendChild(copyButton); + const copyButtonTooltip = Tooltip.getOrCreateInstance(copyButton); + + copyButton.addEventListener('click', event => { + const newList = this.config.getValue(this.modObject) + newList.push(this.config.copyItem(newList[index])); + this.config.setValue(TypedEvent.nextEventID(), this.modObject, newList); + copyButtonTooltip.hide(); + }); + } + + if (this.actionEnabled('delete')) { + const deleteButton = ListPicker.makeActionElem('list-picker-item-delete', `Delete ${this.config.itemLabel}`, 'fa-times'); + deleteButton.classList.add('link-danger'); + itemHeader.appendChild(deleteButton); + const deleteButtonTooltip = Tooltip.getOrCreateInstance(deleteButton); + + deleteButton.addEventListener('click', event => { + const newList = this.config.getValue(this.modObject); + newList.splice(index, 1); + this.config.setValue(TypedEvent.nextEventID(), this.modObject, newList); + deleteButtonTooltip.hide(); + }); + } this.itemPickerPairs.push({ elem: itemContainer, picker: itemPicker }); } + + static makeActionElem(cssClass: string, title: string, iconCssClass: string): HTMLElement { + const actionElem = document.createElement('a'); + actionElem.classList.add('list-picker-item-action', cssClass); + actionElem.href = 'javascript:void(0)'; + actionElem.setAttribute('role', 'button'); + actionElem.setAttribute('data-bs-toggle', 'tooltip'); + actionElem.setAttribute('data-bs-title', title); + + const icon = document.createElement('i'); + icon.classList.add('fa', 'fa-xl', iconCssClass); + actionElem.appendChild(icon); + + return actionElem; + } + + static getItemHeaderElem(itemPicker: Input): HTMLElement { + const itemElem = itemPicker.rootElem.parentElement!; + const headerElem = (itemElem.nextElementSibling || itemElem.previousElementSibling); + if (!headerElem?.classList.contains('list-picker-item-header')) { + throw new Error('Could not find list item header'); + } + return headerElem as HTMLElement; + } } diff --git a/ui/core/individual_sim_ui.ts b/ui/core/individual_sim_ui.ts index 2c6c65ca37..ef6e0c698c 100644 --- a/ui/core/individual_sim_ui.ts +++ b/ui/core/individual_sim_ui.ts @@ -277,7 +277,7 @@ export abstract class IndividualSimUI extends SimUI { this.bt = this.addBulkTab(); this.addSettingsTab(); this.addTalentsTab(); - this.addRotationTab(); + //this.addRotationTab(); if (!this.isWithinRaidSim) { this.addDetailedResultsTab(); diff --git a/ui/scss/core/components/_dropdown_picker.scss b/ui/scss/core/components/_dropdown_picker.scss index 0ba58ebc26..8e9577c4ba 100644 --- a/ui/scss/core/components/_dropdown_picker.scss +++ b/ui/scss/core/components/_dropdown_picker.scss @@ -7,4 +7,8 @@ display: flex; align-items: center; padding: 2px; +} + +.dropdown-picker-root .dropdown { + width: auto; } \ No newline at end of file diff --git a/ui/scss/core/components/_list_picker.scss b/ui/scss/core/components/_list_picker.scss index 9aedfa6b35..4c97793741 100644 --- a/ui/scss/core/components/_list_picker.scss +++ b/ui/scss/core/components/_list_picker.scss @@ -79,3 +79,14 @@ display: none !important; } } + +.list-picker-root.horizontal { + flex-direction: row; + align-items: center; + + .list-picker-items { + display: flex; + flex-wrap: wrap; + align-items: center; + } +} diff --git a/ui/scss/core/components/individual_sim_ui/_apl_helpers.scss b/ui/scss/core/components/individual_sim_ui/_apl_helpers.scss index 8fbd5fd00c..c054a1eb3b 100644 --- a/ui/scss/core/components/individual_sim_ui/_apl_helpers.scss +++ b/ui/scss/core/components/individual_sim_ui/_apl_helpers.scss @@ -10,4 +10,5 @@ .apl-picker-builder-root { flex-direction: row; align-items: center; + width: auto; } \ No newline at end of file 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 index 2aead1274b..268246e036 100644 --- a/ui/scss/core/components/individual_sim_ui/_apl_rotation_picker.scss +++ b/ui/scss/core/components/individual_sim_ui/_apl_rotation_picker.scss @@ -44,14 +44,32 @@ .input-root { margin: 0; + width: auto; + align-items: center; } } .apl-list-item-picker-root > .apl-action-picker-root { flex-direction: column; } +.apl-value-picker-root * { + font-size: calc(var(--bs-btn-font-size) - 0.2rem); +} + +.apl-list-item-picker > * > * > .list-picker-item { + flex-grow: 1; +} + .apl-action-picker-action { display: flex; flex-direction: row; align-items: center; +} + +.apl-value-const-picker-root > input { + text-align: center; +} + +.hide-picker-root { + margin: 0; } \ No newline at end of file diff --git a/ui/scss/shared/_bootstrap_style_overrides.scss b/ui/scss/shared/_bootstrap_style_overrides.scss index cfa5397cb1..7eb6f59c75 100644 --- a/ui/scss/shared/_bootstrap_style_overrides.scss +++ b/ui/scss/shared/_bootstrap_style_overrides.scss @@ -66,6 +66,10 @@ color: $link-danger-color !important; } +.link-success { + color: $success !important; +} + .modal { .modal-dialog { .modal-header,