From 6c39ec721e8b66774df5886b2eeb04eb4633098e Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Thu, 11 Sep 2025 19:47:06 -0700 Subject: [PATCH 01/40] [Dev] Add `build:dev` command to `package.json` --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index ee5b001a589..770e3ce30a9 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "start:podman": "vite --mode development --host 0.0.0.0 --port $PORT", "build": "vite build", "build:beta": "vite build --mode beta", + "build:dev": "vite build --mode development", "preview": "vite preview", "test": "vitest run --no-isolate", "test:cov": "vitest run --coverage --no-isolate", From c458cbb2be477b53c6de72b47f134e9dcde7d258 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Sep 2025 19:54:31 -0700 Subject: [PATCH 02/40] [Deps] Bump vite from 7.0.6 to 7.0.7 (#6546) Bump vite from 7.0.6 to 7.0.7 Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.0.6 to 7.0.7. - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/v7.0.7/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v7.0.7/packages/vite) --- updated-dependencies: - dependency-name: vite dependency-version: 7.0.7 dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- pnpm-lock.yaml | 441 ++++++++++++++++++++++++++----------------------- 2 files changed, 231 insertions(+), 212 deletions(-) diff --git a/package.json b/package.json index 770e3ce30a9..5a47b31ff14 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "typedoc-plugin-coverage": "^4.0.1", "typedoc-plugin-mdn-links": "^5.0.9", "typescript": "^5.8.3", - "vite": "^7.0.6", + "vite": "^7.0.7", "vite-tsconfig-paths": "^5.1.4", "vitest": "^3.2.4", "vitest-canvas-mock": "^0.3.3" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e750095a4c5..0638e3ba49a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -103,11 +103,11 @@ importers: specifier: ^5.8.3 version: 5.8.3 vite: - specifier: ^7.0.6 - version: 7.0.6(@types/node@22.16.5)(yaml@2.8.1) + specifier: ^7.0.7 + version: 7.0.7(@types/node@22.16.5)(yaml@2.8.1) vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@5.8.3)(vite@7.0.6(@types/node@22.16.5)(yaml@2.8.1)) + version: 5.1.4(typescript@5.8.3)(vite@7.0.7(@types/node@22.16.5)(yaml@2.8.1)) vitest: specifier: ^3.2.4 version: 3.2.4(@types/node@22.16.5)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.16.5)(typescript@5.8.3))(yaml@2.8.1) @@ -285,158 +285,158 @@ packages: resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} engines: {node: '>=18'} - '@esbuild/aix-ppc64@0.25.8': - resolution: {integrity: sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==} + '@esbuild/aix-ppc64@0.25.9': + resolution: {integrity: sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.25.8': - resolution: {integrity: sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==} + '@esbuild/android-arm64@0.25.9': + resolution: {integrity: sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.25.8': - resolution: {integrity: sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==} + '@esbuild/android-arm@0.25.9': + resolution: {integrity: sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.25.8': - resolution: {integrity: sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==} + '@esbuild/android-x64@0.25.9': + resolution: {integrity: sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.25.8': - resolution: {integrity: sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==} + '@esbuild/darwin-arm64@0.25.9': + resolution: {integrity: sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.25.8': - resolution: {integrity: sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==} + '@esbuild/darwin-x64@0.25.9': + resolution: {integrity: sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.25.8': - resolution: {integrity: sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==} + '@esbuild/freebsd-arm64@0.25.9': + resolution: {integrity: sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.25.8': - resolution: {integrity: sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==} + '@esbuild/freebsd-x64@0.25.9': + resolution: {integrity: sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.25.8': - resolution: {integrity: sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==} + '@esbuild/linux-arm64@0.25.9': + resolution: {integrity: sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.25.8': - resolution: {integrity: sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==} + '@esbuild/linux-arm@0.25.9': + resolution: {integrity: sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.25.8': - resolution: {integrity: sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==} + '@esbuild/linux-ia32@0.25.9': + resolution: {integrity: sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.25.8': - resolution: {integrity: sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==} + '@esbuild/linux-loong64@0.25.9': + resolution: {integrity: sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.25.8': - resolution: {integrity: sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==} + '@esbuild/linux-mips64el@0.25.9': + resolution: {integrity: sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.25.8': - resolution: {integrity: sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==} + '@esbuild/linux-ppc64@0.25.9': + resolution: {integrity: sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.25.8': - resolution: {integrity: sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==} + '@esbuild/linux-riscv64@0.25.9': + resolution: {integrity: sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.25.8': - resolution: {integrity: sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==} + '@esbuild/linux-s390x@0.25.9': + resolution: {integrity: sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.25.8': - resolution: {integrity: sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==} + '@esbuild/linux-x64@0.25.9': + resolution: {integrity: sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.25.8': - resolution: {integrity: sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==} + '@esbuild/netbsd-arm64@0.25.9': + resolution: {integrity: sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.25.8': - resolution: {integrity: sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==} + '@esbuild/netbsd-x64@0.25.9': + resolution: {integrity: sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.25.8': - resolution: {integrity: sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==} + '@esbuild/openbsd-arm64@0.25.9': + resolution: {integrity: sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.25.8': - resolution: {integrity: sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==} + '@esbuild/openbsd-x64@0.25.9': + resolution: {integrity: sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.25.8': - resolution: {integrity: sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==} + '@esbuild/openharmony-arm64@0.25.9': + resolution: {integrity: sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.25.8': - resolution: {integrity: sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==} + '@esbuild/sunos-x64@0.25.9': + resolution: {integrity: sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.25.8': - resolution: {integrity: sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==} + '@esbuild/win32-arm64@0.25.9': + resolution: {integrity: sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.25.8': - resolution: {integrity: sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==} + '@esbuild/win32-ia32@0.25.9': + resolution: {integrity: sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.25.8': - resolution: {integrity: sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==} + '@esbuild/win32-x64@0.25.9': + resolution: {integrity: sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -612,103 +612,108 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} - '@rollup/rollup-android-arm-eabi@4.46.1': - resolution: {integrity: sha512-oENme6QxtLCqjChRUUo3S6X8hjCXnWmJWnedD7VbGML5GUtaOtAyx+fEEXnBXVf0CBZApMQU0Idwi0FmyxzQhw==} + '@rollup/rollup-android-arm-eabi@4.50.1': + resolution: {integrity: sha512-HJXwzoZN4eYTdD8bVV22DN8gsPCAj3V20NHKOs8ezfXanGpmVPR7kalUHd+Y31IJp9stdB87VKPFbsGY3H/2ag==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.46.1': - resolution: {integrity: sha512-OikvNT3qYTl9+4qQ9Bpn6+XHM+ogtFadRLuT2EXiFQMiNkXFLQfNVppi5o28wvYdHL2s3fM0D/MZJ8UkNFZWsw==} + '@rollup/rollup-android-arm64@4.50.1': + resolution: {integrity: sha512-PZlsJVcjHfcH53mOImyt3bc97Ep3FJDXRpk9sMdGX0qgLmY0EIWxCag6EigerGhLVuL8lDVYNnSo8qnTElO4xw==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.46.1': - resolution: {integrity: sha512-EFYNNGij2WllnzljQDQnlFTXzSJw87cpAs4TVBAWLdkvic5Uh5tISrIL6NRcxoh/b2EFBG/TK8hgRrGx94zD4A==} + '@rollup/rollup-darwin-arm64@4.50.1': + resolution: {integrity: sha512-xc6i2AuWh++oGi4ylOFPmzJOEeAa2lJeGUGb4MudOtgfyyjr4UPNK+eEWTPLvmPJIY/pgw6ssFIox23SyrkkJw==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.46.1': - resolution: {integrity: sha512-ZaNH06O1KeTug9WI2+GRBE5Ujt9kZw4a1+OIwnBHal92I8PxSsl5KpsrPvthRynkhMck4XPdvY0z26Cym/b7oA==} + '@rollup/rollup-darwin-x64@4.50.1': + resolution: {integrity: sha512-2ofU89lEpDYhdLAbRdeyz/kX3Y2lpYc6ShRnDjY35bZhd2ipuDMDi6ZTQ9NIag94K28nFMofdnKeHR7BT0CATw==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.46.1': - resolution: {integrity: sha512-n4SLVebZP8uUlJ2r04+g2U/xFeiQlw09Me5UFqny8HGbARl503LNH5CqFTb5U5jNxTouhRjai6qPT0CR5c/Iig==} + '@rollup/rollup-freebsd-arm64@4.50.1': + resolution: {integrity: sha512-wOsE6H2u6PxsHY/BeFHA4VGQN3KUJFZp7QJBmDYI983fgxq5Th8FDkVuERb2l9vDMs1D5XhOrhBrnqcEY6l8ZA==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.46.1': - resolution: {integrity: sha512-8vu9c02F16heTqpvo3yeiu7Vi1REDEC/yES/dIfq3tSXe6mLndiwvYr3AAvd1tMNUqE9yeGYa5w7PRbI5QUV+w==} + '@rollup/rollup-freebsd-x64@4.50.1': + resolution: {integrity: sha512-A/xeqaHTlKbQggxCqispFAcNjycpUEHP52mwMQZUNqDUJFFYtPHCXS1VAG29uMlDzIVr+i00tSFWFLivMcoIBQ==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.46.1': - resolution: {integrity: sha512-K4ncpWl7sQuyp6rWiGUvb6Q18ba8mzM0rjWJ5JgYKlIXAau1db7hZnR0ldJvqKWWJDxqzSLwGUhA4jp+KqgDtQ==} + '@rollup/rollup-linux-arm-gnueabihf@4.50.1': + resolution: {integrity: sha512-54v4okehwl5TaSIkpp97rAHGp7t3ghinRd/vyC1iXqXMfjYUTm7TfYmCzXDoHUPTTf36L8pr0E7YsD3CfB3ZDg==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.46.1': - resolution: {integrity: sha512-YykPnXsjUjmXE6j6k2QBBGAn1YsJUix7pYaPLK3RVE0bQL2jfdbfykPxfF8AgBlqtYbfEnYHmLXNa6QETjdOjQ==} + '@rollup/rollup-linux-arm-musleabihf@4.50.1': + resolution: {integrity: sha512-p/LaFyajPN/0PUHjv8TNyxLiA7RwmDoVY3flXHPSzqrGcIp/c2FjwPPP5++u87DGHtw+5kSH5bCJz0mvXngYxw==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.46.1': - resolution: {integrity: sha512-kKvqBGbZ8i9pCGW3a1FH3HNIVg49dXXTsChGFsHGXQaVJPLA4f/O+XmTxfklhccxdF5FefUn2hvkoGJH0ScWOA==} + '@rollup/rollup-linux-arm64-gnu@4.50.1': + resolution: {integrity: sha512-2AbMhFFkTo6Ptna1zO7kAXXDLi7H9fGTbVaIq2AAYO7yzcAsuTNWPHhb2aTA6GPiP+JXh85Y8CiS54iZoj4opw==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.46.1': - resolution: {integrity: sha512-zzX5nTw1N1plmqC9RGC9vZHFuiM7ZP7oSWQGqpbmfjK7p947D518cVK1/MQudsBdcD84t6k70WNczJOct6+hdg==} + '@rollup/rollup-linux-arm64-musl@4.50.1': + resolution: {integrity: sha512-Cgef+5aZwuvesQNw9eX7g19FfKX5/pQRIyhoXLCiBOrWopjo7ycfB292TX9MDcDijiuIJlx1IzJz3IoCPfqs9w==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loongarch64-gnu@4.46.1': - resolution: {integrity: sha512-O8CwgSBo6ewPpktFfSDgB6SJN9XDcPSvuwxfejiddbIC/hn9Tg6Ai0f0eYDf3XvB/+PIWzOQL+7+TZoB8p9Yuw==} + '@rollup/rollup-linux-loongarch64-gnu@4.50.1': + resolution: {integrity: sha512-RPhTwWMzpYYrHrJAS7CmpdtHNKtt2Ueo+BlLBjfZEhYBhK00OsEqM08/7f+eohiF6poe0YRDDd8nAvwtE/Y62Q==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-ppc64-gnu@4.46.1': - resolution: {integrity: sha512-JnCfFVEKeq6G3h3z8e60kAp8Rd7QVnWCtPm7cxx+5OtP80g/3nmPtfdCXbVl063e3KsRnGSKDHUQMydmzc/wBA==} + '@rollup/rollup-linux-ppc64-gnu@4.50.1': + resolution: {integrity: sha512-eSGMVQw9iekut62O7eBdbiccRguuDgiPMsw++BVUg+1K7WjZXHOg/YOT9SWMzPZA+w98G+Fa1VqJgHZOHHnY0Q==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.46.1': - resolution: {integrity: sha512-dVxuDqS237eQXkbYzQQfdf/njgeNw6LZuVyEdUaWwRpKHhsLI+y4H/NJV8xJGU19vnOJCVwaBFgr936FHOnJsQ==} + '@rollup/rollup-linux-riscv64-gnu@4.50.1': + resolution: {integrity: sha512-S208ojx8a4ciIPrLgazF6AgdcNJzQE4+S9rsmOmDJkusvctii+ZvEuIC4v/xFqzbuP8yDjn73oBlNDgF6YGSXQ==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-riscv64-musl@4.46.1': - resolution: {integrity: sha512-CvvgNl2hrZrTR9jXK1ye0Go0HQRT6ohQdDfWR47/KFKiLd5oN5T14jRdUVGF4tnsN8y9oSfMOqH6RuHh+ck8+w==} + '@rollup/rollup-linux-riscv64-musl@4.50.1': + resolution: {integrity: sha512-3Ag8Ls1ggqkGUvSZWYcdgFwriy2lWo+0QlYgEFra/5JGtAd6C5Hw59oojx1DeqcA2Wds2ayRgvJ4qxVTzCHgzg==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.46.1': - resolution: {integrity: sha512-x7ANt2VOg2565oGHJ6rIuuAon+A8sfe1IeUx25IKqi49OjSr/K3awoNqr9gCwGEJo9OuXlOn+H2p1VJKx1psxA==} + '@rollup/rollup-linux-s390x-gnu@4.50.1': + resolution: {integrity: sha512-t9YrKfaxCYe7l7ldFERE1BRg/4TATxIg+YieHQ966jwvo7ddHJxPj9cNFWLAzhkVsbBvNA4qTbPVNsZKBO4NSg==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.46.1': - resolution: {integrity: sha512-9OADZYryz/7E8/qt0vnaHQgmia2Y0wrjSSn1V/uL+zw/i7NUhxbX4cHXdEQ7dnJgzYDS81d8+tf6nbIdRFZQoQ==} + '@rollup/rollup-linux-x64-gnu@4.50.1': + resolution: {integrity: sha512-MCgtFB2+SVNuQmmjHf+wfI4CMxy3Tk8XjA5Z//A0AKD7QXUYFMQcns91K6dEHBvZPCnhJSyDWLApk40Iq/H3tA==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.46.1': - resolution: {integrity: sha512-NuvSCbXEKY+NGWHyivzbjSVJi68Xfq1VnIvGmsuXs6TCtveeoDRKutI5vf2ntmNnVq64Q4zInet0UDQ+yMB6tA==} + '@rollup/rollup-linux-x64-musl@4.50.1': + resolution: {integrity: sha512-nEvqG+0jeRmqaUMuwzlfMKwcIVffy/9KGbAGyoa26iu6eSngAYQ512bMXuqqPrlTyfqdlB9FVINs93j534UJrg==} cpu: [x64] os: [linux] - '@rollup/rollup-win32-arm64-msvc@4.46.1': - resolution: {integrity: sha512-mWz+6FSRb82xuUMMV1X3NGiaPFqbLN9aIueHleTZCc46cJvwTlvIh7reQLk4p97dv0nddyewBhwzryBHH7wtPw==} + '@rollup/rollup-openharmony-arm64@4.50.1': + resolution: {integrity: sha512-RDsLm+phmT3MJd9SNxA9MNuEAO/J2fhW8GXk62G/B4G7sLVumNFbRwDL6v5NrESb48k+QMqdGbHgEtfU0LCpbA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.50.1': + resolution: {integrity: sha512-hpZB/TImk2FlAFAIsoElM3tLzq57uxnGYwplg6WDyAxbYczSi8O2eQ+H2Lx74504rwKtZ3N2g4bCUkiamzS6TQ==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.46.1': - resolution: {integrity: sha512-7Thzy9TMXDw9AU4f4vsLNBxh7/VOKuXi73VH3d/kHGr0tZ3x/ewgL9uC7ojUKmH1/zvmZe2tLapYcZllk3SO8Q==} + '@rollup/rollup-win32-ia32-msvc@4.50.1': + resolution: {integrity: sha512-SXjv8JlbzKM0fTJidX4eVsH+Wmnp0/WcD8gJxIZyR6Gay5Qcsmdbi9zVtnbkGPG8v2vMR1AD06lGWy5FLMcG7A==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.46.1': - resolution: {integrity: sha512-7GVB4luhFmGUNXXJhH2jJwZCFB3pIOixv2E3s17GQHBFUOQaISlt7aGcQgqvCaDSxTZJUzlK/QJ1FN8S94MrzQ==} + '@rollup/rollup-win32-x64-msvc@4.50.1': + resolution: {integrity: sha512-StxAO/8ts62KZVRAm4JZYq9+NqNsV7RvimNK+YM7ry//zebEH6meuugqW/P5OFUCjyQgui+9fUxT6d5NShvMvA==} cpu: [x64] os: [win32] @@ -1022,8 +1027,8 @@ packages: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} - esbuild@0.25.8: - resolution: {integrity: sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==} + esbuild@0.25.9: + resolution: {integrity: sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==} engines: {node: '>=18'} hasBin: true @@ -1058,8 +1063,9 @@ packages: fast-uri@3.0.6: resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==} - fdir@6.4.6: - resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==} + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} peerDependencies: picomatch: ^3 || ^4 peerDependenciesMeta: @@ -1597,8 +1603,8 @@ packages: engines: {node: '>= 0.4'} hasBin: true - rollup@4.46.1: - resolution: {integrity: sha512-33xGNBsDJAkzt0PvninskHlWnTIPgDtTwhg0U38CUoNP/7H6wI2Cz6dUeoNPbjdTdsYTGuiFFASuUOWovH0SyQ==} + rollup@4.50.1: + resolution: {integrity: sha512-78E9voJHwnXQMiQdiqswVLZwJIzdBKJ1GdI5Zx6XwoFKUIk09/sSrr+05QFzvYb8q6Y9pPV45zzDuYa3907TZA==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -1734,6 +1740,10 @@ packages: resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} engines: {node: '>=12.0.0'} + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + tinypool@1.1.1: resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} engines: {node: ^18.0.0 || >=20.0.0} @@ -1865,8 +1875,8 @@ packages: vite: optional: true - vite@7.0.6: - resolution: {integrity: sha512-MHFiOENNBd+Bd9uvc8GEsIzdkn1JxMmEeYX35tI3fv0sJBUTfW5tQsoaOwuY4KhBI09A3dUJ/DXf2yxPVPUceg==} + vite@7.0.7: + resolution: {integrity: sha512-hc6LujN/EkJHmxeiDJMs0qBontZ1cdBvvoCbWhVjzUFTU329VRyOC46gHNSA8NcOC5yzCeXpwI40tieI3DEZqg==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -2222,82 +2232,82 @@ snapshots: '@csstools/css-tokenizer@3.0.4': {} - '@esbuild/aix-ppc64@0.25.8': + '@esbuild/aix-ppc64@0.25.9': optional: true - '@esbuild/android-arm64@0.25.8': + '@esbuild/android-arm64@0.25.9': optional: true - '@esbuild/android-arm@0.25.8': + '@esbuild/android-arm@0.25.9': optional: true - '@esbuild/android-x64@0.25.8': + '@esbuild/android-x64@0.25.9': optional: true - '@esbuild/darwin-arm64@0.25.8': + '@esbuild/darwin-arm64@0.25.9': optional: true - '@esbuild/darwin-x64@0.25.8': + '@esbuild/darwin-x64@0.25.9': optional: true - '@esbuild/freebsd-arm64@0.25.8': + '@esbuild/freebsd-arm64@0.25.9': optional: true - '@esbuild/freebsd-x64@0.25.8': + '@esbuild/freebsd-x64@0.25.9': optional: true - '@esbuild/linux-arm64@0.25.8': + '@esbuild/linux-arm64@0.25.9': optional: true - '@esbuild/linux-arm@0.25.8': + '@esbuild/linux-arm@0.25.9': optional: true - '@esbuild/linux-ia32@0.25.8': + '@esbuild/linux-ia32@0.25.9': optional: true - '@esbuild/linux-loong64@0.25.8': + '@esbuild/linux-loong64@0.25.9': optional: true - '@esbuild/linux-mips64el@0.25.8': + '@esbuild/linux-mips64el@0.25.9': optional: true - '@esbuild/linux-ppc64@0.25.8': + '@esbuild/linux-ppc64@0.25.9': optional: true - '@esbuild/linux-riscv64@0.25.8': + '@esbuild/linux-riscv64@0.25.9': optional: true - '@esbuild/linux-s390x@0.25.8': + '@esbuild/linux-s390x@0.25.9': optional: true - '@esbuild/linux-x64@0.25.8': + '@esbuild/linux-x64@0.25.9': optional: true - '@esbuild/netbsd-arm64@0.25.8': + '@esbuild/netbsd-arm64@0.25.9': optional: true - '@esbuild/netbsd-x64@0.25.8': + '@esbuild/netbsd-x64@0.25.9': optional: true - '@esbuild/openbsd-arm64@0.25.8': + '@esbuild/openbsd-arm64@0.25.9': optional: true - '@esbuild/openbsd-x64@0.25.8': + '@esbuild/openbsd-x64@0.25.9': optional: true - '@esbuild/openharmony-arm64@0.25.8': + '@esbuild/openharmony-arm64@0.25.9': optional: true - '@esbuild/sunos-x64@0.25.8': + '@esbuild/sunos-x64@0.25.9': optional: true - '@esbuild/win32-arm64@0.25.8': + '@esbuild/win32-arm64@0.25.9': optional: true - '@esbuild/win32-ia32@0.25.8': + '@esbuild/win32-ia32@0.25.9': optional: true - '@esbuild/win32-x64@0.25.8': + '@esbuild/win32-x64@0.25.9': optional: true '@gerrit0/mini-shiki@3.12.2': @@ -2474,64 +2484,67 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@rollup/rollup-android-arm-eabi@4.46.1': + '@rollup/rollup-android-arm-eabi@4.50.1': optional: true - '@rollup/rollup-android-arm64@4.46.1': + '@rollup/rollup-android-arm64@4.50.1': optional: true - '@rollup/rollup-darwin-arm64@4.46.1': + '@rollup/rollup-darwin-arm64@4.50.1': optional: true - '@rollup/rollup-darwin-x64@4.46.1': + '@rollup/rollup-darwin-x64@4.50.1': optional: true - '@rollup/rollup-freebsd-arm64@4.46.1': + '@rollup/rollup-freebsd-arm64@4.50.1': optional: true - '@rollup/rollup-freebsd-x64@4.46.1': + '@rollup/rollup-freebsd-x64@4.50.1': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.46.1': + '@rollup/rollup-linux-arm-gnueabihf@4.50.1': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.46.1': + '@rollup/rollup-linux-arm-musleabihf@4.50.1': optional: true - '@rollup/rollup-linux-arm64-gnu@4.46.1': + '@rollup/rollup-linux-arm64-gnu@4.50.1': optional: true - '@rollup/rollup-linux-arm64-musl@4.46.1': + '@rollup/rollup-linux-arm64-musl@4.50.1': optional: true - '@rollup/rollup-linux-loongarch64-gnu@4.46.1': + '@rollup/rollup-linux-loongarch64-gnu@4.50.1': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.46.1': + '@rollup/rollup-linux-ppc64-gnu@4.50.1': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.46.1': + '@rollup/rollup-linux-riscv64-gnu@4.50.1': optional: true - '@rollup/rollup-linux-riscv64-musl@4.46.1': + '@rollup/rollup-linux-riscv64-musl@4.50.1': optional: true - '@rollup/rollup-linux-s390x-gnu@4.46.1': + '@rollup/rollup-linux-s390x-gnu@4.50.1': optional: true - '@rollup/rollup-linux-x64-gnu@4.46.1': + '@rollup/rollup-linux-x64-gnu@4.50.1': optional: true - '@rollup/rollup-linux-x64-musl@4.46.1': + '@rollup/rollup-linux-x64-musl@4.50.1': optional: true - '@rollup/rollup-win32-arm64-msvc@4.46.1': + '@rollup/rollup-openharmony-arm64@4.50.1': optional: true - '@rollup/rollup-win32-ia32-msvc@4.46.1': + '@rollup/rollup-win32-arm64-msvc@4.50.1': optional: true - '@rollup/rollup-win32-x64-msvc@4.46.1': + '@rollup/rollup-win32-ia32-msvc@4.50.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.50.1': optional: true '@shikijs/engine-oniguruma@3.12.2': @@ -2610,14 +2623,14 @@ snapshots: chai: 5.2.1 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(msw@2.10.4(@types/node@22.16.5)(typescript@5.8.3))(vite@7.0.6(@types/node@22.16.5)(yaml@2.8.1))': + '@vitest/mocker@3.2.4(msw@2.10.4(@types/node@22.16.5)(typescript@5.8.3))(vite@7.0.7(@types/node@22.16.5)(yaml@2.8.1))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: msw: 2.10.4(@types/node@22.16.5)(typescript@5.8.3) - vite: 7.0.6(@types/node@22.16.5)(yaml@2.8.1) + vite: 7.0.7(@types/node@22.16.5)(yaml@2.8.1) '@vitest/pretty-format@3.2.4': dependencies: @@ -2868,34 +2881,34 @@ snapshots: dependencies: es-errors: 1.3.0 - esbuild@0.25.8: + esbuild@0.25.9: optionalDependencies: - '@esbuild/aix-ppc64': 0.25.8 - '@esbuild/android-arm': 0.25.8 - '@esbuild/android-arm64': 0.25.8 - '@esbuild/android-x64': 0.25.8 - '@esbuild/darwin-arm64': 0.25.8 - '@esbuild/darwin-x64': 0.25.8 - '@esbuild/freebsd-arm64': 0.25.8 - '@esbuild/freebsd-x64': 0.25.8 - '@esbuild/linux-arm': 0.25.8 - '@esbuild/linux-arm64': 0.25.8 - '@esbuild/linux-ia32': 0.25.8 - '@esbuild/linux-loong64': 0.25.8 - '@esbuild/linux-mips64el': 0.25.8 - '@esbuild/linux-ppc64': 0.25.8 - '@esbuild/linux-riscv64': 0.25.8 - '@esbuild/linux-s390x': 0.25.8 - '@esbuild/linux-x64': 0.25.8 - '@esbuild/netbsd-arm64': 0.25.8 - '@esbuild/netbsd-x64': 0.25.8 - '@esbuild/openbsd-arm64': 0.25.8 - '@esbuild/openbsd-x64': 0.25.8 - '@esbuild/openharmony-arm64': 0.25.8 - '@esbuild/sunos-x64': 0.25.8 - '@esbuild/win32-arm64': 0.25.8 - '@esbuild/win32-ia32': 0.25.8 - '@esbuild/win32-x64': 0.25.8 + '@esbuild/aix-ppc64': 0.25.9 + '@esbuild/android-arm': 0.25.9 + '@esbuild/android-arm64': 0.25.9 + '@esbuild/android-x64': 0.25.9 + '@esbuild/darwin-arm64': 0.25.9 + '@esbuild/darwin-x64': 0.25.9 + '@esbuild/freebsd-arm64': 0.25.9 + '@esbuild/freebsd-x64': 0.25.9 + '@esbuild/linux-arm': 0.25.9 + '@esbuild/linux-arm64': 0.25.9 + '@esbuild/linux-ia32': 0.25.9 + '@esbuild/linux-loong64': 0.25.9 + '@esbuild/linux-mips64el': 0.25.9 + '@esbuild/linux-ppc64': 0.25.9 + '@esbuild/linux-riscv64': 0.25.9 + '@esbuild/linux-s390x': 0.25.9 + '@esbuild/linux-x64': 0.25.9 + '@esbuild/netbsd-arm64': 0.25.9 + '@esbuild/netbsd-x64': 0.25.9 + '@esbuild/openbsd-arm64': 0.25.9 + '@esbuild/openbsd-x64': 0.25.9 + '@esbuild/openharmony-arm64': 0.25.9 + '@esbuild/sunos-x64': 0.25.9 + '@esbuild/win32-arm64': 0.25.9 + '@esbuild/win32-ia32': 0.25.9 + '@esbuild/win32-x64': 0.25.9 escalade@3.2.0: {} @@ -2921,7 +2934,7 @@ snapshots: fast-uri@3.0.6: {} - fdir@6.4.6(picomatch@4.0.3): + fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 @@ -3467,30 +3480,31 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - rollup@4.46.1: + rollup@4.50.1: dependencies: '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.46.1 - '@rollup/rollup-android-arm64': 4.46.1 - '@rollup/rollup-darwin-arm64': 4.46.1 - '@rollup/rollup-darwin-x64': 4.46.1 - '@rollup/rollup-freebsd-arm64': 4.46.1 - '@rollup/rollup-freebsd-x64': 4.46.1 - '@rollup/rollup-linux-arm-gnueabihf': 4.46.1 - '@rollup/rollup-linux-arm-musleabihf': 4.46.1 - '@rollup/rollup-linux-arm64-gnu': 4.46.1 - '@rollup/rollup-linux-arm64-musl': 4.46.1 - '@rollup/rollup-linux-loongarch64-gnu': 4.46.1 - '@rollup/rollup-linux-ppc64-gnu': 4.46.1 - '@rollup/rollup-linux-riscv64-gnu': 4.46.1 - '@rollup/rollup-linux-riscv64-musl': 4.46.1 - '@rollup/rollup-linux-s390x-gnu': 4.46.1 - '@rollup/rollup-linux-x64-gnu': 4.46.1 - '@rollup/rollup-linux-x64-musl': 4.46.1 - '@rollup/rollup-win32-arm64-msvc': 4.46.1 - '@rollup/rollup-win32-ia32-msvc': 4.46.1 - '@rollup/rollup-win32-x64-msvc': 4.46.1 + '@rollup/rollup-android-arm-eabi': 4.50.1 + '@rollup/rollup-android-arm64': 4.50.1 + '@rollup/rollup-darwin-arm64': 4.50.1 + '@rollup/rollup-darwin-x64': 4.50.1 + '@rollup/rollup-freebsd-arm64': 4.50.1 + '@rollup/rollup-freebsd-x64': 4.50.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.50.1 + '@rollup/rollup-linux-arm-musleabihf': 4.50.1 + '@rollup/rollup-linux-arm64-gnu': 4.50.1 + '@rollup/rollup-linux-arm64-musl': 4.50.1 + '@rollup/rollup-linux-loongarch64-gnu': 4.50.1 + '@rollup/rollup-linux-ppc64-gnu': 4.50.1 + '@rollup/rollup-linux-riscv64-gnu': 4.50.1 + '@rollup/rollup-linux-riscv64-musl': 4.50.1 + '@rollup/rollup-linux-s390x-gnu': 4.50.1 + '@rollup/rollup-linux-x64-gnu': 4.50.1 + '@rollup/rollup-linux-x64-musl': 4.50.1 + '@rollup/rollup-openharmony-arm64': 4.50.1 + '@rollup/rollup-win32-arm64-msvc': 4.50.1 + '@rollup/rollup-win32-ia32-msvc': 4.50.1 + '@rollup/rollup-win32-x64-msvc': 4.50.1 fsevents: 2.3.3 rrweb-cssom@0.8.0: {} @@ -3604,7 +3618,12 @@ snapshots: tinyglobby@0.2.14: dependencies: - fdir: 6.4.6(picomatch@4.0.3) + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 tinypool@1.1.1: {} @@ -3711,7 +3730,7 @@ snapshots: debug: 4.4.1 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.0.6(@types/node@22.16.5)(yaml@2.8.1) + vite: 7.0.7(@types/node@22.16.5)(yaml@2.8.1) transitivePeerDependencies: - '@types/node' - jiti @@ -3726,25 +3745,25 @@ snapshots: - tsx - yaml - vite-tsconfig-paths@5.1.4(typescript@5.8.3)(vite@7.0.6(@types/node@22.16.5)(yaml@2.8.1)): + vite-tsconfig-paths@5.1.4(typescript@5.8.3)(vite@7.0.7(@types/node@22.16.5)(yaml@2.8.1)): dependencies: debug: 4.4.1 globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.8.3) optionalDependencies: - vite: 7.0.6(@types/node@22.16.5)(yaml@2.8.1) + vite: 7.0.7(@types/node@22.16.5)(yaml@2.8.1) transitivePeerDependencies: - supports-color - typescript - vite@7.0.6(@types/node@22.16.5)(yaml@2.8.1): + vite@7.0.7(@types/node@22.16.5)(yaml@2.8.1): dependencies: - esbuild: 0.25.8 - fdir: 6.4.6(picomatch@4.0.3) + esbuild: 0.25.9 + fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 postcss: 8.5.6 - rollup: 4.46.1 - tinyglobby: 0.2.14 + rollup: 4.50.1 + tinyglobby: 0.2.15 optionalDependencies: '@types/node': 22.16.5 fsevents: 2.3.3 @@ -3759,7 +3778,7 @@ snapshots: dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(msw@2.10.4(@types/node@22.16.5)(typescript@5.8.3))(vite@7.0.6(@types/node@22.16.5)(yaml@2.8.1)) + '@vitest/mocker': 3.2.4(msw@2.10.4(@types/node@22.16.5)(typescript@5.8.3))(vite@7.0.7(@types/node@22.16.5)(yaml@2.8.1)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -3777,7 +3796,7 @@ snapshots: tinyglobby: 0.2.14 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.0.6(@types/node@22.16.5)(yaml@2.8.1) + vite: 7.0.7(@types/node@22.16.5)(yaml@2.8.1) vite-node: 3.2.4(@types/node@22.16.5)(yaml@2.8.1) why-is-node-running: 2.3.0 optionalDependencies: From a6fb32b32be639f3e4a3e780847ded7def1e4e5b Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Thu, 11 Sep 2025 21:28:26 -0700 Subject: [PATCH 03/40] [Deps] Update TypeScript from `5.8.3` to `5.9.2` (#6548) --- package.json | 2 +- pnpm-lock.yaml | 84 +++++++++++++++++++++++++------------------------- 2 files changed, 43 insertions(+), 43 deletions(-) diff --git a/package.json b/package.json index 5a47b31ff14..3086ee85747 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "typedoc-github-theme": "^0.3.1", "typedoc-plugin-coverage": "^4.0.1", "typedoc-plugin-mdn-links": "^5.0.9", - "typescript": "^5.8.3", + "typescript": "^5.9.2", "vite": "^7.0.7", "vite-tsconfig-paths": "^5.1.4", "vitest": "^3.2.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0638e3ba49a..09747b23be3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,7 +19,7 @@ importers: version: 4.2.0 i18next: specifier: ^24.2.3 - version: 24.2.3(typescript@5.8.3) + version: 24.2.3(typescript@5.9.2) i18next-browser-languagedetector: specifier: ^8.2.0 version: 8.2.0 @@ -28,7 +28,7 @@ importers: version: 3.0.2 i18next-korean-postposition-processor: specifier: ^1.0.0 - version: 1.0.0(i18next@24.2.3(typescript@5.8.3)) + version: 1.0.0(i18next@24.2.3(typescript@5.9.2)) json-stable-stringify: specifier: ^1.3.0 version: 1.3.0 @@ -59,7 +59,7 @@ importers: version: 22.16.5 '@vitest/coverage-istanbul': specifier: ^3.2.4 - version: 3.2.4(vitest@3.2.4(@types/node@22.16.5)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.16.5)(typescript@5.8.3))(yaml@2.8.1)) + version: 3.2.4(vitest@3.2.4(@types/node@22.16.5)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.16.5)(typescript@5.9.2))(yaml@2.8.1)) '@vitest/expect': specifier: ^3.2.4 version: 3.2.4 @@ -83,37 +83,37 @@ importers: version: 1.12.2 msw: specifier: ^2.10.4 - version: 2.10.4(@types/node@22.16.5)(typescript@5.8.3) + version: 2.10.4(@types/node@22.16.5)(typescript@5.9.2) phaser3spectorjs: specifier: ^0.0.8 version: 0.0.8 typedoc: specifier: 0.28.7 - version: 0.28.7(typescript@5.8.3) + version: 0.28.7(typescript@5.9.2) typedoc-github-theme: specifier: ^0.3.1 - version: 0.3.1(typedoc@0.28.7(typescript@5.8.3)) + version: 0.3.1(typedoc@0.28.7(typescript@5.9.2)) typedoc-plugin-coverage: specifier: ^4.0.1 - version: 4.0.1(typedoc@0.28.7(typescript@5.8.3)) + version: 4.0.1(typedoc@0.28.7(typescript@5.9.2)) typedoc-plugin-mdn-links: specifier: ^5.0.9 - version: 5.0.9(typedoc@0.28.7(typescript@5.8.3)) + version: 5.0.9(typedoc@0.28.7(typescript@5.9.2)) typescript: - specifier: ^5.8.3 - version: 5.8.3 + specifier: ^5.9.2 + version: 5.9.2 vite: specifier: ^7.0.7 version: 7.0.7(@types/node@22.16.5)(yaml@2.8.1) vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@5.8.3)(vite@7.0.7(@types/node@22.16.5)(yaml@2.8.1)) + version: 5.1.4(typescript@5.9.2)(vite@7.0.7(@types/node@22.16.5)(yaml@2.8.1)) vitest: specifier: ^3.2.4 - version: 3.2.4(@types/node@22.16.5)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.16.5)(typescript@5.8.3))(yaml@2.8.1) + version: 3.2.4(@types/node@22.16.5)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.16.5)(typescript@5.9.2))(yaml@2.8.1) vitest-canvas-mock: specifier: ^0.3.3 - version: 0.3.3(vitest@3.2.4(@types/node@22.16.5)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.16.5)(typescript@5.8.3))(yaml@2.8.1)) + version: 0.3.3(vitest@3.2.4(@types/node@22.16.5)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.16.5)(typescript@5.9.2))(yaml@2.8.1)) packages: @@ -1835,8 +1835,8 @@ packages: peerDependencies: typescript: 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x - typescript@5.8.3: - resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + typescript@5.9.2: + resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} engines: {node: '>=14.17'} hasBin: true @@ -2599,7 +2599,7 @@ snapshots: '@types/unist@3.0.3': {} - '@vitest/coverage-istanbul@3.2.4(vitest@3.2.4(@types/node@22.16.5)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.16.5)(typescript@5.8.3))(yaml@2.8.1))': + '@vitest/coverage-istanbul@3.2.4(vitest@3.2.4(@types/node@22.16.5)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.16.5)(typescript@5.9.2))(yaml@2.8.1))': dependencies: '@istanbuljs/schema': 0.1.3 debug: 4.4.1 @@ -2611,7 +2611,7 @@ snapshots: magicast: 0.3.5 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/node@22.16.5)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.16.5)(typescript@5.8.3))(yaml@2.8.1) + vitest: 3.2.4(@types/node@22.16.5)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.16.5)(typescript@5.9.2))(yaml@2.8.1) transitivePeerDependencies: - supports-color @@ -2623,13 +2623,13 @@ snapshots: chai: 5.2.1 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(msw@2.10.4(@types/node@22.16.5)(typescript@5.8.3))(vite@7.0.7(@types/node@22.16.5)(yaml@2.8.1))': + '@vitest/mocker@3.2.4(msw@2.10.4(@types/node@22.16.5)(typescript@5.9.2))(vite@7.0.7(@types/node@22.16.5)(yaml@2.8.1))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - msw: 2.10.4(@types/node@22.16.5)(typescript@5.8.3) + msw: 2.10.4(@types/node@22.16.5)(typescript@5.9.2) vite: 7.0.7(@types/node@22.16.5)(yaml@2.8.1) '@vitest/pretty-format@3.2.4': @@ -3053,19 +3053,19 @@ snapshots: transitivePeerDependencies: - encoding - i18next-korean-postposition-processor@1.0.0(i18next@24.2.3(typescript@5.8.3)): + i18next-korean-postposition-processor@1.0.0(i18next@24.2.3(typescript@5.9.2)): dependencies: - i18next: 24.2.3(typescript@5.8.3) + i18next: 24.2.3(typescript@5.9.2) i18next@22.5.1: dependencies: '@babel/runtime': 7.28.2 - i18next@24.2.3(typescript@5.8.3): + i18next@24.2.3(typescript@5.9.2): dependencies: '@babel/runtime': 7.28.2 optionalDependencies: - typescript: 5.8.3 + typescript: 5.9.2 iconv-lite@0.4.24: dependencies: @@ -3332,7 +3332,7 @@ snapshots: ms@2.1.3: {} - msw@2.10.4(@types/node@22.16.5)(typescript@5.8.3): + msw@2.10.4(@types/node@22.16.5)(typescript@5.9.2): dependencies: '@bundled-es-modules/cookie': 2.0.1 '@bundled-es-modules/statuses': 1.0.1 @@ -3353,7 +3353,7 @@ snapshots: type-fest: 4.41.0 yargs: 17.7.2 optionalDependencies: - typescript: 5.8.3 + typescript: 5.9.2 transitivePeerDependencies: - '@types/node' @@ -3659,9 +3659,9 @@ snapshots: dependencies: punycode: 2.3.1 - tsconfck@3.1.6(typescript@5.8.3): + tsconfck@3.1.6(typescript@5.9.2): optionalDependencies: - typescript: 5.8.3 + typescript: 5.9.2 tsconfig-paths-webpack-plugin@4.2.0: dependencies: @@ -3682,28 +3682,28 @@ snapshots: type-fest@4.41.0: {} - typedoc-github-theme@0.3.1(typedoc@0.28.7(typescript@5.8.3)): + typedoc-github-theme@0.3.1(typedoc@0.28.7(typescript@5.9.2)): dependencies: - typedoc: 0.28.7(typescript@5.8.3) + typedoc: 0.28.7(typescript@5.9.2) - typedoc-plugin-coverage@4.0.1(typedoc@0.28.7(typescript@5.8.3)): + typedoc-plugin-coverage@4.0.1(typedoc@0.28.7(typescript@5.9.2)): dependencies: - typedoc: 0.28.7(typescript@5.8.3) + typedoc: 0.28.7(typescript@5.9.2) - typedoc-plugin-mdn-links@5.0.9(typedoc@0.28.7(typescript@5.8.3)): + typedoc-plugin-mdn-links@5.0.9(typedoc@0.28.7(typescript@5.9.2)): dependencies: - typedoc: 0.28.7(typescript@5.8.3) + typedoc: 0.28.7(typescript@5.9.2) - typedoc@0.28.7(typescript@5.8.3): + typedoc@0.28.7(typescript@5.9.2): dependencies: '@gerrit0/mini-shiki': 3.12.2 lunr: 2.3.9 markdown-it: 14.1.0 minimatch: 9.0.5 - typescript: 5.8.3 + typescript: 5.9.2 yaml: 2.8.1 - typescript@5.8.3: {} + typescript@5.9.2: {} uc.micro@2.1.0: {} @@ -3745,11 +3745,11 @@ snapshots: - tsx - yaml - vite-tsconfig-paths@5.1.4(typescript@5.8.3)(vite@7.0.7(@types/node@22.16.5)(yaml@2.8.1)): + vite-tsconfig-paths@5.1.4(typescript@5.9.2)(vite@7.0.7(@types/node@22.16.5)(yaml@2.8.1)): dependencies: debug: 4.4.1 globrex: 0.1.2 - tsconfck: 3.1.6(typescript@5.8.3) + tsconfck: 3.1.6(typescript@5.9.2) optionalDependencies: vite: 7.0.7(@types/node@22.16.5)(yaml@2.8.1) transitivePeerDependencies: @@ -3769,16 +3769,16 @@ snapshots: fsevents: 2.3.3 yaml: 2.8.1 - vitest-canvas-mock@0.3.3(vitest@3.2.4(@types/node@22.16.5)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.16.5)(typescript@5.8.3))(yaml@2.8.1)): + vitest-canvas-mock@0.3.3(vitest@3.2.4(@types/node@22.16.5)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.16.5)(typescript@5.9.2))(yaml@2.8.1)): dependencies: jest-canvas-mock: 2.5.2 - vitest: 3.2.4(@types/node@22.16.5)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.16.5)(typescript@5.8.3))(yaml@2.8.1) + vitest: 3.2.4(@types/node@22.16.5)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.16.5)(typescript@5.9.2))(yaml@2.8.1) - vitest@3.2.4(@types/node@22.16.5)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.16.5)(typescript@5.8.3))(yaml@2.8.1): + vitest@3.2.4(@types/node@22.16.5)(jsdom@26.1.0)(msw@2.10.4(@types/node@22.16.5)(typescript@5.9.2))(yaml@2.8.1): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(msw@2.10.4(@types/node@22.16.5)(typescript@5.8.3))(vite@7.0.7(@types/node@22.16.5)(yaml@2.8.1)) + '@vitest/mocker': 3.2.4(msw@2.10.4(@types/node@22.16.5)(typescript@5.9.2))(vite@7.0.7(@types/node@22.16.5)(yaml@2.8.1)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 From c7a2c666af0009bb347616cefd9a767fb66b4b46 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Fri, 12 Sep 2025 14:53:27 -0500 Subject: [PATCH 04/40] [Refactor] Remove `isNullOrUndefined` in favor of loose check against null (#6549) * Remove `isNullOrUndefined` in favor of loose check against null * Fix missing method call * Remove isNullOrUndefined import --- src/battle-scene.ts | 19 +++-- src/data/abilities/ability.ts | 37 ++++------ src/data/balance/pokemon-evolutions.ts | 18 ++--- src/data/battle-anims.ts | 8 +- src/data/battler-tags.ts | 8 +- src/data/daily-run.ts | 6 +- src/data/moves/move-utils.ts | 12 +-- src/data/moves/move.ts | 30 ++++---- .../encounters/bug-type-superfan-encounter.ts | 14 ++-- .../encounters/dark-deal-encounter.ts | 4 +- .../encounters/fiery-fallout-encounter.ts | 4 +- .../global-trade-system-encounter.ts | 6 +- .../the-pokemon-salesman-encounter.ts | 8 +- .../encounters/training-session-encounter.ts | 4 +- .../encounters/uncommon-breed-encounter.ts | 4 +- .../encounters/weird-dream-encounter.ts | 16 ++-- .../mystery-encounter-option.ts | 4 +- .../mystery-encounter-requirements.ts | 73 +++++++----------- .../mystery-encounter-save-data.ts | 3 +- .../mystery-encounters/mystery-encounter.ts | 4 +- .../can-learn-move-requirement.ts | 4 +- .../utils/encounter-dialogue-utils.ts | 3 +- .../utils/encounter-phase-utils.ts | 28 +++---- .../utils/encounter-pokemon-utils.ts | 4 +- src/data/pokemon-species.ts | 10 +-- src/data/pokemon/pokemon-data.ts | 37 +++++----- src/data/trainers/trainer-config.ts | 74 +++++++++---------- src/field/arena.ts | 6 +- src/field/mystery-encounter-intro.ts | 9 +-- src/field/pokemon.ts | 31 ++++---- src/game-mode.ts | 6 +- src/modifier/init-modifier-pools.ts | 5 +- src/modifier/modifier-type.ts | 14 +--- src/modifier/modifier.ts | 11 +-- src/phases/faint-phase.ts | 3 +- src/phases/move-effect-phase.ts | 6 +- src/phases/mystery-encounter-phases.ts | 6 +- src/phases/pokemon-anim-phase.ts | 5 +- src/phases/revival-blessing-phase.ts | 8 +- src/phases/select-modifier-phase.ts | 4 +- src/phases/select-starter-phase.ts | 5 +- src/phases/stat-stage-change-phase.ts | 4 +- src/phases/title-phase.ts | 4 +- src/sprites/variant.ts | 3 +- src/system/ribbons/ribbon-methods.ts | 3 +- .../version-migration/versions/v1_0_4.ts | 5 +- .../version-migration/versions/v1_7_0.ts | 5 +- src/timed-event-manager.ts | 27 ++++--- src/ui/containers/pokedex-mon-container.ts | 9 +-- .../handlers/mystery-encounter-ui-handler.ts | 14 ++-- src/ui/handlers/pokedex-page-ui-handler.ts | 8 +- src/ui/handlers/pokedex-scan-ui-handler.ts | 3 +- .../handlers/save-slot-select-ui-handler.ts | 4 +- src/ui/handlers/starter-select-ui-handler.ts | 18 ++--- src/ui/handlers/summary-ui-handler.ts | 15 +--- src/ui/handlers/target-select-ui-handler.ts | 11 +-- src/ui/handlers/test-dialogue-ui-handler.ts | 7 +- src/utils/common.ts | 9 --- test/abilities/healer.test.ts | 3 +- test/eggs/egg.test.ts | 2 +- .../mystery-encounter/encounter-test-utils.ts | 5 +- test/test-utils/game-manager.ts | 5 +- 62 files changed, 315 insertions(+), 410 deletions(-) diff --git a/src/battle-scene.ts b/src/battle-scene.ts index a8e8aa85121..74b44d324d7 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -138,7 +138,6 @@ import { formatMoney, getIvsFromId, isBetween, - isNullOrUndefined, NumberHolder, randomString, randSeedInt, @@ -867,7 +866,7 @@ export class BattleScene extends SceneBase { * this is weird and causes a lot of random jank */ getPokemonById(pokemonId: number | undefined): Pokemon | null { - if (isNullOrUndefined(pokemonId)) { + if (pokemonId == null) { return null; } @@ -1319,7 +1318,7 @@ export class BattleScene extends SceneBase { if ( !this.gameMode.hasTrainers || Overrides.BATTLE_TYPE_OVERRIDE === BattleType.WILD - || (Overrides.DISABLE_STANDARD_TRAINERS_OVERRIDE && isNullOrUndefined(trainerData)) + || (Overrides.DISABLE_STANDARD_TRAINERS_OVERRIDE && trainerData == null) ) { newBattleType = BattleType.WILD; } else { @@ -1383,7 +1382,7 @@ export class BattleScene extends SceneBase { newDouble = false; } - if (!isNullOrUndefined(Overrides.BATTLE_STYLE_OVERRIDE)) { + if (Overrides.BATTLE_STYLE_OVERRIDE != null) { let doubleOverrideForWave: "single" | "double" | null = null; switch (Overrides.BATTLE_STYLE_OVERRIDE) { @@ -1572,7 +1571,7 @@ export class BattleScene extends SceneBase { // Give trainers with specialty types an appropriately-typed form for Wormadam, Rotom, Arceus, Oricorio, Silvally, or Paldean Tauros. !isEggPhase && this.currentBattle?.battleType === BattleType.TRAINER - && !isNullOrUndefined(this.currentBattle.trainer) + && this.currentBattle.trainer != null && this.currentBattle.trainer.config.hasSpecialtyType() ) { if (species.speciesId === SpeciesId.WORMADAM) { @@ -2692,7 +2691,7 @@ export class BattleScene extends SceneBase { } } else if (modifier instanceof FusePokemonModifier) { args.push(this.getPokemonById(modifier.fusePokemonId) as PlayerPokemon); - } else if (modifier instanceof RememberMoveModifier && !isNullOrUndefined(cost)) { + } else if (modifier instanceof RememberMoveModifier && cost != null) { args.push(cost); } @@ -3007,7 +3006,7 @@ export class BattleScene extends SceneBase { } if ( modifier instanceof PokemonHeldItemModifier - && !isNullOrUndefined(modifier.getSpecies()) + && modifier.getSpecies() != null && !this.getPokemonById(modifier.pokemonId)?.hasSpecies(modifier.getSpecies()!) ) { modifiers.splice(m--, 1); @@ -3573,7 +3572,7 @@ export class BattleScene extends SceneBase { // Loading override or session encounter let encounter: MysteryEncounter | null; if ( - !isNullOrUndefined(Overrides.MYSTERY_ENCOUNTER_OVERRIDE) + Overrides.MYSTERY_ENCOUNTER_OVERRIDE != null && allMysteryEncounters.hasOwnProperty(Overrides.MYSTERY_ENCOUNTER_OVERRIDE) ) { encounter = allMysteryEncounters[Overrides.MYSTERY_ENCOUNTER_OVERRIDE]; @@ -3584,7 +3583,7 @@ export class BattleScene extends SceneBase { encounter = allMysteryEncounters[encounterType ?? -1]; return encounter; } else { - encounter = !isNullOrUndefined(encounterType) ? allMysteryEncounters[encounterType] : null; + encounter = encounterType != null ? allMysteryEncounters[encounterType] : null; } // Check for queued encounters first @@ -3643,7 +3642,7 @@ export class BattleScene extends SceneBase { ? MysteryEncounterTier.ULTRA : MysteryEncounterTier.ROGUE; - if (!isNullOrUndefined(Overrides.MYSTERY_ENCOUNTER_TIER_OVERRIDE)) { + if (Overrides.MYSTERY_ENCOUNTER_TIER_OVERRIDE != null) { tier = Overrides.MYSTERY_ENCOUNTER_TIER_OVERRIDE; } diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index 8bb2f30b243..9e5bd8030d6 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -67,7 +67,6 @@ import type { Constructor } from "#utils/common"; import { BooleanHolder, coerceArray, - isNullOrUndefined, NumberHolder, randSeedFloat, randSeedInt, @@ -1040,7 +1039,7 @@ export class PostDefendStatStageChangeAbAttr extends PostDefendAbAttr { if (this.allOthers) { const ally = pokemon.getAlly(); - const otherPokemon = !isNullOrUndefined(ally) ? pokemon.getOpponents().concat([ally]) : pokemon.getOpponents(); + const otherPokemon = ally != null ? pokemon.getOpponents().concat([ally]) : pokemon.getOpponents(); for (const other of otherPokemon) { globalScene.phaseManager.unshiftNew( "StatStageChangePhase", @@ -1473,7 +1472,7 @@ export class PostDefendMoveDisableAbAttr extends PostDefendAbAttr { override canApply({ move, opponent: attacker, pokemon }: PostMoveInteractionAbAttrParams): boolean { return ( - isNullOrUndefined(attacker.getTag(BattlerTagType.DISABLED)) + attacker.getTag(BattlerTagType.DISABLED) == null && move.doesFlagEffectApply({ flag: MoveFlags.MAKES_CONTACT, user: attacker, target: pokemon }) && (this.chance === -1 || pokemon.randBattleSeedInt(100) < this.chance) ); @@ -2810,7 +2809,7 @@ export class PostSummonAllyHealAbAttr extends PostSummonAbAttr { override apply({ pokemon, simulated }: AbAttrBaseParams): void { const target = pokemon.getAlly(); - if (!simulated && !isNullOrUndefined(target)) { + if (!simulated && target != null) { globalScene.phaseManager.unshiftNew( "PokemonHealPhase", target.getBattlerIndex(), @@ -2841,7 +2840,7 @@ export class PostSummonClearAllyStatStagesAbAttr extends PostSummonAbAttr { override apply({ pokemon, simulated }: AbAttrBaseParams): void { const target = pokemon.getAlly(); - if (!simulated && !isNullOrUndefined(target)) { + if (!simulated && target != null) { for (const s of BATTLE_STATS) { target.setStatStage(s, 0); } @@ -2960,13 +2959,13 @@ export class PostSummonHealStatusAbAttr extends PostSummonRemoveEffectAbAttr { public override canApply({ pokemon }: AbAttrBaseParams): boolean { const status = pokemon.status?.effect; - return !isNullOrUndefined(status) && (this.immuneEffects.length === 0 || this.immuneEffects.includes(status)); + return status != null && (this.immuneEffects.length === 0 || this.immuneEffects.includes(status)); } public override apply({ pokemon }: AbAttrBaseParams): void { // TODO: should probably check against simulated... const status = pokemon.status?.effect; - if (!isNullOrUndefined(status)) { + if (status != null) { this.statusHealed = status; pokemon.resetStatus(false); pokemon.updateInfo(); @@ -3102,7 +3101,7 @@ export class PostSummonCopyAllyStatsAbAttr extends PostSummonAbAttr { } const ally = pokemon.getAlly(); - return !(isNullOrUndefined(ally) || ally.getStatStages().every(s => s === 0)); + return !(ally == null || ally.getStatStages().every(s => s === 0)); } override apply({ pokemon, simulated }: AbAttrBaseParams): void { @@ -3110,7 +3109,7 @@ export class PostSummonCopyAllyStatsAbAttr extends PostSummonAbAttr { return; } const ally = pokemon.getAlly(); - if (!isNullOrUndefined(ally)) { + if (ally != null) { for (const s of BATTLE_STATS) { pokemon.setStatStage(s, ally.getStatStage(s)); } @@ -3240,7 +3239,7 @@ export class CommanderAbAttr extends AbAttr { const ally = pokemon.getAlly(); return ( globalScene.currentBattle?.double - && !isNullOrUndefined(ally) + && ally != null && ally.species.speciesId === SpeciesId.DONDOZO && !(ally.isFainted() || ally.getTag(BattlerTagType.COMMANDED)) ); @@ -3284,7 +3283,7 @@ export class PreSwitchOutResetStatusAbAttr extends PreSwitchOutAbAttr { } override canApply({ pokemon }: AbAttrBaseParams): boolean { - return !isNullOrUndefined(pokemon.status); + return pokemon.status != null; } override apply({ pokemon, simulated }: AbAttrBaseParams): void { @@ -3564,7 +3563,7 @@ export class ProtectStatAbAttr extends PreStatStageChangeAbAttr { } override canApply({ stat, cancelled }: PreStatStageChangeAbAttrParams): boolean { - return !cancelled.value && (isNullOrUndefined(this.protectedStat) || stat === this.protectedStat); + return !cancelled.value && (this.protectedStat == null || stat === this.protectedStat); } /** @@ -3800,11 +3799,7 @@ export class ConditionalUserFieldProtectStatAbAttr extends PreStatStageChangeAbA if (!target) { return false; } - return ( - !cancelled.value - && (isNullOrUndefined(this.protectedStat) || stat === this.protectedStat) - && this.condition(target) - ); + return !cancelled.value && (this.protectedStat == null || stat === this.protectedStat) && this.condition(target); } /** @@ -4561,7 +4556,7 @@ export class PostTurnStatusHealAbAttr extends PostTurnAbAttr { } override canApply({ pokemon }: AbAttrBaseParams): boolean { - return !isNullOrUndefined(pokemon.status) && this.effects.includes(pokemon.status.effect) && !pokemon.isFullHp(); + return pokemon.status != null && this.effects.includes(pokemon.status.effect) && !pokemon.isFullHp(); } override apply({ simulated, passive, pokemon }: AbAttrBaseParams): void { @@ -4896,7 +4891,7 @@ export class PostTurnHurtIfSleepingAbAttr extends PostTurnAbAttr { */ export class FetchBallAbAttr extends PostTurnAbAttr { override canApply({ simulated, pokemon }: AbAttrBaseParams): boolean { - return !simulated && !isNullOrUndefined(globalScene.currentBattle.lastUsedPokeball) && !!pokemon.isPlayer; + return !simulated && globalScene.currentBattle.lastUsedPokeball != null && !!pokemon.isPlayer; } /** @@ -6261,7 +6256,7 @@ class ForceSwitchOutHelper { true, 500, ); - if (globalScene.currentBattle.double && !isNullOrUndefined(allyPokemon)) { + if (globalScene.currentBattle.double && allyPokemon != null) { globalScene.redirectPokemonMoves(switchOutTarget, allyPokemon); } } @@ -7113,7 +7108,7 @@ export function initAbilities() { .attr(PostDefendMoveDisableAbAttr, 30) .bypassFaint(), new Ability(AbilityId.HEALER, 5) - .conditionalAttr(pokemon => !isNullOrUndefined(pokemon.getAlly()) && randSeedInt(10) < 3, PostTurnResetStatusAbAttr, true), + .conditionalAttr(pokemon => pokemon.getAlly() != null && randSeedInt(10) < 3, PostTurnResetStatusAbAttr, true), new Ability(AbilityId.FRIEND_GUARD, 5) .attr(AlliedFieldDamageReductionAbAttr, 0.75) .ignorable(), diff --git a/src/data/balance/pokemon-evolutions.ts b/src/data/balance/pokemon-evolutions.ts index d364dc036b1..fd88fc5da37 100644 --- a/src/data/balance/pokemon-evolutions.ts +++ b/src/data/balance/pokemon-evolutions.ts @@ -14,7 +14,7 @@ import { TimeOfDay } from "#enums/time-of-day"; import { WeatherType } from "#enums/weather-type"; import type { Pokemon } from "#field/pokemon"; import type { SpeciesStatBoosterItem, SpeciesStatBoosterModifierType } from "#modifiers/modifier-type"; -import { coerceArray, isNullOrUndefined, randSeedInt } from "#utils/common"; +import { coerceArray, randSeedInt } from "#utils/common"; import { getPokemonSpecies } from "#utils/pokemon-utils"; import { toCamelCase } from "#utils/strings"; import i18next from "i18next"; @@ -128,7 +128,7 @@ export class SpeciesEvolutionCondition { } public get description(): string[] { - if (!isNullOrUndefined(this.desc)) { + if (this.desc != null) { return this.desc; } this.desc = this.data.map(cond => { @@ -161,7 +161,7 @@ export class SpeciesEvolutionCondition { case EvoCondKey.HELD_ITEM: return i18next.t(`pokemonEvolutions:heldItem.${toCamelCase(cond.itemKey)}`); } - }).filter(s => !isNullOrUndefined(s)); // Filter out stringless conditions + }).filter(s => s != null); // Filter out stringless conditions return this.desc; } @@ -233,7 +233,7 @@ export class SpeciesFormEvolution { this.evoFormKey = evoFormKey; this.level = level; this.item = item || EvolutionItem.NONE; - if (!isNullOrUndefined(condition)) { + if (condition != null) { this.condition = new SpeciesEvolutionCondition(...coerceArray(condition)); } this.wildDelay = wildDelay ?? SpeciesWildEvolutionDelay.NONE; @@ -291,8 +291,8 @@ export class SpeciesFormEvolution { return ( pokemon.level >= this.level && // Check form key, using the fusion's form key if we're checking the fusion - (isNullOrUndefined(this.preFormKey) || (forFusion ? pokemon.getFusionFormKey() : pokemon.getFormKey()) === this.preFormKey) && - (isNullOrUndefined(this.condition) || this.condition.conditionsFulfilled(pokemon)) && + (this.preFormKey == null || (forFusion ? pokemon.getFusionFormKey() : pokemon.getFormKey()) === this.preFormKey) && + (this.condition == null || this.condition.conditionsFulfilled(pokemon)) && ((item ?? EvolutionItem.NONE) === (this.item ?? EvolutionItem.NONE)) ); } @@ -305,11 +305,11 @@ export class SpeciesFormEvolution { */ public isValidItemEvolution(pokemon: Pokemon, forFusion = false): boolean { return ( - !isNullOrUndefined(this.item) && + this.item != null && pokemon.level >= this.level && // Check form key, using the fusion's form key if we're checking the fusion - (isNullOrUndefined(this.preFormKey) || (forFusion ? pokemon.getFusionFormKey() : pokemon.getFormKey()) === this.preFormKey) && - (isNullOrUndefined(this.condition) || this.condition.conditionsFulfilled(pokemon)) + (this.preFormKey == null || (forFusion ? pokemon.getFusionFormKey() : pokemon.getFormKey()) === this.preFormKey) && + (this.condition == null || this.condition.conditionsFulfilled(pokemon)) ); } diff --git a/src/data/battle-anims.ts b/src/data/battle-anims.ts index 1dcb7d7eebf..573a1730796 100644 --- a/src/data/battle-anims.ts +++ b/src/data/battle-anims.ts @@ -7,7 +7,7 @@ import { AnimBlendType, AnimFocus, AnimFrameTarget, ChargeAnim, CommonAnim } fro import { MoveFlags } from "#enums/move-flags"; import { MoveId } from "#enums/move-id"; import type { Pokemon } from "#field/pokemon"; -import { coerceArray, getFrameMs, isNullOrUndefined, type nil } from "#utils/common"; +import { coerceArray, getFrameMs, type nil } from "#utils/common"; import { getEnumKeys, getEnumValues } from "#utils/enums"; import { toKebabCase } from "#utils/strings"; import Phaser from "phaser"; @@ -388,7 +388,7 @@ class AnimTimedAddBgEvent extends AnimTimedBgEvent { moveAnim.bgSprite.setAlpha(this.opacity / 255); globalScene.field.add(moveAnim.bgSprite); const fieldPokemon = globalScene.getEnemyPokemon(false) ?? globalScene.getPlayerPokemon(false); - if (!isNullOrUndefined(priority)) { + if (priority != null) { globalScene.field.moveTo(moveAnim.bgSprite as Phaser.GameObjects.GameObject, priority); } else if (fieldPokemon?.isOnField()) { globalScene.field.moveBelow(moveAnim.bgSprite as Phaser.GameObjects.GameObject, fieldPokemon); @@ -524,7 +524,7 @@ export async function initEncounterAnims(encounterAnim: EncounterAnim | Encounte const encounterAnimNames = getEnumKeys(EncounterAnim); const encounterAnimFetches: Promise>[] = []; for (const anim of anims) { - if (encounterAnims.has(anim) && !isNullOrUndefined(encounterAnims.get(anim))) { + if (encounterAnims.has(anim) && encounterAnims.get(anim) != null) { continue; } encounterAnimFetches.push( @@ -1240,7 +1240,7 @@ export abstract class BattleAnim { const graphicIndex = graphicFrameCount++; const moveSprite = sprites[graphicIndex]; - if (!isNullOrUndefined(frame.priority)) { + if (frame.priority != null) { const setSpritePriority = (priority: number) => { if (existingFieldSprites.length > priority) { // Move to specified priority index diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index c495cdaa604..711a2bd0b44 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -49,7 +49,7 @@ import type { TypeBoostTagType, } from "#types/battler-tags"; import type { Mutable } from "#types/type-helpers"; -import { BooleanHolder, coerceArray, getFrameMs, isNullOrUndefined, NumberHolder, toDmgValue } from "#utils/common"; +import { BooleanHolder, coerceArray, getFrameMs, NumberHolder, toDmgValue } from "#utils/common"; import { toCamelCase } from "#utils/strings"; /** @@ -378,7 +378,7 @@ export class DisabledTag extends MoveRestrictionBattlerTag { // Disable fails against struggle or an empty move history // TODO: Confirm if this is redundant given Disable/Cursed Body's disable conditions const move = pokemon.getLastNonVirtualMove(); - if (isNullOrUndefined(move) || move.move === MoveId.STRUGGLE) { + if (move == null || move.move === MoveId.STRUGGLE) { return; } @@ -451,7 +451,7 @@ export class GorillaTacticsTag extends MoveRestrictionBattlerTag { override canAdd(pokemon: Pokemon): boolean { // Choice items ignore struggle, so Gorilla Tactics should too const lastSelectedMove = pokemon.getLastNonVirtualMove(); - return !isNullOrUndefined(lastSelectedMove) && lastSelectedMove.move !== MoveId.STRUGGLE; + return lastSelectedMove != null && lastSelectedMove.move !== MoveId.STRUGGLE; } /** @@ -1305,7 +1305,7 @@ export class EncoreTag extends MoveRestrictionBattlerTag { override lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { if (lapseType === BattlerTagLapseType.CUSTOM) { const encoredMove = pokemon.getMoveset().find(m => m.moveId === this.moveId); - return !isNullOrUndefined(encoredMove) && encoredMove.getPpRatio() > 0; + return encoredMove != null && encoredMove.getPpRatio() > 0; } return super.lapse(pokemon, lapseType); } diff --git a/src/data/daily-run.ts b/src/data/daily-run.ts index addaebdd238..5f49b9adbb0 100644 --- a/src/data/daily-run.ts +++ b/src/data/daily-run.ts @@ -7,7 +7,7 @@ import { BiomeId } from "#enums/biome-id"; import { PartyMemberStrength } from "#enums/party-member-strength"; import { SpeciesId } from "#enums/species-id"; import type { Starter } from "#ui/starter-select-ui-handler"; -import { isNullOrUndefined, randSeedGauss, randSeedInt, randSeedItem } from "#utils/common"; +import { randSeedGauss, randSeedInt, randSeedItem } from "#utils/common"; import { getEnumValues } from "#utils/enums"; import { getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils"; @@ -32,7 +32,7 @@ export function getDailyRunStarters(seed: string): Starter[] { const startingLevel = globalScene.gameMode.getStartingLevel(); const eventStarters = getDailyEventSeedStarters(seed); - if (!isNullOrUndefined(eventStarters)) { + if (eventStarters != null) { starters.push(...eventStarters); return; } @@ -127,7 +127,7 @@ const dailyBiomeWeights: BiomeWeights = { export function getDailyStartingBiome(): BiomeId { const eventBiome = getDailyEventSeedBiome(globalScene.seed); - if (!isNullOrUndefined(eventBiome)) { + if (eventBiome != null) { return eventBiome; } diff --git a/src/data/moves/move-utils.ts b/src/data/moves/move-utils.ts index eedeea53087..1fe0880317b 100644 --- a/src/data/moves/move-utils.ts +++ b/src/data/moves/move-utils.ts @@ -7,7 +7,7 @@ import { PokemonType } from "#enums/pokemon-type"; import type { Pokemon } from "#field/pokemon"; import { applyMoveAttrs } from "#moves/apply-attrs"; import type { Move, MoveTargetSet, UserMoveConditionFunc } from "#moves/move"; -import { isNullOrUndefined, NumberHolder } from "#utils/common"; +import { NumberHolder } from "#utils/common"; /** * Return whether the move targets the field @@ -78,7 +78,7 @@ export function getMoveTargets(user: Pokemon, move: MoveId, replaceTarget?: Move case MoveTarget.OTHER: case MoveTarget.ALL_NEAR_OTHERS: case MoveTarget.ALL_OTHERS: - set = !isNullOrUndefined(ally) ? opponents.concat([ally]) : opponents; + set = ally != null ? opponents.concat([ally]) : opponents; multiple = moveTarget === MoveTarget.ALL_NEAR_OTHERS || moveTarget === MoveTarget.ALL_OTHERS; break; case MoveTarget.NEAR_ENEMY: @@ -95,22 +95,22 @@ export function getMoveTargets(user: Pokemon, move: MoveId, replaceTarget?: Move return { targets: [-1 as BattlerIndex], multiple: false }; case MoveTarget.NEAR_ALLY: case MoveTarget.ALLY: - set = !isNullOrUndefined(ally) ? [ally] : []; + set = ally != null ? [ally] : []; break; case MoveTarget.USER_OR_NEAR_ALLY: case MoveTarget.USER_AND_ALLIES: case MoveTarget.USER_SIDE: - set = !isNullOrUndefined(ally) ? [user, ally] : [user]; + set = ally != null ? [user, ally] : [user]; multiple = moveTarget !== MoveTarget.USER_OR_NEAR_ALLY; break; case MoveTarget.ALL: case MoveTarget.BOTH_SIDES: - set = (!isNullOrUndefined(ally) ? [user, ally] : [user]).concat(opponents); + set = (ally != null ? [user, ally] : [user]).concat(opponents); multiple = true; break; case MoveTarget.CURSE: { - const extraTargets = !isNullOrUndefined(ally) ? [ally] : []; + const extraTargets = ally != null ? [ally] : []; set = user.getTypes(true).includes(PokemonType.GHOST) ? opponents.concat(extraTargets) : [user]; } break; diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 19d145a36d5..91d61c15b8b 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -88,7 +88,7 @@ import type { AttackMoveResult } from "#types/attack-move-result"; import type { Localizable } from "#types/locales"; import type { ChargingMove, MoveAttrMap, MoveAttrString, MoveClassMap, MoveKindString, MoveMessageFunc } from "#types/move-types"; import type { TurnMove } from "#types/turn-move"; -import { BooleanHolder, coerceArray, type Constructor, isNullOrUndefined, NumberHolder, randSeedFloat, randSeedInt, randSeedItem, toDmgValue } from "#utils/common"; +import { BooleanHolder, coerceArray, type Constructor, NumberHolder, randSeedFloat, randSeedInt, randSeedItem, toDmgValue } from "#utils/common"; import { getEnumValues } from "#utils/enums"; import { toCamelCase, toTitleCase } from "#utils/strings"; import i18next from "i18next"; @@ -835,7 +835,7 @@ export abstract class Move implements Localizable { applyAbAttrs("VariableMovePowerAbAttr", abAttrParams); const ally = source.getAlly(); - if (!isNullOrUndefined(ally)) { + if (ally != null) { applyAbAttrs("AllyMoveCategoryPowerBoostAbAttr", {...abAttrParams, pokemon: ally}); } @@ -965,7 +965,7 @@ export abstract class Move implements Localizable { // ...and cannot enhance Pollen Puff when targeting an ally. const ally = user.getAlly(); - const exceptPollenPuffAlly: boolean = this.id === MoveId.POLLEN_PUFF && !isNullOrUndefined(ally) && targets.includes(ally.getBattlerIndex()) + const exceptPollenPuffAlly: boolean = this.id === MoveId.POLLEN_PUFF && ally != null && targets.includes(ally.getBattlerIndex()) return (!restrictSpread || !isMultiTarget) && !this.isChargingMove() @@ -2114,7 +2114,7 @@ export class FlameBurstAttr extends MoveEffectAttr { const targetAlly = target.getAlly(); const cancelled = new BooleanHolder(false); - if (!isNullOrUndefined(targetAlly)) { + if (targetAlly != null) { applyAbAttrs("BlockNonDirectDamageAbAttr", {pokemon: targetAlly, cancelled}); } @@ -2127,7 +2127,7 @@ export class FlameBurstAttr extends MoveEffectAttr { } getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { - return !isNullOrUndefined(target.getAlly()) ? -5 : 0; + return target.getAlly() != null ? -5 : 0; } } @@ -3156,7 +3156,7 @@ export class WeatherInstantChargeAttr extends InstantChargeAttr { super((user, move) => { const currentWeather = globalScene.arena.weather; - if (isNullOrUndefined(currentWeather?.weatherType)) { + if (currentWeather?.weatherType == null) { return false; } else { return !currentWeather?.isEffectSuppressed() @@ -6293,7 +6293,7 @@ export class RevivalBlessingAttr extends MoveEffectAttr { pokemon.heal(Math.min(toDmgValue(0.5 * pokemon.getMaxHp()), pokemon.getMaxHp())); globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:revivalBlessing", { pokemonName: getPokemonNameWithAffix(pokemon) }), 0, true); const allyPokemon = user.getAlly(); - if (globalScene.currentBattle.double && globalScene.getEnemyParty().length > 1 && !isNullOrUndefined(allyPokemon)) { + if (globalScene.currentBattle.double && globalScene.getEnemyParty().length > 1 && allyPokemon != null) { // Handle cases where revived pokemon needs to get switched in on same turn if (allyPokemon.isFainted() || allyPokemon === pokemon) { // Enemy switch phase should be removed and replaced with the revived pkmn switching in @@ -6462,7 +6462,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:fled", { pokemonName: getPokemonNameWithAffix(switchOutTarget) }), null, true, 500); // in double battles redirect potential moves off fled pokemon - if (globalScene.currentBattle.double && !isNullOrUndefined(allyPokemon)) { + if (globalScene.currentBattle.double && allyPokemon != null) { globalScene.redirectPokemonMoves(switchOutTarget, allyPokemon); } } @@ -7122,7 +7122,7 @@ export class CopyMoveAttr extends CallMoveAttr { getCondition(): MoveConditionFunc { return (_user, target, _move) => { const lastMove = this.mirrorMove ? target.getLastNonVirtualMove(false, false)?.move : globalScene.currentBattle.lastMove; - return !isNullOrUndefined(lastMove) && !this.invalidMoves.has(lastMove); + return lastMove != null && !this.invalidMoves.has(lastMove); }; } } @@ -7169,7 +7169,7 @@ export class RepeatMoveAttr extends MoveEffectAttr { && firstTarget !== target.getAlly() ) { const ally = firstTarget.getAlly(); - if (!isNullOrUndefined(ally) && ally.isActive()) { + if (ally != null && ally.isActive()) { moveTargets = [ ally.getBattlerIndex() ]; } } @@ -7476,7 +7476,7 @@ export class SketchAttr extends MoveEffectAttr { } const targetMove = target.getLastNonVirtualMove(); - return !isNullOrUndefined(targetMove) + return targetMove != null && !invalidSketchMoves.has(targetMove.move) && user.getMoveset().every(m => m.moveId !== targetMove.move) }; @@ -7533,7 +7533,7 @@ export class AbilityCopyAttr extends MoveEffectAttr { user.setTempAbility(target.getAbility()); const ally = user.getAlly(); - if (this.copyToPartner && globalScene.currentBattle?.double && !isNullOrUndefined(ally) && ally.hp) { // TODO is this the best way to check that the ally is active? + if (this.copyToPartner && globalScene.currentBattle?.double && ally != null && ally.hp) { // TODO is this the best way to check that the ally is active? globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:copiedTargetAbility", { pokemonName: getPokemonNameWithAffix(ally), targetName: getPokemonNameWithAffix(target), abilityName: allAbilities[target.getAbility().id].name })); ally.setTempAbility(target.getAbility()); } @@ -8054,7 +8054,7 @@ const failIfGhostTypeCondition: MoveConditionFunc = (user: Pokemon, target: Poke const failIfNoTargetHeldItemsCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => target.getHeldItems().filter(i => i.isTransferable)?.length > 0; const attackedByItemMessageFunc = (user: Pokemon, target: Pokemon, move: Move) => { - if (isNullOrUndefined(target)) { // Fix bug when used against targets that have both fainted + if (target == null) { // Fix bug when used against targets that have both fainted return ""; } const heldItems = target.getHeldItems().filter(i => i.isTransferable); @@ -8611,7 +8611,7 @@ export function initMoves() { .attr(AddBattlerTagAttr, BattlerTagType.DISABLED, false, true) .condition((_user, target, _move) => { const lastNonVirtualMove = target.getLastNonVirtualMove(); - return !isNullOrUndefined(lastNonVirtualMove) && lastNonVirtualMove.move !== MoveId.STRUGGLE; + return lastNonVirtualMove != null && lastNonVirtualMove.move !== MoveId.STRUGGLE; }) .ignoresSubstitute() .reflectable(), @@ -9955,7 +9955,7 @@ export function initMoves() { .condition(failOnGravityCondition) .condition((_user, target, _move) => ![ SpeciesId.DIGLETT, SpeciesId.DUGTRIO, SpeciesId.ALOLA_DIGLETT, SpeciesId.ALOLA_DUGTRIO, SpeciesId.SANDYGAST, SpeciesId.PALOSSAND, SpeciesId.WIGLETT, SpeciesId.WUGTRIO ].includes(target.species.speciesId)) .condition((_user, target, _move) => !(target.species.speciesId === SpeciesId.GENGAR && target.getFormKey() === "mega")) - .condition((_user, target, _move) => isNullOrUndefined(target.getTag(BattlerTagType.INGRAIN)) && isNullOrUndefined(target.getTag(BattlerTagType.IGNORE_FLYING))) + .condition((_user, target, _move) => target.getTag(BattlerTagType.INGRAIN) == null && target.getTag(BattlerTagType.IGNORE_FLYING) == null) .attr(AddBattlerTagAttr, BattlerTagType.TELEKINESIS, false, true, 3) .attr(AddBattlerTagAttr, BattlerTagType.FLOATING, false, true, 3) .reflectable(), diff --git a/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts b/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts index cb34190a584..00e98048ada 100644 --- a/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts +++ b/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts @@ -48,7 +48,7 @@ import { getRandomPartyMemberFunc, trainerConfigs } from "#trainers/trainer-conf import { TrainerPartyCompoundTemplate, TrainerPartyTemplate } from "#trainers/trainer-party-template"; import type { OptionSelectItem } from "#ui/abstract-option-select-ui-handler"; import { MoveInfoOverlay } from "#ui/move-info-overlay"; -import { isNullOrUndefined, randSeedInt, randSeedShuffle } from "#utils/common"; +import { randSeedInt, randSeedShuffle } from "#utils/common"; import i18next from "i18next"; /** the i18n namespace for the encounter */ @@ -571,7 +571,7 @@ function getTrainerConfigForWave(waveIndex: number) { .setPartyMemberFunc( 4, getRandomPartyMemberFunc([pool3Mon.species], TrainerSlot.TRAINER, true, p => { - if (!isNullOrUndefined(pool3Mon.formIndex)) { + if (pool3Mon.formIndex != null) { p.formIndex = pool3Mon.formIndex; p.generateAndPopulateMoveset(); p.generateName(); @@ -603,7 +603,7 @@ function getTrainerConfigForWave(waveIndex: number) { .setPartyMemberFunc( 3, getRandomPartyMemberFunc([pool3Mon.species], TrainerSlot.TRAINER, true, p => { - if (!isNullOrUndefined(pool3Mon.formIndex)) { + if (pool3Mon.formIndex != null) { p.formIndex = pool3Mon.formIndex; p.generateAndPopulateMoveset(); p.generateName(); @@ -613,7 +613,7 @@ function getTrainerConfigForWave(waveIndex: number) { .setPartyMemberFunc( 4, getRandomPartyMemberFunc([pool3Mon2.species], TrainerSlot.TRAINER, true, p => { - if (!isNullOrUndefined(pool3Mon2.formIndex)) { + if (pool3Mon2.formIndex != null) { p.formIndex = pool3Mon2.formIndex; p.generateAndPopulateMoveset(); p.generateName(); @@ -648,7 +648,7 @@ function getTrainerConfigForWave(waveIndex: number) { .setPartyMemberFunc( 3, getRandomPartyMemberFunc([pool3Mon.species], TrainerSlot.TRAINER, true, p => { - if (!isNullOrUndefined(pool3Mon.formIndex)) { + if (pool3Mon.formIndex != null) { p.formIndex = pool3Mon.formIndex; p.generateAndPopulateMoveset(); p.generateName(); @@ -687,7 +687,7 @@ function getTrainerConfigForWave(waveIndex: number) { .setPartyMemberFunc( 2, getRandomPartyMemberFunc([pool3Mon.species], TrainerSlot.TRAINER, true, p => { - if (!isNullOrUndefined(pool3Mon.formIndex)) { + if (pool3Mon.formIndex != null) { p.formIndex = pool3Mon.formIndex; p.generateAndPopulateMoveset(); p.generateName(); @@ -697,7 +697,7 @@ function getTrainerConfigForWave(waveIndex: number) { .setPartyMemberFunc( 3, getRandomPartyMemberFunc([pool3Mon2.species], TrainerSlot.TRAINER, true, p => { - if (!isNullOrUndefined(pool3Mon2.formIndex)) { + if (pool3Mon2.formIndex != null) { p.formIndex = pool3Mon2.formIndex; p.generateAndPopulateMoveset(); p.generateName(); diff --git a/src/data/mystery-encounters/encounters/dark-deal-encounter.ts b/src/data/mystery-encounters/encounters/dark-deal-encounter.ts index 65d22bfc6de..426eafb5e67 100644 --- a/src/data/mystery-encounters/encounters/dark-deal-encounter.ts +++ b/src/data/mystery-encounters/encounters/dark-deal-encounter.ts @@ -15,7 +15,7 @@ import { getRandomPlayerPokemon, getRandomSpeciesByStarterCost } from "#mystery- import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter"; import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter"; import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encounter-option"; -import { isNullOrUndefined, randSeedInt } from "#utils/common"; +import { randSeedInt } from "#utils/common"; import { getPokemonSpecies } from "#utils/pokemon-utils"; /** i18n namespace for encounter */ @@ -192,7 +192,7 @@ export const DarkDealEncounter: MysteryEncounter = MysteryEncounterBuilder.withE }; }), }; - if (!isNullOrUndefined(bossSpecies.forms) && bossSpecies.forms.length > 0) { + if (bossSpecies.forms != null && bossSpecies.forms.length > 0) { pokemonConfig.formIndex = 0; } const config: EnemyPartyConfig = { diff --git a/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts b/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts index 1cc31eaa21f..0f37a1fae94 100644 --- a/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts +++ b/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts @@ -45,7 +45,7 @@ import { TypeRequirement, } from "#mystery-encounters/mystery-encounter-requirements"; import { FIRE_RESISTANT_ABILITIES } from "#mystery-encounters/requirement-groups"; -import { isNullOrUndefined, randSeedInt } from "#utils/common"; +import { randSeedInt } from "#utils/common"; import { getPokemonSpecies } from "#utils/pokemon-utils"; /** the i18n namespace for the encounter */ @@ -238,7 +238,7 @@ export const FieryFalloutEncounter: MysteryEncounter = MysteryEncounterBuilder.w // Burn random member const burnable = nonFireTypes.filter( - p => isNullOrUndefined(p.status) || isNullOrUndefined(p.status.effect) || p.status.effect === StatusEffect.NONE, + p => p.status == null || p.status.effect == null || p.status.effect === StatusEffect.NONE, ); if (burnable?.length > 0) { const roll = randSeedInt(burnable.length); diff --git a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts index 823f016029e..e2166e99f6a 100644 --- a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts +++ b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts @@ -43,7 +43,7 @@ import { PartySizeRequirement } from "#mystery-encounters/mystery-encounter-requ import { PokemonData } from "#system/pokemon-data"; import { MusicPreference } from "#system/settings"; import type { OptionSelectItem } from "#ui/abstract-option-select-ui-handler"; -import { isNullOrUndefined, NumberHolder, randInt, randSeedInt, randSeedItem, randSeedShuffle } from "#utils/common"; +import { NumberHolder, randInt, randSeedInt, randSeedItem, randSeedShuffle } from "#utils/common"; import { getEnumKeys } from "#utils/enums"; import { getRandomLocaleEntry } from "#utils/i18n"; import { getPokemonSpecies } from "#utils/pokemon-utils"; @@ -537,7 +537,7 @@ function generateTradeOption(alreadyUsedSpecies: PokemonSpecies[], originalBst?: bstCap = originalBst + 100; bstMin = originalBst - 100; } - while (isNullOrUndefined(newSpecies)) { + while (newSpecies == null) { // Get all non-legendary species that fall within the Bst range requirements let validSpecies = allSpecies.filter(s => { const isLegendaryOrMythical = s.legendary || s.subLegendary || s.mythical; @@ -550,7 +550,7 @@ function generateTradeOption(alreadyUsedSpecies: PokemonSpecies[], originalBst?: if (validSpecies?.length > 20) { validSpecies = randSeedShuffle(validSpecies); newSpecies = validSpecies.pop(); - while (isNullOrUndefined(newSpecies) || alreadyUsedSpecies.includes(newSpecies)) { + while (newSpecies == null || alreadyUsedSpecies.includes(newSpecies)) { newSpecies = validSpecies.pop(); } } else { diff --git a/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts b/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts index 292c866c0ee..51efa0c7586 100644 --- a/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts @@ -28,7 +28,7 @@ import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter"; import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encounter-option"; import { MoneyRequirement } from "#mystery-encounters/mystery-encounter-requirements"; import { PokemonData } from "#system/pokemon-data"; -import { isNullOrUndefined, randSeedInt, randSeedItem } from "#utils/common"; +import { randSeedInt, randSeedItem } from "#utils/common"; import { getPokemonSpecies } from "#utils/pokemon-utils"; /** the i18n namespace for this encounter */ @@ -81,7 +81,7 @@ export const ThePokemonSalesmanEncounter: MysteryEncounter = MysteryEncounterBui let tries = 0; // Reroll any species that don't have HAs - while ((isNullOrUndefined(species.abilityHidden) || species.abilityHidden === AbilityId.NONE) && tries < 5) { + while ((species.abilityHidden == null || species.abilityHidden === AbilityId.NONE) && tries < 5) { species = getSalesmanSpeciesOffer(); tries++; } @@ -110,7 +110,7 @@ export const ThePokemonSalesmanEncounter: MysteryEncounter = MysteryEncounterBui */ if ( r === 0 - || ((isNullOrUndefined(species.abilityHidden) || species.abilityHidden === AbilityId.NONE) + || ((species.abilityHidden == null || species.abilityHidden === AbilityId.NONE) && validEventEncounters.length === 0) ) { // If you roll 1%, give shiny Magikarp with random variant @@ -118,7 +118,7 @@ export const ThePokemonSalesmanEncounter: MysteryEncounter = MysteryEncounterBui pokemon = new PlayerPokemon(species, 5, 2, undefined, undefined, true); } else if ( validEventEncounters.length > 0 - && (r <= EVENT_THRESHOLD || isNullOrUndefined(species.abilityHidden) || species.abilityHidden === AbilityId.NONE) + && (r <= EVENT_THRESHOLD || species.abilityHidden == null || species.abilityHidden === AbilityId.NONE) ) { tries = 0; do { diff --git a/src/data/mystery-encounters/encounters/training-session-encounter.ts b/src/data/mystery-encounters/encounters/training-session-encounter.ts index 033a54cc5f5..1f3778a5d2c 100644 --- a/src/data/mystery-encounters/encounters/training-session-encounter.ts +++ b/src/data/mystery-encounters/encounters/training-session-encounter.ts @@ -28,7 +28,7 @@ import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encou import { PokemonData } from "#system/pokemon-data"; import type { HeldModifierConfig } from "#types/held-modifier-config"; import type { OptionSelectItem } from "#ui/abstract-option-select-ui-handler"; -import { isNullOrUndefined, randSeedShuffle } from "#utils/common"; +import { randSeedShuffle } from "#utils/common"; import { getEnumValues } from "#utils/enums"; import i18next from "i18next"; @@ -324,7 +324,7 @@ export const TrainingSessionEncounter: MysteryEncounter = MysteryEncounterBuilde // Only update the fusion's dex data if the Pokemon is already caught in dex (ignore rentals) const rootFusionSpecies = playerPokemon.fusionSpecies?.getRootSpeciesId(); if ( - !isNullOrUndefined(rootFusionSpecies) + rootFusionSpecies != null && speciesStarterCosts.hasOwnProperty(rootFusionSpecies) && !!globalScene.gameData.dexData[rootFusionSpecies].caughtAttr ) { diff --git a/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts b/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts index 7bbc4a57757..cd61a6852f7 100644 --- a/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts +++ b/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts @@ -32,7 +32,7 @@ import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encou import { MoveRequirement, PersistentModifierRequirement } from "#mystery-encounters/mystery-encounter-requirements"; import { CHARMING_MOVES } from "#mystery-encounters/requirement-groups"; import { PokemonData } from "#system/pokemon-data"; -import { isNullOrUndefined, randSeedInt } from "#utils/common"; +import { randSeedInt } from "#utils/common"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounters/uncommonBreed"; @@ -167,7 +167,7 @@ export const UncommonBreedEncounter: MysteryEncounter = MysteryEncounterBuilder. const encounter = globalScene.currentBattle.mysteryEncounter!; const eggMove = encounter.misc.eggMove; - if (!isNullOrUndefined(eggMove)) { + if (eggMove != null) { // Check what type of move the egg move is to determine target const pokemonMove = new PokemonMove(eggMove); const move = pokemonMove.getMove(); diff --git a/src/data/mystery-encounters/encounters/weird-dream-encounter.ts b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts index 1fcbd2961d1..abd81fb92ea 100644 --- a/src/data/mystery-encounters/encounters/weird-dream-encounter.ts +++ b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts @@ -41,7 +41,7 @@ import { PokemonData } from "#system/pokemon-data"; import { trainerConfigs } from "#trainers/trainer-config"; import { TrainerPartyTemplate } from "#trainers/trainer-party-template"; import type { HeldModifierConfig } from "#types/held-modifier-config"; -import { isNullOrUndefined, NumberHolder, randSeedInt, randSeedShuffle } from "#utils/common"; +import { NumberHolder, randSeedInt, randSeedShuffle } from "#utils/common"; import { getPokemonSpecies } from "#utils/pokemon-utils"; /** i18n namespace for encounter */ @@ -634,7 +634,7 @@ function getTransformedSpecies( alreadyUsedSpecies: PokemonSpecies[], ): PokemonSpecies { let newSpecies: PokemonSpecies | undefined; - while (isNullOrUndefined(newSpecies)) { + while (newSpecies == null) { const bstCap = originalBst + bstSearchRange[1]; const bstMin = Math.max(originalBst + bstSearchRange[0], 0); @@ -655,7 +655,7 @@ function getTransformedSpecies( if (validSpecies?.length > 20) { validSpecies = randSeedShuffle(validSpecies); newSpecies = validSpecies.pop(); - while (isNullOrUndefined(newSpecies) || alreadyUsedSpecies.includes(newSpecies)) { + while (newSpecies == null || alreadyUsedSpecies.includes(newSpecies)) { newSpecies = validSpecies.pop(); } } else { @@ -771,12 +771,12 @@ async function addEggMoveToNewPokemonMoveset( if (eggMoves) { const eggMoveIndices = randSeedShuffle([0, 1, 2, 3]); let randomEggMoveIndex = eggMoveIndices.pop(); - let randomEggMove = !isNullOrUndefined(randomEggMoveIndex) ? eggMoves[randomEggMoveIndex] : null; + let randomEggMove = randomEggMoveIndex != null ? eggMoves[randomEggMoveIndex] : null; let retries = 0; while (retries < 3 && (!randomEggMove || newPokemon.moveset.some(m => m.moveId === randomEggMove))) { // If Pokemon already knows this move, roll for another egg move randomEggMoveIndex = eggMoveIndices.pop(); - randomEggMove = !isNullOrUndefined(randomEggMoveIndex) ? eggMoves[randomEggMoveIndex] : null; + randomEggMove = randomEggMoveIndex != null ? eggMoves[randomEggMoveIndex] : null; retries++; } @@ -791,11 +791,7 @@ async function addEggMoveToNewPokemonMoveset( } // For pokemon that the player owns (including ones just caught), unlock the egg move - if ( - !forBattle - && !isNullOrUndefined(randomEggMoveIndex) - && !!globalScene.gameData.dexData[speciesRootForm].caughtAttr - ) { + if (!forBattle && randomEggMoveIndex != null && !!globalScene.gameData.dexData[speciesRootForm].caughtAttr) { await globalScene.gameData.setEggMoveUnlocked(getPokemonSpecies(speciesRootForm), randomEggMoveIndex, true); } } diff --git a/src/data/mystery-encounters/mystery-encounter-option.ts b/src/data/mystery-encounters/mystery-encounter-option.ts index fc7bb15d343..1b3b260414d 100644 --- a/src/data/mystery-encounters/mystery-encounter-option.ts +++ b/src/data/mystery-encounters/mystery-encounter-option.ts @@ -12,7 +12,7 @@ import { MoneyRequirement, TypeRequirement, } from "#mystery-encounters/mystery-encounter-requirements"; -import { isNullOrUndefined, randSeedInt } from "#utils/common"; +import { randSeedInt } from "#utils/common"; // biome-ignore lint/suspicious/noConfusingVoidType: void unions in callbacks are OK export type OptionPhaseCallback = () => Promise; @@ -62,7 +62,7 @@ export class MysteryEncounterOption implements IMysteryEncounterOption { onPostOptionPhase?: OptionPhaseCallback; constructor(option: IMysteryEncounterOption | null) { - if (!isNullOrUndefined(option)) { + if (option != null) { Object.assign(this, option); } this.hasDexProgress = this.hasDexProgress ?? false; diff --git a/src/data/mystery-encounters/mystery-encounter-requirements.ts b/src/data/mystery-encounters/mystery-encounter-requirements.ts index f20d513419e..85906044b77 100644 --- a/src/data/mystery-encounters/mystery-encounter-requirements.ts +++ b/src/data/mystery-encounters/mystery-encounter-requirements.ts @@ -15,7 +15,7 @@ import { WeatherType } from "#enums/weather-type"; import type { PlayerPokemon } from "#field/pokemon"; import { AttackTypeBoosterModifier } from "#modifiers/modifier"; import type { AttackTypeBoosterModifierType } from "#modifiers/modifier-type"; -import { coerceArray, isNullOrUndefined } from "#utils/common"; +import { coerceArray } from "#utils/common"; export interface EncounterRequirement { meetsRequirement(): boolean; // Boolean to see if a requirement is met @@ -219,7 +219,7 @@ export class WaveRangeRequirement extends EncounterSceneRequirement { } override meetsRequirement(): boolean { - if (!isNullOrUndefined(this.waveRange) && this.waveRange[0] <= this.waveRange[1]) { + if (this.waveRange != null && this.waveRange[0] <= this.waveRange[1]) { const waveIndex = globalScene.currentBattle.waveIndex; if ( (waveIndex >= 0 && this.waveRange[0] >= 0 && this.waveRange[0] > waveIndex) @@ -275,11 +275,7 @@ export class TimeOfDayRequirement extends EncounterSceneRequirement { override meetsRequirement(): boolean { const timeOfDay = globalScene.arena?.getTimeOfDay(); - return !( - !isNullOrUndefined(timeOfDay) - && this.requiredTimeOfDay?.length > 0 - && !this.requiredTimeOfDay.includes(timeOfDay) - ); + return !(timeOfDay != null && this.requiredTimeOfDay?.length > 0 && !this.requiredTimeOfDay.includes(timeOfDay)); } override getDialogueToken(_pokemon?: PlayerPokemon): [string, string] { @@ -298,7 +294,7 @@ export class WeatherRequirement extends EncounterSceneRequirement { override meetsRequirement(): boolean { const currentWeather = globalScene.arena.weather?.weatherType; return !( - !isNullOrUndefined(currentWeather) + currentWeather != null && this.requiredWeather?.length > 0 && !this.requiredWeather.includes(currentWeather!) ); @@ -307,7 +303,7 @@ export class WeatherRequirement extends EncounterSceneRequirement { override getDialogueToken(_pokemon?: PlayerPokemon): [string, string] { const currentWeather = globalScene.arena.weather?.weatherType; let token = ""; - if (!isNullOrUndefined(currentWeather)) { + if (currentWeather != null) { token = WeatherType[currentWeather].replace("_", " ").toLocaleLowerCase(); } return ["weather", token]; @@ -331,7 +327,7 @@ export class PartySizeRequirement extends EncounterSceneRequirement { } override meetsRequirement(): boolean { - if (!isNullOrUndefined(this.partySizeRange) && this.partySizeRange[0] <= this.partySizeRange[1]) { + if (this.partySizeRange != null && this.partySizeRange[0] <= this.partySizeRange[1]) { const partySize = this.excludeDisallowedPokemon ? globalScene.getPokemonAllowedInBattle().length : globalScene.getPlayerParty().length; @@ -363,7 +359,7 @@ export class PersistentModifierRequirement extends EncounterSceneRequirement { override meetsRequirement(): boolean { const partyPokemon = globalScene.getPlayerParty(); - if (isNullOrUndefined(partyPokemon) || this.requiredHeldItemModifiers?.length < 0) { + if (partyPokemon == null || this.requiredHeldItemModifiers?.length < 0) { return false; } let modifierCount = 0; @@ -396,7 +392,7 @@ export class MoneyRequirement extends EncounterSceneRequirement { override meetsRequirement(): boolean { const money = globalScene.money; - if (isNullOrUndefined(money)) { + if (money == null) { return false; } @@ -429,7 +425,7 @@ export class SpeciesRequirement extends EncounterPokemonRequirement { override meetsRequirement(): boolean { const partyPokemon = globalScene.getPlayerParty(); - if (isNullOrUndefined(partyPokemon) || this.requiredSpecies?.length < 0) { + if (partyPokemon == null || this.requiredSpecies?.length < 0) { return false; } return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; @@ -469,7 +465,7 @@ export class NatureRequirement extends EncounterPokemonRequirement { override meetsRequirement(): boolean { const partyPokemon = globalScene.getPlayerParty(); - if (isNullOrUndefined(partyPokemon) || this.requiredNature?.length < 0) { + if (partyPokemon == null || this.requiredNature?.length < 0) { return false; } return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; @@ -484,7 +480,7 @@ export class NatureRequirement extends EncounterPokemonRequirement { } override getDialogueToken(pokemon?: PlayerPokemon): [string, string] { - if (!isNullOrUndefined(pokemon?.nature) && this.requiredNature.includes(pokemon.nature)) { + if (pokemon?.nature != null && this.requiredNature.includes(pokemon.nature)) { return ["nature", Nature[pokemon.nature]]; } return ["nature", ""]; @@ -508,7 +504,7 @@ export class TypeRequirement extends EncounterPokemonRequirement { override meetsRequirement(): boolean { let partyPokemon = globalScene.getPlayerParty(); - if (isNullOrUndefined(partyPokemon)) { + if (partyPokemon == null) { return false; } @@ -561,7 +557,7 @@ export class MoveRequirement extends EncounterPokemonRequirement { override meetsRequirement(): boolean { const partyPokemon = globalScene.getPlayerParty(); - if (isNullOrUndefined(partyPokemon) || this.requiredMoves?.length < 0) { + if (partyPokemon == null || this.requiredMoves?.length < 0) { return false; } return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; @@ -612,7 +608,7 @@ export class CompatibleMoveRequirement extends EncounterPokemonRequirement { override meetsRequirement(): boolean { const partyPokemon = globalScene.getPlayerParty(); - if (isNullOrUndefined(partyPokemon) || this.requiredMoves?.length < 0) { + if (partyPokemon == null || this.requiredMoves?.length < 0) { return false; } return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; @@ -668,7 +664,7 @@ export class AbilityRequirement extends EncounterPokemonRequirement { override meetsRequirement(): boolean { const partyPokemon = globalScene.getPlayerParty(); - if (isNullOrUndefined(partyPokemon) || this.requiredAbilities?.length < 0) { + if (partyPokemon == null || this.requiredAbilities?.length < 0) { return false; } return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; @@ -692,7 +688,7 @@ export class AbilityRequirement extends EncounterPokemonRequirement { override getDialogueToken(pokemon?: PlayerPokemon): [string, string] { const matchingAbility = this.requiredAbilities.find(a => pokemon?.hasAbility(a, false)); - if (!isNullOrUndefined(matchingAbility)) { + if (matchingAbility != null) { return ["ability", allAbilities[matchingAbility].name]; } return ["ability", ""]; @@ -713,7 +709,7 @@ export class StatusEffectRequirement extends EncounterPokemonRequirement { override meetsRequirement(): boolean { const partyPokemon = globalScene.getPlayerParty(); - if (isNullOrUndefined(partyPokemon) || this.requiredStatusEffect?.length < 0) { + if (partyPokemon == null || this.requiredStatusEffect?.length < 0) { return false; } const x = this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; @@ -727,11 +723,7 @@ export class StatusEffectRequirement extends EncounterPokemonRequirement { return this.requiredStatusEffect.some(statusEffect => { if (statusEffect === StatusEffect.NONE) { // StatusEffect.NONE also checks for null or undefined status - return ( - isNullOrUndefined(pokemon.status) - || isNullOrUndefined(pokemon.status.effect) - || pokemon.status.effect === statusEffect - ); + return pokemon.status == null || pokemon.status.effect == null || pokemon.status.effect === statusEffect; } return pokemon.status?.effect === statusEffect; }); @@ -742,11 +734,7 @@ export class StatusEffectRequirement extends EncounterPokemonRequirement { return !this.requiredStatusEffect.some(statusEffect => { if (statusEffect === StatusEffect.NONE) { // StatusEffect.NONE also checks for null or undefined status - return ( - isNullOrUndefined(pokemon.status) - || isNullOrUndefined(pokemon.status.effect) - || pokemon.status.effect === statusEffect - ); + return pokemon.status == null || pokemon.status.effect == null || pokemon.status.effect === statusEffect; } return pokemon.status?.effect === statusEffect; }); @@ -756,9 +744,7 @@ export class StatusEffectRequirement extends EncounterPokemonRequirement { override getDialogueToken(pokemon?: PlayerPokemon): [string, string] { const reqStatus = this.requiredStatusEffect.filter(a => { if (a === StatusEffect.NONE) { - return ( - isNullOrUndefined(pokemon?.status) || isNullOrUndefined(pokemon.status.effect) || pokemon.status.effect === a - ); + return pokemon?.status == null || pokemon.status.effect == null || pokemon.status.effect === a; } return pokemon!.status?.effect === a; }); @@ -788,7 +774,7 @@ export class CanFormChangeWithItemRequirement extends EncounterPokemonRequiremen override meetsRequirement(): boolean { const partyPokemon = globalScene.getPlayerParty(); - if (isNullOrUndefined(partyPokemon) || this.requiredFormChangeItem?.length < 0) { + if (partyPokemon == null || this.requiredFormChangeItem?.length < 0) { return false; } return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; @@ -847,7 +833,7 @@ export class HeldItemRequirement extends EncounterPokemonRequirement { override meetsRequirement(): boolean { const partyPokemon = globalScene.getPlayerParty(); - if (isNullOrUndefined(partyPokemon)) { + if (partyPokemon == null) { return false; } return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; @@ -911,7 +897,7 @@ export class AttackTypeBoosterHeldItemTypeRequirement extends EncounterPokemonRe override meetsRequirement(): boolean { const partyPokemon = globalScene.getPlayerParty(); - if (isNullOrUndefined(partyPokemon)) { + if (partyPokemon == null) { return false; } return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; @@ -978,7 +964,7 @@ export class LevelRequirement extends EncounterPokemonRequirement { override meetsRequirement(): boolean { // Party Pokemon inside required level range - if (!isNullOrUndefined(this.requiredLevelRange) && this.requiredLevelRange[0] <= this.requiredLevelRange[1]) { + if (this.requiredLevelRange != null && this.requiredLevelRange[0] <= this.requiredLevelRange[1]) { const partyPokemon = globalScene.getPlayerParty(); const pokemonInRange = this.queryParty(partyPokemon); if (pokemonInRange.length < this.minNumberOfPokemon) { @@ -1019,10 +1005,7 @@ export class FriendshipRequirement extends EncounterPokemonRequirement { override meetsRequirement(): boolean { // Party Pokemon inside required friendship range - if ( - !isNullOrUndefined(this.requiredFriendshipRange) - && this.requiredFriendshipRange[0] <= this.requiredFriendshipRange[1] - ) { + if (this.requiredFriendshipRange != null && this.requiredFriendshipRange[0] <= this.requiredFriendshipRange[1]) { const partyPokemon = globalScene.getPlayerParty(); const pokemonInRange = this.queryParty(partyPokemon); if (pokemonInRange.length < this.minNumberOfPokemon) { @@ -1071,7 +1054,7 @@ export class HealthRatioRequirement extends EncounterPokemonRequirement { override meetsRequirement(): boolean { // Party Pokemon's health inside required health range - if (!isNullOrUndefined(this.requiredHealthRange) && this.requiredHealthRange[0] <= this.requiredHealthRange[1]) { + if (this.requiredHealthRange != null && this.requiredHealthRange[0] <= this.requiredHealthRange[1]) { const partyPokemon = globalScene.getPlayerParty(); const pokemonInRange = this.queryParty(partyPokemon); if (pokemonInRange.length < this.minNumberOfPokemon) { @@ -1098,7 +1081,7 @@ export class HealthRatioRequirement extends EncounterPokemonRequirement { override getDialogueToken(pokemon?: PlayerPokemon): [string, string] { const hpRatio = pokemon?.getHpRatio(); - if (!isNullOrUndefined(hpRatio)) { + if (hpRatio != null) { return ["healthRatio", Math.floor(hpRatio * 100).toString() + "%"]; } return ["healthRatio", ""]; @@ -1119,7 +1102,7 @@ export class WeightRequirement extends EncounterPokemonRequirement { override meetsRequirement(): boolean { // Party Pokemon's weight inside required weight range - if (!isNullOrUndefined(this.requiredWeightRange) && this.requiredWeightRange[0] <= this.requiredWeightRange[1]) { + if (this.requiredWeightRange != null && this.requiredWeightRange[0] <= this.requiredWeightRange[1]) { const partyPokemon = globalScene.getPlayerParty(); const pokemonInRange = this.queryParty(partyPokemon); if (pokemonInRange.length < this.minNumberOfPokemon) { diff --git a/src/data/mystery-encounters/mystery-encounter-save-data.ts b/src/data/mystery-encounters/mystery-encounter-save-data.ts index f04abccba5f..71cd2517a95 100644 --- a/src/data/mystery-encounters/mystery-encounter-save-data.ts +++ b/src/data/mystery-encounters/mystery-encounter-save-data.ts @@ -1,7 +1,6 @@ import { BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT } from "#app/constants"; import type { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import type { MysteryEncounterType } from "#enums/mystery-encounter-type"; -import { isNullOrUndefined } from "#utils/common"; export class SeenEncounterData { type: MysteryEncounterType; @@ -28,7 +27,7 @@ export class MysteryEncounterSaveData { queuedEncounters: QueuedEncounter[] = []; constructor(data?: MysteryEncounterSaveData) { - if (!isNullOrUndefined(data)) { + if (data != null) { Object.assign(this, data); } diff --git a/src/data/mystery-encounters/mystery-encounter.ts b/src/data/mystery-encounters/mystery-encounter.ts index 273e14248e6..f18660b5d71 100644 --- a/src/data/mystery-encounters/mystery-encounter.ts +++ b/src/data/mystery-encounters/mystery-encounter.ts @@ -25,7 +25,7 @@ import { StatusEffectRequirement, WaveRangeRequirement, } from "#mystery-encounters/mystery-encounter-requirements"; -import { coerceArray, isNullOrUndefined, randSeedInt } from "#utils/common"; +import { coerceArray, randSeedInt } from "#utils/common"; import { capitalizeFirstLetter } from "#utils/strings"; export interface EncounterStartOfBattleEffect { @@ -275,7 +275,7 @@ export class MysteryEncounter implements IMysteryEncounter { private seedOffset?: any; constructor(encounter: IMysteryEncounter | null) { - if (!isNullOrUndefined(encounter)) { + if (encounter != null) { Object.assign(this, encounter); } this.encounterTier = this.encounterTier ?? MysteryEncounterTier.COMMON; diff --git a/src/data/mystery-encounters/requirements/can-learn-move-requirement.ts b/src/data/mystery-encounters/requirements/can-learn-move-requirement.ts index 26602b8ae31..a5810406ef9 100644 --- a/src/data/mystery-encounters/requirements/can-learn-move-requirement.ts +++ b/src/data/mystery-encounters/requirements/can-learn-move-requirement.ts @@ -3,7 +3,7 @@ import type { MoveId } from "#enums/move-id"; import type { PlayerPokemon } from "#field/pokemon"; import { PokemonMove } from "#moves/pokemon-move"; import { EncounterPokemonRequirement } from "#mystery-encounters/mystery-encounter-requirements"; -import { coerceArray, isNullOrUndefined } from "#utils/common"; +import { coerceArray } from "#utils/common"; /** * {@linkcode CanLearnMoveRequirement} options @@ -44,7 +44,7 @@ export class CanLearnMoveRequirement extends EncounterPokemonRequirement { .getPlayerParty() .filter(pkm => (this.includeFainted ? pkm.isAllowedInChallenge() : pkm.isAllowedInBattle())); - if (isNullOrUndefined(partyPokemon) || this.requiredMoves?.length < 0) { + if (partyPokemon == null || this.requiredMoves?.length < 0) { return false; } diff --git a/src/data/mystery-encounters/utils/encounter-dialogue-utils.ts b/src/data/mystery-encounters/utils/encounter-dialogue-utils.ts index 1a9b008f9e9..be681f731e8 100644 --- a/src/data/mystery-encounters/utils/encounter-dialogue-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-dialogue-utils.ts @@ -1,7 +1,6 @@ import { globalScene } from "#app/global-scene"; import type { TextStyle } from "#enums/text-style"; import { getTextWithColors } from "#ui/text"; -import { isNullOrUndefined } from "#utils/common"; import i18next from "i18next"; /** @@ -11,7 +10,7 @@ import i18next from "i18next"; * @param primaryStyle Can define a text style to be applied to the entire string. Must be defined for BBCodeText styles to be applied correctly */ export function getEncounterText(keyOrString?: string, primaryStyle?: TextStyle): string | null { - if (isNullOrUndefined(keyOrString)) { + if (keyOrString == null) { return null; } diff --git a/src/data/mystery-encounters/utils/encounter-phase-utils.ts b/src/data/mystery-encounters/utils/encounter-phase-utils.ts index aa569f11aca..86cd3fa3a32 100644 --- a/src/data/mystery-encounters/utils/encounter-phase-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -49,7 +49,7 @@ import type { HeldModifierConfig } from "#types/held-modifier-config"; import type { OptionSelectConfig, OptionSelectItem } from "#ui/abstract-option-select-ui-handler"; import type { PartyOption, PokemonSelectFilter } from "#ui/party-ui-handler"; import { PartyUiMode } from "#ui/party-ui-handler"; -import { coerceArray, isNullOrUndefined, randomString, randSeedInt, randSeedItem } from "#utils/common"; +import { coerceArray, randomString, randSeedInt, randSeedItem } from "#utils/common"; import { getPokemonSpecies } from "#utils/pokemon-utils"; import i18next from "i18next"; @@ -143,7 +143,7 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig): const trainerType = partyConfig?.trainerType; const partyTrainerConfig = partyConfig?.trainerConfig; let trainerConfig: TrainerConfig; - if (!isNullOrUndefined(trainerType) || partyTrainerConfig) { + if (trainerType != null || partyTrainerConfig) { globalScene.currentBattle.mysteryEncounter!.encounterMode = MysteryEncounterMode.TRAINER_BATTLE; if (globalScene.currentBattle.trainer) { globalScene.currentBattle.trainer.setVisible(false); @@ -154,7 +154,7 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig): const doubleTrainer = trainerConfig.doubleOnly || (trainerConfig.hasDouble && !!partyConfig.doubleBattle); doubleBattle = doubleTrainer; - const trainerFemale = isNullOrUndefined(partyConfig.female) ? !!randSeedInt(2) : partyConfig.female; + const trainerFemale = partyConfig.female == null ? !!randSeedInt(2) : partyConfig.female; const newTrainer = new Trainer( trainerConfig.trainerType, doubleTrainer ? TrainerVariant.DOUBLE : trainerFemale ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT, @@ -202,7 +202,7 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig): let dataSource: PokemonData | undefined; let isBoss = false; if (!loaded) { - if ((!isNullOrUndefined(trainerType) || trainerConfig) && battle.trainer) { + if ((trainerType != null || trainerConfig) && battle.trainer) { // Allows overriding a trainer's pokemon to use specific species/data if (partyConfig?.pokemonConfigs && e < partyConfig.pokemonConfigs.length) { const config = partyConfig.pokemonConfigs[e]; @@ -258,7 +258,7 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig): enemyPokemon.resetSummonData(); } - if ((!loaded && isNullOrUndefined(partyConfig.countAsSeen)) || partyConfig.countAsSeen) { + if ((!loaded && partyConfig.countAsSeen == null) || partyConfig.countAsSeen) { globalScene.gameData.setPokemonSeen(enemyPokemon, true, !!(trainerType || trainerConfig)); } @@ -266,7 +266,7 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig): const config = partyConfig.pokemonConfigs[e]; // Set form - if (!isNullOrUndefined(config.nickname)) { + if (config.nickname != null) { enemyPokemon.nickname = btoa(unescape(encodeURIComponent(config.nickname))); } @@ -276,22 +276,22 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig): } // Set form - if (!isNullOrUndefined(config.formIndex)) { + if (config.formIndex != null) { enemyPokemon.formIndex = config.formIndex; } // Set shiny - if (!isNullOrUndefined(config.shiny)) { + if (config.shiny != null) { enemyPokemon.shiny = config.shiny; } // Set Variant - if (enemyPokemon.shiny && !isNullOrUndefined(config.variant)) { + if (enemyPokemon.shiny && config.variant != null) { enemyPokemon.variant = config.variant; } // Set custom mystery encounter data fields (such as sprite scale, custom abilities, types, etc.) - if (!isNullOrUndefined(config.customPokemonData)) { + if (config.customPokemonData != null) { enemyPokemon.customPokemonData = config.customPokemonData; } @@ -300,7 +300,7 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig): let segments = config.bossSegments ?? globalScene.getEncounterBossSegments(globalScene.currentBattle.waveIndex, level, enemySpecies, true); - if (!isNullOrUndefined(config.bossSegmentModifier)) { + if (config.bossSegmentModifier != null) { segments += config.bossSegmentModifier; } enemyPokemon.setBoss(true, segments); @@ -335,18 +335,18 @@ export async function initBattleWithEnemyConfig(partyConfig: EnemyPartyConfig): } // Set ability - if (!isNullOrUndefined(config.abilityIndex)) { + if (config.abilityIndex != null) { enemyPokemon.abilityIndex = config.abilityIndex; } // Set gender - if (!isNullOrUndefined(config.gender)) { + if (config.gender != null) { enemyPokemon.gender = config.gender!; enemyPokemon.summonData.gender = config.gender; } // Set AI type - if (!isNullOrUndefined(config.aiType)) { + if (config.aiType != null) { enemyPokemon.aiType = config.aiType; } diff --git a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts index 8f6c78fab9c..01d4659d379 100644 --- a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts @@ -35,7 +35,7 @@ import type { PartyOption } from "#ui/party-ui-handler"; import { PartyUiMode } from "#ui/party-ui-handler"; import { SummaryUiMode } from "#ui/summary-ui-handler"; import { applyChallenges } from "#utils/challenge-utils"; -import { BooleanHolder, isNullOrUndefined, randSeedInt } from "#utils/common"; +import { BooleanHolder, randSeedInt } from "#utils/common"; import { getPokemonSpecies } from "#utils/pokemon-utils"; import i18next from "i18next"; @@ -276,7 +276,7 @@ export function getRandomSpeciesByStarterCost( if (types && types.length > 0) { filteredSpecies = filteredSpecies.filter( - s => types.includes(s[0].type1) || (!isNullOrUndefined(s[0].type2) && types.includes(s[0].type2)), + s => types.includes(s[0].type1) || (s[0].type2 != null && types.includes(s[0].type2)), ); } diff --git a/src/data/pokemon-species.ts b/src/data/pokemon-species.ts index 2d76c2c0400..7c00bf5dff7 100644 --- a/src/data/pokemon-species.ts +++ b/src/data/pokemon-species.ts @@ -28,7 +28,7 @@ import type { Variant, VariantSet } from "#sprites/variant"; import { populateVariantColorCache, variantColorCache, variantData } from "#sprites/variant"; import type { Localizable } from "#types/locales"; import type { StarterMoveset } from "#types/save-data"; -import { isNullOrUndefined, randSeedFloat, randSeedGauss, randSeedInt } from "#utils/common"; +import { randSeedFloat, randSeedGauss, randSeedInt } from "#utils/common"; import { getPokemonSpecies } from "#utils/pokemon-utils"; import { toCamelCase, toPascalCase } from "#utils/strings"; import { argbFromRgba, QuantizerCelebi, rgbaFromArgb } from "@material/material-color-utilities"; @@ -197,7 +197,7 @@ export abstract class PokemonSpeciesForm { * @returns The id of the ability */ getPassiveAbility(formIndex?: number): AbilityId { - if (isNullOrUndefined(formIndex)) { + if (formIndex == null) { formIndex = this.formIndex; } let starterSpeciesId = this.speciesId; @@ -551,7 +551,7 @@ export abstract class PokemonSpeciesForm { const spriteKey = this.getSpriteKey(female, formIndex, shiny, variant, back); globalScene.loadPokemonAtlas(spriteKey, this.getSpriteAtlasPath(female, formIndex, shiny, variant, back)); globalScene.load.audio(this.getCryKey(formIndex), `audio/${this.getCryKey(formIndex)}.m4a`); - if (!isNullOrUndefined(variant)) { + if (variant != null) { await this.loadVariantColors(spriteKey, female, variant, back, formIndex); } return new Promise(resolve => { @@ -579,7 +579,7 @@ export abstract class PokemonSpeciesForm { const spritePath = this.getSpriteAtlasPath(female, formIndex, shiny, variant, back) .replace("variant/", "") .replace(/_[1-3]$/, ""); - if (!isNullOrUndefined(variant)) { + if (variant != null) { loadPokemonVariantAssets(spriteKey, spritePath, variant).then(() => resolve()); } }); @@ -791,7 +791,7 @@ export class PokemonSpecies extends PokemonSpeciesForm implements Localizable { * @returns A randomly rolled gender based on this Species' {@linkcode malePercent}. */ generateGender(): Gender { - if (isNullOrUndefined(this.malePercent)) { + if (this.malePercent == null) { return Gender.GENDERLESS; } diff --git a/src/data/pokemon/pokemon-data.ts b/src/data/pokemon/pokemon-data.ts index 87ffbbab4cd..4fbb70bccb2 100644 --- a/src/data/pokemon/pokemon-data.ts +++ b/src/data/pokemon/pokemon-data.ts @@ -16,7 +16,6 @@ import type { AttackMoveResult } from "#types/attack-move-result"; import type { IllusionData } from "#types/illusion-data"; import type { TurnMove } from "#types/turn-move"; import type { CoerceNullPropertiesToUndefined } from "#types/type-helpers"; -import { isNullOrUndefined } from "#utils/common"; import { getPokemonSpeciesForm } from "#utils/pokemon-utils"; /** @@ -64,14 +63,14 @@ function deserializePokemonSpeciesForm(value: SerializedSpeciesForm | PokemonSpe // @ts-expect-error: We may be deserializing a PokemonSpeciesForm, but we catch later on let { id, formIdx } = value; - if (isNullOrUndefined(id) || isNullOrUndefined(formIdx)) { + if (id == null || formIdx == null) { // @ts-expect-error: Typescript doesn't know that in block, `value` must be a PokemonSpeciesForm id = value.speciesId; // @ts-expect-error: Same as above (plus we are accessing a protected property) formIdx = value._formIndex; } // If for some reason either of these fields are null/undefined, we cannot reconstruct the species form - if (isNullOrUndefined(id) || isNullOrUndefined(formIdx)) { + if (id == null || formIdx == null) { return null; } return getPokemonSpeciesForm(id, formIdx); @@ -151,13 +150,13 @@ export class PokemonSummonData { public moveHistory: TurnMove[] = []; constructor(source?: PokemonSummonData | SerializedPokemonSummonData) { - if (isNullOrUndefined(source)) { + if (source == null) { return; } // TODO: Rework this into an actual generic function for use elsewhere for (const [key, value] of Object.entries(source)) { - if (isNullOrUndefined(value) && this.hasOwnProperty(key)) { + if (value == null && this.hasOwnProperty(key)) { continue; } @@ -171,7 +170,7 @@ export class PokemonSummonData { const illusionData = { ...value, }; - if (!isNullOrUndefined(illusionData.fusionSpecies)) { + if (illusionData.fusionSpecies != null) { switch (typeof illusionData.fusionSpecies) { case "object": illusionData.fusionSpecies = allSpecies[illusionData.fusionSpecies.speciesId]; @@ -224,18 +223,18 @@ export class PokemonSummonData { CoerceNullPropertiesToUndefined, "speciesForm" | "fusionSpeciesForm" | "illusion" >), - speciesForm: isNullOrUndefined(speciesForm) - ? undefined - : { id: speciesForm.speciesId, formIdx: speciesForm.formIndex }, - fusionSpeciesForm: isNullOrUndefined(fusionSpeciesForm) - ? undefined - : { id: fusionSpeciesForm.speciesId, formIdx: fusionSpeciesForm.formIndex }, - illusion: isNullOrUndefined(illusion) - ? undefined - : { - ...(this.illusion as Omit), - fusionSpecies: illusionSpeciesForm?.speciesId, - }, + speciesForm: speciesForm == null ? undefined : { id: speciesForm.speciesId, formIdx: speciesForm.formIndex }, + fusionSpeciesForm: + fusionSpeciesForm == null + ? undefined + : { id: fusionSpeciesForm.speciesId, formIdx: fusionSpeciesForm.formIndex }, + illusion: + illusion == null + ? undefined + : { + ...(this.illusion as Omit), + fusionSpecies: illusionSpeciesForm?.speciesId, + }, }; // Replace `null` with `undefined`, as `undefined` never gets serialized for (const [key, value] of Object.entries(t)) { @@ -278,7 +277,7 @@ export class PokemonBattleData { public berriesEaten: BerryType[] = []; constructor(source?: PokemonBattleData | Partial) { - if (!isNullOrUndefined(source)) { + if (source != null) { this.hitCount = source.hitCount ?? 0; this.hasEatenBerry = source.hasEatenBerry ?? false; this.berriesEaten = source.berriesEaten ?? []; diff --git a/src/data/trainers/trainer-config.ts b/src/data/trainers/trainer-config.ts index 9d891444829..55362017cee 100644 --- a/src/data/trainers/trainer-config.ts +++ b/src/data/trainers/trainer-config.ts @@ -41,7 +41,7 @@ import type { TrainerConfigs, TrainerTierPools, } from "#types/trainer-funcs"; -import { coerceArray, isNullOrUndefined, randSeedInt, randSeedIntRange, randSeedItem } from "#utils/common"; +import { coerceArray, randSeedInt, randSeedIntRange, randSeedItem } from "#utils/common"; import { getPokemonSpecies } from "#utils/pokemon-utils"; import { toCamelCase, toTitleCase } from "#utils/strings"; import i18next from "i18next"; @@ -474,7 +474,7 @@ export class TrainerConfig { .fill(null) .map((_, i) => i) .filter(i => shedinjaCanTera || party[i].species.speciesId !== SpeciesId.SHEDINJA); // Shedinja can only Tera on Bug specialty type (or no specialty type) - const setPartySlot = !isNullOrUndefined(slot) ? Phaser.Math.Wrap(slot, 0, party.length) : -1; // If we have a tera slot defined, wrap it to party size. + const setPartySlot = slot != null ? Phaser.Math.Wrap(slot, 0, party.length) : -1; // If we have a tera slot defined, wrap it to party size. for (let t = 0; t < Math.min(count(), party.length); t++) { const randomIndex = partyMemberIndexes.indexOf(setPartySlot) > -1 ? setPartySlot : randSeedItem(partyMemberIndexes); @@ -537,7 +537,7 @@ export class TrainerConfig { initI18n(); } - if (!isNullOrUndefined(specialtyType)) { + if (specialtyType != null) { this.setSpecialtyType(specialtyType); } @@ -612,7 +612,7 @@ export class TrainerConfig { signatureSpecies.forEach((speciesPool, s) => { this.setPartyMemberFunc(-(s + 1), getRandomPartyMemberFunc(coerceArray(speciesPool))); }); - if (!isNullOrUndefined(specialtyType)) { + if (specialtyType != null) { this.setSpeciesFilter(p => p.isOfType(specialtyType)); this.setSpecialtyType(specialtyType); } @@ -717,7 +717,7 @@ export class TrainerConfig { }); // Set species filter and specialty type if provided, otherwise filter by base total. - if (!isNullOrUndefined(specialtyType)) { + if (specialtyType != null) { this.setSpeciesFilter(p => p.isOfType(specialtyType) && p.baseTotal >= ELITE_FOUR_MINIMUM_BST); this.setSpecialtyType(specialtyType); } else { @@ -895,7 +895,7 @@ export class TrainerConfig { * @returns `true` if `specialtyType` is defined and not {@link PokemonType.UNKNOWN} */ hasSpecialtyType(): boolean { - return !isNullOrUndefined(this.specialtyType) && this.specialtyType !== PokemonType.UNKNOWN; + return this.specialtyType != null && this.specialtyType !== PokemonType.UNKNOWN; } /** @@ -2942,7 +2942,7 @@ export const trainerConfigs: TrainerConfigs = { getRandomPartyMemberFunc([SpeciesId.SLOWBRO, SpeciesId.GALAR_SLOWBRO], TrainerSlot.TRAINER, true, p => { // Tera Ice Slowbro/G-Slowbro p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.ICE_BEAM)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.ICE_BEAM)) { // Check if Ice Beam is in the moveset, if not, replace the third move with Ice Beam. p.moveset[2] = new PokemonMove(MoveId.ICE_BEAM); } @@ -2967,7 +2967,7 @@ export const trainerConfigs: TrainerConfigs = { getRandomPartyMemberFunc([SpeciesId.STEELIX], TrainerSlot.TRAINER, true, p => { // Tera Fighting Steelix p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.BODY_PRESS)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.BODY_PRESS)) { // Check if Body Press is in the moveset, if not, replace the third move with Body Press. p.moveset[2] = new PokemonMove(MoveId.BODY_PRESS); } @@ -2992,7 +2992,7 @@ export const trainerConfigs: TrainerConfigs = { getRandomPartyMemberFunc([SpeciesId.ARBOK, SpeciesId.WEEZING], TrainerSlot.TRAINER, true, p => { // Tera Ghost Arbok/Weezing p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.TERA_BLAST)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.TERA_BLAST)) { // Check if Tera Blast is in the moveset, if not, replace the third move with Tera Blast. p.moveset[2] = new PokemonMove(MoveId.TERA_BLAST); } @@ -3018,7 +3018,7 @@ export const trainerConfigs: TrainerConfigs = { getRandomPartyMemberFunc([SpeciesId.GYARADOS, SpeciesId.AERODACTYL], TrainerSlot.TRAINER, true, p => { // Tera Dragon Gyarados/Aerodactyl p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.TERA_BLAST)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.TERA_BLAST)) { // Check if Tera Blast is in the moveset, if not, replace the third move with Tera Blast. p.moveset[2] = new PokemonMove(MoveId.TERA_BLAST); } @@ -3079,7 +3079,7 @@ export const trainerConfigs: TrainerConfigs = { getRandomPartyMemberFunc([SpeciesId.GENGAR], TrainerSlot.TRAINER, true, p => { // Tera Dark Gengar p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.DARK_PULSE)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.DARK_PULSE)) { // Check if Dark Pulse is in the moveset, if not, replace the third move with Dark Pulse. p.moveset[2] = new PokemonMove(MoveId.DARK_PULSE); } @@ -3163,7 +3163,7 @@ export const trainerConfigs: TrainerConfigs = { getRandomPartyMemberFunc([SpeciesId.DHELMISE], TrainerSlot.TRAINER, true, p => { // Tera Dragon Dhelmise p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.TERA_BLAST)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.TERA_BLAST)) { // Check if Tera Blast is in the moveset, if not, replace the third move with Tera Blast. p.moveset[2] = new PokemonMove(MoveId.TERA_BLAST); } @@ -3193,7 +3193,7 @@ export const trainerConfigs: TrainerConfigs = { p.setBoss(true, 2); p.abilityIndex = 1; // Sniper p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.X_SCISSOR)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.X_SCISSOR)) { // Check if X-Scissor is in the moveset, if not, replace the third move with X-Scissor. p.moveset[2] = new PokemonMove(MoveId.X_SCISSOR); } @@ -3232,7 +3232,7 @@ export const trainerConfigs: TrainerConfigs = { getRandomPartyMemberFunc([SpeciesId.STEELIX, SpeciesId.LOPUNNY], TrainerSlot.TRAINER, true, p => { // Tera Fire Steelix/Lopunny p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.TERA_BLAST)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.TERA_BLAST)) { // Check if Tera Blast is in the moveset, if not, replace the third move with Tera Blast. p.moveset[2] = new PokemonMove(MoveId.TERA_BLAST); } @@ -3375,7 +3375,7 @@ export const trainerConfigs: TrainerConfigs = { getRandomPartyMemberFunc([SpeciesId.CERULEDGE], TrainerSlot.TRAINER, true, p => { // Tera Steel Ceruledge p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.IRON_HEAD)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.IRON_HEAD)) { // Check if Iron Head is in the moveset, if not, replace the third move with Iron Head. p.moveset[2] = new PokemonMove(MoveId.IRON_HEAD); } @@ -3413,7 +3413,7 @@ export const trainerConfigs: TrainerConfigs = { getRandomPartyMemberFunc([SpeciesId.INCINEROAR], TrainerSlot.TRAINER, true, p => { // Tera Fighting Incineroar p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.CROSS_CHOP)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.CROSS_CHOP)) { // Check if Cross Chop is in the moveset, if not, replace the third move with Cross Chop. p.moveset[2] = new PokemonMove(MoveId.CROSS_CHOP); } @@ -3486,7 +3486,7 @@ export const trainerConfigs: TrainerConfigs = { getRandomPartyMemberFunc([SpeciesId.DECIDUEYE], TrainerSlot.TRAINER, true, p => { // Tera Flying Decidueye p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.BRAVE_BIRD)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.BRAVE_BIRD)) { // Check if Brave Bird is in the moveset, if not, replace the third move with Brave Bird. p.moveset[2] = new PokemonMove(MoveId.BRAVE_BIRD); } @@ -3511,7 +3511,7 @@ export const trainerConfigs: TrainerConfigs = { getRandomPartyMemberFunc([SpeciesId.TOXICROAK], TrainerSlot.TRAINER, true, p => { // Tera Dark Toxicroak p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.SUCKER_PUNCH)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.SUCKER_PUNCH)) { // Check if Sucker Punch is in the moveset, if not, replace the third move with Sucker Punch. p.moveset[2] = new PokemonMove(MoveId.SUCKER_PUNCH); } @@ -3536,7 +3536,7 @@ export const trainerConfigs: TrainerConfigs = { getRandomPartyMemberFunc([SpeciesId.EISCUE], TrainerSlot.TRAINER, true, p => { // Tera Water Eiscue p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.LIQUIDATION)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.LIQUIDATION)) { // Check if Liquidation is in the moveset, if not, replace the third move with Liquidation. p.moveset[2] = new PokemonMove(MoveId.LIQUIDATION); } @@ -3598,7 +3598,7 @@ export const trainerConfigs: TrainerConfigs = { // Tera Dragon Torkoal p.abilityIndex = 1; // Drought p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.TERA_BLAST)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.TERA_BLAST)) { // Check if Tera Blast is in the moveset, if not, replace the third move with Tera Blast. p.moveset[2] = new PokemonMove(MoveId.TERA_BLAST); } @@ -3695,7 +3695,7 @@ export const trainerConfigs: TrainerConfigs = { getRandomPartyMemberFunc([SpeciesId.EXEGGUTOR], TrainerSlot.TRAINER, true, p => { // Tera Fire Exeggutor p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.TERA_BLAST)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.TERA_BLAST)) { // Check if Tera Blast is in the moveset, if not, replace the third move with Tera Blast. p.moveset[2] = new PokemonMove(MoveId.TERA_BLAST); } @@ -3705,7 +3705,7 @@ export const trainerConfigs: TrainerConfigs = { 3, getRandomPartyMemberFunc([SpeciesId.TALONFLAME], TrainerSlot.TRAINER, true, p => { p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.SUNNY_DAY)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.SUNNY_DAY)) { // Check if Sunny Day is in the moveset, if not, replace the third move with Sunny Day. p.moveset[2] = new PokemonMove(MoveId.SUNNY_DAY); } @@ -3728,7 +3728,7 @@ export const trainerConfigs: TrainerConfigs = { getRandomPartyMemberFunc([SpeciesId.REUNICLUS], TrainerSlot.TRAINER, true, p => { // Tera Steel Reuniclus p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.FLASH_CANNON)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.FLASH_CANNON)) { // Check if Flash Cannon is in the moveset, if not, replace the third move with Flash Cannon. p.moveset[2] = new PokemonMove(MoveId.FLASH_CANNON); } @@ -3756,7 +3756,7 @@ export const trainerConfigs: TrainerConfigs = { // Tera Fairy Excadrill p.setBoss(true, 2); p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.TERA_BLAST)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.TERA_BLAST)) { // Check if Tera Blast is in the moveset, if not, replace the third move with Tera Blast. p.moveset[2] = new PokemonMove(MoveId.TERA_BLAST); } @@ -3771,7 +3771,7 @@ export const trainerConfigs: TrainerConfigs = { getRandomPartyMemberFunc([SpeciesId.SCEPTILE], TrainerSlot.TRAINER, true, p => { // Tera Dragon Sceptile p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.DUAL_CHOP)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.DUAL_CHOP)) { // Check if Dual Chop is in the moveset, if not, replace the third move with Dual Chop. p.moveset[2] = new PokemonMove(MoveId.DUAL_CHOP); } @@ -3841,7 +3841,7 @@ export const trainerConfigs: TrainerConfigs = { p.formIndex = 1; // Partner Pikachu p.gender = Gender.MALE; p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.VOLT_TACKLE)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.VOLT_TACKLE)) { // Check if Volt Tackle is in the moveset, if not, replace the first move with Volt Tackle. p.moveset[0] = new PokemonMove(MoveId.VOLT_TACKLE); } @@ -4072,7 +4072,7 @@ export const trainerConfigs: TrainerConfigs = { getRandomPartyMemberFunc([SpeciesId.KELDEO], TrainerSlot.TRAINER, true, p => { p.pokeball = PokeballType.ROGUE_BALL; p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.SECRET_SWORD)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.SECRET_SWORD)) { // Check if Secret Sword is in the moveset, if not, replace the third move with Secret Sword. p.moveset[2] = new PokemonMove(MoveId.SECRET_SWORD); } @@ -4401,7 +4401,7 @@ export const trainerConfigs: TrainerConfigs = { 5, getRandomPartyMemberFunc([SpeciesId.KINGAMBIT], TrainerSlot.TRAINER, true, p => { p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.TERA_BLAST)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.TERA_BLAST)) { // Check if Tera Blast is in the moveset, if not, replace the third move with Tera Blast. p.moveset[2] = new PokemonMove(MoveId.TERA_BLAST); } @@ -4480,7 +4480,7 @@ export const trainerConfigs: TrainerConfigs = { 4, getRandomPartyMemberFunc([SpeciesId.TERAPAGOS], TrainerSlot.TRAINER, true, p => { p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.TERA_STARSTORM)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.TERA_STARSTORM)) { // Check if Tera Starstorm is in the moveset, if not, replace the first move with Tera Starstorm. p.moveset[0] = new PokemonMove(MoveId.TERA_STARSTORM); } @@ -4494,7 +4494,7 @@ export const trainerConfigs: TrainerConfigs = { p.setBoss(true, 2); p.teraType = PokemonType.FIGHTING; p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.TERA_BLAST)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.TERA_BLAST)) { // Check if Tera Blast is in the moveset, if not, replace the third move with Tera Blast. p.moveset[2] = new PokemonMove(MoveId.TERA_BLAST); } @@ -5054,7 +5054,7 @@ export const trainerConfigs: TrainerConfigs = { 2, getRandomPartyMemberFunc([SpeciesId.HONCHKROW], TrainerSlot.TRAINER, true, p => { p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.SUCKER_PUNCH)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.SUCKER_PUNCH)) { // Check if Sucker Punch is in the moveset, if not, replace the third move with Sucker Punch. p.moveset[2] = new PokemonMove(MoveId.SUCKER_PUNCH); } @@ -5517,7 +5517,7 @@ export const trainerConfigs: TrainerConfigs = { p.formIndex = randSeedInt(18); // Random Silvally Form p.generateAndPopulateMoveset(); p.pokeball = PokeballType.ROGUE_BALL; - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.MULTI_ATTACK)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.MULTI_ATTACK)) { // Check if Multi Attack is in the moveset, if not, replace the first move with Multi Attack. p.moveset[0] = new PokemonMove(MoveId.MULTI_ATTACK); } @@ -5590,7 +5590,7 @@ export const trainerConfigs: TrainerConfigs = { getRandomPartyMemberFunc([SpeciesId.GOLISOPOD], TrainerSlot.TRAINER, true, p => { p.setBoss(true, 2); p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.FIRST_IMPRESSION)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.FIRST_IMPRESSION)) { // Check if First Impression is in the moveset, if not, replace the third move with First Impression. p.moveset[2] = new PokemonMove(MoveId.FIRST_IMPRESSION); p.gender = Gender.MALE; @@ -5607,7 +5607,7 @@ export const trainerConfigs: TrainerConfigs = { getRandomPartyMemberFunc([SpeciesId.GOLISOPOD], TrainerSlot.TRAINER, true, p => { p.setBoss(true, 2); p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.FIRST_IMPRESSION)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.FIRST_IMPRESSION)) { // Check if First Impression is in the moveset, if not, replace the third move with First Impression. p.moveset[2] = new PokemonMove(MoveId.FIRST_IMPRESSION); p.abilityIndex = 2; // Anticipation @@ -5643,7 +5643,7 @@ export const trainerConfigs: TrainerConfigs = { p.generateAndPopulateMoveset(); p.pokeball = PokeballType.ROGUE_BALL; p.formIndex = randSeedInt(4, 1); // Shock, Burn, Chill, or Douse Drive - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.TECHNO_BLAST)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.TECHNO_BLAST)) { // Check if Techno Blast is in the moveset, if not, replace the third move with Techno Blast. p.moveset[2] = new PokemonMove(MoveId.TECHNO_BLAST); } @@ -5778,7 +5778,7 @@ export const trainerConfigs: TrainerConfigs = { p.setBoss(true, 2); p.abilityIndex = 2; // Pixilate p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.HYPER_VOICE)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.HYPER_VOICE)) { // Check if Hyper Voice is in the moveset, if not, replace the second move with Hyper Voice. p.moveset[1] = new PokemonMove(MoveId.HYPER_VOICE); p.gender = Gender.FEMALE; @@ -5807,7 +5807,7 @@ export const trainerConfigs: TrainerConfigs = { p.setBoss(true, 2); p.abilityIndex = 2; // Pixilate p.generateAndPopulateMoveset(); - if (!p.moveset.some(move => !isNullOrUndefined(move) && move.moveId === MoveId.HYPER_VOICE)) { + if (!p.moveset.some(move => move != null && move.moveId === MoveId.HYPER_VOICE)) { // Check if Hyper Voice is in the moveset, if not, replace the second move with Hyper Voice. p.moveset[1] = new PokemonMove(MoveId.HYPER_VOICE); p.gender = Gender.FEMALE; diff --git a/src/field/arena.ts b/src/field/arena.ts index ff7379b2a4a..c4708be1336 100644 --- a/src/field/arena.ts +++ b/src/field/arena.ts @@ -36,7 +36,7 @@ import type { Pokemon } from "#field/pokemon"; import { FieldEffectModifier } from "#modifiers/modifier"; import type { Move } from "#moves/move"; import type { AbstractConstructor } from "#types/type-helpers"; -import { type Constructor, isNullOrUndefined, NumberHolder, randSeedInt } from "#utils/common"; +import { type Constructor, NumberHolder, randSeedInt } from "#utils/common"; import { getPokemonSpecies } from "#utils/pokemon-utils"; export class Arena { @@ -339,7 +339,7 @@ export class Arena { const weatherDuration = new NumberHolder(0); - if (!isNullOrUndefined(user)) { + if (user != null) { weatherDuration.value = 5; globalScene.applyModifier(FieldEffectModifier, user.isPlayer(), user, weatherDuration); } @@ -420,7 +420,7 @@ export class Arena { const terrainDuration = new NumberHolder(0); - if (!isNullOrUndefined(user)) { + if (user != null) { terrainDuration.value = 5; globalScene.applyModifier(FieldEffectModifier, user.isPlayer(), user, terrainDuration); } diff --git a/src/field/mystery-encounter-intro.ts b/src/field/mystery-encounter-intro.ts index cf0a0f30529..4b4a234251a 100644 --- a/src/field/mystery-encounter-intro.ts +++ b/src/field/mystery-encounter-intro.ts @@ -5,7 +5,6 @@ import { getSpriteKeysFromSpecies } from "#mystery-encounters/encounter-pokemon- import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter"; import { loadPokemonVariantAssets } from "#sprites/pokemon-sprite"; import type { Variant } from "#sprites/variant"; -import { isNullOrUndefined } from "#utils/common"; import type { GameObjects } from "phaser"; type PlayAnimationConfig = Phaser.Types.Animations.PlayAnimationConfig; @@ -98,7 +97,7 @@ export class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Container { ...config, }; - if (!isNullOrUndefined(result.species)) { + if (result.species != null) { const keys = getSpriteKeysFromSpecies(result.species, undefined, undefined, result.isShiny, result.variant); result.spriteKey = keys.spriteKey; result.fileRoot = keys.fileRoot; @@ -205,12 +204,12 @@ export class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Container { n++; } - if (!isNullOrUndefined(pokemonShinySparkle)) { + if (pokemonShinySparkle != null) { // Offset the sparkle to match the Pokemon's position pokemonShinySparkle.setPosition(sprite.x, sprite.y); } - if (!isNullOrUndefined(alpha)) { + if (alpha != null) { sprite.setAlpha(alpha); tintSprite.setAlpha(alpha); } @@ -234,7 +233,7 @@ export class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Container { this.spriteConfigs.forEach(config => { if (config.isPokemon) { globalScene.loadPokemonAtlas(config.spriteKey, config.fileRoot); - if (config.isShiny && !isNullOrUndefined(config.variant)) { + if (config.isShiny && config.variant != null) { shinyPromises.push(loadPokemonVariantAssets(config.spriteKey, config.fileRoot, config.variant)); } } else if (config.isItem) { diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index d3ff7312977..e6a46ac926f 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -159,7 +159,6 @@ import { fixedInt, getIvsFromId, isBetween, - isNullOrUndefined, NumberHolder, randSeedFloat, randSeedInt, @@ -886,7 +885,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return this.fallbackVariantColor(cacheKey, spritePath, useExpSprite, battleSpritePath, error); }) .then(c => { - if (!isNullOrUndefined(c)) { + if (c != null) { variantColorCache[cacheKey] = c; } }); @@ -1475,7 +1474,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } const ally = this.getAlly(); - if (!isNullOrUndefined(ally)) { + if (ally != null) { applyAbAttrs("AllyStatMultiplierAbAttr", { pokemon: ally, stat, @@ -1658,7 +1657,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { if (useIllusion && this.summonData.illusion) { return this.summonData.illusion.gender; } - if (!ignoreOverride && !isNullOrUndefined(this.summonData.gender)) { + if (!ignoreOverride && this.summonData.gender != null) { return this.summonData.gender; } return this.gender; @@ -1674,7 +1673,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { if (useIllusion && this.summonData.illusion?.fusionGender) { return this.summonData.illusion.fusionGender; } - if (!ignoreOverride && !isNullOrUndefined(this.summonData.fusionGender)) { + if (!ignoreOverride && this.summonData.fusionGender != null) { return this.summonData.fusionGender; } return this.fusionGender; @@ -1793,7 +1792,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * @returns Whether this Pokemon has this species as either its base or fusion counterpart. */ hasSpecies(species: SpeciesId, formKey?: string): boolean { - if (isNullOrUndefined(formKey)) { + if (formKey == null) { return this.species.speciesId === species || this.fusionSpecies?.speciesId === species; } @@ -1941,7 +1940,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { secondType = fusionType1; } - if (secondType === PokemonType.UNKNOWN && isNullOrUndefined(fusionType2)) { + if (secondType === PokemonType.UNKNOWN && fusionType2 == null) { // If second pokemon was monotype and shared its primary type secondType = customTypes @@ -2024,12 +2023,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return allAbilities[Overrides.ENEMY_ABILITY_OVERRIDE]; } if (this.isFusion()) { - if (!isNullOrUndefined(this.fusionCustomPokemonData?.ability) && this.fusionCustomPokemonData.ability !== -1) { + if (this.fusionCustomPokemonData?.ability != null && this.fusionCustomPokemonData.ability !== -1) { return allAbilities[this.fusionCustomPokemonData.ability]; } return allAbilities[this.getFusionSpeciesForm(ignoreOverride).getAbility(this.fusionAbilityIndex)]; } - if (!isNullOrUndefined(this.customPokemonData.ability) && this.customPokemonData.ability !== -1) { + if (this.customPokemonData.ability != null && this.customPokemonData.ability !== -1) { return allAbilities[this.customPokemonData.ability]; } let abilityId = this.getSpeciesForm(ignoreOverride).getAbility(this.abilityIndex); @@ -2053,7 +2052,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { if (Overrides.ENEMY_PASSIVE_ABILITY_OVERRIDE && this.isEnemy()) { return allAbilities[Overrides.ENEMY_PASSIVE_ABILITY_OVERRIDE]; } - if (!isNullOrUndefined(this.customPokemonData.passive) && this.customPokemonData.passive !== -1) { + if (this.customPokemonData.passive != null && this.customPokemonData.passive !== -1) { return allAbilities[this.customPokemonData.passive]; } @@ -2237,7 +2236,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { public getWeight(): number { const autotomizedTag = this.getTag(AutotomizedTag); let weightRemoved = 0; - if (!isNullOrUndefined(autotomizedTag)) { + if (autotomizedTag != null) { weightRemoved = 100 * autotomizedTag.autotomizeCount; } const minWeight = 0.1; @@ -2384,7 +2383,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { cancelled?: BooleanHolder, useIllusion = false, ): TypeDamageMultiplier { - if (!isNullOrUndefined(this.turnData?.moveEffectiveness)) { + if (this.turnData?.moveEffectiveness != null) { return this.turnData?.moveEffectiveness; } @@ -3606,7 +3605,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { }); const ally = this.getAlly(); - if (!isNullOrUndefined(ally)) { + if (ally != null) { const ignore = this.hasAbilityWithAttr("MoveAbilityBypassAbAttr") || sourceMove.hasFlag(MoveFlags.IGNORE_ABILITIES); applyAbAttrs("AllyStatMultiplierAbAttr", { @@ -4022,7 +4021,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { const ally = this.getAlly(); /** Additionally apply friend guard damage reduction if ally has it. */ - if (globalScene.currentBattle.double && !isNullOrUndefined(ally) && ally.isActive(true)) { + if (globalScene.currentBattle.double && ally != null && ally.isActive(true)) { applyAbAttrs("AlliedFieldDamageReductionAbAttr", { ...abAttrParams, // Same parameters as before, except we are applying the ally's ability @@ -6397,13 +6396,13 @@ export class EnemyPokemon extends Pokemon { if ( speciesId in Overrides.ENEMY_FORM_OVERRIDES - && !isNullOrUndefined(Overrides.ENEMY_FORM_OVERRIDES[speciesId]) + && Overrides.ENEMY_FORM_OVERRIDES[speciesId] != null && this.species.forms[Overrides.ENEMY_FORM_OVERRIDES[speciesId]] ) { this.formIndex = Overrides.ENEMY_FORM_OVERRIDES[speciesId]; } else if (globalScene.gameMode.isDaily && globalScene.gameMode.isWaveFinal(globalScene.currentBattle.waveIndex)) { const eventBoss = getDailyEventSeedBoss(globalScene.seed); - if (!isNullOrUndefined(eventBoss)) { + if (eventBoss != null) { this.formIndex = eventBoss.formIndex; } } diff --git a/src/game-mode.ts b/src/game-mode.ts index 9ea3adf59d3..e543e3c42ca 100644 --- a/src/game-mode.ts +++ b/src/game-mode.ts @@ -14,7 +14,7 @@ import { SpeciesId } from "#enums/species-id"; import type { Arena } from "#field/arena"; import { classicFixedBattles, type FixedBattleConfigs } from "#trainers/fixed-battle-configs"; import { applyChallenges } from "#utils/challenge-utils"; -import { BooleanHolder, isNullOrUndefined, randSeedInt, randSeedItem } from "#utils/common"; +import { BooleanHolder, randSeedInt, randSeedItem } from "#utils/common"; import { getPokemonSpecies } from "#utils/pokemon-utils"; import i18next from "i18next"; @@ -146,7 +146,7 @@ export class GameMode implements GameModeConfig { * - Town */ getStartingBiome(): BiomeId { - if (!isNullOrUndefined(Overrides.STARTING_BIOME_OVERRIDE)) { + if (Overrides.STARTING_BIOME_OVERRIDE != null) { return Overrides.STARTING_BIOME_OVERRIDE; } @@ -234,7 +234,7 @@ export class GameMode implements GameModeConfig { getOverrideSpecies(waveIndex: number): PokemonSpecies | null { if (this.isDaily && this.isWaveFinal(waveIndex)) { const eventBoss = getDailyEventSeedBoss(globalScene.seed); - if (!isNullOrUndefined(eventBoss)) { + if (eventBoss != null) { // Cannot set form index here, it will be overriden when adding it as enemy pokemon. return getPokemonSpecies(eventBoss.speciesId); } diff --git a/src/modifier/init-modifier-pools.ts b/src/modifier/init-modifier-pools.ts index ba12920407d..e6ec69eac7f 100644 --- a/src/modifier/init-modifier-pools.ts +++ b/src/modifier/init-modifier-pools.ts @@ -31,7 +31,6 @@ import { } from "#modifiers/modifier-pools"; import { WeightedModifierType } from "#modifiers/modifier-type"; import type { WeightedModifierTypeWeightFunc } from "#types/modifier-types"; -import { isNullOrUndefined } from "#utils/common"; /** * Initialize the wild modifier pool @@ -409,7 +408,7 @@ function initUltraModifierPool() { if (!isHoldingOrb) { const moveset = p .getMoveset(true) - .filter(m => !isNullOrUndefined(m)) + .filter(m => m != null) .map(m => m.moveId); const canSetStatus = p.canSetStatus(StatusEffect.TOXIC, true, true, null, true); @@ -455,7 +454,7 @@ function initUltraModifierPool() { if (!isHoldingOrb) { const moveset = p .getMoveset(true) - .filter(m => !isNullOrUndefined(m)) + .filter(m => m != null) .map(m => m.moveId); const canSetStatus = p.canSetStatus(StatusEffect.BURN, true, true, null, true); diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index 579fb75ce3b..d67011bc145 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -119,15 +119,7 @@ import type { PokemonMoveSelectFilter, PokemonSelectFilter } from "#ui/party-ui- import { PartyUiHandler } from "#ui/party-ui-handler"; import { getModifierTierTextTint } from "#ui/text"; import { applyChallenges } from "#utils/challenge-utils"; -import { - BooleanHolder, - formatMoney, - isNullOrUndefined, - NumberHolder, - padInt, - randSeedInt, - randSeedItem, -} from "#utils/common"; +import { BooleanHolder, formatMoney, NumberHolder, padInt, randSeedInt, randSeedItem } from "#utils/common"; import { getEnumKeys, getEnumValues } from "#utils/enums"; import { getModifierPoolForType, getModifierType } from "#utils/modifier-utils"; import { toCamelCase } from "#utils/strings"; @@ -263,7 +255,7 @@ export class ModifierType { this.tier = modifier.modifierType.tier; return this; } - if (isNullOrUndefined(defaultTier)) { + if (defaultTier == null) { // If weight is 0, keep track of the first tier where the item was found defaultTier = modifier.modifierType.tier; } @@ -2920,7 +2912,7 @@ export function getPartyLuckValue(party: Pokemon[]): number { globalScene.executeWithSeedOffset( () => { const eventLuck = getDailyEventSeedLuck(globalScene.seed); - if (!isNullOrUndefined(eventLuck)) { + if (eventLuck != null) { DailyLuck.value = eventLuck; return; } diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index 1f470e592c2..82e64316edd 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -42,7 +42,7 @@ import type { import type { VoucherType } from "#system/voucher"; import type { ModifierInstanceMap, ModifierString } from "#types/modifier-types"; import { addTextObject } from "#ui/text"; -import { BooleanHolder, hslToHex, isNullOrUndefined, NumberHolder, randSeedFloat, toDmgValue } from "#utils/common"; +import { BooleanHolder, hslToHex, NumberHolder, randSeedFloat, toDmgValue } from "#utils/common"; import { getModifierType } from "#utils/modifier-utils"; import i18next from "i18next"; @@ -2113,10 +2113,7 @@ export class PokemonHpRestoreModifier extends ConsumablePokemonModifier { * @returns `true` if the {@linkcode PokemonHpRestoreModifier} should be applied */ override shouldApply(playerPokemon?: PlayerPokemon, multiplier?: number): boolean { - return ( - super.shouldApply(playerPokemon) - && (this.fainted || (!isNullOrUndefined(multiplier) && typeof multiplier === "number")) - ); + return super.shouldApply(playerPokemon) && (this.fainted || (multiplier != null && typeof multiplier === "number")); } /** @@ -2753,10 +2750,10 @@ export class PokemonMultiHitModifier extends PokemonHeldItemModifier { return false; } - if (!isNullOrUndefined(count)) { + if (count != null) { return this.applyHitCountBoost(count); } - if (!isNullOrUndefined(damageMultiplier)) { + if (damageMultiplier != null) { return this.applyDamageModifier(pokemon, damageMultiplier); } diff --git a/src/phases/faint-phase.ts b/src/phases/faint-phase.ts index 349dfcfa8e5..821d16c6546 100644 --- a/src/phases/faint-phase.ts +++ b/src/phases/faint-phase.ts @@ -17,7 +17,6 @@ import type { EnemyPokemon, PlayerPokemon, Pokemon } from "#field/pokemon"; import { PokemonInstantReviveModifier } from "#modifiers/modifier"; import { PokemonMove } from "#moves/pokemon-move"; import { PokemonPhase } from "#phases/pokemon-phase"; -import { isNullOrUndefined } from "#utils/common"; import i18next from "i18next"; export class FaintPhase extends PokemonPhase { @@ -187,7 +186,7 @@ export class FaintPhase extends PokemonPhase { // in double battles redirect potential moves off fainted pokemon const allyPokemon = pokemon.getAlly(); - if (globalScene.currentBattle.double && !isNullOrUndefined(allyPokemon)) { + if (globalScene.currentBattle.double && allyPokemon != null) { globalScene.redirectPokemonMoves(pokemon, allyPokemon); } diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index 6c143f1f8a1..717bc670558 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -40,7 +40,7 @@ import { DamageAchv } from "#system/achv"; import type { DamageResult } from "#types/damage-result"; import type { TurnMove } from "#types/turn-move"; import type { nil } from "#utils/common"; -import { BooleanHolder, isNullOrUndefined, NumberHolder } from "#utils/common"; +import { BooleanHolder, NumberHolder } from "#utils/common"; import i18next from "i18next"; export type HitCheckEntry = [HitCheckResult, TypeDamageMultiplier]; @@ -740,7 +740,7 @@ export class MoveEffectPhase extends PokemonPhase { (attr: MoveAttr) => attr.is("MoveEffectAttr") && attr.trigger === triggerType - && (isNullOrUndefined(selfTarget) || attr.selfTarget === selfTarget) + && (selfTarget == null || attr.selfTarget === selfTarget) && (!attr.firstHitOnly || this.firstHit) && (!attr.lastHitOnly || this.lastHit) && (!attr.firstTargetOnly || (firstTarget ?? true)), @@ -765,7 +765,7 @@ export class MoveEffectPhase extends PokemonPhase { */ protected applyMoveEffects(target: Pokemon, effectiveness: TypeDamageMultiplier, firstTarget: boolean): void { const user = this.getUserPokemon(); - if (isNullOrUndefined(user)) { + if (user == null) { return; } diff --git a/src/phases/mystery-encounter-phases.ts b/src/phases/mystery-encounter-phases.ts index df670deaf26..4f50b40c965 100644 --- a/src/phases/mystery-encounter-phases.ts +++ b/src/phases/mystery-encounter-phases.ts @@ -14,7 +14,7 @@ import type { OptionSelectSettings } from "#mystery-encounters/encounter-phase-u import { transitionMysteryEncounterIntroVisuals } from "#mystery-encounters/encounter-phase-utils"; import type { MysteryEncounterOption, OptionPhaseCallback } from "#mystery-encounters/mystery-encounter-option"; import { SeenEncounterData } from "#mystery-encounters/mystery-encounter-save-data"; -import { isNullOrUndefined, randSeedItem } from "#utils/common"; +import { randSeedItem } from "#utils/common"; import i18next from "i18next"; /** @@ -93,7 +93,7 @@ export class MysteryEncounterPhase extends Phase { if (option.onPreOptionPhase) { globalScene.executeWithSeedOffset(async () => { return await option.onPreOptionPhase!().then(result => { - if (isNullOrUndefined(result) || result) { + if (result == null || result) { this.continueEncounter(); } }); @@ -578,7 +578,7 @@ export class PostMysteryEncounterPhase extends Phase { if (this.onPostOptionSelect) { globalScene.executeWithSeedOffset(async () => { return await this.onPostOptionSelect!().then(result => { - if (isNullOrUndefined(result) || result) { + if (result == null || result) { this.continueEncounter(); } }); diff --git a/src/phases/pokemon-anim-phase.ts b/src/phases/pokemon-anim-phase.ts index 39e9c609aec..c45f201641c 100644 --- a/src/phases/pokemon-anim-phase.ts +++ b/src/phases/pokemon-anim-phase.ts @@ -4,7 +4,6 @@ import { PokemonAnimType } from "#enums/pokemon-anim-type"; import { SpeciesId } from "#enums/species-id"; import type { Pokemon } from "#field/pokemon"; import { BattlePhase } from "#phases/battle-phase"; -import { isNullOrUndefined } from "#utils/common"; export class PokemonAnimPhase extends BattlePhase { public readonly phaseName = "PokemonAnimPhase"; @@ -52,7 +51,7 @@ export class PokemonAnimPhase extends BattlePhase { private doSubstituteAddAnim(): void { const substitute = this.pokemon.getTag(SubstituteTag); - if (isNullOrUndefined(substitute)) { + if (substitute == null) { this.end(); return; } @@ -336,7 +335,7 @@ export class PokemonAnimPhase extends BattlePhase { // Note: unlike the other Commander animation, this is played through the // Dondozo instead of the Tatsugiri. const tatsugiri = this.pokemon.getAlly(); - if (isNullOrUndefined(tatsugiri)) { + if (tatsugiri == null) { console.warn("Aborting COMMANDER_REMOVE anim: Tatsugiri is undefined"); this.end(); return; diff --git a/src/phases/revival-blessing-phase.ts b/src/phases/revival-blessing-phase.ts index fdb108d62ac..5d75f2c9b47 100644 --- a/src/phases/revival-blessing-phase.ts +++ b/src/phases/revival-blessing-phase.ts @@ -5,7 +5,7 @@ import type { PlayerPokemon } from "#field/pokemon"; import { BattlePhase } from "#phases/battle-phase"; import type { PartyOption } from "#ui/party-ui-handler"; import { PartyUiHandler, PartyUiMode } from "#ui/party-ui-handler"; -import { isNullOrUndefined, toDmgValue } from "#utils/common"; +import { toDmgValue } from "#utils/common"; import i18next from "i18next"; /** @@ -42,11 +42,7 @@ export class RevivalBlessingPhase extends BattlePhase { ); const allyPokemon = this.user.getAlly(); - if ( - globalScene.currentBattle.double - && globalScene.getPlayerParty().length > 1 - && !isNullOrUndefined(allyPokemon) - ) { + if (globalScene.currentBattle.double && globalScene.getPlayerParty().length > 1 && allyPokemon != null) { if (slotIndex <= 1) { // Revived ally pokemon globalScene.phaseManager.unshiftNew( diff --git a/src/phases/select-modifier-phase.ts b/src/phases/select-modifier-phase.ts index 3c378a95b2a..2031fc5c5f1 100644 --- a/src/phases/select-modifier-phase.ts +++ b/src/phases/select-modifier-phase.ts @@ -27,7 +27,7 @@ import { BattlePhase } from "#phases/battle-phase"; import type { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; import { SHOP_OPTIONS_ROW_LIMIT } from "#ui/modifier-select-ui-handler"; import { PartyOption, PartyUiHandler, PartyUiMode } from "#ui/party-ui-handler"; -import { isNullOrUndefined, NumberHolder } from "#utils/common"; +import { NumberHolder } from "#utils/common"; import i18next from "i18next"; export type ModifierSelectCallback = (rowCursor: number, cursor: number) => boolean; @@ -429,7 +429,7 @@ export class SelectModifierPhase extends BattlePhase { } let multiplier = 1; - if (!isNullOrUndefined(this.customModifierSettings?.rerollMultiplier)) { + if (this.customModifierSettings?.rerollMultiplier != null) { if (this.customModifierSettings.rerollMultiplier < 0) { // Completely overrides reroll cost to -1 and early exits return -1; diff --git a/src/phases/select-starter-phase.ts b/src/phases/select-starter-phase.ts index a08394e3acb..27e2150fd06 100644 --- a/src/phases/select-starter-phase.ts +++ b/src/phases/select-starter-phase.ts @@ -10,7 +10,6 @@ import { overrideHeldItems, overrideModifiers } from "#modifiers/modifier"; import { SaveSlotUiMode } from "#ui/save-slot-select-ui-handler"; import type { Starter } from "#ui/starter-select-ui-handler"; import { applyChallenges } from "#utils/challenge-utils"; -import { isNullOrUndefined } from "#utils/common"; import { getPokemonSpecies } from "#utils/pokemon-utils"; import SoundFade from "phaser3-rex-plugins/plugins/soundfade"; @@ -51,7 +50,7 @@ export class SelectStarterPhase extends Phase { let starterFormIndex = Math.min(starterProps.formIndex, Math.max(starter.species.forms.length - 1, 0)); if ( starter.species.speciesId in Overrides.STARTER_FORM_OVERRIDES - && !isNullOrUndefined(Overrides.STARTER_FORM_OVERRIDES[starter.species.speciesId]) + && Overrides.STARTER_FORM_OVERRIDES[starter.species.speciesId] != null && starter.species.forms[Overrides.STARTER_FORM_OVERRIDES[starter.species.speciesId]!] ) { starterFormIndex = Overrides.STARTER_FORM_OVERRIDES[starter.species.speciesId]!; @@ -89,7 +88,7 @@ export class SelectStarterPhase extends Phase { starterPokemon.nickname = starter.nickname; } - if (!isNullOrUndefined(starter.teraType)) { + if (starter.teraType != null) { starterPokemon.teraType = starter.teraType; } else { starterPokemon.teraType = starterPokemon.species.type1; diff --git a/src/phases/stat-stage-change-phase.ts b/src/phases/stat-stage-change-phase.ts index 6c15342ddeb..2731c037d5f 100644 --- a/src/phases/stat-stage-change-phase.ts +++ b/src/phases/stat-stage-change-phase.ts @@ -13,7 +13,7 @@ import type { Pokemon } from "#field/pokemon"; import { ResetNegativeStatStageModifier } from "#modifiers/modifier"; import { PokemonPhase } from "#phases/pokemon-phase"; import type { ConditionalUserFieldProtectStatAbAttrParams, PreStatStageChangeAbAttrParams } from "#types/ability-types"; -import { BooleanHolder, isNullOrUndefined, NumberHolder } from "#utils/common"; +import { BooleanHolder, NumberHolder } from "#utils/common"; import i18next from "i18next"; export type StatStageChangeCallback = ( @@ -153,7 +153,7 @@ export class StatStageChangePhase extends PokemonPhase { applyAbAttrs("ConditionalUserFieldProtectStatAbAttr", abAttrParams); // TODO: Consider skipping this call if `cancelled` is false. const ally = pokemon.getAlly(); - if (!isNullOrUndefined(ally)) { + if (ally != null) { applyAbAttrs("ConditionalUserFieldProtectStatAbAttr", { ...abAttrParams, pokemon: ally }); } diff --git a/src/phases/title-phase.ts b/src/phases/title-phase.ts index d422766bf09..414be4c820c 100644 --- a/src/phases/title-phase.ts +++ b/src/phases/title-phase.ts @@ -18,7 +18,7 @@ import { vouchers } from "#system/voucher"; import type { SessionSaveData } from "#types/save-data"; import type { OptionSelectConfig, OptionSelectItem } from "#ui/abstract-option-select-ui-handler"; import { SaveSlotUiMode } from "#ui/save-slot-select-ui-handler"; -import { isLocal, isLocalServerConnected, isNullOrUndefined } from "#utils/common"; +import { isLocal, isLocalServerConnected } from "#utils/common"; import i18next from "i18next"; export class TitlePhase extends Phase { @@ -289,7 +289,7 @@ export class TitlePhase extends Phase { } else { // Grab first 10 chars of ISO date format (YYYY-MM-DD) and convert to base64 let seed: string = btoa(new Date().toISOString().substring(0, 10)); - if (!isNullOrUndefined(Overrides.DAILY_RUN_SEED_OVERRIDE)) { + if (Overrides.DAILY_RUN_SEED_OVERRIDE != null) { seed = Overrides.DAILY_RUN_SEED_OVERRIDE; } generateDaily(seed); diff --git a/src/sprites/variant.ts b/src/sprites/variant.ts index 28d7ed13839..9d7a20bc058 100644 --- a/src/sprites/variant.ts +++ b/src/sprites/variant.ts @@ -2,7 +2,6 @@ import { globalScene } from "#app/global-scene"; import { VariantTier } from "#enums/variant-tier"; import type { Pokemon } from "#field/pokemon"; import { hasExpSprite } from "#sprites/sprite-utils"; -import { isNullOrUndefined } from "#utils/common"; export type Variant = 0 | 1 | 2; @@ -138,7 +137,7 @@ export async function populateVariantColorCache( return fallbackVariantColor(cacheKey, spritePath, useExpSprite, battleSpritePath, error); }) .then(c => { - if (!isNullOrUndefined(c)) { + if (c != null) { variantColorCache[cacheKey] = c; } }); diff --git a/src/system/ribbons/ribbon-methods.ts b/src/system/ribbons/ribbon-methods.ts index 138c0be7b51..f1aeb9fefc2 100644 --- a/src/system/ribbons/ribbon-methods.ts +++ b/src/system/ribbons/ribbon-methods.ts @@ -2,7 +2,6 @@ import { globalScene } from "#app/global-scene"; import { pokemonPrevolutions } from "#balance/pokemon-evolutions"; import type { SpeciesId } from "#enums/species-id"; import type { RibbonFlag } from "#system/ribbons/ribbon-data"; -import { isNullOrUndefined } from "#utils/common"; /** * Award one or more ribbons to a species and its pre-evolutions @@ -14,7 +13,7 @@ export function awardRibbonsToSpeciesLine(id: SpeciesId, ribbons: RibbonFlag): v const dexData = globalScene.gameData.dexData; dexData[id].ribbons.award(ribbons); // Mark all pre-evolutions of the Pokémon with the same ribbon flags. - for (let prevoId = pokemonPrevolutions[id]; !isNullOrUndefined(prevoId); prevoId = pokemonPrevolutions[prevoId]) { + for (let prevoId = pokemonPrevolutions[id]; prevoId != null; prevoId = pokemonPrevolutions[prevoId]) { dexData[prevoId].ribbons.award(ribbons); } } diff --git a/src/system/version-migration/versions/v1_0_4.ts b/src/system/version-migration/versions/v1_0_4.ts index 8229b9320d5..5342396d576 100644 --- a/src/system/version-migration/versions/v1_0_4.ts +++ b/src/system/version-migration/versions/v1_0_4.ts @@ -8,7 +8,6 @@ import type { SessionSaveData, SystemSaveData } from "#types/save-data"; import type { SessionSaveMigrator } from "#types/session-save-migrator"; import type { SettingsSaveMigrator } from "#types/settings-save-migrator"; import type { SystemSaveMigrator } from "#types/system-save-migrator"; -import { isNullOrUndefined } from "#utils/common"; /** * Migrate ability starter data if empty for caught species. @@ -82,7 +81,7 @@ const fixLegendaryStats: SystemSaveMigrator = { const fixStarterData: SystemSaveMigrator = { version: "1.0.4", migrate: (data: SystemSaveData): void => { - if (!isNullOrUndefined(data.starterData)) { + if (data.starterData != null) { for (const starterId of defaultStarterSpecies) { if (data.starterData[starterId]?.abilityAttr) { data.starterData[starterId].abilityAttr |= AbilityAttr.ABILITY_1; @@ -198,7 +197,7 @@ const migrateCustomPokemonData: SessionSaveMigrator = { pokemon["fusionMysteryEncounterPokemonData"] = null; } pokemon.customPokemonData = pokemon.customPokemonData ?? new CustomPokemonData(); - if (!isNullOrUndefined(pokemon["natureOverride"]) && pokemon["natureOverride"] >= 0) { + if (pokemon["natureOverride"] != null && pokemon["natureOverride"] >= 0) { pokemon.customPokemonData.nature = pokemon["natureOverride"]; pokemon["natureOverride"] = -1; } diff --git a/src/system/version-migration/versions/v1_7_0.ts b/src/system/version-migration/versions/v1_7_0.ts index 6d365cf31ac..e526ccd2c2b 100644 --- a/src/system/version-migration/versions/v1_7_0.ts +++ b/src/system/version-migration/versions/v1_7_0.ts @@ -3,7 +3,6 @@ import { DexAttr } from "#enums/dex-attr"; import type { SessionSaveData, SystemSaveData } from "#types/save-data"; import type { SessionSaveMigrator } from "#types/session-save-migrator"; import type { SystemSaveMigrator } from "#types/system-save-migrator"; -import { isNullOrUndefined } from "#utils/common"; import { getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils"; /** @@ -68,13 +67,13 @@ const migrateTera: SessionSaveMigrator = { } data.party.forEach(p => { - if (isNullOrUndefined(p.teraType)) { + if (p.teraType == null) { p.teraType = getPokemonSpeciesForm(p.species, p.formIndex).type1; } }); data.enemyParty.forEach(p => { - if (isNullOrUndefined(p.teraType)) { + if (p.teraType == null) { p.teraType = getPokemonSpeciesForm(p.species, p.formIndex).type1; } }); diff --git a/src/timed-event-manager.ts b/src/timed-event-manager.ts index ed92a1c9ca5..7db89b2a0ef 100644 --- a/src/timed-event-manager.ts +++ b/src/timed-event-manager.ts @@ -9,7 +9,6 @@ import { TextStyle } from "#enums/text-style"; import { WeatherType } from "#enums/weather-type"; import { addTextObject } from "#ui/text"; import type { nil } from "#utils/common"; -import { isNullOrUndefined } from "#utils/common"; import i18next from "i18next"; export enum EventType { @@ -428,7 +427,7 @@ export class TimedEventManager { getEventBannerLangs(): string[] { const ret: string[] = []; - ret.push(...timedEvents.find(te => this.isActive(te) && !isNullOrUndefined(te.availableLangs))?.availableLangs!); + ret.push(...timedEvents.find(te => this.isActive(te) && te.availableLangs != null)?.availableLangs!); return ret; } @@ -437,7 +436,7 @@ export class TimedEventManager { timedEvents .filter(te => this.isActive(te)) .map(te => { - if (!isNullOrUndefined(te.eventEncounters)) { + if (te.eventEncounters != null) { ret.push(...te.eventEncounters); } }); @@ -452,7 +451,7 @@ export class TimedEventManager { let multiplier = CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER; const classicFriendshipEvents = timedEvents.filter(te => this.isActive(te)); for (const fe of classicFriendshipEvents) { - if (!isNullOrUndefined(fe.classicFriendshipMultiplier) && fe.classicFriendshipMultiplier > multiplier) { + if (fe.classicFriendshipMultiplier != null && fe.classicFriendshipMultiplier > multiplier) { multiplier = fe.classicFriendshipMultiplier; } } @@ -476,7 +475,7 @@ export class TimedEventManager { timedEvents .filter(te => this.isActive(te)) .map(te => { - if (!isNullOrUndefined(te.delibirdyBuff)) { + if (te.delibirdyBuff != null) { ret.push(...te.delibirdyBuff); } }); @@ -492,7 +491,7 @@ export class TimedEventManager { timedEvents .filter(te => this.isActive(te)) .map(te => { - if (!isNullOrUndefined(te.weather)) { + if (te.weather != null) { ret.push(...te.weather); } }); @@ -504,7 +503,7 @@ export class TimedEventManager { timedEvents .filter(te => this.isActive(te)) .map(te => { - if (!isNullOrUndefined(te.mysteryEncounterTierChanges)) { + if (te.mysteryEncounterTierChanges != null) { ret.push(...te.mysteryEncounterTierChanges); } }); @@ -514,7 +513,7 @@ export class TimedEventManager { getEventMysteryEncountersDisabled(): MysteryEncounterType[] { const ret: MysteryEncounterType[] = []; timedEvents - .filter(te => this.isActive(te) && !isNullOrUndefined(te.mysteryEncounterTierChanges)) + .filter(te => this.isActive(te) && te.mysteryEncounterTierChanges != null) .map(te => { te.mysteryEncounterTierChanges?.map(metc => { if (metc.disable) { @@ -531,7 +530,7 @@ export class TimedEventManager { ): MysteryEncounterTier { let ret = normal; timedEvents - .filter(te => this.isActive(te) && !isNullOrUndefined(te.mysteryEncounterTierChanges)) + .filter(te => this.isActive(te) && te.mysteryEncounterTierChanges != null) .map(te => { te.mysteryEncounterTierChanges?.map(metc => { if (metc.mysteryEncounter === encounterType) { @@ -544,7 +543,7 @@ export class TimedEventManager { getEventLuckBoost(): number { let ret = 0; - const luckEvents = timedEvents.filter(te => this.isActive(te) && !isNullOrUndefined(te.luckBoost)); + const luckEvents = timedEvents.filter(te => this.isActive(te) && te.luckBoost != null); for (const le of luckEvents) { ret += le.luckBoost!; } @@ -556,7 +555,7 @@ export class TimedEventManager { timedEvents .filter(te => this.isActive(te)) .map(te => { - if (!isNullOrUndefined(te.luckBoostedSpecies)) { + if (te.luckBoostedSpecies != null) { ret.push(...te.luckBoostedSpecies.filter(s => !ret.includes(s))); } }); @@ -576,7 +575,7 @@ export class TimedEventManager { getFixedBattleEventRewards(wave: number): string[] { const ret: string[] = []; timedEvents - .filter(te => this.isActive(te) && !isNullOrUndefined(te.classicWaveRewards)) + .filter(te => this.isActive(te) && te.classicWaveRewards != null) .map(te => { ret.push(...te.classicWaveRewards!.filter(cwr => cwr.wave === wave).map(cwr => cwr.type)); }); @@ -586,7 +585,7 @@ export class TimedEventManager { // Gets the extra shiny chance for trainers due to event (odds/65536) getClassicTrainerShinyChance(): number { let ret = 0; - const tsEvents = timedEvents.filter(te => this.isActive(te) && !isNullOrUndefined(te.trainerShinyChance)); + const tsEvents = timedEvents.filter(te => this.isActive(te) && te.trainerShinyChance != null); tsEvents.map(t => (ret += t.trainerShinyChance!)); return ret; } @@ -594,7 +593,7 @@ export class TimedEventManager { getEventBgmReplacement(bgm: string): string { let ret = bgm; timedEvents.map(te => { - if (this.isActive(te) && !isNullOrUndefined(te.music)) { + if (this.isActive(te) && te.music != null) { te.music.map(mr => { if (mr[0] === bgm) { console.log(`it is ${te.name} so instead of ${mr[0]} we play ${mr[1]}`); diff --git a/src/ui/containers/pokedex-mon-container.ts b/src/ui/containers/pokedex-mon-container.ts index 15ef6c9b5c8..158f42dd42a 100644 --- a/src/ui/containers/pokedex-mon-container.ts +++ b/src/ui/containers/pokedex-mon-container.ts @@ -3,7 +3,6 @@ import type { PokemonSpecies } from "#data/pokemon-species"; import { TextStyle } from "#enums/text-style"; import type { Variant } from "#sprites/variant"; import { addTextObject } from "#ui/text"; -import { isNullOrUndefined } from "#utils/common"; interface SpeciesDetails { shiny?: boolean; @@ -177,16 +176,16 @@ export class PokedexMonContainer extends Phaser.GameObjects.Container { const defaultDexAttr = globalScene.gameData.getSpeciesDefaultDexAttr(species, false, true); const defaultProps = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr); - if (!isNullOrUndefined(formIndex)) { + if (formIndex != null) { defaultProps.formIndex = formIndex; } - if (!isNullOrUndefined(shiny)) { + if (shiny != null) { defaultProps.shiny = shiny; } - if (!isNullOrUndefined(variant)) { + if (variant != null) { defaultProps.variant = variant; } - if (!isNullOrUndefined(female)) { + if (female != null) { defaultProps.female = female; } diff --git a/src/ui/handlers/mystery-encounter-ui-handler.ts b/src/ui/handlers/mystery-encounter-ui-handler.ts index bbbd3cb4af8..e4c9dfbfee3 100644 --- a/src/ui/handlers/mystery-encounter-ui-handler.ts +++ b/src/ui/handlers/mystery-encounter-ui-handler.ts @@ -13,7 +13,7 @@ import { PartyUiMode } from "#ui/party-ui-handler"; import { addBBCodeTextObject, getBBCodeFrag } from "#ui/text"; import { UiHandler } from "#ui/ui-handler"; import { addWindow, WindowVariant } from "#ui/ui-theme"; -import { fixedInt, isNullOrUndefined } from "#utils/common"; +import { fixedInt } from "#utils/common"; import i18next from "i18next"; import type BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext"; @@ -95,12 +95,10 @@ export class MysteryEncounterUiHandler extends UiHandler { super.show(args); this.overrideSettings = (args[0] as OptionSelectSettings) ?? {}; - const showDescriptionContainer = isNullOrUndefined(this.overrideSettings?.hideDescription) - ? true - : !this.overrideSettings.hideDescription; - const slideInDescription = isNullOrUndefined(this.overrideSettings?.slideInDescription) - ? true - : this.overrideSettings.slideInDescription; + const showDescriptionContainer = + this.overrideSettings?.hideDescription == null ? true : !this.overrideSettings.hideDescription; + const slideInDescription = + this.overrideSettings?.slideInDescription == null ? true : this.overrideSettings.slideInDescription; const startingCursorIndex = this.overrideSettings?.startingCursorIndex ?? 0; this.cursorContainer.setVisible(true); @@ -567,7 +565,7 @@ export class MysteryEncounterUiHandler extends UiHandler { } this.tooltipContainer.setVisible(true); - if (isNullOrUndefined(cursor) || cursor > this.optionsContainer.length - 2) { + if (cursor == null || cursor > this.optionsContainer.length - 2) { // Ignore hovers on view party button // Hide dex progress if visible this.showHideDexProgress(false); diff --git a/src/ui/handlers/pokedex-page-ui-handler.ts b/src/ui/handlers/pokedex-page-ui-handler.ts index 253309bf94a..31e2998b850 100644 --- a/src/ui/handlers/pokedex-page-ui-handler.ts +++ b/src/ui/handlers/pokedex-page-ui-handler.ts @@ -54,7 +54,7 @@ import { PokedexInfoOverlay } from "#ui/pokedex-info-overlay"; import { StatsContainer } from "#ui/stats-container"; import { addBBCodeTextObject, addTextObject, getTextColor, getTextStyleOptions } from "#ui/text"; import { addWindow } from "#ui/ui-theme"; -import { BooleanHolder, getLocalizedSpriteKey, isNullOrUndefined, padInt, rgbHexToRgba } from "#utils/common"; +import { BooleanHolder, getLocalizedSpriteKey, padInt, rgbHexToRgba } from "#utils/common"; import { getEnumValues } from "#utils/enums"; import { getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils"; import { toCamelCase, toTitleCase } from "#utils/strings"; @@ -2424,11 +2424,7 @@ export class PokedexPageUiHandler extends MessageUiHandler { // We will only update the sprite if there is a change to form, shiny/variant // or gender for species with gender sprite differences const shouldUpdateSprite = - (species?.genderDiffs && !isNullOrUndefined(female)) - || !isNullOrUndefined(formIndex) - || !isNullOrUndefined(shiny) - || !isNullOrUndefined(variant) - || forceUpdate; + (species?.genderDiffs && female != null) || formIndex != null || shiny != null || variant != null || forceUpdate; if (this.activeTooltip === "CANDY") { if (this.species && this.pokemonCandyContainer.visible) { diff --git a/src/ui/handlers/pokedex-scan-ui-handler.ts b/src/ui/handlers/pokedex-scan-ui-handler.ts index 1f5195588f3..18afd0598c2 100644 --- a/src/ui/handlers/pokedex-scan-ui-handler.ts +++ b/src/ui/handlers/pokedex-scan-ui-handler.ts @@ -6,7 +6,6 @@ import { FilterTextRow } from "#ui/filter-text"; 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 { isNullOrUndefined } from "#utils/common"; import i18next from "i18next"; export class PokedexScanUiHandler extends FormModalUiHandler { @@ -132,7 +131,7 @@ export class PokedexScanUiHandler extends FormModalUiHandler { return { label: value, handler: () => { - if (!isNullOrUndefined(evt.data) || evt.inputType?.toLowerCase() === "deletecontentbackward") { + if (evt.data != null || evt.inputType?.toLowerCase() === "deletecontentbackward") { inputObject.setText(value); } ui.revertMode(); diff --git a/src/ui/handlers/save-slot-select-ui-handler.ts b/src/ui/handlers/save-slot-select-ui-handler.ts index a71be5dd070..194971a005f 100644 --- a/src/ui/handlers/save-slot-select-ui-handler.ts +++ b/src/ui/handlers/save-slot-select-ui-handler.ts @@ -13,7 +13,7 @@ import { MessageUiHandler } from "#ui/message-ui-handler"; import { RunDisplayMode } from "#ui/run-info-ui-handler"; import { addTextObject } from "#ui/text"; import { addWindow } from "#ui/ui-theme"; -import { fixedInt, formatLargeNumber, getPlayTimeString, isNullOrUndefined } from "#utils/common"; +import { fixedInt, formatLargeNumber, getPlayTimeString } from "#utils/common"; import i18next from "i18next"; const SESSION_SLOTS_COUNT = 5; @@ -405,7 +405,7 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler { } this.setArrowVisibility(hasData); } - if (!isNullOrUndefined(prevSlotIndex)) { + if (prevSlotIndex != null) { this.revertSessionSlot(prevSlotIndex); } diff --git a/src/ui/handlers/starter-select-ui-handler.ts b/src/ui/handlers/starter-select-ui-handler.ts index 18be7e130e3..53e566864b1 100644 --- a/src/ui/handlers/starter-select-ui-handler.ts +++ b/src/ui/handlers/starter-select-ui-handler.ts @@ -66,7 +66,6 @@ import { BooleanHolder, fixedInt, getLocalizedSpriteKey, - isNullOrUndefined, NumberHolder, padInt, randIntRange, @@ -2548,7 +2547,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { case Button.CYCLE_TERA: if (this.canCycleTera) { const speciesForm = getPokemonSpeciesForm(this.lastSpecies.speciesId, starterAttributes.form ?? 0); - if (speciesForm.type1 === this.teraCursor && !isNullOrUndefined(speciesForm.type2)) { + if (speciesForm.type1 === this.teraCursor && speciesForm.type2 != null) { starterAttributes.tera = speciesForm.type2; originalStarterAttributes.tera = starterAttributes.tera; this.setSpeciesDetails(this.lastSpecies, { @@ -2790,7 +2789,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { */ switchMoveHandler(targetIndex: number, newMove: MoveId, previousMove: MoveId) { const starterMoveset = this.starterMoveset; - if (isNullOrUndefined(starterMoveset)) { + if (starterMoveset == null) { console.warn("Trying to update a non-existing moveset"); return; } @@ -3687,7 +3686,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { ); } - if (!isNullOrUndefined(props.formIndex)) { + if (props.formIndex != null) { // If switching forms while the pokemon is in the team, update its moveset this.updateSelectedStarterMoveset(species.speciesId); } @@ -3809,10 +3808,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { // We will only update the sprite if there is a change to form, shiny/variant // or gender for species with gender sprite differences const shouldUpdateSprite = - (species?.genderDiffs && !isNullOrUndefined(female)) - || !isNullOrUndefined(formIndex) - || !isNullOrUndefined(shiny) - || !isNullOrUndefined(variant); + (species?.genderDiffs && female != null) || formIndex != null || shiny != null || variant != null; const isFreshStartChallenge = globalScene.gameMode.hasChallenge(Challenges.FRESH_START); @@ -3850,7 +3846,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { ); // TODO: is this bang correct? this.abilityCursor = abilityIndex !== undefined ? abilityIndex : (abilityIndex = oldAbilityIndex); this.natureCursor = natureIndex !== undefined ? natureIndex : (natureIndex = oldNatureIndex); - this.teraCursor = !isNullOrUndefined(teraType) ? teraType : (teraType = oldTeraType); + this.teraCursor = teraType != null ? teraType : (teraType = oldTeraType); const [isInParty, partyIndex]: [boolean, number] = this.isInParty(species); // we use this to firstly check if the pokemon is in the party, and if so, to get the party index in order to update the icon image if (isInParty) { this.updatePartyIcon(species, partyIndex); @@ -3991,7 +3987,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { this.canCycleTera = !this.statsMode && this.allowTera - && !isNullOrUndefined(getPokemonSpeciesForm(species.speciesId, formIndex ?? 0).type2) + && getPokemonSpeciesForm(species.speciesId, formIndex ?? 0).type2 != null && !globalScene.gameMode.hasChallenge(Challenges.FRESH_START); } @@ -4592,7 +4588,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { this.canCycleTera = !this.statsMode && this.allowTera - && !isNullOrUndefined(getPokemonSpeciesForm(this.lastSpecies.speciesId, formIndex ?? 0).type2) + && getPokemonSpeciesForm(this.lastSpecies.speciesId, formIndex ?? 0).type2 != null && !globalScene.gameMode.hasChallenge(Challenges.FRESH_START); this.updateInstructions(); } diff --git a/src/ui/handlers/summary-ui-handler.ts b/src/ui/handlers/summary-ui-handler.ts index 1c647573cbf..c9c8229ebfd 100644 --- a/src/ui/handlers/summary-ui-handler.ts +++ b/src/ui/handlers/summary-ui-handler.ts @@ -27,15 +27,7 @@ import { getVariantTint } from "#sprites/variant"; import { achvs } from "#system/achv"; import { addBBCodeTextObject, addTextObject, getBBCodeFrag, getTextColor } from "#ui/text"; import { UiHandler } from "#ui/ui-handler"; -import { - fixedInt, - formatStat, - getLocalizedSpriteKey, - getShinyDescriptor, - isNullOrUndefined, - padInt, - rgbHexToRgba, -} from "#utils/common"; +import { fixedInt, formatStat, getLocalizedSpriteKey, getShinyDescriptor, padInt, rgbHexToRgba } from "#utils/common"; import { getEnumValues } from "#utils/enums"; import { toCamelCase, toTitleCase } from "#utils/strings"; import { argbFromRgba } from "@material/material-color-utilities"; @@ -895,10 +887,7 @@ export class SummaryUiHandler extends UiHandler { profileContainer.add(luckText); } - if ( - globalScene.gameData.achvUnlocks.hasOwnProperty(achvs.TERASTALLIZE.id) - && !isNullOrUndefined(this.pokemon) - ) { + if (globalScene.gameData.achvUnlocks.hasOwnProperty(achvs.TERASTALLIZE.id) && this.pokemon != null) { const teraIcon = globalScene.add.sprite(123, 26, "button_tera"); teraIcon.setName("terastallize-icon"); teraIcon.setFrame(PokemonType[this.pokemon.getTeraType()].toLowerCase()); diff --git a/src/ui/handlers/target-select-ui-handler.ts b/src/ui/handlers/target-select-ui-handler.ts index 4e3096b96f4..bd81278c661 100644 --- a/src/ui/handlers/target-select-ui-handler.ts +++ b/src/ui/handlers/target-select-ui-handler.ts @@ -8,7 +8,7 @@ import type { Pokemon } from "#field/pokemon"; import type { ModifierBar } from "#modifiers/modifier"; import { getMoveTargets } from "#moves/move-utils"; import { UiHandler } from "#ui/ui-handler"; -import { fixedInt, isNullOrUndefined } from "#utils/common"; +import { fixedInt } from "#utils/common"; export type TargetSelectCallback = (targets: BattlerIndex[]) => void; @@ -71,7 +71,7 @@ export class TargetSelectUiHandler extends UiHandler { */ resetCursor(cursorN: number, user: Pokemon): void { if ( - !isNullOrUndefined(cursorN) + cursorN != null && ([BattlerIndex.PLAYER, BattlerIndex.PLAYER_2].includes(cursorN) || user.tempSummonData.waveTurnCount === 1) ) { // Reset cursor on the first turn of a fight or if an ally was targeted last turn @@ -90,13 +90,10 @@ export class TargetSelectUiHandler extends UiHandler { this.targetSelectCallback(button === Button.ACTION ? targetIndexes : []); success = true; if (this.fieldIndex === BattlerIndex.PLAYER) { - if (isNullOrUndefined(this.cursor0) || this.cursor0 !== this.cursor) { + if (this.cursor0 == null || this.cursor0 !== this.cursor) { this.cursor0 = this.cursor; } - } else if ( - this.fieldIndex === BattlerIndex.PLAYER_2 - && (isNullOrUndefined(this.cursor1) || this.cursor1 !== this.cursor) - ) { + } else if (this.fieldIndex === BattlerIndex.PLAYER_2 && (this.cursor1 == null || this.cursor1 !== this.cursor)) { this.cursor1 = this.cursor; } } else if (this.isMultipleTargets) { diff --git a/src/ui/handlers/test-dialogue-ui-handler.ts b/src/ui/handlers/test-dialogue-ui-handler.ts index bd5c1a1dc37..b33e6726547 100644 --- a/src/ui/handlers/test-dialogue-ui-handler.ts +++ b/src/ui/handlers/test-dialogue-ui-handler.ts @@ -4,7 +4,6 @@ import type { OptionSelectItem } from "#ui/abstract-option-select-ui-handler"; 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 { isNullOrUndefined } from "#utils/common"; import i18next from "i18next"; export class TestDialogueUiHandler extends FormModalUiHandler { @@ -18,7 +17,7 @@ export class TestDialogueUiHandler extends FormModalUiHandler { .map((t, i) => { const value = Object.values(object)[i]; - if (typeof value === "object" && !isNullOrUndefined(value)) { + if (typeof value === "object" && value != null) { // we check for not null or undefined here because if the language json file has a null key, the typeof will still be an object, but that object will be null, causing issues // If the value is an object, execute the same process // si el valor es un objeto ejecuta el mismo proceso @@ -27,7 +26,7 @@ export class TestDialogueUiHandler extends FormModalUiHandler { t => t.length > 0, ); } - if (typeof value === "string" || isNullOrUndefined(value)) { + if (typeof value === "string" || value == null) { // we check for null or undefined here as per above - the typeof is still an object but the value is null so we need to exit out of this and pass the null key // Return in the format expected by i18next @@ -109,7 +108,7 @@ export class TestDialogueUiHandler extends FormModalUiHandler { handler: () => { // this is here to make sure that if you try to backspace then enter, the last known evt.data (backspace) is picked up // this is because evt.data is null for backspace, so without this, the autocomplete windows just closes - if (!isNullOrUndefined(evt.data) || evt.inputType?.toLowerCase() === "deletecontentbackward") { + if (evt.data != null || evt.inputType?.toLowerCase() === "deletecontentbackward") { const separatedArray = inputObject.text.split(" "); separatedArray[separatedArray.length - 1] = value; inputObject.setText(separatedArray.join(" ")); diff --git a/src/utils/common.ts b/src/utils/common.ts index 2734b075a53..f0166b1e74c 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -458,15 +458,6 @@ export function truncateString(str: string, maxLength = 10) { return str; } -/** - * Report whether a given value is nullish (`null`/`undefined`). - * @param val - The value whose nullishness is being checked - * @returns `true` if `val` is either `null` or `undefined` - */ -export function isNullOrUndefined(val: any): val is null | undefined { - return val === null || val === undefined; -} - /** * This function is used in the context of a Pokémon battle game to calculate the actual integer damage value from a float result. * Many damage calculation formulas involve various parameters and result in float values. diff --git a/test/abilities/healer.test.ts b/test/abilities/healer.test.ts index 43280ff8271..fb28cd891ac 100644 --- a/test/abilities/healer.test.ts +++ b/test/abilities/healer.test.ts @@ -6,7 +6,6 @@ import { SpeciesId } from "#enums/species-id"; import { StatusEffect } from "#enums/status-effect"; import type { Pokemon } from "#field/pokemon"; import { GameManager } from "#test/test-utils/game-manager"; -import { isNullOrUndefined } from "#utils/common"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; @@ -37,7 +36,7 @@ describe("Abilities - Healer", () => { // Mock healer to have a 100% chance of healing its ally vi.spyOn(allAbilities[AbilityId.HEALER].getAttrs("PostTurnResetStatusAbAttr")[0], "getCondition").mockReturnValue( - (pokemon: Pokemon) => !isNullOrUndefined(pokemon.getAlly()), + (pokemon: Pokemon) => pokemon.getAlly() != null, ); }); diff --git a/test/eggs/egg.test.ts b/test/eggs/egg.test.ts index 8b47e68f402..001adf83b37 100644 --- a/test/eggs/egg.test.ts +++ b/test/eggs/egg.test.ts @@ -202,7 +202,7 @@ describe("Egg Generation Tests", () => { const scene = game.scene; const eggMoveIndex = new Egg({ scene }).eggMoveIndex; - const result = !Utils.isNullOrUndefined(eggMoveIndex) && eggMoveIndex >= 0 && eggMoveIndex <= 3; + const result = eggMoveIndex != null && eggMoveIndex >= 0 && eggMoveIndex <= 3; expect(result).toBe(true); }); diff --git a/test/mystery-encounter/encounter-test-utils.ts b/test/mystery-encounter/encounter-test-utils.ts index 7b2dbfc9aeb..4aad0e000d9 100644 --- a/test/mystery-encounter/encounter-test-utils.ts +++ b/test/mystery-encounter/encounter-test-utils.ts @@ -17,7 +17,6 @@ import type { MessageUiHandler } from "#ui/message-ui-handler"; import type { MysteryEncounterUiHandler } from "#ui/mystery-encounter-ui-handler"; import type { OptionSelectUiHandler } from "#ui/option-select-ui-handler"; import type { PartyUiHandler } from "#ui/party-ui-handler"; -import { isNullOrUndefined } from "#utils/common"; import { expect, vi } from "vitest"; /** @@ -147,7 +146,7 @@ export async function runSelectMysteryEncounterOption( break; } - if (!isNullOrUndefined(secondaryOptionSelect?.pokemonNo)) { + if (secondaryOptionSelect?.pokemonNo != null) { await handleSecondaryOptionSelect(game, secondaryOptionSelect.pokemonNo, secondaryOptionSelect.optionNo); } else { uiHandler.processInput(Button.ACTION); @@ -174,7 +173,7 @@ async function handleSecondaryOptionSelect(game: GameManager, pokemonNo: number, partyUiHandler.processInput(Button.ACTION); // If there is a second choice to make after selecting a Pokemon - if (!isNullOrUndefined(optionNo)) { + if (optionNo != null) { // Wait for Summary menu to close and second options to spawn const secondOptionUiHandler = game.scene.ui.handlers[UiMode.OPTION_SELECT] as OptionSelectUiHandler; vi.spyOn(secondOptionUiHandler, "show"); diff --git a/test/test-utils/game-manager.ts b/test/test-utils/game-manager.ts index 1b379daae88..f9db964ad26 100644 --- a/test/test-utils/game-manager.ts +++ b/test/test-utils/game-manager.ts @@ -52,7 +52,6 @@ import type { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler"; import type { PartyUiHandler } from "#ui/party-ui-handler"; import type { StarterSelectUiHandler } from "#ui/starter-select-ui-handler"; import type { TargetSelectUiHandler } from "#ui/target-select-ui-handler"; -import { isNullOrUndefined } from "#utils/common"; import fs from "node:fs"; import { AES, enc } from "crypto-js"; import { expect, vi } from "vitest"; @@ -240,7 +239,7 @@ export class GameManager { * @returns A Promise that resolves when the EncounterPhase ends. */ async runToMysteryEncounter(encounterType?: MysteryEncounterType, species?: SpeciesId[]) { - if (!isNullOrUndefined(encounterType)) { + if (encounterType != null) { this.override.disableTrainerWaves(); this.override.mysteryEncounter(encounterType); } @@ -272,7 +271,7 @@ export class GameManager { ); await this.phaseInterceptor.to("EncounterPhase"); - if (!isNullOrUndefined(encounterType)) { + if (encounterType != null) { expect(this.scene.currentBattle?.mysteryEncounter?.encounterType).toBe(encounterType); } } From 465f0c2ced921429cf06ace4c26ebb22bd52db06 Mon Sep 17 00:00:00 2001 From: Bertie690 <136088738+Bertie690@users.noreply.github.com> Date: Sat, 13 Sep 2025 01:19:48 -0400 Subject: [PATCH 05/40] [Refactor] `getPokemonById` returns `undefined` instead of `null` https://github.com/pagefaultgames/pokerogue/pull/6544 * [Refactor] Make `BattleScene.getPokemonById` return `undefined` instead of `null` * fixed substitute unit test :( * Update src/phases/obtain-status-effect-phase.ts Co-authored-by: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> * Update src/phases/pokemon-phase.ts Co-authored-by: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> * Update battle-scene.ts comment for dean * Add todo comment --------- Co-authored-by: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/battle-scene.ts | 19 ++++++++++--------- src/data/arena-tag.ts | 11 +++++------ src/data/battler-tags.ts | 4 ++-- src/modifier/modifier.ts | 2 +- src/phases/move-effect-phase.ts | 11 ++++++++--- src/phases/obtain-status-effect-phase.ts | 1 + src/phases/pokemon-phase.ts | 7 +++++-- test/battler-tags/substitute.test.ts | 2 +- 8 files changed, 33 insertions(+), 24 deletions(-) diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 74b44d324d7..374cf3f270f 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -858,20 +858,21 @@ export class BattleScene extends SceneBase { } /** - * Return the {@linkcode Pokemon} associated with a given ID. - * @param pokemonId - The ID whose Pokemon will be retrieved. - * @returns The {@linkcode Pokemon} associated with the given id. - * Returns `null` if the ID is `undefined` or not present in either party. - * @todo Change the `null` to `undefined` and update callers' signatures - - * this is weird and causes a lot of random jank + * Return the {@linkcode Pokemon} associated with the given ID. + * @param pokemonId - The PID whose Pokemon will be retrieved + * @returns The `Pokemon` associated with the given ID, + * or `undefined` if none is found in either team's party. + * @see {@linkcode Pokemon.id} + * @todo `pokemonId` should not allow `undefined` */ - getPokemonById(pokemonId: number | undefined): Pokemon | null { + public getPokemonById(pokemonId: number | undefined): Pokemon | undefined { if (pokemonId == null) { - return null; + // biome-ignore lint/nursery/noUselessUndefined: More explicit + return undefined; } const party = (this.getPlayerParty() as Pokemon[]).concat(this.getEnemyParty()); - return party.find(p => p.id === pokemonId) ?? null; + return party.find(p => p.id === pokemonId); } addPlayerPokemon( diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index 22955e0a9ac..d3098314beb 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -138,7 +138,7 @@ export abstract class ArenaTag implements BaseArenaTag { } } - onOverlap(_arena: Arena, _source: Pokemon | null): void {} + onOverlap(_arena: Arena, _source: Pokemon | undefined): void {} /** * Trigger this {@linkcode ArenaTag}'s effect, reducing its duration as applicable. @@ -172,9 +172,8 @@ export abstract class ArenaTag implements BaseArenaTag { /** * Helper function that retrieves the source Pokemon * @returns - The source {@linkcode Pokemon} for this tag. - * Returns `null` if `this.sourceId` is `undefined` */ - public getSourcePokemon(): Pokemon | null { + public getSourcePokemon(): Pokemon | undefined { return globalScene.getPokemonById(this.sourceId); } @@ -617,7 +616,7 @@ export class NoCritTag extends SerializableArenaTag { globalScene.phaseManager.queueMessage( i18next.t("arenaTag:noCritOnRemove", { - pokemonNameWithAffix: getPokemonNameWithAffix(source ?? undefined), + pokemonNameWithAffix: getPokemonNameWithAffix(source), moveName: this.getMoveName(), }), ); @@ -1537,7 +1536,7 @@ export class SuppressAbilitiesTag extends SerializableArenaTag { } } - public override onOverlap(_arena: Arena, source: Pokemon | null): void { + public override onOverlap(_arena: Arena, source: Pokemon | undefined): void { (this as Mutable).sourceCount++; this.playActivationMessage(source); } @@ -1580,7 +1579,7 @@ export class SuppressAbilitiesTag extends SerializableArenaTag { return this.sourceCount > 1; } - private playActivationMessage(pokemon: Pokemon | null) { + private playActivationMessage(pokemon: Pokemon | undefined) { if (pokemon) { globalScene.phaseManager.queueMessage( i18next.t("arenaTag:neutralizingGasOnAdd", { diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 711a2bd0b44..6490a6086c4 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -198,7 +198,7 @@ export class BattlerTag implements BaseBattlerTag { * Helper function that retrieves the source Pokemon object * @returns The source {@linkcode Pokemon}, or `null` if none is found */ - public getSourcePokemon(): Pokemon | null { + public getSourcePokemon(): Pokemon | undefined { return globalScene.getPokemonById(this.sourceId); } } @@ -968,7 +968,7 @@ export class InfatuatedTag extends SerializableBattlerTag { phaseManager.queueMessage( i18next.t("battlerTags:infatuatedLapse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), - sourcePokemonName: getPokemonNameWithAffix(globalScene.getPokemonById(this.sourceId!) ?? undefined), // TODO: is that bang correct? + sourcePokemonName: getPokemonNameWithAffix(this.getSourcePokemon()), }), ); phaseManager.unshiftNew("CommonAnimPhase", pokemon.getBattlerIndex(), undefined, CommonAnim.ATTRACT); diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index 82e64316edd..b94c479e96e 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -728,7 +728,7 @@ export abstract class PokemonHeldItemModifier extends PersistentModifier { } getPokemon(): Pokemon | undefined { - return globalScene.getPokemonById(this.pokemonId) ?? undefined; + return globalScene.getPokemonById(this.pokemonId); } getScoreMultiplier(): number { diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index 717bc670558..18e25b328f8 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -645,13 +645,18 @@ export class MoveEffectPhase extends PokemonPhase { return move.getAttrs("HitsTagAttr").some(hta => hta.tagType === semiInvulnerableTag.tagType); } - /** @returns The {@linkcode Pokemon} using this phase's invoked move */ - public getUserPokemon(): Pokemon | null { + /** + * @todo Investigate why this doesn't use `BattlerIndex` + * @returns The {@linkcode Pokemon} using this phase's invoked move + */ + public getUserPokemon(): Pokemon | undefined { // TODO: Make this purely a battler index if (this.battlerIndex > BattlerIndex.ENEMY_2) { return globalScene.getPokemonById(this.battlerIndex); } - return (this.player ? globalScene.getPlayerField() : globalScene.getEnemyField())[this.fieldIndex]; + // TODO: Figure out why this uses `fieldIndex` instead of `BattlerIndex` + // TODO: Remove `?? undefined` once field pokemon getters are made sane + return (this.player ? globalScene.getPlayerField() : globalScene.getEnemyField())[this.fieldIndex] ?? undefined; } /** diff --git a/src/phases/obtain-status-effect-phase.ts b/src/phases/obtain-status-effect-phase.ts index 4846130cf4d..b9f3e266d87 100644 --- a/src/phases/obtain-status-effect-phase.ts +++ b/src/phases/obtain-status-effect-phase.ts @@ -23,6 +23,7 @@ export class ObtainStatusEffectPhase extends PokemonPhase { * @param sourceText - The text to show for the source of the status effect, if any; default `null`. * @param statusMessage - A string containing text to be displayed upon status setting; * defaults to normal key for status if empty or omitted. + * @todo stop passing `null` to the phase */ constructor( battlerIndex: BattlerIndex, diff --git a/src/phases/pokemon-phase.ts b/src/phases/pokemon-phase.ts index 1a1a7e2efa3..92b29889079 100644 --- a/src/phases/pokemon-phase.ts +++ b/src/phases/pokemon-phase.ts @@ -9,7 +9,9 @@ export abstract class PokemonPhase extends FieldPhase { * TODO: Make this either use IDs or `BattlerIndex`es, not a weird mix of both */ protected battlerIndex: BattlerIndex | number; + // TODO: Why is this needed? public player: boolean; + /** @todo Remove in favor of `battlerIndex` pleas for fuck's sake */ public fieldIndex: number; constructor(battlerIndex?: BattlerIndex | number) { @@ -32,10 +34,11 @@ export abstract class PokemonPhase extends FieldPhase { this.fieldIndex = battlerIndex % 2; } + // TODO: This should have `undefined` in its signature getPokemon(): Pokemon { if (this.battlerIndex > BattlerIndex.ENEMY_2) { - return globalScene.getPokemonById(this.battlerIndex)!; //TODO: is this bang correct? + return globalScene.getPokemonById(this.battlerIndex)!; } - return globalScene.getField()[this.battlerIndex]!; //TODO: is this bang correct? + return globalScene.getField()[this.battlerIndex]!; } } diff --git a/test/battler-tags/substitute.test.ts b/test/battler-tags/substitute.test.ts index 7ae60ad1408..a2ff539d2a8 100644 --- a/test/battler-tags/substitute.test.ts +++ b/test/battler-tags/substitute.test.ts @@ -49,7 +49,7 @@ describe("BattlerTag - SubstituteTag", () => { vi.spyOn(messages, "getPokemonNameWithAffix").mockReturnValue(""); vi.spyOn(mockPokemon.scene as BattleScene, "getPokemonById").mockImplementation(pokemonId => - mockPokemon.id === pokemonId ? mockPokemon : null, + mockPokemon.id === pokemonId ? mockPokemon : undefined, ); }); From c217f47942f06c149882378a853231c903e2d509 Mon Sep 17 00:00:00 2001 From: Bertie690 <136088738+Bertie690@users.noreply.github.com> Date: Sat, 13 Sep 2025 14:36:38 -0400 Subject: [PATCH 06/40] [Refactor] Added `playTween` utility function (#6545) * Added `playTween` utility function Co-authored-by: innerthunder <168692175+innerthunder@users.noreply.github.com> * Update comment wording for benjie --------- Co-authored-by: innerthunder <168692175+innerthunder@users.noreply.github.com> --- src/utils/anim-utils.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/utils/anim-utils.ts diff --git a/src/utils/anim-utils.ts b/src/utils/anim-utils.ts new file mode 100644 index 00000000000..f1a06552d38 --- /dev/null +++ b/src/utils/anim-utils.ts @@ -0,0 +1,26 @@ +import { globalScene } from "#app/global-scene"; +import type { SceneBase } from "#app/scene-base"; + +/** + * Plays a Tween animation, resolving once the animation completes. + * @param config - The config for a single Tween + * @param scene - The {@linkcode SceneBase} on which the Tween plays; default {@linkcode globalScene} + * @returns A Promise that resolves once the Tween has been played. + * + * @privateRemarks + * The `config` input should not include an `onComplete` field as that callback is + * used to resolve the Promise containing the Tween animation. + * However, `config`'s type cannot be changed to something like `Omit` + * due to how the type for `TweenBuilderConfig` is defined. + */ +export async function playTween( + config: Phaser.Types.Tweens.TweenBuilderConfig, + scene: SceneBase = globalScene, +): Promise { + await new Promise(resolve => + scene.tweens.add({ + ...config, + onComplete: resolve, + }), + ); +} From e233f24526c7084d281c98928d266e227f477740 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Sat, 13 Sep 2025 15:13:21 -0500 Subject: [PATCH 07/40] [Deps] Update biome to 2.2.4 (#6550) * Update biome and adjust rules * Add noMisusedPromises * Downgrade `noMisusedPromises` to info, enable noImportCycles --- biome.jsonc | 16 +++++++---- package.json | 2 +- pnpm-lock.yaml | 74 ++++++++++++++++++++++++------------------------ src/overrides.ts | 4 +-- 4 files changed, 51 insertions(+), 45 deletions(-) diff --git a/biome.jsonc b/biome.jsonc index e1aac032597..9e87518e636 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.2.3/schema.json", + "$schema": "https://biomejs.dev/schemas/2.2.4/schema.json", "vcs": { "enabled": true, "clientKind": "git", @@ -98,7 +98,9 @@ "useTrimStartEnd": "error", "useReadonlyClassProperties": { "level": "info", // TODO: Graduate to error eventually - "options": { "checkAllProperties": true } + // NOTE: "checkAllProperties" has an immature implementation that + // causes many false positives across files. Enable if/when maturity improves + "options": { "checkAllProperties": false } }, "useConsistentObjectDefinitions": { "level": "error", @@ -209,11 +211,15 @@ "nursery": { "noUselessUndefined": "error", "useMaxParams": { - "level": "warn", // TODO: Change to "error"... eventually... - "options": { "max": 4 } // A lot of stuff has a few params, but + "level": "info", // TODO: Change to "error"... eventually... + "options": { "max": 7 } }, "noShadow": "warn", // TODO: refactor and make "error" - "noNonNullAssertedOptionalChain": "warn" // TODO: refactor and make "error" + "noNonNullAssertedOptionalChain": "warn", // TODO: refactor and make "error" + "noDuplicateDependencies": "error", + "noImportCycles": "error", + // TODO: Change to error once promises are used properly + "noMisusedPromises": "info" } } }, diff --git a/package.json b/package.json index 3086ee85747..f07b9b0e1b3 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "update-locales:remote": "git submodule update --progress --init --recursive --force --remote" }, "devDependencies": { - "@biomejs/biome": "2.2.3", + "@biomejs/biome": "2.2.4", "@ls-lint/ls-lint": "2.3.1", "@types/crypto-js": "^4.2.0", "@types/jsdom": "^21.1.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 09747b23be3..46608772338 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -43,8 +43,8 @@ importers: version: 1.80.16(graphology-types@0.24.8) devDependencies: '@biomejs/biome': - specifier: 2.2.3 - version: 2.2.3 + specifier: 2.2.4 + version: 2.2.4 '@ls-lint/ls-lint': specifier: 2.3.1 version: 2.3.1 @@ -195,55 +195,55 @@ packages: resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==} engines: {node: '>=6.9.0'} - '@biomejs/biome@2.2.3': - resolution: {integrity: sha512-9w0uMTvPrIdvUrxazZ42Ib7t8Y2yoGLKLdNne93RLICmaHw7mcLv4PPb5LvZLJF3141gQHiCColOh/v6VWlWmg==} + '@biomejs/biome@2.2.4': + resolution: {integrity: sha512-TBHU5bUy/Ok6m8c0y3pZiuO/BZoY/OcGxoLlrfQof5s8ISVwbVBdFINPQZyFfKwil8XibYWb7JMwnT8wT4WVPg==} engines: {node: '>=14.21.3'} hasBin: true - '@biomejs/cli-darwin-arm64@2.2.3': - resolution: {integrity: sha512-OrqQVBpadB5eqzinXN4+Q6honBz+tTlKVCsbEuEpljK8ASSItzIRZUA02mTikl3H/1nO2BMPFiJ0nkEZNy3B1w==} + '@biomejs/cli-darwin-arm64@2.2.4': + resolution: {integrity: sha512-RJe2uiyaloN4hne4d2+qVj3d3gFJFbmrr5PYtkkjei1O9c+BjGXgpUPVbi8Pl8syumhzJjFsSIYkcLt2VlVLMA==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [darwin] - '@biomejs/cli-darwin-x64@2.2.3': - resolution: {integrity: sha512-OCdBpb1TmyfsTgBAM1kPMXyYKTohQ48WpiN9tkt9xvU6gKVKHY4oVwteBebiOqyfyzCNaSiuKIPjmHjUZ2ZNMg==} + '@biomejs/cli-darwin-x64@2.2.4': + resolution: {integrity: sha512-cFsdB4ePanVWfTnPVaUX+yr8qV8ifxjBKMkZwN7gKb20qXPxd/PmwqUH8mY5wnM9+U0QwM76CxFyBRJhC9tQwg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [darwin] - '@biomejs/cli-linux-arm64-musl@2.2.3': - resolution: {integrity: sha512-q3w9jJ6JFPZPeqyvwwPeaiS/6NEszZ+pXKF+IczNo8Xj6fsii45a4gEEicKyKIytalV+s829ACZujQlXAiVLBQ==} + '@biomejs/cli-linux-arm64-musl@2.2.4': + resolution: {integrity: sha512-7TNPkMQEWfjvJDaZRSkDCPT/2r5ESFPKx+TEev+I2BXDGIjfCZk2+b88FOhnJNHtksbOZv8ZWnxrA5gyTYhSsQ==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - '@biomejs/cli-linux-arm64@2.2.3': - resolution: {integrity: sha512-g/Uta2DqYpECxG+vUmTAmUKlVhnGEcY7DXWgKP8ruLRa8Si1QHsWknPY3B/wCo0KgYiFIOAZ9hjsHfNb9L85+g==} + '@biomejs/cli-linux-arm64@2.2.4': + resolution: {integrity: sha512-M/Iz48p4NAzMXOuH+tsn5BvG/Jb07KOMTdSVwJpicmhN309BeEyRyQX+n1XDF0JVSlu28+hiTQ2L4rZPvu7nMw==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - '@biomejs/cli-linux-x64-musl@2.2.3': - resolution: {integrity: sha512-y76Dn4vkP1sMRGPFlNc+OTETBhGPJ90jY3il6jAfur8XWrYBQV3swZ1Jo0R2g+JpOeeoA0cOwM7mJG6svDz79w==} + '@biomejs/cli-linux-x64-musl@2.2.4': + resolution: {integrity: sha512-m41nFDS0ksXK2gwXL6W6yZTYPMH0LughqbsxInSKetoH6morVj43szqKx79Iudkp8WRT5SxSh7qVb8KCUiewGg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - '@biomejs/cli-linux-x64@2.2.3': - resolution: {integrity: sha512-LEtyYL1fJsvw35CxrbQ0gZoxOG3oZsAjzfRdvRBRHxOpQ91Q5doRVjvWW/wepgSdgk5hlaNzfeqpyGmfSD0Eyw==} + '@biomejs/cli-linux-x64@2.2.4': + resolution: {integrity: sha512-orr3nnf2Dpb2ssl6aihQtvcKtLySLta4E2UcXdp7+RTa7mfJjBgIsbS0B9GC8gVu0hjOu021aU8b3/I1tn+pVQ==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - '@biomejs/cli-win32-arm64@2.2.3': - resolution: {integrity: sha512-Ms9zFYzjcJK7LV+AOMYnjN3pV3xL8Prxf9aWdDVL74onLn5kcvZ1ZMQswE5XHtnd/r/0bnUd928Rpbs14BzVmA==} + '@biomejs/cli-win32-arm64@2.2.4': + resolution: {integrity: sha512-NXnfTeKHDFUWfxAefa57DiGmu9VyKi0cDqFpdI+1hJWQjGJhJutHPX0b5m+eXvTKOaf+brU+P0JrQAZMb5yYaQ==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [win32] - '@biomejs/cli-win32-x64@2.2.3': - resolution: {integrity: sha512-gvCpewE7mBwBIpqk1YrUqNR4mCiyJm6UI3YWQQXkedSSEwzRdodRpaKhbdbHw1/hmTWOVXQ+Eih5Qctf4TCVOQ==} + '@biomejs/cli-win32-x64@2.2.4': + resolution: {integrity: sha512-3Y4V4zVRarVh/B/eSHczR4LYoSVyv3Dfuvm3cWs5w/HScccS0+Wt/lHOcDTRYeHjQmMYVC3rIRWqyN2EI52+zg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [win32] @@ -2164,39 +2164,39 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 - '@biomejs/biome@2.2.3': + '@biomejs/biome@2.2.4': optionalDependencies: - '@biomejs/cli-darwin-arm64': 2.2.3 - '@biomejs/cli-darwin-x64': 2.2.3 - '@biomejs/cli-linux-arm64': 2.2.3 - '@biomejs/cli-linux-arm64-musl': 2.2.3 - '@biomejs/cli-linux-x64': 2.2.3 - '@biomejs/cli-linux-x64-musl': 2.2.3 - '@biomejs/cli-win32-arm64': 2.2.3 - '@biomejs/cli-win32-x64': 2.2.3 + '@biomejs/cli-darwin-arm64': 2.2.4 + '@biomejs/cli-darwin-x64': 2.2.4 + '@biomejs/cli-linux-arm64': 2.2.4 + '@biomejs/cli-linux-arm64-musl': 2.2.4 + '@biomejs/cli-linux-x64': 2.2.4 + '@biomejs/cli-linux-x64-musl': 2.2.4 + '@biomejs/cli-win32-arm64': 2.2.4 + '@biomejs/cli-win32-x64': 2.2.4 - '@biomejs/cli-darwin-arm64@2.2.3': + '@biomejs/cli-darwin-arm64@2.2.4': optional: true - '@biomejs/cli-darwin-x64@2.2.3': + '@biomejs/cli-darwin-x64@2.2.4': optional: true - '@biomejs/cli-linux-arm64-musl@2.2.3': + '@biomejs/cli-linux-arm64-musl@2.2.4': optional: true - '@biomejs/cli-linux-arm64@2.2.3': + '@biomejs/cli-linux-arm64@2.2.4': optional: true - '@biomejs/cli-linux-x64-musl@2.2.3': + '@biomejs/cli-linux-x64-musl@2.2.4': optional: true - '@biomejs/cli-linux-x64@2.2.3': + '@biomejs/cli-linux-x64@2.2.4': optional: true - '@biomejs/cli-win32-arm64@2.2.3': + '@biomejs/cli-win32-arm64@2.2.4': optional: true - '@biomejs/cli-win32-x64@2.2.3': + '@biomejs/cli-win32-x64@2.2.4': optional: true '@bundled-es-modules/cookie@2.0.1': diff --git a/src/overrides.ts b/src/overrides.ts index b8212ea8fd6..9f6b4ced3a2 100644 --- a/src/overrides.ts +++ b/src/overrides.ts @@ -1,4 +1,4 @@ -import { type PokeballCounts } from "#app/battle-scene"; +import type { PokeballCounts } from "#app/battle-scene"; import { EvolutionItem } from "#balance/pokemon-evolutions"; import { Gender } from "#data/gender"; import { AbilityId } from "#enums/ability-id"; @@ -21,7 +21,7 @@ import { TrainerType } from "#enums/trainer-type"; import { Unlockables } from "#enums/unlockables"; import { VariantTier } from "#enums/variant-tier"; import { WeatherType } from "#enums/weather-type"; -import { type ModifierOverride } from "#modifiers/modifier-type"; +import type { ModifierOverride } from "#modifiers/modifier-type"; import { Variant } from "#sprites/variant"; /** From 3ed6a9a960ff4d643cc438d9fd79727e310fb202 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Sat, 13 Sep 2025 15:21:24 -0500 Subject: [PATCH 08/40] =?UTF-8?q?[Balance]=20Trainer=20boss=20Pok=C3=A9mon?= =?UTF-8?q?=20no=20longer=20gain=20a=20stat=20boost=20when=20a=20boss=20se?= =?UTF-8?q?gment=20is=20broken=20#6552?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Trainer boss Pokémon no longer gain a stat boost when a boss segment is broken --- src/field/pokemon.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index e6a46ac926f..9b2a5725f58 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -6909,7 +6909,7 @@ export class EnemyPokemon extends Pokemon { const segmentSize = this.getMaxHp() / this.bossSegments; clearedBossSegmentIndex = Math.ceil(this.hp / segmentSize); } - if (clearedBossSegmentIndex <= this.bossSegmentIndex) { + if (clearedBossSegmentIndex <= this.bossSegmentIndex && !this.hasTrainer()) { this.handleBossSegmentCleared(clearedBossSegmentIndex); } this.battleInfo.updateBossSegments(this); From b170145fb8983a778222020cd8604a08a1cda2cc Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Sat, 13 Sep 2025 14:55:13 -0700 Subject: [PATCH 09/40] [Dev] Update `pnpm` from `10.14.0` to `10.16.1` --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f07b9b0e1b3..27b1fc4e290 100644 --- a/package.json +++ b/package.json @@ -74,5 +74,5 @@ "engines": { "node": ">=22.0.0" }, - "packageManager": "pnpm@10.14.0" + "packageManager": "pnpm@10.16.1" } From 521b88eabe5cd474baefb9f465b7cc1594b2ac85 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Sat, 13 Sep 2025 18:36:21 -0500 Subject: [PATCH 10/40] [Bug] [Beta] Fix bug where boss segments are not actually broken (#6553) Fix bug where boss segments are not actually broken --- src/field/pokemon.ts | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 9b2a5725f58..9f217cc4d64 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -6909,7 +6909,7 @@ export class EnemyPokemon extends Pokemon { const segmentSize = this.getMaxHp() / this.bossSegments; clearedBossSegmentIndex = Math.ceil(this.hp / segmentSize); } - if (clearedBossSegmentIndex <= this.bossSegmentIndex && !this.hasTrainer()) { + if (clearedBossSegmentIndex <= this.bossSegmentIndex) { this.handleBossSegmentCleared(clearedBossSegmentIndex); } this.battleInfo.updateBossSegments(this); @@ -6938,12 +6938,20 @@ export class EnemyPokemon extends Pokemon { * @param segmentIndex index of the segment to get down to (0 = no shield left, 1 = 1 shield left, etc.) */ handleBossSegmentCleared(segmentIndex: number): void { + let doStatBoost = !this.hasTrainer(); + // TODO: Rewrite this bespoke logic to improve clarity while (this.bossSegmentIndex > 0 && segmentIndex - 1 < this.bossSegmentIndex) { + this.bossSegmentIndex--; + + // Continue, _not_ break here, to ensure that each segment is still broken + if (!doStatBoost) { + continue; + } + let boostedStat: EffectiveStat | undefined; // Filter out already maxed out stat stages and weigh the rest based on existing stats const leftoverStats = EFFECTIVE_STATS.filter((s: EffectiveStat) => this.getStatStage(s) < 6); const statWeights = leftoverStats.map((s: EffectiveStat) => this.getStat(s, false)); - let boostedStat: EffectiveStat | undefined; const statThresholds: number[] = []; let totalWeight = 0; @@ -6962,18 +6970,18 @@ export class EnemyPokemon extends Pokemon { } if (boostedStat === undefined) { - this.bossSegmentIndex--; - return; + doStatBoost = false; + continue; } let stages = 1; // increase the boost if the boss has at least 3 segments and we passed last shield - if (this.bossSegments >= 3 && this.bossSegmentIndex === 1) { + if (this.bossSegments >= 3 && this.bossSegmentIndex === 0) { stages++; } // increase the boost if the boss has at least 5 segments and we passed the second to last shield - if (this.bossSegments >= 5 && this.bossSegmentIndex === 2) { + if (this.bossSegments >= 5 && this.bossSegmentIndex === 1) { stages++; } @@ -6986,7 +6994,6 @@ export class EnemyPokemon extends Pokemon { true, true, ); - this.bossSegmentIndex--; } } From 24a0ca95f40cb9a7c9ce6947e0255ad5d169230c Mon Sep 17 00:00:00 2001 From: Madmadness65 <59298170+Madmadness65@users.noreply.github.com> Date: Sun, 14 Sep 2025 09:58:56 -0500 Subject: [PATCH 11/40] [Balance] Change Happiny evolution method, update Chansey happiness req https://github.com/pagefaultgames/pokerogue/pull/6551 * [Balance] Change Happiny evolution method Happiny now evolves into Chansey by using an Oval Stone during the Dawn or Day, instead of 160 friendship (Blissey's friendship requirement of 200 remains unchanged). Also removed unnecessary comments from Gligar's and Sneasel's evolutions. * Update Chansey -> Blissey 200 -> 180 Friendship Required (Base 140) --------- Co-authored-by: damocleas Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/data/balance/pokemon-evolutions.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/data/balance/pokemon-evolutions.ts b/src/data/balance/pokemon-evolutions.ts index fd88fc5da37..c868a49dbfa 100644 --- a/src/data/balance/pokemon-evolutions.ts +++ b/src/data/balance/pokemon-evolutions.ts @@ -53,6 +53,7 @@ export enum EvolutionItem { PRISM_SCALE, RAZOR_CLAW, RAZOR_FANG, + OVAL_STONE, REAPER_CLOTH, ELECTIRIZER, MAGMARIZER, @@ -1496,10 +1497,13 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesFormEvolution(SpeciesId.DUDUNSPARCE, "", "two-segment", 32, null, {key: EvoCondKey.MOVE, move: MoveId.HYPER_DRILL}, SpeciesWildEvolutionDelay.LONG) ], [SpeciesId.GLIGAR]: [ - new SpeciesEvolution(SpeciesId.GLISCOR, 1, EvolutionItem.RAZOR_FANG, {key: EvoCondKey.TIME, time: [TimeOfDay.DUSK, TimeOfDay.NIGHT]} /* Razor fang at night*/, SpeciesWildEvolutionDelay.VERY_LONG) + new SpeciesEvolution(SpeciesId.GLISCOR, 1, EvolutionItem.RAZOR_FANG, {key: EvoCondKey.TIME, time: [TimeOfDay.DUSK, TimeOfDay.NIGHT]}, SpeciesWildEvolutionDelay.VERY_LONG) ], [SpeciesId.SNEASEL]: [ - new SpeciesEvolution(SpeciesId.WEAVILE, 1, EvolutionItem.RAZOR_CLAW, {key: EvoCondKey.TIME, time: [TimeOfDay.DUSK, TimeOfDay.NIGHT]} /* Razor claw at night*/, SpeciesWildEvolutionDelay.VERY_LONG) + new SpeciesEvolution(SpeciesId.WEAVILE, 1, EvolutionItem.RAZOR_CLAW, {key: EvoCondKey.TIME, time: [TimeOfDay.DUSK, TimeOfDay.NIGHT]}, SpeciesWildEvolutionDelay.VERY_LONG) + ], + [SpeciesId.HAPPINY]: [ + new SpeciesEvolution(SpeciesId.CHANSEY, 1, EvolutionItem.OVAL_STONE, {key: EvoCondKey.TIME, time: [TimeOfDay.DAWN, TimeOfDay.DAY]}, SpeciesWildEvolutionDelay.SHORT) ], [SpeciesId.URSARING]: [ new SpeciesEvolution(SpeciesId.URSALUNA, 1, EvolutionItem.PEAT_BLOCK, null, SpeciesWildEvolutionDelay.VERY_LONG) //Ursaring does not evolve into Bloodmoon Ursaluna @@ -1760,7 +1764,7 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(SpeciesId.CROBAT, 1, null, {key: EvoCondKey.FRIENDSHIP, value: 120}, SpeciesWildEvolutionDelay.VERY_LONG) ], [SpeciesId.CHANSEY]: [ - new SpeciesEvolution(SpeciesId.BLISSEY, 1, null, {key: EvoCondKey.FRIENDSHIP, value: 200}, SpeciesWildEvolutionDelay.LONG) + new SpeciesEvolution(SpeciesId.BLISSEY, 1, null, {key: EvoCondKey.FRIENDSHIP, value: 180}, SpeciesWildEvolutionDelay.LONG) ], [SpeciesId.PICHU]: [ new SpeciesFormEvolution(SpeciesId.PIKACHU, "spiky", "partner", 1, null, {key: EvoCondKey.FRIENDSHIP, value: 90}, SpeciesWildEvolutionDelay.SHORT), @@ -1787,9 +1791,6 @@ export const pokemonEvolutions: PokemonEvolutions = { [SpeciesId.CHINGLING]: [ new SpeciesEvolution(SpeciesId.CHIMECHO, 1, null, [{key: EvoCondKey.FRIENDSHIP, value: 90}, {key: EvoCondKey.TIME, time: [TimeOfDay.DUSK, TimeOfDay.NIGHT]}], SpeciesWildEvolutionDelay.MEDIUM) ], - [SpeciesId.HAPPINY]: [ - new SpeciesEvolution(SpeciesId.CHANSEY, 1, null, {key: EvoCondKey.FRIENDSHIP, value: 160}, SpeciesWildEvolutionDelay.SHORT) - ], [SpeciesId.MUNCHLAX]: [ new SpeciesEvolution(SpeciesId.SNORLAX, 1, null, {key: EvoCondKey.FRIENDSHIP, value: 120}, SpeciesWildEvolutionDelay.LONG) ], From 25a2fb4266ebf84470ae8549c9b6d76a31eb2194 Mon Sep 17 00:00:00 2001 From: Xavion3 Date: Mon, 15 Sep 2025 12:44:58 +1000 Subject: [PATCH 12/40] [Balance] Tweak trainer moveset TM generation. (#6533) * Lower TM weight in moveset generation. * Implement a cap on amount of TMs based on wave. * Extract ai moveset generation to its own file * Minor doc cleanup * Move magic numbers to balance file and export them * Tweak stab move weight generation --------- Co-authored-by: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> --- src/ai/ai-moveset-gen.ts | 715 +++++++++++++++++++++++++ src/data/balance/moveset-generation.ts | 209 ++++++++ src/data/trainers/trainer-config.ts | 33 +- src/field/pokemon.ts | 299 +---------- 4 files changed, 963 insertions(+), 293 deletions(-) create mode 100644 src/ai/ai-moveset-gen.ts create mode 100644 src/data/balance/moveset-generation.ts diff --git a/src/ai/ai-moveset-gen.ts b/src/ai/ai-moveset-gen.ts new file mode 100644 index 00000000000..f46df09b33e --- /dev/null +++ b/src/ai/ai-moveset-gen.ts @@ -0,0 +1,715 @@ +import { globalScene } from "#app/global-scene"; +import { speciesEggMoves } from "#balance/egg-moves"; +import { + BASE_WEIGHT_MULTIPLIER, + BOSS_EXTRA_WEIGHT_MULTIPLIER, + COMMON_TIER_TM_LEVEL_REQUIREMENT, + COMMON_TM_MOVESET_WEIGHT, + EGG_MOVE_LEVEL_REQUIREMENT, + EGG_MOVE_TO_LEVEL_WEIGHT, + EGG_MOVE_WEIGHT_MAX, + EVOLUTION_MOVE_WEIGHT, + GREAT_TIER_TM_LEVEL_REQUIREMENT, + GREAT_TM_MOVESET_WEIGHT, + getMaxEggMoveCount, + getMaxTmCount, + RARE_EGG_MOVE_LEVEL_REQUIREMENT, + STAB_BLACKLIST, + ULTRA_TIER_TM_LEVEL_REQUIREMENT, + ULTRA_TM_MOVESET_WEIGHT, +} from "#balance/moveset-generation"; +import { EVOLVE_MOVE, RELEARN_MOVE } from "#balance/pokemon-level-moves"; +import { speciesTmMoves, tmPoolTiers } from "#balance/tms"; +import { allMoves } from "#data/data-lists"; +import { ModifierTier } from "#enums/modifier-tier"; +import { MoveCategory } from "#enums/move-category"; +import type { MoveId } from "#enums/move-id"; +import { PokemonType } from "#enums/pokemon-type"; +import type { SpeciesId } from "#enums/species-id"; +import { Stat } from "#enums/stat"; +import type { EnemyPokemon, Pokemon } from "#field/pokemon"; +import { PokemonMove } from "#moves/pokemon-move"; +import { NumberHolder, randSeedInt } from "#utils/common"; +import { isBeta } from "#utils/utility-vars"; + +/** + * Compute and assign a weight to the level-up moves currently available to the Pokémon + * + * @param pokemon - The Pokémon to generate a level-based move pool for + * @returns A map of move IDs to their computed weights + * + * @remarks + * A move's weight is determined by its level, as follows: + * 1. If the level is an {@linkcode EVOLVE_MOVE} move, weight is 60 + * 2. If it is level 1 with 80+ BP, it is considered a "move reminder" move and + * weight is 60 + * 3. If the Pokémon has a trainer and the move is a {@linkcode RELEARN_MOVE}, + * weight is 60 + * 4. Otherwise, weight is the earliest level the move can be learned + 20 + */ +function getAndWeightLevelMoves(pokemon: Pokemon): Map { + const movePool = new Map(); + let allLevelMoves: [number, MoveId][]; + // TODO: Investigate why there needs to be error handling here + try { + allLevelMoves = pokemon.getLevelMoves(1, true, true, pokemon.hasTrainer()); + } catch (e) { + console.warn("Error encountered trying to generate moveset for %s: %s", pokemon.species.name, e); + return movePool; + } + + const level = pokemon.level; + const hasTrainer = pokemon.hasTrainer(); + + for (const levelMove of allLevelMoves) { + const [learnLevel, id] = levelMove; + if (level < learnLevel) { + break; + } + const move = allMoves[id]; + // Skip unimplemented moves or moves that are already in the pool + if (move.name.endsWith(" (N)") || movePool.has(id)) { + continue; + } + + let weight = learnLevel + 20; + switch (learnLevel) { + case EVOLVE_MOVE: + weight = EVOLUTION_MOVE_WEIGHT; + break; + // Assume level 1 moves with 80+ BP are "move reminder" moves and bump their weight. Trainers use actual relearn moves. + case 1: + if (move.power >= 80) { + weight = 60; + } + break; + case RELEARN_MOVE: + if (hasTrainer) { + weight = 60; + } + } + + movePool.set(id, weight); + } + + return movePool; +} + +/** + * Determine which TM tiers a Pokémon can learn based on its level + * @param level - The level of the Pokémon + * @returns A tuple indicating whether the Pokémon can learn common, great, and ultra tier TMs + */ +function getAllowedTmTiers(level: number): [common: boolean, great: boolean, ultra: boolean] { + return [ + level >= COMMON_TIER_TM_LEVEL_REQUIREMENT, + level >= GREAT_TIER_TM_LEVEL_REQUIREMENT, + level >= ULTRA_TIER_TM_LEVEL_REQUIREMENT, + ]; +} + +/** + * Get the TMs that a species can learn based on its ID and formKey + * @param speciesId - The species ID of the Pokémon + * @param level - The level of the Pokémon + * @param formKey - The form key of the Pokémon + * @param levelPool - The current level-based move pool, to avoid duplicates + * @param tmPool - The TM move pool to add to, which will be modified in place + * @param allowedTiers - The tiers of TMs the Pokémon is allowed to learn + * + * @privateRemarks + * Split out from `getAndWeightTmMoves` to allow fusion species to add their TMs + * without duplicating code. + */ +function getTmPoolForSpecies( + speciesId: number, + level: number, + formKey: string, + levelPool: ReadonlyMap, + eggPool: ReadonlyMap, + tmPool: Map, + allowedTiers = getAllowedTmTiers(level), +): void { + const [allowCommon, allowGreat, allowUltra] = allowedTiers; + const tms = speciesTmMoves[speciesId]; + + let moveId: MoveId; + for (const tm of tms) { + if (Array.isArray(tm)) { + if (tm[0] !== formKey) { + continue; + } + moveId = tm[1]; + } else { + moveId = tm; + } + + if (levelPool.has(moveId) || eggPool.has(moveId) || tmPool.has(moveId)) { + continue; + } + switch (tmPoolTiers[moveId]) { + case ModifierTier.COMMON: + allowCommon && tmPool.set(moveId, COMMON_TM_MOVESET_WEIGHT); + break; + case ModifierTier.GREAT: + allowGreat && tmPool.set(moveId, GREAT_TM_MOVESET_WEIGHT); + break; + case ModifierTier.ULTRA: + allowUltra && tmPool.set(moveId, ULTRA_TM_MOVESET_WEIGHT); + break; + } + } +} + +/** + * Compute and assign a weight to the TM moves currently available to the Pokémon + * @param pokemon - The Pokémon to generate a TM-based move pool for + * @param currentSet - The current movepool, to avoid duplicates + * @param tmPool - The TM move pool to add to, which will be modified in place + * @returns A map of move IDs to their computed weights + * + * @remarks + * Only trainer pokemon can learn TM moves, and there are restrictions + * as to how many and which TMs are available based on the level of the Pokémon. + * 1. Before level 25, no TM moves are available + * 2. Between levels 25 and 40, only COMMON tier TMs are available, + */ +function getAndWeightTmMoves( + pokemon: Pokemon, + currentPool: ReadonlyMap, + eggPool: ReadonlyMap, + tmPool: Map, +): void { + const level = pokemon.level; + const allowedTiers = getAllowedTmTiers(level); + if (!allowedTiers.includes(true)) { + return; + } + + const form = pokemon.species.forms[pokemon.formIndex]?.formKey ?? ""; + getTmPoolForSpecies(pokemon.species.speciesId, level, form, currentPool, eggPool, tmPool, allowedTiers); + const fusionFormKey = pokemon.getFusionFormKey(); + const fusionSpecies = pokemon.fusionSpecies?.speciesId; + if (fusionSpecies != null && fusionFormKey != null && fusionFormKey !== "") { + getTmPoolForSpecies(fusionSpecies, level, fusionFormKey, currentPool, eggPool, tmPool, allowedTiers); + } +} + +/** + * Get the weight multiplier for an egg move + * @param levelPool - Map of level up moves to their weights + * @param level - The level of the Pokémon + * @param forRare - Whether this is for a rare egg move + * @param isBoss - Whether the Pokémon having the egg move generated is a boss Pokémon + */ +export function getEggMoveWeight( + // biome-ignore-start lint/correctness/noUnusedFunctionParameters: Saved to allow this algorithm to be tweaked easily without adjusting signatures + levelPool: ReadonlyMap, + level: number, + forRare: boolean, + isBoss: boolean, + // biome-ignore-end lint/correctness/noUnusedFunctionParameters: Endrange +): number { + const levelUpWeightedEggMoveWeight = Math.round(Math.max(...levelPool.values()) * EGG_MOVE_TO_LEVEL_WEIGHT); + // Rare egg moves are always weighted at 5/6 the weight of normal egg moves + return Math.min(levelUpWeightedEggMoveWeight, EGG_MOVE_WEIGHT_MAX) * (forRare ? 5 / 6 : 1); +} + +/** + * Submethod of {@linkcode getAndWeightEggMoves} that adds egg moves for a specific species to the egg move pool + * + * @param rootSpeciesId - The ID of the root species for which to generate the egg move pool. + * @param levelPool - A readonly map of move IDs to their levels, representing moves already learned by leveling up. + * @param eggPool - A map to be populated with egg move IDs and their corresponding weights. + * @param eggMoveWeight - The default weight to assign to regular egg moves. + * @param excludeRare - If true, excludes rare egg moves + * @param rareEggMoveWeight - The weight to assign to rare egg moves; default 0 + * + * @privateRemarks + * Split from `getAndWeightEggMoves` to allow fusion species to add their egg moves without duplicating code. + * + * @remarks + * - Moves present in `levelPool` are excluded from the egg pool. + * - If `excludeRare` is true, rare egg moves (at index 3) are skipped. + * - Rare egg moves are assigned `rareEggMoveWeight`, while others receive `eggMoveWeight`. + */ +function getEggPoolForSpecies( + rootSpeciesId: SpeciesId, + levelPool: ReadonlyMap, + eggPool: Map, + eggMoveWeight: number, + excludeRare: boolean, + rareEggMoveWeight = 0, +): void { + for (const [idx, moveId] of speciesEggMoves[rootSpeciesId].entries()) { + if (levelPool.has(moveId) || (idx === 3 && excludeRare)) { + continue; + } + eggPool.set(Math.max(moveId, eggPool.get(moveId) ?? 0), idx === 3 ? rareEggMoveWeight : eggMoveWeight); + } +} + +/** + * Compute and assign a weight to the egg moves currently available to the Pokémon + * @param pokemon - The Pokémon to generate egg moves for + * @param levelPool - The map of level-based moves to their weights + * @param eggPool - A map of move IDs to their weights for egg moves that will be modified in place + * + * @remarks + * This function checks if the Pokémon meets the requirements to learn egg moves, + * and if allowed, calculates the weights for regular and rare egg moves using the provided pools. + */ +function getAndWeightEggMoves( + pokemon: Pokemon, + levelPool: ReadonlyMap, + eggPool: Map, +): void { + const level = pokemon.level; + if (level < EGG_MOVE_LEVEL_REQUIREMENT || !globalScene.currentBattle?.trainer?.config.allowEggMoves) { + return; + } + const isBoss = pokemon.isBoss(); + const excludeRare = isBoss || level < RARE_EGG_MOVE_LEVEL_REQUIREMENT; + const eggMoveWeight = getEggMoveWeight(levelPool, level, false, isBoss); + let rareEggMoveWeight: number | undefined; + if (!excludeRare) { + rareEggMoveWeight = getEggMoveWeight(levelPool, level, true, isBoss); + } + getEggPoolForSpecies( + pokemon.species.getRootSpeciesId(), + levelPool, + eggPool, + eggMoveWeight, + excludeRare, + rareEggMoveWeight, + ); + + const fusionSpecies = pokemon.fusionSpecies?.getRootSpeciesId(); + if (fusionSpecies != null) { + getEggPoolForSpecies(fusionSpecies, levelPool, eggPool, eggMoveWeight, excludeRare, rareEggMoveWeight); + } +} + +/** + * Filter a move pool, removing moves that are not allowed based on conditions + * @param pool - The move pool to filter + * @param isBoss - Whether the Pokémon is a boss + * @param hasTrainer - Whether the Pokémon has a trainer + */ +function filterMovePool(pool: Map, isBoss: boolean, hasTrainer: boolean): void { + for (const [moveId, weight] of pool) { + if (weight <= 0) { + pool.delete(moveId); + continue; + } + const move = allMoves[moveId]; + // Forbid unimplemented moves + if (move.name.endsWith(" (N)")) { + pool.delete(moveId); + continue; + } + // Bosses never get self ko moves or Pain Split + if (isBoss && (move.hasAttr("SacrificialAttr") || move.hasAttr("HpSplitAttr"))) { + pool.delete(moveId); + } + + // No one gets Memento or Final Gambit + if (move.hasAttr("SacrificialAttrOnHit")) { + pool.delete(moveId); + continue; + } + + // Trainers never get OHKO moves + if (hasTrainer && move.hasAttr("OneHitKOAttr")) { + pool.delete(moveId); + } + } +} + +/** + * Perform Trainer-specific adjustments to move weights in a move pool + * @param pool - The move pool to adjust + */ +function adjustWeightsForTrainer(pool: Map): void { + for (const [moveId, weight] of pool.entries()) { + const move = allMoves[moveId]; + let adjustedWeight = weight; + // Half the weight of self KO moves on trainers + adjustedWeight *= move.hasAttr("SacrificialAttr") ? 0.5 : 1; + + // Trainers get a weight bump to stat buffing moves + adjustedWeight *= move.getAttrs("StatStageChangeAttr").some(a => a.stages > 1 && a.selfTarget) ? 1.25 : 1; + + // Trainers get a weight decrease to multiturn moves + adjustedWeight *= !!move.isChargingMove() || !!move.hasAttr("RechargeAttr") ? 0.7 : 1; + if (adjustedWeight !== weight) { + pool.set(moveId, adjustedWeight); + } + } +} + +/** + * Adjust weights of damaging moves in a move pool based on their power and category + * + * @param pool - The move pool to adjust + * @param pokemon - The Pokémon for which the moveset is being generated + * @param willTera - Whether the Pokémon is expected to Tera (i.e., has instant Tera on a Trainer Pokémon); default `false` + * @remarks + * Caps max power at 90 to avoid something like hyper beam ruining the stats. + * pokemon is a pretty soft weighting factor, although it is scaled with the weight multiplier. + */ +function adjustDamageMoveWeights(pool: Map, pokemon: Pokemon, willTera = false): void { + // begin max power at 40 to avoid inflating weights too much when there are only low power moves + let maxPower = 40; + for (const moveId of pool.keys()) { + const move = allMoves[moveId]; + maxPower = Math.max(maxPower, move.calculateEffectivePower()); + if (maxPower >= 90) { + maxPower = 90; + break; + } + } + + const atk = pokemon.getStat(Stat.ATK); + const spAtk = pokemon.getStat(Stat.SPATK); + const lowerStat = Math.min(atk, spAtk); + const higherStat = Math.max(atk, spAtk); + const worseCategory = atk > spAtk ? MoveCategory.SPECIAL : MoveCategory.PHYSICAL; + const statRatio = lowerStat / higherStat; + const adjustmentRatio = Math.min(Math.pow(statRatio, 3) * 1.3, 1); + + for (const [moveId, weight] of pool) { + const move = allMoves[moveId]; + let adjustedWeight = weight; + if (move.category === MoveCategory.STATUS) { + continue; + } + // Scale weight based on their ratio to the highest power move, capping at 50% reduction + adjustedWeight *= Math.max(Math.min(move.calculateEffectivePower() / maxPower, 1), 0.5); + + // Scale weight based the stat it uses to deal damage, based on the ratio between said stat + // and the higher stat + if (move.hasAttr("DefAtkAttr")) { + const def = pokemon.getStat(Stat.DEF); + const defRatio = def / higherStat; + const defAdjustRatio = Math.min(Math.pow(defRatio, 3) * 1.3, 1.1); + adjustedWeight *= defAdjustRatio; + } else if ( + move.category === worseCategory + && !move.hasAttr("PhotonGeyserCategoryAttr") + && !move.hasAttr("ShellSideArmCategoryAttr") + && !(move.hasAttr("TeraMoveCategoryAttr") && willTera) + ) { + // Raw multiply each move's category by the stat it uses to deal damage + // moves that always use the higher offensive stat are left unadjusted + adjustedWeight *= adjustmentRatio; + } + + if (adjustedWeight !== weight) { + pool.set(moveId, adjustedWeight); + } + } +} + +/** + * Calculate the total weight of all moves in a move pool + * @param pool - The move pool to calculate the total weight for + * @returns The total weight of all moves in the pool + */ +// biome-ignore lint/correctness/noUnusedVariables: May be useful +function calculateTotalPoolWeight(pool: Map): number { + let totalWeight = 0; + for (const weight of pool.values()) { + totalWeight += weight; + } + return totalWeight; +} + +/** + * Filter a pool and return a new array of moves that pass the predicate + * @param pool - The move pool to filter + * @param predicate - The predicate function to determine if a move should be included + * @param totalWeight - An output parameter to hold the total weight of the filtered pool. Its value is reset to 0 if provided. + * @returns An array of move ID and weight tuples that pass the predicate + */ +function filterPool( + pool: ReadonlyMap, + predicate: (moveId: MoveId) => boolean, + totalWeight?: NumberHolder, +): [id: MoveId, weight: number][] { + let hasTotalWeight = false; + if (totalWeight != null) { + totalWeight.value = 0; + hasTotalWeight = true; + } + const newPool: [id: MoveId, weight: number][] = []; + for (const [moveId, weight] of pool) { + if (predicate(moveId)) { + newPool.push([moveId, weight]); + if (hasTotalWeight) { + // Bang is safe here because we set `hasTotalWeight` in the if check above + totalWeight!.value += weight; + } + } + } + + return newPool; +} + +/** + * Forcibly add a STAB move to the Pokémon's moveset from the provided pools + * + * @remarks + * If no STAB move is available, add any damaging move. + * If no damaging move is available, no move is added + * @param pool - The master move pool + * @param tmPool - The TM move pool + * @param eggPool - The egg move pool + * @param pokemon - The Pokémon for which the moveset is being generated + * @param tmCount - A holder for the count of TM moves selected + * @param eggMoveCount - A holder for the count of egg moves selected + * @param willTera - Whether the Pokémon is expected to Tera (i.e., has instant Tera on a Trainer Pokémon); default `false` + * @param forceAnyDamageIfNoStab - If true, will force any damaging move if no STAB move is available + */ +// biome-ignore lint/nursery/useMaxParams: This is a complex function that needs all these parameters +function forceStabMove( + pool: Map, + tmPool: Map, + eggPool: Map, + pokemon: Pokemon, + tmCount: NumberHolder, + eggMoveCount: NumberHolder, + willTera = false, + forceAnyDamageIfNoStab = false, +): void { + // All Pokemon force a STAB move first + const totalWeight = new NumberHolder(0); + const stabMovePool = filterPool( + pool, + moveId => { + const move = allMoves[moveId]; + return ( + move.category !== MoveCategory.STATUS + && (pokemon.isOfType(move.type) + || (willTera && move.hasAttr("TeraBlastTypeAttr") && pokemon.getTeraType() !== PokemonType.STELLAR)) + && !STAB_BLACKLIST.has(moveId) + ); + }, + totalWeight, + ); + + const chosenPool = + stabMovePool.length > 0 || !forceAnyDamageIfNoStab + ? stabMovePool + : filterPool( + pool, + m => allMoves[m[0]].category !== MoveCategory.STATUS && !STAB_BLACKLIST.has(m[0]), + totalWeight, + ); + + if (chosenPool.length > 0) { + let rand = randSeedInt(totalWeight.value); + let index = 0; + while (rand > chosenPool[index][1]) { + rand -= chosenPool[index++][1]; + } + const selectedId = chosenPool[index][0]; + pool.delete(selectedId); + if (tmPool.has(selectedId)) { + tmPool.delete(selectedId); + tmCount.value++; + } else if (eggPool.has(selectedId)) { + eggPool.delete(selectedId); + eggMoveCount.value++; + } + pokemon.moveset.push(new PokemonMove(selectedId)); + } +} + +/** + * Adjust weights in the remaining move pool based on existing moves in the Pokémon's moveset + * + * @remarks + * Submethod for step 5 of moveset generation + * @param pool - The move pool to filter + * @param pokemon - The Pokémon for which the moveset is being generated + */ +function filterRemainingTrainerMovePool(pool: [id: MoveId, weight: number][], pokemon: Pokemon) { + // Sqrt the weight of any damaging moves with overlapping types. pokemon is about a 0.05 - 0.1 multiplier. + // Other damaging moves 2x weight if 0-1 damaging moves, 0.5x if 2, 0.125x if 3. These weights get 20x if STAB. + // Status moves remain unchanged on weight, pokemon encourages 1-2 + for (const [idx, [moveId, weight]] of pool.entries()) { + let ret: number; + if ( + pokemon.moveset.some( + mo => mo.getMove().category !== MoveCategory.STATUS && mo.getMove().type === allMoves[moveId].type, + ) + ) { + ret = Math.ceil(Math.sqrt(weight)); + } else if (allMoves[moveId].category !== MoveCategory.STATUS) { + ret = Math.ceil( + (weight / Math.max(Math.pow(4, pokemon.moveset.filter(mo => (mo.getMove().power ?? 0) > 1).length) / 8, 0.5)) + * (pokemon.isOfType(allMoves[moveId].type) && !STAB_BLACKLIST.has(moveId) ? 20 : 1), + ); + } else { + ret = weight; + } + pool[idx] = [moveId, ret]; + } +} + +/** + * Fill in the remaining slots in the Pokémon's moveset from the provided pools + * @param pokemon - The Pokémon for which the moveset is being generated + * @param tmPool - The TM move pool + * @param eggMovePool - The egg move pool + * @param tmCount - A holder for the count of moves that have been added to the moveset from TMs + * @param eggMoveCount - A holder for the count of moves that have been added to the moveset from egg moves + * @param baseWeights - The base weights of all moves in the master pool + * @param remainingPool - The remaining move pool to select from + */ +function fillInRemainingMovesetSlots( + pokemon: Pokemon, + tmPool: Map, + eggMovePool: Map, + tmCount: NumberHolder, + eggMoveCount: NumberHolder, + baseWeights: Map, + remainingPool: [id: MoveId, weight: number][], +): void { + const tmCap = getMaxTmCount(pokemon.level); + const eggCap = getMaxEggMoveCount(pokemon.level); + const remainingPoolWeight = new NumberHolder(0); + while (remainingPool.length > pokemon.moveset.length && pokemon.moveset.length < 4) { + const nonLevelMoveCount = tmCount.value + eggMoveCount.value; + remainingPool = filterPool( + baseWeights, + (m: MoveId) => + !pokemon.moveset.some( + mo => + m === mo.moveId || (allMoves[m]?.hasAttr("SacrificialAttr") && mo.getMove()?.hasAttr("SacrificialAttr")), // Only one self-KO move allowed + ) + && (nonLevelMoveCount < tmCap || !tmPool.has(m)) + && (nonLevelMoveCount < eggCap || !eggMovePool.has(m)), + remainingPoolWeight, + ); + if (pokemon.hasTrainer()) { + filterRemainingTrainerMovePool(remainingPool, pokemon); + } + const totalWeight = remainingPool.reduce((v, m) => v + m[1], 0); + let rand = randSeedInt(totalWeight); + let index = 0; + while (rand > remainingPool[index][1]) { + rand -= remainingPool[index++][1]; + } + const selectedMoveId = remainingPool[index][0]; + baseWeights.delete(selectedMoveId); + if (tmPool.has(selectedMoveId)) { + tmCount.value++; + tmPool.delete(selectedMoveId); + } else if (eggMovePool.has(selectedMoveId)) { + eggMoveCount.value++; + eggMovePool.delete(selectedMoveId); + } + pokemon.moveset.push(new PokemonMove(selectedMoveId)); + } +} + +/** + * Debugging function to log computed move weights for a Pokémon + * @param pokemon - The Pokémon for which the move weights were computed + * @param pool - The move pool containing move IDs and their weights + * @param note - Short note to include in the log for context + */ +function debugMoveWeights(pokemon: Pokemon, pool: Map, note: string): void { + if (isBeta || import.meta.env.DEV) { + const moveNameToWeightMap = new Map(); + const sortedByValue = Array.from(pool.entries()).sort((a, b) => b[1] - a[1]); + for (const [moveId, weight] of sortedByValue) { + moveNameToWeightMap.set(allMoves[moveId].name, weight); + } + console.log("%cComputed move weights [%s] for %s", "color: blue", note, pokemon.name, moveNameToWeightMap); + } +} + +/** + * Generate a moveset for a given Pokémon based on its level, types, stats, and whether it is wild or a trainer's Pokémon. + * @param pokemon - The Pokémon to generate a moveset for + * @returns A reference to the Pokémon's moveset array + */ +export function generateMoveset(pokemon: Pokemon): void { + pokemon.moveset = []; + // Step 1: Generate the pools from various sources: level up, egg moves, and TMs + const learnPool = getAndWeightLevelMoves(pokemon); + debugMoveWeights(pokemon, learnPool, "Initial Level Moves"); + const hasTrainer = pokemon.hasTrainer(); + const tmPool = new Map(); + const eggMovePool = new Map(); + + if (hasTrainer) { + getAndWeightEggMoves(pokemon, learnPool, eggMovePool); + eggMovePool.size > 0 && debugMoveWeights(pokemon, eggMovePool, "Initial Egg Moves"); + getAndWeightTmMoves(pokemon, learnPool, eggMovePool, tmPool); + tmPool.size > 0 && debugMoveWeights(pokemon, tmPool, "Initial Tm Moves"); + } + + // Now, combine pools into one master pool. + // The pools are kept around so we know where the move was sourced from + const movePool = new Map([...tmPool.entries(), ...eggMovePool.entries(), ...learnPool.entries()]); + + // Step 2: Filter out forbidden moves + const isBoss = pokemon.isBoss(); + filterMovePool(movePool, isBoss, hasTrainer); + + // Step 3: Adjust weights for trainers + if (hasTrainer) { + adjustWeightsForTrainer(movePool); + } + + /** Determine whether this pokemon will instantly tera */ + const willTera = + hasTrainer + && globalScene.currentBattle?.trainer?.config.trainerAI.instantTeras.includes( + // The cast to EnemyPokemon is safe; includes will just return false if the property doesn't exist + (pokemon as EnemyPokemon).initialTeamIndex, + ); + + adjustDamageMoveWeights(movePool, pokemon, willTera); + + /** The higher this is, the greater the impact of weight. At `0` all moves are equal weight. */ + let weightMultiplier = BASE_WEIGHT_MULTIPLIER; + if (isBoss) { + weightMultiplier += BOSS_EXTRA_WEIGHT_MULTIPLIER; + } + + const baseWeights = new Map(movePool); + for (const [moveId, weight] of baseWeights) { + if (weight <= 0) { + baseWeights.delete(moveId); + continue; + } + baseWeights.set(moveId, Math.ceil(Math.pow(weight, weightMultiplier) * 100)); + } + + const tmCount = new NumberHolder(0); + const eggMoveCount = new NumberHolder(0); + + debugMoveWeights(pokemon, baseWeights, "Pre STAB Move"); + + // Step 4: Force a STAB move if possible + forceStabMove(movePool, tmPool, eggMovePool, pokemon, tmCount, eggMoveCount, willTera); + // Note: To force a secondary stab, call this a second time, and pass `false` for the last parameter + // Would also tweak the function to not consider moves already in the moveset + // e.g. forceStabMove(..., false); + + // Step 5: Fill in remaining slots + fillInRemainingMovesetSlots( + pokemon, + tmPool, + eggMovePool, + tmCount, + eggMoveCount, + baseWeights, + filterPool(baseWeights, (m: MoveId) => !pokemon.moveset.some(mo => m[0] === mo.moveId)), + ); +} diff --git a/src/data/balance/moveset-generation.ts b/src/data/balance/moveset-generation.ts new file mode 100644 index 00000000000..d6579eb45e7 --- /dev/null +++ b/src/data/balance/moveset-generation.ts @@ -0,0 +1,209 @@ +/* + * SPDX-Copyright-Text: 2025 Pagefault Games + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import { MoveId } from "#enums/move-id"; + + +/** + * # Balance: Moveset Generation Configuration + * + * This module contains configuration constants and functions that control + * the limitations and rules around moveset generation for generated Pokémon. + * + * + * ### Move Weights + * + * The various move weight constants in this module control how likely + * certain categories of moves are to appear in a generated Pokémon's + * moveset. Higher weights make a move more likely to be chosen. + * The constants here specify the *base* weight for a move when first computed. + * These weights are post-processed (and then scaled up such that weights have a larger impact, + * for instance, on boss Pokémon) before being used in the actual moveset generation. + * + * Post Processing of weights includes, but is not limited to: + * - Adjusting weights of status moves + * - Adjusting weights based on the move's power relative to the highest power available + * - Adjusting weights based on the stat the move uses to calculate damage relative to the higher stat + * + * + * All weights go through additional post-processing based on + * their expected power (accuracy * damage * expected number of hits) + * + * @module + */ + + +//#region Constants +/** + * The minimum level for a Pokémon to generate with a move it can only learn + * from a common tier TM + */ +export const COMMON_TIER_TM_LEVEL_REQUIREMENT = 25; +/** + * The minimum level for a Pokémon to generate with a move it can only learn + * from a great tier TM + */ +export const GREAT_TIER_TM_LEVEL_REQUIREMENT = 40; +/** + * The minimum level for a Pokémon to generate with a move it can only learn + * from an ultra tier TM + */ +export const ULTRA_TIER_TM_LEVEL_REQUIREMENT = 55; + +/** Below this level, Pokémon will be unable to generate with any egg moves */ +export const EGG_MOVE_LEVEL_REQUIREMENT = 60; +/** Below this level, Pokémon will be unable to generate with rare egg moves */ +export const RARE_EGG_MOVE_LEVEL_REQUIREMENT = 170; + +// Note: Not exported, only for use with `getMaxTmCount +/** Below this level, Pokémon will be unable to generate with any TMs */ +const ONE_TM_THRESHOLD = 25; +/** Below this level, Pokémon will generate with at most 1 TM */ +const TWO_TM_THRESHOLD = 41; +/** Below this level, Pokémon will generate with at most two TMs */ +const THREE_TM_THRESHOLD = 71; +/** Below this level, Pokémon will generate with at most three TMs */ +const FOUR_TM_THRESHOLD = 101; + +/** Below this level, Pokémon will be unable to generate any egg moves */ +const ONE_EGG_MOVE_THRESHOLD = 80; +/** Below this level, Pokémon will generate with at most 1 egg moves */ +const TWO_EGG_MOVE_THRESHOLD = 121; +/** Below this level, Pokémon will generate with at most 2 egg moves */ +const THREE_EGG_MOVE_THRESHOLD = 161; +/** Above this level, Pokémon will generate with at most 3 egg moves */ +const FOUR_EGG_MOVE_THRESHOLD = 201; + + +/** The weight given to TMs in the common tier during moveset generation */ +export const COMMON_TM_MOVESET_WEIGHT = 12; +/** The weight given to TMs in the great tier during moveset generation */ +export const GREAT_TM_MOVESET_WEIGHT = 14; +/** The weight given to TMs in the ultra tier during moveset generation */ +export const ULTRA_TM_MOVESET_WEIGHT = 18; + +/** + * The maximum weight an egg move can ever have + * @remarks + * Egg moves have their weights adjusted based on the maximum weight of the Pokémon's + * level-up moves. Rare Egg moves are always 5/6th of the computed egg move weight. + * Boss pokemon are not allowed to spawn with rare egg moves. + * @see {@linkcode EGG_MOVE_TO_LEVEL_WEIGHT} + */ +export const EGG_MOVE_WEIGHT_MAX = 60; +/** + * The percentage of the Pokémon's highest weighted level move to the weight an + * egg move can generate with + */ +export const EGG_MOVE_TO_LEVEL_WEIGHT = 0.85; +/** The weight given to evolution moves */ +export const EVOLUTION_MOVE_WEIGHT = 70; +/** The weight given to relearn moves */ +export const RELEARN_MOVE_WEIGHT = 60; + +/** The base weight multiplier to use + * + * The higher the number, the more impact weights have on the final move selection. + * i.e. if set to 0, all moves have equal chance of being selected regardless of their weight. + */ +export const BASE_WEIGHT_MULTIPLIER = 1.6; + +/** The additional weight added onto {@linkcode BASE_WEIGHT_MULTIPLIER} for boss Pokémon */ +export const BOSS_EXTRA_WEIGHT_MULTIPLIER = 0.4; + + + +/** + * Set of moves that should be blacklisted from the forced STAB during moveset generation + * + * @remarks + * During moveset generation, trainer pokemon attempt to force their pokemon to generate with STAB + * moves in their movesets. Moves in this list not be considered to be "STAB" moves for this purpose. + * This does *not* prevent them from appearing in the moveset, but they will never + * be selected as a forced STAB move. + */ +export const STAB_BLACKLIST: ReadonlySet = new Set([ + MoveId.BEAT_UP, + MoveId.BELCH, + MoveId.BIDE, + MoveId.COMEUPPANCE, + MoveId.COUNTER, + MoveId.DOOM_DESIRE, + MoveId.DRAGON_RAGE, + MoveId.DREAM_EATER, + MoveId.ENDEAVOR, + MoveId.EXPLOSION, + MoveId.FAKE_OUT, + MoveId.FIRST_IMPRESSION, + MoveId.FISSURE, + MoveId.FLING, + MoveId.FOCUS_PUNCH, + MoveId.FUTURE_SIGHT, + MoveId.GUILLOTINE, + MoveId.HOLD_BACK, + MoveId.HORN_DRILL, + MoveId.LAST_RESORT, + MoveId.METAL_BURST, + MoveId.MIRROR_COAT, + MoveId.MISTY_EXPLOSION, + MoveId.NATURAL_GIFT, + MoveId.NATURES_MADNESS, + MoveId.NIGHT_SHADE, + MoveId.PSYWAVE, + MoveId.RUINATION, + MoveId.SELF_DESTRUCT, + MoveId.SHEER_COLD, + MoveId.SHELL_TRAP, + MoveId.SKY_DROP, + MoveId.SNORE, + MoveId.SONIC_BOOM, + MoveId.SPIT_UP, + MoveId.STEEL_BEAM, + MoveId.STEEL_ROLLER, + MoveId.SUPER_FANG, + MoveId.SYNCHRONOISE, + MoveId.UPPER_HAND, +]); + +//#endregion Constants + +/** + * Get the maximum number of TMs a Pokémon is allowed to learn based on + * its level + * @param level - The level of the Pokémon + * @returns The number of TMs the Pokémon can learn at this level + */ +export function getMaxTmCount(level: number) { + if (level < ONE_TM_THRESHOLD) { + return 0; + } + if (level < TWO_TM_THRESHOLD) { + return 1; + } + if (level < THREE_TM_THRESHOLD) { + return 2; + } + if (level < FOUR_TM_THRESHOLD) { + return 3; + } + return 4; +} + + +export function getMaxEggMoveCount(level: number): number { + if (level < ONE_EGG_MOVE_THRESHOLD) { + return 0; + } + if (level < TWO_EGG_MOVE_THRESHOLD) { + return 1; + } + if (level < THREE_EGG_MOVE_THRESHOLD) { + return 2; + } + if (level < FOUR_EGG_MOVE_THRESHOLD) { + return 3; + } + return 4; +} diff --git a/src/data/trainers/trainer-config.ts b/src/data/trainers/trainer-config.ts index 55362017cee..b5786d1f0a2 100644 --- a/src/data/trainers/trainer-config.ts +++ b/src/data/trainers/trainer-config.ts @@ -3,6 +3,8 @@ import { globalScene } from "#app/global-scene"; import { pokemonEvolutions, pokemonPrevolutions } from "#balance/pokemon-evolutions"; import { signatureSpecies } from "#balance/signature-species"; import { tmSpecies } from "#balance/tms"; +// biome-ignore lint/correctness/noUnusedImports: Used in a tsdoc comment +import type { RARE_EGG_MOVE_LEVEL_REQUIREMENT } from "#data/balance/moveset-generation"; import { modifierTypes } from "#data/data-lists"; import { doubleBattleDialogue } from "#data/double-battle-dialogue"; import { Gender } from "#data/gender"; @@ -41,6 +43,7 @@ import type { TrainerConfigs, TrainerTierPools, } from "#types/trainer-funcs"; +import type { Mutable } from "#types/type-helpers"; import { coerceArray, randSeedInt, randSeedIntRange, randSeedItem } from "#utils/common"; import { getPokemonSpecies } from "#utils/pokemon-utils"; import { toCamelCase, toTitleCase } from "#utils/strings"; @@ -119,6 +122,15 @@ export class TrainerConfig { public hasVoucher = false; public trainerAI: TrainerAI; + /** + * Whether this trainer's Pokémon are allowed to generate with egg moves + * @defaultValue `false` + * + * @see {@linkcode setEggMovesAllowed} + * @see {@linkcode RARE_EGG_MOVE_LEVEL_THRESHOLD} + */ + public readonly allowEggMoves: boolean = false; + public encounterMessages: string[] = []; public victoryMessages: string[] = []; public defeatMessages: string[] = []; @@ -387,8 +399,27 @@ export class TrainerConfig { return this; } - setBoss(): TrainerConfig { + /** + * Allow this trainer's Pokémon to have egg moves when generating their movesets. + * + * @remarks + * It is redundant to call this if {@linkcode setBoss} is also called on the configuration. + * @returns `this` for method chaining + * @see {@linkcode allowEggMoves} + */ + public setEggMovesAllowed(): this { + (this as Mutable).allowEggMoves = true; + return this; + } + + /** + * Set this trainer as a boss trainer + * @returns `this` for method chaining + * @see {@linkcode isBoss} + */ + public setBoss(): TrainerConfig { this.isBoss = true; + (this as Mutable).allowEggMoves = true; return this; } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 9f217cc4d64..46f88abc61c 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -1,5 +1,6 @@ import type { Ability, PreAttackModifyDamageAbAttrParams } from "#abilities/ability"; import { applyAbAttrs, applyOnGainAbAttrs, applyOnLoseAbAttrs } from "#abilities/apply-ab-attrs"; +import { generateMoveset } from "#app/ai/ai-moveset-gen"; import type { AnySound, BattleScene } from "#app/battle-scene"; import { PLAYER_PARTY_MAX_SIZE, RARE_CANDY_FRIENDSHIP_CAP } from "#app/constants"; import { timedEventManager } from "#app/global-event-manager"; @@ -18,7 +19,7 @@ import type { LevelMoves } from "#balance/pokemon-level-moves"; import { EVOLVE_MOVE, RELEARN_MOVE } from "#balance/pokemon-level-moves"; import { BASE_HIDDEN_ABILITY_CHANCE, BASE_SHINY_CHANCE, SHINY_EPIC_CHANCE, SHINY_VARIANT_CHANCE } from "#balance/rates"; import { getStarterValueFriendshipCap, speciesStarterCosts } from "#balance/starters"; -import { reverseCompatibleTms, tmPoolTiers, tmSpecies } from "#balance/tms"; +import { reverseCompatibleTms, tmSpecies } from "#balance/tms"; import type { SuppressAbilitiesTag } from "#data/arena-tag"; import { NoCritTag, WeakenMoveScreenTag } from "#data/arena-tag"; import { @@ -81,7 +82,6 @@ import { DexAttr } from "#enums/dex-attr"; import { FieldPosition } from "#enums/field-position"; import { HitResult } from "#enums/hit-result"; import { LearnMoveSituation } from "#enums/learn-move-situation"; -import { ModifierTier } from "#enums/modifier-tier"; import { MoveCategory } from "#enums/move-category"; import { MoveFlags } from "#enums/move-flags"; import { MoveId } from "#enums/move-id"; @@ -1440,6 +1440,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { simulated = true, ignoreHeldItems = false, ): number { + // biome-ignore-start lint/nursery/useMaxParams: test const statVal = new NumberHolder(this.getStat(stat, false)); if (!ignoreHeldItems) { globalScene.applyModifiers(StatBoosterModifier, this.isPlayer(), this, stat, statVal); @@ -1538,6 +1539,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } return Math.floor(ret); + // biome-ignore-end lint/nursery/useMaxParams: test } calculateStats(): void { @@ -2774,7 +2776,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * This causes problems when there are intentional duplicates (i.e. Smeargle with Sketch) */ if (levelMoves) { - this.getUniqueMoves(levelMoves, ret); + Pokemon.getUniqueMoves(levelMoves, ret); } return ret; @@ -2788,7 +2790,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * @param levelMoves the input array to search for non-duplicates from * @param ret the output array to be pushed into. */ - private getUniqueMoves(levelMoves: LevelMoves, ret: LevelMoves): void { + private static getUniqueMoves(levelMoves: LevelMoves, ret: LevelMoves): void { const uniqueMoves: MoveId[] = []; for (const lm of levelMoves) { if (!uniqueMoves.find(m => m === lm[1])) { @@ -3046,294 +3048,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** Generates a semi-random moveset for a Pokemon */ public generateAndPopulateMoveset(): void { - this.moveset = []; - let movePool: [MoveId, number][] = []; - const allLevelMoves = this.getLevelMoves(1, true, true, this.hasTrainer()); - if (!allLevelMoves) { - console.warn("Error encountered trying to generate moveset for:", this.species.name); - return; - } - - for (const levelMove of allLevelMoves) { - if (this.level < levelMove[0]) { - break; - } - let weight = levelMove[0] + 20; - // Evolution Moves - if (levelMove[0] === EVOLVE_MOVE) { - weight = 70; - } - // Assume level 1 moves with 80+ BP are "move reminder" moves and bump their weight. Trainers use actual relearn moves. - if ( - (levelMove[0] === 1 && allMoves[levelMove[1]].power >= 80) - || (levelMove[0] === RELEARN_MOVE && this.hasTrainer()) - ) { - weight = 60; - } - if (!movePool.some(m => m[0] === levelMove[1]) && !allMoves[levelMove[1]].name.endsWith(" (N)")) { - movePool.push([levelMove[1], weight]); - } - } - - if (this.hasTrainer()) { - const tms = Object.keys(tmSpecies); - for (const tm of tms) { - const moveId = Number.parseInt(tm) as MoveId; - let compatible = false; - for (const p of tmSpecies[tm]) { - if (Array.isArray(p)) { - if ( - p[0] === this.species.speciesId - || (this.fusionSpecies - && p[0] === this.fusionSpecies.speciesId - && p.slice(1).indexOf(this.species.forms[this.formIndex]) > -1) - ) { - compatible = true; - break; - } - } else if (p === this.species.speciesId || (this.fusionSpecies && p === this.fusionSpecies.speciesId)) { - compatible = true; - break; - } - } - if (compatible && !movePool.some(m => m[0] === moveId) && !allMoves[moveId].name.endsWith(" (N)")) { - if (tmPoolTiers[moveId] === ModifierTier.COMMON && this.level >= 15) { - movePool.push([moveId, 24]); - } else if (tmPoolTiers[moveId] === ModifierTier.GREAT && this.level >= 30) { - movePool.push([moveId, 28]); - } else if (tmPoolTiers[moveId] === ModifierTier.ULTRA && this.level >= 50) { - movePool.push([moveId, 34]); - } - } - } - - // No egg moves below level 60 - if (this.level >= 60) { - for (let i = 0; i < 3; i++) { - const moveId = speciesEggMoves[this.species.getRootSpeciesId()][i]; - if (!movePool.some(m => m[0] === moveId) && !allMoves[moveId].name.endsWith(" (N)")) { - movePool.push([moveId, 60]); - } - } - const moveId = speciesEggMoves[this.species.getRootSpeciesId()][3]; - // No rare egg moves before e4 - if ( - this.level >= 170 - && !movePool.some(m => m[0] === moveId) - && !allMoves[moveId].name.endsWith(" (N)") - && !this.isBoss() - ) { - movePool.push([moveId, 50]); - } - if (this.fusionSpecies) { - for (let i = 0; i < 3; i++) { - const moveId = speciesEggMoves[this.fusionSpecies.getRootSpeciesId()][i]; - if (!movePool.some(m => m[0] === moveId) && !allMoves[moveId].name.endsWith(" (N)")) { - movePool.push([moveId, 60]); - } - } - const moveId = speciesEggMoves[this.fusionSpecies.getRootSpeciesId()][3]; - // No rare egg moves before e4 - if ( - this.level >= 170 - && !movePool.some(m => m[0] === moveId) - && !allMoves[moveId].name.endsWith(" (N)") - && !this.isBoss() - ) { - movePool.push([moveId, 50]); - } - } - } - } - - // Bosses never get self ko moves or Pain Split - if (this.isBoss()) { - movePool = movePool.filter( - m => !allMoves[m[0]].hasAttr("SacrificialAttr") && !allMoves[m[0]].hasAttr("HpSplitAttr"), - ); - } - // No one gets Memento or Final Gambit - movePool = movePool.filter(m => !allMoves[m[0]].hasAttr("SacrificialAttrOnHit")); - if (this.hasTrainer()) { - // Trainers never get OHKO moves - movePool = movePool.filter(m => !allMoves[m[0]].hasAttr("OneHitKOAttr")); - // Half the weight of self KO moves - movePool = movePool.map(m => [m[0], m[1] * (allMoves[m[0]].hasAttr("SacrificialAttr") ? 0.5 : 1)]); - // Trainers get a weight bump to stat buffing moves - movePool = movePool.map(m => [ - m[0], - m[1] * (allMoves[m[0]].getAttrs("StatStageChangeAttr").some(a => a.stages > 1 && a.selfTarget) ? 1.25 : 1), - ]); - // Trainers get a weight decrease to multiturn moves - movePool = movePool.map(m => [ - m[0], - m[1] * (!!allMoves[m[0]].isChargingMove() || !!allMoves[m[0]].hasAttr("RechargeAttr") ? 0.7 : 1), - ]); - } - - // Weight towards higher power moves, by reducing the power of moves below the highest power. - // Caps max power at 90 to avoid something like hyper beam ruining the stats. - // This is a pretty soft weighting factor, although it is scaled with the weight multiplier. - const maxPower = Math.min( - movePool.reduce((v, m) => Math.max(allMoves[m[0]].calculateEffectivePower(), v), 40), - 90, - ); - movePool = movePool.map(m => [ - m[0], - m[1] - * (allMoves[m[0]].category === MoveCategory.STATUS - ? 1 - : Math.max(Math.min(allMoves[m[0]].calculateEffectivePower() / maxPower, 1), 0.5)), - ]); - - // Weight damaging moves against the lower stat. This uses a non-linear relationship. - // If the higher stat is 1 - 1.09x higher, no change. At higher stat ~1.38x lower stat, off-stat moves have half weight. - // One third weight at ~1.58x higher, one quarter weight at ~1.73x higher, one fifth at ~1.87x, and one tenth at ~2.35x higher. - const atk = this.getStat(Stat.ATK); - const spAtk = this.getStat(Stat.SPATK); - const worseCategory: MoveCategory = atk > spAtk ? MoveCategory.SPECIAL : MoveCategory.PHYSICAL; - const statRatio = worseCategory === MoveCategory.PHYSICAL ? atk / spAtk : spAtk / atk; - movePool = movePool.map(m => [ - m[0], - m[1] * (allMoves[m[0]].category === worseCategory ? Math.min(Math.pow(statRatio, 3) * 1.3, 1) : 1), - ]); - - /** The higher this is the more the game weights towards higher level moves. At `0` all moves are equal weight. */ - let weightMultiplier = 1.6; - if (this.isBoss()) { - weightMultiplier += 0.4; - } - const baseWeights: [MoveId, number][] = movePool.map(m => [ - m[0], - Math.ceil(Math.pow(m[1], weightMultiplier) * 100), - ]); - - const STAB_BLACKLIST: ReadonlySet = new Set([ - MoveId.BEAT_UP, - MoveId.BELCH, - MoveId.BIDE, - MoveId.COMEUPPANCE, - MoveId.COUNTER, - MoveId.DOOM_DESIRE, - MoveId.DRAGON_RAGE, - MoveId.DREAM_EATER, - MoveId.ENDEAVOR, - MoveId.EXPLOSION, - MoveId.FAKE_OUT, - MoveId.FIRST_IMPRESSION, - MoveId.FISSURE, - MoveId.FLING, - MoveId.FOCUS_PUNCH, - MoveId.FUTURE_SIGHT, - MoveId.GUILLOTINE, - MoveId.HOLD_BACK, - MoveId.HORN_DRILL, - MoveId.LAST_RESORT, - MoveId.METAL_BURST, - MoveId.MIRROR_COAT, - MoveId.MISTY_EXPLOSION, - MoveId.NATURAL_GIFT, - MoveId.NATURES_MADNESS, - MoveId.NIGHT_SHADE, - MoveId.PSYWAVE, - MoveId.RUINATION, - MoveId.SELF_DESTRUCT, - MoveId.SHEER_COLD, - MoveId.SHELL_TRAP, - MoveId.SKY_DROP, - MoveId.SNORE, - MoveId.SONIC_BOOM, - MoveId.SPIT_UP, - MoveId.STEEL_BEAM, - MoveId.STEEL_ROLLER, - MoveId.SUPER_FANG, - MoveId.SYNCHRONOISE, - MoveId.UPPER_HAND, - ]); - - // All Pokemon force a STAB move first - const stabMovePool = baseWeights.filter( - m => - allMoves[m[0]].category !== MoveCategory.STATUS - && this.isOfType(allMoves[m[0]].type) - && !STAB_BLACKLIST.has(m[0]), - ); - - if (stabMovePool.length > 0) { - const totalWeight = stabMovePool.reduce((v, m) => v + m[1], 0); - let rand = randSeedInt(totalWeight); - let index = 0; - while (rand > stabMovePool[index][1]) { - rand -= stabMovePool[index++][1]; - } - this.moveset.push(new PokemonMove(stabMovePool[index][0])); - } else { - // If there are no damaging STAB moves, just force a random damaging move - const attackMovePool = baseWeights.filter( - m => allMoves[m[0]].category !== MoveCategory.STATUS && !STAB_BLACKLIST.has(m[0]), - ); - if (attackMovePool.length > 0) { - const totalWeight = attackMovePool.reduce((v, m) => v + m[1], 0); - let rand = randSeedInt(totalWeight); - let index = 0; - while (rand > attackMovePool[index][1]) { - rand -= attackMovePool[index++][1]; - } - this.moveset.push(new PokemonMove(attackMovePool[index][0], 0, 0)); - } - } - - while (baseWeights.length > this.moveset.length && this.moveset.length < 4) { - if (this.hasTrainer()) { - // Sqrt the weight of any damaging moves with overlapping types. This is about a 0.05 - 0.1 multiplier. - // Other damaging moves 2x weight if 0-1 damaging moves, 0.5x if 2, 0.125x if 3. These weights get 20x if STAB. - // Status moves remain unchanged on weight, this encourages 1-2 - movePool = baseWeights - .filter( - m => - !this.moveset.some( - mo => - m[0] === mo.moveId - || (allMoves[m[0]].hasAttr("SacrificialAttr") && mo.getMove().hasAttr("SacrificialAttr")), // Only one self-KO move allowed - ), - ) - .map(m => { - let ret: number; - if ( - this.moveset.some( - mo => mo.getMove().category !== MoveCategory.STATUS && mo.getMove().type === allMoves[m[0]].type, - ) - ) { - ret = Math.ceil(Math.sqrt(m[1])); - } else if (allMoves[m[0]].category !== MoveCategory.STATUS) { - ret = Math.ceil( - (m[1] / Math.max(Math.pow(4, this.moveset.filter(mo => (mo.getMove().power ?? 0) > 1).length) / 8, 0.5)) - * (this.isOfType(allMoves[m[0]].type) && !STAB_BLACKLIST.has(m[0]) ? 20 : 1), - ); - } else { - ret = m[1]; - } - return [m[0], ret]; - }); - } else { - // Non-trainer pokemon just use normal weights - movePool = baseWeights.filter( - m => - !this.moveset.some( - mo => - m[0] === mo.moveId - || (allMoves[m[0]].hasAttr("SacrificialAttr") && mo.getMove().hasAttr("SacrificialAttr")), // Only one self-KO move allowed - ), - ); - } - const totalWeight = movePool.reduce((v, m) => v + m[1], 0); - let rand = randSeedInt(totalWeight); - let index = 0; - while (rand > movePool[index][1]) { - rand -= movePool[index++][1]; - } - this.moveset.push(new PokemonMove(movePool[index][0])); - } + generateMoveset(this); // Trigger FormChange, except for enemy Pokemon during Mystery Encounters, to avoid crashes if ( From 793dea002401887678f8c767d97efc1365f10c4f Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Sun, 14 Sep 2025 22:35:12 -0500 Subject: [PATCH 13/40] [Bug] [Docs] Fix `@module` tags (#6557) Fix `@module` tags --- src/constants/colors.ts | 3 +- src/data/arena-tag.ts | 71 ++++++++++++------------ src/data/balance/moveset-generation.ts | 4 +- src/data/battler-tags.ts | 77 +++++++++++++------------- src/phase-manager.ts | 17 +++--- test/test-utils/setup/test-end-log.ts | 2 +- 6 files changed, 85 insertions(+), 89 deletions(-) diff --git a/src/constants/colors.ts b/src/constants/colors.ts index 717c5fa5f0d..a2400ef5f90 100644 --- a/src/constants/colors.ts +++ b/src/constants/colors.ts @@ -1,7 +1,8 @@ /** - * @module + * * A big file storing colors used in logging. * Minified by Terser during production builds, so has no overhead. + * @module */ // Colors used in prod diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index d3098314beb..e59d7173538 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -1,39 +1,4 @@ -/** biome-ignore-start lint/correctness/noUnusedImports: TSDoc imports */ -import type { BattlerTag } from "#app/data/battler-tags"; -/** biome-ignore-end lint/correctness/noUnusedImports: TSDoc imports */ - -import { applyAbAttrs, applyOnGainAbAttrs, applyOnLoseAbAttrs } from "#abilities/apply-ab-attrs"; -import { globalScene } from "#app/global-scene"; -import { getPokemonNameWithAffix } from "#app/messages"; -import { CommonBattleAnim } from "#data/battle-anims"; -import { allMoves } from "#data/data-lists"; -import { AbilityId } from "#enums/ability-id"; -import { ArenaTagSide } from "#enums/arena-tag-side"; -import { ArenaTagType } from "#enums/arena-tag-type"; -import { BattlerTagType } from "#enums/battler-tag-type"; -import { HitResult } from "#enums/hit-result"; -import { CommonAnim } from "#enums/move-anims-common"; -import { MoveCategory } from "#enums/move-category"; -import { MoveId } from "#enums/move-id"; -import { MoveTarget } from "#enums/move-target"; -import { PokemonType } from "#enums/pokemon-type"; -import { Stat } from "#enums/stat"; -import { StatusEffect } from "#enums/status-effect"; -import type { Arena } from "#field/arena"; -import type { Pokemon } from "#field/pokemon"; -import type { - ArenaScreenTagType, - ArenaTagData, - EntryHazardTagType, - RoomArenaTagType, - SerializableArenaTagType, -} from "#types/arena-tags"; -import type { Mutable } from "#types/type-helpers"; -import { BooleanHolder, type NumberHolder, toDmgValue } from "#utils/common"; -import i18next from "i18next"; - /** - * @module * ArenaTags are are meant for effects that are tied to the arena (as opposed to a specific pokemon). * Examples include (but are not limited to) * - Cross-turn effects that persist even if the user/target switches out, such as Happy Hour @@ -75,8 +40,42 @@ import i18next from "i18next"; * } * ``` * Notes - * - If the class has any subclasses, then the second form of `loadTag` *must* be used. + * - If the class has any subclasses, then the second form of `loadTag` *must* be used.\ + * @module */ +// biome-ignore-start lint/correctness/noUnusedImports: TSDoc imports +import type { BattlerTag } from "#app/data/battler-tags"; +// biome-ignore-end lint/correctness/noUnusedImports: TSDoc imports + +import { applyAbAttrs, applyOnGainAbAttrs, applyOnLoseAbAttrs } from "#abilities/apply-ab-attrs"; +import { globalScene } from "#app/global-scene"; +import { getPokemonNameWithAffix } from "#app/messages"; +import { CommonBattleAnim } from "#data/battle-anims"; +import { allMoves } from "#data/data-lists"; +import { AbilityId } from "#enums/ability-id"; +import { ArenaTagSide } from "#enums/arena-tag-side"; +import { ArenaTagType } from "#enums/arena-tag-type"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { HitResult } from "#enums/hit-result"; +import { CommonAnim } from "#enums/move-anims-common"; +import { MoveCategory } from "#enums/move-category"; +import { MoveId } from "#enums/move-id"; +import { MoveTarget } from "#enums/move-target"; +import { PokemonType } from "#enums/pokemon-type"; +import { Stat } from "#enums/stat"; +import { StatusEffect } from "#enums/status-effect"; +import type { Arena } from "#field/arena"; +import type { Pokemon } from "#field/pokemon"; +import type { + ArenaScreenTagType, + ArenaTagData, + EntryHazardTagType, + RoomArenaTagType, + SerializableArenaTagType, +} from "#types/arena-tags"; +import type { Mutable } from "#types/type-helpers"; +import { BooleanHolder, type NumberHolder, toDmgValue } from "#utils/common"; +import i18next from "i18next"; /** Interface containing the serializable fields of ArenaTagData. */ interface BaseArenaTag { diff --git a/src/data/balance/moveset-generation.ts b/src/data/balance/moveset-generation.ts index d6579eb45e7..91423f80f9a 100644 --- a/src/data/balance/moveset-generation.ts +++ b/src/data/balance/moveset-generation.ts @@ -3,9 +3,6 @@ * * SPDX-License-Identifier: AGPL-3.0-only */ -import { MoveId } from "#enums/move-id"; - - /** * # Balance: Moveset Generation Configuration * @@ -33,6 +30,7 @@ import { MoveId } from "#enums/move-id"; * * @module */ +import { MoveId } from "#enums/move-id"; //#region Constants diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 6490a6086c4..dc4a5383e24 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -1,3 +1,41 @@ +/** + * BattlerTags are used to represent semi-persistent effects that can be attached to a Pokemon. + * Note that before serialization, a new tag object is created, and then `loadTag` is called on the + * tag with the object that was serialized. + * + * This means it is straightforward to avoid serializing fields. + * Fields that are not set in the constructor and not set in `loadTag` will thus not be serialized. + * + * Any battler tag that can persist across sessions must extend SerializableBattlerTag in its class definition signature. + * Only tags that persist across waves (meaning their effect can last >1 turn) should be considered + * serializable. + * + * Serializable battler tags have strict requirements for their fields. + * Properties that are not necessary to reconstruct the tag must not be serialized. This can be avoided + * by using a private property. If access to the property is needed outside of the class, then + * a getter (and potentially, a setter) should be used instead. + * + * If a property that is intended to be private must be serialized, then it should instead + * be declared as a public readonly propety. Then, in the `loadTag` method (or any method inside the class that needs to adjust the property) + * use `(this as Mutable).propertyName = value;` + * These rules ensure that Typescript is aware of the shape of the serialized version of the class. + * + * If any new serializable fields *are* added, then the class *must* override the + * `loadTag` method to set the new fields. Its signature *must* match the example below: + * ``` + * class ExampleTag extends SerializableBattlerTag { + * // Example, if we add 2 new fields that should be serialized: + * public a: string; + * public b: number; + * // Then we must also define a loadTag method with one of the following signatures + * public override loadTag(source: BaseBattlerTag & Pick(source: BaseBattlerTag & Pick): void; + * } + * ``` + * Notes + * - If the class has any subclasses, then the second form of `loadTag` *must* be used. + * @module + */ import { applyAbAttrs } from "#abilities/apply-ab-attrs"; import { globalScene } from "#app/global-scene"; import { getPokemonNameWithAffix } from "#app/messages"; @@ -52,45 +90,6 @@ import type { Mutable } from "#types/type-helpers"; import { BooleanHolder, coerceArray, getFrameMs, NumberHolder, toDmgValue } from "#utils/common"; import { toCamelCase } from "#utils/strings"; -/** - * @module - * BattlerTags are used to represent semi-persistent effects that can be attached to a Pokemon. - * Note that before serialization, a new tag object is created, and then `loadTag` is called on the - * tag with the object that was serialized. - * - * This means it is straightforward to avoid serializing fields. - * Fields that are not set in the constructor and not set in `loadTag` will thus not be serialized. - * - * Any battler tag that can persist across sessions must extend SerializableBattlerTag in its class definition signature. - * Only tags that persist across waves (meaning their effect can last >1 turn) should be considered - * serializable. - * - * Serializable battler tags have strict requirements for their fields. - * Properties that are not necessary to reconstruct the tag must not be serialized. This can be avoided - * by using a private property. If access to the property is needed outside of the class, then - * a getter (and potentially, a setter) should be used instead. - * - * If a property that is intended to be private must be serialized, then it should instead - * be declared as a public readonly propety. Then, in the `loadTag` method (or any method inside the class that needs to adjust the property) - * use `(this as Mutable).propertyName = value;` - * These rules ensure that Typescript is aware of the shape of the serialized version of the class. - * - * If any new serializable fields *are* added, then the class *must* override the - * `loadTag` method to set the new fields. Its signature *must* match the example below: - * ``` - * class ExampleTag extends SerializableBattlerTag { - * // Example, if we add 2 new fields that should be serialized: - * public a: string; - * public b: number; - * // Then we must also define a loadTag method with one of the following signatures - * public override loadTag(source: BaseBattlerTag & Pick(source: BaseBattlerTag & Pick): void; - * } - * ``` - * Notes - * - If the class has any subclasses, then the second form of `loadTag` *must* be used. - */ - /** Interface containing the serializable fields of BattlerTag */ interface BaseBattlerTag { /** The tag's remaining duration */ diff --git a/src/phase-manager.ts b/src/phase-manager.ts index 4bb7e0a4b37..253c6d8314d 100644 --- a/src/phase-manager.ts +++ b/src/phase-manager.ts @@ -1,3 +1,11 @@ +/** + * Manager for phases used by battle scene. + * + * @remarks + * **This file must not be imported or used directly.** + * The manager is exclusively used by the Battle Scene and is NOT intended for external use. + * @module + */ import { PHASE_START_COLOR } from "#app/constants/colors"; import { globalScene } from "#app/global-scene"; import type { Phase } from "#app/phase"; @@ -103,15 +111,6 @@ import { WeatherEffectPhase } from "#phases/weather-effect-phase"; import type { PhaseMap, PhaseString } from "#types/phase-types"; import { type Constructor, coerceArray } from "#utils/common"; -/** - * @module - * Manager for phases used by battle scene. - * - * @remarks - * **This file must not be imported or used directly.** - * The manager is exclusively used by the Battle Scene and is NOT intended for external use. - */ - /** * Object that holds all of the phase constructors. * This is used to create new phases dynamically using the `newPhase` method in the `PhaseManager`. diff --git a/test/test-utils/setup/test-end-log.ts b/test/test-utils/setup/test-end-log.ts index 9814ba8a45c..fbcb9480330 100644 --- a/test/test-utils/setup/test-end-log.ts +++ b/test/test-utils/setup/test-end-log.ts @@ -5,10 +5,10 @@ import chalk from "chalk"; import type { RunnerTask, RunnerTaskResult, RunnerTestCase } from "vitest"; /** - * @module * Code to add markers to the beginning and end of tests. * Intended for use with {@linkcode CustomDefaultReporter}, and placed inside test hooks * (rather than as part of the reporter) to ensure Vitest waits for the log messages to be printed. + * @module */ /** A long string of "="s to partition off each test from one another. */ From de94e738fbc5eed77b104152632eaf51e9b8ee86 Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Sun, 14 Sep 2025 20:45:01 -0700 Subject: [PATCH 14/40] [Docs] Add blank space to prevent incorrect comment attachment Biome will "attach" comments to imports if there is no space between them when it sorts imports (this allows suppression comments to work) --- src/data/arena-tag.ts | 3 ++- src/data/balance/moveset-generation.ts | 1 + src/data/battler-tags.ts | 1 + src/phase-manager.ts | 1 + test/test-utils/setup/test-end-log.ts | 12 ++++++------ 5 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index e59d7173538..ff939194bcd 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -40,9 +40,10 @@ * } * ``` * Notes - * - If the class has any subclasses, then the second form of `loadTag` *must* be used.\ + * - If the class has any subclasses, then the second form of `loadTag` *must* be used. * @module */ + // biome-ignore-start lint/correctness/noUnusedImports: TSDoc imports import type { BattlerTag } from "#app/data/battler-tags"; // biome-ignore-end lint/correctness/noUnusedImports: TSDoc imports diff --git a/src/data/balance/moveset-generation.ts b/src/data/balance/moveset-generation.ts index 91423f80f9a..f9c2a03f4a9 100644 --- a/src/data/balance/moveset-generation.ts +++ b/src/data/balance/moveset-generation.ts @@ -30,6 +30,7 @@ * * @module */ + import { MoveId } from "#enums/move-id"; diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index dc4a5383e24..80a30516903 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -36,6 +36,7 @@ * - If the class has any subclasses, then the second form of `loadTag` *must* be used. * @module */ + import { applyAbAttrs } from "#abilities/apply-ab-attrs"; import { globalScene } from "#app/global-scene"; import { getPokemonNameWithAffix } from "#app/messages"; diff --git a/src/phase-manager.ts b/src/phase-manager.ts index 253c6d8314d..3fbf68de60d 100644 --- a/src/phase-manager.ts +++ b/src/phase-manager.ts @@ -6,6 +6,7 @@ * The manager is exclusively used by the Battle Scene and is NOT intended for external use. * @module */ + import { PHASE_START_COLOR } from "#app/constants/colors"; import { globalScene } from "#app/global-scene"; import type { Phase } from "#app/phase"; diff --git a/test/test-utils/setup/test-end-log.ts b/test/test-utils/setup/test-end-log.ts index fbcb9480330..5be8299b124 100644 --- a/test/test-utils/setup/test-end-log.ts +++ b/test/test-utils/setup/test-end-log.ts @@ -1,9 +1,3 @@ -// biome-ignore lint/correctness/noUnusedImports: TSDoc -import type CustomDefaultReporter from "#test/test-utils/reporters/custom-default-reporter"; -import { basename, join, relative } from "path"; -import chalk from "chalk"; -import type { RunnerTask, RunnerTaskResult, RunnerTestCase } from "vitest"; - /** * Code to add markers to the beginning and end of tests. * Intended for use with {@linkcode CustomDefaultReporter}, and placed inside test hooks @@ -11,6 +5,12 @@ import type { RunnerTask, RunnerTaskResult, RunnerTestCase } from "vitest"; * @module */ +// biome-ignore lint/correctness/noUnusedImports: TSDoc +import type CustomDefaultReporter from "#test/test-utils/reporters/custom-default-reporter"; +import { basename, join, relative } from "path"; +import chalk from "chalk"; +import type { RunnerTask, RunnerTaskResult, RunnerTestCase } from "vitest"; + /** A long string of "="s to partition off each test from one another. */ const TEST_END_BARRIER = chalk.bold.hex("#ff7c7cff")("=================="); From b7cee4b3131211587ec8b67f085b456c5fbc0892 Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Mon, 15 Sep 2025 00:25:25 -0700 Subject: [PATCH 15/40] [Misc] Remove leftover temporary comments in `pokemon.ts` --- src/field/pokemon.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 46f88abc61c..4b8a39ee759 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -1440,7 +1440,6 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { simulated = true, ignoreHeldItems = false, ): number { - // biome-ignore-start lint/nursery/useMaxParams: test const statVal = new NumberHolder(this.getStat(stat, false)); if (!ignoreHeldItems) { globalScene.applyModifiers(StatBoosterModifier, this.isPlayer(), this, stat, statVal); @@ -1539,7 +1538,6 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } return Math.floor(ret); - // biome-ignore-end lint/nursery/useMaxParams: test } calculateStats(): void { From 8013093513fd272b6f8aa5d76329c682a098813d Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Mon, 15 Sep 2025 14:04:13 -0500 Subject: [PATCH 16/40] [Bug] Secondary fusions with gender evo condition can now evolve (#6510) --- src/data/balance/pokemon-evolutions.ts | 6 +++--- src/field/pokemon.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/data/balance/pokemon-evolutions.ts b/src/data/balance/pokemon-evolutions.ts index c868a49dbfa..0c2fa4e78fa 100644 --- a/src/data/balance/pokemon-evolutions.ts +++ b/src/data/balance/pokemon-evolutions.ts @@ -166,7 +166,7 @@ export class SpeciesEvolutionCondition { return this.desc; } - public conditionsFulfilled(pokemon: Pokemon): boolean { + public conditionsFulfilled(pokemon: Pokemon, forFusion = false): boolean { console.log(this.data); return this.data.every(cond => { switch (cond.key) { @@ -186,7 +186,7 @@ export class SpeciesEvolutionCondition { m.getStackCount() + pokemon.getPersistentTreasureCount() >= cond.value ); case EvoCondKey.GENDER: - return pokemon.gender === cond.gender; + return cond.gender === (forFusion ? pokemon.fusionGender : pokemon.gender); case EvoCondKey.SHEDINJA: // Shedinja cannot be evolved into directly return false; case EvoCondKey.BIOME: @@ -293,7 +293,7 @@ export class SpeciesFormEvolution { pokemon.level >= this.level && // Check form key, using the fusion's form key if we're checking the fusion (this.preFormKey == null || (forFusion ? pokemon.getFusionFormKey() : pokemon.getFormKey()) === this.preFormKey) && - (this.condition == null || this.condition.conditionsFulfilled(pokemon)) && + (this.condition == null || this.condition.conditionsFulfilled(pokemon, forFusion)) && ((item ?? EvolutionItem.NONE) === (this.item ?? EvolutionItem.NONE)) ); } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 4b8a39ee759..d48f4ae8ad2 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -2635,7 +2635,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { e => new FusionSpeciesFormEvolution(this.species.speciesId, e), ); for (const fe of fusionEvolutions) { - if (fe.validate(this)) { + if (fe.validate(this, true)) { return fe; } } From 65bb58a635c5e32eb852f5ab2db438e97b0ef907 Mon Sep 17 00:00:00 2001 From: Bertie690 <136088738+Bertie690@users.noreply.github.com> Date: Mon, 15 Sep 2025 16:51:28 -0400 Subject: [PATCH 17/40] [Dev] Fix + rename `test:create` boilerplate file (#6560) * Rename `default.ts` to `default.boilerplate.ts` * Update `test:create` script to look in the correct location * Update `biome.jsonc` to remove explicit boilerplate folder check * Apply Biome --- biome.jsonc | 11 ++--------- .../{default.ts => default.boilerplate.ts} | 2 +- scripts/create-test/create-test.js | 4 ++-- 3 files changed, 5 insertions(+), 12 deletions(-) rename scripts/create-test/boilerplates/{default.ts => default.boilerplate.ts} (94%) diff --git a/biome.jsonc b/biome.jsonc index 9e87518e636..2433ba52010 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -254,16 +254,9 @@ }, // Overrides to prevent unused import removal inside `overrides.ts`, enums & `.d.ts` files (for TSDoc linkcodes), - // as well as inside script boilerplate files. + // as well as inside script boilerplate files (whose imports will _presumably_ be used in the generated file). { - // TODO: Rename existing boilerplates in the folder and remove this last alias - "includes": [ - "**/src/overrides.ts", - "**/src/enums/**/*", - "**/*.d.ts", - "scripts/**/*.boilerplate.ts", - "**/boilerplates/*.ts" - ], + "includes": ["**/src/overrides.ts", "**/src/enums/**/*", "**/*.d.ts", "scripts/**/*.boilerplate.ts"], "linter": { "rules": { "correctness": { diff --git a/scripts/create-test/boilerplates/default.ts b/scripts/create-test/boilerplates/default.boilerplate.ts similarity index 94% rename from scripts/create-test/boilerplates/default.ts rename to scripts/create-test/boilerplates/default.boilerplate.ts index e644e740594..7b633cf8276 100644 --- a/scripts/create-test/boilerplates/default.ts +++ b/scripts/create-test/boilerplates/default.boilerplate.ts @@ -47,6 +47,6 @@ describe("{{description}}", () => { await game.toEndOfTurn(); expect(feebas).toHaveUsedMove({ move: MoveId.SPLASH, result: MoveResult.SUCCESS }); - expect(game.textInterceptor.logs).toContain(i18next.t("moveTriggers:splash")); + expect(game).toHaveShownMessage(i18next.t("moveTriggers:splash")); }); }); diff --git a/scripts/create-test/create-test.js b/scripts/create-test/create-test.js index 5e395783da7..df065657346 100644 --- a/scripts/create-test/create-test.js +++ b/scripts/create-test/create-test.js @@ -102,9 +102,9 @@ async function promptFileName(selectedType) { function getBoilerplatePath(choiceType) { switch (choiceType) { // case "Reward": - // return path.join(__dirname, "boilerplates/reward.ts"); + // return path.join(__dirname, "boilerplates/reward.boilerplate.ts"); default: - return path.join(__dirname, "boilerplates/default.ts"); + return path.join(__dirname, "boilerplates/default.boilerplate.ts"); } } From 6ce59ecbd983bccbf24ca3c7f4c2cbf42d78a41d Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Mon, 15 Sep 2025 19:49:18 -0500 Subject: [PATCH 18/40] [Bug] Cookies being fetched improperly --- src/utils/cookies.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/cookies.ts b/src/utils/cookies.ts index e82895d1fac..49c47437241 100644 --- a/src/utils/cookies.ts +++ b/src/utils/cookies.ts @@ -25,9 +25,9 @@ export function getCookie(cName: string): string { const ca = document.cookie.split(";"); // Check all cookies in the document and see if any of them match, grabbing the first one whose value lines up for (const c of ca) { - const cTrimmed = c.trim(); + const cTrimmed = c.trimStart(); if (cTrimmed.startsWith(name)) { - return c.slice(name.length, c.length); + return c.substring(name.length, c.length); } } return ""; From 85c38dfdbe1a6cf6aa26862ec7952ab76f2d7ab0 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Mon, 15 Sep 2025 19:55:45 -0500 Subject: [PATCH 19/40] [Bug] Cookies being fetched improperly v2 --- src/utils/cookies.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/utils/cookies.ts b/src/utils/cookies.ts index 49c47437241..e16d9d78556 100644 --- a/src/utils/cookies.ts +++ b/src/utils/cookies.ts @@ -23,10 +23,12 @@ export function getCookie(cName: string): string { } const name = `${cName}=`; const ca = document.cookie.split(";"); - // Check all cookies in the document and see if any of them match, grabbing the first one whose value lines up - for (const c of ca) { - const cTrimmed = c.trimStart(); - if (cTrimmed.startsWith(name)) { + for (let c of ca) { + // ⚠️ DO NOT REPLACE THIS WITH C = C.TRIM() - IT BREAKS IN NON-CHROMIUM BROWSERS ⚠️ + while (c.charAt(0) === " ") { + c = c.substring(1); + } + if (c.indexOf(name) === 0) { return c.substring(name.length, c.length); } } From 0c921cdb4af7ce735a4d2fe8e272a08b1026aa5c Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Mon, 15 Sep 2025 23:26:57 -0500 Subject: [PATCH 20/40] [Tests][Bug][Beta] Fix ditto bug and add unit tests (#6559) * Fix ditto bug and add unit tests Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com> --- src/ai/ai-moveset-gen.ts | 63 +++++- src/data/balance/moveset-generation.ts | 27 +++ src/vite.env.d.ts | 3 +- test/ai/ai-moveset-gen.test.ts | 285 +++++++++++++++++++++++++ 4 files changed, 373 insertions(+), 5 deletions(-) create mode 100644 test/ai/ai-moveset-gen.test.ts diff --git a/src/ai/ai-moveset-gen.ts b/src/ai/ai-moveset-gen.ts index f46df09b33e..f392ca46d3f 100644 --- a/src/ai/ai-moveset-gen.ts +++ b/src/ai/ai-moveset-gen.ts @@ -1,6 +1,7 @@ import { globalScene } from "#app/global-scene"; import { speciesEggMoves } from "#balance/egg-moves"; import { + BASE_LEVEL_WEIGHT_OFFSET, BASE_WEIGHT_MULTIPLIER, BOSS_EXTRA_WEIGHT_MULTIPLIER, COMMON_TIER_TM_LEVEL_REQUIREMENT, @@ -72,7 +73,7 @@ function getAndWeightLevelMoves(pokemon: Pokemon): Map { continue; } - let weight = learnLevel + 20; + let weight = learnLevel + BASE_LEVEL_WEIGHT_OFFSET; switch (learnLevel) { case EVOLVE_MOVE: weight = EVOLUTION_MOVE_WEIGHT; @@ -132,6 +133,11 @@ function getTmPoolForSpecies( ): void { const [allowCommon, allowGreat, allowUltra] = allowedTiers; const tms = speciesTmMoves[speciesId]; + // Species with no learnable TMs (e.g. Ditto) don't have entries in the `speciesTmMoves` object, + // so this is needed to avoid iterating over `undefined` + if (tms == null) { + return; + } let moveId: MoveId; for (const tm of tms) { @@ -241,7 +247,11 @@ function getEggPoolForSpecies( excludeRare: boolean, rareEggMoveWeight = 0, ): void { - for (const [idx, moveId] of speciesEggMoves[rootSpeciesId].entries()) { + const eggMoves = speciesEggMoves[rootSpeciesId]; + if (eggMoves == null) { + return; + } + for (const [idx, moveId] of eggMoves.entries()) { if (levelPool.has(moveId) || (idx === 3 && excludeRare)) { continue; } @@ -416,7 +426,6 @@ function adjustDamageMoveWeights(pool: Map, pokemon: Pokemon, wi * @param pool - The move pool to calculate the total weight for * @returns The total weight of all moves in the pool */ -// biome-ignore lint/correctness/noUnusedVariables: May be useful function calculateTotalPoolWeight(pool: Map): number { let totalWeight = 0; for (const weight of pool.values()) { @@ -622,7 +631,7 @@ function fillInRemainingMovesetSlots( * @param note - Short note to include in the log for context */ function debugMoveWeights(pokemon: Pokemon, pool: Map, note: string): void { - if (isBeta || import.meta.env.DEV) { + if ((isBeta || import.meta.env.DEV) && import.meta.env.NODE_ENV !== "test") { const moveNameToWeightMap = new Map(); const sortedByValue = Array.from(pool.entries()).sort((a, b) => b[1] - a[1]); for (const [moveId, weight] of sortedByValue) { @@ -713,3 +722,49 @@ export function generateMoveset(pokemon: Pokemon): void { filterPool(baseWeights, (m: MoveId) => !pokemon.moveset.some(mo => m[0] === mo.moveId)), ); } + +/** + * Exports for internal testing purposes. + * ⚠️ These *must not* be used outside of tests, as they will not be defined. + * @internal + */ +export const __INTERNAL_TEST_EXPORTS: { + getAndWeightLevelMoves: typeof getAndWeightLevelMoves; + getAllowedTmTiers: typeof getAllowedTmTiers; + getTmPoolForSpecies: typeof getTmPoolForSpecies; + getAndWeightTmMoves: typeof getAndWeightTmMoves; + getEggMoveWeight: typeof getEggMoveWeight; + getEggPoolForSpecies: typeof getEggPoolForSpecies; + getAndWeightEggMoves: typeof getAndWeightEggMoves; + filterMovePool: typeof filterMovePool; + adjustWeightsForTrainer: typeof adjustWeightsForTrainer; + adjustDamageMoveWeights: typeof adjustDamageMoveWeights; + calculateTotalPoolWeight: typeof calculateTotalPoolWeight; + filterPool: typeof filterPool; + forceStabMove: typeof forceStabMove; + filterRemainingTrainerMovePool: typeof filterRemainingTrainerMovePool; + fillInRemainingMovesetSlots: typeof fillInRemainingMovesetSlots; +} = {} as any; + +// We can't use `import.meta.vitest` here, because this would not be set +// until the tests themselves begin to run, which is after imports +// So we rely on NODE_ENV being test instead +if (import.meta.env.NODE_ENV === "test") { + Object.assign(__INTERNAL_TEST_EXPORTS, { + getAndWeightLevelMoves, + getAllowedTmTiers, + getTmPoolForSpecies, + getAndWeightTmMoves, + getEggMoveWeight, + getEggPoolForSpecies, + getAndWeightEggMoves, + filterMovePool, + adjustWeightsForTrainer, + adjustDamageMoveWeights, + calculateTotalPoolWeight, + filterPool, + forceStabMove, + filterRemainingTrainerMovePool, + fillInRemainingMovesetSlots, + }); +} diff --git a/src/data/balance/moveset-generation.ts b/src/data/balance/moveset-generation.ts index f9c2a03f4a9..90a602ca97e 100644 --- a/src/data/balance/moveset-generation.ts +++ b/src/data/balance/moveset-generation.ts @@ -83,6 +83,33 @@ export const GREAT_TM_MOVESET_WEIGHT = 14; /** The weight given to TMs in the ultra tier during moveset generation */ export const ULTRA_TM_MOVESET_WEIGHT = 18; +/** + * The base weight offset for level moves + * + * @remarks + * The relative likelihood of moves learned at different levels is determined by + * the ratio of their weights, + * or, the formula: + * `(levelB + BASE_LEVEL_WEIGHT_OFFSET) / (levelA + BASE_LEVEL_WEIGHT_OFFSET)` + * + * For example, consider move A and B that are learned at levels 1 and 60, respectively, + * but have no other differences (same power, accuracy, category, etc). + * The following table demonstrates the likelihood of move B being chosen over move A. + * + * | Offset | Likelihood | + * |--------|------------| + * | 0 | 60x | + * | 1 | 30x | + * | 5 | 10.8x | + * | 20 | 3.8x | + * | 60 | 2x | + * + * Note that increasing this without adjusting the other weights will decrease the likelihood of non-level moves + * + * For a complete picture, see {@link https://www.desmos.com/calculator/wgln4dxigl} + */ +export const BASE_LEVEL_WEIGHT_OFFSET = 20; + /** * The maximum weight an egg move can ever have * @remarks diff --git a/src/vite.env.d.ts b/src/vite.env.d.ts index 68159908730..3192b81afd3 100644 --- a/src/vite.env.d.ts +++ b/src/vite.env.d.ts @@ -9,8 +9,9 @@ interface ImportMetaEnv { readonly VITE_DISCORD_CLIENT_ID?: string; readonly VITE_GOOGLE_CLIENT_ID?: string; readonly VITE_I18N_DEBUG?: string; + readonly NODE_ENV?: string; } -interface ImportMeta { +declare interface ImportMeta { readonly env: ImportMetaEnv; } diff --git a/test/ai/ai-moveset-gen.test.ts b/test/ai/ai-moveset-gen.test.ts new file mode 100644 index 00000000000..6d927926131 --- /dev/null +++ b/test/ai/ai-moveset-gen.test.ts @@ -0,0 +1,285 @@ +import { __INTERNAL_TEST_EXPORTS } from "#app/ai/ai-moveset-gen"; +import { + COMMON_TIER_TM_LEVEL_REQUIREMENT, + GREAT_TIER_TM_LEVEL_REQUIREMENT, + ULTRA_TIER_TM_LEVEL_REQUIREMENT, +} from "#balance/moveset-generation"; +import { allMoves, allSpecies } from "#data/data-lists"; +import { MoveId } from "#enums/move-id"; +import { SpeciesId } from "#enums/species-id"; +import { TrainerSlot } from "#enums/trainer-slot"; +import { EnemyPokemon } from "#field/pokemon"; +import { GameManager } from "#test/test-utils/game-manager"; +import { NumberHolder } from "#utils/common"; +import { afterEach } from "node:test"; +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; + +/** + * Parameters for {@linkcode createTestablePokemon} + */ +interface MockPokemonParams { + /** The level to set the Pokémon to */ + level: number; + /** + * Whether the pokemon is a boss or not. + * @defaultValue `false` + */ + boss?: boolean; + /** + * The trainer slot to assign to the pokemon, if any. + * @defaultValue `TrainerSlot.NONE` + */ + trainerSlot?: TrainerSlot; + /** + * The form index to assign to the pokemon, if any. + * This *must* be one of the valid form indices for the species, or the test will break. + * @defaultValue `0` + */ + formIndex?: number; +} + +/** + * Construct an `EnemyPokemon` that can be used for testing + * @param species - The species ID of the pokemon to create + * @returns The newly created `EnemyPokemon`. + * @todo Move this to a dedicated unit test util folder if more tests come to rely on it + */ +function createTestablePokemon( + species: SpeciesId, + { level, trainerSlot = TrainerSlot.NONE, boss = false, formIndex = 0 }: MockPokemonParams, +): EnemyPokemon { + const pokemon = new EnemyPokemon(allSpecies[species], level, trainerSlot, boss); + if (formIndex !== 0) { + const formIndexLength = allSpecies[species]?.forms.length; + const name = allSpecies[species]?.name; + expect(formIndex, `${name} does not have a form with index ${formIndex}`).toBeLessThan(formIndexLength); + pokemon.formIndex = formIndex; + } + + return pokemon; +} + +describe("Unit Tests - ai-moveset-gen.ts", () => { + describe("filterPool", () => { + const { filterPool } = __INTERNAL_TEST_EXPORTS; + it("clones a pool when there are no predicates", () => { + const pool = new Map([ + [MoveId.TACKLE, 1], + [MoveId.FLAMETHROWER, 2], + ]); + + const filtered = filterPool(pool, () => true); + const expected = [ + [MoveId.TACKLE, 1], + [MoveId.FLAMETHROWER, 2], + ]; + expect(filtered).toEqual(expected); + }); + + it("does not modify the original pool", () => { + const pool = new Map([ + [MoveId.TACKLE, 1], + [MoveId.FLAMETHROWER, 2], + ]); + const original = new Map(pool); + + filterPool(pool, moveId => moveId !== MoveId.TACKLE); + expect(pool).toEqual(original); + }); + + it("filters out moves that do not match the predicate", () => { + const pool = new Map([ + [MoveId.TACKLE, 1], + [MoveId.FLAMETHROWER, 2], + [MoveId.SPLASH, 3], + ]); + const filtered = filterPool(pool, moveId => moveId !== MoveId.SPLASH); + expect(filtered).toEqual([ + [MoveId.TACKLE, 1], + [MoveId.FLAMETHROWER, 2], + ]); + }); + + it("returns an empty array if no moves match the predicate", () => { + const pool = new Map([ + [MoveId.TACKLE, 1], + [MoveId.FLAMETHROWER, 2], + ]); + const filtered = filterPool(pool, () => false); + expect(filtered).toEqual([]); + }); + + it("calculates totalWeight correctly when provided", () => { + const pool = new Map([ + [MoveId.TACKLE, 1], + [MoveId.FLAMETHROWER, 2], + [MoveId.SPLASH, 3], + ]); + const totalWeight = new NumberHolder(0); + const filtered = filterPool(pool, moveId => moveId !== MoveId.SPLASH, totalWeight); + expect(filtered).toEqual([ + [MoveId.TACKLE, 1], + [MoveId.FLAMETHROWER, 2], + ]); + expect(totalWeight.value).toBe(3); + }); + + it("Clears totalWeight when provided", () => { + const pool = new Map([ + [MoveId.TACKLE, 1], + [MoveId.FLAMETHROWER, 2], + ]); + const totalWeight = new NumberHolder(42); + const filtered = filterPool(pool, () => false, totalWeight); + expect(filtered).toEqual([]); + expect(totalWeight.value).toBe(0); + }); + }); + + describe("getAllowedTmTiers", () => { + const { getAllowedTmTiers } = __INTERNAL_TEST_EXPORTS; + + it.each([ + { tierName: "common", resIdx: 0, level: COMMON_TIER_TM_LEVEL_REQUIREMENT - 1 }, + { tierName: "great", resIdx: 1, level: GREAT_TIER_TM_LEVEL_REQUIREMENT - 1 }, + { tierName: "ultra", resIdx: 2, level: ULTRA_TIER_TM_LEVEL_REQUIREMENT - 1 }, + ])("should prevent $name TMs when below level $level", ({ level, resIdx }) => { + expect(getAllowedTmTiers(level)[resIdx]).toBe(false); + }); + + it.each([ + { tierName: "common", resIdx: 0, level: COMMON_TIER_TM_LEVEL_REQUIREMENT }, + { tierName: "great", resIdx: 1, level: GREAT_TIER_TM_LEVEL_REQUIREMENT }, + { tierName: "ultra", resIdx: 2, level: ULTRA_TIER_TM_LEVEL_REQUIREMENT }, + ])("should allow $name TMs when at level $level", ({ level, resIdx }) => { + expect(getAllowedTmTiers(level)[resIdx]).toBe(true); + }); + }); + + // Unit tests for methods that require a game context + describe("", () => { + //#region boilerplate + let phaserGame: Phaser.Game; + let game: GameManager; + /**A pokemon object that will be cleaned up after every test */ + let pokemon: EnemyPokemon | null = null; + + beforeAll(async () => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + // Game manager can be reused between tests as we are not really modifying the global state + // So there is no need to put this in a beforeEach with cleanup in afterEach. + game = new GameManager(phaserGame); + }); + + afterEach(() => { + pokemon?.destroy(); + }); + // Sanitize the interceptor after running the suite to ensure other tests are not affected + afterAll(() => { + game.phaseInterceptor.restoreOg(); + }); + //#endregion boilerplate + + function createCharmander(_ = pokemon): asserts _ is EnemyPokemon { + pokemon?.destroy(); + pokemon = createTestablePokemon(SpeciesId.CHARMANDER, { level: 10 }); + expect(pokemon).toBeInstanceOf(EnemyPokemon); + } + describe("getAndWeightLevelMoves", () => { + const { getAndWeightLevelMoves } = __INTERNAL_TEST_EXPORTS; + + it("returns an empty map if getLevelMoves throws", async () => { + createCharmander(pokemon); + vi.spyOn(pokemon, "getLevelMoves").mockImplementation(() => { + throw new Error("fail"); + }); + // Suppress the warning from the test output + const warnMock = vi.spyOn(console, "warn").mockImplementationOnce(() => {}); + + const result = getAndWeightLevelMoves(pokemon); + expect(warnMock).toHaveBeenCalled(); + expect(result.size).toBe(0); + }); + + it("skips unimplemented moves", () => { + createCharmander(pokemon); + vi.spyOn(pokemon, "getLevelMoves").mockReturnValue([ + [1, MoveId.TACKLE], + [5, MoveId.GROWL], + ]); + vi.spyOn(allMoves[MoveId.TACKLE], "name", "get").mockReturnValue("Tackle (N)"); + const result = getAndWeightLevelMoves(pokemon); + expect(result.has(MoveId.TACKLE)).toBe(false); + expect(result.has(MoveId.GROWL)).toBe(true); + }); + + it("skips moves already in the pool", () => { + createCharmander(pokemon); + vi.spyOn(pokemon, "getLevelMoves").mockReturnValue([ + [1, MoveId.TACKLE], + [5, MoveId.TACKLE], + ]); + + const result = getAndWeightLevelMoves(pokemon); + expect(result.get(MoveId.TACKLE)).toBe(21); + }); + + it("weights moves based on level", () => { + createCharmander(pokemon); + vi.spyOn(pokemon, "getLevelMoves").mockReturnValue([ + [1, MoveId.TACKLE], + [5, MoveId.GROWL], + [9, MoveId.EMBER], + ]); + + const result = getAndWeightLevelMoves(pokemon); + expect(result.get(MoveId.TACKLE)).toBe(21); + expect(result.get(MoveId.GROWL)).toBe(25); + expect(result.get(MoveId.EMBER)).toBe(29); + }); + }); + }); +}); + +describe("Regression Tests - ai-moveset-gen.ts", () => { + //#region boilerplate + let phaserGame: Phaser.Game; + let game: GameManager; + /**A pokemon object that will be cleaned up after every test */ + let pokemon: EnemyPokemon | null = null; + + beforeAll(async () => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + // Game manager can be reused between tests as we are not really modifying the global state + // So there is no need to put this in a beforeEach with cleanup in afterEach. + game = new GameManager(phaserGame); + }); + + afterEach(() => { + pokemon?.destroy(); + }); + + afterAll(() => { + game.phaseInterceptor.restoreOg(); + }); + //#endregion boilerplate + + describe("getTmPoolForSpecies", () => { + const { getTmPoolForSpecies } = __INTERNAL_TEST_EXPORTS; + + it("should not crash when generating a moveset for Pokemon without TM moves", () => { + pokemon = createTestablePokemon(SpeciesId.DITTO, { level: 50 }); + expect(() => + getTmPoolForSpecies(SpeciesId.DITTO, ULTRA_TIER_TM_LEVEL_REQUIREMENT, "", new Map(), new Map(), new Map(), [ + true, + true, + true, + ]), + ).not.toThrow(); + }); + }); +}); From e25db1632608c03433c7f334f853112f3315c651 Mon Sep 17 00:00:00 2001 From: Madmadness65 Date: Tue, 16 Sep 2025 02:45:30 -0500 Subject: [PATCH 21/40] [P3 Bug] Fix login screen not playing menu_open SFX properly It did not properly specify that the SFX file was in the ui folder. --- src/phases/login-phase.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/phases/login-phase.ts b/src/phases/login-phase.ts index f310c60b0d4..d81b9b614f2 100644 --- a/src/phases/login-phase.ts +++ b/src/phases/login-phase.ts @@ -33,7 +33,7 @@ export class LoginPhase extends Phase { globalScene.ui.showText(i18next.t("menu:logInOrCreateAccount")); } - globalScene.playSound("menu_open"); + globalScene.playSound("ui/menu_open"); const loadData = () => { updateUserInfo().then(success => { @@ -53,7 +53,7 @@ export class LoginPhase extends Phase { loadData(); }, () => { - globalScene.playSound("menu_open"); + globalScene.playSound("ui/menu_open"); globalScene.ui.setMode(UiMode.REGISTRATION_FORM, { buttonActions: [ () => { From 37e6371eefeb391e0c2006b92340d48a848412ce Mon Sep 17 00:00:00 2001 From: Fabi <192151969+fabske0@users.noreply.github.com> Date: Tue, 16 Sep 2025 17:48:19 +0200 Subject: [PATCH 22/40] [Dev] Allow forcing all trainer variants in trainer override (#6391) --- src/battle-scene.ts | 25 ++++++++++++++++--------- src/overrides.ts | 9 +++++++-- test/moves/whirlwind.test.ts | 3 ++- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 374cf3f270f..cbda368782e 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -1332,13 +1332,12 @@ export class BattleScene extends SceneBase { if (newBattleType === BattleType.TRAINER) { const trainerType = Overrides.RANDOM_TRAINER_OVERRIDE?.trainerType ?? this.arena.randomTrainerType(newWaveIndex); + const hasDouble = trainerConfigs[trainerType].hasDouble; let doubleTrainer = false; if (trainerConfigs[trainerType].doubleOnly) { doubleTrainer = true; - } else if (trainerConfigs[trainerType].hasDouble) { - doubleTrainer = - Overrides.RANDOM_TRAINER_OVERRIDE?.alwaysDouble - || !randSeedInt(this.getDoubleBattleChance(newWaveIndex, playerField)); + } else if (hasDouble) { + doubleTrainer = !randSeedInt(this.getDoubleBattleChance(newWaveIndex, playerField)); // Add a check that special trainers can't be double except for tate and liza - they should use the normal double chance if ( trainerConfigs[trainerType].trainerTypeDouble @@ -1347,11 +1346,19 @@ export class BattleScene extends SceneBase { doubleTrainer = false; } } - const variant = doubleTrainer - ? TrainerVariant.DOUBLE - : randSeedInt(2) - ? TrainerVariant.FEMALE - : TrainerVariant.DEFAULT; + + // Forcing a double battle on wave 1 causes a bug where only one enemy is sent out, + // making it impossible to complete the fight without a reload + const overrideVariant = + Overrides.RANDOM_TRAINER_OVERRIDE?.trainerVariant === TrainerVariant.DOUBLE + && (!hasDouble || newWaveIndex <= 1) + ? TrainerVariant.DEFAULT + : Overrides.RANDOM_TRAINER_OVERRIDE?.trainerVariant; + + const variant = + overrideVariant + ?? (doubleTrainer ? TrainerVariant.DOUBLE : randSeedInt(2) ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT); + newTrainer = trainerData !== undefined ? trainerData.toTrainer() : new Trainer(trainerType, variant); this.field.add(newTrainer); } diff --git a/src/overrides.ts b/src/overrides.ts index 9f6b4ced3a2..3f61196f0b4 100644 --- a/src/overrides.ts +++ b/src/overrides.ts @@ -18,6 +18,7 @@ import { Stat } from "#enums/stat"; import { StatusEffect } from "#enums/status-effect"; import { TimeOfDay } from "#enums/time-of-day"; import { TrainerType } from "#enums/trainer-type"; +import { TrainerVariant } from "#enums/trainer-variant"; import { Unlockables } from "#enums/unlockables"; import { VariantTier } from "#enums/variant-tier"; import { WeatherType } from "#enums/weather-type"; @@ -311,8 +312,12 @@ export type BattleStyle = "double" | "single" | "even-doubles" | "odd-doubles"; export type RandomTrainerOverride = { /** The Type of trainer to force */ trainerType: Exclude; - /* If the selected trainer type has a double version, it will always use its double version. */ - alwaysDouble?: boolean; + /** + * The {@linkcode TrainerVariant} to force. + * @remarks + * `TrainerVariant.DOUBLE` cannot be forced on the first wave of a game due to issues with trainer party generation. + */ + trainerVariant?: TrainerVariant; }; /** The type of the {@linkcode DefaultOverrides} class */ diff --git a/test/moves/whirlwind.test.ts b/test/moves/whirlwind.test.ts index 61c05a30322..ac112f01ea3 100644 --- a/test/moves/whirlwind.test.ts +++ b/test/moves/whirlwind.test.ts @@ -10,6 +10,7 @@ import { PokemonType } from "#enums/pokemon-type"; import { SpeciesId } from "#enums/species-id"; import { StatusEffect } from "#enums/status-effect"; import { TrainerType } from "#enums/trainer-type"; +import { TrainerVariant } from "#enums/trainer-variant"; import { GameManager } from "#test/test-utils/game-manager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; @@ -193,7 +194,7 @@ describe("Moves - Whirlwind", () => { .battleType(BattleType.TRAINER) .randomTrainer({ trainerType: TrainerType.BREEDER, - alwaysDouble: true, + trainerVariant: TrainerVariant.DOUBLE, }) .enemyMoveset([MoveId.SPLASH, MoveId.LUNAR_DANCE]) .moveset([MoveId.WHIRLWIND, MoveId.SPLASH]); From da766f364ccae7199c3de5fbd7e21afa0ad3b601 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Thu, 18 Sep 2025 07:52:15 -0500 Subject: [PATCH 23/40] [Tests] Cleanup `getCookie` and add many unit tests (#6562) Cleanup `getCookie` and add many unit tests --- src/utils/cookies.ts | 16 +++++----- test/utils/cookies.test.ts | 62 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 9 deletions(-) create mode 100644 test/utils/cookies.test.ts diff --git a/src/utils/cookies.ts b/src/utils/cookies.ts index e16d9d78556..407bd75da14 100644 --- a/src/utils/cookies.ts +++ b/src/utils/cookies.ts @@ -17,19 +17,17 @@ export function removeCookie(cName: string): void { export function getCookie(cName: string): string { // check if there are multiple cookies with the same name and delete them - if (document.cookie.split(";").filter(c => c.includes(cName)).length > 1) { + if (document.cookie.split(";").filter(c => c.trim().includes(cName)).length > 1) { removeCookie(cName); return ""; } const name = `${cName}=`; - const ca = document.cookie.split(";"); - for (let c of ca) { - // ⚠️ DO NOT REPLACE THIS WITH C = C.TRIM() - IT BREAKS IN NON-CHROMIUM BROWSERS ⚠️ - while (c.charAt(0) === " ") { - c = c.substring(1); - } - if (c.indexOf(name) === 0) { - return c.substring(name.length, c.length); + const cookieArray = document.cookie.split(";"); + // Check all cookies in the document and see if any of them match, grabbing the first one whose value lines up + for (const cookie of cookieArray) { + const cookieTrimmed = cookie.trim(); + if (cookieTrimmed.startsWith(name)) { + return cookieTrimmed.slice(name.length, cookieTrimmed.length); } } return ""; diff --git a/test/utils/cookies.test.ts b/test/utils/cookies.test.ts new file mode 100644 index 00000000000..a5ea248c236 --- /dev/null +++ b/test/utils/cookies.test.ts @@ -0,0 +1,62 @@ +import { getCookie } from "#utils/cookies"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +describe("Unit Tests - cookies.ts", () => { + describe("getCookie", () => { + const cookieStart = document.cookie; + beforeEach(() => { + // clear cookie before each test + document.cookie = ""; + }); + + afterEach(() => { + // restore original cookie after each test + document.cookie = cookieStart; + }); + /** + * Spies on `document.cookie` and replaces its value with the provided string. + */ + function setDocumentCookie(value: string) { + vi.spyOn(document, "cookie", "get").mockReturnValue(value); + } + it("returns the value of a single cookie", () => { + setDocumentCookie("foo=bar"); + expect(getCookie("foo")).toBe("bar"); + }); + + it("returns empty string if cookie is not found", () => { + setDocumentCookie("foo=bar"); + expect(getCookie("baz")).toBe(""); + }); + + it("returns the value when multiple cookies exist", () => { + setDocumentCookie("foo=bar; baz=qux"); + expect(getCookie("baz")).toBe("qux"); + }); + + it("trims leading spaces in cookies", () => { + setDocumentCookie("foo=bar; baz=qux"); + expect(getCookie("baz")).toBe("qux"); + }); + + it("returns the value of the first matching cookie if only one exists", () => { + setDocumentCookie("foo=bar; test=val"); + expect(getCookie("foo")).toBe("bar"); + }); + + it("returns empty string if document.cookie is empty", () => { + setDocumentCookie(""); + expect(getCookie("foo")).toBe(""); + }); + + it("handles cookies that aren't separated with a space", () => { + setDocumentCookie("foo=bar;baz=qux;quux=corge;grault=garply"); + expect(getCookie("baz")).toBe("qux"); + }); + + it("handles cookies that may have leading tab characters", () => { + setDocumentCookie("foo=bar;\tbaz=qux"); + expect(getCookie("baz")).toBe("qux"); + }); + }); +}); From 3d9e493e5f550ca933413c015bbb644398b1a8a7 Mon Sep 17 00:00:00 2001 From: Bertie690 <136088738+Bertie690@users.noreply.github.com> Date: Thu, 18 Sep 2025 10:36:08 -0400 Subject: [PATCH 24/40] [Test] Updated more uses of `game.scene.getEnemyField` and `game.scene.getPlayerField` to use updated test utils (#6524) --- test/abilities/ability-duplication.test.ts | 5 +- test/abilities/commander.test.ts | 57 +++++++++---------- test/abilities/dancer.test.ts | 4 +- test/abilities/flower-gift.test.ts | 4 +- test/abilities/flower-veil.test.ts | 2 +- test/abilities/forecast.test.ts | 2 +- test/abilities/magic-bounce.test.ts | 2 +- test/abilities/mirror-armor.test.ts | 3 +- test/abilities/no-guard.test.ts | 2 +- test/abilities/storm-drain.test.ts | 13 ++--- test/abilities/unburden.test.ts | 4 +- test/escape-calculations.test.ts | 1 + .../double-battle-chance-booster.test.ts | 4 +- test/items/grip-claw.test.ts | 4 +- test/items/multi-lens.test.ts | 2 +- test/moves/ability-ignore-moves.test.ts | 4 +- test/moves/defog.test.ts | 20 +++---- test/moves/destiny-bond.test.ts | 6 +- test/moves/dragon-tail.test.ts | 2 +- test/moves/fell-stinger.test.ts | 2 +- test/moves/instruct.test.ts | 6 +- test/moves/jaw-lock.test.ts | 9 +-- test/moves/tailwind.test.ts | 3 +- 23 files changed, 73 insertions(+), 88 deletions(-) diff --git a/test/abilities/ability-duplication.test.ts b/test/abilities/ability-duplication.test.ts index da572d94466..f684500ab90 100644 --- a/test/abilities/ability-duplication.test.ts +++ b/test/abilities/ability-duplication.test.ts @@ -30,12 +30,13 @@ describe("Ability Duplication", () => { .enemyMoveset(MoveId.SPLASH); }); + // TODO: Find a cleaner way of checking ability duplication effects than suppressing the ability it("huge power should only be applied once if both normal and passive", async () => { game.override.passiveAbility(AbilityId.HUGE_POWER); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); - const [magikarp] = game.scene.getPlayerField(); + const magikarp = game.field.getPlayerPokemon(); const magikarpAttack = magikarp.getEffectiveStat(Stat.ATK); magikarp.summonData.abilitySuppressed = true; @@ -48,7 +49,7 @@ describe("Ability Duplication", () => { await game.classicMode.startBattle([SpeciesId.MAGIKARP]); - const [magikarp] = game.scene.getPlayerField(); + const magikarp = game.field.getPlayerPokemon(); const magikarpAttack = magikarp.getEffectiveStat(Stat.ATK); magikarp.summonData.abilitySuppressed = true; diff --git a/test/abilities/commander.test.ts b/test/abilities/commander.test.ts index d485cab83a2..8447b2a7d61 100644 --- a/test/abilities/commander.test.ts +++ b/test/abilities/commander.test.ts @@ -5,8 +5,7 @@ import { MoveId } from "#enums/move-id"; import { MoveResult } from "#enums/move-result"; import { PokemonAnimType } from "#enums/pokemon-anim-type"; import { SpeciesId } from "#enums/species-id"; -import type { EffectiveStat } from "#enums/stat"; -import { Stat } from "#enums/stat"; +import { EFFECTIVE_STATS } from "#enums/stat"; import { StatusEffect } from "#enums/status-effect"; import { WeatherType } from "#enums/weather-type"; import { GameManager } from "#test/test-utils/game-manager"; @@ -48,23 +47,24 @@ describe("Abilities - Commander", () => { const [tatsugiri, dondozo] = game.scene.getPlayerField(); - const affectedStats: EffectiveStat[] = [Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD]; - expect(game.scene.triggerPokemonBattleAnim).toHaveBeenLastCalledWith(tatsugiri, PokemonAnimType.COMMANDER_APPLY); - expect(dondozo.getTag(BattlerTagType.COMMANDED)).toBeDefined(); - affectedStats.forEach(stat => expect(dondozo.getStatStage(stat)).toBe(2)); - - game.move.select(MoveId.SPLASH, 1); + expect(dondozo).toHaveBattlerTag(BattlerTagType.COMMANDED); + EFFECTIVE_STATS.forEach(stat => { + expect(dondozo).toHaveStatStage(stat, 2); + }); + game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER_2); expect(game.scene.currentBattle.turnCommands[0]?.skip).toBeTruthy(); // Force both enemies to target the Tatsugiri - await game.move.selectEnemyMove(MoveId.TACKLE, BattlerIndex.PLAYER); - await game.move.selectEnemyMove(MoveId.TACKLE, BattlerIndex.PLAYER); + await game.move.forceEnemyMove(MoveId.TACKLE, BattlerIndex.PLAYER); + await game.move.forceEnemyMove(MoveId.TACKLE, BattlerIndex.PLAYER); - await game.phaseInterceptor.to("BerryPhase", false); - game.scene.getEnemyField().forEach(enemy => expect(enemy.getLastXMoves(1)[0].result).toBe(MoveResult.MISS)); - expect(tatsugiri.isFullHp()).toBeTruthy(); + await game.toEndOfTurn(); + const [enemy1, enemy2] = game.scene.getEnemyField(); + expect(enemy1).toHaveUsedMove({ move: MoveId.TACKLE, result: MoveResult.MISS }); + expect(enemy2).toHaveUsedMove({ move: MoveId.TACKLE, result: MoveResult.MISS }); + expect(tatsugiri).toHaveFullHp(); }); it("should activate when a Dondozo switches in and cancel the source's move", async () => { @@ -72,7 +72,7 @@ describe("Abilities - Commander", () => { await game.classicMode.startBattle([SpeciesId.TATSUGIRI, SpeciesId.MAGIKARP, SpeciesId.DONDOZO]); - const tatsugiri = game.scene.getPlayerField()[0]; + const [tatsugiri, _, dondozo] = game.scene.getPlayerParty(); game.move.select(MoveId.LIQUIDATION, 0, BattlerIndex.ENEMY); game.doSwitchPokemon(2); @@ -80,12 +80,11 @@ describe("Abilities - Commander", () => { await game.phaseInterceptor.to("MovePhase", false); expect(game.scene.triggerPokemonBattleAnim).toHaveBeenCalledWith(tatsugiri, PokemonAnimType.COMMANDER_APPLY); - const dondozo = game.scene.getPlayerField()[1]; expect(dondozo.getTag(BattlerTagType.COMMANDED)).toBeDefined(); await game.phaseInterceptor.to("BerryPhase", false); expect(tatsugiri.getMoveHistory()).toHaveLength(0); - expect(game.scene.getEnemyField()[0].isFullHp()).toBeTruthy(); + expect(game.field.getEnemyPokemon()).toHaveFullHp(); }); it("source should reenter the field when Dondozo faints", async () => { @@ -192,26 +191,26 @@ describe("Abilities - Commander", () => { }); it("should interrupt the source's semi-invulnerability", async () => { - game.override.moveset([MoveId.SPLASH, MoveId.DIVE]).enemyMoveset(MoveId.SPLASH); - await game.classicMode.startBattle([SpeciesId.TATSUGIRI, SpeciesId.MAGIKARP, SpeciesId.DONDOZO]); - const tatsugiri = game.scene.getPlayerField()[0]; + const [tatsugiri, , dondozo] = game.scene.getPlayerParty(); - game.move.select(MoveId.DIVE, 0, BattlerIndex.ENEMY); - game.move.select(MoveId.SPLASH, 1); + game.move.use(MoveId.DIVE, BattlerIndex.PLAYER, BattlerIndex.ENEMY); + game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER_2); + await game.move.forceEnemyMove(MoveId.SPLASH); + await game.move.forceEnemyMove(MoveId.SPLASH); await game.toNextTurn(); - expect(tatsugiri.getTag(BattlerTagType.UNDERWATER)).toBeDefined(); + expect(tatsugiri).toHaveBattlerTag(BattlerTagType.UNDERWATER); + game.doSwitchPokemon(2); - await game.phaseInterceptor.to("MovePhase", false); - const dondozo = game.scene.getPlayerField()[1]; - expect(tatsugiri.getTag(BattlerTagType.UNDERWATER)).toBeUndefined(); - expect(dondozo.getTag(BattlerTagType.COMMANDED)).toBeDefined(); - await game.toNextTurn(); - const enemy = game.scene.getEnemyField()[0]; - expect(enemy.isFullHp()).toBeTruthy(); + expect(tatsugiri).not.toHaveBattlerTag(BattlerTagType.UNDERWATER); + expect(dondozo).toHaveBattlerTag(BattlerTagType.COMMANDED); + + await game.toEndOfTurn(); + + expect(game.field.getEnemyPokemon()).toHaveFullHp(); }); }); diff --git a/test/abilities/dancer.test.ts b/test/abilities/dancer.test.ts index c651a341c42..e640e326d58 100644 --- a/test/abilities/dancer.test.ts +++ b/test/abilities/dancer.test.ts @@ -74,8 +74,8 @@ describe("Abilities - Dancer", () => { .enemyLevel(10); await game.classicMode.startBattle([SpeciesId.ORICORIO, SpeciesId.FEEBAS]); - const [oricorio] = game.scene.getPlayerField(); - const [, shuckle2] = game.scene.getEnemyField(); + const oricorio = game.field.getPlayerPokemon(); + const shuckle2 = game.scene.getEnemyField()[1]; game.move.select(MoveId.REVELATION_DANCE, BattlerIndex.PLAYER, BattlerIndex.ENEMY_2); game.move.select(MoveId.FIERY_DANCE, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY_2); diff --git a/test/abilities/flower-gift.test.ts b/test/abilities/flower-gift.test.ts index 6d8641917aa..74be845ffed 100644 --- a/test/abilities/flower-gift.test.ts +++ b/test/abilities/flower-gift.test.ts @@ -58,12 +58,12 @@ describe("Abilities - Flower Gift", () => { const ally_target = allyAttacker ? BattlerIndex.ENEMY : null; await game.classicMode.startBattle([SpeciesId.CHERRIM, SpeciesId.MAGIKARP]); - const target = allyAttacker ? game.scene.getEnemyField()[0] : game.scene.getPlayerField()[1]; + const target = allyAttacker ? game.field.getEnemyPokemon() : game.scene.getPlayerField()[1]; const initialHp = target.getMaxHp(); // Override the ability for the target and attacker only vi.spyOn(game.scene.getPlayerField()[1], "getAbility").mockReturnValue(allAbilities[allyAbility]); - vi.spyOn(game.scene.getEnemyField()[0], "getAbility").mockReturnValue(allAbilities[enemyAbility]); + vi.spyOn(game.field.getEnemyPokemon(), "getAbility").mockReturnValue(allAbilities[enemyAbility]); // turn 1 game.move.select(MoveId.SUNNY_DAY, 0); diff --git a/test/abilities/flower-veil.test.ts b/test/abilities/flower-veil.test.ts index 44274d86a1b..ec34f696bc9 100644 --- a/test/abilities/flower-veil.test.ts +++ b/test/abilities/flower-veil.test.ts @@ -66,7 +66,7 @@ describe("Abilities - Flower Veil", () => { await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.BULBASAUR]); // Clear the ability of the ally to isolate the test - const ally = game.scene.getPlayerField()[1]!; + const ally = game.scene.getPlayerField()[1]; vi.spyOn(ally, "getAbility").mockReturnValue(allAbilities[AbilityId.BALL_FETCH]); game.move.select(MoveId.SPLASH); game.move.select(MoveId.SPLASH); diff --git a/test/abilities/forecast.test.ts b/test/abilities/forecast.test.ts index 87d1d20acdb..9bd40709a94 100644 --- a/test/abilities/forecast.test.ts +++ b/test/abilities/forecast.test.ts @@ -76,7 +76,7 @@ describe("Abilities - Forecast", () => { vi.spyOn(game.scene.getPlayerParty()[5], "getAbility").mockReturnValue(allAbilities[AbilityId.CLOUD_NINE]); - const castform = game.scene.getPlayerField()[0]; + const castform = game.field.getPlayerPokemon(); expect(castform.formIndex).toBe(NORMAL_FORM); game.move.select(MoveId.RAIN_DANCE); diff --git a/test/abilities/magic-bounce.test.ts b/test/abilities/magic-bounce.test.ts index c15690c3f5d..6b7bc7453ed 100644 --- a/test/abilities/magic-bounce.test.ts +++ b/test/abilities/magic-bounce.test.ts @@ -64,7 +64,7 @@ describe("Abilities - Magic Bounce", () => { game.move.use(MoveId.SPLASH, 1); await game.phaseInterceptor.to("BerryPhase"); - const user = game.scene.getPlayerField()[0]; + const user = game.field.getPlayerPokemon(); expect(user.getStatStage(Stat.ATK)).toBe(-2); }); diff --git a/test/abilities/mirror-armor.test.ts b/test/abilities/mirror-armor.test.ts index b2bd9be4755..85d821d0683 100644 --- a/test/abilities/mirror-armor.test.ts +++ b/test/abilities/mirror-armor.test.ts @@ -92,8 +92,7 @@ describe("Ability - Mirror Armor", () => { game.override.battleStyle("double").enemyAbility(AbilityId.MIRROR_ARMOR).ability(AbilityId.INTIMIDATE); await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER]); - const [enemy1, enemy2] = game.scene.getEnemyField(); - const [player1, player2] = game.scene.getPlayerField(); + const [player1, player2, enemy1, enemy2] = game.scene.getField(); // Enemy has intimidate, enemy should lose -1 atk game.move.select(MoveId.SPLASH); diff --git a/test/abilities/no-guard.test.ts b/test/abilities/no-guard.test.ts index 9ce12e710e5..9fc308ab9e3 100644 --- a/test/abilities/no-guard.test.ts +++ b/test/abilities/no-guard.test.ts @@ -58,6 +58,6 @@ describe("Abilities - No Guard", () => { await game.classicMode.startBattle(); - expect(game.scene.getEnemyField().length).toBe(2); + expect(game.scene.getEnemyField()).toHaveLength(2); }); }); diff --git a/test/abilities/storm-drain.test.ts b/test/abilities/storm-drain.test.ts index bc4d4f15cfa..5439459b1dd 100644 --- a/test/abilities/storm-drain.test.ts +++ b/test/abilities/storm-drain.test.ts @@ -37,9 +37,7 @@ describe("Abilities - Storm Drain", () => { it("should redirect water type moves", async () => { await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MAGIKARP]); - const enemy1 = game.scene.getEnemyField()[0]; - const enemy2 = game.scene.getEnemyField()[1]; - + const [enemy1, enemy2] = game.scene.getEnemyField(); game.field.mockAbility(enemy2, AbilityId.STORM_DRAIN); game.move.select(MoveId.WATER_GUN, BattlerIndex.PLAYER, BattlerIndex.ENEMY); @@ -53,8 +51,7 @@ describe("Abilities - Storm Drain", () => { game.override.moveset([MoveId.SPLASH, MoveId.AERIAL_ACE]); await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MAGIKARP]); - const enemy1 = game.scene.getEnemyField()[0]; - const enemy2 = game.scene.getEnemyField()[1]; + const [enemy1, enemy2] = game.scene.getEnemyField(); game.field.mockAbility(enemy2, AbilityId.STORM_DRAIN); @@ -83,8 +80,7 @@ describe("Abilities - Storm Drain", () => { game.override.ability(AbilityId.NORMALIZE); await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MAGIKARP]); - const enemy1 = game.scene.getEnemyField()[0]; - const enemy2 = game.scene.getEnemyField()[1]; + const [enemy1, enemy2] = game.scene.getEnemyField(); game.field.mockAbility(enemy2, AbilityId.STORM_DRAIN); game.move.select(MoveId.WATER_GUN, BattlerIndex.PLAYER, BattlerIndex.ENEMY); @@ -98,8 +94,7 @@ describe("Abilities - Storm Drain", () => { game.override.ability(AbilityId.LIQUID_VOICE); await game.classicMode.startBattle([SpeciesId.FEEBAS]); - const enemy1 = game.scene.getEnemyField()[0]; - const enemy2 = game.scene.getEnemyField()[1]; + const [enemy1, enemy2] = game.scene.getEnemyField(); game.field.mockAbility(enemy2, AbilityId.STORM_DRAIN); diff --git a/test/abilities/unburden.test.ts b/test/abilities/unburden.test.ts index c10dd404ab9..285ea8af32c 100644 --- a/test/abilities/unburden.test.ts +++ b/test/abilities/unburden.test.ts @@ -362,7 +362,7 @@ describe("Abilities - Unburden", () => { .startingHeldItems([{ name: "WIDE_LENS" }]); await game.classicMode.startBattle([SpeciesId.TREECKO, SpeciesId.FEEBAS, SpeciesId.MILOTIC]); - const treecko = game.scene.getPlayerField()[0]; + const treecko = game.field.getPlayerPokemon(); const treeckoInitialHeldItems = getHeldItemCount(treecko); const initialSpeed = treecko.getStat(Stat.SPD); @@ -374,7 +374,7 @@ describe("Abilities - Unburden", () => { game.doSelectPartyPokemon(0, "RevivalBlessingPhase"); await game.toNextTurn(); - expect(game.scene.getPlayerField()[0]).toBe(treecko); + expect(game.field.getPlayerPokemon()).toBe(treecko); expect(getHeldItemCount(treecko)).toBeLessThan(treeckoInitialHeldItems); expect(treecko.getEffectiveStat(Stat.SPD)).toBe(initialSpeed); }); diff --git a/test/escape-calculations.test.ts b/test/escape-calculations.test.ts index fb677e81a45..e1e521f4394 100644 --- a/test/escape-calculations.test.ts +++ b/test/escape-calculations.test.ts @@ -7,6 +7,7 @@ import { GameManager } from "#test/test-utils/game-manager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +// TODO: These tests are stupid and need to be redone describe("Escape chance calculations", () => { let phaserGame: Phaser.Game; let game: GameManager; diff --git a/test/items/double-battle-chance-booster.test.ts b/test/items/double-battle-chance-booster.test.ts index 2c12b34eba3..ea3c400edb7 100644 --- a/test/items/double-battle-chance-booster.test.ts +++ b/test/items/double-battle-chance-booster.test.ts @@ -31,7 +31,7 @@ describe("Items - Double Battle Chance Boosters", () => { await game.classicMode.startBattle(); - expect(game.scene.getEnemyField().length).toBe(2); + expect(game.scene.getEnemyField()).toHaveLength(2); }); it("should guarantee double boss battle with 3 unique tiers", async () => { @@ -41,7 +41,7 @@ describe("Items - Double Battle Chance Boosters", () => { const enemyField = game.scene.getEnemyField(); - expect(enemyField.length).toBe(2); + expect(enemyField).toHaveLength(2); expect(enemyField[0].isBoss()).toBe(true); expect(enemyField[1].isBoss()).toBe(true); }); diff --git a/test/items/grip-claw.test.ts b/test/items/grip-claw.test.ts index 5ffebd76946..54a40942beb 100644 --- a/test/items/grip-claw.test.ts +++ b/test/items/grip-claw.test.ts @@ -44,7 +44,7 @@ describe("Items - Grip Claw", () => { it("should steal items on contact and only from the attack target", async () => { await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MILOTIC]); - const [playerPokemon] = game.scene.getPlayerField(); + const playerPokemon = game.field.getPlayerPokemon(); const gripClaw = playerPokemon.getHeldItems()[0] as ContactHeldItemTransferChanceModifier; vi.spyOn(gripClaw, "chance", "get").mockReturnValue(100); @@ -73,7 +73,7 @@ describe("Items - Grip Claw", () => { it("should not steal items when using a targetted, non attack move", async () => { await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MILOTIC]); - const [playerPokemon] = game.scene.getPlayerField(); + const playerPokemon = game.field.getPlayerPokemon(); const gripClaw = playerPokemon.getHeldItems()[0] as ContactHeldItemTransferChanceModifier; vi.spyOn(gripClaw, "chance", "get").mockReturnValue(100); diff --git a/test/items/multi-lens.test.ts b/test/items/multi-lens.test.ts index b69a07033c9..3686aff0fcf 100644 --- a/test/items/multi-lens.test.ts +++ b/test/items/multi-lens.test.ts @@ -103,7 +103,7 @@ describe("Items - Multi Lens", () => { await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.FEEBAS]); - const [magikarp] = game.scene.getPlayerField(); + const magikarp = game.field.getPlayerPokemon(); game.move.select(MoveId.SWIFT, 0); game.move.select(MoveId.SPLASH, 1); diff --git a/test/moves/ability-ignore-moves.test.ts b/test/moves/ability-ignore-moves.test.ts index e3a7c7db12f..089af242f87 100644 --- a/test/moves/ability-ignore-moves.test.ts +++ b/test/moves/ability-ignore-moves.test.ts @@ -102,7 +102,7 @@ describe("Moves - Ability-Ignoring Moves", () => { // Both the initial and redirected instruct use ignored sturdy const [enemy1, enemy2] = game.scene.getEnemyField(); - expect(enemy1.isFainted()).toBe(true); - expect(enemy2.isFainted()).toBe(true); + expect(enemy1).toHaveFainted(); + expect(enemy2).toHaveFainted(); }); }); diff --git a/test/moves/defog.test.ts b/test/moves/defog.test.ts index 820dfaa6bcb..4ddb397ee71 100644 --- a/test/moves/defog.test.ts +++ b/test/moves/defog.test.ts @@ -1,4 +1,5 @@ import { AbilityId } from "#enums/ability-id"; +import { ArenaTagType } from "#enums/arena-tag-type"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { Stat } from "#enums/stat"; @@ -32,26 +33,21 @@ describe("Moves - Defog", () => { .enemyMoveset([MoveId.DEFOG, MoveId.GROWL]); }); + // TODO: Refactor these tests they suck ass it("should not allow Safeguard to be active", async () => { await game.classicMode.startBattle([SpeciesId.REGIELEKI]); - const playerPokemon = game.scene.getPlayerField(); - const enemyPokemon = game.scene.getEnemyField(); + game.scene.arena.addTag(ArenaTagType.SAFEGUARD, 0, 0, 0); - game.move.select(MoveId.SAFEGUARD); - await game.move.selectEnemyMove(MoveId.DEFOG); - await game.phaseInterceptor.to("BerryPhase"); + game.move.use(MoveId.DEFOG); + await game.toEndOfTurn(); - expect(playerPokemon[0].isSafeguarded(enemyPokemon[0])).toBe(false); - - expect(true).toBe(true); + expect(game).not.toHaveArenaTag(ArenaTagType.SAFEGUARD); }); it("should not allow Mist to be active", async () => { await game.classicMode.startBattle([SpeciesId.REGIELEKI]); - const playerPokemon = game.scene.getPlayerField(); - game.move.select(MoveId.MIST); await game.move.selectEnemyMove(MoveId.DEFOG); @@ -62,8 +58,6 @@ describe("Moves - Defog", () => { await game.phaseInterceptor.to("BerryPhase"); - expect(playerPokemon[0].getStatStage(Stat.ATK)).toBe(-1); - - expect(true).toBe(true); + expect(game.field.getPlayerPokemon()).toHaveStatStage(Stat.ATK, -1); }); }); diff --git a/test/moves/destiny-bond.test.ts b/test/moves/destiny-bond.test.ts index 118a45e7682..a5020b83944 100644 --- a/test/moves/destiny-bond.test.ts +++ b/test/moves/destiny-bond.test.ts @@ -160,11 +160,7 @@ describe("Moves - Destiny Bond", () => { game.override.moveset([MoveId.DESTINY_BOND, MoveId.CRUNCH]).battleStyle("double"); await game.classicMode.startBattle([SpeciesId.SHEDINJA, SpeciesId.BULBASAUR, SpeciesId.SQUIRTLE]); - const enemyPokemon0 = game.scene.getEnemyField()[0]; - const enemyPokemon1 = game.scene.getEnemyField()[1]; - const playerPokemon0 = game.scene.getPlayerField()[0]; - const playerPokemon1 = game.scene.getPlayerField()[1]; - + const [playerPokemon0, playerPokemon1, enemyPokemon0, enemyPokemon1] = game.scene.getField(); // Shedinja uses Destiny Bond, then ally Bulbasaur KO's Shedinja with Crunch game.move.select(MoveId.DESTINY_BOND, 0); game.move.select(MoveId.CRUNCH, 1, BattlerIndex.PLAYER); diff --git a/test/moves/dragon-tail.test.ts b/test/moves/dragon-tail.test.ts index e3a5bf459e8..28266465523 100644 --- a/test/moves/dragon-tail.test.ts +++ b/test/moves/dragon-tail.test.ts @@ -171,7 +171,7 @@ describe("Moves - Dragon Tail", () => { const enemy = game.field.getEnemyPokemon(); expect(enemy).toBeDefined(); expect(enemy.hp).toBe(Math.floor(enemy.getMaxHp() / 2)); - expect(game.scene.getEnemyField().length).toBe(1); + expect(game.scene.getEnemyField()).toHaveLength(1); }); it("should not cause a softlock when activating a player's reviver seed", async () => { diff --git a/test/moves/fell-stinger.test.ts b/test/moves/fell-stinger.test.ts index ede70b7af9b..4550cdffa12 100644 --- a/test/moves/fell-stinger.test.ts +++ b/test/moves/fell-stinger.test.ts @@ -107,7 +107,7 @@ describe("Moves - Fell Stinger", () => { await game.classicMode.startBattle([SpeciesId.LEAVANNY]); const leadPokemon = game.field.getPlayerPokemon(); - const leftEnemy = game.scene.getEnemyField()[0]!; + const leftEnemy = game.field.getEnemyPokemon(); // Turn 1: set Salt Cure, enemy splashes and does nothing game.move.select(MoveId.SALT_CURE, 0, leftEnemy.getBattlerIndex()); diff --git a/test/moves/instruct.test.ts b/test/moves/instruct.test.ts index 27318105783..eb3eccff400 100644 --- a/test/moves/instruct.test.ts +++ b/test/moves/instruct.test.ts @@ -498,7 +498,7 @@ describe("Moves - Instruct", () => { .enemyLevel(1); await game.classicMode.startBattle([SpeciesId.KORAIDON, SpeciesId.KLEFKI]); - const koraidon = game.scene.getPlayerField()[0]!; + const koraidon = game.field.getPlayerPokemon(); game.move.select(MoveId.BREAKING_SWIPE); await game.phaseInterceptor.to("TurnEndPhase", false); @@ -527,7 +527,7 @@ describe("Moves - Instruct", () => { .enemyLevel(1); await game.classicMode.startBattle([SpeciesId.KORAIDON, SpeciesId.KLEFKI]); - const koraidon = game.scene.getPlayerField()[0]!; + const koraidon = game.field.getPlayerPokemon(); game.move.select(MoveId.BRUTAL_SWING); await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); @@ -587,7 +587,7 @@ describe("Moves - Instruct", () => { .enemyLevel(5); await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.IVYSAUR]); - const [, ivysaur] = game.scene.getPlayerField(); + const ivysaur = game.scene.getPlayerField()[1]; game.move.select(MoveId.SPLASH, BattlerIndex.PLAYER); game.move.select(MoveId.SPLASH, BattlerIndex.PLAYER_2); diff --git a/test/moves/jaw-lock.test.ts b/test/moves/jaw-lock.test.ts index 441c74c7356..0ea2c0bd8bf 100644 --- a/test/moves/jaw-lock.test.ts +++ b/test/moves/jaw-lock.test.ts @@ -111,7 +111,8 @@ describe("Moves - Jaw Lock", () => { await game.classicMode.startBattle([SpeciesId.CHARMANDER, SpeciesId.BULBASAUR]); - const playerPokemon = game.scene.getPlayerField(); + const playerPokemon = game.field.getPlayerPokemon(); + const enemyPokemon = game.scene.getEnemyField(); game.move.select(MoveId.JAW_LOCK, 0, BattlerIndex.ENEMY); @@ -120,7 +121,7 @@ describe("Moves - Jaw Lock", () => { await game.phaseInterceptor.to(MoveEffectPhase); - expect(playerPokemon[0].getTag(BattlerTagType.TRAPPED)).toBeDefined(); + expect(playerPokemon.getTag(BattlerTagType.TRAPPED)).toBeDefined(); expect(enemyPokemon[0].getTag(BattlerTagType.TRAPPED)).toBeDefined(); await game.toNextTurn(); @@ -131,8 +132,8 @@ describe("Moves - Jaw Lock", () => { await game.phaseInterceptor.to(MoveEffectPhase); expect(enemyPokemon[1].getTag(BattlerTagType.TRAPPED)).toBeUndefined(); - expect(playerPokemon[0].getTag(BattlerTagType.TRAPPED)).toBeDefined(); - expect(playerPokemon[0].getTag(BattlerTagType.TRAPPED)?.sourceId).toBe(enemyPokemon[0].id); + expect(playerPokemon.getTag(BattlerTagType.TRAPPED)).toBeDefined(); + expect(playerPokemon.getTag(BattlerTagType.TRAPPED)?.sourceId).toBe(enemyPokemon[0].id); }); it("should not trap either pokemon if the target is protected", async () => { diff --git a/test/moves/tailwind.test.ts b/test/moves/tailwind.test.ts index 5c91a37f786..d9a0bdeb5f1 100644 --- a/test/moves/tailwind.test.ts +++ b/test/moves/tailwind.test.ts @@ -34,8 +34,7 @@ describe("Moves - Tailwind", () => { it("doubles the Speed stat of the Pokemons on its side", async () => { await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MEOWTH]); - const magikarp = game.scene.getPlayerField()[0]; - const meowth = game.scene.getPlayerField()[1]; + const [magikarp, meowth] = game.scene.getPlayerField(); const magikarpSpd = magikarp.getStat(Stat.SPD); const meowthSpd = meowth.getStat(Stat.SPD); From 0e87391b20c86b596a49e76415264099eeb98950 Mon Sep 17 00:00:00 2001 From: Madmadness65 <59298170+Madmadness65@users.noreply.github.com> Date: Thu, 18 Sep 2025 14:13:52 -0500 Subject: [PATCH 25/40] [Misc] Update biome pools in init functions (#6572) Added new biome entries for Type: Null, Silvally, Poipole, Naganadel, Kubfu, Urshifu, Scientist, and Swimmer to their respective init functions, and reran `outputPools`. --- src/data/balance/biomes.ts | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/data/balance/biomes.ts b/src/data/balance/biomes.ts index b253b0ded6e..9af2dbe221c 100644 --- a/src/data/balance/biomes.ts +++ b/src/data/balance/biomes.ts @@ -1119,7 +1119,7 @@ export const biomePokemonPools: BiomePokemonPools = { }, [BiomePoolTier.RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.HITMONLEE, SpeciesId.HITMONCHAN, SpeciesId.LUCARIO, SpeciesId.THROH, SpeciesId.SAWK, { 1: [ SpeciesId.PANCHAM ], 52: [ SpeciesId.PANGORO ] } ] }, [BiomePoolTier.SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.HITMONTOP, SpeciesId.GALLADE, SpeciesId.GALAR_FARFETCHD ] }, - [BiomePoolTier.ULTRA_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.TERRAKION, { 1: [ SpeciesId.KUBFU ], 60: [ SpeciesId.URSHIFU] }, SpeciesId.GALAR_ZAPDOS ] }, + [BiomePoolTier.ULTRA_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.TERRAKION, { 1: [ SpeciesId.KUBFU ], 60: [ SpeciesId.URSHIFU ] }, SpeciesId.GALAR_ZAPDOS ] }, [BiomePoolTier.BOSS]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], @@ -1128,7 +1128,7 @@ export const biomePokemonPools: BiomePokemonPools = { [TimeOfDay.ALL]: [ SpeciesId.HITMONLEE, SpeciesId.HITMONCHAN, SpeciesId.HARIYAMA, SpeciesId.MEDICHAM, SpeciesId.LUCARIO, SpeciesId.TOXICROAK, SpeciesId.THROH, SpeciesId.SAWK, SpeciesId.SCRAFTY, SpeciesId.MIENSHAO, SpeciesId.BEWEAR, SpeciesId.GRAPPLOCT, SpeciesId.ANNIHILAPE ] }, [BiomePoolTier.BOSS_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.HITMONTOP, SpeciesId.GALLADE, SpeciesId.PANGORO, SpeciesId.SIRFETCHD, SpeciesId.HISUI_DECIDUEYE ] }, - [BiomePoolTier.BOSS_SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.TERRAKION, { 1: [ SpeciesId.KUBFU ], 60: [ SpeciesId.URSHIFU] } ] }, + [BiomePoolTier.BOSS_SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.TERRAKION, { 1: [ SpeciesId.KUBFU ], 60: [ SpeciesId.URSHIFU ] } ] }, [BiomePoolTier.BOSS_ULTRA_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.ZAMAZENTA, SpeciesId.GALAR_ZAPDOS ] } }, [BiomeId.FACTORY]: { @@ -1597,10 +1597,10 @@ export const biomePokemonPools: BiomePokemonPools = { [BiomePoolTier.UNCOMMON]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ { 1: [ SpeciesId.SOLOSIS ], 32: [ SpeciesId.DUOSION ], 41: [ SpeciesId.REUNICLUS ] } ] }, [BiomePoolTier.RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.DITTO, { 1: [ SpeciesId.PORYGON ], 30: [ SpeciesId.PORYGON2 ] } ] }, [BiomePoolTier.SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.ROTOM ] }, - [BiomePoolTier.ULTRA_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ { 1: [SpeciesId.TYPE_NULL], 60: [ SpeciesId.SILVALLY ] } ] }, + [BiomePoolTier.ULTRA_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ { 1: [ SpeciesId.TYPE_NULL ], 60: [ SpeciesId.SILVALLY ] } ] }, [BiomePoolTier.BOSS]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.MUK, SpeciesId.ELECTRODE, SpeciesId.BRONZONG, SpeciesId.MAGNEZONE, SpeciesId.PORYGON_Z, SpeciesId.REUNICLUS, SpeciesId.KLINKLANG ] }, [BiomePoolTier.BOSS_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [] }, - [BiomePoolTier.BOSS_SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.ROTOM, SpeciesId.ZYGARDE, { 1: [SpeciesId.TYPE_NULL], 60: [ SpeciesId.SILVALLY ] } ] }, + [BiomePoolTier.BOSS_SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.ROTOM, SpeciesId.ZYGARDE, { 1: [ SpeciesId.TYPE_NULL ], 60: [ SpeciesId.SILVALLY ] } ] }, [BiomePoolTier.BOSS_ULTRA_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ SpeciesId.MEWTWO, SpeciesId.MIRAIDON ] } }, [BiomeId.END]: { @@ -5627,10 +5627,12 @@ export function initBiomes() { ] ], [ SpeciesId.TYPE_NULL, PokemonType.NORMAL, -1, [ - [ BiomeId.LABORATORY, BiomePoolTier.ULTRA_RARE ] + [ BiomeId.LABORATORY, BiomePoolTier.ULTRA_RARE ], + [ BiomeId.LABORATORY, BiomePoolTier.BOSS_SUPER_RARE ] ] ], [ SpeciesId.SILVALLY, PokemonType.NORMAL, -1, [ + [ BiomeId.LABORATORY, BiomePoolTier.ULTRA_RARE ], [ BiomeId.LABORATORY, BiomePoolTier.BOSS_SUPER_RARE ] ] ], @@ -5773,10 +5775,12 @@ export function initBiomes() { ] ], [ SpeciesId.POIPOLE, PokemonType.POISON, -1, [ - [ BiomeId.SWAMP, BiomePoolTier.ULTRA_RARE ] + [ BiomeId.SWAMP, BiomePoolTier.ULTRA_RARE ], + [ BiomeId.SWAMP, BiomePoolTier.BOSS_SUPER_RARE ] ] ], [ SpeciesId.NAGANADEL, PokemonType.POISON, PokemonType.DRAGON, [ + [ BiomeId.SWAMP, BiomePoolTier.ULTRA_RARE ], [ BiomeId.SWAMP, BiomePoolTier.BOSS_SUPER_RARE ] ] ], @@ -6165,10 +6169,12 @@ export function initBiomes() { ] ], [ SpeciesId.KUBFU, PokemonType.FIGHTING, -1, [ - [ BiomeId.DOJO, BiomePoolTier.ULTRA_RARE ] + [ BiomeId.DOJO, BiomePoolTier.ULTRA_RARE ], + [ BiomeId.DOJO, BiomePoolTier.BOSS_SUPER_RARE ] ] ], [ SpeciesId.URSHIFU, PokemonType.FIGHTING, PokemonType.DARK, [ + [ BiomeId.DOJO, BiomePoolTier.ULTRA_RARE ], [ BiomeId.DOJO, BiomePoolTier.BOSS_SUPER_RARE ] ] ], @@ -7209,7 +7215,8 @@ export function initBiomes() { ], [ TrainerType.SCIENTIST, [ [ BiomeId.DESERT, BiomePoolTier.COMMON ], - [ BiomeId.RUINS, BiomePoolTier.COMMON ] + [ BiomeId.RUINS, BiomePoolTier.COMMON ], + [ BiomeId.LABORATORY, BiomePoolTier.COMMON ] ] ], [ TrainerType.SMASHER, []], @@ -7224,7 +7231,8 @@ export function initBiomes() { ] ], [ TrainerType.SWIMMER, [ - [ BiomeId.SEA, BiomePoolTier.COMMON ] + [ BiomeId.SEA, BiomePoolTier.COMMON ], + [ BiomeId.SEABED, BiomePoolTier.COMMON ] ] ], [ TrainerType.TWINS, [ @@ -7590,11 +7598,13 @@ export function initBiomes() { [ TrainerType.ALDER, []], [ TrainerType.IRIS, []], [ TrainerType.DIANTHA, []], + [ TrainerType.KUKUI, []], [ TrainerType.HAU, []], + [ TrainerType.LEON, []], + [ TrainerType.MUSTARD, []], [ TrainerType.GEETA, []], [ TrainerType.NEMONA, []], [ TrainerType.KIERAN, []], - [ TrainerType.LEON, []], [ TrainerType.RIVAL, []] ]; From 7d83a3a24a83b5d3dbebbe46e977c4229d70bf23 Mon Sep 17 00:00:00 2001 From: Fabi <192151969+fabske0@users.noreply.github.com> Date: Thu, 18 Sep 2025 22:35:22 +0200 Subject: [PATCH 26/40] [UI/UX] Show correct max duration in flyout (#6566) * Fix terrain & weather max duration flyout * show correct max duration for tags * maka maxDuration optional in arenaEvent constructor * Apply suggestions from code review Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/data/arena-tag.ts | 7 +++++++ src/data/terrain.ts | 6 ++++-- src/data/weather.ts | 6 ++++-- src/events/arena.ts | 16 ++++++++++------ src/field/arena.ts | 10 +++++----- src/system/arena-data.ts | 8 ++++++-- src/system/game-data.ts | 10 +++++++--- src/ui/containers/arena-flyout.ts | 4 ++-- 8 files changed, 45 insertions(+), 22 deletions(-) diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index ff939194bcd..7d78076e06b 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -84,6 +84,10 @@ interface BaseArenaTag { * The tag's remaining duration. Setting to any number `<=0` will make the tag's duration effectively infinite. */ turnCount: number; + /** + * The tag's max duration. + */ + maxDuration: number; /** * The {@linkcode MoveId} that created this tag, or `undefined` if not set by a move. */ @@ -110,12 +114,14 @@ export abstract class ArenaTag implements BaseArenaTag { /** The type of the arena tag */ public abstract readonly tagType: ArenaTagType; public turnCount: number; + public maxDuration: number; public sourceMove?: MoveId; public sourceId: number | undefined; public side: ArenaTagSide; constructor(turnCount: number, sourceMove?: MoveId, sourceId?: number, side: ArenaTagSide = ArenaTagSide.BOTH) { this.turnCount = turnCount; + this.maxDuration = turnCount; this.sourceMove = sourceMove; this.sourceId = sourceId; this.side = side; @@ -164,6 +170,7 @@ export abstract class ArenaTag implements BaseArenaTag { */ loadTag(source: BaseArenaTag & Pick): void { this.turnCount = source.turnCount; + this.maxDuration = source.maxDuration; this.sourceMove = source.sourceMove; this.sourceId = source.sourceId; this.side = source.side; diff --git a/src/data/terrain.ts b/src/data/terrain.ts index 139230605bf..315ed919e03 100644 --- a/src/data/terrain.ts +++ b/src/data/terrain.ts @@ -22,10 +22,12 @@ export interface SerializedTerrain { export class Terrain { public terrainType: TerrainType; public turnsLeft: number; + public maxDuration: number; - constructor(terrainType: TerrainType, turnsLeft?: number) { + constructor(terrainType: TerrainType, turnsLeft = 0, maxDuration: number = turnsLeft) { this.terrainType = terrainType; - this.turnsLeft = turnsLeft || 0; + this.turnsLeft = turnsLeft; + this.maxDuration = maxDuration; } lapse(): boolean { diff --git a/src/data/weather.ts b/src/data/weather.ts index 84a5e1ba4f8..49af505dc62 100644 --- a/src/data/weather.ts +++ b/src/data/weather.ts @@ -19,10 +19,12 @@ export interface SerializedWeather { export class Weather { public weatherType: WeatherType; public turnsLeft: number; + public maxDuration: number; - constructor(weatherType: WeatherType, turnsLeft?: number) { + constructor(weatherType: WeatherType, turnsLeft = 0, maxDuration: number = turnsLeft) { this.weatherType = weatherType; - this.turnsLeft = !this.isImmutable() ? turnsLeft || 0 : 0; + this.turnsLeft = this.isImmutable() ? 0 : turnsLeft; + this.maxDuration = this.isImmutable() ? 0 : maxDuration; } lapse(): boolean { diff --git a/src/events/arena.ts b/src/events/arena.ts index cf287de3176..9f818a36c89 100644 --- a/src/events/arena.ts +++ b/src/events/arena.ts @@ -20,10 +20,13 @@ export enum ArenaEventType { export class ArenaEvent extends Event { /** The total duration of the {@linkcode ArenaEventType} */ public duration: number; - constructor(eventType: ArenaEventType, duration: number) { + /** The maximum duration of the {@linkcode ArenaEventType} */ + public maxDuration: number; + constructor(eventType: ArenaEventType, duration: number, maxDuration: number = duration) { super(eventType); this.duration = duration; + this.maxDuration = maxDuration; } } /** Container class for {@linkcode ArenaEventType.WEATHER_CHANGED} events */ @@ -32,8 +35,8 @@ export class WeatherChangedEvent extends ArenaEvent { public oldWeatherType: WeatherType; /** The {@linkcode WeatherType} being set */ public newWeatherType: WeatherType; - constructor(oldWeatherType: WeatherType, newWeatherType: WeatherType, duration: number) { - super(ArenaEventType.WEATHER_CHANGED, duration); + constructor(oldWeatherType: WeatherType, newWeatherType: WeatherType, duration: number, maxDuration?: number) { + super(ArenaEventType.WEATHER_CHANGED, duration, maxDuration); this.oldWeatherType = oldWeatherType; this.newWeatherType = newWeatherType; @@ -45,8 +48,8 @@ export class TerrainChangedEvent extends ArenaEvent { public oldTerrainType: TerrainType; /** The {@linkcode TerrainType} being set */ public newTerrainType: TerrainType; - constructor(oldTerrainType: TerrainType, newTerrainType: TerrainType, duration: number) { - super(ArenaEventType.TERRAIN_CHANGED, duration); + constructor(oldTerrainType: TerrainType, newTerrainType: TerrainType, duration: number, maxDuration?: number) { + super(ArenaEventType.TERRAIN_CHANGED, duration, maxDuration); this.oldTerrainType = oldTerrainType; this.newTerrainType = newTerrainType; @@ -68,10 +71,11 @@ export class TagAddedEvent extends ArenaEvent { arenaTagType: ArenaTagType, arenaTagSide: ArenaTagSide, duration: number, + maxDuration?: number, arenaTagLayers?: number, arenaTagMaxLayers?: number, ) { - super(ArenaEventType.TAG_ADDED, duration); + super(ArenaEventType.TAG_ADDED, duration, maxDuration); this.arenaTagType = arenaTagType; this.arenaTagSide = arenaTagSide; diff --git a/src/field/arena.ts b/src/field/arena.ts index c4708be1336..5ab50e540ee 100644 --- a/src/field/arena.ts +++ b/src/field/arena.ts @@ -344,7 +344,7 @@ export class Arena { globalScene.applyModifier(FieldEffectModifier, user.isPlayer(), user, weatherDuration); } - this.weather = weather ? new Weather(weather, weatherDuration.value) : null; + this.weather = weather ? new Weather(weather, weatherDuration.value, weatherDuration.value) : null; this.eventTarget.dispatchEvent( new WeatherChangedEvent(oldWeatherType, this.weather?.weatherType!, this.weather?.turnsLeft!), ); // TODO: is this bang correct? @@ -425,7 +425,7 @@ export class Arena { globalScene.applyModifier(FieldEffectModifier, user.isPlayer(), user, terrainDuration); } - this.terrain = terrain ? new Terrain(terrain, terrainDuration.value) : null; + this.terrain = terrain ? new Terrain(terrain, terrainDuration.value, terrainDuration.value) : null; this.eventTarget.dispatchEvent( new TerrainChangedEvent(oldTerrainType, this.terrain?.terrainType!, this.terrain?.turnsLeft!), @@ -705,8 +705,8 @@ export class Arena { existingTag.onOverlap(this, globalScene.getPokemonById(sourceId)); if (existingTag instanceof EntryHazardTag) { - const { tagType, side, turnCount, layers, maxLayers } = existingTag as EntryHazardTag; - this.eventTarget.dispatchEvent(new TagAddedEvent(tagType, side, turnCount, layers, maxLayers)); + const { tagType, side, turnCount, maxDuration, layers, maxLayers } = existingTag as EntryHazardTag; + this.eventTarget.dispatchEvent(new TagAddedEvent(tagType, side, turnCount, maxDuration, layers, maxLayers)); } return false; @@ -721,7 +721,7 @@ export class Arena { const { layers = 0, maxLayers = 0 } = newTag instanceof EntryHazardTag ? newTag : {}; this.eventTarget.dispatchEvent( - new TagAddedEvent(newTag.tagType, newTag.side, newTag.turnCount, layers, maxLayers), + new TagAddedEvent(newTag.tagType, newTag.side, newTag.turnCount, newTag.maxDuration, layers, maxLayers), ); } diff --git a/src/system/arena-data.ts b/src/system/arena-data.ts index 18620e15223..0d40a9c6234 100644 --- a/src/system/arena-data.ts +++ b/src/system/arena-data.ts @@ -47,8 +47,12 @@ export class ArenaData { } this.biome = source.biome; - this.weather = source.weather ? new Weather(source.weather.weatherType, source.weather.turnsLeft) : null; - this.terrain = source.terrain ? new Terrain(source.terrain.terrainType, source.terrain.turnsLeft) : null; + this.weather = source.weather + ? new Weather(source.weather.weatherType, source.weather.turnsLeft, source.weather.maxDuration) + : null; + this.terrain = source.terrain + ? new Terrain(source.terrain.terrainType, source.terrain.turnsLeft, source.terrain.maxDuration) + : null; this.positionalTags = source.positionalTags ?? []; } } diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 8c2a1219245..3ffa7482706 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -1021,6 +1021,7 @@ export class GameData { WeatherType.NONE, globalScene.arena.weather?.weatherType!, globalScene.arena.weather?.turnsLeft!, + globalScene.arena.weather?.maxDuration!, ), ); // TODO: is this bang correct? @@ -1030,6 +1031,7 @@ export class GameData { TerrainType.NONE, globalScene.arena.terrain?.terrainType!, globalScene.arena.terrain?.turnsLeft!, + globalScene.arena.terrain?.maxDuration!, ), ); // TODO: is this bang correct? @@ -1039,12 +1041,14 @@ export class GameData { if (globalScene.arena.tags) { for (const tag of globalScene.arena.tags) { if (tag instanceof EntryHazardTag) { - const { tagType, side, turnCount, layers, maxLayers } = tag as EntryHazardTag; + const { tagType, side, turnCount, maxDuration, layers, maxLayers } = tag as EntryHazardTag; globalScene.arena.eventTarget.dispatchEvent( - new TagAddedEvent(tagType, side, turnCount, layers, maxLayers), + new TagAddedEvent(tagType, side, turnCount, maxDuration, layers, maxLayers), ); } else { - globalScene.arena.eventTarget.dispatchEvent(new TagAddedEvent(tag.tagType, tag.side, tag.turnCount)); + globalScene.arena.eventTarget.dispatchEvent( + new TagAddedEvent(tag.tagType, tag.side, tag.turnCount, tag.maxDuration), + ); } } } diff --git a/src/ui/containers/arena-flyout.ts b/src/ui/containers/arena-flyout.ts index a73846de1ac..ab95d1a3e7a 100644 --- a/src/ui/containers/arena-flyout.ts +++ b/src/ui/containers/arena-flyout.ts @@ -317,7 +317,7 @@ export class ArenaFlyout extends Phaser.GameObjects.Container { this.fieldEffectInfo.push({ name, effectType: arenaEffectType, - maxDuration: tagAddedEvent.duration, + maxDuration: tagAddedEvent.maxDuration, duration: tagAddedEvent.duration, tagType: tagAddedEvent.arenaTagType, }); @@ -353,7 +353,7 @@ export class ArenaFlyout extends Phaser.GameObjects.Container { ), effectType: fieldEffectChangedEvent instanceof WeatherChangedEvent ? ArenaEffectType.WEATHER : ArenaEffectType.TERRAIN, - maxDuration: fieldEffectChangedEvent.duration, + maxDuration: fieldEffectChangedEvent.maxDuration, duration: fieldEffectChangedEvent.duration, }; From 773f5b23f2227b68f58ac3de5e8e91b6182d1317 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Fri, 19 Sep 2025 19:23:50 -0500 Subject: [PATCH 27/40] [Deps] Bump typedoc version (#6576) --- package.json | 2 +- pnpm-lock.yaml | 68 +++++++++++++++++++++++++------------------------- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/package.json b/package.json index 27b1fc4e290..1fb25c5d4ba 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "lefthook": "^1.12.2", "msw": "^2.10.4", "phaser3spectorjs": "^0.0.8", - "typedoc": "0.28.7", + "typedoc": "^0.28.13", "typedoc-github-theme": "^0.3.1", "typedoc-plugin-coverage": "^4.0.1", "typedoc-plugin-mdn-links": "^5.0.9", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 46608772338..50a8b17b366 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -88,17 +88,17 @@ importers: specifier: ^0.0.8 version: 0.0.8 typedoc: - specifier: 0.28.7 - version: 0.28.7(typescript@5.9.2) + specifier: ^0.28.13 + version: 0.28.13(typescript@5.9.2) typedoc-github-theme: specifier: ^0.3.1 - version: 0.3.1(typedoc@0.28.7(typescript@5.9.2)) + version: 0.3.1(typedoc@0.28.13(typescript@5.9.2)) typedoc-plugin-coverage: specifier: ^4.0.1 - version: 4.0.1(typedoc@0.28.7(typescript@5.9.2)) + version: 4.0.1(typedoc@0.28.13(typescript@5.9.2)) typedoc-plugin-mdn-links: specifier: ^5.0.9 - version: 5.0.9(typedoc@0.28.7(typescript@5.9.2)) + version: 5.0.9(typedoc@0.28.13(typescript@5.9.2)) typescript: specifier: ^5.9.2 version: 5.9.2 @@ -717,17 +717,17 @@ packages: cpu: [x64] os: [win32] - '@shikijs/engine-oniguruma@3.12.2': - resolution: {integrity: sha512-hozwnFHsLvujK4/CPVHNo3Bcg2EsnG8krI/ZQ2FlBlCRpPZW4XAEQmEwqegJsypsTAN9ehu2tEYe30lYKSZW/w==} + '@shikijs/engine-oniguruma@3.13.0': + resolution: {integrity: sha512-O42rBGr4UDSlhT2ZFMxqM7QzIU+IcpoTMzb3W7AlziI1ZF7R8eS2M0yt5Ry35nnnTX/LTLXFPUjRFCIW+Operg==} - '@shikijs/langs@3.12.2': - resolution: {integrity: sha512-bVx5PfuZHDSHoBal+KzJZGheFuyH4qwwcwG/n+MsWno5cTlKmaNtTsGzJpHYQ8YPbB5BdEdKU1rga5/6JGY8ww==} + '@shikijs/langs@3.13.0': + resolution: {integrity: sha512-672c3WAETDYHwrRP0yLy3W1QYB89Hbpj+pO4KhxK6FzIrDI2FoEXNiNCut6BQmEApYLfuYfpgOZaqbY+E9b8wQ==} - '@shikijs/themes@3.12.2': - resolution: {integrity: sha512-fTR3QAgnwYpfGczpIbzPjlRnxyONJOerguQv1iwpyQZ9QXX4qy/XFQqXlf17XTsorxnHoJGbH/LXBvwtqDsF5A==} + '@shikijs/themes@3.13.0': + resolution: {integrity: sha512-Vxw1Nm1/Od8jyA7QuAenaV78BG2nSr3/gCGdBkLpfLscddCkzkL36Q5b67SrLLfvAJTOUzW39x4FHVCFriPVgg==} - '@shikijs/types@3.12.2': - resolution: {integrity: sha512-K5UIBzxCyv0YoxN3LMrKB9zuhp1bV+LgewxuVwHdl4Gz5oePoUFrr9EfgJlGlDeXCU1b/yhdnXeuRvAnz8HN8Q==} + '@shikijs/types@3.13.0': + resolution: {integrity: sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw==} '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} @@ -1828,12 +1828,12 @@ packages: peerDependencies: typedoc: 0.27.x || 0.28.x - typedoc@0.28.7: - resolution: {integrity: sha512-lpz0Oxl6aidFkmS90VQDQjk/Qf2iw0IUvFqirdONBdj7jPSN9mGXhy66BcGNDxx5ZMyKKiBVAREvPEzT6Uxipw==} + typedoc@0.28.13: + resolution: {integrity: sha512-dNWY8msnYB2a+7Audha+aTF1Pu3euiE7ySp53w8kEsXoYw7dMouV5A1UsTUY345aB152RHnmRMDiovuBi7BD+w==} engines: {node: '>= 18', pnpm: '>= 10'} hasBin: true peerDependencies: - typescript: 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x + typescript: 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x typescript@5.9.2: resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} @@ -2312,10 +2312,10 @@ snapshots: '@gerrit0/mini-shiki@3.12.2': dependencies: - '@shikijs/engine-oniguruma': 3.12.2 - '@shikijs/langs': 3.12.2 - '@shikijs/themes': 3.12.2 - '@shikijs/types': 3.12.2 + '@shikijs/engine-oniguruma': 3.13.0 + '@shikijs/langs': 3.13.0 + '@shikijs/themes': 3.13.0 + '@shikijs/types': 3.13.0 '@shikijs/vscode-textmate': 10.0.2 '@inquirer/checkbox@4.2.0(@types/node@22.16.5)': @@ -2547,20 +2547,20 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.50.1': optional: true - '@shikijs/engine-oniguruma@3.12.2': + '@shikijs/engine-oniguruma@3.13.0': dependencies: - '@shikijs/types': 3.12.2 + '@shikijs/types': 3.13.0 '@shikijs/vscode-textmate': 10.0.2 - '@shikijs/langs@3.12.2': + '@shikijs/langs@3.13.0': dependencies: - '@shikijs/types': 3.12.2 + '@shikijs/types': 3.13.0 - '@shikijs/themes@3.12.2': + '@shikijs/themes@3.13.0': dependencies: - '@shikijs/types': 3.12.2 + '@shikijs/types': 3.13.0 - '@shikijs/types@3.12.2': + '@shikijs/types@3.13.0': dependencies: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 @@ -3682,19 +3682,19 @@ snapshots: type-fest@4.41.0: {} - typedoc-github-theme@0.3.1(typedoc@0.28.7(typescript@5.9.2)): + typedoc-github-theme@0.3.1(typedoc@0.28.13(typescript@5.9.2)): dependencies: - typedoc: 0.28.7(typescript@5.9.2) + typedoc: 0.28.13(typescript@5.9.2) - typedoc-plugin-coverage@4.0.1(typedoc@0.28.7(typescript@5.9.2)): + typedoc-plugin-coverage@4.0.1(typedoc@0.28.13(typescript@5.9.2)): dependencies: - typedoc: 0.28.7(typescript@5.9.2) + typedoc: 0.28.13(typescript@5.9.2) - typedoc-plugin-mdn-links@5.0.9(typedoc@0.28.7(typescript@5.9.2)): + typedoc-plugin-mdn-links@5.0.9(typedoc@0.28.13(typescript@5.9.2)): dependencies: - typedoc: 0.28.7(typescript@5.9.2) + typedoc: 0.28.13(typescript@5.9.2) - typedoc@0.28.7(typescript@5.9.2): + typedoc@0.28.13(typescript@5.9.2): dependencies: '@gerrit0/mini-shiki': 3.12.2 lunr: 2.3.9 From 16a903b975593050b6ea2c108dc994b59e9b4203 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Fri, 19 Sep 2025 22:47:35 -0500 Subject: [PATCH 28/40] [Bug][Ability] Remove flyout for rivalry (#6577) Remove flyout for rivalry --- src/data/abilities/ability.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index 9e5bd8030d6..af1f7bab7bc 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -6949,7 +6949,7 @@ export function initAbilities() { .attr(TypeImmunityStatStageChangeAbAttr, PokemonType.ELECTRIC, Stat.SPD, 1) .ignorable(), new Ability(AbilityId.RIVALRY, 4) - .attr(MovePowerBoostAbAttr, (user, target, _move) => user?.gender !== Gender.GENDERLESS && target?.gender !== Gender.GENDERLESS && user?.gender === target?.gender, 1.25, true) + .attr(MovePowerBoostAbAttr, (user, target, _move) => user?.gender !== Gender.GENDERLESS && target?.gender !== Gender.GENDERLESS && user?.gender === target?.gender, 1.25) .attr(MovePowerBoostAbAttr, (user, target, _move) => user?.gender !== Gender.GENDERLESS && target?.gender !== Gender.GENDERLESS && user?.gender !== target?.gender, 0.75), new Ability(AbilityId.STEADFAST, 4) .attr(FlinchStatStageChangeAbAttr, [ Stat.SPD ], 1), From 405b1d389d882b14644622d492984f672cca0c2e Mon Sep 17 00:00:00 2001 From: Dean <69436131+emdeann@users.noreply.github.com> Date: Fri, 19 Sep 2025 21:08:02 -0700 Subject: [PATCH 29/40] [Bug] Apply Supreme Overlord only once on summon (#6575) * Implement supreme overlord as a battler tag * Make ability a conditionalattr * Add tag to map * Update test Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com> * Add edgeCase marker * Extend from AbilityBattlerTag * Run biome --------- Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com> --- src/@types/battler-tags.ts | 3 +- src/data/abilities/ability.ts | 4 +-- src/data/battler-tags.ts | 38 +++++++++++++++++++++++++ src/data/moves/move.ts | 3 ++ src/enums/battler-tag-type.ts | 1 + test/abilities/supreme-overlord.test.ts | 38 +++++++++++++++++++++++++ 6 files changed, 84 insertions(+), 3 deletions(-) diff --git a/src/@types/battler-tags.ts b/src/@types/battler-tags.ts index e47b4f8cfc1..ec72c811447 100644 --- a/src/@types/battler-tags.ts +++ b/src/@types/battler-tags.ts @@ -89,7 +89,8 @@ export type AbilityBattlerTagType = | BattlerTagType.QUARK_DRIVE | BattlerTagType.UNBURDEN | BattlerTagType.SLOW_START - | BattlerTagType.TRUANT; + | BattlerTagType.TRUANT + | BattlerTagType.SUPREME_OVERLORD; /** Subset of {@linkcode BattlerTagType}s that provide type boosts */ export type TypeBoostTagType = BattlerTagType.FIRE_BOOST | BattlerTagType.CHARGED; diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index af1f7bab7bc..ebe8b816e5e 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -7742,8 +7742,8 @@ export function initAbilities() { new Ability(AbilityId.SHARPNESS, 9) .attr(MovePowerBoostAbAttr, (_user, _target, move) => move.hasFlag(MoveFlags.SLICING_MOVE), 1.5), new Ability(AbilityId.SUPREME_OVERLORD, 9) - .attr(VariableMovePowerBoostAbAttr, (user, _target, _move) => 1 + 0.1 * Math.min(user.isPlayer() ? globalScene.arena.playerFaints : globalScene.currentBattle.enemyFaints, 5)) - .partial(), // Should only boost once, on summon + .conditionalAttr((p) => (p.isPlayer() ? globalScene.arena.playerFaints : globalScene.currentBattle.enemyFaints) > 0, PostSummonAddBattlerTagAbAttr, BattlerTagType.SUPREME_OVERLORD, 0, true) + .edgeCase(), // Tag is not tied to ability, so suppression/removal etc will not function until a structure to allow this is implemented new Ability(AbilityId.COSTAR, 9, -2) .attr(PostSummonCopyAllyStatsAbAttr), new Ability(AbilityId.TOXIC_DEBRIS, 9) diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 80a30516903..b6c3cf2b5a6 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -3626,6 +3626,41 @@ export class MagicCoatTag extends BattlerTag { } } +/** + * Tag associated with {@linkcode AbilityId.SUPREME_OVERLORD} + */ +export class SupremeOverlordTag extends AbilityBattlerTag { + public override readonly tagType = BattlerTagType.SUPREME_OVERLORD; + /** The number of faints at the time the user was sent out */ + public readonly faintCount: number; + constructor() { + super(BattlerTagType.SUPREME_OVERLORD, AbilityId.SUPREME_OVERLORD, BattlerTagLapseType.FAINT, 0); + } + + public override onAdd(pokemon: Pokemon): boolean { + (this as Mutable).faintCount = Math.min( + pokemon.isPlayer() ? globalScene.arena.playerFaints : globalScene.currentBattle.enemyFaints, + 5, + ); + globalScene.phaseManager.queueMessage( + i18next.t("battlerTags:supremeOverlordOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }), + ); + return true; + } + + /** + * @returns The damage multiplier for Supreme Overlord + */ + public getBoost(): number { + return 1 + 0.1 * this.faintCount; + } + + public override loadTag(source: BaseBattlerTag & Pick): void { + super.loadTag(source); + (this as Mutable).faintCount = source.faintCount; + } +} + /** * Retrieves a {@linkcode BattlerTag} based on the provided tag type, turn count, source move, and source ID. * @param sourceId - The ID of the pokemon adding the tag @@ -3826,6 +3861,8 @@ export function getBattlerTag( return new PsychoShiftTag(); case BattlerTagType.MAGIC_COAT: return new MagicCoatTag(); + case BattlerTagType.SUPREME_OVERLORD: + return new SupremeOverlordTag(); } } @@ -3960,4 +3997,5 @@ export type BattlerTagTypeMap = { [BattlerTagType.GRUDGE]: GrudgeTag; [BattlerTagType.PSYCHO_SHIFT]: PsychoShiftTag; [BattlerTagType.MAGIC_COAT]: MagicCoatTag; + [BattlerTagType.SUPREME_OVERLORD]: SupremeOverlordTag; }; diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 91d61c15b8b..0fdb0d01e43 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -18,6 +18,7 @@ import { ShellTrapTag, StockpilingTag, SubstituteTag, + SupremeOverlordTag, TrappedTag, TypeBoostTag, } from "#data/battler-tags"; @@ -879,6 +880,8 @@ export abstract class Move implements Localizable { power.value *= 1.5; } + power.value *= (source.getTag(BattlerTagType.SUPREME_OVERLORD) as SupremeOverlordTag | undefined)?.getBoost() ?? 1; + return power.value; } diff --git a/src/enums/battler-tag-type.ts b/src/enums/battler-tag-type.ts index 6d9d2dd4a92..7956e506886 100644 --- a/src/enums/battler-tag-type.ts +++ b/src/enums/battler-tag-type.ts @@ -94,4 +94,5 @@ export enum BattlerTagType { ENDURE_TOKEN = "ENDURE_TOKEN", POWDER = "POWDER", MAGIC_COAT = "MAGIC_COAT", + SUPREME_OVERLORD = "SUPREME_OVERLORD", } diff --git a/test/abilities/supreme-overlord.test.ts b/test/abilities/supreme-overlord.test.ts index a0f2d9050b3..d5470b70476 100644 --- a/test/abilities/supreme-overlord.test.ts +++ b/test/abilities/supreme-overlord.test.ts @@ -1,6 +1,7 @@ import { allMoves } from "#data/data-lists"; import { AbilityId } from "#enums/ability-id"; import { BattlerIndex } from "#enums/battler-index"; +import { BattlerTagType } from "#enums/battler-tag-type"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import type { Move } from "#moves/move"; @@ -166,4 +167,41 @@ describe("Abilities - Supreme Overlord", () => { expect(move.calculateBattlePower).toHaveLastReturnedWith(basePower); }); + + it("should not increase in power if ally faints while on the field", async () => { + game.override.battleStyle("double"); + await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER, SpeciesId.SQUIRTLE]); + + game.move.select(MoveId.TACKLE, BattlerIndex.PLAYER, BattlerIndex.ENEMY); + game.move.select(MoveId.LUNAR_DANCE, BattlerIndex.PLAYER_2); + await game.setTurnOrder([BattlerIndex.PLAYER_2, BattlerIndex.PLAYER, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]); + await game.toEndOfTurn(); + + expect(game.field.getPlayerPokemon()).not.toHaveBattlerTag(BattlerTagType.SUPREME_OVERLORD); + expect(move.calculateBattlePower).toHaveLastReturnedWith(basePower); + }); + + it("should persist fainted count through reload", async () => { + // Avoid learning moves + game.override.startingLevel(1000); + + await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER, SpeciesId.SQUIRTLE]); + + game.move.select(MoveId.LUNAR_DANCE); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + game.move.select(MoveId.TACKLE); + await game.toEndOfTurn(); + expect(move.calculateBattlePower).toHaveLastReturnedWith(basePower * 1.1); + + await game.toNextWave(); + await game.reload.reloadSession(); + + expect(game.field.getPlayerPokemon()).toHaveBattlerTag({ tagType: BattlerTagType.SUPREME_OVERLORD, faintCount: 1 }); + + game.move.select(MoveId.TACKLE); + await game.toEndOfTurn(); + expect(move.calculateBattlePower).toHaveLastReturnedWith(basePower * 1.1); + }); }); From 207808f37d2fc403612d93e5c59dc22b211e129a Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Sat, 20 Sep 2025 17:15:57 -0500 Subject: [PATCH 30/40] [Docs] Cleanup docs in pokemon.ts (#6543) * [Docs] Cleanup docs in pokemon.ts * Apply suggestions from code review Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com> Co-authored-by: Fabi <192151969+fabske0@users.noreply.github.com> * chore: apply biome * revert changes to docstrings that will conflict --- src/@types/damage-params.ts | 44 +++ src/field/pokemon.ts | 723 +++++++++++++++++++++++------------- 2 files changed, 505 insertions(+), 262 deletions(-) create mode 100644 src/@types/damage-params.ts diff --git a/src/@types/damage-params.ts b/src/@types/damage-params.ts new file mode 100644 index 00000000000..b656c60f0ab --- /dev/null +++ b/src/@types/damage-params.ts @@ -0,0 +1,44 @@ +import type { MoveCategory } from "#enums/move-category"; +import type { Pokemon } from "#field/pokemon"; +import type { Move } from "#types/move-types"; + +/** + * Collection of types for methods like {@linkcode Pokemon#getBaseDamage} and {@linkcode Pokemon#getAttackDamage}. + * @module + */ + +/** Base type for damage parameter methods, used for DRY */ +export interface damageParams { + /** The attacking {@linkcode Pokemon} */ + source: Pokemon; + /** The move used in the attack */ + move: Move; + /** The move's {@linkcode MoveCategory} after variable-category effects are applied */ + moveCategory: MoveCategory; + /** If `true`, ignores this Pokemon's defensive ability effects */ + ignoreAbility?: boolean; + /** If `true`, ignores the attacking Pokemon's ability effects */ + ignoreSourceAbility?: boolean; + /** If `true`, ignores the ally Pokemon's ability effects */ + ignoreAllyAbility?: boolean; + /** If `true`, ignores the ability effects of the attacking pokemon's ally */ + ignoreSourceAllyAbility?: boolean; + /** If `true`, calculates damage for a critical hit */ + isCritical?: boolean; + /** If `true`, suppresses changes to game state during the calculation */ + simulated?: boolean; + /** If defined, used in place of calculated effectiveness values */ + effectiveness?: number; +} + +/** + * Type for the parameters of {@linkcode Pokemon#getBaseDamage | getBaseDamage} + * @interface + */ +export type getBaseDamageParams = Omit; + +/** + * Type for the parameters of {@linkcode Pokemon#getAttackDamage | getAttackDamage} + * @interface + */ +export type getAttackDamageParams = Omit; diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index d48f4ae8ad2..3154f273cf5 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -141,6 +141,7 @@ import type { PokemonData } from "#system/pokemon-data"; import { RibbonData } from "#system/ribbons/ribbon-data"; import { awardRibbonsToSpeciesLine } from "#system/ribbons/ribbon-methods"; import type { AbAttrMap, AbAttrString, TypeMultiplierAbAttrParams } from "#types/ability-types"; +import type { getAttackDamageParams, getBaseDamageParams } from "#types/damage-params"; import type { DamageCalculationResult, DamageResult } from "#types/damage-result"; import type { IllusionData } from "#types/illusion-data"; import type { StarterDataEntry, StarterMoveset } from "#types/save-data"; @@ -176,36 +177,6 @@ import i18next from "i18next"; import Phaser from "phaser"; import SoundFade from "phaser3-rex-plugins/plugins/soundfade"; -/** Base typeclass for damage parameter methods, used for DRY */ -type damageParams = { - /** The attacking {@linkcode Pokemon} */ - source: Pokemon; - /** The move used in the attack */ - move: Move; - /** The move's {@linkcode MoveCategory} after variable-category effects are applied */ - moveCategory: MoveCategory; - /** If `true`, ignores this Pokemon's defensive ability effects */ - ignoreAbility?: boolean; - /** If `true`, ignores the attacking Pokemon's ability effects */ - ignoreSourceAbility?: boolean; - /** If `true`, ignores the ally Pokemon's ability effects */ - ignoreAllyAbility?: boolean; - /** If `true`, ignores the ability effects of the attacking pokemon's ally */ - ignoreSourceAllyAbility?: boolean; - /** If `true`, calculates damage for a critical hit */ - isCritical?: boolean; - /** If `true`, suppresses changes to game state during the calculation */ - simulated?: boolean; - /** If defined, used in place of calculated effectiveness values */ - effectiveness?: number; -}; - -/** Type for the parameters of {@linkcode Pokemon#getBaseDamage | getBaseDamage} */ -type getBaseDamageParams = Omit; - -/** Type for the parameters of {@linkcode Pokemon#getAttackDamage | getAttackDamage} */ -type getAttackDamageParams = Omit; - export abstract class Pokemon extends Phaser.GameObjects.Container { /** * This pokemon's {@link https://bulbapedia.bulbagarden.net/wiki/Personality_value | Personality value/PID}, @@ -242,20 +213,46 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * @todo Make private */ public status: Status | null; + /** + * The Pokémon's current friendship value, ranging from 0 to 255. + * @see {@link https://bulbapedia.bulbagarden.net/wiki/Friendship} + */ public friendship: number; + /** + * The level at which this Pokémon was met + * @remarks + * Primarily used for displaying in the summary screen + */ public metLevel: number; + /** + * The ID of the biome this Pokémon was met in + * @remarks + * Primarily used for display in the summary screen. + */ public metBiome: BiomeId | -1; + // TODO: figure out why this is used and document it (seems only to be read for getting the Pokémon's egg moves) public metSpecies: SpeciesId; + /** The wave index at which this Pokémon was met/encountered */ public metWave: number; public luck: number; public pauseEvolutions: boolean; public pokerus: boolean; + /** + * Indicates whether this Pokémon has left or is about to leave the field + * @remarks + * When `true` on a Wild Pokemon, this indicates it is about to flee. + */ public switchOutStatus = false; public evoCounter: number; + /** The type this Pokémon turns into when Terastallized */ public teraType: PokemonType; + /** Whether this Pokémon is currently Terastallized */ public isTerastallized: boolean; + /** The set of Types that have been boosted by this Pokémon's Stellar Terastallization. */ public stellarTypesBoosted: PokemonType[]; + // TODO: Create a fusionData class / interface and move all fusion-related fields there, exposed via getters + /** If this Pokémon is a fusion, the species it is fused with; `null` if not a fusion */ public fusionSpecies: PokemonSpecies | null; public fusionFormIndex: number; public fusionAbilityIndex: number; @@ -287,11 +284,18 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** Used by Mystery Encounters to execute pokemon-specific logic (such as stat boosts) at start of battle */ public mysteryEncounterBattleEffects?: (pokemon: Pokemon) => void; + /** The position of this Pokémon on the field */ public fieldPosition: FieldPosition; public maskEnabled: boolean; public maskSprite: Phaser.GameObjects.Sprite | null; + /** + * The set of all TMs that have been used on this Pokémon + * + * @remarks + * Used to allow re-learning TM moves via, e.g., the Memory Mushroom + */ public usedTMs: MoveId[]; private shinySparkle: Phaser.GameObjects.Sprite; @@ -516,7 +520,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { abstract initBattleInfo(): void; - isOnField(): boolean { + public isOnField(): boolean { if (!globalScene) { return false; } @@ -568,7 +572,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return this.isAllowedInBattle() && (!onField || this.isOnField()); } - getDexAttr(): bigint { + public getDexAttr(): bigint { let ret = 0n; if (this.gender !== Gender.GENDERLESS) { ret |= this.gender !== Gender.FEMALE ? DexAttr.MALE : DexAttr.FEMALE; @@ -582,9 +586,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Sets the Pokemon's name. Only called when loading a Pokemon so this function needs to be called when * initializing hardcoded Pokemon or else it will not display the form index name properly. - * @returns n/a */ - generateName(): void { + public generateName(): void { if (!this.fusionSpecies) { this.name = this.species.getName(this.formIndex); return; @@ -838,11 +841,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * Gracefully handle errors loading a variant sprite. Log if it fails and attempt to fall back on * non-experimental sprites before giving up. * - * @param cacheKey the cache key for the variant color sprite - * @param attemptedSpritePath the sprite path that failed to load - * @param useExpSprite was the attempted sprite experimental - * @param battleSpritePath the filename of the sprite - * @param optionalParams any additional params to log + * @param cacheKey - The cache key for the variant color sprite + * @param attemptedSpritePath - The sprite path that failed to load + * @param useExpSprite - Whether the attempted sprite was experimental + * @param battleSpritePath - The filename of the sprite + * @param optionalParams - Any additional params to log */ async fallbackVariantColor( cacheKey: string, @@ -908,6 +911,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return this.fusionSpecies.forms[this.fusionFormIndex].formKey; } + //#region Atlas and sprite ID methods // TODO: Add more documentation for all these attributes. // They may be all similar, but what each one actually _does_ is quite unclear at first glance @@ -1036,6 +1040,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { fusionVariant, ); } + //#endregion Atlas and sprite ID methods /** * Return this Pokemon's {@linkcode PokemonSpeciesForm | SpeciesForm}. @@ -1065,7 +1070,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * (such as by the effects of {@linkcode MoveId.TRANSFORM} or {@linkcode AbilityId.IMPOSTER}. * @returns Whether this Pokemon is currently transformed. */ - isTransformed(): boolean { + public isTransformed(): boolean { return this.summonData.speciesForm !== null; } @@ -1074,7 +1079,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * @param target - The {@linkcode Pokemon} being transformed into * @returns Whether this Pokemon can transform into `target`. */ - canTransformInto(target: Pokemon): boolean { + public canTransformInto(target: Pokemon): boolean { return !( // Neither pokemon can be already transformed ( @@ -1095,7 +1100,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * @param useIllusion - Whether to consider the species of this Pokemon's illusion; default `false` * @returns The {@linkcode PokemonSpeciesForm} of this Pokemon's fusion counterpart. */ - getFusionSpeciesForm(ignoreOverride = false, useIllusion = false): PokemonSpeciesForm { + public getFusionSpeciesForm(ignoreOverride = false, useIllusion = false): PokemonSpeciesForm { const fusionSpecies: PokemonSpecies = useIllusion && this.summonData.illusion ? this.summonData.illusion.fusionSpecies! : this.fusionSpecies!; const fusionFormIndex = @@ -1140,7 +1145,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** Resets the pokemon's field sprite properties, including position, alpha, and scale */ - resetSprite(): void { + public resetSprite(): void { // Resetting properties should not be shown on the field this.setVisible(false); @@ -1191,9 +1196,9 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Attempts to animate a given {@linkcode Phaser.GameObjects.Sprite} * @see {@linkcode Phaser.GameObjects.Sprite.play} - * @param sprite {@linkcode Phaser.GameObjects.Sprite} to animate - * @param tintSprite {@linkcode Phaser.GameObjects.Sprite} placed on top of the sprite to add a color tint - * @param animConfig {@linkcode String} to pass to {@linkcode Phaser.GameObjects.Sprite.play} + * @param sprite - Sprite to animate + * @param tintSprite - Sprite placed on top of the sprite to add a color tint + * @param animConfig - String to pass to the sprite's {@linkcode Phaser.GameObjects.Sprite.play | play} method * @returns true if the sprite was able to be animated */ tryPlaySprite(sprite: Phaser.GameObjects.Sprite, tintSprite: Phaser.GameObjects.Sprite, key: string): boolean { @@ -1258,7 +1263,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } } - setFieldPosition(fieldPosition: FieldPosition, duration?: number): Promise { + /** + * Set the field position of this Pokémon + * @param fieldPosition - The new field position + * @param duration - How long the transition should take, in milliseconds; if `0` or `undefined`, the position is changed instantly + */ + public setFieldPosition(fieldPosition: FieldPosition, duration?: number): Promise { return new Promise(resolve => { if (fieldPosition === this.fieldPosition) { resolve(); @@ -1608,10 +1618,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return baseStats; } + // TODO: Convert this into a getter getNature(): Nature { return this.customPokemonData.nature !== -1 ? this.customPokemonData.nature : this.nature; } + // TODO: Convert this into a setter OR just add a listener for calculateStats... setNature(nature: Nature): void { this.nature = nature; this.calculateStats(); @@ -1622,7 +1634,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { this.calculateStats(); } - generateNature(naturePool?: Nature[]): void { + /** + * Randomly generate and set this Pokémon's nature + * @param naturePool - An optional array of Natures to choose from. If not provided, all natures will be considered. + */ + private generateNature(naturePool?: Nature[]): void { if (naturePool === undefined) { naturePool = getEnumValues(Nature); } @@ -1630,10 +1646,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { this.setNature(nature); } + // TODO: Convert this into a getter isFullHp(): boolean { return this.hp >= this.getMaxHp(); } + // TODO: Convert this into a getter getMaxHp(): number { return this.getStat(Stat.HP); } @@ -1643,6 +1661,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return this.getMaxHp() - this.hp; } + /** + * Return the ratio of this Pokémon's current HP to its maximum HP + * @param precise - Whether to return the exact HP ratio (e.g. `0.54321`), or one rounded to the nearest %; default `false` + * @returns The current HP ratio + */ getHpRatio(precise = false): number { return precise ? this.hp / this.getMaxHp() : Math.round((this.hp / this.getMaxHp()) * 100) / 100; } @@ -1680,18 +1703,30 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Check whether this Pokemon is shiny. - * @param useIllusion - Whether to consider this pokemon's illusion if present; default `false` + * Check whether this Pokémon is shiny, including its fusion species + * + * @param useIllusion - Whether to consider an active illusion * @returns Whether this Pokemon is shiny + * @see {@linkcode isBaseShiny} */ isShiny(useIllusion = false): boolean { return this.isBaseShiny(useIllusion) || this.isFusionShiny(useIllusion); } + /** + * Get whether this Pokémon's _base_ species is shiny + * @param useIllusion - Whether to consider an active illusion; default `false` + * @returns Whether the pokemon is shiny + */ isBaseShiny(useIllusion = false) { return useIllusion ? (this.summonData.illusion?.shiny ?? this.shiny) : this.shiny; } + /** + * Get whether this Pokémon's _fusion_ species is shiny + * @param useIllusion - Whether to consider an active illusion; default `true` + * @returns Whether this Pokémon's fusion species is shiny, or `false` if there is no fusion + */ isFusionShiny(useIllusion = false) { if (!this.isFusion(useIllusion)) { return false; @@ -1701,7 +1736,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Check whether this Pokemon is doubly shiny (both normal and fusion are shiny). - * @param useIllusion - Whether to consider this pokemon's illusion if present; default `false` + * @param useIllusion - Whether to consider an active illusion; default `false` * @returns Whether this pokemon's base and fusion counterparts are both shiny. */ isDoubleShiny(useIllusion = false): boolean { @@ -1709,10 +1744,10 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Return this Pokemon's {@linkcode Variant | shiny variant}. + * Return this Pokemon's shiny variant. * If a fusion, returns the maximum of the two variants. * Only meaningful if this pokemon is actually shiny. - * @param useIllusion - Whether to consider this pokemon's illusion if present; default `false` + * @param useIllusion - Whether to consider an active illusion; default `false` * @returns The shiny variant of this Pokemon. */ getVariant(useIllusion = false): Variant { @@ -1727,6 +1762,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Return the base pokemon's variant. Equivalent to {@linkcode getVariant} if this pokemon is not a fusion. + * @param useIllusion - Whether to consider an active illusion; default `false` * @returns The shiny variant of this Pokemon's base species. */ getBaseVariant(useIllusion = false): Variant { @@ -1735,10 +1771,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Return the fused pokemon's variant. + * Get the shiny variant of this Pokémon's _fusion_ species * * @remarks * Always returns `0` if the pokemon is not a fusion. + * @param useIllusion - Whether to consider an active illusion * @returns The shiny variant of this pokemon's fusion species. */ getFusionVariant(useIllusion = false): Variant { @@ -1759,7 +1796,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Return whether this {@linkcode Pokemon} is currently fused with anything. - * @param useIllusion - Whether to consider this pokemon's illusion if present; default `false` + * @param useIllusion - Whether to consider an active illusion; default `false` * @returns Whether this Pokemon is currently fused with another species. */ isFusion(useIllusion = false): boolean { @@ -1768,7 +1805,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Return this {@linkcode Pokemon}'s name. - * @param useIllusion - Whether to consider this pokemon's illusion if present; default `false` + * @param useIllusion - Whether to consider an active illusion; default `false` * @returns This Pokemon's name. * @see {@linkcode getNameToRender} - gets this Pokemon's display name. */ @@ -1874,8 +1911,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * @param includeTeraType - Whether to use this Pokemon's tera type if Terastallized; default `false` * @param forDefend - Whether this Pokemon is currently receiving an attack; default `false` * @param ignoreOverride - Whether to ignore any overrides caused by {@linkcode MoveId.TRANSFORM | Transform}; default `false` - * @param useIllusion - Whether to consider this Pokemon's illusion if present; default `false` - * @returns An array of {@linkcode PokemonType}s corresponding to this Pokemon's typing (real or percieved). + * @param useIllusion - Whether to consider an active illusion; default `false` + * @returns An array of {@linkcode PokemonType}s corresponding to this Pokemon's typing (real or perceived). */ public getTypes( includeTeraType = false, @@ -2083,10 +2120,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Sets the {@linkcode Pokemon}'s ability and activates it if it normally activates on summon + * Set this Pokémon's temporary ability, activating it if it normally activates on summon * * Also clears primal weather if it is from the ability being changed - * @param ability New Ability + * @param ability - The temporary ability to set + * @param passive - Whether to set the passive ability instead of the non-passive one; default `false` */ public setTempAbility(ability: Ability, passive = false): void { applyOnLoseAbAttrs({ pokemon: this, passive }); @@ -2146,11 +2184,13 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Checks whether an ability of a pokemon can be currently applied. This should rarely be + * Check whether this Pokémon can apply its current ability + * + * @remarks + * This should rarely be * directly called, as {@linkcode hasAbility} and {@linkcode hasAbilityWithAttr} already call this. - * @see {@linkcode hasAbility} {@linkcode hasAbilityWithAttr} Intended ways to check abilities in most cases - * @param passive If true, check if passive can be applied instead of non-passive - * @returns `true` if the ability can be applied + * @param passive - Whether to check the passive (`true`) or non-passive (`false`) ability; default `false` + * @returns Whether the ability can be applied */ public canApplyAbility(passive = false): boolean { if (passive && !this.hasPassive()) { @@ -2365,14 +2405,14 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Calculates the effectiveness of a move against the Pokémon. - * This includes modifiers from move and ability attributes. - * @param source {@linkcode Pokemon} The attacking Pokémon. - * @param move {@linkcode Move} The move being used by the attacking Pokémon. - * @param ignoreAbility Whether to ignore abilities that might affect type effectiveness or immunity (defaults to `false`). - * @param simulated Whether to apply abilities via simulated calls (defaults to `true`) - * @param cancelled {@linkcode BooleanHolder} Stores whether the move was cancelled by a non-type-based immunity. - * @param useIllusion - Whether we want the attack move effectiveness on the illusion or not + * Calculate the effectiveness of the move against this Pokémon, including + * modifiers from move and ability attributes + * @param source - The attacking Pokémon. + * @param move - The move being used by the attacking Pokémon. + * @param ignoreAbility - Whether to ignore abilities that might affect type effectiveness or immunity (defaults to `false`). + * @param simulated - Whether to apply abilities via simulated calls (defaults to `true`) + * @param cancelled - Stores whether the move was cancelled by a non-type-based immunity. + * @param useIllusion - Whether to consider an active illusion * @returns The type damage multiplier, indicating the effectiveness of the move */ getMoveEffectiveness( @@ -2540,10 +2580,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Computes the given Pokemon's matchup score against this Pokemon. + * Compute the given Pokémon's matchup score against this Pokémon + * @remarks * In most cases, this score ranges from near-zero to 16, but the maximum possible matchup score is 64. - * @param opponent {@linkcode Pokemon} The Pokemon to compare this Pokemon against - * @returns A score value based on how favorable this Pokemon is when fighting the given Pokemon + * @param opponent - The Pokemon to compare this Pokémon against + * @returns A score value based on how favorable this Pokémon is when fighting the given Pokémon */ getMatchupScore(opponent: Pokemon): number { const enemyTypes = opponent.getTypes(true, false, false, true); @@ -2620,6 +2661,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return (atkScore + defScore) * Math.min(hpDiffRatio, 1); } + /** + * Get the first evolution this Pokémon meets the conditions to evolve into + * @remarks + * Fusion evolutions are also considered. + * @returns The evolution this pokemon can currently evolve into, or `null` if it cannot evolve + */ getEvolution(): SpeciesFormEvolution | null { if (pokemonEvolutions.hasOwnProperty(this.species.speciesId)) { const evolutions = pokemonEvolutions[this.species.speciesId]; @@ -2645,11 +2692,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Gets all level up moves in a given range for a particular pokemon. - * @param startingLevel Don't include moves below this level - * @param includeEvolutionMoves Whether to include evolution moves - * @param simulateEvolutionChain Whether to include moves from prior evolutions - * @param includeRelearnerMoves Whether to include moves that would require a relearner. Note the move relearner inherently allows evolution moves + * Get all level up moves in a given range for a particular pokemon. + * @param startingLevel - Don't include moves below this level + * @param includeEvolutionMoves - Whether to include evolution moves + * @param simulateEvolutionChain - Whether to include moves from prior evolutions + * @param includeRelearnerMoves - Whether to include moves that would require a relearner. Note the move relearner inherently allows evolution moves * @returns A list of moves and the levels they can be learned at */ getLevelMoves( @@ -2781,12 +2828,14 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Helper function for getLevelMoves. + * Helper function for getLevelMoves + * + * @remarks * Finds all non-duplicate items from the input, and pushes them into the output. * Two items count as duplicate if they have the same Move, regardless of level. * - * @param levelMoves the input array to search for non-duplicates from - * @param ret the output array to be pushed into. + * @param levelMoves - The input array to search for non-duplicates from + * @param ret - The output array to be pushed into. */ private static getUniqueMoves(levelMoves: LevelMoves, ret: LevelMoves): void { const uniqueMoves: MoveId[] = []; @@ -2800,13 +2849,17 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Get a list of all egg moves - * * @returns list of egg moves */ getEggMoves(): MoveId[] | undefined { return speciesEggMoves[this.getSpeciesForm().getRootSpeciesId()]; } + /** + * Create a new {@linkcode PokemonMove} and set it to the specified move index in this Pokémon's moveset. + * @param moveIndex - The index of the move to set + * @param moveId - The ID of the move to set + */ setMove(moveIndex: number, moveId: MoveId): void { if (moveId === MoveId.NONE) { return; @@ -2819,14 +2872,16 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Function that tries to set a Pokemon shiny based on the trainer's trainer ID and secret ID. + * Attempt to set the Pokémon's shininess based on the trainer's trainer ID and secret ID. * Endless Pokemon in the end biome are unable to be set to shiny * + * @remarks + * * The exact mechanic is that it calculates E as the XOR of the player's trainer ID and secret ID. * F is calculated as the XOR of the first 16 bits of the Pokemon's ID with the last 16 bits. * The XOR of E and F are then compared to the {@linkcode shinyThreshold} (or {@linkcode thresholdOverride} if set) to see whether or not to generate a shiny. * The base shiny odds are {@linkcode BASE_SHINY_CHANCE} / 65536 - * @param thresholdOverride number that is divided by 2^16 (65536) to get the shiny chance, overrides {@linkcode shinyThreshold} if set (bypassing shiny rate modifiers such as Shiny Charm) + * @param thresholdOverride - number that is divided by 2^16 (65536) to get the shiny chance, overrides {@linkcode shinyThreshold} if set (bypassing shiny rate modifiers such as Shiny Charm) * @returns true if the Pokemon has been set as a shiny, false otherwise */ trySetShiny(thresholdOverride?: number): boolean { @@ -2867,14 +2922,16 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Function that tries to set a Pokemon shiny based on seed. + * Tries to set a Pokémon's shininess based on seed + * + * @remarks * For manual use only, usually to roll a Pokemon's shiny chance a second time. * If it rolls shiny, or if it's already shiny, also sets a random variant and give the Pokemon the associated luck. * * The base shiny odds are {@linkcode BASE_SHINY_CHANCE} / `65536` - * @param thresholdOverride number that is divided by `2^16` (`65536`) to get the shiny chance, overrides {@linkcode shinyThreshold} if set (bypassing shiny rate modifiers such as Shiny Charm) - * @param applyModifiersToOverride If {@linkcode thresholdOverride} is set and this is true, will apply Shiny Charm and event modifiers to {@linkcode thresholdOverride} - * @returns `true` if the Pokemon has been set as a shiny, `false` otherwise + * @param thresholdOverride - number that is divided by `2^16` (`65536`) to get the shiny chance, overrides {@linkcode shinyThreshold} if set (bypassing shiny rate modifiers such as Shiny Charm) + * @param applyModifiersToOverride - If {@linkcode thresholdOverride} is set and this is true, will apply Shiny Charm and event modifiers to {@linkcode thresholdOverride} + * @returns Whether this Pokémon was set to shiny */ public trySetShinySeed(thresholdOverride?: number, applyModifiersToOverride?: boolean): boolean { if (!this.shiny) { @@ -2900,11 +2957,18 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Generates a shiny variant - * @returns `0-2`, with the following probabilities: - * - Has a 10% chance of returning `2` (epic variant) - * - Has a 30% chance of returning `1` (rare variant) - * - Has a 60% chance of returning `0` (basic shiny) + * Randomly generate a shiny variant + * + * @remarks + * Variants are returned with the following probabilities: + * + * | Variant | Description | Probability | + * |---------|----------------|-------------| + * | 0 | Basic shiny | 60% | + * | 1 | Rare variant | 30% | + * | 2 | Epic variant | 10% | + * + * @returns The randomly chosen shiny variant */ protected generateShinyVariant(): Variant { const formIndex: number = this.formIndex; @@ -2940,12 +3004,14 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Function that tries to set a Pokemon to have its hidden ability based on seed, if it exists. + * Function that tries to set this Pokemon to have its hidden ability based on seed, if it exists. + * + * @remarks * For manual use only, usually to roll a Pokemon's hidden ability chance a second time. * * The base hidden ability odds are {@linkcode BASE_HIDDEN_ABILITY_CHANCE} / `65536` - * @param thresholdOverride number that is divided by `2^16` (`65536`) to get the HA chance, overrides {@linkcode haThreshold} if set (bypassing HA rate modifiers such as Ability Charm) - * @param applyModifiersToOverride If {@linkcode thresholdOverride} is set and this is true, will apply Ability Charm to {@linkcode thresholdOverride} + * @param thresholdOverride - number that is divided by `2^16` (`65536`) to get the HA chance, overrides {@linkcode haThreshold} if set (bypassing HA rate modifiers such as Ability Charm) + * @param applyModifiersToOverride - If {@linkcode thresholdOverride} is set and this is true, will apply Ability Charm to {@linkcode thresholdOverride} * @returns `true` if the Pokemon has been set to have its hidden ability, `false` otherwise */ public tryRerollHiddenAbilitySeed(thresholdOverride?: number, applyModifiersToOverride?: boolean): boolean { @@ -2964,6 +3030,10 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return this.abilityIndex === 2; } + /** + * Generate a fusion species and add it to this Pokémon + * @param forStarter - Whether this fusion is being generated for a starter Pokémon; default `false` + */ public generateFusionSpecies(forStarter?: boolean): void { const hiddenAbilityChance = new NumberHolder(BASE_HIDDEN_ABILITY_CHANCE); if (!this.hasTrainer()) { @@ -3030,6 +3100,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { this.generateName(); } + /** Remove the fusion species from this Pokémon */ public clearFusionSpecies(): void { this.fusionSpecies = null; this.fusionFormIndex = 0; @@ -3044,7 +3115,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { this.calculateStats(); } - /** Generates a semi-random moveset for a Pokemon */ + /** Generate a semi-random moveset for this Pokémon */ public generateAndPopulateMoveset(): void { generateMoveset(this); @@ -3063,6 +3134,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return move?.isUsable(this, ignorePp) ?? false; } + /** Show this Pokémon's info panel */ showInfo(): void { if (!this.battleInfo.visible) { const otherBattleInfo = globalScene.fieldUI @@ -3091,7 +3163,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } } - hideInfo(): Promise { + /** Hide this Pokémon's info panel */ + async hideInfo(): Promise { return new Promise(resolve => { if (this.battleInfo?.visible) { globalScene.tweens.add({ @@ -3115,14 +3188,6 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { }); } - /** - * sets if the pokemon is switching out (if it's a enemy wild implies it's going to flee) - * @param status - boolean - */ - setSwitchOutStatus(status: boolean): void { - this.switchOutStatus = status; - } - updateInfo(instant?: boolean): Promise { return this.battleInfo.updateInfo(this, instant); } @@ -3133,8 +3198,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Adds experience to this PlayerPokemon, subject to wave based level caps. - * @param exp The amount of experience to add - * @param ignoreLevelCap Whether to ignore level caps when adding experience (defaults to false) + * @param exp - The amount of experience to add + * @param ignoreLevelCap - Whether to ignore level caps when adding experience; default `false` */ addExp(exp: number, ignoreLevelCap = false) { const maxExpLevel = globalScene.getMaxExpLevel(ignoreLevelCap); @@ -3151,8 +3216,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Compares if `this` and {@linkcode target} are on the same team. - * @param target the {@linkcode Pokemon} to compare against. + * Check whether the specified Pokémon is an opponent + * @param target - The {@linkcode Pokemon} to compare against * @returns `true` if the two pokemon are allies, `false` otherwise */ public isOpponent(target: Pokemon): boolean { @@ -3197,17 +3262,18 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Calculates the stat stage multiplier of the user against an opponent. + * Calculate the stat stage multiplier of the user against an opponent * - * Note that this does not apply to evasion or accuracy + * @remarks + * This does not apply to evasion or accuracy * @see {@linkcode getAccuracyMultiplier} * @param stat - The {@linkcode EffectiveStat} to calculate * @param opponent - The {@linkcode Pokemon} being targeted * @param move - The {@linkcode Move} being used - * @param ignoreOppAbility determines whether the effects of the opponent's abilities (i.e. Unaware) should be ignored (`false` by default) - * @param isCritical determines whether a critical hit has occurred or not (`false` by default) - * @param simulated determines whether effects are applied without altering game state (`true` by default) - * @param ignoreHeldItems determines whether this Pokemon's held items should be ignored during the stat calculation, default `false` + * @param ignoreOppAbility - determines whether the effects of the opponent's abilities (i.e. Unaware) should be ignored; default `false` + * @param isCritical - determines whether a critical hit has occurred or not; default `false` + * @param simulated - determines whether effects are applied without altering game state; default `true` + * @param ignoreHeldItems - determines whether this Pokemon's held items should be ignored during the stat calculation; default `false` * @returns the stat stage multiplier to be used for effective stat calculation */ getStatStageMultiplier( @@ -3344,15 +3410,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Calculates the base damage of the given move against this Pokemon when attacked by the given source. * Used during damage calculation and for Shell Side Arm's forecasting effect. - * @param source - The attacking {@linkcode Pokemon}. - * @param move - The {@linkcode Move} used in the attack. - * @param moveCategory - The move's {@linkcode MoveCategory} after variable-category effects are applied. - * @param ignoreAbility - If `true`, ignores this Pokemon's defensive ability effects (defaults to `false`). - * @param ignoreSourceAbility - If `true`, ignore's the attacking Pokemon's ability effects (defaults to `false`). - * @param ignoreAllyAbility - If `true`, ignores the ally Pokemon's ability effects (defaults to `false`). - * @param ignoreSourceAllyAbility - If `true`, ignores the attacking Pokemon's ally's ability effects (defaults to `false`). - * @param isCritical - if `true`, calculates effective stats as if the hit were critical (defaults to `false`). - * @param simulated - if `true`, suppresses changes to game state during calculation (defaults to `true`). + * @param __namedParameters.source - Needed for proper typedoc rendering * @returns The move's base damage against this Pokemon when used by the source Pokemon. */ getBaseDamage({ @@ -3470,15 +3528,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Calculates the damage of an attack made by another Pokemon against this Pokemon - * @param source {@linkcode Pokemon} the attacking Pokemon - * @param move The {@linkcode Move} used in the attack - * @param ignoreAbility If `true`, ignores this Pokemon's defensive ability effects - * @param ignoreSourceAbility If `true`, ignores the attacking Pokemon's ability effects - * @param ignoreAllyAbility If `true`, ignores the ally Pokemon's ability effects - * @param ignoreSourceAllyAbility If `true`, ignores the ability effects of the attacking pokemon's ally - * @param isCritical If `true`, calculates damage for a critical hit. - * @param simulated If `true`, suppresses changes to game state during the calculation. - * @param effectiveness If defined, used in place of calculated effectiveness values + * @param __namedParameters.source - Needed for proper typedoc rendering * @returns The {@linkcode DamageCalculationResult} */ getAttackDamage({ @@ -3807,12 +3857,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Called by damageAndUpdate() - * @param damage integer - * @param ignoreSegments boolean, not currently used - * @param preventEndure used to update damage if endure or sturdy - * @param ignoreFaintPhase flag on whether to add FaintPhase if pokemon after applying damage faints - * @returns integer representing damage dealt + * Submethod called by {@linkcode damageAndUpdate} to apply damage to this Pokemon and adjust its HP. + * @param damage - The damage to deal + * @param _ignoreSegments - Whether to ignore boss segments; default `false` + * @param preventEndure - Whether to allow the damage to bypass an Endure/Sturdy effect + * @param ignoreFaintPhase - Whether to ignore adding a FaintPhase if this damage causes a faint + * @returns The actual damage dealt */ damage(damage: number, _ignoreSegments = false, preventEndure = false, ignoreFaintPhase = false): number { if (this.isFainted()) { @@ -3858,14 +3908,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Given the damage, adds a new DamagePhase and update HP values, etc. * - * Checks for 'Indirect' HitResults to account for Endure/Reviver Seed applying correctly - * @param damage integer - passed to damage() - * @param result an enum if it's super effective, not very, etc. - * @param isCritical boolean if move is a critical hit - * @param ignoreSegments boolean, passed to damage() and not used currently - * @param preventEndure boolean, ignore endure properties of pokemon, passed to damage() - * @param ignoreFaintPhase boolean to ignore adding a FaintPhase, passsed to damage() - * @returns integer of damage done + * @remarks + * Checks for {@linkcode HitResult.INDIRECT | Indirect} hits to account for Endure/Reviver Seed applying correctly + * @param damage - The damage to inflict on this Pokémon + * @param __namedParameters.source - Needed for proper typedoc rendering + * @returns Amount of damage actually done */ damageAndUpdate( damage: number, @@ -3876,10 +3923,17 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { ignoreFaintPhase = false, source, }: { + /** + * An enum if it's super effective, not very effective, etc; default {@linkcode HitResult.EFFECTIVE} + */ result?: DamageResult; + /** Whether the attack was a critical hit */ isCritical?: boolean; + /** Whether to ignore boss segments */ ignoreSegments?: boolean; + /** Whether to ignore adding a FaintPhase if this damage causes a faint; default `false` */ ignoreFaintPhase?: boolean; + /** The Pokémon inflicting the damage, or undefined if not caused by a Pokémon */ source?: Pokemon; } = {}, ): number { @@ -3913,17 +3967,25 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return damage; } - heal(amount: number): number { + /** + * Restore a specific amount of HP to this Pokémon + * @param amount - The amount of HP to restore + * @returns The true amount of HP restored; may be less than `amount` if `amount` would overheal + */ + public heal(amount: number): number { const healAmount = Math.min(amount, this.getMaxHp() - this.hp); this.hp += healAmount; return healAmount; } - isBossImmune(): boolean { + public isBossImmune(): boolean { return this.isBoss(); } - isMax(): boolean { + /** + * @returns Whether this Pokémon is in a Dynamax or Gigantamax form + */ + public isMax(): boolean { const maxForms = [ SpeciesFormKey.GIGANTAMAX, SpeciesFormKey.GIGANTAMAX_RAPID, @@ -3935,7 +3997,10 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { ); } - isMega(): boolean { + /** + * @returns Whether this Pokémon is in a Mega or Primal form + */ + public isMega(): boolean { const megaForms = [ SpeciesFormKey.MEGA, SpeciesFormKey.MEGA_X, @@ -3948,7 +4013,14 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { ); } - canAddTag(tagType: BattlerTagType): boolean { + /** + * Check whether a battler tag can be added to this Pokémon + * + * @param tagType - The tag to check + * @returns - Whether the tag can be added + * @see {@linkcode addTag} + */ + public canAddTag(tagType: BattlerTagType): boolean { if (this.getTag(tagType)) { return false; } @@ -3972,7 +4044,20 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return !cancelled.value; } - addTag(tagType: BattlerTagType, turnCount = 0, sourceMove?: MoveId, sourceId?: number): boolean { + /** + * Add a new {@linkcode BattlerTag} of the specified `tagType` + * + * @remarks + * Also ensures the tag is able to be applied, similar to {@linkcode canAddTag} + * + * @param tagType - The type of tag to add + * @param turnCount - The number of turns the tag should last; default `0` + * @param sourceMove - The id of the move that causing the tag to be added, if caused by a move + * @param sourceId - The {@linkcode Pokemon#id | id} of the pokemon causing the tag to be added, if caused by a Pokémon + * @returns Whether the tag was successfully added + * @see {@linkcode canAddTag} + */ + public addTag(tagType: BattlerTagType, turnCount = 0, sourceMove?: MoveId, sourceId?: number): boolean { const existingTag = this.getTag(tagType); if (existingTag) { existingTag.onOverlap(this); @@ -3981,6 +4066,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { const newTag = getBattlerTag(tagType, turnCount, sourceMove!, sourceId!); // TODO: are the bangs correct? + // TODO: Just call canAddTag() here? Can possibly overload it to accept an actual tag instead of just a type const cancelled = new BooleanHolder(false); applyAbAttrs("BattlerTagImmunityAbAttr", { pokemon: this, tag: newTag, cancelled }); if (cancelled.value) { @@ -4003,31 +4089,46 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return false; } - getTag(tagType: BattlerTagType.GRUDGE): GrudgeTag | undefined; - getTag(tagType: BattlerTagType.SUBSTITUTE): SubstituteTag | undefined; - getTag(tagType: BattlerTagType): BattlerTag | undefined; - getTag(tagType: Constructor): T | undefined; - getTag(tagType: BattlerTagType | Constructor): BattlerTag | undefined { + // TODO: Utilize a type map for these so we can avoid overloads + public getTag(tagType: BattlerTagType.GRUDGE): GrudgeTag | undefined; + public getTag(tagType: BattlerTagType.SUBSTITUTE): SubstituteTag | undefined; + public getTag(tagType: BattlerTagType): BattlerTag | undefined; + public getTag(tagType: Constructor): T | undefined; + public getTag(tagType: BattlerTagType | Constructor): BattlerTag | undefined { return typeof tagType === "function" ? this.summonData.tags.find(t => t instanceof tagType) : this.summonData.tags.find(t => t.tagType === tagType); } - findTag(tagFilter: (tag: BattlerTag) => boolean) { - return this.summonData.tags.find(t => tagFilter(t)); + /** + * Find the first `BattlerTag` matching the specified predicate + * @remarks + * Equivalent to `this.summonData.tags.find(tagFilter)`. + * @param tagFilter - The predicate to match against + * @returns The first matching tag, or `undefined` if none match + */ + public findTag(tagFilter: (tag: BattlerTag) => boolean) { + return this.summonData.tags.find(tagFilter); } - findTags(tagFilter: (tag: BattlerTag) => boolean): BattlerTag[] { - return this.summonData.tags.filter(t => tagFilter(t)); + /** + * Return the list of `BattlerTag`s that satisfy the given predicate + * @remarks + * Equivalent to `this.summonData.tags.filter(tagFilter)`. + * @param tagFilter - The predicate to match against + * @returns The filtered list of tags + */ + public findTags(tagFilter: (tag: BattlerTag) => boolean): BattlerTag[] { + return this.summonData.tags.filter(tagFilter); } /** * Tick down the first {@linkcode BattlerTag} found matching the given {@linkcode BattlerTagType}, * removing it if its duration goes below 0. - * @param tagType the {@linkcode BattlerTagType} to check against - * @returns `true` if the tag was present + * @param tagType - The `BattlerTagType` to lapse + * @returns Whether the tag was present */ - lapseTag(tagType: BattlerTagType): boolean { + public lapseTag(tagType: BattlerTagType): boolean { const tags = this.summonData.tags; const tag = tags.find(t => t.tagType === tagType); if (!tag) { @@ -4042,11 +4143,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Tick down all {@linkcode BattlerTags} matching the given {@linkcode BattlerTagLapseType}, - * removing any whose durations fall below 0. - * @param tagType the {@linkcode BattlerTagLapseType} to tick down + * Tick down all {@linkcode BattlerTags} that lapse on the provided + * `lapseType`, removing any whose durations fall below 0. + * @param lapseType - The type of lapse to process */ - lapseTags(lapseType: BattlerTagLapseType): void { + public lapseTags(lapseType: BattlerTagLapseType): void { const tags = this.summonData.tags; tags .filter( @@ -4061,10 +4162,14 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Remove the first tag matching the given {@linkcode BattlerTagType}. - * @param tagType the {@linkcode BattlerTagType} to search for and remove + * Remove the first tag matching `tagType` and invoke its + * {@linkcode BattlerTag#onRemove | onRemove} method. + * @remarks + * Only removes the first matching tag, if multiple are present; to remove all + * matching tags, use {@linkcode findAndRemoveTags} instead. + * @param tagType - The tag type to search for and remove */ - removeTag(tagType: BattlerTagType): void { + public removeTag(tagType: BattlerTagType): void { const tags = this.summonData.tags; const tag = tags.find(t => t.tagType === tagType); if (tag) { @@ -4074,10 +4179,14 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Find and remove all {@linkcode BattlerTag}s matching the given function. - * @param tagFilter a function dictating which tags to remove + * Find and remove all {@linkcode BattlerTag}s matching the given function and + * invoke their {@linkcode BattlerTag#onRemove | onRemove} methods. + * @remarks + * Removes all matching tags; to remove only the first matching tag, use + * {@linkcode removeTag} instead. + * @param tagFilter - A function dictating which tags to remove */ - findAndRemoveTags(tagFilter: (tag: BattlerTag) => boolean): void { + public findAndRemoveTags(tagFilter: (tag: BattlerTag) => boolean): void { const tags = this.summonData.tags; const tagsToRemove = tags.filter(t => tagFilter(t)); for (const tag of tagsToRemove) { @@ -4087,11 +4196,22 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } } - removeTagsBySourceId(sourceId: number): void { + /** + * Remove all tags that were applied by a Pokémon with the given `sourceId`, + * invoking their {@linkcode BattlerTag#onRemove | onRemove} methods. + * @param sourceId - Tags with this {@linkcode Pokemon#id | id} as their {@linkcode BattlerTag#sourceId | sourceId} will be removed + * @see {@linkcode findAndRemoveTags} + */ + public removeTagsBySourceId(sourceId: number): void { this.findAndRemoveTags(t => t.isSourceLinked() && t.sourceId === sourceId); } - transferTagsBySourceId(sourceId: number, newSourceId: number): void { + /** + * Change the `sourceId` of all tags on this Pokémon with the given `sourceId` to `newSourceId`. + * @param sourceId - The {@linkcode Pokemon#id | id} of the pokemon whose tags are to be transferred + * @param newSourceId - The {@linkcode Pokemon#id | id} of the pokemon to which the tags are being transferred + */ + public transferTagsBySourceId(sourceId: number, newSourceId: number): void { this.summonData.tags.forEach(t => { if (t.sourceId === sourceId) { t.sourceId = newSourceId; @@ -4100,11 +4220,14 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Transferring stat changes and Tags - * @param source {@linkcode Pokemon} the pokemon whose stats/Tags are to be passed on from, ie: the Pokemon using Baton Pass + * Transfer stat changes and Tags from another Pokémon + * + * @remarks + * Used to implement Baton Pass and switching via the Baton item. + * + * @param source - The pokemon whose stats/Tags are to be passed on from, ie: the Pokemon using Baton Pass */ - transferSummon(source: Pokemon): void { - // Copy all stat stages + public transferSummon(source: Pokemon): void { for (const s of BATTLE_STATS) { const sourceStage = source.getStatStage(s); if (this.isPlayer() && sourceStage === 6) { @@ -4134,9 +4257,9 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Gets whether the given move is currently disabled for this Pokemon. + * Get whether the given move is currently disabled for this Pokémon * - * @param moveId - The {@linkcode MoveId} ID of the move to check + * @param moveId - The ID of the move to check * @returns `true` if the move is disabled for this Pokemon, otherwise `false` * * @see {@linkcode MoveRestrictionBattlerTag} @@ -4146,9 +4269,9 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Gets whether the given move is currently disabled for the user based on the player's target selection + * Get whether the given move is currently disabled for the user based on the player's target selection * - * @param moveId - The {@linkcode MoveId} ID of the move to check + * @param moveId - The ID of the move to check * @param user - The move user * @param target - The target of the move * @@ -4166,11 +4289,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Gets the {@link MoveRestrictionBattlerTag} that is restricting a move, if it exists. + * Get the {@link MoveRestrictionBattlerTag} that is restricting a move, if it exists. * - * @param moveId - {@linkcode MoveId} ID of the move to check - * @param user - {@linkcode Pokemon} the move user, optional and used when the target is a factor in the move's restricted status - * @param target - {@linkcode Pokemon} the target of the move, optional and used when the target is a factor in the move's restricted status + * @param moveId - The ID of the move to check + * @param user - The move user, optional and used when the target is a factor in the move's restricted status + * @param target - The target of the move; optional, and used when the target is a factor in the move's restricted status * @returns The first tag on this Pokemon that restricts the move, or `null` if the move is not restricted. */ getRestrictingTag(moveId: MoveId, user?: Pokemon, target?: Pokemon): MoveRestrictionBattlerTag | null { @@ -4195,6 +4318,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return this.summonData.moveHistory; } + /** + * Add a new entry to this Pokemon's move history + * @remarks + * Does nothing if this Pokemon is not currently on the field. + * @param turnMove - The move to add to the history + */ public pushMoveHistory(turnMove: TurnMove): void { if (!this.isOnField()) { return; @@ -4212,7 +4341,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * @returns An array of {@linkcode TurnMove}, as specified above. */ // TODO: Update documentation in dancer PR to mention "getLastNonVirtualMove" - getLastXMoves(moveCount = 1): TurnMove[] { + public getLastXMoves(moveCount = 1): TurnMove[] { const moveHistory = this.getMoveHistory(); if (moveCount > 0) { return moveHistory.slice(Math.max(moveHistory.length - moveCount, 0)).reverse(); @@ -4230,7 +4359,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * @returns The last move this Pokemon has used satisfying the aforementioned conditions, * or `undefined` if no applicable moves have been used since switching in. */ - getLastNonVirtualMove(ignoreStruggle = false, ignoreFollowUp = true): TurnMove | undefined { + public getLastNonVirtualMove(ignoreStruggle = false, ignoreFollowUp = true): TurnMove | undefined { return this.getLastXMoves(-1).find( m => m.move !== MoveId.NONE @@ -4243,7 +4372,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * Return this Pokemon's move queue, consisting of all the moves it is slated to perform. * @returns An array of {@linkcode TurnMove}, as described above */ - getMoveQueue(): TurnMove[] { + public getMoveQueue(): TurnMove[] { return this.summonData.moveQueue; } @@ -4251,11 +4380,17 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * Add a new entry to the end of this Pokemon's move queue. * @param queuedMove - A {@linkcode TurnMove} to push to this Pokemon's queue. */ - pushMoveQueue(queuedMove: TurnMove): void { + public pushMoveQueue(queuedMove: TurnMove): void { this.summonData.moveQueue.push(queuedMove); } - changeForm(formChange: SpeciesFormChange): Promise { + /** + * Change this Pokémon's form to the specified form, loading the required + * assets and updating its stats and info display. + * @param formChange - The form to change to + * @returns A Promise that resolves once the form change has completed. + */ + public async changeForm(formChange: SpeciesFormChange): Promise { return new Promise(resolve => { this.formIndex = Math.max( this.species.forms.findIndex(f => f.formKey === formChange.formKey), @@ -4277,7 +4412,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { }); } - cry(soundConfig?: Phaser.Types.Sound.SoundConfig, sceneOverride?: BattleScene): AnySound | null { + /** + * Play this Pokémon's cry sound + * @param soundConfig - Optional sound configuration to apply to the cry + * @param sceneOverride - Optional scene to use instead of the global scene + */ + public cry(soundConfig?: Phaser.Types.Sound.SoundConfig, sceneOverride?: BattleScene): AnySound | null { const scene = sceneOverride ?? globalScene; // TODO: is `sceneOverride` needed? const cry = this.getSpeciesForm(undefined, true).cry(soundConfig); if (!cry) { @@ -4316,8 +4456,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return cry; } - // biome-ignore lint: there are a ton of issues.. - faintCry(callback: Function): void { + /** + * Play this Pokémon's faint cry, pausing its animation until the cry is finished. + * @param callback - A function to be called once the cry has finished playing + */ + public faintCry(callback: () => any): void { if (this.fusionSpecies && this.getSpeciesForm() !== this.getFusionSpeciesForm()) { this.fusionFaintCry(callback); return; @@ -4389,8 +4532,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { }); } - // biome-ignore lint/complexity/noBannedTypes: Consider refactoring to change type of Function - private fusionFaintCry(callback: Function): void { + /** + * Play this Pokémon's fusion faint cry, which is a mixture of the faint cries + * for both of its species + * @param callback - A function to be called once the cry has finished playing + */ + private fusionFaintCry(callback: () => any): void { const key = this.species.getCryKey(this.formIndex); let i = 0; let rate = 0.85; @@ -4500,7 +4647,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { }); } - isOppositeGender(pokemon: Pokemon): boolean { + /** + * Check the specified pokemon is considered to be the opposite gender as this pokemon + * @param pokemon - The Pokémon to compare against + * @returns Whether the pokemon are considered to be opposite genders + */ + public isOppositeGender(pokemon: Pokemon): boolean { return ( this.gender !== Gender.GENDERLESS && pokemon.gender === (this.gender === Gender.MALE ? Gender.FEMALE : Gender.MALE) @@ -4510,12 +4662,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Display an immunity message for a failed status application. * @param quiet - Whether to suppress message and return early - * @param reason - The reason for the status application failure - + * @param reason - The reason for the status application failure; * can be "overlap" (already has same status), "other" (generic fail message) * or a {@linkcode TerrainType} for terrain-based blockages. * Default `"other"` */ - queueStatusImmuneMessage( + public queueStatusImmuneMessage( quiet: boolean, reason: "overlap" | "other" | Exclude = "other", ): void { @@ -4742,7 +4894,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * * ⚠️ This method does **not** check for feasibility; that is the responsibility of the caller. */ - doSetStatus(effect: Exclude): void; + public doSetStatus(effect: Exclude): void; /** * Set this Pokemon's {@linkcode status | non-volatile status condition} to the specified effect. * @param effect - {@linkcode StatusEffect.SLEEP} @@ -4752,7 +4904,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * * ⚠️ This method does **not** check for feasibility; that is the responsibility of the caller. */ - doSetStatus(effect: StatusEffect.SLEEP, sleepTurnsRemaining?: number): void; + public doSetStatus(effect: StatusEffect.SLEEP, sleepTurnsRemaining?: number): void; /** * Set this Pokemon's {@linkcode status | non-volatile status condition} to the specified effect. * @param effect - The {@linkcode StatusEffect} to set @@ -4763,7 +4915,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * * ⚠️ This method does **not** check for feasibility; that is the responsibility of the caller. */ - doSetStatus(effect: StatusEffect, sleepTurnsRemaining?: number): void; + public doSetStatus(effect: StatusEffect, sleepTurnsRemaining?: number): void; /** * Set this Pokemon's {@linkcode status | non-volatile status condition} to the specified effect. * @param effect - The {@linkcode StatusEffect} to set @@ -4775,7 +4927,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * ⚠️ This method does **not** check for feasibility; that is the responsibility of the caller. * @todo Make this and all related fields private and change tests to use a field-based helper or similar */ - doSetStatus( + public doSetStatus( effect: StatusEffect, sleepTurnsRemaining = effect !== StatusEffect.SLEEP ? 0 : this.randBattleSeedIntRange(2, 4), ): void { @@ -4824,11 +4976,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Resets the status of a pokemon. - * @param revive Whether revive should be cured; defaults to true. - * @param confusion Whether resetStatus should include confusion or not; defaults to false. - * @param reloadAssets Whether to reload the assets or not; defaults to false. - * @param asPhase Whether to reset the status in a phase or immediately + * Reset this Pokémon's status + * @param revive - Whether revive should be cured; default `true` + * @param confusion - Whether to also cure confusion; default `false` + * @param reloadAssets - Whether to reload the assets or not; default `false` + * @param asPhase - Whether to reset the status in a phase or immediately; default `true` */ resetStatus(revive = true, confusion = false, reloadAssets = false, asPhase = true): void { const lastStatus = this.status?.effect; @@ -4844,9 +4996,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Performs the action of clearing a Pokemon's status - * + * Perform the action of clearing a Pokemon's status + * @remarks * This is a helper to {@linkcode resetStatus}, which should be called directly instead of this method + * @param confusion - Whether to also clear this Pokémon's confusion + * @param reloadAssets - Whether to reload this pokemon's assets */ public clearStatus(confusion: boolean, reloadAssets: boolean) { const lastStatus = this.status?.effect; @@ -4865,11 +5019,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Checks if this Pokemon is protected by Safeguard - * @param attacker the {@linkcode Pokemon} inflicting status on this Pokemon - * @returns `true` if this Pokemon is protected by Safeguard; `false` otherwise. + * Check if this Pokémon is protected by Safeguard + * @param attacker - The Pokémon responsible for the interaction that needs to check against Safeguard + * @returns Whether this Pokémon is protected by Safeguard */ - isSafeguarded(attacker: Pokemon): boolean { + public isSafeguarded(attacker: Pokemon): boolean { const defendingSide = this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; if (globalScene.arena.getTagOnSide(ArenaTagType.SAFEGUARD, defendingSide)) { const bypassed = new BooleanHolder(false); @@ -4882,11 +5036,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Performs miscellaneous setup for when the Pokemon is summoned, like generating the substitute sprite + * Perform miscellaneous setup for when the Pokemon is summoned, like generating the substitute sprite * @param resetSummonData - Whether to additionally reset the Pokemon's summon data (default: `false`) */ public fieldSetup(resetSummonData?: boolean): void { - this.setSwitchOutStatus(false); + this.switchOutStatus = false; if (globalScene) { globalScene.triggerPokemonFormChange(this, SpeciesFormChangePostMoveTrigger, true); } @@ -4914,7 +5068,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * Reset this Pokemon's {@linkcode PokemonSummonData | SummonData} and {@linkcode PokemonTempSummonData | TempSummonData} * in preparation for switching pokemon, as well as removing any relevant on-switch tags. */ - resetSummonData(): void { + public resetSummonData(): void { const illusion: IllusionData | null = this.summonData.illusion; if (this.summonData.speciesForm) { this.summonData.speciesForm = null; @@ -4927,17 +5081,21 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Reset a {@linkcode Pokemon}'s per-battle {@linkcode PokemonBattleData | battleData}, + * Reset this Pokémon's per-battle {@linkcode PokemonBattleData | battleData} * as well as any transient {@linkcode PokemonWaveData | waveData} for the current wave. + * + * @remarks * Should be called once per arena transition (new biome/trainer battle/Mystery Encounter). */ - resetBattleAndWaveData(): void { + public resetBattleAndWaveData(): void { this.battleData = new PokemonBattleData(); this.resetWaveData(); } /** - * Reset a {@linkcode Pokemon}'s {@linkcode PokemonWaveData | waveData}. + * Reset this Pokémon's {@linkcode PokemonWaveData | waveData} + * + * @remarks * Should be called upon starting a new wave in addition to whenever an arena transition occurs. * @see {@linkcode resetBattleAndWaveData} */ @@ -4946,6 +5104,16 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { this.tempSummonData.waveTurnCount = 1; } + /** + * Reset this Pokémon's Terastallization state + * + * @remarks + * Responsible for all of the cleanup required when a pokemon goes from being + * terastallized to no longer terastallized: + * - Resetting stellar type boosts + * - Updating the Pokémon's terastallization-dependent form + * - Adjusting the sprite pipeline to remove the Tera effect + */ resetTera(): void { const wasTerastallized = this.isTerastallized; this.isTerastallized = false; @@ -4956,6 +5124,9 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } } + /** + * Clear this Pokémon's transient turn data + */ resetTurnData(): void { this.turnData = new PokemonTurnData(); } @@ -4965,6 +5136,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return (this.getSpeciesForm().getBaseExp() * this.level) / 5 + 1; } + //#region Sprite and Animation Methods setFrameRate(frameRate: number) { globalScene.anims.get(this.getBattleSpriteKey()).frameRate = frameRate; try { @@ -5041,6 +5213,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } } + /** Play the shiny sparkle animation and effects, if applicable */ sparkle(): void { if (this.shinySparkle) { doShinySparkleAnim(this.shinySparkle, this.variant); @@ -5372,15 +5545,19 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { fusionCanvas.remove(); } + //#endregion Sprite and Animation Methods + /** - * Generates a random number using the current battle's seed, or the global seed if `globalScene.currentBattle` is falsy + * Generate a random number using the current battle's seed, or the global seed if `globalScene.currentBattle` is falsy + * + * @remarks * This calls either {@linkcode BattleScene.randBattleSeedInt}({@linkcode range}, {@linkcode min}) in `src/battle-scene.ts` * which calls {@linkcode Battle.randSeedInt}({@linkcode range}, {@linkcode min}) in `src/battle.ts` * which calls {@linkcode randSeedInt randSeedInt}({@linkcode range}, {@linkcode min}) in `src/utils.ts`, * or it directly calls {@linkcode randSeedInt randSeedInt}({@linkcode range}, {@linkcode min}) in `src/utils.ts` if there is no current battle * - * @param range How large of a range of random numbers to choose from. If {@linkcode range} <= 1, returns {@linkcode min} - * @param min The minimum integer to pick, default `0` + * @param range - How large of a range of random numbers to choose from. If {@linkcode range} <= 1, returns {@linkcode min} + * @param min - The minimum integer to pick; default `0` * @returns A random integer between {@linkcode min} and ({@linkcode min} + {@linkcode range} - 1) */ randBattleSeedInt(range: number, min = 0): number { @@ -5388,10 +5565,10 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Generates a random number using the current battle's seed, or the global seed if `globalScene.currentBattle` is falsy - * @param min The minimum integer to generate - * @param max The maximum integer to generate - * @returns a random integer between {@linkcode min} and {@linkcode max} inclusive + * Generate a random number using the current battle's seed, or the global seed if `globalScene.currentBattle` is falsy + * @param min - The minimum integer to generate + * @param max - The maximum integer to generate + * @returns A random integer between {@linkcode min} and {@linkcode max} (inclusive) */ randBattleSeedIntRange(min: number, max: number): number { return globalScene.currentBattle ? globalScene.randBattleSeedInt(max - min + 1, min) : randSeedIntRange(min, max); @@ -5399,9 +5576,9 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Causes a Pokemon to leave the field (such as in preparation for a switch out/escape). - * @param clearEffects Indicates if effects should be cleared (true) or passed - * to the next pokemon, such as during a baton pass (false) - * @param hideInfo Indicates if this should also play the animation to hide the Pokemon's + * @param clearEffects - Indicates if effects should be cleared (true) or passed + * to the next pokemon, such as during a baton pass (false) + * @param hideInfo - Indicates if this should also play the animation to hide the Pokemon's * info container. */ leaveField(clearEffects = true, hideInfo = true, destroy = false) { @@ -5421,25 +5598,33 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } // Trigger abilities that activate upon leaving the field applyAbAttrs("PreLeaveFieldAbAttr", { pokemon: this }); - this.setSwitchOutStatus(true); + this.switchOutStatus = true; globalScene.triggerPokemonFormChange(this, SpeciesFormChangeActiveTrigger, true); globalScene.field.remove(this, destroy); } + /** + * @inheritdoc {@linkcode Phaser.GameObjects.Container#destroy} + * + * ### Custom Behavior + * In addition to the base `destroy` behavior, this also destroys the Pokemon's + * {@linkcode battleInfo} and substitute sprite (as applicable). + */ destroy(): void { this.battleInfo?.destroy(); this.destroySubstitute(); super.destroy(); } + // TODO: Turn this into a getter getBattleInfo(): BattleInfo { return this.battleInfo; } /** - * Checks whether or not the Pokemon's root form has the same ability - * @param abilityIndex the given ability index we are checking - * @returns true if the abilities are the same + * Check whether or not this Pokémon's root form has the same ability + * @param abilityIndex - The ability index to check + * @returns Whether the Pokemon's root form has the same ability */ hasSameAbilityInRootForm(abilityIndex: number): boolean { const currentAbilityIndex = this.abilityIndex; @@ -5448,9 +5633,9 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Helper function to check if the player already owns the starter data of the Pokemon's + * Helper function to check if the player already owns the starter data of this Pokémon's * current ability - * @param ownedAbilityAttrs the owned abilityAttr of this Pokemon's root form + * @param ownedAbilityAttrs - The owned abilityAttr of this Pokemon's root form * @returns true if the player already has it, false otherwise */ checkIfPlayerHasAbilityOfStarter(ownedAbilityAttrs: number): boolean { @@ -5491,8 +5676,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Record a berry being eaten for ability and move triggers. * Only tracks things that proc _every_ time a berry is eaten. - * @param berryType The type of berry being eaten. - * @param updateHarvest Whether to track the berry for harvest; default `true`. + * @param berryType - The type of berry being eaten. + * @param updateHarvest - Whether to track the berry for harvest; default `true`. */ public recordEatenBerry(berryType: BerryType, updateHarvest = true) { this.battleData.hasEatenBerry = true; @@ -5503,6 +5688,14 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { this.turnData.berriesEaten.push(berryType); } + /** + * Get the number of persistent treasure items this Pokemon has + * @remarks + * Persistent treasure items are defined as held items that give money + * after battle, such as the Lucky Egg or the Amulet Coin. + * Used exclusively for Gimmighoul's evolution condition + * @returns The number of persistent treasure items this Pokémon has + */ getPersistentTreasureCount(): number { return ( this.getHeldItems().filter(m => m.is("DamageMoneyRewardModifier")).length @@ -5633,10 +5826,10 @@ export class PlayerPokemon extends Pokemon { } /** - * Causes this mon to leave the field (via {@linkcode leaveField}) and then - * opens the party switcher UI to switch a new mon in - * @param switchType the {@linkcode SwitchType} for this switch-out. If this is - * `BATON_PASS` or `SHED_TAIL`, this Pokemon's effects are not cleared upon leaving + * Cause this Pokémon to leave the field (via {@linkcode leaveField}) and then + * open the party switcher UI to switch in a new Pokémon + * @param switchType - The type of this switch-out. If this is + * `BATON_PASS` or `SHED_TAIL`, this Pokémon's effects are not cleared upon leaving * the field. */ switchOut(switchType: SwitchType = SwitchType.SWITCH): Promise { @@ -5970,8 +6163,8 @@ export class PlayerPokemon extends Pokemon { } /** - * Returns a Promise to fuse two PlayerPokemon together - * @param pokemon The PlayerPokemon to fuse to this one + * Fuse another PlayerPokemon into this one + * @param pokemon - The PlayerPokemon to fuse to this one */ fuse(pokemon: PlayerPokemon): void { this.fusionSpecies = pokemon.species; @@ -6462,7 +6655,7 @@ export class EnemyPokemon extends Pokemon { /** * Determines the Pokemon the given move would target if used by this Pokemon - * @param moveId {@linkcode MoveId} The move to be used + * @param moveId - The move to be used * @returns The indexes of the Pokemon the given move would target */ getNextTargets(moveId: MoveId): BattlerIndex[] { @@ -6575,7 +6768,11 @@ export class EnemyPokemon extends Pokemon { return 0; } - damage(damage: number, ignoreSegments = false, preventEndure = false, ignoreFaintPhase = false): number { + /** + * @inheritdoc + * @param ignoreSegments - Whether to ignore boss segments when applying damage + */ + public damage(damage: number, ignoreSegments = false, preventEndure = false, ignoreFaintPhase = false): number { if (this.isFainted()) { return 0; } @@ -6631,7 +6828,7 @@ export class EnemyPokemon extends Pokemon { return ret; } - canBypassBossSegments(segmentCount = 1): boolean { + private canBypassBossSegments(segmentCount = 1): boolean { if ( globalScene.currentBattle.battleSpec === BattleSpec.FINAL_BOSS && !this.formIndex @@ -6645,10 +6842,12 @@ export class EnemyPokemon extends Pokemon { /** * Go through a boss' health segments and give stats boosts for each newly cleared segment + * + * @remarks * The base boost is 1 to a random stat that's not already maxed out per broken shield * For Pokemon with 3 health segments or more, breaking the last shield gives +2 instead * For Pokemon with 5 health segments or more, breaking the last two shields give +2 each - * @param segmentIndex index of the segment to get down to (0 = no shield left, 1 = 1 shield left, etc.) + * @param segmentIndex - index of the segment to get down to (0 = no shield left, 1 = 1 shield left, etc.) */ handleBossSegmentCleared(segmentIndex: number): void { let doStatBoost = !this.hasTrainer(); @@ -6710,22 +6909,22 @@ export class EnemyPokemon extends Pokemon { } } - getFieldIndex(): number { + public getFieldIndex(): number { return globalScene.getEnemyField().indexOf(this); } - getBattlerIndex(): BattlerIndex { + public getBattlerIndex(): BattlerIndex { return BattlerIndex.ENEMY + this.getFieldIndex(); } /** * Add a new pokemon to the player's party (at `slotIndex` if set). * The new pokemon's visibility will be set to `false`. - * @param pokeballType the type of pokeball the pokemon was caught with - * @param slotIndex an optional index to place the pokemon in the party - * @returns the pokemon that was added or null if the pokemon could not be added + * @param pokeballType - The type of pokeball the pokemon was caught with + * @param slotIndex - An optional index to place the pokemon in the party + * @returns The pokemon that was added or null if the pokemon could not be added */ - addToParty(pokeballType: PokeballType, slotIndex = -1) { + public addToParty(pokeballType: PokeballType, slotIndex = -1) { const party = globalScene.getPlayerParty(); let ret: PlayerPokemon | null = null; @@ -6768,11 +6967,11 @@ export class EnemyPokemon extends Pokemon { * Show or hide the type effectiveness multiplier window * Passing undefined will hide the window */ - updateEffectiveness(effectiveness?: string) { + public updateEffectiveness(effectiveness?: string) { this.battleInfo.updateEffectiveness(effectiveness); } - toggleFlyout(visible: boolean): void { + public toggleFlyout(visible: boolean): void { this.battleInfo.toggleFlyout(visible); } } From 1691951c87918ac66a63510219bbd80ae6cb90fd Mon Sep 17 00:00:00 2001 From: Bertie690 <136088738+Bertie690@users.noreply.github.com> Date: Sat, 20 Sep 2025 18:24:27 -0400 Subject: [PATCH 31/40] [Test] Added Map key matcher; enforced strong typing on matchers (#6561) * Added `toHaveKey` matcher + fixed imports * Broke up the test matchers into multiple smaller interfaces * Added restricted typing on matchers Now we can't call `expect(game).toHaveFullHp()`!!!!! * Updated comment * Renamed `toEqualArrayUnsorted` into `toEqualUnsorted` * Moved comment at top of file * Fix `@module` doc comment * Remove extra space * Fix typo * Fixed key ssue in matchers * Update to-have-key.ts * Update test/@types/vitest.d.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Fixed missing braces inside comment --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- test/@types/vitest.d.ts | 381 ++++++++++-------- test/setup/matchers.setup.ts | 17 +- test/test-utils/helpers/modifiers-helper.ts | 5 +- ...array-unsorted.ts => to-equal-unsorted.ts} | 6 +- test/test-utils/matchers/to-have-key.ts | 47 +++ test/test-utils/matchers/to-have-terrain.ts | 4 +- test/test-utils/matchers/to-have-weather.ts | 4 +- 7 files changed, 274 insertions(+), 190 deletions(-) rename test/test-utils/matchers/{to-equal-array-unsorted.ts => to-equal-unsorted.ts} (92%) create mode 100644 test/test-utils/matchers/to-have-key.ts diff --git a/test/@types/vitest.d.ts b/test/@types/vitest.d.ts index 9a6f07b4afb..43e9df190aa 100644 --- a/test/@types/vitest.d.ts +++ b/test/@types/vitest.d.ts @@ -1,7 +1,7 @@ import "vitest"; -import type { Phase } from "#app/phase"; import type Overrides from "#app/overrides"; +import type { Phase } from "#app/phase"; import type { ArenaTag } from "#data/arena-tag"; import type { TerrainType } from "#data/terrain"; import type { AbilityId } from "#enums/ability-id"; @@ -10,10 +10,14 @@ import type { ArenaTagType } from "#enums/arena-tag-type"; import type { BattlerTagType } from "#enums/battler-tag-type"; import type { MoveId } from "#enums/move-id"; import type { PokemonType } from "#enums/pokemon-type"; +import type { PositionalTag } from "#data/positional-tags/positional-tag"; import type { PositionalTagType } from "#enums/positional-tag-type"; import type { BattleStat, EffectiveStat } from "#enums/stat"; import type { WeatherType } from "#enums/weather-type"; +import type { Pokemon } from "#field/pokemon"; +import type { GameManager } from "#test/test-utils/game-manager"; import type { toHaveArenaTagOptions } from "#test/test-utils/matchers/to-have-arena-tag"; +import type { toHaveBattlerTagOptions } from "#test/test-utils/matchers/to-have-battler-tag"; import type { toHaveEffectiveStatOptions } from "#test/test-utils/matchers/to-have-effective-stat"; import type { toHavePositionalTagOptions } from "#test/test-utils/matchers/to-have-positional-tag"; import type { expectedStatusType } from "#test/test-utils/matchers/to-have-status-effect"; @@ -23,175 +27,212 @@ import type { TurnMove } from "#types/turn-move"; import type { AtLeastOne } from "#types/type-helpers"; import type { toDmgValue } from "#utils/common"; import type { expect } from "vitest"; -import type { toHaveBattlerTagOptions } from "#test/test-utils/matchers/to-have-battler-tag"; +// #region Boilerplate/Helpers declare module "vitest" { - interface Assertion { - // #region Generic Matchers - - /** - * Check whether an array contains EXACTLY the given items (in any order). - * - * Different from {@linkcode expect.arrayContaining} as the latter only checks for subset equality - * (as opposed to full equality). - * - * @param expected - The expected contents of the array, in any order - * @see {@linkcode expect.arrayContaining} - */ - toEqualArrayUnsorted(expected: T[]): void; - - // #endregion Generic Matchers - - // #region GameManager Matchers - - /** - * Check if the {@linkcode GameManager} has shown the given message at least once in the current battle. - * @param expectedMessage - The expected message - */ - toHaveShownMessage(expectedMessage: string): void; - /** - * @param expectedPhase - The expected {@linkcode PhaseString} - */ - toBeAtPhase(expectedPhase: PhaseString): void; - // #endregion GameManager Matchers - - // #region Arena Matchers - - /** - * Check whether the current {@linkcode WeatherType} is as expected. - * @param expectedWeatherType - The expected {@linkcode WeatherType} - */ - toHaveWeather(expectedWeatherType: WeatherType): void; - - /** - * Check whether the current {@linkcode TerrainType} is as expected. - * @param expectedTerrainType - The expected {@linkcode TerrainType} - */ - toHaveTerrain(expectedTerrainType: TerrainType): void; - - /** - * Check whether the current {@linkcode Arena} contains the given {@linkcode ArenaTag}. - * @param expectedTag - A partially-filled {@linkcode ArenaTag} containing the desired properties - */ - toHaveArenaTag(expectedTag: toHaveArenaTagOptions): void; - /** - * Check whether the current {@linkcode Arena} contains the given {@linkcode ArenaTag}. - * @param expectedType - The {@linkcode ArenaTagType} of the desired tag - * @param side - The {@linkcode ArenaTagSide | side(s) of the field} the tag should affect; default {@linkcode ArenaTagSide.BOTH} - */ - toHaveArenaTag(expectedType: ArenaTagType, side?: ArenaTagSide): void; - - /** - * Check whether the current {@linkcode Arena} contains the given {@linkcode PositionalTag}. - * @param expectedTag - A partially-filled `PositionalTag` containing the desired properties - */ - toHavePositionalTag

(expectedTag: toHavePositionalTagOptions

): void; - /** - * Check whether the current {@linkcode Arena} contains the given number of {@linkcode PositionalTag}s. - * @param expectedType - The {@linkcode PositionalTagType} of the desired tag - * @param count - The number of instances of {@linkcode expectedType} that should be active; - * defaults to `1` and must be within the range `[0, 4]` - */ - toHavePositionalTag(expectedType: PositionalTagType, count?: number): void; - - // #endregion Arena Matchers - - // #region Pokemon Matchers - - /** - * Check whether a {@linkcode Pokemon}'s current typing includes the given types. - * @param expectedTypes - The expected {@linkcode PokemonType}s to check against; must have length `>0` - * @param options - The {@linkcode toHaveTypesOptions | options} passed to the matcher - */ - toHaveTypes(expectedTypes: PokemonType[], options?: toHaveTypesOptions): void; - - /** - * Check whether a {@linkcode Pokemon} has used a move matching the given criteria. - * @param expectedMove - The {@linkcode MoveId} the Pokemon is expected to have used, - * or a partially filled {@linkcode TurnMove} containing the desired properties to check - * @param index - The index of the move history entry to check, in order from most recent to least recent; default `0` - * @see {@linkcode Pokemon.getLastXMoves} - */ - toHaveUsedMove(expectedMove: MoveId | AtLeastOne, index?: number): void; - - /** - * Check whether a {@linkcode Pokemon}'s effective stat is as expected - * (checked after all stat value modifications). - * @param stat - The {@linkcode EffectiveStat} to check - * @param expectedValue - The expected value of {@linkcode stat} - * @param options - The {@linkcode toHaveEffectiveStatOptions | options} passed to the matcher - * @remarks - * If you want to check the stat **before** modifiers are applied, use {@linkcode Pokemon.getStat} instead. - */ - toHaveEffectiveStat(stat: EffectiveStat, expectedValue: number, options?: toHaveEffectiveStatOptions): void; - - /** - * Check whether a {@linkcode Pokemon} has a specific {@linkcode StatusEffect | non-volatile status effect}. - * @param expectedStatusEffect - The {@linkcode StatusEffect} the Pokemon is expected to have, - * or a partially filled {@linkcode Status} containing the desired properties - */ - toHaveStatusEffect(expectedStatusEffect: expectedStatusType): void; - - /** - * Check whether a {@linkcode Pokemon} has a specific {@linkcode Stat} stage. - * @param stat - The {@linkcode BattleStat} to check - * @param expectedStage - The expected stat stage value of {@linkcode stat} - */ - toHaveStatStage(stat: BattleStat, expectedStage: number): void; - - /** - * Check whether a {@linkcode Pokemon} has the given {@linkcode BattlerTag}. - * @param expectedTag - A partially-filled {@linkcode BattlerTag} containing the desired properties - */ - toHaveBattlerTag(expectedTag: toHaveBattlerTagOptions): void; - /** - * Check whether a {@linkcode Pokemon} has the given {@linkcode BattlerTag}. - * @param expectedType - The expected {@linkcode BattlerTagType} - */ - toHaveBattlerTag(expectedType: BattlerTagType): void; - - /** - * Check whether a {@linkcode Pokemon} has applied a specific {@linkcode AbilityId}. - * @param expectedAbilityId - The `AbilityId` to check for - */ - toHaveAbilityApplied(expectedAbilityId: AbilityId): void; - - /** - * Check whether a {@linkcode Pokemon} has a specific amount of {@linkcode Stat.HP | HP}. - * @param expectedHp - The expected amount of {@linkcode Stat.HP | HP} to have - */ - toHaveHp(expectedHp: number): void; - - /** - * Check whether a {@linkcode Pokemon} has taken a specific amount of damage. - * @param expectedDamageTaken - The expected amount of damage taken - * @param roundDown - Whether to round down `expectedDamageTaken` with {@linkcode toDmgValue}; default `true` - */ - toHaveTakenDamage(expectedDamageTaken: number, roundDown?: boolean): void; - - /** - * Check whether a {@linkcode Pokemon} is currently fainted (as determined by {@linkcode Pokemon.isFainted}). - * @remarks - * When checking whether an enemy wild Pokemon is fainted, one must store a reference to it in a variable _before_ the fainting effect occurs. - * Otherwise, the Pokemon will be removed from the field and garbage collected. - */ - toHaveFainted(): void; - - /** - * Check whether a {@linkcode Pokemon} is at full HP. - */ - toHaveFullHp(): void; - /** - * Check whether a {@linkcode Pokemon} has consumed the given amount of PP for one of its moves. - * @param moveId - The {@linkcode MoveId} corresponding to the {@linkcode PokemonMove} that should have consumed PP - * @param ppUsed - The numerical amount of PP that should have been consumed, - * or `all` to indicate the move should be _out_ of PP - * @remarks - * If the Pokemon's moveset has been set via {@linkcode Overrides.MOVESET_OVERRIDE}/{@linkcode Overrides.ENEMY_MOVESET_OVERRIDE} - * or does not contain exactly one copy of `moveId`, this will fail the test. - */ - toHaveUsedPP(moveId: MoveId, ppUsed: number | "all"): void; - - // #endregion Pokemon Matchers - } + interface Assertion + extends GenericMatchers, + RestrictMatcher, + RestrictMatcher, + RestrictMatcher {} } + +/** + * Utility type to restrict matchers' properties based on the type of `T`. + * If it does not extend `R`, all methods inside `M` will have their types resolved to `never`. + * @typeParam M - The type of the matchers object to restrict + * @typeParam T - The type parameter of the assertion + * @typeParam R - The type to restrict T based off of + * @privateRemarks + * We cannot remove incompatible methods outright as Typescript requires that + * interfaces extend solely off of types with statically known members. + */ +type RestrictMatcher = { + [k in keyof M]: T extends R ? M[k] : never; +}; +// #endregion Boilerplate/Helpers + +// #region Generic Matchers +interface GenericMatchers { + /** + * Check whether an array contains EXACTLY the given items (in any order). + * + * Different from {@linkcode expect.arrayContaining} as the latter only checks for subset equality + * (as opposed to full equality). + * + * @param expected - The expected contents of the array, in any order + * @see {@linkcode expect.arrayContaining} + */ + toEqualUnsorted: T extends (infer U)[] ? (expected: U[]) => void : never; + + /** + * Check whether a {@linkcode Map} contains the given key, disregarding its value. + * @param expectedKey - The key whose inclusion is being checked + * @privateRemarks + * While this functionality _could_ be simulated by writing + * `expect(x.get(y)).toBeDefined()` or + * `expect(x).toContain([y, expect.anything()])`, + * this is still preferred due to being more ergonomic and provides better error messsages. + */ + toHaveKey: T extends Map ? (expectedKey: K) => void : never; +} +// #endregion Generic Matchers + +// #region GameManager Matchers +interface GameManagerMatchers { + /** + * Check if the {@linkcode GameManager} has shown the given message at least once in the current test case. + * @param expectedMessage - The expected message to be displayed + * @remarks + * Strings consumed by this function should _always_ be produced by a call to `i18next.t` + * to avoid hardcoding text into test files. + */ + toHaveShownMessage(expectedMessage: string): void; + + /** + * Check if the currently-running {@linkcode Phase} is of the given type. + * @param expectedPhase - The expected {@linkcode PhaseString | name of the phase} + */ + toBeAtPhase(expectedPhase: PhaseString): void; +} // #endregion GameManager Matchers + +// #region Arena Matchers +interface ArenaMatchers { + /** + * Check whether the current {@linkcode WeatherType} is as expected. + * @param expectedWeatherType - The expected `WeatherType` + */ + toHaveWeather(expectedWeatherType: WeatherType): void; + + /** + * Check whether the current {@linkcode TerrainType} is as expected. + * @param expectedTerrainType - The expected `TerrainType` + */ + toHaveTerrain(expectedTerrainType: TerrainType): void; + + /** + * Check whether the current {@linkcode Arena} contains the given {@linkcode ArenaTag}. + * @param expectedTag - A partially-filled `ArenaTag` containing the desired properties + */ + toHaveArenaTag(expectedTag: toHaveArenaTagOptions): void; + /** + * Check whether the current {@linkcode Arena} contains the given {@linkcode ArenaTag}. + * @param expectedType - The {@linkcode ArenaTagType} of the desired tag + * @param side - The {@linkcode ArenaTagSide | side(s) of the field} the tag should affect; default {@linkcode ArenaTagSide.BOTH} + */ + toHaveArenaTag(expectedType: ArenaTagType, side?: ArenaTagSide): void; + + /** + * Check whether the current {@linkcode Arena} contains the given {@linkcode PositionalTag}. + * @param expectedTag - A partially-filled `PositionalTag` containing the desired properties + */ + toHavePositionalTag

(expectedTag: toHavePositionalTagOptions

): void; + /** + * Check whether the current {@linkcode Arena} contains the given number of {@linkcode PositionalTag}s. + * @param expectedType - The {@linkcode PositionalTagType} of the desired tag + * @param count - The number of instances of `expectedType` that should be active; + * defaults to `1` and must be within the range `[0, 4]` + */ + toHavePositionalTag(expectedType: PositionalTagType, count?: number): void; +} + +// #endregion Arena Matchers + +// #region Pokemon Matchers +interface PokemonMatchers { + /** + * Check whether a {@linkcode Pokemon}'s current typing includes the given types. + * @param expectedTypes - The expected {@linkcode PokemonType}s to check against; must have length `>0` + * @param options - The {@linkcode toHaveTypesOptions | options} passed to the matcher + */ + toHaveTypes(expectedTypes: PokemonType[], options?: toHaveTypesOptions): void; + + /** + * Check whether a {@linkcode Pokemon} has used a move matching the given criteria. + * @param expectedMove - The {@linkcode MoveId} the Pokemon is expected to have used, + * or a partially filled {@linkcode TurnMove} containing the desired properties to check + * @param index - The index of the move history entry to check, in order from most recent to least recent; default `0` + * @see {@linkcode Pokemon.getLastXMoves} + */ + toHaveUsedMove(expectedMove: MoveId | AtLeastOne, index?: number): void; + + /** + * Check whether a {@linkcode Pokemon}'s effective stat is as expected + * (checked after all stat value modifications). + * @param stat - The {@linkcode EffectiveStat} to check + * @param expectedValue - The expected value of {@linkcode stat} + * @param options - The {@linkcode toHaveEffectiveStatOptions | options} passed to the matcher + * @remarks + * If you want to check the stat **before** modifiers are applied, use {@linkcode Pokemon.getStat} instead. + */ + toHaveEffectiveStat(stat: EffectiveStat, expectedValue: number, options?: toHaveEffectiveStatOptions): void; + + /** + * Check whether a {@linkcode Pokemon} has a specific {@linkcode StatusEffect | non-volatile status effect}. + * @param expectedStatusEffect - The {@linkcode StatusEffect} the Pokemon is expected to have, + * or a partially filled {@linkcode Status} containing the desired properties + */ + toHaveStatusEffect(expectedStatusEffect: expectedStatusType): void; + + /** + * Check whether a {@linkcode Pokemon} has a specific {@linkcode Stat} stage. + * @param stat - The {@linkcode BattleStat} to check + * @param expectedStage - The expected stat stage value of {@linkcode stat} + */ + toHaveStatStage(stat: BattleStat, expectedStage: number): void; + + /** + * Check whether a {@linkcode Pokemon} has the given {@linkcode BattlerTag}. + * @param expectedTag - A partially-filled {@linkcode BattlerTag} containing the desired properties + */ + toHaveBattlerTag(expectedTag: toHaveBattlerTagOptions): void; + /** + * Check whether a {@linkcode Pokemon} has the given {@linkcode BattlerTag}. + * @param expectedType - The expected {@linkcode BattlerTagType} + */ + toHaveBattlerTag(expectedType: BattlerTagType): void; + + /** + * Check whether a {@linkcode Pokemon} has applied a specific {@linkcode AbilityId}. + * @param expectedAbilityId - The `AbilityId` to check for + */ + toHaveAbilityApplied(expectedAbilityId: AbilityId): void; + + /** + * Check whether a {@linkcode Pokemon} has a specific amount of {@linkcode Stat.HP | HP}. + * @param expectedHp - The expected amount of {@linkcode Stat.HP | HP} to have + */ + toHaveHp(expectedHp: number): void; + + /** + * Check whether a {@linkcode Pokemon} has taken a specific amount of damage. + * @param expectedDamageTaken - The expected amount of damage taken + * @param roundDown - Whether to round down `expectedDamageTaken` with {@linkcode toDmgValue}; default `true` + */ + toHaveTakenDamage(expectedDamageTaken: number, roundDown?: boolean): void; + + /** + * Check whether a {@linkcode Pokemon} is currently fainted (as determined by {@linkcode Pokemon.isFainted}). + * @remarks + * When checking whether an enemy wild Pokemon is fainted, one must store a reference to it in a variable _before_ the fainting effect occurs. + * Otherwise, the Pokemon will be removed from the field and garbage collected. + */ + toHaveFainted(): void; + + /** + * Check whether a {@linkcode Pokemon} is at full HP. + */ + toHaveFullHp(): void; + + /** + * Check whether a {@linkcode Pokemon} has consumed the given amount of PP for one of its moves. + * @param moveId - The {@linkcode MoveId} corresponding to the {@linkcode PokemonMove} that should have consumed PP + * @param ppUsed - The numerical amount of PP that should have been consumed, + * or `all` to indicate the move should be _out_ of PP + * @remarks + * If the Pokemon's moveset has been set via {@linkcode Overrides.MOVESET_OVERRIDE}/{@linkcode Overrides.ENEMY_MOVESET_OVERRIDE} + * or does not contain exactly one copy of `moveId`, this will fail the test. + */ + toHaveUsedPP(moveId: MoveId, ppUsed: number | "all"): void; +} +// #endregion Pokemon Matchers diff --git a/test/setup/matchers.setup.ts b/test/setup/matchers.setup.ts index 88ca0a5c6bc..8ad14c8679a 100644 --- a/test/setup/matchers.setup.ts +++ b/test/setup/matchers.setup.ts @@ -1,5 +1,11 @@ +/** + * Setup file for custom matchers. + * Make sure to define the call signatures in `#test/@types/vitest.d.ts` too! + * @module + */ + import { toBeAtPhase } from "#test/test-utils/matchers/to-be-at-phase"; -import { toEqualArrayUnsorted } from "#test/test-utils/matchers/to-equal-array-unsorted"; +import { toEqualUnsorted } from "#test/test-utils/matchers/to-equal-unsorted"; import { toHaveAbilityApplied } from "#test/test-utils/matchers/to-have-ability-applied"; import { toHaveArenaTag } from "#test/test-utils/matchers/to-have-arena-tag"; import { toHaveBattlerTag } from "#test/test-utils/matchers/to-have-battler-tag"; @@ -7,6 +13,7 @@ import { toHaveEffectiveStat } from "#test/test-utils/matchers/to-have-effective import { toHaveFainted } from "#test/test-utils/matchers/to-have-fainted"; import { toHaveFullHp } from "#test/test-utils/matchers/to-have-full-hp"; import { toHaveHp } from "#test/test-utils/matchers/to-have-hp"; +import { toHaveKey } from "#test/test-utils/matchers/to-have-key"; import { toHavePositionalTag } from "#test/test-utils/matchers/to-have-positional-tag"; import { toHaveShownMessage } from "#test/test-utils/matchers/to-have-shown-message"; import { toHaveStatStage } from "#test/test-utils/matchers/to-have-stat-stage"; @@ -19,13 +26,9 @@ import { toHaveUsedPP } from "#test/test-utils/matchers/to-have-used-pp"; import { toHaveWeather } from "#test/test-utils/matchers/to-have-weather"; import { expect } from "vitest"; -/* - * Setup file for custom matchers. - * Make sure to define the call signatures in `#test/@types/vitest.d.ts` too! - */ - expect.extend({ - toEqualArrayUnsorted, + toEqualUnsorted, + toHaveKey, toHaveShownMessage, toBeAtPhase, toHaveWeather, diff --git a/test/test-utils/helpers/modifiers-helper.ts b/test/test-utils/helpers/modifiers-helper.ts index bfda35427fa..7d3e29c420f 100644 --- a/test/test-utils/helpers/modifiers-helper.ts +++ b/test/test-utils/helpers/modifiers-helper.ts @@ -40,10 +40,7 @@ export class ModifierHelper extends GameManagerHelper { * @returns `this` */ testCheck(modifier: ModifierTypeKeys, expectToBePreset: boolean): this { - if (expectToBePreset) { - expect(itemPoolChecks.get(modifier)).toBeTruthy(); - } - expect(itemPoolChecks.get(modifier)).toBeFalsy(); + (expectToBePreset ? expect(itemPoolChecks) : expect(itemPoolChecks).not).toHaveKey(modifier); return this; } diff --git a/test/test-utils/matchers/to-equal-array-unsorted.ts b/test/test-utils/matchers/to-equal-unsorted.ts similarity index 92% rename from test/test-utils/matchers/to-equal-array-unsorted.ts rename to test/test-utils/matchers/to-equal-unsorted.ts index 97398689032..c3d85288815 100644 --- a/test/test-utils/matchers/to-equal-array-unsorted.ts +++ b/test/test-utils/matchers/to-equal-unsorted.ts @@ -8,11 +8,7 @@ import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; * @param expected - The array to check equality with * @returns Whether the matcher passed */ -export function toEqualArrayUnsorted( - this: MatcherState, - received: unknown, - expected: unknown[], -): SyncExpectationResult { +export function toEqualUnsorted(this: MatcherState, received: unknown, expected: unknown[]): SyncExpectationResult { if (!Array.isArray(received)) { return { pass: this.isNot, diff --git a/test/test-utils/matchers/to-have-key.ts b/test/test-utils/matchers/to-have-key.ts new file mode 100644 index 00000000000..73d442fc979 --- /dev/null +++ b/test/test-utils/matchers/to-have-key.ts @@ -0,0 +1,47 @@ +import { getOnelineDiffStr } from "#test/test-utils/string-utils"; +import { receivedStr } from "#test/test-utils/test-utils"; +import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; + +/** + * Matcher that checks if a {@linkcode Map} contains the given key, regardless of its value. + * @param received - The received value. Should be a Map + * @param expectedKey - The key whose inclusion in the map is being checked + * @returns Whether the matcher passed + */ +export function toHaveKey(this: MatcherState, received: unknown, expectedKey: unknown): SyncExpectationResult { + if (!(received instanceof Map)) { + return { + pass: this.isNot, + message: () => `Expected to receive a Map, but got ${receivedStr(received)}!`, + }; + } + + if (received.size === 0) { + return { + pass: this.isNot, + message: () => "Expected to receive a non-empty Map, but received map was empty!", + expected: expectedKey, + actual: received, + }; + } + + const keys = [...received.keys()]; + const pass = this.equals(keys, expectedKey, [ + ...this.customTesters, + this.utils.iterableEquality, + this.utils.subsetEquality, + ]); + + const actualStr = getOnelineDiffStr.call(this, received); + const expectedStr = getOnelineDiffStr.call(this, expectedKey); + + return { + pass, + message: () => + pass + ? `Expected ${actualStr} to NOT have the key ${expectedStr}, but it did!` + : `Expected ${actualStr} to have the key ${expectedStr}, but it didn't!`, + expected: expectedKey, + actual: keys, + }; +} diff --git a/test/test-utils/matchers/to-have-terrain.ts b/test/test-utils/matchers/to-have-terrain.ts index f951abed0b3..9b6939168f0 100644 --- a/test/test-utils/matchers/to-have-terrain.ts +++ b/test/test-utils/matchers/to-have-terrain.ts @@ -8,8 +8,8 @@ import { isGameManagerInstance, receivedStr } from "#test/test-utils/test-utils" import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; /** - * Matcher that checks if the {@linkcode TerrainType} is as expected - * @param received - The object to check. Should be an instance of {@linkcode GameManager}. + * Matcher that checks if the current {@linkcode TerrainType} is as expected. + * @param received - The object to check. Should be the current {@linkcode GameManager}. * @param expectedTerrainType - The expected {@linkcode TerrainType}, or {@linkcode TerrainType.NONE} if no terrain should be active * @returns Whether the matcher passed */ diff --git a/test/test-utils/matchers/to-have-weather.ts b/test/test-utils/matchers/to-have-weather.ts index ffb1e0aad97..7604cd5f890 100644 --- a/test/test-utils/matchers/to-have-weather.ts +++ b/test/test-utils/matchers/to-have-weather.ts @@ -8,8 +8,8 @@ import { toTitleCase } from "#utils/strings"; import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; /** - * Matcher that checks if the {@linkcode WeatherType} is as expected - * @param received - The object to check. Expects an instance of {@linkcode GameManager}. + * Matcher that checks if the current {@linkcode WeatherType} is as expected. + * @param received - The object to check. Should be the current {@linkcode GameManager} * @param expectedWeatherType - The expected {@linkcode WeatherType} * @returns Whether the matcher passed */ From d02980dd4efb6237205375c1f786a47e4e5be342 Mon Sep 17 00:00:00 2001 From: Dean <69436131+emdeann@users.noreply.github.com> Date: Sat, 20 Sep 2025 15:32:31 -0700 Subject: [PATCH 32/40] [Move] Fully implement Healing wish (/Lunar Dance) and remove `nextCommandPhaseQueue` (#6027) * Remove NCPQ * Implement PendingHealTag * Fix test * Code review * Use message directly instead of as key in tag * Update tag for serialization * Update test import * Update src/data/arena-tag.ts Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com> * Remove isNullOrUndefined uses * Fix arena tag type(o) * Fix pendinghealtag * Fix hwish tests * Arena tag denesting --------- Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com> Co-authored-by: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> --- src/data/arena-tag.ts | 143 ++++++++++++ src/data/moves/move.ts | 29 +-- src/enums/arena-tag-type.ts | 1 + src/phase-manager.ts | 12 +- src/phases/pokemon-heal-phase.ts | 3 +- src/phases/post-summon-phase.ts | 4 + src/ui/containers/arena-flyout.ts | 6 + test/moves/healing-wish-lunar-dance.test.ts | 245 ++++++++++++++++++++ test/moves/lunar-dance.test.ts | 73 ------ 9 files changed, 414 insertions(+), 102 deletions(-) create mode 100644 test/moves/healing-wish-lunar-dance.test.ts delete mode 100644 test/moves/lunar-dance.test.ts diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index 7d78076e06b..fd64e271758 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -56,6 +56,7 @@ import { allMoves } from "#data/data-lists"; import { AbilityId } from "#enums/ability-id"; import { ArenaTagSide } from "#enums/arena-tag-side"; import { ArenaTagType } from "#enums/arena-tag-type"; +import type { BattlerIndex } from "#enums/battler-index"; import { BattlerTagType } from "#enums/battler-tag-type"; import { HitResult } from "#enums/hit-result"; import { CommonAnim } from "#enums/move-anims-common"; @@ -1597,6 +1598,145 @@ export class SuppressAbilitiesTag extends SerializableArenaTag { } } +/** + * Interface containing data related to a queued healing effect from + * {@link https://bulbapedia.bulbagarden.net/wiki/Healing_Wish_(move) | Healing Wish} + * or {@link https://bulbapedia.bulbagarden.net/wiki/Lunar_Dance_(move) | Lunar Dance}. + */ +interface PendingHealEffect { + /** The {@linkcode Pokemon.id | PID} of the {@linkcode Pokemon} that created the effect. */ + readonly sourceId: number; + /** The {@linkcode MoveId} of the move that created the effect. */ + readonly moveId: MoveId; + /** If `true`, also restores the target's PP when the effect activates. */ + readonly restorePP: boolean; + /** The message to display when the effect activates */ + readonly healMessage: string; +} + +/** + * Arena tag to contain stored healing effects, namely from + * {@link https://bulbapedia.bulbagarden.net/wiki/Healing_Wish_(move) | Healing Wish} + * and {@link https://bulbapedia.bulbagarden.net/wiki/Lunar_Dance_(move) | Lunar Dance}. + * When a damaged Pokemon first enters the effect's {@linkcode BattlerIndex | field position}, + * their HP is fully restored, and they are cured of any non-volatile status condition. + * If the effect is from Lunar Dance, their PP is also restored. + */ +export class PendingHealTag extends SerializableArenaTag { + public readonly tagType = ArenaTagType.PENDING_HEAL; + /** All pending healing effects, organized by {@linkcode BattlerIndex} */ + public readonly pendingHeals: Partial> = {}; + + constructor() { + super(0); + } + + /** + * Adds a pending healing effect to the field. Effects under the same move *and* + * target index as an existing effect are ignored. + * @param targetIndex - The {@linkcode BattlerIndex} under which the effect applies + * @param healEffect - The {@linkcode PendingHealEffect | data} for the pending heal effect + */ + public queueHeal(targetIndex: BattlerIndex, healEffect: PendingHealEffect): void { + const existingHealEffects = this.pendingHeals[targetIndex]; + if (existingHealEffects) { + if (!existingHealEffects.some(he => he.moveId === healEffect.moveId)) { + existingHealEffects.push(healEffect); + } + } else { + this.pendingHeals[targetIndex] = [healEffect]; + } + } + + /** Removes default on-remove message */ + override onRemove(_arena: Arena): void {} + + /** This arena tag is removed at the end of the turn if no pending healing effects are on the field */ + override lapse(_arena: Arena): boolean { + for (const key in this.pendingHeals) { + if (this.pendingHeals[key].length > 0) { + return true; + } + } + return false; + } + + /** + * Applies a pending healing effect on the given target index. If an effect is found for + * the index, the Pokemon at that index is healed to full HP, is cured of any non-volatile status, + * and has its PP fully restored (if the effect is from Lunar Dance). + * @param arena - The {@linkcode Arena} containing this tag + * @param simulated - If `true`, suppresses changes to game state + * @param pokemon - The {@linkcode Pokemon} receiving the healing effect + * @returns `true` if the target Pokemon was healed by this effect + * @todo This should also be called when a Pokemon moves into a new position via Ally Switch + */ + override apply(arena: Arena, simulated: boolean, pokemon: Pokemon): boolean { + const targetIndex = pokemon.getBattlerIndex(); + const targetEffects = this.pendingHeals[targetIndex]; + + if (targetEffects == null || targetEffects.length === 0) { + return false; + } + + const healEffect = targetEffects.find(effect => this.canApply(effect, pokemon)); + + if (healEffect == null) { + return false; + } + + if (simulated) { + return true; + } + + const { sourceId, moveId, restorePP, healMessage } = healEffect; + const sourcePokemon = globalScene.getPokemonById(sourceId); + if (!sourcePokemon) { + console.warn(`Source of pending ${allMoves[moveId].name} effect is undefined!`); + targetEffects.splice(targetEffects.indexOf(healEffect), 1); + // Re-evaluate after the invalid heal effect is removed + return this.apply(arena, simulated, pokemon); + } + + globalScene.phaseManager.unshiftNew( + "PokemonHealPhase", + targetIndex, + pokemon.getMaxHp(), + healMessage, + true, + false, + false, + true, + false, + restorePP, + ); + + targetEffects.splice(targetEffects.indexOf(healEffect), 1); + + return healEffect != null; + } + + /** + * Determines if the given {@linkcode PendingHealEffect} can immediately heal + * the given target {@linkcode Pokemon}. + * @param healEffect - The {@linkcode PendingHealEffect} to evaluate + * @param pokemon - The {@linkcode Pokemon} to evaluate against + * @returns `true` if the Pokemon can be healed by the effect + */ + private canApply(healEffect: PendingHealEffect, pokemon: Pokemon): boolean { + return ( + !pokemon.isFullHp() + || pokemon.status != null + || (healEffect.restorePP && pokemon.getMoveset().some(mv => mv.ppUsed > 0)) + ); + } + + override loadTag(source: BaseArenaTag & Pick): void { + super.loadTag(source); + (this as Mutable).pendingHeals = source.pendingHeals; + } +} + // TODO: swap `sourceMove` and `sourceId` and make `sourceMove` an optional parameter export function getArenaTag( tagType: ArenaTagType, @@ -1660,6 +1800,8 @@ export function getArenaTag( return new FairyLockTag(turnCount, sourceId); case ArenaTagType.NEUTRALIZING_GAS: return new SuppressAbilitiesTag(sourceId); + case ArenaTagType.PENDING_HEAL: + return new PendingHealTag(); default: return null; } @@ -1708,5 +1850,6 @@ export type ArenaTagTypeMap = { [ArenaTagType.GRASS_WATER_PLEDGE]: GrassWaterPledgeTag; [ArenaTagType.FAIRY_LOCK]: FairyLockTag; [ArenaTagType.NEUTRALIZING_GAS]: SuppressAbilitiesTag; + [ArenaTagType.PENDING_HEAL]: PendingHealTag; [ArenaTagType.NONE]: NoneTag; }; diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 0fdb0d01e43..72376b7934f 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -6,7 +6,7 @@ import { loggedInUser } from "#app/account"; import type { GameMode } from "#app/game-mode"; import { globalScene } from "#app/global-scene"; import { getPokemonNameWithAffix } from "#app/messages"; -import type { EntryHazardTag } from "#data/arena-tag"; +import type { EntryHazardTag, PendingHealTag } from "#data/arena-tag"; import { WeakenMoveTypeTag } from "#data/arena-tag"; import { MoveChargeAnim } from "#data/battle-anims"; import { @@ -2150,24 +2150,15 @@ export class SacrificialFullRestoreAttr extends SacrificialAttr { return false; } - // We don't know which party member will be chosen, so pick the highest max HP in the party - const party = user.isPlayer() ? globalScene.getPlayerParty() : globalScene.getEnemyParty(); - const maxPartyMemberHp = party.map(p => p.getMaxHp()).reduce((maxHp: number, hp: number) => Math.max(hp, maxHp), 0); - - const pm = globalScene.phaseManager; - - pm.pushPhase( - pm.create("PokemonHealPhase", - user.getBattlerIndex(), - maxPartyMemberHp, - i18next.t(this.moveMessage, { pokemonName: getPokemonNameWithAffix(user) }), - true, - false, - false, - true, - false, - this.restorePP), - true); + // Add a tag to the field if it doesn't already exist, then queue a delayed healing effect in the user's current slot. + globalScene.arena.addTag(ArenaTagType.PENDING_HEAL, 0, move.id, user.id); // Arguments after first go completely unused + const tag = globalScene.arena.getTag(ArenaTagType.PENDING_HEAL) as PendingHealTag; + tag.queueHeal(user.getBattlerIndex(), { + sourceId: user.id, + moveId: move.id, + restorePP: this.restorePP, + healMessage: i18next.t(this.moveMessage, { pokemonName: getPokemonNameWithAffix(user) }), + }); return true; } diff --git a/src/enums/arena-tag-type.ts b/src/enums/arena-tag-type.ts index 30f053b98bd..717845cf2d9 100644 --- a/src/enums/arena-tag-type.ts +++ b/src/enums/arena-tag-type.ts @@ -34,4 +34,5 @@ export enum ArenaTagType { GRASS_WATER_PLEDGE = "GRASS_WATER_PLEDGE", FAIRY_LOCK = "FAIRY_LOCK", NEUTRALIZING_GAS = "NEUTRALIZING_GAS", + PENDING_HEAL = "PENDING_HEAL", } diff --git a/src/phase-manager.ts b/src/phase-manager.ts index 3fbf68de60d..125ca00786b 100644 --- a/src/phase-manager.ts +++ b/src/phase-manager.ts @@ -233,7 +233,6 @@ export class PhaseManager { /** overrides default of inserting phases to end of phaseQueuePrepend array. Useful for inserting Phases "out of order" */ private phaseQueuePrependSpliceIndex = -1; - private nextCommandPhaseQueue: Phase[] = []; /** Storage for {@linkcode PhasePriorityQueue}s which hold phases whose order dynamically changes */ private dynamicPhaseQueues: PhasePriorityQueue[]; @@ -291,13 +290,12 @@ export class PhaseManager { /** * Adds a phase to nextCommandPhaseQueue, as long as boolean passed in is false * @param phase {@linkcode Phase} the phase to add - * @param defer boolean on which queue to add to, defaults to false, and adds to phaseQueue */ - pushPhase(phase: Phase, defer = false): void { + pushPhase(phase: Phase): void { if (this.getDynamicPhaseType(phase) !== undefined) { this.pushDynamicPhase(phase); } else { - (!defer ? this.phaseQueue : this.nextCommandPhaseQueue).push(phase); + this.phaseQueue.push(phase); } } @@ -324,7 +322,7 @@ export class PhaseManager { * Clears all phase-related stuff, including all phase queues, the current and standby phases, and a splice index */ clearAllPhases(): void { - for (const queue of [this.phaseQueue, this.phaseQueuePrepend, this.conditionalQueue, this.nextCommandPhaseQueue]) { + for (const queue of [this.phaseQueue, this.phaseQueuePrepend, this.conditionalQueue]) { queue.splice(0, queue.length); } this.dynamicPhaseQueues.forEach(queue => queue.clear()); @@ -623,10 +621,6 @@ export class PhaseManager { * Moves everything from nextCommandPhaseQueue to phaseQueue (keeping order) */ private populatePhaseQueue(): void { - if (this.nextCommandPhaseQueue.length > 0) { - this.phaseQueue.push(...this.nextCommandPhaseQueue); - this.nextCommandPhaseQueue.splice(0, this.nextCommandPhaseQueue.length); - } this.phaseQueue.push(new TurnInitPhase()); } diff --git a/src/phases/pokemon-heal-phase.ts b/src/phases/pokemon-heal-phase.ts index 02bb3a0b968..258ddb0b624 100644 --- a/src/phases/pokemon-heal-phase.ts +++ b/src/phases/pokemon-heal-phase.ts @@ -64,7 +64,8 @@ export class PokemonHealPhase extends CommonAnimPhase { } const hasMessage = !!this.message; - const healOrDamage = !pokemon.isFullHp() || this.hpHealed < 0; + const canRestorePP = this.fullRestorePP && pokemon.getMoveset().some(mv => mv.ppUsed > 0); + const healOrDamage = !pokemon.isFullHp() || this.hpHealed < 0 || canRestorePP; const healBlock = pokemon.getTag(BattlerTagType.HEAL_BLOCK) as HealBlockTag; let lastStatusEffect = StatusEffect.NONE; diff --git a/src/phases/post-summon-phase.ts b/src/phases/post-summon-phase.ts index 5de068f2ae5..5f66cf91eca 100644 --- a/src/phases/post-summon-phase.ts +++ b/src/phases/post-summon-phase.ts @@ -2,6 +2,7 @@ import { applyAbAttrs } from "#abilities/apply-ab-attrs"; import { globalScene } from "#app/global-scene"; import { EntryHazardTag } from "#data/arena-tag"; import { MysteryEncounterPostSummonTag } from "#data/battler-tags"; +import { ArenaTagType } from "#enums/arena-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type"; import { StatusEffect } from "#enums/status-effect"; import { PokemonPhase } from "#phases/pokemon-phase"; @@ -16,6 +17,9 @@ export class PostSummonPhase extends PokemonPhase { if (pokemon.status?.effect === StatusEffect.TOXIC) { pokemon.status.toxicTurnCount = 0; } + + globalScene.arena.applyTags(ArenaTagType.PENDING_HEAL, false, pokemon); + globalScene.arena.applyTags(EntryHazardTag, false, pokemon); // If this is mystery encounter and has post summon phase tag, apply post summon effects diff --git a/src/ui/containers/arena-flyout.ts b/src/ui/containers/arena-flyout.ts index ab95d1a3e7a..355f3edb293 100644 --- a/src/ui/containers/arena-flyout.ts +++ b/src/ui/containers/arena-flyout.ts @@ -285,6 +285,12 @@ export class ArenaFlyout extends Phaser.GameObjects.Container { switch (arenaEffectChangedEvent.constructor) { case TagAddedEvent: { const tagAddedEvent = arenaEffectChangedEvent as TagAddedEvent; + + const excludedTags = [ArenaTagType.PENDING_HEAL]; + if (excludedTags.includes(tagAddedEvent.arenaTagType)) { + return; + } + const isArenaTrapTag = globalScene.arena.getTag(tagAddedEvent.arenaTagType) instanceof EntryHazardTag; let arenaEffectType: ArenaEffectType; diff --git a/test/moves/healing-wish-lunar-dance.test.ts b/test/moves/healing-wish-lunar-dance.test.ts new file mode 100644 index 00000000000..0dcf993aeac --- /dev/null +++ b/test/moves/healing-wish-lunar-dance.test.ts @@ -0,0 +1,245 @@ +import { AbilityId } from "#enums/ability-id"; +import { ArenaTagType } from "#enums/arena-tag-type"; +import { Challenges } from "#enums/challenges"; +import { MoveId } from "#enums/move-id"; +import { MoveResult } from "#enums/move-result"; +import { PokemonType } from "#enums/pokemon-type"; +import { SpeciesId } from "#enums/species-id"; +import { StatusEffect } from "#enums/status-effect"; +import { GameManager } from "#test/test-utils/game-manager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Moves - Lunar Dance and Healing Wish", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override.battleStyle("double").enemyAbility(AbilityId.BALL_FETCH).enemyMoveset(MoveId.SPLASH); + }); + + describe.each([ + { moveName: "Healing Wish", moveId: MoveId.HEALING_WISH }, + { moveName: "Lunar Dance", moveId: MoveId.LUNAR_DANCE }, + ])("$moveName", ({ moveId }) => { + it("should sacrifice the user to restore the switched in Pokemon's HP", async () => { + await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER, SpeciesId.SQUIRTLE]); + + const [bulbasaur, charmander, squirtle] = game.scene.getPlayerParty(); + squirtle.hp = 1; + + game.move.use(MoveId.SPLASH, 0); + game.move.use(moveId, 1); + game.doSelectPartyPokemon(2); + + await game.toNextTurn(); + + expect(bulbasaur.isFullHp()).toBe(true); + expect(charmander.isFainted()).toBe(true); + expect(squirtle.isFullHp()).toBe(true); + }); + + it("should sacrifice the user to cure the switched in Pokemon's status", async () => { + game.override.statusEffect(StatusEffect.BURN); + + await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER, SpeciesId.SQUIRTLE]); + const [bulbasaur, charmander, squirtle] = game.scene.getPlayerParty(); + + game.move.use(MoveId.SPLASH, 0); + game.move.use(moveId, 1); + game.doSelectPartyPokemon(2); + + await game.toNextTurn(); + + expect(bulbasaur.status?.effect).toBe(StatusEffect.BURN); + expect(charmander.isFainted()).toBe(true); + expect(squirtle.status?.effect).toBeUndefined(); + }); + + it("should fail if the user has no non-fainted allies in their party", async () => { + game.override.battleStyle("single"); + + await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER]); + const [bulbasaur, charmander] = game.scene.getPlayerParty(); + + game.move.use(MoveId.MEMENTO); + game.doSelectPartyPokemon(1); + + await game.toNextTurn(); + + expect(bulbasaur.isFainted()).toBe(true); + expect(charmander.isActive(true)).toBe(true); + + game.move.use(moveId); + + await game.toEndOfTurn(); + + expect(charmander.isFullHp()); + expect(charmander.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + }); + + it("should fail if the user has no challenge-eligible allies", async () => { + game.override.battleStyle("single"); + // Mono normal challenge + game.challengeMode.addChallenge(Challenges.SINGLE_TYPE, PokemonType.NORMAL + 1, 0); + await game.challengeMode.startBattle([SpeciesId.RATICATE, SpeciesId.ODDISH]); + + const raticate = game.field.getPlayerPokemon(); + + game.move.use(moveId); + await game.toNextTurn(); + + expect(raticate.isFullHp()).toBe(true); + expect(raticate.getLastXMoves()[0].result).toEqual(MoveResult.FAIL); + }); + + it("should store its effect if the switched-in Pokemon would be unaffected", async () => { + game.override.battleStyle("single"); + + await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER, SpeciesId.SQUIRTLE]); + + const [bulbasaur, charmander, squirtle] = game.scene.getPlayerParty(); + squirtle.hp = 1; + + game.move.use(moveId); + game.doSelectPartyPokemon(1); + + await game.toNextTurn(); + + // Bulbasaur fainted and stored a healing effect + expect(bulbasaur.isFainted()).toBe(true); + expect(charmander.isFullHp()).toBe(true); + expect(game.phaseInterceptor.log).not.toContain("PokemonHealPhase"); + expect(game.scene.arena.getTag(ArenaTagType.PENDING_HEAL)).toBeDefined(); + + // Switch to damaged Squirtle. HW/LD's effect should activate + game.doSwitchPokemon(2); + + await game.toEndOfTurn(); + expect(squirtle.isFullHp()).toBe(true); + expect(game.scene.arena.getTag(ArenaTagType.PENDING_HEAL)).toBeUndefined(); + + // Set Charmander's HP to 1, then switch back to Charmander. + // HW/LD shouldn't activate again + charmander.hp = 1; + game.doSwitchPokemon(2); + + await game.toEndOfTurn(); + expect(charmander.hp).toBe(1); + }); + + it("should only store one charge of the effect at a time", async () => { + game.override.battleStyle("single"); + + await game.classicMode.startBattle([ + SpeciesId.BULBASAUR, + SpeciesId.CHARMANDER, + SpeciesId.SQUIRTLE, + SpeciesId.PIKACHU, + ]); + + const [bulbasaur, charmander, squirtle, pikachu] = game.scene.getPlayerParty(); + [squirtle, pikachu].forEach(p => (p.hp = 1)); + + // Use HW/LD and send in Charmander. HW/LD's effect should be stored + game.move.use(moveId); + game.doSelectPartyPokemon(1); + + await game.toNextTurn(); + expect(bulbasaur.isFainted()).toBe(true); + expect(charmander.isFullHp()).toBe(true); + expect(charmander.isFullHp()); + expect(game.phaseInterceptor.log).not.toContain("PokemonHealPhase"); + expect(game.scene.arena.getTag(ArenaTagType.PENDING_HEAL)).toBeDefined(); + + // Use HW/LD again, sending in Squirtle. HW/LD should activate and heal Squirtle + game.move.use(moveId); + game.doSelectPartyPokemon(2); + + await game.toNextTurn(); + expect(charmander.isFainted()).toBe(true); + expect(squirtle.isFullHp()).toBe(true); + expect(squirtle.isFullHp()); + + // Switch again to Pikachu. HW/LD's effect shouldn't be present + game.doSwitchPokemon(3); + + expect(pikachu.isFullHp()).toBe(false); + }); + }); + + it("Lunar Dance should sacrifice the user to restore the switched in Pokemon's PP", async () => { + game.override.battleStyle("single"); + + await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER]); + + const [bulbasaur, charmander] = game.scene.getPlayerParty(); + + game.move.use(MoveId.SPLASH); + await game.toNextTurn(); + + game.doSwitchPokemon(1); + await game.toNextTurn(); + + game.move.use(MoveId.LUNAR_DANCE); + game.doSelectPartyPokemon(1); + + await game.toNextTurn(); + expect(charmander.isFainted()).toBeTruthy(); + bulbasaur.getMoveset().forEach(mv => expect(mv.ppUsed).toBe(0)); + }); + + it("should stack with each other", async () => { + game.override.battleStyle("single"); + + await game.classicMode.startBattle([ + SpeciesId.BULBASAUR, + SpeciesId.CHARMANDER, + SpeciesId.SQUIRTLE, + SpeciesId.PIKACHU, + ]); + + const [bulbasaur, charmander, squirtle, pikachu] = game.scene.getPlayerParty(); + [squirtle, pikachu].forEach(p => { + p.hp = 1; + p.getMoveset().forEach(mv => (mv.ppUsed = 1)); + }); + + game.move.use(MoveId.LUNAR_DANCE); + game.doSelectPartyPokemon(1); + + await game.toNextTurn(); + expect(bulbasaur.isFainted()).toBe(true); + expect(charmander.isFullHp()).toBe(true); + expect(game.phaseInterceptor.log).not.toContain("PokemonHealPhase"); + expect(game.scene.arena.getTag(ArenaTagType.PENDING_HEAL)).toBeDefined(); + + game.move.use(MoveId.HEALING_WISH); + game.doSelectPartyPokemon(2); + + // Lunar Dance should apply first since it was used first, restoring Squirtle's HP and PP + await game.toNextTurn(); + expect(squirtle.isFullHp()).toBe(true); + squirtle.getMoveset().forEach(mv => expect(mv.ppUsed).toBe(0)); + expect(game.scene.arena.getTag(ArenaTagType.PENDING_HEAL)).toBeDefined(); + + game.doSwitchPokemon(3); + + // Healing Wish should apply on the next switch, restoring Pikachu's HP + await game.toEndOfTurn(); + expect(pikachu.isFullHp()).toBe(true); + pikachu.getMoveset().forEach(mv => expect(mv.ppUsed).toBe(1)); + expect(game.scene.arena.getTag(ArenaTagType.PENDING_HEAL)).toBeUndefined(); + }); +}); diff --git a/test/moves/lunar-dance.test.ts b/test/moves/lunar-dance.test.ts deleted file mode 100644 index 7386d15079b..00000000000 --- a/test/moves/lunar-dance.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { AbilityId } from "#enums/ability-id"; -import { MoveId } from "#enums/move-id"; -import { SpeciesId } from "#enums/species-id"; -import { StatusEffect } from "#enums/status-effect"; -import { GameManager } from "#test/test-utils/game-manager"; -import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; - -describe("Moves - Lunar Dance", () => { - let phaserGame: Phaser.Game; - let game: GameManager; - - beforeAll(() => { - phaserGame = new Phaser.Game({ - type: Phaser.HEADLESS, - }); - }); - - afterEach(() => { - game.phaseInterceptor.restoreOg(); - }); - - beforeEach(() => { - game = new GameManager(phaserGame); - game.override - .statusEffect(StatusEffect.BURN) - .battleStyle("double") - .enemyAbility(AbilityId.BALL_FETCH) - .enemyMoveset(MoveId.SPLASH); - }); - - it("should full restore HP, PP and status of switched in pokemon, then fail second use because no remaining backup pokemon in party", async () => { - await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.ODDISH, SpeciesId.RATTATA]); - - const [bulbasaur, oddish, rattata] = game.scene.getPlayerParty(); - game.move.changeMoveset(bulbasaur, [MoveId.LUNAR_DANCE, MoveId.SPLASH]); - game.move.changeMoveset(oddish, [MoveId.LUNAR_DANCE, MoveId.SPLASH]); - game.move.changeMoveset(rattata, [MoveId.LUNAR_DANCE, MoveId.SPLASH]); - - game.move.select(MoveId.SPLASH, 0); - game.move.select(MoveId.SPLASH, 1); - await game.toNextTurn(); - - // Bulbasaur should still be burned and have used a PP for splash and not at max hp - expect(bulbasaur.status?.effect).toBe(StatusEffect.BURN); - expect(bulbasaur.moveset[1]?.ppUsed).toBe(1); - expect(bulbasaur.hp).toBeLessThan(bulbasaur.getMaxHp()); - - // Switch out Bulbasaur for Rattata so we can swtich bulbasaur back in with lunar dance - game.doSwitchPokemon(2); - game.move.select(MoveId.SPLASH, 1); - await game.toNextTurn(); - - game.move.select(MoveId.SPLASH, 0); - game.move.select(MoveId.LUNAR_DANCE); - game.doSelectPartyPokemon(2); - await game.phaseInterceptor.to("SwitchPhase", false); - await game.toNextTurn(); - - // Bulbasaur should NOT have any status and have full PP for splash and be at max hp - expect(bulbasaur.status?.effect).toBeUndefined(); - expect(bulbasaur.moveset[1]?.ppUsed).toBe(0); - expect(bulbasaur.isFullHp()).toBe(true); - - game.move.select(MoveId.SPLASH, 0); - game.move.select(MoveId.LUNAR_DANCE); - await game.toNextTurn(); - - // Using Lunar dance again should fail because nothing in party and rattata should be alive - expect(rattata.status?.effect).toBe(StatusEffect.BURN); - expect(rattata.hp).toBeLessThan(rattata.getMaxHp()); - }); -}); From 87e6095a001f24e81118e65957f760e7d247834d Mon Sep 17 00:00:00 2001 From: Dean <69436131+emdeann@users.noreply.github.com> Date: Sat, 20 Sep 2025 15:49:40 -0700 Subject: [PATCH 33/40] [Misc/Feature] Add dynamic turn order (#6036) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add new priority queues * Add dynamic queue manager * Add timing modifier and fix post speed ordering * Make `phaseQueue` private * Fix `gameManager.setTurnOrder` * Update `findPhase` to also check dynamic queues * Modify existing phase manager methods to check dynamic queues * Fix move order persisting through tests * Fix magic coat/bounce * Use append for magic coat/bounce * Remove `getSpeedOrder` from `TurnStartPhase`, fix references to `getCommandOrder` in tests * Fix round queuing last instead of next * Add quick draw application * Add quick claw activation * Fix turn order tracking * Add move header queue to fix ordering * Fix abilities activating immediately on summon * Fix `postsummonphases` being shuffled (need to handle speed ties differently here) * Update speed order function * Add `StaticSwitchSummonPhase` * Fix magic coat/bounce error from conflict resolution * Remove conditional queue * Fix dancer and baton pass tests * Automatically queue consecutive Pokémon phases as dynamic * Move turn end phases queuing back to `TurnStartPhase` * Fix `LearnMovePhase` * Remove `PrependSplice` * Move DQM to phase manager * Fix various phases being pushed instead of unshifted * Remove `StaticSwitchSummonPhase` * Ensure the top queue is always at length - 1 * Fix encounter `PostSummonPhase`s and Revival Blessing * Fix move headers * Remove implicit ordering from DQM * Fix `PostSummonPhase`s in encounters running too early * Fix `tryRemovePhase` usages * Add `MovePhase` after `MoveEndPhase` automatically * Implement an `inSpeedOrder` function * Merge fixes * Fix encounter rewards * Defer `FaintPhase`s where splice was used previously * Separate speed order utils to avoid circular imports * Temporarily disable lunar dance test * Simplify deferral * Remove move priority modifier * Fix TS errors in code files * Fix ts errors in tests * Fix more test files * Fix postsummon + checkswitch ability activations * Fix `removeAll` * Reposition `positionalTagPhase` * Re-add `startCurrentPhase` * Avoid overwriting `currentPhase` after `turnStart` * Delete `switchSummonPhasePriorityQueue` * Update `phase-manager.ts` * Remove uses of `isNullOrUndefined` * Rename deferral methods * Update docs and use `getPlayerField(true)` in turn start phase * Use `.getEnemyField(true)` * Update docs for post summon phase priority queue (psppq) * Update speed order utils * Remove null from `nextPhase` * Update move phase timing modifier docs * Remove mention of phases from base priority queue class * Remove and replace `applyInSpeedOrder` * Don't sort weather effect phases * Order priority queues before removing - Add some `readonly` and `public` modifiers - Remove unused `queuedPhases` field from `MoveEffectPhase` * Fix linting in `phase-manager.ts` * Remove unnecessary turn order modification in Rage Fist test --------- Co-authored-by: Bertie690 <136088738+Bertie690@users.noreply.github.com> Co-authored-by: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/@types/phase-types.ts | 13 + src/battle-scene.ts | 31 +- src/data/abilities/ability.ts | 48 +- src/data/abilities/apply-ab-attrs.ts | 1 - src/data/battler-tags.ts | 53 +- src/data/moves/move.ts | 113 +--- .../encounters/fun-and-games-encounter.ts | 2 +- .../the-expert-pokemon-breeder-encounter.ts | 1 - .../utils/encounter-phase-utils.ts | 9 +- src/data/phase-priority-queue.ts | 125 ----- src/dynamic-queue-manager.ts | 182 +++++++ src/enums/arena-tag-side.ts | 1 + src/enums/battler-tag-type.ts | 1 + src/enums/dynamic-phase-type.ts | 7 - src/enums/move-phase-timing-modifier.ts | 16 + src/field/arena.ts | 8 +- src/field/pokemon.ts | 13 +- src/modifier/modifier.ts | 21 +- src/phase-manager.ts | 504 +++++++----------- src/phase-tree.ts | 206 +++++++ src/phases/activate-priority-queue-phase.ts | 23 - src/phases/battle-end-phase.ts | 22 +- src/phases/check-status-effect-phase.ts | 12 +- src/phases/check-switch-phase.ts | 18 +- src/phases/dynamic-phase-marker.ts | 17 + src/phases/egg-hatch-phase.ts | 2 +- src/phases/encounter-phase.ts | 53 +- src/phases/game-over-phase.ts | 18 +- src/phases/learn-move-phase.ts | 4 +- src/phases/move-charge-phase.ts | 2 +- src/phases/move-effect-phase.ts | 45 +- src/phases/move-header-phase.ts | 6 +- src/phases/move-phase.ts | 41 +- src/phases/mystery-encounter-phases.ts | 26 +- src/phases/new-battle-phase.ts | 7 +- src/phases/party-member-pokemon-phase.ts | 4 + .../post-summon-activate-ability-phase.ts | 4 +- src/phases/post-summon-phase.ts | 15 +- src/phases/quiet-form-change-phase.ts | 8 +- src/phases/stat-stage-change-phase.ts | 48 +- src/phases/summon-phase.ts | 22 +- src/phases/switch-phase.ts | 9 - src/phases/switch-summon-phase.ts | 4 +- src/phases/title-phase.ts | 22 +- src/phases/turn-end-phase.ts | 1 + src/phases/turn-start-phase.ts | 127 +---- src/queues/move-phase-priority-queue.ts | 103 ++++ src/queues/pokemon-phase-priority-queue.ts | 20 + src/queues/pokemon-priority-queue.ts | 10 + .../post-summon-phase-priority-queue.ts | 45 ++ src/queues/priority-queue.ts | 78 +++ src/utils/speed-order-generator.ts | 39 ++ src/utils/speed-order.ts | 57 ++ test/abilities/dancer.test.ts | 5 +- test/abilities/mycelium-might.test.ts | 47 +- test/abilities/neutralizing-gas.test.ts | 2 +- test/abilities/quick-draw.test.ts | 41 +- test/abilities/stall.test.ts | 34 +- test/battle/battle-order.test.ts | 71 ++- test/moves/baton-pass.test.ts | 7 +- test/moves/delayed-attack.test.ts | 2 +- test/moves/focus-punch.test.ts | 3 +- test/moves/rage-fist.test.ts | 1 - test/moves/revival-blessing.test.ts | 7 +- test/moves/shell-trap.test.ts | 2 +- test/moves/trick-room.test.ts | 12 +- test/moves/wish.test.ts | 2 +- .../mystery-encounter/encounter-test-utils.ts | 2 - .../the-winstrate-challenge-encounter.test.ts | 1 - test/test-utils/game-manager.ts | 7 +- 70 files changed, 1340 insertions(+), 1173 deletions(-) delete mode 100644 src/data/phase-priority-queue.ts create mode 100644 src/dynamic-queue-manager.ts delete mode 100644 src/enums/dynamic-phase-type.ts create mode 100644 src/enums/move-phase-timing-modifier.ts create mode 100644 src/phase-tree.ts delete mode 100644 src/phases/activate-priority-queue-phase.ts create mode 100644 src/phases/dynamic-phase-marker.ts create mode 100644 src/queues/move-phase-priority-queue.ts create mode 100644 src/queues/pokemon-phase-priority-queue.ts create mode 100644 src/queues/pokemon-priority-queue.ts create mode 100644 src/queues/post-summon-phase-priority-queue.ts create mode 100644 src/queues/priority-queue.ts create mode 100644 src/utils/speed-order-generator.ts create mode 100644 src/utils/speed-order.ts diff --git a/src/@types/phase-types.ts b/src/@types/phase-types.ts index 91673053747..d396375c5fa 100644 --- a/src/@types/phase-types.ts +++ b/src/@types/phase-types.ts @@ -1,3 +1,5 @@ +import type { Pokemon } from "#app/field/pokemon"; +import type { Phase } from "#app/phase"; import type { PhaseConstructorMap } from "#app/phase-manager"; import type { ObjectValues } from "#types/type-helpers"; @@ -24,3 +26,14 @@ export type PhaseClass = ObjectValues; * Union type of all phase names as strings. */ export type PhaseString = keyof PhaseMap; + +/** Type for predicate functions operating on a specific type of {@linkcode Phase}. */ + +export type PhaseConditionFunc = (phase: PhaseMap[T]) => boolean; + +/** + * Interface type representing the assumption that all phases with pokemon associated are dynamic + */ +export interface DynamicPhase extends Phase { + getPokemon(): Pokemon; +} diff --git a/src/battle-scene.ts b/src/battle-scene.ts index cbda368782e..289c9a8f051 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -104,7 +104,6 @@ import { import { MysteryEncounter } from "#mystery-encounters/mystery-encounter"; import { MysteryEncounterSaveData } from "#mystery-encounters/mystery-encounter-save-data"; import { allMysteryEncounters, mysteryEncountersByBiome } from "#mystery-encounters/mystery-encounters"; -import type { MovePhase } from "#phases/move-phase"; import { expSpriteKeys } from "#sprites/sprite-keys"; import { hasExpSprite } from "#sprites/sprite-utils"; import type { Variant } from "#sprites/variant"; @@ -787,12 +786,14 @@ export class BattleScene extends SceneBase { /** * Returns an array of EnemyPokemon of length 1 or 2 depending on if in a double battle or not. - * Does not actually check if the pokemon are on the field or not. + * @param active - (Default `false`) Whether to consider only {@linkcode Pokemon.isActive | active} on-field pokemon * @returns array of {@linkcode EnemyPokemon} */ - public getEnemyField(): EnemyPokemon[] { + public getEnemyField(active = false): EnemyPokemon[] { const party = this.getEnemyParty(); - return party.slice(0, Math.min(party.length, this.currentBattle?.double ? 2 : 1)); + return party + .slice(0, Math.min(party.length, this.currentBattle?.double ? 2 : 1)) + .filter(p => !active || p.isActive()); } /** @@ -817,25 +818,7 @@ export class BattleScene extends SceneBase { * @param allyPokemon - The {@linkcode Pokemon} allied with the removed Pokemon; will have moves redirected to it */ redirectPokemonMoves(removedPokemon: Pokemon, allyPokemon: Pokemon): void { - // failsafe: if not a double battle just return - if (this.currentBattle.double === false) { - return; - } - if (allyPokemon?.isActive(true)) { - let targetingMovePhase: MovePhase; - do { - targetingMovePhase = this.phaseManager.findPhase( - mp => - mp.is("MovePhase") - && mp.targets.length === 1 - && mp.targets[0] === removedPokemon.getBattlerIndex() - && mp.pokemon.isPlayer() !== allyPokemon.isPlayer(), - ) as MovePhase; - if (targetingMovePhase && targetingMovePhase.targets[0] !== allyPokemon.getBattlerIndex()) { - targetingMovePhase.targets[0] = allyPokemon.getBattlerIndex(); - } - } while (targetingMovePhase); - } + this.phaseManager.redirectMoves(removedPokemon, allyPokemon); } /** @@ -1433,7 +1416,7 @@ export class BattleScene extends SceneBase { } if (lastBattle?.double && !newDouble) { - this.phaseManager.tryRemovePhase((p: Phase) => p.is("SwitchPhase")); + this.phaseManager.tryRemovePhase("SwitchPhase"); for (const p of this.getPlayerField()) { p.lapseTag(BattlerTagType.COMMANDED); } diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index ebe8b816e5e..f6494548b99 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -33,6 +33,7 @@ import { CommonAnim } from "#enums/move-anims-common"; import { MoveCategory } from "#enums/move-category"; import { MoveFlags } from "#enums/move-flags"; import { MoveId } from "#enums/move-id"; +import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier"; import { MoveResult } from "#enums/move-result"; import { MoveTarget } from "#enums/move-target"; import { MoveUseMode } from "#enums/move-use-mode"; @@ -2555,7 +2556,7 @@ export class PostIntimidateStatStageChangeAbAttr extends AbAttr { override apply({ pokemon, simulated, cancelled }: AbAttrParamsWithCancel): void { if (!simulated) { - globalScene.phaseManager.pushNew( + globalScene.phaseManager.unshiftNew( "StatStageChangePhase", pokemon.getBattlerIndex(), false, @@ -3240,6 +3241,7 @@ export class CommanderAbAttr extends AbAttr { return ( globalScene.currentBattle?.double && ally != null + && ally.isActive(true) && ally.species.speciesId === SpeciesId.DONDOZO && !(ally.isFainted() || ally.getTag(BattlerTagType.COMMANDED)) ); @@ -3254,7 +3256,7 @@ export class CommanderAbAttr extends AbAttr { // Apply boosts from this effect to the ally Dondozo pokemon.getAlly()?.addTag(BattlerTagType.COMMANDED, 0, MoveId.NONE, pokemon.id); // Cancel the source Pokemon's next move (if a move is queued) - globalScene.phaseManager.tryRemovePhase(phase => phase.is("MovePhase") && phase.pokemon === pokemon); + globalScene.phaseManager.tryRemovePhase("MovePhase", phase => phase.pokemon === pokemon); } } } @@ -5004,7 +5006,14 @@ export class PostDancingMoveAbAttr extends PostMoveUsedAbAttr { // If the move is an AttackMove or a StatusMove the Dancer must replicate the move on the source of the Dance if (move.getMove().is("AttackMove") || move.getMove().is("StatusMove")) { const target = this.getTarget(pokemon, source, targets); - globalScene.phaseManager.unshiftNew("MovePhase", pokemon, target, move, MoveUseMode.INDIRECT); + globalScene.phaseManager.unshiftNew( + "MovePhase", + pokemon, + target, + move, + MoveUseMode.INDIRECT, + MovePhaseTimingModifier.FIRST, + ); } else if (move.getMove().is("SelfStatusMove")) { // If the move is a SelfStatusMove (ie. Swords Dance) the Dancer should replicate it on itself globalScene.phaseManager.unshiftNew( @@ -5013,6 +5022,7 @@ export class PostDancingMoveAbAttr extends PostMoveUsedAbAttr { [pokemon.getBattlerIndex()], move, MoveUseMode.INDIRECT, + MovePhaseTimingModifier.FIRST, ); } } @@ -6028,11 +6038,6 @@ export class IllusionPostBattleAbAttr extends PostBattleAbAttr { } } -export interface BypassSpeedChanceAbAttrParams extends AbAttrBaseParams { - /** Holds whether the speed check is bypassed after ability application */ - bypass: BooleanHolder; -} - /** * If a Pokémon with this Ability selects a damaging move, it has a 30% chance of going first in its priority bracket. If the Ability activates, this is announced at the start of the turn (after move selection). * @sealed @@ -6048,26 +6053,28 @@ export class BypassSpeedChanceAbAttr extends AbAttr { this.chance = chance; } - override canApply({ bypass, simulated, pokemon }: BypassSpeedChanceAbAttrParams): boolean { + override canApply({ simulated, pokemon }: AbAttrBaseParams): boolean { // TODO: Consider whether we can move the simulated check to the `apply` method // May be difficult as we likely do not want to modify the randBattleSeed const turnCommand = globalScene.currentBattle.turnCommands[pokemon.getBattlerIndex()]; - const isCommandFight = turnCommand?.command === Command.FIGHT; const move = turnCommand?.move?.move ? allMoves[turnCommand.move.move] : null; const isDamageMove = move?.category === MoveCategory.PHYSICAL || move?.category === MoveCategory.SPECIAL; return ( - !simulated && !bypass.value && pokemon.randBattleSeedInt(100) < this.chance && isCommandFight && isDamageMove + !simulated + && pokemon.randBattleSeedInt(100) < this.chance + && isDamageMove + && pokemon.canAddTag(BattlerTagType.BYPASS_SPEED) ); } /** * bypass move order in their priority bracket when pokemon choose damaging move */ - override apply({ bypass }: BypassSpeedChanceAbAttrParams): void { - bypass.value = true; + override apply({ pokemon }: AbAttrBaseParams): void { + pokemon.addTag(BattlerTagType.BYPASS_SPEED); } - override getTriggerMessage({ pokemon }: BypassSpeedChanceAbAttrParams, _abilityName: string): string { + override getTriggerMessage({ pokemon }: AbAttrBaseParams, _abilityName: string): string { return i18next.t("abilityTriggers:quickDraw", { pokemonName: getPokemonNameWithAffix(pokemon) }); } } @@ -6075,8 +6082,6 @@ export class BypassSpeedChanceAbAttr extends AbAttr { export interface PreventBypassSpeedChanceAbAttrParams extends AbAttrBaseParams { /** Holds whether the speed check is bypassed after ability application */ bypass: BooleanHolder; - /** Holds whether the Pokemon can check held items for Quick Claw's effects */ - canCheckHeldItems: BooleanHolder; } /** @@ -6103,9 +6108,8 @@ export class PreventBypassSpeedChanceAbAttr extends AbAttr { return isCommandFight && this.condition(pokemon, move!); } - override apply({ bypass, canCheckHeldItems }: PreventBypassSpeedChanceAbAttrParams): void { + override apply({ bypass }: PreventBypassSpeedChanceAbAttrParams): void { bypass.value = false; - canCheckHeldItems.value = false; } } @@ -6203,8 +6207,7 @@ class ForceSwitchOutHelper { if (switchOutTarget.hp > 0) { switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH); - globalScene.phaseManager.prependNewToPhase( - "MoveEndPhase", + globalScene.phaseManager.queueDeferred( "SwitchPhase", this.switchType, switchOutTarget.getFieldIndex(), @@ -6226,8 +6229,7 @@ class ForceSwitchOutHelper { const summonIndex = globalScene.currentBattle.trainer ? globalScene.currentBattle.trainer.getNextSummonIndex((switchOutTarget as EnemyPokemon).trainerSlot) : 0; - globalScene.phaseManager.prependNewToPhase( - "MoveEndPhase", + globalScene.phaseManager.queueDeferred( "SwitchSummonPhase", this.switchType, switchOutTarget.getFieldIndex(), @@ -7161,7 +7163,7 @@ export function initAbilities() { new Ability(AbilityId.ANALYTIC, 5) .attr(MovePowerBoostAbAttr, (user) => // Boost power if all other Pokemon have already moved (no other moves are slated to execute) - !globalScene.phaseManager.findPhase((phase) => phase.is("MovePhase") && phase.pokemon.id !== user?.id), + !globalScene.phaseManager.hasPhaseOfType("MovePhase", phase => phase.pokemon.id !== user?.id), 1.3), new Ability(AbilityId.ILLUSION, 5) // The Pokemon generate an illusion if it's available diff --git a/src/data/abilities/apply-ab-attrs.ts b/src/data/abilities/apply-ab-attrs.ts index 58f63c5924a..23b16a4cac7 100644 --- a/src/data/abilities/apply-ab-attrs.ts +++ b/src/data/abilities/apply-ab-attrs.ts @@ -74,7 +74,6 @@ function applyAbAttrsInternal( for (const passive of [false, true]) { params.passive = passive; applySingleAbAttrs(attrType, params, gainedMidTurn, messages); - globalScene.phaseManager.clearPhaseQueueSplice(); } // We need to restore passive to its original state in the case that it was undefined on entry // this is necessary in case this method is called with an object that is reused. diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index b6c3cf2b5a6..8abd98f4683 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -606,17 +606,7 @@ export class ShellTrapTag extends BattlerTag { // Trap should only be triggered by opponent's Physical moves if (phaseData?.move.category === MoveCategory.PHYSICAL && pokemon.isOpponent(phaseData.attacker)) { - const shellTrapPhaseIndex = globalScene.phaseManager.phaseQueue.findIndex( - phase => phase.is("MovePhase") && phase.pokemon === pokemon, - ); - const firstMovePhaseIndex = globalScene.phaseManager.phaseQueue.findIndex(phase => phase.is("MovePhase")); - - // Only shift MovePhase timing if it's not already next up - if (shellTrapPhaseIndex !== -1 && shellTrapPhaseIndex !== firstMovePhaseIndex) { - const shellTrapMovePhase = globalScene.phaseManager.phaseQueue.splice(shellTrapPhaseIndex, 1)[0]; - globalScene.phaseManager.prependToPhase(shellTrapMovePhase, "MovePhase"); - } - + globalScene.phaseManager.forceMoveNext((phase: MovePhase) => phase.pokemon === pokemon); this.activated = true; } @@ -1279,22 +1269,9 @@ export class EncoreTag extends MoveRestrictionBattlerTag { }), ); - const movePhase = globalScene.phaseManager.findPhase(m => m.is("MovePhase") && m.pokemon === pokemon); - if (movePhase) { - const movesetMove = pokemon.getMoveset().find(m => m.moveId === this.moveId); - if (movesetMove) { - const lastMove = pokemon.getLastXMoves(1)[0]; - globalScene.phaseManager.tryReplacePhase( - m => m.is("MovePhase") && m.pokemon === pokemon, - globalScene.phaseManager.create( - "MovePhase", - pokemon, - lastMove.targets ?? [], - movesetMove, - MoveUseMode.NORMAL, - ), - ); - } + const movesetMove = pokemon.getMoveset().find(m => m.moveId === this.moveId); + if (movesetMove) { + globalScene.phaseManager.changePhaseMove((phase: MovePhase) => phase.pokemon === pokemon, movesetMove); } } @@ -3578,6 +3555,25 @@ export class GrudgeTag extends SerializableBattlerTag { } } +/** + * Tag to allow the affected Pokemon's move to go first in its priority bracket. + * Used for {@link https://bulbapedia.bulbagarden.net/wiki/Quick_Draw_(Ability) | Quick Draw} + * and {@link https://bulbapedia.bulbagarden.net/wiki/Quick_Claw | Quick Claw}. + */ +export class BypassSpeedTag extends BattlerTag { + public override readonly tagType = BattlerTagType.BYPASS_SPEED; + + constructor() { + super(BattlerTagType.BYPASS_SPEED, BattlerTagLapseType.TURN_END, 1); + } + + override canAdd(pokemon: Pokemon): boolean { + const bypass = new BooleanHolder(true); + applyAbAttrs("PreventBypassSpeedChanceAbAttr", { pokemon, bypass }); + return bypass.value; + } +} + /** * Tag used to heal the user of Psycho Shift of its status effect if Psycho Shift succeeds in transferring its status effect to the target Pokemon */ @@ -3863,6 +3859,8 @@ export function getBattlerTag( return new MagicCoatTag(); case BattlerTagType.SUPREME_OVERLORD: return new SupremeOverlordTag(); + case BattlerTagType.BYPASS_SPEED: + return new BypassSpeedTag(); } } @@ -3998,4 +3996,5 @@ export type BattlerTagTypeMap = { [BattlerTagType.PSYCHO_SHIFT]: PsychoShiftTag; [BattlerTagType.MAGIC_COAT]: MagicCoatTag; [BattlerTagType.SUPREME_OVERLORD]: SupremeOverlordTag; + [BattlerTagType.BYPASS_SPEED]: BypassSpeedTag; }; diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 72376b7934f..075876d8ddd 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -81,10 +81,8 @@ import { applyMoveAttrs } from "#moves/apply-attrs"; import { invalidAssistMoves, invalidCopycatMoves, invalidMetronomeMoves, invalidMirrorMoveMoves, invalidSketchMoves, invalidSleepTalkMoves } from "#moves/invalid-moves"; import { frenzyMissFunc, getMoveTargets } from "#moves/move-utils"; import { PokemonMove } from "#moves/pokemon-move"; -import { MoveEndPhase } from "#phases/move-end-phase"; import { MovePhase } from "#phases/move-phase"; import { PokemonHealPhase } from "#phases/pokemon-heal-phase"; -import { SwitchSummonPhase } from "#phases/switch-summon-phase"; import type { AttackMoveResult } from "#types/attack-move-result"; import type { Localizable } from "#types/locales"; import type { ChargingMove, MoveAttrMap, MoveAttrString, MoveClassMap, MoveKindString, MoveMessageFunc } from "#types/move-types"; @@ -94,6 +92,7 @@ import { getEnumValues } from "#utils/enums"; import { toCamelCase, toTitleCase } from "#utils/strings"; import i18next from "i18next"; import { applyChallenges } from "#utils/challenge-utils"; +import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier"; import type { AbstractConstructor } from "#types/type-helpers"; /** @@ -891,6 +890,10 @@ export abstract class Move implements Localizable { applyMoveAttrs("IncrementMovePriorityAttr", user, null, this, priority); applyAbAttrs("ChangeMovePriorityAbAttr", {pokemon: user, simulated, move: this, priority}); + if (user.getTag(BattlerTagType.BYPASS_SPEED)) { + priority.value += 0.2; + } + return priority.value; } @@ -3298,7 +3301,7 @@ export class AwaitCombinedPledgeAttr extends OverrideMoveEffectAttr { const overridden = args[0] as BooleanHolder; - const allyMovePhase = globalScene.phaseManager.findPhase((phase) => phase.is("MovePhase") && phase.pokemon.isPlayer() === user.isPlayer()); + const allyMovePhase = globalScene.phaseManager.getMovePhase((phase) => phase.pokemon.isPlayer() === user.isPlayer()); if (allyMovePhase) { const allyMove = allyMovePhase.move.getMove(); if (allyMove !== move && allyMove.hasAttr("AwaitCombinedPledgeAttr")) { @@ -3311,11 +3314,7 @@ export class AwaitCombinedPledgeAttr extends OverrideMoveEffectAttr { })); // Move the ally's MovePhase (if needed) so that the ally moves next - const allyMovePhaseIndex = globalScene.phaseManager.phaseQueue.indexOf(allyMovePhase); - const firstMovePhaseIndex = globalScene.phaseManager.phaseQueue.findIndex((phase) => phase.is("MovePhase")); - if (allyMovePhaseIndex !== firstMovePhaseIndex) { - globalScene.phaseManager.prependToPhase(globalScene.phaseManager.phaseQueue.splice(allyMovePhaseIndex, 1)[0], "MovePhase"); - } + globalScene.phaseManager.forceMoveNext((phase: MovePhase) => phase.pokemon === user.getAlly()); overridden.value = true; return true; @@ -4550,28 +4549,7 @@ export class LastMoveDoublePowerAttr extends VariablePowerAttr { */ apply(user: Pokemon, _target: Pokemon, _move: Move, args: any[]): boolean { const power = args[0] as NumberHolder; - const enemy = user.getOpponent(0); - const pokemonActed: Pokemon[] = []; - - if (enemy?.turnData.acted) { - pokemonActed.push(enemy); - } - - if (globalScene.currentBattle.double) { - const userAlly = user.getAlly(); - const enemyAlly = enemy?.getAlly(); - - if (userAlly?.turnData.acted) { - pokemonActed.push(userAlly); - } - if (enemyAlly?.turnData.acted) { - pokemonActed.push(enemyAlly); - } - } - - pokemonActed.sort((a, b) => b.turnData.order - a.turnData.order); - - for (const p of pokemonActed) { + for (const p of globalScene.phaseManager.dynamicQueueManager.getLastTurnOrder().slice(0, -1).reverse()) { const [ lastMove ] = p.getLastXMoves(1); if (lastMove.result !== MoveResult.FAIL) { if ((lastMove.result === MoveResult.SUCCESS) && (lastMove.move === this.move)) { @@ -4653,20 +4631,13 @@ export class CueNextRoundAttr extends MoveEffectAttr { } override apply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean { - const nextRoundPhase = globalScene.phaseManager.findPhase(phase => - phase.is("MovePhase") && phase.move.moveId === MoveId.ROUND - ); + const nextRoundPhase = globalScene.phaseManager.getMovePhase(phase => phase.move.moveId === MoveId.ROUND); if (!nextRoundPhase) { return false; } - // Update the phase queue so that the next Pokemon using Round moves next - const nextRoundIndex = globalScene.phaseManager.phaseQueue.indexOf(nextRoundPhase); - const nextMoveIndex = globalScene.phaseManager.phaseQueue.findIndex(phase => phase.is("MovePhase")); - if (nextRoundIndex !== nextMoveIndex) { - globalScene.phaseManager.prependToPhase(globalScene.phaseManager.phaseQueue.splice(nextRoundIndex, 1)[0], "MovePhase"); - } + globalScene.phaseManager.forceMoveNext(phase => phase.move.moveId === MoveId.ROUND); // Mark the corresponding Pokemon as having "joined the Round" (for doubling power later) nextRoundPhase.pokemon.turnData.joinedRound = true; @@ -6291,11 +6262,11 @@ export class RevivalBlessingAttr extends MoveEffectAttr { // Handle cases where revived pokemon needs to get switched in on same turn if (allyPokemon.isFainted() || allyPokemon === pokemon) { // Enemy switch phase should be removed and replaced with the revived pkmn switching in - globalScene.phaseManager.tryRemovePhase((phase: SwitchSummonPhase) => phase.is("SwitchSummonPhase") && phase.getPokemon() === pokemon); + globalScene.phaseManager.tryRemovePhase("SwitchSummonPhase", phase => phase.getFieldIndex() === slotIndex); // If the pokemon being revived was alive earlier in the turn, cancel its move // (revived pokemon can't move in the turn they're brought back) // TODO: might make sense to move this to `FaintPhase` after checking for Rev Seed (rather than handling it in the move) - globalScene.phaseManager.findPhase((phase: MovePhase) => phase.pokemon === pokemon)?.cancel(); + globalScene.phaseManager.getMovePhase((phase: MovePhase) => phase.pokemon === pokemon)?.cancel(); if (user.fieldPosition === FieldPosition.CENTER) { user.setFieldPosition(FieldPosition.LEFT); } @@ -6376,8 +6347,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { if (this.switchType === SwitchType.FORCE_SWITCH) { switchOutTarget.leaveField(true); const slotIndex = eligibleNewIndices[user.randBattleSeedInt(eligibleNewIndices.length)]; - globalScene.phaseManager.prependNewToPhase( - "MoveEndPhase", + globalScene.phaseManager.queueDeferred( "SwitchSummonPhase", this.switchType, switchOutTarget.getFieldIndex(), @@ -6387,7 +6357,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { ); } else { switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH); - globalScene.phaseManager.prependNewToPhase("MoveEndPhase", + globalScene.phaseManager.queueDeferred( "SwitchPhase", this.switchType, switchOutTarget.getFieldIndex(), @@ -6416,7 +6386,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { if (this.switchType === SwitchType.FORCE_SWITCH) { switchOutTarget.leaveField(true); const slotIndex = eligibleNewIndices[user.randBattleSeedInt(eligibleNewIndices.length)]; - globalScene.phaseManager.prependNewToPhase("MoveEndPhase", + globalScene.phaseManager.queueDeferred( "SwitchSummonPhase", this.switchType, switchOutTarget.getFieldIndex(), @@ -6426,7 +6396,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { ); } else { switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH); - globalScene.phaseManager.prependNewToPhase("MoveEndPhase", + globalScene.phaseManager.queueDeferred( "SwitchSummonPhase", this.switchType, switchOutTarget.getFieldIndex(), @@ -6857,7 +6827,7 @@ class CallMoveAttr extends OverrideMoveEffectAttr { : moveTargets.targets[user.randBattleSeedInt(moveTargets.targets.length)]]; globalScene.phaseManager.unshiftNew("LoadMoveAnimPhase", move.id); - globalScene.phaseManager.unshiftNew("MovePhase", user, targets, new PokemonMove(move.id), MoveUseMode.FOLLOW_UP); + globalScene.phaseManager.unshiftNew("MovePhase", user, targets, new PokemonMove(move.id), MoveUseMode.FOLLOW_UP, MovePhaseTimingModifier.FIRST); return true; } } @@ -7089,7 +7059,7 @@ export class NaturePowerAttr extends OverrideMoveEffectAttr { // Load the move's animation if we didn't already and unshift a new usage phase globalScene.phaseManager.unshiftNew("LoadMoveAnimPhase", moveId); - globalScene.phaseManager.unshiftNew("MovePhase", user, [ target.getBattlerIndex() ], new PokemonMove(moveId), MoveUseMode.FOLLOW_UP); + globalScene.phaseManager.unshiftNew("MovePhase", user, [ target.getBattlerIndex() ], new PokemonMove(moveId), MoveUseMode.FOLLOW_UP, MovePhaseTimingModifier.FIRST); return true; } } @@ -7173,7 +7143,7 @@ export class RepeatMoveAttr extends MoveEffectAttr { targetPokemonName: getPokemonNameWithAffix(target) })); target.turnData.extraTurns++; - globalScene.phaseManager.appendNewToPhase("MoveEndPhase", "MovePhase", target, moveTargets, movesetMove, MoveUseMode.NORMAL); + globalScene.phaseManager.unshiftNew("MovePhase", target, moveTargets, movesetMove, MoveUseMode.NORMAL, MovePhaseTimingModifier.FIRST); return true; } @@ -7946,12 +7916,7 @@ export class AfterYouAttr extends MoveEffectAttr { */ override apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean { globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:afterYou", { targetName: getPokemonNameWithAffix(target) })); - - // Will find next acting phase of the targeted pokémon, delete it and queue it right after us. - const targetNextPhase = globalScene.phaseManager.findPhase(phase => phase.pokemon === target); - if (targetNextPhase && globalScene.phaseManager.tryRemovePhase((phase: MovePhase) => phase.pokemon === target)) { - globalScene.phaseManager.prependToPhase(targetNextPhase, "MovePhase"); - } + globalScene.phaseManager.forceMoveNext((phase: MovePhase) => phase.pokemon === target); return true; } @@ -7974,45 +7939,11 @@ export class ForceLastAttr extends MoveEffectAttr { override apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean { globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:forceLast", { targetPokemonName: getPokemonNameWithAffix(target) })); - // TODO: Refactor this to be more readable and less janky - const targetMovePhase = globalScene.phaseManager.findPhase((phase) => phase.pokemon === target); - if (targetMovePhase && !targetMovePhase.isForcedLast() && globalScene.phaseManager.tryRemovePhase((phase: MovePhase) => phase.pokemon === target)) { - // Finding the phase to insert the move in front of - - // Either the end of the turn or in front of another, slower move which has also been forced last - const prependPhase = globalScene.phaseManager.findPhase((phase) => - [ MovePhase, MoveEndPhase ].every(cls => !(phase instanceof cls)) - || (phase.is("MovePhase")) && phaseForcedSlower(phase, target, !!globalScene.arena.getTag(ArenaTagType.TRICK_ROOM)) - ); - if (prependPhase) { - globalScene.phaseManager.phaseQueue.splice( - globalScene.phaseManager.phaseQueue.indexOf(prependPhase), - 0, - globalScene.phaseManager.create("MovePhase", target, [ ...targetMovePhase.targets ], targetMovePhase.move, targetMovePhase.useMode, true) - ); - } - } + globalScene.phaseManager.forceMoveLast((phase: MovePhase) => phase.pokemon === target); return true; } } -/** - * Returns whether a {@linkcode MovePhase} has been forced last and the corresponding pokemon is slower than {@linkcode target}. - - * TODO: - - Make this a class method - - Make this look at speed order from TurnStartPhase -*/ -const phaseForcedSlower = (phase: MovePhase, target: Pokemon, trickRoom: boolean): boolean => { - let slower: boolean; - // quashed pokemon still have speed ties - if (phase.pokemon.getEffectiveStat(Stat.SPD) === target.getEffectiveStat(Stat.SPD)) { - slower = !!target.randBattleSeedInt(2); - } else { - slower = !trickRoom ? phase.pokemon.getEffectiveStat(Stat.SPD) < target.getEffectiveStat(Stat.SPD) : phase.pokemon.getEffectiveStat(Stat.SPD) > target.getEffectiveStat(Stat.SPD); - } - return phase.isForcedLast() && slower; -}; - const failOnGravityCondition: MoveConditionFunc = (user, target, move) => !globalScene.arena.getTag(ArenaTagType.GRAVITY); const failOnBossCondition: MoveConditionFunc = (user, target, move) => !target.isBossImmune(); @@ -8036,7 +7967,7 @@ const userSleptOrComatoseCondition: MoveConditionFunc = (user) => user.status?.e const targetSleptOrComatoseCondition: MoveConditionFunc = (_user: Pokemon, target: Pokemon, _move: Move) => target.status?.effect === StatusEffect.SLEEP || target.hasAbility(AbilityId.COMATOSE); -const failIfLastCondition: MoveConditionFunc = () => globalScene.phaseManager.findPhase(phase => phase.is("MovePhase")) !== undefined; +const failIfLastCondition: MoveConditionFunc = () => globalScene.phaseManager.hasPhaseOfType("MovePhase"); const failIfLastInPartyCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => { const party: Pokemon[] = user.isPlayer() ? globalScene.getPlayerParty() : globalScene.getEnemyParty(); diff --git a/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts b/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts index d883fdbb567..f2363ade500 100644 --- a/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts +++ b/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts @@ -414,7 +414,7 @@ function summonPlayerPokemonAnimation(pokemon: PlayerPokemon): Promise { pokemon.resetTurnData(); globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true); - globalScene.phaseManager.pushNew("PostSummonPhase", pokemon.getBattlerIndex()); + globalScene.phaseManager.unshiftNew("PostSummonPhase", pokemon.getBattlerIndex()); resolve(); }); }, diff --git a/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts b/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts index b5084743613..67e778d8c4b 100644 --- a/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts @@ -669,7 +669,6 @@ function onGameOver() { // Clear any leftover battle phases globalScene.phaseManager.clearPhaseQueue(); - globalScene.phaseManager.clearPhaseQueueSplice(); // Return enemy Pokemon const pokemon = globalScene.getEnemyPokemon(); diff --git a/src/data/mystery-encounters/utils/encounter-phase-utils.ts b/src/data/mystery-encounters/utils/encounter-phase-utils.ts index 86cd3fa3a32..0ba0dec896a 100644 --- a/src/data/mystery-encounters/utils/encounter-phase-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -738,7 +738,7 @@ export function setEncounterRewards( if (customShopRewards) { globalScene.phaseManager.unshiftNew("SelectModifierPhase", 0, undefined, customShopRewards); } else { - globalScene.phaseManager.tryRemovePhase(p => p.is("MysteryEncounterRewardsPhase")); + globalScene.phaseManager.removeAllPhasesOfType("MysteryEncounterRewardsPhase"); } if (eggRewards) { @@ -812,8 +812,7 @@ export function leaveEncounterWithoutBattle( encounterMode: MysteryEncounterMode = MysteryEncounterMode.NO_BATTLE, ) { globalScene.currentBattle.mysteryEncounter!.encounterMode = encounterMode; - globalScene.phaseManager.clearPhaseQueue(); - globalScene.phaseManager.clearPhaseQueueSplice(); + globalScene.phaseManager.clearPhaseQueue(true); handleMysteryEncounterVictory(addHealPhase); } @@ -826,7 +825,7 @@ export function handleMysteryEncounterVictory(addHealPhase = false, doNotContinu const allowedPkm = globalScene.getPlayerParty().filter(pkm => pkm.isAllowedInBattle()); if (allowedPkm.length === 0) { - globalScene.phaseManager.clearPhaseQueue(); + globalScene.phaseManager.clearPhaseQueue(true); globalScene.phaseManager.unshiftNew("GameOverPhase"); return; } @@ -869,7 +868,7 @@ export function handleMysteryEncounterBattleFailed(addHealPhase = false, doNotCo const allowedPkm = globalScene.getPlayerParty().filter(pkm => pkm.isAllowedInBattle()); if (allowedPkm.length === 0) { - globalScene.phaseManager.clearPhaseQueue(); + globalScene.phaseManager.clearPhaseQueue(true); globalScene.phaseManager.unshiftNew("GameOverPhase"); return; } diff --git a/src/data/phase-priority-queue.ts b/src/data/phase-priority-queue.ts deleted file mode 100644 index 2c83348cc7b..00000000000 --- a/src/data/phase-priority-queue.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { globalScene } from "#app/global-scene"; -import type { Phase } from "#app/phase"; -import { TrickRoomTag } from "#data/arena-tag"; -import { DynamicPhaseType } from "#enums/dynamic-phase-type"; -import { Stat } from "#enums/stat"; -import { ActivatePriorityQueuePhase } from "#phases/activate-priority-queue-phase"; -import { PostSummonActivateAbilityPhase } from "#phases/post-summon-activate-ability-phase"; -import type { PostSummonPhase } from "#phases/post-summon-phase"; -import { BooleanHolder } from "#utils/common"; - -/** - * Stores a list of {@linkcode Phase}s - * - * Dynamically updates ordering to always pop the highest "priority", based on implementation of {@linkcode reorder} - */ -export abstract class PhasePriorityQueue { - protected abstract queue: Phase[]; - - /** - * Sorts the elements in the queue - */ - public abstract reorder(): void; - - /** - * Calls {@linkcode reorder} and shifts the queue - * @returns The front element of the queue after sorting - */ - public pop(): Phase | undefined { - this.reorder(); - return this.queue.shift(); - } - - /** - * Adds a phase to the queue - * @param phase The phase to add - */ - public push(phase: Phase): void { - this.queue.push(phase); - } - - /** - * Removes all phases from the queue - */ - public clear(): void { - this.queue.splice(0, this.queue.length); - } - - /** - * Attempt to remove one or more Phases from the current queue. - * @param phaseFilter - The function to select phases for removal - * @param removeCount - The maximum number of phases to remove, or `all` to remove all matching phases; - * default `1` - * @returns The number of successfully removed phases - * @todo Remove this eventually once the patchwork bug this is used for is fixed - */ - public tryRemovePhase(phaseFilter: (phase: Phase) => boolean, removeCount: number | "all" = 1): number { - if (removeCount === "all") { - removeCount = this.queue.length; - } else if (removeCount < 1) { - return 0; - } - let numRemoved = 0; - - do { - const phaseIndex = this.queue.findIndex(phaseFilter); - if (phaseIndex === -1) { - break; - } - this.queue.splice(phaseIndex, 1); - numRemoved++; - } while (numRemoved < removeCount && this.queue.length > 0); - - return numRemoved; - } -} - -/** - * Priority Queue for {@linkcode PostSummonPhase} and {@linkcode PostSummonActivateAbilityPhase} - * - * Orders phases first by ability priority, then by the {@linkcode Pokemon}'s effective speed - */ -export class PostSummonPhasePriorityQueue extends PhasePriorityQueue { - protected override queue: PostSummonPhase[] = []; - - public override reorder(): void { - this.queue.sort((phaseA: PostSummonPhase, phaseB: PostSummonPhase) => { - if (phaseA.getPriority() === phaseB.getPriority()) { - return ( - (phaseB.getPokemon().getEffectiveStat(Stat.SPD) - phaseA.getPokemon().getEffectiveStat(Stat.SPD)) - * (isTrickRoom() ? -1 : 1) - ); - } - - return phaseB.getPriority() - phaseA.getPriority(); - }); - } - - public override push(phase: PostSummonPhase): void { - super.push(phase); - this.queueAbilityPhase(phase); - } - - /** - * Queues all necessary {@linkcode PostSummonActivateAbilityPhase}s for each pushed {@linkcode PostSummonPhase} - * @param phase The {@linkcode PostSummonPhase} that was pushed onto the queue - */ - private queueAbilityPhase(phase: PostSummonPhase): void { - const phasePokemon = phase.getPokemon(); - - phasePokemon.getAbilityPriorities().forEach((priority, idx) => { - this.queue.push(new PostSummonActivateAbilityPhase(phasePokemon.getBattlerIndex(), priority, !!idx)); - globalScene.phaseManager.appendToPhase( - new ActivatePriorityQueuePhase(DynamicPhaseType.POST_SUMMON), - "ActivatePriorityQueuePhase", - (p: ActivatePriorityQueuePhase) => p.getType() === DynamicPhaseType.POST_SUMMON, - ); - }); - } -} - -function isTrickRoom(): boolean { - const speedReversed = new BooleanHolder(false); - globalScene.arena.applyTags(TrickRoomTag, false, speedReversed); - return speedReversed.value; -} diff --git a/src/dynamic-queue-manager.ts b/src/dynamic-queue-manager.ts new file mode 100644 index 00000000000..7356f67bc1d --- /dev/null +++ b/src/dynamic-queue-manager.ts @@ -0,0 +1,182 @@ +import type { DynamicPhase, PhaseConditionFunc, PhaseString } from "#app/@types/phase-types"; +import type { PokemonMove } from "#app/data/moves/pokemon-move"; +import type { Pokemon } from "#app/field/pokemon"; +import type { Phase } from "#app/phase"; +import type { MovePhase } from "#app/phases/move-phase"; +import { MovePhasePriorityQueue } from "#app/queues/move-phase-priority-queue"; +import { PokemonPhasePriorityQueue } from "#app/queues/pokemon-phase-priority-queue"; +import { PostSummonPhasePriorityQueue } from "#app/queues/post-summon-phase-priority-queue"; +import type { PriorityQueue } from "#app/queues/priority-queue"; +import type { BattlerIndex } from "#enums/battler-index"; +import type { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier"; + +// TODO: might be easier to define which phases should be dynamic instead +/** All phases which have defined a `getPokemon` method but should not be sorted dynamically */ +const nonDynamicPokemonPhases: readonly PhaseString[] = [ + "SummonPhase", + "CommandPhase", + "LearnMovePhase", + "MoveEffectPhase", + "MoveEndPhase", + "FaintPhase", + "DamageAnimPhase", + "VictoryPhase", + "PokemonHealPhase", + "WeatherEffectPhase", +] as const; + +/** + * The dynamic queue manager holds priority queues for phases which are queued as dynamic. + * + * Dynamic phases are generally those which hold a pokemon and are unshifted, not pushed. \ + * Queues work by sorting their entries in speed order (and possibly with more complex ordering) before each time a phase is popped. + * + * As the holder, this structure is also used to access and modify queued phases. + * This is mostly used in redirection, cancellation, etc. of {@linkcode MovePhase}s. + */ +export class DynamicQueueManager { + /** Maps phase types to their corresponding queues */ + private readonly dynamicPhaseMap: Map>; + + constructor() { + this.dynamicPhaseMap = new Map(); + // PostSummon and Move phases have specialized queues + this.dynamicPhaseMap.set("PostSummonPhase", new PostSummonPhasePriorityQueue()); + this.dynamicPhaseMap.set("MovePhase", new MovePhasePriorityQueue()); + } + + /** Removes all phases from the manager */ + public clearQueues(): void { + for (const queue of this.dynamicPhaseMap.values()) { + queue.clear(); + } + } + + /** + * Adds a new phase to the manager and creates the priority queue for it if one does not exist. + * @param phase - The {@linkcode Phase} to add + * @returns `true` if the phase was added, or `false` if it is not dynamic + */ + public queueDynamicPhase(phase: T): boolean { + if (!this.isDynamicPhase(phase)) { + return false; + } + + if (!this.dynamicPhaseMap.has(phase.phaseName)) { + // TS can't figure out that T is dynamic at this point, but it does know that `typeof phase` is + this.dynamicPhaseMap.set(phase.phaseName, new PokemonPhasePriorityQueue()); + } + this.dynamicPhaseMap.get(phase.phaseName)?.push(phase); + return true; + } + + /** + * Returns the highest-priority (generally by speed) {@linkcode Phase} of the specified type + * @param type - The {@linkcode PhaseString | type} to pop + * @returns The popped {@linkcode Phase}, or `undefined` if none of the specified type exist + */ + public popNextPhase(type: PhaseString): Phase | undefined { + return this.dynamicPhaseMap.get(type)?.pop(); + } + + /** + * Determines if there is a queued dynamic {@linkcode Phase} meeting the conditions + * @param type - The {@linkcode PhaseString | type} of phase to search for + * @param condition - An optional {@linkcode PhaseConditionFunc} to add conditions to the search + * @returns Whether a matching phase exists + */ + public exists(type: T, condition?: PhaseConditionFunc): boolean { + return !!this.dynamicPhaseMap.get(type)?.has(condition); + } + + /** + * Finds and removes a single queued {@linkcode Phase} + * @param type - The {@linkcode PhaseString | type} of phase to search for + * @param phaseFilter - A {@linkcode PhaseConditionFunc} to specify conditions for the phase + * @returns Whether a removal occurred + */ + public removePhase(type: T, condition?: PhaseConditionFunc): boolean { + return !!this.dynamicPhaseMap.get(type)?.remove(condition); + } + + /** + * Sets the timing modifier of a move (i.e. to force it first or last) + * @param condition - A {@linkcode PhaseConditionFunc} to specify conditions for the move + * @param modifier - The {@linkcode MovePhaseTimingModifier} to switch the move to + */ + public setMoveTimingModifier(condition: PhaseConditionFunc<"MovePhase">, modifier: MovePhaseTimingModifier): void { + this.getMovePhaseQueue().setTimingModifier(condition, modifier); + } + + /** + * Finds the {@linkcode MovePhase} meeting the condition and changes its move + * @param phaseCondition - The {@linkcode PhaseConditionFunc | condition} function + * @param move - The {@linkcode PokemonMove | move} to use in replacement + */ + public setMoveForPhase(condition: PhaseConditionFunc<"MovePhase">, move: PokemonMove): void { + this.getMovePhaseQueue().setMoveForPhase(condition, move); + } + + /** + * Redirects moves which were targeted at a {@linkcode Pokemon} that has been removed + * @param removedPokemon - The removed {@linkcode Pokemon} + * @param allyPokemon - The ally of the removed pokemon + */ + public redirectMoves(removedPokemon: Pokemon, allyPokemon: Pokemon): void { + this.getMovePhaseQueue().redirectMoves(removedPokemon, allyPokemon); + } + + /** + * Finds a {@linkcode MovePhase} meeting the condition + * @param phaseCondition - The {@linkcode PhaseConditionFunc | condition} function + * @returns The MovePhase, or `undefined` if it does not exist + */ + public getMovePhase(condition: PhaseConditionFunc<"MovePhase">): MovePhase | undefined { + return this.getMovePhaseQueue().find(condition); + } + + /** + * Finds and cancels a {@linkcode MovePhase} meeting the condition + * @param phaseCondition - The {@linkcode PhaseConditionFunc | condition} function + */ + public cancelMovePhase(condition: PhaseConditionFunc<"MovePhase">): void { + this.getMovePhaseQueue().cancelMove(condition); + } + + /** + * Sets the move order to a static array rather than a dynamic queue + * @param order - The order of {@linkcode BattlerIndex}s + */ + public setMoveOrder(order: BattlerIndex[]): void { + this.getMovePhaseQueue().setMoveOrder(order); + } + + /** + * @returns An in-order array of {@linkcode Pokemon}, representing the turn order as played out in the most recent turn + */ + public getLastTurnOrder(): Pokemon[] { + return this.getMovePhaseQueue().getTurnOrder(); + } + + /** Clears the stored `Move` turn order */ + public clearLastTurnOrder(): void { + this.getMovePhaseQueue().clearTurnOrder(); + } + + /** Internal helper to get the {@linkcode MovePhasePriorityQueue} */ + private getMovePhaseQueue(): MovePhasePriorityQueue { + return this.dynamicPhaseMap.get("MovePhase") as MovePhasePriorityQueue; + } + + /** + * Internal helper to determine if a phase is dynamic. + * @param phase - The {@linkcode Phase} to check + * @returns Whether `phase` is dynamic + * @privateRemarks + * Currently, this checks that `phase` has a `getPokemon` method + * and is not blacklisted in `nonDynamicPokemonPhases`. + */ + private isDynamicPhase(phase: Phase): phase is DynamicPhase { + return typeof (phase as any).getPokemon === "function" && !nonDynamicPokemonPhases.includes(phase.phaseName); + } +} diff --git a/src/enums/arena-tag-side.ts b/src/enums/arena-tag-side.ts index 5f25a74ab36..50741751fbb 100644 --- a/src/enums/arena-tag-side.ts +++ b/src/enums/arena-tag-side.ts @@ -1,3 +1,4 @@ +// TODO: rename to something else (this isn't used only for arena tags) export enum ArenaTagSide { BOTH, PLAYER, diff --git a/src/enums/battler-tag-type.ts b/src/enums/battler-tag-type.ts index 7956e506886..4f0ac491e8b 100644 --- a/src/enums/battler-tag-type.ts +++ b/src/enums/battler-tag-type.ts @@ -95,4 +95,5 @@ export enum BattlerTagType { POWDER = "POWDER", MAGIC_COAT = "MAGIC_COAT", SUPREME_OVERLORD = "SUPREME_OVERLORD", + BYPASS_SPEED = "BYPASS_SPEED", } diff --git a/src/enums/dynamic-phase-type.ts b/src/enums/dynamic-phase-type.ts deleted file mode 100644 index 3146b136dac..00000000000 --- a/src/enums/dynamic-phase-type.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Enum representation of the phase types held by implementations of {@linkcode PhasePriorityQueue}. - */ -// TODO: We currently assume these are in order -export enum DynamicPhaseType { - POST_SUMMON, -} diff --git a/src/enums/move-phase-timing-modifier.ts b/src/enums/move-phase-timing-modifier.ts new file mode 100644 index 00000000000..a452d37e7ff --- /dev/null +++ b/src/enums/move-phase-timing-modifier.ts @@ -0,0 +1,16 @@ +import type { ObjectValues } from "#types/type-helpers"; + +/** + * Enum representing modifiers for the timing of MovePhases. + * + * @remarks + * This system is entirely independent of and takes precedence over move priority + */ +export const MovePhaseTimingModifier = Object.freeze({ + /** Used when moves go last regardless of speed and priority (i.e. Quash) */ + LAST: 0, + NORMAL: 1, + /** Used to trigger moves immediately (i.e. ones that were called through Instruct). */ + FIRST: 2, +}); +export type MovePhaseTimingModifier = ObjectValues; diff --git a/src/field/arena.ts b/src/field/arena.ts index 5ab50e540ee..3e214ff1ea7 100644 --- a/src/field/arena.ts +++ b/src/field/arena.ts @@ -371,9 +371,15 @@ export class Arena { /** * Function to trigger all weather based form changes + * @param source - The Pokemon causing the changes by removing itself from the field */ - triggerWeatherBasedFormChanges(): void { + triggerWeatherBasedFormChanges(source?: Pokemon): void { globalScene.getField(true).forEach(p => { + // TODO - This is a bandaid. Abilities leaving the field needs a better approach than + // calling this method for every switch out that happens + if (p === source) { + return; + } const isCastformWithForecast = p.hasAbility(AbilityId.FORECAST) && p.species.speciesId === SpeciesId.CASTFORM; const isCherrimWithFlowerGift = p.hasAbility(AbilityId.FLOWER_GIFT) && p.species.speciesId === SpeciesId.CHERRIM; diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 3154f273cf5..ec813e52e56 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -3890,15 +3890,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { damage = Math.min(damage, this.hp); this.hp = this.hp - damage; if (this.isFainted() && !ignoreFaintPhase) { - /** - * When adding the FaintPhase, want to toggle future unshiftPhase() and queueMessage() calls - * to appear before the FaintPhase (as FaintPhase will potentially end the encounter and add Phases such as - * GameOverPhase, VictoryPhase, etc.. that will interfere with anything else that happens during this MoveEffectPhase) - * - * Once the MoveEffectPhase is over (and calls it's .end() function, shiftPhase() will reset the PhaseQueueSplice via clearPhaseQueueSplice() ) - */ - globalScene.phaseManager.setPhaseQueueSplice(); - globalScene.phaseManager.unshiftNew("FaintPhase", this.getBattlerIndex(), preventEndure); + globalScene.phaseManager.queueFaintPhase(this.getBattlerIndex(), preventEndure); this.destroySubstitute(); this.lapseTag(BattlerTagType.COMMANDED); } @@ -5842,8 +5834,7 @@ export class PlayerPokemon extends Pokemon { this.getFieldIndex(), (slotIndex: number, _option: PartyOption) => { if (slotIndex >= globalScene.currentBattle.getBattlerCount() && slotIndex < 6) { - globalScene.phaseManager.prependNewToPhase( - "MoveEndPhase", + globalScene.phaseManager.queueDeferred( "SwitchSummonPhase", switchType, this.getFieldIndex(), diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index b94c479e96e..19ddc77d436 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -13,7 +13,6 @@ import { getStatusEffectHealText } from "#data/status-effect"; import { BattlerTagType } from "#enums/battler-tag-type"; import { BerryType } from "#enums/berry-type"; import { Color, ShadowColor } from "#enums/color"; -import { Command } from "#enums/command"; import type { FormChangeItem } from "#enums/form-change-item"; import { LearnMoveType } from "#enums/learn-move-type"; import type { MoveId } from "#enums/move-id"; @@ -1542,30 +1541,16 @@ export class BypassSpeedChanceModifier extends PokemonHeldItemModifier { return new BypassSpeedChanceModifier(this.type, this.pokemonId, this.stackCount); } - /** - * Checks if {@linkcode BypassSpeedChanceModifier} should be applied - * @param pokemon the {@linkcode Pokemon} that holds the item - * @param doBypassSpeed {@linkcode BooleanHolder} that is `true` if speed should be bypassed - * @returns `true` if {@linkcode BypassSpeedChanceModifier} should be applied - */ - override shouldApply(pokemon?: Pokemon, doBypassSpeed?: BooleanHolder): boolean { - return super.shouldApply(pokemon, doBypassSpeed) && !!doBypassSpeed; - } - /** * Applies {@linkcode BypassSpeedChanceModifier} * @param pokemon the {@linkcode Pokemon} that holds the item - * @param doBypassSpeed {@linkcode BooleanHolder} that is `true` if speed should be bypassed * @returns `true` if {@linkcode BypassSpeedChanceModifier} has been applied */ - override apply(pokemon: Pokemon, doBypassSpeed: BooleanHolder): boolean { - if (!doBypassSpeed.value && pokemon.randBattleSeedInt(10) < this.getStackCount()) { - doBypassSpeed.value = true; - const isCommandFight = - globalScene.currentBattle.turnCommands[pokemon.getBattlerIndex()]?.command === Command.FIGHT; + override apply(pokemon: Pokemon): boolean { + if (pokemon.randBattleSeedInt(10) < this.getStackCount() && pokemon.addTag(BattlerTagType.BYPASS_SPEED)) { const hasQuickClaw = this.type.is("PokemonHeldItemModifierType") && this.type.id === "QUICK_CLAW"; - if (isCommandFight && hasQuickClaw) { + if (hasQuickClaw) { globalScene.phaseManager.queueMessage( i18next.t("modifier:bypassSpeedChanceApply", { pokemonName: getPokemonNameWithAffix(pokemon), diff --git a/src/phase-manager.ts b/src/phase-manager.ts index 125ca00786b..350e77e52eb 100644 --- a/src/phase-manager.ts +++ b/src/phase-manager.ts @@ -8,12 +8,14 @@ */ import { PHASE_START_COLOR } from "#app/constants/colors"; +import { DynamicQueueManager } from "#app/dynamic-queue-manager"; import { globalScene } from "#app/global-scene"; import type { Phase } from "#app/phase"; -import { type PhasePriorityQueue, PostSummonPhasePriorityQueue } from "#data/phase-priority-queue"; -import type { DynamicPhaseType } from "#enums/dynamic-phase-type"; +import { PhaseTree } from "#app/phase-tree"; +import { BattleType } from "#enums/battle-type"; +import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier"; import type { Pokemon } from "#field/pokemon"; -import { ActivatePriorityQueuePhase } from "#phases/activate-priority-queue-phase"; +import type { PokemonMove } from "#moves/pokemon-move"; import { AddEnemyBuffModifierPhase } from "#phases/add-enemy-buff-modifier-phase"; import { AttemptCapturePhase } from "#phases/attempt-capture-phase"; import { AttemptRunPhase } from "#phases/attempt-run-phase"; @@ -25,6 +27,7 @@ import { CheckSwitchPhase } from "#phases/check-switch-phase"; import { CommandPhase } from "#phases/command-phase"; import { CommonAnimPhase } from "#phases/common-anim-phase"; import { DamageAnimPhase } from "#phases/damage-anim-phase"; +import { DynamicPhaseMarker } from "#phases/dynamic-phase-marker"; import { EggHatchPhase } from "#phases/egg-hatch-phase"; import { EggLapsePhase } from "#phases/egg-lapse-phase"; import { EggSummaryPhase } from "#phases/egg-summary-phase"; @@ -109,8 +112,7 @@ import { UnavailablePhase } from "#phases/unavailable-phase"; import { UnlockPhase } from "#phases/unlock-phase"; import { VictoryPhase } from "#phases/victory-phase"; import { WeatherEffectPhase } from "#phases/weather-effect-phase"; -import type { PhaseMap, PhaseString } from "#types/phase-types"; -import { type Constructor, coerceArray } from "#utils/common"; +import type { PhaseConditionFunc, PhaseMap, PhaseString } from "#types/phase-types"; /** * Object that holds all of the phase constructors. @@ -121,7 +123,6 @@ import { type Constructor, coerceArray } from "#utils/common"; * This allows for easy creation of new phases without needing to import each phase individually. */ const PHASES = Object.freeze({ - ActivatePriorityQueuePhase, AddEnemyBuffModifierPhase, AttemptCapturePhase, AttemptRunPhase, @@ -133,6 +134,7 @@ const PHASES = Object.freeze({ CommandPhase, CommonAnimPhase, DamageAnimPhase, + DynamicPhaseMarker, EggHatchPhase, EggLapsePhase, EggSummaryPhase, @@ -221,32 +223,30 @@ const PHASES = Object.freeze({ /** Maps Phase strings to their constructors */ export type PhaseConstructorMap = typeof PHASES; +/** Phases pushed at the end of each {@linkcode TurnStartPhase} */ +const turnEndPhases: readonly PhaseString[] = [ + "WeatherEffectPhase", + "PositionalTagPhase", + "BerryPhase", + "CheckStatusEffectPhase", + "TurnEndPhase", +] as const; + /** * PhaseManager is responsible for managing the phases in the battle scene */ export class PhaseManager { /** PhaseQueue: dequeue/remove the first element to get the next phase */ - public phaseQueue: Phase[] = []; - public conditionalQueue: Array<[() => boolean, Phase]> = []; - /** PhaseQueuePrepend: is a temp storage of what will be added to PhaseQueue */ - private phaseQueuePrepend: Phase[] = []; + private readonly phaseQueue: PhaseTree = new PhaseTree(); - /** overrides default of inserting phases to end of phaseQueuePrepend array. Useful for inserting Phases "out of order" */ - private phaseQueuePrependSpliceIndex = -1; - - /** Storage for {@linkcode PhasePriorityQueue}s which hold phases whose order dynamically changes */ - private dynamicPhaseQueues: PhasePriorityQueue[]; - /** Parallel array to {@linkcode dynamicPhaseQueues} - matches phase types to their queues */ - private dynamicPhaseTypes: Constructor[]; + /** Holds priority queues for dynamically ordered phases */ + public dynamicQueueManager = new DynamicQueueManager(); + /** The currently-running phase */ private currentPhase: Phase; + /** The phase put on standby if {@linkcode overridePhase} is called */ private standbyPhase: Phase | null = null; - constructor() { - this.dynamicPhaseQueues = [new PostSummonPhasePriorityQueue()]; - this.dynamicPhaseTypes = [PostSummonPhase]; - } - /** * Clear all previously set phases, then add a new {@linkcode TitlePhase} to transition to the title screen. * @param addLogin - Whether to add a new {@linkcode LoginPhase} before the {@linkcode TitlePhase} @@ -274,122 +274,76 @@ export class PhaseManager { } /** - * Adds a phase to the conditional queue and ensures it is executed only when the specified condition is met. - * - * This method allows deferring the execution of a phase until certain conditions are met, which is useful for handling - * situations like abilities and entry hazards that depend on specific game states. - * - * @param phase - The phase to be added to the conditional queue. - * @param condition - A function that returns a boolean indicating whether the phase should be executed. - * + * Adds a phase to the end of the queue + * @param phase - The {@linkcode Phase} to add */ - pushConditionalPhase(phase: Phase, condition: () => boolean): void { - this.conditionalQueue.push([condition, phase]); + public pushPhase(phase: Phase): void { + this.phaseQueue.pushPhase(this.checkDynamic(phase)); } /** - * Adds a phase to nextCommandPhaseQueue, as long as boolean passed in is false - * @param phase {@linkcode Phase} the phase to add + * Queue a phase to be run immediately after the current phase finishes. \ + * Unshifted phases are run in FIFO order if multiple are queued during a single phase's execution. + * @param phase - The {@linkcode Phase} to add */ - pushPhase(phase: Phase): void { - if (this.getDynamicPhaseType(phase) !== undefined) { - this.pushDynamicPhase(phase); - } else { - this.phaseQueue.push(phase); - } + public unshiftPhase(phase: Phase): void { + const toAdd = this.checkDynamic(phase); + phase.is("MovePhase") ? this.phaseQueue.addAfter(toAdd, "MoveEndPhase") : this.phaseQueue.addPhase(toAdd); } /** - * Adds Phase(s) to the end of phaseQueuePrepend, or at phaseQueuePrependSpliceIndex - * @param phases {@linkcode Phase} the phase(s) to add + * Helper method to queue a phase as dynamic if necessary + * @param phase - The phase to check + * @returns The {@linkcode Phase} or a {@linkcode DynamicPhaseMarker} to be used in its place */ - unshiftPhase(...phases: Phase[]): void { - if (this.phaseQueuePrependSpliceIndex === -1) { - this.phaseQueuePrepend.push(...phases); - } else { - this.phaseQueuePrepend.splice(this.phaseQueuePrependSpliceIndex, 0, ...phases); + private checkDynamic(phase: Phase): Phase { + if (this.dynamicQueueManager.queueDynamicPhase(phase)) { + return new DynamicPhaseMarker(phase.phaseName); } + return phase; } /** * Clears the phaseQueue + * @param leaveUnshifted - If `true`, leaves the top level of the tree intact; default `false` */ - clearPhaseQueue(): void { - this.phaseQueue.splice(0, this.phaseQueue.length); + public clearPhaseQueue(leaveUnshifted = false): void { + this.phaseQueue.clear(leaveUnshifted); } - /** - * Clears all phase-related stuff, including all phase queues, the current and standby phases, and a splice index - */ - clearAllPhases(): void { - for (const queue of [this.phaseQueue, this.phaseQueuePrepend, this.conditionalQueue]) { - queue.splice(0, queue.length); - } - this.dynamicPhaseQueues.forEach(queue => queue.clear()); + /** Clears all phase queues and the standby phase */ + public clearAllPhases(): void { + this.clearPhaseQueue(); + this.dynamicQueueManager.clearQueues(); this.standbyPhase = null; - this.clearPhaseQueueSplice(); } /** - * Used by function unshiftPhase(), sets index to start inserting at current length instead of the end of the array, useful if phaseQueuePrepend gets longer with Phases + * Determines the next phase to run and starts it. + * @privateRemarks + * This is called by {@linkcode Phase.end} by default, and should not be called by other methods. */ - setPhaseQueueSplice(): void { - this.phaseQueuePrependSpliceIndex = this.phaseQueuePrepend.length; - } - - /** - * Resets phaseQueuePrependSpliceIndex to -1, implies that calls to unshiftPhase will insert at end of phaseQueuePrepend - */ - clearPhaseQueueSplice(): void { - this.phaseQueuePrependSpliceIndex = -1; - } - - /** - * Is called by each Phase implementations "end()" by default - * We dump everything from phaseQueuePrepend to the start of of phaseQueue - * then removes first Phase and starts it - */ - shiftPhase(): void { + public shiftPhase(): void { if (this.standbyPhase) { this.currentPhase = this.standbyPhase; this.standbyPhase = null; return; } - if (this.phaseQueuePrependSpliceIndex > -1) { - this.clearPhaseQueueSplice(); - } - this.phaseQueue.unshift(...this.phaseQueuePrepend); - this.phaseQueuePrepend.splice(0); + let nextPhase = this.phaseQueue.getNextPhase(); - const unactivatedConditionalPhases: [() => boolean, Phase][] = []; - // Check if there are any conditional phases queued - for (const [condition, phase] of this.conditionalQueue) { - // Evaluate the condition associated with the phase - if (condition()) { - // If the condition is met, add the phase to the phase queue - this.pushPhase(phase); - } else { - // If the condition is not met, re-add the phase back to the end of the conditional queue - unactivatedConditionalPhases.push([condition, phase]); - } + if (nextPhase?.is("DynamicPhaseMarker")) { + nextPhase = this.dynamicQueueManager.popNextPhase(nextPhase.phaseType); } - this.conditionalQueue = unactivatedConditionalPhases; - - // If no phases are left, unshift phases to start a new turn. - if (this.phaseQueue.length === 0) { - this.populatePhaseQueue(); - // Clear the conditionalQueue if there are no phases left in the phaseQueue - this.conditionalQueue = []; + if (nextPhase == null) { + this.turnStart(); + } else { + this.currentPhase = nextPhase; } - // Bang is justified as `populatePhaseQueue` ensures we always have _something_ in the queue at all times - this.currentPhase = this.phaseQueue.shift()!; - this.startCurrentPhase(); } - /** * Helper method to start and log the current phase. */ @@ -398,7 +352,14 @@ export class PhaseManager { this.currentPhase.start(); } - overridePhase(phase: Phase): boolean { + /** + * Overrides the currently running phase with another + * @param phase - The {@linkcode Phase} to override the current one with + * @returns If the override succeeded + * + * @todo This is antithetical to the phase structure and used a single time. Remove it. + */ + public overridePhase(phase: Phase): boolean { if (this.standbyPhase) { return false; } @@ -411,173 +372,47 @@ export class PhaseManager { } /** - * Find a specific {@linkcode Phase} in the phase queue. + * Determine if there is a queued {@linkcode Phase} meeting the specified conditions. + * @param type - The {@linkcode PhaseString | type} of phase to search for + * @param condition - An optional {@linkcode PhaseConditionFunc} to add conditions to the search + * @returns Whether a matching phase exists + */ + public hasPhaseOfType(type: T, condition?: PhaseConditionFunc): boolean { + return this.dynamicQueueManager.exists(type, condition) || this.phaseQueue.exists(type, condition); + } + + /** + * Attempt to find and remove the first queued {@linkcode Phase} matching the given conditions. + * @param type - The {@linkcode PhaseString | type} of phase to search for + * @param phaseFilter - An optional {@linkcode PhaseConditionFunc} to add conditions to the search + * @returns Whether a phase was successfully removed + */ + public tryRemovePhase(type: T, phaseFilter?: PhaseConditionFunc): boolean { + if (this.dynamicQueueManager.removePhase(type, phaseFilter)) { + return true; + } + return this.phaseQueue.remove(type, phaseFilter); + } + + /** + * Removes all {@linkcode Phase}s of the given type from the queue + * @param phaseType - The {@linkcode PhaseString | type} of phase to search for * - * @param phaseFilter filter function to use to find the wanted phase - * @returns the found phase or undefined if none found + * @remarks + * This is not intended to be used with dynamically ordered phases, and does not operate on the dynamic queue. \ + * However, it does remove {@linkcode DynamicPhaseMarker}s and so would prevent such phases from activating. */ - findPhase

(phaseFilter: (phase: P) => boolean): P | undefined { - return this.phaseQueue.find(phaseFilter) as P | undefined; - } - - tryReplacePhase(phaseFilter: (phase: Phase) => boolean, phase: Phase): boolean { - const phaseIndex = this.phaseQueue.findIndex(phaseFilter); - if (phaseIndex > -1) { - this.phaseQueue[phaseIndex] = phase; - return true; - } - return false; - } - - tryRemovePhase(phaseFilter: (phase: Phase) => boolean): boolean { - const phaseIndex = this.phaseQueue.findIndex(phaseFilter); - if (phaseIndex > -1) { - this.phaseQueue.splice(phaseIndex, 1); - return true; - } - return false; + public removeAllPhasesOfType(type: PhaseString): void { + this.phaseQueue.removeAll(type); } /** - * Will search for a specific phase in {@linkcode phaseQueuePrepend} via filter, and remove the first result if a match is found. - * @param phaseFilter filter function - */ - tryRemoveUnshiftedPhase(phaseFilter: (phase: Phase) => boolean): boolean { - const phaseIndex = this.phaseQueuePrepend.findIndex(phaseFilter); - if (phaseIndex > -1) { - this.phaseQueuePrepend.splice(phaseIndex, 1); - return true; - } - return false; - } - - /** - * Tries to add the input phase to index before target phase in the phaseQueue, else simply calls unshiftPhase() - * @param phase - The phase to be added - * @param targetPhase - The phase to search for in phaseQueue - * @returns boolean if a targetPhase was found and added - */ - prependToPhase(phase: Phase | Phase[], targetPhase: PhaseString): boolean { - phase = coerceArray(phase); - const target = PHASES[targetPhase]; - const targetIndex = this.phaseQueue.findIndex(ph => ph instanceof target); - - if (targetIndex !== -1) { - this.phaseQueue.splice(targetIndex, 0, ...phase); - return true; - } - this.unshiftPhase(...phase); - return false; - } - - /** - * Tries to add the input phase(s) to index after target phase in the {@linkcode phaseQueue}, else simply calls {@linkcode unshiftPhase()} - * @param phase {@linkcode Phase} the phase(s) to be added - * @param targetPhase {@linkcode Phase} the type of phase to search for in {@linkcode phaseQueue} - * @param condition Condition the target phase must meet to be appended to - * @returns `true` if a `targetPhase` was found to append to - */ - appendToPhase(phase: Phase | Phase[], targetPhase: PhaseString, condition?: (p: Phase) => boolean): boolean { - phase = coerceArray(phase); - const target = PHASES[targetPhase]; - const targetIndex = this.phaseQueue.findIndex(ph => ph instanceof target && (!condition || condition(ph))); - - if (targetIndex !== -1 && this.phaseQueue.length > targetIndex) { - this.phaseQueue.splice(targetIndex + 1, 0, ...phase); - return true; - } - this.unshiftPhase(...phase); - return false; - } - - /** - * Checks a phase and returns the matching {@linkcode DynamicPhaseType}, or undefined if it does not match one - * @param phase The phase to check - * @returns The corresponding {@linkcode DynamicPhaseType} or `undefined` - */ - public getDynamicPhaseType(phase: Phase | null): DynamicPhaseType | undefined { - let phaseType: DynamicPhaseType | undefined; - this.dynamicPhaseTypes.forEach((cls, index) => { - if (phase instanceof cls) { - phaseType = index; - } - }); - - return phaseType; - } - - /** - * Pushes a phase onto its corresponding dynamic queue and marks the activation point in {@linkcode phaseQueue} - * - * The {@linkcode ActivatePriorityQueuePhase} will run the top phase in the dynamic queue (not necessarily {@linkcode phase}) - * @param phase The phase to push - */ - public pushDynamicPhase(phase: Phase): void { - const type = this.getDynamicPhaseType(phase); - if (type === undefined) { - return; - } - - this.pushPhase(new ActivatePriorityQueuePhase(type)); - this.dynamicPhaseQueues[type].push(phase); - } - - /** - * Attempt to remove one or more Phases from the given DynamicPhaseQueue, removing the equivalent amount of {@linkcode ActivatePriorityQueuePhase}s from the queue. - * @param type - The {@linkcode DynamicPhaseType} to check - * @param phaseFilter - The function to select phases for removal - * @param removeCount - The maximum number of phases to remove, or `all` to remove all matching phases; - * default `1` - * @todo Remove this eventually once the patchwork bug this is used for is fixed - */ - public tryRemoveDynamicPhase( - type: DynamicPhaseType, - phaseFilter: (phase: Phase) => boolean, - removeCount: number | "all" = 1, - ): void { - const numRemoved = this.dynamicPhaseQueues[type].tryRemovePhase(phaseFilter, removeCount); - for (let x = 0; x < numRemoved; x++) { - this.tryRemovePhase(p => p.is("ActivatePriorityQueuePhase")); - } - } - - /** - * Unshifts the top phase from the corresponding dynamic queue onto {@linkcode phaseQueue} - * @param type {@linkcode DynamicPhaseType} The type of dynamic phase to start - */ - public startDynamicPhaseType(type: DynamicPhaseType): void { - const phase = this.dynamicPhaseQueues[type].pop(); - if (phase) { - this.unshiftPhase(phase); - } - } - - /** - * Unshifts an {@linkcode ActivatePriorityQueuePhase} for {@linkcode phase}, then pushes {@linkcode phase} to its dynamic queue - * - * This is the same as {@linkcode pushDynamicPhase}, except the activation phase is unshifted - * - * {@linkcode phase} is not guaranteed to be the next phase from the queue to run (if the queue is not empty) - * @param phase The phase to add - * @returns - */ - public startDynamicPhase(phase: Phase): void { - const type = this.getDynamicPhaseType(phase); - if (type === undefined) { - return; - } - - this.unshiftPhase(new ActivatePriorityQueuePhase(type)); - this.dynamicPhaseQueues[type].push(phase); - } - - /** - * Adds a MessagePhase, either to PhaseQueuePrepend or nextCommandPhaseQueue + * Adds a `MessagePhase` to the queue * @param message - string for MessagePhase * @param callbackDelay - optional param for MessagePhase constructor * @param prompt - optional param for MessagePhase constructor * @param promptDelay - optional param for MessagePhase constructor - * @param defer - Whether to allow the phase to be deferred + * @param defer - If `true`, push the phase instead of unshifting; default `false` * * @see {@linkcode MessagePhase} for more details on the parameters */ @@ -589,20 +424,18 @@ export class PhaseManager { defer?: boolean | null, ) { const phase = new MessagePhase(message, callbackDelay, prompt, promptDelay); - if (!defer) { - // adds to the end of PhaseQueuePrepend - this.unshiftPhase(phase); - } else { - //remember that pushPhase adds it to nextCommandPhaseQueue + if (defer) { this.pushPhase(phase); + } else { + this.unshiftPhase(phase); } } /** - * Queue a phase to show or hide the ability flyout bar. + * Queues an ability bar flyout phase via {@linkcode unshiftPhase} * @param pokemon - The {@linkcode Pokemon} whose ability is being activated * @param passive - Whether the ability is a passive - * @param show - Whether to show or hide the bar + * @param show - If `true`, show the bar. Otherwise, hide it */ public queueAbilityDisplay(pokemon: Pokemon, passive: boolean, show: boolean): void { this.unshiftPhase(show ? new ShowAbilityPhase(pokemon.getBattlerIndex(), passive) : new HideAbilityPhase()); @@ -618,10 +451,12 @@ export class PhaseManager { } /** - * Moves everything from nextCommandPhaseQueue to phaseQueue (keeping order) + * Clear all dynamic queues and begin a new {@linkcode TurnInitPhase} for the new turn. + * Called whenever the current phase queue is empty. */ - private populatePhaseQueue(): void { - this.phaseQueue.push(new TurnInitPhase()); + private turnStart(): void { + this.dynamicQueueManager.clearQueues(); + this.currentPhase = new TurnInitPhase(); } /** @@ -663,50 +498,119 @@ export class PhaseManager { } /** - * Create a new phase and immediately prepend it to an existing phase in the phase queue. - * Equivalent to calling {@linkcode create} followed by {@linkcode prependToPhase}. - * @param targetPhase - The phase to search for in phaseQueue - * @param phase - The name of the phase to create + * Add a {@linkcode FaintPhase} to the queue * @param args - The arguments to pass to the phase constructor - * @returns `true` if a `targetPhase` was found to prepend to + * + * @remarks + * + * Faint phases are ordered in a special way to allow battle effects to settle before the pokemon faints. + * @see {@linkcode PhaseTree.addPhase} */ - public prependNewToPhase( - targetPhase: PhaseString, - phase: T, - ...args: ConstructorParameters - ): boolean { - return this.prependToPhase(this.create(phase, ...args), targetPhase); + public queueFaintPhase(...args: ConstructorParameters): void { + this.phaseQueue.addPhase(this.create("FaintPhase", ...args), true); } /** - * Create a new phase and immediately append it to an existing phase the phase queue. - * Equivalent to calling {@linkcode create} followed by {@linkcode appendToPhase}. - * @param targetPhase - The phase to search for in phaseQueue - * @param phase - The name of the phase to create - * @param args - The arguments to pass to the phase constructor - * @returns `true` if a `targetPhase` was found to append to + * Attempts to add {@linkcode PostSummonPhase}s for the enemy pokemon + * + * This is used to ensure that wild pokemon (which have no {@linkcode SummonPhase}) do not queue a {@linkcode PostSummonPhase} + * until all pokemon are on the field. */ - public appendNewToPhase( - targetPhase: PhaseString, - phase: T, - ...args: ConstructorParameters - ): boolean { - return this.appendToPhase(this.create(phase, ...args), targetPhase); + public tryAddEnemyPostSummonPhases(): void { + if ( + ![BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER].includes(globalScene.currentBattle.battleType) + && !this.phaseQueue.exists("SummonPhase") + ) { + globalScene.getEnemyField().forEach(p => { + this.pushPhase(new PostSummonPhase(p.getBattlerIndex(), "SummonPhase")); + }); + } } - public startNewDynamicPhase( + /** + * Create a new phase and queue it to run after all others queued by the currently running phase. + * @param phase - The name of the phase to create + * @param args - The arguments to pass to the phase constructor + * + * @deprecated Only used for switches and should be phased out eventually. + */ + public queueDeferred( phase: T, ...args: ConstructorParameters ): void { - this.startDynamicPhase(this.create(phase, ...args)); + this.phaseQueue.unshiftToCurrent(this.create(phase, ...args)); + } + + /** + * Finds the first {@linkcode MovePhase} meeting the condition + * @param phaseCondition - The {@linkcode PhaseConditionFunc | condition} function + * @returns The MovePhase, or `undefined` if it does not exist + */ + public getMovePhase(phaseCondition: PhaseConditionFunc<"MovePhase">): MovePhase | undefined { + return this.dynamicQueueManager.getMovePhase(phaseCondition); + } + + /** + * Finds and cancels the first {@linkcode MovePhase} meeting the condition + * @param phaseCondition - The {@linkcode PhaseConditionFunc | condition} function + */ + public cancelMove(phaseCondition: PhaseConditionFunc<"MovePhase">): void { + this.dynamicQueueManager.cancelMovePhase(phaseCondition); + } + + /** + * Finds the first {@linkcode MovePhase} meeting the condition and forces it next + * @param phaseCondition - The {@linkcode PhaseConditionFunc | condition} function + */ + public forceMoveNext(phaseCondition: PhaseConditionFunc<"MovePhase">): void { + this.dynamicQueueManager.setMoveTimingModifier(phaseCondition, MovePhaseTimingModifier.FIRST); + } + + /** + * Finds the first {@linkcode MovePhase} meeting the condition and forces it last + * @param phaseCondition - The {@linkcode PhaseConditionFunc | condition} function + */ + public forceMoveLast(phaseCondition: PhaseConditionFunc<"MovePhase">): void { + this.dynamicQueueManager.setMoveTimingModifier(phaseCondition, MovePhaseTimingModifier.LAST); + } + + /** + * Finds the first {@linkcode MovePhase} meeting the condition and changes its move + * @param phaseCondition - The {@linkcode PhaseConditionFunc | condition} function + * @param move - The {@linkcode PokemonMove | move} to use in replacement + */ + public changePhaseMove(phaseCondition: PhaseConditionFunc<"MovePhase">, move: PokemonMove): void { + this.dynamicQueueManager.setMoveForPhase(phaseCondition, move); + } + + /** + * Redirects moves which were targeted at a {@linkcode Pokemon} that has been removed + * @param removedPokemon - The removed {@linkcode Pokemon} + * @param allyPokemon - The ally of the removed pokemon + */ + public redirectMoves(removedPokemon: Pokemon, allyPokemon: Pokemon): void { + this.dynamicQueueManager.redirectMoves(removedPokemon, allyPokemon); + } + + /** Queues phases which run at the end of each turn */ + public queueTurnEndPhases(): void { + turnEndPhases.forEach(p => { + this.pushNew(p); + }); } /** Prevents end of turn effects from triggering when transitioning to a new biome on a X0 wave */ public onInterlude(): void { - const phasesToRemove = ["WeatherEffectPhase", "BerryPhase", "CheckStatusEffectPhase"]; - this.phaseQueue = this.phaseQueue.filter(p => !phasesToRemove.includes(p.phaseName)); + const phasesToRemove: readonly PhaseString[] = [ + "WeatherEffectPhase", + "BerryPhase", + "CheckStatusEffectPhase", + ] as const; + for (const phaseType of phasesToRemove) { + this.phaseQueue.removeAll(phaseType); + } - const turnEndPhase = this.findPhase(p => p.phaseName === "TurnEndPhase"); + const turnEndPhase = this.phaseQueue.find("TurnEndPhase"); if (turnEndPhase) { turnEndPhase.upcomingInterlude = true; } diff --git a/src/phase-tree.ts b/src/phase-tree.ts new file mode 100644 index 00000000000..55476f38d65 --- /dev/null +++ b/src/phase-tree.ts @@ -0,0 +1,206 @@ +// biome-ignore-start lint/correctness/noUnusedImports: TSDoc imports +import type { PhaseManager } from "#app/@types/phase-types"; +import type { DynamicPhaseMarker } from "#phases/dynamic-phase-marker"; + +// biome-ignore-end lint/correctness/noUnusedImports: TSDoc imports + +import type { PhaseMap, PhaseString } from "#app/@types/phase-types"; +import type { Phase } from "#app/phase"; +import type { PhaseConditionFunc } from "#types/phase-types"; + +/** + * The PhaseTree is the central storage location for {@linkcode Phase}s by the {@linkcode PhaseManager}. + * + * It has a tiered structure, where unshifted phases are added one level above the currently running Phase. Phases are generally popped from the Tree in FIFO order. + * + * Dynamically ordered phases are queued into the Tree only as {@linkcode DynamicPhaseMarker | Marker}s and as such are not guaranteed to run FIFO (otherwise, they would not be dynamic) + */ +export class PhaseTree { + /** Storage for all levels in the tree. This is a simple array because only one Phase may have "children" at a time. */ + private levels: Phase[][] = [[]]; + /** The level of the currently running {@linkcode Phase} in the Tree (note that such phase is not actually in the Tree while it is running) */ + private currentLevel = 0; + /** + * True if a "deferred" level exists + * @see {@linkcode addPhase} + */ + private deferredActive = false; + + /** + * Adds a {@linkcode Phase} to the specified level + * @param phase - The phase to add + * @param level - The numeric level to add the phase + * @throws Error if `level` is out of legal bounds + */ + private add(phase: Phase, level: number): void { + const addLevel = this.levels[level]; + if (addLevel == null) { + throw new Error("Attempted to add a phase to a nonexistent level of the PhaseTree!\nLevel: " + level.toString()); + } + this.levels[level].push(phase); + } + + /** + * Used by the {@linkcode PhaseManager} to add phases to the Tree + * @param phase - The {@linkcode Phase} to be added + * @param defer - Whether to defer the execution of this phase by allowing subsequently-added phases to run before it + * + * @privateRemarks + * Deferral is implemented by moving the queue at {@linkcode currentLevel} up one level and inserting the new phase below it. + * {@linkcode deferredActive} is set until the moved queue (and anything added to it) is exhausted. + * + * If {@linkcode deferredActive} is `true` when a deferred phase is added, the phase will be pushed to the second-highest level queue. + * That is, it will execute after the originally deferred phase, but there is no possibility for nesting with deferral. + * + * @todo `setPhaseQueueSplice` had strange behavior. This is simpler, but there are probably some remnant edge cases with the current implementation + */ + public addPhase(phase: Phase, defer = false): void { + if (defer && !this.deferredActive) { + this.deferredActive = true; + this.levels.splice(-1, 0, []); + } + this.add(phase, this.levels.length - 1 - +defer); + } + + /** + * Adds a {@linkcode Phase} after the first occurence of the given type, or to the top of the Tree if no such phase exists + * @param phase - The {@linkcode Phase} to be added + * @param type - A {@linkcode PhaseString} representing the type to search for + */ + public addAfter(phase: Phase, type: PhaseString): void { + for (let i = this.levels.length - 1; i >= 0; i--) { + const insertIdx = this.levels[i].findIndex(p => p.is(type)) + 1; + if (insertIdx !== 0) { + this.levels[i].splice(insertIdx, 0, phase); + return; + } + } + + this.addPhase(phase); + } + + /** + * Unshifts a {@linkcode Phase} to the current level. + * This is effectively the same as if the phase were added immediately after the currently-running phase, before it started. + * @param phase - The {@linkcode Phase} to be added + */ + public unshiftToCurrent(phase: Phase): void { + this.levels[this.currentLevel].unshift(phase); + } + + /** + * Pushes a {@linkcode Phase} to the last level of the queue. It will run only after all previously queued phases have been executed. + * @param phase - The {@linkcode Phase} to be added + */ + public pushPhase(phase: Phase): void { + this.add(phase, 0); + } + + /** + * Removes and returns the first {@linkcode Phase} from the topmost level of the tree + * @returns - The next {@linkcode Phase}, or `undefined` if the Tree is empty + */ + public getNextPhase(): Phase | undefined { + this.currentLevel = this.levels.length - 1; + while (this.currentLevel > 0 && this.levels[this.currentLevel].length === 0) { + this.deferredActive = false; + this.levels.pop(); + this.currentLevel--; + } + + // TODO: right now, this is preventing properly marking when one set of unshifted phases ends + this.levels.push([]); + return this.levels[this.currentLevel].shift(); + } + + /** + * Finds a particular {@linkcode Phase} in the Tree by searching in pop order + * @param phaseType - The {@linkcode PhaseString | type} of phase to search for + * @param phaseFilter - A {@linkcode PhaseConditionFunc} to specify conditions for the phase + * @returns The matching {@linkcode Phase}, or `undefined` if none exists + */ + public find

(phaseType: P, phaseFilter?: PhaseConditionFunc

): PhaseMap[P] | undefined { + for (let i = this.levels.length - 1; i >= 0; i--) { + const level = this.levels[i]; + const phase = level.find((p): p is PhaseMap[P] => p.is(phaseType) && (!phaseFilter || phaseFilter(p))); + if (phase) { + return phase; + } + } + } + + /** + * Finds a particular {@linkcode Phase} in the Tree by searching in pop order + * @param phaseType - The {@linkcode PhaseString | type} of phase to search for + * @param phaseFilter - A {@linkcode PhaseConditionFunc} to specify conditions for the phase + * @returns The matching {@linkcode Phase}, or `undefined` if none exists + */ + public findAll

(phaseType: P, phaseFilter?: PhaseConditionFunc

): PhaseMap[P][] { + const phases: PhaseMap[P][] = []; + for (let i = this.levels.length - 1; i >= 0; i--) { + const level = this.levels[i]; + const levelPhases = level.filter((p): p is PhaseMap[P] => p.is(phaseType) && (!phaseFilter || phaseFilter(p))); + phases.push(...levelPhases); + } + return phases; + } + + /** + * Clears the Tree + * @param leaveFirstLevel - If `true`, leaves the top level of the tree intact + * + * @privateremarks + * The parameter on this method exists because {@linkcode PhaseManager.clearPhaseQueue} previously (probably by mistake) ignored `phaseQueuePrepend`. + * + * This is (probably by mistake) relied upon by certain ME functions. + */ + public clear(leaveFirstLevel = false) { + this.levels = [leaveFirstLevel ? (this.levels.at(-1) ?? []) : []]; + } + + /** + * Finds and removes a single {@linkcode Phase} from the Tree + * @param phaseType - The {@linkcode PhaseString | type} of phase to search for + * @param phaseFilter - A {@linkcode PhaseConditionFunc} to specify conditions for the phase + * @returns Whether a removal occurred + */ + public remove

(phaseType: P, phaseFilter?: PhaseConditionFunc

): boolean { + for (let i = this.levels.length - 1; i >= 0; i--) { + const level = this.levels[i]; + const phaseIndex = level.findIndex(p => p.is(phaseType) && (!phaseFilter || phaseFilter(p))); + if (phaseIndex !== -1) { + level.splice(phaseIndex, 1); + return true; + } + } + return false; + } + + /** + * Removes all occurrences of {@linkcode Phase}s of the given type + * @param phaseType - The {@linkcode PhaseString | type} of phase to search for + */ + public removeAll(phaseType: PhaseString): void { + for (let i = 0; i < this.levels.length; i++) { + const level = this.levels[i].filter(phase => !phase.is(phaseType)); + this.levels[i] = level; + } + } + + /** + * Determines if a particular phase exists in the Tree + * @param phaseType - The {@linkcode PhaseString | type} of phase to search for + * @param phaseFilter - A {@linkcode PhaseConditionFunc} to specify conditions for the phase + * @returns Whether a matching phase exists + */ + public exists

(phaseType: P, phaseFilter?: PhaseConditionFunc

): boolean { + for (const level of this.levels) { + for (const phase of level) { + if (phase.is(phaseType) && (!phaseFilter || phaseFilter(phase))) { + return true; + } + } + } + return false; + } +} diff --git a/src/phases/activate-priority-queue-phase.ts b/src/phases/activate-priority-queue-phase.ts deleted file mode 100644 index a31d3291a60..00000000000 --- a/src/phases/activate-priority-queue-phase.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { globalScene } from "#app/global-scene"; -import { Phase } from "#app/phase"; -import type { DynamicPhaseType } from "#enums/dynamic-phase-type"; - -export class ActivatePriorityQueuePhase extends Phase { - public readonly phaseName = "ActivatePriorityQueuePhase"; - private type: DynamicPhaseType; - - constructor(type: DynamicPhaseType) { - super(); - this.type = type; - } - - override start() { - super.start(); - globalScene.phaseManager.startDynamicPhaseType(this.type); - this.end(); - } - - public getType(): DynamicPhaseType { - return this.type; - } -} diff --git a/src/phases/battle-end-phase.ts b/src/phases/battle-end-phase.ts index 8a798d67554..45b0db76ced 100644 --- a/src/phases/battle-end-phase.ts +++ b/src/phases/battle-end-phase.ts @@ -18,23 +18,11 @@ export class BattleEndPhase extends BattlePhase { super.start(); // cull any extra `BattleEnd` phases from the queue. - globalScene.phaseManager.phaseQueue = globalScene.phaseManager.phaseQueue.filter(phase => { - if (phase.is("BattleEndPhase")) { - this.isVictory ||= phase.isVictory; - return false; - } - return true; - }); - // `phaseQueuePrepend` is private, so we have to use this inefficient loop. - while ( - globalScene.phaseManager.tryRemoveUnshiftedPhase(phase => { - if (phase.is("BattleEndPhase")) { - this.isVictory ||= phase.isVictory; - return true; - } - return false; - }) - ) {} + this.isVictory ||= globalScene.phaseManager.hasPhaseOfType( + "BattleEndPhase", + (phase: BattleEndPhase) => phase.isVictory, + ); + globalScene.phaseManager.removeAllPhasesOfType("BattleEndPhase"); globalScene.gameData.gameStats.battles++; if ( diff --git a/src/phases/check-status-effect-phase.ts b/src/phases/check-status-effect-phase.ts index bdaa536986a..5955cd42c55 100644 --- a/src/phases/check-status-effect-phase.ts +++ b/src/phases/check-status-effect-phase.ts @@ -1,20 +1,14 @@ import { globalScene } from "#app/global-scene"; import { Phase } from "#app/phase"; -import type { BattlerIndex } from "#enums/battler-index"; export class CheckStatusEffectPhase extends Phase { public readonly phaseName = "CheckStatusEffectPhase"; - private order: BattlerIndex[]; - constructor(order: BattlerIndex[]) { - super(); - this.order = order; - } start() { const field = globalScene.getField(); - for (const o of this.order) { - if (field[o].status?.isPostTurn()) { - globalScene.phaseManager.unshiftNew("PostTurnStatusEffectPhase", o); + for (const p of field) { + if (p?.status?.isPostTurn()) { + globalScene.phaseManager.unshiftNew("PostTurnStatusEffectPhase", p.getBattlerIndex()); } } this.end(); diff --git a/src/phases/check-switch-phase.ts b/src/phases/check-switch-phase.ts index 504bb6eb4bd..a55db4203bc 100644 --- a/src/phases/check-switch-phase.ts +++ b/src/phases/check-switch-phase.ts @@ -28,7 +28,8 @@ export class CheckSwitchPhase extends BattlePhase { // ...if the user is playing in Set Mode if (globalScene.battleStyle === BattleStyle.SET) { - return super.end(); + this.end(true); + return; } // ...if the checked Pokemon is somehow not on the field @@ -44,7 +45,8 @@ export class CheckSwitchPhase extends BattlePhase { .slice(1) .filter(p => p.isActive()).length === 0 ) { - return super.end(); + this.end(true); + return; } // ...or if any player Pokemon has an effect that prevents the checked Pokemon from switching @@ -53,7 +55,8 @@ export class CheckSwitchPhase extends BattlePhase { || pokemon.isTrapped() || globalScene.getPlayerField().some(p => p.getTag(BattlerTagType.COMMANDED)) ) { - return super.end(); + this.end(true); + return; } globalScene.ui.showText( @@ -71,10 +74,17 @@ export class CheckSwitchPhase extends BattlePhase { }, () => { globalScene.ui.setMode(UiMode.MESSAGE); - this.end(); + this.end(true); }, ); }, ); } + + public override end(queuePostSummon = false): void { + if (queuePostSummon) { + globalScene.phaseManager.unshiftNew("PostSummonPhase", this.fieldIndex); + } + super.end(); + } } diff --git a/src/phases/dynamic-phase-marker.ts b/src/phases/dynamic-phase-marker.ts new file mode 100644 index 00000000000..e2b241f29de --- /dev/null +++ b/src/phases/dynamic-phase-marker.ts @@ -0,0 +1,17 @@ +import type { PhaseString } from "#app/@types/phase-types"; +import { Phase } from "#app/phase"; + +/** + * This phase exists for the sole purpose of marking the location and type of a dynamic phase for the phase manager + */ +export class DynamicPhaseMarker extends Phase { + public override readonly phaseName = "DynamicPhaseMarker"; + + /** The type of phase which this phase is a marker for */ + public phaseType: PhaseString; + + constructor(type: PhaseString) { + super(); + this.phaseType = type; + } +} diff --git a/src/phases/egg-hatch-phase.ts b/src/phases/egg-hatch-phase.ts index 946288c4fd8..3f9b999e0c1 100644 --- a/src/phases/egg-hatch-phase.ts +++ b/src/phases/egg-hatch-phase.ts @@ -225,7 +225,7 @@ export class EggHatchPhase extends Phase { } end() { - if (globalScene.phaseManager.findPhase(p => p.is("EggHatchPhase"))) { + if (globalScene.phaseManager.hasPhaseOfType("EggHatchPhase")) { this.eggHatchHandler.clear(); } else { globalScene.time.delayedCall(250, () => globalScene.setModifiersVisible(true)); diff --git a/src/phases/encounter-phase.ts b/src/phases/encounter-phase.ts index 0918ced65e5..9345170e718 100644 --- a/src/phases/encounter-phase.ts +++ b/src/phases/encounter-phase.ts @@ -565,29 +565,6 @@ export class EncounterPhase extends BattlePhase { }); if (![BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER].includes(globalScene.currentBattle.battleType)) { - enemyField.map(p => - globalScene.phaseManager.pushConditionalPhase( - globalScene.phaseManager.create("PostSummonPhase", p.getBattlerIndex()), - () => { - // if there is not a player party, we can't continue - if (globalScene.getPlayerParty().length === 0) { - return false; - } - // how many player pokemon are on the field ? - const pokemonsOnFieldCount = globalScene.getPlayerParty().filter(p => p.isOnField()).length; - // if it's a 2vs1, there will never be a 2nd pokemon on our field even - const requiredPokemonsOnField = Math.min( - globalScene.getPlayerParty().filter(p => !p.isFainted()).length, - 2, - ); - // if it's a double, there should be 2, otherwise 1 - if (globalScene.currentBattle.double) { - return pokemonsOnFieldCount === requiredPokemonsOnField; - } - return pokemonsOnFieldCount === 1; - }, - ), - ); const ivScannerModifier = globalScene.findModifier(m => m instanceof IvScannerModifier); if (ivScannerModifier) { enemyField.map(p => globalScene.phaseManager.pushNew("ScanIvsPhase", p.getBattlerIndex())); @@ -596,36 +573,30 @@ export class EncounterPhase extends BattlePhase { if (!this.loaded) { const availablePartyMembers = globalScene.getPokemonAllowedInBattle(); + const minPartySize = globalScene.currentBattle.double ? 2 : 1; + const currentBattle = globalScene.currentBattle; + const checkSwitch = + currentBattle.battleType !== BattleType.TRAINER + && (currentBattle.waveIndex > 1 || !globalScene.gameMode.isDaily) + && availablePartyMembers.length > minPartySize; + const phaseManager = globalScene.phaseManager; if (!availablePartyMembers[0].isOnField()) { - globalScene.phaseManager.pushNew("SummonPhase", 0); + phaseManager.pushNew("SummonPhase", 0, true, false, checkSwitch); } - if (globalScene.currentBattle.double) { + if (currentBattle.double) { if (availablePartyMembers.length > 1) { - globalScene.phaseManager.pushNew("ToggleDoublePositionPhase", true); + phaseManager.pushNew("ToggleDoublePositionPhase", true); if (!availablePartyMembers[1].isOnField()) { - globalScene.phaseManager.pushNew("SummonPhase", 1); + phaseManager.pushNew("SummonPhase", 1, true, false, checkSwitch); } } } else { if (availablePartyMembers.length > 1 && availablePartyMembers[1].isOnField()) { globalScene.phaseManager.pushNew("ReturnPhase", 1); } - globalScene.phaseManager.pushNew("ToggleDoublePositionPhase", false); - } - - if ( - globalScene.currentBattle.battleType !== BattleType.TRAINER - && (globalScene.currentBattle.waveIndex > 1 || !globalScene.gameMode.isDaily) - ) { - const minPartySize = globalScene.currentBattle.double ? 2 : 1; - if (availablePartyMembers.length > minPartySize) { - globalScene.phaseManager.pushNew("CheckSwitchPhase", 0, globalScene.currentBattle.double); - if (globalScene.currentBattle.double) { - globalScene.phaseManager.pushNew("CheckSwitchPhase", 1, globalScene.currentBattle.double); - } - } + phaseManager.pushNew("ToggleDoublePositionPhase", false); } } handleTutorial(Tutorial.Access_Menu).then(() => super.end()); diff --git a/src/phases/game-over-phase.ts b/src/phases/game-over-phase.ts index dd29b97d590..f229f872958 100644 --- a/src/phases/game-over-phase.ts +++ b/src/phases/game-over-phase.ts @@ -84,19 +84,12 @@ export class GameOverPhase extends BattlePhase { globalScene.phaseManager.pushNew("EncounterPhase", true); const availablePartyMembers = globalScene.getPokemonAllowedInBattle().length; - - globalScene.phaseManager.pushNew("SummonPhase", 0); - if (globalScene.currentBattle.double && availablePartyMembers > 1) { - globalScene.phaseManager.pushNew("SummonPhase", 1); - } - if ( + const checkSwitch = globalScene.currentBattle.waveIndex > 1 - && globalScene.currentBattle.battleType !== BattleType.TRAINER - ) { - globalScene.phaseManager.pushNew("CheckSwitchPhase", 0, globalScene.currentBattle.double); - if (globalScene.currentBattle.double && availablePartyMembers > 1) { - globalScene.phaseManager.pushNew("CheckSwitchPhase", 1, globalScene.currentBattle.double); - } + && globalScene.currentBattle.battleType !== BattleType.TRAINER; + globalScene.phaseManager.pushNew("SummonPhase", 0, true, false, checkSwitch); + if (globalScene.currentBattle.double && availablePartyMembers > 1) { + globalScene.phaseManager.pushNew("SummonPhase", 1, true, false, checkSwitch); } globalScene.ui.fadeIn(1250); @@ -267,7 +260,6 @@ export class GameOverPhase extends BattlePhase { .then(success => doGameOver(!globalScene.gameMode.isDaily || !!success)) .catch(_err => { globalScene.phaseManager.clearPhaseQueue(); - globalScene.phaseManager.clearPhaseQueueSplice(); globalScene.phaseManager.unshiftNew("MessagePhase", i18next.t("menu:serverCommunicationFailed"), 2500); // force the game to reload after 2 seconds. setTimeout(() => { diff --git a/src/phases/learn-move-phase.ts b/src/phases/learn-move-phase.ts index 4fc38b08d16..bbd1d0f5a2e 100644 --- a/src/phases/learn-move-phase.ts +++ b/src/phases/learn-move-phase.ts @@ -187,7 +187,7 @@ export class LearnMovePhase extends PlayerPartyMemberPokemonPhase { pokemon.usedTMs = []; } pokemon.usedTMs.push(this.moveId); - globalScene.phaseManager.tryRemovePhase(phase => phase.is("SelectModifierPhase")); + globalScene.phaseManager.tryRemovePhase("SelectModifierPhase"); } else if (this.learnMoveType === LearnMoveType.MEMORY) { if (this.cost !== -1) { if (!Overrides.WAIVE_ROLL_FEE_OVERRIDE) { @@ -197,7 +197,7 @@ export class LearnMovePhase extends PlayerPartyMemberPokemonPhase { } globalScene.playSound("se/buy"); } else { - globalScene.phaseManager.tryRemovePhase(phase => phase.is("SelectModifierPhase")); + globalScene.phaseManager.tryRemovePhase("SelectModifierPhase"); } } pokemon.setMove(index, this.moveId); diff --git a/src/phases/move-charge-phase.ts b/src/phases/move-charge-phase.ts index 0c83db10511..5dd75f4bab8 100644 --- a/src/phases/move-charge-phase.ts +++ b/src/phases/move-charge-phase.ts @@ -75,7 +75,7 @@ export class MoveChargePhase extends PokemonPhase { // Otherwise, add the attack portion to the user's move queue to execute next turn. // TODO: This checks status twice for a single-turn usage... if (instantCharge.value) { - globalScene.phaseManager.tryRemovePhase(phase => phase.is("MoveEndPhase") && phase.getPokemon() === user); + globalScene.phaseManager.tryRemovePhase("MoveEndPhase", phase => phase.getPokemon() === user); globalScene.phaseManager.unshiftNew("MovePhase", user, [this.targetIndex], this.move, this.useMode); } else { user.pushMoveQueue({ move: move.id, targets: [this.targetIndex], useMode: this.useMode }); diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index 18e25b328f8..be6d0164698 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -1,7 +1,6 @@ import { applyAbAttrs } from "#abilities/apply-ab-attrs"; import { globalScene } from "#app/global-scene"; import { getPokemonNameWithAffix } from "#app/messages"; -import type { Phase } from "#app/phase"; import { ConditionalProtectTag } from "#data/arena-tag"; import { MoveAnim } from "#data/battle-anims"; import { DamageProtectedTag, ProtectedTag, SemiInvulnerableTag, SubstituteTag, TypeBoostTag } from "#data/battler-tags"; @@ -17,6 +16,7 @@ import { MoveCategory } from "#enums/move-category"; import { MoveEffectTrigger } from "#enums/move-effect-trigger"; import { MoveFlags } from "#enums/move-flags"; import { MoveId } from "#enums/move-id"; +import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier"; import { MoveResult } from "#enums/move-result"; import { MoveTarget } from "#enums/move-target"; import { isReflected, MoveUseMode } from "#enums/move-use-mode"; @@ -67,12 +67,6 @@ export class MoveEffectPhase extends PokemonPhase { /** Is this the last strike of a move? */ private lastHit: boolean; - /** - * Phases queued during moves; used to add a new MovePhase for reflected moves after triggering. - * TODO: Remove this and move the reflection logic to ability-side - */ - private queuedPhases: Phase[] = []; - /** * @param useMode - The {@linkcode MoveUseMode} corresponding to how this move was used. */ @@ -148,7 +142,7 @@ export class MoveEffectPhase extends PokemonPhase { } /** - * Queue the phaes that should occur when the target reflects the move back to the user + * Queue the phases that should occur when the target reflects the move back to the user * @param user - The {@linkcode Pokemon} using this phase's invoked move * @param target - The {@linkcode Pokemon} that is reflecting the move * TODO: Rework this to use `onApply` of Magic Coat @@ -159,24 +153,21 @@ export class MoveEffectPhase extends PokemonPhase { : [user.getBattlerIndex()]; // TODO: ability displays should be handled by the ability if (!target.getTag(BattlerTagType.MAGIC_COAT)) { - this.queuedPhases.push( - globalScene.phaseManager.create( - "ShowAbilityPhase", - target.getBattlerIndex(), - target.getPassiveAbility().hasAttr("ReflectStatusMoveAbAttr"), - ), + globalScene.phaseManager.unshiftNew( + "ShowAbilityPhase", + target.getBattlerIndex(), + target.getPassiveAbility().hasAttr("ReflectStatusMoveAbAttr"), ); - this.queuedPhases.push(globalScene.phaseManager.create("HideAbilityPhase")); + globalScene.phaseManager.unshiftNew("HideAbilityPhase"); } - this.queuedPhases.push( - globalScene.phaseManager.create( - "MovePhase", - target, - newTargets, - new PokemonMove(this.move.id), - MoveUseMode.REFLECTED, - ), + globalScene.phaseManager.unshiftNew( + "MovePhase", + target, + newTargets, + new PokemonMove(this.move.id), + MoveUseMode.REFLECTED, + MovePhaseTimingModifier.FIRST, ); } @@ -344,9 +335,6 @@ export class MoveEffectPhase extends PokemonPhase { return; } - if (this.queuedPhases.length > 0) { - globalScene.phaseManager.appendToPhase(this.queuedPhases, "MoveEndPhase"); - } const moveType = user.getMoveType(this.move, true); if (this.move.category !== MoveCategory.STATUS && !user.stellarTypesBoosted.includes(moveType)) { user.stellarTypesBoosted.push(moveType); @@ -905,10 +893,7 @@ export class MoveEffectPhase extends PokemonPhase { * @param target - The {@linkcode Pokemon} that fainted */ protected onFaintTarget(user: Pokemon, target: Pokemon): void { - // set splice index here, so future scene queues happen before FaintedPhase - globalScene.phaseManager.setPhaseQueueSplice(); - - globalScene.phaseManager.unshiftNew("FaintPhase", target.getBattlerIndex(), false, user); + globalScene.phaseManager.queueFaintPhase(target.getBattlerIndex(), false, user); target.destroySubstitute(); target.lapseTag(BattlerTagType.COMMANDED); diff --git a/src/phases/move-header-phase.ts b/src/phases/move-header-phase.ts index 5c69dcd1217..5b8a6f998a1 100644 --- a/src/phases/move-header-phase.ts +++ b/src/phases/move-header-phase.ts @@ -5,8 +5,8 @@ import { BattlePhase } from "#phases/battle-phase"; export class MoveHeaderPhase extends BattlePhase { public readonly phaseName = "MoveHeaderPhase"; - public pokemon: Pokemon; public move: PokemonMove; + public pokemon: Pokemon; constructor(pokemon: Pokemon, move: PokemonMove) { super(); @@ -15,6 +15,10 @@ export class MoveHeaderPhase extends BattlePhase { this.move = move; } + public getPokemon(): Pokemon { + return this.pokemon; + } + canMove(): boolean { return this.pokemon.isActive(true) && this.move.isUsable(this.pokemon); } diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index 96943065ff0..5e85401db77 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -3,6 +3,7 @@ import { MOVE_COLOR } from "#app/constants/colors"; import { globalScene } from "#app/global-scene"; import { getPokemonNameWithAffix } from "#app/messages"; import Overrides from "#app/overrides"; +import { PokemonPhase } from "#app/phases/pokemon-phase"; import { CenterOfAttentionTag } from "#data/battler-tags"; import { SpeciesFormChangePreMoveTrigger } from "#data/form-change-triggers"; import { getStatusEffectActivationText, getStatusEffectHealText } from "#data/status-effect"; @@ -15,6 +16,7 @@ import { BattlerTagType } from "#enums/battler-tag-type"; import { CommonAnim } from "#enums/move-anims-common"; import { MoveFlags } from "#enums/move-flags"; import { MoveId } from "#enums/move-id"; +import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier"; import { MoveResult } from "#enums/move-result"; import { isIgnorePP, isIgnoreStatus, isReflected, isVirtual, MoveUseMode } from "#enums/move-use-mode"; import { PokemonType } from "#enums/pokemon-type"; @@ -24,20 +26,19 @@ import type { Pokemon } from "#field/pokemon"; import { applyMoveAttrs } from "#moves/apply-attrs"; import { frenzyMissFunc } from "#moves/move-utils"; import type { PokemonMove } from "#moves/pokemon-move"; -import { BattlePhase } from "#phases/battle-phase"; import type { TurnMove } from "#types/turn-move"; import { NumberHolder } from "#utils/common"; import { enumValueToKey } from "#utils/enums"; import i18next from "i18next"; -export class MovePhase extends BattlePhase { +export class MovePhase extends PokemonPhase { public readonly phaseName = "MovePhase"; protected _pokemon: Pokemon; - protected _move: PokemonMove; + public move: PokemonMove; protected _targets: BattlerIndex[]; public readonly useMode: MoveUseMode; // Made public for quash - /** Whether the current move is forced last (used for Quash). */ - protected forcedLast: boolean; + /** The timing modifier of the move (used by Quash and to force called moves to the front of their queue) */ + public timingModifier: MovePhaseTimingModifier; /** Whether the current move should fail but still use PP. */ protected failed = false; /** Whether the current move should fail and retain PP. */ @@ -59,14 +60,6 @@ export class MovePhase extends BattlePhase { this._pokemon = pokemon; } - public get move(): PokemonMove { - return this._move; - } - - protected set move(move: PokemonMove) { - this._move = move; - } - public get targets(): BattlerIndex[] { return this._targets; } @@ -81,16 +74,22 @@ export class MovePhase extends BattlePhase { * @param move - The {@linkcode PokemonMove} to use * @param useMode - The {@linkcode MoveUseMode} corresponding to this move's means of execution (usually `MoveUseMode.NORMAL`). * Not marked optional to ensure callers correctly pass on `useModes`. - * @param forcedLast - Whether to force this phase to occur last in order (for {@linkcode MoveId.QUASH}); default `false` + * @param timingModifier - The {@linkcode MovePhaseTimingModifier} for the move; Default {@linkcode MovePhaseTimingModifier.NORMAL} */ - constructor(pokemon: Pokemon, targets: BattlerIndex[], move: PokemonMove, useMode: MoveUseMode, forcedLast = false) { - super(); + constructor( + pokemon: Pokemon, + targets: BattlerIndex[], + move: PokemonMove, + useMode: MoveUseMode, + timingModifier: MovePhaseTimingModifier = MovePhaseTimingModifier.NORMAL, + ) { + super(pokemon.getBattlerIndex()); this.pokemon = pokemon; this.targets = targets; this.move = move; this.useMode = useMode; - this.forcedLast = forcedLast; + this.timingModifier = timingModifier; this.moveHistoryEntry = { move: MoveId.NONE, targets, @@ -121,14 +120,6 @@ export class MovePhase extends BattlePhase { this.cancelled = true; } - /** - * Shows whether the current move has been forced to the end of the turn - * Needed for speed order, see {@linkcode MoveId.QUASH} - */ - public isForcedLast(): boolean { - return this.forcedLast; - } - public start(): void { super.start(); diff --git a/src/phases/mystery-encounter-phases.ts b/src/phases/mystery-encounter-phases.ts index 4f50b40c965..bb3f4a92033 100644 --- a/src/phases/mystery-encounter-phases.ts +++ b/src/phases/mystery-encounter-phases.ts @@ -48,7 +48,6 @@ export class MysteryEncounterPhase extends Phase { // Clears out queued phases that are part of standard battle globalScene.phaseManager.clearPhaseQueue(); - globalScene.phaseManager.clearPhaseQueueSplice(); const encounter = globalScene.currentBattle.mysteryEncounter!; encounter.updateSeedOffset(); @@ -233,9 +232,7 @@ export class MysteryEncounterBattleStartCleanupPhase extends Phase { }); // Remove any status tick phases - while (globalScene.phaseManager.findPhase(p => p.is("PostTurnStatusEffectPhase"))) { - globalScene.phaseManager.tryRemovePhase(p => p.is("PostTurnStatusEffectPhase")); - } + globalScene.phaseManager.removeAllPhasesOfType("PostTurnStatusEffectPhase"); // The total number of Pokemon in the player's party that can legally fight const legalPlayerPokemon = globalScene.getPokemonAllowedInBattle(); @@ -412,16 +409,21 @@ export class MysteryEncounterBattlePhase extends Phase { } const availablePartyMembers = globalScene.getPlayerParty().filter(p => p.isAllowedInBattle()); + const minPartySize = globalScene.currentBattle.double ? 2 : 1; + const checkSwitch = + encounterMode !== MysteryEncounterMode.TRAINER_BATTLE + && !this.disableSwitch + && availablePartyMembers.length > minPartySize; if (!availablePartyMembers[0].isOnField()) { - globalScene.phaseManager.pushNew("SummonPhase", 0); + globalScene.phaseManager.pushNew("SummonPhase", 0, true, false, checkSwitch); } if (globalScene.currentBattle.double) { if (availablePartyMembers.length > 1) { globalScene.phaseManager.pushNew("ToggleDoublePositionPhase", true); if (!availablePartyMembers[1].isOnField()) { - globalScene.phaseManager.pushNew("SummonPhase", 1); + globalScene.phaseManager.pushNew("SummonPhase", 1, true, false, checkSwitch); } } } else { @@ -432,16 +434,6 @@ export class MysteryEncounterBattlePhase extends Phase { globalScene.phaseManager.pushNew("ToggleDoublePositionPhase", false); } - if (encounterMode !== MysteryEncounterMode.TRAINER_BATTLE && !this.disableSwitch) { - const minPartySize = globalScene.currentBattle.double ? 2 : 1; - if (availablePartyMembers.length > minPartySize) { - globalScene.phaseManager.pushNew("CheckSwitchPhase", 0, globalScene.currentBattle.double); - if (globalScene.currentBattle.double) { - globalScene.phaseManager.pushNew("CheckSwitchPhase", 1, globalScene.currentBattle.double); - } - } - } - this.end(); } @@ -540,7 +532,7 @@ export class MysteryEncounterRewardsPhase extends Phase { if (encounter.doEncounterRewards) { encounter.doEncounterRewards(); } else if (this.addHealPhase) { - globalScene.phaseManager.tryRemovePhase(p => p.is("SelectModifierPhase")); + globalScene.phaseManager.removeAllPhasesOfType("SelectModifierPhase"); globalScene.phaseManager.unshiftNew("SelectModifierPhase", 0, undefined, { fillRemaining: false, rerollMultiplier: -1, diff --git a/src/phases/new-battle-phase.ts b/src/phases/new-battle-phase.ts index b9a57161bd0..7b5d132ccd2 100644 --- a/src/phases/new-battle-phase.ts +++ b/src/phases/new-battle-phase.ts @@ -6,12 +6,7 @@ export class NewBattlePhase extends BattlePhase { start() { super.start(); - // cull any extra `NewBattle` phases from the queue. - globalScene.phaseManager.phaseQueue = globalScene.phaseManager.phaseQueue.filter( - phase => !phase.is("NewBattlePhase"), - ); - // `phaseQueuePrepend` is private, so we have to use this inefficient loop. - while (globalScene.phaseManager.tryRemoveUnshiftedPhase(phase => phase.is("NewBattlePhase"))) {} + globalScene.phaseManager.removeAllPhasesOfType("NewBattlePhase"); globalScene.newBattle(); diff --git a/src/phases/party-member-pokemon-phase.ts b/src/phases/party-member-pokemon-phase.ts index 9536dafda60..545799cf36a 100644 --- a/src/phases/party-member-pokemon-phase.ts +++ b/src/phases/party-member-pokemon-phase.ts @@ -22,4 +22,8 @@ export abstract class PartyMemberPokemonPhase extends FieldPhase { getPokemon(): Pokemon { return this.getParty()[this.partyMemberIndex]; } + + isPlayer(): boolean { + return this.player; + } } diff --git a/src/phases/post-summon-activate-ability-phase.ts b/src/phases/post-summon-activate-ability-phase.ts index 5f790c01ad1..a2b6c059bee 100644 --- a/src/phases/post-summon-activate-ability-phase.ts +++ b/src/phases/post-summon-activate-ability-phase.ts @@ -6,8 +6,8 @@ import { PostSummonPhase } from "#phases/post-summon-phase"; * Helper to {@linkcode PostSummonPhase} which applies abilities */ export class PostSummonActivateAbilityPhase extends PostSummonPhase { - private priority: number; - private passive: boolean; + private readonly priority: number; + private readonly passive: boolean; constructor(battlerIndex: BattlerIndex, priority: number, passive: boolean) { super(battlerIndex); diff --git a/src/phases/post-summon-phase.ts b/src/phases/post-summon-phase.ts index 5f66cf91eca..136f2fbd601 100644 --- a/src/phases/post-summon-phase.ts +++ b/src/phases/post-summon-phase.ts @@ -1,19 +1,29 @@ import { applyAbAttrs } from "#abilities/apply-ab-attrs"; +import type { PhaseString } from "#app/@types/phase-types"; import { globalScene } from "#app/global-scene"; import { EntryHazardTag } from "#data/arena-tag"; import { MysteryEncounterPostSummonTag } from "#data/battler-tags"; import { ArenaTagType } from "#enums/arena-tag-type"; +import type { BattlerIndex } from "#enums/battler-index"; import { BattlerTagType } from "#enums/battler-tag-type"; import { StatusEffect } from "#enums/status-effect"; import { PokemonPhase } from "#phases/pokemon-phase"; export class PostSummonPhase extends PokemonPhase { public readonly phaseName = "PostSummonPhase"; + /** Used to determine whether to push or unshift {@linkcode PostSummonActivateAbilityPhase}s */ + public readonly source: PhaseString; + + constructor(battlerIndex?: BattlerIndex | number, source: PhaseString = "SwitchSummonPhase") { + super(battlerIndex); + this.source = source; + } + start() { super.start(); const pokemon = this.getPokemon(); - + console.log("Ran PSP for:", pokemon.name); if (pokemon.status?.effect === StatusEffect.TOXIC) { pokemon.status.toxicTurnCount = 0; } @@ -29,8 +39,7 @@ export class PostSummonPhase extends PokemonPhase { ) { pokemon.lapseTag(BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON); } - - const field = pokemon.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField(); + const field = pokemon.isPlayer() ? globalScene.getPlayerField(true) : globalScene.getEnemyField(true); for (const p of field) { applyAbAttrs("CommanderAbAttr", { pokemon: p }); } diff --git a/src/phases/quiet-form-change-phase.ts b/src/phases/quiet-form-change-phase.ts index ef53b16cc56..920ff2252b8 100644 --- a/src/phases/quiet-form-change-phase.ts +++ b/src/phases/quiet-form-change-phase.ts @@ -9,7 +9,6 @@ import { BattleSpec } from "#enums/battle-spec"; import { BattlerTagType } from "#enums/battler-tag-type"; import type { Pokemon } from "#field/pokemon"; import { BattlePhase } from "#phases/battle-phase"; -import type { MovePhase } from "#phases/move-phase"; export class QuietFormChangePhase extends BattlePhase { public readonly phaseName = "QuietFormChangePhase"; @@ -170,12 +169,7 @@ export class QuietFormChangePhase extends BattlePhase { this.pokemon.initBattleInfo(); this.pokemon.cry(); - const movePhase = globalScene.phaseManager.findPhase( - p => p.is("MovePhase") && p.pokemon === this.pokemon, - ) as MovePhase; - if (movePhase) { - movePhase.cancel(); - } + globalScene.phaseManager.cancelMove(p => p.pokemon === this.pokemon); } if (this.formChange.trigger instanceof SpeciesFormChangeTeraTrigger) { const params = { pokemon: this.pokemon }; diff --git a/src/phases/stat-stage-change-phase.ts b/src/phases/stat-stage-change-phase.ts index 2731c037d5f..3c2d1cb5fad 100644 --- a/src/phases/stat-stage-change-phase.ts +++ b/src/phases/stat-stage-change-phase.ts @@ -223,10 +223,7 @@ export class StatStageChangePhase extends PokemonPhase { }); // Look for any other stat change phases; if this is the last one, do White Herb check - const existingPhase = globalScene.phaseManager.findPhase( - p => p.is("StatStageChangePhase") && p.battlerIndex === this.battlerIndex, - ); - if (!existingPhase?.is("StatStageChangePhase")) { + if (!globalScene.phaseManager.hasPhaseOfType("StatStageChangePhase", p => p.battlerIndex === this.battlerIndex)) { // Apply White Herb if needed const whiteHerb = globalScene.applyModifier( ResetNegativeStatStageModifier, @@ -297,49 +294,6 @@ export class StatStageChangePhase extends PokemonPhase { } } - aggregateStatStageChanges(): void { - const accEva: BattleStat[] = [Stat.ACC, Stat.EVA]; - const isAccEva = accEva.some(s => this.stats.includes(s)); - let existingPhase: StatStageChangePhase; - if (this.stats.length === 1) { - while ( - (existingPhase = globalScene.phaseManager.findPhase( - p => - p.is("StatStageChangePhase") - && p.battlerIndex === this.battlerIndex - && p.stats.length === 1 - && p.stats[0] === this.stats[0] - && p.selfTarget === this.selfTarget - && p.showMessage === this.showMessage - && p.ignoreAbilities === this.ignoreAbilities, - ) as StatStageChangePhase) - ) { - this.stages += existingPhase.stages; - - if (!globalScene.phaseManager.tryRemovePhase(p => p === existingPhase)) { - break; - } - } - } - while ( - (existingPhase = globalScene.phaseManager.findPhase( - p => - p.is("StatStageChangePhase") - && p.battlerIndex === this.battlerIndex - && p.selfTarget === this.selfTarget - && accEva.some(s => p.stats.includes(s)) === isAccEva - && p.stages === this.stages - && p.showMessage === this.showMessage - && p.ignoreAbilities === this.ignoreAbilities, - ) as StatStageChangePhase) - ) { - this.stats.push(...existingPhase.stats); - if (!globalScene.phaseManager.tryRemovePhase(p => p === existingPhase)) { - break; - } - } - } - getStatStageChangeMessages(stats: BattleStat[], stages: number, relStages: number[]): string[] { const messages: string[] = []; diff --git a/src/phases/summon-phase.ts b/src/phases/summon-phase.ts index dda70f46ec9..26a8ba40ffc 100644 --- a/src/phases/summon-phase.ts +++ b/src/phases/summon-phase.ts @@ -16,12 +16,14 @@ import i18next from "i18next"; export class SummonPhase extends PartyMemberPokemonPhase { // The union type is needed to keep typescript happy as these phases extend from SummonPhase public readonly phaseName: "SummonPhase" | "SummonMissingPhase" | "SwitchSummonPhase" | "ReturnPhase" = "SummonPhase"; - private loaded: boolean; + private readonly loaded: boolean; + private readonly checkSwitch: boolean; - constructor(fieldIndex: number, player = true, loaded = false) { + constructor(fieldIndex: number, player = true, loaded = false, checkSwitch = false) { super(fieldIndex, player); this.loaded = loaded; + this.checkSwitch = checkSwitch; } start() { @@ -288,7 +290,17 @@ export class SummonPhase extends PartyMemberPokemonPhase { } queuePostSummon(): void { - globalScene.phaseManager.pushNew("PostSummonPhase", this.getPokemon().getBattlerIndex()); + if (this.checkSwitch) { + globalScene.phaseManager.pushNew( + "CheckSwitchPhase", + this.getPokemon().getFieldIndex(), + globalScene.currentBattle.double, + ); + } else { + globalScene.phaseManager.pushNew("PostSummonPhase", this.getPokemon().getBattlerIndex(), this.phaseName); + } + + globalScene.phaseManager.tryAddEnemyPostSummonPhases(); } end() { @@ -296,4 +308,8 @@ export class SummonPhase extends PartyMemberPokemonPhase { super.end(); } + + public getFieldIndex(): number { + return this.fieldIndex; + } } diff --git a/src/phases/switch-phase.ts b/src/phases/switch-phase.ts index 83a699b6b08..9ab06ec827c 100644 --- a/src/phases/switch-phase.ts +++ b/src/phases/switch-phase.ts @@ -1,5 +1,4 @@ import { globalScene } from "#app/global-scene"; -import { DynamicPhaseType } from "#enums/dynamic-phase-type"; import { SwitchType } from "#enums/switch-type"; import { UiMode } from "#enums/ui-mode"; import { BattlePhase } from "#phases/battle-phase"; @@ -77,14 +76,6 @@ export class SwitchPhase extends BattlePhase { fieldIndex, (slotIndex: number, option: PartyOption) => { if (slotIndex >= globalScene.currentBattle.getBattlerCount() && slotIndex < 6) { - // Remove any pre-existing PostSummonPhase under the same field index. - // Pre-existing PostSummonPhases may occur when this phase is invoked during a prompt to switch at the start of a wave. - // TODO: Separate the animations from `SwitchSummonPhase` and co. into another phase and use that on initial switch - this is a band-aid fix - globalScene.phaseManager.tryRemoveDynamicPhase( - DynamicPhaseType.POST_SUMMON, - p => p.is("PostSummonPhase") && p.player && p.fieldIndex === this.fieldIndex, - "all", - ); const switchType = option === PartyOption.PASS_BATON ? SwitchType.BATON_PASS : this.switchType; globalScene.phaseManager.unshiftNew("SwitchSummonPhase", switchType, fieldIndex, slotIndex, this.doReturn); } diff --git a/src/phases/switch-summon-phase.ts b/src/phases/switch-summon-phase.ts index ac47068c619..8cc7843b55f 100644 --- a/src/phases/switch-summon-phase.ts +++ b/src/phases/switch-summon-phase.ts @@ -241,11 +241,11 @@ export class SwitchSummonPhase extends SummonPhase { globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true); // Reverts to weather-based forms when weather suppressors (Cloud Nine/Air Lock) are switched out - globalScene.arena.triggerWeatherBasedFormChanges(); + globalScene.arena.triggerWeatherBasedFormChanges(pokemon); } queuePostSummon(): void { - globalScene.phaseManager.startNewDynamicPhase("PostSummonPhase", this.getPokemon().getBattlerIndex()); + globalScene.phaseManager.unshiftNew("PostSummonPhase", this.getPokemon().getBattlerIndex()); } /** diff --git a/src/phases/title-phase.ts b/src/phases/title-phase.ts index 414be4c820c..1920db8d20e 100644 --- a/src/phases/title-phase.ts +++ b/src/phases/title-phase.ts @@ -315,23 +315,15 @@ export class TitlePhase extends Phase { if (this.loaded) { const availablePartyMembers = globalScene.getPokemonAllowedInBattle().length; - - globalScene.phaseManager.pushNew("SummonPhase", 0, true, true); - if (globalScene.currentBattle.double && availablePartyMembers > 1) { - globalScene.phaseManager.pushNew("SummonPhase", 1, true, true); - } - - if ( + const minPartySize = globalScene.currentBattle.double ? 2 : 1; + const checkSwitch = globalScene.currentBattle.battleType !== BattleType.TRAINER && (globalScene.currentBattle.waveIndex > 1 || !globalScene.gameMode.isDaily) - ) { - const minPartySize = globalScene.currentBattle.double ? 2 : 1; - if (availablePartyMembers > minPartySize) { - globalScene.phaseManager.pushNew("CheckSwitchPhase", 0, globalScene.currentBattle.double); - if (globalScene.currentBattle.double) { - globalScene.phaseManager.pushNew("CheckSwitchPhase", 1, globalScene.currentBattle.double); - } - } + && availablePartyMembers > minPartySize; + + globalScene.phaseManager.pushNew("SummonPhase", 0, true, true, checkSwitch); + if (globalScene.currentBattle.double && availablePartyMembers > 1) { + globalScene.phaseManager.pushNew("SummonPhase", 1, true, true, checkSwitch); } } diff --git a/src/phases/turn-end-phase.ts b/src/phases/turn-end-phase.ts index 463f26e73a2..22ebbd2607b 100644 --- a/src/phases/turn-end-phase.ts +++ b/src/phases/turn-end-phase.ts @@ -25,6 +25,7 @@ export class TurnEndPhase extends FieldPhase { globalScene.currentBattle.incrementTurn(); globalScene.eventTarget.dispatchEvent(new TurnEndEvent(globalScene.currentBattle.turn)); + globalScene.phaseManager.dynamicQueueManager.clearLastTurnOrder(); globalScene.phaseManager.hideAbilityBar(); diff --git a/src/phases/turn-start-phase.ts b/src/phases/turn-start-phase.ts index 1733901d527..cd45a73c813 100644 --- a/src/phases/turn-start-phase.ts +++ b/src/phases/turn-start-phase.ts @@ -1,89 +1,31 @@ import { applyAbAttrs } from "#abilities/apply-ab-attrs"; import type { TurnCommand } from "#app/battle"; import { globalScene } from "#app/global-scene"; -import { TrickRoomTag } from "#data/arena-tag"; -import { allMoves } from "#data/data-lists"; -import { BattlerIndex } from "#enums/battler-index"; +import { ArenaTagSide } from "#enums/arena-tag-side"; +import type { BattlerIndex } from "#enums/battler-index"; import { Command } from "#enums/command"; -import { Stat } from "#enums/stat"; import { SwitchType } from "#enums/switch-type"; import type { Pokemon } from "#field/pokemon"; import { BypassSpeedChanceModifier } from "#modifiers/modifier"; import { PokemonMove } from "#moves/pokemon-move"; import { FieldPhase } from "#phases/field-phase"; -import { BooleanHolder, randSeedShuffle } from "#utils/common"; +import { inSpeedOrder } from "#utils/speed-order-generator"; export class TurnStartPhase extends FieldPhase { public readonly phaseName = "TurnStartPhase"; /** - * Helper method to retrieve the current speed order of the combattants. - * It also checks for Trick Room and reverses the array if it is present. - * @returns The {@linkcode BattlerIndex}es of all on-field Pokemon, sorted in speed order. - * @todo Make this private - */ - getSpeedOrder(): BattlerIndex[] { - const playerField = globalScene.getPlayerField().filter(p => p.isActive()); - const enemyField = globalScene.getEnemyField().filter(p => p.isActive()); - - // Shuffle the list before sorting so speed ties produce random results - // This is seeded with the current turn to prevent turn order varying - // based on how long since you last reloaded. - let orderedTargets = (playerField as Pokemon[]).concat(enemyField); - globalScene.executeWithSeedOffset( - () => { - orderedTargets = randSeedShuffle(orderedTargets); - }, - globalScene.currentBattle.turn, - globalScene.waveSeed, - ); - - // Check for Trick Room and reverse sort order if active. - // Notably, Pokerogue does NOT have the "outspeed trick room" glitch at >1809 spd. - const speedReversed = new BooleanHolder(false); - globalScene.arena.applyTags(TrickRoomTag, false, speedReversed); - - orderedTargets.sort((a: Pokemon, b: Pokemon) => { - const aSpeed = a.getEffectiveStat(Stat.SPD); - const bSpeed = b.getEffectiveStat(Stat.SPD); - - return speedReversed.value ? aSpeed - bSpeed : bSpeed - aSpeed; - }); - - return orderedTargets.map(t => t.getFieldIndex() + (t.isEnemy() ? BattlerIndex.ENEMY : BattlerIndex.PLAYER)); - } - - /** - * This takes the result of {@linkcode getSpeedOrder} and applies priority / bypass speed attributes to it. - * This also considers the priority levels of various commands and changes the result of `getSpeedOrder` based on such. - * @returns The `BattlerIndex`es of all on-field Pokemon sorted in action order. + * Returns an ordering of the current field based on command priority + * @returns The sequence of commands for this turn */ getCommandOrder(): BattlerIndex[] { - let moveOrder = this.getSpeedOrder(); - // The creation of the battlerBypassSpeed object contains checks for the ability Quick Draw and the held item Quick Claw - // The ability Mycelium Might disables Quick Claw's activation when using a status move - // This occurs before the main loop because of battles with more than two Pokemon - const battlerBypassSpeed = {}; - - globalScene.getField(true).forEach(p => { - const bypassSpeed = new BooleanHolder(false); - const canCheckHeldItems = new BooleanHolder(true); - applyAbAttrs("BypassSpeedChanceAbAttr", { pokemon: p, bypass: bypassSpeed }); - applyAbAttrs("PreventBypassSpeedChanceAbAttr", { - pokemon: p, - bypass: bypassSpeed, - canCheckHeldItems, - }); - if (canCheckHeldItems.value) { - globalScene.applyModifiers(BypassSpeedChanceModifier, p.isPlayer(), p, bypassSpeed); - } - battlerBypassSpeed[p.getBattlerIndex()] = bypassSpeed; - }); + const playerField = globalScene.getPlayerField(true).map(p => p.getBattlerIndex()); + const enemyField = globalScene.getEnemyField(true).map(p => p.getBattlerIndex()); + const orderedTargets: BattlerIndex[] = playerField.concat(enemyField); // The function begins sorting orderedTargets based on command priority, move priority, and possible speed bypasses. // Non-FIGHT commands (SWITCH, BALL, RUN) have a higher command priority and will always occur before any FIGHT commands. - moveOrder = moveOrder.slice(0); - moveOrder.sort((a, b) => { + orderedTargets.sort((a, b) => { const aCommand = globalScene.currentBattle.turnCommands[a]; const bCommand = globalScene.currentBattle.turnCommands[b]; @@ -94,41 +36,14 @@ export class TurnStartPhase extends FieldPhase { if (bCommand?.command === Command.FIGHT) { return -1; } - } else if (aCommand?.command === Command.FIGHT) { - const aMove = allMoves[aCommand.move!.move]; - const bMove = allMoves[bCommand!.move!.move]; - - const aUser = globalScene.getField(true).find(p => p.getBattlerIndex() === a)!; - const bUser = globalScene.getField(true).find(p => p.getBattlerIndex() === b)!; - - const aPriority = aMove.getPriority(aUser, false); - const bPriority = bMove.getPriority(bUser, false); - - // The game now checks for differences in priority levels. - // If the moves share the same original priority bracket, it can check for differences in battlerBypassSpeed and return the result. - // This conditional is used to ensure that Quick Claw can still activate with abilities like Stall and Mycelium Might (attack moves only) - // Otherwise, the game returns the user of the move with the highest priority. - const isSameBracket = Math.ceil(aPriority) - Math.ceil(bPriority) === 0; - if (aPriority !== bPriority) { - if (isSameBracket && battlerBypassSpeed[a].value !== battlerBypassSpeed[b].value) { - return battlerBypassSpeed[a].value ? -1 : 1; - } - return aPriority < bPriority ? 1 : -1; - } } - // If there is no difference between the move's calculated priorities, - // check for differences in battlerBypassSpeed and returns the result. - if (battlerBypassSpeed[a].value !== battlerBypassSpeed[b].value) { - return battlerBypassSpeed[a].value ? -1 : 1; - } - - const aIndex = moveOrder.indexOf(a); - const bIndex = moveOrder.indexOf(b); + const aIndex = orderedTargets.indexOf(a); + const bIndex = orderedTargets.indexOf(b); return aIndex < bIndex ? -1 : aIndex > bIndex ? 1 : 0; }); - return moveOrder; + return orderedTargets; } // TODO: Refactor this alongside `CommandPhase.handleCommand` to use SEPARATE METHODS @@ -139,9 +54,8 @@ export class TurnStartPhase extends FieldPhase { const field = globalScene.getField(); const moveOrder = this.getCommandOrder(); - for (const o of this.getSpeedOrder()) { - const pokemon = field[o]; - const preTurnCommand = globalScene.currentBattle.preTurnCommands[o]; + for (const pokemon of inSpeedOrder(ArenaTagSide.BOTH)) { + const preTurnCommand = globalScene.currentBattle.preTurnCommands[pokemon.getBattlerIndex()]; if (preTurnCommand?.skip) { continue; @@ -154,6 +68,10 @@ export class TurnStartPhase extends FieldPhase { } const phaseManager = globalScene.phaseManager; + for (const pokemon of inSpeedOrder(ArenaTagSide.BOTH)) { + applyAbAttrs("BypassSpeedChanceAbAttr", { pokemon }); + globalScene.applyModifiers(BypassSpeedChanceModifier, pokemon.isPlayer(), pokemon); + } moveOrder.forEach((o, index) => { const pokemon = field[o]; @@ -178,13 +96,8 @@ export class TurnStartPhase extends FieldPhase { // TODO: Re-order these phases to be consistent with mainline turn order: // https://www.smogon.com/forums/threads/sword-shield-battle-mechanics-research.3655528/page-64#post-9244179 - phaseManager.pushNew("WeatherEffectPhase"); - phaseManager.pushNew("PositionalTagPhase"); - phaseManager.pushNew("BerryPhase"); - - phaseManager.pushNew("CheckStatusEffectPhase", moveOrder); - - phaseManager.pushNew("TurnEndPhase"); + // TODO: In an ideal world, this is handled by the phase manager. The change is nontrivial due to the ordering of post-turn phases like those queued by VictoryPhase + globalScene.phaseManager.queueTurnEndPhases(); /* * `this.end()` will call `PhaseManager#shiftPhase()`, which dumps everything from `phaseQueuePrepend` diff --git a/src/queues/move-phase-priority-queue.ts b/src/queues/move-phase-priority-queue.ts new file mode 100644 index 00000000000..5f0b20c3c2e --- /dev/null +++ b/src/queues/move-phase-priority-queue.ts @@ -0,0 +1,103 @@ +import type { PokemonMove } from "#app/data/moves/pokemon-move"; +import type { Pokemon } from "#app/field/pokemon"; +import { globalScene } from "#app/global-scene"; +import type { MovePhase } from "#app/phases/move-phase"; +import { PokemonPhasePriorityQueue } from "#app/queues/pokemon-phase-priority-queue"; +import type { BattlerIndex } from "#enums/battler-index"; +import type { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier"; +import type { PhaseConditionFunc } from "#types/phase-types"; + +/** A priority queue responsible for the ordering of {@linkcode MovePhase}s */ +export class MovePhasePriorityQueue extends PokemonPhasePriorityQueue { + private lastTurnOrder: Pokemon[] = []; + + protected override reorder(): void { + super.reorder(); + this.sortPostSpeed(); + } + + public cancelMove(condition: PhaseConditionFunc<"MovePhase">): void { + this.queue.find(p => condition(p))?.cancel(); + } + + public setTimingModifier(condition: PhaseConditionFunc<"MovePhase">, modifier: MovePhaseTimingModifier): void { + const phase = this.queue.find(p => condition(p)); + if (phase != null) { + phase.timingModifier = modifier; + } + } + + public setMoveForPhase(condition: PhaseConditionFunc<"MovePhase">, move: PokemonMove) { + const phase = this.queue.find(p => condition(p)); + if (phase != null) { + phase.move = move; + } + } + + public redirectMoves(removedPokemon: Pokemon, allyPokemon: Pokemon): void { + // failsafe: if not a double battle just return + if (!globalScene.currentBattle.double) { + return; + } + + // TODO: simplify later + if (allyPokemon?.isActive(true)) { + this.queue + .filter( + mp => + mp.targets.length === 1 + && mp.targets[0] === removedPokemon.getBattlerIndex() + && mp.pokemon.isPlayer() !== allyPokemon.isPlayer(), + ) + .forEach(targetingMovePhase => { + if (targetingMovePhase && targetingMovePhase.targets[0] !== allyPokemon.getBattlerIndex()) { + targetingMovePhase.targets[0] = allyPokemon.getBattlerIndex(); + } + }); + } + } + + public setMoveOrder(order: BattlerIndex[]) { + this.setOrder = order; + } + + public override pop(): MovePhase | undefined { + this.reorder(); + const phase = this.queue.shift(); + if (phase) { + this.lastTurnOrder.push(phase.pokemon); + } + return phase; + } + + public getTurnOrder(): Pokemon[] { + return this.lastTurnOrder; + } + + public clearTurnOrder(): void { + this.lastTurnOrder = []; + } + + public override clear(): void { + this.setOrder = undefined; + this.lastTurnOrder = []; + super.clear(); + } + + private sortPostSpeed(): void { + this.queue.sort((a: MovePhase, b: MovePhase) => { + const priority = [a, b].map(movePhase => { + const move = movePhase.move.getMove(); + return move.getPriority(movePhase.pokemon, true); + }); + + const timingModifiers = [a, b].map(movePhase => movePhase.timingModifier); + + if (timingModifiers[0] !== timingModifiers[1]) { + return timingModifiers[1] - timingModifiers[0]; + } + + return priority[1] - priority[0]; + }); + } +} diff --git a/src/queues/pokemon-phase-priority-queue.ts b/src/queues/pokemon-phase-priority-queue.ts new file mode 100644 index 00000000000..3098c5be435 --- /dev/null +++ b/src/queues/pokemon-phase-priority-queue.ts @@ -0,0 +1,20 @@ +import type { DynamicPhase } from "#app/@types/phase-types"; +import { PriorityQueue } from "#app/queues/priority-queue"; +import { sortInSpeedOrder } from "#app/utils/speed-order"; +import type { BattlerIndex } from "#enums/battler-index"; + +/** A generic speed-based priority queue of {@linkcode DynamicPhase}s */ +export class PokemonPhasePriorityQueue extends PriorityQueue { + protected setOrder: BattlerIndex[] | undefined; + protected override reorder(): void { + const setOrder = this.setOrder; + if (setOrder) { + this.queue.sort( + (a, b) => + setOrder.indexOf(a.getPokemon().getBattlerIndex()) - setOrder.indexOf(b.getPokemon().getBattlerIndex()), + ); + } else { + this.queue = sortInSpeedOrder(this.queue); + } + } +} diff --git a/src/queues/pokemon-priority-queue.ts b/src/queues/pokemon-priority-queue.ts new file mode 100644 index 00000000000..597bfb32c0d --- /dev/null +++ b/src/queues/pokemon-priority-queue.ts @@ -0,0 +1,10 @@ +import type { Pokemon } from "#app/field/pokemon"; +import { PriorityQueue } from "#app/queues/priority-queue"; +import { sortInSpeedOrder } from "#app/utils/speed-order"; + +/** A priority queue of {@linkcode Pokemon}s */ +export class PokemonPriorityQueue extends PriorityQueue { + protected override reorder(): void { + this.queue = sortInSpeedOrder(this.queue); + } +} diff --git a/src/queues/post-summon-phase-priority-queue.ts b/src/queues/post-summon-phase-priority-queue.ts new file mode 100644 index 00000000000..37da90a1427 --- /dev/null +++ b/src/queues/post-summon-phase-priority-queue.ts @@ -0,0 +1,45 @@ +import { globalScene } from "#app/global-scene"; +import { PostSummonActivateAbilityPhase } from "#app/phases/post-summon-activate-ability-phase"; +import type { PostSummonPhase } from "#app/phases/post-summon-phase"; +import { PokemonPhasePriorityQueue } from "#app/queues/pokemon-phase-priority-queue"; +import { sortInSpeedOrder } from "#app/utils/speed-order"; + +/** + * Priority Queue for {@linkcode PostSummonPhase} and {@linkcode PostSummonActivateAbilityPhase} + * + * Orders phases first by ability priority, then by the {@linkcode Pokemon}'s effective speed + */ +export class PostSummonPhasePriorityQueue extends PokemonPhasePriorityQueue { + protected override reorder(): void { + this.queue = sortInSpeedOrder(this.queue, false); + this.queue.sort((phaseA, phaseB) => phaseB.getPriority() - phaseA.getPriority()); + } + + public override push(phase: PostSummonPhase): void { + super.push(phase); + this.queueAbilityPhase(phase); + } + + /** + * Queues all necessary {@linkcode PostSummonActivateAbilityPhase}s for each pushed {@linkcode PostSummonPhase} + * @param phase - The {@linkcode PostSummonPhase} that was pushed onto the queue + */ + private queueAbilityPhase(phase: PostSummonPhase): void { + if (phase instanceof PostSummonActivateAbilityPhase) { + return; + } + + const phasePokemon = phase.getPokemon(); + + phasePokemon.getAbilityPriorities().forEach((priority, idx) => { + const activateAbilityPhase = new PostSummonActivateAbilityPhase( + phasePokemon.getBattlerIndex(), + priority, + idx !== 0, + ); + phase.source === "SummonPhase" + ? globalScene.phaseManager.pushPhase(activateAbilityPhase) + : globalScene.phaseManager.unshiftPhase(activateAbilityPhase); + }); + } +} diff --git a/src/queues/priority-queue.ts b/src/queues/priority-queue.ts new file mode 100644 index 00000000000..b53cfec3f4d --- /dev/null +++ b/src/queues/priority-queue.ts @@ -0,0 +1,78 @@ +/** + * Stores a list of elements. + * + * Dynamically updates ordering to always pop the highest "priority", based on implementation of {@linkcode reorder}. + */ +export abstract class PriorityQueue { + protected queue: T[] = []; + + /** + * Sorts the elements in the queue + */ + protected abstract reorder(): void; + + /** + * Calls {@linkcode reorder} and shifts the queue + * @returns The front element of the queue after sorting, or `undefined` if the queue is empty + * @sealed + */ + public pop(): T | undefined { + if (this.isEmpty()) { + return; + } + + this.reorder(); + return this.queue.shift(); + } + + /** + * Adds an element to the queue + * @param element The element to add + */ + public push(element: T): void { + this.queue.push(element); + } + + /** + * Removes all elements from the queue + * @sealed + */ + public clear(): void { + this.queue.splice(0, this.queue.length); + } + + /** + * @returns Whether the queue is empty + * @sealed + */ + public isEmpty(): boolean { + return this.queue.length === 0; + } + + /** + * Removes the first element matching the condition + * @param condition - An optional condition function (defaults to a function that always returns `true`) + * @returns Whether a removal occurred + */ + public remove(condition: (t: T) => boolean = () => true): boolean { + // Reorder to remove the first element + this.reorder(); + const index = this.queue.findIndex(condition); + if (index === -1) { + return false; + } + + this.queue.splice(index, 1); + return true; + } + + /** @returns An element matching the condition function */ + public find(condition?: (t: T) => boolean): T | undefined { + return this.queue.find(e => !condition || condition(e)); + } + + /** @returns Whether an element matching the condition function exists */ + public has(condition?: (t: T) => boolean): boolean { + return this.queue.some(e => !condition || condition(e)); + } +} diff --git a/src/utils/speed-order-generator.ts b/src/utils/speed-order-generator.ts new file mode 100644 index 00000000000..24f95de665f --- /dev/null +++ b/src/utils/speed-order-generator.ts @@ -0,0 +1,39 @@ +import { globalScene } from "#app/global-scene"; +import { PokemonPriorityQueue } from "#app/queues/pokemon-priority-queue"; +import { ArenaTagSide } from "#enums/arena-tag-side"; +import type { Pokemon } from "#field/pokemon"; + +/** + * A generator function which uses a priority queue to yield each pokemon from a given side of the field in speed order. + * @param side - The {@linkcode ArenaTagSide | side} of the field to use + * @returns A {@linkcode Generator} of {@linkcode Pokemon} + * + * @remarks + * This should almost always be used by iteration in a `for...of` loop + */ +export function* inSpeedOrder(side: ArenaTagSide = ArenaTagSide.BOTH): Generator { + let pokemonList: Pokemon[]; + switch (side) { + case ArenaTagSide.PLAYER: + pokemonList = globalScene.getPlayerField(true); + break; + case ArenaTagSide.ENEMY: + pokemonList = globalScene.getEnemyField(true); + break; + default: + pokemonList = globalScene.getField(true); + } + + const queue = new PokemonPriorityQueue(); + let i = 0; + pokemonList.forEach(p => { + queue.push(p); + }); + while (!queue.isEmpty()) { + // If the queue is not empty, this can never be undefined + i++; + yield queue.pop()!; + } + + return i; +} diff --git a/src/utils/speed-order.ts b/src/utils/speed-order.ts new file mode 100644 index 00000000000..1d894369bb3 --- /dev/null +++ b/src/utils/speed-order.ts @@ -0,0 +1,57 @@ +import { Pokemon } from "#app/field/pokemon"; +import { globalScene } from "#app/global-scene"; +import { BooleanHolder, randSeedShuffle } from "#app/utils/common"; +import { ArenaTagType } from "#enums/arena-tag-type"; +import { Stat } from "#enums/stat"; + +/** Interface representing an object associated with a specific Pokemon */ +interface hasPokemon { + getPokemon(): Pokemon; +} + +/** + * Sorts an array of {@linkcode Pokemon} by speed, taking Trick Room into account. + * @param pokemonList - The list of Pokemon or objects containing Pokemon + * @param shuffleFirst - Whether to shuffle the list before sorting (to handle speed ties). Default `true`. + * @returns The sorted array of {@linkcode Pokemon} + */ +export function sortInSpeedOrder(pokemonList: T[], shuffleFirst = true): T[] { + pokemonList = shuffleFirst ? shufflePokemonList(pokemonList) : pokemonList; + sortBySpeed(pokemonList); + return pokemonList; +} + +/** + * @param pokemonList - The array of Pokemon or objects containing Pokemon + * @returns The shuffled array + */ +function shufflePokemonList(pokemonList: T[]): T[] { + // This is seeded with the current turn to prevent an inconsistency where it + // was varying based on how long since you last reloaded + globalScene.executeWithSeedOffset( + () => { + pokemonList = randSeedShuffle(pokemonList); + }, + globalScene.currentBattle.turn * 1000 + pokemonList.length, + globalScene.waveSeed, + ); + return pokemonList; +} + +/** Sorts an array of {@linkcode Pokemon} by speed (without shuffling) */ +function sortBySpeed(pokemonList: T[]): void { + pokemonList.sort((a, b) => { + const aSpeed = (a instanceof Pokemon ? a : a.getPokemon()).getEffectiveStat(Stat.SPD); + const bSpeed = (b instanceof Pokemon ? b : b.getPokemon()).getEffectiveStat(Stat.SPD); + + return bSpeed - aSpeed; + }); + + /** 'true' if Trick Room is on the field. */ + const speedReversed = new BooleanHolder(false); + globalScene.arena.applyTags(ArenaTagType.TRICK_ROOM, false, speedReversed); + + if (speedReversed.value) { + pokemonList.reverse(); + } +} diff --git a/test/abilities/dancer.test.ts b/test/abilities/dancer.test.ts index e640e326d58..e206152715e 100644 --- a/test/abilities/dancer.test.ts +++ b/test/abilities/dancer.test.ts @@ -34,7 +34,7 @@ describe("Abilities - Dancer", () => { game.override.enemyAbility(AbilityId.DANCER).enemySpecies(SpeciesId.MAGIKARP).enemyMoveset(MoveId.VICTORY_DANCE); await game.classicMode.startBattle([SpeciesId.ORICORIO, SpeciesId.FEEBAS]); - const [oricorio, feebas] = game.scene.getPlayerField(); + const [oricorio, feebas, magikarp1] = game.scene.getField(); game.move.changeMoveset(oricorio, [MoveId.SWORDS_DANCE, MoveId.VICTORY_DANCE, MoveId.SPLASH]); game.move.changeMoveset(feebas, [MoveId.SWORDS_DANCE, MoveId.SPLASH]); @@ -44,8 +44,9 @@ describe("Abilities - Dancer", () => { await game.phaseInterceptor.to("MovePhase"); // feebas uses swords dance await game.phaseInterceptor.to("MovePhase", false); // oricorio copies swords dance + // Dancer order will be Magikarp, Oricorio, Magikarp based on set turn order let currentPhase = game.scene.phaseManager.getCurrentPhase() as MovePhase; - expect(currentPhase.pokemon).toBe(oricorio); + expect(currentPhase.pokemon).toBe(magikarp1); expect(currentPhase.move.moveId).toBe(MoveId.SWORDS_DANCE); await game.phaseInterceptor.to("MoveEndPhase"); // end oricorio's move diff --git a/test/abilities/mycelium-might.test.ts b/test/abilities/mycelium-might.test.ts index c3b7b4753b6..21b856d341e 100644 --- a/test/abilities/mycelium-might.test.ts +++ b/test/abilities/mycelium-might.test.ts @@ -2,8 +2,6 @@ import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { Stat } from "#enums/stat"; -import { TurnEndPhase } from "#phases/turn-end-phase"; -import { TurnStartPhase } from "#phases/turn-start-phase"; import { GameManager } from "#test/test-utils/game-manager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; @@ -45,65 +43,50 @@ describe("Abilities - Mycelium Might", () => { it("should move last in its priority bracket and ignore protective abilities", async () => { await game.classicMode.startBattle([SpeciesId.REGIELEKI]); - const enemyPokemon = game.field.getEnemyPokemon(); - const playerIndex = game.field.getPlayerPokemon().getBattlerIndex(); - const enemyIndex = enemyPokemon.getBattlerIndex(); + const enemy = game.field.getEnemyPokemon(); + const player = game.field.getPlayerPokemon(); game.move.select(MoveId.BABY_DOLL_EYES); - await game.phaseInterceptor.to(TurnStartPhase, false); - const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase; - const speedOrder = phase.getSpeedOrder(); - const commandOrder = phase.getCommandOrder(); + await game.phaseInterceptor.to("MoveEndPhase", false); // The opponent Pokemon (without Mycelium Might) goes first despite having lower speed than the player Pokemon. // The player Pokemon (with Mycelium Might) goes last despite having higher speed than the opponent. - expect(speedOrder).toEqual([playerIndex, enemyIndex]); - expect(commandOrder).toEqual([enemyIndex, playerIndex]); - await game.phaseInterceptor.to(TurnEndPhase); + expect(player.hp).not.toEqual(player.getMaxHp()); + await game.phaseInterceptor.to("TurnEndPhase"); // Despite the opponent's ability (Clear Body), its ATK stat stage is still reduced. - expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-1); + expect(enemy.getStatStage(Stat.ATK)).toBe(-1); }); it("should still go first if a status move that is in a higher priority bracket than the opponent's move is used", async () => { game.override.enemyMoveset(MoveId.TACKLE); await game.classicMode.startBattle([SpeciesId.REGIELEKI]); - const enemyPokemon = game.field.getEnemyPokemon(); - const playerIndex = game.field.getPlayerPokemon().getBattlerIndex(); - const enemyIndex = enemyPokemon.getBattlerIndex(); + const enemy = game.field.getEnemyPokemon(); + const player = game.field.getPlayerPokemon(); game.move.select(MoveId.BABY_DOLL_EYES); - await game.phaseInterceptor.to(TurnStartPhase, false); - const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase; - const speedOrder = phase.getSpeedOrder(); - const commandOrder = phase.getCommandOrder(); + await game.phaseInterceptor.to("MoveEndPhase", false); // The player Pokemon (with M.M.) goes first because its move is still within a higher priority bracket than its opponent. // The enemy Pokemon goes second because its move is in a lower priority bracket. - expect(speedOrder).toEqual([playerIndex, enemyIndex]); - expect(commandOrder).toEqual([playerIndex, enemyIndex]); - await game.phaseInterceptor.to(TurnEndPhase); + expect(player.hp).toEqual(player.getMaxHp()); + await game.phaseInterceptor.to("TurnEndPhase"); // Despite the opponent's ability (Clear Body), its ATK stat stage is still reduced. - expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-1); + expect(enemy.getStatStage(Stat.ATK)).toBe(-1); }); it("should not affect non-status moves", async () => { await game.classicMode.startBattle([SpeciesId.REGIELEKI]); - const playerIndex = game.field.getPlayerPokemon().getBattlerIndex(); - const enemyIndex = game.field.getEnemyPokemon().getBattlerIndex(); + const player = game.field.getPlayerPokemon(); game.move.select(MoveId.QUICK_ATTACK); - await game.phaseInterceptor.to(TurnStartPhase, false); - const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase; - const speedOrder = phase.getSpeedOrder(); - const commandOrder = phase.getCommandOrder(); + await game.phaseInterceptor.to("MoveEndPhase", false); // The player Pokemon (with M.M.) goes first because it has a higher speed and did not use a status move. // The enemy Pokemon (without M.M.) goes second because its speed is lower. // This means that the commandOrder should be identical to the speedOrder - expect(speedOrder).toEqual([playerIndex, enemyIndex]); - expect(commandOrder).toEqual([playerIndex, enemyIndex]); + expect(player.hp).toEqual(player.getMaxHp()); }); }); diff --git a/test/abilities/neutralizing-gas.test.ts b/test/abilities/neutralizing-gas.test.ts index 555e5f8a19c..fd9138e4174 100644 --- a/test/abilities/neutralizing-gas.test.ts +++ b/test/abilities/neutralizing-gas.test.ts @@ -59,7 +59,7 @@ describe("Abilities - Neutralizing Gas", () => { expect(game.field.getPlayerPokemon().getStatStage(Stat.ATK)).toBe(1); }); - it.todo("should activate before other abilities", async () => { + it("should activate before other abilities", async () => { game.override.enemySpecies(SpeciesId.ACCELGOR).enemyLevel(100).enemyAbility(AbilityId.INTIMIDATE); await game.classicMode.startBattle([SpeciesId.FEEBAS]); diff --git a/test/abilities/quick-draw.test.ts b/test/abilities/quick-draw.test.ts index ce5873af3a8..257892145e5 100644 --- a/test/abilities/quick-draw.test.ts +++ b/test/abilities/quick-draw.test.ts @@ -5,7 +5,7 @@ import { SpeciesId } from "#enums/species-id"; import { FaintPhase } from "#phases/faint-phase"; import { GameManager } from "#test/test-utils/game-manager"; import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; describe("Abilities - Quick Draw", () => { let phaserGame: Phaser.Game; @@ -25,7 +25,6 @@ describe("Abilities - Quick Draw", () => { game = new GameManager(phaserGame); game.override .battleStyle("single") - .starterSpecies(SpeciesId.MAGIKARP) .ability(AbilityId.QUICK_DRAW) .moveset([MoveId.TACKLE, MoveId.TAIL_WHIP]) .enemyLevel(100) @@ -40,8 +39,8 @@ describe("Abilities - Quick Draw", () => { ).mockReturnValue(100); }); - test("makes pokemon going first in its priority bracket", async () => { - await game.classicMode.startBattle(); + it("makes pokemon go first in its priority bracket", async () => { + await game.classicMode.startBattle([SpeciesId.MAGIKARP]); const pokemon = game.field.getPlayerPokemon(); const enemy = game.field.getEnemyPokemon(); @@ -57,33 +56,27 @@ describe("Abilities - Quick Draw", () => { expect(pokemon.waveData.abilitiesApplied).toContain(AbilityId.QUICK_DRAW); }); - test( - "does not triggered by non damage moves", - { - retry: 5, - }, - async () => { - await game.classicMode.startBattle(); + it("is not triggered by non damaging moves", async () => { + await game.classicMode.startBattle([SpeciesId.MAGIKARP]); - const pokemon = game.field.getPlayerPokemon(); - const enemy = game.field.getEnemyPokemon(); + const pokemon = game.field.getPlayerPokemon(); + const enemy = game.field.getEnemyPokemon(); - pokemon.hp = 1; - enemy.hp = 1; + pokemon.hp = 1; + enemy.hp = 1; - game.move.select(MoveId.TAIL_WHIP); - await game.phaseInterceptor.to(FaintPhase, false); + game.move.select(MoveId.TAIL_WHIP); + await game.phaseInterceptor.to(FaintPhase, false); - expect(pokemon.isFainted()).toBe(true); - expect(enemy.isFainted()).toBe(false); - expect(pokemon.waveData.abilitiesApplied).not.contain(AbilityId.QUICK_DRAW); - }, - ); + expect(pokemon.isFainted()).toBe(true); + expect(enemy.isFainted()).toBe(false); + expect(pokemon.waveData.abilitiesApplied).not.contain(AbilityId.QUICK_DRAW); + }); - test("does not increase priority", async () => { + it("does not increase priority", async () => { game.override.enemyMoveset([MoveId.EXTREME_SPEED]); - await game.classicMode.startBattle(); + await game.classicMode.startBattle([SpeciesId.MAGIKARP]); const pokemon = game.field.getPlayerPokemon(); const enemy = game.field.getEnemyPokemon(); diff --git a/test/abilities/stall.test.ts b/test/abilities/stall.test.ts index 5b4e38f7099..b6a88964e09 100644 --- a/test/abilities/stall.test.ts +++ b/test/abilities/stall.test.ts @@ -1,7 +1,6 @@ import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; -import { TurnStartPhase } from "#phases/turn-start-phase"; import { GameManager } from "#test/test-utils/game-manager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; @@ -40,56 +39,41 @@ describe("Abilities - Stall", () => { it("Pokemon with Stall should move last in its priority bracket regardless of speed", async () => { await game.classicMode.startBattle([SpeciesId.SHUCKLE]); - const playerIndex = game.field.getPlayerPokemon().getBattlerIndex(); - const enemyIndex = game.field.getEnemyPokemon().getBattlerIndex(); + const player = game.field.getPlayerPokemon(); game.move.select(MoveId.QUICK_ATTACK); - await game.phaseInterceptor.to(TurnStartPhase, false); - const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase; - const speedOrder = phase.getSpeedOrder(); - const commandOrder = phase.getCommandOrder(); + await game.phaseInterceptor.to("MoveEndPhase", false); // The player Pokemon (without Stall) goes first despite having lower speed than the opponent. // The opponent Pokemon (with Stall) goes last despite having higher speed than the player Pokemon. - expect(speedOrder).toEqual([enemyIndex, playerIndex]); - expect(commandOrder).toEqual([playerIndex, enemyIndex]); + expect(player).toHaveFullHp(); }); it("Pokemon with Stall will go first if a move that is in a higher priority bracket than the opponent's move is used", async () => { await game.classicMode.startBattle([SpeciesId.SHUCKLE]); - const playerIndex = game.field.getPlayerPokemon().getBattlerIndex(); - const enemyIndex = game.field.getEnemyPokemon().getBattlerIndex(); + const player = game.field.getPlayerPokemon(); game.move.select(MoveId.TACKLE); - await game.phaseInterceptor.to(TurnStartPhase, false); - const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase; - const speedOrder = phase.getSpeedOrder(); - const commandOrder = phase.getCommandOrder(); + await game.phaseInterceptor.to("MoveEndPhase", false); // The opponent Pokemon (with Stall) goes first because its move is still within a higher priority bracket than its opponent. // The player Pokemon goes second because its move is in a lower priority bracket. - expect(speedOrder).toEqual([enemyIndex, playerIndex]); - expect(commandOrder).toEqual([enemyIndex, playerIndex]); + expect(player).not.toHaveFullHp(); }); it("If both Pokemon have stall and use the same move, speed is used to determine who goes first.", async () => { game.override.ability(AbilityId.STALL); await game.classicMode.startBattle([SpeciesId.SHUCKLE]); - const playerIndex = game.field.getPlayerPokemon().getBattlerIndex(); - const enemyIndex = game.field.getEnemyPokemon().getBattlerIndex(); + const player = game.field.getPlayerPokemon(); game.move.select(MoveId.TACKLE); - await game.phaseInterceptor.to(TurnStartPhase, false); - const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase; - const speedOrder = phase.getSpeedOrder(); - const commandOrder = phase.getCommandOrder(); + await game.phaseInterceptor.to("MoveEndPhase", false); // The opponent Pokemon (with Stall) goes first because it has a higher speed. // The player Pokemon (with Stall) goes second because its speed is lower. - expect(speedOrder).toEqual([enemyIndex, playerIndex]); - expect(commandOrder).toEqual([enemyIndex, playerIndex]); + expect(player).not.toHaveFullHp(); }); }); diff --git a/test/battle/battle-order.test.ts b/test/battle/battle-order.test.ts index 0b24fcbfa7d..de13b22df79 100644 --- a/test/battle/battle-order.test.ts +++ b/test/battle/battle-order.test.ts @@ -1,7 +1,8 @@ import { AbilityId } from "#enums/ability-id"; +import { BattlerIndex } from "#enums/battler-index"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; -import type { TurnStartPhase } from "#phases/turn-start-phase"; +import type { MovePhase } from "#phases/move-phase"; import { GameManager } from "#test/test-utils/game-manager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; @@ -34,38 +35,34 @@ describe("Battle order", () => { await game.classicMode.startBattle([SpeciesId.BULBASAUR]); const playerPokemon = game.field.getPlayerPokemon(); + const playerStartHp = playerPokemon.hp; const enemyPokemon = game.field.getEnemyPokemon(); + const enemyStartHp = enemyPokemon.hp; + vi.spyOn(playerPokemon, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 50]); // set playerPokemon's speed to 50 vi.spyOn(enemyPokemon, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150]); // set enemyPokemon's speed to 150 - game.move.select(MoveId.TACKLE); - await game.phaseInterceptor.to("TurnStartPhase", false); - const playerPokemonIndex = playerPokemon.getBattlerIndex(); - const enemyPokemonIndex = enemyPokemon.getBattlerIndex(); - const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase; - const order = phase.getCommandOrder(); - expect(order[0]).toBe(enemyPokemonIndex); - expect(order[1]).toBe(playerPokemonIndex); + await game.phaseInterceptor.to("MoveEndPhase", false); + expect(playerPokemon.hp).not.toEqual(playerStartHp); + expect(enemyPokemon.hp).toEqual(enemyStartHp); }); it("Player faster than opponent 150 vs 50", async () => { await game.classicMode.startBattle([SpeciesId.BULBASAUR]); const playerPokemon = game.field.getPlayerPokemon(); + const playerStartHp = playerPokemon.hp; const enemyPokemon = game.field.getEnemyPokemon(); + const enemyStartHp = enemyPokemon.hp; vi.spyOn(playerPokemon, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150]); // set playerPokemon's speed to 150 vi.spyOn(enemyPokemon, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 50]); // set enemyPokemon's speed to 50 game.move.select(MoveId.TACKLE); - await game.phaseInterceptor.to("TurnStartPhase", false); - const playerPokemonIndex = playerPokemon.getBattlerIndex(); - const enemyPokemonIndex = enemyPokemon.getBattlerIndex(); - const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase; - const order = phase.getCommandOrder(); - expect(order[0]).toBe(playerPokemonIndex); - expect(order[1]).toBe(enemyPokemonIndex); + await game.phaseInterceptor.to("MoveEndPhase", false); + expect(playerPokemon.hp).toEqual(playerStartHp); + expect(enemyPokemon.hp).not.toEqual(enemyStartHp); }); it("double - both opponents faster than player 50/50 vs 150/150", async () => { @@ -73,23 +70,24 @@ describe("Battle order", () => { await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.BLASTOISE]); const playerPokemon = game.scene.getPlayerField(); + const playerHps = playerPokemon.map(p => p.hp); const enemyPokemon = game.scene.getEnemyField(); + const enemyHps = enemyPokemon.map(p => p.hp); playerPokemon.forEach(p => vi.spyOn(p, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 50])); // set both playerPokemons' speed to 50 enemyPokemon.forEach(p => vi.spyOn(p, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150])); // set both enemyPokemons' speed to 150 - const playerIndices = playerPokemon.map(p => p?.getBattlerIndex()); - const enemyIndices = enemyPokemon.map(p => p?.getBattlerIndex()); game.move.select(MoveId.TACKLE); game.move.select(MoveId.TACKLE, 1); - await game.phaseInterceptor.to("TurnStartPhase", false); + await game.move.selectEnemyMove(MoveId.TACKLE, BattlerIndex.PLAYER); + await game.move.selectEnemyMove(MoveId.TACKLE, BattlerIndex.PLAYER_2); - const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase; - const order = phase.getCommandOrder(); - expect(order.slice(0, 2).includes(enemyIndices[0])).toBe(true); - expect(order.slice(0, 2).includes(enemyIndices[1])).toBe(true); - expect(order.slice(2, 4).includes(playerIndices[0])).toBe(true); - expect(order.slice(2, 4).includes(playerIndices[1])).toBe(true); + await game.phaseInterceptor.to("MoveEndPhase", true); + await game.phaseInterceptor.to("MoveEndPhase", false); + for (let i = 0; i < 2; i++) { + expect(playerPokemon[i].hp).not.toEqual(playerHps[i]); + expect(enemyPokemon[i].hp).toEqual(enemyHps[i]); + } }); it("double - speed tie except 1 - 100/100 vs 100/150", async () => { @@ -101,18 +99,13 @@ describe("Battle order", () => { playerPokemon.forEach(p => vi.spyOn(p, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 100])); //set both playerPokemons' speed to 100 vi.spyOn(enemyPokemon[0], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 100]); // set enemyPokemon's speed to 100 vi.spyOn(enemyPokemon[1], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150]); // set enemyPokemon's speed to 150 - const playerIndices = playerPokemon.map(p => p?.getBattlerIndex()); - const enemyIndices = enemyPokemon.map(p => p?.getBattlerIndex()); game.move.select(MoveId.TACKLE); game.move.select(MoveId.TACKLE, 1); - await game.phaseInterceptor.to("TurnStartPhase", false); + await game.phaseInterceptor.to("MovePhase", false); - const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase; - const order = phase.getCommandOrder(); - // enemy 2 should be first, followed by some other assortment of the other 3 pokemon - expect(order[0]).toBe(enemyIndices[1]); - expect(order.slice(1, 4)).toEqual(expect.arrayContaining([enemyIndices[0], ...playerIndices])); + const phase = game.scene.phaseManager.getCurrentPhase() as MovePhase; + expect(phase.pokemon).toEqual(enemyPokemon[1]); }); it("double - speed tie 100/150 vs 100/150", async () => { @@ -125,17 +118,13 @@ describe("Battle order", () => { vi.spyOn(playerPokemon[1], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150]); // set other playerPokemon's speed to 150 vi.spyOn(enemyPokemon[0], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 100]); // set one enemyPokemon's speed to 100 vi.spyOn(enemyPokemon[1], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150]); // set other enemyPokemon's speed to 150 - const playerIndices = playerPokemon.map(p => p?.getBattlerIndex()); - const enemyIndices = enemyPokemon.map(p => p?.getBattlerIndex()); game.move.select(MoveId.TACKLE); game.move.select(MoveId.TACKLE, 1); - await game.phaseInterceptor.to("TurnStartPhase", false); - const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase; - const order = phase.getCommandOrder(); - // P2/E2 should be randomly first/second, then P1/E1 randomly 3rd/4th - expect(order.slice(0, 2)).toStrictEqual(expect.arrayContaining([playerIndices[1], enemyIndices[1]])); - expect(order.slice(2, 4)).toStrictEqual(expect.arrayContaining([playerIndices[0], enemyIndices[0]])); + await game.phaseInterceptor.to("MovePhase", false); + + const phase = game.scene.phaseManager.getCurrentPhase() as MovePhase; + expect(enemyPokemon[1] === phase.pokemon || playerPokemon[1] === phase.pokemon); }); }); diff --git a/test/moves/baton-pass.test.ts b/test/moves/baton-pass.test.ts index f9bd92a63cd..caabcfa7158 100644 --- a/test/moves/baton-pass.test.ts +++ b/test/moves/baton-pass.test.ts @@ -76,12 +76,7 @@ describe("Moves - Baton Pass", () => { expect(game.field.getEnemyPokemon().getStatStage(Stat.SPATK)).toEqual(2); // confirm that a switch actually happened. can't use species because I // can't find a way to override trainer parties with more than 1 pokemon species - expect(game.phaseInterceptor.log.slice(-4)).toEqual([ - "MoveEffectPhase", - "SwitchSummonPhase", - "SummonPhase", - "PostSummonPhase", - ]); + expect(game.field.getEnemyPokemon().summonData.moveHistory).toHaveLength(0); }); it("doesn't transfer effects that aren't transferrable", async () => { diff --git a/test/moves/delayed-attack.test.ts b/test/moves/delayed-attack.test.ts index 6817c7fd17a..e31c7f28e48 100644 --- a/test/moves/delayed-attack.test.ts +++ b/test/moves/delayed-attack.test.ts @@ -193,7 +193,7 @@ describe("Moves - Delayed Attacks", () => { // All attacks have concluded at this point, unshifting new `MoveEffectPhase`s to the queue. expectFutureSightActive(0); - const MEPs = game.scene.phaseManager.phaseQueue.filter(p => p.is("MoveEffectPhase")); + const MEPs = game.scene.phaseManager["phaseQueue"].findAll("MoveEffectPhase"); expect(MEPs).toHaveLength(4); expect(MEPs.map(mep => mep.getPokemon())).toEqual(oldOrder); }); diff --git a/test/moves/focus-punch.test.ts b/test/moves/focus-punch.test.ts index d7b40569aaa..06594e85e27 100644 --- a/test/moves/focus-punch.test.ts +++ b/test/moves/focus-punch.test.ts @@ -3,7 +3,6 @@ import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { BerryPhase } from "#phases/berry-phase"; import { MessagePhase } from "#phases/message-phase"; -import { MoveHeaderPhase } from "#phases/move-header-phase"; import { SwitchSummonPhase } from "#phases/switch-summon-phase"; import { TurnStartPhase } from "#phases/turn-start-phase"; import { GameManager } from "#test/test-utils/game-manager"; @@ -116,7 +115,7 @@ describe("Moves - Focus Punch", () => { await game.phaseInterceptor.to(TurnStartPhase); expect(game.scene.phaseManager.getCurrentPhase() instanceof SwitchSummonPhase).toBeTruthy(); - expect(game.scene.phaseManager.phaseQueue.find(phase => phase instanceof MoveHeaderPhase)).toBeDefined(); + expect(game.scene.phaseManager.hasPhaseOfType("MoveHeaderPhase")).toBe(true); }); it("should replace the 'but it failed' text when the user gets hit", async () => { game.override.enemyMoveset([MoveId.TACKLE]); diff --git a/test/moves/rage-fist.test.ts b/test/moves/rage-fist.test.ts index 61164b5710c..c58d1296ac5 100644 --- a/test/moves/rage-fist.test.ts +++ b/test/moves/rage-fist.test.ts @@ -166,7 +166,6 @@ describe("Moves - Rage Fist", () => { // Charizard hit game.move.select(MoveId.SPLASH); - await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.toNextTurn(); expect(getPartyHitCount()).toEqual([1, 0]); diff --git a/test/moves/revival-blessing.test.ts b/test/moves/revival-blessing.test.ts index 4dc7cb97f2d..8c751458ff7 100644 --- a/test/moves/revival-blessing.test.ts +++ b/test/moves/revival-blessing.test.ts @@ -119,17 +119,16 @@ describe("Moves - Revival Blessing", () => { game.override .battleStyle("double") .enemyMoveset([MoveId.REVIVAL_BLESSING]) - .moveset([MoveId.SPLASH]) + .moveset([MoveId.SPLASH, MoveId.JUDGMENT]) + .startingLevel(100) .startingWave(25); // 2nd rival battle - must have 3+ pokemon await game.classicMode.startBattle([SpeciesId.ARCEUS, SpeciesId.GIRATINA]); const enemyFainting = game.scene.getEnemyField()[0]; - game.move.select(MoveId.SPLASH, 0); + game.move.use(MoveId.JUDGMENT, 0, BattlerIndex.ENEMY); game.move.select(MoveId.SPLASH, 1); - await game.killPokemon(enemyFainting); - await game.phaseInterceptor.to("BerryPhase"); await game.toNextTurn(); // If there are incorrectly two switch phases into this slot, the fainted pokemon will end up in slot 3 // Make sure it's still in slot 1 diff --git a/test/moves/shell-trap.test.ts b/test/moves/shell-trap.test.ts index 5ecad3116af..2a83f2c3266 100644 --- a/test/moves/shell-trap.test.ts +++ b/test/moves/shell-trap.test.ts @@ -48,7 +48,7 @@ describe("Moves - Shell Trap", () => { await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER, BattlerIndex.PLAYER_2]); - await game.phaseInterceptor.to(MoveEndPhase); + await game.phaseInterceptor.to("MoveEndPhase"); const movePhase = game.scene.phaseManager.getCurrentPhase(); expect(movePhase instanceof MovePhase).toBeTruthy(); diff --git a/test/moves/trick-room.test.ts b/test/moves/trick-room.test.ts index a1d81efb17e..d970dc9762d 100644 --- a/test/moves/trick-room.test.ts +++ b/test/moves/trick-room.test.ts @@ -5,10 +5,10 @@ import { BattlerIndex } from "#enums/battler-index"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { Stat } from "#enums/stat"; -import { TurnStartPhase } from "#phases/turn-start-phase"; +import { WeatherType } from "#enums/weather-type"; import { GameManager } from "#test/test-utils/game-manager"; import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; describe("Move - Trick Room", () => { let phaserGame: Phaser.Game; @@ -56,13 +56,11 @@ describe("Move - Trick Room", () => { turnCount: 4, // The 5 turn limit _includes_ the current turn! }); - // Now, check that speed was indeed reduced - const turnOrderSpy = vi.spyOn(TurnStartPhase.prototype, "getSpeedOrder"); - - game.move.use(MoveId.SPLASH); + game.move.use(MoveId.SUNNY_DAY); + await game.move.forceEnemyMove(MoveId.RAIN_DANCE); await game.toEndOfTurn(); - expect(turnOrderSpy).toHaveLastReturnedWith([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + expect(game.scene.arena.getWeatherType()).toBe(WeatherType.SUNNY); }); it("should be removed when overlapped", async () => { diff --git a/test/moves/wish.test.ts b/test/moves/wish.test.ts index 1c1f3f3b8ba..b64a15ac654 100644 --- a/test/moves/wish.test.ts +++ b/test/moves/wish.test.ts @@ -135,7 +135,7 @@ describe("Move - Wish", () => { // all wishes have activated and added healing phases expect(game).toHavePositionalTag(PositionalTagType.WISH, 0); - const healPhases = game.scene.phaseManager.phaseQueue.filter(p => p.is("PokemonHealPhase")); + const healPhases = game.scene.phaseManager["phaseQueue"].findAll("PokemonHealPhase"); expect(healPhases).toHaveLength(4); expect.soft(healPhases.map(php => php.getPokemon())).toEqual(oldOrder); diff --git a/test/mystery-encounter/encounter-test-utils.ts b/test/mystery-encounter/encounter-test-utils.ts index 4aad0e000d9..165678a88da 100644 --- a/test/mystery-encounter/encounter-test-utils.ts +++ b/test/mystery-encounter/encounter-test-utils.ts @@ -70,7 +70,6 @@ export async function runMysteryEncounterToEnd( // If a battle is started, fast forward to end of the battle game.onNextPrompt("CommandPhase", UiMode.COMMAND, () => { game.scene.phaseManager.clearPhaseQueue(); - game.scene.phaseManager.clearPhaseQueueSplice(); game.scene.phaseManager.unshiftPhase(new VictoryPhase(0)); game.endPhase(); }); @@ -196,7 +195,6 @@ async function handleSecondaryOptionSelect(game: GameManager, pokemonNo: number, */ export async function skipBattleRunMysteryEncounterRewardsPhase(game: GameManager, runRewardsPhase = true) { game.scene.phaseManager.clearPhaseQueue(); - game.scene.phaseManager.clearPhaseQueueSplice(); game.scene.getEnemyParty().forEach(p => { p.hp = 0; p.status = new Status(StatusEffect.FAINT); diff --git a/test/mystery-encounter/encounters/the-winstrate-challenge-encounter.test.ts b/test/mystery-encounter/encounters/the-winstrate-challenge-encounter.test.ts index 814e2ee07fb..3bbb858a15d 100644 --- a/test/mystery-encounter/encounters/the-winstrate-challenge-encounter.test.ts +++ b/test/mystery-encounter/encounters/the-winstrate-challenge-encounter.test.ts @@ -355,7 +355,6 @@ describe("The Winstrate Challenge - Mystery Encounter", () => { */ async function skipBattleToNextBattle(game: GameManager, isFinalBattle = false) { game.scene.phaseManager.clearPhaseQueue(); - game.scene.phaseManager.clearPhaseQueueSplice(); const commandUiHandler = game.scene.ui.handlers[UiMode.COMMAND]; commandUiHandler.clear(); game.scene.getEnemyParty().forEach(p => { diff --git a/test/test-utils/game-manager.ts b/test/test-utils/game-manager.ts index f9db964ad26..f681846d935 100644 --- a/test/test-utils/game-manager.ts +++ b/test/test-utils/game-manager.ts @@ -464,6 +464,9 @@ export class GameManager { * Faint a player or enemy pokemon instantly by setting their HP to 0. * @param pokemon - The player/enemy pokemon being fainted * @returns A Promise that resolves once the fainted pokemon's FaintPhase finishes running. + * @remarks + * This method *pushes* a FaintPhase and runs until it's finished. This may cause a turn to play out unexpectedly + * @todo Consider whether running the faint phase immediately can be done */ async killPokemon(pokemon: PlayerPokemon | EnemyPokemon) { pokemon.hp = 0; @@ -533,7 +536,7 @@ export class GameManager { } /** - * Intercepts `TurnStartPhase` and mocks {@linkcode TurnStartPhase.getSpeedOrder}'s return value. + * Modifies the queue manager to return move phases in a particular order * Used to manually modify Pokemon turn order. * Note: This *DOES NOT* account for priority. * @param order - The turn order to set as an array of {@linkcode BattlerIndex}es. @@ -545,7 +548,7 @@ export class GameManager { async setTurnOrder(order: BattlerIndex[]): Promise { await this.phaseInterceptor.to("TurnStartPhase", false); - vi.spyOn(this.scene.phaseManager.getCurrentPhase() as TurnStartPhase, "getSpeedOrder").mockReturnValue(order); + this.scene.phaseManager.dynamicQueueManager.setMoveOrder(order); } /** From 25416ebf47bd1e55e5fbdfa35c2f8ee0093e0624 Mon Sep 17 00:00:00 2001 From: Dean <69436131+emdeann@users.noreply.github.com> Date: Sat, 20 Sep 2025 20:24:50 -0700 Subject: [PATCH 34/40] [UI] Avoid prematurely updating HP bar when applying damage (#6582) Avoid prematurely updating HP bar when applying damage --- src/field/pokemon.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index ec813e52e56..ea7c74904d8 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -3942,11 +3942,6 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { damage = 0; } damage = this.damage(damage, ignoreSegments, isIndirectDamage, ignoreFaintPhase); - // Ensure the battle-info bar's HP is updated, though only if the battle info is visible - // TODO: When battle-info UI is refactored, make this only update the HP bar - if (this.battleInfo.visible) { - this.updateInfo(); - } // Damage amount may have changed, but needed to be queued before calling damage function damagePhase.updateAmount(damage); /** From 9f851591cb64fe499aa63502936d0e5cb9f59754 Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Sun, 21 Sep 2025 00:21:27 -0700 Subject: [PATCH 35/40] [Dev] Update `pnpm` --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1fb25c5d4ba..ac8bca50f76 100644 --- a/package.json +++ b/package.json @@ -74,5 +74,5 @@ "engines": { "node": ">=22.0.0" }, - "packageManager": "pnpm@10.16.1" + "packageManager": "pnpm@10.17.0" } From be61996044f748294e00f9186008c46c9c71443c Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Sun, 21 Sep 2025 00:22:53 -0700 Subject: [PATCH 36/40] [Docs] Fix/update some comments and spacing --- src/@types/phase-types.ts | 22 +++++----------------- src/phase-tree.ts | 1 - 2 files changed, 5 insertions(+), 18 deletions(-) diff --git a/src/@types/phase-types.ts b/src/@types/phase-types.ts index d396375c5fa..2324c927e3a 100644 --- a/src/@types/phase-types.ts +++ b/src/@types/phase-types.ts @@ -3,37 +3,25 @@ import type { Phase } from "#app/phase"; import type { PhaseConstructorMap } from "#app/phase-manager"; import type { ObjectValues } from "#types/type-helpers"; -// Intentionally export the types of everything in phase-manager, as this file is meant to be +// Intentionally [re-]export the types of everything in phase-manager, as this file is meant to be // the centralized place for type definitions for the phase system. export type * from "#app/phase-manager"; -// This file includes helpful types for the phase system. -// It intentionally imports the phase constructor map from the phase manager (and re-exports it) - -/** - * Map of phase names to constructors for said phase - */ +/** Map of phase names to constructors for said phase */ export type PhaseMap = { [K in keyof PhaseConstructorMap]: InstanceType; }; -/** - * Union type of all phase constructors. - */ +/** Union type of all phase constructors. */ export type PhaseClass = ObjectValues; -/** - * Union type of all phase names as strings. - */ +/** Union type of all phase names as strings. */ export type PhaseString = keyof PhaseMap; /** Type for predicate functions operating on a specific type of {@linkcode Phase}. */ - export type PhaseConditionFunc = (phase: PhaseMap[T]) => boolean; -/** - * Interface type representing the assumption that all phases with pokemon associated are dynamic - */ +/** Interface type representing the assumption that all phases with pokemon associated are dynamic */ export interface DynamicPhase extends Phase { getPokemon(): Pokemon; } diff --git a/src/phase-tree.ts b/src/phase-tree.ts index 55476f38d65..69bb72ca4f0 100644 --- a/src/phase-tree.ts +++ b/src/phase-tree.ts @@ -1,7 +1,6 @@ // biome-ignore-start lint/correctness/noUnusedImports: TSDoc imports import type { PhaseManager } from "#app/@types/phase-types"; import type { DynamicPhaseMarker } from "#phases/dynamic-phase-marker"; - // biome-ignore-end lint/correctness/noUnusedImports: TSDoc imports import type { PhaseMap, PhaseString } from "#app/@types/phase-types"; From cf5e7fd981504bcb41ff7b9ba1148b8e2b7c02f6 Mon Sep 17 00:00:00 2001 From: Lugiad Date: Sun, 21 Sep 2025 15:55:54 +0200 Subject: [PATCH 37/40] [UI/UX] Optimize text PNGs (#6584) --- .../ca/summary/summary_dexnb_label_ca.png | Bin 1618 -> 131 bytes .../summary_dexnb_label_overlay_shiny_ca.png | Bin 1753 -> 202 bytes .../summary/summary_stats_expbar_title_ca.png | Bin 1600 -> 119 bytes .../en/battle_ui/overlay_exp_label.png | Bin 1401 -> 116 bytes .../en/summary/summary_dexnb_label.png | Bin 1612 -> 124 bytes .../summary_moves_descriptions_title.png | Bin 2173 -> 235 bytes .../en/summary/summary_moves_effect_title.png | Bin 1870 -> 186 bytes .../en/summary/summary_moves_moves_title.png | Bin 2035 -> 180 bytes .../en/summary/summary_profile_ability.png | Bin 1675 -> 181 bytes .../en/summary/summary_profile_memo_title.png | Bin 1965 -> 242 bytes .../en/summary/summary_profile_passive.png | Bin 1503 -> 198 bytes .../summary/summary_profile_profile_title.png | Bin 2050 -> 196 bytes .../en/summary/summary_stats_expbar_title.png | Bin 1600 -> 119 bytes .../en/summary/summary_stats_item_title.png | Bin 1892 -> 183 bytes .../en/summary/summary_stats_stats_title.png | Bin 1869 -> 169 bytes .../summary/summary_dexnb_label_es-ES.png | Bin 1618 -> 131 bytes ...summary_dexnb_label_overlay_shiny_es-ES.png | Bin 1753 -> 202 bytes .../summary_moves_descriptions_title_es-ES.png | Bin 2193 -> 249 bytes .../summary_moves_effect_title_es-ES.png | Bin 2033 -> 190 bytes .../summary_moves_moves_title_es-ES.png | Bin 2231 -> 244 bytes .../summary/summary_profile_ability_es-ES.png | Bin 2059 -> 195 bytes .../summary_profile_memo_title_es-ES.png | Bin 1976 -> 235 bytes .../summary/summary_profile_passive_es-ES.png | Bin 1834 -> 193 bytes .../summary_profile_profile_title_es-ES.png | Bin 2030 -> 180 bytes .../summary_stats_expbar_title_es-ES.png | Bin 1600 -> 119 bytes .../summary/summary_stats_item_title_es-ES.png | Bin 2172 -> 197 bytes .../summary_stats_stats_title_es-ES.png | Bin 2532 -> 307 bytes .../summary/summary_dexnb_label_es-MX.png | Bin 1618 -> 131 bytes ...summary_dexnb_label_overlay_shiny_es-MX.png | Bin 1753 -> 202 bytes .../summary_moves_descriptions_title_es-MX.png | Bin 2193 -> 249 bytes .../summary_moves_effect_title_es-MX.png | Bin 2033 -> 190 bytes .../summary_moves_moves_title_es-MX.png | Bin 2231 -> 244 bytes .../summary/summary_profile_ability_es-MX.png | Bin 2059 -> 195 bytes .../summary_profile_memo_title_es-MX.png | Bin 1976 -> 235 bytes .../summary/summary_profile_passive_es-MX.png | Bin 1834 -> 193 bytes .../summary_profile_profile_title_es-MX.png | Bin 2030 -> 180 bytes .../summary_stats_expbar_title_es-MX.png | Bin 1600 -> 119 bytes .../summary/summary_stats_item_title_es-MX.png | Bin 2172 -> 197 bytes .../summary_stats_stats_title_es-MX.png | Bin 2532 -> 307 bytes .../fr/battle_ui/overlay_exp_label_fr.png | Bin 1401 -> 116 bytes .../ja/battle_ui/overlay_exp_label_ja.png | Bin 1401 -> 116 bytes .../summary/summary_stats_expbar_title_ja.png | Bin 1600 -> 119 bytes .../ko/battle_ui/overlay_exp_label_ko.png | Bin 1401 -> 116 bytes .../summary/summary_stats_expbar_title_ko.png | Bin 1600 -> 119 bytes ...summary_dexnb_label_overlay_shiny_pt-BR.png | Bin 1753 -> 202 bytes .../summary_stats_expbar_title_pt-BR.png | Bin 1600 -> 119 bytes .../ro/battle_ui/overlay_exp_label_ro.png | Bin 1401 -> 116 bytes .../summary/summary_stats_expbar_title_ro.png | Bin 1600 -> 119 bytes .../tl/battle_ui/overlay_exp_label_tl.png | Bin 1401 -> 116 bytes .../tl/summary/summary_stats_expbar_title.png | Bin 1600 -> 119 bytes .../summary/summary_stats_expbar_title_tr.png | Bin 1600 -> 119 bytes .../battle_ui/overlay_exp_label_zh-CN.png | Bin 1401 -> 116 bytes .../summary/summary_dexnb_label_zh-CN.png | Bin 1612 -> 124 bytes .../summary_moves_descriptions_title_zh-CN.png | Bin 2173 -> 235 bytes .../summary_moves_effect_title_zh-CN.png | Bin 1870 -> 187 bytes .../summary_moves_moves_title_zh-CN.png | Bin 2035 -> 180 bytes .../summary/summary_profile_ability_zh-CN.png | Bin 1675 -> 181 bytes .../summary_profile_memo_title_zh-CN.png | Bin 1965 -> 242 bytes .../summary/summary_profile_passive_zh-CN.png | Bin 1503 -> 198 bytes .../summary_profile_profile_title_zh-CN.png | Bin 2050 -> 196 bytes .../summary_stats_expbar_title_zh-CN.png | Bin 1600 -> 119 bytes .../summary/summary_stats_item_title_zh-CN.png | Bin 1892 -> 183 bytes .../summary_stats_stats_title_zh-CN.png | Bin 1869 -> 169 bytes .../battle_ui/overlay_exp_label_zh-TW.png | Bin 1401 -> 116 bytes .../summary/summary_dexnb_label_zh-TW.png | Bin 1612 -> 124 bytes .../summary_moves_descriptions_title_zh-TW.png | Bin 2173 -> 235 bytes .../summary_moves_effect_title_zh-TW.png | Bin 1870 -> 187 bytes .../summary_moves_moves_title_zh-TW.png | Bin 2035 -> 180 bytes .../summary/summary_profile_ability_zh-TW.png | Bin 1675 -> 181 bytes .../summary_profile_memo_title_zh-TW.png | Bin 1965 -> 242 bytes .../summary/summary_profile_passive_zh-TW.png | Bin 1503 -> 198 bytes .../summary_profile_profile_title_zh-TW.png | Bin 2050 -> 196 bytes .../summary_stats_expbar_title_zh-TW.png | Bin 1600 -> 119 bytes .../summary/summary_stats_item_title_zh-TW.png | Bin 1892 -> 183 bytes .../summary_stats_stats_title_zh-TW.png | Bin 1869 -> 169 bytes 75 files changed, 0 insertions(+), 0 deletions(-) diff --git a/public/images/ui/legacy/text_images/ca/summary/summary_dexnb_label_ca.png b/public/images/ui/legacy/text_images/ca/summary/summary_dexnb_label_ca.png index a457468d8d0630acdec6da6a17d0174454a9f198..5477e3385a8c5fe552c6e601a21330829e7e4f13 100644 GIT binary patch delta 114 zcmcb_)66(QBAbbsfq|jyL}&w$;tcQ!asB`Qe?mgSj~_o6Qbl(G1sO|%{DK)Ap4~_T zax^?$978JNL=SJ|WKa+|df|%SclYk5Ly`9vBz;WiQ2iUbQnqhHi>8XreFp9_!`mm1 Q0QE35hjuH9vadPoEx>Q3p{k&hWt1hH)|2`D>)9Lx@~*wR)aVUG(SGhDfj@0a@J zl1MwqbQ}j67P2fI5PG&6OES>o?C2zd$1|FSi3~-o<`GFCmZhr!(@_agQiP4OahsrF zXh0HVmW;flA&}7mmn^5fVRA+h@9~JoG6SqxV3V$pBI}ARLKmMmB!JfXevxBSE>TpZ z$mGlllre6|rs%A-nsBthGqIdfKC=QekLJ}RxRG)qMcNXgHyPCYgtFRbsK+jZF%$jl zlb7J!Ba*wCWmpy6sOgs7GEHoo*s6VISlBQ|P+zdHy}B8+0u#13Kw)7Rk>rhFMsZuD z5rJ~Uh;%qg;*O>kO~eh+6DcGHcI6=p-^a~3lO(3R>A4zITna;mExT4_HY`f5Vz>2x z*fl)}NLjZ7+qT;dEe8Re_2y20$KP6)SjR%6wm;sjY<@LnhZgg7{->`h|PJG-_+9h#^gGeGR zF9qdtG8zbabymY~FG0E23GxeO zU?`h>)&j`O@N{tusfgQh?jR?Ff&lZu1)@usEv;|7-fymZkU{T>rNGU+Kl21!7KHO1 z@(vJf?45kX=OBmUN~41B;&Z+|o598)(7qXt17rY|XWt~;~4bI3L(yRlj5#wFR3RK@1(nX|iNcIHfH zW^X|uu0AMu0R>Y;O8a1?FX~HMq1a-fQXeD_)+YF3r4RK*qAiHNh-daPwh~(`4$RCs zv)}o?|NGB>{(U1qvwtZ1Sd?Lyq3m>KmcAG1{o;dz^qJmXc!9olIMatbhS~X0a7UOA zJ{)0~ftFcV@E39i03wzE4N}BH({d=9VaCRr4n#}XXN$OO+9_`H+y#y`jTASp<)oaG z#uamV*~N3qGX=D~gmi-&-^Y$M0VS}o580+wwLQ>GaUEVj$H8ZjV>=LkDaB0$g6u*r z&!&lsSxt!ZNLCd#p$l?c(iL?tt4MM}loFyG<0TnLaS+ql-o;Th*C>J6%+ozBI!kdC z-*mGRKBV1Zh$wE)}NH#X3>sav*DaKC}_eW>Oq=ESRPN za8XW{Ak=uQV~vlgD&~twQg~e}Ce^sEp;#RE*JlW-1^e1xZ~Vvg&$uQ%SFn2DJhcA* zcGzc`ppHDcabE9qOu}++R5kYnHiIw?T~#Cvm3U2;3?9m=#wQacorgsQ%7%gCs8k#i zgQGwfvE5OZNW8mZr$E898AQ

CWLJVFYZaCILjjSxa#!C^Bqtz0c--??{)nQNfh- zF6GySd8Fhwpo=HUbocI2++yE;*M;~`gPkPZ)<61i+LVG}*bV6CK)6}pIdov$Nt3P@cgx}2DP_NeVMUudd*`GaKHZc z-Pub=XQLMme0%-YrR3WutWEis#_HHl@biyG(B=7-@M`(`z~!?i&B-4cUnh4xyP~z= z#?{kRZ8Lgt{ruX@&d;8I?c#}Zw;s7t`{I=+ULM@2@7id;pI&=-?^@){qw9aZex$vg zua6#o>FAA*hwFudr&o{74PRS*XWN^Rft77fMUFFhW|SFbqRjs={rzy{kCSawIQaPC Y#qBq4*3RttH2Cw$PR(RKnOtoD1sggu#{d8T diff --git a/public/images/ui/legacy/text_images/ca/summary/summary_stats_expbar_title_ca.png b/public/images/ui/legacy/text_images/ca/summary/summary_stats_expbar_title_ca.png index e9dfb10e5d62cbd9e2c24bef5d40012719e0a292..da999975932c33cd88790ae41711c3d9f5d39f5d 100644 GIT binary patch delta 101 zcmX@WQ$9f=o{5=(f#KbWe@B57XMj(L>;M1%U)+$mu){*}=Oq`QAY)08UoeBivm0qZ zj*O>^V@O3@GDjh2VnIP+B8Ol>V#bSzgBuntW@OOc!MayliTNy09fPN< zkdPP{KnO7~FfcG6CKwo4;2*HUA7E$Vbv`T#<nL%kbKr%wNr5e}8^&Qyx7&1b|mzYgsOP=Lr*0i3Y;wr9qSc zT2W5T3=+y#xKw@K31e6L^5(6khJmYHsd(5+>bx7yZ>0R<#zKp2tS~3gW=^Z81_THq zE~z?*dT~Yut~TT)u+F!UrVb%;#nonWhw8GosMbZw)k?|MnNc>?s#7v-?3m?~s)>y% z!c}BgIyMNliRGx{O9O9d&?ZZbvtwWIy4QPUl63#;!0s~gylGU^~ zbRICodpzQ?%m8Z?*rY3@$hzVe)Wz>j4j{FjSNPacOB59@GC8*fVT=>9B|2-ZCmb#D zOsuAi&#i&Xqj@w5si&Muk+y{BO$xO*k*wD1>airP|01*#AZduHQlsY zhD|CJV%1JylVCi8dLjth>pwutEyE&Km6)5M&@cg&^pD_xk+w)93gLzk?QoRD9ZfBg zNa~^|Qg96Ftf30eBh5IIG-kZna5ady6ovsYOoLXcc2%d)h`K?`w(bO&=@r9oQ>TXg zT9r=h8-lIob)DD;U{BkeGMkq5nqxS+<>&U;ujqko2ORshNoj3je<2OwT+!Zt^W>*3 zZwN9UW>Bj2@p-t&Z;V&HP#yLFp)BuhS7Uimd7zEA!!0uKcM*(e`&|yCzbW?!mWj6P z)0EG4AX|TECiE-m8D0Cm@V_hFmcoB-zJC`!PATisxWi#cA}tRE<>O>j5cFfMhX1}B zJrg+0vwT?$QuruFF^_>dg+cp#U=YEPbi-Fpe9zoInf78)-+V2pYQMQqou{`Bg5mv9LJ3m z7OZ79kFjxbaDe@HhX#^qC|tN5bKI%XWb|>*o}cBo6JNdJ8eOw*LWlSga!CnGO+RF4 zj+>rohR9jRlrQ0m7ns7g*KY*gb4_7Yvwca7O<^_>$gkN;e4a#@*QA{2C|Q-)4N1uXL(N|1RiNlH z&}Ahf0tJE`%ou$46PUEfEyHDNzAKCEOrc6?2xYm^Xh@BWM52nU7=|GORaRAzA;h>9 zP}CHIc%sK(;n<11ka{HG6Gl`bb!rMs(~b##n9>Gu*GZzL))lgM*5F%cVICo(*}z0Y=wF2h-8wP>T*(p9V_HN4Q$`-tJfZ!HGyQA zb|1D$`xg#a(~4Mk+Ieu}KI?3`f;C&59CLo|?alOWD?5Yw_m7{y?19r~-p)_F8{K`s sdv$#4>G>zWK#OYDx#166k1zD~b1&{}|M;ZSS4nd$%q?0kuibt48<8ltA^-pY diff --git a/public/images/ui/legacy/text_images/en/summary/summary_dexnb_label.png b/public/images/ui/legacy/text_images/en/summary/summary_dexnb_label.png index eab90a91c7fc8dd67b9cf24a7e8c37dffd62b2d8..bf568c486aac5e8af4e95a237819caf922a9e50c 100644 GIT binary patch delta 106 zcmX@ZQ!_y#g^8Jgfno2IHdP?S8Q>G*`v3p`A3uI1BqZFKcTEW>$XF8O7tG-B>_!@p zqv+}47*Y|J?BS7+(82KV3QOCTEi7$53Vtk)R21qDu3}}VtK-z%ysM-MsF=ai)z4*} HQ$iB}`|BmX literal 1612 zcmbVMJB-{!7&Zl`a8XbYLOM~690`G7c4q8-oJaA6WA|WJ!N+k~DTkoQcxJq7-P&X9 zx!b)HH0T;iB4`i=Qc4h_KnRI~K!cEIXsJMHsEFtgO&IUP%Hu8xSh79#eEDonv;6e3Wm#u>-Ojdom+Mofmdx||Kk3)z^;p(@qp+;gPnP#1*2kZ$Sk|Lo#r++< z6TC{fN@`4~P}U|%X3&;({=y_j)PLb!#93o)U%NnH=jFWW@RMMSqdFVAC{FW(+&ol%6w<8f{5)>J+~ z#Pd9a9ppIBAYgHjYBqssac<$EBMY9#nT}NoN{=j5d)l`R(^(2hR=G`!d6`VX(1c}( z)Nq;76exIw%l7ioG`QeMj$|TJT^KB>u-Q;)RSeY$uB+`k0+`YUK^5azE=f{_DD=j@ zNn>7+W6?$bAd_fY7HTi&a%11nJXdCu(RMDGR(W5k(V|gX3(BC~2G3rOQ=!JibGN}| zhiU2C4t5$4uR^EZCxq4sbylCp4#ju|4U~wZgL|P4bqFQS9Z)kcf@$_Zu;4UOdBRM( zal!@?W$C~MRS{`hjZ|&|8+PuLMG(+lT41}Nj@Z2LAZn<@>1+3)z63Orl}GyCyS z8mISZ_Y793NRL@A*9WFr4;UtN67_=Z-=FxyweBS1cMacvCq6GJAF^~HjYuN9Oa+y4 zGHVFByI0e_+oWd!H+ohsDl@!a=xXz8x{`&5RZyAwZ>vh)qDe)!{^m8FZ|;?>X3KJ)mEYu(?j{l5G`@cn0JBl!CAFHe2?%@Yy)d5qWSlJ(`A WfBw`TT)k52eQ#s4^TkV7-uoA*z5f^h diff --git a/public/images/ui/legacy/text_images/en/summary/summary_moves_descriptions_title.png b/public/images/ui/legacy/text_images/en/summary/summary_moves_descriptions_title.png index 3d2b4d083760bfed09c6fae182fb0a997420f282..e83e8cafbfcc09a3761b9d915f3c0f0205109579 100644 GIT binary patch delta 219 zcmew>@S1UgL_G^L0|Udt0+u8o#UJ1k;+l|<@Z-mi|Ns9389;JX#u*);6mv@JZ!+jay6~7t*qfntizYh4ms& z?VDfRyXn1dOPhm}$?=zuzWYlXm|asivL|;gD`Uu@lRJ(3&GNQSdOPmq zObx5dI(>ife9ZpI@-s@b8fx~JIF2VHs#uB=gC2r7U_u05*FON(mZ}fv*moA TSG>1@j$-h1^>bP0l+XkK`;cKC literal 2173 zcmb_eU2NM_6iy{zTeraj0(DeqmisWK;n==$?AU5r+N510HC@*Rtz%o)>+9>pYGS*# z)1-YWAW=YTi%_5FC=!)GeE?|%A*O-WZ5k6oJOE+{_JV{E51UqvfrJ=;*ZCuZt{W4P z9A965=bZ2S-QySf`}S^ce55fD2y9RE#0SW|mt31S-$(ujhj;8Jw=H(hK{pWC`k;R` z1YSAb6bNie8mS>~DA}tZD;I>CrQ%>QXA^WF(Ar+KAsWRVrQ)<@pWfyXGO|9ux!bAeb7NO1ZLC$xn1k_LsXXl-4$+(7CoW(h8dM?58HLRVnJj=~k zw@_VseUkwa+GMh3V_hz}T+M{*b&V4@Dgjv+?WQJd%no4J$~y>mjT4$JzBjfKb1?KQ zCuLdLYNGn9l&M&ZdhCE&RYCN0g- zC+eXQ1_&u$Qo=wHA~!)v!D!Hfw*_lR(JdzjiMvJ)rZH=qX_~49QHfbu%OQpdyHJTm zGN~j?*Mla)iFlMI9tRCWQxH%EK@}s6$V(Cv!2)Jf4G0WUMJa+rBuY@O%Ev8~_w!nn z*N8k3gAgKt(IKxhnk0l6Rg@uvHIWN*9H%3WugdRp4ANIHd)InM{r~M@RZ9o)$R&w$ zE8Wov)0I`$X!G6lX#xWcBDg$|lK^O>70%DzXNl%#uyD{HVc`H4J&wrv~cMy-&7*M5(Rv_{_ zHA*ADM6pI!wvBc2d1v`3t1VrZCZo96?GSmZ%iCZ5iX*V z$^)7j_G1hfHADifM?hEr2=F)ztB%(bFSkNN=Q)#nrTdqe3rg@SMIE* z{+)w$Plf->l~PfbYm5D7*_Gba%>nQNi5_3N{KQ91FVBDV#ZwDEo?wrz9eKC- z$?&JAn|FShSepwzw0iZPeGTugXy3Km*!|(Jk3RkTPOd9`YGhj9^k#2!;p)SC=9bZ7 z=LZYZm*2VAGG)x`uXX3=8v~=q#v9JH77ugR?FTM2b*~J4KeI^P*!Jy_)s<@tQy-5# zcd+GT;IqaPR}N>@`QaI7YV3OC=il0YnwgzFHuv=#=g)SZy)@H&@6xNPmij>)`DW(X p>G98Z%|>6m_Qz5}Ji5HNb?=qecdQ;dzsvtXNObkZ-|ifF<}ZAE%%=bV diff --git a/public/images/ui/legacy/text_images/en/summary/summary_moves_effect_title.png b/public/images/ui/legacy/text_images/en/summary/summary_moves_effect_title.png index 55fb0efd832da789da351d76a5e3c7eea57b224e..55c4b545d9886576fbe0deef5921f1989e23d844 100644 GIT binary patch delta 170 zcmX@dw~KLtL_HHT0|SG|1HX+xiYLG)#P$FG{|N~R5Hi32$W5R)Q%R6tFatx`TZTbv(57wEbEikz3u(E z`Y^+zmJL=6f)g00{hZF=;=_Jm&T3vBo_giv^-ZY~&a4uWE`Ob*BpKxA%XdX|T>J~P Ohr!d;&t;ucLK6TGCqAwK literal 1870 zcmb_dU5MON6pjyFcU!8UAf+ON)CWZ~H@W$nTsE%jWON72y6z5i`{tdSJ2PP?xoMKw z$?l?}PgQGC=#vQc$p^*0iLDA+@j(%!4W&Q^Io}QlK&)~@05`WFa3kOm`*mZYt-zL2A<{m+q zdOm2bW~<(lh_JANeb%CtJd8P75N7A{7?U-ci7na*B1gLL(I=7^_>Q#S@Dwku(QdG? znb4KZ#U|NYBepNiJtEHLh#LrLhQ&PWMJdW1X=oSmaWM@faR|xQ9BICAD6V=-VvQwK zG%BV{R2_--Xu{=6Ut((CO)t~ zhSIpZ>knDAkx*#puAvw zoZ%aSeS+F72{D%>$XN9CakI3?izr9!t1m&Q2S$@I2Ir(hNt0He5&IiYr0jBjOWE z+nhcnRFMr|*X62ZSh8iPW=l7HwPhQV^)4g*BCnJ6K3|VDLNIJ$8JdPB`<7mnahqV7 z=o+>yXz0}zoUDH;3HZLky<4_Z?6#sH&_a;%RBep+!wNk(9`yooSOW-?qP8806iKDN zG@cH&iNMV%7?~Y(DK~nZbdO*uYi9$T(D@FJ)(wgYT#I^&*RM}pgSbjyOP4iGC9-Df zP_FV^$ZcEqb<;4Y-m)jcuO@CFMMIh~DZc+sd>m5J#ZiayLlQ{0F6HK`aN8VQy^sDc zDWEtRN3{j6EW6>qOHNUq#NiY_nu}rC6Zl8jRcOScl<=PM*4gh)@qW{9xbw~GSMMKv z`i@V<>DtyeKmK#q?`t!)J;%DA-G1l&ubPj#rw{$|+R^zVhtKc*@LZ#E@RiQr$MauL zd@*VAxx1)=2^v32@d6!;b-nJ);%G$21~PVuE6 zqP&*N10t)C*NvQq4a|kS2WIJfO{A4EoKLJ(OuoA~#Ijz~@ts*a{Ysg20%zC3s=x z){i@BVhIzQI27faluBv=ZIa=_g{a;NZWEE#sYm@JW>`7HhP5CG;##nU>+JJ20a$8H zGmEh)7thN=#L37alhH}Yrs%k`6jE`T#=$~F=*S|Yxj$7CqEbY05=50CsCNrB)uqgr zO8kKt*SCX4{ODD1TE+=22vC9=kOlx$E3$+X1!>yj5=4^JhMIxxI!phC>PUk~QLllr zhOu#iZwR&ta)QXiOs?zUDiuS&D)3nnQ7Nbg5ev-NDJ>SJiN^gn!9Jnm<$}Nz=UmrD zwxT%DQV38Tf`Nfm1z6CrK;FT`G6rlNYEIX_9FT>yu3dYZ*+Z!43bAbf9YY61O6_%- z0?DA%&}GA+7j49NhiglPD64Oy&MtpnKb|h{evDKhx%uN|Ab>beO&t(kwR!<^K z(pf7AByBQk3!T^QCKb84jJDl2YLq$Mrv2KmIB=2%j%cyUs&|9t7Pq1v)WpN?W(_ew};tQkuI}6ialhz9Q9e2BDRNlqp#%I{`b;&xmX!^ z`K>cMdY{gT7xrKhbyQ*s-5hR{z{{>6@m@8{0m^Y9z7w0im7ed)G5 NJ~CN;@6hq*{|0a5ki7r^ diff --git a/public/images/ui/legacy/text_images/en/summary/summary_profile_ability.png b/public/images/ui/legacy/text_images/en/summary/summary_profile_ability.png index 6600db26802ed202ae0536dd122605998b551fdf..a05c22b7d47b527d91b0406ae0ffcbfcb21882aa 100644 GIT binary patch delta 165 zcmeC?-O4yYqMnJFfq~)E$=0txiYLG)#1%;Y|NlQBApuAxwcg_e@|jA4{DK)6$|j$+ z0P?~-T^vIy;*vMK|NrrS?1}&1|I67VRD8|5yQ@_3b=Y2}P4oA4cYo&YEUw*D&3O3o zcYB4Ly=-m&-(PdQQN!c#wa~yU;H>@v#sBl0Zg7Xmt&L!1;T9_}WMEjGV45Ff*((gR Og2B_(&t;ucLK6U0ibQJw literal 1675 zcmbVNU5MON6iyX&tyH1)rwZydb=&%r|Ln}>vT<2wcHJpvyX=nL?Thx#&CN{MNp4KC zJF`pM2a6BBs1G8t_*j${>{FFiS@b~!6?_mB3Pn`#MPWtggNjm5<|lMz`lBJ4+$7&Q z=ljk%_slD^GtcbU{=jxY5O!3i%X4sl9Im0ETjBS$cLNV@w@1^56G6CRS9Wa^*5BSO z2)CT|tBdKP^DL%3kcrD{Ol}7epao%Uyd4p`#8Omabw9MlUqAX-M80c_3nPx|L?zbn zr`KXOzcy2)YfIE}#qp=mSQ~?bfTaYrgJziEwk>x2VpwOd6%lnH>5?r@WC78lGmA<* zW@tn%NK`X*RJ3HRpjx{5B+^x_sHjCnGbB~RY5^M->K!6Pi(L=Tm8W{K;K>#nX&PZg zX|-B%%aD0oS2WAA6jfJrT>^xZtcEFROJTC7@1e{R8v9Y|^AKeoNsTY3wg^nS5`rjq z8z#LpLBf!-5ac0}baDk`^b#@SoJ3m1~zs&z}dEZ60`_VUEJz+d@V6w6QCgISc_ksTH56ACYId|%J`L6HY zKQ?~ur`zsdIKKCbAD?^ZfO+?kmrp&s>)r?1Z^K`$f3tJ=>lcRCKi}Lueb_&_F?H$n zv74Jym*0GT=ilPSpYN59PF;HI#Ps-T(jqk4Z#9RCr!(kAVrpKnw#v`5z$z6gLBymp731pQ-kzw1n0+ zLW0BC&8j3ye#j+q=M}fZ&yd1T7uyAb)k~mBqTF^+0Yqu|1zQ>)H0G4avWy8v#I%v> z;~yB5e1V7%2kxK*X@Q-DOISAoBlWg1oLJyaTEzuU`vIS%@$+@Y@7ECDU_*5czS_F? ca^5}T3!11op!@JzlmGw#07*qoM6N<$g4_0GJpcdz literal 1965 zcmb_dU1%It6y64mvC@DQv5F7l6!EXKbLY;?PUf;}NOocq?WQ4JO@ayb?#!LtF*`q* znPhhZBD7*vsI(6SE72AOt5vJg5*3L)DAg)egy@6HYO(s1l2R3vLiNsOcS$w`X@>!W*B z?75kBu~_S4R(3cX&TK-uYsazS=19D3dlVguB|FO=*2hW6^nHnS>4F3qMU4vyph9T_1A(CMK;Z?E0|EjGB&tmPVyQRZFwvlPZ`~LD zO0k77^bpUNN~L&7jJtlG7gSZ{0pua%D1r+rPKe8#6RdAo&`6;BmKR#C!$cNw&YcWX zEM>YR!SE`eA9MI5aGEE;Var=;+omgX$$)G+b z2(hD+J}t#k#c|6rkRS+z2ugyJbHw08Q|jP4L~L*%Az(vM1Vsc=W4-3;lTlt9>kYbI zRIq7Cipb?Uu*jJ?DZ#0xkmIm`36!CP6_{wO-|Sm-U*X~v+lh8tR1m1w3TUb-_5HAg zOw>n3i>cKB!g^HODOQh?N({C>ZLJW2%Tusqwp1Y0=ta`K1Pfdmk(( z*vS)mNb>BJOS!x%{C5sk+(-YH6h1l`muiduXW7;MEjvYN5^GcRXpV->o-7EQ z*^SyO{n}}@9X$HDUi|SyenOr-x|oA!f3F^VZS9VI+b7$9KKT5eoqTeprK(l$8-8Ns z?V+1?e)iDetB*hV=ZRAX_CCDrr#YJ)OFme+9n5t~o)V`j6?NImDZGQuSe~S_T diff --git a/public/images/ui/legacy/text_images/en/summary/summary_profile_passive.png b/public/images/ui/legacy/text_images/en/summary/summary_profile_passive.png index 66f56ff435e5148fc3c48b19c28e38351538635b..c026e87a2157734bad03093215bae165aa01e8c0 100644 GIT binary patch delta 182 zcmcc5eT;E}L_G^L0|P^CXF({C;t%i%aRt%|2?_uI|A&wpnLmU9g_ui%{DOh>ud*Ze zfK;-li(^PdoM}%t=TQTmBRuQ5XXsr>Yhp=lEV@^}to$W=H#2uY$k(@vy}T-&Up(G7 zMd8Yp69&G|H@&TSyCzTW^|IX-TQ+Z)D|}JknP;K)*-f`)vf8;T#SKD#WmYu%Oq!d> c@c)4Pi6x4b2J3}#fc7zXy85}Sb4q9e087bAdjJ3c literal 1503 zcmbVMU5MO798a-muX+V(5v?F&`rv6VnI!v>%XqPzo88_*bDnn#JrzG@lgVy+n@pO_ z-tHbEa`dGqQbnpDMNpK!w8aO%K(Q4pKKLM(`k+`UJ{0YX;FB+>PWB^QrCMxYGGD*> z{lEW5=jZm1Z{NLL5QOpSthK=BhxpjG?NFg0S^h zzqZ7d>;uT5fs9>RCvrE4I9d><_jM!eEE5Lmq~V9A_}iyvMc})pc(7nAc2puwe|9w{ zi>q@rXLZ>zTyftcV7iMqK|mM=-JlgFsB4OSUc}ewF%&@`!j?^OCKU*l?0Hb4F#!d+ zC^>3g1G*urMa9talR#5c9V$9hbCRMWrHFC{7=9v`7P}r=uqs1Yd}oSH#v%k^r_+%; zIhn={s2YX=6%A^d#1T@`3mNW8VX|k$V3EX${fPNA1Sun~(>60juIa#pAj)XNWauXD zFxbTrRAnVKsSk9W3>UTIR$sX5K++-s30cCiYKD!Pl+mO~|AjiczD59dt!-yAuB%HB zWFit)Ug2g89dccCQtL$oE|7$_V~3PixSD%XYa&#N31&2|QQ8^>YJQ|LD3!p(K|ge9 zCwXuaoLHCwCS;&?!mF*VH0XrchqL2Fer0 z#SGsN>^jJ!ae%qGet;VUMqxt)nG;cowrI=+bM10{Eo>WA!-V0`Ayvy1xy7>YyC|nR zxg7Ca$uo#2>BW3qs@IFURKR&daf=?&mBQ$~MV)pU*U@|Tf4+Y#_IX|5)=l%I)s`j% zn)MSNs@||37Rlk^s^x=z4j}BLxowJ08dTzn!(;!ZJusl{GzlmDk8lrQ3H4Y9$7H6# zy>)|Rf?Rc&*5JSj_)i1-cbn>U1m{g4U1r^fZ?gVHLf*7u-kr)H9nyJcJ5jY}YExgI zu8r+D2kyAKxbfEmKa!p9Pwd+xlciG=m)^NB@!rLzbKA4$Ke_UIPm}>PzgRnSPJZF% z4YI2}w#&NZ-dCOvU;NnbK6Ck=q=GLk((fj|dhO1K|2+2i_fHuHJd(fq zL#6!9tM`AtsvmII->4j>)2G(oI=|)Q%g2A&|Jla++fVbpK6l-(Q zUsv>$DwqV6rmCeM+tOa#5mL8gv#Zu~T@9s0ow^x854%+6%cst|^P?d5%6%^hKmp#RqK>g%q?{j3_>c0rf$Rh`xvq1)&x_vwyaAo7UpM%-osz z&N<(Ae&#+sGkwpFq1%Qyj@vOcnV*Hb2It`5b#Og?^4kpDuJG7tI$pVY`6010FM)>W6HGy|%MmVS!q=KJg zRcCS~UE$j=vM z_?#CoUQZf`$eO~NR#G-3OVP%7MUqWXGDTTMl59(cty+BN5FlDW%l2&k?oKRt$_Ujc z@@-LUG#bfdcKoz{)+WU-cp{ta_Jl-TQn& z07z}2(2LQROTFF;5k?bBAfuCzzUZ*H>@#teh2CO7*u)ah+#9R$?OeccT<5I5?-gzFJ<#EI^f+8t8l2S-fi;8ONnk`$Sl446z8(Q$FQ(nFZsug9^ z))iZkFMvYBP#odQf+?}fUQov%*Qw(Q6MeTL@VzA3Ij`mg5E$60Ef$4>J>`ZGb_tux zX9Q53bR24{hGi02DI?8-3sW=$l`JYFlcrLpDQS{XB3=8uM;7C{cI_$HYZ@R8lOoN; zB_x3X!n&>@RW&q1Bur%(lQ_Q+|Ng*%xx%%p_5=O@kHW5(EGP=0aA9XU_Tx%tRde>m zGGm$s)fK}~3@DM7Lb|0jDW#OI@V>-#=*I3a{CDD=l#(iTD-3?x zqHy(6u52p$*5KcB{eMYmwP(Ax#EaXm_3gaBdbfvRtB9AqJqjLryBu>NO9AX*FFb*6 zg#GWOseHDWT0d3H?mWp4{&@7y~Ztf9pG8!7JA&)*z+_uMZdpKc$JzYte< zzqPS@`1jM_-{3r1+4(^2u8-5-sGCovZ~D@)M&B7re01XBWTLon__OzaJ@(b9_0;M1%U)+$mu){*}=Oq`QAY)08UoeBivm0qZ zj*O>^V@O3@GDjh2VnIP+B8Ol>V#bSzgBuntW@OOc!MayliTNy09fPN< zkdPP{KnO7~FfcG6CKwo4;2*HUA7E$Vbv`T#<nL%kbKr%wNr5e}8^&Qyx7&1b|mzYgsOP=Lr*0i3Y;wr9qSc zT2W5T3=+y#xKw@K31e6L^5(6khJmYHsd(5+>bx7yZ>0R<#zKp2tS~3gW=^Z81_THq zE~z?*dT~Yut~TT)u+F!UrVb%;#nonWhw8GosMbZw)k?|MnNc>?s#7v-?3m?~s)>y% z!c}BgIyMNliRGx{O9O9d&?ZZbvtwWIy4QPUl63#;!0s~gylGU^~ zbRICodpzQ?%m8Z?*rY3@$hzVe)Wz>j4j{FjSNPacOB59@GC8*fVT=>9B|2-ZCmb#D zOsuAi&#i&Xqj@w5si&Muk+y{BO$xO*k*wD1>airP|01*#AZduHQlsY zhD|CJV%1JylVCi8dLjth>pwutEyE&Km6)5M&@cg&^pD_xk+w)93gLzk?QoRD9ZfBg zNa~^|Qg96Ftf30eBh5IIG-kZna5ady6ovsYOoLXcc2%d)h`K?`w(bO&=@r9oQ>TXg zT9r=h8-lIob)DD;U{BkeGMkq5nqxS+<>&U;ujqko2ORshNoj3je<2OwT+!Zt^W>*3 zZwN9UW>Bj2@p-t&Z;V&HP#yLFp)BuhS7Uimd7zEA!!0uKcM*(e`&|yCzbW?!mWj6P z)0EG4AX|TECiE-m8D0Cm@V_hFmcoB-zJC`!PATisxWi#cA}tRE<>O>j5cFfMhX1}B zJrg+0vwT?$QuruFF^_>dg+cp#U=YEPbi-Fpe9zoInf78)q5E3)-o`thv>2-tjsh5 PTEgJz>gTe~DWM4foEpm6y6GGLQxP<5QttZ=Riv{>;JVqWLv^IZ5H7qBr7$Lf;u}h-nDk^acw8N zNrfut0jeCi1uh72Ktdcql@JI465_y*5G?{E)C&g?0x1Y_K|;WIf2{tbKU7$C{ZjEN1ND779c4zmF!sg5O z6$(2~`n9EW$$boB78DU-bz1BO5l0salLxyI!Y!H#b=vU5vbgo$2cqDUvUtRBWjCtO zra!YD)5Z1K8eVTgD~-cNnI34N>R=v^?(HDxM(GA_nZ?f(KZcem?j*n$%wZ7q$tF+>Xb#Uxaj)? znx<(HPceX9(g{FB_W)8Y9hizvWMr2-OU>6i46kH)9j+&QJ+7;UrV|a6bRC1bt((A7 zEDUT*r<$xO9#)m1|GC)b`wF#h*iN?FvVwp!e!^3=HrNk~^!Q-Z_Jv*zK!mf}E{ixz zDkb7z+TSJu*QcOww$r5C=vC6)hb7EQJ1C}84IZs)6q9r%>IquCI&nfurloiu(6FZg z?CA!m*O3RXZP_ZtNH&l$6n;5z6RIYZjbWYscjAMP;wB0klpm6kc;iy8uL`%#!R7nt z|B@nQCu6_1q)W@L_wRyJlqaz_#gFD}nD+$!k#`ju@+if;XDm;Se8v0CM%9_BS)aYO z`0Ut+!ssur|NYO+FP+wwcF+m`^Ub&JdGL-qkJe8-+4}wYt)DjjJpa+DGfyS0+h2Y7 zUg_9@v&VNo{>&%Bp>N-QXzWK{mAhL<|JZj5-ldN0hvQE>FBYfY*b?1OjpoODX5YPA z+4IVG(K~;^Tjn14=H&eOAHIKKZuwVd;%89$%{_bK%ddmYk#k?P&pmq6;da&stJAa2 Jn~xlO?qB$|S|0!a diff --git a/public/images/ui/legacy/text_images/en/summary/summary_stats_stats_title.png b/public/images/ui/legacy/text_images/en/summary/summary_stats_stats_title.png index 5531819ef665bd054b5eb15d84500a1a4fe9dff9..f602a43c39d509720f1cfb19ab25d4aeaf676ed3 100644 GIT binary patch delta 153 zcmX@hw~}#!L_HHT0|P^i@R?2^#S`EY;+l|<@c;jR2zmO6f-g{vTY z&&$)rF{C0cxugD1y`6wgTe~DWM4f(nL8j literal 1869 zcmb_dO^Dq@9M3AO>&o^}L8TXCsIZrf^eAYlllKwYy4%)bAjxF%o8SNc z`+v;5yt=Y9H@j z!j5jPcw}>(I8-@!K;Az(|j^n7BuIf4v2#{_>86JQr-92Hbv6Li1oCQ3Rb4J|c-K?SrPe&5MxS);F zahOEF)B%oFSkiJyLm(vuF776sp>ax7)?p!wvQ%JUfsNZd<7u03LtXs7!hi^^>lQY) z^d$@n6KS^C6EemD*%F<48!=NGEalyVu*IHmb9b&LM%9Gjj3*xFok^lrCoap?s=W7D z5K-Pw_gw~OHJq`EqHDSZw0WQ#9<-5VB4|IP=}6N?P?yud-?$2Dda#Tvh%Dy{s3;hU zGkjezCCKMVh=trB#4V=AQA?4FAfhVo@I)9E-WfwLTo=`&G{X^L^;$&{ic3L2k=-o& zI(9H%^A-Vi)AWI3YYgZ%(V*?y#4*TZeT|cDp4Z8GD%KOKW4&x?z@fwdCNUvs+FBX- z79%F6Hq>=ES$`x6L|@^~P4nd4mKOwC4pNb-jd4F5WzUR9oj@Me0Kz1%?TSM3q%x|E zr-LmbaAOKawEZ>{q}NFI2$pg`>*Itiv_!P7Q%vf1uBW(nZO0k4Ox=cMP@Xp&VA-Y# znoY|B%=Ue1*u-a06yP?F>l%V4GKxC=?~ad|NgGEkCN@b`xp^r!R)t$i`F9`vUsBY3 zXB^d*dUe?i|6R6=iX;xF#MYb-i!(ud6sHP{M3fS7X1sgqj3Le&(MJp3-fzw~>N}+S zW`2JA;-x+4ZnLwq`yl!B`Ooi~J=FO2*IOTzN`89$yCcoHSARNwV(EY<{qR-quP8XreFp9_!`mm1 Q0QE35hjuH9vadPoEx>Q3p{k&hWt1hH)|2`D>)9Lx@~*wR)aVUG(SGhDfj@0a@J zl1MwqbQ}j67P2fI5PG&6OES>o?C2zd$1|FSi3~-o<`GFCmZhr!(@_agQiP4OahsrF zXh0HVmW;flA&}7mmn^5fVRA+h@9~JoG6SqxV3V$pBI}ARLKmMmB!JfXevxBSE>TpZ z$mGlllre6|rs%A-nsBthGqIdfKC=QekLJ}RxRG)qMcNXgHyPCYgtFRbsK+jZF%$jl zlb7J!Ba*wCWmpy6sOgs7GEHoo*s6VISlBQ|P+zdHy}B8+0u#13Kw)7Rk>rhFMsZuD z5rJ~Uh;%qg;*O>kO~eh+6DcGHcI6=p-^a~3lO(3R>A4zITna;mExT4_HY`f5Vz>2x z*fl)}NLjZ7+qT;dEe8Re_2y20$KP6)SjR%6wm;sjY<@LnhZgg7{->`h|PJG-_+9h#^gGeGR zF9qdtG8zbabymY~FG0E23GxeO zU?`h>)&j`O@N{tusfgQh?jR?Ff&lZu1)@usEv;|7-fymZkU{T>rNGU+Kl21!7KHO1 z@(vJf?45kX=OBmUN~41B;&Z+|o598)(7qXt17rY|XWt~;~4bI3L(yRlj5#wFR3RK@1(nX|iNcIHfH zW^X|uu0AMu0R>Y;O8a1?FX~HMq1a-fQXeD_)+YF3r4RK*qAiHNh-daPwh~(`4$RCs zv)}o?|NGB>{(U1qvwtZ1Sd?Lyq3m>KmcAG1{o;dz^qJmXc!9olIMatbhS~X0a7UOA zJ{)0~ftFcV@E39i03wzE4N}BH({d=9VaCRr4n#}XXN$OO+9_`H+y#y`jTASp<)oaG z#uamV*~N3qGX=D~gmi-&-^Y$M0VS}o580+wwLQ>GaUEVj$H8ZjV>=LkDaB0$g6u*r z&!&lsSxt!ZNLCd#p$l?c(iL?tt4MM}loFyG<0TnLaS+ql-o;Th*C>J6%+ozBI!kdC z-*mGRKBV1Zh$wE)}NH#X3>sav*DaKC}_eW>Oq=ESRPN za8XW{Ak=uQV~vlgD&~twQg~e}Ce^sEp;#RE*JlW-1^e1xZ~Vvg&$uQ%SFn2DJhcA* zcGzc`ppHDcabE9qOu}++R5kYnHiIw?T~#Cvm3U2;3?9m=#wQacorgsQ%7%gCs8k#i zgQGwfvE5OZNW8mZr$E898AQ

CWLJVFYZaCILjjSxa#!C^Bqtz0c--??{)nQNfh- zF6GySd8Fhwpo=HUbocI2++yE;*M;~`gPkPZ)<61i+LVG}*bV6CK)6}pIdov$Nt3P@cgx}2DP_NeVMUudd*`GaKHZc z-Pub=XQLMme0%-YrR3WutWEis#_HHl@biyG(B=7-@M`(`z~!?i&B-4cUnh4xyP~z= z#?{kRZ8Lgt{ruX@&d;8I?c#}Zw;s7t`{I=+ULM@2@7id;pI&=-?^@){qw9aZex$vg zua6#o>FAA*hwFudr&o{74PRS*XWN^Rft77fMUFFhW|SFbqRjs={rzy{kCSawIQaPC Y#qBq4*3RttH2Cw$PR(RKnOtoD1sggu#{d8T diff --git a/public/images/ui/legacy/text_images/es-ES/summary/summary_moves_descriptions_title_es-ES.png b/public/images/ui/legacy/text_images/es-ES/summary/summary_moves_descriptions_title_es-ES.png index ffcae31894d0e1ed67909908144200a707e7b342..3a4e3c7c375b17ab3a0154262c6a3b7e7668f453 100644 GIT binary patch delta 233 zcmbOz_>*yhL_G^L0|SHAL$?wj#UJ1k;+l|<@Z-mi|Ns9389;JX#u*);6mvkbc|KAbOW`^<;{D!h7%akDAP?T-; z12Th&UM6Nagb4>F4hte=WF;s_K!M^m1_D9mfy@gb4hSlc)R4kdek_gVXu8@Lf2XwGgh?HbP}o60$7XO%`p!_Yv0`b}(rhp)?zPHMSaa2=Xi^ zX<6B7p?a&7nOKZ@c)&0$t4G;wK0~O}6`HpUb1mJ=BZsu6Y3*+?8vOOByJ%!(;zW*sND7J^ z)KXzT$B;{fXrlE942uv$frL?2_)_BKQ7WyJ09L2&zZ0*d6lai`CUnW?*;|)#b2Iht z8Z5gi{9jT^OK`PCe4yVcfes=l7lQ z)?~+L3s3I4d!DKL=B+RPy#0@NclXQQ^qW_Vsm{jUoiDd`o$h|1#U9FivhT!}-=~K6 zy>o8sy7#{Q;pEi)*7+m9<}c2w>)#(ccch`Po?W;!`E}#fx#6D`X3s%DCPrRs*x0aX zgY@GUtC}WHOc(1H%?FR3`S795-BaQfajq*_w;vzt+JCz7gEh%Rzm4PiRozb%XWRS6 zRxbw8`Ck&JE*(F0^wpUcGcOH}Y?FkYXD^S>A1}^~o|VUDXJ-}aY!KUqvlAxD{x_ W0zI{-OLMJ(ECx?kKbLh*2~7Y^s!U)2 literal 2033 zcmb_dU5MON6pkWwU2QECto5OWl!~p*B)R#&>}=VcS-XQf>$;uNwGZOW&7GOBGr2Lz zZf3XsArviW1)sF|AP6me>5C|pLRUfXp#>@UQbDC4J``&eL`#vyo0&=M&vv^O14(X@ z@0|0UbH0;%cxrOb`oYHrIgVRj881(>yTs0cfpzTa9XTej+XKP)^AX2AxG6pF;f{Xz zD97FZuveRjXQ~qr({=&5)FFjVJ78#z8`|9o5S}G5?~sP)7lkXQmjvE(i^6`rDp!LN zX?o*}A=$e)S;LF7*mi~8!~9SOG6!uEBfisa`4Q|Cg|1)7*6C|W;Jc7`wkV9G0r{Eg z6knnt;q`(cVnqYIX%`ejwt=>b2eM*HvMDL5C@WAlplb8|LtxQDw+^SvPxoW7r=rk| z;{Zxhk|c#hEzqzbDYk7(GLQfe8A6N}{1|maKiZagD3b_>UJ!fK=Tnc!p>uIjU`%@{ zw1eEOAN9+`3MO?>ASneoO{oiXagGb-!d5r9izU(`ZQ{of!zwv8XwsNQO?n&G`R5w~ zu+mnmd5jggwA*=zC?1<5n+k8XvhLHc50VJwF)bK6eAy#O1UVo zj0>LULRmwqW2-=vP1g`@RWn4#HeC^6$Fxb^L;$#%eVO99w6B@H%j}VgF#-+}fuXCS zYigE=>KKVwQ;}@~U9%jJ**_b4Y_3r2uKh6m|C?dvWrSrEvBpLH=@><1chXz!Mb+`aZ9(O9-;ZwiK_rv7&w}!stOiWLj$74v?11Q&D9KD zCz@ktiQh_Ghq4M)JsXDqPP|`I+(dqZu%EUh+LIeEaKnm)~3-Jay{J6U&#rPd2~sGP(Btk5`W; zzkHHxSw8#u`4b->dG*f=a~rmP#a%l0_{A^wefHa#*LE2{TuDB%6zh>CW9aa`hxRNh voAhFR-Lr;#O%4LBiP>Er*@_wXoHdqgl<05TxbZWOwDu;oTE??X3p<^? zbsQA7#owF1U5o3`r!LHi_o_f+ z;oa?8x6bJ{ZYyU}SRv>5W$PA+dFH1kZ0cpqOLJ^O6Bzju4ny-lG|XnQgdA0XF0ax^sDM$UsrcON2`Mkn!@7Yc3N85~|R2kxrMd_{} zAT^vyQ!(4cloXCIh!c5AR>E8aD7@H4@qm+AAhVpn08RxFRZytvhbGZn-B1VOk5prk zyC|LWeMe>4VzC%53SryLvYetQEZ|w5X9$AvrYs+p7|Uy^d5B{Vxu)ZrwnYUVp=M9` zQJOHVNGLdUx0Y8;6G<3bf)2}tK_ICN)KMMhOt|@Sa2>HYj|J8!!rFkvT@S)@{G((-(0 zA)JgyX`(o6n!1X3NdpE#jG|zPQQAeaf|AAn17MDmL?k1vW*@iFM6j#kV&C={(gH|9$l`dOlEy&Q&MVUxzhXP}0VuVo)PGcYkF)#5Vl=;Z|))7vI5F(K= zAZIYT9Hc0sKnCj(AOS$K54csbve^&lRv^QX%Fph6V%d6Y&$ zfn%MnJ~r2=hRy9}#ceT%iPH^AzXJ1Y!!JS?cV$WC+@P`8^{9Jra%19H6XgitAtMNi z!f3ikYMB!iMiG&OMIK5B@-^XW5tjs26l;z6--%aKigM7(VlpsT`sSrsS`WE0De$e0S2_4!C{>Jt`QBug%tpCco^f05aHJ{j-IBtD-X z+S(L*VB0T;=2u$PA6`k1o&QGN`^6J44x_%I%+Pa3zJL5+=Z{ym%m(YJ@WJiOR=waY`bOW(!=6=PtN}I z#$w}Q^OeiTUuaYI?P$C6{kD>O*S9>HyE1d~(GP##lUr(Q_R@R7Cl5V8`1ah)h{Prr z_spTw)7r%|-M_uG5HI6LTAUyEc4gTe~DWM4f(1=DT literal 2059 zcmb_dPmI)57%wrdfV&Wc5Tiz#5|xd1Ui;c}Rp3P2eHl8(}(XsF5kmZ{s*Kvac&W!Ep1PITOm^De=@rvB#99i19*NsQQz+w>y6Q{=jKb-GR-1CH%N;<+MMN*uDY z#uQujL&D~B3Xeq@vZ|I76+wgY6D$-&6$mO2C0-D9LD40RO@AEq7FxDGU3oI~MemAS zBaQ-kRLK0FJXyFVOw8CcBxrG5~5|?;!M6qIq4H|yzM-BfLrnA>8 z9MIGnM&@H67uU^PMDh4M4I@p+Ky*}F2nd)ak>3h28K0*#cP7yUdO0L0_QRU*H~WQ} z>QiRRWp>v=$Fux)G^J?8I;QNTE(^oZt6mf%50h%8 z$kB*%j$`SfVnP*(7H{Q6nU_pi<4sg3@TQGSEI?b+u-e~W@o_7uYk$8*_bZZFK$5EP zf}~nJlx3Mm*i?8^lw}nYRkno8|NhXSeTABLY=`Rqp9uR_N;IN~7A{J=V=td<#ifs(0To$W zMtf%44ML5sBEKFi^6j{dLQ<;J>fJ`%;8xTlG`~7=l$W3_*t8Qc#Jmh;%xhE&yiAZO z5hU8l7c&ifCGn&gl%yF%vEK~;op@>)H;`8+^rH>9I}c@TT`{Nz1N!>^3Z*O0p0>ch zW!HUo(^tK(VLB?3Wj3SeO*Z9-N3#^t8TQekch1uJcdA+`)e2uiO2@sKzRR|Dr30oR~->a z@E|6LnoKF=`<<)0&B~GlIlMlrQHr@F+Ev}ws)NpIG<3=nr&Q1=im>{$9(30`HyZ!Y zL*fsbsZ2KN;QF9Lrf#(Nn~iEu*ZT(=-AP3oBmhF?>sf%FL1J8-bT5NDKpf5nz%S>^ VFr(m;+!g=;002ovPDHLkV1i-)V!Z$W literal 1976 zcmb_dU2GIp6dp+=1+BFBfIpaw(*%?Tb0q-MY4mt%{MmGk12U-JKa` zrrm9MFcOSP;!90UG^nWwF$B@5F-3W>5mOSdMqe5dAsT{FFj}KTq6YEK{-tR7kvPfB z+`03ebH4AKbMJjV)$?HE@;jC@4Aa=vndqhOG`$-dmeOZ-?eQdiEps~`^%>@-Tf_SX z=9N9SG0f7Ztn@%IknBdfQ;1;0$&g5?;8HZhY}{0Gu|7lsHbb(O9pmN?eax|z5###h zBuKh(lCwI;JkmGTlh((Ebj{#4-N$Y$A?lz&0?d{QdD};&7+3a-=s27fIJOK4hGJZM z7?2%Err5aS5mt^Uye>+RRkety01Zm(SqMZ`0IDEHc_1R7ps2=HFOEj@3={Px9;n8m zUokEh1TGSUVzC%0Mk9`w6+}(b1ONpHd5YluQ9HmT-u7E+9umaYJLL7~V}#16CS*~xpB{CI&`W%0*wabJ2&LH)s&P@=BRFup zwBzJ!g-X>Zv++24PrqdwPSL-20h}aoKw=yOQ09TkLoqFCNEQ*4TLDA>RG>-6u*}hg z&^8`QX;DR@fydauASxBdJ<9G$vYkmOxZ=tERspo zW%~iPb<&lHaa3`{vJ36b^8fdTi6h&)ADJlMyuXhnG>H*WFrbX+{PLBa6G+b zx3`1&Z2t9=Uu^j8)c22xz}(e*Mq9xwnV!{WF06j`mfQar_c!N;X1+W)urIZD>&4e5&y2j)bad+RCwg9fa&CP6 zT=(avyh#4Qw+CLBoti(sY-*qW;v<8nj~qMt$=Y}SdUN}Y-?cIqmcZT3=f^+l?hnt_ Nu8y9>+xKtV`8P7%k4pdm diff --git a/public/images/ui/legacy/text_images/es-ES/summary/summary_profile_passive_es-ES.png b/public/images/ui/legacy/text_images/es-ES/summary/summary_profile_passive_es-ES.png index 885453e3e9874c1a04fb25952324292a7f03692e..a50e3cacf580d1ed549af7117013779fdb86ae85 100644 GIT binary patch delta 177 zcmZ3*caU*{L_G^L0|P^{gTpf*#UJ1k;tHe_5)%IZ{|_NIGJgmI3Ne=i`2_>%Uu8$` z0jW4o7srr_IMbesf=rG)PM;H*-`s3i&Kjd4%JKL56SY2O`TPWhH?N<)e!6ApQeMR@ zn<(Ce!O0FUIR3Ag;~jlA@$$9J?klqR3|+Nq&&Armo4Q~B%!c32@Bb+|d`OsdmpicZI5Ty>-7*PN_JCXiCv;(%LWjL+41bI$=c)E zPIePo#?Eyh0geoN9fDjT_4l9KtQUyXiw3muY5!WDb=mAuf@jgrnX&SI(dp!Q; z`@jD&|Gc|Ues=u8BL@UQ7`Nw(i~Rirzeh*+^XCWtD_8hyESP^W5`=pXCHFnTCl?Z{6=C(RD1Ts+T}1DA1-m zzY)@DpbsrE$E~k#hvsFNt!M_)0D}43 zpuAul9OJu!9fImC^e}hV^>Bl#f!~nCG>E9cS}f#-xw=eGMX7{rKZ>zWs9m%q?y=&! z4yw_tsX+tEI?U*D#>fI0JNcXp>rgix0the1;rb#Yt4Usm>m9y6qmfL8)*ZQS(zRj<@*Y^{x=V=|GzyPvdnOgBA&QtusddPV=!vDQ;C~< zl{u*@A{w z+IEw2(p}204~tkmZsU;7Gta-3pZwKsW^QreSR&Idr# O89ZJ6T-G@yGywpZ*hyso literal 2030 zcmb_dU5MON6pjkIU2O~gKv@)SQ_&U8UO)m)aMVzG;O%R4Ae%+6qcNnLpb)-PU3t$xZUz zbH4BV|kFhx9TohXr2u zO6w6nus&aL*Oy(3iF+qdu5E*XCQm48HyeIzxAS7ht_|z-wJf3zBw5ajlc__rR9--Z zAmT{R7?P`KDl)B%Vqi)m9>z|i#!fiB9~9E0?j?Cn$RvpoF+k334%sHQ44*_ zs8B%ThdiGJt@yDk;Jio^o)=ZD>JrXMDygW%)-_wP9>=PUaTi(+m{(o92C7vQ(^dhb zUj>DNVKkvP1T)vJ22qoOT(3!MTn_!3hz3El3qd1@z%j5R9Tw%XJ?qB_^<6$&%!{Bn z<9W=s9Ac_jP9-G^)j}N2m7FRj5;2vmX^|??Fz@d#2JT9l*ZzJ6`!&sES;J(KW>QDO zSi=(4bxk6~&|DW&ra+sd@df|;BMy{+P&8;Nt8j1vsun1>{w7n#B`mk+7~{QxNZ}p-wgkqcrT=Go%%HnKW$mO zbtyO36+?6I@4o)Oq;$%&TU+wAW!L$3#b3Q&!!Rn+;M1%U)+$mu){*}=Oq`QAY)08UoeBivm0qZ zj*O>^V@O3@GDjh2VnIP+B8Ol>V#bSzgBuntW@OOc!MayliTNy09fPN< zkdPP{KnO7~FfcG6CKwo4;2*HUA7E$Vbv`T#<nL%kbKr%wNr5e}8^&Qyx7&1b|mzYgsOP=Lr*0i3Y;wr9qSc zT2W5T3=+y#xKw@K31e6L^5(6khJmYHsd(5+>bx7yZ>0R<#zKp2tS~3gW=^Z81_THq zE~z?*dT~Yut~TT)u+F!UrVb%;#nonWhw8GosMbZw)k?|MnNc>?s#7v-?3m?~s)>y% z!c}BgIyMNliRGx{O9O9d&?ZZbvtwWIy4QPUl63#;!0s~gylGU^~ zbRICodpzQ?%m8Z?*rY3@$hzVe)Wz>j4j{FjSNPacOB59@GC8*fVT=>9B|2-ZCmb#D zOsuAi&#i&Xqj@w5si&Muk+y{BO$xO*k*wD1>airP|01*#AZduHQlsY zhD|CJV%1JylVCi8dLjth>pwutEyE&Km6)5M&@cg&^pD_xk+w)93gLzk?QoRD9ZfBg zNa~^|Qg96Ftf30eBh5IIG-kZna5ady6ovsYOoLXcc2%d)h`K?`w(bO&=@r9oQ>TXg zT9r=h8-lIob)DD;U{BkeGMkq5nqxS+<>&U;ujqko2ORshNoj3je<2OwT+!Zt^W>*3 zZwN9UW>Bj2@p-t&Z;V&HP#yLFp)BuhS7Uimd7zEA!!0uKcM*(e`&|yCzbW?!mWj6P z)0EG4AX|TECiE-m8D0Cm@V_hFmcoB-zJC`!PATisxWi#cA}tRE<>O>j5cFfMhX1}B zJrg+0vwT?$QuruFF^_>dg+cp#U=YEPbi-Fpe9zoInf78)`>N3cf&brjj7PUFR-BTMm8w>s3v3CbWQM<}x)&#jraiQ%;1Wi#xBkc&HX`IqFu6u#WTt0J-p*_cBCgloOiAuQP zjm^jS$b7Yi=BJVFFeAhCP}?95e4IkM?KgwOXq!y#*C6X`n`P)6l1`gUF$+jfRmSO3 z7-L#4C;;)2K&yIzS2$ge9-;+~S6NPFc@c2D!6}BQ)7=k4qQ#DDOjw7yvB;CjG}1IO zShm$_6pIH`tRMh_0LfgC!Zrw!{XGvBPEhPcsTT%x<`LTAOlmTOX-9$| z_1y+ZH%%mAY#T-_U*IxHInY6UTr?9m^WY9*aTEJENE3qP``D-vreV?uH*np5eO&;O z+DfG#V_h!3-w%6Lwwr9fccWvsNHRoi1BtEkjus97hA1j1-`_k`6Ro zCoWVOsyc@d6!eX)x00e&);Wug2zGO|d2<+JUZ#Go%lZgD*5^;>{a`?p^|{HQtiZ9u zbho{As$nZsd-kmcCQdgg{Z8~Gbki1$aj{M+=LU_%ZbUtSvzrrl97WZ5*99VSMSxsM z2DS}dfOJh4FoGO8!i|ZOMpO+>?lt0nC*DmdYQUh5$-rQlo0qb+j_hz;a|ZwZP5Hm1 zW5$XJMDp$E5h^MZi=seHhlTdyNWMeed^VxmluyeH?d?D7A};3 zsNVY4fpdqJPEP)0RoNL_{M_Dq`oWPs%fEAHFTG|xelL4;Vc$2Ood4z8oi9)yT|B!2 T=C}LV|AX>q)ml3E%<;bgudBG? diff --git a/public/images/ui/legacy/text_images/es-ES/summary/summary_stats_stats_title_es-ES.png b/public/images/ui/legacy/text_images/es-ES/summary/summary_stats_stats_title_es-ES.png index 2233461522c4dfcf2279b5cae51cfff1dcbfc942..5b59c12984fa162071fcfdda9970f0814bc2fd6d 100644 GIT binary patch delta 292 zcmV+<0o(rM6SD%47=Ho-0001R(Y@$iKR@%V@T>>vJ^O3UiiTPqmyUpMdA-Ms!qMg8yY{lEUTxBp-N8pvhvTGX^ctEp>&hSsX4 qRV!98HLdDe1Rdlugkl9DxWoa=AB!X-iM>Mr0000!kKf1teO=f52 z%=dr){Xgf8m2J<=nD*GTSS&W9wI#WV-JfUI^eGQSD_ z$aSPva!sC3IW?{UEGdxF&A6nACRCp0peX5rs0)$|M9C60OE$Uck7wF^;#jMaORKur zTY}GoVZjoFVzC%6%5l%{6eQC$1rZ7m0)_xVZ$3mNkPqrd7?L!=zFP=gFV97c$o9I! z1kWt3xR5KE=DeufdtoZg>FAvR!*=$vouHZVZg9b4O_@~p%-Ml zaj0vbZ&AQPn@ZJmjA=_QS5py$%{|PGYCy(B2kG7d6;{!}>-I5i?qO!uN8S{yCZD3v z^V6P}9ZA&65tF&5Chp00H&47`uy7QdCQ(QeJQSe{L<2xMEoqjjSd#XX2rW^pKvN!Z zo!;A^N?Ot_Xjo9X1wguax*d$YAS>d!GP)#1PW0^U^_$yvSS#EgH4-gVuKL10u!oq*(hm6(TSny z1SmSPfhaO*q^b&#Wlh0YL_{*l_|hs;O-P6V3>9lYa}*O8rpXM@Riv9D#t53@OKU`h zng%siT#5lyQz0zCstl+?WfRJhEE8>fX(UJVHUcaFG9WsmKn_L#D>5>5s49jH$Ct)7 zRAq(8KvNV9Y*W#JAsHB$22(D|l7pc%f%ILJ#wh7>^ef2|CeCtsC|E6Sl=a?fwpP=% zwY+iVIbb0i&9?-PqsFF$uP(b|biv(KafP;+p^S8bmao79&k2jjr;VMgW9~3egz-!d zP|w7MQ=%$RlXRdPvI$g{Ct%yE3aI8dMAooFB{qb{F&v7Pq*!uoR{eLwtIRlq@|~1z zlmdV6uH4-u8q<}3&(i7i?8@Ip?W8P;iAs+?s4mcMJ@*pM0o~oBl)p#lK#)hj~YD|9O_sw{z;lCr<4@{@R<{k2wSB z)9LOn4-CD0^@_8ry(9bRLH?uj?%)~Yh`g-b`Dw?{CgHMv;ny2yd%a)M7q%`cZZ=xK ze|y`^;ZH8EJ=P$}bI$E$`#-(<`Eu^O{0eUlzI9?+=J?0&9k_4PyzQw!>Ibw_-@Utc!8aTGUl*RJyS#7b zjP>V#Cp+fMuba8z^sdd9*8iF5e8XreFp9_!`mm1 Q0QE35hjuH9vadPoEx>Q3p{k&hWt1hH)|2`D>)9Lx@~*wR)aVUG(SGhDfj@0a@J zl1MwqbQ}j67P2fI5PG&6OES>o?C2zd$1|FSi3~-o<`GFCmZhr!(@_agQiP4OahsrF zXh0HVmW;flA&}7mmn^5fVRA+h@9~JoG6SqxV3V$pBI}ARLKmMmB!JfXevxBSE>TpZ z$mGlllre6|rs%A-nsBthGqIdfKC=QekLJ}RxRG)qMcNXgHyPCYgtFRbsK+jZF%$jl zlb7J!Ba*wCWmpy6sOgs7GEHoo*s6VISlBQ|P+zdHy}B8+0u#13Kw)7Rk>rhFMsZuD z5rJ~Uh;%qg;*O>kO~eh+6DcGHcI6=p-^a~3lO(3R>A4zITna;mExT4_HY`f5Vz>2x z*fl)}NLjZ7+qT;dEe8Re_2y20$KP6)SjR%6wm;sjY<@LnhZgg7{->`h|PJG-_+9h#^gGeGR zF9qdtG8zbabymY~FG0E23GxeO zU?`h>)&j`O@N{tusfgQh?jR?Ff&lZu1)@usEv;|7-fymZkU{T>rNGU+Kl21!7KHO1 z@(vJf?45kX=OBmUN~41B;&Z+|o598)(7qXt17rY|XWt~;~4bI3L(yRlj5#wFR3RK@1(nX|iNcIHfH zW^X|uu0AMu0R>Y;O8a1?FX~HMq1a-fQXeD_)+YF3r4RK*qAiHNh-daPwh~(`4$RCs zv)}o?|NGB>{(U1qvwtZ1Sd?Lyq3m>KmcAG1{o;dz^qJmXc!9olIMatbhS~X0a7UOA zJ{)0~ftFcV@E39i03wzE4N}BH({d=9VaCRr4n#}XXN$OO+9_`H+y#y`jTASp<)oaG z#uamV*~N3qGX=D~gmi-&-^Y$M0VS}o580+wwLQ>GaUEVj$H8ZjV>=LkDaB0$g6u*r z&!&lsSxt!ZNLCd#p$l?c(iL?tt4MM}loFyG<0TnLaS+ql-o;Th*C>J6%+ozBI!kdC z-*mGRKBV1Zh$wE)}NH#X3>sav*DaKC}_eW>Oq=ESRPN za8XW{Ak=uQV~vlgD&~twQg~e}Ce^sEp;#RE*JlW-1^e1xZ~Vvg&$uQ%SFn2DJhcA* zcGzc`ppHDcabE9qOu}++R5kYnHiIw?T~#Cvm3U2;3?9m=#wQacorgsQ%7%gCs8k#i zgQGwfvE5OZNW8mZr$E898AQ

CWLJVFYZaCILjjSxa#!C^Bqtz0c--??{)nQNfh- zF6GySd8Fhwpo=HUbocI2++yE;*M;~`gPkPZ)<61i+LVG}*bV6CK)6}pIdov$Nt3P@cgx}2DP_NeVMUudd*`GaKHZc z-Pub=XQLMme0%-YrR3WutWEis#_HHl@biyG(B=7-@M`(`z~!?i&B-4cUnh4xyP~z= z#?{kRZ8Lgt{ruX@&d;8I?c#}Zw;s7t`{I=+ULM@2@7id;pI&=-?^@){qw9aZex$vg zua6#o>FAA*hwFudr&o{74PRS*XWN^Rft77fMUFFhW|SFbqRjs={rzy{kCSawIQaPC Y#qBq4*3RttH2Cw$PR(RKnOtoD1sggu#{d8T diff --git a/public/images/ui/legacy/text_images/es-MX/summary/summary_moves_descriptions_title_es-MX.png b/public/images/ui/legacy/text_images/es-MX/summary/summary_moves_descriptions_title_es-MX.png index ffcae31894d0e1ed67909908144200a707e7b342..3a4e3c7c375b17ab3a0154262c6a3b7e7668f453 100644 GIT binary patch delta 233 zcmbOz_>*yhL_G^L0|SHAL$?wj#UJ1k;+l|<@Z-mi|Ns9389;JX#u*);6mvkbc|KAbOW`^<;{D!h7%akDAP?T-; z12Th&UM6Nagb4>F4hte=WF;s_K!M^m1_D9mfy@gb4hSlc)R4kdek_gVXu8@Lf2XwGgh?HbP}o60$7XO%`p!_Yv0`b}(rhp)?zPHMSaa2=Xi^ zX<6B7p?a&7nOKZ@c)&0$t4G;wK0~O}6`HpUb1mJ=BZsu6Y3*+?8vOOByJ%!(;zW*sND7J^ z)KXzT$B;{fXrlE942uv$frL?2_)_BKQ7WyJ09L2&zZ0*d6lai`CUnW?*;|)#b2Iht z8Z5gi{9jT^OK`PCe4yVcfes=l7lQ z)?~+L3s3I4d!DKL=B+RPy#0@NclXQQ^qW_Vsm{jUoiDd`o$h|1#U9FivhT!}-=~K6 zy>o8sy7#{Q;pEi)*7+m9<}c2w>)#(ccch`Po?W;!`E}#fx#6D`X3s%DCPrRs*x0aX zgY@GUtC}WHOc(1H%?FR3`S795-BaQfajq*_w;vzt+JCz7gEh%Rzm4PiRozb%XWRS6 zRxbw8`Ck&JE*(F0^wpUcGcOH}Y?FkYXD^S>A1}^~o|VUDXJ-}aY!KUqvlAxD{x_ W0zI{-OLMJ(ECx?kKbLh*2~7Y^s!U)2 literal 2033 zcmb_dU5MON6pkWwU2QECto5OWl!~p*B)R#&>}=VcS-XQf>$;uNwGZOW&7GOBGr2Lz zZf3XsArviW1)sF|AP6me>5C|pLRUfXp#>@UQbDC4J``&eL`#vyo0&=M&vv^O14(X@ z@0|0UbH0;%cxrOb`oYHrIgVRj881(>yTs0cfpzTa9XTej+XKP)^AX2AxG6pF;f{Xz zD97FZuveRjXQ~qr({=&5)FFjVJ78#z8`|9o5S}G5?~sP)7lkXQmjvE(i^6`rDp!LN zX?o*}A=$e)S;LF7*mi~8!~9SOG6!uEBfisa`4Q|Cg|1)7*6C|W;Jc7`wkV9G0r{Eg z6knnt;q`(cVnqYIX%`ejwt=>b2eM*HvMDL5C@WAlplb8|LtxQDw+^SvPxoW7r=rk| z;{Zxhk|c#hEzqzbDYk7(GLQfe8A6N}{1|maKiZagD3b_>UJ!fK=Tnc!p>uIjU`%@{ zw1eEOAN9+`3MO?>ASneoO{oiXagGb-!d5r9izU(`ZQ{of!zwv8XwsNQO?n&G`R5w~ zu+mnmd5jggwA*=zC?1<5n+k8XvhLHc50VJwF)bK6eAy#O1UVo zj0>LULRmwqW2-=vP1g`@RWn4#HeC^6$Fxb^L;$#%eVO99w6B@H%j}VgF#-+}fuXCS zYigE=>KKVwQ;}@~U9%jJ**_b4Y_3r2uKh6m|C?dvWrSrEvBpLH=@><1chXz!Mb+`aZ9(O9-;ZwiK_rv7&w}!stOiWLj$74v?11Q&D9KD zCz@ktiQh_Ghq4M)JsXDqPP|`I+(dqZu%EUh+LIeEaKnm)~3-Jay{J6U&#rPd2~sGP(Btk5`W; zzkHHxSw8#u`4b->dG*f=a~rmP#a%l0_{A^wefHa#*LE2{TuDB%6zh>CW9aa`hxRNh voAhFR-Lr;#O%4LBiP>Er*@_wXoHdqgl<05TxbZWOwDu;oTE??X3p<^? zbsQA7#owF1U5o3`r!LHi_o_f+ z;oa?8x6bJ{ZYyU}SRv>5W$PA+dFH1kZ0cpqOLJ^O6Bzju4ny-lG|XnQgdA0XF0ax^sDM$UsrcON2`Mkn!@7Yc3N85~|R2kxrMd_{} zAT^vyQ!(4cloXCIh!c5AR>E8aD7@H4@qm+AAhVpn08RxFRZytvhbGZn-B1VOk5prk zyC|LWeMe>4VzC%53SryLvYetQEZ|w5X9$AvrYs+p7|Uy^d5B{Vxu)ZrwnYUVp=M9` zQJOHVNGLdUx0Y8;6G<3bf)2}tK_ICN)KMMhOt|@Sa2>HYj|J8!!rFkvT@S)@{G((-(0 zA)JgyX`(o6n!1X3NdpE#jG|zPQQAeaf|AAn17MDmL?k1vW*@iFM6j#kV&C={(gH|9$l`dOlEy&Q&MVUxzhXP}0VuVo)PGcYkF)#5Vl=;Z|))7vI5F(K= zAZIYT9Hc0sKnCj(AOS$K54csbve^&lRv^QX%Fph6V%d6Y&$ zfn%MnJ~r2=hRy9}#ceT%iPH^AzXJ1Y!!JS?cV$WC+@P`8^{9Jra%19H6XgitAtMNi z!f3ikYMB!iMiG&OMIK5B@-^XW5tjs26l;z6--%aKigM7(VlpsT`sSrsS`WE0De$e0S2_4!C{>Jt`QBug%tpCco^f05aHJ{j-IBtD-X z+S(L*VB0T;=2u$PA6`k1o&QGN`^6J44x_%I%+Pa3zJL5+=Z{ym%m(YJ@WJiOR=waY`bOW(!=6=PtN}I z#$w}Q^OeiTUuaYI?P$C6{kD>O*S9>HyE1d~(GP##lUr(Q_R@R7Cl5V8`1ah)h{Prr z_spTw)7r%|-M_uG5HI6LTAUyEc4gTe~DWM4f(1=DT literal 2059 zcmb_dPmI)57%wrdfV&Wc5Tiz#5|xd1Ui;c}Rp3P2eHl8(}(XsF5kmZ{s*Kvac&W!Ep1PITOm^De=@rvB#99i19*NsQQz+w>y6Q{=jKb-GR-1CH%N;<+MMN*uDY z#uQujL&D~B3Xeq@vZ|I76+wgY6D$-&6$mO2C0-D9LD40RO@AEq7FxDGU3oI~MemAS zBaQ-kRLK0FJXyFVOw8CcBxrG5~5|?;!M6qIq4H|yzM-BfLrnA>8 z9MIGnM&@H67uU^PMDh4M4I@p+Ky*}F2nd)ak>3h28K0*#cP7yUdO0L0_QRU*H~WQ} z>QiRRWp>v=$Fux)G^J?8I;QNTE(^oZt6mf%50h%8 z$kB*%j$`SfVnP*(7H{Q6nU_pi<4sg3@TQGSEI?b+u-e~W@o_7uYk$8*_bZZFK$5EP zf}~nJlx3Mm*i?8^lw}nYRkno8|NhXSeTABLY=`Rqp9uR_N;IN~7A{J=V=td<#ifs(0To$W zMtf%44ML5sBEKFi^6j{dLQ<;J>fJ`%;8xTlG`~7=l$W3_*t8Qc#Jmh;%xhE&yiAZO z5hU8l7c&ifCGn&gl%yF%vEK~;op@>)H;`8+^rH>9I}c@TT`{Nz1N!>^3Z*O0p0>ch zW!HUo(^tK(VLB?3Wj3SeO*Z9-N3#^t8TQekch1uJcdA+`)e2uiO2@sKzRR|Dr30oR~->a z@E|6LnoKF=`<<)0&B~GlIlMlrQHr@F+Ev}ws)NpIG<3=nr&Q1=im>{$9(30`HyZ!Y zL*fsbsZ2KN;QF9Lrf#(Nn~iEu*ZT(=-AP3oBmhF?>sf%FL1J8-bT5NDKpf5nz%S>^ VFr(m;+!g=;002ovPDHLkV1i-)V!Z$W literal 1976 zcmb_dU2GIp6dp+=1+BFBfIpaw(*%?Tb0q-MY4mt%{MmGk12U-JKa` zrrm9MFcOSP;!90UG^nWwF$B@5F-3W>5mOSdMqe5dAsT{FFj}KTq6YEK{-tR7kvPfB z+`03ebH4AKbMJjV)$?HE@;jC@4Aa=vndqhOG`$-dmeOZ-?eQdiEps~`^%>@-Tf_SX z=9N9SG0f7Ztn@%IknBdfQ;1;0$&g5?;8HZhY}{0Gu|7lsHbb(O9pmN?eax|z5###h zBuKh(lCwI;JkmGTlh((Ebj{#4-N$Y$A?lz&0?d{QdD};&7+3a-=s27fIJOK4hGJZM z7?2%Err5aS5mt^Uye>+RRkety01Zm(SqMZ`0IDEHc_1R7ps2=HFOEj@3={Px9;n8m zUokEh1TGSUVzC%0Mk9`w6+}(b1ONpHd5YluQ9HmT-u7E+9umaYJLL7~V}#16CS*~xpB{CI&`W%0*wabJ2&LH)s&P@=BRFup zwBzJ!g-X>Zv++24PrqdwPSL-20h}aoKw=yOQ09TkLoqFCNEQ*4TLDA>RG>-6u*}hg z&^8`QX;DR@fydauASxBdJ<9G$vYkmOxZ=tERspo zW%~iPb<&lHaa3`{vJ36b^8fdTi6h&)ADJlMyuXhnG>H*WFrbX+{PLBa6G+b zx3`1&Z2t9=Uu^j8)c22xz}(e*Mq9xwnV!{WF06j`mfQar_c!N;X1+W)urIZD>&4e5&y2j)bad+RCwg9fa&CP6 zT=(avyh#4Qw+CLBoti(sY-*qW;v<8nj~qMt$=Y}SdUN}Y-?cIqmcZT3=f^+l?hnt_ Nu8y9>+xKtV`8P7%k4pdm diff --git a/public/images/ui/legacy/text_images/es-MX/summary/summary_profile_passive_es-MX.png b/public/images/ui/legacy/text_images/es-MX/summary/summary_profile_passive_es-MX.png index 885453e3e9874c1a04fb25952324292a7f03692e..a50e3cacf580d1ed549af7117013779fdb86ae85 100644 GIT binary patch delta 177 zcmZ3*caU*{L_G^L0|P^{gTpf*#UJ1k;tHe_5)%IZ{|_NIGJgmI3Ne=i`2_>%Uu8$` z0jW4o7srr_IMbesf=rG)PM;H*-`s3i&Kjd4%JKL56SY2O`TPWhH?N<)e!6ApQeMR@ zn<(Ce!O0FUIR3Ag;~jlA@$$9J?klqR3|+Nq&&Armo4Q~B%!c32@Bb+|d`OsdmpicZI5Ty>-7*PN_JCXiCv;(%LWjL+41bI$=c)E zPIePo#?Eyh0geoN9fDjT_4l9KtQUyXiw3muY5!WDb=mAuf@jgrnX&SI(dp!Q; z`@jD&|Gc|Ues=u8BL@UQ7`Nw(i~Rirzeh*+^XCWtD_8hyESP^W5`=pXCHFnTCl?Z{6=C(RD1Ts+T}1DA1-m zzY)@DpbsrE$E~k#hvsFNt!M_)0D}43 zpuAul9OJu!9fImC^e}hV^>Bl#f!~nCG>E9cS}f#-xw=eGMX7{rKZ>zWs9m%q?y=&! z4yw_tsX+tEI?U*D#>fI0JNcXp>rgix0the1;rb#Yt4Usm>m9y6qmfL8)*ZQS(zRj<@*Y^{x=V=|GzyPvdnOgBA&QtusddPV=!vDQ;C~< zl{u*@A{w z+IEw2(p}204~tkmZsU;7Gta-3pZwKsW^QreSR&Idr# O89ZJ6T-G@yGywpZ*hyso literal 2030 zcmb_dU5MON6pjkIU2O~gKv@)SQ_&U8UO)m)aMVzG;O%R4Ae%+6qcNnLpb)-PU3t$xZUz zbH4BV|kFhx9TohXr2u zO6w6nus&aL*Oy(3iF+qdu5E*XCQm48HyeIzxAS7ht_|z-wJf3zBw5ajlc__rR9--Z zAmT{R7?P`KDl)B%Vqi)m9>z|i#!fiB9~9E0?j?Cn$RvpoF+k334%sHQ44*_ zs8B%ThdiGJt@yDk;Jio^o)=ZD>JrXMDygW%)-_wP9>=PUaTi(+m{(o92C7vQ(^dhb zUj>DNVKkvP1T)vJ22qoOT(3!MTn_!3hz3El3qd1@z%j5R9Tw%XJ?qB_^<6$&%!{Bn z<9W=s9Ac_jP9-G^)j}N2m7FRj5;2vmX^|??Fz@d#2JT9l*ZzJ6`!&sES;J(KW>QDO zSi=(4bxk6~&|DW&ra+sd@df|;BMy{+P&8;Nt8j1vsun1>{w7n#B`mk+7~{QxNZ}p-wgkqcrT=Go%%HnKW$mO zbtyO36+?6I@4o)Oq;$%&TU+wAW!L$3#b3Q&!!Rn+;M1%U)+$mu){*}=Oq`QAY)08UoeBivm0qZ zj*O>^V@O3@GDjh2VnIP+B8Ol>V#bSzgBuntW@OOc!MayliTNy09fPN< zkdPP{KnO7~FfcG6CKwo4;2*HUA7E$Vbv`T#<nL%kbKr%wNr5e}8^&Qyx7&1b|mzYgsOP=Lr*0i3Y;wr9qSc zT2W5T3=+y#xKw@K31e6L^5(6khJmYHsd(5+>bx7yZ>0R<#zKp2tS~3gW=^Z81_THq zE~z?*dT~Yut~TT)u+F!UrVb%;#nonWhw8GosMbZw)k?|MnNc>?s#7v-?3m?~s)>y% z!c}BgIyMNliRGx{O9O9d&?ZZbvtwWIy4QPUl63#;!0s~gylGU^~ zbRICodpzQ?%m8Z?*rY3@$hzVe)Wz>j4j{FjSNPacOB59@GC8*fVT=>9B|2-ZCmb#D zOsuAi&#i&Xqj@w5si&Muk+y{BO$xO*k*wD1>airP|01*#AZduHQlsY zhD|CJV%1JylVCi8dLjth>pwutEyE&Km6)5M&@cg&^pD_xk+w)93gLzk?QoRD9ZfBg zNa~^|Qg96Ftf30eBh5IIG-kZna5ady6ovsYOoLXcc2%d)h`K?`w(bO&=@r9oQ>TXg zT9r=h8-lIob)DD;U{BkeGMkq5nqxS+<>&U;ujqko2ORshNoj3je<2OwT+!Zt^W>*3 zZwN9UW>Bj2@p-t&Z;V&HP#yLFp)BuhS7Uimd7zEA!!0uKcM*(e`&|yCzbW?!mWj6P z)0EG4AX|TECiE-m8D0Cm@V_hFmcoB-zJC`!PATisxWi#cA}tRE<>O>j5cFfMhX1}B zJrg+0vwT?$QuruFF^_>dg+cp#U=YEPbi-Fpe9zoInf78)`>N3cf&brjj7PUFR-BTMm8w>s3v3CbWQM<}x)&#jraiQ%;1Wi#xBkc&HX`IqFu6u#WTt0J-p*_cBCgloOiAuQP zjm^jS$b7Yi=BJVFFeAhCP}?95e4IkM?KgwOXq!y#*C6X`n`P)6l1`gUF$+jfRmSO3 z7-L#4C;;)2K&yIzS2$ge9-;+~S6NPFc@c2D!6}BQ)7=k4qQ#DDOjw7yvB;CjG}1IO zShm$_6pIH`tRMh_0LfgC!Zrw!{XGvBPEhPcsTT%x<`LTAOlmTOX-9$| z_1y+ZH%%mAY#T-_U*IxHInY6UTr?9m^WY9*aTEJENE3qP``D-vreV?uH*np5eO&;O z+DfG#V_h!3-w%6Lwwr9fccWvsNHRoi1BtEkjus97hA1j1-`_k`6Ro zCoWVOsyc@d6!eX)x00e&);Wug2zGO|d2<+JUZ#Go%lZgD*5^;>{a`?p^|{HQtiZ9u zbho{As$nZsd-kmcCQdgg{Z8~Gbki1$aj{M+=LU_%ZbUtSvzrrl97WZ5*99VSMSxsM z2DS}dfOJh4FoGO8!i|ZOMpO+>?lt0nC*DmdYQUh5$-rQlo0qb+j_hz;a|ZwZP5Hm1 zW5$XJMDp$E5h^MZi=seHhlTdyNWMeed^VxmluyeH?d?D7A};3 zsNVY4fpdqJPEP)0RoNL_{M_Dq`oWPs%fEAHFTG|xelL4;Vc$2Ood4z8oi9)yT|B!2 T=C}LV|AX>q)ml3E%<;bgudBG? diff --git a/public/images/ui/legacy/text_images/es-MX/summary/summary_stats_stats_title_es-MX.png b/public/images/ui/legacy/text_images/es-MX/summary/summary_stats_stats_title_es-MX.png index 2233461522c4dfcf2279b5cae51cfff1dcbfc942..5b59c12984fa162071fcfdda9970f0814bc2fd6d 100644 GIT binary patch delta 292 zcmV+<0o(rM6SD%47=Ho-0001R(Y@$iKR@%V@T>>vJ^O3UiiTPqmyUpMdA-Ms!qMg8yY{lEUTxBp-N8pvhvTGX^ctEp>&hSsX4 qRV!98HLdDe1Rdlugkl9DxWoa=AB!X-iM>Mr0000!kKf1teO=f52 z%=dr){Xgf8m2J<=nD*GTSS&W9wI#WV-JfUI^eGQSD_ z$aSPva!sC3IW?{UEGdxF&A6nACRCp0peX5rs0)$|M9C60OE$Uck7wF^;#jMaORKur zTY}GoVZjoFVzC%6%5l%{6eQC$1rZ7m0)_xVZ$3mNkPqrd7?L!=zFP=gFV97c$o9I! z1kWt3xR5KE=DeufdtoZg>FAvR!*=$vouHZVZg9b4O_@~p%-Ml zaj0vbZ&AQPn@ZJmjA=_QS5py$%{|PGYCy(B2kG7d6;{!}>-I5i?qO!uN8S{yCZD3v z^V6P}9ZA&65tF&5Chp00H&47`uy7QdCQ(QeJQSe{L<2xMEoqjjSd#XX2rW^pKvN!Z zo!;A^N?Ot_Xjo9X1wguax*d$YAS>d!GP)#1PW0^U^_$yvSS#EgH4-gVuKL10u!oq*(hm6(TSny z1SmSPfhaO*q^b&#Wlh0YL_{*l_|hs;O-P6V3>9lYa}*O8rpXM@Riv9D#t53@OKU`h zng%siT#5lyQz0zCstl+?WfRJhEE8>fX(UJVHUcaFG9WsmKn_L#D>5>5s49jH$Ct)7 zRAq(8KvNV9Y*W#JAsHB$22(D|l7pc%f%ILJ#wh7>^ef2|CeCtsC|E6Sl=a?fwpP=% zwY+iVIbb0i&9?-PqsFF$uP(b|biv(KafP;+p^S8bmao79&k2jjr;VMgW9~3egz-!d zP|w7MQ=%$RlXRdPvI$g{Ct%yE3aI8dMAooFB{qb{F&v7Pq*!uoR{eLwtIRlq@|~1z zlmdV6uH4-u8q<}3&(i7i?8@Ip?W8P;iAs+?s4mcMJ@*pM0o~oBl)p#lK#)hj~YD|9O_sw{z;lCr<4@{@R<{k2wSB z)9LOn4-CD0^@_8ry(9bRLH?uj?%)~Yh`g-b`Dw?{CgHMv;ny2yd%a)M7q%`cZZ=xK ze|y`^;ZH8EJ=P$}bI$E$`#-(<`Eu^O{0eUlzI9?+=J?0&9k_4PyzQw!>Ibw_-@Utc!8aTGUl*RJyS#7b zjP>V#Cp+fMuba8z^sdd9*8iF5e-+V2pYQMQqou{`Bg5mv9LJ3m z7OZ79kFjxbaDe@HhX#^qC|tN5bKI%XWb|>*o}cBo6JNdJ8eOw*LWlSga!CnGO+RF4 zj+>rohR9jRlrQ0m7ns7g*KY*gb4_7Yvwca7O<^_>$gkN;e4a#@*QA{2C|Q-)4N1uXL(N|1RiNlH z&}Ahf0tJE`%ou$46PUEfEyHDNzAKCEOrc6?2xYm^Xh@BWM52nU7=|GORaRAzA;h>9 zP}CHIc%sK(;n<11ka{HG6Gl`bb!rMs(~b##n9>Gu*GZzL))lgM*5F%cVICo(*}z0Y=wF2h-8wP>T*(p9V_HN4Q$`-tJfZ!HGyQA zb|1D$`xg#a(~4Mk+Ieu}KI?3`f;C&59CLo|?alOWD?5Yw_m7{y?19r~-p)_F8{K`s sdv$#4>G>zWK#OYDx#166k1zD~b1&{}|M;ZSS4nd$%q?0kuibt48<8ltA^-pY diff --git a/public/images/ui/legacy/text_images/ja/battle_ui/overlay_exp_label_ja.png b/public/images/ui/legacy/text_images/ja/battle_ui/overlay_exp_label_ja.png index 40b5e8925a1b3dbfcb82560db7225ebbda8e3d88..acb04a84a31793070378d9cf958105fa8b104d28 100644 GIT binary patch delta 98 zcmey#RWd;$hKZShfniqqyQx5mGr%Xr^*<0iXE?UEm{*1QK2VUcB*-tA!Qt7BG$2RP x)5S5QA};v=dsxXDhCoIpMFp)RM;SdB8T3Cgrz)te^aUzn@O1TaS?83{1OQe$9`pbJ literal 1401 zcmbVMzfaUq952Bjpezy+Wok+^7;XF7yI$!F74Zsk#v*}Bz)9b=@9r9|eYJhZ-I3^E zSd5FV#y`Or7bXWy7~OQy#leL*xtOREzTS@p3`(TQ>-+V2pYQMQqou{`Bg5mv9LJ3m z7OZ79kFjxbaDe@HhX#^qC|tN5bKI%XWb|>*o}cBo6JNdJ8eOw*LWlSga!CnGO+RF4 zj+>rohR9jRlrQ0m7ns7g*KY*gb4_7Yvwca7O<^_>$gkN;e4a#@*QA{2C|Q-)4N1uXL(N|1RiNlH z&}Ahf0tJE`%ou$46PUEfEyHDNzAKCEOrc6?2xYm^Xh@BWM52nU7=|GORaRAzA;h>9 zP}CHIc%sK(;n<11ka{HG6Gl`bb!rMs(~b##n9>Gu*GZzL))lgM*5F%cVICo(*}z0Y=wF2h-8wP>T*(p9V_HN4Q$`-tJfZ!HGyQA zb|1D$`xg#a(~4Mk+Ieu}KI?3`f;C&59CLo|?alOWD?5Yw_m7{y?19r~-p)_F8{K`s sdv$#4>G>zWK#OYDx#166k1zD~b1&{}|M;ZSS4nd$%q?0kuibt48<8ltA^-pY diff --git a/public/images/ui/legacy/text_images/ja/summary/summary_stats_expbar_title_ja.png b/public/images/ui/legacy/text_images/ja/summary/summary_stats_expbar_title_ja.png index e9dfb10e5d62cbd9e2c24bef5d40012719e0a292..da999975932c33cd88790ae41711c3d9f5d39f5d 100644 GIT binary patch delta 101 zcmX@WQ$9f=o{5=(f#KbWe@B57XMj(L>;M1%U)+$mu){*}=Oq`QAY)08UoeBivm0qZ zj*O>^V@O3@GDjh2VnIP+B8Ol>V#bSzgBuntW@OOc!MayliTNy09fPN< zkdPP{KnO7~FfcG6CKwo4;2*HUA7E$Vbv`T#<nL%kbKr%wNr5e}8^&Qyx7&1b|mzYgsOP=Lr*0i3Y;wr9qSc zT2W5T3=+y#xKw@K31e6L^5(6khJmYHsd(5+>bx7yZ>0R<#zKp2tS~3gW=^Z81_THq zE~z?*dT~Yut~TT)u+F!UrVb%;#nonWhw8GosMbZw)k?|MnNc>?s#7v-?3m?~s)>y% z!c}BgIyMNliRGx{O9O9d&?ZZbvtwWIy4QPUl63#;!0s~gylGU^~ zbRICodpzQ?%m8Z?*rY3@$hzVe)Wz>j4j{FjSNPacOB59@GC8*fVT=>9B|2-ZCmb#D zOsuAi&#i&Xqj@w5si&Muk+y{BO$xO*k*wD1>airP|01*#AZduHQlsY zhD|CJV%1JylVCi8dLjth>pwutEyE&Km6)5M&@cg&^pD_xk+w)93gLzk?QoRD9ZfBg zNa~^|Qg96Ftf30eBh5IIG-kZna5ady6ovsYOoLXcc2%d)h`K?`w(bO&=@r9oQ>TXg zT9r=h8-lIob)DD;U{BkeGMkq5nqxS+<>&U;ujqko2ORshNoj3je<2OwT+!Zt^W>*3 zZwN9UW>Bj2@p-t&Z;V&HP#yLFp)BuhS7Uimd7zEA!!0uKcM*(e`&|yCzbW?!mWj6P z)0EG4AX|TECiE-m8D0Cm@V_hFmcoB-zJC`!PATisxWi#cA}tRE<>O>j5cFfMhX1}B zJrg+0vwT?$QuruFF^_>dg+cp#U=YEPbi-Fpe9zoInf78)-+V2pYQMQqou{`Bg5mv9LJ3m z7OZ79kFjxbaDe@HhX#^qC|tN5bKI%XWb|>*o}cBo6JNdJ8eOw*LWlSga!CnGO+RF4 zj+>rohR9jRlrQ0m7ns7g*KY*gb4_7Yvwca7O<^_>$gkN;e4a#@*QA{2C|Q-)4N1uXL(N|1RiNlH z&}Ahf0tJE`%ou$46PUEfEyHDNzAKCEOrc6?2xYm^Xh@BWM52nU7=|GORaRAzA;h>9 zP}CHIc%sK(;n<11ka{HG6Gl`bb!rMs(~b##n9>Gu*GZzL))lgM*5F%cVICo(*}z0Y=wF2h-8wP>T*(p9V_HN4Q$`-tJfZ!HGyQA zb|1D$`xg#a(~4Mk+Ieu}KI?3`f;C&59CLo|?alOWD?5Yw_m7{y?19r~-p)_F8{K`s sdv$#4>G>zWK#OYDx#166k1zD~b1&{}|M;ZSS4nd$%q?0kuibt48<8ltA^-pY diff --git a/public/images/ui/legacy/text_images/ko/summary/summary_stats_expbar_title_ko.png b/public/images/ui/legacy/text_images/ko/summary/summary_stats_expbar_title_ko.png index e9dfb10e5d62cbd9e2c24bef5d40012719e0a292..da999975932c33cd88790ae41711c3d9f5d39f5d 100644 GIT binary patch delta 101 zcmX@WQ$9f=o{5=(f#KbWe@B57XMj(L>;M1%U)+$mu){*}=Oq`QAY)08UoeBivm0qZ zj*O>^V@O3@GDjh2VnIP+B8Ol>V#bSzgBuntW@OOc!MayliTNy09fPN< zkdPP{KnO7~FfcG6CKwo4;2*HUA7E$Vbv`T#<nL%kbKr%wNr5e}8^&Qyx7&1b|mzYgsOP=Lr*0i3Y;wr9qSc zT2W5T3=+y#xKw@K31e6L^5(6khJmYHsd(5+>bx7yZ>0R<#zKp2tS~3gW=^Z81_THq zE~z?*dT~Yut~TT)u+F!UrVb%;#nonWhw8GosMbZw)k?|MnNc>?s#7v-?3m?~s)>y% z!c}BgIyMNliRGx{O9O9d&?ZZbvtwWIy4QPUl63#;!0s~gylGU^~ zbRICodpzQ?%m8Z?*rY3@$hzVe)Wz>j4j{FjSNPacOB59@GC8*fVT=>9B|2-ZCmb#D zOsuAi&#i&Xqj@w5si&Muk+y{BO$xO*k*wD1>airP|01*#AZduHQlsY zhD|CJV%1JylVCi8dLjth>pwutEyE&Km6)5M&@cg&^pD_xk+w)93gLzk?QoRD9ZfBg zNa~^|Qg96Ftf30eBh5IIG-kZna5ady6ovsYOoLXcc2%d)h`K?`w(bO&=@r9oQ>TXg zT9r=h8-lIob)DD;U{BkeGMkq5nqxS+<>&U;ujqko2ORshNoj3je<2OwT+!Zt^W>*3 zZwN9UW>Bj2@p-t&Z;V&HP#yLFp)BuhS7Uimd7zEA!!0uKcM*(e`&|yCzbW?!mWj6P z)0EG4AX|TECiE-m8D0Cm@V_hFmcoB-zJC`!PATisxWi#cA}tRE<>O>j5cFfMhX1}B zJrg+0vwT?$QuruFF^_>dg+cp#U=YEPbi-Fpe9zoInf78)0E23GxeO zU?`h>)&j`O@N{tusfgQh?jR?Ff&lZu1)@usEv;|7-fymZkU{T>rNGU+Kl21!7KHO1 z@(vJf?45kX=OBmUN~41B;&Z+|o598)(7qXt17rY|XWt~;~4bI3L(yRlj5#wFR3RK@1(nX|iNcIHfH zW^X|uu0AMu0R>Y;O8a1?FX~HMq1a-fQXeD_)+YF3r4RK*qAiHNh-daPwh~(`4$RCs zv)}o?|NGB>{(U1qvwtZ1Sd?Lyq3m>KmcAG1{o;dz^qJmXc!9olIMatbhS~X0a7UOA zJ{)0~ftFcV@E39i03wzE4N}BH({d=9VaCRr4n#}XXN$OO+9_`H+y#y`jTASp<)oaG z#uamV*~N3qGX=D~gmi-&-^Y$M0VS}o580+wwLQ>GaUEVj$H8ZjV>=LkDaB0$g6u*r z&!&lsSxt!ZNLCd#p$l?c(iL?tt4MM}loFyG<0TnLaS+ql-o;Th*C>J6%+ozBI!kdC z-*mGRKBV1Zh$wE)}NH#X3>sav*DaKC}_eW>Oq=ESRPN za8XW{Ak=uQV~vlgD&~twQg~e}Ce^sEp;#RE*JlW-1^e1xZ~Vvg&$uQ%SFn2DJhcA* zcGzc`ppHDcabE9qOu}++R5kYnHiIw?T~#Cvm3U2;3?9m=#wQacorgsQ%7%gCs8k#i zgQGwfvE5OZNW8mZr$E898AQ

CWLJVFYZaCILjjSxa#!C^Bqtz0c--??{)nQNfh- zF6GySd8Fhwpo=HUbocI2++yE;*M;~`gPkPZ)<61i+LVG}*bV6CK)6}pIdov$Nt3P@cgx}2DP_NeVMUudd*`GaKHZc z-Pub=XQLMme0%-YrR3WutWEis#_HHl@biyG(B=7-@M`(`z~!?i&B-4cUnh4xyP~z= z#?{kRZ8Lgt{ruX@&d;8I?c#}Zw;s7t`{I=+ULM@2@7id;pI&=-?^@){qw9aZex$vg zua6#o>FAA*hwFudr&o{74PRS*XWN^Rft77fMUFFhW|SFbqRjs={rzy{kCSawIQaPC Y#qBq4*3RttH2Cw$PR(RKnOtoD1sggu#{d8T diff --git a/public/images/ui/legacy/text_images/pt-BR/summary/summary_stats_expbar_title_pt-BR.png b/public/images/ui/legacy/text_images/pt-BR/summary/summary_stats_expbar_title_pt-BR.png index e9dfb10e5d62cbd9e2c24bef5d40012719e0a292..da999975932c33cd88790ae41711c3d9f5d39f5d 100644 GIT binary patch delta 101 zcmX@WQ$9f=o{5=(f#KbWe@B57XMj(L>;M1%U)+$mu){*}=Oq`QAY)08UoeBivm0qZ zj*O>^V@O3@GDjh2VnIP+B8Ol>V#bSzgBuntW@OOc!MayliTNy09fPN< zkdPP{KnO7~FfcG6CKwo4;2*HUA7E$Vbv`T#<nL%kbKr%wNr5e}8^&Qyx7&1b|mzYgsOP=Lr*0i3Y;wr9qSc zT2W5T3=+y#xKw@K31e6L^5(6khJmYHsd(5+>bx7yZ>0R<#zKp2tS~3gW=^Z81_THq zE~z?*dT~Yut~TT)u+F!UrVb%;#nonWhw8GosMbZw)k?|MnNc>?s#7v-?3m?~s)>y% z!c}BgIyMNliRGx{O9O9d&?ZZbvtwWIy4QPUl63#;!0s~gylGU^~ zbRICodpzQ?%m8Z?*rY3@$hzVe)Wz>j4j{FjSNPacOB59@GC8*fVT=>9B|2-ZCmb#D zOsuAi&#i&Xqj@w5si&Muk+y{BO$xO*k*wD1>airP|01*#AZduHQlsY zhD|CJV%1JylVCi8dLjth>pwutEyE&Km6)5M&@cg&^pD_xk+w)93gLzk?QoRD9ZfBg zNa~^|Qg96Ftf30eBh5IIG-kZna5ady6ovsYOoLXcc2%d)h`K?`w(bO&=@r9oQ>TXg zT9r=h8-lIob)DD;U{BkeGMkq5nqxS+<>&U;ujqko2ORshNoj3je<2OwT+!Zt^W>*3 zZwN9UW>Bj2@p-t&Z;V&HP#yLFp)BuhS7Uimd7zEA!!0uKcM*(e`&|yCzbW?!mWj6P z)0EG4AX|TECiE-m8D0Cm@V_hFmcoB-zJC`!PATisxWi#cA}tRE<>O>j5cFfMhX1}B zJrg+0vwT?$QuruFF^_>dg+cp#U=YEPbi-Fpe9zoInf78)-+V2pYQMQqou{`Bg5mv9LJ3m z7OZ79kFjxbaDe@HhX#^qC|tN5bKI%XWb|>*o}cBo6JNdJ8eOw*LWlSga!CnGO+RF4 zj+>rohR9jRlrQ0m7ns7g*KY*gb4_7Yvwca7O<^_>$gkN;e4a#@*QA{2C|Q-)4N1uXL(N|1RiNlH z&}Ahf0tJE`%ou$46PUEfEyHDNzAKCEOrc6?2xYm^Xh@BWM52nU7=|GORaRAzA;h>9 zP}CHIc%sK(;n<11ka{HG6Gl`bb!rMs(~b##n9>Gu*GZzL))lgM*5F%cVICo(*}z0Y=wF2h-8wP>T*(p9V_HN4Q$`-tJfZ!HGyQA zb|1D$`xg#a(~4Mk+Ieu}KI?3`f;C&59CLo|?alOWD?5Yw_m7{y?19r~-p)_F8{K`s sdv$#4>G>zWK#OYDx#166k1zD~b1&{}|M;ZSS4nd$%q?0kuibt48<8ltA^-pY diff --git a/public/images/ui/legacy/text_images/ro/summary/summary_stats_expbar_title_ro.png b/public/images/ui/legacy/text_images/ro/summary/summary_stats_expbar_title_ro.png index e9dfb10e5d62cbd9e2c24bef5d40012719e0a292..da999975932c33cd88790ae41711c3d9f5d39f5d 100644 GIT binary patch delta 101 zcmX@WQ$9f=o{5=(f#KbWe@B57XMj(L>;M1%U)+$mu){*}=Oq`QAY)08UoeBivm0qZ zj*O>^V@O3@GDjh2VnIP+B8Ol>V#bSzgBuntW@OOc!MayliTNy09fPN< zkdPP{KnO7~FfcG6CKwo4;2*HUA7E$Vbv`T#<nL%kbKr%wNr5e}8^&Qyx7&1b|mzYgsOP=Lr*0i3Y;wr9qSc zT2W5T3=+y#xKw@K31e6L^5(6khJmYHsd(5+>bx7yZ>0R<#zKp2tS~3gW=^Z81_THq zE~z?*dT~Yut~TT)u+F!UrVb%;#nonWhw8GosMbZw)k?|MnNc>?s#7v-?3m?~s)>y% z!c}BgIyMNliRGx{O9O9d&?ZZbvtwWIy4QPUl63#;!0s~gylGU^~ zbRICodpzQ?%m8Z?*rY3@$hzVe)Wz>j4j{FjSNPacOB59@GC8*fVT=>9B|2-ZCmb#D zOsuAi&#i&Xqj@w5si&Muk+y{BO$xO*k*wD1>airP|01*#AZduHQlsY zhD|CJV%1JylVCi8dLjth>pwutEyE&Km6)5M&@cg&^pD_xk+w)93gLzk?QoRD9ZfBg zNa~^|Qg96Ftf30eBh5IIG-kZna5ady6ovsYOoLXcc2%d)h`K?`w(bO&=@r9oQ>TXg zT9r=h8-lIob)DD;U{BkeGMkq5nqxS+<>&U;ujqko2ORshNoj3je<2OwT+!Zt^W>*3 zZwN9UW>Bj2@p-t&Z;V&HP#yLFp)BuhS7Uimd7zEA!!0uKcM*(e`&|yCzbW?!mWj6P z)0EG4AX|TECiE-m8D0Cm@V_hFmcoB-zJC`!PATisxWi#cA}tRE<>O>j5cFfMhX1}B zJrg+0vwT?$QuruFF^_>dg+cp#U=YEPbi-Fpe9zoInf78)-+V2pYQMQqou{`Bg5mv9LJ3m z7OZ79kFjxbaDe@HhX#^qC|tN5bKI%XWb|>*o}cBo6JNdJ8eOw*LWlSga!CnGO+RF4 zj+>rohR9jRlrQ0m7ns7g*KY*gb4_7Yvwca7O<^_>$gkN;e4a#@*QA{2C|Q-)4N1uXL(N|1RiNlH z&}Ahf0tJE`%ou$46PUEfEyHDNzAKCEOrc6?2xYm^Xh@BWM52nU7=|GORaRAzA;h>9 zP}CHIc%sK(;n<11ka{HG6Gl`bb!rMs(~b##n9>Gu*GZzL))lgM*5F%cVICo(*}z0Y=wF2h-8wP>T*(p9V_HN4Q$`-tJfZ!HGyQA zb|1D$`xg#a(~4Mk+Ieu}KI?3`f;C&59CLo|?alOWD?5Yw_m7{y?19r~-p)_F8{K`s sdv$#4>G>zWK#OYDx#166k1zD~b1&{}|M;ZSS4nd$%q?0kuibt48<8ltA^-pY diff --git a/public/images/ui/legacy/text_images/tl/summary/summary_stats_expbar_title.png b/public/images/ui/legacy/text_images/tl/summary/summary_stats_expbar_title.png index e9dfb10e5d62cbd9e2c24bef5d40012719e0a292..da999975932c33cd88790ae41711c3d9f5d39f5d 100644 GIT binary patch delta 101 zcmX@WQ$9f=o{5=(f#KbWe@B57XMj(L>;M1%U)+$mu){*}=Oq`QAY)08UoeBivm0qZ zj*O>^V@O3@GDjh2VnIP+B8Ol>V#bSzgBuntW@OOc!MayliTNy09fPN< zkdPP{KnO7~FfcG6CKwo4;2*HUA7E$Vbv`T#<nL%kbKr%wNr5e}8^&Qyx7&1b|mzYgsOP=Lr*0i3Y;wr9qSc zT2W5T3=+y#xKw@K31e6L^5(6khJmYHsd(5+>bx7yZ>0R<#zKp2tS~3gW=^Z81_THq zE~z?*dT~Yut~TT)u+F!UrVb%;#nonWhw8GosMbZw)k?|MnNc>?s#7v-?3m?~s)>y% z!c}BgIyMNliRGx{O9O9d&?ZZbvtwWIy4QPUl63#;!0s~gylGU^~ zbRICodpzQ?%m8Z?*rY3@$hzVe)Wz>j4j{FjSNPacOB59@GC8*fVT=>9B|2-ZCmb#D zOsuAi&#i&Xqj@w5si&Muk+y{BO$xO*k*wD1>airP|01*#AZduHQlsY zhD|CJV%1JylVCi8dLjth>pwutEyE&Km6)5M&@cg&^pD_xk+w)93gLzk?QoRD9ZfBg zNa~^|Qg96Ftf30eBh5IIG-kZna5ady6ovsYOoLXcc2%d)h`K?`w(bO&=@r9oQ>TXg zT9r=h8-lIob)DD;U{BkeGMkq5nqxS+<>&U;ujqko2ORshNoj3je<2OwT+!Zt^W>*3 zZwN9UW>Bj2@p-t&Z;V&HP#yLFp)BuhS7Uimd7zEA!!0uKcM*(e`&|yCzbW?!mWj6P z)0EG4AX|TECiE-m8D0Cm@V_hFmcoB-zJC`!PATisxWi#cA}tRE<>O>j5cFfMhX1}B zJrg+0vwT?$QuruFF^_>dg+cp#U=YEPbi-Fpe9zoInf78);M1%U)+$mu){*}=Oq`QAY)08UoeBivm0qZ zj*O>^V@O3@GDjh2VnIP+B8Ol>V#bSzgBuntW@OOc!MayliTNy09fPN< zkdPP{KnO7~FfcG6CKwo4;2*HUA7E$Vbv`T#<nL%kbKr%wNr5e}8^&Qyx7&1b|mzYgsOP=Lr*0i3Y;wr9qSc zT2W5T3=+y#xKw@K31e6L^5(6khJmYHsd(5+>bx7yZ>0R<#zKp2tS~3gW=^Z81_THq zE~z?*dT~Yut~TT)u+F!UrVb%;#nonWhw8GosMbZw)k?|MnNc>?s#7v-?3m?~s)>y% z!c}BgIyMNliRGx{O9O9d&?ZZbvtwWIy4QPUl63#;!0s~gylGU^~ zbRICodpzQ?%m8Z?*rY3@$hzVe)Wz>j4j{FjSNPacOB59@GC8*fVT=>9B|2-ZCmb#D zOsuAi&#i&Xqj@w5si&Muk+y{BO$xO*k*wD1>airP|01*#AZduHQlsY zhD|CJV%1JylVCi8dLjth>pwutEyE&Km6)5M&@cg&^pD_xk+w)93gLzk?QoRD9ZfBg zNa~^|Qg96Ftf30eBh5IIG-kZna5ady6ovsYOoLXcc2%d)h`K?`w(bO&=@r9oQ>TXg zT9r=h8-lIob)DD;U{BkeGMkq5nqxS+<>&U;ujqko2ORshNoj3je<2OwT+!Zt^W>*3 zZwN9UW>Bj2@p-t&Z;V&HP#yLFp)BuhS7Uimd7zEA!!0uKcM*(e`&|yCzbW?!mWj6P z)0EG4AX|TECiE-m8D0Cm@V_hFmcoB-zJC`!PATisxWi#cA}tRE<>O>j5cFfMhX1}B zJrg+0vwT?$QuruFF^_>dg+cp#U=YEPbi-Fpe9zoInf78)-+V2pYQMQqou{`Bg5mv9LJ3m z7OZ79kFjxbaDe@HhX#^qC|tN5bKI%XWb|>*o}cBo6JNdJ8eOw*LWlSga!CnGO+RF4 zj+>rohR9jRlrQ0m7ns7g*KY*gb4_7Yvwca7O<^_>$gkN;e4a#@*QA{2C|Q-)4N1uXL(N|1RiNlH z&}Ahf0tJE`%ou$46PUEfEyHDNzAKCEOrc6?2xYm^Xh@BWM52nU7=|GORaRAzA;h>9 zP}CHIc%sK(;n<11ka{HG6Gl`bb!rMs(~b##n9>Gu*GZzL))lgM*5F%cVICo(*}z0Y=wF2h-8wP>T*(p9V_HN4Q$`-tJfZ!HGyQA zb|1D$`xg#a(~4Mk+Ieu}KI?3`f;C&59CLo|?alOWD?5Yw_m7{y?19r~-p)_F8{K`s sdv$#4>G>zWK#OYDx#166k1zD~b1&{}|M;ZSS4nd$%q?0kuibt48<8ltA^-pY diff --git a/public/images/ui/legacy/text_images/zh-CN/summary/summary_dexnb_label_zh-CN.png b/public/images/ui/legacy/text_images/zh-CN/summary/summary_dexnb_label_zh-CN.png index eab90a91c7fc8dd67b9cf24a7e8c37dffd62b2d8..bf568c486aac5e8af4e95a237819caf922a9e50c 100644 GIT binary patch delta 106 zcmX@ZQ!_y#g^8Jgfno2IHdP?S8Q>G*`v3p`A3uI1BqZFKcTEW>$XF8O7tG-B>_!@p zqv+}47*Y|J?BS7+(82KV3QOCTEi7$53Vtk)R21qDu3}}VtK-z%ysM-MsF=ai)z4*} HQ$iB}`|BmX literal 1612 zcmbVMJB-{!7&Zl`a8XbYLOM~690`G7c4q8-oJaA6WA|WJ!N+k~DTkoQcxJq7-P&X9 zx!b)HH0T;iB4`i=Qc4h_KnRI~K!cEIXsJMHsEFtgO&IUP%Hu8xSh79#eEDonv;6e3Wm#u>-Ojdom+Mofmdx||Kk3)z^;p(@qp+;gPnP#1*2kZ$Sk|Lo#r++< z6TC{fN@`4~P}U|%X3&;({=y_j)PLb!#93o)U%NnH=jFWW@RMMSqdFVAC{FW(+&ol%6w<8f{5)>J+~ z#Pd9a9ppIBAYgHjYBqssac<$EBMY9#nT}NoN{=j5d)l`R(^(2hR=G`!d6`VX(1c}( z)Nq;76exIw%l7ioG`QeMj$|TJT^KB>u-Q;)RSeY$uB+`k0+`YUK^5azE=f{_DD=j@ zNn>7+W6?$bAd_fY7HTi&a%11nJXdCu(RMDGR(W5k(V|gX3(BC~2G3rOQ=!JibGN}| zhiU2C4t5$4uR^EZCxq4sbylCp4#ju|4U~wZgL|P4bqFQS9Z)kcf@$_Zu;4UOdBRM( zal!@?W$C~MRS{`hjZ|&|8+PuLMG(+lT41}Nj@Z2LAZn<@>1+3)z63Orl}GyCyS z8mISZ_Y793NRL@A*9WFr4;UtN67_=Z-=FxyweBS1cMacvCq6GJAF^~HjYuN9Oa+y4 zGHVFByI0e_+oWd!H+ohsDl@!a=xXz8x{`&5RZyAwZ>vh)qDe)!{^m8FZ|;?>X3KJ)mEYu(?j{l5G`@cn0JBl!CAFHe2?%@Yy)d5qWSlJ(`A WfBw`TT)k52eQ#s4^TkV7-uoA*z5f^h diff --git a/public/images/ui/legacy/text_images/zh-CN/summary/summary_moves_descriptions_title_zh-CN.png b/public/images/ui/legacy/text_images/zh-CN/summary/summary_moves_descriptions_title_zh-CN.png index 3d2b4d083760bfed09c6fae182fb0a997420f282..e83e8cafbfcc09a3761b9d915f3c0f0205109579 100644 GIT binary patch delta 219 zcmew>@S1UgL_G^L0|Udt0+u8o#UJ1k;+l|<@Z-mi|Ns9389;JX#u*);6mv@JZ!+jay6~7t*qfntizYh4ms& z?VDfRyXn1dOPhm}$?=zuzWYlXm|asivL|;gD`Uu@lRJ(3&GNQSdOPmq zObx5dI(>ife9ZpI@-s@b8fx~JIF2VHs#uB=gC2r7U_u05*FON(mZ}fv*moA TSG>1@j$-h1^>bP0l+XkK`;cKC literal 2173 zcmb_eU2NM_6iy{zTeraj0(DeqmisWK;n==$?AU5r+N510HC@*Rtz%o)>+9>pYGS*# z)1-YWAW=YTi%_5FC=!)GeE?|%A*O-WZ5k6oJOE+{_JV{E51UqvfrJ=;*ZCuZt{W4P z9A965=bZ2S-QySf`}S^ce55fD2y9RE#0SW|mt31S-$(ujhj;8Jw=H(hK{pWC`k;R` z1YSAb6bNie8mS>~DA}tZD;I>CrQ%>QXA^WF(Ar+KAsWRVrQ)<@pWfyXGO|9ux!bAeb7NO1ZLC$xn1k_LsXXl-4$+(7CoW(h8dM?58HLRVnJj=~k zw@_VseUkwa+GMh3V_hz}T+M{*b&V4@Dgjv+?WQJd%no4J$~y>mjT4$JzBjfKb1?KQ zCuLdLYNGn9l&M&ZdhCE&RYCN0g- zC+eXQ1_&u$Qo=wHA~!)v!D!Hfw*_lR(JdzjiMvJ)rZH=qX_~49QHfbu%OQpdyHJTm zGN~j?*Mla)iFlMI9tRCWQxH%EK@}s6$V(Cv!2)Jf4G0WUMJa+rBuY@O%Ev8~_w!nn z*N8k3gAgKt(IKxhnk0l6Rg@uvHIWN*9H%3WugdRp4ANIHd)InM{r~M@RZ9o)$R&w$ zE8Wov)0I`$X!G6lX#xWcBDg$|lK^O>70%DzXNl%#uyD{HVc`H4J&wrv~cMy-&7*M5(Rv_{_ zHA*ADM6pI!wvBc2d1v`3t1VrZCZo96?GSmZ%iCZ5iX*V z$^)7j_G1hfHADifM?hEr2=F)ztB%(bFSkNN=Q)#nrTdqe3rg@SMIE* z{+)w$Plf->l~PfbYm5D7*_Gba%>nQNi5_3N{KQ91FVBDV#ZwDEo?wrz9eKC- z$?&JAn|FShSepwzw0iZPeGTugXy3Km*!|(Jk3RkTPOd9`YGhj9^k#2!;p)SC=9bZ7 z=LZYZm*2VAGG)x`uXX3=8v~=q#v9JH77ugR?FTM2b*~J4KeI^P*!Jy_)s<@tQy-5# zcd+GT;IqaPR}N>@`QaI7YV3OC=il0YnwgzFHuv=#=g)SZy)@H&@6xNPmij>)`DW(X p>G98Z%|>6m_Qz5}Ji5HNb?=qecdQ;dzsvtXNObkZ-|ifF<}ZAE%%=bV diff --git a/public/images/ui/legacy/text_images/zh-CN/summary/summary_moves_effect_title_zh-CN.png b/public/images/ui/legacy/text_images/zh-CN/summary/summary_moves_effect_title_zh-CN.png index 55fb0efd832da789da351d76a5e3c7eea57b224e..fbbaac0b260570166eb5a51383c843ca90975205 100644 GIT binary patch delta 171 zcmX@dx0`W-L_HHT0|SG|1HX+xiYLG)#P$FG{|N~R5Hi32$W5R)Q%R6tFatx`?PJj1B) SP+KFQMGT&W&Q^Io}QlK&)~@05`WFa3kOm`*mZYt-zL2A<{m+q zdOm2bW~<(lh_JANeb%CtJd8P75N7A{7?U-ci7na*B1gLL(I=7^_>Q#S@Dwku(QdG? znb4KZ#U|NYBepNiJtEHLh#LrLhQ&PWMJdW1X=oSmaWM@faR|xQ9BICAD6V=-VvQwK zG%BV{R2_--Xu{=6Ut((CO)t~ zhSIpZ>knDAkx*#puAvw zoZ%aSeS+F72{D%>$XN9CakI3?izr9!t1m&Q2S$@I2Ir(hNt0He5&IiYr0jBjOWE z+nhcnRFMr|*X62ZSh8iPW=l7HwPhQV^)4g*BCnJ6K3|VDLNIJ$8JdPB`<7mnahqV7 z=o+>yXz0}zoUDH;3HZLky<4_Z?6#sH&_a;%RBep+!wNk(9`yooSOW-?qP8806iKDN zG@cH&iNMV%7?~Y(DK~nZbdO*uYi9$T(D@FJ)(wgYT#I^&*RM}pgSbjyOP4iGC9-Df zP_FV^$ZcEqb<;4Y-m)jcuO@CFMMIh~DZc+sd>m5J#ZiayLlQ{0F6HK`aN8VQy^sDc zDWEtRN3{j6EW6>qOHNUq#NiY_nu}rC6Zl8jRcOScl<=PM*4gh)@qW{9xbw~GSMMKv z`i@V<>DtyeKmK#q?`t!)J;%DA-G1l&ubPj#rw{$|+R^zVhtKc*@LZ#E@RiQr$MauL zd@*VAxx1)=2^v32@d6!;b-nJ);%G$21~PVuE6 zqP&*N10t)C*NvQq4a|kS2WIJfO{A4EoKLJ(OuoA~#Ijz~@ts*a{Ysg20%zC3s=x z){i@BVhIzQI27faluBv=ZIa=_g{a;NZWEE#sYm@JW>`7HhP5CG;##nU>+JJ20a$8H zGmEh)7thN=#L37alhH}Yrs%k`6jE`T#=$~F=*S|Yxj$7CqEbY05=50CsCNrB)uqgr zO8kKt*SCX4{ODD1TE+=22vC9=kOlx$E3$+X1!>yj5=4^JhMIxxI!phC>PUk~QLllr zhOu#iZwR&ta)QXiOs?zUDiuS&D)3nnQ7Nbg5ev-NDJ>SJiN^gn!9Jnm<$}Nz=UmrD zwxT%DQV38Tf`Nfm1z6CrK;FT`G6rlNYEIX_9FT>yu3dYZ*+Z!43bAbf9YY61O6_%- z0?DA%&}GA+7j49NhiglPD64Oy&MtpnKb|h{evDKhx%uN|Ab>beO&t(kwR!<^K z(pf7AByBQk3!T^QCKb84jJDl2YLq$Mrv2KmIB=2%j%cyUs&|9t7Pq1v)WpN?W(_ew};tQkuI}6ialhz9Q9e2BDRNlqp#%I{`b;&xmX!^ z`K>cMdY{gT7xrKhbyQ*s-5hR{z{{>6@m@8{0m^Y9z7w0im7ed)G5 NJ~CN;@6hq*{|0a5ki7r^ diff --git a/public/images/ui/legacy/text_images/zh-CN/summary/summary_profile_ability_zh-CN.png b/public/images/ui/legacy/text_images/zh-CN/summary/summary_profile_ability_zh-CN.png index 6600db26802ed202ae0536dd122605998b551fdf..a05c22b7d47b527d91b0406ae0ffcbfcb21882aa 100644 GIT binary patch delta 165 zcmeC?-O4yYqMnJFfq~)E$=0txiYLG)#1%;Y|NlQBApuAxwcg_e@|jA4{DK)6$|j$+ z0P?~-T^vIy;*vMK|NrrS?1}&1|I67VRD8|5yQ@_3b=Y2}P4oA4cYo&YEUw*D&3O3o zcYB4Ly=-m&-(PdQQN!c#wa~yU;H>@v#sBl0Zg7Xmt&L!1;T9_}WMEjGV45Ff*((gR Og2B_(&t;ucLK6U0ibQJw literal 1675 zcmbVNU5MON6iyX&tyH1)rwZydb=&%r|Ln}>vT<2wcHJpvyX=nL?Thx#&CN{MNp4KC zJF`pM2a6BBs1G8t_*j${>{FFiS@b~!6?_mB3Pn`#MPWtggNjm5<|lMz`lBJ4+$7&Q z=ljk%_slD^GtcbU{=jxY5O!3i%X4sl9Im0ETjBS$cLNV@w@1^56G6CRS9Wa^*5BSO z2)CT|tBdKP^DL%3kcrD{Ol}7epao%Uyd4p`#8Omabw9MlUqAX-M80c_3nPx|L?zbn zr`KXOzcy2)YfIE}#qp=mSQ~?bfTaYrgJziEwk>x2VpwOd6%lnH>5?r@WC78lGmA<* zW@tn%NK`X*RJ3HRpjx{5B+^x_sHjCnGbB~RY5^M->K!6Pi(L=Tm8W{K;K>#nX&PZg zX|-B%%aD0oS2WAA6jfJrT>^xZtcEFROJTC7@1e{R8v9Y|^AKeoNsTY3wg^nS5`rjq z8z#LpLBf!-5ac0}baDk`^b#@SoJ3m1~zs&z}dEZ60`_VUEJz+d@V6w6QCgISc_ksTH56ACYId|%J`L6HY zKQ?~ur`zsdIKKCbAD?^ZfO+?kmrp&s>)r?1Z^K`$f3tJ=>lcRCKi}Lueb_&_F?H$n zv74Jym*0GT=ilPSpYN59PF;HI#Ps-T(jqk4Z#9RCr!(kAVrpKnw#v`5z$z6gLBymp731pQ-kzw1n0+ zLW0BC&8j3ye#j+q=M}fZ&yd1T7uyAb)k~mBqTF^+0Yqu|1zQ>)H0G4avWy8v#I%v> z;~yB5e1V7%2kxK*X@Q-DOISAoBlWg1oLJyaTEzuU`vIS%@$+@Y@7ECDU_*5czS_F? ca^5}T3!11op!@JzlmGw#07*qoM6N<$g4_0GJpcdz literal 1965 zcmb_dU1%It6y64mvC@DQv5F7l6!EXKbLY;?PUf;}NOocq?WQ4JO@ayb?#!LtF*`q* znPhhZBD7*vsI(6SE72AOt5vJg5*3L)DAg)egy@6HYO(s1l2R3vLiNsOcS$w`X@>!W*B z?75kBu~_S4R(3cX&TK-uYsazS=19D3dlVguB|FO=*2hW6^nHnS>4F3qMU4vyph9T_1A(CMK;Z?E0|EjGB&tmPVyQRZFwvlPZ`~LD zO0k77^bpUNN~L&7jJtlG7gSZ{0pua%D1r+rPKe8#6RdAo&`6;BmKR#C!$cNw&YcWX zEM>YR!SE`eA9MI5aGEE;Var=;+omgX$$)G+b z2(hD+J}t#k#c|6rkRS+z2ugyJbHw08Q|jP4L~L*%Az(vM1Vsc=W4-3;lTlt9>kYbI zRIq7Cipb?Uu*jJ?DZ#0xkmIm`36!CP6_{wO-|Sm-U*X~v+lh8tR1m1w3TUb-_5HAg zOw>n3i>cKB!g^HODOQh?N({C>ZLJW2%Tusqwp1Y0=ta`K1Pfdmk(( z*vS)mNb>BJOS!x%{C5sk+(-YH6h1l`muiduXW7;MEjvYN5^GcRXpV->o-7EQ z*^SyO{n}}@9X$HDUi|SyenOr-x|oA!f3F^VZS9VI+b7$9KKT5eoqTeprK(l$8-8Ns z?V+1?e)iDetB*hV=ZRAX_CCDrr#YJ)OFme+9n5t~o)V`j6?NImDZGQuSe~S_T diff --git a/public/images/ui/legacy/text_images/zh-CN/summary/summary_profile_passive_zh-CN.png b/public/images/ui/legacy/text_images/zh-CN/summary/summary_profile_passive_zh-CN.png index 66f56ff435e5148fc3c48b19c28e38351538635b..c026e87a2157734bad03093215bae165aa01e8c0 100644 GIT binary patch delta 182 zcmcc5eT;E}L_G^L0|P^CXF({C;t%i%aRt%|2?_uI|A&wpnLmU9g_ui%{DOh>ud*Ze zfK;-li(^PdoM}%t=TQTmBRuQ5XXsr>Yhp=lEV@^}to$W=H#2uY$k(@vy}T-&Up(G7 zMd8Yp69&G|H@&TSyCzTW^|IX-TQ+Z)D|}JknP;K)*-f`)vf8;T#SKD#WmYu%Oq!d> c@c)4Pi6x4b2J3}#fc7zXy85}Sb4q9e087bAdjJ3c literal 1503 zcmbVMU5MO798a-muX+V(5v?F&`rv6VnI!v>%XqPzo88_*bDnn#JrzG@lgVy+n@pO_ z-tHbEa`dGqQbnpDMNpK!w8aO%K(Q4pKKLM(`k+`UJ{0YX;FB+>PWB^QrCMxYGGD*> z{lEW5=jZm1Z{NLL5QOpSthK=BhxpjG?NFg0S^h zzqZ7d>;uT5fs9>RCvrE4I9d><_jM!eEE5Lmq~V9A_}iyvMc})pc(7nAc2puwe|9w{ zi>q@rXLZ>zTyftcV7iMqK|mM=-JlgFsB4OSUc}ewF%&@`!j?^OCKU*l?0Hb4F#!d+ zC^>3g1G*urMa9talR#5c9V$9hbCRMWrHFC{7=9v`7P}r=uqs1Yd}oSH#v%k^r_+%; zIhn={s2YX=6%A^d#1T@`3mNW8VX|k$V3EX${fPNA1Sun~(>60juIa#pAj)XNWauXD zFxbTrRAnVKsSk9W3>UTIR$sX5K++-s30cCiYKD!Pl+mO~|AjiczD59dt!-yAuB%HB zWFit)Ug2g89dccCQtL$oE|7$_V~3PixSD%XYa&#N31&2|QQ8^>YJQ|LD3!p(K|ge9 zCwXuaoLHCwCS;&?!mF*VH0XrchqL2Fer0 z#SGsN>^jJ!ae%qGet;VUMqxt)nG;cowrI=+bM10{Eo>WA!-V0`Ayvy1xy7>YyC|nR zxg7Ca$uo#2>BW3qs@IFURKR&daf=?&mBQ$~MV)pU*U@|Tf4+Y#_IX|5)=l%I)s`j% zn)MSNs@||37Rlk^s^x=z4j}BLxowJ08dTzn!(;!ZJusl{GzlmDk8lrQ3H4Y9$7H6# zy>)|Rf?Rc&*5JSj_)i1-cbn>U1m{g4U1r^fZ?gVHLf*7u-kr)H9nyJcJ5jY}YExgI zu8r+D2kyAKxbfEmKa!p9Pwd+xlciG=m)^NB@!rLzbKA4$Ke_UIPm}>PzgRnSPJZF% z4YI2}w#&NZ-dCOvU;NnbK6Ck=q=GLk((fj|dhO1K|2+2i_fHuHJd(fq zL#6!9tM`AtsvmII->4j>)2G(oI=|)Q%g2A&|Jla++fVbpK6l-(Q zUsv>$DwqV6rmCeM+tOa#5mL8gv#Zu~T@9s0ow^x854%+6%cst|^P?d5%6%^hKmp#RqK>g%q?{j3_>c0rf$Rh`xvq1)&x_vwyaAo7UpM%-osz z&N<(Ae&#+sGkwpFq1%Qyj@vOcnV*Hb2It`5b#Og?^4kpDuJG7tI$pVY`6010FM)>W6HGy|%MmVS!q=KJg zRcCS~UE$j=vM z_?#CoUQZf`$eO~NR#G-3OVP%7MUqWXGDTTMl59(cty+BN5FlDW%l2&k?oKRt$_Ujc z@@-LUG#bfdcKoz{)+WU-cp{ta_Jl-TQn& z07z}2(2LQROTFF;5k?bBAfuCzzUZ*H>@#teh2CO7*u)ah+#9R$?OeccT<5I5?-gzFJ<#EI^f+8t8l2S-fi;8ONnk`$Sl446z8(Q$FQ(nFZsug9^ z))iZkFMvYBP#odQf+?}fUQov%*Qw(Q6MeTL@VzA3Ij`mg5E$60Ef$4>J>`ZGb_tux zX9Q53bR24{hGi02DI?8-3sW=$l`JYFlcrLpDQS{XB3=8uM;7C{cI_$HYZ@R8lOoN; zB_x3X!n&>@RW&q1Bur%(lQ_Q+|Ng*%xx%%p_5=O@kHW5(EGP=0aA9XU_Tx%tRde>m zGGm$s)fK}~3@DM7Lb|0jDW#OI@V>-#=*I3a{CDD=l#(iTD-3?x zqHy(6u52p$*5KcB{eMYmwP(Ax#EaXm_3gaBdbfvRtB9AqJqjLryBu>NO9AX*FFb*6 zg#GWOseHDWT0d3H?mWp4{&@7y~Ztf9pG8!7JA&)*z+_uMZdpKc$JzYte< zzqPS@`1jM_-{3r1+4(^2u8-5-sGCovZ~D@)M&B7re01XBWTLon__OzaJ@(b9_0;M1%U)+$mu){*}=Oq`QAY)08UoeBivm0qZ zj*O>^V@O3@GDjh2VnIP+B8Ol>V#bSzgBuntW@OOc!MayliTNy09fPN< zkdPP{KnO7~FfcG6CKwo4;2*HUA7E$Vbv`T#<nL%kbKr%wNr5e}8^&Qyx7&1b|mzYgsOP=Lr*0i3Y;wr9qSc zT2W5T3=+y#xKw@K31e6L^5(6khJmYHsd(5+>bx7yZ>0R<#zKp2tS~3gW=^Z81_THq zE~z?*dT~Yut~TT)u+F!UrVb%;#nonWhw8GosMbZw)k?|MnNc>?s#7v-?3m?~s)>y% z!c}BgIyMNliRGx{O9O9d&?ZZbvtwWIy4QPUl63#;!0s~gylGU^~ zbRICodpzQ?%m8Z?*rY3@$hzVe)Wz>j4j{FjSNPacOB59@GC8*fVT=>9B|2-ZCmb#D zOsuAi&#i&Xqj@w5si&Muk+y{BO$xO*k*wD1>airP|01*#AZduHQlsY zhD|CJV%1JylVCi8dLjth>pwutEyE&Km6)5M&@cg&^pD_xk+w)93gLzk?QoRD9ZfBg zNa~^|Qg96Ftf30eBh5IIG-kZna5ady6ovsYOoLXcc2%d)h`K?`w(bO&=@r9oQ>TXg zT9r=h8-lIob)DD;U{BkeGMkq5nqxS+<>&U;ujqko2ORshNoj3je<2OwT+!Zt^W>*3 zZwN9UW>Bj2@p-t&Z;V&HP#yLFp)BuhS7Uimd7zEA!!0uKcM*(e`&|yCzbW?!mWj6P z)0EG4AX|TECiE-m8D0Cm@V_hFmcoB-zJC`!PATisxWi#cA}tRE<>O>j5cFfMhX1}B zJrg+0vwT?$QuruFF^_>dg+cp#U=YEPbi-Fpe9zoInf78)q5E3)-o`thv>2-tjsh5 PTEgJz>gTe~DWM4foEpm6y6GGLQxP<5QttZ=Riv{>;JVqWLv^IZ5H7qBr7$Lf;u}h-nDk^acw8N zNrfut0jeCi1uh72Ktdcql@JI465_y*5G?{E)C&g?0x1Y_K|;WIf2{tbKU7$C{ZjEN1ND779c4zmF!sg5O z6$(2~`n9EW$$boB78DU-bz1BO5l0salLxyI!Y!H#b=vU5vbgo$2cqDUvUtRBWjCtO zra!YD)5Z1K8eVTgD~-cNnI34N>R=v^?(HDxM(GA_nZ?f(KZcem?j*n$%wZ7q$tF+>Xb#Uxaj)? znx<(HPceX9(g{FB_W)8Y9hizvWMr2-OU>6i46kH)9j+&QJ+7;UrV|a6bRC1bt((A7 zEDUT*r<$xO9#)m1|GC)b`wF#h*iN?FvVwp!e!^3=HrNk~^!Q-Z_Jv*zK!mf}E{ixz zDkb7z+TSJu*QcOww$r5C=vC6)hb7EQJ1C}84IZs)6q9r%>IquCI&nfurloiu(6FZg z?CA!m*O3RXZP_ZtNH&l$6n;5z6RIYZjbWYscjAMP;wB0klpm6kc;iy8uL`%#!R7nt z|B@nQCu6_1q)W@L_wRyJlqaz_#gFD}nD+$!k#`ju@+if;XDm;Se8v0CM%9_BS)aYO z`0Ut+!ssur|NYO+FP+wwcF+m`^Ub&JdGL-qkJe8-+4}wYt)DjjJpa+DGfyS0+h2Y7 zUg_9@v&VNo{>&%Bp>N-QXzWK{mAhL<|JZj5-ldN0hvQE>FBYfY*b?1OjpoODX5YPA z+4IVG(K~;^Tjn14=H&eOAHIKKZuwVd;%89$%{_bK%ddmYk#k?P&pmq6;da&stJAa2 Jn~xlO?qB$|S|0!a diff --git a/public/images/ui/legacy/text_images/zh-CN/summary/summary_stats_stats_title_zh-CN.png b/public/images/ui/legacy/text_images/zh-CN/summary/summary_stats_stats_title_zh-CN.png index 5531819ef665bd054b5eb15d84500a1a4fe9dff9..f602a43c39d509720f1cfb19ab25d4aeaf676ed3 100644 GIT binary patch delta 153 zcmX@hw~}#!L_HHT0|P^i@R?2^#S`EY;+l|<@c;jR2zmO6f-g{vTY z&&$)rF{C0cxugD1y`6wgTe~DWM4f(nL8j literal 1869 zcmb_dO^Dq@9M3AO>&o^}L8TXCsIZrf^eAYlllKwYy4%)bAjxF%o8SNc z`+v;5yt=Y9H@j z!j5jPcw}>(I8-@!K;Az(|j^n7BuIf4v2#{_>86JQr-92Hbv6Li1oCQ3Rb4J|c-K?SrPe&5MxS);F zahOEF)B%oFSkiJyLm(vuF776sp>ax7)?p!wvQ%JUfsNZd<7u03LtXs7!hi^^>lQY) z^d$@n6KS^C6EemD*%F<48!=NGEalyVu*IHmb9b&LM%9Gjj3*xFok^lrCoap?s=W7D z5K-Pw_gw~OHJq`EqHDSZw0WQ#9<-5VB4|IP=}6N?P?yud-?$2Dda#Tvh%Dy{s3;hU zGkjezCCKMVh=trB#4V=AQA?4FAfhVo@I)9E-WfwLTo=`&G{X^L^;$&{ic3L2k=-o& zI(9H%^A-Vi)AWI3YYgZ%(V*?y#4*TZeT|cDp4Z8GD%KOKW4&x?z@fwdCNUvs+FBX- z79%F6Hq>=ES$`x6L|@^~P4nd4mKOwC4pNb-jd4F5WzUR9oj@Me0Kz1%?TSM3q%x|E zr-LmbaAOKawEZ>{q}NFI2$pg`>*Itiv_!P7Q%vf1uBW(nZO0k4Ox=cMP@Xp&VA-Y# znoY|B%=Ue1*u-a06yP?F>l%V4GKxC=?~ad|NgGEkCN@b`xp^r!R)t$i`F9`vUsBY3 zXB^d*dUe?i|6R6=iX;xF#MYb-i!(ud6sHP{M3fS7X1sgqj3Le&(MJp3-fzw~>N}+S zW`2JA;-x+4ZnLwq`yl!B`Ooi~J=FO2*IOTzN`89$yCcoHSARNwV(EY<{qR-quP-+V2pYQMQqou{`Bg5mv9LJ3m z7OZ79kFjxbaDe@HhX#^qC|tN5bKI%XWb|>*o}cBo6JNdJ8eOw*LWlSga!CnGO+RF4 zj+>rohR9jRlrQ0m7ns7g*KY*gb4_7Yvwca7O<^_>$gkN;e4a#@*QA{2C|Q-)4N1uXL(N|1RiNlH z&}Ahf0tJE`%ou$46PUEfEyHDNzAKCEOrc6?2xYm^Xh@BWM52nU7=|GORaRAzA;h>9 zP}CHIc%sK(;n<11ka{HG6Gl`bb!rMs(~b##n9>Gu*GZzL))lgM*5F%cVICo(*}z0Y=wF2h-8wP>T*(p9V_HN4Q$`-tJfZ!HGyQA zb|1D$`xg#a(~4Mk+Ieu}KI?3`f;C&59CLo|?alOWD?5Yw_m7{y?19r~-p)_F8{K`s sdv$#4>G>zWK#OYDx#166k1zD~b1&{}|M;ZSS4nd$%q?0kuibt48<8ltA^-pY diff --git a/public/images/ui/legacy/text_images/zh-TW/summary/summary_dexnb_label_zh-TW.png b/public/images/ui/legacy/text_images/zh-TW/summary/summary_dexnb_label_zh-TW.png index eab90a91c7fc8dd67b9cf24a7e8c37dffd62b2d8..bf568c486aac5e8af4e95a237819caf922a9e50c 100644 GIT binary patch delta 106 zcmX@ZQ!_y#g^8Jgfno2IHdP?S8Q>G*`v3p`A3uI1BqZFKcTEW>$XF8O7tG-B>_!@p zqv+}47*Y|J?BS7+(82KV3QOCTEi7$53Vtk)R21qDu3}}VtK-z%ysM-MsF=ai)z4*} HQ$iB}`|BmX literal 1612 zcmbVMJB-{!7&Zl`a8XbYLOM~690`G7c4q8-oJaA6WA|WJ!N+k~DTkoQcxJq7-P&X9 zx!b)HH0T;iB4`i=Qc4h_KnRI~K!cEIXsJMHsEFtgO&IUP%Hu8xSh79#eEDonv;6e3Wm#u>-Ojdom+Mofmdx||Kk3)z^;p(@qp+;gPnP#1*2kZ$Sk|Lo#r++< z6TC{fN@`4~P}U|%X3&;({=y_j)PLb!#93o)U%NnH=jFWW@RMMSqdFVAC{FW(+&ol%6w<8f{5)>J+~ z#Pd9a9ppIBAYgHjYBqssac<$EBMY9#nT}NoN{=j5d)l`R(^(2hR=G`!d6`VX(1c}( z)Nq;76exIw%l7ioG`QeMj$|TJT^KB>u-Q;)RSeY$uB+`k0+`YUK^5azE=f{_DD=j@ zNn>7+W6?$bAd_fY7HTi&a%11nJXdCu(RMDGR(W5k(V|gX3(BC~2G3rOQ=!JibGN}| zhiU2C4t5$4uR^EZCxq4sbylCp4#ju|4U~wZgL|P4bqFQS9Z)kcf@$_Zu;4UOdBRM( zal!@?W$C~MRS{`hjZ|&|8+PuLMG(+lT41}Nj@Z2LAZn<@>1+3)z63Orl}GyCyS z8mISZ_Y793NRL@A*9WFr4;UtN67_=Z-=FxyweBS1cMacvCq6GJAF^~HjYuN9Oa+y4 zGHVFByI0e_+oWd!H+ohsDl@!a=xXz8x{`&5RZyAwZ>vh)qDe)!{^m8FZ|;?>X3KJ)mEYu(?j{l5G`@cn0JBl!CAFHe2?%@Yy)d5qWSlJ(`A WfBw`TT)k52eQ#s4^TkV7-uoA*z5f^h diff --git a/public/images/ui/legacy/text_images/zh-TW/summary/summary_moves_descriptions_title_zh-TW.png b/public/images/ui/legacy/text_images/zh-TW/summary/summary_moves_descriptions_title_zh-TW.png index 3d2b4d083760bfed09c6fae182fb0a997420f282..e83e8cafbfcc09a3761b9d915f3c0f0205109579 100644 GIT binary patch delta 219 zcmew>@S1UgL_G^L0|Udt0+u8o#UJ1k;+l|<@Z-mi|Ns9389;JX#u*);6mv@JZ!+jay6~7t*qfntizYh4ms& z?VDfRyXn1dOPhm}$?=zuzWYlXm|asivL|;gD`Uu@lRJ(3&GNQSdOPmq zObx5dI(>ife9ZpI@-s@b8fx~JIF2VHs#uB=gC2r7U_u05*FON(mZ}fv*moA TSG>1@j$-h1^>bP0l+XkK`;cKC literal 2173 zcmb_eU2NM_6iy{zTeraj0(DeqmisWK;n==$?AU5r+N510HC@*Rtz%o)>+9>pYGS*# z)1-YWAW=YTi%_5FC=!)GeE?|%A*O-WZ5k6oJOE+{_JV{E51UqvfrJ=;*ZCuZt{W4P z9A965=bZ2S-QySf`}S^ce55fD2y9RE#0SW|mt31S-$(ujhj;8Jw=H(hK{pWC`k;R` z1YSAb6bNie8mS>~DA}tZD;I>CrQ%>QXA^WF(Ar+KAsWRVrQ)<@pWfyXGO|9ux!bAeb7NO1ZLC$xn1k_LsXXl-4$+(7CoW(h8dM?58HLRVnJj=~k zw@_VseUkwa+GMh3V_hz}T+M{*b&V4@Dgjv+?WQJd%no4J$~y>mjT4$JzBjfKb1?KQ zCuLdLYNGn9l&M&ZdhCE&RYCN0g- zC+eXQ1_&u$Qo=wHA~!)v!D!Hfw*_lR(JdzjiMvJ)rZH=qX_~49QHfbu%OQpdyHJTm zGN~j?*Mla)iFlMI9tRCWQxH%EK@}s6$V(Cv!2)Jf4G0WUMJa+rBuY@O%Ev8~_w!nn z*N8k3gAgKt(IKxhnk0l6Rg@uvHIWN*9H%3WugdRp4ANIHd)InM{r~M@RZ9o)$R&w$ zE8Wov)0I`$X!G6lX#xWcBDg$|lK^O>70%DzXNl%#uyD{HVc`H4J&wrv~cMy-&7*M5(Rv_{_ zHA*ADM6pI!wvBc2d1v`3t1VrZCZo96?GSmZ%iCZ5iX*V z$^)7j_G1hfHADifM?hEr2=F)ztB%(bFSkNN=Q)#nrTdqe3rg@SMIE* z{+)w$Plf->l~PfbYm5D7*_Gba%>nQNi5_3N{KQ91FVBDV#ZwDEo?wrz9eKC- z$?&JAn|FShSepwzw0iZPeGTugXy3Km*!|(Jk3RkTPOd9`YGhj9^k#2!;p)SC=9bZ7 z=LZYZm*2VAGG)x`uXX3=8v~=q#v9JH77ugR?FTM2b*~J4KeI^P*!Jy_)s<@tQy-5# zcd+GT;IqaPR}N>@`QaI7YV3OC=il0YnwgzFHuv=#=g)SZy)@H&@6xNPmij>)`DW(X p>G98Z%|>6m_Qz5}Ji5HNb?=qecdQ;dzsvtXNObkZ-|ifF<}ZAE%%=bV diff --git a/public/images/ui/legacy/text_images/zh-TW/summary/summary_moves_effect_title_zh-TW.png b/public/images/ui/legacy/text_images/zh-TW/summary/summary_moves_effect_title_zh-TW.png index 55fb0efd832da789da351d76a5e3c7eea57b224e..fbbaac0b260570166eb5a51383c843ca90975205 100644 GIT binary patch delta 171 zcmX@dx0`W-L_HHT0|SG|1HX+xiYLG)#P$FG{|N~R5Hi32$W5R)Q%R6tFatx`?PJj1B) SP+KFQMGT&W&Q^Io}QlK&)~@05`WFa3kOm`*mZYt-zL2A<{m+q zdOm2bW~<(lh_JANeb%CtJd8P75N7A{7?U-ci7na*B1gLL(I=7^_>Q#S@Dwku(QdG? znb4KZ#U|NYBepNiJtEHLh#LrLhQ&PWMJdW1X=oSmaWM@faR|xQ9BICAD6V=-VvQwK zG%BV{R2_--Xu{=6Ut((CO)t~ zhSIpZ>knDAkx*#puAvw zoZ%aSeS+F72{D%>$XN9CakI3?izr9!t1m&Q2S$@I2Ir(hNt0He5&IiYr0jBjOWE z+nhcnRFMr|*X62ZSh8iPW=l7HwPhQV^)4g*BCnJ6K3|VDLNIJ$8JdPB`<7mnahqV7 z=o+>yXz0}zoUDH;3HZLky<4_Z?6#sH&_a;%RBep+!wNk(9`yooSOW-?qP8806iKDN zG@cH&iNMV%7?~Y(DK~nZbdO*uYi9$T(D@FJ)(wgYT#I^&*RM}pgSbjyOP4iGC9-Df zP_FV^$ZcEqb<;4Y-m)jcuO@CFMMIh~DZc+sd>m5J#ZiayLlQ{0F6HK`aN8VQy^sDc zDWEtRN3{j6EW6>qOHNUq#NiY_nu}rC6Zl8jRcOScl<=PM*4gh)@qW{9xbw~GSMMKv z`i@V<>DtyeKmK#q?`t!)J;%DA-G1l&ubPj#rw{$|+R^zVhtKc*@LZ#E@RiQr$MauL zd@*VAxx1)=2^v32@d6!;b-nJ);%G$21~PVuE6 zqP&*N10t)C*NvQq4a|kS2WIJfO{A4EoKLJ(OuoA~#Ijz~@ts*a{Ysg20%zC3s=x z){i@BVhIzQI27faluBv=ZIa=_g{a;NZWEE#sYm@JW>`7HhP5CG;##nU>+JJ20a$8H zGmEh)7thN=#L37alhH}Yrs%k`6jE`T#=$~F=*S|Yxj$7CqEbY05=50CsCNrB)uqgr zO8kKt*SCX4{ODD1TE+=22vC9=kOlx$E3$+X1!>yj5=4^JhMIxxI!phC>PUk~QLllr zhOu#iZwR&ta)QXiOs?zUDiuS&D)3nnQ7Nbg5ev-NDJ>SJiN^gn!9Jnm<$}Nz=UmrD zwxT%DQV38Tf`Nfm1z6CrK;FT`G6rlNYEIX_9FT>yu3dYZ*+Z!43bAbf9YY61O6_%- z0?DA%&}GA+7j49NhiglPD64Oy&MtpnKb|h{evDKhx%uN|Ab>beO&t(kwR!<^K z(pf7AByBQk3!T^QCKb84jJDl2YLq$Mrv2KmIB=2%j%cyUs&|9t7Pq1v)WpN?W(_ew};tQkuI}6ialhz9Q9e2BDRNlqp#%I{`b;&xmX!^ z`K>cMdY{gT7xrKhbyQ*s-5hR{z{{>6@m@8{0m^Y9z7w0im7ed)G5 NJ~CN;@6hq*{|0a5ki7r^ diff --git a/public/images/ui/legacy/text_images/zh-TW/summary/summary_profile_ability_zh-TW.png b/public/images/ui/legacy/text_images/zh-TW/summary/summary_profile_ability_zh-TW.png index 6600db26802ed202ae0536dd122605998b551fdf..a05c22b7d47b527d91b0406ae0ffcbfcb21882aa 100644 GIT binary patch delta 165 zcmeC?-O4yYqMnJFfq~)E$=0txiYLG)#1%;Y|NlQBApuAxwcg_e@|jA4{DK)6$|j$+ z0P?~-T^vIy;*vMK|NrrS?1}&1|I67VRD8|5yQ@_3b=Y2}P4oA4cYo&YEUw*D&3O3o zcYB4Ly=-m&-(PdQQN!c#wa~yU;H>@v#sBl0Zg7Xmt&L!1;T9_}WMEjGV45Ff*((gR Og2B_(&t;ucLK6U0ibQJw literal 1675 zcmbVNU5MON6iyX&tyH1)rwZydb=&%r|Ln}>vT<2wcHJpvyX=nL?Thx#&CN{MNp4KC zJF`pM2a6BBs1G8t_*j${>{FFiS@b~!6?_mB3Pn`#MPWtggNjm5<|lMz`lBJ4+$7&Q z=ljk%_slD^GtcbU{=jxY5O!3i%X4sl9Im0ETjBS$cLNV@w@1^56G6CRS9Wa^*5BSO z2)CT|tBdKP^DL%3kcrD{Ol}7epao%Uyd4p`#8Omabw9MlUqAX-M80c_3nPx|L?zbn zr`KXOzcy2)YfIE}#qp=mSQ~?bfTaYrgJziEwk>x2VpwOd6%lnH>5?r@WC78lGmA<* zW@tn%NK`X*RJ3HRpjx{5B+^x_sHjCnGbB~RY5^M->K!6Pi(L=Tm8W{K;K>#nX&PZg zX|-B%%aD0oS2WAA6jfJrT>^xZtcEFROJTC7@1e{R8v9Y|^AKeoNsTY3wg^nS5`rjq z8z#LpLBf!-5ac0}baDk`^b#@SoJ3m1~zs&z}dEZ60`_VUEJz+d@V6w6QCgISc_ksTH56ACYId|%J`L6HY zKQ?~ur`zsdIKKCbAD?^ZfO+?kmrp&s>)r?1Z^K`$f3tJ=>lcRCKi}Lueb_&_F?H$n zv74Jym*0GT=ilPSpYN59PF;HI#Ps-T(jqk4Z#9RCr!(kAVrpKnw#v`5z$z6gLBymp731pQ-kzw1n0+ zLW0BC&8j3ye#j+q=M}fZ&yd1T7uyAb)k~mBqTF^+0Yqu|1zQ>)H0G4avWy8v#I%v> z;~yB5e1V7%2kxK*X@Q-DOISAoBlWg1oLJyaTEzuU`vIS%@$+@Y@7ECDU_*5czS_F? ca^5}T3!11op!@JzlmGw#07*qoM6N<$g4_0GJpcdz literal 1965 zcmb_dU1%It6y64mvC@DQv5F7l6!EXKbLY;?PUf;}NOocq?WQ4JO@ayb?#!LtF*`q* znPhhZBD7*vsI(6SE72AOt5vJg5*3L)DAg)egy@6HYO(s1l2R3vLiNsOcS$w`X@>!W*B z?75kBu~_S4R(3cX&TK-uYsazS=19D3dlVguB|FO=*2hW6^nHnS>4F3qMU4vyph9T_1A(CMK;Z?E0|EjGB&tmPVyQRZFwvlPZ`~LD zO0k77^bpUNN~L&7jJtlG7gSZ{0pua%D1r+rPKe8#6RdAo&`6;BmKR#C!$cNw&YcWX zEM>YR!SE`eA9MI5aGEE;Var=;+omgX$$)G+b z2(hD+J}t#k#c|6rkRS+z2ugyJbHw08Q|jP4L~L*%Az(vM1Vsc=W4-3;lTlt9>kYbI zRIq7Cipb?Uu*jJ?DZ#0xkmIm`36!CP6_{wO-|Sm-U*X~v+lh8tR1m1w3TUb-_5HAg zOw>n3i>cKB!g^HODOQh?N({C>ZLJW2%Tusqwp1Y0=ta`K1Pfdmk(( z*vS)mNb>BJOS!x%{C5sk+(-YH6h1l`muiduXW7;MEjvYN5^GcRXpV->o-7EQ z*^SyO{n}}@9X$HDUi|SyenOr-x|oA!f3F^VZS9VI+b7$9KKT5eoqTeprK(l$8-8Ns z?V+1?e)iDetB*hV=ZRAX_CCDrr#YJ)OFme+9n5t~o)V`j6?NImDZGQuSe~S_T diff --git a/public/images/ui/legacy/text_images/zh-TW/summary/summary_profile_passive_zh-TW.png b/public/images/ui/legacy/text_images/zh-TW/summary/summary_profile_passive_zh-TW.png index 66f56ff435e5148fc3c48b19c28e38351538635b..c026e87a2157734bad03093215bae165aa01e8c0 100644 GIT binary patch delta 182 zcmcc5eT;E}L_G^L0|P^CXF({C;t%i%aRt%|2?_uI|A&wpnLmU9g_ui%{DOh>ud*Ze zfK;-li(^PdoM}%t=TQTmBRuQ5XXsr>Yhp=lEV@^}to$W=H#2uY$k(@vy}T-&Up(G7 zMd8Yp69&G|H@&TSyCzTW^|IX-TQ+Z)D|}JknP;K)*-f`)vf8;T#SKD#WmYu%Oq!d> c@c)4Pi6x4b2J3}#fc7zXy85}Sb4q9e087bAdjJ3c literal 1503 zcmbVMU5MO798a-muX+V(5v?F&`rv6VnI!v>%XqPzo88_*bDnn#JrzG@lgVy+n@pO_ z-tHbEa`dGqQbnpDMNpK!w8aO%K(Q4pKKLM(`k+`UJ{0YX;FB+>PWB^QrCMxYGGD*> z{lEW5=jZm1Z{NLL5QOpSthK=BhxpjG?NFg0S^h zzqZ7d>;uT5fs9>RCvrE4I9d><_jM!eEE5Lmq~V9A_}iyvMc})pc(7nAc2puwe|9w{ zi>q@rXLZ>zTyftcV7iMqK|mM=-JlgFsB4OSUc}ewF%&@`!j?^OCKU*l?0Hb4F#!d+ zC^>3g1G*urMa9talR#5c9V$9hbCRMWrHFC{7=9v`7P}r=uqs1Yd}oSH#v%k^r_+%; zIhn={s2YX=6%A^d#1T@`3mNW8VX|k$V3EX${fPNA1Sun~(>60juIa#pAj)XNWauXD zFxbTrRAnVKsSk9W3>UTIR$sX5K++-s30cCiYKD!Pl+mO~|AjiczD59dt!-yAuB%HB zWFit)Ug2g89dccCQtL$oE|7$_V~3PixSD%XYa&#N31&2|QQ8^>YJQ|LD3!p(K|ge9 zCwXuaoLHCwCS;&?!mF*VH0XrchqL2Fer0 z#SGsN>^jJ!ae%qGet;VUMqxt)nG;cowrI=+bM10{Eo>WA!-V0`Ayvy1xy7>YyC|nR zxg7Ca$uo#2>BW3qs@IFURKR&daf=?&mBQ$~MV)pU*U@|Tf4+Y#_IX|5)=l%I)s`j% zn)MSNs@||37Rlk^s^x=z4j}BLxowJ08dTzn!(;!ZJusl{GzlmDk8lrQ3H4Y9$7H6# zy>)|Rf?Rc&*5JSj_)i1-cbn>U1m{g4U1r^fZ?gVHLf*7u-kr)H9nyJcJ5jY}YExgI zu8r+D2kyAKxbfEmKa!p9Pwd+xlciG=m)^NB@!rLzbKA4$Ke_UIPm}>PzgRnSPJZF% z4YI2}w#&NZ-dCOvU;NnbK6Ck=q=GLk((fj|dhO1K|2+2i_fHuHJd(fq zL#6!9tM`AtsvmII->4j>)2G(oI=|)Q%g2A&|Jla++fVbpK6l-(Q zUsv>$DwqV6rmCeM+tOa#5mL8gv#Zu~T@9s0ow^x854%+6%cst|^P?d5%6%^hKmp#RqK>g%q?{j3_>c0rf$Rh`xvq1)&x_vwyaAo7UpM%-osz z&N<(Ae&#+sGkwpFq1%Qyj@vOcnV*Hb2It`5b#Og?^4kpDuJG7tI$pVY`6010FM)>W6HGy|%MmVS!q=KJg zRcCS~UE$j=vM z_?#CoUQZf`$eO~NR#G-3OVP%7MUqWXGDTTMl59(cty+BN5FlDW%l2&k?oKRt$_Ujc z@@-LUG#bfdcKoz{)+WU-cp{ta_Jl-TQn& z07z}2(2LQROTFF;5k?bBAfuCzzUZ*H>@#teh2CO7*u)ah+#9R$?OeccT<5I5?-gzFJ<#EI^f+8t8l2S-fi;8ONnk`$Sl446z8(Q$FQ(nFZsug9^ z))iZkFMvYBP#odQf+?}fUQov%*Qw(Q6MeTL@VzA3Ij`mg5E$60Ef$4>J>`ZGb_tux zX9Q53bR24{hGi02DI?8-3sW=$l`JYFlcrLpDQS{XB3=8uM;7C{cI_$HYZ@R8lOoN; zB_x3X!n&>@RW&q1Bur%(lQ_Q+|Ng*%xx%%p_5=O@kHW5(EGP=0aA9XU_Tx%tRde>m zGGm$s)fK}~3@DM7Lb|0jDW#OI@V>-#=*I3a{CDD=l#(iTD-3?x zqHy(6u52p$*5KcB{eMYmwP(Ax#EaXm_3gaBdbfvRtB9AqJqjLryBu>NO9AX*FFb*6 zg#GWOseHDWT0d3H?mWp4{&@7y~Ztf9pG8!7JA&)*z+_uMZdpKc$JzYte< zzqPS@`1jM_-{3r1+4(^2u8-5-sGCovZ~D@)M&B7re01XBWTLon__OzaJ@(b9_0;M1%U)+$mu){*}=Oq`QAY)08UoeBivm0qZ zj*O>^V@O3@GDjh2VnIP+B8Ol>V#bSzgBuntW@OOc!MayliTNy09fPN< zkdPP{KnO7~FfcG6CKwo4;2*HUA7E$Vbv`T#<nL%kbKr%wNr5e}8^&Qyx7&1b|mzYgsOP=Lr*0i3Y;wr9qSc zT2W5T3=+y#xKw@K31e6L^5(6khJmYHsd(5+>bx7yZ>0R<#zKp2tS~3gW=^Z81_THq zE~z?*dT~Yut~TT)u+F!UrVb%;#nonWhw8GosMbZw)k?|MnNc>?s#7v-?3m?~s)>y% z!c}BgIyMNliRGx{O9O9d&?ZZbvtwWIy4QPUl63#;!0s~gylGU^~ zbRICodpzQ?%m8Z?*rY3@$hzVe)Wz>j4j{FjSNPacOB59@GC8*fVT=>9B|2-ZCmb#D zOsuAi&#i&Xqj@w5si&Muk+y{BO$xO*k*wD1>airP|01*#AZduHQlsY zhD|CJV%1JylVCi8dLjth>pwutEyE&Km6)5M&@cg&^pD_xk+w)93gLzk?QoRD9ZfBg zNa~^|Qg96Ftf30eBh5IIG-kZna5ady6ovsYOoLXcc2%d)h`K?`w(bO&=@r9oQ>TXg zT9r=h8-lIob)DD;U{BkeGMkq5nqxS+<>&U;ujqko2ORshNoj3je<2OwT+!Zt^W>*3 zZwN9UW>Bj2@p-t&Z;V&HP#yLFp)BuhS7Uimd7zEA!!0uKcM*(e`&|yCzbW?!mWj6P z)0EG4AX|TECiE-m8D0Cm@V_hFmcoB-zJC`!PATisxWi#cA}tRE<>O>j5cFfMhX1}B zJrg+0vwT?$QuruFF^_>dg+cp#U=YEPbi-Fpe9zoInf78)q5E3)-o`thv>2-tjsh5 PTEgJz>gTe~DWM4foEpm6y6GGLQxP<5QttZ=Riv{>;JVqWLv^IZ5H7qBr7$Lf;u}h-nDk^acw8N zNrfut0jeCi1uh72Ktdcql@JI465_y*5G?{E)C&g?0x1Y_K|;WIf2{tbKU7$C{ZjEN1ND779c4zmF!sg5O z6$(2~`n9EW$$boB78DU-bz1BO5l0salLxyI!Y!H#b=vU5vbgo$2cqDUvUtRBWjCtO zra!YD)5Z1K8eVTgD~-cNnI34N>R=v^?(HDxM(GA_nZ?f(KZcem?j*n$%wZ7q$tF+>Xb#Uxaj)? znx<(HPceX9(g{FB_W)8Y9hizvWMr2-OU>6i46kH)9j+&QJ+7;UrV|a6bRC1bt((A7 zEDUT*r<$xO9#)m1|GC)b`wF#h*iN?FvVwp!e!^3=HrNk~^!Q-Z_Jv*zK!mf}E{ixz zDkb7z+TSJu*QcOww$r5C=vC6)hb7EQJ1C}84IZs)6q9r%>IquCI&nfurloiu(6FZg z?CA!m*O3RXZP_ZtNH&l$6n;5z6RIYZjbWYscjAMP;wB0klpm6kc;iy8uL`%#!R7nt z|B@nQCu6_1q)W@L_wRyJlqaz_#gFD}nD+$!k#`ju@+if;XDm;Se8v0CM%9_BS)aYO z`0Ut+!ssur|NYO+FP+wwcF+m`^Ub&JdGL-qkJe8-+4}wYt)DjjJpa+DGfyS0+h2Y7 zUg_9@v&VNo{>&%Bp>N-QXzWK{mAhL<|JZj5-ldN0hvQE>FBYfY*b?1OjpoODX5YPA z+4IVG(K~;^Tjn14=H&eOAHIKKZuwVd;%89$%{_bK%ddmYk#k?P&pmq6;da&stJAa2 Jn~xlO?qB$|S|0!a diff --git a/public/images/ui/legacy/text_images/zh-TW/summary/summary_stats_stats_title_zh-TW.png b/public/images/ui/legacy/text_images/zh-TW/summary/summary_stats_stats_title_zh-TW.png index 5531819ef665bd054b5eb15d84500a1a4fe9dff9..f602a43c39d509720f1cfb19ab25d4aeaf676ed3 100644 GIT binary patch delta 153 zcmX@hw~}#!L_HHT0|P^i@R?2^#S`EY;+l|<@c;jR2zmO6f-g{vTY z&&$)rF{C0cxugD1y`6wgTe~DWM4f(nL8j literal 1869 zcmb_dO^Dq@9M3AO>&o^}L8TXCsIZrf^eAYlllKwYy4%)bAjxF%o8SNc z`+v;5yt=Y9H@j z!j5jPcw}>(I8-@!K;Az(|j^n7BuIf4v2#{_>86JQr-92Hbv6Li1oCQ3Rb4J|c-K?SrPe&5MxS);F zahOEF)B%oFSkiJyLm(vuF776sp>ax7)?p!wvQ%JUfsNZd<7u03LtXs7!hi^^>lQY) z^d$@n6KS^C6EemD*%F<48!=NGEalyVu*IHmb9b&LM%9Gjj3*xFok^lrCoap?s=W7D z5K-Pw_gw~OHJq`EqHDSZw0WQ#9<-5VB4|IP=}6N?P?yud-?$2Dda#Tvh%Dy{s3;hU zGkjezCCKMVh=trB#4V=AQA?4FAfhVo@I)9E-WfwLTo=`&G{X^L^;$&{ic3L2k=-o& zI(9H%^A-Vi)AWI3YYgZ%(V*?y#4*TZeT|cDp4Z8GD%KOKW4&x?z@fwdCNUvs+FBX- z79%F6Hq>=ES$`x6L|@^~P4nd4mKOwC4pNb-jd4F5WzUR9oj@Me0Kz1%?TSM3q%x|E zr-LmbaAOKawEZ>{q}NFI2$pg`>*Itiv_!P7Q%vf1uBW(nZO0k4Ox=cMP@Xp&VA-Y# znoY|B%=Ue1*u-a06yP?F>l%V4GKxC=?~ad|NgGEkCN@b`xp^r!R)t$i`F9`vUsBY3 zXB^d*dUe?i|6R6=iX;xF#MYb-i!(ud6sHP{M3fS7X1sgqj3Le&(MJp3-fzw~>N}+S zW`2JA;-x+4ZnLwq`yl!B`Ooi~J=FO2*IOTzN`89$yCcoHSARNwV(EY<{qR-quP Date: Sun, 21 Sep 2025 20:07:08 +0200 Subject: [PATCH 38/40] [Refactor] Refactor `Starter` and its handling (#6477) * Reworked `Starter` interface with more explicit information * Use Starter in ssui * Fixed some bugs * Passing starter.ivs to playerPokemon * Using speciesIds * Fixed getTestRunStarters * Reverted some parameter changes * Initialize starters in ssui * Don't clear starters before starting run * Fix to game manager * Apply suggestions from code review Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Set ivs to 0 in part timer test * Setting the right ivs * Moved ssui to handlers folder * Ran biome all * Fixed broken imports --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/@types/save-data.ts | 18 +++ src/data/daily-run.ts | 9 +- src/phases/select-starter-phase.ts | 32 +++-- src/phases/title-phase.ts | 19 ++- src/ui/handlers/starter-select-ui-handler.ts | 117 ++++++++---------- .../encounters/part-timer-encounter.test.ts | 1 + test/test-utils/game-manager-utils.ts | 52 ++++---- test/test-utils/game-manager.ts | 6 +- .../helpers/challenge-mode-helper.ts | 6 +- .../test-utils/helpers/classic-mode-helper.ts | 6 +- 10 files changed, 137 insertions(+), 129 deletions(-) diff --git a/src/@types/save-data.ts b/src/@types/save-data.ts index 4c20d63da53..ae359c20949 100644 --- a/src/@types/save-data.ts +++ b/src/@types/save-data.ts @@ -4,8 +4,10 @@ import type { BattleType } from "#enums/battle-type"; import type { GameModes } from "#enums/game-modes"; import type { MoveId } from "#enums/move-id"; import type { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import type { Nature } from "#enums/nature"; import type { PlayerGender } from "#enums/player-gender"; import type { PokemonType } from "#enums/pokemon-type"; +import type { SpeciesId } from "#enums/species-id"; import type { MysteryEncounterSaveData } from "#mystery-encounters/mystery-encounter-save-data"; import type { Variant } from "#sprites/variant"; import type { ArenaData } from "#system/arena-data"; @@ -108,6 +110,22 @@ export interface DexAttrProps { formIndex: number; } +export interface Starter { + speciesId: SpeciesId; + shiny: boolean; + variant: Variant; + formIndex: number; + female?: boolean; + abilityIndex: number; + passive: boolean; + nature: Nature; + moveset?: StarterMoveset; + pokerus: boolean; + nickname?: string; + teraType?: PokemonType; + ivs: number[]; +} + export type RunHistoryData = Record; export interface RunEntry { diff --git a/src/data/daily-run.ts b/src/data/daily-run.ts index 5f49b9adbb0..776dff1bf46 100644 --- a/src/data/daily-run.ts +++ b/src/data/daily-run.ts @@ -6,7 +6,7 @@ import { PokemonSpecies } from "#data/pokemon-species"; import { BiomeId } from "#enums/biome-id"; import { PartyMemberStrength } from "#enums/party-member-strength"; import { SpeciesId } from "#enums/species-id"; -import type { Starter } from "#ui/starter-select-ui-handler"; +import type { Starter } from "#types/save-data"; import { randSeedGauss, randSeedInt, randSeedItem } from "#utils/common"; import { getEnumValues } from "#utils/enums"; import { getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils"; @@ -66,8 +66,11 @@ function getDailyRunStarter(starterSpeciesForm: PokemonSpeciesForm, startingLeve const formIndex = starterSpeciesForm instanceof PokemonSpecies ? undefined : starterSpeciesForm.formIndex; const pokemon = globalScene.addPlayerPokemon(starterSpecies, startingLevel, undefined, formIndex); const starter: Starter = { - species: starterSpecies, - dexAttr: pokemon.getDexAttr(), + speciesId: starterSpecies.speciesId, + shiny: pokemon.shiny, + variant: pokemon.variant, + formIndex: pokemon.formIndex, + ivs: pokemon.ivs, abilityIndex: pokemon.abilityIndex, passive: false, nature: pokemon.getNature(), diff --git a/src/phases/select-starter-phase.ts b/src/phases/select-starter-phase.ts index 27e2150fd06..e923efaa678 100644 --- a/src/phases/select-starter-phase.ts +++ b/src/phases/select-starter-phase.ts @@ -4,11 +4,10 @@ import { Phase } from "#app/phase"; import { SpeciesFormChangeMoveLearnedTrigger } from "#data/form-change-triggers"; import { Gender } from "#data/gender"; import { ChallengeType } from "#enums/challenge-type"; -import type { SpeciesId } from "#enums/species-id"; import { UiMode } from "#enums/ui-mode"; import { overrideHeldItems, overrideModifiers } from "#modifiers/modifier"; -import { SaveSlotUiMode } from "#ui/save-slot-select-ui-handler"; -import type { Starter } from "#ui/starter-select-ui-handler"; +import type { Starter } from "#types/save-data"; +import { SaveSlotUiMode } from "#ui/handlers/save-slot-select-ui-handler"; import { applyChallenges } from "#utils/challenge-utils"; import { getPokemonSpecies } from "#utils/pokemon-utils"; import SoundFade from "phaser3-rex-plugins/plugins/soundfade"; @@ -44,33 +43,32 @@ export class SelectStarterPhase extends Phase { const loadPokemonAssets: Promise[] = []; starters.forEach((starter: Starter, i: number) => { if (!i && Overrides.STARTER_SPECIES_OVERRIDE) { - starter.species = getPokemonSpecies(Overrides.STARTER_SPECIES_OVERRIDE as SpeciesId); + starter.speciesId = Overrides.STARTER_SPECIES_OVERRIDE; } - const starterProps = globalScene.gameData.getSpeciesDexAttrProps(starter.species, starter.dexAttr); - let starterFormIndex = Math.min(starterProps.formIndex, Math.max(starter.species.forms.length - 1, 0)); + const species = getPokemonSpecies(starter.speciesId); + let starterFormIndex = starter.formIndex; if ( - starter.species.speciesId in Overrides.STARTER_FORM_OVERRIDES - && Overrides.STARTER_FORM_OVERRIDES[starter.species.speciesId] != null - && starter.species.forms[Overrides.STARTER_FORM_OVERRIDES[starter.species.speciesId]!] + starter.speciesId in Overrides.STARTER_FORM_OVERRIDES + && Overrides.STARTER_FORM_OVERRIDES[starter.speciesId] != null + && species.forms[Overrides.STARTER_FORM_OVERRIDES[starter.speciesId]!] ) { - starterFormIndex = Overrides.STARTER_FORM_OVERRIDES[starter.species.speciesId]!; + starterFormIndex = Overrides.STARTER_FORM_OVERRIDES[starter.speciesId]!; } let starterGender = - starter.species.malePercent !== null ? (!starterProps.female ? Gender.MALE : Gender.FEMALE) : Gender.GENDERLESS; + species.malePercent !== null ? (!starter.female ? Gender.MALE : Gender.FEMALE) : Gender.GENDERLESS; if (Overrides.GENDER_OVERRIDE !== null) { starterGender = Overrides.GENDER_OVERRIDE; } - const starterIvs = globalScene.gameData.dexData[starter.species.speciesId].ivs.slice(0); const starterPokemon = globalScene.addPlayerPokemon( - starter.species, + species, globalScene.gameMode.getStartingLevel(), starter.abilityIndex, starterFormIndex, starterGender, - starterProps.shiny, - starterProps.variant, - starterIvs, + starter.shiny, + starter.variant, + starter.ivs, starter.nature, ); starter.moveset && starterPokemon.tryPopulateMoveset(starter.moveset); @@ -78,7 +76,7 @@ export class SelectStarterPhase extends Phase { starterPokemon.passive = true; } starterPokemon.luck = globalScene.gameData.getDexAttrLuck( - globalScene.gameData.dexData[starter.species.speciesId].caughtAttr, + globalScene.gameData.dexData[species.speciesId].caughtAttr, ); if (starter.pokerus) { starterPokemon.pokerus = true; diff --git a/src/phases/title-phase.ts b/src/phases/title-phase.ts index 1920db8d20e..9535ea1c8e9 100644 --- a/src/phases/title-phase.ts +++ b/src/phases/title-phase.ts @@ -19,6 +19,7 @@ import type { SessionSaveData } from "#types/save-data"; import type { OptionSelectConfig, OptionSelectItem } from "#ui/abstract-option-select-ui-handler"; import { SaveSlotUiMode } from "#ui/save-slot-select-ui-handler"; import { isLocal, isLocalServerConnected } from "#utils/common"; +import { getPokemonSpecies } from "#utils/pokemon-utils"; import i18next from "i18next"; export class TitlePhase extends Phase { @@ -218,23 +219,19 @@ export class TitlePhase extends Phase { const party = globalScene.getPlayerParty(); const loadPokemonAssets: Promise[] = []; for (const starter of starters) { - const starterProps = globalScene.gameData.getSpeciesDexAttrProps(starter.species, starter.dexAttr); - const starterFormIndex = Math.min(starterProps.formIndex, Math.max(starter.species.forms.length - 1, 0)); + const species = getPokemonSpecies(starter.speciesId); + const starterFormIndex = starter.formIndex; const starterGender = - starter.species.malePercent !== null - ? !starterProps.female - ? Gender.MALE - : Gender.FEMALE - : Gender.GENDERLESS; + species.malePercent !== null ? (starter.female ? Gender.FEMALE : Gender.MALE) : Gender.GENDERLESS; const starterPokemon = globalScene.addPlayerPokemon( - starter.species, + species, startingLevel, starter.abilityIndex, starterFormIndex, starterGender, - starterProps.shiny, - starterProps.variant, - undefined, + starter.shiny, + starter.variant, + starter.ivs, starter.nature, ); starterPokemon.setVisible(false); diff --git a/src/ui/handlers/starter-select-ui-handler.ts b/src/ui/handlers/starter-select-ui-handler.ts index 53e566864b1..8c3594546f1 100644 --- a/src/ui/handlers/starter-select-ui-handler.ts +++ b/src/ui/handlers/starter-select-ui-handler.ts @@ -49,7 +49,7 @@ import { achvs } from "#system/achv"; import { RibbonData } from "#system/ribbons/ribbon-data"; import { SettingKeyboard } from "#system/settings-keyboard"; import type { DexEntry } from "#types/dex-data"; -import type { DexAttrProps, StarterAttributes, StarterDataEntry, StarterMoveset } from "#types/save-data"; +import type { Starter, StarterAttributes, StarterDataEntry, StarterMoveset } from "#types/save-data"; import type { OptionSelectItem } from "#ui/abstract-option-select-ui-handler"; import { DropDown, DropDownLabel, DropDownOption, DropDownState, DropDownType, SortCriteria } from "#ui/dropdown"; import { FilterBar } from "#ui/filter-bar"; @@ -82,18 +82,6 @@ import type BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext"; export type StarterSelectCallback = (starters: Starter[]) => void; -export interface Starter { - species: PokemonSpecies; - dexAttr: bigint; - abilityIndex: number; - passive: boolean; - nature: Nature; - moveset?: StarterMoveset; - pokerus: boolean; - nickname?: string; - teraType?: PokemonType; -} - interface LanguageSetting { starterInfoTextSize: string; instructionTextSize: string; @@ -364,15 +352,13 @@ export class StarterSelectUiHandler extends MessageUiHandler { private allSpecies: PokemonSpecies[] = []; private lastSpecies: PokemonSpecies; private speciesLoaded: Map = new Map(); + + private starters: Starter[] = []; public starterSpecies: PokemonSpecies[] = []; private pokerusSpecies: PokemonSpecies[] = []; - private starterAttr: bigint[] = []; - private starterAbilityIndexes: number[] = []; - private starterNatures: Nature[] = []; - private starterTeras: PokemonType[] = []; - private starterMovesets: StarterMoveset[] = []; private speciesStarterDexEntry: DexEntry | null; private speciesStarterMoves: MoveId[]; + private canCycleShiny: boolean; private canCycleForm: boolean; private canCycleGender: boolean; @@ -2758,12 +2744,26 @@ export class StarterSelectUiHandler extends MessageUiHandler { props.variant, ); + const { dexEntry, starterDataEntry } = this.getSpeciesData(species.speciesId); + + const starter = { + speciesId: species.speciesId, + shiny: props.shiny, + variant: props.variant, + formIndex: props.formIndex, + female: props.female, + abilityIndex, + passive: !(starterDataEntry.passiveAttr ^ (PassiveAttr.ENABLED | PassiveAttr.UNLOCKED)), + nature, + moveset, + pokerus: this.pokerusSpecies.includes(species), + nickname: this.starterPreferences[species.speciesId]?.nickname, + teraType, + ivs: dexEntry.ivs, + }; + + this.starters.push(starter); this.starterSpecies.push(species); - this.starterAttr.push(dexAttr); - this.starterAbilityIndexes.push(abilityIndex); - this.starterNatures.push(nature); - this.starterTeras.push(teraType); - this.starterMovesets.push(moveset); if (this.speciesLoaded.get(species.speciesId) || randomSelection) { getPokemonSpeciesForm(species.speciesId, props.formIndex).cry(); } @@ -2833,7 +2833,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { for (const [index, species] of this.starterSpecies.entries()) { if (species.speciesId === id) { - this.starterMovesets[index] = this.starterMoveset; + this.starters[index].moveset = this.starterMoveset; } } } @@ -3640,20 +3640,20 @@ export class StarterSelectUiHandler extends MessageUiHandler { const starterIndex = this.starterSpecies.indexOf(species); - let props: DexAttrProps; + const props = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr); if (starterIndex > -1) { - props = globalScene.gameData.getSpeciesDexAttrProps(species, this.starterAttr[starterIndex]); + const starter = this.starters[starterIndex]; this.setSpeciesDetails( species, { - shiny: props.shiny, - formIndex: props.formIndex, - female: props.female, - variant: props.variant, - abilityIndex: this.starterAbilityIndexes[starterIndex], - natureIndex: this.starterNatures[starterIndex], - teraType: this.starterTeras[starterIndex], + shiny: starter.shiny, + formIndex: starter.formIndex, + female: starter.female, + variant: starter.variant, + abilityIndex: starter.abilityIndex, + natureIndex: starter.nature, + teraType: starter.teraType, }, false, ); @@ -3664,7 +3664,6 @@ export class StarterSelectUiHandler extends MessageUiHandler { const { dexEntry } = this.getSpeciesData(species.speciesId); const defaultNature = starterAttributes?.nature || globalScene.gameData.getSpeciesDefaultNature(species, dexEntry); - props = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr); if (starterAttributes?.variant && !Number.isNaN(starterAttributes.variant) && props.shiny) { props.variant = starterAttributes.variant as Variant; } @@ -3910,10 +3909,15 @@ export class StarterSelectUiHandler extends MessageUiHandler { const starterIndex = this.starterSpecies.indexOf(species); if (starterIndex > -1) { - this.starterAttr[starterIndex] = this.dexAttrCursor; - this.starterAbilityIndexes[starterIndex] = this.abilityCursor; - this.starterNatures[starterIndex] = this.natureCursor; - this.starterTeras[starterIndex] = this.teraCursor; + const starter = this.starters[starterIndex]; + const props = globalScene.gameData.getSpeciesDexAttrProps(species, this.dexAttrCursor); + starter.shiny = props.shiny; + starter.variant = props.variant; + starter.female = props.female; + starter.formIndex = props.formIndex; + starter.abilityIndex = this.abilityCursor; + starter.nature = this.natureCursor; + starter.teraType = this.teraCursor; } const assetLoadCancelled = new BooleanHolder(false); @@ -4215,11 +4219,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { popStarter(index: number): void { this.starterSpecies.splice(index, 1); - this.starterAttr.splice(index, 1); - this.starterAbilityIndexes.splice(index, 1); - this.starterNatures.splice(index, 1); - this.starterTeras.splice(index, 1); - this.starterMovesets.splice(index, 1); + this.starters.splice(index, 1); for (let s = 0; s < this.starterSpecies.length; s++) { const species = this.starterSpecies[s]; @@ -4443,27 +4443,11 @@ export class StarterSelectUiHandler extends MessageUiHandler { () => { const startRun = () => { globalScene.money = globalScene.gameMode.getStartingMoney(); + const starters = this.starters.slice(0); ui.setMode(UiMode.STARTER_SELECT); - const thisObj = this; const originalStarterSelectCallback = this.starterSelectCallback; this.starterSelectCallback = null; - originalStarterSelectCallback?.( - new Array(this.starterSpecies.length).fill(0).map((_, i) => { - const starterSpecies = thisObj.starterSpecies[i]; - const { starterDataEntry } = this.getSpeciesData(starterSpecies.speciesId); - return { - species: starterSpecies, - dexAttr: thisObj.starterAttr[i], - abilityIndex: thisObj.starterAbilityIndexes[i], - passive: !(starterDataEntry.passiveAttr ^ (PassiveAttr.ENABLED | PassiveAttr.UNLOCKED)), - nature: thisObj.starterNatures[i] as Nature, - teraType: thisObj.starterTeras[i] as PokemonType, - moveset: thisObj.starterMovesets[i], - pokerus: thisObj.pokerusSpecies.includes(starterSpecies), - nickname: thisObj.starterPreferences[starterSpecies.speciesId]?.nickname, - }; - }), - ); + originalStarterSelectCallback?.(starters); }; startRun(); }, @@ -4492,10 +4476,17 @@ export class StarterSelectUiHandler extends MessageUiHandler { */ isPartyValid(): boolean { let canStart = false; - for (const species of this.starterSpecies) { + for (let s = 0; s < this.starterSpecies.length; s++) { + const species = this.starterSpecies[s]; + const starter = this.starters[s]; const isValidForChallenge = checkStarterValidForChallenge( species, - globalScene.gameData.getSpeciesDexAttrProps(species, this.getCurrentDexProps(species.speciesId)), + { + formIndex: starter.formIndex, + shiny: starter.shiny, + variant: starter.variant, + female: starter.female ?? false, + }, false, ); canStart ||= isValidForChallenge; diff --git a/test/mystery-encounter/encounters/part-timer-encounter.test.ts b/test/mystery-encounter/encounters/part-timer-encounter.test.ts index 1826c75381a..15d2664364c 100644 --- a/test/mystery-encounter/encounters/part-timer-encounter.test.ts +++ b/test/mystery-encounter/encounters/part-timer-encounter.test.ts @@ -168,6 +168,7 @@ describe("Part-Timer - Mystery Encounter", () => { // Override party levels to 50 so stats can be fully reflective scene.getPlayerParty().forEach(p => { p.level = 50; + p.ivs = [0, 0, 0, 0, 0, 0]; p.calculateStats(); }); await runMysteryEncounterToEnd(game, 2, { pokemonNo: 3 }); diff --git a/test/test-utils/game-manager-utils.ts b/test/test-utils/game-manager-utils.ts index 26b7ccf1020..4be05bf0ddb 100644 --- a/test/test-utils/game-manager-utils.ts +++ b/test/test-utils/game-manager-utils.ts @@ -8,8 +8,7 @@ import { GameModes } from "#enums/game-modes"; import type { MoveId } from "#enums/move-id"; import type { SpeciesId } from "#enums/species-id"; import { PlayerPokemon } from "#field/pokemon"; -import type { StarterMoveset } from "#types/save-data"; -import type { Starter } from "#ui/starter-select-ui-handler"; +import type { Starter, StarterMoveset } from "#types/save-data"; import { getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils"; /** Function to convert Blob to string */ @@ -33,24 +32,24 @@ export function holdOn(ms: number) { return new Promise(resolve => setTimeout(resolve, ms)); } -export function generateStarter(scene: BattleScene, species?: SpeciesId[]): Starter[] { +export function generateStarters(scene: BattleScene, speciesIds?: SpeciesId[]): Starter[] { const seed = "test"; - const starters = getTestRunStarters(seed, species); + const starters = getTestRunStarters(seed, speciesIds); const startingLevel = scene.gameMode.getStartingLevel(); for (const starter of starters) { - const starterProps = scene.gameData.getSpeciesDexAttrProps(starter.species, starter.dexAttr); - const starterFormIndex = Math.min(starterProps.formIndex, Math.max(starter.species.forms.length - 1, 0)); + const species = getPokemonSpecies(starter.speciesId); + const starterFormIndex = starter.formIndex; const starterGender = - starter.species.malePercent !== null ? (!starterProps.female ? Gender.MALE : Gender.FEMALE) : Gender.GENDERLESS; + species.malePercent !== null ? (starter.female ? Gender.FEMALE : Gender.MALE) : Gender.GENDERLESS; const starterPokemon = scene.addPlayerPokemon( - starter.species, + species, startingLevel, starter.abilityIndex, starterFormIndex, starterGender, - starterProps.shiny, - starterProps.variant, - undefined, + starter.shiny, + starter.variant, + starter.ivs, starter.nature, ); const moveset: MoveId[] = []; @@ -62,20 +61,23 @@ export function generateStarter(scene: BattleScene, species?: SpeciesId[]): Star return starters; } -function getTestRunStarters(seed: string, species?: SpeciesId[]): Starter[] { - if (!species) { +function getTestRunStarters(seed: string, speciesIds?: SpeciesId[]): Starter[] { + if (!speciesIds || speciesIds.length === 0) { return getDailyRunStarters(seed); } const starters: Starter[] = []; const startingLevel = getGameMode(GameModes.CLASSIC).getStartingLevel(); - for (const specie of species) { - const starterSpeciesForm = getPokemonSpeciesForm(specie, 0); + for (const speciesId of speciesIds) { + const starterSpeciesForm = getPokemonSpeciesForm(speciesId, 0); const starterSpecies = getPokemonSpecies(starterSpeciesForm.speciesId); const pokemon = new PlayerPokemon(starterSpecies, startingLevel, undefined, 0); const starter: Starter = { - species: starterSpecies, - dexAttr: pokemon.getDexAttr(), + speciesId, + shiny: pokemon.shiny, + variant: pokemon.variant, + formIndex: pokemon.formIndex, + ivs: pokemon.ivs, abilityIndex: pokemon.abilityIndex, passive: false, nature: pokemon.getNature(), @@ -89,22 +91,20 @@ function getTestRunStarters(seed: string, species?: SpeciesId[]): Starter[] { /** * Useful for populating party, wave index, etc. without having to spin up and run through an entire EncounterPhase */ -export function initSceneWithoutEncounterPhase(scene: BattleScene, species?: SpeciesId[]): void { - const starters = generateStarter(scene, species); +export function initSceneWithoutEncounterPhase(scene: BattleScene, speciesIds?: SpeciesId[]): void { + const starters = generateStarters(scene, speciesIds); starters.forEach(starter => { - const starterProps = scene.gameData.getSpeciesDexAttrProps(starter.species, starter.dexAttr); - const starterFormIndex = Math.min(starterProps.formIndex, Math.max(starter.species.forms.length - 1, 0)); + const starterFormIndex = starter.formIndex; const starterGender = Gender.MALE; - const starterIvs = scene.gameData.dexData[starter.species.speciesId].ivs.slice(0); const starterPokemon = scene.addPlayerPokemon( - starter.species, + getPokemonSpecies(starter.speciesId), scene.gameMode.getStartingLevel(), starter.abilityIndex, starterFormIndex, starterGender, - starterProps.shiny, - starterProps.variant, - starterIvs, + starter.shiny, + starter.variant, + starter.ivs, starter.nature, ); starter.moveset && starterPokemon.tryPopulateMoveset(starter.moveset); diff --git a/test/test-utils/game-manager.ts b/test/test-utils/game-manager.ts index f681846d935..abe0b8cfcf6 100644 --- a/test/test-utils/game-manager.ts +++ b/test/test-utils/game-manager.ts @@ -29,7 +29,7 @@ import { TurnEndPhase } from "#phases/turn-end-phase"; import { TurnInitPhase } from "#phases/turn-init-phase"; import { TurnStartPhase } from "#phases/turn-start-phase"; import { ErrorInterceptor } from "#test/test-utils/error-interceptor"; -import { generateStarter } from "#test/test-utils/game-manager-utils"; +import { generateStarters } from "#test/test-utils/game-manager-utils"; import { GameWrapper } from "#test/test-utils/game-wrapper"; import { ChallengeModeHelper } from "#test/test-utils/helpers/challenge-mode-helper"; import { ClassicModeHelper } from "#test/test-utils/helpers/classic-mode-helper"; @@ -215,7 +215,7 @@ export class GameManager { this.onNextPrompt("TitlePhase", UiMode.TITLE, () => { this.scene.gameMode = getGameMode(mode); - const starters = generateStarter(this.scene, species); + const starters = generateStarters(this.scene, species); const selectStarterPhase = new SelectStarterPhase(); this.scene.phaseManager.pushPhase(new EncounterPhase(false)); selectStarterPhase.initBattle(starters); @@ -251,7 +251,7 @@ export class GameManager { UiMode.TITLE, () => { this.scene.gameMode = getGameMode(GameModes.CLASSIC); - const starters = generateStarter(this.scene, species); + const starters = generateStarters(this.scene, species); const selectStarterPhase = new SelectStarterPhase(); this.scene.phaseManager.pushPhase(new EncounterPhase(false)); selectStarterPhase.initBattle(starters); diff --git a/test/test-utils/helpers/challenge-mode-helper.ts b/test/test-utils/helpers/challenge-mode-helper.ts index 7bc40aec035..a1fe114013d 100644 --- a/test/test-utils/helpers/challenge-mode-helper.ts +++ b/test/test-utils/helpers/challenge-mode-helper.ts @@ -9,7 +9,7 @@ import { CommandPhase } from "#phases/command-phase"; import { EncounterPhase } from "#phases/encounter-phase"; import { SelectStarterPhase } from "#phases/select-starter-phase"; import { TurnInitPhase } from "#phases/turn-init-phase"; -import { generateStarter } from "#test/test-utils/game-manager-utils"; +import { generateStarters } from "#test/test-utils/game-manager-utils"; import { GameManagerHelper } from "#test/test-utils/helpers/game-manager-helper"; /** @@ -34,7 +34,7 @@ export class ChallengeModeHelper extends GameManagerHelper { * @param gameMode - Optional game mode to set. * @returns A promise that resolves when the summon phase is reached. */ - async runToSummon(species?: SpeciesId[]) { + async runToSummon(speciesIds?: SpeciesId[]) { await this.game.runToTitle(); if (this.game.override.disableShinies) { @@ -43,7 +43,7 @@ export class ChallengeModeHelper extends GameManagerHelper { this.game.onNextPrompt("TitlePhase", UiMode.TITLE, () => { this.game.scene.gameMode.challenges = this.challenges; - const starters = generateStarter(this.game.scene, species); + const starters = generateStarters(this.game.scene, speciesIds); const selectStarterPhase = new SelectStarterPhase(); this.game.scene.phaseManager.pushPhase(new EncounterPhase(false)); selectStarterPhase.initBattle(starters); diff --git a/test/test-utils/helpers/classic-mode-helper.ts b/test/test-utils/helpers/classic-mode-helper.ts index f813a8f797e..896de7a8b6f 100644 --- a/test/test-utils/helpers/classic-mode-helper.ts +++ b/test/test-utils/helpers/classic-mode-helper.ts @@ -10,7 +10,7 @@ import { CommandPhase } from "#phases/command-phase"; import { EncounterPhase } from "#phases/encounter-phase"; import { SelectStarterPhase } from "#phases/select-starter-phase"; import { TurnInitPhase } from "#phases/turn-init-phase"; -import { generateStarter } from "#test/test-utils/game-manager-utils"; +import { generateStarters } from "#test/test-utils/game-manager-utils"; import { GameManagerHelper } from "#test/test-utils/helpers/game-manager-helper"; /** @@ -35,7 +35,7 @@ export class ClassicModeHelper extends GameManagerHelper { // biome-ignore lint/style/useUnifiedTypeSignatures: Marks the overload for deprecation async runToSummon(): Promise; async runToSummon(species: SpeciesId[] | undefined): Promise; - async runToSummon(species?: SpeciesId[]): Promise { + async runToSummon(speciesIds?: SpeciesId[]): Promise { await this.game.runToTitle(); if (this.game.override.disableShinies) { @@ -50,7 +50,7 @@ export class ClassicModeHelper extends GameManagerHelper { this.game.onNextPrompt("TitlePhase", UiMode.TITLE, () => { this.game.scene.gameMode = getGameMode(GameModes.CLASSIC); - const starters = generateStarter(this.game.scene, species); + const starters = generateStarters(this.game.scene, speciesIds); const selectStarterPhase = new SelectStarterPhase(); this.game.scene.phaseManager.pushPhase(new EncounterPhase(false)); selectStarterPhase.initBattle(starters); From d3edfb25a1f0868fe58107b1fd3a438f1435aca2 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Sun, 21 Sep 2025 14:04:57 -0500 Subject: [PATCH 39/40] [Bug][Ability] Queenly Majesty flyout during move select (#6569) --- src/field/pokemon.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index ea7c74904d8..06e5e0d85aa 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -2466,14 +2466,15 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { if (!cancelledHolder.value) { const defendingSidePlayField = this.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField(); - defendingSidePlayField.forEach(p => + defendingSidePlayField.forEach((p: (typeof defendingSidePlayField)[0]) => { applyAbAttrs("FieldPriorityMoveImmunityAbAttr", { pokemon: p, opponent: source, move, cancelled: cancelledHolder, - }), - ); + simulated, + }); + }); } } @@ -2494,7 +2495,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { typeMultiplier.value = 0; } - return (!cancelledHolder.value ? typeMultiplier.value : 0) as TypeDamageMultiplier; + return (cancelledHolder.value ? 0 : typeMultiplier.value) as TypeDamageMultiplier; } /** From 1c9843618ca852f736a03ede807eae69b7c64821 Mon Sep 17 00:00:00 2001 From: Dean <69436131+emdeann@users.noreply.github.com> Date: Sun, 21 Sep 2025 12:05:13 -0700 Subject: [PATCH 40/40] [Misc] Blacklist more dynamic phases (#6585) --- src/dynamic-queue-manager.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/dynamic-queue-manager.ts b/src/dynamic-queue-manager.ts index 7356f67bc1d..7c65a79d743 100644 --- a/src/dynamic-queue-manager.ts +++ b/src/dynamic-queue-manager.ts @@ -23,6 +23,11 @@ const nonDynamicPokemonPhases: readonly PhaseString[] = [ "VictoryPhase", "PokemonHealPhase", "WeatherEffectPhase", + "ShowAbilityPhase", + "HideAbilityPhase", + "ExpPhase", + "ShowPartyExpBarPhase", + "HidePartyExpBarPhase", ] as const; /**