mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-08-08 08:29:37 +02:00
[UI/UX] Allow adjustable column count in stats-ui-handler
https://github.com/pagefaultgames/pokerogue/pull/6087 * Make column count modular Co-authored by: ShinigamiHolo <128856544+ShinigamiHolo@users.noreply.github.com> * Make game stats ui handler use phaser method chaining * Adjust max cursor calculation * Make arrowUp start invisible * Add implementations for setFlip methods in MockSprite * Misc cleanup * Address kev's review comments * Address kev's review comments Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * improve clarity of doc comment Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com> --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com>
This commit is contained in:
parent
adee68a6d5
commit
ef843debee
@ -2,7 +2,6 @@ import { globalScene } from "#app/global-scene";
|
||||
import { speciesStarterCosts } from "#balance/starters";
|
||||
import { Button } from "#enums/buttons";
|
||||
import { DexAttr } from "#enums/dex-attr";
|
||||
import type { UiMode } from "#enums/ui-mode";
|
||||
import { UiTheme } from "#enums/ui-theme";
|
||||
import type { GameData } from "#system/game-data";
|
||||
import { addTextObject, TextStyle } from "#ui/text";
|
||||
@ -217,152 +216,207 @@ export class GameStatsUiHandler extends UiHandler {
|
||||
private gameStatsContainer: Phaser.GameObjects.Container;
|
||||
private statsContainer: Phaser.GameObjects.Container;
|
||||
|
||||
private statLabels: Phaser.GameObjects.Text[];
|
||||
private statValues: Phaser.GameObjects.Text[];
|
||||
/** The number of rows enabled per page. */
|
||||
private static readonly ROWS_PER_PAGE = 9;
|
||||
|
||||
private statLabels: Phaser.GameObjects.Text[] = [];
|
||||
private statValues: Phaser.GameObjects.Text[] = [];
|
||||
|
||||
private arrowUp: Phaser.GameObjects.Sprite;
|
||||
private arrowDown: Phaser.GameObjects.Sprite;
|
||||
|
||||
constructor(mode: UiMode | null = null) {
|
||||
super(mode);
|
||||
|
||||
this.statLabels = [];
|
||||
this.statValues = [];
|
||||
/** Whether the UI is single column mode */
|
||||
private get singleCol(): boolean {
|
||||
const resolvedLang = i18next.resolvedLanguage ?? "en";
|
||||
// NOTE TO TRANSLATION TEAM: Add more languages that want to display
|
||||
// in a single-column inside of the `[]` (e.g. `["ru", "fr"]`)
|
||||
return ["ru"].includes(resolvedLang);
|
||||
}
|
||||
/** The number of columns used by this menu in the resolved language */
|
||||
private get columnCount(): 1 | 2 {
|
||||
return this.singleCol ? 1 : 2;
|
||||
}
|
||||
|
||||
// #region Columnar-specific properties
|
||||
|
||||
/** The with of each column in the stats view */
|
||||
private get colWidth(): number {
|
||||
return (globalScene.scaledCanvas.width - 2) / this.columnCount;
|
||||
}
|
||||
|
||||
/** THe width of a column's background window */
|
||||
private get colBgWidth(): number {
|
||||
return this.colWidth - 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the `x` position of the stat label based on its index.
|
||||
*
|
||||
* @remarks
|
||||
* Should be used for stat labels (e.g. stat name, not its value). For stat value, use {@linkcode calcTextX}.
|
||||
* @param index - The index of the stat label
|
||||
* @returns The `x` position for the stat label
|
||||
*/
|
||||
private calcLabelX(index: number): number {
|
||||
if (this.singleCol || !(index & 1)) {
|
||||
return 8;
|
||||
}
|
||||
return 8 + (index & 1 ? this.colBgWidth : 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the `y` position of the stat label/text based on its index.
|
||||
* @param index - The index of the stat label
|
||||
* @returns The `y` position for the stat label
|
||||
*/
|
||||
private calcEntryY(index: number): number {
|
||||
if (!this.singleCol) {
|
||||
// Floor division by 2 as we want 1 to go to 0
|
||||
index >>= 1;
|
||||
}
|
||||
return 28 + index * 16;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the `x` position of the stat value based on its index.
|
||||
* @param index - The index of the stat value
|
||||
* @returns The calculated `x` position
|
||||
*/
|
||||
private calcTextX(index: number): number {
|
||||
if (this.singleCol || !(index & 1)) {
|
||||
return this.colBgWidth - 8;
|
||||
}
|
||||
return this.colBgWidth * 2 - 8;
|
||||
}
|
||||
|
||||
/** The number of stats on screen at one time (varies with column count) */
|
||||
private get statsPerPage(): number {
|
||||
return GameStatsUiHandler.ROWS_PER_PAGE * this.columnCount;
|
||||
}
|
||||
|
||||
// #endregion Columnar-specific properties
|
||||
|
||||
setup() {
|
||||
const ui = this.getUi();
|
||||
|
||||
this.gameStatsContainer = globalScene.add.container(1, -(globalScene.game.canvas.height / 6) + 1);
|
||||
/** The scaled width of the global canvas */
|
||||
const sWidth = globalScene.scaledCanvas.width;
|
||||
/** The scaled height of the global canvas */
|
||||
const sHeight = globalScene.scaledCanvas.height;
|
||||
|
||||
const gameStatsContainer = globalScene.add.container(1, -sHeight + 1);
|
||||
this.gameStatsContainer = gameStatsContainer;
|
||||
|
||||
this.gameStatsContainer.setInteractive(
|
||||
new Phaser.Geom.Rectangle(0, 0, globalScene.game.canvas.width / 6, globalScene.game.canvas.height / 6),
|
||||
new Phaser.Geom.Rectangle(0, 0, sWidth, sHeight),
|
||||
Phaser.Geom.Rectangle.Contains,
|
||||
);
|
||||
|
||||
const headerBg = addWindow(0, 0, globalScene.game.canvas.width / 6 - 2, 24);
|
||||
headerBg.setOrigin(0, 0);
|
||||
const headerBg = addWindow(0, 0, sWidth - 2, 24).setOrigin(0);
|
||||
|
||||
const headerText = addTextObject(0, 0, i18next.t("gameStatsUiHandler:stats"), TextStyle.HEADER_LABEL);
|
||||
headerText.setOrigin(0, 0);
|
||||
headerText.setPositionRelative(headerBg, 8, 4);
|
||||
const headerText = addTextObject(0, 0, i18next.t("gameStatsUiHandler:stats"), TextStyle.HEADER_LABEL)
|
||||
.setOrigin(0)
|
||||
.setPositionRelative(headerBg, 8, 4);
|
||||
|
||||
const statsBgWidth = (globalScene.game.canvas.width / 6 - 2) / 2;
|
||||
const [statsBgLeft, statsBgRight] = new Array(2).fill(null).map((_, i) => {
|
||||
const width = statsBgWidth + 2;
|
||||
const height = Math.floor(globalScene.game.canvas.height / 6 - headerBg.height - 2);
|
||||
const statsBg = addWindow(
|
||||
(statsBgWidth - 2) * i,
|
||||
headerBg.height,
|
||||
width,
|
||||
height,
|
||||
false,
|
||||
false,
|
||||
i > 0 ? -3 : 0,
|
||||
1,
|
||||
);
|
||||
statsBg.setOrigin(0, 0);
|
||||
return statsBg;
|
||||
});
|
||||
this.gameStatsContainer.add([headerBg, headerText]);
|
||||
|
||||
this.statsContainer = globalScene.add.container(0, 0);
|
||||
const colWidth = this.colWidth;
|
||||
|
||||
for (let i = 0; i < 18; i++) {
|
||||
const statLabel = addTextObject(
|
||||
8 + (i % 2 === 1 ? statsBgWidth : 0),
|
||||
28 + Math.floor(i / 2) * 16,
|
||||
"",
|
||||
TextStyle.STATS_LABEL,
|
||||
);
|
||||
statLabel.setOrigin(0, 0);
|
||||
this.statsContainer.add(statLabel);
|
||||
this.statLabels.push(statLabel);
|
||||
|
||||
const statValue = addTextObject(statsBgWidth * ((i % 2) + 1) - 8, statLabel.y, "", TextStyle.STATS_VALUE);
|
||||
statValue.setOrigin(1, 0);
|
||||
this.statsContainer.add(statValue);
|
||||
this.statValues.push(statValue);
|
||||
{
|
||||
const columnCount = this.columnCount;
|
||||
const headerHeight = headerBg.height;
|
||||
const statsBgHeight = Math.floor(globalScene.scaledCanvas.height - headerBg.height - 2);
|
||||
const maskOffsetX = columnCount === 1 ? 0 : -3;
|
||||
for (let i = 0; i < columnCount; i++) {
|
||||
gameStatsContainer.add(
|
||||
addWindow(i * this.colBgWidth, headerHeight, colWidth, statsBgHeight, false, false, maskOffsetX, 1, undefined) // formatting
|
||||
.setOrigin(0),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.gameStatsContainer.add(headerBg);
|
||||
this.gameStatsContainer.add(headerText);
|
||||
this.gameStatsContainer.add(statsBgLeft);
|
||||
this.gameStatsContainer.add(statsBgRight);
|
||||
const length = this.statsPerPage;
|
||||
this.statLabels = Array.from({ length }, (_, i) =>
|
||||
addTextObject(this.calcLabelX(i), this.calcEntryY(i), "", TextStyle.STATS_LABEL).setOrigin(0),
|
||||
);
|
||||
|
||||
this.statValues = Array.from({ length }, (_, i) =>
|
||||
addTextObject(this.calcTextX(i), this.calcEntryY(i), "", TextStyle.STATS_VALUE).setOrigin(1, 0),
|
||||
);
|
||||
this.statsContainer = globalScene.add.container(0, 0, [...this.statLabels, ...this.statValues]);
|
||||
|
||||
this.gameStatsContainer.add(this.statsContainer);
|
||||
|
||||
// arrows to show that we can scroll through the stats
|
||||
const isLegacyTheme = globalScene.uiTheme === UiTheme.LEGACY;
|
||||
this.arrowDown = globalScene.add.sprite(
|
||||
statsBgWidth,
|
||||
globalScene.game.canvas.height / 6 - (isLegacyTheme ? 9 : 5),
|
||||
"prompt",
|
||||
);
|
||||
this.gameStatsContainer.add(this.arrowDown);
|
||||
this.arrowUp = globalScene.add.sprite(statsBgWidth, headerBg.height + (isLegacyTheme ? 7 : 3), "prompt");
|
||||
this.arrowUp.flipY = true;
|
||||
this.gameStatsContainer.add(this.arrowUp);
|
||||
const arrowX = this.singleCol ? colWidth / 2 : colWidth;
|
||||
this.arrowDown = globalScene.add.sprite(arrowX, sHeight - (isLegacyTheme ? 9 : 5), "prompt");
|
||||
|
||||
this.arrowUp = globalScene.add
|
||||
.sprite(arrowX, headerBg.height + (isLegacyTheme ? 7 : 3), "prompt") //
|
||||
.setFlipY(true);
|
||||
|
||||
this.gameStatsContainer.add([this.arrowDown, this.arrowUp]);
|
||||
|
||||
ui.add(this.gameStatsContainer);
|
||||
|
||||
this.setCursor(0);
|
||||
|
||||
this.gameStatsContainer.setVisible(false);
|
||||
}
|
||||
|
||||
show(args: any[]): boolean {
|
||||
super.show(args);
|
||||
this.gameStatsContainer.setActive(true).setVisible(true);
|
||||
|
||||
this.setCursor(0);
|
||||
|
||||
this.updateStats();
|
||||
|
||||
this.arrowUp.play("prompt");
|
||||
this.arrowDown.play("prompt");
|
||||
this.arrowUp.setActive(true).play("prompt").setVisible(false);
|
||||
this.arrowDown.setActive(true).play("prompt");
|
||||
/* `setCursor` handles updating stats if the position is different from before.
|
||||
When opening this UI, we want to update stats regardless of the prior position. */
|
||||
if (!this.setCursor(0)) {
|
||||
this.updateStats();
|
||||
}
|
||||
if (globalScene.uiTheme === UiTheme.LEGACY) {
|
||||
this.arrowUp.setTint(0x484848);
|
||||
this.arrowDown.setTint(0x484848);
|
||||
}
|
||||
|
||||
this.updateArrows();
|
||||
|
||||
this.gameStatsContainer.setVisible(true);
|
||||
|
||||
this.getUi().moveTo(this.gameStatsContainer, this.getUi().length - 1);
|
||||
|
||||
this.getUi().hideTooltip();
|
||||
this.getUi()
|
||||
.moveTo(this.gameStatsContainer, this.getUi().length - 1)
|
||||
.hideTooltip();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
updateStats(): void {
|
||||
const statKeys = Object.keys(displayStats).slice(this.cursor * 2, this.cursor * 2 + 18);
|
||||
/**
|
||||
* Update the stat labels and values to reflect the current cursor position.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* Invokes each stat's {@linkcode DisplayStat.sourceFunc | sourceFunc} to obtain its value.
|
||||
* Stat labels are shown as `???` if the stat is marked as hidden and its value is zero.
|
||||
*/
|
||||
private updateStats(): void {
|
||||
const perPage = this.statsPerPage;
|
||||
const columns = this.columnCount;
|
||||
const statKeys = Object.keys(displayStats).slice(this.cursor * columns, this.cursor * columns + perPage);
|
||||
statKeys.forEach((key, s) => {
|
||||
const stat = displayStats[key] as DisplayStat;
|
||||
const value = stat.sourceFunc!(globalScene.gameData); // TODO: is this bang correct?
|
||||
const value = stat.sourceFunc?.(globalScene.gameData) ?? "-";
|
||||
const valAsInt = Number.parseInt(value);
|
||||
this.statLabels[s].setText(
|
||||
!stat.hidden || Number.isNaN(Number.parseInt(value)) || Number.parseInt(value)
|
||||
? i18next.t(`gameStatsUiHandler:${stat.label_key}`)
|
||||
: "???",
|
||||
!stat.hidden || Number.isNaN(value) || valAsInt ? i18next.t(`gameStatsUiHandler:${stat.label_key}`) : "???",
|
||||
);
|
||||
this.statValues[s].setText(value);
|
||||
});
|
||||
if (statKeys.length < 18) {
|
||||
for (let s = statKeys.length; s < 18; s++) {
|
||||
this.statLabels[s].setText("");
|
||||
this.statValues[s].setText("");
|
||||
}
|
||||
for (let s = statKeys.length; s < perPage; s++) {
|
||||
this.statLabels[s].setText("");
|
||||
this.statValues[s].setText("");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show arrows at the top / bottom of the page if it's possible to scroll in that direction
|
||||
*/
|
||||
updateArrows(): void {
|
||||
const showUpArrow = this.cursor > 0;
|
||||
this.arrowUp.setVisible(showUpArrow);
|
||||
|
||||
const showDownArrow = this.cursor < Math.ceil((Object.keys(displayStats).length - 18) / 2);
|
||||
this.arrowDown.setVisible(showDownArrow);
|
||||
/** The maximum cursor position */
|
||||
private get maxCursorPos(): number {
|
||||
return Math.ceil((Object.keys(displayStats).length - this.statsPerPage) / this.columnCount);
|
||||
}
|
||||
|
||||
processInput(button: Button): boolean {
|
||||
@ -370,45 +424,59 @@ export class GameStatsUiHandler extends UiHandler {
|
||||
|
||||
let success = false;
|
||||
|
||||
if (button === Button.CANCEL) {
|
||||
success = true;
|
||||
globalScene.ui.revertMode();
|
||||
} else {
|
||||
switch (button) {
|
||||
case Button.UP:
|
||||
if (this.cursor) {
|
||||
success = this.setCursor(this.cursor - 1);
|
||||
}
|
||||
break;
|
||||
case Button.DOWN:
|
||||
if (this.cursor < Math.ceil((Object.keys(displayStats).length - 18) / 2)) {
|
||||
success = this.setCursor(this.cursor + 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
/** The direction to move the cursor (up/down) */
|
||||
let dir: 1 | -1 = 1;
|
||||
switch (button) {
|
||||
case Button.CANCEL:
|
||||
success = true;
|
||||
globalScene.ui.revertMode();
|
||||
break;
|
||||
// biome-ignore lint/suspicious/noFallthroughSwitchClause: intentional
|
||||
case Button.UP:
|
||||
dir = -1;
|
||||
case Button.DOWN:
|
||||
success = this.setCursor(this.cursor + dir);
|
||||
}
|
||||
|
||||
if (success) {
|
||||
ui.playSelect();
|
||||
return true;
|
||||
}
|
||||
|
||||
return success;
|
||||
return false;
|
||||
}
|
||||
|
||||
setCursor(cursor: number): boolean {
|
||||
const ret = super.setCursor(cursor);
|
||||
|
||||
if (ret) {
|
||||
this.updateStats();
|
||||
this.updateArrows();
|
||||
/**
|
||||
* Set the cursor to the specified position, if able and update the stats display.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* If `newCursor` is not between `0` and {@linkcode maxCursorPos}, or if it is the same as {@linkcode newCursor}
|
||||
* then no updates happen and `false` is returned.
|
||||
*
|
||||
* Otherwise, updates the up/down arrow visibility and calls {@linkcode updateStats}
|
||||
*
|
||||
* @param newCursor - The position to set the cursor to.
|
||||
* @returns Whether the cursor successfully moved to a new position
|
||||
*/
|
||||
override setCursor(newCursor: number): boolean {
|
||||
if (newCursor < 0 || newCursor > this.maxCursorPos || this.cursor === newCursor) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ret;
|
||||
this.cursor = newCursor;
|
||||
|
||||
this.updateStats();
|
||||
// NOTE: Do not toggle the arrows' "active" property here, as this would cause their animations to desync
|
||||
this.arrowUp.setVisible(this.cursor > 0);
|
||||
this.arrowDown.setVisible(this.cursor < this.maxCursorPos);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
clear() {
|
||||
super.clear();
|
||||
this.gameStatsContainer.setVisible(false);
|
||||
this.gameStatsContainer.setVisible(false).setActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -154,6 +154,17 @@ export class MockSprite implements MockGameObject {
|
||||
return this;
|
||||
}
|
||||
|
||||
setFlipY(flip: boolean): this {
|
||||
// Sets the vertical flip state of this Game Object.
|
||||
this.phaserSprite.setFlipY(flip);
|
||||
return this;
|
||||
}
|
||||
|
||||
setFlipX(flip: boolean): this {
|
||||
this.phaserSprite.setFlipX(flip);
|
||||
return this;
|
||||
}
|
||||
|
||||
setCrop(x: number, y: number, width: number, height: number): this {
|
||||
// Sets the crop size of this Game Object.
|
||||
this.phaserSprite.setCrop(x, y, width, height);
|
||||
|
Loading…
Reference in New Issue
Block a user