diff --git a/src/@types/api/pokerogue-admin-api.ts b/src/@types/api/pokerogue-admin-api.ts index 2ee25b560d9..7ec59f8fb6e 100644 --- a/src/@types/api/pokerogue-admin-api.ts +++ b/src/@types/api/pokerogue-admin-api.ts @@ -1,31 +1,32 @@ -export interface LinkAccountToDiscordIdRequest { - 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; -} +import type { SystemSaveData } from "#types/save-data"; export interface SearchAccountRequest { username: string; } +export interface DiscordRequest extends SearchAccountRequest { + discordId: string; +} + +export interface GoogleRequest extends SearchAccountRequest { + googleId: string; +} + export interface SearchAccountResponse { username: string; discordId: string; googleId: string; lastLoggedIn: 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 { + discord: DiscordRequest; + google: GoogleRequest; } diff --git a/src/enums/admin-mode.ts b/src/enums/admin-mode.ts new file mode 100644 index 00000000000..8f0922382d6 --- /dev/null +++ b/src/enums/admin-mode.ts @@ -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 ""; + } +} diff --git a/src/plugins/api/pokerogue-admin-api.ts b/src/plugins/api/pokerogue-admin-api.ts index 7ce4cf8b973..106aa0d2f52 100644 --- a/src/plugins/api/pokerogue-admin-api.ts +++ b/src/plugins/api/pokerogue-admin-api.ts @@ -1,112 +1,47 @@ import { ApiBase } from "#api/api-base"; import type { - LinkAccountToDiscordIdRequest, - LinkAccountToGoogledIdRequest, + AdminUiHandlerService, + AdminUiHandlerServiceMode, + PokerogueAdminApiParams, SearchAccountRequest, SearchAccountResponse, - UnlinkAccountFromDiscordIdRequest, - UnlinkAccountFromGoogledIdRequest, } from "#types/api/pokerogue-admin-api"; export class PokerogueAdminApi extends ApiBase { public readonly ERR_USERNAME_NOT_FOUND: string = "Username not found!"; /** - * Links an account to a discord id. - * @param params The {@linkcode LinkAccountToDiscordIdRequest} to send - * @returns `null` if successful, error message if not + * Link or unlink a third party service to/from a user account + * @param mode - The mode, either "Link" or "Unlink" + * @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 { + const endpoint = "/admin/account/" + service + mode; + const preposition = mode === "Link" ? "with " : "from "; + const errMsg = "Could not " + mode.toLowerCase() + " account " + preposition + service + "!"; try { - const response = await this.doPost("/admin/account/discordLink", params, "form-urlencoded"); + const response = await this.doPost(endpoint, params, "form-urlencoded"); if (response.ok) { 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) { return this.ERR_USERNAME_NOT_FOUND; } } catch (err) { - console.warn("Could not link account with discord!", err); + console.warn(errMsg, err); } 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. * @param params The {@linkcode SearchAccountRequest} to send diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 3ffa7482706..e40fc141a0c 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -146,12 +146,20 @@ export class GameData { public eggPity: number[]; public unlockPity: number[]; - constructor() { - this.loadSettings(); - this.loadGamepadSettings(); - this.loadMappingConfigs(); - this.trainerId = randInt(65536); - this.secretId = randInt(65536); + /** + * @param fromRaw - If true, will skip initialization of fields that are normally randomized on new game start. Used for the admin panel; default `false` + */ + constructor(fromRaw = false) { + if (fromRaw) { + this.trainerId = 0; + this.secretId = 0; + } else { + this.loadSettings(); + this.loadGamepadSettings(); + this.loadMappingConfigs(); + this.trainerId = randInt(65536); + this.secretId = randInt(65536); + } this.starterData = {}; this.gameStats = new GameStats(); 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 { const { promise, resolve } = Promise.withResolvers(); try { - let systemData = this.parseSystemData(systemDataStr); + let systemData = GameData.parseSystemData(systemDataStr); if (cachedSystemDataStr) { - const cachedSystemData = this.parseSystemData(cachedSystemDataStr); + const cachedSystemData = GameData.parseSystemData(cachedSystemDataStr); if (cachedSystemData.timestamp > systemData.timestamp) { console.debug("Use cached system"); systemData = cachedSystemData; @@ -307,7 +417,9 @@ export class GameData { if (isLocal || isBeta) { try { 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) { console.debug("Attempt to log system data failed:", err); @@ -322,90 +434,7 @@ export class GameData { localStorage.setItem(lsItemKey, ""); } - 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.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; - + this.initParsedSystem(systemData); resolve(true); } catch (err) { console.error(err); @@ -507,7 +536,7 @@ export class GameData { return true; } - parseSystemData(dataStr: string): SystemSaveData { + static parseSystemData(dataStr: string): SystemSaveData { return JSON.parse(dataStr, (k: string, v: any) => { if (k === "gameStats") { return new GameStats(v); @@ -1296,7 +1325,7 @@ export class GameData { : this.getSessionSaveData(); const maxIntAttrValue = 0x80000000; 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? const request = { @@ -1426,7 +1455,7 @@ export class GameData { case GameDataType.SYSTEM: { dataStr = this.convertSystemDataStr(dataStr); 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; break; } diff --git a/src/ui/handlers/admin-ui-handler.ts b/src/ui/handlers/admin-ui-handler.ts index 38420c61010..ce3798fec4d 100644 --- a/src/ui/handlers/admin-ui-handler.ts +++ b/src/ui/handlers/admin-ui-handler.ts @@ -1,33 +1,41 @@ import { pokerogueApi } from "#api/pokerogue-api"; 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 { TextStyle } from "#enums/text-style"; 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 { FormModalUiHandler } from "#ui/form-modal-ui-handler"; import type { ModalConfig } from "#ui/modal-ui-handler"; import { getTextColor } from "#ui/text"; import { toTitleCase } from "#utils/strings"; -type AdminUiHandlerService = "discord" | "google"; -type AdminUiHandlerServiceMode = "Link" | "Unlink"; - export class AdminUiHandler extends FormModalUiHandler { private adminMode: AdminMode; - private adminResult: AdminSearchInfo; + private adminResult: SearchAccountResponse; private config: ModalConfig; + private tempGameData: GameData | null = null; + 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") { return `${toTitleCase(field)} is required`; } return `${toTitleCase(field)} Id is required`; - }; - // returns a string saying whether a username has been successfully linked/unlinked to discord/google - private readonly SUCCESS_SERVICE_MODE = (service: string, mode: string) => { + } + /** @returns "Username and [service] successfully [mode]ed" */ + private static SUCCESS_SERVICE_MODE(service: string, mode: string) { return `Username and ${service} successfully ${mode.toLowerCase()}ed`; - }; + } constructor(mode: UiMode | null = null) { super(mode); @@ -48,50 +56,41 @@ export class AdminUiHandler extends FormModalUiHandler { override getButtonLabels(): string[] { switch (this.adminMode) { case AdminMode.LINK: - return ["Link Account", "Cancel"]; + return ["Link Account", "Cancel", "", ""]; case AdminMode.SEARCH: - return ["Find account", "Cancel"]; + return ["Find account", "Cancel", "", ""]; case AdminMode.ADMIN: - return ["Back to search", "Cancel"]; + return ["Back to search", "Cancel", "Stats", "Pokedex"]; default: - return ["Activate ADMIN", "Cancel"]; + return ["Activate ADMIN", "Cancel", "Stats", "Pokedex"]; } } override getInputFieldConfigs(): InputFieldConfig[] { - const inputFieldConfigs: InputFieldConfig[] = []; switch (this.adminMode) { case AdminMode.LINK: - inputFieldConfigs.push({ label: "Username" }); - inputFieldConfigs.push({ label: "Discord ID" }); - break; + return [{ label: "Username" }, { label: "Discord ID" }]; case AdminMode.SEARCH: - inputFieldConfigs.push({ label: "Username" }); - break; + return [{ label: "Username" }]; 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 - inputFieldConfigs.push({ label: "Username", isReadOnly: true }); - inputFieldConfigs.push({ - label: "Discord ID", - isReadOnly: adminResult.discordId !== "", - }); - inputFieldConfigs.push({ - label: "Google ID", - isReadOnly: adminResult.googleId !== "", - }); - inputFieldConfigs.push({ label: "Last played", isReadOnly: true }); - inputFieldConfigs.push({ label: "Registered", isReadOnly: true }); - break; + return [ + { label: "Username", isReadOnly: true }, + { + label: "Discord ID", + isReadOnly: (this.adminResult?.discordId ?? "") !== "", + }, + { + label: "Google ID", + isReadOnly: (this.adminResult?.googleId ?? "") !== "", + }, + { label: "Last played", isReadOnly: true }, + { label: "Registered", isReadOnly: true }, + ]; } + default: + return []; } - return inputFieldConfigs; } processInput(button: Button): boolean { @@ -126,36 +125,41 @@ export class AdminUiHandler extends FormModalUiHandler { 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 - if (isMessageError) { - this.errorMessage.setColor(getTextColor(TextStyle.SUMMARY_PINK)); - this.errorMessage.setShadowColor(getTextColor(TextStyle.SUMMARY_PINK, true)); - } else { - this.errorMessage.setColor(getTextColor(TextStyle.SUMMARY_GREEN)); - this.errorMessage.setShadowColor(getTextColor(TextStyle.SUMMARY_GREEN, true)); + const msgColor = isMessageError ? TextStyle.SUMMARY_PINK : TextStyle.SUMMARY_GREEN; + + this.errorMessage + .setPosition(10, (hasTitle ? 31 : 5) + 20 * (fields.length - 1) + 16 + this.getButtonTopMargin()) + .setColor(getTextColor(msgColor)) + .setShadowColor(getTextColor(msgColor, true)); + + if (!super.show(args)) { + return false; } - if (super.show(args)) { - this.populateFields(this.adminMode, this.adminResult); - const originalSubmitAction = this.submitAction; - this.submitAction = _ => { - this.submitAction = originalSubmitAction; - const adminSearchResult: AdminSearchInfo = this.convertInputsToAdmin(); // this converts the input texts into a single object for use later - const validFields = this.areFieldsValid(this.adminMode); - 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 ?? "", adminSearchResult, true); - } - globalScene.ui.setMode(UiMode.LOADING, { buttonActions: [] }); - if (this.adminMode === AdminMode.LINK) { + this.hideLastButtons(this.adminMode === AdminMode.ADMIN ? 0 : 2); + + this.populateFields(this.adminMode, this.adminResult); + const originalSubmitAction = this.submitAction; + this.submitAction = () => { + this.submitAction = originalSubmitAction; + const adminSearchResult: SearchAccountResponse = this.convertInputsToAdmin(); // this converts the input texts into a single object for use later + const validFields = this.areFieldsValid(this.adminMode); + 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 ?? "", adminSearchResult, true); + } + globalScene.ui.setMode(UiMode.LOADING, { buttonActions: [] }); + switch (this.adminMode) { + case AdminMode.LINK: this.adminLinkUnlink(adminSearchResult, "discord", "Link") // calls server to link discord .then(response => { if (response.error) { 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 .then(response => { if (response.error) { @@ -163,16 +167,16 @@ export class AdminUiHandler extends FormModalUiHandler { } this.updateAdminPanelInfo(response.adminSearchResult ?? adminSearchResult); // success }); - } else if (this.adminMode === AdminMode.ADMIN) { + break; + case AdminMode.ADMIN: this.updateAdminPanelInfo(adminSearchResult, AdminMode.SEARCH); - } - }; - return true; - } - return false; + break; + } + }; + return true; } - showMessage(message: string, adminResult: AdminSearchInfo, isError: boolean) { + showMessage(message: string, adminResult: SearchAccountResponse, isError: boolean) { globalScene.ui.setMode( UiMode.ADMIN, 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} * 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 * 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) { case AdminMode.LINK: this.inputs[0].setText(adminResult.username); @@ -203,53 +259,7 @@ export class AdminUiHandler extends FormModalUiHandler { this.inputs[0].setText(adminResult.username); break; case AdminMode.ADMIN: - Object.keys(adminResult).forEach((aR, i) => { - 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); - } - }); + this.populateAdminFields(adminResult); break; } } @@ -261,23 +271,23 @@ export class AdminUiHandler extends FormModalUiHandler { // username missing from link panel return { error: true, - errorMessage: this.ERR_REQUIRED_FIELD("username"), + errorMessage: AdminUiHandler.ERR_REQUIRED_FIELD("username"), }; } if (!this.inputs[1].text) { // discordId missing from linking panel return { error: true, - errorMessage: this.ERR_REQUIRED_FIELD("discord"), + errorMessage: AdminUiHandler.ERR_REQUIRED_FIELD("discord"), }; } break; case AdminMode.SEARCH: - if (!this.inputs[0].text) { - // username missing from search panel + if (!this.inputs[0].text && !bypassLogin) { + // username missing from search panel, skip check for local testing return { error: true, - errorMessage: this.ERR_REQUIRED_FIELD("username"), + errorMessage: AdminUiHandler.ERR_REQUIRED_FIELD("username"), }; } break; @@ -286,14 +296,14 @@ export class AdminUiHandler extends FormModalUiHandler { // discordId missing from admin panel return { error: true, - errorMessage: this.ERR_REQUIRED_FIELD(service), + errorMessage: AdminUiHandler.ERR_REQUIRED_FIELD(service), }; } if (!this.inputs[2].text && service === "google") { // googleId missing from admin panel return { error: true, - errorMessage: this.ERR_REQUIRED_FIELD(service), + errorMessage: AdminUiHandler.ERR_REQUIRED_FIELD(service), }; } break; @@ -303,17 +313,32 @@ export class AdminUiHandler extends FormModalUiHandler { }; } - private convertInputsToAdmin(): AdminSearchInfo { + private convertInputsToAdmin(): SearchAccountResponse { + const inputs = this.inputs; return { - username: this.inputs[0]?.node ? this.inputs[0].text : "", - discordId: this.inputs[1]?.node ? this.inputs[1]?.text : "", - googleId: this.inputs[2]?.node ? this.inputs[2]?.text : "", - lastLoggedIn: this.inputs[3]?.node ? this.inputs[3]?.text : "", - registered: this.inputs[4]?.node ? this.inputs[4]?.text : "", + username: inputs[0]?.node ? inputs[0].text : "", + discordId: inputs[1]?.node ? inputs[1]?.text : "", + googleId: inputs[2]?.node ? inputs[2]?.text : "", + lastLoggedIn: inputs[3]?.node ? inputs[3]?.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 { const [adminInfo, errorType] = await pokerogueApi.admin.searchAccount({ 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 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 }; } catch (err) { console.error(err); @@ -331,58 +363,23 @@ export class AdminUiHandler extends FormModalUiHandler { } private async adminLinkUnlink( - adminSearchResult: AdminSearchInfo, + adminSearchResult: SearchAccountResponse, service: AdminUiHandlerService, mode: AdminUiHandlerServiceMode, ) { try { - let errorType: string | null = null; - - if (service === "discord") { - 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); + const error = await pokerogueApi.admin.linkUnlinkRequest(mode, service, adminSearchResult); + if (error != null) { + return { error: true, errorType: error }; } - - 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) { console.error(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; globalScene.ui.setMode( UiMode.ADMIN, @@ -397,6 +394,27 @@ export class AdminUiHandler extends FormModalUiHandler { 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, @@ -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; -} diff --git a/src/ui/handlers/form-modal-ui-handler.ts b/src/ui/handlers/form-modal-ui-handler.ts index 2efd39ca359..19d09c172e2 100644 --- a/src/ui/handlers/form-modal-ui-handler.ts +++ b/src/ui/handlers/form-modal-ui-handler.ts @@ -1,7 +1,7 @@ import { globalScene } from "#app/global-scene"; import { Button } from "#enums/buttons"; 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 { ModalUiHandler } from "#ui/modal-ui-handler"; import { addTextInputObject, addTextObject, getTextColor } from "#ui/text"; @@ -14,23 +14,14 @@ export interface FormModalConfig extends ModalConfig { } export abstract class FormModalUiHandler extends ModalUiHandler { - protected editing: boolean; - protected inputContainers: Phaser.GameObjects.Container[]; - protected inputs: InputText[]; + protected editing = false; + protected inputContainers: Phaser.GameObjects.Container[] = []; + protected inputs: InputText[] = []; protected errorMessage: Phaser.GameObjects.Text; - protected submitAction: Function | null; - protected cancelAction: (() => void) | null; - protected tween: Phaser.Tweens.Tween; - protected formLabels: Phaser.GameObjects.Text[]; - - constructor(mode: UiMode | null = null) { - super(mode); - - this.editing = false; - this.inputContainers = []; - this.inputs = []; - this.formLabels = []; - } + protected submitAction: AnyFn | undefined; + protected cancelAction: (() => void) | undefined; + protected tween: Phaser.Tweens.Tween | undefined; + protected formLabels: Phaser.GameObjects.Text[] = []; /** * Get configuration for all fields that should be part of the modal @@ -77,18 +68,18 @@ export abstract class FormModalUiHandler extends ModalUiHandler { fontSize: "42px", wordWrap: { width: 850 }, }, - ); - this.errorMessage.setColor(getTextColor(TextStyle.SUMMARY_PINK)); - this.errorMessage.setShadowColor(getTextColor(TextStyle.SUMMARY_PINK, true)); - this.errorMessage.setVisible(false); + ) + .setColor(getTextColor(TextStyle.SUMMARY_PINK)) + .setShadowColor(getTextColor(TextStyle.SUMMARY_PINK, true)) + .setVisible(false); this.modalContainer.add(this.errorMessage); } protected updateFields(fieldsConfig: InputFieldConfig[], hasTitle: boolean) { - this.inputContainers = []; - this.inputs = []; - this.formLabels = []; - fieldsConfig.forEach((config, f) => { + const inputContainers = (this.inputContainers = new Array(fieldsConfig.length)); + const inputs = (this.inputs = new Array(fieldsConfig.length)); + const formLabels = (this.formLabels = new Array(fieldsConfig.length)); + for (const [f, config] of fieldsConfig.entries()) { // The Pokédex Scan Window uses width `300` instead of `160` like the other forms // Therefore, the label does not need to be shortened const label = addTextObject( @@ -99,12 +90,13 @@ export abstract class FormModalUiHandler extends ModalUiHandler { ); label.name = "formLabel" + f; - this.formLabels.push(label); + formLabels[f] = label; this.modalContainer.add(label); 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); - inputContainer.setVisible(false); + const inputContainer = globalScene.add + .container(70 + (80 - inputWidth), (hasTitle ? 28 : 2) + 20 * f) + .setVisible(false); 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", maxLength: isPassword ? 64 : 20, readOnly: isReadOnly, - }); - input.setOrigin(0, 0); + }).setOrigin(0); - inputContainer.add(inputBg); - inputContainer.add(input); + inputContainer.add([inputBg, input]); - this.inputContainers.push(inputContainer); + inputContainers[f] = inputContainer; this.modalContainer.add(inputContainer); - this.inputs.push(input); - }); + inputs[f] = input; + } } override show(args: any[]): boolean { 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 buttonActions = config.buttonActions ?? []; - this.submitAction = config.buttonActions.length > 0 ? config.buttonActions[0] : null; - this.cancelAction = config.buttonActions[1] ?? null; + [this.submitAction, this.cancelAction] = buttonActions; // Auto focus the first input field after a short delay, to prevent accidental inputs 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 // Some subclasses use this to add behavior to the submit and cancel action - this.buttonBgs[0].off("pointerdown"); - this.buttonBgs[0].on("pointerdown", () => { - if (this.submitAction && globalScene.tweens.getTweensOf(this.modalContainer).length === 0) { - this.submitAction(); - } - }); - const cancelBg = this.buttonBgs[1]; - if (cancelBg) { - cancelBg.off("pointerdown"); - cancelBg.on("pointerdown", () => { + this.buttonBgs[0] // formatting + .off("pointerdown") + .on("pointerdown", () => { + if (this.submitAction && globalScene.tweens.getTweensOf(this.modalContainer).length === 0) { + this.submitAction(); + } + }); + this.buttonBgs[1] // formatting + ?.off("pointerdown") + .on("pointerdown", () => { // The seemingly redundant cancelAction check is intentionally left in as a defensive programming measure if (this.cancelAction && globalScene.tweens.getTweensOf(this.modalContainer).length === 0) { this.cancelAction(); } }); - } //#endregion: Override pointerDown events - this.modalContainer.y += 24; - this.modalContainer.setAlpha(0); + this.modalContainer.setAlpha(0).y += 24; this.tween = globalScene.tweens.add({ targets: this.modalContainer, @@ -199,21 +189,37 @@ export abstract class FormModalUiHandler extends ModalUiHandler { updateContainer(config?: ModalConfig): void { super.updateContainer(config); - this.errorMessage.setText(this.getReadableErrorMessage((config as FormModalConfig)?.errorMessage || "")); - this.errorMessage.setVisible(!!this.errorMessage.text); + this.errorMessage + .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 { super.clear(); this.modalContainer.setVisible(false); - this.inputContainers.map(ic => ic.setVisible(false)); - - this.submitAction = null; - - if (this.tween) { - this.tween.remove(); + for (const ic of this.inputContainers) { + ic.setVisible(false).setActive(false); } + + this.submitAction = undefined; + + this.tween?.remove().destroy(); + this.tween = undefined; } } diff --git a/src/ui/handlers/game-stats-ui-handler.ts b/src/ui/handlers/game-stats-ui-handler.ts index 58b00e3d50f..53b23781584 100644 --- a/src/ui/handlers/game-stats-ui-handler.ts +++ b/src/ui/handlers/game-stats-ui-handler.ts @@ -7,6 +7,7 @@ import { PlayerGender } from "#enums/player-gender"; import { TextStyle } from "#enums/text-style"; import { UiTheme } from "#enums/ui-theme"; import type { GameData } from "#system/game-data"; +import type { AnyFn } from "#types/type-helpers"; import { addTextObject } from "#ui/text"; import { UiHandler } from "#ui/ui-handler"; import { addWindow } from "#ui/ui-theme"; @@ -239,6 +240,12 @@ export class GameStatsUiHandler extends UiHandler { /** Logged in username */ 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 */ private get singleCol(): boolean { const resolvedLang = i18next.resolvedLanguage ?? "en"; @@ -318,9 +325,9 @@ export class GameStatsUiHandler extends UiHandler { ? i18next.t("trainerNames:playerF") : i18next.t("trainerNames:playerM"); - const displayName = !globalScene.hideUsername - ? (loggedInUser?.username ?? i18next.t("common:guest")) - : usernameReplacement; + const displayName = globalScene.hideUsername + ? usernameReplacement + : (loggedInUser?.username ?? i18next.t("common:guest")); return i18next.t("gameStatsUiHandler:stats", { username: displayName }); } @@ -395,11 +402,19 @@ export class GameStatsUiHandler extends UiHandler { this.gameStatsContainer.setVisible(false); } - show(args: any[]): boolean { - super.show(args); + show([username, data, callback]: [] | [username: string, data: GameData, callback?: AnyFn]): boolean { + super.show([]); - // show updated username on every render - this.headerText.setText(this.getUsername()); + if (username != null && data != null) { + 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); @@ -436,7 +451,7 @@ export class GameStatsUiHandler extends UiHandler { const statKeys = Object.keys(displayStats).slice(this.cursor * columns, this.cursor * columns + perPage); statKeys.forEach((key, s) => { const stat = displayStats[key] as DisplayStat; - const value = stat.sourceFunc?.(globalScene.gameData) ?? "-"; + const value = stat.sourceFunc?.(this.gameData) ?? "-"; const valAsInt = Number.parseInt(value); this.statLabels[s].setText( !stat.hidden || Number.isNaN(value) || valAsInt ? i18next.t(`gameStatsUiHandler:${stat.label_key}`) : "???", @@ -512,6 +527,12 @@ export class GameStatsUiHandler extends UiHandler { clear() { super.clear(); this.gameStatsContainer.setVisible(false).setActive(false); + + const callback = this.exitCallback; + if (callback != null) { + this.exitCallback = undefined; + callback(); + } } } diff --git a/src/ui/handlers/menu-ui-handler.ts b/src/ui/handlers/menu-ui-handler.ts index 419f2489818..ad9e9e30a1b 100644 --- a/src/ui/handlers/menu-ui-handler.ts +++ b/src/ui/handlers/menu-ui-handler.ts @@ -3,6 +3,7 @@ import { loggedInUser, updateUserInfo } from "#app/account"; import { globalScene } from "#app/global-scene"; import { bypassLogin } from "#app/global-vars/bypass-login"; import { handleTutorial, Tutorial } from "#app/tutorial"; +import { AdminMode, getAdminModeName } from "#enums/admin-mode"; import { Button } from "#enums/buttons"; import { GameDataType } from "#enums/game-data-type"; import { TextStyle } from "#enums/text-style"; @@ -19,7 +20,6 @@ import { getEnumValues } from "#utils/enums"; import { toCamelCase } from "#utils/strings"; import { isBeta } from "#utils/utility-vars"; import i18next from "i18next"; -import { AdminMode, getAdminModeName } from "./admin-ui-handler"; enum MenuOptions { GAME_SETTINGS, @@ -452,7 +452,7 @@ export class MenuUiHandler extends MessageUiHandler { keepOpen: true, }, ]; - if (!bypassLogin && loggedInUser?.hasAdminRole) { + if (bypassLogin || loggedInUser?.hasAdminRole) { communityOptions.push({ label: "Admin", handler: () => { diff --git a/src/ui/handlers/modal-ui-handler.ts b/src/ui/handlers/modal-ui-handler.ts index c145363b244..f8d8d408f6c 100644 --- a/src/ui/handlers/modal-ui-handler.ts +++ b/src/ui/handlers/modal-ui-handler.ts @@ -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 { return false; } diff --git a/src/ui/handlers/pokedex-ui-handler.ts b/src/ui/handlers/pokedex-ui-handler.ts index c6f9dbee448..ccc30761be0 100644 --- a/src/ui/handlers/pokedex-ui-handler.ts +++ b/src/ui/handlers/pokedex-ui-handler.ts @@ -31,9 +31,11 @@ import { UiMode } from "#enums/ui-mode"; import { UiTheme } from "#enums/ui-theme"; import type { Variant } from "#sprites/variant"; import { getVariantIcon, getVariantTint } from "#sprites/variant"; +import type { GameData } from "#system/game-data"; import { SettingKeyboard } from "#system/settings-keyboard"; import type { DexEntry } from "#types/dex-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 { DropDown, DropDownLabel, DropDownOption, DropDownState, DropDownType, SortCriteria } from "#ui/dropdown"; import { FilterBar } from "#ui/filter-bar"; @@ -237,6 +239,10 @@ export class PokedexUiHandler extends MessageUiHandler { private canShowFormTray: boolean; private filteredIndices: SpeciesId[]; + private gameData: GameData; + private exitCallback?: AnyFn; + private blockOpenPage = false; + constructor() { super(UiMode.POKEDEX); } @@ -642,8 +648,15 @@ export class PokedexUiHandler extends MessageUiHandler { this.pokerusSpecies = getPokerusStarters(); // When calling with "refresh", we do not reset the cursor and filters - if (args.length > 0 && args[0] === "refresh") { - return false; + if (args.length > 0) { + 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); @@ -685,8 +698,8 @@ export class PokedexUiHandler extends MessageUiHandler { */ initStarterPrefs(species: PokemonSpecies): StarterAttributes { const starterAttributes = this.starterPreferences[species.speciesId]; - const dexEntry = globalScene.gameData.dexData[species.speciesId]; - const starterData = globalScene.gameData.starterData[species.speciesId]; + const dexEntry = this.gameData.dexData[species.speciesId]; + const starterData = this.gameData.starterData[species.speciesId]; // no preferences or Pokemon wasn't caught, return empty attribute if (!starterAttributes || !dexEntry.caughtAttr) { @@ -753,15 +766,14 @@ export class PokedexUiHandler extends MessageUiHandler { const selectedForm = starterAttributes.form; if ( selectedForm !== undefined - && (!species.forms[selectedForm]?.isStarterSelectable - || !(caughtAttr & globalScene.gameData.getFormAttr(selectedForm))) + && (!species.forms[selectedForm]?.isStarterSelectable || !(caughtAttr & this.gameData.getFormAttr(selectedForm))) ) { // requested form wasn't unlocked/isn't a starter form, purging setting starterAttributes.form = 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) { // requested nature wasn't unlocked, purging setting starterAttributes.nature = undefined; @@ -812,7 +824,7 @@ export class PokedexUiHandler extends MessageUiHandler { return true; } if (!seenFilter) { - const starterDexEntry = globalScene.gameData.dexData[this.getStarterSpeciesId(species.speciesId)]; + const starterDexEntry = this.gameData.dexData[this.getStarterSpeciesId(species.speciesId)]; return !!starterDexEntry?.caughtAttr; } return false; @@ -851,7 +863,7 @@ export class PokedexUiHandler extends MessageUiHandler { */ isPassiveAvailable(speciesId: number): boolean { // Get this species ID's starter data - const starterData = globalScene.gameData.starterData[this.getStarterSpeciesId(speciesId)]; + const starterData = this.gameData.starterData[this.getStarterSpeciesId(speciesId)]; return ( starterData.candyCount >= getPassiveCandyCount(speciesStarterCosts[this.getStarterSpeciesId(speciesId)]) @@ -866,7 +878,7 @@ export class PokedexUiHandler extends MessageUiHandler { */ isValueReductionAvailable(speciesId: number): boolean { // Get this species ID's starter data - const starterData = globalScene.gameData.starterData[this.getStarterSpeciesId(speciesId)]; + const starterData = this.gameData.starterData[this.getStarterSpeciesId(speciesId)]; return ( starterData.candyCount @@ -883,7 +895,7 @@ export class PokedexUiHandler extends MessageUiHandler { */ isSameSpeciesEggAvailable(speciesId: number): boolean { // Get this species ID's starter data - const starterData = globalScene.gameData.starterData[this.getStarterSpeciesId(speciesId)]; + const starterData = this.gameData.starterData[this.getStarterSpeciesId(speciesId)]; return ( starterData.candyCount >= getSameSpeciesEggCandyCounts(speciesStarterCosts[this.getStarterSpeciesId(speciesId)]) @@ -1161,8 +1173,13 @@ export class PokedexUiHandler extends MessageUiHandler { } else if (this.showingTray) { if (button === Button.ACTION) { const formIndex = this.trayForms[this.trayCursor].formIndex; - ui.setOverlayMode(UiMode.POKEDEX_PAGE, this.lastSpecies, { form: formIndex }, this.filteredIndices); - success = true; + if (this.blockOpenPage) { + success = false; + error = true; + } else { + ui.setOverlayMode(UiMode.POKEDEX_PAGE, this.lastSpecies, { form: formIndex }, this.filteredIndices); + success = true; + } } else { const numberOfForms = this.trayContainers.length; const numOfRows = Math.ceil(numberOfForms / maxColumns); @@ -1209,8 +1226,13 @@ export class PokedexUiHandler extends MessageUiHandler { } } } else if (button === Button.ACTION) { - ui.setOverlayMode(UiMode.POKEDEX_PAGE, this.lastSpecies, null, this.filteredIndices); - success = true; + if (this.blockOpenPage) { + success = false; + error = true; + } else { + ui.setOverlayMode(UiMode.POKEDEX_PAGE, this.lastSpecies, null, this.filteredIndices); + success = true; + } } else { switch (button) { case Button.UP: @@ -1372,21 +1394,21 @@ export class PokedexUiHandler extends MessageUiHandler { const starterId = this.getStarterSpeciesId(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 = { species, - cost: globalScene.gameData.getSpeciesStarterValue(starterId), + cost: this.gameData.getSpeciesStarterValue(starterId), props, }; // 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 const caughtAttr = - (globalScene.gameData.dexData[species.speciesId]?.caughtAttr || BigInt(0)) - & (globalScene.gameData.dexData[this.getStarterSpeciesId(species.speciesId)]?.caughtAttr || BigInt(0)) + (this.gameData.dexData[species.speciesId]?.caughtAttr || BigInt(0)) + & (this.gameData.dexData[this.getStarterSpeciesId(species.speciesId)]?.caughtAttr || BigInt(0)) & species.getFullUnlocksData(); - const starterData = globalScene.gameData.starterData[starterId]; + const starterData = this.gameData.starterData[starterId]; const isStarterProgressable = speciesEggMoves.hasOwnProperty(starterId); // Name filter @@ -1635,7 +1657,7 @@ export class PokedexUiHandler extends MessageUiHandler { }); // 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 fitsSeen = this.filterBar.getVals(DropDownColumn.MISC).some(misc => { if (misc.val === "SEEN_SPECIES" && misc.state === DropDownState.ON) { @@ -1725,33 +1747,31 @@ export class PokedexUiHandler extends MessageUiHandler { case SortCriteria.COST: return (a.cost - b.cost) * -sort.dir; case SortCriteria.CANDY: { - const candyCountA = - globalScene.gameData.starterData[this.getStarterSpeciesId(a.species.speciesId)].candyCount; - const candyCountB = - globalScene.gameData.starterData[this.getStarterSpeciesId(b.species.speciesId)].candyCount; + const candyCountA = this.gameData.starterData[this.getStarterSpeciesId(a.species.speciesId)].candyCount; + const candyCountB = this.gameData.starterData[this.getStarterSpeciesId(b.species.speciesId)].candyCount; return (candyCountA - candyCountB) * -sort.dir; } case SortCriteria.IV: { const avgIVsA = - globalScene.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.reduce((a, b) => a + b, 0) + / this.gameData.dexData[a.species.speciesId].ivs.length; const avgIVsB = - globalScene.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.reduce((a, b) => a + b, 0) + / this.gameData.dexData[b.species.speciesId].ivs.length; return (avgIVsA - avgIVsB) * -sort.dir; } case SortCriteria.NAME: return a.species.name.localeCompare(b.species.name) * -sort.dir; case SortCriteria.CAUGHT: return ( - (globalScene.gameData.dexData[a.species.speciesId].caughtCount - - globalScene.gameData.dexData[b.species.speciesId].caughtCount) + (this.gameData.dexData[a.species.speciesId].caughtCount + - this.gameData.dexData[b.species.speciesId].caughtCount) * -sort.dir ); case SortCriteria.HATCHED: return ( - (globalScene.gameData.dexData[this.getStarterSpeciesId(a.species.speciesId)].hatchedCount - - globalScene.gameData.dexData[this.getStarterSpeciesId(b.species.speciesId)].hatchedCount) + (this.gameData.dexData[this.getStarterSpeciesId(a.species.speciesId)].hatchedCount + - this.gameData.dexData[this.getStarterSpeciesId(b.species.speciesId)].hatchedCount) * -sort.dir ); default: @@ -1795,10 +1815,10 @@ export class PokedexUiHandler extends MessageUiHandler { container.checkIconId(props.female, props.formIndex, props.shiny, props.variant); const speciesId = data.species.speciesId; - const dexEntry = globalScene.gameData.dexData[speciesId]; + const dexEntry = this.gameData.dexData[speciesId]; const caughtAttr = dexEntry.caughtAttr - & globalScene.gameData.dexData[this.getStarterSpeciesId(speciesId)].caughtAttr + & this.gameData.dexData[this.getStarterSpeciesId(speciesId)].caughtAttr & data.species.getFullUnlocksData(); if (caughtAttr & data.species.getFullUnlocksData() || globalScene.dexForDevs) { @@ -1857,13 +1877,13 @@ export class PokedexUiHandler extends MessageUiHandler { } container.starterPassiveBgs.setVisible( - !!globalScene.gameData.starterData[this.getStarterSpeciesId(speciesId)].passiveAttr, + !!this.gameData.starterData[this.getStarterSpeciesId(speciesId)].passiveAttr, ); container.hiddenAbilityIcon.setVisible( - !!caughtAttr && !!(globalScene.gameData.starterData[this.getStarterSpeciesId(speciesId)].abilityAttr & 4), + !!caughtAttr && !!(this.gameData.starterData[this.getStarterSpeciesId(speciesId)].abilityAttr & 4), ); 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); @@ -1989,15 +2009,15 @@ export class PokedexUiHandler extends MessageUiHandler { 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); - const dexEntry = globalScene.gameData.dexData[species.speciesId]; + const dexEntry = this.gameData.dexData[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 = []; const isFormSeen = this.isSeen(species, dexEntry); this.trayForms.map((f, index) => { 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; const formContainer = new PokedexMonContainer(species, { formIndex: f.formIndex, @@ -2066,7 +2086,7 @@ export class PokedexUiHandler extends MessageUiHandler { } 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) { currentFriendship = 0; } @@ -2094,7 +2114,7 @@ export class PokedexUiHandler extends MessageUiHandler { if (container) { const lastSpeciesIcon = container.icon; 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.iconAnimHandler.addOrUpdate(lastSpeciesIcon, PokemonIconAnimMode.NONE); // Resume the animation for the previously selected species @@ -2103,7 +2123,7 @@ export class PokedexUiHandler extends MessageUiHandler { } 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) { globalScene.ui.hideTooltip(); @@ -2182,15 +2202,15 @@ export class PokedexUiHandler extends MessageUiHandler { } if (species) { - const dexEntry = globalScene.gameData.dexData[species.speciesId]; + const dexEntry = this.gameData.dexData[species.speciesId]; const caughtAttr = dexEntry.caughtAttr - & globalScene.gameData.dexData[this.getStarterSpeciesId(species.speciesId)].caughtAttr + & this.gameData.dexData[this.getStarterSpeciesId(species.speciesId)].caughtAttr & species.getFullUnlocksData(); if (caughtAttr) { const props = this.getSanitizedProps( - globalScene.gameData.getSpeciesDexAttrProps(species, this.getCurrentDexProps(species.speciesId)), + this.gameData.getSpeciesDexAttrProps(species, this.getCurrentDexProps(species.speciesId)), ); 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 assetLoadCancelled = new BooleanHolder(false); @@ -2291,7 +2311,7 @@ export class PokedexUiHandler extends MessageUiHandler { updateStarterValueLabel(starter: PokedexMonContainer): void { const speciesId = starter.species.speciesId; const baseStarterValue = speciesStarterCosts[speciesId]; - const starterValue = globalScene.gameData.getSpeciesStarterValue(this.getStarterSpeciesId(speciesId)); + const starterValue = this.gameData.getSpeciesStarterValue(this.getStarterSpeciesId(speciesId)); starter.cost = starterValue; let valueStr = starterValue.toString(); if (valueStr.startsWith("0.")) { @@ -2356,8 +2376,8 @@ export class PokedexUiHandler extends MessageUiHandler { let props = 0n; const species = allSpecies.find(sp => sp.speciesId === speciesId); const caughtAttr = - globalScene.gameData.dexData[speciesId].caughtAttr - & globalScene.gameData.dexData[this.getStarterSpeciesId(speciesId)].caughtAttr + this.gameData.dexData[speciesId].caughtAttr + & this.gameData.dexData[this.getStarterSpeciesId(speciesId)].caughtAttr & (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 @@ -2401,7 +2421,7 @@ export class PokedexUiHandler extends MessageUiHandler { props += BigInt(Math.pow(2, this.starterPreferences[speciesId]?.form)) * DexAttr.DEFAULT_FORM; } else { // Get the first unlocked form - props += globalScene.gameData.getFormAttr(globalScene.gameData.getFormIndex(caughtAttr)); + props += this.gameData.getFormAttr(this.gameData.getFormIndex(caughtAttr)); } return props; @@ -2426,6 +2446,13 @@ export class PokedexUiHandler extends MessageUiHandler { this.starterSelectContainer.setVisible(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( diff --git a/test/plugins/api/pokerogue-admin-api.test.ts b/test/plugins/api/pokerogue-admin-api.test.ts index a40fa23698f..1b60c4cb272 100644 --- a/test/plugins/api/pokerogue-admin-api.test.ts +++ b/test/plugins/api/pokerogue-admin-api.test.ts @@ -2,12 +2,10 @@ import { PokerogueAdminApi } from "#api/pokerogue-admin-api"; import { initServerForApiTests } from "#test/test-utils/test-file-initialization"; import { getApiBaseUrl } from "#test/test-utils/test-utils"; import type { - LinkAccountToDiscordIdRequest, - LinkAccountToGoogledIdRequest, + DiscordRequest, + GoogleRequest, SearchAccountRequest, SearchAccountResponse, - UnlinkAccountFromDiscordIdRequest, - UnlinkAccountFromGoogledIdRequest, } from "#types/api/pokerogue-admin-api"; import { HttpResponse, http } from "msw"; import type { SetupServerApi } from "msw/node"; @@ -31,7 +29,7 @@ describe("Pokerogue Admin API", () => { }); describe("Link Account to Discord", () => { - const params: LinkAccountToDiscordIdRequest = { + const params: DiscordRequest = { username: "test", discordId: "test-12575756", }; @@ -39,7 +37,7 @@ describe("Pokerogue Admin API", () => { it("should return null on SUCCESS", async () => { 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(); }); @@ -47,7 +45,7 @@ describe("Pokerogue Admin API", () => { 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 }))); - const success = await adminApi.linkAccountToDiscord(params); + const success = await adminApi.linkUnlinkRequest("Link", "discord", params); expect(success).toBe(adminApi.ERR_GENERIC); 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 () => { 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(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 () => { 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(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", () => { - const params: UnlinkAccountFromDiscordIdRequest = { + const params: DiscordRequest = { username: "test", discordId: "test-12575756", }; @@ -81,7 +79,7 @@ describe("Pokerogue Admin API", () => { it("should return null on SUCCESS", async () => { 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(); }); @@ -89,7 +87,7 @@ describe("Pokerogue Admin API", () => { 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 }))); - const success = await adminApi.unlinkAccountFromDiscord(params); + const success = await adminApi.linkUnlinkRequest("Unlink", "discord", params); expect(success).toBe(adminApi.ERR_GENERIC); 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 () => { 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(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 () => { 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(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", () => { - const params: LinkAccountToGoogledIdRequest = { + const params: GoogleRequest = { username: "test", googleId: "test-12575756", }; @@ -123,7 +121,7 @@ describe("Pokerogue Admin API", () => { it("should return null on SUCCESS", async () => { 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(); }); @@ -131,7 +129,7 @@ describe("Pokerogue Admin API", () => { 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 }))); - const success = await adminApi.linkAccountToGoogleId(params); + const success = await adminApi.linkUnlinkRequest("Link", "google", params); expect(success).toBe(adminApi.ERR_GENERIC); 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 () => { 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(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 () => { 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(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", () => { - const params: UnlinkAccountFromGoogledIdRequest = { + const params: GoogleRequest = { username: "test", googleId: "test-12575756", }; @@ -165,7 +163,7 @@ describe("Pokerogue Admin API", () => { it("should return null on SUCCESS", async () => { 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(); }); @@ -173,7 +171,7 @@ describe("Pokerogue Admin API", () => { 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 }))); - const success = await adminApi.unlinkAccountFromGoogleId(params); + const success = await adminApi.linkUnlinkRequest("Unlink", "google", params); expect(success).toBe(adminApi.ERR_GENERIC); 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 () => { 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(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 () => { 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(console.warn).toHaveBeenCalledWith("Could not unlink account from google!", expect.any(Error)); diff --git a/test/test-utils/game-manager.ts b/test/test-utils/game-manager.ts index abe0b8cfcf6..aab7ebabb2e 100644 --- a/test/test-utils/game-manager.ts +++ b/test/test-utils/game-manager.ts @@ -28,6 +28,7 @@ import type { SelectTargetPhase } from "#phases/select-target-phase"; import { TurnEndPhase } from "#phases/turn-end-phase"; import { TurnInitPhase } from "#phases/turn-init-phase"; import { TurnStartPhase } from "#phases/turn-start-phase"; +import { GameData } from "#system/game-data"; import { ErrorInterceptor } from "#test/test-utils/error-interceptor"; import { generateStarters } from "#test/test-utils/game-manager-utils"; import { GameWrapper } from "#test/test-utils/game-wrapper"; @@ -451,7 +452,7 @@ export class GameManager { const dataRaw = fs.readFileSync(path, { encoding: "utf8", flag: "r" }); let dataStr = AES.decrypt(dataRaw, saveKey).toString(enc.Utf8); dataStr = this.scene.gameData.convertSystemDataStr(dataStr); - const systemData = this.scene.gameData.parseSystemData(dataStr); + const systemData = GameData.parseSystemData(dataStr); const valid = !!systemData.dexData && !!systemData.timestamp; if (valid) { await updateUserInfo();