[Enhancement] Improvements to starter selection and filtering user experience (#3325)

* [filter-ui] Improvements to starter selection and filtering user experience
Original messages of 14 squashed commits:
* final cleanup and code comments
* automatically go to the list of starters when closing filters
* FilterBar cleanup. Associate each DropDown with an id and access them through it
* reset all filters when creating a new game. Set different default gen filter for challenge mode
* start of code cleanup plus some documentation
* fix filter bar label coloring for legacy theme
* change generation filter default values to be all generations selected
* fix navigation between team and filtered Pokemon
* add missing localisation keys
* first pass at improving navigation between the UI elements
* have each filter group handle its default values instead of the filter bar
* revamp dropdown class. add possibility to display both a sprite and text label at the same time
* groundwork to be able to move around starter ui elements more easily
* add hybrid filtering type for Gen and Type filters, clean up implementation for radial type

* [loc][ko][zh] localisation of starter ui filters for Chinese and Korean

Co-authored-by: Enoch <enoch.jwsong@gmail.com>
Co-authored-by: Yonmaru40 <47717431+40chyan@users.noreply.github.com>

* [loc][de] German translations for the filters

Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com>

---------

Co-authored-by: Enoch <enoch.jwsong@gmail.com>
Co-authored-by: Yonmaru40 <47717431+40chyan@users.noreply.github.com>
Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com>
This commit is contained in:
MokaStitcher 2024-08-04 08:50:13 +02:00 committed by GitHub
parent 22349da663
commit 0796a9fce8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 746 additions and 348 deletions

View File

@ -3,14 +3,17 @@ import { SimpleTranslationEntries } from "#app/interfaces/locales";
export const filterBar: SimpleTranslationEntries = {
"genFilter": "Gen.",
"typeFilter": "Typ",
"dexFilter": "Dex.",
"unlocksFilter": "Freisch.",
"winFilter": "Abschluss",
"miscFilter": "Sonst.",
"sortFilter": "Sort.",
"all": "Alle",
"normal": "Normal",
"uncaught": "Nicht gefangen",
"passive": "Passive",
"passiveUnlocked": "Passive freigeschaltet",
"passiveLocked": "Passive gesperrt",
"ribbon": "Band",
"hasWon": "Hat Klassik-Modus gewonnen",
"hasNotWon": "Hat Klassik-Modus nicht gewonnen",
"sortByNumber": "Pokédex-Nummer",

View File

@ -3,16 +3,19 @@ import { SimpleTranslationEntries } from "#app/interfaces/locales";
export const filterBar: SimpleTranslationEntries = {
"genFilter": "Gen",
"typeFilter": "Type",
"dexFilter": "Dex",
"unlocksFilter": "Unlocks",
"winFilter": "Win",
"miscFilter": "Misc",
"sortFilter": "Sort",
"all": "All",
"normal": "Normal",
"uncaught": "Uncaught",
"passive": "Passive",
"passiveUnlocked": "Passive Unlocked",
"passiveLocked": "Passive Locked",
"hasWon": "Yes",
"hasNotWon": "No",
"ribbon": "Ribbon",
"hasWon": "Ribbon - Yes",
"hasNotWon": "Ribbon - No",
"sortByNumber": "No.",
"sortByCost": "Cost",
"sortByCandies": "Candy Count",

View File

@ -3,14 +3,17 @@ import { SimpleTranslationEntries } from "#app/interfaces/locales";
export const filterBar: SimpleTranslationEntries = {
"genFilter": "Gen.",
"typeFilter": "Tipo",
"dexFilter": "Dex",
"unlocksFilter": "Otros",
"winFilter": "Vic.",
"miscFilter": "Misc",
"sortFilter": "Orden",
"all": "Todo",
"normal": "Normal",
"uncaught": "No Capt.",
"passive": "Passive",
"passiveUnlocked": "Pasiva Desbloq.",
"passiveLocked": "Pasiva Bloq.",
"ribbon": "Ribbon",
"hasWon": "Ya ha ganado",
"hasNotWon": "Aún no ha ganado",
"sortByNumber": "Núm.",

View File

@ -3,16 +3,19 @@ import { SimpleTranslationEntries } from "#app/interfaces/locales";
export const filterBar: SimpleTranslationEntries = {
"genFilter": "Gen",
"typeFilter": "Type",
"unlocksFilter": "Autres",
"winFilter": "Victoires",
"dexFilter": "Dex",
"unlocksFilter": "Débloq.",
"miscFilter": "Divers",
"sortFilter": "Tri",
"all": "Tous",
"normal": "Normal",
"uncaught": "Non-capturé",
"passive": "Passif",
"passiveUnlocked": "Passif débloqué",
"passiveLocked": "Passif verrouillé",
"hasWon": "Oui",
"hasNotWon": "Aucune",
"ribbon": "Médaille",
"hasWon": "Médaille - Oui",
"hasNotWon": "Médaille - Non",
"sortByNumber": "Par N°",
"sortByCost": "Par cout",
"sortByCandies": "Par # bonbons",

View File

@ -3,16 +3,19 @@ import { SimpleTranslationEntries } from "#app/interfaces/locales";
export const filterBar: SimpleTranslationEntries = {
"genFilter": "Gen",
"typeFilter": "Tipo",
"dexFilter": "Dex",
"unlocksFilter": "Altro",
"winFilter": "Vinto",
"miscFilter": "Misc",
"sortFilter": "Ordina",
"all": "Tutto",
"normal": "Normale",
"uncaught": "Mancante",
"passive": "Passive",
"passiveUnlocked": "Passiva sbloccata",
"passiveLocked": "Passiva bloccata",
"hasWon": "Si",
"hasNotWon": "No",
"ribbon": "Ribbon",
"hasWon": "Ribbon - Yes",
"hasNotWon": "Ribbon - No",
"sortByNumber": "Num. Dex",
"sortByCost": "Costo",
"sortByCandies": "Caramelle",

View File

@ -3,16 +3,19 @@ import { SimpleTranslationEntries } from "#app/interfaces/locales";
export const filterBar: SimpleTranslationEntries = {
"genFilter": "세대",
"typeFilter": "타입",
"unlocksFilter": "등록",
"winFilter": "클리어",
"dexFilter": "도감",
"unlocksFilter": "해금",
"miscFilter": "기타",
"sortFilter": "정렬",
"all": "전체",
"normal": "기본",
"uncaught": "미포획",
"passive": "패시브",
"passiveUnlocked": "패시브 해금",
"passiveLocked": "패시브 잠김",
"hasWon": "완료",
"hasNotWon": "미완료",
"ribbon": "클리어 여부",
"hasWon": "클리어 함",
"hasNotwon": "클리어 안함",
"sortByNumber": "도감번호",
"sortByCost": "코스트",
"sortByCandies": "사탕 수",

View File

@ -3,16 +3,19 @@ import { SimpleTranslationEntries } from "#app/interfaces/locales";
export const filterBar: SimpleTranslationEntries = {
"genFilter": "Ger.",
"typeFilter": "Tipo",
"dexFilter": "Dex",
"unlocksFilter": "Outros",
"winFilter": "Vit.",
"miscFilter": "Misc",
"sortFilter": "Ordem",
"all": "Tudo",
"normal": "Normal",
"uncaught": "Não Capturado",
"passive": "Passive",
"passiveUnlocked": "Passiva Desbloq.",
"passiveLocked": "Passiva Bloq.",
"hasWon": "Sim",
"hasNotWon": "Não",
"ribbon": "Ribbon",
"hasWon": "Ribbon - Yes",
"hasNotWon": "Ribbon - No",
"sortByNumber": "Núm.",
"sortByCost": "Custo",
"sortByCandies": "# Doces",

View File

@ -3,16 +3,19 @@ import { SimpleTranslationEntries } from "#app/interfaces/locales";
export const filterBar: SimpleTranslationEntries = {
"genFilter": "世代",
"typeFilter": "属性",
"dexFilter": "Dex",
"unlocksFilter": "解锁",
"winFilter": "通关",
"miscFilter": "混合",
"sortFilter": "排序",
"all": "全部",
"normal": "无闪光",
"uncaught": "未捕获",
"passive": "被动",
"passiveUnlocked": "被动解锁",
"passiveLocked": "被动未解锁",
"hasWon": "已通关",
"hasNotWon": "未通关",
"ribbon": "缎带",
"hasWon": "有缎带",
"hasNotWon": "无缎带",
"sortByNumber": "编号",
"sortByCost": "费用",
"sortByCandies": "糖果",

View File

@ -3,16 +3,19 @@ import { SimpleTranslationEntries } from "#app/interfaces/locales";
export const filterBar: SimpleTranslationEntries = {
"genFilter": "世代",
"typeFilter": "屬性",
"dexFilter": "Dex",
"unlocksFilter": "解鎖",
"winFilter": "通關",
"miscFilter": "混合",
"sortFilter": "排序",
"all": "全部",
"normal": "通常",
"uncaught": "未捕獲",
"passive": "被動",
"passiveUnlocked": "被動解鎖",
"passiveLocked": "被動未解鎖",
"hasWon": "已通關",
"hasNotWon": "未通關",
"ribbon": "緞帶",
"hasWon": "有緞帶",
"hasNotWon": "無緞帶",
"sortByNumber": "編號",
"sortByCost": "花費",
"sortByCandies": "糖果",

View File

@ -7,14 +7,14 @@ import i18next from "i18next";
export enum DropDownState {
ON = 0,
OFF = 1,
INCLUDE = 2,
EXCLUDE = 3,
EXCLUDE = 2
}
export enum DropDownType {
MULTI = 0,
SINGLE = 1,
TRI = 2
SINGLE = 0,
MULTI = 1,
HYBRID = 2,
RADIAL = 3
}
export enum SortDirection {
@ -22,130 +22,252 @@ export enum SortDirection {
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.ON) {
this.text = label || "";
this.sprite = sprite;
this.state = state || DropDownState.ON;
}
}
export class DropDownOption extends Phaser.GameObjects.Container {
public state: DropDownState = DropDownState.ON;
public toggle: Phaser.GameObjects.Sprite;
public text: Phaser.GameObjects.Text;
public sprite?: Phaser.GameObjects.Sprite;
public val: any;
public dir: SortDirection = SortDirection.ASC;
public offStateLabel: string; // label for OFF state in TRI dropdown
public includeStateLabel: string; // label for INCLUDE state in TRI dropdown
public excludeStateLabel: string; // label for EXCLUDE state in TRI dropdown
private onColor = 0x55ff55;
private currentLabelIndex: number;
private labels: DropDownLabel[];
private onColor = 0x33bbff;
private offColor = 0x272727;
private includeColor = 0x55ff55;
private excludeColor = 0xff5555;
constructor(scene: SceneBase, val: any, text: string | string[], sprite?: Phaser.GameObjects.Sprite, state: DropDownState = DropDownState.ON) {
constructor(scene: SceneBase, val: any, labels: DropDownLabel | DropDownLabel[]) {
super(scene);
this.val = val;
this.state = state;
if (text) {
if (Array.isArray(text)) {
this.offStateLabel = text[0];
this.includeStateLabel = text[1];
this.excludeStateLabel = text[2];
text = text[0];
} else {
this.offStateLabel = undefined;
this.includeStateLabel = undefined;
this.excludeStateLabel = undefined;
}
this.text = addTextObject(scene, 0, 0, text, TextStyle.TOOLTIP_CONTENT);
this.text.setOrigin(0, 0.5);
this.add(this.text);
}
if (sprite) {
this.sprite = sprite.setOrigin(0, 0.5);
this.add(this.sprite);
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);
}
}
}
}
public setupToggle(type: DropDownType): void {
if (type === DropDownType.MULTI || type === DropDownType.TRI) {
this.toggle = this.scene.add.sprite(0, 0, "candy");
this.toggle.setScale(0.3);
this.toggle.setOrigin(0, 0.5);
} else {
/**
* 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();
}
public setOptionState(type: DropDownType, state: DropDownState): DropDownState {
this.state = state;
// if type is MULTI or SINGLE, set the color of the toggle based on the state
if (type === DropDownType.MULTI || type === DropDownType.SINGLE) {
if (this.state === DropDownState.OFF) {
this.toggle.setTint(this.offColor);
} else if (this.state === DropDownState.ON) {
this.toggle.setTint(this.onColor);
}
} else if (type === DropDownType.TRI) {
if (this.state === DropDownState.OFF) {
this.text.setText(this.offStateLabel);
this.toggle.setTint(this.offColor);
} else if (this.state === DropDownState.INCLUDE) {
this.text.setText(this.includeStateLabel);
this.toggle.setTint(this.includeColor);
} else if (this.state === DropDownState.EXCLUDE) {
this.text.setText(this.excludeStateLabel);
this.toggle.setTint(this.excludeColor);
}
/**
* 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;
}
}
/**
* 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;
}
public toggleOptionState(type: DropDownType): DropDownState {
if (type === DropDownType.TRI) {
switch (this.state) {
case DropDownState.OFF:
this.state = DropDownState.INCLUDE;
break;
case DropDownState.INCLUDE:
this.state = DropDownState.EXCLUDE;
break;
case DropDownState.EXCLUDE:
this.state = DropDownState.OFF;
break;
}
} else {
switch (this.state) {
case DropDownState.ON:
this.state = DropDownState.OFF;
break;
case DropDownState.OFF:
this.state = DropDownState.ON;
break;
}
}
return this.setOptionState(type, 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 {
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;
private dropDownType: DropDownType = DropDownType.MULTI;
public cursor: integer = 0;
public cursor: number = 0;
public defaultCursor: number = 0;
private onChange: () => void;
private lastDir: SortDirection = SortDirection.ASC;
private defaultValues: any[];
constructor(scene: BattleScene, x: number, y: number, options: DropDownOption[], onChange: () => void, type: DropDownType = DropDownType.MULTI, optionSpacing: number = 2) {
const windowPadding = 5;
@ -165,36 +287,31 @@ export class DropDown extends Phaser.GameObjects.Container {
this.cursorObj.setOrigin(0, 0.5);
this.cursorObj.setVisible(false);
if (this.dropDownType === DropDownType.MULTI) {
this.options.unshift(new DropDownOption(scene, "ALL", i18next.t("filterBar:all"), null, this.checkForAllOn() ? DropDownState.ON : DropDownState.OFF));
// 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.defaultValues = this.getVals();
// Place ui elements in the correct spot
options.forEach((option, index) => {
option.setupToggle(type);
if (type === DropDownType.SINGLE && option.state === DropDownState.OFF) {
option.toggle.setVisible(false);
}
option.setOptionState(type, option.state);
const toggleVisibility = type !== DropDownType.SINGLE || option.state === DropDownState.ON;
option.setupToggleIcon(type, toggleVisibility);
option.width = optionWidth;
option.y = index * optionHeight + index * optionSpacing + optionPaddingY;
if (option.text) {
option.text.x = cursorOffset + optionPaddingX + 3 + 8;
option.text.y = optionHeight / 2;
}
if (option.sprite) {
option.sprite.x = cursorOffset + optionPaddingX + 3 + 8;
option.sprite.y = optionHeight / 2;
}
const baseX = cursorOffset + optionPaddingX + 3;
const baseY = optionHeight / 2;
option.setLabelPosition(baseX + 8, baseY);
if (type === DropDownType.SINGLE) {
option.toggle.x = cursorOffset + optionPaddingX + 3 + 3;
option.toggle.y = optionHeight / 2 + 1;
option.setTogglePosition(baseX + 3, baseY + 1);
} else {
option.toggle.x = cursorOffset + optionPaddingX + 3;
option.toggle.y = optionHeight / 2;
option.setTogglePosition(baseX, baseY);
}
});
this.window = addWindow(scene, 0, 0, optionWidth, options[options.length - 1].y + optionHeight + optionPaddingY, false, false, null, null, WindowVariant.XTHIN);
this.add(this.window);
this.add(options);
@ -202,10 +319,32 @@ export class DropDown extends Phaser.GameObjects.Container {
this.setVisible(false);
}
toggle(): void {
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() && this.cursor > 0) {
return false;
}
return this.setCursor(this.defaultCursor);
}
setCursor(cursor: integer): boolean {
this.cursor = cursor;
if (cursor < 0) {
@ -220,96 +359,213 @@ export class DropDown extends Phaser.GameObjects.Container {
} 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.onChange();
}
}
return true;
}
toggleOptionState(): void {
if (this.dropDownType === DropDownType.MULTI) {
const newState = this.options[this.cursor].toggleOptionState(this.dropDownType);
if (this.cursor === 0) {
this.options.forEach((option, index) => {
if (index !== this.cursor) {
option.setOptionState(this.dropDownType, newState);
}
});
/**
* 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 {
if (this.checkForAllOff()) {
this.options[0].setOptionState(this.dropDownType, DropDownState.OFF);
} else if (this.checkForAllOn()) {
this.options[0].setOptionState(this.dropDownType, DropDownState.ON);
// 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(this.dropDownType, DropDownState.OFF);
this.options[0].setOptionState(DropDownState.OFF);
}
}
} else if (this.dropDownType === DropDownType.SINGLE) {
if (this.options[this.cursor].state === DropDownState.OFF) {
if (option.state === DropDownState.OFF) {
this.options.forEach((option) => {
option.setOptionState(this.dropDownType, DropDownState.OFF);
option.setOptionState(DropDownState.OFF);
option.setDirection(SortDirection.ASC);
option.toggle.setVisible(false);
});
this.options[this.cursor].setOptionState(this.dropDownType, DropDownState.ON);
this.options[this.cursor].setDirection(this.lastDir);
this.options[this.cursor].toggle.setVisible(true);
option.setOptionState(DropDownState.ON);
option.setDirection(this.lastDir);
option.toggle.setVisible(true);
} else {
this.options[this.cursor].toggleDirection();
option.toggleDirection();
this.lastDir = this.options[this.cursor].dir;
}
} else if (this.dropDownType === DropDownType.TRI) {
this.options[this.cursor].toggleOptionState(this.dropDownType);
this.autoSize();
} else if (this.dropDownType === DropDownType.RADIAL) {
option.toggleOptionState();
}
this.onChange();
}
setVisible(value: boolean): this {
super.setVisible(value);
if (value) {
this.autoSize();
}
return this;
}
/**
* 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);
// in TRI dropdown, if state is ON, return the "ON" with the value, if state is OFF, return the "OFF" with the value, if state is TRI, return the "TRI" with the value
} else if (this.dropDownType === DropDownType.TRI) {
return this.options.filter((option, i) => option.state === DropDownState.OFF || option.state === DropDownState.INCLUDE || option.state === DropDownState.EXCLUDE).map((option) => {
return {val: option.val, state: option.state};
} 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, i) => option.state === DropDownState.ON).map((option) => {
return {val: option.val, dir: option.dir};
return this.options.filter(option => option.state === DropDownState.ON).map((option) => {
return { val: option.val, dir: option.dir };
});
}
}
/**
* 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.getVals();
switch (this.dropDownType) {
case DropDownType.MULTI:
case DropDownType.HYBRID:
return currentValues.length === this.defaultValues.length && currentValues.every((value, index) => value === this.defaultValues[index]);
case DropDownType.RADIAL:
return currentValues.every((value, index) => value["val"] === this.defaultValues[index]["val"] && value["state"] === this.defaultValues[index]["state"]);
case DropDownType.SINGLE:
return currentValues[0]["dir"] === this.defaultValues[0]["dir"] && currentValues[0]["val"] === this.defaultValues[0]["val"];
default:
return false;
}
}
/**
* Set all values to their default state
*/
public resetToDefault(): void {
this.setCursor(this.defaultCursor);
for (let i = 0; i < this.options.length; i++) {
const option = this.options[i];
// reset values
switch (this.dropDownType) {
case DropDownType.HYBRID:
case DropDownType.MULTI:
if (this.defaultValues.includes(option.val)) {
option.setOptionState(DropDownState.ON);
} else {
option.setOptionState(DropDownState.OFF);
}
break;
case DropDownType.RADIAL:
const targetValue = this.defaultValues.find(value => value.val === option.val);
option.setOptionState(targetValue.state);
break;
case DropDownType.SINGLE:
if (option.val === this.defaultValues[0].val) {
if (option.state !== DropDownState.ON) {
this.toggleOptionState(i);
}
if (option.dir !== this.defaultValues[0].dir) {
this.toggleOptionState(i);
}
}
break;
}
}
// Select or unselect "ALL" button if applicable
if (this.dropDownType === DropDownType.MULTI || this.dropDownType === DropDownType.HYBRID) {
if (this.checkForAllOn()) {
this.options[0].setOptionState(DropDownState.ON);
} else {
this.options[0].setOptionState(DropDownState.OFF);
}
}
}
/**
* 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++) {
if (this.options[i].sprite) {
if (this.options[i].sprite.displayWidth > maxWidth) {
maxWidth = this.options[i].sprite.displayWidth;
x = this.options[i].sprite.x;
}
} else {
if (this.options[i].text.displayWidth > maxWidth) {
maxWidth = this.options[i].text.displayWidth;
x = this.options[i].text.x;
}
const optionWidth = this.options[i].getWidth();
if (optionWidth > maxWidth) {
maxWidth = optionWidth;
x = this.options[i].getCurrentLabelX();
}
}
this.window.width = maxWidth + x - this.window.x + 6;
@ -319,7 +575,4 @@ export class DropDown extends Phaser.GameObjects.Container {
}
}
isActive(): boolean {
return this.options.some((option) => option.state === DropDownState.ON);
}
}

View File

@ -1,13 +1,14 @@
import BattleScene from "#app/battle-scene.js";
import { DropDown } from "./dropdown";
import { StarterContainer } from "./starter-container";
import { addTextObject, TextStyle } from "./text";
import { addTextObject, getTextColor, TextStyle } from "./text";
import { UiTheme } from "#enums/ui-theme";
import { addWindow, WindowVariant } from "./ui-theme";
export enum DropDownColumn {
GEN,
TYPES,
SHINY,
DEX,
UNLOCKS,
MISC,
SORT
@ -15,18 +16,14 @@ export enum DropDownColumn {
export class FilterBar extends Phaser.GameObjects.Container {
private window: Phaser.GameObjects.NineSlice;
public labels: Phaser.GameObjects.Text[] = [];
public dropDowns: DropDown[] = [];
private labels: Phaser.GameObjects.Text[] = [];
private dropDowns: DropDown[] = [];
private columns: DropDownColumn[] = [];
public cursorObj: Phaser.GameObjects.Image;
public numFilters: number = 0;
public openDropDown: boolean = false;
private lastCursor: number = -1;
public defaultGenVals: any[] = [];
public defaultTypeVals: any[] = [];
public defaultShinyVals: any[] = [];
public defaultUnlocksVals: any[] = [];
public defaultMiscVals: any[] = [];
public defaultSortVals: any[] = [];
private uiTheme: UiTheme;
constructor(scene: BattleScene, x: number, y: number, width: number, height: number) {
super(scene, x, y);
@ -42,10 +39,26 @@ export class FilterBar extends Phaser.GameObjects.Container {
this.cursorObj.setVisible(false);
this.cursorObj.setOrigin(0, 0);
this.add(this.cursorObj);
this.uiTheme = scene.uiTheme;
}
addFilter(text: string, dropDown: DropDown): void {
const filterTypesLabel = addTextObject(this.scene, 0, 3, text, TextStyle.TOOLTIP_CONTENT);
/**
* Add a new filter to the FilterBar, as long that a unique DropDownColumn is provided
* @param column the DropDownColumn that will be used to access the filter values
* @param title the string that will get displayed in the filter bar
* @param dropDown the DropDown with all options for this filter
* @returns true if successful, false if the provided column was already in use for another filter
*/
addFilter(column: DropDownColumn, title: string, dropDown: DropDown): boolean {
// The column should be unique to each filter,
if (this.columns.includes(column)) {
return false;
}
this.columns.push(column);
const filterTypesLabel = addTextObject(this.scene, 0, 3, title, TextStyle.TOOLTIP_CONTENT);
this.labels.push(filterTypesLabel);
this.add(filterTypesLabel);
this.dropDowns.push(dropDown);
@ -53,69 +66,39 @@ export class FilterBar extends Phaser.GameObjects.Container {
this.calcFilterPositions();
this.numFilters++;
return true;
}
/**
* Get the DropDown associated to a given filter
* @param col the DropDownColumn used to register the filter to retrieve
* @returns the associated DropDown if it exists, undefined otherwise
*/
getFilter(col: DropDownColumn) : DropDown {
return this.dropDowns[this.columns.indexOf(col)];
}
/**
* Highlight the labels of the FilterBar if the filters are different from their default values
*/
updateFilterLabels(): void {
const genVals = this.getVals(DropDownColumn.GEN);
const typeVals = this.getVals(DropDownColumn.TYPES);
const shinyVals = this.getVals(DropDownColumn.SHINY);
const unlocksVals = this.getVals(DropDownColumn.UNLOCKS);
const miscVals = this.getVals(DropDownColumn.MISC);
const sortVals = this.getVals(DropDownColumn.SORT);
// onColor is Yellow, offColor is White
const onColor = 0xffef5c;
const offColor = 0xffffff;
// if genVals and defaultGenVals has same elements, set the label to offColor else set it to onColor
if (genVals.length === this.defaultGenVals.length && genVals.every((value, index) => value === this.defaultGenVals[index])) {
this.labels[DropDownColumn.GEN].setTint(offColor);
} else {
this.labels[DropDownColumn.GEN].setTint(onColor);
}
// if typeVals and defaultTypeVals has same elements, set the label to offColor else set it to onColor
if (typeVals.length === this.defaultTypeVals.length && typeVals.every((value, index) => value === this.defaultTypeVals[index])) {
this.labels[DropDownColumn.TYPES].setTint(offColor);
} else {
this.labels[DropDownColumn.TYPES].setTint(onColor);
}
// if shinyVals and defaultShinyVals has same elements, set the label to offColor else set it to onColor
if (shinyVals.length === this.defaultShinyVals.length && shinyVals.every((value, index) => value === this.defaultShinyVals[index])) {
this.labels[DropDownColumn.SHINY].setTint(offColor);
} else {
this.labels[DropDownColumn.SHINY].setTint(onColor);
}
// if unlocksVals and defaultUnlocksVals has same elements, set the label to offColor else set it to onColor
if (unlocksVals.every((value, index) => value["val"] === this.defaultUnlocksVals[index]["val"] && value["state"] === this.defaultUnlocksVals[index]["state"])) {
this.labels[DropDownColumn.UNLOCKS].setTint(offColor);
} else {
this.labels[DropDownColumn.UNLOCKS].setTint(onColor);
}
// if miscVals and defaultMiscVals has same elements, set the label to offColor else set it to onColor
if (miscVals.every((value, index) => value["val"] === this.defaultMiscVals[index]["val"] && value["state"] === this.defaultMiscVals[index]["state"])) {
this.labels[DropDownColumn.MISC].setTint(offColor);
} else {
this.labels[DropDownColumn.MISC].setTint(onColor);
}
// if sortVals and defaultSortVals has same value and dir, set the label to offColor else set it to onColor
if (sortVals[0]["dir"] === this.defaultSortVals[0]["dir"] && sortVals[0]["val"] === this.defaultSortVals[0]["val"]) {
this.labels[DropDownColumn.SORT].setTint(offColor);
} else {
this.labels[DropDownColumn.SORT].setTint(onColor);
for (let i = 0; i < this.numFilters; i++) {
if (this.dropDowns[i].hasDefaultValues()) {
this.labels[i].setColor(getTextColor(TextStyle.TOOLTIP_CONTENT, false, this.uiTheme));
} else {
this.labels[i].setColor(getTextColor(TextStyle.STATS_LABEL, false, this.uiTheme));
}
}
}
calcFilterPositions(): void {
/**
* Position the filter dropdowns evenly across the width of the container
*/
private calcFilterPositions(): void {
const paddingX = 6;
const cursorOffset = 8;
// position labels with even space across the width of the container
let totalWidth = paddingX * 2 + cursorOffset;
this.labels.forEach(label => {
totalWidth += label.displayWidth + cursorOffset;
@ -134,12 +117,23 @@ export class FilterBar extends Phaser.GameObjects.Container {
}
}
/**
* Move the leftmost dropdown to the left of the FilterBar instead of below it
*/
offsetFirstFilter(): void {
if (this.dropDowns[0]) {
this.dropDowns[0].autoSize();
this.dropDowns[0].x -= this.dropDowns[0].getWidth();
this.dropDowns[0].y = 0;
}
}
setCursor(cursor: number): void {
if (this.lastCursor > -1) {
if (this.dropDowns[this.lastCursor].visible) {
this.dropDowns[this.lastCursor].setVisible(false);
this.dropDowns[cursor].setVisible(true);
this.dropDowns[cursor].setCursor(0);
this.dropDowns[cursor].resetCursor();
}
}
@ -149,9 +143,9 @@ export class FilterBar extends Phaser.GameObjects.Container {
}
toggleDropDown(index: number): void {
this.dropDowns[index].toggle();
this.dropDowns[index].toggleVisibility();
this.openDropDown = this.dropDowns[index].visible;
this.dropDowns[index].setCursor(0);
this.dropDowns[index].resetCursor();
}
hideDropDowns(): void {
@ -182,11 +176,22 @@ export class FilterBar extends Phaser.GameObjects.Container {
}
getVals(col: DropDownColumn): any[] {
return this.dropDowns[col].getVals();
return this.getFilter(col).getVals();
}
setValsToDefault(): void {
for (const dropDown of this.dropDowns) {
dropDown.resetToDefault();
}
}
/**
* Find the nearest filter to the provided container
* @param container the StarterContainer to compare position against
* @returns the index of the closest filter
*/
getNearestFilter(container: StarterContainer): number {
// find the nearest filter to the x position
const midx = container.x + container.icon.displayWidth / 2;
let nearest = 0;
let nearestDist = 1000;
@ -201,11 +206,4 @@ export class FilterBar extends Phaser.GameObjects.Container {
return nearest;
}
getLastFilterX(): number {
return this.labels[this.lastCursor].x + this.labels[this.lastCursor].displayWidth / 2;
}
isFilterActive(index: number) {
return this.dropDowns[index].isActive();
}
}

View File

@ -40,7 +40,7 @@ import { Species } from "#enums/species";
import {Button} from "#enums/buttons";
import { EggSourceType } from "#app/enums/egg-source-types.js";
import AwaitableUiHandler from "./awaitable-ui-handler";
import { DropDown, DropDownOption, DropDownState, DropDownType } from "./dropdown";
import { DropDown, DropDownLabel, DropDownOption, DropDownState, DropDownType } from "./dropdown";
import { StarterContainer } from "./starter-container";
import { DropDownColumn, FilterBar } from "./filter-bar";
import { ScrollBar } from "./scroll-bar";
@ -119,6 +119,14 @@ const starterCandyCosts: { passive: integer, costReduction: [integer, integer],
{ passive: 10, costReduction: [3, 10], egg: 10 }, // 10
];
// Position of UI elements
const filterBarHeight = 17;
const speciesContainerX = 109; // if team on the RIGHT: 109 / if on the LEFT: 143
const teamWindowX = 285; // if team on the RIGHT: 285 / if on the LEFT: 109
const teamWindowY = 18;
const teamWindowWidth = 34;
const teamWindowHeight = 132;
function getPassiveCandyCount(baseValue: integer): integer {
return starterCandyCosts[baseValue - 1].passive;
}
@ -145,6 +153,57 @@ function calcStarterPosition(index: number, scrollCursor:number = 0): {x: number
return {x: x, y: y};
}
/**
* Calculates the y position for the icon of stater pokemon selected for the team
* @param index index of the Pokemon in the team (0-5)
* @returns the y position to use for the icon
*/
function calcStarterIconY(index: number) {
const starterSpacing = teamWindowHeight / 7;
const firstStarterY = teamWindowY + starterSpacing / 2;
return Math.round(firstStarterY + starterSpacing * index);
}
/**
* Finds the index of the team Pokemon closest vertically to the given y position
* @param y the y position to find closest starter Pokemon
* @param teamSize how many Pokemon are in the team (0-6)
* @returns index of the closest Pokemon in the team container
*/
function findClosestStarterIndex(y: number, teamSize: number = 6): number {
let smallestDistance = teamWindowHeight;
let closestStarterIndex = 0;
for (let i = 0; i < teamSize; i++) {
const distance = Math.abs(y - (calcStarterIconY(i) - 13));
if (distance < smallestDistance) {
closestStarterIndex = i;
smallestDistance = distance;
}
}
return closestStarterIndex;
}
/**
* Finds the row of the filtered Pokemon closest vertically to the given Pokemon in the team
* @param index index of the Pokemon in the team (0-5)
* @param numberOfRows the number of rows to check against
* @returns index of the row closest vertically to the given Pokemon
*/
function findClosestStarterRow(index: number, numberOfRows: number) {
const currentY = calcStarterIconY(index) - 13;
let smallestDistance = teamWindowHeight;
let closestRowIndex = 0;
for (let i=0; i < numberOfRows; i++) {
const distance = Math.abs(currentY - calcStarterPosition(i * 9).y);
if (distance < smallestDistance) {
closestRowIndex = i;
smallestDistance = distance;
}
}
return closestRowIndex;
}
export default class StarterSelectUiHandler extends MessageUiHandler {
private starterSelectContainer: Phaser.GameObjects.Container;
private starterSelectScrollBar: ScrollBar;
@ -297,40 +356,33 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
this.shinyOverlay.setVisible(false);
this.starterSelectContainer.add(this.shinyOverlay);
const starterContainerWindow = addWindow(this.scene, 109, 18, 175, 161);
const starterContainerBg = this.scene.add.image(110, 19, "starter_container_bg");
const starterContainerWindow = addWindow(this.scene, speciesContainerX, filterBarHeight + 1, 175, 161);
const starterContainerBg = this.scene.add.image(speciesContainerX+1, filterBarHeight + 2, "starter_container_bg");
starterContainerBg.setOrigin(0, 0);
this.starterSelectContainer.add(starterContainerBg);
this.starterSelectContainer.add(addWindow(this.scene, 285, 59, 34, 91));
this.starterSelectContainer.add(addWindow(this.scene, 285, 145, 34, 34, true));
this.starterSelectContainer.add(addWindow(this.scene, teamWindowX, teamWindowY, teamWindowWidth, teamWindowHeight));
this.starterSelectContainer.add(addWindow(this.scene, teamWindowX, teamWindowY + teamWindowHeight - 5, teamWindowWidth, teamWindowWidth, true));
this.starterSelectContainer.add(starterContainerWindow);
// Create and initialise filter bar
this.filterBarContainer = this.scene.add.container(0, 0);
// this.filterBar = new FilterBar(this.scene, 143, 1, 175, 17);
this.filterBar = new FilterBar(this.scene, 109, 1, 175, 17);
this.filterBar = new FilterBar(this.scene, Math.min(speciesContainerX, teamWindowX), 1, 210, filterBarHeight);
// gen filter
const genOptions: DropDownOption[] = [
new DropDownOption(this.scene, 1, i18next.t("starterSelectUiHandler:gen1"), null, DropDownState.ON),
new DropDownOption(this.scene, 2, i18next.t("starterSelectUiHandler:gen2"), null, DropDownState.ON),
new DropDownOption(this.scene, 3, i18next.t("starterSelectUiHandler:gen3"), null, DropDownState.ON),
new DropDownOption(this.scene, 4, i18next.t("starterSelectUiHandler:gen4"), null, DropDownState.ON),
new DropDownOption(this.scene, 5, i18next.t("starterSelectUiHandler:gen5"), null, DropDownState.ON),
new DropDownOption(this.scene, 6, i18next.t("starterSelectUiHandler:gen6"), null, DropDownState.ON),
new DropDownOption(this.scene, 7, i18next.t("starterSelectUiHandler:gen7"), null, DropDownState.ON),
new DropDownOption(this.scene, 8, i18next.t("starterSelectUiHandler:gen8"), null, DropDownState.ON),
new DropDownOption(this.scene, 9, i18next.t("starterSelectUiHandler:gen9"), null, DropDownState.ON),
new DropDownOption(this.scene, 1, new DropDownLabel(i18next.t("starterSelectUiHandler:gen1"))),
new DropDownOption(this.scene, 2, new DropDownLabel(i18next.t("starterSelectUiHandler:gen2"))),
new DropDownOption(this.scene, 3, new DropDownLabel(i18next.t("starterSelectUiHandler:gen3"))),
new DropDownOption(this.scene, 4, new DropDownLabel(i18next.t("starterSelectUiHandler:gen4"))),
new DropDownOption(this.scene, 5, new DropDownLabel(i18next.t("starterSelectUiHandler:gen5"))),
new DropDownOption(this.scene, 6, new DropDownLabel(i18next.t("starterSelectUiHandler:gen6"))),
new DropDownOption(this.scene, 7, new DropDownLabel(i18next.t("starterSelectUiHandler:gen7"))),
new DropDownOption(this.scene, 8, new DropDownLabel(i18next.t("starterSelectUiHandler:gen8"))),
new DropDownOption(this.scene, 9, new DropDownLabel(i18next.t("starterSelectUiHandler:gen9"))),
];
this.filterBar.addFilter(i18next.t("filterBar:genFilter"), new DropDown(this.scene, 0, 0, genOptions, this.updateStarters, DropDownType.MULTI));
this.filterBar.defaultGenVals = this.filterBar.getVals(DropDownColumn.GEN);
// set gen filter to all off except for the I GEN
for (const option of genOptions) {
if (option.val !== 1) {
option.setOptionState(DropDownType.MULTI ,DropDownState.OFF);
}
}
const genDropDown: DropDown = new DropDown(this.scene, 0, 0, genOptions, this.updateStarters, DropDownType.HYBRID);
this.filterBar.addFilter(DropDownColumn.GEN, i18next.t("filterBar:genFilter"), genDropDown);
// type filter
const typeKeys = Object.keys(Type).filter(v => isNaN(Number(v)));
@ -342,10 +394,9 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
const typeSprite = this.scene.add.sprite(0, 0, `types${Utils.verifyLang(i18next.resolvedLanguage) ? `_${i18next.resolvedLanguage}` : ""}`);
typeSprite.setScale(0.5);
typeSprite.setFrame(type.toLowerCase());
typeOptions.push(new DropDownOption(this.scene, index, null, typeSprite));
typeOptions.push(new DropDownOption(this.scene, index, new DropDownLabel("", typeSprite)));
});
this.filterBar.addFilter(i18next.t("filterBar:typeFilter"), new DropDown(this.scene, 0, 0, typeOptions, this.updateStarters, DropDownType.MULTI, 0.5));
this.filterBar.defaultTypeVals = this.filterBar.getVals(DropDownColumn.TYPES);
this.filterBar.addFilter(DropDownColumn.TYPES, i18next.t("filterBar:typeFilter"), new DropDown(this.scene, 0, 0, typeOptions, this.updateStarters, DropDownType.HYBRID, 0.5));
// shiny filter
const shiny1Sprite = this.scene.add.sprite(0, 0, "shiny_icons");
@ -365,45 +416,54 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
shiny3Sprite.setTint(getVariantTint(2));
const shinyOptions = [
new DropDownOption(this.scene, "SHINY3", null, shiny3Sprite),
new DropDownOption(this.scene, "SHINY2", null, shiny2Sprite),
new DropDownOption(this.scene, "SHINY", null, shiny1Sprite),
new DropDownOption(this.scene, "NORMAL", i18next.t("filterBar:normal")),
new DropDownOption(this.scene, "UNCAUGHT", i18next.t("filterBar:uncaught")),
new DropDownOption(this.scene, "SHINY3", new DropDownLabel("", shiny3Sprite)),
new DropDownOption(this.scene, "SHINY2", new DropDownLabel("", shiny2Sprite)),
new DropDownOption(this.scene, "SHINY", new DropDownLabel("", shiny1Sprite)),
new DropDownOption(this.scene, "NORMAL", new DropDownLabel(i18next.t("filterBar:normal"))),
new DropDownOption(this.scene, "UNCAUGHT", new DropDownLabel(i18next.t("filterBar:uncaught")))
];
this.filterBar.addFilter("Owned", new DropDown(this.scene, 0, 0, shinyOptions, this.updateStarters, DropDownType.MULTI));
this.filterBar.defaultShinyVals = this.filterBar.getVals(DropDownColumn.SHINY);
this.filterBar.addFilter(DropDownColumn.DEX, i18next.t("filterBar:dexFilter"), new DropDown(this.scene, 0, 0, shinyOptions, this.updateStarters, DropDownType.HYBRID));
// unlocks filter
const passiveLabels = [
new DropDownLabel(i18next.t("filterBar:passive"), undefined, DropDownState.OFF),
new DropDownLabel(i18next.t("filterBar:passiveUnlocked"), undefined, DropDownState.ON),
new DropDownLabel(i18next.t("filterBar:passiveLocked"), undefined, DropDownState.EXCLUDE),
];
const unlocksOptions = [
new DropDownOption(this.scene, "PASSIVE", ["Passive", i18next.t("filterBar:passiveUnlocked"), i18next.t("filterBar:passiveLocked")], null, DropDownState.OFF),
new DropDownOption(this.scene, "PASSIVE", passiveLabels),
];
this.filterBar.addFilter(i18next.t("filterBar:unlocksFilter"), new DropDown(this.scene, 0, 0, unlocksOptions, this.updateStarters, DropDownType.TRI));
this.filterBar.defaultUnlocksVals = this.filterBar.getVals(DropDownColumn.UNLOCKS);
this.filterBar.addFilter(DropDownColumn.UNLOCKS, i18next.t("filterBar:unlocksFilter"), new DropDown(this.scene, 0, 0, unlocksOptions, this.updateStarters, DropDownType.RADIAL));
// misc filter
const miscOptions = [
new DropDownOption(this.scene, "WIN", ["Win", "Win - Yes", "Win - No"], null, DropDownState.OFF),
const winLabels = [
new DropDownLabel(i18next.t("filterBar:ribbon"), undefined, DropDownState.OFF),
new DropDownLabel(i18next.t("filterBar:hasWon"), undefined, DropDownState.ON),
new DropDownLabel(i18next.t("filterBar:hasNotWon"), undefined, DropDownState.EXCLUDE),
];
this.filterBar.addFilter("Misc", new DropDown(this.scene, 0, 0, miscOptions, this.updateStarters, DropDownType.TRI));
this.filterBar.defaultMiscVals = this.filterBar.getVals(DropDownColumn.MISC);
const miscOptions = [
new DropDownOption(this.scene, "WIN", winLabels),
];
this.filterBar.addFilter(DropDownColumn.MISC, i18next.t("filterBar:miscFilter"), new DropDown(this.scene, 0, 0, miscOptions, this.updateStarters, DropDownType.RADIAL));
// sort filter
const sortOptions = [
new DropDownOption(this.scene, 0, i18next.t("filterBar:sortByNumber")),
new DropDownOption(this.scene, 1, i18next.t("filterBar:sortByCost"), null, DropDownState.OFF),
new DropDownOption(this.scene, 2, i18next.t("filterBar:sortByCandies"), null, DropDownState.OFF),
new DropDownOption(this.scene, 3, i18next.t("filterBar:sortByIVs"), null, DropDownState.OFF),
new DropDownOption(this.scene, 4, i18next.t("filterBar:sortByName"), null, DropDownState.OFF)];
this.filterBar.addFilter(i18next.t("filterBar:sortFilter"), new DropDown(this.scene, 0, 0, sortOptions, this.updateStarters, DropDownType.SINGLE));
new DropDownOption(this.scene, 0, new DropDownLabel(i18next.t("filterBar:sortByNumber"))),
new DropDownOption(this.scene, 1, new DropDownLabel(i18next.t("filterBar:sortByCost"), undefined, DropDownState.OFF)),
new DropDownOption(this.scene, 2, new DropDownLabel(i18next.t("filterBar:sortByCandies"), undefined, DropDownState.OFF)),
new DropDownOption(this.scene, 3, new DropDownLabel(i18next.t("filterBar:sortByIVs"), undefined, DropDownState.OFF)),
new DropDownOption(this.scene, 4, new DropDownLabel(i18next.t("filterBar:sortByName"), undefined, DropDownState.OFF))
];
this.filterBar.addFilter(DropDownColumn.SORT, i18next.t("filterBar:sortFilter"), new DropDown(this.scene, 0, 0, sortOptions, this.updateStarters, DropDownType.SINGLE));
this.filterBarContainer.add(this.filterBar);
this.filterBar.defaultSortVals = this.filterBar.getVals(DropDownColumn.SORT);
this.starterSelectContainer.add(this.filterBarContainer);
// Offset the generation filter dropdown to avoid covering the filtered pokemon
this.filterBar.offsetFirstFilter();
if (!this.scene.uiTheme) {
starterContainerWindow.setVisible(false);
}
@ -479,22 +539,22 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
this.pokemonEggMoveBgs = [];
this.pokemonEggMoveLabels = [];
this.valueLimitLabel = addTextObject(this.scene, 302, 150, "0/10", TextStyle.TOOLTIP_CONTENT);
this.valueLimitLabel = addTextObject(this.scene, teamWindowX+17, 150, "0/10", TextStyle.TOOLTIP_CONTENT);
this.valueLimitLabel.setOrigin(0.5, 0);
this.starterSelectContainer.add(this.valueLimitLabel);
const startLabel = addTextObject(this.scene, 302, 162, i18next.t("common:start"), TextStyle.TOOLTIP_CONTENT);
const startLabel = addTextObject(this.scene, teamWindowX+17, 162, i18next.t("common:start"), TextStyle.TOOLTIP_CONTENT);
startLabel.setOrigin(0.5, 0);
this.starterSelectContainer.add(startLabel);
this.startCursorObj = this.scene.add.nineslice(289, 160, "select_cursor", null, 26, 15, 6, 6, 6, 6);
this.startCursorObj = this.scene.add.nineslice(teamWindowX+4, 160, "select_cursor", null, 26, 15, 6, 6, 6, 6);
this.startCursorObj.setVisible(false);
this.startCursorObj.setOrigin(0, 0);
this.starterSelectContainer.add(this.startCursorObj);
const starterSpecies: Species[] = [];
const starterBoxContainer = this.scene.add.container(115, 9);
const starterBoxContainer = this.scene.add.container(speciesContainerX + 6, 9); //115
this.starterSelectScrollBar = new ScrollBar(this.scene, 161, 12, 0);
@ -544,7 +604,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
this.starterSelectContainer.add(starterBoxContainer);
this.starterIcons = new Array(6).fill(null).map((_, i) => {
const icon = this.scene.add.sprite(292, 63 + 13 * i, "pokemon_icons_0");
const icon = this.scene.add.sprite(teamWindowX + 7, calcStarterIconY(i), "pokemon_icons_0");
icon.setScale(0.5);
icon.setOrigin(0, 0);
icon.setFrame("unknown");
@ -833,6 +893,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
this.setUpgradeAnimation(icon, species);
});
this.resetFilters();
this.updateStarters();
this.setFilterMode(false);
@ -848,6 +909,27 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
return false;
}
/**
* Set the selections for all filters to their default starting value
*/
resetFilters() : void {
const genDropDown: DropDown = this.filterBar.getFilter(DropDownColumn.GEN);
if (this.scene.gameMode.isChallenge) {
// In challenge mode all gens are selected by default
genDropDown.defaultCursor = 0;
} else {
// in other modes, gen 1 is selected by default, and all options disabled
genDropDown.defaultCursor = 1;
}
this.filterBar.setValsToDefault();
// for all modes except challenge, disable all gen options to enable hovering behavior
if (!this.scene.gameMode.isChallenge) {
genDropDown.unselectAllOptions();
}
}
showText(text: string, delay?: integer, callback?: Function, callbackDelay?: integer, prompt?: boolean, promptDelay?: integer) {
super.showText(text, delay, callback, callbackDelay, prompt, promptDelay);
@ -1035,11 +1117,8 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
const onScreenLastIndex = Math.min(this.filteredStarterContainers.length - 1, onScreenFirstIndex + maxRows * maxColumns - 1); // this is the last starter index on the screen
const onScreenNumberOfStarters = onScreenLastIndex - onScreenFirstIndex + 1;
const onScreenNumberOfRows = Math.ceil(onScreenNumberOfStarters / maxColumns);
// const onScreenFirstRow = Math.floor(onScreenFirstIndex / maxColumns);
const onScreenCurrentRow = Math.floor((this.cursor - onScreenFirstIndex) / maxColumns);
// console.log("this.cursor: ", this.cursor, "this.scrollCursor" , this.scrollCursor, "numberOfStarters: ", numberOfStarters, "numOfRows: ", numOfRows, "currentRow: ", currentRow, "onScreenFirstIndex: ", onScreenFirstIndex, "onScreenLastIndex: ", onScreenLastIndex, "onScreenNumberOfStarters: ", onScreenNumberOfStarters, "onScreenNumberOfRow: ", onScreenNumberOfRows, "onScreenCurrentRow: ", onScreenCurrentRow);
const ui = this.getUi();
let success = false;
@ -1053,8 +1132,18 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
}
} else if (button === Button.CANCEL) {
if (this.filterMode && this.filterBar.openDropDown) {
// CANCEL with a filter menu open > close it
this.filterBar.toggleDropDown(this.filterBarCursor);
// if there are possible starters go the first one of the list
if (numberOfStarters > 0) {
this.setFilterMode(false);
this.scrollCursor = 0;
this.updateScroll();
this.setCursor(0);
}
success = true;
} else if (this.statsMode) {
this.toggleStatsMode(false);
success = true;
@ -1081,6 +1170,9 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
this.starterIconsCursorIndex = this.starterSpecies.length - 1;
this.moveStarterIconsCursor(this.starterIconsCursorIndex);
} else {
// up from start button with no Pokemon in the team > go to filter
this.startCursorObj.setVisible(false);
this.filterBarCursor = Math.max(1, this.filterBar.numFilters - 1);
this.setFilterMode(true);
}
success = true;
@ -1091,6 +1183,9 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
this.starterIconsCursorIndex = 0;
this.moveStarterIconsCursor(this.starterIconsCursorIndex);
} else {
// down from start button with no Pokemon in the team > go to filter
this.startCursorObj.setVisible(false);
this.filterBarCursor = Math.max(1, this.filterBar.numFilters - 1);
this.setFilterMode(true);
}
success = true;
@ -1129,27 +1224,31 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
success = this.filterBar.decDropDownCursor();
// else if there is filtered starters
} else if (numberOfStarters > 0) {
// UP from filter bar to bottom of Pokemon list
this.setFilterMode(false);
this.scrollCursor = Math.max(0,numOfRows - 9);
this.updateScroll();
const proportion = (this.filterBarCursor + 0.5) / this.filterBar.numFilters;
const targetCol = Math.floor(proportion * 9);
const targetCol = Math.min(8, Math.floor(proportion * 11));
if (numberOfStarters % 9 > targetCol) {
success = this.setCursor(numberOfStarters - (numberOfStarters) % 9 + targetCol);
this.setCursor(numberOfStarters - (numberOfStarters) % 9 + targetCol);
} else {
success = this.setCursor(Math.max(numberOfStarters - (numberOfStarters) % 9 + targetCol - 9,0));
this.setCursor(Math.max(numberOfStarters - (numberOfStarters) % 9 + targetCol - 9, 0));
}
success = true;
}
break;
case Button.DOWN:
if (this.filterBar.openDropDown) {
success = this.filterBar.incDropDownCursor();
} else if (numberOfStarters > 0) {
// DOWN from filter bar to top of Pokemon list
this.setFilterMode(false);
this.scrollCursor = 0;
this.updateScroll();
const proportion = this.filterBarCursor / Math.max(1, this.filterBar.numFilters - 1);
this.setCursor(Math.round(proportion * (Math.min(9, numberOfStarters) - 1)));
const targetCol = Math.min(8, Math.floor(proportion * 11));
this.setCursor(Math.min(targetCol, numberOfStarters));
success = true;
}
break;
@ -1669,9 +1768,11 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
}
} else {
if (this.starterIconsCursorIndex === 0) {
// Up from first Pokemon in the team > go to filter
this.starterIconsCursorObj.setVisible(false);
this.setSpecies(null);
this.startCursorObj.setVisible(true);
this.filterBarCursor = Math.max(1, this.filterBar.numFilters - 1);
this.setFilterMode(true);
} else {
this.starterIconsCursorIndex--;
this.moveStarterIconsCursor(this.starterIconsCursorIndex);
@ -1687,7 +1788,14 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
}
success = this.setCursor(this.cursor + 9);
this.updateScroll();
} else { // last row
} else if (numOfRows > 1) {
// DOWN from last row of Pokemon > Wrap around to first row
this.scrollCursor = 0;
this.updateScroll();
success = this.setCursor(this.cursor % 9);
} else {
// DOWN from single row of Pokemon > Go to filters
this.filterBarCursor = this.filterBar.getNearestFilter(this.filteredStarterContainers[this.cursor]);
this.setFilterMode(true);
success = true;
}
@ -1708,29 +1816,36 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
if (this.cursor % 9 !== 0) {
success = this.setCursor(this.cursor - 1);
} else {
// LEFT from filtered Pokemon, on the left edge
if (this.starterSpecies.length === 0) {
// just wrap around to the last column
// no starter in team > wrap around to the last column
success = this.setCursor(this.cursor + Math.min(8, numberOfStarters - this.cursor));
} else if (onScreenCurrentRow < 3) {
// always to the first starter
this.cursorObj.setVisible(false);
this.starterIconsCursorIndex = 0;
this.moveStarterIconsCursor(this.starterIconsCursorIndex);
} else if (onScreenCurrentRow < 7) {
// at least one pokemon in team > for the first 7 rows, go to closest starter
this.cursorObj.setVisible(false);
this.starterIconsCursorIndex = Math.min(onScreenCurrentRow-2, this.starterSpecies.length - 1);
this.starterIconsCursorIndex = findClosestStarterIndex(this.cursorObj.y - 1, this.starterSpecies.length);
this.moveStarterIconsCursor(this.starterIconsCursorIndex);
} else {
// at least one pokemon in team > from the bottom 2 rows, go to start run button
this.cursorObj.setVisible(false);
this.setSpecies(null);
this.startCursorObj.setVisible(true);
}
success = true;
}
} else {
} else if (numberOfStarters > 0) {
// LEFT from team > Go to closest filtered Pokemon
const closestRowIndex = findClosestStarterRow(this.starterIconsCursorIndex, onScreenNumberOfRows);
this.starterIconsCursorObj.setVisible(false);
this.cursorObj.setVisible(true);
success = this.setCursor(Math.min(onScreenFirstIndex + (this.starterIconsCursorIndex + 2) * 9 + 8,onScreenLastIndex)); // set last column
this.setCursor(Math.min(onScreenFirstIndex + closestRowIndex * 9 + 8, onScreenLastIndex));
success = true;
} else {
// LEFT from team and no Pokemon in filter > do nothing
success = false;
}
break;
case Button.RIGHT:
@ -1739,33 +1854,37 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
if (this.cursor % 9 < (currentRow < numOfRows - 1 ? 8 : (numberOfStarters - 1) % 9)) {
success = this.setCursor(this.cursor + 1);
} else {
// in right edge
// RIGHT from filtered Pokemon, on the right edge
if (this.starterSpecies.length === 0) {
// just wrap around to the first column
// no selected starter in team > wrap around to the first column
success = this.setCursor(this.cursor - Math.min(8, this.cursor % 9));
} else if (onScreenCurrentRow < 3) {
// always to the first starter
this.cursorObj.setVisible(false);
this.starterIconsCursorIndex = 0;
this.moveStarterIconsCursor(this.starterIconsCursorIndex);
} else if (onScreenCurrentRow < 7) {
// at least one pokemon in team > for the first 7 rows, go to closest starter
this.cursorObj.setVisible(false);
this.starterIconsCursorIndex = Math.min(onScreenCurrentRow-2, this.starterSpecies.length - 1);
this.starterIconsCursorIndex = findClosestStarterIndex(this.cursorObj.y - 1, this.starterSpecies.length);
this.moveStarterIconsCursor(this.starterIconsCursorIndex);
} else {
// at least one pokemon in team > from the bottom 2 rows, go to start run button
this.cursorObj.setVisible(false);
this.setSpecies(null);
this.startCursorObj.setVisible(true);
}
success = true;
}
break;
} else {
} else if (numberOfStarters > 0) {
// RIGHT from team > Go to closest filtered Pokemon
const closestRowIndex = findClosestStarterRow(this.starterIconsCursorIndex, onScreenNumberOfRows);
this.starterIconsCursorObj.setVisible(false);
this.cursorObj.setVisible(true);
success = this.setCursor(Math.min(onScreenFirstIndex + (this.starterIconsCursorIndex + 2) * 9, onScreenLastIndex - (onScreenLastIndex % 9))); // set first column
break;
this.setCursor(Math.min(onScreenFirstIndex + closestRowIndex * 9, onScreenLastIndex - (onScreenLastIndex % 9)));
success = true;
} else {
// RIGHT from team and no Pokemon in filter > do nothing
success = false;
}
break;
}
}
}
@ -1959,7 +2078,6 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
// pre filter for challenges
if (this.scene.gameMode.modeId === GameModes.CHALLENGE) {
console.log("this.scene.gameMode.modeId", this.scene.gameMode.modeId);
this.starterContainers.forEach(container => {
const isValidForChallenge = new Utils.BooleanHolder(true);
Challenge.applyChallenges(this.scene.gameMode, Challenge.ChallengeType.STARTER_CHOICE, container.species, isValidForChallenge, this.scene.gameData.getSpeciesDexAttrProps(container.species, this.scene.gameData.getSpeciesDefaultDexAttr(container.species, false, true)), true);
@ -1997,7 +2115,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
const fitsType = this.filterBar.getVals(DropDownColumn.TYPES).some(type => container.species.isOfType((type as number) - 1));
const fitsShiny = this.filterBar.getVals(DropDownColumn.SHINY).some(variant => {
const fitsShiny = this.filterBar.getVals(DropDownColumn.DEX).some(variant => {
if (variant === "SHINY3") {
return isVariant3Caught;
} else if (variant === "SHINY2") {
@ -2012,7 +2130,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
});
const fitsPassive = this.filterBar.getVals(DropDownColumn.UNLOCKS).some(unlocks => {
if (unlocks.val === "PASSIVE" && unlocks.state === DropDownState.INCLUDE) {
if (unlocks.val === "PASSIVE" && unlocks.state === DropDownState.ON) {
return isPassiveUnlocked;
} else if (unlocks.val === "PASSIVE" && unlocks.state === DropDownState.EXCLUDE) {
return !isPassiveUnlocked;
@ -2024,7 +2142,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
const fitsWin = this.filterBar.getVals(DropDownColumn.MISC).some(misc => {
if (container.species.speciesId < 10) {
}
if (misc.val === "WIN" && misc.state === DropDownState.INCLUDE) {
if (misc.val === "WIN" && misc.state === DropDownState.ON) {
return isWin;
} else if (misc.val === "WIN" && misc.state === DropDownState.EXCLUDE) {
return isNotWin || isUndefined;
@ -2749,8 +2867,10 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
if (this.starterSpecies.length > 0) {
this.starterIconsCursorIndex--;
} else {
// No more Pokemon selected, go back to filters
this.starterIconsCursorObj.setVisible(false);
this.setSpecies(null);
this.filterBarCursor = Math.max(1, this.filterBar.numFilters - 1);
this.setFilterMode(true);
}
}