diff --git a/package-lock.json b/package-lock.json index be946306471..9e512884922 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pokemon-rogue-battle", - "version": "1.1.0", + "version": "1.1.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pokemon-rogue-battle", - "version": "1.1.0", + "version": "1.1.6", "hasInstallScript": true, "dependencies": { "@material/material-color-utilities": "^0.2.7", diff --git a/package.json b/package.json index a31296d1644..f106fb1a773 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "pokemon-rogue-battle", "private": true, - "version": "1.1.0", + "version": "1.1.6", "type": "module", "scripts": { "start": "vite", diff --git a/public/audio/bgm/graveyard.mp3 b/public/audio/bgm/graveyard.mp3 index 48092fa3ec2..45571f16d09 100644 Binary files a/public/audio/bgm/graveyard.mp3 and b/public/audio/bgm/graveyard.mp3 differ diff --git a/public/images/events/halloween2024-event-es.png b/public/images/events/halloween2024-event-es-ES.png similarity index 100% rename from public/images/events/halloween2024-event-es.png rename to public/images/events/halloween2024-event-es-ES.png diff --git a/public/images/pokemon/658-ash.json b/public/images/pokemon/658-ash.json index 8fbe19f5ccb..c2b8eede9ef 100644 --- a/public/images/pokemon/658-ash.json +++ b/public/images/pokemon/658-ash.json @@ -1,41 +1,19 @@ -{ - "textures": [ - { - "image": "658-ash.png", - "format": "RGBA8888", - "size": { - "w": 79, - "h": 79 - }, - "scale": 1, - "frames": [ - { - "filename": "0001.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 79, - "h": 74 - }, - "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 79, - "h": 74 - }, - "frame": { - "x": 0, - "y": 0, - "w": 79, - "h": 74 - } - } - ] - } - ], - "meta": { - "app": "https://www.codeandweb.com/texturepacker", - "version": "3.0", - "smartupdate": "$TexturePacker:SmartUpdate:936f62fa49ba4d6e402bb2e2eaf2afd0:ed00ba047a44b4bf1309bc147dd000e3:bfbf521a5c7bd80bcd95a96d9789c0dd$" - } +{ "frames": [ + { + "filename": "0001.png", + "frame": { "x": 0, "y": 0, "w": 79, "h": 74 }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { "x": 0, "y": 0, "w": 79, "h": 74 }, + "sourceSize": { "w": 79, "h": 74 }, + "duration": 100 + } + ], + "meta": { + "app": "https://www.aseprite.org/", + "version": "1.3.7-x64", + "format": "I8", + "size": { "w": 79, "h": 74 }, + "scale": "1" + } } diff --git a/public/images/pokemon/658.json b/public/images/pokemon/658.json index 4fd918e0f50..219645ec240 100644 --- a/public/images/pokemon/658.json +++ b/public/images/pokemon/658.json @@ -1,41 +1,19 @@ -{ - "textures": [ - { - "image": "658.png", - "format": "RGBA8888", - "size": { - "w": 75, - "h": 75 - }, - "scale": 1, - "frames": [ - { - "filename": "0001.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 75, - "h": 65 - }, - "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 75, - "h": 65 - }, - "frame": { - "x": 0, - "y": 0, - "w": 75, - "h": 65 - } - } - ] - } - ], - "meta": { - "app": "https://www.codeandweb.com/texturepacker", - "version": "3.0", - "smartupdate": "$TexturePacker:SmartUpdate:e0b10df331bd4ce6760edab61dee144b:061561c45beff89a92bf0158d065204f:5affcab976148657d36bf4ff3410f92d$" - } +{ "frames": [ + { + "filename": "0001.png", + "frame": { "x": 0, "y": 0, "w": 85, "h": 67 }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { "x": 0, "y": 0, "w": 85, "h": 67 }, + "sourceSize": { "w": 85, "h": 67 }, + "duration": 100 + } + ], + "meta": { + "app": "https://www.aseprite.org/", + "version": "1.3.7-x64", + "format": "I8", + "size": { "w": 85, "h": 67 }, + "scale": "1" + } } diff --git a/public/images/pokemon/688.json b/public/images/pokemon/688.json index 069d176730e..3d9aa902a54 100644 --- a/public/images/pokemon/688.json +++ b/public/images/pokemon/688.json @@ -5,8 +5,7 @@ "rotated": false, "trimmed": false, "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 63 }, - "sourceSize": { "w": 64, "h": 63 }, - "duration": 100 + "sourceSize": { "w": 64, "h": 63 } } ], "meta": { diff --git a/public/images/pokemon/back/658-ash.json b/public/images/pokemon/back/658-ash.json index 4ddab1765b2..51a722070b3 100644 --- a/public/images/pokemon/back/658-ash.json +++ b/public/images/pokemon/back/658-ash.json @@ -1,41 +1,19 @@ -{ - "textures": [ - { - "image": "658-ash.png", - "format": "RGBA8888", - "size": { - "w": 73, - "h": 73 - }, - "scale": 1, - "frames": [ - { - "filename": "0001.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 73, - "h": 69 - }, - "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 73, - "h": 69 - }, - "frame": { - "x": 0, - "y": 0, - "w": 73, - "h": 69 - } - } - ] - } - ], - "meta": { - "app": "https://www.codeandweb.com/texturepacker", - "version": "3.0", - "smartupdate": "$TexturePacker:SmartUpdate:4f38801bb3afeda5faff04bdcf6a666f:0c78ce2715e7510bf55da0a92b42661c:bfbf521a5c7bd80bcd95a96d9789c0dd$" - } +{ "frames": [ + { + "filename": "0001.png", + "frame": { "x": 0, "y": 0, "w": 73, "h": 73 }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { "x": 0, "y": 0, "w": 73, "h": 73 }, + "sourceSize": { "w": 73, "h": 73 }, + "duration": 100 + } + ], + "meta": { + "app": "https://www.aseprite.org/", + "version": "1.3.7-x64", + "format": "I8", + "size": { "w": 73, "h": 73 }, + "scale": "1" + } } diff --git a/public/images/pokemon/back/658.json b/public/images/pokemon/back/658.json index 119002876a6..050b63e3592 100644 --- a/public/images/pokemon/back/658.json +++ b/public/images/pokemon/back/658.json @@ -1,41 +1,19 @@ -{ - "textures": [ - { - "image": "658.png", - "format": "RGBA8888", - "size": { - "w": 77, - "h": 77 - }, - "scale": 1, - "frames": [ - { - "filename": "0001.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 77, - "h": 65 - }, - "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 77, - "h": 65 - }, - "frame": { - "x": 0, - "y": 0, - "w": 77, - "h": 65 - } - } - ] - } - ], - "meta": { - "app": "https://www.codeandweb.com/texturepacker", - "version": "3.0", - "smartupdate": "$TexturePacker:SmartUpdate:acdb9925f3f23b947504eec7cc28c92d:1a13d9d418f6c107bb9e5d621d9154bb:5affcab976148657d36bf4ff3410f92d$" - } +{ "frames": [ + { + "filename": "0001.png", + "frame": { "x": 0, "y": 0, "w": 77, "h": 77 }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { "x": 0, "y": 0, "w": 77, "h": 77 }, + "sourceSize": { "w": 77, "h": 77 }, + "duration": 100 + } + ], + "meta": { + "app": "https://www.aseprite.org/", + "version": "1.3.7-x64", + "format": "I8", + "size": { "w": 77, "h": 77 }, + "scale": "1" + } } diff --git a/public/images/pokemon/back/688.json b/public/images/pokemon/back/688.json index 872fbc1f9db..d9f30db34a8 100644 --- a/public/images/pokemon/back/688.json +++ b/public/images/pokemon/back/688.json @@ -1,41 +1,19 @@ -{ - "textures": [ - { - "image": "688.png", - "format": "RGBA8888", - "size": { - "w": 52, - "h": 52 - }, - "scale": 1, - "frames": [ - { - "filename": "0001.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 41, - "h": 52 - }, - "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 41, - "h": 52 - }, - "frame": { - "x": 0, - "y": 0, - "w": 41, - "h": 52 - } - } - ] - } - ], - "meta": { - "app": "https://www.codeandweb.com/texturepacker", - "version": "3.0", - "smartupdate": "$TexturePacker:SmartUpdate:ea462f2b1b46327e3b8fcb7ec5e44f08:2d2598cc03dec73182dbea237ad83b34:176060351d0044923af938ba7932a6ef$" - } +{ "frames": [ + { + "filename": "0001.png", + "frame": { "x": 0, "y": 0, "w": 51, "h": 65 }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { "x": 0, "y": 0, "w": 51, "h": 65 }, + "sourceSize": { "w": 51, "h": 65 } + } + ], + "meta": { + "app": "https://www.aseprite.org/", + "version": "1.3.7-dev", + "image": "688.png", + "format": "I8", + "size": { "w": 51, "h": 65 }, + "scale": "1" + } } diff --git a/public/images/pokemon/back/shiny/658-ash.json b/public/images/pokemon/back/shiny/658-ash.json index a796ad08246..51a722070b3 100644 --- a/public/images/pokemon/back/shiny/658-ash.json +++ b/public/images/pokemon/back/shiny/658-ash.json @@ -1,41 +1,19 @@ -{ - "textures": [ - { - "image": "658-ash.png", - "format": "RGBA8888", - "size": { - "w": 73, - "h": 73 - }, - "scale": 1, - "frames": [ - { - "filename": "0001.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 73, - "h": 69 - }, - "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 73, - "h": 69 - }, - "frame": { - "x": 0, - "y": 0, - "w": 73, - "h": 69 - } - } - ] - } - ], - "meta": { - "app": "https://www.codeandweb.com/texturepacker", - "version": "3.0", - "smartupdate": "$TexturePacker:SmartUpdate:d474b821316a87dfe09b397bdc2db5ef:497de0c2ec59ceba163e870b3226c76c:bfbf521a5c7bd80bcd95a96d9789c0dd$" - } +{ "frames": [ + { + "filename": "0001.png", + "frame": { "x": 0, "y": 0, "w": 73, "h": 73 }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { "x": 0, "y": 0, "w": 73, "h": 73 }, + "sourceSize": { "w": 73, "h": 73 }, + "duration": 100 + } + ], + "meta": { + "app": "https://www.aseprite.org/", + "version": "1.3.7-x64", + "format": "I8", + "size": { "w": 73, "h": 73 }, + "scale": "1" + } } diff --git a/public/images/pokemon/back/shiny/658.json b/public/images/pokemon/back/shiny/658.json index cc5d3c5b9ff..050b63e3592 100644 --- a/public/images/pokemon/back/shiny/658.json +++ b/public/images/pokemon/back/shiny/658.json @@ -1,41 +1,19 @@ -{ - "textures": [ - { - "image": "658.png", - "format": "RGBA8888", - "size": { - "w": 77, - "h": 77 - }, - "scale": 1, - "frames": [ - { - "filename": "0001.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 77, - "h": 65 - }, - "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 77, - "h": 65 - }, - "frame": { - "x": 0, - "y": 0, - "w": 77, - "h": 65 - } - } - ] - } - ], - "meta": { - "app": "https://www.codeandweb.com/texturepacker", - "version": "3.0", - "smartupdate": "$TexturePacker:SmartUpdate:5891f87a78022cde3402e7d9714cc7bf:756360084290e39c139e3fef91c81759:5affcab976148657d36bf4ff3410f92d$" - } +{ "frames": [ + { + "filename": "0001.png", + "frame": { "x": 0, "y": 0, "w": 77, "h": 77 }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { "x": 0, "y": 0, "w": 77, "h": 77 }, + "sourceSize": { "w": 77, "h": 77 }, + "duration": 100 + } + ], + "meta": { + "app": "https://www.aseprite.org/", + "version": "1.3.7-x64", + "format": "I8", + "size": { "w": 77, "h": 77 }, + "scale": "1" + } } diff --git a/public/images/pokemon/back/shiny/688.json b/public/images/pokemon/back/shiny/688.json index 56cf47aab5a..d9f30db34a8 100644 --- a/public/images/pokemon/back/shiny/688.json +++ b/public/images/pokemon/back/shiny/688.json @@ -1,41 +1,19 @@ -{ - "textures": [ - { - "image": "688.png", - "format": "RGBA8888", - "size": { - "w": 52, - "h": 52 - }, - "scale": 1, - "frames": [ - { - "filename": "0001.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 41, - "h": 52 - }, - "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 41, - "h": 52 - }, - "frame": { - "x": 0, - "y": 0, - "w": 41, - "h": 52 - } - } - ] - } - ], - "meta": { - "app": "https://www.codeandweb.com/texturepacker", - "version": "3.0", - "smartupdate": "$TexturePacker:SmartUpdate:0261b6c9242bba728fcfbfc515875b27:de0d9ddceed9311b33ae50ba86e969d1:176060351d0044923af938ba7932a6ef$" - } +{ "frames": [ + { + "filename": "0001.png", + "frame": { "x": 0, "y": 0, "w": 51, "h": 65 }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { "x": 0, "y": 0, "w": 51, "h": 65 }, + "sourceSize": { "w": 51, "h": 65 } + } + ], + "meta": { + "app": "https://www.aseprite.org/", + "version": "1.3.7-dev", + "image": "688.png", + "format": "I8", + "size": { "w": 51, "h": 65 }, + "scale": "1" + } } diff --git a/public/images/pokemon/shiny/658-ash.json b/public/images/pokemon/shiny/658-ash.json index 7ac419b1686..c2b8eede9ef 100644 --- a/public/images/pokemon/shiny/658-ash.json +++ b/public/images/pokemon/shiny/658-ash.json @@ -1,41 +1,19 @@ -{ - "textures": [ - { - "image": "658-ash.png", - "format": "RGBA8888", - "size": { - "w": 79, - "h": 79 - }, - "scale": 1, - "frames": [ - { - "filename": "0001.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 79, - "h": 74 - }, - "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 79, - "h": 74 - }, - "frame": { - "x": 0, - "y": 0, - "w": 79, - "h": 74 - } - } - ] - } - ], - "meta": { - "app": "https://www.codeandweb.com/texturepacker", - "version": "3.0", - "smartupdate": "$TexturePacker:SmartUpdate:3dd081ba5490f090a73de8423aac2f6b:f088fafaea755476f2abf488e7340cab:bfbf521a5c7bd80bcd95a96d9789c0dd$" - } +{ "frames": [ + { + "filename": "0001.png", + "frame": { "x": 0, "y": 0, "w": 79, "h": 74 }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { "x": 0, "y": 0, "w": 79, "h": 74 }, + "sourceSize": { "w": 79, "h": 74 }, + "duration": 100 + } + ], + "meta": { + "app": "https://www.aseprite.org/", + "version": "1.3.7-x64", + "format": "I8", + "size": { "w": 79, "h": 74 }, + "scale": "1" + } } diff --git a/public/images/pokemon/shiny/658.json b/public/images/pokemon/shiny/658.json index 92f9b29175c..219645ec240 100644 --- a/public/images/pokemon/shiny/658.json +++ b/public/images/pokemon/shiny/658.json @@ -1,41 +1,19 @@ -{ - "textures": [ - { - "image": "658.png", - "format": "RGBA8888", - "size": { - "w": 75, - "h": 75 - }, - "scale": 1, - "frames": [ - { - "filename": "0001.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 75, - "h": 65 - }, - "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 75, - "h": 65 - }, - "frame": { - "x": 0, - "y": 0, - "w": 75, - "h": 65 - } - } - ] - } - ], - "meta": { - "app": "https://www.codeandweb.com/texturepacker", - "version": "3.0", - "smartupdate": "$TexturePacker:SmartUpdate:be07c062265a19e890f1e2d2d1b5527d:ad4583a5a0498c496e9a93574c55ee03:5affcab976148657d36bf4ff3410f92d$" - } +{ "frames": [ + { + "filename": "0001.png", + "frame": { "x": 0, "y": 0, "w": 85, "h": 67 }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { "x": 0, "y": 0, "w": 85, "h": 67 }, + "sourceSize": { "w": 85, "h": 67 }, + "duration": 100 + } + ], + "meta": { + "app": "https://www.aseprite.org/", + "version": "1.3.7-x64", + "format": "I8", + "size": { "w": 85, "h": 67 }, + "scale": "1" + } } diff --git a/public/images/pokemon/shiny/688.json b/public/images/pokemon/shiny/688.json index 069d176730e..3d9aa902a54 100644 --- a/public/images/pokemon/shiny/688.json +++ b/public/images/pokemon/shiny/688.json @@ -5,8 +5,7 @@ "rotated": false, "trimmed": false, "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 63 }, - "sourceSize": { "w": 64, "h": 63 }, - "duration": 100 + "sourceSize": { "w": 64, "h": 63 } } ], "meta": { diff --git a/public/images/statuses_es.json b/public/images/statuses_es-ES.json similarity index 98% rename from public/images/statuses_es.json rename to public/images/statuses_es-ES.json index 4b44aa117e4..dbb3783842a 100644 --- a/public/images/statuses_es.json +++ b/public/images/statuses_es-ES.json @@ -1,7 +1,7 @@ { "textures": [ { - "image": "statuses_es.png", + "image": "statuses_es-ES.png", "format": "RGBA8888", "size": { "w": 22, diff --git a/public/images/statuses_es.png b/public/images/statuses_es-ES.png similarity index 100% rename from public/images/statuses_es.png rename to public/images/statuses_es-ES.png diff --git a/public/images/types_es.json b/public/images/types_es-ES.json similarity index 99% rename from public/images/types_es.json rename to public/images/types_es-ES.json index 0fb922e8939..198899c0f12 100644 --- a/public/images/types_es.json +++ b/public/images/types_es-ES.json @@ -1,7 +1,7 @@ { "textures": [ { - "image": "types_es.png", + "image": "types_es-ES.png", "format": "RGBA8888", "size": { "w": 32, diff --git a/public/images/types_es.png b/public/images/types_es-ES.png similarity index 100% rename from public/images/types_es.png rename to public/images/types_es-ES.png diff --git a/public/locales b/public/locales index 71390cba88f..fc4a1effd51 160000 --- a/public/locales +++ b/public/locales @@ -1 +1 @@ -Subproject commit 71390cba88f4103d0d2273d59a6dd8340a4fa54f +Subproject commit fc4a1effd5170def3c8314208a52cd0d8e6913ef diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 3cbf4d7b422..3c561206abe 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -323,6 +323,7 @@ export default class BattleScene extends SceneBase { this.conditionalQueue = []; this.phaseQueuePrependSpliceIndex = -1; this.nextCommandPhaseQueue = []; + this.eventManager = new TimedEventManager(); this.updateGameInfo(); } @@ -378,7 +379,6 @@ export default class BattleScene extends SceneBase { this.fieldSpritePipeline = new FieldSpritePipeline(this.game); (this.renderer as Phaser.Renderer.WebGL.WebGLRenderer).pipelines.add("FieldSprite", this.fieldSpritePipeline); - this.eventManager = new TimedEventManager(); this.launchBattle(); } diff --git a/src/data/ability.ts b/src/data/ability.ts index 01e99966ff8..87f9e89e534 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -1,4 +1,4 @@ -import Pokemon, { HitResult, PlayerPokemon, PokemonMove } from "../field/pokemon"; +import Pokemon, { EnemyPokemon, HitResult, MoveResult, PlayerPokemon, PokemonMove } from "../field/pokemon"; import { Type } from "./type"; import { Constructor } from "#app/utils"; import * as Utils from "../utils"; @@ -9,7 +9,7 @@ import { StatusEffect, getNonVolatileStatusEffects, getStatusEffectDescriptor, g import { Gender } from "./gender"; import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, IncrementMovePriorityAttr, VariableMoveTypeAttr, RandomMovesetMoveAttr, RandomMoveAttr, NaturePowerAttr, CopyMoveAttr, MoveAttr, MultiHitAttr, SacrificialAttr, SacrificialAttrOnHit, NeutralDamageAgainstFlyingTypeMultiplierAttr, FixedDamageAttr } from "./move"; import { ArenaTagSide, ArenaTrapTag } from "./arena-tag"; -import { BerryModifier, PokemonHeldItemModifier } from "../modifier/modifier"; +import { BerryModifier, HitHealModifier, PokemonHeldItemModifier } from "../modifier/modifier"; import { TerrainType } from "./terrain"; import { SpeciesFormChangeManualTrigger, SpeciesFormChangeRevertWeatherFormTrigger, SpeciesFormChangeWeatherTrigger } from "./pokemon-forms"; import i18next from "i18next"; @@ -17,7 +17,7 @@ import { Localizable } from "#app/interfaces/locales"; import { Command } from "../ui/command-ui-handler"; import { BerryModifierType } from "#app/modifier/modifier-type"; import { getPokeballName } from "./pokeball"; -import { BattlerIndex } from "#app/battle"; +import { BattlerIndex, BattleType } from "#app/battle"; import { Abilities } from "#enums/abilities"; import { ArenaTagType } from "#enums/arena-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type"; @@ -29,6 +29,12 @@ import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase"; import { ShowAbilityPhase } from "#app/phases/show-ability-phase"; import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; import BattleScene from "#app/battle-scene"; +import { SwitchType } from "#app/enums/switch-type"; +import { SwitchPhase } from "#app/phases/switch-phase"; +import { SwitchSummonPhase } from "#app/phases/switch-summon-phase"; +import { BattleEndPhase } from "#app/phases/battle-end-phase"; +import { NewBattlePhase } from "#app/phases/new-battle-phase"; +import { MoveEndPhase } from "#app/phases/move-end-phase"; export class Ability implements Localizable { public id: Abilities; @@ -330,6 +336,30 @@ export class ReceivedMoveDamageMultiplierAbAttr extends PreDefendAbAttr { } } +/** + * Reduces the damage dealt to an allied Pokemon. Used by Friend Guard. + * @see {@linkcode applyPreDefend} + */ +export class AlliedFieldDamageReductionAbAttr extends PreDefendAbAttr { + private damageMultiplier: number; + + constructor(damageMultiplier: number) { + super(); + this.damageMultiplier = damageMultiplier; + } + + /** + * Handles the damage reduction + * @param args + * - `[0]` {@linkcode Utils.NumberHolder} - The damage being dealt + */ + override applyPreDefend(_pokemon: Pokemon, _passive: boolean, _simulated: boolean, _attacker: Pokemon, _move: Move, _cancelled: Utils.BooleanHolder, args: any[]): boolean { + const damage = args[0] as Utils.NumberHolder; + damage.value = Utils.toDmgValue(damage.value * this.damageMultiplier); + return true; + } +} + export class ReceivedTypeDamageMultiplierAbAttr extends ReceivedMoveDamageMultiplierAbAttr { constructor(moveType: Type, damageMultiplier: number) { super((target, user, move) => user.getMoveType(move) === moveType, damageMultiplier); @@ -3068,7 +3098,7 @@ export class SuppressWeatherEffectAbAttr extends PreWeatherEffectAbAttr { /** * Condition function to applied to abilities related to Sheer Force. * Checks if last move used against target was affected by a Sheer Force user and: - * Disables: Color Change, Pickpocket, Wimp Out, Emergency Exit, Berserk, Anger Shell + * Disables: Color Change, Pickpocket, Berserk, Anger Shell * @returns {AbAttrCondition} If false disables the ability which the condition is applied to. */ function getSheerForceHitDisableAbCondition(): AbAttrCondition { @@ -3590,22 +3620,19 @@ export class MoodyAbAttr extends PostTurnAbAttr { } } -export class PostTurnStatStageChangeAbAttr extends PostTurnAbAttr { - private stats: BattleStat[]; - private stages: number; +export class SpeedBoostAbAttr extends PostTurnAbAttr { - constructor(stats: BattleStat[], stages: number) { + constructor() { super(true); - - this.stats = Array.isArray(stats) - ? stats - : [ stats ]; - this.stages = stages; } applyPostTurn(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean { if (!simulated) { - pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, this.stats, this.stages)); + if (!pokemon.turnData.switchedInThisTurn && !pokemon.turnData.failedRunAway) { + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ Stat.SPD ], 1)); + } else { + return false; + } } return true; } @@ -4679,6 +4706,84 @@ export class PreventBypassSpeedChanceAbAttr extends AbAttr { } } +/** + * This applies a terrain-based type change to the Pokemon. + * Used by Mimicry. + */ +export class TerrainEventTypeChangeAbAttr extends PostSummonAbAttr { + constructor() { + super(true); + } + + override apply(pokemon: Pokemon, _passive: boolean, _simulated: boolean, _cancelled: Utils.BooleanHolder, _args: any[]): boolean { + if (pokemon.isTerastallized()) { + return false; + } + const currentTerrain = pokemon.scene.arena.getTerrainType(); + const typeChange: Type[] = this.determineTypeChange(pokemon, currentTerrain); + if (typeChange.length !== 0) { + if (pokemon.summonData.addedType && typeChange.includes(pokemon.summonData.addedType)) { + pokemon.summonData.addedType = null; + } + pokemon.summonData.types = typeChange; + pokemon.updateInfo(); + } + return true; + } + + /** + * Retrieves the type(s) the Pokemon should change to in response to a terrain + * @param pokemon + * @param currentTerrain {@linkcode TerrainType} + * @returns a list of type(s) + */ + private determineTypeChange(pokemon: Pokemon, currentTerrain: TerrainType): Type[] { + const typeChange: Type[] = []; + switch (currentTerrain) { + case TerrainType.ELECTRIC: + typeChange.push(Type.ELECTRIC); + break; + case TerrainType.MISTY: + typeChange.push(Type.FAIRY); + break; + case TerrainType.GRASSY: + typeChange.push(Type.GRASS); + break; + case TerrainType.PSYCHIC: + typeChange.push(Type.PSYCHIC); + break; + default: + pokemon.getTypes(false, false, true).forEach(t => { + typeChange.push(t); + }); + break; + } + return typeChange; + } + + /** + * Checks if the Pokemon should change types if summoned into an active terrain + * @returns `true` if there is an active terrain requiring a type change | `false` if not + */ + override applyPostSummon(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean | Promise { + if (pokemon.scene.arena.getTerrainType() !== TerrainType.NONE) { + return this.apply(pokemon, passive, simulated, new Utils.BooleanHolder(false), []); + } + return false; + } + + override getTriggerMessage(pokemon: Pokemon, abilityName: string, ...args: any[]) { + const currentTerrain = pokemon.scene.arena.getTerrainType(); + const pokemonNameWithAffix = getPokemonNameWithAffix(pokemon); + if (currentTerrain === TerrainType.NONE) { + return i18next.t("abilityTriggers:pokemonTypeChangeRevert", { pokemonNameWithAffix }); + } else { + const moveType = i18next.t(`pokemonInfo:Type.${Type[this.determineTypeChange(pokemon, currentTerrain)[0]]}`); + return i18next.t("abilityTriggers:pokemonTypeChange", { pokemonNameWithAffix, moveType }); + } + } +} + async function applyAbAttrsInternal( attrType: Constructor, pokemon: Pokemon | null, @@ -4734,6 +4839,239 @@ async function applyAbAttrsInternal( } } +class ForceSwitchOutHelper { + constructor(private switchType: SwitchType) {} + + /** + * Handles the logic for switching out a Pokémon based on battle conditions, HP, and the switch type. + * + * @param pokemon The {@linkcode Pokemon} attempting to switch out. + * @returns `true` if the switch is successful + */ + public switchOutLogic(pokemon: Pokemon): boolean { + const switchOutTarget = pokemon; + /** + * If the switch-out target is a player-controlled Pokémon, the function checks: + * - Whether there are available party members to switch in. + * - If the Pokémon is still alive (hp > 0), and if so, it leaves the field and a new SwitchPhase is initiated. + */ + if (switchOutTarget instanceof PlayerPokemon) { + if (switchOutTarget.scene.getParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) { + return false; + } + + if (switchOutTarget.hp > 0) { + switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH); + pokemon.scene.prependToPhase(new SwitchPhase(pokemon.scene, this.switchType, switchOutTarget.getFieldIndex(), true, true), MoveEndPhase); + return true; + } + /** + * For non-wild battles, it checks if the opposing party has any available Pokémon to switch in. + * If yes, the Pokémon leaves the field and a new SwitchSummonPhase is initiated. + */ + } else if (pokemon.scene.currentBattle.battleType !== BattleType.WILD) { + if (switchOutTarget.scene.getEnemyParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) { + return false; + } + if (switchOutTarget.hp > 0) { + switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH); + pokemon.scene.prependToPhase(new SwitchSummonPhase(pokemon.scene, this.switchType, switchOutTarget.getFieldIndex(), + (pokemon.scene.currentBattle.trainer ? pokemon.scene.currentBattle.trainer.getNextSummonIndex((switchOutTarget as EnemyPokemon).trainerSlot) : 0), + false, false), MoveEndPhase); + return true; + } + /** + * For wild Pokémon battles, the Pokémon will flee if the conditions are met (waveIndex and double battles). + */ + } else { + if (!pokemon.scene.currentBattle.waveIndex && pokemon.scene.currentBattle.waveIndex % 10 === 0) { + return false; + } + + if (switchOutTarget.hp > 0) { + switchOutTarget.leaveField(false); + pokemon.scene.queueMessage(i18next.t("moveTriggers:fled", { pokemonName: getPokemonNameWithAffix(switchOutTarget) }), null, true, 500); + + if (switchOutTarget.scene.currentBattle.double) { + const allyPokemon = switchOutTarget.getAlly(); + switchOutTarget.scene.redirectPokemonMoves(switchOutTarget, allyPokemon); + } + } + + if (!switchOutTarget.getAlly()?.isActive(true)) { + pokemon.scene.clearEnemyHeldItemModifiers(); + + if (switchOutTarget.hp) { + pokemon.scene.pushPhase(new BattleEndPhase(pokemon.scene)); + pokemon.scene.pushPhase(new NewBattlePhase(pokemon.scene)); + } + } + } + return false; + } + + /** + * Determines if a Pokémon can switch out based on its status, the opponent's status, and battle conditions. + * + * @param pokemon The Pokémon attempting to switch out. + * @param opponent The opponent Pokémon. + * @returns `true` if the switch-out condition is met + */ + public getSwitchOutCondition(pokemon: Pokemon, opponent: Pokemon): boolean { + const switchOutTarget = pokemon; + const player = switchOutTarget instanceof PlayerPokemon; + + if (player) { + const blockedByAbility = new Utils.BooleanHolder(false); + applyAbAttrs(ForceSwitchOutImmunityAbAttr, opponent, blockedByAbility); + return !blockedByAbility.value; + } + + if (!player && pokemon.scene.currentBattle.battleType === BattleType.WILD) { + if (!pokemon.scene.currentBattle.waveIndex && pokemon.scene.currentBattle.waveIndex % 10 === 0) { + return false; + } + } + + if (!player && pokemon.scene.currentBattle.isBattleMysteryEncounter() && !pokemon.scene.currentBattle.mysteryEncounter?.fleeAllowed) { + return false; + } + + const party = player ? pokemon.scene.getParty() : pokemon.scene.getEnemyParty(); + return (!player && pokemon.scene.currentBattle.battleType === BattleType.WILD) + || party.filter(p => p.isAllowedInBattle() + && (player || (p as EnemyPokemon).trainerSlot === (switchOutTarget as EnemyPokemon).trainerSlot)).length > pokemon.scene.currentBattle.getBattlerCount(); + } + + /** + * Returns a message if the switch-out attempt fails due to ability effects. + * + * @param target The target Pokémon. + * @returns The failure message, or `null` if no failure. + */ + public getFailedText(target: Pokemon): string | null { + const blockedByAbility = new Utils.BooleanHolder(false); + applyAbAttrs(ForceSwitchOutImmunityAbAttr, target, blockedByAbility); + return blockedByAbility.value ? i18next.t("moveTriggers:cannotBeSwitchedOut", { pokemonName: getPokemonNameWithAffix(target) }) : null; + } +} + +/** + * Calculates the amount of recovery from the Shell Bell item. + * + * If the Pokémon is holding a Shell Bell, this function computes the amount of health + * recovered based on the damage dealt in the current turn. The recovery is multiplied by the + * Shell Bell's modifier (if any). + * + * @param pokemon - The Pokémon whose Shell Bell recovery is being calculated. + * @returns The amount of health recovered by Shell Bell. + */ +function calculateShellBellRecovery(pokemon: Pokemon): number { + const shellBellModifier = pokemon.getHeldItems().find(m => m instanceof HitHealModifier); + if (shellBellModifier) { + return Utils.toDmgValue(pokemon.turnData.totalDamageDealt / 8) * shellBellModifier.stackCount; + } + return 0; +} + +/** + * Triggers after the Pokemon takes any damage + * @extends AbAttr + */ +export class PostDamageAbAttr extends AbAttr { + public applyPostDamage(pokemon: Pokemon, damage: number, passive: boolean, simulated: boolean, args: any[], source?: Pokemon): boolean | Promise { + return false; + } +} + +/** + * Ability attribute for forcing a Pokémon to switch out after its health drops below half. + * This attribute checks various conditions related to the damage received, the moves used by the Pokémon + * and its opponents, and determines whether a forced switch-out should occur. + * + * Used by Wimp Out and Emergency Exit + * + * @extends PostDamageAbAttr + * @see {@linkcode applyPostDamage} + */ +export class PostDamageForceSwitchAbAttr extends PostDamageAbAttr { + private helper: ForceSwitchOutHelper = new ForceSwitchOutHelper(SwitchType.SWITCH); + private hpRatio: number; + + constructor(hpRatio: number = 0.5) { + super(); + this.hpRatio = hpRatio; + } + + /** + * Applies the switch-out logic after the Pokémon takes damage. + * Checks various conditions based on the moves used by the Pokémon, the opponents' moves, and + * the Pokémon's health after damage to determine whether the switch-out should occur. + * + * @param pokemon The Pokémon that took damage. + * @param damage The amount of damage taken by the Pokémon. + * @param passive N/A + * @param simulated Whether the ability is being simulated. + * @param args N/A + * @param source The Pokemon that dealt damage + * @returns `true` if the switch-out logic was successfully applied + */ + public override applyPostDamage(pokemon: Pokemon, damage: number, passive: boolean, simulated: boolean, args: any[], source?: Pokemon): boolean | Promise { + const moveHistory = pokemon.getMoveHistory(); + // Will not activate when the Pokémon's HP is lowered by cutting its own HP + const fordbiddenAttackingMoves = [ Moves.BELLY_DRUM, Moves.SUBSTITUTE, Moves.CURSE, Moves.PAIN_SPLIT ]; + if (moveHistory.length > 0) { + const lastMoveUsed = moveHistory[moveHistory.length - 1]; + if (fordbiddenAttackingMoves.includes(lastMoveUsed.move)) { + return false; + } + } + + // Dragon Tail and Circle Throw switch out Pokémon before the Ability activates. + const fordbiddenDefendingMoves = [ Moves.DRAGON_TAIL, Moves.CIRCLE_THROW ]; + if (source) { + const enemyMoveHistory = source.getMoveHistory(); + if (enemyMoveHistory.length > 0) { + const enemyLastMoveUsed = enemyMoveHistory[enemyMoveHistory.length - 1]; + // Will not activate if the Pokémon's HP falls below half while it is in the air during Sky Drop. + if (fordbiddenDefendingMoves.includes(enemyLastMoveUsed.move) || enemyLastMoveUsed.move === Moves.SKY_DROP && enemyLastMoveUsed.result === MoveResult.OTHER) { + return false; + // Will not activate if the Pokémon's HP falls below half by a move affected by Sheer Force. + } else if (allMoves[enemyLastMoveUsed.move].chance >= 0 && source.hasAbility(Abilities.SHEER_FORCE)) { + return false; + // Activate only after the last hit of multistrike moves + } else if (source.turnData.hitsLeft > 1) { + return false; + } + if (source.turnData.hitCount > 1) { + damage = pokemon.turnData.damageTaken; + } + } + } + + if (pokemon.hp + damage >= pokemon.getMaxHp() * this.hpRatio) { + // Activates if it falls below half and recovers back above half from a Shell Bell + const shellBellHeal = calculateShellBellRecovery(pokemon); + if (pokemon.hp - shellBellHeal < pokemon.getMaxHp() * this.hpRatio) { + for (const opponent of pokemon.getOpponents()) { + if (!this.helper.getSwitchOutCondition(pokemon, opponent)) { + return false; + } + } + return this.helper.switchOutLogic(pokemon); + } else { + return false; + } + } else { + return false; + } + } + public getFailedText(user: Pokemon, target: Pokemon, move: Move, cancelled: Utils.BooleanHolder): string | null { + return this.helper.getFailedText(target); + } +} + + export function applyAbAttrs(attrType: Constructor, pokemon: Pokemon, cancelled: Utils.BooleanHolder | null, simulated: boolean = false, ...args: any[]): Promise { return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.apply(pokemon, passive, simulated, cancelled, args), args, false, simulated); } @@ -4767,6 +5105,11 @@ export function applyPostSetStatusAbAttrs(attrType: Constructor(attrType, pokemon, (attr, passive) => attr.applyPostSetStatus(pokemon, sourcePokemon, passive, effect, simulated, args), args, false, simulated); } +export function applyPostDamageAbAttrs(attrType: Constructor, + pokemon: Pokemon, damage: number, passive: boolean, simulated: boolean = false, args: any[], source?: Pokemon): Promise { + return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.applyPostDamage(pokemon, damage, passive, simulated, args, source), args); +} + /** * Applies a field Stat multiplier attribute * @param attrType {@linkcode FieldMultiplyStatAbAttr} should always be FieldMultiplyBattleStatAbAttr for the time being @@ -4909,7 +5252,7 @@ export function initAbilities() { .attr(PostSummonWeatherChangeAbAttr, WeatherType.RAIN) .attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.RAIN), new Ability(Abilities.SPEED_BOOST, 3) - .attr(PostTurnStatStageChangeAbAttr, [ Stat.SPD ], 1), + .attr(SpeedBoostAbAttr), new Ability(Abilities.BATTLE_ARMOR, 3) .attr(BlockCritAbAttr) .ignorable(), @@ -4963,7 +5306,8 @@ export function initAbilities() { .attr(TypeImmunityAddBattlerTagAbAttr, Type.FIRE, BattlerTagType.FIRE_BOOST, 1) .ignorable(), new Ability(Abilities.SHIELD_DUST, 3) - .attr(IgnoreMoveEffectsAbAttr), + .attr(IgnoreMoveEffectsAbAttr) + .ignorable(), new Ability(Abilities.OWN_TEMPO, 3) .attr(BattlerTagImmunityAbAttr, BattlerTagType.CONFUSED) .attr(IntimidateImmunityAbAttr) @@ -5081,11 +5425,9 @@ export function initAbilities() { new Ability(Abilities.CUTE_CHARM, 3) .attr(PostDefendContactApplyTagChanceAbAttr, 30, BattlerTagType.INFATUATED), new Ability(Abilities.PLUS, 3) - .conditionalAttr(p => p.scene.currentBattle.double && [ Abilities.PLUS, Abilities.MINUS ].some(a => p.getAlly().hasAbility(a)), StatMultiplierAbAttr, Stat.SPATK, 1.5) - .ignorable(), + .conditionalAttr(p => p.scene.currentBattle.double && [ Abilities.PLUS, Abilities.MINUS ].some(a => p.getAlly().hasAbility(a)), StatMultiplierAbAttr, Stat.SPATK, 1.5), new Ability(Abilities.MINUS, 3) - .conditionalAttr(p => p.scene.currentBattle.double && [ Abilities.PLUS, Abilities.MINUS ].some(a => p.getAlly().hasAbility(a)), StatMultiplierAbAttr, Stat.SPATK, 1.5) - .ignorable(), + .conditionalAttr(p => p.scene.currentBattle.double && [ Abilities.PLUS, Abilities.MINUS ].some(a => p.getAlly().hasAbility(a)), StatMultiplierAbAttr, Stat.SPATK, 1.5), new Ability(Abilities.FORECAST, 3) .attr(UncopiableAbilityAbAttr) .attr(NoFusionAbilityAbAttr) @@ -5310,8 +5652,8 @@ export function initAbilities() { new Ability(Abilities.HEALER, 5) .conditionalAttr(pokemon => pokemon.getAlly() && Utils.randSeedInt(10) < 3, PostTurnResetStatusAbAttr, true), new Ability(Abilities.FRIEND_GUARD, 5) - .ignorable() - .unimplemented(), + .attr(AlliedFieldDamageReductionAbAttr, 0.75) + .ignorable(), new Ability(Abilities.WEAK_ARMOR, 5) .attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.category === MoveCategory.PHYSICAL, Stat.DEF, -1) .attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.category === MoveCategory.PHYSICAL, Stat.SPD, 2), @@ -5423,7 +5765,8 @@ export function initAbilities() { .attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonTeravolt", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })) .attr(MoveAbilityBypassAbAttr), new Ability(Abilities.AROMA_VEIL, 6) - .attr(UserFieldBattlerTagImmunityAbAttr, [ BattlerTagType.INFATUATED, BattlerTagType.TAUNT, BattlerTagType.DISABLED, BattlerTagType.TORMENT, BattlerTagType.HEAL_BLOCK ]), + .attr(UserFieldBattlerTagImmunityAbAttr, [ BattlerTagType.INFATUATED, BattlerTagType.TAUNT, BattlerTagType.DISABLED, BattlerTagType.TORMENT, BattlerTagType.HEAL_BLOCK ]) + .ignorable(), new Ability(Abilities.FLOWER_VEIL, 6) .ignorable() .unimplemented(), @@ -5508,11 +5851,11 @@ export function initAbilities() { new Ability(Abilities.STAMINA, 7) .attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS, Stat.DEF, 1), new Ability(Abilities.WIMP_OUT, 7) - .condition(getSheerForceHitDisableAbCondition()) - .unimplemented(), + .attr(PostDamageForceSwitchAbAttr) + .edgeCase(), // Should not trigger when hurting itself in confusion new Ability(Abilities.EMERGENCY_EXIT, 7) - .condition(getSheerForceHitDisableAbCondition()) - .unimplemented(), + .attr(PostDamageForceSwitchAbAttr) + .edgeCase(), // Should not trigger when hurting itself in confusion new Ability(Abilities.WATER_COMPACTION, 7) .attr(PostDefendStatStageChangeAbAttr, (target, user, move) => user.getMoveType(move) === Type.WATER && move.category !== MoveCategory.STATUS, Stat.DEF, 2), new Ability(Abilities.MERCILESS, 7) @@ -5743,7 +6086,7 @@ export function initAbilities() { new Ability(Abilities.POWER_SPOT, 8) .attr(AllyMoveCategoryPowerBoostAbAttr, [ MoveCategory.SPECIAL, MoveCategory.PHYSICAL ], 1.3), new Ability(Abilities.MIMICRY, 8) - .unimplemented(), + .attr(TerrainEventTypeChangeAbAttr), new Ability(Abilities.SCREEN_CLEANER, 8) .attr(PostSummonRemoveArenaTagAbAttr, [ ArenaTagType.AURORA_VEIL, ArenaTagType.LIGHT_SCREEN, ArenaTagType.REFLECT ]), new Ability(Abilities.STEELY_SPIRIT, 8) @@ -5874,16 +6217,14 @@ export function initAbilities() { .ignorable(), new Ability(Abilities.SWORD_OF_RUIN, 9) .attr(FieldMultiplyStatAbAttr, Stat.DEF, 0.75) - .attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonSwordOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.DEF)) })) - .ignorable(), + .attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonSwordOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.DEF)) })), new Ability(Abilities.TABLETS_OF_RUIN, 9) .attr(FieldMultiplyStatAbAttr, Stat.ATK, 0.75) .attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonTabletsOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.ATK)) })) .ignorable(), new Ability(Abilities.BEADS_OF_RUIN, 9) .attr(FieldMultiplyStatAbAttr, Stat.SPDEF, 0.75) - .attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonBeadsOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.SPDEF)) })) - .ignorable(), + .attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonBeadsOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.SPDEF)) })), new Ability(Abilities.ORICHALCUM_PULSE, 9) .attr(PostSummonWeatherChangeAbAttr, WeatherType.SUNNY) .attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.SUNNY) @@ -5900,7 +6241,7 @@ export function initAbilities() { .attr(MovePowerBoostAbAttr, (user, target, move) => move.hasFlag(MoveFlags.SLICING_MOVE), 1.5), new Ability(Abilities.SUPREME_OVERLORD, 9) .attr(VariableMovePowerBoostAbAttr, (user, target, move) => 1 + 0.1 * Math.min(user.isPlayer() ? user.scene.currentBattle.playerFaints : user.scene.currentBattle.enemyFaints, 5)) - .partial(), // Counter resets every wave + .partial(), // Counter resets every wave instead of on arena reset new Ability(Abilities.COSTAR, 9) .attr(PostSummonCopyAllyStatsAbAttr), new Ability(Abilities.TOXIC_DEBRIS, 9) diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index d2c95b7ccdf..43de6e02dcb 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -126,6 +126,7 @@ export class MistTag extends ArenaTag { * Cancels the lowering of stats * @param arena the {@linkcode Arena} containing this effect * @param simulated `true` if the effect should be applied quietly + * @param attacker the {@linkcode Pokemon} using a move into this effect. * @param cancelled a {@linkcode BooleanHolder} whose value is set to `true` * to flag the stat reduction as cancelled * @returns `true` if a stat reduction was cancelled; `false` otherwise diff --git a/src/data/balance/pokemon-evolutions.ts b/src/data/balance/pokemon-evolutions.ts index 4a6e44e0d51..c0b213e4def 100644 --- a/src/data/balance/pokemon-evolutions.ts +++ b/src/data/balance/pokemon-evolutions.ts @@ -1443,7 +1443,7 @@ export const pokemonEvolutions: PokemonEvolutions = { ], [Species.ROCKRUFF]: [ new SpeciesFormEvolution(Species.LYCANROC, "", "midday", 25, null, new SpeciesEvolutionCondition(p => (p.scene.arena.getTimeOfDay() === TimeOfDay.DAWN || p.scene.arena.getTimeOfDay() === TimeOfDay.DAY) && (p.formIndex === 0))), - new SpeciesFormEvolution(Species.LYCANROC, "", "dusk", 25, null, new SpeciesEvolutionCondition(p => p.formIndex === 1)), + new SpeciesFormEvolution(Species.LYCANROC, "own-tempo", "dusk", 25, null, new SpeciesEvolutionCondition(p => p.formIndex === 1)), new SpeciesFormEvolution(Species.LYCANROC, "", "midnight", 25, null, new SpeciesEvolutionCondition(p => (p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT) && (p.formIndex === 0))) ], [Species.STEENEE]: [ diff --git a/src/data/balance/species-egg-tiers.ts b/src/data/balance/species-egg-tiers.ts index cd266dfcf54..27baa18151a 100644 --- a/src/data/balance/species-egg-tiers.ts +++ b/src/data/balance/species-egg-tiers.ts @@ -497,7 +497,7 @@ export const speciesEggTiers = { [Species.DREEPY]: EggTier.RARE, [Species.ZACIAN]: EggTier.LEGENDARY, [Species.ZAMAZENTA]: EggTier.LEGENDARY, - [Species.ETERNATUS]: EggTier.COMMON, + [Species.ETERNATUS]: EggTier.LEGENDARY, [Species.KUBFU]: EggTier.EPIC, [Species.ZARUDE]: EggTier.EPIC, [Species.REGIELEKI]: EggTier.EPIC, diff --git a/src/data/balance/tms.ts b/src/data/balance/tms.ts index 1a509637e05..7b65ae65ec4 100644 --- a/src/data/balance/tms.ts +++ b/src/data/balance/tms.ts @@ -12318,6 +12318,7 @@ export const tmSpecies: TmSpecies = { Species.TURTWIG, Species.GROTLE, Species.TORTERRA, + Species.BASTIODON, Species.CHINGLING, Species.BRONZOR, Species.BRONZONG, @@ -12325,6 +12326,7 @@ export const tmSpecies: TmSpecies = { Species.WEAVILE, Species.MAGNEZONE, Species.TANGROWTH, + Species.ELECTIVIRE, Species.TOGEKISS, Species.MAMOSWINE, Species.GALLADE, @@ -12357,6 +12359,8 @@ export const tmSpecies: TmSpecies = { Species.CRYOGONAL, Species.MIENFOO, Species.MIENSHAO, + Species.GOLETT, + Species.GOLURK, Species.HYDREIGON, Species.COBALION, Species.TERRAKION, @@ -40233,6 +40237,1065 @@ export const tmSpecies: TmSpecies = { Species.HISUI_ZORUA, Species.HISUI_ZOROARK, ], + [Moves.SECRET_POWER]: [ + Species.BULBASAUR, + Species.IVYSAUR, + Species.VENUSAUR, + Species.CHARMANDER, + Species.CHARMELEON, + Species.CHARIZARD, + Species.SQUIRTLE, + Species.WARTORTLE, + Species.BLASTOISE, + Species.BUTTERFREE, + Species.BEEDRILL, + Species.PIDGEY, + Species.PIDGEOTTO, + Species.PIDGEOT, + Species.RATTATA, + Species.RATICATE, + Species.SPEAROW, + Species.FEAROW, + Species.EKANS, + Species.ARBOK, + Species.PIKACHU, + Species.RAICHU, + Species.SANDSHREW, + Species.SANDSLASH, + Species.NIDORAN_F, + Species.NIDORINA, + Species.NIDOQUEEN, + Species.NIDORAN_M, + Species.NIDORINO, + Species.NIDOKING, + Species.CLEFAIRY, + Species.CLEFABLE, + Species.VULPIX, + Species.NINETALES, + Species.JIGGLYPUFF, + Species.WIGGLYTUFF, + Species.ZUBAT, + Species.GOLBAT, + Species.ODDISH, + Species.GLOOM, + Species.VILEPLUME, + Species.PARAS, + Species.PARASECT, + Species.VENONAT, + Species.VENOMOTH, + Species.DIGLETT, + Species.DUGTRIO, + Species.MEOWTH, + Species.PERSIAN, + Species.PSYDUCK, + Species.GOLDUCK, + Species.MANKEY, + Species.PRIMEAPE, + Species.GROWLITHE, + Species.ARCANINE, + Species.POLIWAG, + Species.POLIWHIRL, + Species.POLIWRATH, + Species.ABRA, + Species.KADABRA, + Species.ALAKAZAM, + Species.MACHOP, + Species.MACHOKE, + Species.MACHAMP, + Species.BELLSPROUT, + Species.WEEPINBELL, + Species.VICTREEBEL, + Species.TENTACOOL, + Species.TENTACRUEL, + Species.GEODUDE, + Species.GRAVELER, + Species.GOLEM, + Species.PONYTA, + Species.RAPIDASH, + Species.SLOWPOKE, + Species.SLOWBRO, + Species.MAGNEMITE, + Species.MAGNETON, + Species.FARFETCHD, + Species.DODUO, + Species.DODRIO, + Species.SEEL, + Species.DEWGONG, + Species.GRIMER, + Species.MUK, + Species.SHELLDER, + Species.CLOYSTER, + Species.GASTLY, + Species.HAUNTER, + Species.GENGAR, + Species.ONIX, + Species.DROWZEE, + Species.HYPNO, + Species.KRABBY, + Species.KINGLER, + Species.VOLTORB, + Species.ELECTRODE, + Species.EXEGGCUTE, + Species.EXEGGUTOR, + Species.CUBONE, + Species.MAROWAK, + Species.HITMONLEE, + Species.HITMONCHAN, + Species.LICKITUNG, + Species.KOFFING, + Species.WEEZING, + Species.RHYHORN, + Species.RHYDON, + Species.CHANSEY, + Species.TANGELA, + Species.KANGASKHAN, + Species.HORSEA, + Species.SEADRA, + Species.GOLDEEN, + Species.SEAKING, + Species.STARYU, + Species.STARMIE, + Species.MR_MIME, + Species.SCYTHER, + Species.JYNX, + Species.ELECTABUZZ, + Species.MAGMAR, + Species.PINSIR, + Species.TAUROS, + Species.GYARADOS, + Species.LAPRAS, + Species.EEVEE, + Species.VAPOREON, + Species.JOLTEON, + Species.FLAREON, + Species.PORYGON, + Species.OMANYTE, + Species.OMASTAR, + Species.KABUTO, + Species.KABUTOPS, + Species.AERODACTYL, + Species.SNORLAX, + Species.ARTICUNO, + Species.ZAPDOS, + Species.MOLTRES, + Species.DRATINI, + Species.DRAGONAIR, + Species.DRAGONITE, + Species.MEWTWO, + Species.MEW, + Species.CHIKORITA, + Species.BAYLEEF, + Species.MEGANIUM, + Species.CYNDAQUIL, + Species.QUILAVA, + Species.TYPHLOSION, + Species.TOTODILE, + Species.CROCONAW, + Species.FERALIGATR, + Species.SENTRET, + Species.FURRET, + Species.HOOTHOOT, + Species.NOCTOWL, + Species.LEDYBA, + Species.LEDIAN, + Species.SPINARAK, + Species.ARIADOS, + Species.CROBAT, + Species.CHINCHOU, + Species.LANTURN, + Species.PICHU, + Species.CLEFFA, + Species.IGGLYBUFF, + Species.TOGEPI, + Species.TOGETIC, + Species.NATU, + Species.XATU, + Species.MAREEP, + Species.FLAAFFY, + Species.AMPHAROS, + Species.BELLOSSOM, + Species.MARILL, + Species.AZUMARILL, + Species.SUDOWOODO, + Species.POLITOED, + Species.HOPPIP, + Species.SKIPLOOM, + Species.JUMPLUFF, + Species.AIPOM, + Species.SUNKERN, + Species.SUNFLORA, + Species.YANMA, + Species.WOOPER, + Species.QUAGSIRE, + Species.ESPEON, + Species.UMBREON, + Species.MURKROW, + Species.SLOWKING, + Species.MISDREAVUS, + Species.GIRAFARIG, + Species.PINECO, + Species.FORRETRESS, + Species.DUNSPARCE, + Species.GLIGAR, + Species.STEELIX, + Species.SNUBBULL, + Species.GRANBULL, + Species.QWILFISH, + Species.SCIZOR, + Species.SHUCKLE, + Species.HERACROSS, + Species.SNEASEL, + Species.TEDDIURSA, + Species.URSARING, + Species.SLUGMA, + Species.MAGCARGO, + Species.SWINUB, + Species.PILOSWINE, + Species.CORSOLA, + Species.REMORAID, + Species.OCTILLERY, + Species.DELIBIRD, + Species.MANTINE, + Species.SKARMORY, + Species.HOUNDOUR, + Species.HOUNDOOM, + Species.KINGDRA, + Species.PHANPY, + Species.DONPHAN, + Species.PORYGON2, + Species.STANTLER, + Species.TYROGUE, + Species.HITMONTOP, + Species.SMOOCHUM, + Species.ELEKID, + Species.MAGBY, + Species.MILTANK, + Species.BLISSEY, + Species.RAIKOU, + Species.ENTEI, + Species.SUICUNE, + Species.LARVITAR, + Species.PUPITAR, + Species.TYRANITAR, + Species.LUGIA, + Species.HO_OH, + Species.CELEBI, + Species.TREECKO, + Species.GROVYLE, + Species.SCEPTILE, + Species.TORCHIC, + Species.COMBUSKEN, + Species.BLAZIKEN, + Species.MUDKIP, + Species.MARSHTOMP, + Species.SWAMPERT, + Species.POOCHYENA, + Species.MIGHTYENA, + Species.ZIGZAGOON, + Species.LINOONE, + Species.BEAUTIFLY, + Species.DUSTOX, + Species.LOTAD, + Species.LOMBRE, + Species.LUDICOLO, + Species.SEEDOT, + Species.NUZLEAF, + Species.SHIFTRY, + Species.TAILLOW, + Species.SWELLOW, + Species.WINGULL, + Species.PELIPPER, + Species.RALTS, + Species.KIRLIA, + Species.GARDEVOIR, + Species.SURSKIT, + Species.MASQUERAIN, + Species.SHROOMISH, + Species.BRELOOM, + Species.SLAKOTH, + Species.VIGOROTH, + Species.SLAKING, + Species.NINCADA, + Species.NINJASK, + Species.SHEDINJA, + Species.WHISMUR, + Species.LOUDRED, + Species.EXPLOUD, + Species.MAKUHITA, + Species.HARIYAMA, + Species.AZURILL, + Species.NOSEPASS, + Species.SKITTY, + Species.DELCATTY, + Species.SABLEYE, + Species.MAWILE, + Species.ARON, + Species.LAIRON, + Species.AGGRON, + Species.MEDITITE, + Species.MEDICHAM, + Species.ELECTRIKE, + Species.MANECTRIC, + Species.PLUSLE, + Species.MINUN, + Species.VOLBEAT, + Species.ILLUMISE, + Species.ROSELIA, + Species.GULPIN, + Species.SWALOT, + Species.CARVANHA, + Species.SHARPEDO, + Species.WAILMER, + Species.WAILORD, + Species.NUMEL, + Species.CAMERUPT, + Species.TORKOAL, + Species.SPOINK, + Species.GRUMPIG, + Species.SPINDA, + Species.TRAPINCH, + Species.VIBRAVA, + Species.FLYGON, + Species.CACNEA, + Species.CACTURNE, + Species.SWABLU, + Species.ALTARIA, + Species.ZANGOOSE, + Species.SEVIPER, + Species.LUNATONE, + Species.SOLROCK, + Species.BARBOACH, + Species.WHISCASH, + Species.CORPHISH, + Species.CRAWDAUNT, + Species.BALTOY, + Species.CLAYDOL, + Species.LILEEP, + Species.CRADILY, + Species.ANORITH, + Species.ARMALDO, + Species.FEEBAS, + Species.MILOTIC, + Species.CASTFORM, + Species.KECLEON, + Species.SHUPPET, + Species.BANETTE, + Species.DUSKULL, + Species.DUSCLOPS, + Species.TROPIUS, + Species.CHIMECHO, + Species.ABSOL, + Species.SNORUNT, + Species.GLALIE, + Species.SPHEAL, + Species.SEALEO, + Species.WALREIN, + Species.CLAMPERL, + Species.HUNTAIL, + Species.GOREBYSS, + Species.RELICANTH, + Species.LUVDISC, + Species.BAGON, + Species.SHELGON, + Species.SALAMENCE, + Species.METANG, + Species.METAGROSS, + Species.REGIROCK, + Species.REGICE, + Species.REGISTEEL, + Species.LATIAS, + Species.LATIOS, + Species.KYOGRE, + Species.GROUDON, + Species.RAYQUAZA, + Species.JIRACHI, + Species.DEOXYS, + Species.TURTWIG, + Species.GROTLE, + Species.TORTERRA, + Species.CHIMCHAR, + Species.MONFERNO, + Species.INFERNAPE, + Species.PIPLUP, + Species.PRINPLUP, + Species.EMPOLEON, + Species.STARLY, + Species.STARAVIA, + Species.STARAPTOR, + Species.BIDOOF, + Species.BIBAREL, + Species.KRICKETUNE, + Species.SHINX, + Species.LUXIO, + Species.LUXRAY, + Species.BUDEW, + Species.ROSERADE, + Species.CRANIDOS, + Species.RAMPARDOS, + Species.SHIELDON, + Species.BASTIODON, + Species.WORMADAM, + Species.MOTHIM, + Species.VESPIQUEN, + Species.PACHIRISU, + Species.BUIZEL, + Species.FLOATZEL, + Species.CHERUBI, + Species.CHERRIM, + Species.SHELLOS, + Species.GASTRODON, + Species.AMBIPOM, + Species.DRIFLOON, + Species.DRIFBLIM, + Species.BUNEARY, + Species.LOPUNNY, + Species.MISMAGIUS, + Species.HONCHKROW, + Species.GLAMEOW, + Species.PURUGLY, + Species.CHINGLING, + Species.STUNKY, + Species.SKUNTANK, + Species.BRONZOR, + Species.BRONZONG, + Species.BONSLY, + Species.MIME_JR, + Species.HAPPINY, + Species.CHATOT, + Species.SPIRITOMB, + Species.GIBLE, + Species.GABITE, + Species.GARCHOMP, + Species.MUNCHLAX, + Species.RIOLU, + Species.LUCARIO, + Species.HIPPOPOTAS, + Species.HIPPOWDON, + Species.SKORUPI, + Species.DRAPION, + Species.CROAGUNK, + Species.TOXICROAK, + Species.CARNIVINE, + Species.FINNEON, + Species.LUMINEON, + Species.MANTYKE, + Species.SNOVER, + Species.ABOMASNOW, + Species.WEAVILE, + Species.MAGNEZONE, + Species.LICKILICKY, + Species.RHYPERIOR, + Species.TANGROWTH, + Species.ELECTIVIRE, + Species.MAGMORTAR, + Species.TOGEKISS, + Species.YANMEGA, + Species.LEAFEON, + Species.GLACEON, + Species.GLISCOR, + Species.MAMOSWINE, + Species.PORYGON_Z, + Species.GALLADE, + Species.PROBOPASS, + Species.DUSKNOIR, + Species.FROSLASS, + Species.ROTOM, + Species.UXIE, + Species.MESPRIT, + Species.AZELF, + Species.DIALGA, + Species.PALKIA, + Species.HEATRAN, + Species.REGIGIGAS, + Species.GIRATINA, + Species.CRESSELIA, + Species.PHIONE, + Species.MANAPHY, + Species.DARKRAI, + Species.SHAYMIN, + Species.ARCEUS, + Species.VICTINI, + Species.SNIVY, + Species.SERVINE, + Species.SERPERIOR, + Species.TEPIG, + Species.PIGNITE, + Species.EMBOAR, + Species.OSHAWOTT, + Species.DEWOTT, + Species.SAMUROTT, + Species.PATRAT, + Species.WATCHOG, + Species.LILLIPUP, + Species.HERDIER, + Species.STOUTLAND, + Species.PURRLOIN, + Species.LIEPARD, + Species.PANSAGE, + Species.SIMISAGE, + Species.PANSEAR, + Species.SIMISEAR, + Species.PANPOUR, + Species.SIMIPOUR, + Species.MUNNA, + Species.MUSHARNA, + Species.PIDOVE, + Species.TRANQUILL, + Species.UNFEZANT, + Species.BLITZLE, + Species.ZEBSTRIKA, + Species.ROGGENROLA, + Species.BOLDORE, + Species.GIGALITH, + Species.WOOBAT, + Species.SWOOBAT, + Species.DRILBUR, + Species.EXCADRILL, + Species.AUDINO, + Species.TIMBURR, + Species.GURDURR, + Species.CONKELDURR, + Species.TYMPOLE, + Species.PALPITOAD, + Species.SEISMITOAD, + Species.THROH, + Species.SAWK, + Species.SEWADDLE, + Species.SWADLOON, + Species.LEAVANNY, + Species.VENIPEDE, + Species.WHIRLIPEDE, + Species.SCOLIPEDE, + Species.COTTONEE, + Species.WHIMSICOTT, + Species.PETILIL, + Species.LILLIGANT, + Species.BASCULIN, + Species.SANDILE, + Species.KROKOROK, + Species.KROOKODILE, + Species.DARUMAKA, + Species.DARMANITAN, + Species.MARACTUS, + Species.DWEBBLE, + Species.CRUSTLE, + Species.SCRAGGY, + Species.SCRAFTY, + Species.SIGILYPH, + Species.YAMASK, + Species.COFAGRIGUS, + Species.TIRTOUGA, + Species.CARRACOSTA, + Species.ARCHEN, + Species.ARCHEOPS, + Species.TRUBBISH, + Species.GARBODOR, + Species.ZORUA, + Species.ZOROARK, + Species.MINCCINO, + Species.CINCCINO, + Species.GOTHITA, + Species.GOTHORITA, + Species.GOTHITELLE, + Species.SOLOSIS, + Species.DUOSION, + Species.REUNICLUS, + Species.DUCKLETT, + Species.SWANNA, + Species.VANILLITE, + Species.VANILLISH, + Species.VANILLUXE, + Species.DEERLING, + Species.SAWSBUCK, + Species.EMOLGA, + Species.KARRABLAST, + Species.ESCAVALIER, + Species.FOONGUS, + Species.AMOONGUSS, + Species.FRILLISH, + Species.JELLICENT, + Species.ALOMOMOLA, + Species.JOLTIK, + Species.GALVANTULA, + Species.FERROSEED, + Species.FERROTHORN, + Species.KLINK, + Species.KLANG, + Species.KLINKLANG, + Species.EELEKTRIK, + Species.EELEKTROSS, + Species.ELGYEM, + Species.BEHEEYEM, + Species.LITWICK, + Species.LAMPENT, + Species.CHANDELURE, + Species.AXEW, + Species.FRAXURE, + Species.HAXORUS, + Species.CUBCHOO, + Species.BEARTIC, + Species.CRYOGONAL, + Species.SHELMET, + Species.ACCELGOR, + Species.STUNFISK, + Species.MIENFOO, + Species.MIENSHAO, + Species.DRUDDIGON, + Species.GOLETT, + Species.GOLURK, + Species.PAWNIARD, + Species.BISHARP, + Species.BOUFFALANT, + Species.RUFFLET, + Species.BRAVIARY, + Species.VULLABY, + Species.MANDIBUZZ, + Species.HEATMOR, + Species.DURANT, + Species.DEINO, + Species.ZWEILOUS, + Species.HYDREIGON, + Species.LARVESTA, + Species.VOLCARONA, + Species.COBALION, + Species.TERRAKION, + Species.VIRIZION, + Species.TORNADUS, + Species.THUNDURUS, + Species.RESHIRAM, + Species.ZEKROM, + Species.LANDORUS, + Species.KYUREM, + Species.KELDEO, + Species.MELOETTA, + Species.GENESECT, + Species.CHESPIN, + Species.QUILLADIN, + Species.CHESNAUGHT, + Species.FENNEKIN, + Species.BRAIXEN, + Species.DELPHOX, + Species.FROAKIE, + Species.FROGADIER, + Species.GRENINJA, + Species.BUNNELBY, + Species.DIGGERSBY, + Species.FLETCHLING, + Species.FLETCHINDER, + Species.TALONFLAME, + Species.VIVILLON, + Species.LITLEO, + Species.PYROAR, + Species.FLABEBE, + Species.FLOETTE, + Species.FLORGES, + Species.SKIDDO, + Species.GOGOAT, + Species.PANCHAM, + Species.PANGORO, + Species.FURFROU, + Species.ESPURR, + Species.MEOWSTIC, + Species.HONEDGE, + Species.DOUBLADE, + Species.AEGISLASH, + Species.SPRITZEE, + Species.AROMATISSE, + Species.SWIRLIX, + Species.SLURPUFF, + Species.INKAY, + Species.MALAMAR, + Species.BINACLE, + Species.BARBARACLE, + Species.SKRELP, + Species.DRAGALGE, + Species.CLAUNCHER, + Species.CLAWITZER, + Species.HELIOPTILE, + Species.HELIOLISK, + Species.TYRUNT, + Species.TYRANTRUM, + Species.AMAURA, + Species.AURORUS, + Species.SYLVEON, + Species.HAWLUCHA, + Species.DEDENNE, + Species.CARBINK, + Species.GOOMY, + Species.SLIGGOO, + Species.GOODRA, + Species.KLEFKI, + Species.PHANTUMP, + Species.TREVENANT, + Species.PUMPKABOO, + Species.GOURGEIST, + Species.BERGMITE, + Species.AVALUGG, + Species.NOIBAT, + Species.NOIVERN, + Species.XERNEAS, + Species.YVELTAL, + Species.ZYGARDE, + Species.DIANCIE, + Species.HOOPA, + Species.VOLCANION, + Species.ROWLET, + Species.DARTRIX, + Species.DECIDUEYE, + Species.LITTEN, + Species.TORRACAT, + Species.INCINEROAR, + Species.POPPLIO, + Species.BRIONNE, + Species.PRIMARINA, + Species.PIKIPEK, + Species.TRUMBEAK, + Species.TOUCANNON, + Species.YUNGOOS, + Species.GUMSHOOS, + Species.GRUBBIN, + Species.CHARJABUG, + Species.VIKAVOLT, + Species.CRABRAWLER, + Species.CRABOMINABLE, + Species.ORICORIO, + Species.CUTIEFLY, + Species.RIBOMBEE, + Species.ROCKRUFF, + Species.LYCANROC, + Species.WISHIWASHI, + Species.MAREANIE, + Species.TOXAPEX, + Species.MUDBRAY, + Species.MUDSDALE, + Species.DEWPIDER, + Species.ARAQUANID, + Species.FOMANTIS, + Species.LURANTIS, + Species.MORELULL, + Species.SHIINOTIC, + Species.SALANDIT, + Species.SALAZZLE, + Species.STUFFUL, + Species.BEWEAR, + Species.BOUNSWEET, + Species.STEENEE, + Species.TSAREENA, + Species.COMFEY, + Species.ORANGURU, + Species.PASSIMIAN, + Species.WIMPOD, + Species.GOLISOPOD, + Species.SANDYGAST, + Species.PALOSSAND, + Species.TYPE_NULL, + Species.SILVALLY, + Species.MINIOR, + Species.KOMALA, + Species.TURTONATOR, + Species.TOGEDEMARU, + Species.MIMIKYU, + Species.BRUXISH, + Species.DRAMPA, + Species.DHELMISE, + Species.JANGMO_O, + Species.HAKAMO_O, + Species.KOMMO_O, + Species.TAPU_KOKO, + Species.TAPU_LELE, + Species.TAPU_BULU, + Species.TAPU_FINI, + Species.SOLGALEO, + Species.LUNALA, + Species.NIHILEGO, + Species.BUZZWOLE, + Species.PHEROMOSA, + Species.XURKITREE, + Species.CELESTEELA, + Species.KARTANA, + Species.GUZZLORD, + Species.NECROZMA, + Species.MAGEARNA, + Species.MARSHADOW, + Species.POIPOLE, + Species.NAGANADEL, + Species.STAKATAKA, + Species.BLACEPHALON, + Species.ZERAORA, + Species.MELTAN, + Species.MELMETAL, + Species.GROOKEY, + Species.THWACKEY, + Species.RILLABOOM, + Species.SCORBUNNY, + Species.RABOOT, + Species.CINDERACE, + Species.SOBBLE, + Species.DRIZZILE, + Species.INTELEON, + Species.SKWOVET, + Species.GREEDENT, + Species.ROOKIDEE, + Species.CORVISQUIRE, + Species.CORVIKNIGHT, + Species.DOTTLER, + Species.ORBEETLE, + Species.NICKIT, + Species.THIEVUL, + Species.GOSSIFLEUR, + Species.ELDEGOSS, + Species.WOOLOO, + Species.DUBWOOL, + Species.CHEWTLE, + Species.DREDNAW, + Species.YAMPER, + Species.BOLTUND, + Species.ROLYCOLY, + Species.CARKOL, + Species.COALOSSAL, + Species.FLAPPLE, + Species.APPLETUN, + Species.SILICOBRA, + Species.SANDACONDA, + Species.CRAMORANT, + Species.ARROKUDA, + Species.BARRASKEWDA, + Species.TOXEL, + Species.TOXTRICITY, + Species.SIZZLIPEDE, + Species.CENTISKORCH, + Species.CLOBBOPUS, + Species.GRAPPLOCT, + Species.SINISTEA, + Species.POLTEAGEIST, + Species.HATENNA, + Species.HATTREM, + Species.HATTERENE, + Species.IMPIDIMP, + Species.MORGREM, + Species.GRIMMSNARL, + Species.OBSTAGOON, + Species.PERRSERKER, + Species.CURSOLA, + Species.SIRFETCHD, + Species.MR_RIME, + Species.RUNERIGUS, + Species.MILCERY, + Species.ALCREMIE, + Species.FALINKS, + Species.PINCURCHIN, + Species.SNOM, + Species.FROSMOTH, + Species.STONJOURNER, + Species.EISCUE, + Species.INDEEDEE, + Species.MORPEKO, + Species.CUFANT, + Species.COPPERAJAH, + Species.DRACOZOLT, + Species.ARCTOZOLT, + Species.DRACOVISH, + Species.ARCTOVISH, + Species.DURALUDON, + Species.DREEPY, + Species.DRAKLOAK, + Species.DRAGAPULT, + Species.ZACIAN, + Species.ZAMAZENTA, + Species.ETERNATUS, + Species.KUBFU, + Species.URSHIFU, + Species.ZARUDE, + Species.REGIELEKI, + Species.REGIDRAGO, + Species.GLASTRIER, + Species.SPECTRIER, + Species.CALYREX, + Species.WYRDEER, + Species.KLEAVOR, + Species.URSALUNA, + Species.BASCULEGION, + Species.SNEASLER, + Species.OVERQWIL, + Species.ENAMORUS, + Species.SPRIGATITO, + Species.FLORAGATO, + Species.MEOWSCARADA, + Species.FUECOCO, + Species.CROCALOR, + Species.SKELEDIRGE, + Species.QUAXLY, + Species.QUAXWELL, + Species.QUAQUAVAL, + Species.LECHONK, + Species.OINKOLOGNE, + Species.TAROUNTULA, + Species.SPIDOPS, + Species.NYMBLE, + Species.LOKIX, + Species.PAWMI, + Species.PAWMO, + Species.PAWMOT, + Species.TANDEMAUS, + Species.MAUSHOLD, + Species.FIDOUGH, + Species.DACHSBUN, + Species.SMOLIV, + Species.DOLLIV, + Species.ARBOLIVA, + Species.SQUAWKABILLY, + Species.NACLI, + Species.NACLSTACK, + Species.GARGANACL, + Species.CHARCADET, + Species.ARMAROUGE, + Species.CERULEDGE, + Species.TADBULB, + Species.BELLIBOLT, + Species.WATTREL, + Species.KILOWATTREL, + Species.MASCHIFF, + Species.MABOSSTIFF, + Species.SHROODLE, + Species.GRAFAIAI, + Species.BRAMBLIN, + Species.BRAMBLEGHAST, + Species.TOEDSCOOL, + Species.TOEDSCRUEL, + Species.KLAWF, + Species.CAPSAKID, + Species.SCOVILLAIN, + Species.RELLOR, + Species.RABSCA, + Species.FLITTLE, + Species.ESPATHRA, + Species.TINKATINK, + Species.TINKATUFF, + Species.TINKATON, + Species.WIGLETT, + Species.WUGTRIO, + Species.BOMBIRDIER, + Species.FINIZEN, + Species.PALAFIN, + Species.VAROOM, + Species.REVAVROOM, + Species.CYCLIZAR, + Species.ORTHWORM, + Species.GLIMMET, + Species.GLIMMORA, + Species.GREAVARD, + Species.HOUNDSTONE, + Species.FLAMIGO, + Species.CETODDLE, + Species.CETITAN, + Species.VELUZA, + Species.DONDOZO, + Species.TATSUGIRI, + Species.ANNIHILAPE, + Species.CLODSIRE, + Species.FARIGIRAF, + Species.DUDUNSPARCE, + Species.KINGAMBIT, + Species.GREAT_TUSK, + Species.SCREAM_TAIL, + Species.BRUTE_BONNET, + Species.FLUTTER_MANE, + Species.SLITHER_WING, + Species.SANDY_SHOCKS, + Species.IRON_TREADS, + Species.IRON_BUNDLE, + Species.IRON_HANDS, + Species.IRON_JUGULIS, + Species.IRON_MOTH, + Species.IRON_THORNS, + Species.FRIGIBAX, + Species.ARCTIBAX, + Species.BAXCALIBUR, + Species.GIMMIGHOUL, + Species.GHOLDENGO, + Species.WO_CHIEN, + Species.CHIEN_PAO, + Species.TING_LU, + Species.CHI_YU, + Species.ROARING_MOON, + Species.IRON_VALIANT, + Species.KORAIDON, + Species.MIRAIDON, + Species.WALKING_WAKE, + Species.IRON_LEAVES, + Species.DIPPLIN, + Species.POLTCHAGEIST, + Species.SINISTCHA, + Species.OKIDOGI, + Species.MUNKIDORI, + Species.FEZANDIPITI, + Species.OGERPON, + Species.ARCHALUDON, + Species.HYDRAPPLE, + Species.GOUGING_FIRE, + Species.RAGING_BOLT, + Species.IRON_BOULDER, + Species.IRON_CROWN, + Species.TERAPAGOS, + Species.PECHARUNT, + Species.ALOLA_RATTATA, + Species.ALOLA_RATICATE, + Species.ALOLA_RAICHU, + Species.ALOLA_SANDSHREW, + Species.ALOLA_SANDSLASH, + Species.ALOLA_VULPIX, + Species.ALOLA_NINETALES, + Species.ALOLA_DIGLETT, + Species.ALOLA_DUGTRIO, + Species.ALOLA_MEOWTH, + Species.ALOLA_PERSIAN, + Species.ALOLA_GEODUDE, + Species.ALOLA_GRAVELER, + Species.ALOLA_GOLEM, + Species.ALOLA_GRIMER, + Species.ALOLA_MUK, + Species.ALOLA_EXEGGUTOR, + Species.ALOLA_MAROWAK, + Species.ETERNAL_FLOETTE, + Species.GALAR_MEOWTH, + Species.GALAR_PONYTA, + Species.GALAR_RAPIDASH, + Species.GALAR_SLOWPOKE, + Species.GALAR_SLOWBRO, + Species.GALAR_FARFETCHD, + Species.GALAR_WEEZING, + Species.GALAR_MR_MIME, + Species.GALAR_ARTICUNO, + Species.GALAR_ZAPDOS, + Species.GALAR_MOLTRES, + Species.GALAR_SLOWKING, + Species.GALAR_CORSOLA, + Species.GALAR_ZIGZAGOON, + Species.GALAR_LINOONE, + Species.GALAR_DARUMAKA, + Species.GALAR_DARMANITAN, + Species.GALAR_YAMASK, + Species.GALAR_STUNFISK, + Species.HISUI_GROWLITHE, + Species.HISUI_ARCANINE, + Species.HISUI_VOLTORB, + Species.HISUI_ELECTRODE, + Species.HISUI_TYPHLOSION, + Species.HISUI_QWILFISH, + Species.HISUI_SNEASEL, + Species.HISUI_SAMUROTT, + Species.HISUI_LILLIGANT, + Species.HISUI_ZORUA, + Species.HISUI_ZOROARK, + Species.HISUI_BRAVIARY, + Species.HISUI_SLIGGOO, + Species.HISUI_GOODRA, + Species.HISUI_AVALUGG, + Species.HISUI_DECIDUEYE, + Species.PALDEA_TAUROS, + Species.PALDEA_WOOPER, + Species.BLOODMOON_URSALUNA, + ], [Moves.DIVE]: [ Species.SQUIRTLE, Species.WARTORTLE, @@ -59529,26 +60592,6 @@ export const tmSpecies: TmSpecies = { Species.HISUI_SAMUROTT, Species.PALDEA_TAUROS, ], - [Moves.SACRED_SWORD]: [ - Species.GALLADE, - Species.OSHAWOTT, - Species.DEWOTT, - Species.SAMUROTT, - Species.COBALION, - Species.TERRAKION, - Species.VIRIZION, - Species.KELDEO, - Species.HONEDGE, - Species.DOUBLADE, - Species.AEGISLASH, - Species.KARTANA, - Species.ZACIAN, - Species.CHIEN_PAO, - Species.IRON_LEAVES, - Species.IRON_BOULDER, - Species.IRON_CROWN, - Species.HISUI_SAMUROTT, - ], [Moves.RAZOR_SHELL]: [ Species.SLOWBRO, Species.SHELLDER, @@ -67389,6 +68432,7 @@ export const tmPoolTiers: TmPoolTiers = { [Moves.ENDEAVOR]: ModifierTier.COMMON, [Moves.SKILL_SWAP]: ModifierTier.COMMON, [Moves.IMPRISON]: ModifierTier.COMMON, + [Moves.SECRET_POWER]: ModifierTier.COMMON, [Moves.DIVE]: ModifierTier.GREAT, [Moves.FEATHER_DANCE]: ModifierTier.COMMON, [Moves.BLAZE_KICK]: ModifierTier.GREAT, @@ -67517,7 +68561,6 @@ export const tmPoolTiers: TmPoolTiers = { [Moves.ELECTROWEB]: ModifierTier.GREAT, [Moves.WILD_CHARGE]: ModifierTier.GREAT, [Moves.DRILL_RUN]: ModifierTier.GREAT, - [Moves.SACRED_SWORD]: ModifierTier.ULTRA, [Moves.RAZOR_SHELL]: ModifierTier.GREAT, [Moves.HEAT_CRASH]: ModifierTier.GREAT, [Moves.TAIL_SLAP]: ModifierTier.GREAT, diff --git a/src/data/egg.ts b/src/data/egg.ts index 0f88c53b748..f7d109dfa62 100644 --- a/src/data/egg.ts +++ b/src/data/egg.ts @@ -544,11 +544,15 @@ export class Egg { //// } -export function getLegendaryGachaSpeciesForTimestamp(scene: BattleScene, timestamp: number): Species { - const legendarySpecies = Object.entries(speciesEggTiers) +export function getValidLegendaryGachaSpecies() : Species[] { + return Object.entries(speciesEggTiers) .filter(s => s[1] === EggTier.LEGENDARY) .map(s => parseInt(s[0])) - .filter(s => getPokemonSpecies(s).isObtainable()); + .filter(s => getPokemonSpecies(s).isObtainable() && s !== Species.ETERNATUS); +} + +export function getLegendaryGachaSpeciesForTimestamp(scene: BattleScene, timestamp: number): Species { + const legendarySpecies = getValidLegendaryGachaSpecies(); let ret: Species; diff --git a/src/data/move.ts b/src/data/move.ts index 9979b24cc24..cd206755238 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -8,7 +8,7 @@ import { Constructor, NumberHolder } from "#app/utils"; import * as Utils from "../utils"; import { WeatherType } from "./weather"; import { ArenaTagSide, ArenaTrapTag, WeakenMoveTypeTag } from "./arena-tag"; -import { allAbilities, AllyMoveCategoryPowerBoostAbAttr, applyAbAttrs, applyPostAttackAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, BlockItemTheftAbAttr, BlockNonDirectDamageAbAttr, BlockOneHitKOAbAttr, BlockRecoilDamageAttr, ConfusionOnStatusEffectAbAttr, FieldMoveTypePowerBoostAbAttr, FieldPreventExplosiveMovesAbAttr, ForceSwitchOutImmunityAbAttr, HealFromBerryUseAbAttr, IgnoreContactAbAttr, IgnoreMoveEffectsAbAttr, IgnoreProtectOnContactAbAttr, InfiltratorAbAttr, MaxMultiHitAbAttr, MoveAbilityBypassAbAttr, MoveEffectChanceMultiplierAbAttr, MoveTypeChangeAbAttr, ReverseDrainAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, UnswappableAbilityAbAttr, UserFieldMoveTypePowerBoostAbAttr, VariableMovePowerAbAttr, WonderSkinAbAttr } from "./ability"; +import { allAbilities, AllyMoveCategoryPowerBoostAbAttr, applyAbAttrs, applyPostAttackAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, BlockItemTheftAbAttr, BlockNonDirectDamageAbAttr, BlockOneHitKOAbAttr, BlockRecoilDamageAttr, ConfusionOnStatusEffectAbAttr, FieldMoveTypePowerBoostAbAttr, FieldPreventExplosiveMovesAbAttr, ForceSwitchOutImmunityAbAttr, HealFromBerryUseAbAttr, IgnoreContactAbAttr, IgnoreMoveEffectsAbAttr, IgnoreProtectOnContactAbAttr, InfiltratorAbAttr, MaxMultiHitAbAttr, MoveAbilityBypassAbAttr, MoveEffectChanceMultiplierAbAttr, MoveTypeChangeAbAttr, PostDamageForceSwitchAbAttr, ReverseDrainAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, UnswappableAbilityAbAttr, UserFieldMoveTypePowerBoostAbAttr, VariableMovePowerAbAttr, WonderSkinAbAttr } from "./ability"; import { AttackTypeBoosterModifier, BerryModifier, PokemonHeldItemModifier, PokemonMoveAccuracyBoosterModifier, PokemonMultiHitModifier, PreserveBerryModifier } from "../modifier/modifier"; import { BattlerIndex, BattleType } from "../battle"; import { TerrainType } from "./terrain"; @@ -1049,31 +1049,80 @@ export enum MoveEffectTrigger { POST_TARGET, } +interface MoveEffectAttrOptions { + /** + * Defines when this effect should trigger in the move's effect order + * @see {@linkcode MoveEffectPhase} + */ + trigger?: MoveEffectTrigger; + /** Should this effect only apply on the first hit? */ + firstHitOnly?: boolean; + /** Should this effect only apply on the last hit? */ + lastHitOnly?: boolean; + /** Should this effect only apply on the first target hit? */ + firstTargetOnly?: boolean; + /** Overrides the secondary effect chance for this attr if set. */ + effectChanceOverride?: number; +} + /** Base class defining all Move Effect Attributes * @extends MoveAttr * @see {@linkcode apply} */ export class MoveEffectAttr extends MoveAttr { - /** Defines when this effect should trigger in the move's effect order - * @see {@linkcode phases.MoveEffectPhase.start} + /** + * A container for this attribute's optional parameters + * @see {@linkcode MoveEffectAttrOptions} for supported params. */ - public trigger: MoveEffectTrigger; - /** Should this effect only apply on the first hit? */ - public firstHitOnly: boolean; - /** Should this effect only apply on the last hit? */ - public lastHitOnly: boolean; - /** Should this effect only apply on the first target hit? */ - public firstTargetOnly: boolean; - /** Overrides the secondary effect chance for this attr if set. */ - public effectChanceOverride?: number; + protected options?: MoveEffectAttrOptions; - constructor(selfTarget?: boolean, trigger?: MoveEffectTrigger, firstHitOnly: boolean = false, lastHitOnly: boolean = false, firstTargetOnly: boolean = false, effectChanceOverride?: number) { + constructor(selfTarget?: boolean, options?: MoveEffectAttrOptions) { super(selfTarget); - this.trigger = trigger ?? MoveEffectTrigger.POST_APPLY; - this.firstHitOnly = firstHitOnly; - this.lastHitOnly = lastHitOnly; - this.firstTargetOnly = firstTargetOnly; - this.effectChanceOverride = effectChanceOverride; + this.options = options; + } + + /** + * Defines when this effect should trigger in the move's effect order. + * @default MoveEffectTrigger.POST_APPLY + * @see {@linkcode MoveEffectTrigger} + */ + public get trigger () { + return this.options?.trigger ?? MoveEffectTrigger.POST_APPLY; + } + + /** + * `true` if this effect should only trigger on the first hit of + * multi-hit moves. + * @default false + */ + public get firstHitOnly () { + return this.options?.firstHitOnly ?? false; + } + + /** + * `true` if this effect should only trigger on the last hit of + * multi-hit moves. + * @default false + */ + public get lastHitOnly () { + return this.options?.lastHitOnly ?? false; + } + + /** + * `true` if this effect should apply only upon hitting a target + * for the first time when targeting multiple {@linkcode Pokemon}. + * @default false + */ + public get firstTargetOnly () { + return this.options?.firstTargetOnly ?? false; + } + + /** + * If defined, overrides the move's base chance for this + * secondary effect to trigger. + */ + public get effectChanceOverride () { + return this.options?.effectChanceOverride; } /** @@ -1398,7 +1447,7 @@ export class RecoilAttr extends MoveEffectAttr { private unblockable: boolean; constructor(useHp: boolean = false, damageRatio: number = 0.25, unblockable: boolean = false) { - super(true, MoveEffectTrigger.POST_APPLY, false, true); + super(true, { lastHitOnly: true }); this.useHp = useHp; this.damageRatio = damageRatio; @@ -1420,8 +1469,13 @@ export class RecoilAttr extends MoveEffectAttr { return false; } - const damageValue = (!this.useHp ? user.turnData.damageDealt : user.getMaxHp()) * this.damageRatio; - const minValue = user.turnData.damageDealt ? 1 : 0; + // Chloroblast and Struggle should not deal recoil damage if the move was not successful + if (this.useHp && [ MoveResult.FAIL, MoveResult.MISS ].includes(user.getLastXMoves(1)[0]?.result)) { + return false; + } + + const damageValue = (!this.useHp ? user.turnData.totalDamageDealt : user.getMaxHp()) * this.damageRatio; + const minValue = user.turnData.totalDamageDealt ? 1 : 0; const recoilDamage = Utils.toDmgValue(damageValue, minValue); if (!recoilDamage) { return false; @@ -1451,7 +1505,7 @@ export class RecoilAttr extends MoveEffectAttr { **/ export class SacrificialAttr extends MoveEffectAttr { constructor() { - super(true, MoveEffectTrigger.POST_TARGET); + super(true, { trigger: MoveEffectTrigger.POST_TARGET }); } /** @@ -1484,7 +1538,7 @@ export class SacrificialAttr extends MoveEffectAttr { **/ export class SacrificialAttrOnHit extends MoveEffectAttr { constructor() { - super(true, MoveEffectTrigger.HIT); + super(true, { trigger: MoveEffectTrigger.HIT }); } /** @@ -1523,7 +1577,7 @@ export class SacrificialAttrOnHit extends MoveEffectAttr { */ export class HalfSacrificialAttr extends MoveEffectAttr { constructor() { - super(true, MoveEffectTrigger.POST_TARGET); + super(true, { trigger: MoveEffectTrigger.POST_TARGET }); } /** @@ -1927,7 +1981,7 @@ export class HitHealAttr extends MoveEffectAttr { private healStat: EffectiveStat | null; constructor(healRatio?: number | null, healStat?: EffectiveStat) { - super(true, MoveEffectTrigger.HIT); + super(true, { trigger: MoveEffectTrigger.HIT }); this.healRatio = healRatio ?? 0.5; this.healStat = healStat ?? null; @@ -1952,7 +2006,7 @@ export class HitHealAttr extends MoveEffectAttr { message = i18next.t("battle:drainMessage", { pokemonName: getPokemonNameWithAffix(target) }); } else { // Default healing formula used by draining moves like Absorb, Draining Kiss, Bitter Blade, etc. - healAmount = Utils.toDmgValue(user.turnData.currDamageDealt * this.healRatio); + healAmount = Utils.toDmgValue(user.turnData.singleHitDamageDealt * this.healRatio); message = i18next.t("battle:regainHealth", { pokemonName: getPokemonNameWithAffix(user) }); } if (reverseDrain) { @@ -2136,7 +2190,7 @@ export class StatusEffectAttr extends MoveEffectAttr { public overrideStatus: boolean = false; constructor(effect: StatusEffect, selfTarget?: boolean, turnsRemaining?: number, overrideStatus: boolean = false) { - super(selfTarget, MoveEffectTrigger.HIT); + super(selfTarget, { trigger: MoveEffectTrigger.HIT }); this.effect = effect; this.turnsRemaining = turnsRemaining; @@ -2177,7 +2231,10 @@ export class StatusEffectAttr extends MoveEffectAttr { getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { const moveChance = this.getMoveChance(user, target, move, this.selfTarget, false); - return !(this.selfTarget ? user : target).status && (this.selfTarget ? user : target).canSetStatus(this.effect, true, false, user) ? Math.floor(moveChance * -0.1) : 0; + const score = (moveChance < 0) ? -10 : Math.floor(moveChance * -0.1); + const pokemon = this.selfTarget ? user : target; + + return !pokemon.status && pokemon.canSetStatus(this.effect, true, false, user) ? score : 0; } } @@ -2197,13 +2254,16 @@ export class MultiStatusEffectAttr extends StatusEffectAttr { getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { const moveChance = this.getMoveChance(user, target, move, this.selfTarget, false); - return !(this.selfTarget ? user : target).status && (this.selfTarget ? user : target).canSetStatus(this.effect, true, false, user) ? Math.floor(moveChance * -0.1) : 0; + const score = (moveChance < 0) ? -10 : Math.floor(moveChance * -0.1); + const pokemon = this.selfTarget ? user : target; + + return !pokemon.status && pokemon.canSetStatus(this.effect, true, false, user) ? score : 0; } } export class PsychoShiftEffectAttr extends MoveEffectAttr { constructor() { - super(false, MoveEffectTrigger.HIT); + super(false, { trigger: MoveEffectTrigger.HIT }); } apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { @@ -2228,7 +2288,7 @@ export class PsychoShiftEffectAttr extends MoveEffectAttr { } getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { - return !(this.selfTarget ? user : target).status && (this.selfTarget ? user : target).canSetStatus(user.status?.effect, true, false, user) ? Math.floor(move.chance * -0.1) : 0; + return !target.status && target.canSetStatus(user.status?.effect, true, false, user) ? -10 : 0; } } /** @@ -2240,7 +2300,7 @@ export class StealHeldItemChanceAttr extends MoveEffectAttr { private chance: number; constructor(chance: number) { - super(false, MoveEffectTrigger.HIT); + super(false, { trigger: MoveEffectTrigger.HIT }); this.chance = chance; } @@ -2301,7 +2361,7 @@ export class RemoveHeldItemAttr extends MoveEffectAttr { private berriesOnly: boolean; constructor(berriesOnly: boolean) { - super(false, MoveEffectTrigger.HIT); + super(false, { trigger: MoveEffectTrigger.HIT }); this.berriesOnly = berriesOnly; } @@ -2375,7 +2435,7 @@ export class RemoveHeldItemAttr extends MoveEffectAttr { export class EatBerryAttr extends MoveEffectAttr { protected chosenBerry: BerryModifier | undefined; constructor() { - super(true, MoveEffectTrigger.HIT); + super(true, { trigger: MoveEffectTrigger.HIT }); } /** * Causes the target to eat a berry. @@ -2478,7 +2538,7 @@ export class HealStatusEffectAttr extends MoveEffectAttr { * @param ...effects - List of status effects to cure */ constructor(selfTarget: boolean, ...effects: StatusEffect[]) { - super(selfTarget, MoveEffectTrigger.POST_APPLY, false, true); + super(selfTarget, { lastHitOnly: true }); this.effects = effects; } @@ -2808,35 +2868,67 @@ export class AwaitCombinedPledgeAttr extends OverrideMoveEffectAttr { } } +/** + * Set of optional parameters that may be applied to stat stage changing effects + * @extends MoveEffectAttrOptions + * @see {@linkcode StatStageChangeAttr} + */ +interface StatStageChangeAttrOptions extends MoveEffectAttrOptions { + /** If defined, needs to be met in order for the stat change to apply */ + condition?: MoveConditionFunc, + /** `true` to display a message */ + showMessage?: boolean +} + /** * Attribute used for moves that change stat stages * * @param stats {@linkcode BattleStat} Array of stat(s) to change * @param stages How many stages to change the stat(s) by, [-6, 6] * @param selfTarget `true` if the move is self-targetting - * @param condition {@linkcode MoveConditionFunc} Optional condition to be checked in order to apply the changes - * @param showMessage `true` to display a message; default `true` - * @param firstHitOnly `true` if only the first hit of a multi hit move should cause a stat stage change; default `false` - * @param moveEffectTrigger {@linkcode MoveEffectTrigger} When the stat change should trigger; default {@linkcode MoveEffectTrigger.HIT} - * @param firstTargetOnly `true` if a move that hits multiple pokemon should only trigger the stat change if it hits at least one pokemon, rather than once per hit pokemon; default `false` - * @param lastHitOnly `true` if the effect should only apply after the last hit of a multi hit move; default `false` - * @param effectChanceOverride Will override the move's normal secondary effect chance if specified + * @param options {@linkcode StatStageChangeAttrOptions} Container for any optional parameters for this attribute. * * @extends MoveEffectAttr * @see {@linkcode apply} */ export class StatStageChangeAttr extends MoveEffectAttr { public stats: BattleStat[]; - public stages: integer; - private condition?: MoveConditionFunc | null; - private showMessage: boolean; + public stages: number; + /** + * Container for optional parameters to this attribute. + * @see {@linkcode StatStageChangeAttrOptions} for available optional params + */ + protected override options?: StatStageChangeAttrOptions; - constructor(stats: BattleStat[], stages: integer, selfTarget?: boolean, condition?: MoveConditionFunc | null, showMessage: boolean = true, firstHitOnly: boolean = false, moveEffectTrigger: MoveEffectTrigger = MoveEffectTrigger.HIT, firstTargetOnly: boolean = false, lastHitOnly: boolean = false, effectChanceOverride?: number) { - super(selfTarget, moveEffectTrigger, firstHitOnly, lastHitOnly, firstTargetOnly, effectChanceOverride); + constructor(stats: BattleStat[], stages: number, selfTarget?: boolean, options?: StatStageChangeAttrOptions) { + super(selfTarget, options); this.stats = stats; this.stages = stages; - this.condition = condition; - this.showMessage = showMessage; + this.options = options; + } + + /** + * The condition required for the stat stage change to apply. + * Defaults to `null` (i.e. no condition required). + */ + private get condition () { + return this.options?.condition ?? null; + } + + /** + * `true` to display a message for the stat change. + * @default true + */ + private get showMessage () { + return this.options?.showMessage ?? true; + } + + /** + * Indicates when the stat change should trigger + * @default MoveEffectTrigger.HIT + */ + public override get trigger () { + return this.options?.trigger ?? MoveEffectTrigger.HIT; } /** @@ -2921,20 +3013,6 @@ export class SecretPowerAttr extends MoveEffectAttr { super(false); } - /** - * Used to determine if the move should apply a secondary effect based on Secret Power's 30% chance - * @returns `true` if the move's secondary effect should apply - */ - override canApply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean { - this.effectChanceOverride = move.chance; - const moveChance = this.getMoveChance(user, target, move, this.selfTarget); - if (moveChance < 0 || moveChance === 100 || user.randSeedInt(100) < moveChance) { - return true; - } else { - return false; - } - } - /** * Used to apply the secondary effect to the target Pokemon * @returns `true` if a secondary effect is successfully applied @@ -2951,8 +3029,6 @@ export class SecretPowerAttr extends MoveEffectAttr { const biome = user.scene.arena.biomeType; secondaryEffect = this.determineBiomeEffect(biome); } - // effectChanceOverride used in the application of the actual secondary effect - secondaryEffect.effectChanceOverride = 100; return secondaryEffect.apply(user, target, move, []); } @@ -3128,7 +3204,7 @@ export class CutHpStatStageBoostAttr extends StatStageChangeAttr { private messageCallback: ((user: Pokemon) => void) | undefined; constructor(stat: BattleStat[], levels: integer, cutRatio: integer, messageCallback?: ((user: Pokemon) => void) | undefined) { - super(stat, levels, true, null, true); + super(stat, levels, true); this.cutRatio = cutRatio; this.messageCallback = messageCallback; @@ -4389,7 +4465,7 @@ export class FormChangeItemTypeAttr extends VariableMoveTypeAttr { } if ([ user.species.speciesId, user.fusionSpecies?.speciesId ].includes(Species.ARCEUS) || [ user.species.speciesId, user.fusionSpecies?.speciesId ].includes(Species.SILVALLY)) { - const form = user.species.speciesId === Species.ARCEUS || user.species.speciesId === Species.SILVALLY ? user.formIndex : user.fusionSpecies?.formIndex!; // TODO: is this bang correct? + const form = user.species.speciesId === Species.ARCEUS || user.species.speciesId === Species.SILVALLY ? user.formIndex : user.fusionSpecies?.formIndex!; moveType.value = Type[Type[form]]; return true; @@ -4878,7 +4954,7 @@ export class BypassRedirectAttr extends MoveAttr { export class FrenzyAttr extends MoveEffectAttr { constructor() { - super(true, MoveEffectTrigger.HIT, false, true); + super(true, { trigger: MoveEffectTrigger.HIT, lastHitOnly: true }); } canApply(user: Pokemon, target: Pokemon, move: Move, args: any[]) { @@ -4951,7 +5027,7 @@ export class AddBattlerTagAttr extends MoveEffectAttr { private failOnOverlap: boolean; constructor(tagType: BattlerTagType, selfTarget: boolean = false, failOnOverlap: boolean = false, turnCountMin: integer = 0, turnCountMax?: integer, lastHitOnly: boolean = false, cancelOnFail: boolean = false) { - super(selfTarget, MoveEffectTrigger.POST_APPLY, false, lastHitOnly); + super(selfTarget, { lastHitOnly: lastHitOnly }); this.tagType = tagType; this.turnCountMin = turnCountMin; @@ -5386,7 +5462,7 @@ export class AddArenaTagAttr extends MoveEffectAttr { public selfSideTarget: boolean; constructor(tagType: ArenaTagType, turnCount?: integer | null, failOnOverlap: boolean = false, selfSideTarget: boolean = false) { - super(true, MoveEffectTrigger.POST_APPLY); + super(true); this.tagType = tagType; this.turnCount = turnCount!; // TODO: is the bang correct? @@ -5424,7 +5500,7 @@ export class RemoveArenaTagsAttr extends MoveEffectAttr { public selfSideTarget: boolean; constructor(tagTypes: ArenaTagType[], selfSideTarget: boolean) { - super(true, MoveEffectTrigger.POST_APPLY); + super(true); this.tagTypes = tagTypes; this.selfSideTarget = selfSideTarget; @@ -5490,7 +5566,7 @@ export class RemoveArenaTrapAttr extends MoveEffectAttr { private targetBothSides: boolean; constructor(targetBothSides: boolean = false) { - super(true, MoveEffectTrigger.PRE_APPLY); + super(true, { trigger: MoveEffectTrigger.PRE_APPLY }); this.targetBothSides = targetBothSides; } @@ -5526,7 +5602,7 @@ export class RemoveScreensAttr extends MoveEffectAttr { private targetBothSides: boolean; constructor(targetBothSides: boolean = false) { - super(true, MoveEffectTrigger.PRE_APPLY); + super(true, { trigger: MoveEffectTrigger.PRE_APPLY }); this.targetBothSides = targetBothSides; } @@ -5564,7 +5640,7 @@ export class SwapArenaTagsAttr extends MoveEffectAttr { constructor(SwapTags: ArenaTagType[]) { - super(true, MoveEffectTrigger.POST_APPLY); + super(true); this.SwapTags = SwapTags; } @@ -5685,12 +5761,13 @@ export class RevivalBlessingAttr extends MoveEffectAttr { } } + export class ForceSwitchOutAttr extends MoveEffectAttr { constructor( private selfSwitch: boolean = false, private switchType: SwitchType = SwitchType.SWITCH ) { - super(false, MoveEffectTrigger.POST_APPLY, false, true); + super(false, { lastHitOnly: true }); } isBatonPass() { @@ -5703,12 +5780,19 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { return 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) { + /** + * Check if Wimp Out/Emergency Exit activates due to being hit by U-turn or Volt Switch + * If it did, the user of U-turn or Volt Switch will not be switched out. + */ + if (target.getAbility().hasAttr(PostDamageForceSwitchAbAttr) && + (move.id === Moves.U_TURN || move.id === Moves.VOLT_SWITCH || move.id === Moves.FLIP_TURN) + ) { + if (this.hpDroppedBelowHalf(target)) { + return false; + } + } // Switch out logic for the player's Pokemon if (switchOutTarget.scene.getParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) { return false; @@ -5734,11 +5818,27 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { false, false), MoveEndPhase); } } else { + /** + * Check if Wimp Out/Emergency Exit activates due to being hit by U-turn or Volt Switch + * If it did, the user of U-turn or Volt Switch will not be switched out. + */ + if (target.getAbility().hasAttr(PostDamageForceSwitchAbAttr) && + (move.id === Moves.U_TURN || move.id === Moves.VOLT_SWITCH) || move.id === Moves.FLIP_TURN) { + if (this.hpDroppedBelowHalf(target)) { + return false; + } + } + // Switch out logic for everything else (eg: WILD battles) if (user.scene.currentBattle.waveIndex % 10 === 0) { return false; } + // Don't allow wild mons to flee with U-turn et al + if (this.selfSwitch && !user.isPlayer() && move.category !== MoveCategory.STATUS) { + return false; + } + if (switchOutTarget.hp > 0) { switchOutTarget.leaveField(false); user.scene.queueMessage(i18next.t("moveTriggers:fled", { pokemonName: getPokemonNameWithAffix(switchOutTarget) }), null, true, 500); @@ -5821,8 +5921,22 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { } return ret; } -} + /** + * Helper function to check if the Pokémon's health is below half after taking damage. + * Used for an edge case interaction with Wimp Out/Emergency Exit. + * If the Ability activates due to being hit by U-turn or Volt Switch, the user of that move will not be switched out. + */ + hpDroppedBelowHalf(target: Pokemon): boolean { + const pokemonHealth = target.hp; + const maxPokemonHealth = target.getMaxHp(); + const damageTaken = target.turnData.damageTaken; + const initialHealth = pokemonHealth + damageTaken; + + // Check if the Pokémon's health has dropped below half after the damage + return initialHealth >= maxPokemonHealth / 2 && pokemonHealth < maxPokemonHealth / 2; + } +} export class ChillyReceptionAttr extends ForceSwitchOutAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { @@ -5841,7 +5955,7 @@ export class RemoveTypeAttr extends MoveEffectAttr { private messageCallback: ((user: Pokemon) => void) | undefined; constructor(removedType: Type, messageCallback?: (user: Pokemon) => void) { - super(true, MoveEffectTrigger.POST_TARGET); + super(true, { trigger: MoveEffectTrigger.POST_TARGET }); this.removedType = removedType; this.messageCallback = messageCallback; @@ -5858,6 +5972,9 @@ export class RemoveTypeAttr extends MoveEffectAttr { const userTypes = user.getTypes(true); const modifiedTypes = userTypes.filter(type => type !== this.removedType); + if (modifiedTypes.length === 0) { + modifiedTypes.push(Type.UNKNOWN); + } user.summonData.types = modifiedTypes; user.updateInfo(); @@ -5880,7 +5997,11 @@ export class CopyTypeAttr extends MoveEffectAttr { return false; } - user.summonData.types = target.getTypes(true); + const targetTypes = target.getTypes(true); + if (targetTypes.includes(Type.UNKNOWN) && targetTypes.indexOf(Type.UNKNOWN) > -1) { + targetTypes[targetTypes.indexOf(Type.UNKNOWN)] = Type.NORMAL; + } + user.summonData.types = targetTypes; user.updateInfo(); user.scene.queueMessage(i18next.t("moveTriggers:copyType", { pokemonName: getPokemonNameWithAffix(user), targetPokemonName: getPokemonNameWithAffix(target) })); @@ -5889,7 +6010,7 @@ export class CopyTypeAttr extends MoveEffectAttr { } getCondition(): MoveConditionFunc { - return (user, target, move) => target.getTypes()[0] !== Type.UNKNOWN; + return (user, target, move) => target.getTypes()[0] !== Type.UNKNOWN || target.summonData.addedType !== null; } } @@ -5903,22 +6024,114 @@ export class CopyBiomeTypeAttr extends MoveEffectAttr { return false; } - const biomeType = user.scene.arena.getTypeForBiome(); + const terrainType = user.scene.arena.getTerrainType(); + let typeChange: Type; + if (terrainType !== TerrainType.NONE) { + typeChange = this.getTypeForTerrain(user.scene.arena.getTerrainType()); + } else { + typeChange = this.getTypeForBiome(user.scene.arena.biomeType); + } - user.summonData.types = [ biomeType ]; + user.summonData.types = [ typeChange ]; user.updateInfo(); - user.scene.queueMessage(i18next.t("moveTriggers:transformedIntoType", { pokemonName: getPokemonNameWithAffix(user), typeName: i18next.t(`pokemonInfo:Type.${Type[biomeType]}`) })); + user.scene.queueMessage(i18next.t("moveTriggers:transformedIntoType", { pokemonName: getPokemonNameWithAffix(user), typeName: i18next.t(`pokemonInfo:Type.${Type[typeChange]}`) })); return true; } + + /** + * Retrieves a type from the current terrain + * @param terrainType {@linkcode TerrainType} + * @returns {@linkcode Type} + */ + private getTypeForTerrain(terrainType: TerrainType): Type { + switch (terrainType) { + case TerrainType.ELECTRIC: + return Type.ELECTRIC; + case TerrainType.MISTY: + return Type.FAIRY; + case TerrainType.GRASSY: + return Type.GRASS; + case TerrainType.PSYCHIC: + return Type.PSYCHIC; + case TerrainType.NONE: + default: + return Type.UNKNOWN; + } + } + + /** + * Retrieves a type from the current biome + * @param biomeType {@linkcode Biome} + * @returns {@linkcode Type} + */ + private getTypeForBiome(biomeType: Biome): Type { + switch (biomeType) { + case Biome.TOWN: + case Biome.PLAINS: + case Biome.METROPOLIS: + return Type.NORMAL; + case Biome.GRASS: + case Biome.TALL_GRASS: + return Type.GRASS; + case Biome.FOREST: + case Biome.JUNGLE: + return Type.BUG; + case Biome.SLUM: + case Biome.SWAMP: + return Type.POISON; + case Biome.SEA: + case Biome.BEACH: + case Biome.LAKE: + case Biome.SEABED: + return Type.WATER; + case Biome.MOUNTAIN: + return Type.FLYING; + case Biome.BADLANDS: + return Type.GROUND; + case Biome.CAVE: + case Biome.DESERT: + return Type.ROCK; + case Biome.ICE_CAVE: + case Biome.SNOWY_FOREST: + return Type.ICE; + case Biome.MEADOW: + case Biome.FAIRY_CAVE: + case Biome.ISLAND: + return Type.FAIRY; + case Biome.POWER_PLANT: + return Type.ELECTRIC; + case Biome.VOLCANO: + return Type.FIRE; + case Biome.GRAVEYARD: + case Biome.TEMPLE: + return Type.GHOST; + case Biome.DOJO: + case Biome.CONSTRUCTION_SITE: + return Type.FIGHTING; + case Biome.FACTORY: + case Biome.LABORATORY: + return Type.STEEL; + case Biome.RUINS: + case Biome.SPACE: + return Type.PSYCHIC; + case Biome.WASTELAND: + case Biome.END: + return Type.DRAGON; + case Biome.ABYSS: + return Type.DARK; + default: + return Type.UNKNOWN; + } + } } export class ChangeTypeAttr extends MoveEffectAttr { private type: Type; constructor(type: Type) { - super(false, MoveEffectTrigger.HIT); + super(false, { trigger: MoveEffectTrigger.HIT }); this.type = type; } @@ -5941,17 +6154,13 @@ export class AddTypeAttr extends MoveEffectAttr { private type: Type; constructor(type: Type) { - super(false, MoveEffectTrigger.HIT); + super(false, { trigger: MoveEffectTrigger.HIT }); this.type = type; } apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - const types = target.getTypes().slice(0, 2).filter(t => t !== Type.UNKNOWN); // TODO: Figure out some way to actually check if another version of this effect is already applied - if (this.type !== Type.UNKNOWN) { - types.push(this.type); - } - target.summonData.types = types; + target.summonData.addedType = this.type; target.updateInfo(); user.scene.queueMessage(i18next.t("moveTriggers:addType", { typeName: i18next.t(`pokemonInfo:Type.${Type[this.type]}`), pokemonName: getPokemonNameWithAffix(target) })); @@ -6472,7 +6681,7 @@ export class AbilityChangeAttr extends MoveEffectAttr { public ability: Abilities; constructor(ability: Abilities, selfTarget?: boolean) { - super(selfTarget, MoveEffectTrigger.HIT); + super(selfTarget, { trigger: MoveEffectTrigger.HIT }); this.ability = ability; } @@ -6501,7 +6710,7 @@ export class AbilityCopyAttr extends MoveEffectAttr { public copyToPartner: boolean; constructor(copyToPartner: boolean = false) { - super(false, MoveEffectTrigger.HIT); + super(false, { trigger: MoveEffectTrigger.HIT }); this.copyToPartner = copyToPartner; } @@ -6540,7 +6749,7 @@ export class AbilityGiveAttr extends MoveEffectAttr { public copyToPartner: boolean; constructor() { - super(false, MoveEffectTrigger.HIT); + super(false, { trigger: MoveEffectTrigger.HIT }); } apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { @@ -6852,7 +7061,7 @@ export class DiscourageFrequentUseAttr extends MoveAttr { export class MoneyAttr extends MoveEffectAttr { constructor() { - super(true, MoveEffectTrigger.HIT, true); + super(true, { trigger: MoveEffectTrigger.HIT, firstHitOnly: true }); } apply(user: Pokemon, target: Pokemon, move: Move): boolean { @@ -6869,7 +7078,7 @@ export class MoneyAttr extends MoveEffectAttr { */ export class DestinyBondAttr extends MoveEffectAttr { constructor() { - super(true, MoveEffectTrigger.PRE_APPLY); + super(true, { trigger: MoveEffectTrigger.PRE_APPLY }); } /** @@ -6919,7 +7128,7 @@ export class StatusIfBoostedAttr extends MoveEffectAttr { public effect: StatusEffect; constructor(effect: StatusEffect) { - super(true, MoveEffectTrigger.HIT); + super(true, { trigger: MoveEffectTrigger.HIT }); this.effect = effect; } @@ -7044,6 +7253,11 @@ const targetSleptOrComatoseCondition: MoveConditionFunc = (user: Pokemon, target const failIfLastCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => user.scene.phaseQueue.find(phase => phase instanceof MovePhase) !== undefined; +const failIfLastInPartyCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => { + const party: Pokemon[] = user.isPlayer() ? user.scene.getParty() : user.scene.getEnemyParty(); + return party.some(pokemon => pokemon.isActive() && !pokemon.isOnField()); +}; + export type MoveAttrFilter = (attr: MoveAttr) => boolean; function applyMoveAttrsInternal(attrFilter: MoveAttrFilter, user: Pokemon | null, target: Pokemon | null, move: Move, args: any[]): Promise { @@ -7953,6 +8167,7 @@ export function initMoves() { .attr(StatusEffectAttr, StatusEffect.PARALYSIS), new SelfStatusMove(Moves.BATON_PASS, Type.NORMAL, -1, 40, -1, 0, 2) .attr(ForceSwitchOutAttr, true, SwitchType.BATON_PASS) + .condition(failIfLastInPartyCondition) .hidesUser(), new StatusMove(Moves.ENCORE, Type.NORMAL, 100, 5, -1, 0, 2) .attr(AddBattlerTagAttr, BattlerTagType.ENCORE, false, true) @@ -8971,7 +9186,7 @@ export function initMoves() { // If any fielded pokémon is grass-type and grounded. return [ ...user.scene.getEnemyParty(), ...user.scene.getParty() ].some((poke) => poke.isOfType(Type.GRASS) && poke.isGrounded()); }) - .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, (user, target, move) => target.isOfType(Type.GRASS) && target.isGrounded()), + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, { condition: (user, target, move) => target.isOfType(Type.GRASS) && target.isGrounded() }), new StatusMove(Moves.STICKY_WEB, Type.BUG, -1, 20, -1, 0, 6) .attr(AddArenaTrapTagAttr, ArenaTagType.STICKY_WEB) .target(MoveTarget.ENEMY_SIDE), @@ -8983,8 +9198,7 @@ export function initMoves() { .ignoresProtect() .ignoresVirtual(), new StatusMove(Moves.TRICK_OR_TREAT, Type.GHOST, 100, 20, -1, 0, 6) - .attr(AddTypeAttr, Type.GHOST) - .edgeCase(), // Weird interaction with Forest's Curse, reflect type, burn up + .attr(AddTypeAttr, Type.GHOST), new StatusMove(Moves.NOBLE_ROAR, Type.NORMAL, 100, 30, -1, 0, 6) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1) .soundBased(), @@ -8996,8 +9210,7 @@ export function initMoves() { .target(MoveTarget.ALL_NEAR_OTHERS) .triageMove(), new StatusMove(Moves.FORESTS_CURSE, Type.GRASS, 100, 20, -1, 0, 6) - .attr(AddTypeAttr, Type.GRASS) - .edgeCase(), // Weird interaction with Trick or Treat, reflect type, burn up + .attr(AddTypeAttr, Type.GRASS), new AttackMove(Moves.PETAL_BLIZZARD, Type.GRASS, MoveCategory.PHYSICAL, 90, 100, 15, -1, 0, 6) .windMove() .makesContact(false) @@ -9010,7 +9223,7 @@ export function initMoves() { .soundBased() .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(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, false, { trigger: MoveEffectTrigger.PRE_APPLY }) .attr(ForceSwitchOutAttr, true) .soundBased(), new StatusMove(Moves.TOPSY_TURVY, Type.DARK, -1, 20, -1, 0, 6) @@ -9025,7 +9238,7 @@ export function initMoves() { .condition(failIfLastCondition), new StatusMove(Moves.FLOWER_SHIELD, Type.FAIRY, -1, 10, -1, 0, 6) .target(MoveTarget.ALL) - .attr(StatStageChangeAttr, [ Stat.DEF ], 1, false, (user, target, move) => target.getTypes().includes(Type.GRASS) && !target.getTag(SemiInvulnerableTag)), + .attr(StatStageChangeAttr, [ Stat.DEF ], 1, false, { condition: (user, target, move) => target.getTypes().includes(Type.GRASS) && !target.getTag(SemiInvulnerableTag) }), new StatusMove(Moves.GRASSY_TERRAIN, Type.GRASS, -1, 10, -1, 0, 6) .attr(TerrainChangeAttr, TerrainType.GRASSY) .target(MoveTarget.BOTH_SIDES), @@ -9057,7 +9270,7 @@ export function initMoves() { .attr(StatStageChangeAttr, [ Stat.SPATK ], -1) .soundBased(), new AttackMove(Moves.DIAMOND_STORM, Type.ROCK, MoveCategory.PHYSICAL, 100, 95, 5, 50, 0, 6) - .attr(StatStageChangeAttr, [ Stat.DEF ], 2, true, undefined, undefined, undefined, undefined, true) + .attr(StatStageChangeAttr, [ Stat.DEF ], 2, true, { firstTargetOnly: true }) .makesContact(false) .target(MoveTarget.ALL_NEAR_ENEMIES), new AttackMove(Moves.STEAM_ERUPTION, Type.WATER, MoveCategory.SPECIAL, 110, 95, 5, 30, 0, 6) @@ -9083,7 +9296,7 @@ export function initMoves() { new StatusMove(Moves.EERIE_IMPULSE, Type.ELECTRIC, 100, 15, -1, 0, 6) .attr(StatStageChangeAttr, [ Stat.SPATK ], -2), new StatusMove(Moves.VENOM_DRENCH, Type.POISON, 100, 20, -1, 0, 6) - .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK, Stat.SPD ], -1, false, (user, target, move) => target.status?.effect === StatusEffect.POISON || target.status?.effect === StatusEffect.TOXIC) + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK, Stat.SPD ], -1, false, { condition: (user, target, move) => target.status?.effect === StatusEffect.POISON || target.status?.effect === StatusEffect.TOXIC }) .target(MoveTarget.ALL_NEAR_ENEMIES), new StatusMove(Moves.POWDER, Type.BUG, 100, 20, -1, 1, 6) .ignoresSubstitute() @@ -9094,7 +9307,7 @@ export function initMoves() { .attr(StatStageChangeAttr, [ Stat.SPATK, Stat.SPDEF, Stat.SPD ], 2, true) .ignoresVirtual(), new StatusMove(Moves.MAGNETIC_FLUX, Type.ELECTRIC, -1, 20, -1, 0, 6) - .attr(StatStageChangeAttr, [ Stat.DEF, Stat.SPDEF ], 1, false, (user, target, move) => !![ Abilities.PLUS, Abilities.MINUS ].find(a => target.hasAbility(a, false))) + .attr(StatStageChangeAttr, [ Stat.DEF, Stat.SPDEF ], 1, false, { condition: (user, target, move) => !![ Abilities.PLUS, Abilities.MINUS ].find(a => target.hasAbility(a, false)) }) .ignoresSubstitute() .target(MoveTarget.USER_AND_ALLIES) .condition((user, target, move) => !![ user, user.getAlly() ].filter(p => p?.isActive()).find(p => !![ Abilities.PLUS, Abilities.MINUS ].find(a => p.hasAbility(a, false)))), @@ -9314,7 +9527,7 @@ export function initMoves() { new SelfStatusMove(Moves.LASER_FOCUS, Type.NORMAL, -1, 30, -1, 0, 7) .attr(AddBattlerTagAttr, BattlerTagType.ALWAYS_CRIT, true, false), new StatusMove(Moves.GEAR_UP, Type.STEEL, -1, 20, -1, 0, 7) - .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, (user, target, move) => !![ Abilities.PLUS, Abilities.MINUS ].find(a => target.hasAbility(a, false))) + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], 1, false, { condition: (user, target, move) => !![ Abilities.PLUS, Abilities.MINUS ].find(a => target.hasAbility(a, false)) }) .ignoresSubstitute() .target(MoveTarget.USER_AND_ALLIES) .condition((user, target, move) => !![ user, user.getAlly() ].filter(p => p?.isActive()).find(p => !![ Abilities.PLUS, Abilities.MINUS ].find(a => p.hasAbility(a, false)))), @@ -9371,7 +9584,7 @@ export function initMoves() { .ballBombMove() .makesContact(false), new AttackMove(Moves.CLANGING_SCALES, Type.DRAGON, MoveCategory.SPECIAL, 110, 100, 5, -1, 0, 7) - .attr(StatStageChangeAttr, [ Stat.DEF ], -1, true, null, true, false, MoveEffectTrigger.HIT, true) + .attr(StatStageChangeAttr, [ Stat.DEF ], -1, true, { firstTargetOnly: true }) .soundBased() .target(MoveTarget.ALL_NEAR_ENEMIES), new AttackMove(Moves.DRAGON_HAMMER, Type.DRAGON, MoveCategory.PHYSICAL, 90, 100, 15, -1, 0, 7), @@ -9485,13 +9698,13 @@ export function initMoves() { .makesContact(false) .ignoresVirtual(), new AttackMove(Moves.CLANGOROUS_SOULBLAZE, Type.DRAGON, MoveCategory.SPECIAL, 185, -1, 1, 100, 0, 7) - .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true, undefined, undefined, undefined, undefined, true) + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true, { firstTargetOnly: true }) .soundBased() .target(MoveTarget.ALL_NEAR_ENEMIES) .edgeCase() // I assume it needs clanging scales and Kommo-O .ignoresVirtual(), /* End Unused */ - new AttackMove(Moves.ZIPPY_ZAP, Type.ELECTRIC, MoveCategory.PHYSICAL, 50, 100, 15, -1, 2, 7) //LGPE Implementation + new AttackMove(Moves.ZIPPY_ZAP, Type.ELECTRIC, MoveCategory.PHYSICAL, 50, 100, 15, -1, 2, 7) // LGPE Implementation .attr(CritOnlyAttr), new AttackMove(Moves.SPLISHY_SPLASH, Type.WATER, MoveCategory.SPECIAL, 90, 100, 15, 30, 0, 7) .attr(StatusEffectAttr, StatusEffect.PARALYSIS) @@ -9501,7 +9714,7 @@ export function initMoves() { new AttackMove(Moves.PIKA_PAPOW, Type.ELECTRIC, MoveCategory.SPECIAL, -1, -1, 20, -1, 0, 7) .attr(FriendshipPowerAttr), new AttackMove(Moves.BOUNCY_BUBBLE, Type.WATER, MoveCategory.SPECIAL, 60, 100, 20, -1, 0, 7) - .attr(HitHealAttr, 1.0) + .attr(HitHealAttr) // Custom .triageMove() .target(MoveTarget.ALL_NEAR_ENEMIES), new AttackMove(Moves.BUZZY_BUZZ, Type.ELECTRIC, MoveCategory.SPECIAL, 60, 100, 20, 100, 0, 7) @@ -9723,8 +9936,8 @@ export function initMoves() { .attr(ClearTerrainAttr) .condition((user, target, move) => !!user.scene.arena.terrain), new AttackMove(Moves.SCALE_SHOT, Type.DRAGON, MoveCategory.PHYSICAL, 25, 90, 20, -1, 0, 8) - .attr(StatStageChangeAttr, [ Stat.SPD ], 1, true, null, true, false, MoveEffectTrigger.HIT, false, true) - .attr(StatStageChangeAttr, [ Stat.DEF ], -1, true, null, true, false, MoveEffectTrigger.HIT, false, true) + .attr(StatStageChangeAttr, [ Stat.SPD ], 1, true, { lastHitOnly: true }) + .attr(StatStageChangeAttr, [ Stat.DEF ], -1, true, { lastHitOnly: true }) .attr(MultiHitAttr) .makesContact(false), new ChargingAttackMove(Moves.METEOR_BEAM, Type.ROCK, MoveCategory.SPECIAL, 120, 90, 10, -1, 0, 8) @@ -9858,7 +10071,7 @@ export function initMoves() { new AttackMove(Moves.TRIPLE_ARROWS, Type.FIGHTING, MoveCategory.PHYSICAL, 90, 100, 10, 30, 0, 8) .makesContact(false) .attr(HighCritAttr) - .attr(StatStageChangeAttr, [ Stat.DEF ], -1, undefined, undefined, undefined, undefined, undefined, undefined, undefined, 50) + .attr(StatStageChangeAttr, [ Stat.DEF ], -1, false, { effectChanceOverride: 50 }) .attr(FlinchAttr), new AttackMove(Moves.INFERNAL_PARADE, Type.GHOST, MoveCategory.SPECIAL, 60, 100, 15, 30, 0, 8) .attr(StatusEffectAttr, StatusEffect.BURN) @@ -9994,7 +10207,7 @@ export function initMoves() { .attr(TeraMoveCategoryAttr) .attr(TeraBlastTypeAttr) .attr(TeraBlastPowerAttr) - .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, true, (user, target, move) => user.isTerastallized() && user.isOfType(Type.STELLAR)) + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1, true, { condition: (user, target, move) => user.isTerastallized() && user.isOfType(Type.STELLAR) }) .partial(), /** Does not ignore abilities that affect stats, relevant in determining the move's category {@see TeraMoveCategoryAttr} */ new SelfStatusMove(Moves.SILK_TRAP, Type.BUG, -1, 10, -1, 4, 9) .attr(ProtectAttr, BattlerTagType.SILK_TRAP) @@ -10005,6 +10218,7 @@ export function initMoves() { .attr(ConfuseAttr) .recklessMove(), new AttackMove(Moves.LAST_RESPECTS, Type.GHOST, MoveCategory.PHYSICAL, 50, 100, 10, -1, 0, 9) + .partial() // Counter resets every wave instead of on arena reset .attr(MovePowerMultiplierAttr, (user, target, move) => 1 + Math.min(user.isPlayer() ? user.scene.currentBattle.playerFaints : user.scene.currentBattle.enemyFaints, 100)) .makesContact(false), new AttackMove(Moves.LUMINA_CRASH, Type.PSYCHIC, MoveCategory.SPECIAL, 80, 100, 10, 100, 0, 9) @@ -10077,7 +10291,7 @@ export function initMoves() { .attr(RemoveScreensAttr), new AttackMove(Moves.MAKE_IT_RAIN, Type.STEEL, MoveCategory.SPECIAL, 120, 100, 5, -1, 0, 9) .attr(MoneyAttr) - .attr(StatStageChangeAttr, [ Stat.SPATK ], -1, true, null, true, false, MoveEffectTrigger.HIT, true) + .attr(StatStageChangeAttr, [ Stat.SPATK ], -1, true, { firstTargetOnly: true }) .target(MoveTarget.ALL_NEAR_ENEMIES), new AttackMove(Moves.PSYBLADE, Type.PSYCHIC, MoveCategory.PHYSICAL, 80, 100, 15, -1, 0, 9) .attr(MovePowerMultiplierAttr, (user, target, move) => user.scene.arena.getTerrainType() === TerrainType.ELECTRIC && user.isGrounded() ? 1.5 : 1) @@ -10094,12 +10308,13 @@ export function initMoves() { .makesContact(), new SelfStatusMove(Moves.SHED_TAIL, Type.NORMAL, -1, 10, -1, 0, 9) .attr(AddSubstituteAttr, 0.5) - .attr(ForceSwitchOutAttr, true, SwitchType.SHED_TAIL), + .attr(ForceSwitchOutAttr, true, SwitchType.SHED_TAIL) + .condition(failIfLastInPartyCondition), 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), new SelfStatusMove(Moves.TIDY_UP, Type.NORMAL, -1, 10, -1, 0, 9) - .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPD ], 1, true, null, true, true) + .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPD ], 1, true) .attr(RemoveArenaTrapAttr, true) .attr(RemoveAllSubstitutesAttr), new StatusMove(Moves.SNOWSCAPE, Type.ICE, -1, 10, -1, 0, 9) diff --git a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts index c4b03660bde..0a88c5a699c 100644 --- a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts +++ b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts @@ -1,7 +1,7 @@ import { EnemyPartyConfig, generateModifierType, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, loadCustomMovesForEncounter, selectPokemonForOption, setEncounterRewards, transitionMysteryEncounterIntroVisuals } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { trainerConfigs, TrainerPartyCompoundTemplate, TrainerPartyTemplate, } from "#app/data/trainer-config"; import { ModifierTier } from "#app/modifier/modifier-tier"; -import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; +import { ModifierPoolType, modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { PartyMemberStrength } from "#enums/party-member-strength"; import BattleScene from "#app/battle-scene"; @@ -280,7 +280,7 @@ export const ClowningAroundEncounter: MysteryEncounter = let numRogue = 0; items.filter(m => m.isTransferable && !(m instanceof BerryModifier)) .forEach(m => { - const type = m.type.withTierFromPool(); + const type = m.type.withTierFromPool(ModifierPoolType.PLAYER, party); const tier = type.tier ?? ModifierTier.ULTRA; if (type.id === "GOLDEN_EGG" || tier === ModifierTier.ROGUE) { numRogue += m.stackCount; diff --git a/src/data/mystery-encounters/utils/encounter-phase-utils.ts b/src/data/mystery-encounters/utils/encounter-phase-utils.ts index 5cd2fbffd5f..66459c96ede 100644 --- a/src/data/mystery-encounters/utils/encounter-phase-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -418,7 +418,7 @@ export function generateModifierType(scene: BattleScene, modifier: () => Modifie // Populates item id and tier (order matters) result = result .withIdFromFunc(modifierTypes[modifierId]) - .withTierFromPool(); + .withTierFromPool(ModifierPoolType.PLAYER, scene.getParty()); return result instanceof ModifierTypeGenerator ? result.generateType(scene.getParty(), pregenArgs) : result; } diff --git a/src/data/pokemon-species.ts b/src/data/pokemon-species.ts index a93c35829ea..107fa5073af 100644 --- a/src/data/pokemon-species.ts +++ b/src/data/pokemon-species.ts @@ -47,7 +47,7 @@ export function getPokemonSpecies(species: Species | Species[] | undefined): Pok return allSpecies[species - 1]; } -export function getPokemonSpeciesForm(species: Species, formIndex: integer): PokemonSpeciesForm { +export function getPokemonSpeciesForm(species: Species, formIndex: number): PokemonSpeciesForm { const retSpecies: PokemonSpecies = species >= 2000 ? allSpecies.find(s => s.speciesId === species)! // TODO: is the bang correct? : allSpecies[species - 1]; @@ -129,26 +129,27 @@ export type PokemonSpeciesFilter = (species: PokemonSpecies) => boolean; export abstract class PokemonSpeciesForm { public speciesId: Species; - public formIndex: integer; - public generation: integer; - public type1: Type; - public type2: Type | null; - public height: number; - public weight: number; - public ability1: Abilities; - public ability2: Abilities; - public abilityHidden: Abilities; - public baseTotal: integer; - public baseStats: integer[]; - public catchRate: integer; - public baseFriendship: integer; - public baseExp: integer; - public genderDiffs: boolean; - public isStarterSelectable: boolean; + protected _formIndex: number; + protected _generation: number; + readonly type1: Type; + readonly type2: Type | null; + readonly height: number; + readonly weight: number; + readonly ability1: Abilities; + readonly ability2: Abilities; + readonly abilityHidden: Abilities; + readonly baseTotal: number; + readonly baseStats: number[]; + readonly catchRate: number; + readonly baseFriendship: number; + readonly baseExp: number; + readonly genderDiffs: boolean; + readonly isStarterSelectable: boolean; constructor(type1: Type, type2: Type | null, height: number, weight: number, ability1: Abilities, ability2: Abilities, abilityHidden: Abilities, - baseTotal: integer, baseHp: integer, baseAtk: integer, baseDef: integer, baseSpatk: integer, baseSpdef: integer, baseSpd: integer, - catchRate: integer, baseFriendship: integer, baseExp: integer, genderDiffs: boolean, isStarterSelectable: boolean) { + baseTotal: number, baseHp: number, baseAtk: number, baseDef: number, baseSpatk: number, baseSpdef: number, baseSpd: number, + catchRate: number, baseFriendship: number, baseExp: number, genderDiffs: boolean, isStarterSelectable: boolean + ) { this.type1 = type1; this.type2 = type2; this.height = height; @@ -180,7 +181,23 @@ export abstract class PokemonSpeciesForm { return ret; } - isOfType(type: integer): boolean { + get generation(): number { + return this._generation; + } + + set generation(generation: number) { + this._generation = generation; + } + + get formIndex(): number { + return this._formIndex; + } + + set formIndex(formIndex: number) { + this._formIndex = formIndex; + } + + isOfType(type: number): boolean { return this.type1 === type || (this.type2 !== null && this.type2 === type); } @@ -188,7 +205,7 @@ export abstract class PokemonSpeciesForm { * Method to get the total number of abilities a Pokemon species has. * @returns Number of abilities */ - getAbilityCount(): integer { + getAbilityCount(): number { return this.abilityHidden !== Abilities.NONE ? 3 : 2; } @@ -197,7 +214,7 @@ export abstract class PokemonSpeciesForm { * @param abilityIndex Which ability to get (should only be 0-2) * @returns The id of the Ability */ - getAbility(abilityIndex: integer): Abilities { + getAbility(abilityIndex: number): Abilities { let ret: Abilities; if (abilityIndex === 0) { ret = this.ability1; @@ -277,12 +294,12 @@ export abstract class PokemonSpeciesForm { return ret; } - getSpriteAtlasPath(female: boolean, formIndex?: integer, shiny?: boolean, variant?: integer): string { + getSpriteAtlasPath(female: boolean, formIndex?: number, shiny?: boolean, variant?: number): string { const spriteId = this.getSpriteId(female, formIndex, shiny, variant).replace(/\_{2}/g, "/"); return `${/_[1-3]$/.test(spriteId) ? "variant/" : ""}${spriteId}`; } - getSpriteId(female: boolean, formIndex?: integer, shiny?: boolean, variant: integer = 0, back?: boolean): string { + getSpriteId(female: boolean, formIndex?: number, shiny?: boolean, variant: number = 0, back?: boolean): string { if (formIndex === undefined || this instanceof PokemonForm) { formIndex = this.formIndex; } @@ -299,11 +316,11 @@ export abstract class PokemonSpeciesForm { return `${back ? "back__" : ""}${shiny && (!variantSet || (!variant && !variantSet[variant || 0])) ? "shiny__" : ""}${baseSpriteKey}${shiny && variantSet && variantSet[variant] === 2 ? `_${variant + 1}` : ""}`; } - getSpriteKey(female: boolean, formIndex?: integer, shiny?: boolean, variant?: integer): string { + getSpriteKey(female: boolean, formIndex?: number, shiny?: boolean, variant?: number): string { return `pkmn__${this.getSpriteId(female, formIndex, shiny, variant)}`; } - abstract getFormSpriteKey(formIndex?: integer): string; + abstract getFormSpriteKey(formIndex?: number): string; /** @@ -311,9 +328,9 @@ export abstract class PokemonSpeciesForm { * @param formIndex optional form index for pokemon with different forms * @returns species id if no additional forms, index with formkey if a pokemon with a form */ - getVariantDataIndex(formIndex?: integer) { + getVariantDataIndex(formIndex?: number) { let formkey: string | null = null; - let variantDataIndex: integer | string = this.speciesId; + let variantDataIndex: number | string = this.speciesId; const species = getPokemonSpecies(this.speciesId); if (species.forms.length > 0 && formIndex !== undefined) { formkey = species.forms[formIndex]?.getFormSpriteKey(formIndex); @@ -324,13 +341,13 @@ export abstract class PokemonSpeciesForm { return variantDataIndex; } - getIconAtlasKey(formIndex?: integer, shiny?: boolean, variant?: integer): string { + getIconAtlasKey(formIndex?: number, shiny?: boolean, variant?: number): string { const variantDataIndex = this.getVariantDataIndex(formIndex); const isVariant = shiny && variantData[variantDataIndex] && (variant !== undefined && variantData[variantDataIndex][variant]); return `pokemon_icons_${this.generation}${isVariant ? "v" : ""}`; } - getIconId(female: boolean, formIndex?: integer, shiny?: boolean, variant?: integer): string { + getIconId(female: boolean, formIndex?: number, shiny?: boolean, variant?: number): string { if (formIndex === undefined) { formIndex = this.formIndex; } @@ -379,7 +396,7 @@ export abstract class PokemonSpeciesForm { return ret; } - getCryKey(formIndex?: integer): string { + getCryKey(formIndex?: number): string { let speciesId = this.speciesId; if (this.speciesId > 2000) { switch (this.speciesId) { @@ -446,7 +463,7 @@ export abstract class PokemonSpeciesForm { return ret; } - validateStarterMoveset(moveset: StarterMoveset, eggMoves: integer): boolean { + validateStarterMoveset(moveset: StarterMoveset, eggMoves: number): boolean { const rootSpeciesId = this.getRootSpeciesId(); for (const moveId of moveset) { if (speciesEggMoves.hasOwnProperty(rootSpeciesId)) { @@ -467,7 +484,7 @@ export abstract class PokemonSpeciesForm { return true; } - loadAssets(scene: BattleScene, female: boolean, formIndex?: integer, shiny?: boolean, variant?: Variant, startLoad?: boolean): Promise { + loadAssets(scene: BattleScene, female: boolean, formIndex?: number, shiny?: boolean, variant?: Variant, startLoad?: boolean): Promise { return new Promise(resolve => { const spriteKey = this.getSpriteKey(female, formIndex, shiny, variant); scene.loadPokemonAtlas(spriteKey, this.getSpriteAtlasPath(female, formIndex, shiny, variant)); @@ -536,7 +553,7 @@ export abstract class PokemonSpeciesForm { return cry; } - generateCandyColors(scene: BattleScene): integer[][] { + generateCandyColors(scene: BattleScene): number[][] { const sourceTexture = scene.textures.get(this.getSpriteKey(false)); const sourceFrame = sourceTexture.frames[sourceTexture.firstFrame]; @@ -544,7 +561,7 @@ export abstract class PokemonSpeciesForm { const canvas = document.createElement("canvas"); - const spriteColors: integer[][] = []; + const spriteColors: number[][] = []; const context = canvas.getContext("2d"); const frame = sourceFrame; @@ -567,7 +584,7 @@ export abstract class PokemonSpeciesForm { } for (let i = 0; i < pixelData.length; i += 4) { - const total = pixelData.slice(i, i + 3).reduce((total: integer, value: integer) => total + value, 0); + const total = pixelData.slice(i, i + 3).reduce((total: number, value: number) => total + value, 0); if (!total) { continue; } @@ -586,27 +603,28 @@ export abstract class PokemonSpeciesForm { Math.random = originalRandom; - return Array.from(paletteColors.keys()).map(c => Object.values(rgbaFromArgb(c)) as integer[]); + return Array.from(paletteColors.keys()).map(c => Object.values(rgbaFromArgb(c)) as number[]); } } export default class PokemonSpecies extends PokemonSpeciesForm implements Localizable { public name: string; - public subLegendary: boolean; - public legendary: boolean; - public mythical: boolean; - public species: string; - public growthRate: GrowthRate; - public malePercent: number | null; - public genderDiffs: boolean; - public canChangeForm: boolean; - public forms: PokemonForm[]; + readonly subLegendary: boolean; + readonly legendary: boolean; + readonly mythical: boolean; + readonly species: string; + readonly growthRate: GrowthRate; + readonly malePercent: number | null; + readonly genderDiffs: boolean; + readonly canChangeForm: boolean; + readonly forms: PokemonForm[]; - constructor(id: Species, generation: integer, subLegendary: boolean, legendary: boolean, mythical: boolean, species: string, + constructor(id: Species, generation: number, subLegendary: boolean, legendary: boolean, mythical: boolean, species: string, type1: Type, type2: Type | null, height: number, weight: number, ability1: Abilities, ability2: Abilities, abilityHidden: Abilities, - baseTotal: integer, baseHp: integer, baseAtk: integer, baseDef: integer, baseSpatk: integer, baseSpdef: integer, baseSpd: integer, - catchRate: integer, baseFriendship: integer, baseExp: integer, growthRate: GrowthRate, malePercent: number | null, - genderDiffs: boolean, canChangeForm?: boolean, ...forms: PokemonForm[]) { + baseTotal: number, baseHp: number, baseAtk: number, baseDef: number, baseSpatk: number, baseSpdef: number, baseSpd: number, + catchRate: number, baseFriendship: number, baseExp: number, growthRate: GrowthRate, malePercent: number | null, + genderDiffs: boolean, canChangeForm?: boolean, ...forms: PokemonForm[] + ) { super(type1, type2, height, weight, ability1, ability2, abilityHidden, baseTotal, baseHp, baseAtk, baseDef, baseSpatk, baseSpdef, baseSpd, catchRate, baseFriendship, baseExp, genderDiffs, false); this.speciesId = id; @@ -631,7 +649,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali }); } - getName(formIndex?: integer): string { + getName(formIndex?: number): string { if (formIndex !== undefined && this.forms.length) { const form = this.forms[formIndex]; let key: string | null; @@ -662,11 +680,11 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali this.name = i18next.t(`pokemon:${Species[this.speciesId].toLowerCase()}`); } - getWildSpeciesForLevel(level: integer, allowEvolving: boolean, isBoss: boolean, gameMode: GameMode): Species { + getWildSpeciesForLevel(level: number, allowEvolving: boolean, isBoss: boolean, gameMode: GameMode): Species { return this.getSpeciesForLevel(level, allowEvolving, false, (isBoss ? PartyMemberStrength.WEAKER : PartyMemberStrength.AVERAGE) + (gameMode?.isEndless ? 1 : 0)); } - getTrainerSpeciesForLevel(level: integer, allowEvolving: boolean = false, strength: PartyMemberStrength, currentWave: number = 0): Species { + getTrainerSpeciesForLevel(level: number, allowEvolving: boolean = false, strength: PartyMemberStrength, currentWave: number = 0): Species { return this.getSpeciesForLevel(level, allowEvolving, true, strength, currentWave); } @@ -688,7 +706,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali * @param strength {@linkcode PartyMemberStrength} The strength of the party member in question * @returns {@linkcode integer} The level difference from expected evolution level tolerated for a mon to be unevolved. Lower value = higher evolution chance. */ - private getStrengthLevelDiff(strength: PartyMemberStrength): integer { + private getStrengthLevelDiff(strength: PartyMemberStrength): number { switch (Math.min(strength, PartyMemberStrength.STRONGER)) { case PartyMemberStrength.WEAKEST: return 60; @@ -705,7 +723,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali } } - getSpeciesForLevel(level: integer, allowEvolving: boolean = false, forTrainer: boolean = false, strength: PartyMemberStrength = PartyMemberStrength.WEAKER, currentWave: number = 0): Species { + getSpeciesForLevel(level: number, allowEvolving: boolean = false, forTrainer: boolean = false, strength: PartyMemberStrength = PartyMemberStrength.WEAKER, currentWave: number = 0): Species { const prevolutionLevels = this.getPrevolutionLevels(); if (prevolutionLevels.length) { @@ -847,7 +865,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali } // This could definitely be written better and more accurate to the getSpeciesForLevel logic, but it is only for generating movesets for evolved Pokemon - getSimulatedEvolutionChain(currentLevel: integer, forTrainer: boolean = false, isBoss: boolean = false, player: boolean = false): EvolutionLevel[] { + getSimulatedEvolutionChain(currentLevel: number, forTrainer: boolean = false, isBoss: boolean = false, player: boolean = false): EvolutionLevel[] { const ret: EvolutionLevel[] = []; if (pokemonPrevolutions.hasOwnProperty(this.speciesId)) { const prevolutionLevels = this.getPrevolutionLevels().reverse(); @@ -899,7 +917,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali return variantData.hasOwnProperty(variantDataIndex) || variantData.hasOwnProperty(this.speciesId); } - getFormSpriteKey(formIndex?: integer) { + getFormSpriteKey(formIndex?: number) { if (this.forms.length && (formIndex !== undefined && formIndex >= this.forms.length)) { console.warn(`Attempted accessing form with index ${formIndex} of species ${this.getName()} with only ${this.forms.length || 0} forms`); formIndex = Math.min(formIndex, this.forms.length - 1); @@ -919,16 +937,17 @@ export class PokemonForm extends PokemonSpeciesForm { private starterSelectableKeys: string[] = [ "10", "50", "10-pc", "50-pc", "red", "orange", "yellow", "green", "blue", "indigo", "violet" ]; constructor(formName: string, formKey: string, type1: Type, type2: Type | null, height: number, weight: number, ability1: Abilities, ability2: Abilities, abilityHidden: Abilities, - baseTotal: integer, baseHp: integer, baseAtk: integer, baseDef: integer, baseSpatk: integer, baseSpdef: integer, baseSpd: integer, - catchRate: integer, baseFriendship: integer, baseExp: integer, genderDiffs?: boolean, formSpriteKey?: string | null, isStarterSelectable?: boolean, ) { + baseTotal: number, baseHp: number, baseAtk: number, baseDef: number, baseSpatk: number, baseSpdef: number, baseSpd: number, + catchRate: number, baseFriendship: number, baseExp: number, genderDiffs: boolean = false, formSpriteKey: string | null = null, isStarterSelectable: boolean = false + ) { super(type1, type2, height, weight, ability1, ability2, abilityHidden, baseTotal, baseHp, baseAtk, baseDef, baseSpatk, baseSpdef, baseSpd, - catchRate, baseFriendship, baseExp, !!genderDiffs, (!!isStarterSelectable || !formKey)); + catchRate, baseFriendship, baseExp, genderDiffs, (isStarterSelectable || !formKey)); this.formName = formName; this.formKey = formKey; - this.formSpriteKey = formSpriteKey !== undefined ? formSpriteKey : null; + this.formSpriteKey = formSpriteKey; } - getFormSpriteKey(_formIndex?: integer) { + getFormSpriteKey(_formIndex?: number) { return this.formSpriteKey !== null ? this.formSpriteKey : this.formKey; } } diff --git a/src/enums/switch-type.ts b/src/enums/switch-type.ts index b25ba6ad119..752c0902636 100644 --- a/src/enums/switch-type.ts +++ b/src/enums/switch-type.ts @@ -3,6 +3,8 @@ * or {@linkcode SwitchSummonPhase} will carry out. */ export enum SwitchType { + /** Switchout specifically for when combat starts and the player is prompted if they will switch Pokemon */ + INITIAL_SWITCH, /** 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 */ diff --git a/src/field/arena.ts b/src/field/arena.ts index 7bfdf9a0000..abc2b89569c 100644 --- a/src/field/arena.ts +++ b/src/field/arena.ts @@ -10,7 +10,14 @@ import Move from "#app/data/move"; import { ArenaTag, ArenaTagSide, ArenaTrapTag, getArenaTag } from "#app/data/arena-tag"; import { BattlerIndex } from "#app/battle"; import { Terrain, TerrainType } from "#app/data/terrain"; -import { applyPostTerrainChangeAbAttrs, applyPostWeatherChangeAbAttrs, PostTerrainChangeAbAttr, PostWeatherChangeAbAttr } from "#app/data/ability"; +import { + applyAbAttrs, + applyPostTerrainChangeAbAttrs, + applyPostWeatherChangeAbAttrs, + PostTerrainChangeAbAttr, + PostWeatherChangeAbAttr, + TerrainEventTypeChangeAbAttr +} from "#app/data/ability"; import Pokemon from "#app/field/pokemon"; import Overrides from "#app/overrides"; import { TagAddedEvent, TagRemovedEvent, TerrainChangedEvent, WeatherChangedEvent } from "#app/events/arena"; @@ -217,66 +224,6 @@ export class Arena { return 0; } - getTypeForBiome() { - switch (this.biomeType) { - case Biome.TOWN: - case Biome.PLAINS: - case Biome.METROPOLIS: - return Type.NORMAL; - case Biome.GRASS: - case Biome.TALL_GRASS: - return Type.GRASS; - case Biome.FOREST: - case Biome.JUNGLE: - return Type.BUG; - case Biome.SLUM: - case Biome.SWAMP: - return Type.POISON; - case Biome.SEA: - case Biome.BEACH: - case Biome.LAKE: - case Biome.SEABED: - return Type.WATER; - case Biome.MOUNTAIN: - return Type.FLYING; - case Biome.BADLANDS: - return Type.GROUND; - case Biome.CAVE: - case Biome.DESERT: - return Type.ROCK; - case Biome.ICE_CAVE: - case Biome.SNOWY_FOREST: - return Type.ICE; - case Biome.MEADOW: - case Biome.FAIRY_CAVE: - case Biome.ISLAND: - return Type.FAIRY; - case Biome.POWER_PLANT: - return Type.ELECTRIC; - case Biome.VOLCANO: - return Type.FIRE; - case Biome.GRAVEYARD: - case Biome.TEMPLE: - return Type.GHOST; - case Biome.DOJO: - case Biome.CONSTRUCTION_SITE: - return Type.FIGHTING; - case Biome.FACTORY: - case Biome.LABORATORY: - return Type.STEEL; - case Biome.RUINS: - case Biome.SPACE: - return Type.PSYCHIC; - case Biome.WASTELAND: - case Biome.END: - return Type.DRAGON; - case Biome.ABYSS: - return Type.DARK; - default: - return Type.UNKNOWN; - } - } - getBgTerrainColorRatioForBiome(): number { switch (this.biomeType) { case Biome.SPACE: @@ -387,6 +334,7 @@ export class Arena { this.scene.getField(true).filter(p => p.isOnField()).map(pokemon => { pokemon.findAndRemoveTags(t => "terrainTypes" in t && !(t.terrainTypes as TerrainType[]).find(t => t === terrain)); applyPostTerrainChangeAbAttrs(PostTerrainChangeAbAttr, pokemon, terrain); + applyAbAttrs(TerrainEventTypeChangeAbAttr, pokemon, null, false); }); return true; @@ -786,7 +734,7 @@ export class Arena { case Biome.VOLCANO: return 17.637; case Biome.GRAVEYARD: - return 3.232; + return 13.711; case Biome.DOJO: return 6.205; case Biome.FACTORY: diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 4cb8390d4f4..6c328fa25cc 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -12,7 +12,7 @@ import * as Utils from "#app/utils"; import { Type, TypeDamageMultiplier, getTypeDamageMultiplier, getTypeRgb } from "#app/data/type"; import { getLevelTotalExp } from "#app/data/exp"; import { Stat, type PermanentStat, type BattleStat, type EffectiveStat, PERMANENT_STATS, BATTLE_STATS, EFFECTIVE_STATS } from "#enums/stat"; -import { DamageMoneyRewardModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, HiddenAbilityRateBoosterModifier, BaseStatModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonNatureWeightModifier, ShinyRateBoosterModifier, SurviveDamageModifier, TempStatStageBoosterModifier, TempCritBoosterModifier, StatBoosterModifier, CritBoosterModifier, TerastallizeModifier, PokemonBaseStatFlatModifier, PokemonBaseStatTotalModifier, PokemonIncrementingStatModifier, EvoTrackerModifier } from "#app/modifier/modifier"; +import { DamageMoneyRewardModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, HiddenAbilityRateBoosterModifier, BaseStatModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonNatureWeightModifier, ShinyRateBoosterModifier, SurviveDamageModifier, TempStatStageBoosterModifier, TempCritBoosterModifier, StatBoosterModifier, CritBoosterModifier, TerastallizeModifier, PokemonBaseStatFlatModifier, PokemonBaseStatTotalModifier, PokemonIncrementingStatModifier, EvoTrackerModifier, PokemonMultiHitModifier } from "#app/modifier/modifier"; import { PokeballType } from "#app/data/pokeball"; import { Gender } from "#app/data/gender"; import { initMoveAnim, loadMoveAnimAssets } from "#app/data/battle-anims"; @@ -22,7 +22,7 @@ import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "#app/data/balance/ import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, SubstituteTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, MoveRestrictionBattlerTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag, TarShotTag, AutotomizedTag, PowerTrickTag } from "../data/battler-tags"; import { WeatherType } from "#app/data/weather"; import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "#app/data/arena-tag"; -import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr, PostSetStatusAbAttr, applyPostSetStatusAbAttrs, InfiltratorAbAttr } from "#app/data/ability"; +import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr, PostSetStatusAbAttr, applyPostSetStatusAbAttrs, InfiltratorAbAttr, AlliedFieldDamageReductionAbAttr, PostDamageAbAttr, applyPostDamageAbAttrs, PostDamageForceSwitchAbAttr } from "#app/data/ability"; import PokemonData from "#app/system/pokemon-data"; import { BattlerIndex } from "#app/battle"; import { Mode } from "#app/ui/ui"; @@ -428,38 +428,26 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { resolve(); }; if (this.shiny) { - const populateVariantColors = (key: string, back: boolean = false): Promise => { + const populateVariantColors = (isBackSprite: boolean = false): Promise => { return new Promise(resolve => { - const battleSpritePath = this.getBattleSpriteAtlasPath(back, ignoreOverride).replace("variant/", "").replace(/_[1-3]$/, ""); + const battleSpritePath = this.getBattleSpriteAtlasPath(isBackSprite, ignoreOverride).replace("variant/", "").replace(/_[1-3]$/, ""); let config = variantData; - const useExpSprite = this.scene.experimentalSprites && this.scene.hasExpSprite(this.getBattleSpriteKey(back, ignoreOverride)); + const useExpSprite = this.scene.experimentalSprites && this.scene.hasExpSprite(this.getBattleSpriteKey(isBackSprite, ignoreOverride)); battleSpritePath.split("/").map(p => config ? config = config[p] : null); const variantSet: VariantSet = config as VariantSet; if (variantSet && variantSet[this.variant] === 1) { - if (variantColorCache.hasOwnProperty(key)) { - return resolve(); + const cacheKey = this.getBattleSpriteKey(isBackSprite); + if (!variantColorCache.hasOwnProperty(cacheKey)) { + this.populateVariantColorCache(cacheKey, useExpSprite, battleSpritePath); } - this.scene.cachedFetch(`./images/pokemon/variant/${useExpSprite ? "exp/" : ""}${battleSpritePath}.json`). - then(res => { - // Prevent the JSON from processing if it failed to load - if (!res.ok) { - console.error(`Could not load ${res.url}!`); - return; - } - return res.json(); - }).then(c => { - variantColorCache[key] = c; - resolve(); - }); - } else { - resolve(); } + resolve(); }); }; if (this.isPlayer()) { - Promise.all([ populateVariantColors(this.getBattleSpriteKey(false)), populateVariantColors(this.getBattleSpriteKey(true), true) ]).then(() => updateFusionPaletteAndResolve()); + Promise.all([ populateVariantColors(false), populateVariantColors(true) ]).then(() => updateFusionPaletteAndResolve()); } else { - populateVariantColors(this.getBattleSpriteKey(false)).then(() => updateFusionPaletteAndResolve()); + populateVariantColors(false).then(() => updateFusionPaletteAndResolve()); } } else { updateFusionPaletteAndResolve(); @@ -472,6 +460,45 @@ export default 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 + */ + fallbackVariantColor(cacheKey: string, attemptedSpritePath: string, useExpSprite: boolean, battleSpritePath: string, ...optionalParams: any[]) { + console.warn(`Could not load ${attemptedSpritePath}!`, ...optionalParams); + if (useExpSprite) { + this.populateVariantColorCache(cacheKey, false, battleSpritePath); + } + } + + /** + * Attempt to process variant sprite. + * + * @param cacheKey the cache key for the variant color sprite + * @param useExpSprite should the experimental sprite be used + * @param battleSpritePath the filename of the sprite + */ + populateVariantColorCache(cacheKey: string, useExpSprite: boolean, battleSpritePath: string) { + const spritePath = `./images/pokemon/variant/${useExpSprite ? "exp/" : ""}${battleSpritePath}.json`; + this.scene.cachedFetch(spritePath).then(res => { + // Prevent the JSON from processing if it failed to load + if (!res.ok) { + return this.fallbackVariantColor(cacheKey, res.url, useExpSprite, battleSpritePath, res.status, res.statusText); + } + return res.json(); + }).catch(error => { + this.fallbackVariantColor(cacheKey, spritePath, useExpSprite, battleSpritePath, error); + }).then(c => { + variantColorCache[cacheKey] = c; + }); + } + getFormKey(): string { if (!this.species.forms.length || this.species.forms.length <= this.formIndex) { return ""; @@ -1258,6 +1285,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } } + // the type added to Pokemon from moves like Forest's Curse or Trick Or Treat + if (!ignoreOverride && this.summonData && this.summonData.addedType && !types.includes(this.summonData.addedType)) { + types.push(this.summonData.addedType); + } + // If both types are the same (can happen in weird custom typing scenarios), reduce to single type if (types.length > 1 && types[0] === types[1]) { types.splice(0, 1); @@ -2667,9 +2699,15 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.scene.applyModifiers(EnemyDamageReducerModifier, false, damage); } + /** Apply this Pokemon's post-calc defensive modifiers (e.g. Fur Coat) */ if (!ignoreAbility) { applyPreDefendAbAttrs(ReceivedMoveDamageMultiplierAbAttr, this, source, move, cancelled, simulated, damage); + + /** Additionally apply friend guard damage reduction if ally has it. */ + if (this.scene.currentBattle.double && this.getAlly()?.isActive(true)) { + applyPreDefendAbAttrs(AlliedFieldDamageReductionAbAttr, this.getAlly(), source, move, cancelled, simulated, damage); + } } // This attribute may modify damage arbitrarily, so be careful about changing its order of application. @@ -2780,7 +2818,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * We explicitly require to ignore the faint phase here, as we want to show the messages * about the critical hit and the super effective/not very effective messages before the faint phase. */ - const damage = this.damageAndUpdate(isBlockedBySubstitute ? 0 : dmg, result as DamageResult, isCritical, isOneHitKo, isOneHitKo, true); + const damage = this.damageAndUpdate(isBlockedBySubstitute ? 0 : dmg, result as DamageResult, isCritical, isOneHitKo, isOneHitKo, true, source); if (damage > 0) { if (source.isPlayer()) { @@ -2789,10 +2827,19 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.scene.gameData.gameStats.highestDamage = damage; } } - source.turnData.damageDealt += damage; - source.turnData.currDamageDealt = damage; + source.turnData.totalDamageDealt += damage; + source.turnData.singleHitDamageDealt = damage; this.turnData.damageTaken += damage; this.battleData.hitCount++; + + // Multi-Lens and Parental Bond check for Wimp Out/Emergency Exit + if (this.hasAbilityWithAttr(PostDamageForceSwitchAbAttr)) { + const multiHitModifier = source.getHeldItems().find(m => m instanceof PokemonMultiHitModifier); + if (multiHitModifier || source.hasAbilityWithAttr(AddSecondStrikeAbAttr)) { + applyPostDamageAbAttrs(PostDamageAbAttr, this, damage, this.hasPassive(), false, [], source); + } + } + const attackResult = { move: move.id, result: result as DamageResult, damage: damage, critical: isCritical, sourceId: source.id, sourceBattlerIndex: source.getBattlerIndex() }; this.turnData.attacksReceived.unshift(attackResult); if (source.isPlayer() && !this.isPlayer()) { @@ -2880,7 +2927,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.destroySubstitute(); this.resetSummonData(); } - return damage; } @@ -2894,12 +2940,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @param ignoreFaintPhase boolean to ignore adding a FaintPhase, passsed to damage() * @returns integer of damage done */ - damageAndUpdate(damage: integer, result?: DamageResult, critical: boolean = false, ignoreSegments: boolean = false, preventEndure: boolean = false, ignoreFaintPhase: boolean = false): integer { + damageAndUpdate(damage: integer, result?: DamageResult, critical: boolean = false, ignoreSegments: boolean = false, preventEndure: boolean = false, ignoreFaintPhase: boolean = false, source?: Pokemon): integer { const damagePhase = new DamagePhase(this.scene, this.getBattlerIndex(), damage, result as DamageResult, critical); this.scene.unshiftPhase(damagePhase); damage = this.damage(damage, ignoreSegments, preventEndure, ignoreFaintPhase); // Damage amount may have changed, but needed to be queued before calling damage function damagePhase.updateAmount(damage); + applyPostDamageAbAttrs(PostDamageAbAttr, this, damage, this.hasPassive(), false, [], source); return damage; } @@ -5083,7 +5130,6 @@ export class PokemonSummonData { public tags: BattlerTag[] = []; public abilitySuppressed: boolean = false; public abilitiesApplied: Abilities[] = []; - public speciesForm: PokemonSpeciesForm | null; public fusionSpeciesForm: PokemonSpeciesForm; public ability: Abilities = Abilities.NONE; @@ -5094,6 +5140,7 @@ export class PokemonSummonData { public moveset: (PokemonMove | null)[]; // If not initialized this value will not be populated from save data. public types: Type[] = []; + public addedType: Type | null = null; } export class PokemonBattleData { @@ -5122,8 +5169,8 @@ export class PokemonTurnData { * - `0` = Move is finished */ public hitsLeft: number = -1; - public damageDealt: number = 0; - public currDamageDealt: number = 0; + public totalDamageDealt: number = 0; + public singleHitDamageDealt: number = 0; public damageTaken: number = 0; public attacksReceived: AttackMoveResult[] = []; public order: number; @@ -5131,6 +5178,8 @@ export class PokemonTurnData { public statStagesDecreased: boolean = false; public moveEffectiveness: TypeDamageMultiplier | null = null; public combiningPledge?: Moves; + public switchedInThisTurn: boolean = false; + public failedRunAway: boolean = false; } export enum AiType { diff --git a/src/loading-scene.ts b/src/loading-scene.ts index 578b9aba4fc..1aed61b34ba 100644 --- a/src/loading-scene.ts +++ b/src/loading-scene.ts @@ -244,7 +244,7 @@ export class LoadingScene extends SceneBase { this.loadAtlas("statuses", ""); this.loadAtlas("types", ""); } - const availableLangs = [ "en", "de", "it", "fr", "ja", "ko", "es", "pt-BR", "zh-CN" ]; + const availableLangs = [ "en", "de", "it", "fr", "ja", "ko", "es-ES", "pt-BR", "zh-CN" ]; if (lang && availableLangs.includes(lang)) { this.loadImage("halloween2024-event-" + lang, "events"); } else { diff --git a/src/main.ts b/src/main.ts index 92ee267bf65..993bd1018ae 100644 --- a/src/main.ts +++ b/src/main.ts @@ -44,7 +44,7 @@ document.fonts.load("16px emerald").then(() => document.fonts.load("10px pkmnems let game; -const startGame = async () => { +const startGame = async (manifest?: any) => { await initI18n(); const LoadingScene = (await import("./loading-scene")).LoadingScene; const BattleScene = (await import("./battle-scene")).default; @@ -94,13 +94,15 @@ const startGame = async () => { version: version }); game.sound.pauseOnBlur = false; + if (manifest) { + game["manifest"] = manifest; + } }; fetch("/manifest.json") .then(res => res.json()) .then(jsonResponse => { - startGame(); - game["manifest"] = jsonResponse.manifest; + startGame(jsonResponse.manifest); }).catch(() => { // Manifest not found (likely local build) startGame(); diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index cf2d74483f9..dfa46ce3667 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -19,7 +19,7 @@ import { Unlockables } from "#app/system/unlockables"; import { getVoucherTypeIcon, getVoucherTypeName, VoucherType } from "#app/system/voucher"; import PartyUiHandler, { PokemonMoveSelectFilter, PokemonSelectFilter } from "#app/ui/party-ui-handler"; import { getModifierTierTextTint } from "#app/ui/text"; -import { formatMoney, getEnumKeys, getEnumValues, IntegerHolder, NumberHolder, padInt, randSeedInt, randSeedItem } from "#app/utils"; +import { formatMoney, getEnumKeys, getEnumValues, IntegerHolder, isNullOrUndefined, NumberHolder, padInt, randSeedInt, randSeedItem } from "#app/utils"; import { Abilities } from "#enums/abilities"; import { BattlerTagType } from "#enums/battler-tag-type"; import { BerryType } from "#enums/berry-type"; @@ -121,18 +121,41 @@ export class ModifierType { * Populates item tier for ModifierType instance * Tier is a necessary field for items that appear in player shop (determines the Pokeball visual they use) * To find the tier, this function performs a reverse lookup of the item type in modifier pools + * It checks the weight of the item and will use the first tier for which the weight is greater than 0 + * This is to allow items to be in multiple item pools depending on the conditions, for example for events + * If all tiers have a weight of 0 for the item, the first tier where the item was found is used * @param poolType Default 'ModifierPoolType.PLAYER'. Which pool to lookup item tier from + * @param party optional. Needed to check the weight of modifiers with conditional weight (see {@linkcode WeightedModifierTypeWeightFunc}) + * if not provided or empty, the weight check will be ignored + * @param rerollCount Default `0`. Used to check the weight of modifiers with conditional weight (see {@linkcode WeightedModifierTypeWeightFunc}) */ - withTierFromPool(poolType: ModifierPoolType = ModifierPoolType.PLAYER): ModifierType { + withTierFromPool(poolType: ModifierPoolType = ModifierPoolType.PLAYER, party?: PlayerPokemon[], rerollCount: number = 0): ModifierType { + let defaultTier: undefined | ModifierTier; for (const tier of Object.values(getModifierPoolForType(poolType))) { for (const modifier of tier) { if (this.id === modifier.modifierType.id) { - this.tier = modifier.modifierType.tier; - return this; + let weight: number; + if (modifier.weight instanceof Function) { + weight = party ? modifier.weight(party, rerollCount) : 0; + } else { + weight = modifier.weight; + } + if (weight > 0) { + this.tier = modifier.modifierType.tier; + return this; + } else if (isNullOrUndefined(defaultTier)) { + // If weight is 0, keep track of the first tier where the item was found + defaultTier = modifier.modifierType.tier; + } } } } + // Didn't find a pool with weight > 0, fallback to first tier where the item was found, if any + if (defaultTier) { + this.tier = defaultTier; + } + return this; } @@ -502,45 +525,25 @@ export class BerryModifierType extends PokemonHeldItemModifierType implements Ge } } -function getAttackTypeBoosterItemName(type: Type) { - switch (type) { - case Type.NORMAL: - return "Silk Scarf"; - case Type.FIGHTING: - return "Black Belt"; - case Type.FLYING: - return "Sharp Beak"; - case Type.POISON: - return "Poison Barb"; - case Type.GROUND: - return "Soft Sand"; - case Type.ROCK: - return "Hard Stone"; - case Type.BUG: - return "Silver Powder"; - case Type.GHOST: - return "Spell Tag"; - case Type.STEEL: - return "Metal Coat"; - case Type.FIRE: - return "Charcoal"; - case Type.WATER: - return "Mystic Water"; - case Type.GRASS: - return "Miracle Seed"; - case Type.ELECTRIC: - return "Magnet"; - case Type.PSYCHIC: - return "Twisted Spoon"; - case Type.ICE: - return "Never-Melt Ice"; - case Type.DRAGON: - return "Dragon Fang"; - case Type.DARK: - return "Black Glasses"; - case Type.FAIRY: - return "Fairy Feather"; - } +enum AttackTypeBoosterItem { + SILK_SCARF, + BLACK_BELT, + SHARP_BEAK, + POISON_BARB, + SOFT_SAND, + HARD_STONE, + SILVER_POWDER, + SPELL_TAG, + METAL_COAT, + CHARCOAL, + MYSTIC_WATER, + MIRACLE_SEED, + MAGNET, + TWISTED_SPOON, + NEVER_MELT_ICE, + DRAGON_FANG, + BLACK_GLASSES, + FAIRY_FEATHER } export class AttackTypeBoosterModifierType extends PokemonHeldItemModifierType implements GeneratedPersistentModifierType { @@ -548,7 +551,7 @@ export class AttackTypeBoosterModifierType extends PokemonHeldItemModifierType i public boostPercent: integer; constructor(moveType: Type, boostPercent: integer) { - super("", `${getAttackTypeBoosterItemName(moveType)?.replace(/[ \-]/g, "_").toLowerCase()}`, + super("", `${AttackTypeBoosterItem[moveType]?.toLowerCase()}`, (_type, args) => new AttackTypeBoosterModifier(this, (args[0] as Pokemon).id, moveType, boostPercent)); this.moveType = moveType; @@ -556,7 +559,7 @@ export class AttackTypeBoosterModifierType extends PokemonHeldItemModifierType i } get name(): string { - return i18next.t(`modifierType:AttackTypeBoosterItem.${getAttackTypeBoosterItemName(this.moveType)?.replace(/[ \-]/g, "_").toLowerCase()}`); + return i18next.t(`modifierType:AttackTypeBoosterItem.${AttackTypeBoosterItem[this.moveType]?.toLowerCase()}`); } getDescription(scene: BattleScene): string { @@ -2137,7 +2140,7 @@ export function getPlayerModifierTypeOptions(count: integer, party: PlayerPokemo // Populates item id and tier guaranteedMod = guaranteedMod .withIdFromFunc(modifierTypes[modifierId]) - .withTierFromPool(); + .withTierFromPool(ModifierPoolType.PLAYER, party); const modType = guaranteedMod instanceof ModifierTypeGenerator ? guaranteedMod.generateType(party) : guaranteedMod; if (modType) { @@ -2206,7 +2209,7 @@ export function overridePlayerModifierTypeOptions(options: ModifierTypeOption[], } if (modifierType) { - options[i].type = modifierType.withIdFromFunc(modifierFunc).withTierFromPool(); + options[i].type = modifierType.withIdFromFunc(modifierFunc).withTierFromPool(ModifierPoolType.PLAYER, party); } } } diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index 36f94b99b20..d34804ba040 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -1767,10 +1767,10 @@ export class HitHealModifier extends PokemonHeldItemModifier { * @returns `true` if the {@linkcode Pokemon} was healed */ override apply(pokemon: Pokemon): boolean { - if (pokemon.turnData.damageDealt && !pokemon.isFullHp()) { + if (pokemon.turnData.totalDamageDealt && !pokemon.isFullHp()) { const scene = pokemon.scene; scene.unshiftPhase(new PokemonHealPhase(scene, pokemon.getBattlerIndex(), - toDmgValue(pokemon.turnData.damageDealt / 8) * this.stackCount, i18next.t("modifier:hitHealApply", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), typeName: this.type.name }), true)); + toDmgValue(pokemon.turnData.totalDamageDealt / 8) * this.stackCount, i18next.t("modifier:hitHealApply", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), typeName: this.type.name }), true)); } return true; diff --git a/src/phases/attempt-run-phase.ts b/src/phases/attempt-run-phase.ts index e0dd7fa72fd..b4768dc9a26 100644 --- a/src/phases/attempt-run-phase.ts +++ b/src/phases/attempt-run-phase.ts @@ -10,6 +10,10 @@ import { NewBattlePhase } from "./new-battle-phase"; import { PokemonPhase } from "./pokemon-phase"; export class AttemptRunPhase extends PokemonPhase { + + /** For testing purposes: this is to force the pokemon to fail and escape */ + public forceFailEscape = false; + constructor(scene: BattleScene, fieldIndex: number) { super(scene, fieldIndex); } @@ -28,7 +32,7 @@ export class AttemptRunPhase extends PokemonPhase { applyAbAttrs(RunSuccessAbAttr, playerPokemon, null, false, escapeChance); - if (playerPokemon.randSeedInt(100) < escapeChance.value) { + if (playerPokemon.randSeedInt(100) < escapeChance.value && !this.forceFailEscape) { this.scene.playSound("se/flee"); this.scene.queueMessage(i18next.t("battle:runAwaySuccess"), null, true, 500); @@ -51,6 +55,7 @@ export class AttemptRunPhase extends PokemonPhase { this.scene.pushPhase(new BattleEndPhase(this.scene)); this.scene.pushPhase(new NewBattlePhase(this.scene)); } else { + playerPokemon.turnData.failedRunAway = true; this.scene.queueMessage(i18next.t("battle:runAwayCannotEscape"), null, true, 500); } diff --git a/src/phases/check-switch-phase.ts b/src/phases/check-switch-phase.ts index 8849d304435..5e459d0e6b5 100644 --- a/src/phases/check-switch-phase.ts +++ b/src/phases/check-switch-phase.ts @@ -51,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, SwitchType.SWITCH, this.fieldIndex, false, true)); + this.scene.unshiftPhase(new SwitchPhase(this.scene, SwitchType.INITIAL_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 eee1fd52938..8eb84beded5 100644 --- a/src/phases/faint-phase.ts +++ b/src/phases/faint-phase.ts @@ -65,6 +65,15 @@ export class FaintPhase extends PokemonPhase { } } + /** In case the current pokemon was just switched in, make sure it is counted as participating in the combat */ + this.scene.getPlayerField().forEach((pokemon, i) => { + if (pokemon?.isActive(true)) { + if (pokemon.isPlayer()) { + this.scene.currentBattle.addParticipant(pokemon as PlayerPokemon); + } + } + }); + if (!this.tryOverrideForBattleSpec()) { this.doFaint(); } diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index 2b898f7d66b..8b4b462380c 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -1,20 +1,62 @@ -import BattleScene from "#app/battle-scene"; import { BattlerIndex } from "#app/battle"; -import { applyPreAttackAbAttrs, AddSecondStrikeAbAttr, IgnoreMoveEffectsAbAttr, applyPostDefendAbAttrs, PostDefendAbAttr, applyPostAttackAbAttrs, PostAttackAbAttr, MaxMultiHitAbAttr, AlwaysHitAbAttr, TypeImmunityAbAttr } from "#app/data/ability"; +import BattleScene from "#app/battle-scene"; +import { + AddSecondStrikeAbAttr, + AlwaysHitAbAttr, + applyPostAttackAbAttrs, + applyPostDefendAbAttrs, + applyPreAttackAbAttrs, + IgnoreMoveEffectsAbAttr, + MaxMultiHitAbAttr, + PostAttackAbAttr, + PostDefendAbAttr, + TypeImmunityAbAttr, +} from "#app/data/ability"; import { ArenaTagSide, ConditionalProtectTag } from "#app/data/arena-tag"; import { MoveAnim } from "#app/data/battle-anims"; -import { BattlerTagLapseType, DamageProtectedTag, ProtectedTag, SemiInvulnerableTag, SubstituteTag } from "#app/data/battler-tags"; -import { MoveTarget, applyMoveAttrs, OverrideMoveEffectAttr, MultiHitAttr, AttackMove, FixedDamageAttr, VariableTargetAttr, MissEffectAttr, MoveFlags, applyFilteredMoveAttrs, MoveAttr, MoveEffectAttr, OneHitKOAttr, MoveEffectTrigger, MoveCategory, NoEffectAttr, HitsTagAttr, ToxicAccuracyAttr } from "#app/data/move"; +import { + BattlerTagLapseType, + DamageProtectedTag, + ProtectedTag, + SemiInvulnerableTag, + SubstituteTag, +} from "#app/data/battler-tags"; +import { + applyFilteredMoveAttrs, + applyMoveAttrs, + AttackMove, + FixedDamageAttr, + HitsTagAttr, + MissEffectAttr, + MoveAttr, + MoveCategory, + MoveEffectAttr, + MoveEffectTrigger, + MoveFlags, + MoveTarget, + MultiHitAttr, + NoEffectAttr, + OneHitKOAttr, + OverrideMoveEffectAttr, + ToxicAccuracyAttr, + VariableTargetAttr, +} from "#app/data/move"; import { SpeciesFormChangePostMoveTrigger } from "#app/data/pokemon-forms"; -import { BattlerTagType } from "#app/enums/battler-tag-type"; -import { Moves } from "#app/enums/moves"; -import Pokemon, { PokemonMove, MoveResult, HitResult } from "#app/field/pokemon"; -import { getPokemonNameWithAffix } from "#app/messages"; -import { PokemonMultiHitModifier, FlinchChanceModifier, EnemyAttackStatusEffectChanceModifier, ContactHeldItemTransferChanceModifier, HitHealModifier } from "#app/modifier/modifier"; -import i18next from "i18next"; -import * as Utils from "#app/utils"; -import { PokemonPhase } from "./pokemon-phase"; import { Type } from "#app/data/type"; +import Pokemon, { HitResult, MoveResult, PokemonMove } from "#app/field/pokemon"; +import { getPokemonNameWithAffix } from "#app/messages"; +import { + ContactHeldItemTransferChanceModifier, + EnemyAttackStatusEffectChanceModifier, + FlinchChanceModifier, + HitHealModifier, + PokemonMultiHitModifier, +} from "#app/modifier/modifier"; +import { BooleanHolder, executeIf, NumberHolder } from "#app/utils"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { Moves } from "#enums/moves"; +import i18next from "i18next"; +import { PokemonPhase } from "./pokemon-phase"; export class MoveEffectPhase extends PokemonPhase { public move: PokemonMove; @@ -35,7 +77,7 @@ export class MoveEffectPhase extends PokemonPhase { this.targets = targets; } - start() { + public override start(): void { super.start(); /** The Pokemon using this phase's invoked move */ @@ -52,12 +94,12 @@ export class MoveEffectPhase extends PokemonPhase { * Does an effect from this move override other effects on this turn? * e.g. Charging moves (Fly, etc.) on their first turn of use. */ - const overridden = new Utils.BooleanHolder(false); + const overridden = new BooleanHolder(false); /** The {@linkcode Move} object from {@linkcode allMoves} invoked by this phase */ const move = this.move.getMove(); // Assume single target for override - applyMoveAttrs(OverrideMoveEffectAttr, user, this.getTarget() ?? null, move, overridden, this.move.virtual).then(() => { + applyMoveAttrs(OverrideMoveEffectAttr, user, this.getFirstTarget() ?? null, move, overridden, this.move.virtual).then(() => { // If other effects were overriden, stop this phase before they can be applied if (overridden.value) { return this.end(); @@ -71,14 +113,14 @@ export class MoveEffectPhase extends PokemonPhase { * effects of the move itself, Parental Bond, and Multi-Lens to do so. */ if (user.turnData.hitsLeft === -1) { - const hitCount = new Utils.IntegerHolder(1); + const hitCount = new NumberHolder(1); // Assume single target for multi hit - applyMoveAttrs(MultiHitAttr, user, this.getTarget() ?? null, move, hitCount); + applyMoveAttrs(MultiHitAttr, user, this.getFirstTarget() ?? null, move, hitCount); // If Parental Bond is applicable, double the hit count - applyPreAttackAbAttrs(AddSecondStrikeAbAttr, user, null, move, false, targets.length, hitCount, new Utils.IntegerHolder(0)); + applyPreAttackAbAttrs(AddSecondStrikeAbAttr, user, null, move, false, targets.length, hitCount, new NumberHolder(0)); // If Multi-Lens is applicable, multiply the hit count by 1 + the number of Multi-Lenses held by the user if (move instanceof AttackMove && !move.hasAttr(FixedDamageAttr)) { - this.scene.applyModifiers(PokemonMultiHitModifier, user.isPlayer(), user, hitCount, new Utils.IntegerHolder(0)); + this.scene.applyModifiers(PokemonMultiHitModifier, user.isPlayer(), user, hitCount, new NumberHolder(0)); } // Set the user's relevant turnData fields to reflect the final hit count user.turnData.hitCount = hitCount.value; @@ -100,8 +142,9 @@ export class MoveEffectPhase extends PokemonPhase { const hasActiveTargets = targets.some(t => t.isActive(true)); /** Check if the target is immune via ability to the attacking move, and NOT in semi invulnerable state */ - const isImmune = targets[0].hasAbilityWithAttr(TypeImmunityAbAttr) && (targets[0].getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move)) - && !targets[0].getTag(SemiInvulnerableTag); + const isImmune = targets[0].hasAbilityWithAttr(TypeImmunityAbAttr) + && (targets[0].getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move)) + && !targets[0].getTag(SemiInvulnerableTag); /** * If no targets are left for the move to hit (FAIL), or the invoked move is single-target @@ -111,7 +154,7 @@ export class MoveEffectPhase extends PokemonPhase { if (!hasActiveTargets || (!move.hasAttr(VariableTargetAttr) && !move.isMultiTarget() && !targetHitChecks[this.targets[0]] && !targets[0].getTag(ProtectedTag) && !isImmune)) { this.stopMultiHit(); if (hasActiveTargets) { - this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: this.getTarget() ? getPokemonNameWithAffix(this.getTarget()!) : "" })); + this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: this.getFirstTarget() ? getPokemonNameWithAffix(this.getFirstTarget()!) : "" })); moveHistoryEntry.result = MoveResult.MISS; applyMoveAttrs(MissEffectAttr, user, null, move); } else { @@ -127,30 +170,40 @@ export class MoveEffectPhase extends PokemonPhase { const playOnEmptyField = this.scene.currentBattle?.mysteryEncounter?.hasBattleAnimationsWithoutTargets ?? false; // Move animation only needs one target - new MoveAnim(move.id as Moves, user, this.getTarget()!.getBattlerIndex()!, playOnEmptyField).play(this.scene, move.hitsSubstitute(user, this.getTarget()!), () => { + new MoveAnim(move.id as Moves, user, this.getFirstTarget()!.getBattlerIndex()!, playOnEmptyField).play(this.scene, move.hitsSubstitute(user, this.getFirstTarget()!), () => { /** Has the move successfully hit a target (for damage) yet? */ let hasHit: boolean = false; for (const target of targets) { + // Prevent ENEMY_SIDE targeted moves from occurring twice in double battles + if (move.moveTarget === MoveTarget.ENEMY_SIDE && target !== targets[targets.length - 1]) { + continue; + } /** The {@linkcode ArenaTagSide} to which the target belongs */ const targetSide = target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; /** Has the invoked move been cancelled by conditional protection (e.g Quick Guard)? */ - const hasConditionalProtectApplied = new Utils.BooleanHolder(false); + const hasConditionalProtectApplied = new BooleanHolder(false); /** Does the applied conditional protection bypass Protect-ignoring effects? */ - const bypassIgnoreProtect = new Utils.BooleanHolder(false); + const bypassIgnoreProtect = new BooleanHolder(false); /** If the move is not targeting a Pokemon on the user's side, try to apply conditional protection effects */ if (!this.move.getMove().isAllyTarget()) { this.scene.arena.applyTagsForSide(ConditionalProtectTag, targetSide, false, hasConditionalProtectApplied, user, target, move.id, bypassIgnoreProtect); } /** Is the target protected by Protect, etc. or a relevant conditional protection effect? */ - const isProtected = (bypassIgnoreProtect.value || !this.move.getMove().checkFlag(MoveFlags.IGNORE_PROTECT, user, target)) - && (hasConditionalProtectApplied.value || (!target.findTags(t => t instanceof DamageProtectedTag).length && target.findTags(t => t instanceof ProtectedTag).find(t => target.lapseTag(t.tagType))) - || (this.move.getMove().category !== MoveCategory.STATUS && target.findTags(t => t instanceof DamageProtectedTag).find(t => target.lapseTag(t.tagType)))); + const isProtected = ( + bypassIgnoreProtect.value + || !this.move.getMove().checkFlag(MoveFlags.IGNORE_PROTECT, user, target)) + && (hasConditionalProtectApplied.value + || (!target.findTags(t => t instanceof DamageProtectedTag).length + && target.findTags(t => t instanceof ProtectedTag).find(t => target.lapseTag(t.tagType))) + || (this.move.getMove().category !== MoveCategory.STATUS + && target.findTags(t => t instanceof DamageProtectedTag).find(t => target.lapseTag(t.tagType)))); /** Is the pokemon immune due to an ablility, and also not in a semi invulnerable state? */ - const isImmune = target.hasAbilityWithAttr(TypeImmunityAbAttr) && (target.getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move)) - && !target.getTag(SemiInvulnerableTag); + const isImmune = target.hasAbilityWithAttr(TypeImmunityAbAttr) + && (target.getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move)) + && !target.getTag(SemiInvulnerableTag); /** * If the move missed a target, stop all future hits against that target @@ -218,7 +271,7 @@ export class MoveEffectPhase extends PokemonPhase { } /** Does this phase represent the invoked move's last strike? */ - const lastHit = (user.turnData.hitsLeft === 1 || !this.getTarget()?.isActive()); + const lastHit = (user.turnData.hitsLeft === 1 || !this.getFirstTarget()?.isActive()); /** * If the user can change forms by using the invoked move, @@ -234,85 +287,48 @@ export class MoveEffectPhase extends PokemonPhase { * These are ordered by trigger type (see {@linkcode MoveEffectTrigger}), and each trigger * type requires different conditions to be met with respect to the move's hit result. */ - applyAttrs.push(new Promise(resolve => { - // Apply all effects with PRE_MOVE triggers (if the target isn't immune to the move) - applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.PRE_APPLY && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit) && hitResult !== HitResult.NO_EFFECT, - user, target, move).then(() => { - // All other effects require the move to not have failed or have been cancelled to trigger - if (hitResult !== HitResult.FAIL) { - /** - * If the invoked move's effects are meant to trigger during the move's "charge turn," - * ignore all effects after this point. - * Otherwise, apply all self-targeted POST_APPLY effects. - */ - applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_APPLY - && attr.selfTarget && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit), user, target, move).then(() => { - // All effects past this point require the move to have hit the target - if (hitResult !== HitResult.NO_EFFECT) { - // Apply all non-self-targeted POST_APPLY effects - applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.POST_APPLY - && !(attr as MoveEffectAttr).selfTarget && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit), user, target, this.move.getMove()).then(() => { - /** - * If the move hit, and the target doesn't have Shield Dust, - * apply the chance to flinch the target gained from King's Rock - */ - if (dealsDamage && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && !move.hitsSubstitute(user, target)) { - const flinched = new Utils.BooleanHolder(false); - user.scene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched); - if (flinched.value) { - target.addTag(BattlerTagType.FLINCHED, undefined, this.move.moveId, user.id); - } - } - // If the move was not protected against, apply all HIT effects - Utils.executeIf(!isProtected, () => applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.HIT - && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit) && (!attr.firstTargetOnly || firstTarget), user, target, this.move.getMove()).then(() => { - // Apply the target's post-defend ability effects (as long as the target is active or can otherwise apply them) - return Utils.executeIf(!target.isFainted() || target.canApplyAbility(), () => applyPostDefendAbAttrs(PostDefendAbAttr, target, user, this.move.getMove(), hitResult).then(() => { - // Only apply the following effects if the move was not deflected by a substitute - if (move.hitsSubstitute(user, target)) { - return resolve(); - } + const k = new Promise((resolve) => { + //Start promise chain and apply PRE_APPLY move attributes + let promiseChain: Promise = applyFilteredMoveAttrs((attr: MoveAttr) => + attr instanceof MoveEffectAttr + && attr.trigger === MoveEffectTrigger.PRE_APPLY + && (!attr.firstHitOnly || firstHit) + && (!attr.lastHitOnly || lastHit) + && hitResult !== HitResult.NO_EFFECT, user, target, move); - // If the invoked move is an enemy attack, apply the enemy's status effect-inflicting tokens - if (!user.isPlayer() && this.move.getMove() instanceof AttackMove) { - user.scene.applyShuffledModifiers(this.scene, EnemyAttackStatusEffectChanceModifier, false, target); - } - target.lapseTags(BattlerTagLapseType.AFTER_HIT); + /** Don't complete if the move failed */ + if (hitResult === HitResult.FAIL) { + return resolve(); + } - })).then(() => { - // Apply the user's post-attack ability effects - applyPostAttackAbAttrs(PostAttackAbAttr, user, target, this.move.getMove(), hitResult).then(() => { - /** - * If the invoked move is an attack, apply the user's chance to - * steal an item from the target granted by Grip Claw - */ - if (this.move.getMove() instanceof AttackMove) { - this.scene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target); - } - resolve(); - }); - }); - }) - ).then(() => resolve()); - }); - } else { - applyMoveAttrs(NoEffectAttr, user, null, move).then(() => resolve()); - } - }); - } else { - resolve(); - } - }); - })); + /** Apply Move/Ability Effects in correct order */ + promiseChain = promiseChain + .then(this.applySelfTargetEffects(user, target, firstHit, lastHit)); + + if (hitResult !== HitResult.NO_EFFECT) { + promiseChain + .then(this.applyPostApplyEffects(user, target, firstHit, lastHit)) + .then(this.applyHeldItemFlinchCheck(user, target, dealsDamage)) + .then(this.applySuccessfulAttackEffects(user, target, firstHit, lastHit, !!isProtected, hitResult, firstTarget)) + .then(() => resolve()); + } else { + promiseChain + .then(() => applyMoveAttrs(NoEffectAttr, user, null, move)) + .then(resolve); + } + }); + + applyAttrs.push(k); } + // Apply the move's POST_TARGET effects on the move's last hit, after all targeted effects have resolved - const postTarget = (user.turnData.hitsLeft === 1 || !this.getTarget()?.isActive()) ? + const postTarget = (user.turnData.hitsLeft === 1 || !this.getFirstTarget()?.isActive()) ? applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_TARGET, user, null, move) : null; - if (!!postTarget) { + if (postTarget) { if (applyAttrs.length) { // If there is a pending asynchronous move effect, do this after - applyAttrs[applyAttrs.length - 1]?.then(() => postTarget); + applyAttrs[applyAttrs.length - 1].then(() => postTarget); } else { // Otherwise, push a new asynchronous move effect applyAttrs.push(postTarget); } @@ -327,7 +343,7 @@ export class MoveEffectPhase extends PokemonPhase { */ targets.forEach(target => { const substitute = target.getTag(SubstituteTag); - if (!!substitute && substitute.hp <= 0) { + if (substitute && substitute.hp <= 0) { target.lapseTag(BattlerTagType.SUBSTITUTE); } }); @@ -337,7 +353,7 @@ export class MoveEffectPhase extends PokemonPhase { }); } - end() { + public override end(): void { const user = this.getUserPokemon(); /** * If this phase isn't for the invoked move's last strike, @@ -347,7 +363,7 @@ export class MoveEffectPhase extends PokemonPhase { * to the user. */ if (user) { - if (user.turnData.hitsLeft && --user.turnData.hitsLeft >= 1 && this.getTarget()?.isActive()) { + if (user.turnData.hitsLeft && --user.turnData.hitsLeft >= 1 && this.getFirstTarget()?.isActive()) { this.scene.unshiftPhase(this.getNewHitPhase()); } else { // Queue message for number of hits made by multi-move @@ -367,11 +383,135 @@ export class MoveEffectPhase extends PokemonPhase { } /** - * Resolves whether this phase's invoked move hits or misses the given target - * @param target {@linkcode Pokemon} the Pokemon targeted by the invoked move - * @returns `true` if the move does not miss the target; `false` otherwise - */ - hitCheck(target: Pokemon): boolean { + * Apply self-targeted effects that trigger `POST_APPLY` + * + * @param user - The {@linkcode Pokemon} using this phase's invoked move + * @param target - {@linkcode Pokemon} the current target of this phase's invoked move + * @param firstHit - `true` if this is the first hit in a multi-hit attack + * @param lastHit - `true` if this is the last hit in a multi-hit attack + * @returns a function intended to be passed into a `then()` call. + */ + protected applySelfTargetEffects(user: Pokemon, target: Pokemon, firstHit: boolean, lastHit: boolean): () => Promise { + return () => applyFilteredMoveAttrs((attr: MoveAttr) => + attr instanceof MoveEffectAttr + && attr.trigger === MoveEffectTrigger.POST_APPLY + && attr.selfTarget + && (!attr.firstHitOnly || firstHit) + && (!attr.lastHitOnly || lastHit), user, target, this.move.getMove()); + } + + /** + * Applies non-self-targeted effects that trigger `POST_APPLY` + * (i.e. Smelling Salts curing Paralysis, and the forced switch from U-Turn, Dragon Tail, etc) + * @param user - The {@linkcode Pokemon} using this phase's invoked move + * @param target - {@linkcode Pokemon} the current target of this phase's invoked move + * @param firstHit - `true` if this is the first hit in a multi-hit attack + * @param lastHit - `true` if this is the last hit in a multi-hit attack + * @returns a function intended to be passed into a `then()` call. + */ + protected applyPostApplyEffects(user: Pokemon, target: Pokemon, firstHit: boolean, lastHit: boolean): () => Promise { + return () => applyFilteredMoveAttrs((attr: MoveAttr) => + attr instanceof MoveEffectAttr + && attr.trigger === MoveEffectTrigger.POST_APPLY + && !attr.selfTarget + && (!attr.firstHitOnly || firstHit) + && (!attr.lastHitOnly || lastHit), user, target, this.move.getMove()); + } + + /** + * Applies effects that trigger on HIT + * (i.e. Final Gambit, Power-Up Punch, Drain Punch) + * @param user - The {@linkcode Pokemon} using this phase's invoked move + * @param target - {@linkcode Pokemon} the current target of this phase's invoked move + * @param firstHit - `true` if this is the first hit in a multi-hit attack + * @param lastHit - `true` if this is the last hit in a multi-hit attack + * @param firstTarget - `true` if {@linkcode target} is the first target hit by this strike of {@linkcode move} + * @returns a function intended to be passed into a `then()` call. + */ + protected applyOnHitEffects(user: Pokemon, target: Pokemon, firstHit : boolean, lastHit: boolean, firstTarget: boolean): Promise { + return applyFilteredMoveAttrs((attr: MoveAttr) => + attr instanceof MoveEffectAttr + && attr.trigger === MoveEffectTrigger.HIT + && (!attr.firstHitOnly || firstHit) + && (!attr.lastHitOnly || lastHit) + && (!attr.firstTargetOnly || firstTarget), user, target, this.move.getMove()); + } + + /** + * Applies reactive effects that occur when a Pokémon is hit. + * (i.e. Effect Spore, Disguise, Liquid Ooze, Beak Blast) + * @param user - The {@linkcode Pokemon} using this phase's invoked move + * @param target - {@linkcode Pokemon} the current target of this phase's invoked move + * @param hitResult - The {@linkcode HitResult} of the attempted move + * @returns a `Promise` intended to be passed into a `then()` call. + */ + protected applyOnGetHitAbEffects(user: Pokemon, target: Pokemon, hitResult: HitResult): Promise { + return executeIf(!target.isFainted() || target.canApplyAbility(), () => + applyPostDefendAbAttrs(PostDefendAbAttr, target, user, this.move.getMove(), hitResult) + .then(() => { + + if (!this.move.getMove().hitsSubstitute(user, target)) { + if (!user.isPlayer() && this.move.getMove() instanceof AttackMove) { + user.scene.applyShuffledModifiers(this.scene, EnemyAttackStatusEffectChanceModifier, false, target); + } + + target.lapseTags(BattlerTagLapseType.AFTER_HIT); + } + + }) + ); + } + + /** + * Applies all effects and attributes that require a move to connect with a target, + * namely reactive effects like Weak Armor, on-hit effects like that of Power-Up Punch, and item stealing effects + * @param user - The {@linkcode Pokemon} using this phase's invoked move + * @param target - {@linkcode Pokemon} the current target of this phase's invoked move + * @param firstHit - `true` if this is the first hit in a multi-hit attack + * @param lastHit - `true` if this is the last hit in a multi-hit attack + * @param isProtected - `true` if the target is protected by effects such as Protect + * @param hitResult - The {@linkcode HitResult} of the attempted move + * @param firstTarget - `true` if {@linkcode target} is the first target hit by this strike of {@linkcode move} + * @returns a function intended to be passed into a `then()` call. + */ + protected applySuccessfulAttackEffects(user: Pokemon, target: Pokemon, firstHit : boolean, lastHit: boolean, isProtected : boolean, hitResult: HitResult, firstTarget: boolean) : () => Promise { + return () => executeIf(!isProtected, () => + this.applyOnHitEffects(user, target, firstHit, lastHit, firstTarget).then(() => + this.applyOnGetHitAbEffects(user, target, hitResult)).then(() => + applyPostAttackAbAttrs(PostAttackAbAttr, user, target, this.move.getMove(), hitResult)).then(() => { // Item Stealing Effects + + if (this.move.getMove() instanceof AttackMove) { + this.scene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target); + } + }) + ); + } + + /** + * Handles checking for and applying Flinches + * @param user - The {@linkcode Pokemon} using this phase's invoked move + * @param target - {@linkcode Pokemon} the current target of this phase's invoked move + * @param dealsDamage - `true` if the attempted move successfully dealt damage + * @returns a function intended to be passed into a `then()` call. + */ + protected applyHeldItemFlinchCheck(user: Pokemon, target: Pokemon, dealsDamage: boolean) : () => void { + return () => { + if (dealsDamage && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && !this.move.getMove().hitsSubstitute(user, target)) { + const flinched = new BooleanHolder(false); + user.scene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched); + if (flinched.value) { + target.addTag(BattlerTagType.FLINCHED, undefined, this.move.moveId, user.id); + } + } + }; + } + + /** + * Resolves whether this phase's invoked move hits the given target + * @param target - The {@linkcode Pokemon} targeted by the invoked move + * @returns `true` if the move hits the target + */ + public hitCheck(target: Pokemon): boolean { // Moves targeting the user and entry hazards can't miss if ([ MoveTarget.USER, MoveTarget.ENEMY_SIDE ].includes(this.move.getMove().moveTarget)) { return true; @@ -425,29 +565,29 @@ export class MoveEffectPhase extends PokemonPhase { return rand < (moveAccuracy * accuracyMultiplier); } - /** Returns the {@linkcode Pokemon} using this phase's invoked move */ - getUserPokemon(): Pokemon | undefined { + /** @returns The {@linkcode Pokemon} using this phase's invoked move */ + public getUserPokemon(): Pokemon | undefined { if (this.battlerIndex > BattlerIndex.ENEMY_2) { return this.scene.getPokemonById(this.battlerIndex) ?? undefined; } return (this.player ? this.scene.getPlayerField() : this.scene.getEnemyField())[this.fieldIndex]; } - /** Returns an array of all {@linkcode Pokemon} targeted by this phase's invoked move */ - getTargets(): Pokemon[] { + /** @returns An array of all {@linkcode Pokemon} targeted by this phase's invoked move */ + public getTargets(): Pokemon[] { return this.scene.getField(true).filter(p => this.targets.indexOf(p.getBattlerIndex()) > -1); } - /** Returns the first target of this phase's invoked move */ - getTarget(): Pokemon | undefined { + /** @returns The first target of this phase's invoked move */ + public getFirstTarget(): Pokemon | undefined { return this.getTargets()[0]; } /** * Removes the given {@linkcode Pokemon} from this phase's target list - * @param target {@linkcode Pokemon} the Pokemon to be removed + * @param target - The {@linkcode Pokemon} to be removed */ - removeTarget(target: Pokemon): void { + protected removeTarget(target: Pokemon): void { const targetIndex = this.targets.findIndex(ind => ind === target.getBattlerIndex()); if (targetIndex !== -1) { this.targets.splice(this.targets.findIndex(ind => ind === target.getBattlerIndex()), 1); @@ -459,23 +599,25 @@ export class MoveEffectPhase extends PokemonPhase { * @param target {@linkcode Pokemon} if defined, only stop subsequent * strikes against this Pokemon */ - stopMultiHit(target?: Pokemon): void { - /** If given a specific target, remove the target from subsequent strikes */ + public stopMultiHit(target?: Pokemon): void { + // If given a specific target, remove the target from subsequent strikes if (target) { this.removeTarget(target); } - /** - * If no target specified, or the specified target was the last of this move's - * targets, completely cancel all subsequent strikes. - */ + const user = this.getUserPokemon(); + if (!user) { + return; + } + // If no target specified, or the specified target was the last of this move's + // targets, completely cancel all subsequent strikes. if (!target || this.targets.length === 0 ) { - this.getUserPokemon()!.turnData.hitCount = 1; // TODO: is the bang correct here? - this.getUserPokemon()!.turnData.hitsLeft = 1; // TODO: is the bang correct here? + user.turnData.hitCount = 1; + user.turnData.hitsLeft = 1; } } - /** Returns a new MoveEffectPhase with the same properties as this phase */ - getNewHitPhase() { + /** @returns A new `MoveEffectPhase` with the same properties as this phase */ + protected getNewHitPhase(): MoveEffectPhase { return new MoveEffectPhase(this.scene, this.battlerIndex, this.targets, this.move); } } diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index a516fd8593d..5c6c339ffa5 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -3,7 +3,7 @@ import BattleScene from "#app/battle-scene"; import { applyAbAttrs, applyPostMoveUsedAbAttrs, applyPreAttackAbAttrs, BlockRedirectAbAttr, IncreasePpAbAttr, PokemonTypeChangeAbAttr, PostMoveUsedAbAttr, RedirectMoveAbAttr, ReduceStatusEffectDurationAbAttr } from "#app/data/ability"; import { CommonAnim } from "#app/data/battle-anims"; import { BattlerTagLapseType, CenterOfAttentionTag } from "#app/data/battler-tags"; -import { allMoves, applyMoveAttrs, BypassRedirectAttr, BypassSleepAttr, CopyMoveAttr, HealStatusEffectAttr, MoveFlags, PreMoveMessageAttr } from "#app/data/move"; +import { allMoves, applyMoveAttrs, BypassRedirectAttr, BypassSleepAttr, CopyMoveAttr, frenzyMissFunc, HealStatusEffectAttr, MoveFlags, PreMoveMessageAttr } from "#app/data/move"; import { SpeciesFormChangePreMoveTrigger } from "#app/data/pokemon-forms"; import { getStatusEffectActivationText, getStatusEffectHealText } from "#app/data/status-effect"; import { Type } from "#app/data/type"; @@ -470,6 +470,10 @@ export class MovePhase extends BattlePhase { this.scene.eventTarget.dispatchEvent(new MoveUsedEvent(this.pokemon?.id, this.move.getMove(), ppUsed)); } + if (this.cancelled && this.pokemon.summonData?.tags?.find(t => t.tagType === BattlerTagType.FRENZY)) { + frenzyMissFunc(this.pokemon, this.move.getMove()); + } + this.pokemon.pushMoveHistory({ move: Moves.NONE, result: MoveResult.FAIL }); this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT); diff --git a/src/phases/post-turn-status-effect-phase.ts b/src/phases/post-turn-status-effect-phase.ts index 2efd992a2b5..08e4d7cb952 100644 --- a/src/phases/post-turn-status-effect-phase.ts +++ b/src/phases/post-turn-status-effect-phase.ts @@ -1,6 +1,6 @@ import BattleScene from "#app/battle-scene"; import { BattlerIndex } from "#app/battle"; -import { applyAbAttrs, BlockNonDirectDamageAbAttr, BlockStatusDamageAbAttr, ReduceBurnDamageAbAttr } from "#app/data/ability"; +import { applyAbAttrs, applyPostDamageAbAttrs, BlockNonDirectDamageAbAttr, BlockStatusDamageAbAttr, PostDamageAbAttr, ReduceBurnDamageAbAttr } from "#app/data/ability"; import { CommonBattleAnim, CommonAnim } from "#app/data/battle-anims"; import { getStatusEffectActivationText } from "#app/data/status-effect"; import { BattleSpec } from "#app/enums/battle-spec"; @@ -41,6 +41,7 @@ export class PostTurnStatusEffectPhase extends PokemonPhase { // Set preventEndure flag to avoid pokemon surviving thanks to focus band, sturdy, endure ... this.scene.damageNumberHandler.add(this.getPokemon(), pokemon.damage(damage.value, false, true)); pokemon.updateInfo(); + applyPostDamageAbAttrs(PostDamageAbAttr, pokemon, damage.value, pokemon.hasPassive(), false, []); } new CommonBattleAnim(CommonAnim.POISON + (pokemon.status.effect - 1), pokemon).play(this.scene, false, () => this.end()); } else { diff --git a/src/phases/stat-stage-change-phase.ts b/src/phases/stat-stage-change-phase.ts index 2d4b3ce6c6f..ce6ebea2442 100644 --- a/src/phases/stat-stage-change-phase.ts +++ b/src/phases/stat-stage-change-phase.ts @@ -65,7 +65,7 @@ export class StatStageChangePhase extends PokemonPhase { if (!this.selfTarget && stages.value < 0) { // TODO: add a reference to the source of the stat change to fix Infiltrator interaction - this.scene.arena.applyTagsForSide(MistTag, pokemon.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY, false, null, false, cancelled); + this.scene.arena.applyTagsForSide(MistTag, pokemon.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY, false, null, cancelled); } if (!cancelled.value && !this.selfTarget && stages.value < 0) { diff --git a/src/phases/switch-summon-phase.ts b/src/phases/switch-summon-phase.ts index 37652b3cfa4..c7e7bbe011e 100644 --- a/src/phases/switch-summon-phase.ts +++ b/src/phases/switch-summon-phase.ts @@ -64,10 +64,8 @@ export class SwitchSummonPhase extends SummonPhase { } const pokemon = this.getPokemon(); - (this.player ? this.scene.getEnemyField() : this.scene.getPlayerField()).forEach(enemyPokemon => enemyPokemon.removeTagsBySourceId(pokemon.id)); - - if (this.switchType === SwitchType.SWITCH) { + if (this.switchType === SwitchType.SWITCH || this.switchType === SwitchType.INITIAL_SWITCH) { const substitute = pokemon.getTag(SubstituteTag); if (substitute) { this.scene.tweens.add({ @@ -186,6 +184,11 @@ export class SwitchSummonPhase extends SummonPhase { } } + if (this.switchType !== SwitchType.INITIAL_SWITCH) { + pokemon.resetTurnData(); + pokemon.turnData.switchedInThisTurn = true; + } + this.lastPokemon?.resetSummonData(); this.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true); diff --git a/src/phases/turn-start-phase.ts b/src/phases/turn-start-phase.ts index 497d449912f..dc3ee3f660a 100644 --- a/src/phases/turn-start-phase.ts +++ b/src/phases/turn-start-phase.ts @@ -205,11 +205,11 @@ export class TurnStartPhase extends FieldPhase { } this.scene.pushPhase(new WeatherEffectPhase(this.scene)); + this.scene.pushPhase(new BerryPhase(this.scene)); /** Add a new phase to check who should be taking status damage */ this.scene.pushPhase(new CheckStatusEffectPhase(this.scene, moveOrder)); - this.scene.pushPhase(new BerryPhase(this.scene)); this.scene.pushPhase(new TurnEndPhase(this.scene)); /** diff --git a/src/plugins/i18n.ts b/src/plugins/i18n.ts index f1aaa823a10..bb9786f5509 100644 --- a/src/plugins/i18n.ts +++ b/src/plugins/i18n.ts @@ -153,7 +153,7 @@ export async function initI18n(): Promise { i18next.use(new KoreanPostpositionProcessor()); await i18next.init({ fallbackLng: "en", - supportedLngs: [ "en", "es", "fr", "it", "de", "zh-CN", "zh-TW", "pt-BR", "ko", "ja", "ca-ES", "da" ], + supportedLngs: [ "en", "es-ES", "fr", "it", "de", "zh-CN", "zh-TW", "pt-BR", "ko", "ja", "ca-ES", "da" ], backend: { loadPath(lng: string, [ ns ]: string[]) { let fileName: string; @@ -164,7 +164,7 @@ export async function initI18n(): Promise { } else { fileName = camelCaseToKebabCase(ns); } - return `/locales/${lng}/${fileName}.json?v=${pkg.version}`; + return `./locales/${lng}/${fileName}.json?v=${pkg.version}`; }, }, defaultNS: "menu", diff --git a/src/system/settings/settings.ts b/src/system/settings/settings.ts index 6efeed21e3d..560bdf7b0e0 100644 --- a/src/system/settings/settings.ts +++ b/src/system/settings/settings.ts @@ -866,8 +866,8 @@ export function setSetting(scene: BattleScene, setting: string, value: integer): handler: () => changeLocaleHandler("en") }, { - label: "Español", - handler: () => changeLocaleHandler("es") + label: "Español (ES)", + handler: () => changeLocaleHandler("es-ES") }, { label: "Italiano", diff --git a/src/test/abilities/friend_guard.test.ts b/src/test/abilities/friend_guard.test.ts new file mode 100644 index 00000000000..4ce64468c43 --- /dev/null +++ b/src/test/abilities/friend_guard.test.ts @@ -0,0 +1,120 @@ +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, vi } from "vitest"; +import { BattlerIndex } from "#app/battle"; +import { allAbilities } from "#app/data/ability"; +import { allMoves, MoveCategory } from "#app/data/move"; + +describe("Moves - Friend Guard", () => { + 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.TACKLE, Moves.SPLASH, Moves.DRAGON_RAGE ]) + .enemySpecies(Species.SHUCKLE) + .moveset([ Moves.SPLASH ]) + .startingLevel(100); + }); + + it("should reduce damage that other allied Pokémon receive from attacks (from any Pokémon) by 25%", async () => { + await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER ]); + const [ player1, player2 ] = game.scene.getPlayerField(); + const spy = vi.spyOn(player1, "getAttackDamage"); + + const enemy1 = game.scene.getEnemyField()[0]; + + game.move.select(Moves.SPLASH); + game.move.select(Moves.SPLASH, 1); + await game.forceEnemyMove(Moves.TACKLE, BattlerIndex.PLAYER); + await game.forceEnemyMove(Moves.SPLASH); + await game.toNextTurn(); + + // Get the last return value from `getAttackDamage` + const turn1Damage = spy.mock.results[spy.mock.results.length - 1].value.damage; + // Making sure the test is controlled; turn 1 damage is equal to base damage (after rounding) + expect(turn1Damage).toBe(Math.floor(player1.getBaseDamage(enemy1, allMoves[Moves.TACKLE], MoveCategory.PHYSICAL))); + + vi.spyOn(player2, "getAbility").mockReturnValue(allAbilities[Abilities.FRIEND_GUARD]); + + game.move.select(Moves.SPLASH); + game.move.select(Moves.SPLASH, 1); + await game.forceEnemyMove(Moves.TACKLE, BattlerIndex.PLAYER); + await game.forceEnemyMove(Moves.SPLASH); + await game.toNextTurn(); + + // Get the last return value from `getAttackDamage` + const turn2Damage = spy.mock.results[spy.mock.results.length - 1].value.damage; + // With the ally's Friend Guard, damage should have been reduced from base damage by 25% + expect(turn2Damage).toBe(Math.floor(player1.getBaseDamage(enemy1, allMoves[Moves.TACKLE], MoveCategory.PHYSICAL) * 0.75)); + }); + + it("should NOT reduce damage to pokemon with friend guard", async () => { + await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER ]); + + const player2 = game.scene.getPlayerField()[1]; + const spy = vi.spyOn(player2, "getAttackDamage"); + + game.move.select(Moves.SPLASH); + game.move.select(Moves.SPLASH, 1); + await game.forceEnemyMove(Moves.TACKLE, BattlerIndex.PLAYER_2); + await game.forceEnemyMove(Moves.SPLASH); + await game.toNextTurn(); + + const turn1Damage = spy.mock.results[spy.mock.results.length - 1].value.damage; + + vi.spyOn(player2, "getAbility").mockReturnValue(allAbilities[Abilities.FRIEND_GUARD]); + + game.move.select(Moves.SPLASH); + game.move.select(Moves.SPLASH, 1); + await game.forceEnemyMove(Moves.TACKLE, BattlerIndex.PLAYER_2); + await game.forceEnemyMove(Moves.SPLASH); + await game.toNextTurn(); + + const turn2Damage = spy.mock.results[spy.mock.results.length - 1].value.damage; + expect(turn2Damage).toBe(turn1Damage); + }); + + it("should NOT reduce damage from fixed damage attacks", async () => { + await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER ]); + + const [ player1, player2 ] = game.scene.getPlayerField(); + const spy = vi.spyOn(player1, "getAttackDamage"); + + game.move.select(Moves.SPLASH); + game.move.select(Moves.SPLASH, 1); + await game.forceEnemyMove(Moves.DRAGON_RAGE, BattlerIndex.PLAYER); + await game.forceEnemyMove(Moves.SPLASH); + await game.toNextTurn(); + + const turn1Damage = spy.mock.results[spy.mock.results.length - 1].value.damage; + expect(turn1Damage).toBe(40); + + vi.spyOn(player2, "getAbility").mockReturnValue(allAbilities[Abilities.FRIEND_GUARD]); + + game.move.select(Moves.SPLASH); + game.move.select(Moves.SPLASH, 1); + await game.forceEnemyMove(Moves.DRAGON_RAGE, BattlerIndex.PLAYER); + await game.forceEnemyMove(Moves.SPLASH); + await game.toNextTurn(); + + const turn2Damage = spy.mock.results[spy.mock.results.length - 1].value.damage; + expect(turn2Damage).toBe(40); + }); +}); diff --git a/src/test/abilities/mimicry.test.ts b/src/test/abilities/mimicry.test.ts new file mode 100644 index 00000000000..4e62ddc0b76 --- /dev/null +++ b/src/test/abilities/mimicry.test.ts @@ -0,0 +1,91 @@ +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { Type } from "#app/data/type"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Abilities - Mimicry", () => { + 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.SPLASH ]) + .ability(Abilities.MIMICRY) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyMoveset(Moves.SPLASH); + }); + + it("Mimicry activates after the Pokémon with Mimicry is switched in while terrain is present, or whenever there is a change in terrain", async () => { + game.override.enemyAbility(Abilities.MISTY_SURGE); + await game.classicMode.startBattle([ Species.FEEBAS, Species.ABRA ]); + + const [ playerPokemon1, playerPokemon2 ] = game.scene.getParty(); + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + expect(playerPokemon1.getTypes().includes(Type.FAIRY)).toBe(true); + + game.doSwitchPokemon(1); + await game.toNextTurn(); + + expect(playerPokemon2.getTypes().includes(Type.FAIRY)).toBe(true); + }); + + it("Pokemon should revert back to its original, root type once terrain ends", async () => { + game.override + .moveset([ Moves.SPLASH, Moves.TRANSFORM ]) + .enemyAbility(Abilities.MIMICRY) + .enemyMoveset([ Moves.SPLASH, Moves.PSYCHIC_TERRAIN ]); + await game.classicMode.startBattle([ Species.REGIELEKI ]); + + const playerPokemon = game.scene.getPlayerPokemon(); + game.move.select(Moves.TRANSFORM); + await game.forceEnemyMove(Moves.PSYCHIC_TERRAIN); + await game.toNextTurn(); + expect(playerPokemon?.getTypes().includes(Type.PSYCHIC)).toBe(true); + + if (game.scene.arena.terrain) { + game.scene.arena.terrain.turnsLeft = 1; + } + + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.SPLASH); + await game.toNextTurn(); + expect(playerPokemon?.getTypes().includes(Type.ELECTRIC)).toBe(true); + }); + + it("If the Pokemon is under the effect of a type-adding move and an equivalent terrain activates, the move's effect disappears", async () => { + game.override + .enemyMoveset([ Moves.FORESTS_CURSE, Moves.GRASSY_TERRAIN ]); + await game.classicMode.startBattle([ Species.FEEBAS ]); + + const playerPokemon = game.scene.getPlayerPokemon(); + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.FORESTS_CURSE); + await game.toNextTurn(); + + expect(playerPokemon?.summonData.addedType).toBe(Type.GRASS); + + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.GRASSY_TERRAIN); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(playerPokemon?.summonData.addedType).toBeNull(); + expect(playerPokemon?.getTypes().includes(Type.GRASS)).toBe(true); + }); +}); diff --git a/src/test/abilities/serene_grace.test.ts b/src/test/abilities/serene_grace.test.ts index 3155594c81d..ddca87496e9 100644 --- a/src/test/abilities/serene_grace.test.ts +++ b/src/test/abilities/serene_grace.test.ts @@ -57,7 +57,7 @@ describe("Abilities - Serene Grace", () => { const chance = new Utils.IntegerHolder(move.chance); console.log(move.chance + " Their ability is " + phase.getUserPokemon()!.getAbility().name); - applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getTarget(), false); + applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false); expect(chance.value).toBe(30); }, 20000); @@ -83,7 +83,7 @@ describe("Abilities - Serene Grace", () => { expect(move.id).toBe(Moves.AIR_SLASH); const chance = new Utils.IntegerHolder(move.chance); - applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getTarget(), false); + applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false); expect(chance.value).toBe(60); }, 20000); diff --git a/src/test/abilities/sheer_force.test.ts b/src/test/abilities/sheer_force.test.ts index a2600476d6d..63c81e9aafc 100644 --- a/src/test/abilities/sheer_force.test.ts +++ b/src/test/abilities/sheer_force.test.ts @@ -60,8 +60,8 @@ describe("Abilities - Sheer Force", () => { const power = new Utils.IntegerHolder(move.power); const chance = new Utils.IntegerHolder(move.chance); - applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getTarget(), false); - applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getTarget()!, move, false, power); + applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false); + applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getFirstTarget()!, move, false, power); expect(chance.value).toBe(0); expect(power.value).toBe(move.power * 5461 / 4096); @@ -93,8 +93,8 @@ describe("Abilities - Sheer Force", () => { const power = new Utils.IntegerHolder(move.power); const chance = new Utils.IntegerHolder(move.chance); - applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getTarget(), false); - applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getTarget()!, move, false, power); + applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false); + applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getFirstTarget()!, move, false, power); expect(chance.value).toBe(-1); expect(power.value).toBe(move.power); @@ -126,8 +126,8 @@ describe("Abilities - Sheer Force", () => { const power = new Utils.IntegerHolder(move.power); const chance = new Utils.IntegerHolder(move.chance); - applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getTarget(), false); - applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getTarget()!, move, false, power); + applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false); + applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getFirstTarget()!, move, false, power); expect(chance.value).toBe(-1); expect(power.value).toBe(move.power); @@ -161,7 +161,7 @@ describe("Abilities - Sheer Force", () => { const power = new Utils.IntegerHolder(move.power); const chance = new Utils.IntegerHolder(move.chance); const user = phase.getUserPokemon()!; - const target = phase.getTarget()!; + const target = phase.getFirstTarget()!; const opponentType = target.getTypes()[0]; applyAbAttrs(MoveEffectChanceMultiplierAbAttr, user, null, false, chance, move, target, false); diff --git a/src/test/abilities/shield_dust.test.ts b/src/test/abilities/shield_dust.test.ts index 0f831fcf3fa..ccedf6873d7 100644 --- a/src/test/abilities/shield_dust.test.ts +++ b/src/test/abilities/shield_dust.test.ts @@ -57,8 +57,8 @@ describe("Abilities - Shield Dust", () => { expect(move.id).toBe(Moves.AIR_SLASH); const chance = new Utils.IntegerHolder(move.chance); - applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getTarget(), false); - applyPreDefendAbAttrs(IgnoreMoveEffectsAbAttr, phase.getTarget()!, phase.getUserPokemon()!, null, null, false, chance); + applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false); + applyPreDefendAbAttrs(IgnoreMoveEffectsAbAttr, phase.getFirstTarget()!, phase.getUserPokemon()!, null, null, false, chance); expect(chance.value).toBe(0); }, 20000); diff --git a/src/test/abilities/speed_boost.test.ts b/src/test/abilities/speed_boost.test.ts new file mode 100644 index 00000000000..dd2e83aaa88 --- /dev/null +++ b/src/test/abilities/speed_boost.test.ts @@ -0,0 +1,125 @@ +import { Stat } from "#enums/stat"; +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, expect, it } from "vitest"; +import { CommandPhase } from "#app/phases/command-phase"; +import { Command } from "#app/ui/command-ui-handler"; +import { AttemptRunPhase } from "#app/phases/attempt-run-phase"; + +describe("Abilities - Speed Boost", () => { + 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") + .enemySpecies(Species.DRAGAPULT) + .ability(Abilities.SPEED_BOOST) + .enemyMoveset(Moves.SPLASH) + .moveset([ Moves.SPLASH, Moves.U_TURN ]); + }); + + it("should increase speed by 1 stage at end of turn", + async () => { + await game.classicMode.startBattle(); + + const playerPokemon = game.scene.getPlayerPokemon()!; + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + + expect(playerPokemon.getStatStage(Stat.SPD)).toBe(1); + }); + + it("should not trigger this turn if pokemon was switched into combat via attack, but the turn after", + async () => { + await game.classicMode.startBattle([ + Species.SHUCKLE, + Species.NINJASK + ]); + + game.move.select(Moves.U_TURN); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + const playerPokemon = game.scene.getPlayerPokemon()!; + expect(playerPokemon.getStatStage(Stat.SPD)).toBe(0); + + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + expect(playerPokemon.getStatStage(Stat.SPD)).toBe(1); + }); + + it("checking back to back swtiches", + async () => { + await game.classicMode.startBattle([ + Species.SHUCKLE, + Species.NINJASK + ]); + + game.move.select(Moves.U_TURN); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + let playerPokemon = game.scene.getPlayerPokemon()!; + expect(playerPokemon.getStatStage(Stat.SPD)).toBe(0); + + game.move.select(Moves.U_TURN); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + playerPokemon = game.scene.getPlayerPokemon()!; + expect(playerPokemon.getStatStage(Stat.SPD)).toBe(0); + + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + expect(playerPokemon.getStatStage(Stat.SPD)).toBe(1); + }); + + it("should not trigger this turn if pokemon was switched into combat via normal switch, but the turn after", + async () => { + await game.classicMode.startBattle([ + Species.SHUCKLE, + Species.NINJASK + ]); + + game.doSwitchPokemon(1); + await game.toNextTurn(); + const playerPokemon = game.scene.getPlayerPokemon()!; + expect(playerPokemon.getStatStage(Stat.SPD)).toBe(0); + + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + expect(playerPokemon.getStatStage(Stat.SPD)).toBe(1); + }); + + it("should not trigger if pokemon fails to escape", + async () => { + await game.classicMode.startBattle([ Species.SHUCKLE ]); + + const commandPhase = game.scene.getCurrentPhase() as CommandPhase; + commandPhase.handleCommand(Command.RUN, 0); + const runPhase = game.scene.getCurrentPhase() as AttemptRunPhase; + runPhase.forceFailEscape = true; + await game.phaseInterceptor.to(AttemptRunPhase); + await game.toNextTurn(); + + const playerPokemon = game.scene.getPlayerPokemon()!; + expect(playerPokemon.getStatStage(Stat.SPD)).toBe(0); + + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + expect(playerPokemon.getStatStage(Stat.SPD)).toBe(1); + }); +}); diff --git a/src/test/abilities/wimp_out.test.ts b/src/test/abilities/wimp_out.test.ts new file mode 100644 index 00000000000..ee943befc16 --- /dev/null +++ b/src/test/abilities/wimp_out.test.ts @@ -0,0 +1,614 @@ +import { BattlerIndex } from "#app/battle"; +import { ArenaTagSide } from "#app/data/arena-tag"; +import { allMoves } from "#app/data/move"; +import GameManager from "#app/test/utils/gameManager"; +import { Abilities } from "#enums/abilities"; +import { ArenaTagType } from "#enums/arena-tag-type"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { Stat } from "#enums/stat"; +import { StatusEffect } from "#enums/status-effect"; +import { WeatherType } from "#enums/weather-type"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +describe("Abilities - Wimp Out", () => { + 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") + .ability(Abilities.WIMP_OUT) + .enemySpecies(Species.NINJASK) + .enemyPassiveAbility(Abilities.NO_GUARD) + .startingLevel(90) + .enemyLevel(70) + .moveset([ Moves.SPLASH, Moves.FALSE_SWIPE, Moves.ENDURE ]) + .enemyMoveset(Moves.FALSE_SWIPE) + .disableCrits(); + }); + + function confirmSwitch(): void { + const [ pokemon1, pokemon2 ] = game.scene.getParty(); + + expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase"); + + expect(pokemon1.species.speciesId).not.toBe(Species.WIMPOD); + + expect(pokemon2.species.speciesId).toBe(Species.WIMPOD); + expect(pokemon2.isFainted()).toBe(false); + expect(pokemon2.getHpRatio()).toBeLessThan(0.5); + } + + function confirmNoSwitch(): void { + const [ pokemon1, pokemon2 ] = game.scene.getParty(); + + expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase"); + + expect(pokemon2.species.speciesId).not.toBe(Species.WIMPOD); + + expect(pokemon1.species.speciesId).toBe(Species.WIMPOD); + expect(pokemon1.isFainted()).toBe(false); + expect(pokemon1.getHpRatio()).toBeLessThan(0.5); + } + + it("triggers regenerator passive single time when switching out with wimp out", async () => { + game.override + .passiveAbility(Abilities.REGENERATOR) + .startingLevel(5) + .enemyLevel(100); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + + const wimpod = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.SPLASH); + game.doSelectPartyPokemon(1); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(wimpod.hp).toEqual(Math.floor(wimpod.getMaxHp() * 0.33 + 1)); + confirmSwitch(); + }); + + it("It makes wild pokemon flee if triggered", async () => { + game.override.enemyAbility(Abilities.WIMP_OUT); + await game.classicMode.startBattle([ + Species.GOLISOPOD, + Species.TYRUNT + ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + enemyPokemon.hp *= 0.52; + + game.move.select(Moves.FALSE_SWIPE); + await game.phaseInterceptor.to("BerryPhase"); + + const isVisible = enemyPokemon.visible; + const hasFled = enemyPokemon.switchOutStatus; + expect(!isVisible && hasFled).toBe(true); + }); + + it("Does not trigger when HP already below half", async () => { + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + const wimpod = game.scene.getPlayerPokemon()!; + wimpod.hp = 5; + + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(wimpod.hp).toEqual(1); + confirmNoSwitch(); + }); + + it("Trapping moves do not prevent Wimp Out from activating.", async () => { + game.override + .enemyMoveset([ Moves.SPIRIT_SHACKLE ]) + .startingLevel(53) + .enemyLevel(45); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + + game.move.select(Moves.SPLASH); + game.doSelectPartyPokemon(1); + + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase"); + expect(game.scene.getPlayerPokemon()!.getTag(BattlerTagType.TRAPPED)).toBeUndefined(); + expect(game.scene.getParty()[1].getTag(BattlerTagType.TRAPPED)).toBeUndefined(); + confirmSwitch(); + }); + + it("If this Ability activates due to being hit by U-turn or Volt Switch, the user of that move will not be switched out.", async () => { + game.override + .startingLevel(95) + .enemyMoveset([ Moves.U_TURN ]); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + + game.move.select(Moves.SPLASH); + game.doSelectPartyPokemon(1); + await game.phaseInterceptor.to("TurnEndPhase"); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + const hasFled = enemyPokemon.switchOutStatus; + expect(hasFled).toBe(false); + confirmSwitch(); + }); + + it("If this Ability does not activate due to being hit by U-turn or Volt Switch, the user of that move will be switched out.", async () => { + game.override + .startingLevel(190) + .startingWave(8) + .enemyMoveset([ Moves.U_TURN ]); + await game.classicMode.startBattle([ + Species.GOLISOPOD, + Species.TYRUNT + ]); + const RIVAL_NINJASK1 = game.scene.getEnemyPokemon()?.id; + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("BerryPhase", false); + expect(game.scene.getEnemyPokemon()?.id !== RIVAL_NINJASK1); + }); + + it("Dragon Tail and Circle Throw switch out Pokémon before the Ability activates.", async () => { + game.override + .startingLevel(69) + .enemyMoveset([ Moves.DRAGON_TAIL ]); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + + const wimpod = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.SPLASH); + game.doSelectPartyPokemon(1); + await game.phaseInterceptor.to("SwitchSummonPhase", false); + + expect(wimpod.summonData.abilitiesApplied).not.toContain(Abilities.WIMP_OUT); + + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(game.scene.getPlayerPokemon()!.species.speciesId).not.toBe(Species.WIMPOD); + }); + + it("triggers when recoil damage is taken", async () => { + game.override + .moveset([ Moves.HEAD_SMASH ]) + .enemyMoveset([ Moves.SPLASH ]); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + + game.move.select(Moves.HEAD_SMASH); + game.doSelectPartyPokemon(1); + await game.phaseInterceptor.to("TurnEndPhase"); + + confirmSwitch(); + }); + + it("It does not activate when the Pokémon cuts its own HP", async () => { + game.override + .moveset([ Moves.SUBSTITUTE ]) + .enemyMoveset([ Moves.SPLASH ]); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + + const wimpod = game.scene.getPlayerPokemon()!; + wimpod.hp *= 0.52; + + game.move.select(Moves.SUBSTITUTE); + await game.phaseInterceptor.to("TurnEndPhase"); + + confirmNoSwitch(); + }); + + it("Does not trigger when neutralized", async () => { + game.override + .enemyAbility(Abilities.NEUTRALIZING_GAS) + .startingLevel(5); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("TurnEndPhase"); + + confirmNoSwitch(); + }); + + it("If it falls below half and recovers back above half from a Shell Bell, Wimp Out will activate even after the Shell Bell recovery", async () => { + game.override + .moveset([ Moves.DOUBLE_EDGE ]) + .enemyMoveset([ Moves.SPLASH ]) + .startingHeldItems([ + { name: "SHELL_BELL", count: 3 }, + { name: "HEALING_CHARM", count: 5 }, + ]); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + + game.scene.getPlayerPokemon()!.hp *= 0.75; + + game.move.select(Moves.DOUBLE_EDGE); + game.doSelectPartyPokemon(1); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(game.scene.getParty()[1].getHpRatio()).toBeGreaterThan(0.5); + expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase"); + expect(game.scene.getPlayerPokemon()!.species.speciesId).toBe(Species.TYRUNT); + }); + + it("Wimp Out will activate due to weather damage", async () => { + game.override + .weather(WeatherType.HAIL) + .enemyMoveset([ Moves.SPLASH ]); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + + game.scene.getPlayerPokemon()!.hp *= 0.51; + + game.move.select(Moves.SPLASH); + game.doSelectPartyPokemon(1); + await game.phaseInterceptor.to("TurnEndPhase"); + + confirmSwitch(); + }); + + it("Does not trigger when enemy has sheer force", async () => { + game.override + .enemyAbility(Abilities.SHEER_FORCE) + .enemyMoveset(Moves.SLUDGE_BOMB) + .startingLevel(95); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("TurnEndPhase"); + + confirmNoSwitch(); + }); + + it("Wimp Out will activate due to post turn status damage", async () => { + game.override + .statusEffect(StatusEffect.POISON) + .enemyMoveset([ Moves.SPLASH ]); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + + game.scene.getPlayerPokemon()!.hp *= 0.51; + + game.move.select(Moves.SPLASH); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + confirmSwitch(); + }); + + it("Wimp Out will activate due to bad dreams", async () => { + game.override + .statusEffect(StatusEffect.SLEEP) + .enemyAbility(Abilities.BAD_DREAMS); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + + game.scene.getPlayerPokemon()!.hp *= 0.52; + + game.move.select(Moves.SPLASH); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + confirmSwitch(); + }); + + it("Wimp Out will activate due to leech seed", async () => { + game.override + .enemyMoveset([ Moves.LEECH_SEED ]); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + game.scene.getPlayerPokemon()!.hp *= 0.52; + + game.move.select(Moves.SPLASH); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + confirmSwitch(); + }); + + it("Wimp Out will activate due to curse damage", async () => { + game.override + .enemySpecies(Species.DUSKNOIR) + .enemyMoveset([ Moves.CURSE ]); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + game.scene.getPlayerPokemon()!.hp *= 0.52; + + game.move.select(Moves.SPLASH); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + confirmSwitch(); + }); + + it("Wimp Out will activate due to salt cure damage", async () => { + game.override + .enemySpecies(Species.NACLI) + .enemyMoveset([ Moves.SALT_CURE ]) + .enemyLevel(1); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + game.scene.getPlayerPokemon()!.hp *= 0.70; + + game.move.select(Moves.SPLASH); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + confirmSwitch(); + }); + + it("Wimp Out will activate due to damaging trap damage", async () => { + game.override + .enemySpecies(Species.MAGIKARP) + .enemyMoveset([ Moves.WHIRLPOOL ]) + .enemyLevel(1); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + game.scene.getPlayerPokemon()!.hp *= 0.55; + + game.move.select(Moves.SPLASH); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + confirmSwitch(); + }); + + it("Magic Guard passive should not allow indirect damage to trigger Wimp Out", async () => { + game.scene.arena.addTag(ArenaTagType.STEALTH_ROCK, 1, Moves.STEALTH_ROCK, 0, ArenaTagSide.ENEMY); + game.scene.arena.addTag(ArenaTagType.SPIKES, 1, Moves.SPIKES, 0, ArenaTagSide.ENEMY); + game.override + .passiveAbility(Abilities.MAGIC_GUARD) + .enemyMoveset([ Moves.LEECH_SEED ]) + .weather(WeatherType.HAIL) + .statusEffect(StatusEffect.POISON); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + game.scene.getPlayerPokemon()!.hp *= 0.51; + + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(game.scene.getParty()[0].getHpRatio()).toEqual(0.51); + expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase"); + expect(game.scene.getPlayerPokemon()!.species.speciesId).toBe(Species.WIMPOD); + }); + + it("Wimp Out activating should not cancel a double battle", async () => { + game.override + .battleType("double") + .enemyAbility(Abilities.WIMP_OUT) + .enemyMoveset([ Moves.SPLASH ]) + .enemyLevel(1); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + const enemyLeadPokemon = game.scene.getEnemyParty()[0]; + const enemySecPokemon = game.scene.getEnemyParty()[1]; + + game.move.select(Moves.FALSE_SWIPE, 0, BattlerIndex.ENEMY); + game.move.select(Moves.SPLASH, 1); + + await game.phaseInterceptor.to("BerryPhase"); + + const isVisibleLead = enemyLeadPokemon.visible; + const hasFledLead = enemyLeadPokemon.switchOutStatus; + const isVisibleSec = enemySecPokemon.visible; + const hasFledSec = enemySecPokemon.switchOutStatus; + expect(!isVisibleLead && hasFledLead && isVisibleSec && !hasFledSec).toBe(true); + expect(enemyLeadPokemon.hp).toBeLessThan(enemyLeadPokemon.getMaxHp()); + expect(enemySecPokemon.hp).toEqual(enemySecPokemon.getMaxHp()); + }); + + it("Wimp Out will activate due to aftermath", async () => { + game.override + .moveset([ Moves.THUNDER_PUNCH ]) + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.AFTERMATH) + .enemyMoveset([ Moves.SPLASH ]) + .enemyLevel(1); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + game.scene.getPlayerPokemon()!.hp *= 0.51; + + game.move.select(Moves.THUNDER_PUNCH); + game.doSelectPartyPokemon(1); + await game.phaseInterceptor.to("TurnEndPhase"); + + confirmSwitch(); + }); + + it("Activates due to entry hazards", async () => { + game.scene.arena.addTag(ArenaTagType.STEALTH_ROCK, 1, Moves.STEALTH_ROCK, 0, ArenaTagSide.ENEMY); + game.scene.arena.addTag(ArenaTagType.SPIKES, 1, Moves.SPIKES, 0, ArenaTagSide.ENEMY); + game.override + .enemySpecies(Species.CENTISKORCH) + .enemyAbility(Abilities.WIMP_OUT) + .startingWave(4); + await game.classicMode.startBattle([ + Species.TYRUNT + ]); + + expect(game.phaseInterceptor.log).not.toContain("MovePhase"); + expect(game.phaseInterceptor.log).toContain("BattleEndPhase"); + }); + + it("Wimp Out will activate due to Nightmare", async () => { + game.override + .enemyMoveset([ Moves.NIGHTMARE ]) + .statusEffect(StatusEffect.SLEEP); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + game.scene.getPlayerPokemon()!.hp *= 0.65; + + game.move.select(Moves.SPLASH); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + confirmSwitch(); + }); + + it("triggers status on the wimp out user before a new pokemon is switched in", async () => { + game.override + .enemyMoveset(Moves.SLUDGE_BOMB) + .startingLevel(80); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + vi.spyOn(allMoves[Moves.SLUDGE_BOMB], "chance", "get").mockReturnValue(100); + + game.move.select(Moves.SPLASH); + game.doSelectPartyPokemon(1); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(game.scene.getParty()[1].status?.effect).toEqual(StatusEffect.POISON); + confirmSwitch(); + }); + + it("triggers after last hit of multi hit move", async () => { + game.override + .enemyMoveset(Moves.BULLET_SEED) + .enemyAbility(Abilities.SKILL_LINK); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + + game.scene.getPlayerPokemon()!.hp *= 0.51; + + game.move.select(Moves.ENDURE); + game.doSelectPartyPokemon(1); + await game.phaseInterceptor.to("TurnEndPhase"); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + expect(enemyPokemon.turnData.hitsLeft).toBe(0); + expect(enemyPokemon.turnData.hitCount).toBe(5); + confirmSwitch(); + }); + + it("triggers after last hit of multi hit move (multi lens)", async () => { + game.override + .enemyMoveset(Moves.TACKLE) + .enemyHeldItems([{ name: "MULTI_LENS", count: 1 }]); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + + game.scene.getPlayerPokemon()!.hp *= 0.51; + + game.move.select(Moves.ENDURE); + game.doSelectPartyPokemon(1); + await game.phaseInterceptor.to("TurnEndPhase"); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + expect(enemyPokemon.turnData.hitsLeft).toBe(0); + expect(enemyPokemon.turnData.hitCount).toBe(2); + confirmSwitch(); + }); + it("triggers after last hit of Parental Bond", async () => { + game.override + .enemyMoveset(Moves.TACKLE) + .enemyAbility(Abilities.PARENTAL_BOND); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + + game.scene.getPlayerPokemon()!.hp *= 0.51; + + game.move.select(Moves.ENDURE); + game.doSelectPartyPokemon(1); + await game.phaseInterceptor.to("TurnEndPhase"); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + expect(enemyPokemon.turnData.hitsLeft).toBe(0); + expect(enemyPokemon.turnData.hitCount).toBe(2); + confirmSwitch(); + }); + + // TODO: This interaction is not implemented yet + it.todo("Wimp Out will not activate if the Pokémon's HP falls below half due to hurting itself in confusion", async () => { + game.override + .moveset([ Moves.SWORDS_DANCE ]) + .enemyMoveset([ Moves.SWAGGER ]); + await game.classicMode.startBattle([ + Species.WIMPOD, + Species.TYRUNT + ]); + const playerPokemon = game.scene.getPlayerPokemon()!; + playerPokemon.hp *= 0.51; + playerPokemon.setStatStage(Stat.ATK, 6); + playerPokemon.addTag(BattlerTagType.CONFUSED); + + // TODO: add helper function to force confusion self-hits + + while (playerPokemon.getHpRatio() > 0.49) { + game.move.select(Moves.SWORDS_DANCE); + await game.phaseInterceptor.to("TurnEndPhase"); + } + + confirmNoSwitch(); + }); +}); diff --git a/src/test/eggs/egg.test.ts b/src/test/eggs/egg.test.ts index 6f57af63e6b..aebb196ff5e 100644 --- a/src/test/eggs/egg.test.ts +++ b/src/test/eggs/egg.test.ts @@ -1,4 +1,7 @@ -import { Egg, getLegendaryGachaSpeciesForTimestamp } from "#app/data/egg"; +import { speciesEggTiers } from "#app/data/balance/species-egg-tiers"; +import { speciesStarterCosts } from "#app/data/balance/starters"; +import { Egg, getLegendaryGachaSpeciesForTimestamp, getValidLegendaryGachaSpecies } from "#app/data/egg"; +import { allSpecies } from "#app/data/pokemon-species"; import { EggSourceType } from "#app/enums/egg-source-types"; import { EggTier } from "#app/enums/egg-type"; import { VariantTier } from "#app/enums/variant-tier"; @@ -64,6 +67,12 @@ describe("Egg Generation Tests", () => { expect(gachaSpeciesCount).toBeGreaterThan(0.4 * EGG_HATCH_COUNT); expect(gachaSpeciesCount).toBeLessThan(0.6 * EGG_HATCH_COUNT); }); + it("should never be allowed to generate Eternatus via the legendary gacha", () => { + const validLegendaryGachaSpecies = getValidLegendaryGachaSpecies(); + expect(validLegendaryGachaSpecies.every(s => speciesEggTiers[s] === EggTier.LEGENDARY)).toBe(true); + expect(validLegendaryGachaSpecies.every(s => allSpecies[s].isObtainable())).toBe(true); + expect(validLegendaryGachaSpecies.includes(Species.ETERNATUS)).toBe(false); + }); it("should hatch an Arceus. Set from species", () => { const scene = game.scene; const expectedSpecies = Species.ARCEUS; @@ -376,4 +385,23 @@ describe("Egg Generation Tests", () => { expect(diffShiny).toBe(true); expect(diffAbility).toBe(true); }); + + // For now, we are using this test to detect oversights in egg tiers. + // Delete this test if the balance team rebalances species costs independently of egg tiers. + it("should have correct egg tiers based on species costs", () => { + const getExpectedEggTier = (starterCost) => + starterCost <= 3 ? EggTier.COMMON + : starterCost <= 5 ? EggTier.RARE + : starterCost <= 7 ? EggTier.EPIC + : EggTier.LEGENDARY; + + allSpecies.forEach(pokemonSpecies => { + const rootSpecies = pokemonSpecies.getRootSpeciesId(); + const speciesCost = speciesStarterCosts[rootSpecies]; + const expectedEggTier = getExpectedEggTier(speciesCost); + const actualEggTier = speciesEggTiers[rootSpecies]; + + expect(actualEggTier).toBe(expectedEggTier); + }); + }); }); diff --git a/src/test/moves/camouflage.test.ts b/src/test/moves/camouflage.test.ts new file mode 100644 index 00000000000..acf37635c47 --- /dev/null +++ b/src/test/moves/camouflage.test.ts @@ -0,0 +1,49 @@ +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { TerrainType } from "#app/data/terrain"; +import { Type } from "#app/data/type"; +import { BattlerIndex } from "#app/battle"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Moves - Camouflage", () => { + 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.CAMOUFLAGE ]) + .ability(Abilities.BALL_FETCH) + .battleType("single") + .disableCrits() + .enemySpecies(Species.REGIELEKI) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.PSYCHIC_TERRAIN); + }); + + it("Camouflage should look at terrain first when selecting a type to change into", async () => { + await game.classicMode.startBattle([ Species.SHUCKLE ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.CAMOUFLAGE); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("BerryPhase"); + expect(game.scene.arena.getTerrainType()).toBe(TerrainType.PSYCHIC); + const pokemonType = playerPokemon.getTypes()[0]; + expect(pokemonType).toBe(Type.PSYCHIC); + }); +}); diff --git a/src/test/moves/chloroblast.test.ts b/src/test/moves/chloroblast.test.ts new file mode 100644 index 00000000000..5e55bf46958 --- /dev/null +++ b/src/test/moves/chloroblast.test.ts @@ -0,0 +1,42 @@ +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, expect, it } from "vitest"; + +describe("Moves - Chloroblast", () => { + 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.CHLOROBLAST ]) + .ability(Abilities.BALL_FETCH) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.PROTECT); + }); + + it("should not deal recoil damage if the opponent uses protect", async () => { + await game.classicMode.startBattle([ Species.FEEBAS ]); + + game.move.select(Moves.CHLOROBLAST); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getPlayerPokemon()!.isFullHp()).toBe(true); + }); +}); diff --git a/src/test/moves/dynamax_cannon.test.ts b/src/test/moves/dynamax_cannon.test.ts index 9dd48d3c94c..001f986bd52 100644 --- a/src/test/moves/dynamax_cannon.test.ts +++ b/src/test/moves/dynamax_cannon.test.ts @@ -81,7 +81,7 @@ describe("Moves - Dynamax Cannon", () => { const phase = game.scene.getCurrentPhase() as MoveEffectPhase; expect(phase.move.moveId).toBe(dynamaxCannon.id); // Force level cap to be 100 - vi.spyOn(phase.getTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); + vi.spyOn(phase.getFirstTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); await game.phaseInterceptor.to(DamagePhase, false); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(120); }, 20000); @@ -98,7 +98,7 @@ describe("Moves - Dynamax Cannon", () => { const phase = game.scene.getCurrentPhase() as MoveEffectPhase; expect(phase.move.moveId).toBe(dynamaxCannon.id); // Force level cap to be 100 - vi.spyOn(phase.getTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); + vi.spyOn(phase.getFirstTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); await game.phaseInterceptor.to(DamagePhase, false); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(140); }, 20000); @@ -115,7 +115,7 @@ describe("Moves - Dynamax Cannon", () => { const phase = game.scene.getCurrentPhase() as MoveEffectPhase; expect(phase.move.moveId).toBe(dynamaxCannon.id); // Force level cap to be 100 - vi.spyOn(phase.getTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); + vi.spyOn(phase.getFirstTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); await game.phaseInterceptor.to(DamagePhase, false); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(160); }, 20000); @@ -132,7 +132,7 @@ describe("Moves - Dynamax Cannon", () => { const phase = game.scene.getCurrentPhase() as MoveEffectPhase; expect(phase.move.moveId).toBe(dynamaxCannon.id); // Force level cap to be 100 - vi.spyOn(phase.getTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); + vi.spyOn(phase.getFirstTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); await game.phaseInterceptor.to(DamagePhase, false); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(180); }, 20000); @@ -149,7 +149,7 @@ describe("Moves - Dynamax Cannon", () => { const phase = game.scene.getCurrentPhase() as MoveEffectPhase; expect(phase.move.moveId).toBe(dynamaxCannon.id); // Force level cap to be 100 - vi.spyOn(phase.getTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); + vi.spyOn(phase.getFirstTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); await game.phaseInterceptor.to(DamagePhase, false); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(200); }, 20000); diff --git a/src/test/moves/focus_punch.test.ts b/src/test/moves/focus_punch.test.ts index 386eb2537ee..352e3b60aa4 100644 --- a/src/test/moves/focus_punch.test.ts +++ b/src/test/moves/focus_punch.test.ts @@ -59,7 +59,7 @@ describe("Moves - Focus Punch", () => { expect(enemyPokemon.hp).toBeLessThan(enemyStartingHp); expect(leadPokemon.getMoveHistory().length).toBe(1); - expect(leadPokemon.turnData.damageDealt).toBe(enemyStartingHp - enemyPokemon.hp); + expect(leadPokemon.turnData.totalDamageDealt).toBe(enemyStartingHp - enemyPokemon.hp); } ); @@ -86,7 +86,7 @@ describe("Moves - Focus Punch", () => { expect(enemyPokemon.hp).toBe(enemyStartingHp); expect(leadPokemon.getMoveHistory().length).toBe(1); - expect(leadPokemon.turnData.damageDealt).toBe(0); + expect(leadPokemon.turnData.totalDamageDealt).toBe(0); } ); diff --git a/src/test/moves/forests_curse.test.ts b/src/test/moves/forests_curse.test.ts new file mode 100644 index 00000000000..c49bdab5255 --- /dev/null +++ b/src/test/moves/forests_curse.test.ts @@ -0,0 +1,47 @@ +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { Type } from "#app/data/type"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Moves - Forest's Curse", () => { + 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.FORESTS_CURSE, Moves.TRICK_OR_TREAT ]) + .ability(Abilities.BALL_FETCH) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("will replace the added type from Trick Or Treat", async () => { + await game.classicMode.startBattle([ Species.FEEBAS ]); + + const enemyPokemon = game.scene.getEnemyPokemon(); + game.move.select(Moves.TRICK_OR_TREAT); + await game.phaseInterceptor.to("TurnEndPhase"); + expect(enemyPokemon!.summonData.addedType).toBe(Type.GHOST); + + game.move.select(Moves.FORESTS_CURSE); + await game.phaseInterceptor.to("TurnEndPhase"); + expect(enemyPokemon?.summonData.addedType).toBe(Type.GRASS); + }); +}); diff --git a/src/test/moves/mist.test.ts b/src/test/moves/mist.test.ts new file mode 100644 index 00000000000..cd338f79412 --- /dev/null +++ b/src/test/moves/mist.test.ts @@ -0,0 +1,49 @@ +import { Stat } from "#enums/stat"; +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, expect, it } from "vitest"; + +describe("Moves - Mist", () => { + 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.MIST, Moves.SPLASH ]) + .ability(Abilities.BALL_FETCH) + .battleType("double") + .disableCrits() + .enemySpecies(Species.SNORLAX) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.GROWL); + }); + + it("should prevent the user's side from having stats lowered", async () => { + await game.classicMode.startBattle([ Species.MAGIKARP, Species.FEEBAS ]); + + const playerPokemon = game.scene.getPlayerField(); + + game.move.select(Moves.MIST, 0); + game.move.select(Moves.SPLASH, 1); + + await game.phaseInterceptor.to("BerryPhase"); + + playerPokemon.forEach(p => expect(p.getStatStage(Stat.ATK)).toBe(0)); + }); + + it.todo("should be ignored by opponents with Infiltrator"); +}); diff --git a/src/test/moves/reflect_type.test.ts b/src/test/moves/reflect_type.test.ts new file mode 100644 index 00000000000..0e47d4b00fc --- /dev/null +++ b/src/test/moves/reflect_type.test.ts @@ -0,0 +1,59 @@ +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { Type } from "#app/data/type"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Moves - Reflect Type", () => { + 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 + .ability(Abilities.BALL_FETCH) + .battleType("single") + .disableCrits() + .enemyAbility(Abilities.BALL_FETCH); + }); + + it("will make the user Normal/Grass if targetting a typeless Pokemon affected by Forest's Curse", async () => { + game.override + .moveset([ Moves.FORESTS_CURSE, Moves.REFLECT_TYPE ]) + .startingLevel(60) + .enemySpecies(Species.CHARMANDER) + .enemyMoveset([ Moves.BURN_UP, Moves.SPLASH ]); + await game.classicMode.startBattle([ Species.FEEBAS ]); + + const playerPokemon = game.scene.getPlayerPokemon(); + const enemyPokemon = game.scene.getEnemyPokemon(); + + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.BURN_UP); + await game.toNextTurn(); + + game.move.select(Moves.FORESTS_CURSE); + await game.forceEnemyMove(Moves.SPLASH); + await game.toNextTurn(); + expect(enemyPokemon?.getTypes().includes(Type.UNKNOWN)).toBe(true); + expect(enemyPokemon?.getTypes().includes(Type.GRASS)).toBe(true); + + game.move.select(Moves.REFLECT_TYPE); + await game.forceEnemyMove(Moves.SPLASH); + await game.phaseInterceptor.to("TurnEndPhase"); + expect(playerPokemon?.getTypes()[0]).toBe(Type.NORMAL); + expect(playerPokemon?.getTypes().includes(Type.GRASS)).toBe(true); + }); +}); diff --git a/src/test/moves/secret_power.test.ts b/src/test/moves/secret_power.test.ts index ff0b5ae8c24..09fe5faa50b 100644 --- a/src/test/moves/secret_power.test.ts +++ b/src/test/moves/secret_power.test.ts @@ -2,7 +2,7 @@ import { Abilities } from "#enums/abilities"; import { Biome } from "#enums/biome"; import { Moves } from "#enums/moves"; import { Stat } from "#enums/stat"; -import { allMoves, SecretPowerAttr } from "#app/data/move"; +import { allMoves } from "#app/data/move"; import { Species } from "#enums/species"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; @@ -11,6 +11,7 @@ import { StatusEffect } from "#enums/status-effect"; import { BattlerIndex } from "#app/battle"; import { ArenaTagType } from "#enums/arena-tag-type"; import { ArenaTagSide } from "#app/data/arena-tag"; +import { allAbilities, MoveEffectChanceMultiplierAbAttr } from "#app/data/ability"; describe("Moves - Secret Power", () => { let phaserGame: Phaser.Game; @@ -60,30 +61,38 @@ describe("Moves - Secret Power", () => { expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(-1); }); - it("the 'rainbow' effect of fire+water pledge does not double the chance of secret power's secondary effect", + it("Secret Power's effect chance is doubled by Serene Grace, but not by the 'rainbow' effect from Fire/Water Pledge", async () => { game.override .moveset([ Moves.FIRE_PLEDGE, Moves.WATER_PLEDGE, Moves.SECRET_POWER, Moves.SPLASH ]) + .ability(Abilities.SERENE_GRACE) .enemyMoveset([ Moves.SPLASH ]) .battleType("double"); await game.classicMode.startBattle([ Species.BLASTOISE, Species.CHARIZARD ]); - const secretPowerAttr = allMoves[Moves.SECRET_POWER].getAttrs(SecretPowerAttr)[0]; - vi.spyOn(secretPowerAttr, "getMoveChance"); + const sereneGraceAttr = allAbilities[Abilities.SERENE_GRACE].getAttrs(MoveEffectChanceMultiplierAbAttr)[0]; + vi.spyOn(sereneGraceAttr, "apply"); game.move.select(Moves.WATER_PLEDGE, 0, BattlerIndex.ENEMY); game.move.select(Moves.FIRE_PLEDGE, 1, BattlerIndex.ENEMY_2); await game.phaseInterceptor.to("TurnEndPhase"); - expect(game.scene.arena.getTagOnSide(ArenaTagType.WATER_FIRE_PLEDGE, ArenaTagSide.PLAYER)).toBeDefined(); + let rainbowEffect = game.scene.arena.getTagOnSide(ArenaTagType.WATER_FIRE_PLEDGE, ArenaTagSide.PLAYER); + expect(rainbowEffect).toBeDefined(); + + rainbowEffect = rainbowEffect!; + vi.spyOn(rainbowEffect, "apply"); game.move.select(Moves.SECRET_POWER, 0, BattlerIndex.ENEMY); game.move.select(Moves.SPLASH, 1); await game.phaseInterceptor.to("BerryPhase", false); - expect(secretPowerAttr.getMoveChance).toHaveLastReturnedWith(30); + expect(sereneGraceAttr.apply).toHaveBeenCalledOnce(); + expect(sereneGraceAttr.apply).toHaveLastReturnedWith(true); + + expect(rainbowEffect.apply).toHaveBeenCalledTimes(0); } ); }); diff --git a/src/test/moves/shed_tail.test.ts b/src/test/moves/shed_tail.test.ts index c4df6c574cb..4d761a8af24 100644 --- a/src/test/moves/shed_tail.test.ts +++ b/src/test/moves/shed_tail.test.ts @@ -1,4 +1,5 @@ import { SubstituteTag } from "#app/data/battler-tags"; +import { MoveResult } from "#app/field/pokemon"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; @@ -53,4 +54,18 @@ describe("Moves - Shed Tail", () => { expect(substituteTag).toBeDefined(); expect(substituteTag?.hp).toBe(Math.floor(magikarp.getMaxHp() / 4)); }); + + it("should fail if no ally is available to switch in", async () => { + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const magikarp = game.scene.getPlayerPokemon()!; + expect(game.scene.getParty().length).toBe(1); + + game.move.select(Moves.SHED_TAIL); + + await game.phaseInterceptor.to("TurnEndPhase", false); + + expect(magikarp.isOnField()).toBeTruthy(); + expect(magikarp.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + }); }); diff --git a/src/test/moves/trick_or_treat.test.ts b/src/test/moves/trick_or_treat.test.ts new file mode 100644 index 00000000000..7ecd00ed076 --- /dev/null +++ b/src/test/moves/trick_or_treat.test.ts @@ -0,0 +1,47 @@ +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { Type } from "#app/data/type"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Moves - Trick Or Treat", () => { + 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.FORESTS_CURSE, Moves.TRICK_OR_TREAT ]) + .ability(Abilities.BALL_FETCH) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("will replace added type from Forest's Curse", async () => { + await game.classicMode.startBattle([ Species.FEEBAS ]); + + const enemyPokemon = game.scene.getEnemyPokemon(); + game.move.select(Moves.FORESTS_CURSE); + await game.phaseInterceptor.to("TurnEndPhase"); + expect(enemyPokemon!.summonData.addedType).toBe(Type.GRASS); + + game.move.select(Moves.TRICK_OR_TREAT); + await game.phaseInterceptor.to("TurnEndPhase"); + expect(enemyPokemon?.summonData.addedType).toBe(Type.GHOST); + }); +}); diff --git a/src/test/phases/frenzy-move-reset.test.ts b/src/test/phases/frenzy-move-reset.test.ts new file mode 100644 index 00000000000..db9ec2bfe66 --- /dev/null +++ b/src/test/phases/frenzy-move-reset.test.ts @@ -0,0 +1,72 @@ +import { BattlerIndex } from "#app/battle"; +import { Abilities } from "#enums/abilities"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { StatusEffect } from "#enums/status-effect"; +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("Frenzy Move Reset", () => { + 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") + .disableCrits() + .starterSpecies(Species.MAGIKARP) + .moveset(Moves.THRASH) + .statusEffect(StatusEffect.PARALYSIS) + .enemyMoveset(Moves.SPLASH) + .enemyLevel(100) + .enemySpecies(Species.SHUCKLE) + .enemyAbility(Abilities.BALL_FETCH); + }); + + /* + * Thrash (or frenzy moves in general) should not continue to run if attack fails due to paralysis + * + * This is a 3-turn Thrash test: + * 1. Thrash is selected and succeeds to hit the enemy -> Enemy Faints + * + * 2. Thrash is automatically selected but misses due to paralysis + * Note: After missing the Pokemon should stop automatically attacking + * + * 3. At the start of the 3rd turn the Player should be able to select a move/switch Pokemon/etc. + * Note: This means that BattlerTag.FRENZY is not anymore in pokemon.summonData.tags and pokemon.summonData.moveQueue is empty + * + */ + it("should cancel frenzy move if move fails turn 2", async () => { + await game.classicMode.startBattle(); + + const playerPokemon = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.THRASH); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.move.forceStatusActivation(false); + await game.toNextTurn(); + + expect(playerPokemon.summonData.moveQueue.length).toBe(2); + expect(playerPokemon.summonData.tags.some(tag => tag.tagType === BattlerTagType.FRENZY)).toBe(true); + + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.move.forceStatusActivation(true); + await game.toNextTurn(); + + expect(playerPokemon.summonData.moveQueue.length).toBe(0); + expect(playerPokemon.summonData.tags.some(tag => tag.tagType === BattlerTagType.FRENZY)).toBe(false); + }); +}); diff --git a/src/test/utils/gameWrapper.ts b/src/test/utils/gameWrapper.ts index 48c0007118b..22517502a05 100644 --- a/src/test/utils/gameWrapper.ts +++ b/src/test/utils/gameWrapper.ts @@ -24,6 +24,7 @@ import GamepadPlugin = Phaser.Input.Gamepad.GamepadPlugin; import EventEmitter = Phaser.Events.EventEmitter; import UpdateList = Phaser.GameObjects.UpdateList; import { version } from "../../../package.json"; +import { MockTimedEventManager } from "./mocks/mockTimedEventManager"; Object.defineProperty(window, "localStorage", { value: mockLocalStorage(), @@ -232,6 +233,7 @@ export default class GameWrapper { this.scene.make = new MockGameObjectCreator(mockTextureManager); this.scene.time = new MockClock(this.scene); this.scene.remove = vi.fn(); // TODO: this should be stubbed differently + this.scene.eventManager = new MockTimedEventManager(); // Disable Timed Events } } diff --git a/src/test/utils/mocks/mockTimedEventManager.ts b/src/test/utils/mocks/mockTimedEventManager.ts new file mode 100644 index 00000000000..b44729996a7 --- /dev/null +++ b/src/test/utils/mocks/mockTimedEventManager.ts @@ -0,0 +1,17 @@ +import { TimedEventManager } from "#app/timed-event-manager"; + +/** Mock TimedEventManager so that ongoing events don't impact tests */ +export class MockTimedEventManager extends TimedEventManager { + override activeEvent() { + return undefined; + } + override isEventActive(): boolean { + return false; + } + override getFriendshipMultiplier(): number { + return 1; + } + override getShinyMultiplier(): number { + return 1; + } +} diff --git a/src/timed-event-manager.ts b/src/timed-event-manager.ts index 4c56ada1c94..3b2b3619397 100644 --- a/src/timed-event-manager.ts +++ b/src/timed-event-manager.ts @@ -35,7 +35,7 @@ const timedEvents: TimedEvent[] = [ endDate: new Date(Date.UTC(2024, 10, 4, 0)), bannerKey: "halloween2024-event-", scale: 0.21, - availableLangs: [ "en", "de", "it", "fr", "ja", "ko", "es", "pt-BR", "zh-CN" ] + availableLangs: [ "en", "de", "it", "fr", "ja", "ko", "es-ES", "pt-BR", "zh-CN" ] } ]; diff --git a/src/ui/egg-gacha-ui-handler.ts b/src/ui/egg-gacha-ui-handler.ts index 8f977ba2ac0..b14f5381a84 100644 --- a/src/ui/egg-gacha-ui-handler.ts +++ b/src/ui/egg-gacha-ui-handler.ts @@ -107,7 +107,7 @@ export default class EggGachaUiHandler extends MessageUiHandler { let pokemonIconX = -20; let pokemonIconY = 6; - if ([ "de", "es", "fr", "ko", "pt-BR" ].includes(currentLanguage)) { + if ([ "de", "es-ES", "fr", "ko", "pt-BR" ].includes(currentLanguage)) { gachaTextStyle = TextStyle.SMALLER_WINDOW_ALT; gachaX = 2; gachaY = 2; @@ -115,7 +115,7 @@ export default class EggGachaUiHandler extends MessageUiHandler { let legendaryLabelX = gachaX; let legendaryLabelY = gachaY; - if ([ "de", "es" ].includes(currentLanguage)) { + if ([ "de", "es-ES" ].includes(currentLanguage)) { pokemonIconX = -25; pokemonIconY = 10; legendaryLabelX = -6; @@ -128,7 +128,7 @@ export default class EggGachaUiHandler extends MessageUiHandler { switch (gachaType as GachaType) { case GachaType.LEGENDARY: - if ([ "de", "es" ].includes(currentLanguage)) { + if ([ "de", "es-ES" ].includes(currentLanguage)) { gachaUpLabel.setAlign("center"); gachaUpLabel.setY(0); } @@ -149,7 +149,7 @@ export default class EggGachaUiHandler extends MessageUiHandler { gachaInfoContainer.add(pokemonIcon); break; case GachaType.MOVE: - if ([ "de", "es", "fr", "pt-BR" ].includes(currentLanguage)) { + if ([ "de", "es-ES", "fr", "pt-BR" ].includes(currentLanguage)) { gachaUpLabel.setAlign("center"); gachaUpLabel.setY(0); } diff --git a/src/ui/menu-ui-handler.ts b/src/ui/menu-ui-handler.ts index 301d54daa3a..fea0a70af91 100644 --- a/src/ui/menu-ui-handler.ts +++ b/src/ui/menu-ui-handler.ts @@ -32,7 +32,7 @@ let wikiUrl = "https://wiki.pokerogue.net/start"; const discordUrl = "https://discord.gg/uWpTfdKG49"; const githubUrl = "https://github.com/pagefaultgames/pokerogue"; const redditUrl = "https://www.reddit.com/r/pokerogue"; -const donateUrl = "https://github.com/sponsors/patapancakes"; +const donateUrl = "https://github.com/sponsors/pagefaultgames"; export default class MenuUiHandler extends MessageUiHandler { private readonly textPadding = 8; diff --git a/src/ui/pokemon-info-container.ts b/src/ui/pokemon-info-container.ts index 5c3a22639dd..5b11aff43b1 100644 --- a/src/ui/pokemon-info-container.ts +++ b/src/ui/pokemon-info-container.ts @@ -21,24 +21,6 @@ interface LanguageSetting { } const languageSettings: { [key: string]: LanguageSetting } = { - "en": { - infoContainerTextSize: "64px" - }, - "de": { - infoContainerTextSize: "64px", - }, - "es": { - infoContainerTextSize: "64px" - }, - "fr": { - infoContainerTextSize: "64px" - }, - "it": { - infoContainerTextSize: "64px" - }, - "zh": { - infoContainerTextSize: "64px" - }, "pt": { infoContainerTextSize: "60px", infoContainerLabelXPos: -15, @@ -237,14 +219,20 @@ export default class PokemonInfoContainer extends Phaser.GameObjects.Container { const formKey = (pokemon.species?.forms?.[pokemon.formIndex!]?.formKey); const formText = Utils.capitalizeString(formKey, "-", false, false) || ""; - const speciesName = Utils.capitalizeString(Species[pokemon.species.getRootSpeciesId()], "_", true, false); + const speciesName = Utils.capitalizeString(Species[pokemon.species.speciesId], "_", true, false); let formName = ""; if (pokemon.species.speciesId === Species.ARCEUS) { formName = i18next.t(`pokemonInfo:Type.${formText?.toUpperCase()}`); } else { const i18key = `pokemonForm:${speciesName}${formText}`; - formName = i18next.exists(i18key) ? i18next.t(i18key) : formText; + if (i18next.exists(i18key)) { + formName = i18next.t(i18key); + } else { + const rootSpeciesName = Utils.capitalizeString(Species[pokemon.species.getRootSpeciesId()], "_", true, false); + const i18RootKey = `pokemonForm:${rootSpeciesName}${formText}`; + formName = i18next.exists(i18RootKey) ? i18next.t(i18RootKey) : formText; + } } if (formName) { diff --git a/src/ui/registration-form-ui-handler.ts b/src/ui/registration-form-ui-handler.ts index fc9eb85cbaf..2c35ff8ee7f 100644 --- a/src/ui/registration-form-ui-handler.ts +++ b/src/ui/registration-form-ui-handler.ts @@ -13,7 +13,7 @@ interface LanguageSetting { } const languageSettings: { [key: string]: LanguageSetting } = { - "es":{ + "es-ES": { inputFieldFontSize: "50px", errorMessageFontSize: "40px", } diff --git a/src/ui/run-info-ui-handler.ts b/src/ui/run-info-ui-handler.ts index 0ca47241136..4975f05b8a3 100644 --- a/src/ui/run-info-ui-handler.ts +++ b/src/ui/run-info-ui-handler.ts @@ -674,7 +674,7 @@ export default class RunInfoUiHandler extends UiHandler { const def = i18next.t("pokemonInfo:Stat.DEFshortened") + ": " + pStats[2]; const spatk = i18next.t("pokemonInfo:Stat.SPATKshortened") + ": " + pStats[3]; const spdef = i18next.t("pokemonInfo:Stat.SPDEFshortened") + ": " + pStats[4]; - const speedLabel = (currentLanguage === "es" || currentLanguage === "pt_BR") ? i18next.t("runHistory:SPDshortened") : i18next.t("pokemonInfo:Stat.SPDshortened"); + const speedLabel = (currentLanguage === "es-ES" || currentLanguage === "pt_BR") ? i18next.t("runHistory:SPDshortened") : i18next.t("pokemonInfo:Stat.SPDshortened"); const speed = speedLabel + ": " + pStats[5]; // Column 1: HP Atk Def const pokeStatText1 = addBBCodeTextObject(this.scene, -5, 0, hp, TextStyle.SUMMARY, { fontSize: textContainerFontSize, lineSpacing: lineSpacing }); diff --git a/src/ui/settings/settings-display-ui-handler.ts b/src/ui/settings/settings-display-ui-handler.ts index 5a4fd38a034..0b37917c156 100644 --- a/src/ui/settings/settings-display-ui-handler.ts +++ b/src/ui/settings/settings-display-ui-handler.ts @@ -29,10 +29,10 @@ export default class SettingsDisplayUiHandler extends AbstractSettingsUiHandler label: "English", }; break; - case "es": + case "es-ES": this.settings[languageIndex].options[0] = { - value: "Español", - label: "Español", + value: "Español (ES)", + label: "Español (ES)", }; break; case "it": diff --git a/src/ui/starter-select-ui-handler.ts b/src/ui/starter-select-ui-handler.ts index 34b0ce52a32..e445c6b9c15 100644 --- a/src/ui/starter-select-ui-handler.ts +++ b/src/ui/starter-select-ui-handler.ts @@ -81,7 +81,7 @@ const languageSettings: { [key: string]: LanguageSetting } = { instructionTextSize: "35px", starterInfoXPos: 33, }, - "es":{ + "es-ES":{ starterInfoTextSize: "56px", instructionTextSize: "35px", }, diff --git a/src/ui/target-select-ui-handler.ts b/src/ui/target-select-ui-handler.ts index 4c55a4b960e..ecc15e5985e 100644 --- a/src/ui/target-select-ui-handler.ts +++ b/src/ui/target-select-ui-handler.ts @@ -184,6 +184,7 @@ export default class TargetSelectUiHandler extends UiHandler { } clear() { + this.cursor = -1; super.clear(); this.eraseCursor(); } diff --git a/src/utils.ts b/src/utils.ts index 90b8fe1238a..6c3d46ebaf7 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -487,7 +487,7 @@ export function verifyLang(lang?: string): boolean { } switch (lang) { - case "es": + case "es-ES": case "fr": case "da": case "de":