pokerogue/src/ui/dropdown.ts
Leo Kim 53e54df9ee
[Enhancement] Add Passive / Cost Reduction - Can Unlock option and Egg purchasable filter (#3478)
* add unlockable state in passive/cost reduction filter. add egg purchasable filter

* update placeholders

* update option order of UNLOCKABLE between ON and EXCLUDE

* remove egg restriction when buying eggs
2024-08-12 00:44:15 -04:00

589 lines
18 KiB
TypeScript

import BattleScene from "#app/battle-scene.js";
import { SceneBase } from "#app/scene-base.js";
import { addTextObject, TextStyle } from "./text";
import { addWindow, WindowVariant } from "./ui-theme";
import i18next from "i18next";
export enum DropDownState {
ON = 0,
OFF = 1,
EXCLUDE = 2,
UNLOCKABLE = 3
}
export enum DropDownType {
SINGLE = 0,
MULTI = 1,
HYBRID = 2,
RADIAL = 3
}
export enum SortDirection {
ASC = -1,
DESC = 1
}
export class DropDownLabel {
public state: DropDownState;
public text: string;
public sprite?: Phaser.GameObjects.Sprite;
constructor(label: string, sprite?: Phaser.GameObjects.Sprite, state: DropDownState = DropDownState.OFF) {
this.text = label || "";
this.sprite = sprite;
this.state = state;
}
}
export class DropDownOption extends Phaser.GameObjects.Container {
public state: DropDownState = DropDownState.ON;
public toggle: Phaser.GameObjects.Sprite;
public text: Phaser.GameObjects.Text;
public val: any;
public dir: SortDirection = SortDirection.ASC;
private currentLabelIndex: number;
private labels: DropDownLabel[];
private onColor = 0x33bbff;
private offColor = 0x272727;
private excludeColor = 0xff5555;
private unlockableColor = 0xffff00;
constructor(scene: SceneBase, val: any, labels: DropDownLabel | DropDownLabel[]) {
super(scene);
this.val = val;
if (Array.isArray(labels)) {
this.labels = labels;
} else {
this.labels = labels? [ labels ] : [ new DropDownLabel("") ];
}
this.currentLabelIndex = 0;
const currentLabel = this.labels[this.currentLabelIndex];
this.state = currentLabel.state;
this.text = addTextObject(scene, 0, 0, currentLabel.text || "", TextStyle.TOOLTIP_CONTENT);
this.text.setOrigin(0, 0.5);
this.add(this.text);
// Add to container the sprite for each label if there is one
for (let i=0; i < this.labels.length; i++) {
const sprite = this.labels[i].sprite;
if (sprite) {
this.add(sprite);
sprite.setOrigin(0, 0.5);
if (i!== this.currentLabelIndex) {
sprite.setVisible(false);
}
}
}
}
/**
* Initialize the toggle icon based on the provided DropDownType
* For DropDownType.SINGLE: uses a cursor arrow icon
* For other types: uses a candy icon
* @param type the DropDownType to use
* @param visible whether the icon should be visible or not
*/
setupToggleIcon(type: DropDownType, visible: boolean): void {
if (type === DropDownType.SINGLE) {
this.toggle = this.scene.add.sprite(0, 0, "cursor");
this.toggle.setScale(0.5);
this.toggle.setOrigin(0, 0.5);
this.toggle.setRotation(Math.PI / 180 * -90);
} else {
this.toggle = this.scene.add.sprite(0, 0, "candy");
this.toggle.setScale(0.3);
this.toggle.setOrigin(0, 0.5);
}
this.add(this.toggle);
this.toggle.setVisible(visible);
this.updateToggleIconColor();
}
/**
* Set the toggle icon color based on the current state
*/
private updateToggleIconColor(): void {
switch (this.state) {
case DropDownState.ON:
this.toggle.setTint(this.onColor);
break;
case DropDownState.OFF:
this.toggle.setTint(this.offColor);
break;
case DropDownState.EXCLUDE:
this.toggle.setTint(this.excludeColor);
break;
case DropDownState.UNLOCKABLE:
this.toggle.setTint(this.unlockableColor);
break;
}
}
/**
* Switch the option to its next state and update visuals
* If only ON/OFF are possible, toggle between the two
* For radials, move to the next state in the list
* @returns the updated DropDownState
*/
public toggleOptionState(): DropDownState {
if (this.labels.length > 1) {
return this.setCurrentLabel((this.currentLabelIndex + 1) % this.labels.length);
}
const newState = this.state === DropDownState.ON ? DropDownState.OFF : DropDownState.ON;
return this.setOptionState(newState);
}
/**
* Set the option to the given state and update visuals
* @param newState the state to switch to
* @returns the new DropDownState
*/
public setOptionState(newState: DropDownState): DropDownState {
const newLabelIndex = this.labels.findIndex(label => label.state === newState);
if (newLabelIndex !== -1 && newLabelIndex !== this.currentLabelIndex) {
return this.setCurrentLabel(newLabelIndex);
}
this.state = newState;
this.updateToggleIconColor();
return newState;
}
/**
* Change the option state to the one at the given index and update visuals
* @param index index of the state to switch to
* @returns the new DropDownState
*/
private setCurrentLabel(index: number): DropDownState {
const currentLabel = this.labels[this.currentLabelIndex];
const newLabel = this.labels[index];
if (!newLabel) {
return this.state;
}
this.currentLabelIndex = index;
// update state, sprite and text to fit the new label
this.state = newLabel.state;
this.updateToggleIconColor();
if (currentLabel.sprite) {
this.text.x -= currentLabel.sprite.displayWidth + 2;
currentLabel.sprite.setVisible(false);
}
if (newLabel.sprite) {
this.text.x += newLabel.sprite.displayWidth + 2;
newLabel.sprite.setVisible(true);
}
this.text.setText(newLabel.text);
return this.state;
}
/**
* Set the current SortDirection to the provided value and update icon accordingly
* @param SortDirection the new SortDirection to use
*/
public setDirection(dir: SortDirection): void {
this.dir = dir;
this.toggle.flipX = this.dir === SortDirection.DESC;
}
/**
* Toggle the current SortDirection value
*/
public toggleDirection(): void {
this.setDirection(this.dir * -1);
}
/**
* Place the label elements (text and sprite if there is one) to the provided x and y position
* @param x the horizontal position
* @param y the vertical position
*/
setLabelPosition(x: number, y: number) {
let textX = x;
for (let i=0; i < this.labels.length; i++) {
const label = this.labels[i];
if (label.sprite) {
label.sprite.x = x;
label.sprite.y = y;
if (i === this.currentLabelIndex) {
textX += label.sprite.displayWidth + 2;
}
}
}
if (this.text) {
this.text.x = textX;
this.text.y = y;
}
}
/**
* Place the toggle icon at the provided position
* @param x the horizontal position
* @param y the vertical position
*/
setTogglePosition(x: number, y: number) {
if (this.toggle) {
this.toggle.x = x;
this.toggle.y = y;
}
}
/**
* @returns the x position to use for the current label depending on if it has a sprite or not
*/
getCurrentLabelX(): number | undefined {
if (this.labels[this.currentLabelIndex].sprite) {
return this.labels[this.currentLabelIndex].sprite?.x;
}
return this.text.x;
}
/**
* @returns max width needed to display all of the labels
*/
getWidth(): number {
let w = 0;
const currentText = this.text.text;
for (const label of this.labels) {
this.text.setText(label.text);
const spriteWidth = label.sprite? label.sprite.displayWidth + 2 : 0;
w = Math.max(w, this.text.displayWidth + spriteWidth);
}
this.text.setText(currentText);
return w;
}
}
export class DropDown extends Phaser.GameObjects.Container {
public options: DropDownOption[];
private window: Phaser.GameObjects.NineSlice;
private cursorObj: Phaser.GameObjects.Image;
public dropDownType: DropDownType = DropDownType.MULTI;
public cursor: number = 0;
private lastCursor: number = -1;
public defaultCursor: number = 0;
private onChange: () => void;
private lastDir: SortDirection = SortDirection.ASC;
private defaultSettings: any[];
constructor(scene: BattleScene, x: number, y: number, options: DropDownOption[], onChange: () => void, type: DropDownType = DropDownType.MULTI, optionSpacing: number = 2) {
const windowPadding = 5;
const optionHeight = 7;
const optionPaddingX = 4;
const optionPaddingY = 6;
const cursorOffset = 7;
const optionWidth = 100;
super(scene, x - cursorOffset - windowPadding, y);
this.options = options;
this.dropDownType = type;
this.onChange = onChange;
this.cursorObj = scene.add.image(optionPaddingX + 3, 0, "cursor");
this.cursorObj.setScale(0.5);
this.cursorObj.setOrigin(0, 0.5);
this.cursorObj.setVisible(false);
// For MULTI and HYBRID filter, add an ALL option at the top
if (this.dropDownType === DropDownType.MULTI || this.dropDownType === DropDownType.HYBRID) {
this.options.unshift(new DropDownOption(scene, "ALL", new DropDownLabel(i18next.t("filterBar:all"), undefined, this.checkForAllOn() ? DropDownState.ON : DropDownState.OFF)));
}
this.defaultSettings = this.getSettings();
// Place ui elements in the correct spot
options.forEach((option, index) => {
const toggleVisibility = type !== DropDownType.SINGLE || option.state === DropDownState.ON;
option.setupToggleIcon(type, toggleVisibility);
option.width = optionWidth;
option.y = index * optionHeight + index * optionSpacing + optionPaddingY;
const baseX = cursorOffset + optionPaddingX + 3;
const baseY = optionHeight / 2;
option.setLabelPosition(baseX + 8, baseY);
if (type === DropDownType.SINGLE) {
option.setTogglePosition(baseX + 3, baseY + 1);
} else {
option.setTogglePosition(baseX, baseY);
}
});
this.window = addWindow(scene, 0, 0, optionWidth, options[options.length - 1].y + optionHeight + optionPaddingY, false, false, undefined, undefined, WindowVariant.XTHIN);
this.add(this.window);
this.add(options);
this.add(this.cursorObj);
this.setVisible(false);
}
getWidth(): number {
return this.window? this.window.width : this.width;
}
toggleVisibility(): void {
this.setVisible(!this.visible);
}
setVisible(value: boolean): this {
super.setVisible(value);
if (value) {
this.autoSize();
}
return this;
}
resetCursor(): boolean {
// If we are an hybrid dropdown in "hover" mode, don't move the cursor back to 0
if (this.dropDownType === DropDownType.HYBRID && this.checkForAllOff()) {
return this.setCursor(this.lastCursor);
}
return this.setCursor(this.defaultCursor);
}
setCursor(cursor: integer): boolean {
this.cursor = cursor;
if (cursor < 0) {
cursor = 0;
this.cursorObj.setVisible(false);
return false;
} else if (cursor >= this.options.length) {
cursor = this.options.length - 1;
this.cursorObj.y = this.options[cursor].y + 3.5;
this.cursorObj.setVisible(true);
return false;
} else {
this.cursorObj.y = this.options[cursor].y + 3.5;
this.cursorObj.setVisible(true);
// If hydrid type, we need to update the filters when going up/down in the list
if (this.dropDownType === DropDownType.HYBRID) {
this.lastCursor = cursor;
this.onChange();
}
}
return true;
}
/**
* Switch the option at the provided index to its next state and update visuals
* Update accordingly the other options if needed:
* - if "all" is toggled, also update all other options
* - for DropDownType.SINGLE, unselect the previously selected option if applicable
* @param index the index of the option for which to update the state
*/
toggleOptionState(index: number = this.cursor): void {
const option: DropDownOption = this.options[index];
if (this.dropDownType === DropDownType.MULTI || this.dropDownType === DropDownType.HYBRID) {
const newState = option.toggleOptionState();
if (index === 0) {
// we are on the All option > put all other options to the newState
this.setAllOptions(newState);
} else {
// select the "all" option if all others are selected, other unselect it
if (newState === DropDownState.ON && this.checkForAllOn()) {
this.options[0].setOptionState(DropDownState.ON);
} else {
this.options[0].setOptionState(DropDownState.OFF);
}
}
} else if (this.dropDownType === DropDownType.SINGLE) {
if (option.state === DropDownState.OFF) {
this.options.forEach((option) => {
option.setOptionState(DropDownState.OFF);
option.setDirection(SortDirection.ASC);
option.toggle.setVisible(false);
});
option.setOptionState(DropDownState.ON);
option.setDirection(this.lastDir);
option.toggle.setVisible(true);
} else {
option.toggleDirection();
this.lastDir = this.options[this.cursor].dir;
}
} else if (this.dropDownType === DropDownType.RADIAL) {
option.toggleOptionState();
}
this.onChange();
}
/**
* Check whether all options except the "ALL" one are ON
* @returns true if all options are set to DropDownState.ON, false otherwise
*/
checkForAllOn(): boolean {
return this.options.every((option, i) => i === 0 || option.state === DropDownState.ON);
}
/**
* Check whether all options except the "ALL" one are OFF
* @returns true if all options are set to DropDownState.OFF, false otherwise
*/
checkForAllOff(): boolean {
return this.options.every((option, i) => i === 0 || option.state === DropDownState.OFF);
}
/**
* Get the current selected values for each option
* @returns an array of values, depending on the DropDownType
* - if MULTI or HYBRID, an array of all the values of the options set to ON (except the ALL one)
* - if RADIAL, an array where the value for each option is of the form { val: any, state: DropDownState }
* - if SINGLE, a single object of the form { val: any, state: SortDirection }
*/
getVals(): any[] {
if (this.dropDownType === DropDownType.MULTI) {
return this.options.filter((option, i) => i > 0 && option.state === DropDownState.ON).map((option) => option.val);
} else if (this.dropDownType === DropDownType.HYBRID) {
const selected = this.options.filter((option, i) => i > 0 && option.state === DropDownState.ON).map((option) => option.val);
if (selected.length > 0) {
return selected;
}
// if nothing is selected and the ALL option is hovered, return all elements
if (this.cursor === 0) {
return this.options.filter((_, i) => i > 0).map(option => option.val);
}
// if nothing is selected and a single option is hovered, return that one
return [this.options[this.cursor].val];
} else if (this.dropDownType === DropDownType.RADIAL) {
return this.options.map((option) => {
return { val: option.val, state: option.state };
});
} else {
return this.options.filter(option => option.state === DropDownState.ON).map((option) => {
return { val: option.val, dir: option.dir };
});
}
}
/**
* Get the current selected settings dictionary for each option
* @returns an array of dictionaries with the current state of each option
* - the settings dictionary is like this { val: any, state: DropDownState, cursor: boolean, dir: SortDirection }
*/
private getSettings(): any[] {
const settings : any[] = [];
for (let i = 0; i < this.options.length; i++) {
settings.push({ val: this.options[i].val, state: this.options[i].state , cursor: (this.cursor === i), dir: this.options[i].dir });
}
return settings;
}
/**
* Check whether the values of all options are the same as the default ones
* @returns true if they are the same, false otherwise
*/
public hasDefaultValues(): boolean {
const currentValues = this.getSettings();
const compareValues = (keys: string[]): boolean => {
return currentValues.length === this.defaultSettings.length &&
currentValues.every((value, index) =>
keys.every(key => value[key] === this.defaultSettings[index][key])
);
};
switch (this.dropDownType) {
case DropDownType.MULTI:
case DropDownType.RADIAL:
return compareValues(["val", "state"]);
case DropDownType.HYBRID:
return compareValues(["val", "state", "cursor"]);
case DropDownType.SINGLE:
return compareValues(["val", "state", "dir"]);
default:
return false;
}
}
/**
* Set all values to their default state
*/
public resetToDefault(): void {
if (this.defaultSettings.length > 0) {
this.setCursor(this.defaultCursor);
this.lastDir = SortDirection.ASC;
for (let i = 0; i < this.options.length; i++) {
// reset values with the defaultValues
if (this.dropDownType === DropDownType.SINGLE) {
if (this.defaultSettings[i].state === DropDownState.OFF) {
this.options[i].setOptionState(DropDownState.OFF);
this.options[i].setDirection(SortDirection.ASC);
this.options[i].toggle.setVisible(false);
} else {
this.options[i].setOptionState(DropDownState.ON);
this.options[i].setDirection(SortDirection.ASC);
this.options[i].toggle.setVisible(true);
}
} else {
if (this.defaultSettings[i]) {
this.options[i].setOptionState(this.defaultSettings[i]["state"]);
}
}
}
}
}
/**
* Set all options to a specific state
* @param state the DropDownState to assign to each option
*/
private setAllOptions(state: DropDownState) : void {
// For single type dropdown, setting all options is not relevant
if (this.dropDownType === DropDownType.SINGLE) {
return;
}
for (const option of this.options) {
option.setOptionState(state);
}
}
/**
* Set all options to their ON state
*/
public selectAllOptions() {
this.setAllOptions(DropDownState.ON);
}
/**
* Set all options to their OFF state
*/
public unselectAllOptions() {
this.setAllOptions(DropDownState.OFF);
}
/**
* Automatically set the width and position based on the size of options
*/
autoSize(): void {
let maxWidth = 0;
let x = 0;
for (let i = 0; i < this.options.length; i++) {
const optionWidth = this.options[i].getWidth();
if (optionWidth > maxWidth) {
maxWidth = optionWidth;
x = this.options[i].getCurrentLabelX() ?? 0;
}
}
this.window.width = maxWidth + x - this.window.x + 6;
if (this.x + this.window.width > this.parentContainer.width) {
this.x = this.parentContainer.width - this.window.width;
}
}
}