From b2990aaa15f8ef673ff9af7c5f72a7e84f8b2aa1 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Thu, 14 Aug 2025 16:57:01 -0500 Subject: [PATCH 01/12] [Bug] [Beta] Fix renaming runs (#6268) Rename run name field, don't encrypt before updating --- package.json | 1 + pnpm-lock.yaml | 8 ++ src/account.ts | 71 ++++++++--------- .../api/pokerogue-session-savedata-api.ts | 8 +- src/system/game-data.ts | 79 +++++++++---------- src/ui/run-info-ui-handler.ts | 2 +- src/ui/save-slot-select-ui-handler.ts | 6 +- src/utils/data.ts | 16 ++-- 8 files changed, 96 insertions(+), 95 deletions(-) diff --git a/package.json b/package.json index d3494da677c..3f523ed5c3e 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "devDependencies": { "@biomejs/biome": "2.0.0", "@ls-lint/ls-lint": "2.3.1", + "@types/crypto-js": "^4.2.0", "@types/jsdom": "^21.1.7", "@types/node": "^22.16.5", "@vitest/coverage-istanbul": "^3.2.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 900be6fd76e..c3b58a60f48 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -48,6 +48,9 @@ importers: '@ls-lint/ls-lint': specifier: 2.3.1 version: 2.3.1 + '@types/crypto-js': + specifier: ^4.2.0 + version: 4.2.2 '@types/jsdom': specifier: ^21.1.7 version: 21.1.7 @@ -718,6 +721,9 @@ packages: '@types/cookie@0.6.0': resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + '@types/crypto-js@4.2.2': + resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==} + '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} @@ -2525,6 +2531,8 @@ snapshots: '@types/cookie@0.6.0': {} + '@types/crypto-js@4.2.2': {} + '@types/deep-eql@4.0.2': {} '@types/estree@1.0.8': {} diff --git a/src/account.ts b/src/account.ts index b01691ce940..c97721889ae 100644 --- a/src/account.ts +++ b/src/account.ts @@ -17,45 +17,42 @@ export function initLoggedInUser(): void { }; } -export function updateUserInfo(): Promise<[boolean, number]> { - return new Promise<[boolean, number]>(resolve => { - if (bypassLogin) { - loggedInUser = { - username: "Guest", - lastSessionSlot: -1, - discordId: "", - googleId: "", - hasAdminRole: false, - }; - let lastSessionSlot = -1; - for (let s = 0; s < 5; s++) { - if (localStorage.getItem(`sessionData${s ? s : ""}_${loggedInUser.username}`)) { - lastSessionSlot = s; - break; - } +export async function updateUserInfo(): Promise<[boolean, number]> { + if (bypassLogin) { + loggedInUser = { + username: "Guest", + lastSessionSlot: -1, + discordId: "", + googleId: "", + hasAdminRole: false, + }; + let lastSessionSlot = -1; + for (let s = 0; s < 5; s++) { + if (localStorage.getItem(`sessionData${s ? s : ""}_${loggedInUser.username}`)) { + lastSessionSlot = s; + break; } - loggedInUser.lastSessionSlot = lastSessionSlot; - // Migrate old data from before the username was appended - ["data", "sessionData", "sessionData1", "sessionData2", "sessionData3", "sessionData4"].map(d => { - const lsItem = localStorage.getItem(d); - if (lsItem && !!loggedInUser?.username) { - const lsUserItem = localStorage.getItem(`${d}_${loggedInUser.username}`); - if (lsUserItem) { - localStorage.setItem(`${d}_${loggedInUser.username}_bak`, lsUserItem); - } - localStorage.setItem(`${d}_${loggedInUser.username}`, lsItem); - localStorage.removeItem(d); - } - }); - return resolve([true, 200]); } - pokerogueApi.account.getInfo().then(([accountInfo, status]) => { - if (!accountInfo) { - resolve([false, status]); - return; + loggedInUser.lastSessionSlot = lastSessionSlot; + // Migrate old data from before the username was appended + ["data", "sessionData", "sessionData1", "sessionData2", "sessionData3", "sessionData4"].forEach(d => { + const lsItem = localStorage.getItem(d); + if (lsItem && !!loggedInUser?.username) { + const lsUserItem = localStorage.getItem(`${d}_${loggedInUser.username}`); + if (lsUserItem) { + localStorage.setItem(`${d}_${loggedInUser.username}_bak`, lsUserItem); + } + localStorage.setItem(`${d}_${loggedInUser.username}`, lsItem); + localStorage.removeItem(d); } - loggedInUser = accountInfo; - resolve([true, 200]); }); - }); + return [true, 200]; + } + + const [accountInfo, status] = await pokerogueApi.account.getInfo(); + if (!accountInfo) { + return [false, status]; + } + loggedInUser = accountInfo; + return [true, 200]; } diff --git a/src/plugins/api/pokerogue-session-savedata-api.ts b/src/plugins/api/pokerogue-session-savedata-api.ts index 4ffb0a5d8da..39fa292f9f1 100644 --- a/src/plugins/api/pokerogue-session-savedata-api.ts +++ b/src/plugins/api/pokerogue-session-savedata-api.ts @@ -56,15 +56,15 @@ export class PokerogueSessionSavedataApi extends ApiBase { /** * Update a session savedata. - * @param params The {@linkcode UpdateSessionSavedataRequest} to send - * @param rawSavedata The raw savedata (as `string`) + * @param params - The request to send + * @param rawSavedata - The raw, unencrypted savedata * @returns An error message if something went wrong */ - public async update(params: UpdateSessionSavedataRequest, rawSavedata: string) { + public async update(params: UpdateSessionSavedataRequest, rawSavedata: string): Promise { try { const urlSearchParams = this.toUrlSearchParams(params); - const response = await this.doPost(`/savedata/session/update?${urlSearchParams}`, rawSavedata); + const response = await this.doPost(`/savedata/session/update?${urlSearchParams}`, rawSavedata); return await response.text(); } catch (err) { console.warn("Could not update session savedata!", err); diff --git a/src/system/game-data.ts b/src/system/game-data.ts index a1213990053..90cbf6e18cc 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -128,7 +128,8 @@ export interface SessionSaveData { battleType: BattleType; trainer: TrainerData; gameVersion: string; - runNameText: string; + /** The player-chosen name of the run */ + name: string; timestamp: number; challenges: ChallengeData[]; mysteryEncounterType: MysteryEncounterType | -1; // Only defined when current wave is ME, @@ -986,51 +987,45 @@ export class GameData { } async renameSession(slotId: number, newName: string): Promise { - return new Promise(async resolve => { - if (slotId < 0) { - return resolve(false); - } - const sessionData: SessionSaveData | null = await this.getSession(slotId); + if (slotId < 0) { + return false; + } + if (newName === "") { + return true; + } + const sessionData: SessionSaveData | null = await this.getSession(slotId); - if (!sessionData) { - return resolve(false); - } + if (!sessionData) { + return false; + } - if (newName === "") { - return resolve(true); - } + sessionData.name = newName; + // update timestamp by 1 to ensure the session is saved + sessionData.timestamp += 1; + const updatedDataStr = JSON.stringify(sessionData); + const encrypted = encrypt(updatedDataStr, bypassLogin); + const secretId = this.secretId; + const trainerId = this.trainerId; - sessionData.runNameText = newName; - const updatedDataStr = JSON.stringify(sessionData); - const encrypted = encrypt(updatedDataStr, bypassLogin); - const secretId = this.secretId; - const trainerId = this.trainerId; + if (bypassLogin) { + localStorage.setItem( + `sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`, + encrypt(updatedDataStr, bypassLogin), + ); + return true; + } - if (bypassLogin) { - localStorage.setItem( - `sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`, - encrypt(updatedDataStr, bypassLogin), - ); - resolve(true); - return; - } - pokerogueApi.savedata.session - .update({ slot: slotId, trainerId, secretId, clientSessionId }, encrypted) - .then(error => { - if (error) { - console.error("Failed to update session name:", error); - resolve(false); - } else { - localStorage.setItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`, encrypted); - updateUserInfo().then(success => { - if (success !== null && !success) { - return resolve(false); - } - }); - resolve(true); - } - }); - }); + const response = await pokerogueApi.savedata.session.update( + { slot: slotId, trainerId, secretId, clientSessionId }, + updatedDataStr, + ); + + if (response) { + return false; + } + localStorage.setItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`, encrypted); + const success = await updateUserInfo(); + return !(success !== null && !success); } loadSession(slotId: number, sessionData?: SessionSaveData): Promise { diff --git a/src/ui/run-info-ui-handler.ts b/src/ui/run-info-ui-handler.ts index 8facd8e73b1..572b8ccf560 100644 --- a/src/ui/run-info-ui-handler.ts +++ b/src/ui/run-info-ui-handler.ts @@ -208,7 +208,7 @@ export class RunInfoUiHandler extends UiHandler { headerText.setOrigin(0, 0); headerText.setPositionRelative(headerBg, 8, 4); this.runContainer.add(headerText); - const runName = addTextObject(0, 0, this.runInfo.runNameText, TextStyle.WINDOW); + const runName = addTextObject(0, 0, this.runInfo.name, TextStyle.WINDOW); runName.setOrigin(0, 0); runName.setPositionRelative(headerBg, 60, 4); this.runContainer.add(runName); diff --git a/src/ui/save-slot-select-ui-handler.ts b/src/ui/save-slot-select-ui-handler.ts index 52e145e6439..e9f9c5a0038 100644 --- a/src/ui/save-slot-select-ui-handler.ts +++ b/src/ui/save-slot-select-ui-handler.ts @@ -377,7 +377,7 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler { "select_cursor_highlight_thick", undefined, 294, - this.sessionSlots[prevSlotIndex ?? 0]?.saveData?.runNameText ? 50 : 60, + this.sessionSlots[prevSlotIndex ?? 0]?.saveData?.name ? 50 : 60, 6, 6, 6, @@ -553,10 +553,10 @@ class SessionSlot extends Phaser.GameObjects.Container { } async setupWithData(data: SessionSaveData) { - const hasName = data?.runNameText; + const hasName = data?.name; this.remove(this.loadingLabel, true); if (hasName) { - const nameLabel = addTextObject(8, 5, data.runNameText, TextStyle.WINDOW); + const nameLabel = addTextObject(8, 5, data.name, TextStyle.WINDOW); this.add(nameLabel); } else { const fallbackName = this.decideFallback(data); diff --git a/src/utils/data.ts b/src/utils/data.ts index 932ea38d504..6580ecf2ee9 100644 --- a/src/utils/data.ts +++ b/src/utils/data.ts @@ -45,17 +45,17 @@ export function deepMergeSpriteData(dest: object, source: object) { } export function encrypt(data: string, bypassLogin: boolean): string { - return (bypassLogin - ? (data: string) => btoa(encodeURIComponent(data)) - : (data: string) => AES.encrypt(data, saveKey))(data) as unknown as string; // TODO: is this correct? + if (bypassLogin) { + return btoa(encodeURIComponent(data)); + } + return AES.encrypt(data, saveKey).toString(); } export function decrypt(data: string, bypassLogin: boolean): string { - return ( - bypassLogin - ? (data: string) => decodeURIComponent(atob(data)) - : (data: string) => AES.decrypt(data, saveKey).toString(enc.Utf8) - )(data); + if (bypassLogin) { + return decodeURIComponent(atob(data)); + } + return AES.decrypt(data, saveKey).toString(enc.Utf8); } // the latest data saved/loaded for the Starter Preferences. Required to reduce read/writes. Initialize as "{}", since this is the default value and no data needs to be stored if present. From 70e7f8b4d446a22c00c5acff471c7bf219f9ebfe Mon Sep 17 00:00:00 2001 From: Bertie690 <136088738+Bertie690@users.noreply.github.com> Date: Fri, 15 Aug 2025 11:11:37 -0400 Subject: [PATCH 02/12] [Misc] Removed `populateAnims` script (#6229) Removed `populateAnims` Co-authored-by: Wlowscha <54003515+Wlowscha@users.noreply.github.com> --- src/battle-scene.ts | 9 +- src/data/battle-anims.ts | 304 ++------------------------------------- 2 files changed, 13 insertions(+), 300 deletions(-) diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 4a136a1696a..4d3f190c02a 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -27,13 +27,7 @@ import { UiInputs } from "#app/ui-inputs"; import { biomeDepths, getBiomeName } from "#balance/biomes"; import { pokemonPrevolutions } from "#balance/pokemon-evolutions"; import { FRIENDSHIP_GAIN_FROM_BATTLE } from "#balance/starters"; -import { - initCommonAnims, - initMoveAnim, - loadCommonAnimAssets, - loadMoveAnimAssets, - populateAnims, -} from "#data/battle-anims"; +import { initCommonAnims, initMoveAnim, loadCommonAnimAssets, loadMoveAnimAssets } from "#data/battle-anims"; import { allAbilities, allMoves, allSpecies, modifierTypes } from "#data/data-lists"; import { battleSpecDialogue } from "#data/dialogue"; import type { SpeciesFormChangeTrigger } from "#data/form-change-triggers"; @@ -388,7 +382,6 @@ export class BattleScene extends SceneBase { const defaultMoves = [MoveId.TACKLE, MoveId.TAIL_WHIP, MoveId.FOCUS_ENERGY, MoveId.STRUGGLE]; await Promise.all([ - populateAnims(), this.initVariantData(), initCommonAnims().then(() => loadCommonAnimAssets(true)), Promise.all(defaultMoves.map(m => initMoveAnim(m))).then(() => loadMoveAnimAssets(defaultMoves, true)), diff --git a/src/data/battle-anims.ts b/src/data/battle-anims.ts index 55a3cc4e916..aa4951f3263 100644 --- a/src/data/battle-anims.ts +++ b/src/data/battle-anims.ts @@ -404,22 +404,18 @@ export const chargeAnims = new Map(); export const encounterAnims = new Map(); -export function initCommonAnims(): Promise { - return new Promise(resolve => { - const commonAnimNames = getEnumKeys(CommonAnim); - const commonAnimIds = getEnumValues(CommonAnim); - const commonAnimFetches: Promise>[] = []; - for (let ca = 0; ca < commonAnimIds.length; ca++) { - const commonAnimId = commonAnimIds[ca]; - commonAnimFetches.push( - globalScene - .cachedFetch(`./battle-anims/common-${toKebabCase(commonAnimNames[ca])}.json`) - .then(response => response.json()) - .then(cas => commonAnims.set(commonAnimId, new AnimConfig(cas))), - ); - } - Promise.allSettled(commonAnimFetches).then(() => resolve()); - }); +export async function initCommonAnims(): Promise { + const commonAnimFetches: Promise>[] = []; + for (const commonAnimName of getEnumKeys(CommonAnim)) { + const commonAnimId = CommonAnim[commonAnimName]; + commonAnimFetches.push( + globalScene + .cachedFetch(`./battle-anims/common-${toKebabCase(commonAnimName)}.json`) + .then(response => response.json()) + .then(cas => commonAnims.set(commonAnimId, new AnimConfig(cas))), + ); + } + await Promise.allSettled(commonAnimFetches); } export function initMoveAnim(move: MoveId): Promise { @@ -1396,279 +1392,3 @@ export class EncounterBattleAnim extends BattleAnim { return this.oppAnim; } } - -export async function populateAnims() { - const commonAnimNames = getEnumKeys(CommonAnim).map(k => k.toLowerCase()); - const commonAnimMatchNames = commonAnimNames.map(k => k.replace(/_/g, "")); - const commonAnimIds = getEnumValues(CommonAnim); - const chargeAnimNames = getEnumKeys(ChargeAnim).map(k => k.toLowerCase()); - const chargeAnimMatchNames = chargeAnimNames.map(k => k.replace(/_/g, " ")); - const chargeAnimIds = getEnumValues(ChargeAnim); - const commonNamePattern = /name: (?:Common:)?(Opp )?(.*)/; - const moveNameToId = {}; - // Exclude MoveId.NONE; - for (const move of getEnumValues(MoveId).slice(1)) { - // KARATE_CHOP => KARATECHOP - const moveName = MoveId[move].toUpperCase().replace(/_/g, ""); - moveNameToId[moveName] = move; - } - - const seNames: string[] = []; //(await fs.readdir('./public/audio/se/battle_anims/')).map(se => se.toString()); - - const animsData: any[] = []; //battleAnimRawData.split('!ruby/array:PBAnimation').slice(1); // TODO: add a proper type - for (let a = 0; a < animsData.length; a++) { - const fields = animsData[a].split("@").slice(1); - - const nameField = fields.find(f => f.startsWith("name: ")); - - let isOppMove: boolean | undefined; - let commonAnimId: CommonAnim | undefined; - let chargeAnimId: ChargeAnim | undefined; - if (!nameField.startsWith("name: Move:") && !(isOppMove = nameField.startsWith("name: OppMove:"))) { - const nameMatch = commonNamePattern.exec(nameField)!; // TODO: is this bang correct? - const name = nameMatch[2].toLowerCase(); - if (commonAnimMatchNames.indexOf(name) > -1) { - commonAnimId = commonAnimIds[commonAnimMatchNames.indexOf(name)]; - } else if (chargeAnimMatchNames.indexOf(name) > -1) { - isOppMove = nameField.startsWith("name: Opp "); - chargeAnimId = chargeAnimIds[chargeAnimMatchNames.indexOf(name)]; - } - } - const nameIndex = nameField.indexOf(":", 5) + 1; - const animName = nameField.slice(nameIndex, nameField.indexOf("\n", nameIndex)); - if (!moveNameToId.hasOwnProperty(animName) && !commonAnimId && !chargeAnimId) { - continue; - } - const anim = commonAnimId || chargeAnimId ? new AnimConfig() : new AnimConfig(); - if (anim instanceof AnimConfig) { - (anim as AnimConfig).id = moveNameToId[animName]; - } - if (commonAnimId) { - commonAnims.set(commonAnimId, anim); - } else if (chargeAnimId) { - chargeAnims.set(chargeAnimId, !isOppMove ? anim : [chargeAnims.get(chargeAnimId) as AnimConfig, anim]); - } else { - moveAnims.set( - moveNameToId[animName], - !isOppMove ? (anim as AnimConfig) : [moveAnims.get(moveNameToId[animName]) as AnimConfig, anim as AnimConfig], - ); - } - for (let f = 0; f < fields.length; f++) { - const field = fields[f]; - const fieldName = field.slice(0, field.indexOf(":")); - const fieldData = field.slice(fieldName.length + 1, field.lastIndexOf("\n")).trim(); - switch (fieldName) { - case "array": { - const framesData = fieldData.split(" - - - ").slice(1); - for (let fd = 0; fd < framesData.length; fd++) { - anim.frames.push([]); - const frameData = framesData[fd]; - const focusFramesData = frameData.split(" - - "); - for (let tf = 0; tf < focusFramesData.length; tf++) { - const values = focusFramesData[tf].replace(/ {6}- /g, "").split("\n"); - const targetFrame = new AnimFrame( - Number.parseFloat(values[0]), - Number.parseFloat(values[1]), - Number.parseFloat(values[2]), - Number.parseFloat(values[11]), - Number.parseFloat(values[3]), - Number.parseInt(values[4]) === 1, - Number.parseInt(values[6]) === 1, - Number.parseInt(values[5]), - Number.parseInt(values[7]), - Number.parseInt(values[8]), - Number.parseInt(values[12]), - Number.parseInt(values[13]), - Number.parseInt(values[14]), - Number.parseInt(values[15]), - Number.parseInt(values[16]), - Number.parseInt(values[17]), - Number.parseInt(values[18]), - Number.parseInt(values[19]), - Number.parseInt(values[21]), - Number.parseInt(values[22]), - Number.parseInt(values[23]), - Number.parseInt(values[24]), - Number.parseInt(values[20]) === 1, - Number.parseInt(values[25]), - Number.parseInt(values[26]) as AnimFocus, - ); - anim.frames[fd].push(targetFrame); - } - } - break; - } - case "graphic": { - const graphic = fieldData !== "''" ? fieldData : ""; - anim.graphic = graphic.indexOf(".") > -1 ? graphic.slice(0, fieldData.indexOf(".")) : graphic; - break; - } - case "timing": { - const timingEntries = fieldData.split("- !ruby/object:PBAnimTiming ").slice(1); - for (let t = 0; t < timingEntries.length; t++) { - const timingData = timingEntries[t] - .replace(/\n/g, " ") - .replace(/[ ]{2,}/g, " ") - .replace(/[a-z]+: ! '', /gi, "") - .replace(/name: (.*?),/, 'name: "$1",') - .replace( - /flashColor: !ruby\/object:Color { alpha: ([\d.]+), blue: ([\d.]+), green: ([\d.]+), red: ([\d.]+)}/, - "flashRed: $4, flashGreen: $3, flashBlue: $2, flashAlpha: $1", - ); - const frameIndex = Number.parseInt(/frame: (\d+)/.exec(timingData)![1]); // TODO: is the bang correct? - let resourceName = /name: "(.*?)"/.exec(timingData)![1].replace("''", ""); // TODO: is the bang correct? - const timingType = Number.parseInt(/timingType: (\d)/.exec(timingData)![1]); // TODO: is the bang correct? - let timedEvent: AnimTimedEvent | undefined; - switch (timingType) { - case 0: - if (resourceName && resourceName.indexOf(".") === -1) { - let ext: string | undefined; - ["wav", "mp3", "m4a"].every(e => { - if (seNames.indexOf(`${resourceName}.${e}`) > -1) { - ext = e; - return false; - } - return true; - }); - if (!ext) { - ext = ".wav"; - } - resourceName += `.${ext}`; - } - timedEvent = new AnimTimedSoundEvent(frameIndex, resourceName); - break; - case 1: - timedEvent = new AnimTimedAddBgEvent(frameIndex, resourceName.slice(0, resourceName.indexOf("."))); - break; - case 2: - timedEvent = new AnimTimedUpdateBgEvent(frameIndex, resourceName.slice(0, resourceName.indexOf("."))); - break; - } - if (!timedEvent) { - continue; - } - const propPattern = /([a-z]+): (.*?)(?:,|\})/gi; - let propMatch: RegExpExecArray; - while ((propMatch = propPattern.exec(timingData)!)) { - // TODO: is this bang correct? - const prop = propMatch[1]; - let value: any = propMatch[2]; - switch (prop) { - case "bgX": - case "bgY": - value = Number.parseFloat(value); - break; - case "volume": - case "pitch": - case "opacity": - case "colorRed": - case "colorGreen": - case "colorBlue": - case "colorAlpha": - case "duration": - case "flashScope": - case "flashRed": - case "flashGreen": - case "flashBlue": - case "flashAlpha": - case "flashDuration": - value = Number.parseInt(value); - break; - } - if (timedEvent.hasOwnProperty(prop)) { - timedEvent[prop] = value; - } - } - if (!anim.frameTimedEvents.has(frameIndex)) { - anim.frameTimedEvents.set(frameIndex, []); - } - anim.frameTimedEvents.get(frameIndex)!.push(timedEvent); // TODO: is this bang correct? - } - break; - } - case "position": - anim.position = Number.parseInt(fieldData); - break; - case "hue": - anim.hue = Number.parseInt(fieldData); - break; - } - } - } - - // biome-ignore lint/correctness/noUnusedVariables: used in commented code - const animReplacer = (k, v) => { - if (k === "id" && !v) { - return undefined; - } - if (v instanceof Map) { - return Object.fromEntries(v); - } - if (v instanceof AnimTimedEvent) { - v["eventType"] = v.getEventType(); - } - return v; - }; - - const animConfigProps = ["id", "graphic", "frames", "frameTimedEvents", "position", "hue"]; - const animFrameProps = [ - "x", - "y", - "zoomX", - "zoomY", - "angle", - "mirror", - "visible", - "blendType", - "target", - "graphicFrame", - "opacity", - "color", - "tone", - "flash", - "locked", - "priority", - "focus", - ]; - const propSets = [animConfigProps, animFrameProps]; - - // biome-ignore lint/correctness/noUnusedVariables: used in commented code - const animComparator = (a: Element, b: Element) => { - let props: string[]; - for (let p = 0; p < propSets.length; p++) { - props = propSets[p]; - // @ts-expect-error TODO - const ai = props.indexOf(a.key); - if (ai === -1) { - continue; - } - // @ts-expect-error TODO - const bi = props.indexOf(b.key); - - return ai < bi ? -1 : ai > bi ? 1 : 0; - } - - return 0; - }; - - /*for (let ma of moveAnims.keys()) { - const data = moveAnims.get(ma); - (async () => { - await fs.writeFile(`../public/battle-anims/${Moves[ma].toLowerCase().replace(/\_/g, '-')}.json`, stringify(data, { replacer: animReplacer, cmp: animComparator, space: ' ' })); - })(); - } - - for (let ca of chargeAnims.keys()) { - const data = chargeAnims.get(ca); - (async () => { - await fs.writeFile(`../public/battle-anims/${chargeAnimNames[chargeAnimIds.indexOf(ca)].replace(/\_/g, '-')}.json`, stringify(data, { replacer: animReplacer, cmp: animComparator, space: ' ' })); - })(); - } - - for (let cma of commonAnims.keys()) { - const data = commonAnims.get(cma); - (async () => { - await fs.writeFile(`../public/battle-anims/common-${commonAnimNames[commonAnimIds.indexOf(cma)].replace(/\_/g, '-')}.json`, stringify(data, { replacer: animReplacer, cmp: animComparator, space: ' ' })); - })(); - }*/ -} From da7903ab924a124676d87675e209b8d902abbe82 Mon Sep 17 00:00:00 2001 From: fabske0 <192151969+fabske0@users.noreply.github.com> Date: Fri, 15 Aug 2025 17:34:54 +0200 Subject: [PATCH 03/12] [i18n] rename cancel to cancelButton (#6267) rename cancel to cancelButton --- src/ui/party-ui-handler.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/ui/party-ui-handler.ts b/src/ui/party-ui-handler.ts index 566eeee4e44..3101f46f098 100644 --- a/src/ui/party-ui-handler.ts +++ b/src/ui/party-ui-handler.ts @@ -2142,7 +2142,12 @@ class PartyCancelButton extends Phaser.GameObjects.Container { this.partyCancelPb = partyCancelPb; - const partyCancelText = addTextObject(-10, -7, i18next.t("partyUiHandler:cancel"), TextStyle.PARTY_CANCEL_BUTTON); + const partyCancelText = addTextObject( + -10, + -7, + i18next.t("partyUiHandler:cancelButton"), + TextStyle.PARTY_CANCEL_BUTTON, + ); this.add(partyCancelText); } From 8e61b642a3af07c71bf9c45af78deca2d0d73f86 Mon Sep 17 00:00:00 2001 From: fabske0 <192151969+fabske0@users.noreply.github.com> Date: Fri, 15 Aug 2025 19:16:37 +0200 Subject: [PATCH 04/12] [UI/UX Bug] Position runname dynamically (#6271) Fix runname position Co-authored-by: Wlowscha <54003515+Wlowscha@users.noreply.github.com> --- src/ui/run-info-ui-handler.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ui/run-info-ui-handler.ts b/src/ui/run-info-ui-handler.ts index 572b8ccf560..db0790275fc 100644 --- a/src/ui/run-info-ui-handler.ts +++ b/src/ui/run-info-ui-handler.ts @@ -210,7 +210,8 @@ export class RunInfoUiHandler extends UiHandler { this.runContainer.add(headerText); const runName = addTextObject(0, 0, this.runInfo.name, TextStyle.WINDOW); runName.setOrigin(0, 0); - runName.setPositionRelative(headerBg, 60, 4); + const runNameX = headerText.width / 6 + headerText.x + 4; + runName.setPositionRelative(headerBg, runNameX, 4); this.runContainer.add(runName); } From 19af9bdb8b85a94facb56a1af3236d21aac48ebb Mon Sep 17 00:00:00 2001 From: "Amani H." <109637146+xsn34kzx@users.noreply.github.com> Date: Fri, 15 Aug 2025 13:22:59 -0400 Subject: [PATCH 05/12] [Beta] [Bug] Fix Various Nuzlocke-related Issues (#6261) * [Bug] Fix Various Nuzlocke-related Issues * Update encounter-pokemon-utils.ts * Update attempt-capture-phase.ts --------- Co-authored-by: damocleas Co-authored-by: Wlowscha <54003515+Wlowscha@users.noreply.github.com> --- .../encounters/dark-deal-encounter.ts | 1 + .../utils/encounter-pokemon-utils.ts | 18 +++-- src/phases/attempt-capture-phase.ts | 7 +- src/phases/select-biome-phase.ts | 26 +++++--- src/phases/victory-phase.ts | 66 +++++++------------ src/system/achv.ts | 2 + test/challenges/limited-catch.test.ts | 16 +++++ test/challenges/limited-support.test.ts | 2 + 8 files changed, 77 insertions(+), 61 deletions(-) diff --git a/src/data/mystery-encounters/encounters/dark-deal-encounter.ts b/src/data/mystery-encounters/encounters/dark-deal-encounter.ts index d90e207cc9a..820c7823320 100644 --- a/src/data/mystery-encounters/encounters/dark-deal-encounter.ts +++ b/src/data/mystery-encounters/encounters/dark-deal-encounter.ts @@ -99,6 +99,7 @@ export const DarkDealEncounter: MysteryEncounter = MysteryEncounterBuilder.withE MysteryEncounterType.DARK_DEAL, ) .withEncounterTier(MysteryEncounterTier.ROGUE) + .withDisallowedChallenges(Challenges.HARDCORE) .withIntroSpriteConfigs([ { spriteKey: "dark_deal_scientist", diff --git a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts index 7617fb5a89e..0c6a8e25452 100644 --- a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts @@ -673,6 +673,8 @@ export async function catchPokemon( globalScene.gameData.updateSpeciesDexIvs(pokemon.species.getRootSpeciesId(true), pokemon.ivs); return new Promise(resolve => { + const addStatus = new BooleanHolder(true); + applyChallenges(ChallengeType.POKEMON_ADD_TO_PARTY, pokemon, addStatus); const doPokemonCatchMenu = () => { const end = () => { // Ensure the pokemon is in the enemy party in all situations @@ -708,9 +710,7 @@ export async function catchPokemon( }); }; Promise.all([pokemon.hideInfo(), globalScene.gameData.setPokemonCaught(pokemon)]).then(() => { - const addStatus = new BooleanHolder(true); - applyChallenges(ChallengeType.POKEMON_ADD_TO_PARTY, pokemon, addStatus); - if (!addStatus.value) { + if (!(isObtain || addStatus.value)) { removePokemon(); end(); return; @@ -807,10 +807,16 @@ export async function catchPokemon( }; if (showCatchObtainMessage) { + let catchMessage: string; + if (isObtain) { + catchMessage = "battle:pokemonObtained"; + } else if (addStatus.value) { + catchMessage = "battle:pokemonCaught"; + } else { + catchMessage = "battle:pokemonCaughtButChallenge"; + } globalScene.ui.showText( - i18next.t(isObtain ? "battle:pokemonObtained" : "battle:pokemonCaught", { - pokemonName: pokemon.getNameToRender(), - }), + i18next.t(catchMessage, { pokemonName: pokemon.getNameToRender() }), null, doPokemonCatchMenu, 0, diff --git a/src/phases/attempt-capture-phase.ts b/src/phases/attempt-capture-phase.ts index aea39cff294..b34ddb0c59a 100644 --- a/src/phases/attempt-capture-phase.ts +++ b/src/phases/attempt-capture-phase.ts @@ -253,8 +253,11 @@ export class AttemptCapturePhase extends PokemonPhase { globalScene.gameData.updateSpeciesDexIvs(pokemon.species.getRootSpeciesId(true), pokemon.ivs); + const addStatus = new BooleanHolder(true); + applyChallenges(ChallengeType.POKEMON_ADD_TO_PARTY, pokemon, addStatus); + globalScene.ui.showText( - i18next.t("battle:pokemonCaught", { + i18next.t(addStatus.value ? "battle:pokemonCaught" : "battle:pokemonCaughtButChallenge", { pokemonName: getPokemonNameWithAffix(pokemon), }), null, @@ -290,8 +293,6 @@ export class AttemptCapturePhase extends PokemonPhase { }); }; Promise.all([pokemon.hideInfo(), globalScene.gameData.setPokemonCaught(pokemon)]).then(() => { - const addStatus = new BooleanHolder(true); - applyChallenges(ChallengeType.POKEMON_ADD_TO_PARTY, pokemon, addStatus); if (!addStatus.value) { removePokemon(); end(); diff --git a/src/phases/select-biome-phase.ts b/src/phases/select-biome-phase.ts index 4089f0c2852..d02d69fc934 100644 --- a/src/phases/select-biome-phase.ts +++ b/src/phases/select-biome-phase.ts @@ -16,8 +16,10 @@ export class SelectBiomePhase extends BattlePhase { globalScene.resetSeed(); + const gameMode = globalScene.gameMode; const currentBiome = globalScene.arena.biomeType; - const nextWaveIndex = globalScene.currentBattle.waveIndex + 1; + const currentWaveIndex = globalScene.currentBattle.waveIndex; + const nextWaveIndex = currentWaveIndex + 1; const setNextBiome = (nextBiome: BiomeId) => { if (nextWaveIndex % 10 === 1) { @@ -26,6 +28,15 @@ export class SelectBiomePhase extends BattlePhase { applyChallenges(ChallengeType.PARTY_HEAL, healStatus); if (healStatus.value) { globalScene.phaseManager.unshiftNew("PartyHealPhase", false); + } else { + globalScene.phaseManager.unshiftNew( + "SelectModifierPhase", + undefined, + undefined, + gameMode.isFixedBattle(currentWaveIndex) + ? gameMode.getFixedBattle(currentWaveIndex).customModifierRewardSettings + : undefined, + ); } } globalScene.phaseManager.unshiftNew("SwitchBiomePhase", nextBiome); @@ -33,12 +44,12 @@ export class SelectBiomePhase extends BattlePhase { }; if ( - (globalScene.gameMode.isClassic && globalScene.gameMode.isWaveFinal(nextWaveIndex + 9)) || - (globalScene.gameMode.isDaily && globalScene.gameMode.isWaveFinal(nextWaveIndex)) || - (globalScene.gameMode.hasShortBiomes && !(nextWaveIndex % 50)) + (gameMode.isClassic && gameMode.isWaveFinal(nextWaveIndex + 9)) || + (gameMode.isDaily && gameMode.isWaveFinal(nextWaveIndex)) || + (gameMode.hasShortBiomes && !(nextWaveIndex % 50)) ) { setNextBiome(BiomeId.END); - } else if (globalScene.gameMode.hasRandomBiomes) { + } else if (gameMode.hasRandomBiomes) { setNextBiome(this.generateNextBiome(nextWaveIndex)); } else if (Array.isArray(biomeLinks[currentBiome])) { const biomes: BiomeId[] = (biomeLinks[currentBiome] as (BiomeId | [BiomeId, number])[]) @@ -73,9 +84,6 @@ export class SelectBiomePhase extends BattlePhase { } generateNextBiome(waveIndex: number): BiomeId { - if (!(waveIndex % 50)) { - return BiomeId.END; - } - return globalScene.generateRandomBiome(waveIndex); + return waveIndex % 50 === 0 ? BiomeId.END : globalScene.generateRandomBiome(waveIndex); } } diff --git a/src/phases/victory-phase.ts b/src/phases/victory-phase.ts index c0f4a32d7e1..ac567cc99c5 100644 --- a/src/phases/victory-phase.ts +++ b/src/phases/victory-phase.ts @@ -3,13 +3,9 @@ import { globalScene } from "#app/global-scene"; import { modifierTypes } from "#data/data-lists"; import { BattleType } from "#enums/battle-type"; import type { BattlerIndex } from "#enums/battler-index"; -import { ChallengeType } from "#enums/challenge-type"; import { ClassicFixedBossWaves } from "#enums/fixed-boss-waves"; -import type { CustomModifierSettings } from "#modifiers/modifier-type"; import { handleMysteryEncounterVictory } from "#mystery-encounters/encounter-phase-utils"; import { PokemonPhase } from "#phases/pokemon-phase"; -import { applyChallenges } from "#utils/challenge-utils"; -import { BooleanHolder } from "#utils/common"; export class VictoryPhase extends PokemonPhase { public readonly phaseName = "VictoryPhase"; @@ -49,15 +45,19 @@ export class VictoryPhase extends PokemonPhase { if (globalScene.currentBattle.battleType === BattleType.TRAINER) { globalScene.phaseManager.pushNew("TrainerVictoryPhase"); } - if (globalScene.gameMode.isEndless || !globalScene.gameMode.isWaveFinal(globalScene.currentBattle.waveIndex)) { + + const gameMode = globalScene.gameMode; + const currentWaveIndex = globalScene.currentBattle.waveIndex; + + if (gameMode.isEndless || !gameMode.isWaveFinal(currentWaveIndex)) { globalScene.phaseManager.pushNew("EggLapsePhase"); - if (globalScene.gameMode.isClassic) { - switch (globalScene.currentBattle.waveIndex) { + if (gameMode.isClassic) { + switch (currentWaveIndex) { case ClassicFixedBossWaves.RIVAL_1: case ClassicFixedBossWaves.RIVAL_2: // Get event modifiers for this wave timedEventManager - .getFixedBattleEventRewards(globalScene.currentBattle.waveIndex) + .getFixedBattleEventRewards(currentWaveIndex) .map(r => globalScene.phaseManager.pushNew("ModifierRewardPhase", modifierTypes[r])); break; case ClassicFixedBossWaves.EVIL_BOSS_2: @@ -66,59 +66,53 @@ export class VictoryPhase extends PokemonPhase { break; } } - const healStatus = new BooleanHolder(globalScene.currentBattle.waveIndex % 10 === 0); - applyChallenges(ChallengeType.PARTY_HEAL, healStatus); - if (!healStatus.value) { + if (currentWaveIndex % 10) { globalScene.phaseManager.pushNew( "SelectModifierPhase", undefined, undefined, - this.getFixedBattleCustomModifiers(), + gameMode.isFixedBattle(currentWaveIndex) + ? gameMode.getFixedBattle(currentWaveIndex).customModifierRewardSettings + : undefined, ); - } else if (globalScene.gameMode.isDaily) { + } else if (gameMode.isDaily) { globalScene.phaseManager.pushNew("ModifierRewardPhase", modifierTypes.EXP_CHARM); - if ( - globalScene.currentBattle.waveIndex > 10 && - !globalScene.gameMode.isWaveFinal(globalScene.currentBattle.waveIndex) - ) { + if (currentWaveIndex > 10 && !gameMode.isWaveFinal(currentWaveIndex)) { globalScene.phaseManager.pushNew("ModifierRewardPhase", modifierTypes.GOLDEN_POKEBALL); } } else { - const superExpWave = !globalScene.gameMode.isEndless ? (globalScene.offsetGym ? 0 : 20) : 10; - if (globalScene.gameMode.isEndless && globalScene.currentBattle.waveIndex === 10) { + const superExpWave = !gameMode.isEndless ? (globalScene.offsetGym ? 0 : 20) : 10; + if (gameMode.isEndless && currentWaveIndex === 10) { globalScene.phaseManager.pushNew("ModifierRewardPhase", modifierTypes.EXP_SHARE); } - if ( - globalScene.currentBattle.waveIndex <= 750 && - (globalScene.currentBattle.waveIndex <= 500 || globalScene.currentBattle.waveIndex % 30 === superExpWave) - ) { + if (currentWaveIndex <= 750 && (currentWaveIndex <= 500 || currentWaveIndex % 30 === superExpWave)) { globalScene.phaseManager.pushNew( "ModifierRewardPhase", - globalScene.currentBattle.waveIndex % 30 !== superExpWave || globalScene.currentBattle.waveIndex > 250 + currentWaveIndex % 30 !== superExpWave || currentWaveIndex > 250 ? modifierTypes.EXP_CHARM : modifierTypes.SUPER_EXP_CHARM, ); } - if (globalScene.currentBattle.waveIndex <= 150 && !(globalScene.currentBattle.waveIndex % 50)) { + if (currentWaveIndex <= 150 && !(currentWaveIndex % 50)) { globalScene.phaseManager.pushNew("ModifierRewardPhase", modifierTypes.GOLDEN_POKEBALL); } - if (globalScene.gameMode.isEndless && !(globalScene.currentBattle.waveIndex % 50)) { + if (gameMode.isEndless && !(currentWaveIndex % 50)) { globalScene.phaseManager.pushNew( "ModifierRewardPhase", - !(globalScene.currentBattle.waveIndex % 250) ? modifierTypes.VOUCHER_PREMIUM : modifierTypes.VOUCHER_PLUS, + !(currentWaveIndex % 250) ? modifierTypes.VOUCHER_PREMIUM : modifierTypes.VOUCHER_PLUS, ); globalScene.phaseManager.pushNew("AddEnemyBuffModifierPhase"); } } - if (globalScene.gameMode.hasRandomBiomes || globalScene.isNewBiome()) { + if (gameMode.hasRandomBiomes || globalScene.isNewBiome()) { globalScene.phaseManager.pushNew("SelectBiomePhase"); } globalScene.phaseManager.pushNew("NewBattlePhase"); } else { globalScene.currentBattle.battleType = BattleType.CLEAR; - globalScene.score += globalScene.gameMode.getClearScoreBonus(); + globalScene.score += gameMode.getClearScoreBonus(); globalScene.updateScoreText(); globalScene.phaseManager.pushNew("GameOverPhase", true); } @@ -126,18 +120,4 @@ export class VictoryPhase extends PokemonPhase { this.end(); } - - /** - * If this wave is a fixed battle with special custom modifier rewards, - * will pass those settings to the upcoming {@linkcode SelectModifierPhase}`. - */ - getFixedBattleCustomModifiers(): CustomModifierSettings | undefined { - const gameMode = globalScene.gameMode; - const waveIndex = globalScene.currentBattle.waveIndex; - if (gameMode.isFixedBattle(waveIndex)) { - return gameMode.getFixedBattle(waveIndex).customModifierRewardSettings; - } - - return undefined; - } } diff --git a/src/system/achv.ts b/src/system/achv.ts index 8e312e3d590..f238acbda3a 100644 --- a/src/system/achv.ts +++ b/src/system/achv.ts @@ -448,6 +448,8 @@ export function getAchievementDescription(localizationKey: string): string { return i18next.t("achv:FLIP_STATS.description", { context: genderStr }); case "FLIP_INVERSE": return i18next.t("achv:FLIP_INVERSE.description", { context: genderStr }); + case "NUZLOCKE": + return i18next.t("achv:NUZLOCKE.description", { context: genderStr }); case "BREEDERS_IN_SPACE": return i18next.t("achv:BREEDERS_IN_SPACE.description", { context: genderStr, diff --git a/test/challenges/limited-catch.test.ts b/test/challenges/limited-catch.test.ts index 80be52df2fb..b51732305d0 100644 --- a/test/challenges/limited-catch.test.ts +++ b/test/challenges/limited-catch.test.ts @@ -1,8 +1,10 @@ import { AbilityId } from "#enums/ability-id"; import { Challenges } from "#enums/challenges"; import { MoveId } from "#enums/move-id"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { PokeballType } from "#enums/pokeball"; import { SpeciesId } from "#enums/species-id"; +import { runMysteryEncounterToEnd } from "#test/mystery-encounter/encounter-test-utils"; import { GameManager } from "#test/test-utils/game-manager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; @@ -52,4 +54,18 @@ describe("Challenges - Limited Catch", () => { expect(game.scene.getPlayerParty()).toHaveLength(1); }); + + it("should allow gift Pokémon from Mystery Encounters to be added to party", async () => { + game.override + .mysteryEncounterChance(100) + .mysteryEncounter(MysteryEncounterType.THE_POKEMON_SALESMAN) + .startingWave(12); + game.scene.money = 20000; + + await game.challengeMode.runToSummon([SpeciesId.NUZLEAF]); + + await runMysteryEncounterToEnd(game, 1); + + expect(game.scene.getPlayerParty()).toHaveLength(2); + }); }); diff --git a/test/challenges/limited-support.test.ts b/test/challenges/limited-support.test.ts index 5c0eb2bd420..35413220550 100644 --- a/test/challenges/limited-support.test.ts +++ b/test/challenges/limited-support.test.ts @@ -3,6 +3,7 @@ import { Challenges } from "#enums/challenges"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { UiMode } from "#enums/ui-mode"; +import { ExpBoosterModifier } from "#modifiers/modifier"; import { GameManager } from "#test/test-utils/game-manager"; import { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; import Phaser from "phaser"; @@ -75,6 +76,7 @@ describe("Challenges - Limited Support", () => { await game.doKillOpponents(); await game.toNextWave(); + expect(game.scene.getModifiers(ExpBoosterModifier)).toHaveLength(1); expect(playerPokemon).not.toHaveFullHp(); game.move.use(MoveId.SPLASH); From f6b99780fb59f36e651a1f972d7a40e5d7f3cff1 Mon Sep 17 00:00:00 2001 From: SmhMyHead <191356399+SmhMyHead@users.noreply.github.com> Date: Fri, 15 Aug 2025 21:27:16 +0200 Subject: [PATCH 06/12] [UI/UIX] Dex unseen species filter (#5909) * [UI/UIX] Dex unseen species filter * Removed changes to icon visibility rules * Update src/ui/pokedex-ui-handler.ts --------- Co-authored-by: Wlowscha <54003515+Wlowscha@users.noreply.github.com> --- src/ui/pokedex-ui-handler.ts | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/src/ui/pokedex-ui-handler.ts b/src/ui/pokedex-ui-handler.ts index aa2a5cda459..6a6afea9798 100644 --- a/src/ui/pokedex-ui-handler.ts +++ b/src/ui/pokedex-ui-handler.ts @@ -410,6 +410,11 @@ export class PokedexUiHandler extends MessageUiHandler { new DropDownLabel(i18next.t("filterBar:hasHiddenAbility"), undefined, DropDownState.ON), new DropDownLabel(i18next.t("filterBar:noHiddenAbility"), undefined, DropDownState.EXCLUDE), ]; + const seenSpeciesLabels = [ + new DropDownLabel(i18next.t("filterBar:seenSpecies"), undefined, DropDownState.OFF), + new DropDownLabel(i18next.t("filterBar:isSeen"), undefined, DropDownState.ON), + new DropDownLabel(i18next.t("filterBar:isUnseen"), undefined, DropDownState.EXCLUDE), + ]; const eggLabels = [ new DropDownLabel(i18next.t("filterBar:egg"), undefined, DropDownState.OFF), new DropDownLabel(i18next.t("filterBar:eggPurchasable"), undefined, DropDownState.ON), @@ -423,6 +428,7 @@ export class PokedexUiHandler extends MessageUiHandler { new DropDownOption("FAVORITE", favoriteLabels), new DropDownOption("WIN", winLabels), new DropDownOption("HIDDEN_ABILITY", hiddenAbilityLabels), + new DropDownOption("SEEN_SPECIES", seenSpeciesLabels), new DropDownOption("EGG", eggLabels), new DropDownOption("POKERUS", pokerusLabels), ]; @@ -792,13 +798,15 @@ export class PokedexUiHandler extends MessageUiHandler { this.starterSelectMessageBoxContainer.setVisible(!!text?.length); } - isSeen(species: PokemonSpecies, dexEntry: DexEntry): boolean { + isSeen(species: PokemonSpecies, dexEntry: DexEntry, seenFilter?: boolean): boolean { if (dexEntry?.seenAttr) { return true; } - - const starterDexEntry = globalScene.gameData.dexData[this.getStarterSpeciesId(species.speciesId)]; - return !!starterDexEntry?.caughtAttr; + if (!seenFilter) { + const starterDexEntry = globalScene.gameData.dexData[this.getStarterSpeciesId(species.speciesId)]; + return !!starterDexEntry?.caughtAttr; + } + return false; } /** @@ -1617,6 +1625,21 @@ export class PokedexUiHandler extends MessageUiHandler { } }); + // Seen Filter + const dexEntry = globalScene.gameData.dexData[species.speciesId]; + const isItSeen = this.isSeen(species, dexEntry, true); + const fitsSeen = this.filterBar.getVals(DropDownColumn.MISC).some(misc => { + if (misc.val === "SEEN_SPECIES" && misc.state === DropDownState.ON) { + return isItSeen; + } + if (misc.val === "SEEN_SPECIES" && misc.state === DropDownState.EXCLUDE) { + return !isItSeen; + } + if (misc.val === "SEEN_SPECIES" && misc.state === DropDownState.OFF) { + return true; + } + }); + // Egg Purchasable Filter const isEggPurchasable = this.isSameSpeciesEggAvailable(species.speciesId); const fitsEgg = this.filterBar.getVals(DropDownColumn.MISC).some(misc => { @@ -1658,6 +1681,7 @@ export class PokedexUiHandler extends MessageUiHandler { fitsFavorite && fitsWin && fitsHA && + fitsSeen && fitsEgg && fitsPokerus ) { From e0559e03ff2d93d4cb2061d9f1c1de641aa2fe5d Mon Sep 17 00:00:00 2001 From: damocleas Date: Fri, 15 Aug 2025 17:09:51 -0400 Subject: [PATCH 07/12] Update locales --- public/locales | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/locales b/public/locales index ab2716d5440..1ea8f865e30 160000 --- a/public/locales +++ b/public/locales @@ -1 +1 @@ -Subproject commit ab2716d5440c25f73986664aa3f3131821c3c392 +Subproject commit 1ea8f865e30d1940caa0fceeabf37ae2e4689471 From 98809c28bda065b886f3cd944d4e46d6b9a7faf9 Mon Sep 17 00:00:00 2001 From: AJ Fontaine <36677462+Fontbane@users.noreply.github.com> Date: Fri, 15 Aug 2025 18:23:13 -0400 Subject: [PATCH 08/12] [Balance] Add TM for Shock Wave (#6274) Add TM for Shock Wave --- src/data/balance/tms.ts | 280 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 280 insertions(+) diff --git a/src/data/balance/tms.ts b/src/data/balance/tms.ts index e194dc4040c..bd7cf401ad1 100644 --- a/src/data/balance/tms.ts +++ b/src/data/balance/tms.ts @@ -45736,6 +45736,285 @@ export const tmSpecies: TmSpecies = { SpeciesId.HISUI_ARCANINE, SpeciesId.HISUI_AVALUGG, ], + [MoveId.SHOCK_WAVE]: [ + SpeciesId.RATTATA, + SpeciesId.RATICATE, + SpeciesId.PIKACHU, + SpeciesId.RAICHU, + SpeciesId.NIDORAN_F, + SpeciesId.NIDORINA, + SpeciesId.NIDOQUEEN, + SpeciesId.NIDORAN_M, + SpeciesId.NIDORINO, + SpeciesId.NIDOKING, + SpeciesId.CLEFAIRY, + SpeciesId.CLEFABLE, + SpeciesId.JIGGLYPUFF, + SpeciesId.WIGGLYTUFF, + SpeciesId.MEOWTH, + SpeciesId.PERSIAN, + SpeciesId.ABRA, + SpeciesId.KADABRA, + SpeciesId.ALAKAZAM, + SpeciesId.MAGNEMITE, + SpeciesId.MAGNETON, + SpeciesId.GRIMER, + SpeciesId.MUK, + SpeciesId.VOLTORB, + SpeciesId.ELECTRODE, + SpeciesId.LICKITUNG, + SpeciesId.KOFFING, + SpeciesId.WEEZING, + SpeciesId.RHYHORN, + SpeciesId.RHYDON, + SpeciesId.CHANSEY, + SpeciesId.TANGELA, + SpeciesId.KANGASKHAN, + SpeciesId.MR_MIME, + SpeciesId.ELECTABUZZ, + SpeciesId.TAUROS, + SpeciesId.LAPRAS, + SpeciesId.JOLTEON, + SpeciesId.PORYGON, + SpeciesId.SNORLAX, + SpeciesId.ZAPDOS, + SpeciesId.DRATINI, + SpeciesId.DRAGONAIR, + SpeciesId.DRAGONITE, + SpeciesId.MEWTWO, + SpeciesId.MEW, + SpeciesId.SENTRET, + SpeciesId.FURRET, + SpeciesId.CHINCHOU, + SpeciesId.LANTURN, + SpeciesId.PICHU, + SpeciesId.CLEFFA, + SpeciesId.IGGLYBUFF, + SpeciesId.TOGEPI, + SpeciesId.TOGETIC, + SpeciesId.MAREEP, + SpeciesId.FLAAFFY, + SpeciesId.AMPHAROS, + SpeciesId.AIPOM, + SpeciesId.MISDREAVUS, + SpeciesId.GIRAFARIG, + SpeciesId.DUNSPARCE, + SpeciesId.SNUBBULL, + SpeciesId.GRANBULL, + SpeciesId.QWILFISH, + SpeciesId.PORYGON2, + SpeciesId.STANTLER, + SpeciesId.ELEKID, + SpeciesId.MILTANK, + SpeciesId.BLISSEY, + SpeciesId.RAIKOU, + SpeciesId.TYRANITAR, + SpeciesId.LUGIA, + SpeciesId.HO_OH, + SpeciesId.CELEBI, + SpeciesId.ZIGZAGOON, + SpeciesId.LINOONE, + SpeciesId.WINGULL, + SpeciesId.PELIPPER, + SpeciesId.RALTS, + SpeciesId.KIRLIA, + SpeciesId.GARDEVOIR, + SpeciesId.SLAKOTH, + SpeciesId.VIGOROTH, + SpeciesId.SLAKING, + SpeciesId.WHISMUR, + SpeciesId.LOUDRED, + SpeciesId.EXPLOUD, + SpeciesId.NOSEPASS, + SpeciesId.SKITTY, + SpeciesId.DELCATTY, + SpeciesId.SABLEYE, + SpeciesId.ARON, + SpeciesId.LAIRON, + SpeciesId.AGGRON, + SpeciesId.ELECTRIKE, + SpeciesId.MANECTRIC, + SpeciesId.PLUSLE, + SpeciesId.MINUN, + SpeciesId.VOLBEAT, + SpeciesId.ILLUMISE, + SpeciesId.GULPIN, + SpeciesId.SWALOT, + SpeciesId.SPOINK, + SpeciesId.GRUMPIG, + SpeciesId.SPINDA, + SpeciesId.ZANGOOSE, + SpeciesId.CASTFORM, + SpeciesId.KECLEON, + SpeciesId.SHUPPET, + SpeciesId.BANETTE, + SpeciesId.CHIMECHO, + SpeciesId.ABSOL, + SpeciesId.REGIROCK, + SpeciesId.REGICE, + SpeciesId.REGISTEEL, + SpeciesId.LATIAS, + SpeciesId.LATIOS, + SpeciesId.KYOGRE, + SpeciesId.GROUDON, + SpeciesId.RAYQUAZA, + SpeciesId.JIRACHI, + SpeciesId.DEOXYS, + SpeciesId.BIDOOF, + SpeciesId.BIBAREL, + SpeciesId.SHINX, + SpeciesId.LUXIO, + SpeciesId.LUXRAY, + SpeciesId.CRANIDOS, + SpeciesId.RAMPARDOS, + SpeciesId.SHIELDON, + SpeciesId.BASTIODON, + SpeciesId.PACHIRISU, + SpeciesId.AMBIPOM, + SpeciesId.DRIFLOON, + SpeciesId.DRIFBLIM, + SpeciesId.BUNEARY, + SpeciesId.LOPUNNY, + SpeciesId.MISMAGIUS, + SpeciesId.GLAMEOW, + SpeciesId.PURUGLY, + SpeciesId.CHINGLING, + SpeciesId.MIME_JR, + SpeciesId.HAPPINY, + SpeciesId.SPIRITOMB, + SpeciesId.MUNCHLAX, + SpeciesId.MAGNEZONE, + SpeciesId.LICKILICKY, + SpeciesId.RHYPERIOR, + SpeciesId.TANGROWTH, + SpeciesId.ELECTIVIRE, + SpeciesId.TOGEKISS, + SpeciesId.PORYGON_Z, + SpeciesId.GALLADE, + SpeciesId.PROBOPASS, + SpeciesId.FROSLASS, + SpeciesId.ROTOM, + SpeciesId.UXIE, + SpeciesId.MESPRIT, + SpeciesId.AZELF, + SpeciesId.DIALGA, + SpeciesId.PALKIA, + SpeciesId.REGIGIGAS, + SpeciesId.GIRATINA, + SpeciesId.DARKRAI, + SpeciesId.ARCEUS, + SpeciesId.VICTINI, + SpeciesId.PATRAT, + SpeciesId.WATCHOG, + SpeciesId.LILLIPUP, + SpeciesId.HERDIER, + SpeciesId.STOUTLAND, + SpeciesId.MUNNA, + SpeciesId.MUSHARNA, + SpeciesId.BLITZLE, + SpeciesId.ZEBSTRIKA, + SpeciesId.WOOBAT, + SpeciesId.SWOOBAT, + SpeciesId.SIGILYPH, + SpeciesId.YAMASK, + SpeciesId.COFAGRIGUS, + SpeciesId.MINCCINO, + SpeciesId.CINCCINO, + SpeciesId.GOTHITA, + SpeciesId.GOTHORITA, + SpeciesId.GOTHITELLE, + SpeciesId.SOLOSIS, + SpeciesId.DUOSION, + SpeciesId.REUNICLUS, + SpeciesId.EMOLGA, + SpeciesId.FRILLISH, + SpeciesId.JELLICENT, + SpeciesId.JOLTIK, + SpeciesId.GALVANTULA, + SpeciesId.KLINK, + SpeciesId.KLANG, + SpeciesId.KLINKLANG, + SpeciesId.EELEKTRIK, + SpeciesId.EELEKTROSS, + SpeciesId.ELGYEM, + SpeciesId.BEHEEYEM, + SpeciesId.LITWICK, + SpeciesId.LAMPENT, + SpeciesId.CHANDELURE, + SpeciesId.AXEW, + SpeciesId.FRAXURE, + SpeciesId.HAXORUS, + SpeciesId.STUNFISK, + SpeciesId.DRUDDIGON, + SpeciesId.GOLETT, + SpeciesId.GOLURK, + SpeciesId.DEINO, + SpeciesId.ZWEILOUS, + SpeciesId.HYDREIGON, + SpeciesId.THUNDURUS, + SpeciesId.ZEKROM, + SpeciesId.MELOETTA, + SpeciesId.GENESECT, + SpeciesId.BRAIXEN, + SpeciesId.DELPHOX, + SpeciesId.ESPURR, + SpeciesId.MEOWSTIC, + SpeciesId.HONEDGE, + SpeciesId.DOUBLADE, + SpeciesId.AEGISLASH, + SpeciesId.SKRELP, + SpeciesId.DRAGALGE, + SpeciesId.HELIOPTILE, + SpeciesId.HELIOLISK, + SpeciesId.DEDENNE, + SpeciesId.GOOMY, + SpeciesId.SLIGGOO, + SpeciesId.GOODRA, + SpeciesId.ZYGARDE, + SpeciesId.HOOPA, + SpeciesId.YUNGOOS, + SpeciesId.GUMSHOOS, + SpeciesId.GRUBBIN, + SpeciesId.CHARJABUG, + SpeciesId.VIKAVOLT, + SpeciesId.PASSIMIAN, + SpeciesId.TURTONATOR, + SpeciesId.TOGEDEMARU, + SpeciesId.DRAMPA, + SpeciesId.KOMMO_O, + SpeciesId.TAPU_KOKO, + SpeciesId.SOLGALEO, + SpeciesId.LUNALA, + SpeciesId.PHEROMOSA, + SpeciesId.XURKITREE, + SpeciesId.CELESTEELA, + SpeciesId.GUZZLORD, + SpeciesId.NECROZMA, + SpeciesId.MAGEARNA, + SpeciesId.NAGANADEL, + SpeciesId.ZERAORA, + SpeciesId.TOXTRICITY, + SpeciesId.MR_RIME, + SpeciesId.REGIELEKI, + SpeciesId.WYRDEER, + SpeciesId.FARIGIRAF, + SpeciesId.DUDUNSPARCE, + SpeciesId.MIRAIDON, + SpeciesId.RAGING_BOLT, + SpeciesId.ALOLA_RATTATA, + SpeciesId.ALOLA_RATICATE, + SpeciesId.ALOLA_RAICHU, + SpeciesId.ALOLA_MEOWTH, + SpeciesId.ALOLA_PERSIAN, + SpeciesId.ALOLA_GRAVELER, + SpeciesId.ALOLA_GOLEM, + SpeciesId.ALOLA_GRIMER, + SpeciesId.ALOLA_MUK, + SpeciesId.GALAR_WEEZING, + SpeciesId.GALAR_MR_MIME, + SpeciesId.HISUI_SLIGGOO, + SpeciesId.HISUI_GOODRA, + ], [MoveId.WATER_PULSE]: [ SpeciesId.SQUIRTLE, SpeciesId.WARTORTLE, @@ -68747,6 +69026,7 @@ export const tmPoolTiers: TmPoolTiers = { [MoveId.LEAF_BLADE]: ModifierTier.ULTRA, [MoveId.DRAGON_DANCE]: ModifierTier.GREAT, [MoveId.ROCK_BLAST]: ModifierTier.GREAT, + [MoveId.SHOCK_WAVE]: ModifierTier.GREAT, [MoveId.WATER_PULSE]: ModifierTier.GREAT, [MoveId.ROOST]: ModifierTier.GREAT, [MoveId.GRAVITY]: ModifierTier.COMMON, From 46c78a05405aacf9cac7592a77ff8782d816aa62 Mon Sep 17 00:00:00 2001 From: Wlowscha <54003515+Wlowscha@users.noreply.github.com> Date: Sat, 16 Aug 2025 00:51:28 +0200 Subject: [PATCH 09/12] [Bug][UI/UX] Bringing mon icon overlays on top correctly (#6272) Bringing mon icon overlays on top correctly --- src/ui/pokedex-mon-container.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/ui/pokedex-mon-container.ts b/src/ui/pokedex-mon-container.ts index cfb8555e6c9..832d7e4bcd6 100644 --- a/src/ui/pokedex-mon-container.ts +++ b/src/ui/pokedex-mon-container.ts @@ -208,6 +208,26 @@ export class PokedexMonContainer extends Phaser.GameObjects.Container { ); this.checkIconId(defaultProps.female, defaultProps.formIndex, defaultProps.shiny, defaultProps.variant); this.add(this.icon); + + [ + this.hiddenAbilityIcon, + this.favoriteIcon, + this.classicWinIcon, + this.candyUpgradeIcon, + this.candyUpgradeOverlayIcon, + this.eggMove1Icon, + this.tmMove1Icon, + this.eggMove2Icon, + this.tmMove2Icon, + this.passive1Icon, + this.passive2Icon, + this.passive1OverlayIcon, + this.passive2OverlayIcon, + ].forEach(icon => { + if (icon) { + this.bringToTop(icon); + } + }); } checkIconId(female, formIndex, shiny, variant) { From 7d0bde460460745ea92275581426f113f7caf50e Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Sat, 16 Aug 2025 15:17:43 -0700 Subject: [PATCH 10/12] [Dev] Remove obsolete ESLint commands from `package.json` --- package.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/package.json b/package.json index 3f523ed5c3e..d3ea890c005 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,6 @@ "test:silent": "vitest run --silent='passed-only' --no-isolate", "test:create": "node scripts/create-test/create-test.js", "typecheck": "tsc --noEmit", - "eslint": "eslint --fix .", - "eslint-ci": "eslint .", "biome": "biome check --write --changed --no-errors-on-unmatched --diagnostic-level=error", "biome-ci": "biome ci --diagnostic-level=error --reporter=github --no-errors-on-unmatched", "docs": "typedoc", From 0b107bcbcd4bbd7a945b7262d7f2c2e01d9f51e2 Mon Sep 17 00:00:00 2001 From: damocleas Date: Sat, 16 Aug 2025 19:09:31 -0400 Subject: [PATCH 11/12] [Balance] Buff OHKO moves from 200 -> 250 Base Power against Bosses (#6278) OHKO moves 200 -> 250 Base Power --- src/data/moves/move.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 5c4061ec388..4b8c4f2129b 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -8512,7 +8512,7 @@ export function initMoves() { .punchingMove(), new AttackMove(MoveId.SCRATCH, PokemonType.NORMAL, MoveCategory.PHYSICAL, 40, 100, 35, -1, 0, 1), new AttackMove(MoveId.VISE_GRIP, PokemonType.NORMAL, MoveCategory.PHYSICAL, 55, 100, 30, -1, 0, 1), - new AttackMove(MoveId.GUILLOTINE, PokemonType.NORMAL, MoveCategory.PHYSICAL, 200, 30, 5, -1, 0, 1) + new AttackMove(MoveId.GUILLOTINE, PokemonType.NORMAL, MoveCategory.PHYSICAL, 250, 30, 5, -1, 0, 1) .attr(OneHitKOAttr) .attr(OneHitKOAccuracyAttr), new ChargingAttackMove(MoveId.RAZOR_WIND, PokemonType.NORMAL, MoveCategory.SPECIAL, 80, 100, 10, -1, 0, 1) @@ -8565,7 +8565,7 @@ export function initMoves() { new AttackMove(MoveId.HORN_ATTACK, PokemonType.NORMAL, MoveCategory.PHYSICAL, 65, 100, 25, -1, 0, 1), new AttackMove(MoveId.FURY_ATTACK, PokemonType.NORMAL, MoveCategory.PHYSICAL, 15, 85, 20, -1, 0, 1) .attr(MultiHitAttr), - new AttackMove(MoveId.HORN_DRILL, PokemonType.NORMAL, MoveCategory.PHYSICAL, 200, 30, 5, -1, 0, 1) + new AttackMove(MoveId.HORN_DRILL, PokemonType.NORMAL, MoveCategory.PHYSICAL, 250, 30, 5, -1, 0, 1) .attr(OneHitKOAttr) .attr(OneHitKOAccuracyAttr), new AttackMove(MoveId.TACKLE, PokemonType.NORMAL, MoveCategory.PHYSICAL, 40, 100, 35, -1, 0, 1), @@ -8746,7 +8746,7 @@ export function initMoves() { .attr(MovePowerMultiplierAttr, (user, target, move) => globalScene.arena.getTerrainType() === TerrainType.GRASSY && target.isGrounded() ? 0.5 : 1) .makesContact(false) .target(MoveTarget.ALL_NEAR_OTHERS), - new AttackMove(MoveId.FISSURE, PokemonType.GROUND, MoveCategory.PHYSICAL, 200, 30, 5, -1, 0, 1) + new AttackMove(MoveId.FISSURE, PokemonType.GROUND, MoveCategory.PHYSICAL, 250, 30, 5, -1, 0, 1) .attr(OneHitKOAttr) .attr(OneHitKOAccuracyAttr) .attr(HitsTagAttr, BattlerTagType.UNDERGROUND) @@ -9528,7 +9528,7 @@ export function initMoves() { new AttackMove(MoveId.SAND_TOMB, PokemonType.GROUND, MoveCategory.PHYSICAL, 35, 85, 15, -1, 0, 3) .attr(TrapAttr, BattlerTagType.SAND_TOMB) .makesContact(false), - new AttackMove(MoveId.SHEER_COLD, PokemonType.ICE, MoveCategory.SPECIAL, 200, 30, 5, -1, 0, 3) + new AttackMove(MoveId.SHEER_COLD, PokemonType.ICE, MoveCategory.SPECIAL, 250, 30, 5, -1, 0, 3) .attr(IceNoEffectTypeAttr) .attr(OneHitKOAttr) .attr(SheerColdAccuracyAttr), From c41a3dc79bd5b3902ca64e2af19f7e25e47daf65 Mon Sep 17 00:00:00 2001 From: damocleas Date: Sat, 16 Aug 2025 19:26:33 -0400 Subject: [PATCH 12/12] [Balance] Adjust Eternatus' Catchrate from 255 -> 45 (#6279) Eternatus Catchrate 255 -> 45 --- src/data/balance/pokemon-species.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/data/balance/pokemon-species.ts b/src/data/balance/pokemon-species.ts index 13269308958..c6c17986257 100644 --- a/src/data/balance/pokemon-species.ts +++ b/src/data/balance/pokemon-species.ts @@ -1505,9 +1505,9 @@ export function initSpecies() { new PokemonForm("Hero of Many Battles", "hero-of-many-battles", PokemonType.FIGHTING, null, 2.9, 210, AbilityId.DAUNTLESS_SHIELD, AbilityId.NONE, AbilityId.NONE, 660, 92, 120, 115, 80, 115, 138, 10, 0, 335, false, "", true), new PokemonForm("Crowned", "crowned", PokemonType.FIGHTING, PokemonType.STEEL, 2.9, 785, AbilityId.DAUNTLESS_SHIELD, AbilityId.NONE, AbilityId.NONE, 700, 92, 120, 140, 80, 140, 128, 10, 0, 360) ), - new PokemonSpecies(SpeciesId.ETERNATUS, 8, false, true, false, "Gigantic Pokémon", PokemonType.POISON, PokemonType.DRAGON, 20, 950, AbilityId.PRESSURE, AbilityId.NONE, AbilityId.NONE, 690, 140, 85, 95, 145, 95, 130, 255, 0, 345, GrowthRate.SLOW, null, false, true, - new PokemonForm("Normal", "", PokemonType.POISON, PokemonType.DRAGON, 20, 950, AbilityId.PRESSURE, AbilityId.NONE, AbilityId.NONE, 690, 140, 85, 95, 145, 95, 130, 255, 0, 345, false, null, true), - new PokemonForm("E-Max", "eternamax", PokemonType.POISON, PokemonType.DRAGON, 100, 999.9, AbilityId.PRESSURE, AbilityId.NONE, AbilityId.NONE, 1125, 255, 115, 250, 125, 250, 130, 255, 0, 345) + new PokemonSpecies(SpeciesId.ETERNATUS, 8, false, true, false, "Gigantic Pokémon", PokemonType.POISON, PokemonType.DRAGON, 20, 950, AbilityId.PRESSURE, AbilityId.NONE, AbilityId.NONE, 690, 140, 85, 95, 145, 95, 130, 45, 0, 345, GrowthRate.SLOW, null, false, true, + new PokemonForm("Normal", "", PokemonType.POISON, PokemonType.DRAGON, 20, 950, AbilityId.PRESSURE, AbilityId.NONE, AbilityId.NONE, 690, 140, 85, 95, 145, 95, 130, 45, 0, 345, false, null, true), + new PokemonForm("E-Max", "eternamax", PokemonType.POISON, PokemonType.DRAGON, 100, 999.9, AbilityId.PRESSURE, AbilityId.NONE, AbilityId.NONE, 1125, 255, 115, 250, 125, 250, 130, 45, 0, 345) ), new PokemonSpecies(SpeciesId.KUBFU, 8, true, false, false, "Wushu Pokémon", PokemonType.FIGHTING, null, 0.6, 12, AbilityId.INNER_FOCUS, AbilityId.NONE, AbilityId.NONE, 385, 60, 90, 60, 53, 50, 72, 3, 50, 77, GrowthRate.SLOW, 87.5, false), new PokemonSpecies(SpeciesId.URSHIFU, 8, true, false, false, "Wushu Pokémon", PokemonType.FIGHTING, PokemonType.DARK, 1.9, 105, AbilityId.UNSEEN_FIST, AbilityId.NONE, AbilityId.NONE, 550, 100, 130, 100, 63, 60, 97, 3, 50, 275, GrowthRate.SLOW, 87.5, false, true,