[Misc][UI/UX] See stats of other users in admin panel (#6579)

* Game stats ui handler takes save data as input

* Make admin panel functional for local testing

* Added button to show stats; mocking for local testing with current save data

* Adding pokédex to admin panel

* Many nice things

* Fixed typo

* Add backend support

* Fixed button width in admin panel

* Apply suggestions from code review

---------

Co-authored-by: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com>
This commit is contained in:
Wlowscha 2025-09-27 23:44:29 +02:00 committed by GitHub
parent 4349ee82b9
commit f8edaeb1ae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 602 additions and 542 deletions

View File

@ -1,31 +1,32 @@
export interface LinkAccountToDiscordIdRequest { import type { SystemSaveData } from "#types/save-data";
username: string;
discordId: string;
}
export interface UnlinkAccountFromDiscordIdRequest {
username: string;
discordId: string;
}
export interface LinkAccountToGoogledIdRequest {
username: string;
googleId: string;
}
export interface UnlinkAccountFromGoogledIdRequest {
username: string;
googleId: string;
}
export interface SearchAccountRequest { export interface SearchAccountRequest {
username: string; username: string;
} }
export interface DiscordRequest extends SearchAccountRequest {
discordId: string;
}
export interface GoogleRequest extends SearchAccountRequest {
googleId: string;
}
export interface SearchAccountResponse { export interface SearchAccountResponse {
username: string; username: string;
discordId: string; discordId: string;
googleId: string; googleId: string;
lastLoggedIn: string; lastLoggedIn: string;
registered: string; registered: string;
systemData?: SystemSaveData;
}
/** Third party login services */
export type AdminUiHandlerService = "discord" | "google";
/** Mode for the admin UI handler */
export type AdminUiHandlerServiceMode = "Link" | "Unlink";
export interface PokerogueAdminApiParams extends Record<AdminUiHandlerService, SearchAccountRequest> {
discord: DiscordRequest;
google: GoogleRequest;
} }

23
src/enums/admin-mode.ts Normal file
View File

@ -0,0 +1,23 @@
export const AdminMode = Object.freeze({
LINK: 0,
SEARCH: 1,
ADMIN: 2,
});
export type AdminMode = (typeof AdminMode)[keyof typeof AdminMode];
/**
* Get the name of the admin mode.
* @param adminMode - The admin mode.
* @returns The Uppercase name of the admin mode.
*/
export function getAdminModeName(adminMode: AdminMode): string {
switch (adminMode) {
case AdminMode.LINK:
return "Link";
case AdminMode.SEARCH:
return "Search";
default:
return "";
}
}

View File

@ -1,112 +1,47 @@
import { ApiBase } from "#api/api-base"; import { ApiBase } from "#api/api-base";
import type { import type {
LinkAccountToDiscordIdRequest, AdminUiHandlerService,
LinkAccountToGoogledIdRequest, AdminUiHandlerServiceMode,
PokerogueAdminApiParams,
SearchAccountRequest, SearchAccountRequest,
SearchAccountResponse, SearchAccountResponse,
UnlinkAccountFromDiscordIdRequest,
UnlinkAccountFromGoogledIdRequest,
} from "#types/api/pokerogue-admin-api"; } from "#types/api/pokerogue-admin-api";
export class PokerogueAdminApi extends ApiBase { export class PokerogueAdminApi extends ApiBase {
public readonly ERR_USERNAME_NOT_FOUND: string = "Username not found!"; public readonly ERR_USERNAME_NOT_FOUND: string = "Username not found!";
/** /**
* Links an account to a discord id. * Link or unlink a third party service to/from a user account
* @param params The {@linkcode LinkAccountToDiscordIdRequest} to send * @param mode - The mode, either "Link" or "Unlink"
* @returns `null` if successful, error message if not * @param service - The third party service to perform the action with
* @param params - The parameters for the user to perform the action on
* @returns `null` if successful, otherwise an error message
*/ */
public async linkAccountToDiscord(params: LinkAccountToDiscordIdRequest) { public async linkUnlinkRequest(
mode: AdminUiHandlerServiceMode,
service: AdminUiHandlerService,
params: PokerogueAdminApiParams[typeof service],
): Promise<string | null> {
const endpoint = "/admin/account/" + service + mode;
const preposition = mode === "Link" ? "with " : "from ";
const errMsg = "Could not " + mode.toLowerCase() + " account " + preposition + service + "!";
try { try {
const response = await this.doPost("/admin/account/discordLink", params, "form-urlencoded"); const response = await this.doPost(endpoint, params, "form-urlencoded");
if (response.ok) { if (response.ok) {
return null; return null;
} }
console.warn("Could not link account with discord!", response.status, response.statusText); console.warn(errMsg, response.status, response.statusText);
if (response.status === 404) { if (response.status === 404) {
return this.ERR_USERNAME_NOT_FOUND; return this.ERR_USERNAME_NOT_FOUND;
} }
} catch (err) { } catch (err) {
console.warn("Could not link account with discord!", err); console.warn(errMsg, err);
} }
return this.ERR_GENERIC; return this.ERR_GENERIC;
} }
/**
* Unlinks an account from a discord id.
* @param params The {@linkcode UnlinkAccountFromDiscordIdRequest} to send
* @returns `null` if successful, error message if not
*/
public async unlinkAccountFromDiscord(params: UnlinkAccountFromDiscordIdRequest) {
try {
const response = await this.doPost("/admin/account/discordUnlink", params, "form-urlencoded");
if (response.ok) {
return null;
}
console.warn("Could not unlink account from discord!", response.status, response.statusText);
if (response.status === 404) {
return this.ERR_USERNAME_NOT_FOUND;
}
} catch (err) {
console.warn("Could not unlink account from discord!", err);
}
return this.ERR_GENERIC;
}
/**
* Links an account to a google id.
* @param params The {@linkcode LinkAccountToGoogledIdRequest} to send
* @returns `null` if successful, error message if not
*/
public async linkAccountToGoogleId(params: LinkAccountToGoogledIdRequest) {
try {
const response = await this.doPost("/admin/account/googleLink", params, "form-urlencoded");
if (response.ok) {
return null;
}
console.warn("Could not link account with google!", response.status, response.statusText);
if (response.status === 404) {
return this.ERR_USERNAME_NOT_FOUND;
}
} catch (err) {
console.warn("Could not link account with google!", err);
}
return this.ERR_GENERIC;
}
/**
* Unlinks an account from a google id.
* @param params The {@linkcode UnlinkAccountFromGoogledIdRequest} to send
* @returns `null` if successful, error message if not
*/
public async unlinkAccountFromGoogleId(params: UnlinkAccountFromGoogledIdRequest) {
try {
const response = await this.doPost("/admin/account/googleUnlink", params, "form-urlencoded");
if (response.ok) {
return null;
}
console.warn("Could not unlink account from google!", response.status, response.statusText);
if (response.status === 404) {
return this.ERR_USERNAME_NOT_FOUND;
}
} catch (err) {
console.warn("Could not unlink account from google!", err);
}
return this.ERR_GENERIC;
}
/** /**
* Search an account. * Search an account.
* @param params The {@linkcode SearchAccountRequest} to send * @param params The {@linkcode SearchAccountRequest} to send

View File

@ -146,12 +146,20 @@ export class GameData {
public eggPity: number[]; public eggPity: number[];
public unlockPity: number[]; public unlockPity: number[];
constructor() { /**
this.loadSettings(); * @param fromRaw - If true, will skip initialization of fields that are normally randomized on new game start. Used for the admin panel; default `false`
this.loadGamepadSettings(); */
this.loadMappingConfigs(); constructor(fromRaw = false) {
this.trainerId = randInt(65536); if (fromRaw) {
this.secretId = randInt(65536); this.trainerId = 0;
this.secretId = 0;
} else {
this.loadSettings();
this.loadGamepadSettings();
this.loadMappingConfigs();
this.trainerId = randInt(65536);
this.secretId = randInt(65536);
}
this.starterData = {}; this.starterData = {};
this.gameStats = new GameStats(); this.gameStats = new GameStats();
this.runHistory = {}; this.runHistory = {};
@ -288,13 +296,115 @@ export class GameData {
}); });
} }
/**
*
* @param dataStr - The raw JSON string of the `SystemSaveData`
* @returns - A new `GameData` instance initialized with the parsed `SystemSaveData`
*/
public static fromRawSystem(dataStr: string): GameData {
const gameData = new GameData(true);
const systemData = GameData.parseSystemData(dataStr);
gameData.initParsedSystem(systemData);
return gameData;
}
/**
* Initialize system data _after_ it has been parsed from JSON.
* @param systemData The parsed `SystemSaveData` to initialize from
*/
private initParsedSystem(systemData: SystemSaveData): void {
applySystemVersionMigration(systemData);
this.trainerId = systemData.trainerId;
this.secretId = systemData.secretId;
this.gender = systemData.gender;
this.saveSetting(SettingKeys.Player_Gender, systemData.gender === PlayerGender.FEMALE ? 1 : 0);
if (systemData.starterData) {
this.starterData = systemData.starterData;
} else {
this.initStarterData();
if (systemData["starterMoveData"]) {
const starterMoveData = systemData["starterMoveData"];
for (const s of Object.keys(starterMoveData)) {
this.starterData[s].moveset = starterMoveData[s];
}
}
if (systemData["starterEggMoveData"]) {
const starterEggMoveData = systemData["starterEggMoveData"];
for (const s of Object.keys(starterEggMoveData)) {
this.starterData[s].eggMoves = starterEggMoveData[s];
}
}
this.migrateStarterAbilities(systemData, this.starterData);
const starterIds = Object.keys(this.starterData).map(s => Number.parseInt(s) as SpeciesId);
for (const s of starterIds) {
this.starterData[s].candyCount += systemData.dexData[s].caughtCount;
this.starterData[s].candyCount += systemData.dexData[s].hatchedCount * 2;
if (systemData.dexData[s].caughtAttr & DexAttr.SHINY) {
this.starterData[s].candyCount += 4;
}
}
}
if (systemData.gameStats) {
this.gameStats = systemData.gameStats;
}
if (systemData.unlocks) {
for (const key of Object.keys(systemData.unlocks)) {
if (this.unlocks.hasOwnProperty(key)) {
this.unlocks[key] = systemData.unlocks[key];
}
}
}
if (systemData.achvUnlocks) {
for (const a of Object.keys(systemData.achvUnlocks)) {
if (achvs.hasOwnProperty(a)) {
this.achvUnlocks[a] = systemData.achvUnlocks[a];
}
}
}
if (systemData.voucherUnlocks) {
for (const v of Object.keys(systemData.voucherUnlocks)) {
if (vouchers.hasOwnProperty(v)) {
this.voucherUnlocks[v] = systemData.voucherUnlocks[v];
}
}
}
if (systemData.voucherCounts) {
getEnumKeys(VoucherType).forEach(key => {
const index = VoucherType[key];
this.voucherCounts[index] = systemData.voucherCounts[index] || 0;
});
}
this.eggs = systemData.eggs ? systemData.eggs.map(e => e.toEgg()) : [];
this.eggPity = systemData.eggPity ? systemData.eggPity.slice(0) : [0, 0, 0, 0];
this.unlockPity = systemData.unlockPity ? systemData.unlockPity.slice(0) : [0, 0, 0, 0];
this.dexData = Object.assign(this.dexData, systemData.dexData);
this.consolidateDexData(this.dexData);
this.defaultDexData = null;
}
public initSystem(systemDataStr: string, cachedSystemDataStr?: string): Promise<boolean> { public initSystem(systemDataStr: string, cachedSystemDataStr?: string): Promise<boolean> {
const { promise, resolve } = Promise.withResolvers<boolean>(); const { promise, resolve } = Promise.withResolvers<boolean>();
try { try {
let systemData = this.parseSystemData(systemDataStr); let systemData = GameData.parseSystemData(systemDataStr);
if (cachedSystemDataStr) { if (cachedSystemDataStr) {
const cachedSystemData = this.parseSystemData(cachedSystemDataStr); const cachedSystemData = GameData.parseSystemData(cachedSystemDataStr);
if (cachedSystemData.timestamp > systemData.timestamp) { if (cachedSystemData.timestamp > systemData.timestamp) {
console.debug("Use cached system"); console.debug("Use cached system");
systemData = cachedSystemData; systemData = cachedSystemData;
@ -307,7 +417,9 @@ export class GameData {
if (isLocal || isBeta) { if (isLocal || isBeta) {
try { try {
console.debug( console.debug(
this.parseSystemData(JSON.stringify(systemData, (_, v: any) => (typeof v === "bigint" ? v.toString() : v))), GameData.parseSystemData(
JSON.stringify(systemData, (_, v: any) => (typeof v === "bigint" ? v.toString() : v)),
),
); );
} catch (err) { } catch (err) {
console.debug("Attempt to log system data failed:", err); console.debug("Attempt to log system data failed:", err);
@ -322,90 +434,7 @@ export class GameData {
localStorage.setItem(lsItemKey, ""); localStorage.setItem(lsItemKey, "");
} }
applySystemVersionMigration(systemData); this.initParsedSystem(systemData);
this.trainerId = systemData.trainerId;
this.secretId = systemData.secretId;
this.gender = systemData.gender;
this.saveSetting(SettingKeys.Player_Gender, systemData.gender === PlayerGender.FEMALE ? 1 : 0);
if (!systemData.starterData) {
this.initStarterData();
if (systemData["starterMoveData"]) {
const starterMoveData = systemData["starterMoveData"];
for (const s of Object.keys(starterMoveData)) {
this.starterData[s].moveset = starterMoveData[s];
}
}
if (systemData["starterEggMoveData"]) {
const starterEggMoveData = systemData["starterEggMoveData"];
for (const s of Object.keys(starterEggMoveData)) {
this.starterData[s].eggMoves = starterEggMoveData[s];
}
}
this.migrateStarterAbilities(systemData, this.starterData);
const starterIds = Object.keys(this.starterData).map(s => Number.parseInt(s) as SpeciesId);
for (const s of starterIds) {
this.starterData[s].candyCount += systemData.dexData[s].caughtCount;
this.starterData[s].candyCount += systemData.dexData[s].hatchedCount * 2;
if (systemData.dexData[s].caughtAttr & DexAttr.SHINY) {
this.starterData[s].candyCount += 4;
}
}
} else {
this.starterData = systemData.starterData;
}
if (systemData.gameStats) {
this.gameStats = systemData.gameStats;
}
if (systemData.unlocks) {
for (const key of Object.keys(systemData.unlocks)) {
if (this.unlocks.hasOwnProperty(key)) {
this.unlocks[key] = systemData.unlocks[key];
}
}
}
if (systemData.achvUnlocks) {
for (const a of Object.keys(systemData.achvUnlocks)) {
if (achvs.hasOwnProperty(a)) {
this.achvUnlocks[a] = systemData.achvUnlocks[a];
}
}
}
if (systemData.voucherUnlocks) {
for (const v of Object.keys(systemData.voucherUnlocks)) {
if (vouchers.hasOwnProperty(v)) {
this.voucherUnlocks[v] = systemData.voucherUnlocks[v];
}
}
}
if (systemData.voucherCounts) {
getEnumKeys(VoucherType).forEach(key => {
const index = VoucherType[key];
this.voucherCounts[index] = systemData.voucherCounts[index] || 0;
});
}
this.eggs = systemData.eggs ? systemData.eggs.map(e => e.toEgg()) : [];
this.eggPity = systemData.eggPity ? systemData.eggPity.slice(0) : [0, 0, 0, 0];
this.unlockPity = systemData.unlockPity ? systemData.unlockPity.slice(0) : [0, 0, 0, 0];
this.dexData = Object.assign(this.dexData, systemData.dexData);
this.consolidateDexData(this.dexData);
this.defaultDexData = null;
resolve(true); resolve(true);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@ -507,7 +536,7 @@ export class GameData {
return true; return true;
} }
parseSystemData(dataStr: string): SystemSaveData { static parseSystemData(dataStr: string): SystemSaveData {
return JSON.parse(dataStr, (k: string, v: any) => { return JSON.parse(dataStr, (k: string, v: any) => {
if (k === "gameStats") { if (k === "gameStats") {
return new GameStats(v); return new GameStats(v);
@ -1296,7 +1325,7 @@ export class GameData {
: this.getSessionSaveData(); : this.getSessionSaveData();
const maxIntAttrValue = 0x80000000; const maxIntAttrValue = 0x80000000;
const systemData = useCachedSystem const systemData = useCachedSystem
? this.parseSystemData(decrypt(localStorage.getItem(`data_${loggedInUser?.username}`)!, bypassLogin)) ? GameData.parseSystemData(decrypt(localStorage.getItem(`data_${loggedInUser?.username}`)!, bypassLogin))
: this.getSystemSaveData(); // TODO: is this bang correct? : this.getSystemSaveData(); // TODO: is this bang correct?
const request = { const request = {
@ -1426,7 +1455,7 @@ export class GameData {
case GameDataType.SYSTEM: { case GameDataType.SYSTEM: {
dataStr = this.convertSystemDataStr(dataStr); dataStr = this.convertSystemDataStr(dataStr);
dataStr = dataStr.replace(/"playTime":\d+/, `"playTime":${this.gameStats.playTime + 60}`); dataStr = dataStr.replace(/"playTime":\d+/, `"playTime":${this.gameStats.playTime + 60}`);
const systemData = this.parseSystemData(dataStr); const systemData = GameData.parseSystemData(dataStr);
valid = !!systemData.dexData && !!systemData.timestamp; valid = !!systemData.dexData && !!systemData.timestamp;
break; break;
} }

View File

@ -1,33 +1,41 @@
import { pokerogueApi } from "#api/pokerogue-api"; import { pokerogueApi } from "#api/pokerogue-api";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { bypassLogin } from "#app/global-vars/bypass-login";
import { AdminMode } from "#enums/admin-mode";
import { Button } from "#enums/buttons"; import { Button } from "#enums/buttons";
import { TextStyle } from "#enums/text-style"; import { TextStyle } from "#enums/text-style";
import { UiMode } from "#enums/ui-mode"; import { UiMode } from "#enums/ui-mode";
import { GameData } from "#system/game-data";
import type {
AdminUiHandlerService,
AdminUiHandlerServiceMode,
SearchAccountResponse,
} from "#types/api/pokerogue-admin-api";
import type { InputFieldConfig } from "#ui/form-modal-ui-handler"; import type { InputFieldConfig } from "#ui/form-modal-ui-handler";
import { FormModalUiHandler } from "#ui/form-modal-ui-handler"; import { FormModalUiHandler } from "#ui/form-modal-ui-handler";
import type { ModalConfig } from "#ui/modal-ui-handler"; import type { ModalConfig } from "#ui/modal-ui-handler";
import { getTextColor } from "#ui/text"; import { getTextColor } from "#ui/text";
import { toTitleCase } from "#utils/strings"; import { toTitleCase } from "#utils/strings";
type AdminUiHandlerService = "discord" | "google";
type AdminUiHandlerServiceMode = "Link" | "Unlink";
export class AdminUiHandler extends FormModalUiHandler { export class AdminUiHandler extends FormModalUiHandler {
private adminMode: AdminMode; private adminMode: AdminMode;
private adminResult: AdminSearchInfo; private adminResult: SearchAccountResponse;
private config: ModalConfig; private config: ModalConfig;
private tempGameData: GameData | null = null;
private readonly buttonGap = 10; private readonly buttonGap = 10;
private readonly ERR_REQUIRED_FIELD = (field: string) => { /** @returns "[field] is required" */
private static ERR_REQUIRED_FIELD(field: string) {
if (field === "username") { if (field === "username") {
return `${toTitleCase(field)} is required`; return `${toTitleCase(field)} is required`;
} }
return `${toTitleCase(field)} Id is required`; return `${toTitleCase(field)} Id is required`;
}; }
// returns a string saying whether a username has been successfully linked/unlinked to discord/google /** @returns "Username and [service] successfully [mode]ed" */
private readonly SUCCESS_SERVICE_MODE = (service: string, mode: string) => { private static SUCCESS_SERVICE_MODE(service: string, mode: string) {
return `Username and ${service} successfully ${mode.toLowerCase()}ed`; return `Username and ${service} successfully ${mode.toLowerCase()}ed`;
}; }
constructor(mode: UiMode | null = null) { constructor(mode: UiMode | null = null) {
super(mode); super(mode);
@ -48,50 +56,41 @@ export class AdminUiHandler extends FormModalUiHandler {
override getButtonLabels(): string[] { override getButtonLabels(): string[] {
switch (this.adminMode) { switch (this.adminMode) {
case AdminMode.LINK: case AdminMode.LINK:
return ["Link Account", "Cancel"]; return ["Link Account", "Cancel", "", ""];
case AdminMode.SEARCH: case AdminMode.SEARCH:
return ["Find account", "Cancel"]; return ["Find account", "Cancel", "", ""];
case AdminMode.ADMIN: case AdminMode.ADMIN:
return ["Back to search", "Cancel"]; return ["Back to search", "Cancel", "Stats", "Pokedex"];
default: default:
return ["Activate ADMIN", "Cancel"]; return ["Activate ADMIN", "Cancel", "Stats", "Pokedex"];
} }
} }
override getInputFieldConfigs(): InputFieldConfig[] { override getInputFieldConfigs(): InputFieldConfig[] {
const inputFieldConfigs: InputFieldConfig[] = [];
switch (this.adminMode) { switch (this.adminMode) {
case AdminMode.LINK: case AdminMode.LINK:
inputFieldConfigs.push({ label: "Username" }); return [{ label: "Username" }, { label: "Discord ID" }];
inputFieldConfigs.push({ label: "Discord ID" });
break;
case AdminMode.SEARCH: case AdminMode.SEARCH:
inputFieldConfigs.push({ label: "Username" }); return [{ label: "Username" }];
break;
case AdminMode.ADMIN: { case AdminMode.ADMIN: {
const adminResult = this.adminResult ?? {
username: "",
discordId: "",
googleId: "",
lastLoggedIn: "",
registered: "",
};
// Discord and Google ID fields that are not empty get locked, other fields are all locked // Discord and Google ID fields that are not empty get locked, other fields are all locked
inputFieldConfigs.push({ label: "Username", isReadOnly: true }); return [
inputFieldConfigs.push({ { label: "Username", isReadOnly: true },
label: "Discord ID", {
isReadOnly: adminResult.discordId !== "", label: "Discord ID",
}); isReadOnly: (this.adminResult?.discordId ?? "") !== "",
inputFieldConfigs.push({ },
label: "Google ID", {
isReadOnly: adminResult.googleId !== "", label: "Google ID",
}); isReadOnly: (this.adminResult?.googleId ?? "") !== "",
inputFieldConfigs.push({ label: "Last played", isReadOnly: true }); },
inputFieldConfigs.push({ label: "Registered", isReadOnly: true }); { label: "Last played", isReadOnly: true },
break; { label: "Registered", isReadOnly: true },
];
} }
default:
return [];
} }
return inputFieldConfigs;
} }
processInput(button: Button): boolean { processInput(button: Button): boolean {
@ -126,36 +125,41 @@ export class AdminUiHandler extends FormModalUiHandler {
this.buttonLabels[i].setText(labels[i]); // sets the label text this.buttonLabels[i].setText(labels[i]); // sets the label text
} }
this.errorMessage.setPosition(10, (hasTitle ? 31 : 5) + 20 * (fields.length - 1) + 16 + this.getButtonTopMargin()); // sets the position of the message dynamically const msgColor = isMessageError ? TextStyle.SUMMARY_PINK : TextStyle.SUMMARY_GREEN;
if (isMessageError) {
this.errorMessage.setColor(getTextColor(TextStyle.SUMMARY_PINK)); this.errorMessage
this.errorMessage.setShadowColor(getTextColor(TextStyle.SUMMARY_PINK, true)); .setPosition(10, (hasTitle ? 31 : 5) + 20 * (fields.length - 1) + 16 + this.getButtonTopMargin())
} else { .setColor(getTextColor(msgColor))
this.errorMessage.setColor(getTextColor(TextStyle.SUMMARY_GREEN)); .setShadowColor(getTextColor(msgColor, true));
this.errorMessage.setShadowColor(getTextColor(TextStyle.SUMMARY_GREEN, true));
if (!super.show(args)) {
return false;
} }
if (super.show(args)) { this.hideLastButtons(this.adminMode === AdminMode.ADMIN ? 0 : 2);
this.populateFields(this.adminMode, this.adminResult);
const originalSubmitAction = this.submitAction; this.populateFields(this.adminMode, this.adminResult);
this.submitAction = _ => { const originalSubmitAction = this.submitAction;
this.submitAction = originalSubmitAction; this.submitAction = () => {
const adminSearchResult: AdminSearchInfo = this.convertInputsToAdmin(); // this converts the input texts into a single object for use later this.submitAction = originalSubmitAction;
const validFields = this.areFieldsValid(this.adminMode); const adminSearchResult: SearchAccountResponse = this.convertInputsToAdmin(); // this converts the input texts into a single object for use later
if (validFields.error) { const validFields = this.areFieldsValid(this.adminMode);
globalScene.ui.setMode(UiMode.LOADING, { buttonActions: [] }); // this is here to force a loading screen to allow the admin tool to reopen again if there's an error if (validFields.error) {
return this.showMessage(validFields.errorMessage ?? "", adminSearchResult, true); globalScene.ui.setMode(UiMode.LOADING, { buttonActions: [] }); // this is here to force a loading screen to allow the admin tool to reopen again if there's an error
} return this.showMessage(validFields.errorMessage ?? "", adminSearchResult, true);
globalScene.ui.setMode(UiMode.LOADING, { buttonActions: [] }); }
if (this.adminMode === AdminMode.LINK) { globalScene.ui.setMode(UiMode.LOADING, { buttonActions: [] });
switch (this.adminMode) {
case AdminMode.LINK:
this.adminLinkUnlink(adminSearchResult, "discord", "Link") // calls server to link discord this.adminLinkUnlink(adminSearchResult, "discord", "Link") // calls server to link discord
.then(response => { .then(response => {
if (response.error) { if (response.error) {
return this.showMessage(response.errorType, adminSearchResult, true); // error or some kind return this.showMessage(response.errorType, adminSearchResult, true); // error or some kind
} }
return this.showMessage(this.SUCCESS_SERVICE_MODE("discord", "link"), adminSearchResult, false); // success return this.showMessage(AdminUiHandler.SUCCESS_SERVICE_MODE("discord", "link"), adminSearchResult, false); // success
}); });
} else if (this.adminMode === AdminMode.SEARCH) { break;
case AdminMode.SEARCH:
this.adminSearch(adminSearchResult) // admin search for username this.adminSearch(adminSearchResult) // admin search for username
.then(response => { .then(response => {
if (response.error) { if (response.error) {
@ -163,16 +167,16 @@ export class AdminUiHandler extends FormModalUiHandler {
} }
this.updateAdminPanelInfo(response.adminSearchResult ?? adminSearchResult); // success this.updateAdminPanelInfo(response.adminSearchResult ?? adminSearchResult); // success
}); });
} else if (this.adminMode === AdminMode.ADMIN) { break;
case AdminMode.ADMIN:
this.updateAdminPanelInfo(adminSearchResult, AdminMode.SEARCH); this.updateAdminPanelInfo(adminSearchResult, AdminMode.SEARCH);
} break;
}; }
return true; };
} return true;
return false;
} }
showMessage(message: string, adminResult: AdminSearchInfo, isError: boolean) { showMessage(message: string, adminResult: SearchAccountResponse, isError: boolean) {
globalScene.ui.setMode( globalScene.ui.setMode(
UiMode.ADMIN, UiMode.ADMIN,
Object.assign(this.config, { errorMessage: message?.trim() }), Object.assign(this.config, { errorMessage: message?.trim() }),
@ -187,13 +191,65 @@ export class AdminUiHandler extends FormModalUiHandler {
} }
} }
private populateAdminFields(adminResult: SearchAccountResponse) {
for (const [i, aR] of Object.keys(adminResult).entries()) {
if (aR === "systemData") {
continue;
}
this.inputs[i].setText(adminResult[aR]);
if (aR === "discordId" || aR === "googleId") {
// this is here to add the icons for linking/unlinking of google/discord IDs
const nineSlice = this.inputContainers[i].list.find(iC => iC.type === "NineSlice");
const img = globalScene.add.image(
this.inputContainers[i].x + nineSlice!.width + this.buttonGap,
this.inputContainers[i].y + Math.floor(nineSlice!.height / 2),
adminResult[aR] === "" ? "link_icon" : "unlink_icon",
);
img
.setName(`adminBtn_${aR}`)
.setOrigin()
.setInteractive()
.on("pointerdown", () => {
const service = aR.toLowerCase().replace("id", ""); // this takes our key (discordId or googleId) and removes the "Id" at the end to make it more url friendly
const mode = adminResult[aR] === "" ? "Link" : "Unlink"; // this figures out if we're linking or unlinking a service
const validFields = this.areFieldsValid(this.adminMode, service);
if (validFields.error) {
globalScene.ui.setMode(UiMode.LOADING, { buttonActions: [] }); // this is here to force a loading screen to allow the admin tool to reopen again if there's an error
return this.showMessage(validFields.errorMessage ?? "", adminResult, true);
}
this.adminLinkUnlink(this.convertInputsToAdmin(), service as AdminUiHandlerService, mode).then(response => {
// attempts to link/unlink depending on the service
if (response.error) {
globalScene.ui.setMode(UiMode.LOADING, { buttonActions: [] });
return this.showMessage(response.errorType, adminResult, true); // fail
}
// success, reload panel with new results
globalScene.ui.setMode(UiMode.LOADING, { buttonActions: [] });
this.adminSearch(adminResult).then(searchResponse => {
if (searchResponse.error) {
return this.showMessage(searchResponse.errorType, adminResult, true);
}
return this.showMessage(
AdminUiHandler.SUCCESS_SERVICE_MODE(service, mode),
searchResponse.adminSearchResult ?? adminResult,
false,
);
});
});
});
this.addInteractionHoverEffect(img);
this.modalContainer.add(img);
}
}
}
/** /**
* This is used to update the fields' text when loading in a new admin ui handler. It uses the {@linkcode adminResult} * This is used to update the fields' text when loading in a new admin ui handler. It uses the {@linkcode adminResult}
* to update the input text based on the {@linkcode adminMode}. For a linking adminMode, it sets the username and discord. * to update the input text based on the {@linkcode adminMode}. For a linking adminMode, it sets the username and discord.
* For a search adminMode, it sets the username. For an admin adminMode, it sets all the info from adminResult in the * For a search adminMode, it sets the username. For an admin adminMode, it sets all the info from adminResult in the
* appropriate text boxes, and also sets the link/unlink icons for discord/google depending on the result * appropriate text boxes, and also sets the link/unlink icons for discord/google depending on the result
*/ */
private populateFields(adminMode: AdminMode, adminResult: AdminSearchInfo) { private populateFields(adminMode: AdminMode, adminResult: SearchAccountResponse) {
switch (adminMode) { switch (adminMode) {
case AdminMode.LINK: case AdminMode.LINK:
this.inputs[0].setText(adminResult.username); this.inputs[0].setText(adminResult.username);
@ -203,53 +259,7 @@ export class AdminUiHandler extends FormModalUiHandler {
this.inputs[0].setText(adminResult.username); this.inputs[0].setText(adminResult.username);
break; break;
case AdminMode.ADMIN: case AdminMode.ADMIN:
Object.keys(adminResult).forEach((aR, i) => { this.populateAdminFields(adminResult);
this.inputs[i].setText(adminResult[aR]);
if (aR === "discordId" || aR === "googleId") {
// this is here to add the icons for linking/unlinking of google/discord IDs
const nineSlice = this.inputContainers[i].list.find(iC => iC.type === "NineSlice");
const img = globalScene.add.image(
this.inputContainers[i].x + nineSlice!.width + this.buttonGap,
this.inputContainers[i].y + Math.floor(nineSlice!.height / 2),
adminResult[aR] === "" ? "link_icon" : "unlink_icon",
);
img.setName(`adminBtn_${aR}`);
img.setOrigin(0.5, 0.5);
img.setInteractive();
img.on("pointerdown", () => {
const service = aR.toLowerCase().replace("id", ""); // this takes our key (discordId or googleId) and removes the "Id" at the end to make it more url friendly
const mode = adminResult[aR] === "" ? "Link" : "Unlink"; // this figures out if we're linking or unlinking a service
const validFields = this.areFieldsValid(this.adminMode, service);
if (validFields.error) {
globalScene.ui.setMode(UiMode.LOADING, { buttonActions: [] }); // this is here to force a loading screen to allow the admin tool to reopen again if there's an error
return this.showMessage(validFields.errorMessage ?? "", adminResult, true);
}
this.adminLinkUnlink(this.convertInputsToAdmin(), service as AdminUiHandlerService, mode).then(
response => {
// attempts to link/unlink depending on the service
if (response.error) {
globalScene.ui.setMode(UiMode.LOADING, { buttonActions: [] });
return this.showMessage(response.errorType, adminResult, true); // fail
}
// success, reload panel with new results
globalScene.ui.setMode(UiMode.LOADING, { buttonActions: [] });
this.adminSearch(adminResult).then(response => {
if (response.error) {
return this.showMessage(response.errorType, adminResult, true);
}
return this.showMessage(
this.SUCCESS_SERVICE_MODE(service, mode),
response.adminSearchResult ?? adminResult,
false,
);
});
},
);
});
this.addInteractionHoverEffect(img);
this.modalContainer.add(img);
}
});
break; break;
} }
} }
@ -261,23 +271,23 @@ export class AdminUiHandler extends FormModalUiHandler {
// username missing from link panel // username missing from link panel
return { return {
error: true, error: true,
errorMessage: this.ERR_REQUIRED_FIELD("username"), errorMessage: AdminUiHandler.ERR_REQUIRED_FIELD("username"),
}; };
} }
if (!this.inputs[1].text) { if (!this.inputs[1].text) {
// discordId missing from linking panel // discordId missing from linking panel
return { return {
error: true, error: true,
errorMessage: this.ERR_REQUIRED_FIELD("discord"), errorMessage: AdminUiHandler.ERR_REQUIRED_FIELD("discord"),
}; };
} }
break; break;
case AdminMode.SEARCH: case AdminMode.SEARCH:
if (!this.inputs[0].text) { if (!this.inputs[0].text && !bypassLogin) {
// username missing from search panel // username missing from search panel, skip check for local testing
return { return {
error: true, error: true,
errorMessage: this.ERR_REQUIRED_FIELD("username"), errorMessage: AdminUiHandler.ERR_REQUIRED_FIELD("username"),
}; };
} }
break; break;
@ -286,14 +296,14 @@ export class AdminUiHandler extends FormModalUiHandler {
// discordId missing from admin panel // discordId missing from admin panel
return { return {
error: true, error: true,
errorMessage: this.ERR_REQUIRED_FIELD(service), errorMessage: AdminUiHandler.ERR_REQUIRED_FIELD(service),
}; };
} }
if (!this.inputs[2].text && service === "google") { if (!this.inputs[2].text && service === "google") {
// googleId missing from admin panel // googleId missing from admin panel
return { return {
error: true, error: true,
errorMessage: this.ERR_REQUIRED_FIELD(service), errorMessage: AdminUiHandler.ERR_REQUIRED_FIELD(service),
}; };
} }
break; break;
@ -303,17 +313,32 @@ export class AdminUiHandler extends FormModalUiHandler {
}; };
} }
private convertInputsToAdmin(): AdminSearchInfo { private convertInputsToAdmin(): SearchAccountResponse {
const inputs = this.inputs;
return { return {
username: this.inputs[0]?.node ? this.inputs[0].text : "", username: inputs[0]?.node ? inputs[0].text : "",
discordId: this.inputs[1]?.node ? this.inputs[1]?.text : "", discordId: inputs[1]?.node ? inputs[1]?.text : "",
googleId: this.inputs[2]?.node ? this.inputs[2]?.text : "", googleId: inputs[2]?.node ? inputs[2]?.text : "",
lastLoggedIn: this.inputs[3]?.node ? this.inputs[3]?.text : "", lastLoggedIn: inputs[3]?.node ? inputs[3]?.text : "",
registered: this.inputs[4]?.node ? this.inputs[4]?.text : "", registered: inputs[4]?.node ? inputs[4]?.text : "",
}; };
} }
private async adminSearch(adminSearchResult: AdminSearchInfo) { private async adminSearch(adminSearchResult: SearchAccountResponse) {
this.tempGameData = null;
// Mocking response, solely for local testing
if (bypassLogin) {
const fakeResponse: SearchAccountResponse = {
username: adminSearchResult.username,
discordId: "",
googleId: "",
lastLoggedIn: "",
registered: "",
};
this.tempGameData = globalScene.gameData;
return { adminSearchResult: fakeResponse, error: false };
}
try { try {
const [adminInfo, errorType] = await pokerogueApi.admin.searchAccount({ const [adminInfo, errorType] = await pokerogueApi.admin.searchAccount({
username: adminSearchResult.username, username: adminSearchResult.username,
@ -322,7 +347,14 @@ export class AdminUiHandler extends FormModalUiHandler {
// error - if adminInfo.status === this.httpUserNotFoundErrorCode that means the username can't be found in the db // error - if adminInfo.status === this.httpUserNotFoundErrorCode that means the username can't be found in the db
return { adminSearchResult, error: true, errorType }; return { adminSearchResult, error: true, errorType };
} }
// success if (adminInfo.systemData) {
const rawSystem = JSON.stringify(adminInfo.systemData);
try {
this.tempGameData = GameData.fromRawSystem(rawSystem);
} catch {
console.warn("Could not parse system data for admin panel, stats/pokedex will be unavailable!");
}
}
return { adminSearchResult: adminInfo, error: false }; return { adminSearchResult: adminInfo, error: false };
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@ -331,58 +363,23 @@ export class AdminUiHandler extends FormModalUiHandler {
} }
private async adminLinkUnlink( private async adminLinkUnlink(
adminSearchResult: AdminSearchInfo, adminSearchResult: SearchAccountResponse,
service: AdminUiHandlerService, service: AdminUiHandlerService,
mode: AdminUiHandlerServiceMode, mode: AdminUiHandlerServiceMode,
) { ) {
try { try {
let errorType: string | null = null; const error = await pokerogueApi.admin.linkUnlinkRequest(mode, service, adminSearchResult);
if (error != null) {
if (service === "discord") { return { error: true, errorType: error };
if (mode === "Link") {
errorType = await pokerogueApi.admin.linkAccountToDiscord({
discordId: adminSearchResult.discordId,
username: adminSearchResult.username,
});
} else if (mode === "Unlink") {
errorType = await pokerogueApi.admin.unlinkAccountFromDiscord({
discordId: adminSearchResult.discordId,
username: adminSearchResult.username,
});
} else {
console.warn("Unknown mode", mode, "for service", service);
}
} else if (service === "google") {
if (mode === "Link") {
errorType = await pokerogueApi.admin.linkAccountToGoogleId({
googleId: adminSearchResult.googleId,
username: adminSearchResult.username,
});
} else if (mode === "Unlink") {
errorType = await pokerogueApi.admin.unlinkAccountFromGoogleId({
googleId: adminSearchResult.googleId,
username: adminSearchResult.username,
});
} else {
console.warn("Unknown mode", mode, "for service", service);
}
} else {
console.warn("Unknown service", service);
} }
if (errorType) {
// error - if response.status === this.httpUserNotFoundErrorCode that means the username can't be found in the db
return { adminSearchResult, error: true, errorType };
}
// success!
return { adminSearchResult, error: false };
} catch (err) { } catch (err) {
console.error(err); console.error(err);
return { error: true, errorType: err }; return { error: true, errorType: err };
} }
return { adminSearchResult, error: false };
} }
private updateAdminPanelInfo(adminSearchResult: AdminSearchInfo, mode?: AdminMode) { private updateAdminPanelInfo(adminSearchResult: SearchAccountResponse, mode?: AdminMode) {
mode = mode ?? AdminMode.ADMIN; mode = mode ?? AdminMode.ADMIN;
globalScene.ui.setMode( globalScene.ui.setMode(
UiMode.ADMIN, UiMode.ADMIN,
@ -397,6 +394,27 @@ export class AdminUiHandler extends FormModalUiHandler {
globalScene.ui.revertMode(); globalScene.ui.revertMode();
globalScene.ui.revertMode(); globalScene.ui.revertMode();
}, },
() => {
if (this.tempGameData == null) {
globalScene.ui.playError();
return;
}
this.hide();
globalScene.ui.setOverlayMode(
UiMode.GAME_STATS,
adminSearchResult.username,
this.tempGameData,
this.unhide.bind(this),
);
},
() => {
if (this.tempGameData == null) {
globalScene.ui.playError();
return;
}
this.hide();
globalScene.ui.setOverlayMode(UiMode.POKEDEX, this.tempGameData, this.unhide.bind(this));
},
], ],
}, },
mode, mode,
@ -432,28 +450,3 @@ export class AdminUiHandler extends FormModalUiHandler {
} }
} }
} }
export enum AdminMode {
LINK,
SEARCH,
ADMIN,
}
export function getAdminModeName(adminMode: AdminMode): string {
switch (adminMode) {
case AdminMode.LINK:
return "Link";
case AdminMode.SEARCH:
return "Search";
default:
return "";
}
}
interface AdminSearchInfo {
username: string;
discordId: string;
googleId: string;
lastLoggedIn: string;
registered: string;
}

View File

@ -1,7 +1,7 @@
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { Button } from "#enums/buttons"; import { Button } from "#enums/buttons";
import { TextStyle } from "#enums/text-style"; import { TextStyle } from "#enums/text-style";
import type { UiMode } from "#enums/ui-mode"; import type { AnyFn } from "#types/type-helpers";
import type { ModalConfig } from "#ui/modal-ui-handler"; import type { ModalConfig } from "#ui/modal-ui-handler";
import { ModalUiHandler } from "#ui/modal-ui-handler"; import { ModalUiHandler } from "#ui/modal-ui-handler";
import { addTextInputObject, addTextObject, getTextColor } from "#ui/text"; import { addTextInputObject, addTextObject, getTextColor } from "#ui/text";
@ -14,23 +14,14 @@ export interface FormModalConfig extends ModalConfig {
} }
export abstract class FormModalUiHandler extends ModalUiHandler { export abstract class FormModalUiHandler extends ModalUiHandler {
protected editing: boolean; protected editing = false;
protected inputContainers: Phaser.GameObjects.Container[]; protected inputContainers: Phaser.GameObjects.Container[] = [];
protected inputs: InputText[]; protected inputs: InputText[] = [];
protected errorMessage: Phaser.GameObjects.Text; protected errorMessage: Phaser.GameObjects.Text;
protected submitAction: Function | null; protected submitAction: AnyFn | undefined;
protected cancelAction: (() => void) | null; protected cancelAction: (() => void) | undefined;
protected tween: Phaser.Tweens.Tween; protected tween: Phaser.Tweens.Tween | undefined;
protected formLabels: Phaser.GameObjects.Text[]; protected formLabels: Phaser.GameObjects.Text[] = [];
constructor(mode: UiMode | null = null) {
super(mode);
this.editing = false;
this.inputContainers = [];
this.inputs = [];
this.formLabels = [];
}
/** /**
* Get configuration for all fields that should be part of the modal * Get configuration for all fields that should be part of the modal
@ -77,18 +68,18 @@ export abstract class FormModalUiHandler extends ModalUiHandler {
fontSize: "42px", fontSize: "42px",
wordWrap: { width: 850 }, wordWrap: { width: 850 },
}, },
); )
this.errorMessage.setColor(getTextColor(TextStyle.SUMMARY_PINK)); .setColor(getTextColor(TextStyle.SUMMARY_PINK))
this.errorMessage.setShadowColor(getTextColor(TextStyle.SUMMARY_PINK, true)); .setShadowColor(getTextColor(TextStyle.SUMMARY_PINK, true))
this.errorMessage.setVisible(false); .setVisible(false);
this.modalContainer.add(this.errorMessage); this.modalContainer.add(this.errorMessage);
} }
protected updateFields(fieldsConfig: InputFieldConfig[], hasTitle: boolean) { protected updateFields(fieldsConfig: InputFieldConfig[], hasTitle: boolean) {
this.inputContainers = []; const inputContainers = (this.inputContainers = new Array(fieldsConfig.length));
this.inputs = []; const inputs = (this.inputs = new Array(fieldsConfig.length));
this.formLabels = []; const formLabels = (this.formLabels = new Array(fieldsConfig.length));
fieldsConfig.forEach((config, f) => { for (const [f, config] of fieldsConfig.entries()) {
// The Pokédex Scan Window uses width `300` instead of `160` like the other forms // The Pokédex Scan Window uses width `300` instead of `160` like the other forms
// Therefore, the label does not need to be shortened // Therefore, the label does not need to be shortened
const label = addTextObject( const label = addTextObject(
@ -99,12 +90,13 @@ export abstract class FormModalUiHandler extends ModalUiHandler {
); );
label.name = "formLabel" + f; label.name = "formLabel" + f;
this.formLabels.push(label); formLabels[f] = label;
this.modalContainer.add(label); this.modalContainer.add(label);
const inputWidth = label.width < 320 ? 80 : 80 - (label.width - 320) / 5.5; const inputWidth = label.width < 320 ? 80 : 80 - (label.width - 320) / 5.5;
const inputContainer = globalScene.add.container(70 + (80 - inputWidth), (hasTitle ? 28 : 2) + 20 * f); const inputContainer = globalScene.add
inputContainer.setVisible(false); .container(70 + (80 - inputWidth), (hasTitle ? 28 : 2) + 20 * f)
.setVisible(false);
const inputBg = addWindow(0, 0, inputWidth, 16, false, false, 0, 0, WindowVariant.XTHIN); const inputBg = addWindow(0, 0, inputWidth, 16, false, false, 0, 0, WindowVariant.XTHIN);
@ -114,27 +106,27 @@ export abstract class FormModalUiHandler extends ModalUiHandler {
type: isPassword ? "password" : "text", type: isPassword ? "password" : "text",
maxLength: isPassword ? 64 : 20, maxLength: isPassword ? 64 : 20,
readOnly: isReadOnly, readOnly: isReadOnly,
}); }).setOrigin(0);
input.setOrigin(0, 0);
inputContainer.add(inputBg); inputContainer.add([inputBg, input]);
inputContainer.add(input);
this.inputContainers.push(inputContainer); inputContainers[f] = inputContainer;
this.modalContainer.add(inputContainer); this.modalContainer.add(inputContainer);
this.inputs.push(input); inputs[f] = input;
}); }
} }
override show(args: any[]): boolean { override show(args: any[]): boolean {
if (super.show(args)) { if (super.show(args)) {
this.inputContainers.map(ic => ic.setVisible(true)); for (const ic of this.inputContainers) {
ic.setActive(true).setVisible(true);
}
const config = args[0] as FormModalConfig; const config = args[0] as FormModalConfig;
const buttonActions = config.buttonActions ?? [];
this.submitAction = config.buttonActions.length > 0 ? config.buttonActions[0] : null; [this.submitAction, this.cancelAction] = buttonActions;
this.cancelAction = config.buttonActions[1] ?? null;
// Auto focus the first input field after a short delay, to prevent accidental inputs // Auto focus the first input field after a short delay, to prevent accidental inputs
setTimeout(() => { setTimeout(() => {
@ -146,26 +138,24 @@ export abstract class FormModalUiHandler extends ModalUiHandler {
// properties that we set above, allowing their behavior to change after this method terminates // properties that we set above, allowing their behavior to change after this method terminates
// Some subclasses use this to add behavior to the submit and cancel action // Some subclasses use this to add behavior to the submit and cancel action
this.buttonBgs[0].off("pointerdown"); this.buttonBgs[0] // formatting
this.buttonBgs[0].on("pointerdown", () => { .off("pointerdown")
if (this.submitAction && globalScene.tweens.getTweensOf(this.modalContainer).length === 0) { .on("pointerdown", () => {
this.submitAction(); if (this.submitAction && globalScene.tweens.getTweensOf(this.modalContainer).length === 0) {
} this.submitAction();
}); }
const cancelBg = this.buttonBgs[1]; });
if (cancelBg) { this.buttonBgs[1] // formatting
cancelBg.off("pointerdown"); ?.off("pointerdown")
cancelBg.on("pointerdown", () => { .on("pointerdown", () => {
// The seemingly redundant cancelAction check is intentionally left in as a defensive programming measure // The seemingly redundant cancelAction check is intentionally left in as a defensive programming measure
if (this.cancelAction && globalScene.tweens.getTweensOf(this.modalContainer).length === 0) { if (this.cancelAction && globalScene.tweens.getTweensOf(this.modalContainer).length === 0) {
this.cancelAction(); this.cancelAction();
} }
}); });
}
//#endregion: Override pointerDown events //#endregion: Override pointerDown events
this.modalContainer.y += 24; this.modalContainer.setAlpha(0).y += 24;
this.modalContainer.setAlpha(0);
this.tween = globalScene.tweens.add({ this.tween = globalScene.tweens.add({
targets: this.modalContainer, targets: this.modalContainer,
@ -199,21 +189,37 @@ export abstract class FormModalUiHandler extends ModalUiHandler {
updateContainer(config?: ModalConfig): void { updateContainer(config?: ModalConfig): void {
super.updateContainer(config); super.updateContainer(config);
this.errorMessage.setText(this.getReadableErrorMessage((config as FormModalConfig)?.errorMessage || "")); this.errorMessage
this.errorMessage.setVisible(!!this.errorMessage.text); .setText(this.getReadableErrorMessage((config as FormModalConfig)?.errorMessage || ""))
.setVisible(!!this.errorMessage.text);
}
hide(): void {
this.modalContainer.setVisible(false).setActive(false);
for (const ic of this.inputContainers) {
ic.setVisible(false).setActive(false);
}
}
unhide(): void {
this.modalContainer.setActive(true).setVisible(true);
for (const ic of this.inputContainers) {
ic.setActive(true).setVisible(true);
}
} }
clear(): void { clear(): void {
super.clear(); super.clear();
this.modalContainer.setVisible(false); this.modalContainer.setVisible(false);
this.inputContainers.map(ic => ic.setVisible(false)); for (const ic of this.inputContainers) {
ic.setVisible(false).setActive(false);
this.submitAction = null;
if (this.tween) {
this.tween.remove();
} }
this.submitAction = undefined;
this.tween?.remove().destroy();
this.tween = undefined;
} }
} }

View File

@ -7,6 +7,7 @@ import { PlayerGender } from "#enums/player-gender";
import { TextStyle } from "#enums/text-style"; import { TextStyle } from "#enums/text-style";
import { UiTheme } from "#enums/ui-theme"; import { UiTheme } from "#enums/ui-theme";
import type { GameData } from "#system/game-data"; import type { GameData } from "#system/game-data";
import type { AnyFn } from "#types/type-helpers";
import { addTextObject } from "#ui/text"; import { addTextObject } from "#ui/text";
import { UiHandler } from "#ui/ui-handler"; import { UiHandler } from "#ui/ui-handler";
import { addWindow } from "#ui/ui-theme"; import { addWindow } from "#ui/ui-theme";
@ -239,6 +240,12 @@ export class GameStatsUiHandler extends UiHandler {
/** Logged in username */ /** Logged in username */
private headerText: Phaser.GameObjects.Text; private headerText: Phaser.GameObjects.Text;
/** The game data to display */
private gameData: GameData;
/** A callback invoked when {@linkcode clear} is called */
private exitCallback?: AnyFn | undefined;
/** Whether the UI is single column mode */ /** Whether the UI is single column mode */
private get singleCol(): boolean { private get singleCol(): boolean {
const resolvedLang = i18next.resolvedLanguage ?? "en"; const resolvedLang = i18next.resolvedLanguage ?? "en";
@ -318,9 +325,9 @@ export class GameStatsUiHandler extends UiHandler {
? i18next.t("trainerNames:playerF") ? i18next.t("trainerNames:playerF")
: i18next.t("trainerNames:playerM"); : i18next.t("trainerNames:playerM");
const displayName = !globalScene.hideUsername const displayName = globalScene.hideUsername
? (loggedInUser?.username ?? i18next.t("common:guest")) ? usernameReplacement
: usernameReplacement; : (loggedInUser?.username ?? i18next.t("common:guest"));
return i18next.t("gameStatsUiHandler:stats", { username: displayName }); return i18next.t("gameStatsUiHandler:stats", { username: displayName });
} }
@ -395,11 +402,19 @@ export class GameStatsUiHandler extends UiHandler {
this.gameStatsContainer.setVisible(false); this.gameStatsContainer.setVisible(false);
} }
show(args: any[]): boolean { show([username, data, callback]: [] | [username: string, data: GameData, callback?: AnyFn]): boolean {
super.show(args); super.show([]);
// show updated username on every render if (username != null && data != null) {
this.headerText.setText(this.getUsername()); this.gameData = data;
this.exitCallback = callback;
this.headerText.setText(username);
} else {
this.gameData = globalScene.gameData;
this.exitCallback = undefined;
// show updated username on every render
this.headerText.setText(this.getUsername());
}
this.gameStatsContainer.setActive(true).setVisible(true); this.gameStatsContainer.setActive(true).setVisible(true);
@ -436,7 +451,7 @@ export class GameStatsUiHandler extends UiHandler {
const statKeys = Object.keys(displayStats).slice(this.cursor * columns, this.cursor * columns + perPage); const statKeys = Object.keys(displayStats).slice(this.cursor * columns, this.cursor * columns + perPage);
statKeys.forEach((key, s) => { statKeys.forEach((key, s) => {
const stat = displayStats[key] as DisplayStat; const stat = displayStats[key] as DisplayStat;
const value = stat.sourceFunc?.(globalScene.gameData) ?? "-"; const value = stat.sourceFunc?.(this.gameData) ?? "-";
const valAsInt = Number.parseInt(value); const valAsInt = Number.parseInt(value);
this.statLabels[s].setText( this.statLabels[s].setText(
!stat.hidden || Number.isNaN(value) || valAsInt ? i18next.t(`gameStatsUiHandler:${stat.label_key}`) : "???", !stat.hidden || Number.isNaN(value) || valAsInt ? i18next.t(`gameStatsUiHandler:${stat.label_key}`) : "???",
@ -512,6 +527,12 @@ export class GameStatsUiHandler extends UiHandler {
clear() { clear() {
super.clear(); super.clear();
this.gameStatsContainer.setVisible(false).setActive(false); this.gameStatsContainer.setVisible(false).setActive(false);
const callback = this.exitCallback;
if (callback != null) {
this.exitCallback = undefined;
callback();
}
} }
} }

View File

@ -3,6 +3,7 @@ import { loggedInUser, updateUserInfo } from "#app/account";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { bypassLogin } from "#app/global-vars/bypass-login"; import { bypassLogin } from "#app/global-vars/bypass-login";
import { handleTutorial, Tutorial } from "#app/tutorial"; import { handleTutorial, Tutorial } from "#app/tutorial";
import { AdminMode, getAdminModeName } from "#enums/admin-mode";
import { Button } from "#enums/buttons"; import { Button } from "#enums/buttons";
import { GameDataType } from "#enums/game-data-type"; import { GameDataType } from "#enums/game-data-type";
import { TextStyle } from "#enums/text-style"; import { TextStyle } from "#enums/text-style";
@ -19,7 +20,6 @@ import { getEnumValues } from "#utils/enums";
import { toCamelCase } from "#utils/strings"; import { toCamelCase } from "#utils/strings";
import { isBeta } from "#utils/utility-vars"; import { isBeta } from "#utils/utility-vars";
import i18next from "i18next"; import i18next from "i18next";
import { AdminMode, getAdminModeName } from "./admin-ui-handler";
enum MenuOptions { enum MenuOptions {
GAME_SETTINGS, GAME_SETTINGS,
@ -452,7 +452,7 @@ export class MenuUiHandler extends MessageUiHandler {
keepOpen: true, keepOpen: true,
}, },
]; ];
if (!bypassLogin && loggedInUser?.hasAdminRole) { if (bypassLogin || loggedInUser?.hasAdminRole) {
communityOptions.push({ communityOptions.push({
label: "Admin", label: "Admin",
handler: () => { handler: () => {

View File

@ -182,6 +182,32 @@ export abstract class ModalUiHandler extends UiHandler {
} }
} }
hideLastButtons(hideCount = 0) {
const visibleCount = this.buttonBgs.length - hideCount;
const totalButtonWidth = this.buttonBgs.slice(0, visibleCount).reduce((sum, bg) => sum + bg.width, 0);
// Clamping the button spacing between 2 and 12
// Dividing by visibleCount rather than visibleCount-1 to leave space at the edge
// -8 is to take the border of the background into account
const spacing = Math.max(2, Math.min(12, (this.modalBg.width - 8 - totalButtonWidth) / visibleCount));
const totalVisibleWidth = totalButtonWidth + spacing * Math.max(visibleCount - 1, 0);
let x = (this.modalBg.width - totalVisibleWidth) / 2;
this.buttonContainers.forEach((container, i) => {
const visible = i < visibleCount;
container.setActive(visible).setVisible(visible);
if (visible) {
container.setPosition(x + this.buttonBgs[i].width / 2, this.modalBg.height - (this.buttonBgs[i].height + 8));
x += this.buttonBgs[i].width + spacing;
}
});
}
processInput(_button: Button): boolean { processInput(_button: Button): boolean {
return false; return false;
} }

View File

@ -31,9 +31,11 @@ import { UiMode } from "#enums/ui-mode";
import { UiTheme } from "#enums/ui-theme"; import { UiTheme } from "#enums/ui-theme";
import type { Variant } from "#sprites/variant"; import type { Variant } from "#sprites/variant";
import { getVariantIcon, getVariantTint } from "#sprites/variant"; import { getVariantIcon, getVariantTint } from "#sprites/variant";
import type { GameData } from "#system/game-data";
import { SettingKeyboard } from "#system/settings-keyboard"; import { SettingKeyboard } from "#system/settings-keyboard";
import type { DexEntry } from "#types/dex-data"; import type { DexEntry } from "#types/dex-data";
import type { DexAttrProps, StarterAttributes } from "#types/save-data"; import type { DexAttrProps, StarterAttributes } from "#types/save-data";
import type { AnyFn } from "#types/type-helpers";
import type { OptionSelectConfig } from "#ui/abstract-option-select-ui-handler"; import type { OptionSelectConfig } from "#ui/abstract-option-select-ui-handler";
import { DropDown, DropDownLabel, DropDownOption, DropDownState, DropDownType, SortCriteria } from "#ui/dropdown"; import { DropDown, DropDownLabel, DropDownOption, DropDownState, DropDownType, SortCriteria } from "#ui/dropdown";
import { FilterBar } from "#ui/filter-bar"; import { FilterBar } from "#ui/filter-bar";
@ -237,6 +239,10 @@ export class PokedexUiHandler extends MessageUiHandler {
private canShowFormTray: boolean; private canShowFormTray: boolean;
private filteredIndices: SpeciesId[]; private filteredIndices: SpeciesId[];
private gameData: GameData;
private exitCallback?: AnyFn;
private blockOpenPage = false;
constructor() { constructor() {
super(UiMode.POKEDEX); super(UiMode.POKEDEX);
} }
@ -642,8 +648,15 @@ export class PokedexUiHandler extends MessageUiHandler {
this.pokerusSpecies = getPokerusStarters(); this.pokerusSpecies = getPokerusStarters();
// When calling with "refresh", we do not reset the cursor and filters // When calling with "refresh", we do not reset the cursor and filters
if (args.length > 0 && args[0] === "refresh") { if (args.length > 0) {
return false; if (args[0] === "refresh") {
return false;
}
[this.gameData, this.exitCallback] = args;
this.blockOpenPage = true;
} else {
this.gameData = globalScene.gameData;
this.blockOpenPage = false;
} }
super.show(args); super.show(args);
@ -685,8 +698,8 @@ export class PokedexUiHandler extends MessageUiHandler {
*/ */
initStarterPrefs(species: PokemonSpecies): StarterAttributes { initStarterPrefs(species: PokemonSpecies): StarterAttributes {
const starterAttributes = this.starterPreferences[species.speciesId]; const starterAttributes = this.starterPreferences[species.speciesId];
const dexEntry = globalScene.gameData.dexData[species.speciesId]; const dexEntry = this.gameData.dexData[species.speciesId];
const starterData = globalScene.gameData.starterData[species.speciesId]; const starterData = this.gameData.starterData[species.speciesId];
// no preferences or Pokemon wasn't caught, return empty attribute // no preferences or Pokemon wasn't caught, return empty attribute
if (!starterAttributes || !dexEntry.caughtAttr) { if (!starterAttributes || !dexEntry.caughtAttr) {
@ -753,15 +766,14 @@ export class PokedexUiHandler extends MessageUiHandler {
const selectedForm = starterAttributes.form; const selectedForm = starterAttributes.form;
if ( if (
selectedForm !== undefined selectedForm !== undefined
&& (!species.forms[selectedForm]?.isStarterSelectable && (!species.forms[selectedForm]?.isStarterSelectable || !(caughtAttr & this.gameData.getFormAttr(selectedForm)))
|| !(caughtAttr & globalScene.gameData.getFormAttr(selectedForm)))
) { ) {
// requested form wasn't unlocked/isn't a starter form, purging setting // requested form wasn't unlocked/isn't a starter form, purging setting
starterAttributes.form = undefined; starterAttributes.form = undefined;
} }
if (starterAttributes.nature !== undefined) { if (starterAttributes.nature !== undefined) {
const unlockedNatures = globalScene.gameData.getNaturesForAttr(dexEntry.natureAttr); const unlockedNatures = this.gameData.getNaturesForAttr(dexEntry.natureAttr);
if (unlockedNatures.indexOf(starterAttributes.nature as unknown as Nature) < 0) { if (unlockedNatures.indexOf(starterAttributes.nature as unknown as Nature) < 0) {
// requested nature wasn't unlocked, purging setting // requested nature wasn't unlocked, purging setting
starterAttributes.nature = undefined; starterAttributes.nature = undefined;
@ -812,7 +824,7 @@ export class PokedexUiHandler extends MessageUiHandler {
return true; return true;
} }
if (!seenFilter) { if (!seenFilter) {
const starterDexEntry = globalScene.gameData.dexData[this.getStarterSpeciesId(species.speciesId)]; const starterDexEntry = this.gameData.dexData[this.getStarterSpeciesId(species.speciesId)];
return !!starterDexEntry?.caughtAttr; return !!starterDexEntry?.caughtAttr;
} }
return false; return false;
@ -851,7 +863,7 @@ export class PokedexUiHandler extends MessageUiHandler {
*/ */
isPassiveAvailable(speciesId: number): boolean { isPassiveAvailable(speciesId: number): boolean {
// Get this species ID's starter data // Get this species ID's starter data
const starterData = globalScene.gameData.starterData[this.getStarterSpeciesId(speciesId)]; const starterData = this.gameData.starterData[this.getStarterSpeciesId(speciesId)];
return ( return (
starterData.candyCount >= getPassiveCandyCount(speciesStarterCosts[this.getStarterSpeciesId(speciesId)]) starterData.candyCount >= getPassiveCandyCount(speciesStarterCosts[this.getStarterSpeciesId(speciesId)])
@ -866,7 +878,7 @@ export class PokedexUiHandler extends MessageUiHandler {
*/ */
isValueReductionAvailable(speciesId: number): boolean { isValueReductionAvailable(speciesId: number): boolean {
// Get this species ID's starter data // Get this species ID's starter data
const starterData = globalScene.gameData.starterData[this.getStarterSpeciesId(speciesId)]; const starterData = this.gameData.starterData[this.getStarterSpeciesId(speciesId)];
return ( return (
starterData.candyCount starterData.candyCount
@ -883,7 +895,7 @@ export class PokedexUiHandler extends MessageUiHandler {
*/ */
isSameSpeciesEggAvailable(speciesId: number): boolean { isSameSpeciesEggAvailable(speciesId: number): boolean {
// Get this species ID's starter data // Get this species ID's starter data
const starterData = globalScene.gameData.starterData[this.getStarterSpeciesId(speciesId)]; const starterData = this.gameData.starterData[this.getStarterSpeciesId(speciesId)];
return ( return (
starterData.candyCount >= getSameSpeciesEggCandyCounts(speciesStarterCosts[this.getStarterSpeciesId(speciesId)]) starterData.candyCount >= getSameSpeciesEggCandyCounts(speciesStarterCosts[this.getStarterSpeciesId(speciesId)])
@ -1161,8 +1173,13 @@ export class PokedexUiHandler extends MessageUiHandler {
} else if (this.showingTray) { } else if (this.showingTray) {
if (button === Button.ACTION) { if (button === Button.ACTION) {
const formIndex = this.trayForms[this.trayCursor].formIndex; const formIndex = this.trayForms[this.trayCursor].formIndex;
ui.setOverlayMode(UiMode.POKEDEX_PAGE, this.lastSpecies, { form: formIndex }, this.filteredIndices); if (this.blockOpenPage) {
success = true; success = false;
error = true;
} else {
ui.setOverlayMode(UiMode.POKEDEX_PAGE, this.lastSpecies, { form: formIndex }, this.filteredIndices);
success = true;
}
} else { } else {
const numberOfForms = this.trayContainers.length; const numberOfForms = this.trayContainers.length;
const numOfRows = Math.ceil(numberOfForms / maxColumns); const numOfRows = Math.ceil(numberOfForms / maxColumns);
@ -1209,8 +1226,13 @@ export class PokedexUiHandler extends MessageUiHandler {
} }
} }
} else if (button === Button.ACTION) { } else if (button === Button.ACTION) {
ui.setOverlayMode(UiMode.POKEDEX_PAGE, this.lastSpecies, null, this.filteredIndices); if (this.blockOpenPage) {
success = true; success = false;
error = true;
} else {
ui.setOverlayMode(UiMode.POKEDEX_PAGE, this.lastSpecies, null, this.filteredIndices);
success = true;
}
} else { } else {
switch (button) { switch (button) {
case Button.UP: case Button.UP:
@ -1372,21 +1394,21 @@ export class PokedexUiHandler extends MessageUiHandler {
const starterId = this.getStarterSpeciesId(species.speciesId); const starterId = this.getStarterSpeciesId(species.speciesId);
const currentDexAttr = this.getCurrentDexProps(species.speciesId); const currentDexAttr = this.getCurrentDexProps(species.speciesId);
const props = this.getSanitizedProps(globalScene.gameData.getSpeciesDexAttrProps(species, currentDexAttr)); const props = this.getSanitizedProps(this.gameData.getSpeciesDexAttrProps(species, currentDexAttr));
const data: ContainerData = { const data: ContainerData = {
species, species,
cost: globalScene.gameData.getSpeciesStarterValue(starterId), cost: this.gameData.getSpeciesStarterValue(starterId),
props, props,
}; };
// First, ensure you have the caught attributes for the species else default to bigint 0 // First, ensure you have the caught attributes for the species else default to bigint 0
// TODO: This might be removed depending on how accessible we want the pokedex function to be // TODO: This might be removed depending on how accessible we want the pokedex function to be
const caughtAttr = const caughtAttr =
(globalScene.gameData.dexData[species.speciesId]?.caughtAttr || BigInt(0)) (this.gameData.dexData[species.speciesId]?.caughtAttr || BigInt(0))
& (globalScene.gameData.dexData[this.getStarterSpeciesId(species.speciesId)]?.caughtAttr || BigInt(0)) & (this.gameData.dexData[this.getStarterSpeciesId(species.speciesId)]?.caughtAttr || BigInt(0))
& species.getFullUnlocksData(); & species.getFullUnlocksData();
const starterData = globalScene.gameData.starterData[starterId]; const starterData = this.gameData.starterData[starterId];
const isStarterProgressable = speciesEggMoves.hasOwnProperty(starterId); const isStarterProgressable = speciesEggMoves.hasOwnProperty(starterId);
// Name filter // Name filter
@ -1635,7 +1657,7 @@ export class PokedexUiHandler extends MessageUiHandler {
}); });
// Seen Filter // Seen Filter
const dexEntry = globalScene.gameData.dexData[species.speciesId]; const dexEntry = this.gameData.dexData[species.speciesId];
const isItSeen = this.isSeen(species, dexEntry, true) || !!dexEntry.caughtAttr; const isItSeen = this.isSeen(species, dexEntry, true) || !!dexEntry.caughtAttr;
const fitsSeen = this.filterBar.getVals(DropDownColumn.MISC).some(misc => { const fitsSeen = this.filterBar.getVals(DropDownColumn.MISC).some(misc => {
if (misc.val === "SEEN_SPECIES" && misc.state === DropDownState.ON) { if (misc.val === "SEEN_SPECIES" && misc.state === DropDownState.ON) {
@ -1725,33 +1747,31 @@ export class PokedexUiHandler extends MessageUiHandler {
case SortCriteria.COST: case SortCriteria.COST:
return (a.cost - b.cost) * -sort.dir; return (a.cost - b.cost) * -sort.dir;
case SortCriteria.CANDY: { case SortCriteria.CANDY: {
const candyCountA = const candyCountA = this.gameData.starterData[this.getStarterSpeciesId(a.species.speciesId)].candyCount;
globalScene.gameData.starterData[this.getStarterSpeciesId(a.species.speciesId)].candyCount; const candyCountB = this.gameData.starterData[this.getStarterSpeciesId(b.species.speciesId)].candyCount;
const candyCountB =
globalScene.gameData.starterData[this.getStarterSpeciesId(b.species.speciesId)].candyCount;
return (candyCountA - candyCountB) * -sort.dir; return (candyCountA - candyCountB) * -sort.dir;
} }
case SortCriteria.IV: { case SortCriteria.IV: {
const avgIVsA = const avgIVsA =
globalScene.gameData.dexData[a.species.speciesId].ivs.reduce((a, b) => a + b, 0) this.gameData.dexData[a.species.speciesId].ivs.reduce((a, b) => a + b, 0)
/ globalScene.gameData.dexData[a.species.speciesId].ivs.length; / this.gameData.dexData[a.species.speciesId].ivs.length;
const avgIVsB = const avgIVsB =
globalScene.gameData.dexData[b.species.speciesId].ivs.reduce((a, b) => a + b, 0) this.gameData.dexData[b.species.speciesId].ivs.reduce((a, b) => a + b, 0)
/ globalScene.gameData.dexData[b.species.speciesId].ivs.length; / this.gameData.dexData[b.species.speciesId].ivs.length;
return (avgIVsA - avgIVsB) * -sort.dir; return (avgIVsA - avgIVsB) * -sort.dir;
} }
case SortCriteria.NAME: case SortCriteria.NAME:
return a.species.name.localeCompare(b.species.name) * -sort.dir; return a.species.name.localeCompare(b.species.name) * -sort.dir;
case SortCriteria.CAUGHT: case SortCriteria.CAUGHT:
return ( return (
(globalScene.gameData.dexData[a.species.speciesId].caughtCount (this.gameData.dexData[a.species.speciesId].caughtCount
- globalScene.gameData.dexData[b.species.speciesId].caughtCount) - this.gameData.dexData[b.species.speciesId].caughtCount)
* -sort.dir * -sort.dir
); );
case SortCriteria.HATCHED: case SortCriteria.HATCHED:
return ( return (
(globalScene.gameData.dexData[this.getStarterSpeciesId(a.species.speciesId)].hatchedCount (this.gameData.dexData[this.getStarterSpeciesId(a.species.speciesId)].hatchedCount
- globalScene.gameData.dexData[this.getStarterSpeciesId(b.species.speciesId)].hatchedCount) - this.gameData.dexData[this.getStarterSpeciesId(b.species.speciesId)].hatchedCount)
* -sort.dir * -sort.dir
); );
default: default:
@ -1795,10 +1815,10 @@ export class PokedexUiHandler extends MessageUiHandler {
container.checkIconId(props.female, props.formIndex, props.shiny, props.variant); container.checkIconId(props.female, props.formIndex, props.shiny, props.variant);
const speciesId = data.species.speciesId; const speciesId = data.species.speciesId;
const dexEntry = globalScene.gameData.dexData[speciesId]; const dexEntry = this.gameData.dexData[speciesId];
const caughtAttr = const caughtAttr =
dexEntry.caughtAttr dexEntry.caughtAttr
& globalScene.gameData.dexData[this.getStarterSpeciesId(speciesId)].caughtAttr & this.gameData.dexData[this.getStarterSpeciesId(speciesId)].caughtAttr
& data.species.getFullUnlocksData(); & data.species.getFullUnlocksData();
if (caughtAttr & data.species.getFullUnlocksData() || globalScene.dexForDevs) { if (caughtAttr & data.species.getFullUnlocksData() || globalScene.dexForDevs) {
@ -1857,13 +1877,13 @@ export class PokedexUiHandler extends MessageUiHandler {
} }
container.starterPassiveBgs.setVisible( container.starterPassiveBgs.setVisible(
!!globalScene.gameData.starterData[this.getStarterSpeciesId(speciesId)].passiveAttr, !!this.gameData.starterData[this.getStarterSpeciesId(speciesId)].passiveAttr,
); );
container.hiddenAbilityIcon.setVisible( container.hiddenAbilityIcon.setVisible(
!!caughtAttr && !!(globalScene.gameData.starterData[this.getStarterSpeciesId(speciesId)].abilityAttr & 4), !!caughtAttr && !!(this.gameData.starterData[this.getStarterSpeciesId(speciesId)].abilityAttr & 4),
); );
container.classicWinIcon.setVisible( container.classicWinIcon.setVisible(
globalScene.gameData.starterData[this.getStarterSpeciesId(speciesId)].classicWinCount > 0, this.gameData.starterData[this.getStarterSpeciesId(speciesId)].classicWinCount > 0,
); );
container.favoriteIcon.setVisible(this.starterPreferences[speciesId]?.favorite ?? false); container.favoriteIcon.setVisible(this.starterPreferences[speciesId]?.favorite ?? false);
@ -1989,15 +2009,15 @@ export class PokedexUiHandler extends MessageUiHandler {
this.formTrayContainer.setX((goLeft ? boxPos.x - 18 * (this.trayColumns - spaceRight) : boxPos.x) - 3); this.formTrayContainer.setX((goLeft ? boxPos.x - 18 * (this.trayColumns - spaceRight) : boxPos.x) - 3);
this.formTrayContainer.setY(goUp ? boxPos.y - this.trayBg.height : boxPos.y + 17); this.formTrayContainer.setY(goUp ? boxPos.y - this.trayBg.height : boxPos.y + 17);
const dexEntry = globalScene.gameData.dexData[species.speciesId]; const dexEntry = this.gameData.dexData[species.speciesId];
const dexAttr = this.getCurrentDexProps(species.speciesId); const dexAttr = this.getCurrentDexProps(species.speciesId);
const props = this.getSanitizedProps(globalScene.gameData.getSpeciesDexAttrProps(this.lastSpecies, dexAttr)); const props = this.getSanitizedProps(this.gameData.getSpeciesDexAttrProps(this.lastSpecies, dexAttr));
this.trayContainers = []; this.trayContainers = [];
const isFormSeen = this.isSeen(species, dexEntry); const isFormSeen = this.isSeen(species, dexEntry);
this.trayForms.map((f, index) => { this.trayForms.map((f, index) => {
const isFormCaught = dexEntry const isFormCaught = dexEntry
? (dexEntry.caughtAttr & species.getFullUnlocksData() & globalScene.gameData.getFormAttr(f.formIndex ?? 0)) > 0n ? (dexEntry.caughtAttr & species.getFullUnlocksData() & this.gameData.getFormAttr(f.formIndex ?? 0)) > 0n
: false; : false;
const formContainer = new PokedexMonContainer(species, { const formContainer = new PokedexMonContainer(species, {
formIndex: f.formIndex, formIndex: f.formIndex,
@ -2066,7 +2086,7 @@ export class PokedexUiHandler extends MessageUiHandler {
} }
getFriendship(speciesId: number) { getFriendship(speciesId: number) {
let currentFriendship = globalScene.gameData.starterData[this.getStarterSpeciesId(speciesId)].friendship; let currentFriendship = this.gameData.starterData[this.getStarterSpeciesId(speciesId)].friendship;
if (!currentFriendship || currentFriendship === undefined) { if (!currentFriendship || currentFriendship === undefined) {
currentFriendship = 0; currentFriendship = 0;
} }
@ -2094,7 +2114,7 @@ export class PokedexUiHandler extends MessageUiHandler {
if (container) { if (container) {
const lastSpeciesIcon = container.icon; const lastSpeciesIcon = container.icon;
const dexAttr = this.getCurrentDexProps(container.species.speciesId); const dexAttr = this.getCurrentDexProps(container.species.speciesId);
const props = this.getSanitizedProps(globalScene.gameData.getSpeciesDexAttrProps(container.species, dexAttr)); const props = this.getSanitizedProps(this.gameData.getSpeciesDexAttrProps(container.species, dexAttr));
this.checkIconId(lastSpeciesIcon, container.species, props.female, props.formIndex, props.shiny, props.variant); this.checkIconId(lastSpeciesIcon, container.species, props.female, props.formIndex, props.shiny, props.variant);
this.iconAnimHandler.addOrUpdate(lastSpeciesIcon, PokemonIconAnimMode.NONE); this.iconAnimHandler.addOrUpdate(lastSpeciesIcon, PokemonIconAnimMode.NONE);
// Resume the animation for the previously selected species // Resume the animation for the previously selected species
@ -2103,7 +2123,7 @@ export class PokedexUiHandler extends MessageUiHandler {
} }
setSpecies(species: PokemonSpecies | null) { setSpecies(species: PokemonSpecies | null) {
this.speciesStarterDexEntry = species ? globalScene.gameData.dexData[species.speciesId] : null; this.speciesStarterDexEntry = species ? this.gameData.dexData[species.speciesId] : null;
if (!species && globalScene.ui.getTooltip().visible) { if (!species && globalScene.ui.getTooltip().visible) {
globalScene.ui.hideTooltip(); globalScene.ui.hideTooltip();
@ -2182,15 +2202,15 @@ export class PokedexUiHandler extends MessageUiHandler {
} }
if (species) { if (species) {
const dexEntry = globalScene.gameData.dexData[species.speciesId]; const dexEntry = this.gameData.dexData[species.speciesId];
const caughtAttr = const caughtAttr =
dexEntry.caughtAttr dexEntry.caughtAttr
& globalScene.gameData.dexData[this.getStarterSpeciesId(species.speciesId)].caughtAttr & this.gameData.dexData[this.getStarterSpeciesId(species.speciesId)].caughtAttr
& species.getFullUnlocksData(); & species.getFullUnlocksData();
if (caughtAttr) { if (caughtAttr) {
const props = this.getSanitizedProps( const props = this.getSanitizedProps(
globalScene.gameData.getSpeciesDexAttrProps(species, this.getCurrentDexProps(species.speciesId)), this.gameData.getSpeciesDexAttrProps(species, this.getCurrentDexProps(species.speciesId)),
); );
if (shiny === undefined) { if (shiny === undefined) {
@ -2207,7 +2227,7 @@ export class PokedexUiHandler extends MessageUiHandler {
} }
} }
const isFormCaught = dexEntry ? (caughtAttr & globalScene.gameData.getFormAttr(formIndex ?? 0)) > 0n : false; const isFormCaught = dexEntry ? (caughtAttr & this.gameData.getFormAttr(formIndex ?? 0)) > 0n : false;
const isFormSeen = this.isSeen(species, dexEntry); const isFormSeen = this.isSeen(species, dexEntry);
const assetLoadCancelled = new BooleanHolder(false); const assetLoadCancelled = new BooleanHolder(false);
@ -2291,7 +2311,7 @@ export class PokedexUiHandler extends MessageUiHandler {
updateStarterValueLabel(starter: PokedexMonContainer): void { updateStarterValueLabel(starter: PokedexMonContainer): void {
const speciesId = starter.species.speciesId; const speciesId = starter.species.speciesId;
const baseStarterValue = speciesStarterCosts[speciesId]; const baseStarterValue = speciesStarterCosts[speciesId];
const starterValue = globalScene.gameData.getSpeciesStarterValue(this.getStarterSpeciesId(speciesId)); const starterValue = this.gameData.getSpeciesStarterValue(this.getStarterSpeciesId(speciesId));
starter.cost = starterValue; starter.cost = starterValue;
let valueStr = starterValue.toString(); let valueStr = starterValue.toString();
if (valueStr.startsWith("0.")) { if (valueStr.startsWith("0.")) {
@ -2356,8 +2376,8 @@ export class PokedexUiHandler extends MessageUiHandler {
let props = 0n; let props = 0n;
const species = allSpecies.find(sp => sp.speciesId === speciesId); const species = allSpecies.find(sp => sp.speciesId === speciesId);
const caughtAttr = const caughtAttr =
globalScene.gameData.dexData[speciesId].caughtAttr this.gameData.dexData[speciesId].caughtAttr
& globalScene.gameData.dexData[this.getStarterSpeciesId(speciesId)].caughtAttr & this.gameData.dexData[this.getStarterSpeciesId(speciesId)].caughtAttr
& (species?.getFullUnlocksData() ?? 0n); & (species?.getFullUnlocksData() ?? 0n);
/* this checks the gender of the pokemon; this works by checking a) that the starter preferences for the species exist, and if so, is it female. If so, it'll add DexAttr.FEMALE to our temp props /* this checks the gender of the pokemon; this works by checking a) that the starter preferences for the species exist, and if so, is it female. If so, it'll add DexAttr.FEMALE to our temp props
@ -2401,7 +2421,7 @@ export class PokedexUiHandler extends MessageUiHandler {
props += BigInt(Math.pow(2, this.starterPreferences[speciesId]?.form)) * DexAttr.DEFAULT_FORM; props += BigInt(Math.pow(2, this.starterPreferences[speciesId]?.form)) * DexAttr.DEFAULT_FORM;
} else { } else {
// Get the first unlocked form // Get the first unlocked form
props += globalScene.gameData.getFormAttr(globalScene.gameData.getFormIndex(caughtAttr)); props += this.gameData.getFormAttr(this.gameData.getFormIndex(caughtAttr));
} }
return props; return props;
@ -2426,6 +2446,13 @@ export class PokedexUiHandler extends MessageUiHandler {
this.starterSelectContainer.setVisible(false); this.starterSelectContainer.setVisible(false);
this.blockInput = false; this.blockInput = false;
// sanitize exit callback so it does not leak into future calls
const exitCallback = this.exitCallback;
if (exitCallback != null) {
this.exitCallback = undefined;
exitCallback();
}
} }
checkIconId( checkIconId(

View File

@ -2,12 +2,10 @@ import { PokerogueAdminApi } from "#api/pokerogue-admin-api";
import { initServerForApiTests } from "#test/test-utils/test-file-initialization"; import { initServerForApiTests } from "#test/test-utils/test-file-initialization";
import { getApiBaseUrl } from "#test/test-utils/test-utils"; import { getApiBaseUrl } from "#test/test-utils/test-utils";
import type { import type {
LinkAccountToDiscordIdRequest, DiscordRequest,
LinkAccountToGoogledIdRequest, GoogleRequest,
SearchAccountRequest, SearchAccountRequest,
SearchAccountResponse, SearchAccountResponse,
UnlinkAccountFromDiscordIdRequest,
UnlinkAccountFromGoogledIdRequest,
} from "#types/api/pokerogue-admin-api"; } from "#types/api/pokerogue-admin-api";
import { HttpResponse, http } from "msw"; import { HttpResponse, http } from "msw";
import type { SetupServerApi } from "msw/node"; import type { SetupServerApi } from "msw/node";
@ -31,7 +29,7 @@ describe("Pokerogue Admin API", () => {
}); });
describe("Link Account to Discord", () => { describe("Link Account to Discord", () => {
const params: LinkAccountToDiscordIdRequest = { const params: DiscordRequest = {
username: "test", username: "test",
discordId: "test-12575756", discordId: "test-12575756",
}; };
@ -39,7 +37,7 @@ describe("Pokerogue Admin API", () => {
it("should return null on SUCCESS", async () => { it("should return null on SUCCESS", async () => {
server.use(http.post(`${apiBase}/admin/account/discordLink`, () => HttpResponse.json(true))); server.use(http.post(`${apiBase}/admin/account/discordLink`, () => HttpResponse.json(true)));
const success = await adminApi.linkAccountToDiscord(params); const success = await adminApi.linkUnlinkRequest("Link", "discord", params);
expect(success).toBeNull(); expect(success).toBeNull();
}); });
@ -47,7 +45,7 @@ describe("Pokerogue Admin API", () => {
it("should return a ERR_GENERIC and report a warning on FAILURE", async () => { it("should return a ERR_GENERIC and report a warning on FAILURE", async () => {
server.use(http.post(`${apiBase}/admin/account/discordLink`, () => new HttpResponse("", { status: 400 }))); server.use(http.post(`${apiBase}/admin/account/discordLink`, () => new HttpResponse("", { status: 400 })));
const success = await adminApi.linkAccountToDiscord(params); const success = await adminApi.linkUnlinkRequest("Link", "discord", params);
expect(success).toBe(adminApi.ERR_GENERIC); expect(success).toBe(adminApi.ERR_GENERIC);
expect(console.warn).toHaveBeenCalledWith("Could not link account with discord!", 400, "Bad Request"); expect(console.warn).toHaveBeenCalledWith("Could not link account with discord!", 400, "Bad Request");
@ -56,7 +54,7 @@ describe("Pokerogue Admin API", () => {
it("should return a ERR_USERNAME_NOT_FOUND and report a warning on 404", async () => { it("should return a ERR_USERNAME_NOT_FOUND and report a warning on 404", async () => {
server.use(http.post(`${apiBase}/admin/account/discordLink`, () => new HttpResponse("", { status: 404 }))); server.use(http.post(`${apiBase}/admin/account/discordLink`, () => new HttpResponse("", { status: 404 })));
const success = await adminApi.linkAccountToDiscord(params); const success = await adminApi.linkUnlinkRequest("Link", "discord", params);
expect(success).toBe(adminApi.ERR_USERNAME_NOT_FOUND); expect(success).toBe(adminApi.ERR_USERNAME_NOT_FOUND);
expect(console.warn).toHaveBeenCalledWith("Could not link account with discord!", 404, "Not Found"); expect(console.warn).toHaveBeenCalledWith("Could not link account with discord!", 404, "Not Found");
@ -65,7 +63,7 @@ describe("Pokerogue Admin API", () => {
it("should return a ERR_GENERIC and report a warning on ERROR", async () => { it("should return a ERR_GENERIC and report a warning on ERROR", async () => {
server.use(http.post(`${apiBase}/admin/account/discordLink`, () => HttpResponse.error())); server.use(http.post(`${apiBase}/admin/account/discordLink`, () => HttpResponse.error()));
const success = await adminApi.linkAccountToDiscord(params); const success = await adminApi.linkUnlinkRequest("Link", "discord", params);
expect(success).toBe(adminApi.ERR_GENERIC); expect(success).toBe(adminApi.ERR_GENERIC);
expect(console.warn).toHaveBeenCalledWith("Could not link account with discord!", expect.any(Error)); expect(console.warn).toHaveBeenCalledWith("Could not link account with discord!", expect.any(Error));
@ -73,7 +71,7 @@ describe("Pokerogue Admin API", () => {
}); });
describe("Unlink Account from Discord", () => { describe("Unlink Account from Discord", () => {
const params: UnlinkAccountFromDiscordIdRequest = { const params: DiscordRequest = {
username: "test", username: "test",
discordId: "test-12575756", discordId: "test-12575756",
}; };
@ -81,7 +79,7 @@ describe("Pokerogue Admin API", () => {
it("should return null on SUCCESS", async () => { it("should return null on SUCCESS", async () => {
server.use(http.post(`${apiBase}/admin/account/discordUnlink`, () => HttpResponse.json(true))); server.use(http.post(`${apiBase}/admin/account/discordUnlink`, () => HttpResponse.json(true)));
const success = await adminApi.unlinkAccountFromDiscord(params); const success = await adminApi.linkUnlinkRequest("Unlink", "discord", params);
expect(success).toBeNull(); expect(success).toBeNull();
}); });
@ -89,7 +87,7 @@ describe("Pokerogue Admin API", () => {
it("should return a ERR_GENERIC and report a warning on FAILURE", async () => { it("should return a ERR_GENERIC and report a warning on FAILURE", async () => {
server.use(http.post(`${apiBase}/admin/account/discordUnlink`, () => new HttpResponse("", { status: 400 }))); server.use(http.post(`${apiBase}/admin/account/discordUnlink`, () => new HttpResponse("", { status: 400 })));
const success = await adminApi.unlinkAccountFromDiscord(params); const success = await adminApi.linkUnlinkRequest("Unlink", "discord", params);
expect(success).toBe(adminApi.ERR_GENERIC); expect(success).toBe(adminApi.ERR_GENERIC);
expect(console.warn).toHaveBeenCalledWith("Could not unlink account from discord!", 400, "Bad Request"); expect(console.warn).toHaveBeenCalledWith("Could not unlink account from discord!", 400, "Bad Request");
@ -98,7 +96,7 @@ describe("Pokerogue Admin API", () => {
it("should return a ERR_USERNAME_NOT_FOUND and report a warning on 404", async () => { it("should return a ERR_USERNAME_NOT_FOUND and report a warning on 404", async () => {
server.use(http.post(`${apiBase}/admin/account/discordUnlink`, () => new HttpResponse("", { status: 404 }))); server.use(http.post(`${apiBase}/admin/account/discordUnlink`, () => new HttpResponse("", { status: 404 })));
const success = await adminApi.unlinkAccountFromDiscord(params); const success = await adminApi.linkUnlinkRequest("Unlink", "discord", params);
expect(success).toBe(adminApi.ERR_USERNAME_NOT_FOUND); expect(success).toBe(adminApi.ERR_USERNAME_NOT_FOUND);
expect(console.warn).toHaveBeenCalledWith("Could not unlink account from discord!", 404, "Not Found"); expect(console.warn).toHaveBeenCalledWith("Could not unlink account from discord!", 404, "Not Found");
@ -107,7 +105,7 @@ describe("Pokerogue Admin API", () => {
it("should return a ERR_GENERIC and report a warning on ERROR", async () => { it("should return a ERR_GENERIC and report a warning on ERROR", async () => {
server.use(http.post(`${apiBase}/admin/account/discordUnlink`, () => HttpResponse.error())); server.use(http.post(`${apiBase}/admin/account/discordUnlink`, () => HttpResponse.error()));
const success = await adminApi.unlinkAccountFromDiscord(params); const success = await adminApi.linkUnlinkRequest("Unlink", "discord", params);
expect(success).toBe(adminApi.ERR_GENERIC); expect(success).toBe(adminApi.ERR_GENERIC);
expect(console.warn).toHaveBeenCalledWith("Could not unlink account from discord!", expect.any(Error)); expect(console.warn).toHaveBeenCalledWith("Could not unlink account from discord!", expect.any(Error));
@ -115,7 +113,7 @@ describe("Pokerogue Admin API", () => {
}); });
describe("Link Account to Google", () => { describe("Link Account to Google", () => {
const params: LinkAccountToGoogledIdRequest = { const params: GoogleRequest = {
username: "test", username: "test",
googleId: "test-12575756", googleId: "test-12575756",
}; };
@ -123,7 +121,7 @@ describe("Pokerogue Admin API", () => {
it("should return null on SUCCESS", async () => { it("should return null on SUCCESS", async () => {
server.use(http.post(`${apiBase}/admin/account/googleLink`, () => HttpResponse.json(true))); server.use(http.post(`${apiBase}/admin/account/googleLink`, () => HttpResponse.json(true)));
const success = await adminApi.linkAccountToGoogleId(params); const success = await adminApi.linkUnlinkRequest("Link", "google", params);
expect(success).toBeNull(); expect(success).toBeNull();
}); });
@ -131,7 +129,7 @@ describe("Pokerogue Admin API", () => {
it("should return a ERR_GENERIC and report a warning on FAILURE", async () => { it("should return a ERR_GENERIC and report a warning on FAILURE", async () => {
server.use(http.post(`${apiBase}/admin/account/googleLink`, () => new HttpResponse("", { status: 400 }))); server.use(http.post(`${apiBase}/admin/account/googleLink`, () => new HttpResponse("", { status: 400 })));
const success = await adminApi.linkAccountToGoogleId(params); const success = await adminApi.linkUnlinkRequest("Link", "google", params);
expect(success).toBe(adminApi.ERR_GENERIC); expect(success).toBe(adminApi.ERR_GENERIC);
expect(console.warn).toHaveBeenCalledWith("Could not link account with google!", 400, "Bad Request"); expect(console.warn).toHaveBeenCalledWith("Could not link account with google!", 400, "Bad Request");
@ -140,7 +138,7 @@ describe("Pokerogue Admin API", () => {
it("should return a ERR_USERNAME_NOT_FOUND and report a warning on 404", async () => { it("should return a ERR_USERNAME_NOT_FOUND and report a warning on 404", async () => {
server.use(http.post(`${apiBase}/admin/account/googleLink`, () => new HttpResponse("", { status: 404 }))); server.use(http.post(`${apiBase}/admin/account/googleLink`, () => new HttpResponse("", { status: 404 })));
const success = await adminApi.linkAccountToGoogleId(params); const success = await adminApi.linkUnlinkRequest("Link", "google", params);
expect(success).toBe(adminApi.ERR_USERNAME_NOT_FOUND); expect(success).toBe(adminApi.ERR_USERNAME_NOT_FOUND);
expect(console.warn).toHaveBeenCalledWith("Could not link account with google!", 404, "Not Found"); expect(console.warn).toHaveBeenCalledWith("Could not link account with google!", 404, "Not Found");
@ -149,7 +147,7 @@ describe("Pokerogue Admin API", () => {
it("should return a ERR_GENERIC and report a warning on ERROR", async () => { it("should return a ERR_GENERIC and report a warning on ERROR", async () => {
server.use(http.post(`${apiBase}/admin/account/googleLink`, () => HttpResponse.error())); server.use(http.post(`${apiBase}/admin/account/googleLink`, () => HttpResponse.error()));
const success = await adminApi.linkAccountToGoogleId(params); const success = await adminApi.linkUnlinkRequest("Link", "google", params);
expect(success).toBe(adminApi.ERR_GENERIC); expect(success).toBe(adminApi.ERR_GENERIC);
expect(console.warn).toHaveBeenCalledWith("Could not link account with google!", expect.any(Error)); expect(console.warn).toHaveBeenCalledWith("Could not link account with google!", expect.any(Error));
@ -157,7 +155,7 @@ describe("Pokerogue Admin API", () => {
}); });
describe("Unlink Account from Google", () => { describe("Unlink Account from Google", () => {
const params: UnlinkAccountFromGoogledIdRequest = { const params: GoogleRequest = {
username: "test", username: "test",
googleId: "test-12575756", googleId: "test-12575756",
}; };
@ -165,7 +163,7 @@ describe("Pokerogue Admin API", () => {
it("should return null on SUCCESS", async () => { it("should return null on SUCCESS", async () => {
server.use(http.post(`${apiBase}/admin/account/googleUnlink`, () => HttpResponse.json(true))); server.use(http.post(`${apiBase}/admin/account/googleUnlink`, () => HttpResponse.json(true)));
const success = await adminApi.unlinkAccountFromGoogleId(params); const success = await adminApi.linkUnlinkRequest("Unlink", "google", params);
expect(success).toBeNull(); expect(success).toBeNull();
}); });
@ -173,7 +171,7 @@ describe("Pokerogue Admin API", () => {
it("should return a ERR_GENERIC and report a warning on FAILURE", async () => { it("should return a ERR_GENERIC and report a warning on FAILURE", async () => {
server.use(http.post(`${apiBase}/admin/account/googleUnlink`, () => new HttpResponse("", { status: 400 }))); server.use(http.post(`${apiBase}/admin/account/googleUnlink`, () => new HttpResponse("", { status: 400 })));
const success = await adminApi.unlinkAccountFromGoogleId(params); const success = await adminApi.linkUnlinkRequest("Unlink", "google", params);
expect(success).toBe(adminApi.ERR_GENERIC); expect(success).toBe(adminApi.ERR_GENERIC);
expect(console.warn).toHaveBeenCalledWith("Could not unlink account from google!", 400, "Bad Request"); expect(console.warn).toHaveBeenCalledWith("Could not unlink account from google!", 400, "Bad Request");
@ -182,7 +180,7 @@ describe("Pokerogue Admin API", () => {
it("should return a ERR_USERNAME_NOT_FOUND and report a warning on 404", async () => { it("should return a ERR_USERNAME_NOT_FOUND and report a warning on 404", async () => {
server.use(http.post(`${apiBase}/admin/account/googleUnlink`, () => new HttpResponse("", { status: 404 }))); server.use(http.post(`${apiBase}/admin/account/googleUnlink`, () => new HttpResponse("", { status: 404 })));
const success = await adminApi.unlinkAccountFromGoogleId(params); const success = await adminApi.linkUnlinkRequest("Unlink", "google", params);
expect(success).toBe(adminApi.ERR_USERNAME_NOT_FOUND); expect(success).toBe(adminApi.ERR_USERNAME_NOT_FOUND);
expect(console.warn).toHaveBeenCalledWith("Could not unlink account from google!", 404, "Not Found"); expect(console.warn).toHaveBeenCalledWith("Could not unlink account from google!", 404, "Not Found");
@ -191,7 +189,7 @@ describe("Pokerogue Admin API", () => {
it("should return a ERR_GENERIC and report a warning on ERROR", async () => { it("should return a ERR_GENERIC and report a warning on ERROR", async () => {
server.use(http.post(`${apiBase}/admin/account/googleUnlink`, () => HttpResponse.error())); server.use(http.post(`${apiBase}/admin/account/googleUnlink`, () => HttpResponse.error()));
const success = await adminApi.unlinkAccountFromGoogleId(params); const success = await adminApi.linkUnlinkRequest("Unlink", "google", params);
expect(success).toBe(adminApi.ERR_GENERIC); expect(success).toBe(adminApi.ERR_GENERIC);
expect(console.warn).toHaveBeenCalledWith("Could not unlink account from google!", expect.any(Error)); expect(console.warn).toHaveBeenCalledWith("Could not unlink account from google!", expect.any(Error));

View File

@ -28,6 +28,7 @@ import type { SelectTargetPhase } from "#phases/select-target-phase";
import { TurnEndPhase } from "#phases/turn-end-phase"; import { TurnEndPhase } from "#phases/turn-end-phase";
import { TurnInitPhase } from "#phases/turn-init-phase"; import { TurnInitPhase } from "#phases/turn-init-phase";
import { TurnStartPhase } from "#phases/turn-start-phase"; import { TurnStartPhase } from "#phases/turn-start-phase";
import { GameData } from "#system/game-data";
import { ErrorInterceptor } from "#test/test-utils/error-interceptor"; import { ErrorInterceptor } from "#test/test-utils/error-interceptor";
import { generateStarters } from "#test/test-utils/game-manager-utils"; import { generateStarters } from "#test/test-utils/game-manager-utils";
import { GameWrapper } from "#test/test-utils/game-wrapper"; import { GameWrapper } from "#test/test-utils/game-wrapper";
@ -451,7 +452,7 @@ export class GameManager {
const dataRaw = fs.readFileSync(path, { encoding: "utf8", flag: "r" }); const dataRaw = fs.readFileSync(path, { encoding: "utf8", flag: "r" });
let dataStr = AES.decrypt(dataRaw, saveKey).toString(enc.Utf8); let dataStr = AES.decrypt(dataRaw, saveKey).toString(enc.Utf8);
dataStr = this.scene.gameData.convertSystemDataStr(dataStr); dataStr = this.scene.gameData.convertSystemDataStr(dataStr);
const systemData = this.scene.gameData.parseSystemData(dataStr); const systemData = GameData.parseSystemData(dataStr);
const valid = !!systemData.dexData && !!systemData.timestamp; const valid = !!systemData.dexData && !!systemData.timestamp;
if (valid) { if (valid) {
await updateUserInfo(); await updateUserInfo();