diff --git a/package-lock.json b/package-lock.json index 5cf71701bda..1f6fc6e8967 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1512,224 +1512,208 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.1.tgz", - "integrity": "sha512-lncuC4aHicncmbORnx+dUaAgzee9cm/PbIqgWz1PpXuwc+sa1Ct83tnqUDy/GFKleLiN7ZIeytM6KJ4cAn1SxA==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz", + "integrity": "sha512-Fxamp4aEZnfPOcGA8KSNEohV8hX7zVHOemC8jVBoBUHu5zpJK/Eu3uJwt6BMgy9fkvzxDaurgj96F/NiLukF2w==", "cpu": [ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.1.tgz", - "integrity": "sha512-F/tkdw0WSs4ojqz5Ovrw5r9odqzFjb5LIgHdHZG65dFI1lWTWRVy32KDJLKRISHgJvqUeUhdIvy43fX41znyDg==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.4.tgz", + "integrity": "sha512-VXoK5UMrgECLYaMuGuVTOx5kcuap1Jm8g/M83RnCHBKOqvPPmROFJGQaZhGccnsFtfXQ3XYa4/jMCJvZnbJBdA==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.1.tgz", - "integrity": "sha512-vk+ma8iC1ebje/ahpxpnrfVQJibTMyHdWpOGZ3JpQ7Mgn/3QNHmPq7YwjZbIE7km73dH5M1e6MRRsnEBW7v5CQ==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.4.tgz", + "integrity": "sha512-xMM9ORBqu81jyMKCDP+SZDhnX2QEVQzTcC6G18KlTQEzWK8r/oNZtKuZaCcHhnsa6fEeOBionoyl5JsAbE/36Q==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.1.tgz", - "integrity": "sha512-IgpzXKauRe1Tafcej9STjSSuG0Ghu/xGYH+qG6JwsAUxXrnkvNHcq/NL6nz1+jzvWAnQkuAJ4uIwGB48K9OCGA==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.4.tgz", + "integrity": "sha512-aJJyYKQwbHuhTUrjWjxEvGnNNBCnmpHDvrb8JFDbeSH3m2XdHcxDd3jthAzvmoI8w/kSjd2y0udT+4okADsZIw==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.1.tgz", - "integrity": "sha512-P9bSiAUnSSM7EmyRK+e5wgpqai86QOSv8BwvkGjLwYuOpaeomiZWifEos517CwbG+aZl1T4clSE1YqqH2JRs+g==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.4.tgz", + "integrity": "sha512-j63YtCIRAzbO+gC2L9dWXRh5BFetsv0j0va0Wi9epXDgU/XUi5dJKo4USTttVyK7fGw2nPWK0PbAvyliz50SCQ==", "cpu": [ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.1.tgz", - "integrity": "sha512-5RnjpACoxtS+aWOI1dURKno11d7krfpGDEn19jI8BuWmSBbUC4ytIADfROM1FZrFhQPSoP+KEa3NlEScznBTyQ==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.4.tgz", + "integrity": "sha512-dJnWUgwWBX1YBRsuKKMOlXCzh2Wu1mlHzv20TpqEsfdZLb3WoJW2kIEsGwLkroYf24IrPAvOT/ZQ2OYMV6vlrg==", "cpu": [ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.1.tgz", - "integrity": "sha512-8mwmGD668m8WaGbthrEYZ9CBmPug2QPGWxhJxh/vCgBjro5o96gL04WLlg5BA233OCWLqERy4YUzX3bJGXaJgQ==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.4.tgz", + "integrity": "sha512-AdPRoNi3NKVLolCN/Sp4F4N1d98c4SBnHMKoLuiG6RXgoZ4sllseuGioszumnPGmPM2O7qaAX/IJdeDU8f26Aw==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.1.tgz", - "integrity": "sha512-dJX9u4r4bqInMGOAQoGYdwDP8lQiisWb9et+T84l2WXk41yEej8v2iGKodmdKimT8cTAYt0jFb+UEBxnPkbXEQ==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.4.tgz", + "integrity": "sha512-Gl0AxBtDg8uoAn5CCqQDMqAx22Wx22pjDOjBdmG0VIWX3qUBHzYmOKh8KXHL4UpogfJ14G4wk16EQogF+v8hmA==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.1.tgz", - "integrity": "sha512-V72cXdTl4EI0x6FNmho4D502sy7ed+LuVW6Ym8aI6DRQ9hQZdp5sj0a2usYOlqvFBNKQnLQGwmYnujo2HvjCxQ==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.4.tgz", + "integrity": "sha512-3aVCK9xfWW1oGQpTsYJJPF6bfpWfhbRnhdlyhak2ZiyFLDaayz0EP5j9V1RVLAAxlmWKTDfS9wyRyY3hvhPoOg==", "cpu": [ "ppc64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.1.tgz", - "integrity": "sha512-f+pJih7sxoKmbjghrM2RkWo2WHUW8UbfxIQiWo5yeCaCM0TveMEuAzKJte4QskBp1TIinpnRcxkquY+4WuY/tg==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.4.tgz", + "integrity": "sha512-ePYIir6VYnhgv2C5Xe9u+ico4t8sZWXschR6fMgoPUK31yQu7hTEJb7bCqivHECwIClJfKgE7zYsh1qTP3WHUA==", "cpu": [ "riscv64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.1.tgz", - "integrity": "sha512-qb1hMMT3Fr/Qz1OKovCuUM11MUNLUuHeBC2DPPAWUYYUAOFWaxInaTwTQmc7Fl5La7DShTEpmYwgdt2hG+4TEg==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.4.tgz", + "integrity": "sha512-GqFJ9wLlbB9daxhVlrTe61vJtEY99/xB3C8e4ULVsVfflcpmR6c8UZXjtkMA6FhNONhj2eA5Tk9uAVw5orEs4Q==", "cpu": [ "s390x" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.1.tgz", - "integrity": "sha512-7O5u/p6oKUFYjRbZkL2FLbwsyoJAjyeXHCU3O4ndvzg2OFO2GinFPSJFGbiwFDaCFc+k7gs9CF243PwdPQFh5g==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.4.tgz", + "integrity": "sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.1.tgz", - "integrity": "sha512-pDLkYITdYrH/9Cv/Vlj8HppDuLMDUBmgsM0+N+xLtFd18aXgM9Nyqupb/Uw+HeidhfYg2lD6CXvz6CjoVOaKjQ==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.4.tgz", + "integrity": "sha512-UV6FZMUgePDZrFjrNGIWzDo/vABebuXBhJEqrHxrGiU6HikPy0Z3LfdtciIttEUQfuDdCn8fqh7wiFJjCNwO+g==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.1.tgz", - "integrity": "sha512-W2ZNI323O/8pJdBGil1oCauuCzmVd9lDmWBBqxYZcOqWD6aWqJtVBQ1dFrF4dYpZPks6F+xCZHfzG5hYlSHZ6g==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.4.tgz", + "integrity": "sha512-BjI+NVVEGAXjGWYHz/vv0pBqfGoUH0IGZ0cICTn7kB9PyjrATSkX+8WkguNjWoj2qSr1im/+tTGRaY+4/PdcQw==", "cpu": [ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.1.tgz", - "integrity": "sha512-ELfEX1/+eGZYMaCIbK4jqLxO1gyTSOIlZr6pbC4SRYFaSIDVKOnZNMdoZ+ON0mrFDp4+H5MhwNC1H/AhE3zQLg==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.4.tgz", + "integrity": "sha512-SiWG/1TuUdPvYmzmYnmd3IEifzR61Tragkbx9D3+R8mzQqDBz8v+BvZNDlkiTtI9T15KYZhP0ehn3Dld4n9J5g==", "cpu": [ "ia32" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.1.tgz", - "integrity": "sha512-yjk2MAkQmoaPYCSu35RLJ62+dz358nE83VfTePJRp8CG7aMg25mEJYpXFiD+NcevhX8LxD5OP5tktPXnXN7GDw==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.4.tgz", + "integrity": "sha512-j8pPKp53/lq9lMXN57S8cFz0MynJk8OWNuUnXct/9KCpKU7DgU3bYMJhwWmcqC0UU29p8Lr0/7KEVcaM6bf47Q==", "cpu": [ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" @@ -5448,9 +5432,9 @@ } }, "node_modules/rollup": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.1.tgz", - "integrity": "sha512-Elx2UT8lzxxOXMpy5HWQGZqkrQOtrVDDa/bm9l10+U4rQnVzbL/LgZ4NOM1MPIDyHk69W4InuYDF5dzRh4Kw1A==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz", + "integrity": "sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==", "dev": true, "dependencies": { "@types/estree": "1.0.5" @@ -5463,22 +5447,22 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.18.1", - "@rollup/rollup-android-arm64": "4.18.1", - "@rollup/rollup-darwin-arm64": "4.18.1", - "@rollup/rollup-darwin-x64": "4.18.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.18.1", - "@rollup/rollup-linux-arm-musleabihf": "4.18.1", - "@rollup/rollup-linux-arm64-gnu": "4.18.1", - "@rollup/rollup-linux-arm64-musl": "4.18.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.18.1", - "@rollup/rollup-linux-riscv64-gnu": "4.18.1", - "@rollup/rollup-linux-s390x-gnu": "4.18.1", - "@rollup/rollup-linux-x64-gnu": "4.18.1", - "@rollup/rollup-linux-x64-musl": "4.18.1", - "@rollup/rollup-win32-arm64-msvc": "4.18.1", - "@rollup/rollup-win32-ia32-msvc": "4.18.1", - "@rollup/rollup-win32-x64-msvc": "4.18.1", + "@rollup/rollup-android-arm-eabi": "4.22.4", + "@rollup/rollup-android-arm64": "4.22.4", + "@rollup/rollup-darwin-arm64": "4.22.4", + "@rollup/rollup-darwin-x64": "4.22.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.22.4", + "@rollup/rollup-linux-arm-musleabihf": "4.22.4", + "@rollup/rollup-linux-arm64-gnu": "4.22.4", + "@rollup/rollup-linux-arm64-musl": "4.22.4", + "@rollup/rollup-linux-powerpc64le-gnu": "4.22.4", + "@rollup/rollup-linux-riscv64-gnu": "4.22.4", + "@rollup/rollup-linux-s390x-gnu": "4.22.4", + "@rollup/rollup-linux-x64-gnu": "4.22.4", + "@rollup/rollup-linux-x64-musl": "4.22.4", + "@rollup/rollup-win32-arm64-msvc": "4.22.4", + "@rollup/rollup-win32-ia32-msvc": "4.22.4", + "@rollup/rollup-win32-x64-msvc": "4.22.4", "fsevents": "~2.3.2" } }, diff --git a/public/locales/de/battler-tags.json b/public/locales/de/battler-tags.json index 2f8a8d0c438..6cc0e93ffb2 100644 --- a/public/locales/de/battler-tags.json +++ b/public/locales/de/battler-tags.json @@ -71,6 +71,7 @@ "disabledOnAdd": " {{moveName}} von {{pokemonNameWithAffix}} wurde blockiert!", "disabledLapse": "{{moveName}} von {{pokemonNameWithAffix}} ist nicht länger blockiert!", "tarShotOnAdd": "{{pokemonNameWithAffix}} ist nun schwach gegenüber Feuer-Attacken!", + "shedTailOnAdd": "{{pokemonNameWithAffix}} wirft seinen Schwanz ab, um eine Ablenkung zu schaffen!", "substituteOnAdd": "Ein Delegator von {{pokemonNameWithAffix}} ist erschienen!", "substituteOnHit": "Der Delegator steckt den Schlag für {{pokemonNameWithAffix}} ein!", "substituteOnRemove": "Der Delegator von {{pokemonNameWithAffix}} hört auf zu wirken!" diff --git a/public/locales/en/battler-tags.json b/public/locales/en/battler-tags.json index 481f69db250..520ac2a6202 100644 --- a/public/locales/en/battler-tags.json +++ b/public/locales/en/battler-tags.json @@ -71,8 +71,12 @@ "disabledOnAdd": "{{pokemonNameWithAffix}}'s {{moveName}}\nwas disabled!", "disabledLapse": "{{pokemonNameWithAffix}}'s {{moveName}}\nis no longer disabled.", "tarShotOnAdd": "{{pokemonNameWithAffix}} became weaker to fire!", + "shedTailOnAdd": "{{pokemonNameWithAffix}} shed its tail to create a decoy!", "substituteOnAdd": "{{pokemonNameWithAffix}} put in a substitute!", "substituteOnHit": "The substitute took damage for {{pokemonNameWithAffix}}!", "substituteOnRemove": "{{pokemonNameWithAffix}}'s substitute faded!", + "tormentOnAdd": "{{pokemonNameWithAffix}} was subjected to torment!", + "tauntOnAdd": "{{pokemonNameWithAffix}} fell for the taunt!", + "imprisonOnAdd": "{{pokemonNameWithAffix}} sealed the opponents move(s)!", "autotomizeOnAdd": "{{pokemonNameWithAffix}} became nimble!" } diff --git a/public/locales/es/battler-tags.json b/public/locales/es/battler-tags.json index bb4f0fe6c8a..b4d26f590c3 100644 --- a/public/locales/es/battler-tags.json +++ b/public/locales/es/battler-tags.json @@ -71,6 +71,7 @@ "disabledOnAdd": "¡Se ha anulado el movimiento {{moveName}}\nde {{pokemonNameWithAffix}}!", "disabledLapse": "¡El movimiento {{moveName}} de {{pokemonNameWithAffix}} ya no está anulado!", "tarShotOnAdd": "¡{{pokemonNameWithAffix}} se ha vuelto débil ante el fuego!", + "shedTailOnAdd": "{{pokemonNameWithAffix}} se desprende\nde un segmento de su cuerpo y lo usa comoseñuelo!", "substituteOnAdd": "¡{{pokemonNameWithAffix}} creó un sustituto!", "substituteOnHit": "¡El sustituto recibe daño en lugar del {{pokemonNameWithAffix}}!", "substituteOnRemove": "¡El sustituto del {{pokemonNameWithAffix}} se debilitó!" diff --git a/public/locales/fr/battler-tags.json b/public/locales/fr/battler-tags.json index 93b70490ed6..b52f688320e 100644 --- a/public/locales/fr/battler-tags.json +++ b/public/locales/fr/battler-tags.json @@ -71,6 +71,7 @@ "disabledOnAdd": "La capacité {{moveName}}\nde {{pokemonNameWithAffix}} est mise sous entrave !", "disabledLapse": "La capacité {{moveName}}\nde {{pokemonNameWithAffix}} n’est plus sous entrave !", "tarShotOnAdd": "{{pokemonNameWithAffix}} est maintenant\nvulnérable au feu !", + "shedTailOnAdd": "{{pokemonNameWithAffix}} détache\nsa queue pour créer un leurre !", "substituteOnAdd": "{{pokemonNameWithAffix}}\ncrée un clone !", "substituteOnHit": "Le clone subit les dégâts à la place\nde {{pokemonNameWithAffix}} !", "substituteOnRemove": "Le clone de {{pokemonNameWithAffix}}\ndisparait…" diff --git a/public/locales/it/battler-tags.json b/public/locales/it/battler-tags.json index 6ab69f4efa2..a7062c46677 100644 --- a/public/locales/it/battler-tags.json +++ b/public/locales/it/battler-tags.json @@ -71,6 +71,7 @@ "disabledOnAdd": "La mossa {{moveName}} di\n{{pokemonNameWithAffix}} è stata bloccata!", "disabledLapse": "La mossa {{moveName}} di\n{{pokemonNameWithAffix}} non è più bloccata!", "tarShotOnAdd": "{{pokemonNameWithAffix}} è diventato vulnerabile\nal tipo Fuoco!", + "shedTailOnAdd": "{{pokemonNameWithAffix}} si taglia\nla coda e ne fa un sostituto!", "substituteOnAdd": "Appare un sostituto di {{pokemonNameWithAffix}}!", "substituteOnHit": "Il sostituto viene colpito al posto di {{pokemonNameWithAffix}}!", "substituteOnRemove": "Il sostituto di {{pokemonNameWithAffix}} svanisce!" diff --git a/public/locales/ja/battler-tags.json b/public/locales/ja/battler-tags.json index f8c6d44c0b4..fef8def1328 100644 --- a/public/locales/ja/battler-tags.json +++ b/public/locales/ja/battler-tags.json @@ -70,5 +70,6 @@ "stockpilingOnAdd": "{{pokemonNameWithAffix}}は {{stockpiledCount}}つ たくわえた!", "disabledOnAdd": "{{pokemonNameWithAffix}}の\n{{moveName}}\nを 封じこめた!", "disabledLapse": "{{pokemonNameWithAffix}}の\nかなしばりが 解けた!", - "tarShotOnAdd": "{{pokemonNameWithAffix}}は ほのおに 弱くなった!" + "tarShotOnAdd": "{{pokemonNameWithAffix}}は ほのおに 弱くなった!", + "shedTailOnAdd": "{{pokemonNameWithAffix}}は\nしっぽを 切って みがわりにした!" } diff --git a/public/locales/ko/battler-tags.json b/public/locales/ko/battler-tags.json index 1cd6c86377e..53fab7d10ba 100644 --- a/public/locales/ko/battler-tags.json +++ b/public/locales/ko/battler-tags.json @@ -71,6 +71,7 @@ "disabledOnAdd": "{{pokemonNameWithAffix}}의 {{moveName}}[[는]]\n사용할 수 없다!", "disabledLapse": "{{pokemonNameWithAffix}}의 {{moveName}}[[는]]\n이제 사용할 수 있다.", "tarShotOnAdd": "{{pokemonNameWithAffix}}[[는]] 불꽃에 약해졌다!", + "shedTailOnAdd": "{{pokemonNameWithAffix}}[[는]] 꼬리를 잘라 대타로 삼았다!", "substituteOnAdd": "{{pokemonNameWithAffix}}의\n대타가 나타났다!", "substituteOnHit": "{{pokemonNameWithAffix}}[[를]] 대신하여\n대타가 공격을 받았다!", "substituteOnRemove": "{{pokemonNameWithAffix}}의\n대타는 사라져 버렸다..." diff --git a/public/locales/zh_CN/battler-tags.json b/public/locales/zh_CN/battler-tags.json index a7859380b7a..565fa8e754e 100644 --- a/public/locales/zh_CN/battler-tags.json +++ b/public/locales/zh_CN/battler-tags.json @@ -71,6 +71,7 @@ "disabledOnAdd": "封住了{{pokemonNameWithAffix}}的\n{{moveName}}!", "disabledLapse": "{{pokemonNameWithAffix}}的\n定身法解除了!", "tarShotOnAdd": "{{pokemonNameWithAffix}}\n变得怕火了!", + "shedTailOnAdd": "{{pokemonNameWithAffix}}\n断掉尾巴并将其作为替身了!", "substituteOnAdd": "{{pokemonNameWithAffix}}的\n替身出现了!", "substituteOnHit": "替身代替{{pokemonNameWithAffix}}\n承受了攻击!", "substituteOnRemove": "{{pokemonNameWithAffix}}的\n替身消失了……" diff --git a/public/locales/zh_TW/battler-tags.json b/public/locales/zh_TW/battler-tags.json index 49b19f5efdc..d6cb5d0aa43 100644 --- a/public/locales/zh_TW/battler-tags.json +++ b/public/locales/zh_TW/battler-tags.json @@ -71,6 +71,7 @@ "disabledOnAdd": "封住了{{pokemonNameWithAffix}}的\n{moveName}}!", "disabledLapse": "{{pokemonNameWithAffix}}的\n定身法解除了!", "tarShotOnAdd": "{{pokemonNameWithAffix}}\n變得怕火了!", + "shedTailOnAdd": "{{pokemonNameWithAffix}}\n截斷尾巴,把它做成了替身!", "substituteOnAdd": "{{pokemonNameWithAffix}}的\n替身出現了!", "substituteOnHit": "替身代替{{pokemonNameWithAffix}}承受了攻擊!", "substituteOnRemove": "{{pokemonNameWithAffix}}的\n替身消失了……" diff --git a/src/data/ability.ts b/src/data/ability.ts index 9e9c423623d..3ace872de3c 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -2026,6 +2026,7 @@ export class PostSummonAbAttr extends AbAttr { return false; } } + /** * Removes specified arena tags when a Pokemon is summoned. */ @@ -2852,17 +2853,17 @@ export class PreApplyBattlerTagAbAttr extends AbAttr { * Provides immunity to BattlerTags {@linkcode BattlerTag} to specified targets. */ export class PreApplyBattlerTagImmunityAbAttr extends PreApplyBattlerTagAbAttr { - private immuneTagType: BattlerTagType; + private immuneTagTypes: BattlerTagType[]; private battlerTag: BattlerTag; - constructor(immuneTagType: BattlerTagType) { + constructor(immuneTagTypes: BattlerTagType | BattlerTagType[]) { super(); - this.immuneTagType = immuneTagType; + this.immuneTagTypes = Array.isArray(immuneTagTypes) ? immuneTagTypes : [immuneTagTypes]; } applyPreApplyBattlerTag(pokemon: Pokemon, passive: boolean, simulated: boolean, tag: BattlerTag, cancelled: Utils.BooleanHolder, args: any[]): boolean { - if (tag.tagType === this.immuneTagType) { + if (this.immuneTagTypes.includes(tag.tagType)) { cancelled.value = true; if (!simulated) { this.battlerTag = tag; @@ -4916,7 +4917,7 @@ export function initAbilities() { .attr(TypeImmunityHealAbAttr, Type.WATER) .ignorable(), new Ability(Abilities.OBLIVIOUS, 3) - .attr(BattlerTagImmunityAbAttr, BattlerTagType.INFATUATED) + .attr(BattlerTagImmunityAbAttr, [BattlerTagType.INFATUATED, BattlerTagType.TAUNT]) .attr(IntimidateImmunityAbAttr) .ignorable(), new Ability(Abilities.CLOUD_NINE, 3) @@ -5402,8 +5403,7 @@ export function initAbilities() { .attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonTeravolt", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })) .attr(MoveAbilityBypassAbAttr), new Ability(Abilities.AROMA_VEIL, 6) - .ignorable() - .unimplemented(), + .attr(UserFieldBattlerTagImmunityAbAttr, [BattlerTagType.INFATUATED, BattlerTagType.TAUNT, BattlerTagType.DISABLED, BattlerTagType.TORMENT, BattlerTagType.HEAL_BLOCK]), new Ability(Abilities.FLOWER_VEIL, 6) .ignorable() .unimplemented(), @@ -5885,7 +5885,6 @@ export function initAbilities() { .ignorable(), new Ability(Abilities.EARTH_EATER, 9) .attr(TypeImmunityHealAbAttr, Type.GROUND) - .partial() // Healing not blocked by Heal Block .ignorable(), new Ability(Abilities.MYCELIUM_MIGHT, 9) .attr(ChangeMovePriorityAbAttr, (pokemon, move) => move.category === MoveCategory.STATUS, -0.2) @@ -5900,8 +5899,7 @@ export function initAbilities() { .attr(PostSummonStatStageChangeAbAttr, [ Stat.EVA ], -1) .condition(getOncePerBattleCondition(Abilities.SUPERSWEET_SYRUP)), new Ability(Abilities.HOSPITALITY, 9) - .attr(PostSummonAllyHealAbAttr, 4, true) - .partial(), // Healing not blocked by Heal Block + .attr(PostSummonAllyHealAbAttr, 4, true), new Ability(Abilities.TOXIC_CHAIN, 9) .attr(PostAttackApplyStatusEffectAbAttr, false, 30, StatusEffect.TOXIC), new Ability(Abilities.EMBODY_ASPECT_TEAL, 9) diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index fdc32b75c19..8ffb3038629 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -1,14 +1,15 @@ -import { Arena } from "../field/arena"; -import { Type } from "./type"; -import * as Utils from "../utils"; -import { MoveCategory, allMoves, MoveTarget, IncrementMovePriorityAttr, applyMoveAttrs } from "./move"; -import { getPokemonNameWithAffix } from "../messages"; -import Pokemon, { HitResult, PokemonMove } from "../field/pokemon"; -import { StatusEffect } from "./status-effect"; -import { BattlerIndex } from "../battle"; -import { BlockNonDirectDamageAbAttr, ChangeMovePriorityAbAttr, ProtectStatAbAttr, applyAbAttrs } from "./ability"; +import { Arena } from "#app/field/arena"; +import BattleScene from "#app/battle-scene"; +import { Type } from "#app/data/type"; +import * as Utils from "#app/utils"; +import { MoveCategory, allMoves, MoveTarget, IncrementMovePriorityAttr, applyMoveAttrs } from "#app/data/move"; +import { getPokemonNameWithAffix } from "#app/messages"; +import Pokemon, { HitResult, PlayerPokemon, PokemonMove, EnemyPokemon } from "#app/field/pokemon"; +import { StatusEffect } from "#app/data/status-effect"; +import { BattlerIndex } from "#app/battle"; +import { BlockNonDirectDamageAbAttr, ChangeMovePriorityAbAttr, ProtectStatAbAttr, applyAbAttrs } from "#app/data/ability"; import { Stat } from "#enums/stat"; -import { CommonAnim, CommonBattleAnim } from "./battle-anims"; +import { CommonAnim, CommonBattleAnim } from "#app/data/battle-anims"; import i18next from "i18next"; import { Abilities } from "#enums/abilities"; import { ArenaTagType } from "#enums/arena-tag-type"; @@ -919,6 +920,77 @@ class SafeguardTag extends ArenaTag { } } +/** + * This arena tag facilitates the application of the move Imprison + * Imprison remains in effect as long as the source Pokemon is active and present on the field. + * Imprison will apply to any opposing Pokemon that switch onto the field as well. + */ +class ImprisonTag extends ArenaTrapTag { + private source: Pokemon; + + constructor(sourceId: number, side: ArenaTagSide) { + super(ArenaTagType.IMPRISON, Moves.IMPRISON, sourceId, side, 1); + } + + /** + * Helper function that retrieves the Pokemon effected + * @param {BattleScene} scene medium to retrieve the involved Pokemon + * @returns list of PlayerPokemon or EnemyPokemon on the field + */ + private retrieveField(scene: BattleScene): PlayerPokemon[] | EnemyPokemon[] { + if (!this.source.isPlayer()) { + return scene.getPlayerField() ?? []; + } + return scene.getEnemyField() ?? []; + } + + /** + * This function applies the effects of Imprison to the opposing Pokemon already present on the field. + * @param arena + */ + override onAdd({ scene }: Arena) { + this.source = scene.getPokemonById(this.sourceId!)!; + if (this.source) { + const party = this.retrieveField(scene); + party?.forEach((p: PlayerPokemon | EnemyPokemon ) => { + p.addTag(BattlerTagType.IMPRISON, 1, Moves.IMPRISON, this.sourceId); + }); + scene.queueMessage(i18next.t("battlerTags:imprisonOnAdd", {pokemonNameWithAffix: getPokemonNameWithAffix(this.source)})); + } + } + + /** + * Checks if the source Pokemon is still active on the field + * @param _arena + * @returns `true` if the source of the tag is still active on the field | `false` if not + */ + override lapse(_arena: Arena): boolean { + return this.source.isActive(true); + } + + /** + * This applies the effects of Imprison to any opposing Pokemon that switch into the field while the source Pokemon is still active + * @param {Pokemon} pokemon the Pokemon Imprison is applied to + * @returns `true` + */ + override activateTrap(pokemon: Pokemon): boolean { + if (this.source.isActive(true)) { + pokemon.addTag(BattlerTagType.IMPRISON, 1, Moves.IMPRISON, this.sourceId); + } + return true; + } + + /** + * When the arena tag is removed, it also attempts to remove any related Battler Tags if they haven't already been removed from the affected Pokemon + * @param arena + */ + override onRemove({ scene }: Arena): void { + const party = this.retrieveField(scene); + party?.forEach((p: PlayerPokemon | EnemyPokemon) => { + p.removeTag(BattlerTagType.IMPRISON); + }); + } +} export function getArenaTag(tagType: ArenaTagType, turnCount: integer, sourceMove: Moves | undefined, sourceId: integer, targetIndex?: BattlerIndex, side: ArenaTagSide = ArenaTagSide.BOTH): ArenaTag | null { switch (tagType) { @@ -967,6 +1039,8 @@ export function getArenaTag(tagType: ArenaTagType, turnCount: integer, sourceMov return new HappyHourTag(turnCount, sourceId, side); case ArenaTagType.SAFEGUARD: return new SafeguardTag(turnCount, sourceId, side); + case ArenaTagType.IMPRISON: + return new ImprisonTag(sourceId, side); default: return null; } diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index e92446ef5a2..579d068e882 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -3,7 +3,7 @@ import { getPokemonNameWithAffix } from "../messages"; import Pokemon, { MoveResult, HitResult } from "../field/pokemon"; import { StatusEffect } from "./status-effect"; import * as Utils from "../utils"; -import { ChargeAttr, MoveFlags, allMoves, MoveCategory, applyMoveAttrs, StatusCategoryOnAllyAttr, HealOnAllyAttr } from "./move"; +import { ChargeAttr, MoveFlags, allMoves, MoveCategory, applyMoveAttrs, StatusCategoryOnAllyAttr, HealOnAllyAttr, ConsecutiveUseDoublePowerAttr } from "./move"; import { Type } from "./type"; import { BlockNonDirectDamageAbAttr, FlinchEffectAbAttr, ReverseDrainAbAttr, applyAbAttrs, ProtectStatAbAttr } from "./ability"; import { TerrainType } from "./terrain"; @@ -2311,6 +2311,11 @@ export class AutotomizedTag extends BattlerTag { } } +/** + * Tag implementing the {@link https://bulbapedia.bulbagarden.net/wiki/Substitute_(doll)#Effect | Substitute Doll} effect, + * for use with the moves Substitute and Shed Tail. Pokemon with this tag deflect most forms of received attack damage + * onto the tag. This tag also grants immunity to most Status moves and several move effects. + */ export class SubstituteTag extends BattlerTag { /** The substitute's remaining HP. If HP is depleted, the Substitute fades. */ public hp: number; @@ -2330,7 +2335,11 @@ export class SubstituteTag extends BattlerTag { // Queue battle animation and message pokemon.scene.triggerPokemonBattleAnim(pokemon, PokemonAnimType.SUBSTITUTE_ADD); - pokemon.scene.queueMessage(i18next.t("battlerTags:substituteOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }), 1500); + if (this.sourceMove === Moves.SHED_TAIL) { + pokemon.scene.queueMessage(i18next.t("battlerTags:shedTailOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }), 1500); + } else { + pokemon.scene.queueMessage(i18next.t("battlerTags:substituteOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }), 1500); + } // Remove any binding effects from the user pokemon.findAndRemoveTags(tag => tag instanceof DamagingTrapTag); @@ -2437,6 +2446,150 @@ export class MysteryEncounterPostSummonTag extends BattlerTag { } } +/** + * Battle Tag that applies the move Torment to the target Pokemon + * Torment restricts the use of moves twice in a row. + * The tag is only removed if the target leaves the battle. + * Torment does not interrupt the move if the move is performed consecutively in the same turn and right after Torment is applied + */ +export class TormentTag extends MoveRestrictionBattlerTag { + private target: Pokemon; + + constructor(sourceId: number) { + super(BattlerTagType.TORMENT, BattlerTagLapseType.AFTER_MOVE, 1, Moves.TORMENT, sourceId); + } + + /** + * Adds the battler tag to the target Pokemon and defines the private class variable 'target' + * 'Target' is used to track the Pokemon's current status + * @param {Pokemon} pokemon the Pokemon tormented + */ + override onAdd(pokemon: Pokemon) { + super.onAdd(pokemon); + this.target = pokemon; + pokemon.scene.queueMessage(i18next.t("battlerTags:tormentOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }), 1500); + } + + /** + * Torment only ends when the affected Pokemon leaves the battle field + * @param {Pokemon} pokemon the Pokemon under the effects of Torment + * @param _tagType + * @returns `true` if still present | `false` if not + */ + override lapse(pokemon: Pokemon, _tagType: BattlerTagLapseType): boolean { + return !pokemon.isActive(true); + } + + /** + * This checks if the current move used is identical to the last used move with a {@linkcode MoveResult} of `SUCCESS`/`MISS` + * @param {Moves} move the move under investigation + * @returns `true` if there is valid consecutive usage | `false` if the moves are different from each other + */ + override isMoveRestricted(move: Moves): boolean { + const lastMove = this.target.getLastXMoves(1)[0]; + if ( !lastMove ) { + return false; + } + // This checks for locking / momentum moves like Rollout and Hydro Cannon + if the user is under the influence of BattlerTagType.FRENZY + // Because Uproar's unique behavior is not implemented, it does not check for Uproar. Torment has been marked as partial in moves.ts + const moveObj = allMoves[lastMove.move]; + const isUnaffected = moveObj.hasAttr(ConsecutiveUseDoublePowerAttr) || this.target.getTag(BattlerTagType.FRENZY) || moveObj.hasAttr(ChargeAttr); + const validLastMoveResult = (lastMove.result === MoveResult.SUCCESS) || (lastMove.result === MoveResult.MISS); + if (lastMove.move === move && validLastMoveResult && lastMove.move !== Moves.STRUGGLE && !isUnaffected) { + return true; + } + return false; + } + + override selectionDeniedText(_pokemon: Pokemon, move: Moves): string { + return i18next.t("battle:moveCannotBeSelected", { moveName: allMoves[move].name }); + } +} + +/** + * BattlerTag that applies the effects of Taunt to the target Pokemon + * Taunt restricts the use of status moves. + * The tag is removed after 4 turns. + */ +export class TauntTag extends MoveRestrictionBattlerTag { + constructor() { + super(BattlerTagType.TAUNT, [BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.AFTER_MOVE], 4, Moves.TAUNT); + } + + override onAdd(pokemon: Pokemon) { + super.onAdd(pokemon); + pokemon.scene.queueMessage(i18next.t("battlerTags:tauntOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }), 1500); + } + + /** + * Checks if a move is a status move and determines its restriction status on that basis + * @param {Moves} move the move under investigation + * @returns `true` if the move is a status move + */ + override isMoveRestricted(move: Moves): boolean { + return allMoves[move].category === MoveCategory.STATUS; + } + + override selectionDeniedText(_pokemon: Pokemon, move: Moves): string { + return i18next.t("battle:moveCannotBeSelected", { moveName: allMoves[move].name }); + } + + override interruptedText(pokemon: Pokemon, move: Moves): string { + return i18next.t("battle:disableInterruptedMove", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), moveName: allMoves[move].name }); + } +} + +/** + * BattlerTag that applies the effects of Imprison to the target Pokemon + * Imprison restricts the opposing side's usage of moves shared by the source-user of Imprison. + * The tag is only removed when the source-user is removed from the field. + */ +export class ImprisonTag extends MoveRestrictionBattlerTag { + private source: Pokemon | null; + + constructor(sourceId: number) { + super(BattlerTagType.IMPRISON, [BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.AFTER_MOVE], 1, Moves.IMPRISON, sourceId); + } + + override onAdd(pokemon: Pokemon) { + if (this.sourceId) { + this.source = pokemon.scene.getPokemonById(this.sourceId); + } + } + + /** + * Checks if the source of Imprison is still active + * @param _pokemon + * @param _lapseType + * @returns `true` if the source is still active + */ + override lapse(_pokemon: Pokemon, _lapseType: BattlerTagLapseType): boolean { + return this.source?.isActive(true) ?? false; + } + + /** + * Checks if the source of the tag has the parameter move in its moveset and that the source is still active + * @param {Moves} move the move under investigation + * @returns `false` if either condition is not met + */ + override isMoveRestricted(move: Moves): boolean { + if (this.source) { + const sourceMoveset = this.source.getMoveset().map(m => m!.moveId); + return sourceMoveset?.includes(move) && this.source.isActive(true); + } + return false; + } + + override selectionDeniedText(_pokemon: Pokemon, move: Moves): string { + return i18next.t("battle:moveCannotBeSelected", { moveName: allMoves[move].name }); + } + + override interruptedText(pokemon: Pokemon, move: Moves): string { + return i18next.t("battle:disableInterruptedMove", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), moveName: allMoves[move].name }); + } +} + + /** * Retrieves a {@linkcode BattlerTag} based on the provided tag type, turn count, source move, and source ID. * @@ -2604,6 +2757,12 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source return new MysteryEncounterPostSummonTag(); case BattlerTagType.HEAL_BLOCK: return new HealBlockTag(turnCount, sourceMove); + case BattlerTagType.TORMENT: + return new TormentTag(sourceId); + case BattlerTagType.TAUNT: + return new TauntTag(); + case BattlerTagType.IMPRISON: + return new ImprisonTag(sourceId); case BattlerTagType.NONE: default: return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId); diff --git a/src/data/move.ts b/src/data/move.ts index 8866e86f708..71d97e4fb5c 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -37,6 +37,7 @@ import { SwitchSummonPhase } from "#app/phases/switch-summon-phase"; import { SpeciesFormChangeRevertWeatherFormTrigger } from "./pokemon-forms"; import { GameMode } from "#app/game-mode"; import { applyChallenges, ChallengeType } from "./challenge"; +import { SwitchType } from "#enums/switch-type"; export enum MoveCategory { PHYSICAL, @@ -1476,8 +1477,13 @@ export class HalfSacrificialAttr extends MoveEffectAttr { * @see {@linkcode apply} */ export class AddSubstituteAttr extends MoveEffectAttr { - constructor() { + /** The ratio of the user's max HP that is required to apply this effect */ + private hpCost: number; + + constructor(hpCost: number = 0.25) { super(true); + + this.hpCost = hpCost; } /** @@ -1493,8 +1499,7 @@ export class AddSubstituteAttr extends MoveEffectAttr { return false; } - const hpCost = Math.floor(user.getMaxHp() / 4); - user.damageAndUpdate(hpCost, HitResult.OTHER, false, true, true); + user.damageAndUpdate(Math.floor(user.getMaxHp() * this.hpCost), HitResult.OTHER, false, true, true); user.addTag(BattlerTagType.SUBSTITUTE, 0, move.id, user.id); return true; } @@ -1507,7 +1512,7 @@ export class AddSubstituteAttr extends MoveEffectAttr { } getCondition(): MoveConditionFunc { - return (user, target, move) => !user.getTag(SubstituteTag) && user.hp > Math.floor(user.getMaxHp() / 4) && user.getMaxHp() > 1; + return (user, target, move) => !user.getTag(SubstituteTag) && user.hp > Math.floor(user.getMaxHp() * this.hpCost) && user.getMaxHp() > 1; } getFailedText(user: Pokemon, target: Pokemon, move: Move, cancelled: Utils.BooleanHolder): string | null { @@ -5151,9 +5156,9 @@ export class RevivalBlessingAttr extends MoveEffectAttr { if (user.scene.currentBattle.double && user.scene.getEnemyParty().length > 1) { const allyPokemon = user.getAlly(); if (slotIndex<=1) { - user.scene.unshiftPhase(new SwitchSummonPhase(user.scene, pokemon.getFieldIndex(), slotIndex, false, false, false)); + user.scene.unshiftPhase(new SwitchSummonPhase(user.scene, SwitchType.SWITCH, pokemon.getFieldIndex(), slotIndex, false, false)); } else if (allyPokemon.isFainted()) { - user.scene.unshiftPhase(new SwitchSummonPhase(user.scene, allyPokemon.getFieldIndex(), slotIndex, false, false, false)); + user.scene.unshiftPhase(new SwitchSummonPhase(user.scene, SwitchType.SWITCH, allyPokemon.getFieldIndex(), slotIndex, false, false)); } } resolve(true); @@ -5176,72 +5181,69 @@ export class RevivalBlessingAttr extends MoveEffectAttr { export class ForceSwitchOutAttr extends MoveEffectAttr { constructor( private selfSwitch: boolean = false, - private batonPass: boolean = false + private switchType: SwitchType = SwitchType.SWITCH ) { super(false, MoveEffectTrigger.POST_APPLY, false, true); } isBatonPass() { - return this.batonPass; + return this.switchType === SwitchType.BATON_PASS; } - // TODO: Why is this a Promise? - apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise { - return new Promise(resolve => { + apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { + // Check if the move category is not STATUS or if the switch out condition is not met + if (!this.getSwitchOutCondition()(user, target, move)) { + return false; + } - if (!this.getSwitchOutCondition()(user, target, move)) { - return resolve(false); + /** + * Move the switch out logic inside the conditional block + * This ensures that the switch out only happens when the conditions are met + */ + const switchOutTarget = this.selfSwitch ? user : target; + if (switchOutTarget instanceof PlayerPokemon) { + switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH); + + if (switchOutTarget.hp > 0) { + user.scene.prependToPhase(new SwitchPhase(user.scene, this.switchType, switchOutTarget.getFieldIndex(), true, true), MoveEndPhase); + return true; + } + return false; + } else if (user.scene.currentBattle.battleType !== BattleType.WILD) { + // Switch out logic for trainer battles + switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH); + + if (switchOutTarget.hp > 0) { + // for opponent switching out + user.scene.prependToPhase(new SwitchSummonPhase(user.scene, this.switchType, switchOutTarget.getFieldIndex(), + (user.scene.currentBattle.trainer ? user.scene.currentBattle.trainer.getNextSummonIndex((switchOutTarget as EnemyPokemon).trainerSlot) : 0), + false, false), MoveEndPhase); + } + } else { + // Switch out logic for everything else (eg: WILD battles) + switchOutTarget.leaveField(false); + + if (switchOutTarget.hp) { + user.scene.queueMessage(i18next.t("moveTriggers:fled", {pokemonName: getPokemonNameWithAffix(switchOutTarget)}), null, true, 500); + + // in double battles redirect potential moves off fled pokemon + if (switchOutTarget.scene.currentBattle.double) { + const allyPokemon = switchOutTarget.getAlly(); + switchOutTarget.scene.redirectPokemonMoves(switchOutTarget, allyPokemon); + } } - // Move the switch out logic inside the conditional block - // This ensures that the switch out only happens when the conditions are met - const switchOutTarget = this.selfSwitch ? user : target; - if (switchOutTarget instanceof PlayerPokemon) { - switchOutTarget.leaveField(!this.batonPass); - - if (switchOutTarget.hp > 0) { - user.scene.prependToPhase(new SwitchPhase(user.scene, switchOutTarget.getFieldIndex(), true, true), MoveEndPhase); - resolve(true); - } else { - resolve(false); - } - return; - } else if (user.scene.currentBattle.battleType !== BattleType.WILD) { - // Switch out logic for trainer battles - switchOutTarget.leaveField(!this.batonPass); - - if (switchOutTarget.hp > 0) { - // for opponent switching out - user.scene.prependToPhase(new SwitchSummonPhase(user.scene, switchOutTarget.getFieldIndex(), - (user.scene.currentBattle.trainer ? user.scene.currentBattle.trainer.getNextSummonIndex((switchOutTarget as EnemyPokemon).trainerSlot) : 0), - false, this.batonPass, false), MoveEndPhase); - } - } else { - // Switch out logic for everything else (eg: WILD battles) - switchOutTarget.leaveField(false); + if (!switchOutTarget.getAlly()?.isActive(true)) { + user.scene.clearEnemyHeldItemModifiers(); if (switchOutTarget.hp) { - user.scene.queueMessage(i18next.t("moveTriggers:fled", {pokemonName: getPokemonNameWithAffix(switchOutTarget)}), null, true, 500); - - // in double battles redirect potential moves off fled pokemon - if (switchOutTarget.scene.currentBattle.double) { - const allyPokemon = switchOutTarget.getAlly(); - switchOutTarget.scene.redirectPokemonMoves(switchOutTarget, allyPokemon); - } - } - - if (!switchOutTarget.getAlly()?.isActive(true)) { - user.scene.clearEnemyHeldItemModifiers(); - - if (switchOutTarget.hp) { - user.scene.pushPhase(new BattleEndPhase(user.scene)); - user.scene.pushPhase(new NewBattlePhase(user.scene)); - } + user.scene.pushPhase(new BattleEndPhase(user.scene)); + user.scene.pushPhase(new NewBattlePhase(user.scene)); } } + } - resolve(true); - }); + return true; } getCondition(): MoveConditionFunc { @@ -5270,7 +5272,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { } if (!player && user.scene.currentBattle.battleType === BattleType.WILD) { - if (this.batonPass) { + if (this.isBatonPass()) { return false; } // Don't allow wild opponents to flee on the boss stage since it can ruin a run early on @@ -5291,7 +5293,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { return -20; } let ret = this.selfSwitch ? Math.floor((1 - user.getHpRatio()) * 20) : super.getUserBenefitScore(user, target, move); - if (this.selfSwitch && this.batonPass) { + if (this.selfSwitch && this.isBatonPass()) { const statStageTotal = user.getStatStages().reduce((s: integer, total: integer) => total += s, 0); ret = ret / 2 + (Phaser.Tweens.Builders.GetEaseFunction("Sine.easeOut")(Math.min(Math.abs(statStageTotal), 10) / 10) * (statStageTotal >= 0 ? 10 : -10)); } @@ -5301,10 +5303,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { export class ChillyReceptionAttr extends ForceSwitchOutAttr { - - // using inherited constructor - - apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise { + apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { user.scene.arena.trySetWeather(WeatherType.SNOW, true); return super.apply(user, target, move, args); } @@ -7394,7 +7393,7 @@ export function initMoves() { new AttackMove(Moves.DRAGON_BREATH, Type.DRAGON, MoveCategory.SPECIAL, 60, 100, 20, 30, 0, 2) .attr(StatusEffectAttr, StatusEffect.PARALYSIS), new SelfStatusMove(Moves.BATON_PASS, Type.NORMAL, -1, 40, -1, 0, 2) - .attr(ForceSwitchOutAttr, true, true) + .attr(ForceSwitchOutAttr, true, SwitchType.BATON_PASS) .hidesUser(), new StatusMove(Moves.ENCORE, Type.NORMAL, 100, 5, -1, 0, 2) .attr(AddBattlerTagAttr, BattlerTagType.ENCORE, false, true) @@ -7507,7 +7506,8 @@ export function initMoves() { .target(MoveTarget.BOTH_SIDES), new StatusMove(Moves.TORMENT, Type.DARK, 100, 15, -1, 0, 3) .ignoresSubstitute() - .unimplemented(), + .partial() // Incomplete implementation because of Uproar's partial implementation + .attr(AddBattlerTagAttr, BattlerTagType.TORMENT, false, true, 1), new StatusMove(Moves.FLATTER, Type.DARK, 100, 15, -1, 0, 3) .attr(StatStageChangeAttr, [ Stat.SPATK ], 1) .attr(ConfuseAttr), @@ -7538,7 +7538,7 @@ export function initMoves() { .attr(AddBattlerTagAttr, BattlerTagType.CHARGED, true, false), new StatusMove(Moves.TAUNT, Type.DARK, 100, 20, -1, 0, 3) .ignoresSubstitute() - .unimplemented(), + .attr(AddBattlerTagAttr, BattlerTagType.TAUNT, false, true, 4), new StatusMove(Moves.HELPING_HAND, Type.NORMAL, -1, 20, -1, 5, 3) .attr(AddBattlerTagAttr, BattlerTagType.HELPING_HAND) .ignoresSubstitute() @@ -7581,9 +7581,9 @@ export function initMoves() { new StatusMove(Moves.SKILL_SWAP, Type.PSYCHIC, -1, 10, -1, 0, 3) .ignoresSubstitute() .attr(SwitchAbilitiesAttr), - new SelfStatusMove(Moves.IMPRISON, Type.PSYCHIC, -1, 10, -1, 0, 3) + new StatusMove(Moves.IMPRISON, Type.PSYCHIC, 100, 10, -1, 0, 3) .ignoresSubstitute() - .unimplemented(), + .attr(AddArenaTagAttr, ArenaTagType.IMPRISON, 1, true, false), new SelfStatusMove(Moves.REFRESH, Type.NORMAL, -1, 20, -1, 0, 3) .attr(HealStatusEffectAttr, true, StatusEffect.PARALYSIS, StatusEffect.POISON, StatusEffect.TOXIC, StatusEffect.BURN) .condition((user, target, move) => !!user.status && (user.status.effect === StatusEffect.PARALYSIS || user.status.effect === StatusEffect.POISON || user.status.effect === StatusEffect.TOXIC || user.status.effect === StatusEffect.BURN)), @@ -7819,7 +7819,7 @@ export function initMoves() { .makesContact(false) .target(MoveTarget.ATTACKER), new AttackMove(Moves.U_TURN, Type.BUG, MoveCategory.PHYSICAL, 70, 100, 20, -1, 0, 4) - .attr(ForceSwitchOutAttr, true, false), + .attr(ForceSwitchOutAttr, true), new AttackMove(Moves.CLOSE_COMBAT, Type.FIGHTING, MoveCategory.PHYSICAL, 120, 100, 5, -1, 0, 4) .attr(StatStageChangeAttr, [ Stat.DEF, Stat.SPDEF ], -1, true), new AttackMove(Moves.PAYBACK, Type.DARK, MoveCategory.PHYSICAL, 50, 100, 10, -1, 0, 4) @@ -8252,7 +8252,7 @@ export function initMoves() { new AttackMove(Moves.GRASS_PLEDGE, Type.GRASS, MoveCategory.SPECIAL, 80, 100, 10, -1, 0, 5) .partial(), new AttackMove(Moves.VOLT_SWITCH, Type.ELECTRIC, MoveCategory.SPECIAL, 70, 100, 20, -1, 0, 5) - .attr(ForceSwitchOutAttr, true, false), + .attr(ForceSwitchOutAttr, true), new AttackMove(Moves.STRUGGLE_BUG, Type.BUG, MoveCategory.SPECIAL, 50, 100, 20, 100, 0, 5) .attr(StatStageChangeAttr, [ Stat.SPATK ], -1) .target(MoveTarget.ALL_NEAR_ENEMIES), @@ -8420,7 +8420,7 @@ export function initMoves() { .target(MoveTarget.ALL_NEAR_ENEMIES), new StatusMove(Moves.PARTING_SHOT, Type.DARK, 100, 20, -1, 0, 6) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, false, null, true, true, MoveEffectTrigger.PRE_APPLY) - .attr(ForceSwitchOutAttr, true, false) + .attr(ForceSwitchOutAttr, true) .soundBased(), new StatusMove(Moves.TOPSY_TURVY, Type.DARK, -1, 20, -1, 0, 6) .attr(InvertStatsAttr), @@ -9175,7 +9175,7 @@ export function initMoves() { .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF ], 1) .target(MoveTarget.NEAR_ALLY), new AttackMove(Moves.FLIP_TURN, Type.WATER, MoveCategory.PHYSICAL, 60, 100, 20, -1, 0, 8) - .attr(ForceSwitchOutAttr, true, false), + .attr(ForceSwitchOutAttr, true), new AttackMove(Moves.TRIPLE_AXEL, Type.ICE, MoveCategory.PHYSICAL, 20, 90, 10, -1, 0, 8) .attr(MultiHitAttr, MultiHitType._3) .attr(MultiHitPowerIncrementAttr, 3) @@ -9502,10 +9502,11 @@ export function initMoves() { .attr(MovePowerMultiplierAttr, (user, target, move) => target.getAttackTypeEffectiveness(move.type, user) >= 2 ? 5461/4096 : 1) .makesContact(), new SelfStatusMove(Moves.SHED_TAIL, Type.NORMAL, -1, 10, -1, 0, 9) - .unimplemented(), + .attr(AddSubstituteAttr, 0.5) + .attr(ForceSwitchOutAttr, true, SwitchType.SHED_TAIL), new SelfStatusMove(Moves.CHILLY_RECEPTION, Type.ICE, -1, 10, -1, 0, 9) .attr(PreMoveMessageAttr, (user, move) => i18next.t("moveTriggers:chillyReception", {pokemonName: getPokemonNameWithAffix(user)})) - .attr(ChillyReceptionAttr, true, false), + .attr(ChillyReceptionAttr, true), new SelfStatusMove(Moves.TIDY_UP, Type.NORMAL, -1, 10, -1, 0, 9) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPD ], 1, true, null, true, true) .attr(RemoveArenaTrapAttr, true) diff --git a/src/enums/arena-tag-type.ts b/src/enums/arena-tag-type.ts index 1c79750c91a..c6f911cb493 100644 --- a/src/enums/arena-tag-type.ts +++ b/src/enums/arena-tag-type.ts @@ -23,5 +23,6 @@ export enum ArenaTagType { TAILWIND = "TAILWIND", HAPPY_HOUR = "HAPPY_HOUR", SAFEGUARD = "SAFEGUARD", - NO_CRIT = "NO_CRIT" + NO_CRIT = "NO_CRIT", + IMPRISON = "IMPRISON", } diff --git a/src/enums/battler-tag-type.ts b/src/enums/battler-tag-type.ts index d606ae319f7..9ed3b629746 100644 --- a/src/enums/battler-tag-type.ts +++ b/src/enums/battler-tag-type.ts @@ -82,4 +82,7 @@ export enum BattlerTagType { AUTOTOMIZED = "AUTOTOMIZED", MYSTERY_ENCOUNTER_POST_SUMMON = "MYSTERY_ENCOUNTER_POST_SUMMON", HEAL_BLOCK = "HEAL_BLOCK", + TORMENT = "TORMENT", + TAUNT = "TAUNT", + IMPRISON = "IMPRISON", } diff --git a/src/enums/switch-type.ts b/src/enums/switch-type.ts new file mode 100644 index 00000000000..b25ba6ad119 --- /dev/null +++ b/src/enums/switch-type.ts @@ -0,0 +1,12 @@ +/** + * Indicates the type of switch functionality that a {@linkcode SwitchPhase} + * or {@linkcode SwitchSummonPhase} will carry out. + */ +export enum SwitchType { + /** Basic switchout where the Pokemon to switch in is selected */ + SWITCH, + /** Transfers stat stages and other effects from the returning Pokemon to the switched in Pokemon */ + BATON_PASS, + /** Transfers the returning Pokemon's Substitute to the switched in Pokemon */ + SHED_TAIL +} diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 14f93809414..21deb3790e1 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -61,6 +61,7 @@ import { Challenges } from "#enums/challenges"; import { PokemonAnimType } from "#app/enums/pokemon-anim-type"; import { PLAYER_PARTY_MAX_SIZE } from "#app/constants"; import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data"; +import { SwitchType } from "#enums/switch-type"; export enum FieldPosition { CENTER, @@ -4003,16 +4004,17 @@ 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 batonPass Indicates if this switch was caused by a baton pass (and - * thus should maintain active mon effects) + * @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 + * the field. */ - switchOut(batonPass: boolean): Promise { + switchOut(switchType: SwitchType = SwitchType.SWITCH): Promise { return new Promise(resolve => { - this.leaveField(!batonPass); + this.leaveField(switchType === SwitchType.SWITCH); this.scene.ui.setMode(Mode.PARTY, PartyUiMode.FAINT_SWITCH, this.getFieldIndex(), (slotIndex: integer, option: PartyOption) => { if (slotIndex >= this.scene.currentBattle.getBattlerCount() && slotIndex < 6) { - this.scene.prependToPhase(new SwitchSummonPhase(this.scene, this.getFieldIndex(), slotIndex, false, batonPass), MoveEndPhase); + this.scene.prependToPhase(new SwitchSummonPhase(this.scene, switchType, this.getFieldIndex(), slotIndex, false), MoveEndPhase); } this.scene.ui.setMode(Mode.MESSAGE).then(resolve); }, PartyUiHandler.FilterNonFainted); @@ -4074,11 +4076,11 @@ export class PlayerPokemon extends Pokemon { const allyPokemon = this.getAlly(); if (slotIndex<=1) { // Revived ally pokemon - this.scene.unshiftPhase(new SwitchSummonPhase(this.scene, pokemon.getFieldIndex(), slotIndex, false, false, true)); + this.scene.unshiftPhase(new SwitchSummonPhase(this.scene, SwitchType.SWITCH, pokemon.getFieldIndex(), slotIndex, false, true)); this.scene.unshiftPhase(new ToggleDoublePositionPhase(this.scene, true)); } else if (allyPokemon.isFainted()) { // Revived party pokemon, and ally pokemon is fainted - this.scene.unshiftPhase(new SwitchSummonPhase(this.scene, allyPokemon.getFieldIndex(), slotIndex, false, false, true)); + this.scene.unshiftPhase(new SwitchSummonPhase(this.scene, SwitchType.SWITCH, allyPokemon.getFieldIndex(), slotIndex, false, true)); this.scene.unshiftPhase(new ToggleDoublePositionPhase(this.scene, true)); } } diff --git a/src/phases/check-switch-phase.ts b/src/phases/check-switch-phase.ts index a069ba224a2..8849d304435 100644 --- a/src/phases/check-switch-phase.ts +++ b/src/phases/check-switch-phase.ts @@ -8,6 +8,7 @@ import { BattlePhase } from "./battle-phase"; import { PostSummonPhase } from "./post-summon-phase"; import { SummonMissingPhase } from "./summon-missing-phase"; import { SwitchPhase } from "./switch-phase"; +import { SwitchType } from "#enums/switch-type"; export class CheckSwitchPhase extends BattlePhase { protected fieldIndex: integer; @@ -50,7 +51,7 @@ export class CheckSwitchPhase extends BattlePhase { this.scene.ui.setMode(Mode.CONFIRM, () => { this.scene.ui.setMode(Mode.MESSAGE); this.scene.tryRemovePhase(p => p instanceof PostSummonPhase && p.player && p.fieldIndex === this.fieldIndex); - this.scene.unshiftPhase(new SwitchPhase(this.scene, this.fieldIndex, false, true)); + this.scene.unshiftPhase(new SwitchPhase(this.scene, SwitchType.SWITCH, this.fieldIndex, false, true)); this.end(); }, () => { this.scene.ui.setMode(Mode.MESSAGE); diff --git a/src/phases/faint-phase.ts b/src/phases/faint-phase.ts index 41384e5e491..ba25225f6e0 100644 --- a/src/phases/faint-phase.ts +++ b/src/phases/faint-phase.ts @@ -18,6 +18,7 @@ import { GameOverPhase } from "./game-over-phase"; import { SwitchPhase } from "./switch-phase"; import { VictoryPhase } from "./victory-phase"; import { SpeciesFormChangeActiveTrigger } from "#app/data/pokemon-forms"; +import { SwitchType } from "#enums/switch-type"; export class FaintPhase extends PokemonPhase { private preventEndure: boolean; @@ -106,14 +107,14 @@ export class FaintPhase extends PokemonPhase { * If previous conditions weren't met, and the player has at least 1 legal Pokemon off the field, * push a phase that prompts the player to summon a Pokemon from their party. */ - this.scene.pushPhase(new SwitchPhase(this.scene, this.fieldIndex, true, false)); + this.scene.pushPhase(new SwitchPhase(this.scene, SwitchType.SWITCH, this.fieldIndex, true, false)); } } else { this.scene.unshiftPhase(new VictoryPhase(this.scene, this.battlerIndex)); if ([BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER].includes(this.scene.currentBattle.battleType)) { const hasReservePartyMember = !!this.scene.getEnemyParty().filter(p => p.isActive() && !p.isOnField() && p.trainerSlot === (pokemon as EnemyPokemon).trainerSlot).length; if (hasReservePartyMember) { - this.scene.pushPhase(new SwitchSummonPhase(this.scene, this.fieldIndex, -1, false, false, false)); + this.scene.pushPhase(new SwitchSummonPhase(this.scene, SwitchType.SWITCH, this.fieldIndex, -1, false, false)); } } } diff --git a/src/phases/mystery-encounter-phases.ts b/src/phases/mystery-encounter-phases.ts index 007b69650b9..60755095cca 100644 --- a/src/phases/mystery-encounter-phases.ts +++ b/src/phases/mystery-encounter-phases.ts @@ -24,6 +24,7 @@ import { NewBattlePhase } from "#app/phases/new-battle-phase"; import { GameOverPhase } from "#app/phases/game-over-phase"; import { SwitchPhase } from "#app/phases/switch-phase"; import { SeenEncounterData } from "#app/data/mystery-encounters/mystery-encounter-save-data"; +import { SwitchType } from "#enums/switch-type"; /** * Will handle (in order): @@ -241,7 +242,7 @@ export class MysteryEncounterBattleStartCleanupPhase extends Phase { const playerField = this.scene.getPlayerField(); playerField.forEach((pokemon, i) => { if (!pokemon.isAllowedInBattle() && legalPlayerPartyPokemon.length > i) { - this.scene.unshiftPhase(new SwitchPhase(this.scene, i, true, false)); + this.scene.unshiftPhase(new SwitchPhase(this.scene, SwitchType.SWITCH, i, true, false)); } }); diff --git a/src/phases/return-phase.ts b/src/phases/return-phase.ts index 19c73816b36..eb587201585 100644 --- a/src/phases/return-phase.ts +++ b/src/phases/return-phase.ts @@ -1,10 +1,11 @@ import BattleScene from "#app/battle-scene"; import { SpeciesFormChangeActiveTrigger } from "#app/data/pokemon-forms"; +import { SwitchType } from "#enums/switch-type"; import { SwitchSummonPhase } from "./switch-summon-phase"; export class ReturnPhase extends SwitchSummonPhase { constructor(scene: BattleScene, fieldIndex: integer) { - super(scene, fieldIndex, -1, true, false); + super(scene, SwitchType.SWITCH, fieldIndex, -1, true); } switchAndSummon(): void { diff --git a/src/phases/switch-phase.ts b/src/phases/switch-phase.ts index b1a2e991ed8..f5ce2179715 100644 --- a/src/phases/switch-phase.ts +++ b/src/phases/switch-phase.ts @@ -1,4 +1,5 @@ import BattleScene from "#app/battle-scene"; +import { SwitchType } from "#enums/switch-type"; import PartyUiHandler, { PartyUiMode, PartyOption } from "#app/ui/party-ui-handler"; import { Mode } from "#app/ui/ui"; import { BattlePhase } from "./battle-phase"; @@ -9,22 +10,25 @@ import { SwitchSummonPhase } from "./switch-summon-phase"; * for the player (if a switch would be valid for the current battle state). */ export class SwitchPhase extends BattlePhase { - protected fieldIndex: integer; - private isModal: boolean; - private doReturn: boolean; + protected readonly fieldIndex: integer; + private readonly switchType: SwitchType; + private readonly isModal: boolean; + private readonly doReturn: boolean; /** * Creates a new SwitchPhase * @param scene {@linkcode BattleScene} Current battle scene + * @param switchType {@linkcode SwitchType} The type of switch logic this phase implements * @param fieldIndex Field index to switch out * @param isModal Indicates if the switch should be forced (true) or is * optional (false). * @param doReturn Indicates if the party member on the field should be * recalled to ball or has already left the field. Passed to {@linkcode SwitchSummonPhase}. */ - constructor(scene: BattleScene, fieldIndex: integer, isModal: boolean, doReturn: boolean) { + constructor(scene: BattleScene, switchType: SwitchType, fieldIndex: integer, isModal: boolean, doReturn: boolean) { super(scene); + this.switchType = switchType; this.fieldIndex = fieldIndex; this.isModal = isModal; this.doReturn = doReturn; @@ -38,11 +42,13 @@ export class SwitchPhase extends BattlePhase { return super.end(); } - // Skip if the fainted party member has been revived already. doReturn is - // only passed as `false` from FaintPhase (as opposed to other usages such - // as ForceSwitchOutAttr or CheckSwitchPhase), so we only want to check this - // if the mon should have already been returned but is still alive and well - // on the field. see also; battle.test.ts + /** + * Skip if the fainted party member has been revived already. doReturn is + * only passed as `false` from FaintPhase (as opposed to other usages such + * as ForceSwitchOutAttr or CheckSwitchPhase), so we only want to check this + * if the mon should have already been returned but is still alive and well + * on the field. see also; battle.test.ts + */ if (this.isModal && !this.doReturn && !this.scene.getParty()[this.fieldIndex].isFainted()) { return super.end(); } @@ -57,7 +63,8 @@ export class SwitchPhase extends BattlePhase { this.scene.ui.setMode(Mode.PARTY, this.isModal ? PartyUiMode.FAINT_SWITCH : PartyUiMode.POST_BATTLE_SWITCH, fieldIndex, (slotIndex: integer, option: PartyOption) => { if (slotIndex >= this.scene.currentBattle.getBattlerCount() && slotIndex < 6) { - this.scene.unshiftPhase(new SwitchSummonPhase(this.scene, fieldIndex, slotIndex, this.doReturn, option === PartyOption.PASS_BATON)); + const switchType = (option === PartyOption.PASS_BATON) ? SwitchType.BATON_PASS : this.switchType; + this.scene.unshiftPhase(new SwitchSummonPhase(this.scene, switchType, fieldIndex, slotIndex, this.doReturn)); } this.scene.ui.setMode(Mode.MESSAGE).then(() => super.end()); }, PartyUiHandler.FilterNonFainted); diff --git a/src/phases/switch-summon-phase.ts b/src/phases/switch-summon-phase.ts index 525f74e896f..eb1e089543b 100644 --- a/src/phases/switch-summon-phase.ts +++ b/src/phases/switch-summon-phase.ts @@ -12,29 +12,30 @@ import i18next from "i18next"; import { PostSummonPhase } from "./post-summon-phase"; import { SummonPhase } from "./summon-phase"; import { SubstituteTag } from "#app/data/battler-tags"; +import { SwitchType } from "#enums/switch-type"; export class SwitchSummonPhase extends SummonPhase { - private slotIndex: integer; - private doReturn: boolean; - private batonPass: boolean; + private readonly switchType: SwitchType; + private readonly slotIndex: integer; + private readonly doReturn: boolean; private lastPokemon: Pokemon; /** * Constructor for creating a new SwitchSummonPhase * @param scene {@linkcode BattleScene} the scene the phase is associated with + * @param switchType the type of switch behavior * @param fieldIndex integer representing position on the battle field * @param slotIndex integer for the index of pokemon (in party of 6) to switch into * @param doReturn boolean whether to render "comeback" dialogue - * @param batonPass boolean if the switch is from baton pass * @param player boolean if the switch is from the player */ - constructor(scene: BattleScene, fieldIndex: integer, slotIndex: integer, doReturn: boolean, batonPass: boolean, player?: boolean) { + constructor(scene: BattleScene, switchType: SwitchType, fieldIndex: integer, slotIndex: integer, doReturn: boolean, player?: boolean) { super(scene, fieldIndex, player !== undefined ? player : true); + this.switchType = switchType; this.slotIndex = slotIndex; this.doReturn = doReturn; - this.batonPass = batonPass; } start(): void { @@ -64,7 +65,7 @@ export class SwitchSummonPhase extends SummonPhase { const pokemon = this.getPokemon(); - if (!this.batonPass) { + if (this.switchType === SwitchType.SWITCH) { (this.player ? this.scene.getEnemyField() : this.scene.getPlayerField()).forEach(enemyPokemon => enemyPokemon.removeTagsBySourceId(pokemon.id)); const substitute = pokemon.getTag(SubstituteTag); if (substitute) { @@ -94,7 +95,7 @@ export class SwitchSummonPhase extends SummonPhase { ease: "Sine.easeIn", scale: 0.5, onComplete: () => { - pokemon.leaveField(!this.batonPass, false); + pokemon.leaveField(this.switchType === SwitchType.SWITCH, false); this.scene.time.delayedCall(750, () => this.switchAndSummon()); } }); @@ -105,7 +106,7 @@ export class SwitchSummonPhase extends SummonPhase { const switchedInPokemon = party[this.slotIndex]; this.lastPokemon = this.getPokemon(); applyPreSwitchOutAbAttrs(PreSwitchOutAbAttr, this.lastPokemon); - if (this.batonPass && switchedInPokemon) { + if (this.switchType === SwitchType.BATON_PASS && switchedInPokemon) { (this.player ? this.scene.getEnemyField() : this.scene.getPlayerField()).forEach(enemyPokemon => enemyPokemon.transferTagsBySourceId(this.lastPokemon.id, switchedInPokemon.id)); if (!this.scene.findModifier(m => m instanceof SwitchEffectTransferModifier && (m as SwitchEffectTransferModifier).pokemonId === switchedInPokemon.id)) { const batonPassModifier = this.scene.findModifier(m => m instanceof SwitchEffectTransferModifier @@ -130,7 +131,7 @@ export class SwitchSummonPhase extends SummonPhase { * If this switch is passing a Substitute, make the switched Pokemon match the returned Pokemon's state as it left. * Otherwise, clear any persisting tags on the returned Pokemon. */ - if (this.batonPass) { + if (this.switchType === SwitchType.BATON_PASS || this.switchType === SwitchType.SHED_TAIL) { const substitute = this.lastPokemon.getTag(SubstituteTag); if (substitute) { switchedInPokemon.x += this.lastPokemon.getSubstituteOffset()[0]; @@ -174,8 +175,13 @@ export class SwitchSummonPhase extends SummonPhase { pokemon.battleSummonData.turnCount--; } - if (this.batonPass && pokemon) { + if (this.switchType === SwitchType.BATON_PASS && pokemon) { pokemon.transferSummon(this.lastPokemon); + } else if (this.switchType === SwitchType.SHED_TAIL && pokemon) { + const subTag = this.lastPokemon.getTag(SubstituteTag); + if (subTag) { + pokemon.summonData.tags.push(subTag); + } } this.lastPokemon?.resetSummonData(); diff --git a/src/phases/turn-init-phase.ts b/src/phases/turn-init-phase.ts index 92547878f12..2f1b539cdcf 100644 --- a/src/phases/turn-init-phase.ts +++ b/src/phases/turn-init-phase.ts @@ -32,7 +32,7 @@ export class TurnInitPhase extends FieldPhase { this.scene.unshiftPhase(new GameOverPhase(this.scene)); } else if (allowedPokemon.length >= this.scene.currentBattle.getBattlerCount() || (this.scene.currentBattle.double && !allowedPokemon[0].isActive(true))) { // If there is at least one pokemon in the back that is legal to switch in, force a switch. - p.switchOut(false); + p.switchOut(); } else { // If there are no pokemon in the back but we're not game overing, just hide the pokemon. // This should only happen in double battles. diff --git a/src/phases/turn-start-phase.ts b/src/phases/turn-start-phase.ts index 5c1af4228c6..b070abb390a 100644 --- a/src/phases/turn-start-phase.ts +++ b/src/phases/turn-start-phase.ts @@ -19,6 +19,7 @@ import { TurnEndPhase } from "./turn-end-phase"; import { WeatherEffectPhase } from "./weather-effect-phase"; import { BattlerIndex } from "#app/battle"; import { TrickRoomTag } from "#app/data/arena-tag"; +import { SwitchType } from "#enums/switch-type"; export class TurnStartPhase extends FieldPhase { constructor(scene: BattleScene) { @@ -179,7 +180,8 @@ export class TurnStartPhase extends FieldPhase { this.scene.unshiftPhase(new AttemptCapturePhase(this.scene, turnCommand.targets![0] % 2, turnCommand.cursor!));//TODO: is the bang correct here? break; case Command.POKEMON: - this.scene.unshiftPhase(new SwitchSummonPhase(this.scene, pokemon.getFieldIndex(), turnCommand.cursor!, true, turnCommand.args![0] as boolean, pokemon.isPlayer()));//TODO: is the bang correct here? + const switchType = turnCommand.args?.[0] ? SwitchType.BATON_PASS : SwitchType.SWITCH; + this.scene.unshiftPhase(new SwitchSummonPhase(this.scene, switchType, pokemon.getFieldIndex(), turnCommand.cursor!, true, pokemon.isPlayer())); break; case Command.RUN: let runningPokemon = pokemon; diff --git a/src/system/version-converter.ts b/src/system/version-converter.ts index c297782ba66..0591647aeaa 100644 --- a/src/system/version-converter.ts +++ b/src/system/version-converter.ts @@ -22,15 +22,25 @@ export function applySessionDataPatches(data: SessionSaveData) { } else if (m.className === "PokemonResetNegativeStatStageModifier") { m.className = "ResetNegativeStatStageModifier"; } else if (m.className === "TempBattleStatBoosterModifier") { - m.className = "TempStatStageBoosterModifier"; - m.typeId = "TEMP_STAT_STAGE_BOOSTER"; + // Dire Hit no longer a part of the TempBattleStatBoosterModifierTypeGenerator + if (m.typeId !== "DIRE_HIT") { + m.className = "TempStatStageBoosterModifier"; + m.typeId = "TEMP_STAT_STAGE_BOOSTER"; - // Migration from TempBattleStat to Stat - const newStat = m.typePregenArgs[0] + 1; - m.typePregenArgs[0] = newStat; + // Migration from TempBattleStat to Stat + const newStat = m.typePregenArgs[0] + 1; + m.typePregenArgs[0] = newStat; + + // From [ stat, battlesLeft ] to [ stat, maxBattles, battleCount ] + m.args = [ newStat, 5, m.args[1] ]; + } else { + m.className = "TempCritBoosterModifier"; + m.typePregenArgs = []; + + // From [ stat, battlesLeft ] to [ maxBattles, battleCount ] + m.args = [ 5, m.args[1] ]; + } - // From [ stat, battlesLeft ] to [ stat, maxBattles, battleCount ] - m.args = [ newStat, 5, m.args[1] ]; } else if (m.className === "DoubleBattleChanceBoosterModifier" && m.args.length === 1) { let maxBattles: number; switch (m.typeId) { @@ -73,7 +83,7 @@ export function applySystemDataPatches(data: SystemSaveData) { case "1.0.3": case "1.0.4": // --- LEGACY PATCHES --- - if (data.starterData) { + if (data.starterData && data.dexData) { // Migrate ability starter data if empty for caught species Object.keys(data.starterData).forEach(sd => { if (data.dexData[sd]?.caughtAttr && (data.starterData[sd] && !data.starterData[sd].abilityAttr)) { @@ -104,12 +114,14 @@ export function applySystemDataPatches(data: SystemSaveData) { // --- PATCHES --- // Fix Starter Data - for (const starterId of defaultStarterSpecies) { - if (data.starterData[starterId]?.abilityAttr) { - data.starterData[starterId].abilityAttr |= AbilityAttr.ABILITY_1; - } - if (data.dexData[starterId]?.caughtAttr) { - data.dexData[starterId].caughtAttr |= DexAttr.FEMALE; + if (data.starterData && data.dexData) { + for (const starterId of defaultStarterSpecies) { + if (data.starterData[starterId]?.abilityAttr) { + data.starterData[starterId].abilityAttr |= AbilityAttr.ABILITY_1; + } + if (data.dexData[starterId]?.caughtAttr) { + data.dexData[starterId].caughtAttr |= DexAttr.FEMALE; + } } } } diff --git a/src/test/abilities/aroma_veil.test.ts b/src/test/abilities/aroma_veil.test.ts new file mode 100644 index 00000000000..b70308a5d60 --- /dev/null +++ b/src/test/abilities/aroma_veil.test.ts @@ -0,0 +1,65 @@ +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { Abilities } from "#enums/abilities"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { ArenaTagType } from "#enums/arena-tag-type"; +import { BattlerIndex } from "#app/battle"; +import { PlayerPokemon } from "#app/field/pokemon"; + +describe("Moves - Aroma Veil", () => { + 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 + .battleType("double") + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset([Moves.HEAL_BLOCK, Moves.IMPRISON, Moves.SPLASH]) + .enemySpecies(Species.SHUCKLE) + .ability(Abilities.AROMA_VEIL) + .moveset([Moves.GROWL]); + }); + + it("Aroma Veil protects the Pokemon's side against most Move Restriction Battler Tags", async () => { + await game.classicMode.startBattle([Species.REGIELEKI, Species.BULBASAUR]); + + const party = game.scene.getParty()! as PlayerPokemon[]; + + game.move.select(Moves.GROWL); + game.move.select(Moves.GROWL); + await game.forceEnemyMove(Moves.HEAL_BLOCK); + await game.toNextTurn(); + party.forEach(p => { + expect(p.getTag(BattlerTagType.HEAL_BLOCK)).toBeUndefined(); + }); + }); + + it("Aroma Veil does not protect against Imprison", async () => { + await game.classicMode.startBattle([Species.REGIELEKI, Species.BULBASAUR]); + + const party = game.scene.getParty()! as PlayerPokemon[]; + + game.move.select(Moves.GROWL); + game.move.select(Moves.GROWL, 1); + await game.forceEnemyMove(Moves.IMPRISON, BattlerIndex.PLAYER); + await game.forceEnemyMove(Moves.SPLASH); + await game.toNextTurn(); + expect(game.scene.arena.getTag(ArenaTagType.IMPRISON)).toBeDefined(); + party.forEach(p => { + expect(p.getTag(BattlerTagType.IMPRISON)).toBeDefined(); + }); + }); +}); diff --git a/src/test/abilities/zen_mode.test.ts b/src/test/abilities/zen_mode.test.ts index c7cbd9014e0..b4c60aa7a7f 100644 --- a/src/test/abilities/zen_mode.test.ts +++ b/src/test/abilities/zen_mode.test.ts @@ -18,6 +18,7 @@ import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest"; import { Status, StatusEffect } from "#app/data/status-effect"; +import { SwitchType } from "#enums/switch-type"; @@ -113,7 +114,7 @@ describe("Abilities - ZEN MODE", () => { await game.phaseInterceptor.run(EnemyCommandPhase); await game.phaseInterceptor.run(TurnStartPhase); game.onNextPrompt("SwitchPhase", Mode.PARTY, () => { - game.scene.unshiftPhase(new SwitchSummonPhase(game.scene, 0, 1, false, false)); + game.scene.unshiftPhase(new SwitchSummonPhase(game.scene, SwitchType.SWITCH, 0, 1, false)); game.scene.ui.setMode(Mode.MESSAGE); }); game.onNextPrompt("SwitchPhase", Mode.MESSAGE, () => { diff --git a/src/test/moves/imprison.test.ts b/src/test/moves/imprison.test.ts new file mode 100644 index 00000000000..abb4b3cac6c --- /dev/null +++ b/src/test/moves/imprison.test.ts @@ -0,0 +1,98 @@ +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { Abilities } from "#enums/abilities"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { ArenaTagType } from "#enums/arena-tag-type"; + +describe("Moves - Imprison", () => { + 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 + .battleType("single") + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset([Moves.IMPRISON, Moves.SPLASH, Moves.GROWL]) + .enemySpecies(Species.SHUCKLE) + .moveset([Moves.TRANSFORM, Moves.SPLASH]); + }); + + it("Pokemon under Imprison cannot use shared moves", async () => { + await game.classicMode.startBattle([Species.REGIELEKI]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.TRANSFORM); + await game.forceEnemyMove(Moves.IMPRISON); + await game.toNextTurn(); + const playerMoveset = playerPokemon.getMoveset().map(x => x?.moveId); + const enemyMoveset = game.scene.getEnemyPokemon()!.getMoveset().map(x => x?.moveId); + expect(enemyMoveset.includes(playerMoveset[0])).toBeTruthy(); + const imprisonArenaTag = game.scene.arena.getTag(ArenaTagType.IMPRISON); + const imprisonBattlerTag = playerPokemon.getTag(BattlerTagType.IMPRISON); + expect(imprisonArenaTag).toBeDefined(); + expect(imprisonBattlerTag).toBeDefined(); + + // Second turn, Imprison forces Struggle to occur + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.SPLASH); + await game.toNextTurn(); + const move1 = playerPokemon.getLastXMoves(1)[0]!; + expect(move1.move).toBe(Moves.STRUGGLE); + }); + + it("Imprison applies to Pokemon switched into Battle", async () => { + await game.classicMode.startBattle([Species.REGIELEKI, Species.BULBASAUR]); + + const playerPokemon1 = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.IMPRISON); + await game.toNextTurn(); + const imprisonArenaTag = game.scene.arena.getTag(ArenaTagType.IMPRISON); + const imprisonBattlerTag1 = playerPokemon1.getTag(BattlerTagType.IMPRISON); + expect(imprisonArenaTag).toBeDefined(); + expect(imprisonBattlerTag1).toBeDefined(); + + // Second turn, Imprison forces Struggle to occur + game.doSwitchPokemon(1); + await game.forceEnemyMove(Moves.SPLASH); + await game.toNextTurn(); + const playerPokemon2 = game.scene.getPlayerPokemon()!; + const imprisonBattlerTag2 = playerPokemon2.getTag(BattlerTagType.IMPRISON); + expect(playerPokemon1).not.toEqual(playerPokemon2); + expect(imprisonBattlerTag2).toBeDefined(); + }); + + it("The effects of Imprison only end when the source is no longer active", async () => { + game.override.moveset([Moves.SPLASH, Moves.IMPRISON]); + await game.classicMode.startBattle([Species.REGIELEKI, Species.BULBASAUR]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + game.move.select(Moves.IMPRISON); + await game.forceEnemyMove(Moves.GROWL); + await game.toNextTurn(); + expect(game.scene.arena.getTag(ArenaTagType.IMPRISON)).toBeDefined(); + expect(enemyPokemon.getTag(BattlerTagType.IMPRISON)).toBeDefined(); + game.doSwitchPokemon(1); + await game.forceEnemyMove(Moves.SPLASH); + await game.toNextTurn(); + expect(playerPokemon.isActive(true)).toBeFalsy(); + expect(game.scene.arena.getTag(ArenaTagType.IMPRISON)).toBeUndefined(); + expect(enemyPokemon.getTag(BattlerTagType.IMPRISON)).toBeUndefined(); + }); +}); diff --git a/src/test/moves/shed_tail.test.ts b/src/test/moves/shed_tail.test.ts new file mode 100644 index 00000000000..a976a614792 --- /dev/null +++ b/src/test/moves/shed_tail.test.ts @@ -0,0 +1,56 @@ +import { SubstituteTag } from "#app/data/battler-tags"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, it, expect } from "vitest"; + +describe("Moves - Shed Tail", () => { + 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 + .moveset([Moves.SHED_TAIL]) + .battleType("single") + .enemySpecies(Species.SNORLAX) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("transfers a Substitute doll to the switched in Pokemon", async () => { + await game.classicMode.startBattle([Species.MAGIKARP, Species.FEEBAS]); + + const magikarp = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.SHED_TAIL); + game.doSelectPartyPokemon(1); + + await game.phaseInterceptor.to("TurnEndPhase", false); + + const feebas = game.scene.getPlayerPokemon()!; + const substituteTag = feebas.getTag(SubstituteTag); + + expect(feebas).not.toBe(magikarp); + expect(feebas.hp).toBe(feebas.getMaxHp()); + // Note: Shed Tail's HP cost is currently not accurate to mainline, as it + // should cost ceil(maxHP / 2) instead of max(floor(maxHp / 2), 1). The current + // implementation is consistent with Substitute's HP cost logic, but that's not + // the case in mainline for some reason :regiDespair:. + expect(magikarp.hp).toBe(Math.ceil(magikarp.getMaxHp() / 2)); + expect(substituteTag).toBeDefined(); + expect(substituteTag?.hp).toBe(Math.floor(magikarp.getMaxHp() / 4)); + }); +}); diff --git a/src/test/moves/taunt.test.ts b/src/test/moves/taunt.test.ts new file mode 100644 index 00000000000..50bb2fee9df --- /dev/null +++ b/src/test/moves/taunt.test.ts @@ -0,0 +1,54 @@ +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { Abilities } from "#enums/abilities"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { MoveResult } from "#app/field/pokemon"; +import { BattlerTagType } from "#enums/battler-tag-type"; + +describe("Moves - Taunt", () => { + 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 + .battleType("single") + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset([Moves.TAUNT, Moves.SPLASH]) + .enemySpecies(Species.SHUCKLE) + .moveset([Moves.GROWL]); + }); + + it("Pokemon should not be able to use Status Moves", async () => { + await game.classicMode.startBattle([Species.REGIELEKI]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + + // First turn, Player Pokemon succeeds using Growl without Taunt + game.move.select(Moves.GROWL); + await game.forceEnemyMove(Moves.TAUNT); + await game.toNextTurn(); + const move1 = playerPokemon.getLastXMoves(1)[0]!; + expect(move1.move).toBe(Moves.GROWL); + expect(move1.result).toBe(MoveResult.SUCCESS); + expect(playerPokemon?.getTag(BattlerTagType.TAUNT)).toBeDefined(); + + // Second turn, Taunt forces Struggle to occur + game.move.select(Moves.GROWL); + await game.forceEnemyMove(Moves.SPLASH); + await game.toNextTurn(); + const move2 = playerPokemon.getLastXMoves(1)[0]!; + expect(move2.move).toBe(Moves.STRUGGLE); + }); +}); diff --git a/src/test/moves/torment.test.ts b/src/test/moves/torment.test.ts new file mode 100644 index 00000000000..f725f2bc34a --- /dev/null +++ b/src/test/moves/torment.test.ts @@ -0,0 +1,64 @@ +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { Abilities } from "#enums/abilities"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { MoveResult } from "#app/field/pokemon"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { TurnEndPhase } from "#app/phases/turn-end-phase"; + +describe("Moves - Torment", () => { + 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 + .battleType("single") + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset([Moves.TORMENT, Moves.SPLASH]) + .enemySpecies(Species.SHUCKLE) + .enemyLevel(30) + .moveset([Moves.TACKLE]) + .ability(Abilities.BALL_FETCH); + }); + + it("Pokemon should not be able to use the same move consecutively", async () => { + await game.classicMode.startBattle([Species.CHANSEY]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + + // First turn, Player Pokemon uses Tackle successfully + game.move.select(Moves.TACKLE); + await game.forceEnemyMove(Moves.TORMENT); + await game.toNextTurn(); + const move1 = playerPokemon.getLastXMoves(1)[0]!; + expect(move1.move).toBe(Moves.TACKLE); + expect(move1.result).toBe(MoveResult.SUCCESS); + expect(playerPokemon?.getTag(BattlerTagType.TORMENT)).toBeDefined(); + + // Second turn, Torment forces Struggle to occur + game.move.select(Moves.TACKLE); + await game.forceEnemyMove(Moves.SPLASH); + await game.toNextTurn(); + const move2 = playerPokemon.getLastXMoves(1)[0]!; + expect(move2.move).toBe(Moves.STRUGGLE); + + // Third turn, Tackle can be used. + game.move.select(Moves.TACKLE); + await game.forceEnemyMove(Moves.SPLASH); + await game.phaseInterceptor.to(TurnEndPhase); + const move3 = playerPokemon.getLastXMoves(1)[0]!; + expect(move3.move).toBe(Moves.TACKLE); + }); +});